diff --git a/.icons/opencode.png b/.icons/opencode.png
new file mode 100644
index 000000000..b7436235d
Binary files /dev/null and b/.icons/opencode.png differ
diff --git a/bun.lock b/bun.lock
index 16c21d09b..b8a3e27c9 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
+ "configVersion": 0,
"workspaces": {
"": {
"name": "registry",
diff --git a/registry/rothnic/.images/avatar.png b/registry/rothnic/.images/avatar.png
new file mode 100644
index 000000000..be46cf071
Binary files /dev/null and b/registry/rothnic/.images/avatar.png differ
diff --git a/registry/rothnic/README.md b/registry/rothnic/README.md
new file mode 100644
index 000000000..a81fd1d3c
--- /dev/null
+++ b/registry/rothnic/README.md
@@ -0,0 +1,25 @@
+---
+display_name: "Nick Roth"
+bio: "Product leader and engineer focused on end-to-end product delivery and agentic AI systems. Based in Huntsville, AL."
+avatar: "./.images/avatar.png"
+github: "rothnic"
+linkedin: "http://www.linkedin.com/in/nicholasleeroth/"
+website: "https://www.nickroth.com"
+status: "community"
+---
+
+# Nick Roth
+
+Product manager and engineer with 14 years of experience leading product strategy, engineering delivery, and platform initiatives. I focus on end-to-end product ownershipβfrom discovery and design through implementation, deployment, and iterative improvement. My current work centers on building self-learning, agentic systems that automate content and operational workflows while keeping humans in the loop for quality and governance.
+
+## Expertise
+
+- **Product Leadership:** Cross-functional product strategy, roadmapping, team leadership, vendor and stakeholder management.
+- **Agentic Systems & Automation:** Designing and delivering self-learning agent pipelines, human-in-the-loop workflows, and production-ready automation for content and operations.
+- **Platform Delivery & Modernization:** Architecture, procurement, migrations, and shipping reliable systems from prototype to production.
+- **Growth & Experimentation:** SEO-led content strategy, A/B testing, experimentation frameworks, and monetization optimization.
+- **Systems Engineering:** Requirements, architecture, and integration across complex systems.
+
+## Modules
+
+- [opencode](./modules/opencode/) - Execute AI-driven coding tasks and agentic workflows directly within Coder workspaces using OpenCode.
diff --git a/registry/rothnic/modules/opencode/README.md b/registry/rothnic/modules/opencode/README.md
new file mode 100644
index 000000000..78bc3497e
--- /dev/null
+++ b/registry/rothnic/modules/opencode/README.md
@@ -0,0 +1,316 @@
+---
+display_name: OpenCode
+description: AI-powered terminal coding agent with support for GitHub Copilot, Anthropic, and OpenAI
+icon: ../../../../.icons/opencode.png
+maintainer_github: rothnic
+verified: false
+tags: [agent, ai, opencode, coding-assistant, copilot, terminal]
+---
+
+# OpenCode
+
+Integrate [OpenCode.ai](https://opencode.ai/) - an AI coding agent that executes tasks in your terminal and reports progress to Coder's task system via [AgentAPI](https://github.com/coder/agentapi). Supports GitHub Copilot, Anthropic Claude, OpenAI, and other providers.
+
+## Quick Start
+
+**Basic setup:**
+
+```tf
+module "opencode" {
+ source = "registry.coder.com/rothnic/opencode/coder"
+ agent_id = coder_agent.main.id
+ workdir = "/home/coder"
+}
+```
+
+**With Coder Tasks (recommended for AI workspaces):**
+
+```tf
+data "coder_task" "me" {}
+
+module "opencode" {
+ source = "registry.coder.com/rothnic/opencode/coder"
+ agent_id = coder_agent.main.id
+ workdir = "/home/coder"
+
+ ai_prompt = data.coder_task.me.prompt
+ report_tasks = true
+}
+```
+
+**With model and MCP servers:**
+
+```tf
+module "opencode" {
+ source = "registry.coder.com/rothnic/opencode/coder"
+ agent_id = coder_agent.main.id
+ workdir = "/home/coder"
+
+ opencode_model = "claude-3.7-sonnet"
+ mcp_servers = jsonencode({
+ filesystem = {
+ type = "local"
+ command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/home/coder"]
+ }
+ })
+}
+```
+
+## Authentication
+
+OpenCode supports many AI providers (GitHub Copilot, Anthropic Claude, OpenAI, etc.). Authentication is managed via `~/.local/share/opencode/auth.json`, created by running `opencode auth login`.
+
+### GitHub Copilot
+
+GitHub Copilot requires OAuth device flow authentication - you cannot use standard GitHub tokens.
+
+**Option 1: Pre-configured auth (recommended for automation)**
+
+Generate auth on your local machine, then embed in your template:
+
+```bash
+# On your local machine
+npm install -g opencode-ai
+opencode auth login # Select "GitHub Copilot", complete device flow
+cat ~/.local/share/opencode/auth.json # Copy this content
+```
+
+Create `opencode-auth.json` in your template directory, then reference it:
+
+```tf
+module "opencode" {
+ source = "registry.coder.com/rothnic/opencode/coder"
+ agent_id = coder_agent.main.id
+ workdir = "/workspaces"
+ opencode_auth_config = file("${path.module}/opencode-auth.json")
+}
+```
+
+**Option 2: Manual login per workspace**
+
+Users run `opencode auth login` inside the workspace. Auth persists across restarts.
+
+### Other Providers (Claude, OpenAI, etc.)
+
+For Anthropic, OpenAI, DeepSeek, Groq, and [other providers](https://opencode.ai/docs/providers/):
+
+1. Users run `opencode auth login` in the workspace
+2. Select their provider and enter API key
+3. Credentials persist in `~/.local/share/opencode/auth.json`
+
+Or provide API keys via environment variables (see provider docs).
+
+**Note:** The `opencode_provider` variable is for documentation only - it doesn't configure OpenCode. Provider selection happens via `opencode auth login`.
+
+## Configuration
+
+### Core Variables
+
+| Variable | Description | Default | Required |
+| ---------- | ------------------------------ | ------- | -------- |
+| `agent_id` | Coder agent ID | - | Yes |
+| `workdir` | Working directory for OpenCode | - | Yes |
+
+### Authentication & Provider
+
+| Variable | Description | Default |
+| ---------------------- | -------------------------------------------------------------------------------------- | ----------- |
+| `opencode_auth_config` | Pre-configured auth.json content (for GitHub Copilot or any provider) | `""` |
+| `opencode_provider` | Intended provider (documentation only - actual provider set via `opencode auth login`) | `"copilot"` |
+| `github_token` | GitHub token for git operations (not AI provider auth) | `""` |
+| `external_auth_id` | Coder external auth provider ID for git operations | `"github"` |
+
+### Task Integration
+
+| Variable | Description | Default |
+| ---------------- | ----------------------------------------------------- | --------------- |
+| `ai_prompt` | Initial task prompt (use `data.coder_task.me.prompt`) | `""` |
+| `system_prompt` | Custom system prompt for the AI | Built-in prompt |
+| `report_tasks` | Enable task reporting to Coder UI | `true` |
+| `resume_session` | Auto-resume latest session on restart | `true` |
+
+### Installation & Versioning
+
+| Variable | Description | Default |
+| ------------------ | ----------------------------------------------- | ----------- |
+| `opencode_version` | OpenCode version (`latest` or specific version) | `"latest"` |
+| `install_method` | Installation method (`npm` recommended) | `"npm"` |
+| `install_agentapi` | Install AgentAPI | `true` |
+| `agentapi_version` | AgentAPI version | `"v0.10.0"` |
+
+### UI & Apps
+
+| Variable | Description | Default |
+| ---------------------- | -------------------------------------------------------------------------- | ---------------------- |
+| `web_app_display_name` | Display name in Coder UI | `"OpenCode"` |
+| `order` | App position in UI | `null` |
+| `group` | App group name | `null` |
+| `icon` | App icon path | `"/icon/opencode.png"` |
+| `subdomain` | Use subdomain for app access (requires [wildcard DNS][wildcard-dns-setup]) | `false` |
+| `cli_app` | Create CLI app entry | `false` |
+
+[wildcard-dns-setup]: https://coder.com/docs/admin/setup#wildcard-access-url
+
+### Model & MCP Configuration
+
+| Variable | Description | Default |
+| ---------------- | ------------------------------------------------------------------------------------ | ------- |
+| `opencode_model` | Model to use (e.g., `claude-3.7-sonnet`, `gpt-4o`). If empty, uses provider default. | `""` |
+| `mcp_servers` | MCP servers configuration as JSON string (see example below) | `""` |
+
+**MCP Servers Example:**
+
+OpenCode uses `type: "local"` for local MCP servers with `command` as an array (including all arguments):
+
+```tf
+module "opencode" {
+ source = "registry.coder.com/rothnic/opencode/coder"
+ agent_id = coder_agent.main.id
+ workdir = "/workspaces"
+
+ opencode_model = "claude-3.7-sonnet"
+
+ mcp_servers = jsonencode({
+ filesystem = {
+ type = "local"
+ command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/workspaces"]
+ }
+ github = {
+ type = "local"
+ command = ["npx", "-y", "@modelcontextprotocol/server-github"]
+ environment = {
+ GITHUB_TOKEN = var.github_token
+ }
+ }
+ })
+}
+```
+
+For remote MCP servers, use `type: "remote"` with a `url`:
+
+```tf
+mcp_servers = jsonencode({
+ context7 = {
+ type = "remote"
+ url = "https://mcp.context7.com/mcp"
+ }
+})
+```
+
+See [OpenCode MCP documentation](https://opencode.ai/docs/mcp-servers/) for more details.
+
+### Advanced
+
+| Variable | Description | Default |
+| --------------------- | ------------------------------------------------------------------- | ------- |
+| `opencode_config` | Full custom OpenCode config (JSON). Overrides other config options. | `""` |
+| `pre_install_script` | Script to run before install | `null` |
+| `post_install_script` | Script to run after install | `null` |
+
+See [main.tf](./main.tf) for complete variable definitions.
+
+## Features
+
+- π€ Multiple AI providers (Copilot, Claude, GPT)
+- π§ MCP servers configuration support
+- π― Model selection per deployment
+- π Task reporting to Coder UI
+- πΎ Session persistence
+- π Flexible authentication
+- π Node.js tarball installation (no apt/nvm)
+- π Version pinning support via `opencode_version`
+- πΎ Shared Node.js cache across workspaces
+
+## Complete Template Example
+
+Here's a lightweight template for Coder task execution:
+
+```tf
+terraform {
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ }
+ docker = {
+ source = "kreuzwerker/docker"
+ }
+ }
+}
+
+provider "docker" {}
+
+data "coder_workspace" "me" {}
+data "coder_workspace_owner" "me" {}
+
+# Task data source - provides prompts from Coder Tasks UI
+data "coder_task" "me" {}
+
+resource "coder_agent" "main" {
+ arch = "amd64"
+ os = "linux"
+
+ startup_script = <<-EOT
+ set -e
+ mkdir -p /workspaces
+ EOT
+
+ env = {
+ GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
+ GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email
+ GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
+ GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email
+ }
+}
+
+# OpenCode AI coding agent
+module "opencode" {
+ source = "registry.coder.com/rothnic/opencode/coder"
+ agent_id = coder_agent.main.id
+ workdir = "/workspaces"
+
+ # Pre-configured authentication (from 'opencode auth login' output)
+ opencode_auth_config = file("${path.module}/opencode-auth.json")
+
+ # Pass task prompt from Coder Tasks UI
+ ai_prompt = data.coder_task.me.prompt
+
+ # Enable task reporting for Coder UI integration
+ report_tasks = true
+
+ # Use subdomain for better app routing (requires wildcard DNS)
+ # subdomain = true
+
+ # Display settings
+ order = 1
+ web_app_display_name = "OpenCode AI"
+}
+
+resource "docker_container" "workspace" {
+ count = data.coder_workspace.me.start_count
+ image = "codercom/enterprise-base:ubuntu"
+ name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
+
+ entrypoint = ["sh", "-c", coder_agent.main.init_script]
+ env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
+}
+```
+
+## Prerequisites
+
+- None - Node.js 20 is automatically installed via tarball
+
+## Notes
+
+- **Node.js installation**: Installed via tarball (not apt/nvm) to `/workspaces/.coder-tools` for reliability and workspace portability
+- **OpenCode installation**: Installed via `npm install -g opencode-ai@{version}`
+- **Caching**: Node.js and npm cache shared across workspaces in `/workspaces/.coder-tools`
+- **Version pinning**: Use `opencode_version` variable to lock to specific versions
+- **TUI limitations**: Some interactive features (slash commands, menus) may have limitations through AgentAPI
+- **Subdomain access**: For production, use `subdomain = true` with [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url)
+- **Task reporting**: When `report_tasks = true`, the module automatically configures system prompts for granular task status updates
+
+## Resources
+
+- [OpenCode Documentation](https://opencode.ai/docs/)
+- [OpenCode GitHub](https://github.com/opencode-ai/opencode)
+- [AgentAPI](https://github.com/coder/agentapi)
diff --git a/registry/rothnic/modules/opencode/examples/minimal-agent-task/main.tf b/registry/rothnic/modules/opencode/examples/minimal-agent-task/main.tf
new file mode 100644
index 000000000..580bb98de
--- /dev/null
+++ b/registry/rothnic/modules/opencode/examples/minimal-agent-task/main.tf
@@ -0,0 +1,202 @@
+terraform {
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ }
+ docker = {
+ source = "kreuzwerker/docker"
+ }
+ }
+}
+
+# This template requires a valid Docker socket
+# You can reference Kubernetes/VM example templates and adapt:
+# see: https://registry.coder.com/templates
+provider "docker" {}
+
+data "coder_provisioner" "me" {}
+data "coder_workspace" "me" {}
+data "coder_workspace_owner" "me" {}
+
+# OpenCode module handles automatic task reporting via agentapi
+# For testing with current branch:
+module "opencode" {
+ count = data.coder_workspace.me.start_count
+ source = "git::https://github.com/coder/registry.git//registry/rothnic/modules/opencode?ref=claude/review-module-guidelines-014yytiyG8n6Rj4V8BbxZb2B"
+
+ # After merge, use:
+ # source = "registry.coder.com/rothnic/opencode/coder"
+ # version = "~> 1.0"
+
+ agent_id = coder_agent.main.id
+ workdir = "/home/coder/projects"
+ order = 999
+ ai_prompt = data.coder_parameter.ai_prompt.value
+ subdomain = true
+
+ # GitHub Copilot Authentication:
+ # 1. Run `opencode auth login` locally and select GitHub Copilot
+ # 2. Copy ~/.local/share/opencode/auth.json to opencode-auth.json
+ # 3. Uncomment the line below:
+ # opencode_auth_config = file("${path.module}/opencode-auth.json")
+
+ # Configure model (optional):
+ # opencode_model = "claude-sonnet-4-20250514"
+
+ # MCP Servers (optional):
+ # mcp_servers = jsonencode({
+ # filesystem = {
+ # type = "local"
+ # command = ["npx", "-y", "@anthropic/mcp-server-filesystem", "/home/coder/projects"]
+ # }
+ # })
+}
+
+# Workspace presets for different use cases
+# See https://coder.com/docs/admin/templates/extending-templates/parameters#workspace-presets
+data "coder_workspace_preset" "default" {
+ name = "Default OpenCode Workspace"
+ default = true
+ parameters = {
+ "system_prompt" = <<-EOT
+ You are a helpful coding assistant running inside a Coder workspace.
+ Stay on track and feel free to debug, but when the original plan fails,
+ do not choose a different route/architecture without checking the user first.
+ EOT
+ "container_image" = "codercom/enterprise-base:ubuntu"
+ }
+}
+
+# Parameters (set via preset or manually)
+data "coder_parameter" "ai_prompt" {
+ type = "string"
+ name = "AI Prompt"
+ default = ""
+ description = "Write a prompt for OpenCode"
+ display_name = "AI Prompt"
+ mutable = true
+}
+
+data "coder_parameter" "system_prompt" {
+ name = "system_prompt"
+ display_name = "System Prompt"
+ type = "string"
+ form_type = "textarea"
+ description = "System prompt for the agent with generalized instructions"
+ mutable = false
+ default = ""
+}
+
+data "coder_parameter" "container_image" {
+ name = "container_image"
+ display_name = "Container Image"
+ type = "string"
+ default = "codercom/enterprise-base:ubuntu"
+ mutable = false
+}
+
+resource "coder_agent" "main" {
+ arch = data.coder_provisioner.me.arch
+ os = "linux"
+ startup_script = <<-EOT
+ set -e
+ # Prepare user home with default files on first start
+ if [ ! -f ~/.init_done ]; then
+ cp -rT /etc/skel ~
+ touch ~/.init_done
+ fi
+ # Create projects directory
+ mkdir -p /home/coder/projects
+ EOT
+
+ # Git configuration from workspace owner
+ env = {
+ GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
+ GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
+ GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
+ GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
+ }
+
+ metadata {
+ display_name = "CPU Usage"
+ key = "0_cpu_usage"
+ script = "coder stat cpu"
+ interval = 10
+ timeout = 1
+ }
+
+ metadata {
+ display_name = "RAM Usage"
+ key = "1_ram_usage"
+ script = "coder stat mem"
+ interval = 10
+ timeout = 1
+ }
+
+ metadata {
+ display_name = "Home Disk"
+ key = "3_home_disk"
+ script = "coder stat disk --path $${HOME}"
+ interval = 60
+ timeout = 1
+ }
+}
+
+resource "docker_volume" "home_volume" {
+ name = "coder-${data.coder_workspace.me.id}-home"
+ lifecycle {
+ ignore_changes = all
+ }
+ labels {
+ label = "coder.owner"
+ value = data.coder_workspace_owner.me.name
+ }
+ labels {
+ label = "coder.owner_id"
+ value = data.coder_workspace_owner.me.id
+ }
+ labels {
+ label = "coder.workspace_id"
+ value = data.coder_workspace.me.id
+ }
+ labels {
+ label = "coder.workspace_name_at_creation"
+ value = data.coder_workspace.me.name
+ }
+}
+
+resource "docker_container" "workspace" {
+ count = data.coder_workspace.me.start_count
+ image = data.coder_parameter.container_image.value
+ name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
+ hostname = data.coder_workspace.me.name
+ user = "coder"
+ # Use the docker gateway if the access URL is 127.0.0.1
+ entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
+ env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
+ host {
+ host = "host.docker.internal"
+ ip = "host-gateway"
+ }
+ volumes {
+ container_path = "/home/coder"
+ volume_name = docker_volume.home_volume.name
+ read_only = false
+ }
+ labels {
+ label = "coder.owner"
+ value = data.coder_workspace_owner.me.name
+ }
+ labels {
+ label = "coder.owner_id"
+ value = data.coder_workspace_owner.me.id
+ }
+ labels {
+ label = "coder.workspace_id"
+ value = data.coder_workspace.me.id
+ }
+ labels {
+ label = "coder.workspace_name"
+ value = data.coder_workspace.me.name
+ }
+}
diff --git a/registry/rothnic/modules/opencode/main.tf b/registry/rothnic/modules/opencode/main.tf
new file mode 100644
index 000000000..c87eaf037
--- /dev/null
+++ b/registry/rothnic/modules/opencode/main.tf
@@ -0,0 +1,264 @@
+terraform {
+ required_version = ">= 1.0"
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 2.7"
+ }
+ }
+}
+
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+variable "workdir" {
+ type = string
+ description = "The folder to run OpenCode in."
+}
+
+variable "external_auth_id" {
+ type = string
+ description = "ID of the GitHub external auth provider configured in Coder."
+ default = "github"
+}
+
+variable "github_token" {
+ type = string
+ description = "GitHub OAuth token or Personal Access Token. If provided, this will be used instead of auto-detecting authentication."
+ default = ""
+ sensitive = true
+}
+
+variable "opencode_provider" {
+ type = string
+ description = "Intended AI provider (for documentation/defaults). Actual provider is configured via opencode_auth_config or by running 'opencode auth login' in the workspace."
+ default = "copilot"
+}
+
+variable "opencode_model" {
+ type = string
+ description = "The model for OpenCode to use (e.g., 'claude-3.7-sonnet', 'gpt-4o'). If empty, uses provider default."
+ default = ""
+}
+
+variable "mcp_servers" {
+ type = string
+ description = "MCP servers configuration as JSON string. Will be merged into the 'mcp' section of opencode.json. OpenCode format uses type='local' with command as array. Example: '{\"filesystem\":{\"type\":\"local\",\"command\":[\"npx\",\"-y\",\"@modelcontextprotocol/server-filesystem\",\"/workspaces\"]}}'"
+ default = ""
+}
+
+variable "opencode_config" {
+ type = string
+ description = "Complete custom OpenCode configuration as JSON string. If provided, this overrides default config generation. For partial config (just MCP servers), use mcp_servers variable instead."
+ default = ""
+}
+
+variable "opencode_auth_config" {
+ type = string
+ description = "Pre-configured OpenCode auth.json content as JSON string. Use this to provide GitHub Copilot credentials obtained from running 'opencode auth login' locally."
+ default = ""
+ sensitive = true
+}
+
+variable "ai_prompt" {
+ type = string
+ description = "Initial task prompt for programmatic mode."
+ default = ""
+}
+
+variable "system_prompt" {
+ type = string
+ description = "The system prompt to use for OpenCode. Task reporting instructions are automatically added when report_tasks is enabled."
+ default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
+}
+
+variable "install_agentapi" {
+ type = bool
+ description = "Whether to install AgentAPI."
+ default = true
+}
+
+variable "agentapi_version" {
+ type = string
+ description = "The version of AgentAPI to install."
+ default = "v0.10.0"
+}
+
+variable "opencode_version" {
+ type = string
+ description = "The version of OpenCode to install. Use 'latest' for the latest version or specify a version."
+ default = "latest"
+}
+
+variable "install_method" {
+ type = string
+ description = "Installation method for OpenCode. Use 'npm' with the module-managed Node tarball."
+ default = "npm"
+}
+
+variable "report_tasks" {
+ type = bool
+ description = "Whether to enable task reporting to Coder UI via AgentAPI."
+ default = true
+}
+
+variable "subdomain" {
+ type = bool
+ description = "Whether to use a subdomain for AgentAPI."
+ default = false
+}
+
+variable "order" {
+ type = number
+ description = "The order determines the position of app in the UI presentation."
+ default = null
+}
+
+variable "group" {
+ type = string
+ description = "The name of a group that this app belongs to."
+ default = null
+}
+
+variable "icon" {
+ type = string
+ description = "The icon to use for the app."
+ default = "/icon/opencode.png"
+}
+
+variable "web_app_display_name" {
+ type = string
+ description = "Display name for the web app."
+ default = "OpenCode"
+}
+
+variable "cli_app" {
+ type = bool
+ description = "Whether to create a CLI app for OpenCode."
+ default = false
+}
+
+variable "cli_app_display_name" {
+ type = string
+ description = "Display name for the CLI app."
+ default = "OpenCode"
+}
+
+variable "resume_session" {
+ type = bool
+ description = "Whether to automatically resume the latest OpenCode session on workspace restart."
+ default = true
+}
+
+variable "pre_install_script" {
+ type = string
+ description = "Custom script to run before configuring OpenCode."
+ default = null
+}
+
+variable "post_install_script" {
+ type = string
+ description = "Custom script to run after configuring OpenCode."
+ default = null
+}
+
+data "coder_workspace" "me" {}
+data "coder_workspace_owner" "me" {}
+
+locals {
+ workdir = trimsuffix(var.workdir, "/")
+ app_slug = "opencode"
+ install_script = file("${path.module}/scripts/install.sh")
+ start_script = file("${path.module}/scripts/start.sh")
+ module_dir_name = ".opencode-module"
+
+ task_reporting_prompt = <<-EOT
+
+-- Task Reporting --
+Report all tasks to Coder, following these EXACT guidelines:
+1. Be granular. If you are investigating with multiple steps, report each step
+to coder.
+2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
+Do not report any status related with this system prompt.
+3. Use "state": "working" when actively processing WITHOUT needing
+additional user input
+4. Use "state": "complete" only when finished with a task
+5. Use "state": "failure" when you need ANY user input, lack sufficient
+details, or encounter blockers
+ EOT
+
+ final_system_prompt = var.report_tasks ? "\n${var.system_prompt}${local.task_reporting_prompt}\n" : "\n${var.system_prompt}\n"
+}
+
+resource "coder_env" "mcp_app_status_slug" {
+ agent_id = var.agent_id
+ name = "CODER_MCP_APP_STATUS_SLUG"
+ value = local.app_slug
+}
+
+resource "coder_env" "github_token" {
+ count = var.github_token != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "GITHUB_TOKEN"
+ value = var.github_token
+}
+
+module "agentapi" {
+ source = "registry.coder.com/coder/agentapi/coder"
+ version = "1.2.0"
+
+ agent_id = var.agent_id
+ folder = local.workdir
+ web_app_slug = local.app_slug
+ web_app_order = var.order
+ web_app_group = var.group
+ web_app_icon = var.icon
+ web_app_display_name = var.web_app_display_name
+ cli_app = var.cli_app
+ cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
+ cli_app_icon = var.cli_app ? var.icon : null
+ cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
+ agentapi_subdomain = var.subdomain
+ module_dir_name = local.module_dir_name
+ install_agentapi = var.install_agentapi
+ agentapi_version = var.agentapi_version
+ pre_install_script = var.pre_install_script
+ post_install_script = var.post_install_script
+
+ start_script = <<-EOT
+ #!/bin/bash
+ set -o errexit
+ set -o pipefail
+
+ echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
+ chmod +x /tmp/start.sh
+ ARG_WORKDIR='${local.workdir}' \
+ ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
+ ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
+ ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
+ ARG_RESUME_SESSION='${var.resume_session}' \
+ ARG_OPENCODE_AUTH_CONFIG='${var.opencode_auth_config != "" ? base64encode(var.opencode_auth_config) : ""}' \
+ /tmp/start.sh
+ EOT
+
+ install_script = <<-EOT
+ #!/bin/bash
+ set -o errexit
+ set -o pipefail
+
+ echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
+ chmod +x /tmp/install.sh
+ ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
+ ARG_REPORT_TASKS='${var.report_tasks}' \
+ ARG_WORKDIR='${local.workdir}' \
+ ARG_OPENCODE_CONFIG='${var.opencode_config != "" ? base64encode(var.opencode_config) : ""}' \
+ ARG_MCP_SERVERS='${var.mcp_servers != "" ? base64encode(var.mcp_servers) : ""}' \
+ ARG_OPENCODE_MODEL='${var.opencode_model}' \
+ ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
+ ARG_OPENCODE_VERSION='${var.opencode_version}' \
+ ARG_INSTALL_METHOD='${var.install_method}' \
+ /tmp/install.sh
+ EOT
+}
diff --git a/registry/rothnic/modules/opencode/opencode.tftest.hcl b/registry/rothnic/modules/opencode/opencode.tftest.hcl
new file mode 100644
index 000000000..5ece555a8
--- /dev/null
+++ b/registry/rothnic/modules/opencode/opencode.tftest.hcl
@@ -0,0 +1,286 @@
+run "defaults_are_correct" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ }
+
+ assert {
+ condition = var.opencode_provider == "copilot"
+ error_message = "Default provider should be 'copilot'"
+ }
+
+ assert {
+ condition = var.report_tasks == true
+ error_message = "Task reporting should be enabled by default"
+ }
+
+ assert {
+ condition = var.resume_session == true
+ error_message = "Session resumption should be enabled by default"
+ }
+
+ assert {
+ condition = var.install_method == "npm"
+ error_message = "Default install method should be 'npm'"
+ }
+
+ assert {
+ condition = resource.coder_env.mcp_app_status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
+ error_message = "Status slug env var should be created"
+ }
+
+ assert {
+ condition = resource.coder_env.mcp_app_status_slug.value == "opencode"
+ error_message = "Status slug value should be 'opencode'"
+ }
+}
+
+run "github_token_creates_env_var" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ github_token = "test_github_token_abc123"
+ }
+
+ assert {
+ condition = length(resource.coder_env.github_token) == 1
+ error_message = "github_token env var should be created when token is provided"
+ }
+
+ assert {
+ condition = resource.coder_env.github_token[0].name == "GITHUB_TOKEN"
+ error_message = "github_token env var name should be 'GITHUB_TOKEN'"
+ }
+
+ assert {
+ condition = resource.coder_env.github_token[0].value == "test_github_token_abc123"
+ error_message = "github_token env var value should match input"
+ }
+}
+
+run "github_token_not_created_when_empty" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ github_token = ""
+ }
+
+ assert {
+ condition = length(resource.coder_env.github_token) == 0
+ error_message = "github_token env var should not be created when empty"
+ }
+}
+
+run "install_method_validation" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ install_method = "curl"
+ }
+
+ assert {
+ condition = contains(["npm", "curl"], var.install_method)
+ error_message = "Install method should be either 'npm' or 'curl'"
+ }
+}
+
+run "workdir_trimmed_of_trailing_slash" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project/"
+ }
+
+ assert {
+ condition = local.workdir == "/home/coder/project"
+ error_message = "workdir should be trimmed of trailing slash"
+ }
+}
+
+run "app_slug_is_consistent" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ }
+
+ assert {
+ condition = local.app_slug == "opencode"
+ error_message = "app_slug should be 'opencode'"
+ }
+
+ assert {
+ condition = local.module_dir_name == ".opencode-module"
+ error_message = "module_dir_name should be '.opencode-module'"
+ }
+}
+
+run "custom_opencode_config" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ opencode_config = jsonencode({
+ theme = "dark"
+ })
+ }
+
+ assert {
+ condition = var.opencode_config != ""
+ error_message = "Custom opencode config should be set"
+ }
+}
+
+run "task_reporting_prompt_included" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ report_tasks = true
+ }
+
+ assert {
+ condition = length(local.final_system_prompt) > 0
+ error_message = "final_system_prompt should be computed"
+ }
+
+ assert {
+ condition = can(regex("Task Reporting", local.final_system_prompt))
+ error_message = "Task reporting prompt should be included when report_tasks is true"
+ }
+}
+
+run "task_reporting_prompt_excluded" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ report_tasks = false
+ }
+
+ assert {
+ condition = !can(regex("Task Reporting", local.final_system_prompt))
+ error_message = "Task reporting prompt should not be included when report_tasks is false"
+ }
+}
+
+run "version_defaults_to_latest" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ }
+
+ assert {
+ condition = var.opencode_version == "latest"
+ error_message = "OpenCode version should default to 'latest'"
+ }
+}
+
+run "agentapi_version_is_set" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ }
+
+ assert {
+ condition = var.agentapi_version == "v0.10.0"
+ error_message = "AgentAPI version should be set to v0.10.0"
+ }
+}
+
+run "mcp_servers_config" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ mcp_servers = jsonencode({
+ filesystem = {
+ command = "npx"
+ args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspaces"]
+ }
+ })
+ }
+
+ assert {
+ condition = var.mcp_servers != ""
+ error_message = "MCP servers configuration should be provided"
+ }
+}
+
+run "opencode_model_config" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ opencode_model = "claude-3.7-sonnet"
+ }
+
+ assert {
+ condition = var.opencode_model == "claude-3.7-sonnet"
+ error_message = "OpenCode model should be set to 'claude-3.7-sonnet'"
+ }
+}
+
+run "mcp_and_model_combined" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ opencode_model = "gpt-4o"
+ mcp_servers = jsonencode({
+ github = {
+ command = "npx"
+ args = ["-y", "@modelcontextprotocol/server-github"]
+ }
+ })
+ }
+
+ assert {
+ condition = var.opencode_model == "gpt-4o"
+ error_message = "OpenCode model should be set"
+ }
+
+ assert {
+ condition = var.mcp_servers != ""
+ error_message = "MCP servers should be configured"
+ }
+}
+
+run "model_defaults_to_empty" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder"
+ }
+
+ assert {
+ condition = var.opencode_model == ""
+ error_message = "OpenCode model should default to empty (provider default)"
+ }
+
+ assert {
+ condition = var.mcp_servers == ""
+ error_message = "MCP servers should default to empty"
+ }
+}
diff --git a/registry/rothnic/modules/opencode/scripts/install.sh b/registry/rothnic/modules/opencode/scripts/install.sh
new file mode 100644
index 000000000..991daf95b
--- /dev/null
+++ b/registry/rothnic/modules/opencode/scripts/install.sh
@@ -0,0 +1,222 @@
+#!/bin/bash
+set -euo pipefail
+
+source "$HOME"/.bashrc 2> /dev/null || true
+
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+# Configuration
+ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
+ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
+ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
+ARG_OPENCODE_CONFIG=$(echo -n "${ARG_OPENCODE_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
+ARG_MCP_SERVERS=$(echo -n "${ARG_MCP_SERVERS:-}" | base64 -d 2> /dev/null || echo "")
+ARG_OPENCODE_MODEL=${ARG_OPENCODE_MODEL:-}
+ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
+ARG_OPENCODE_VERSION=${ARG_OPENCODE_VERSION:-latest}
+ARG_INSTALL_METHOD=${ARG_INSTALL_METHOD:-npm}
+
+NODE_VERSION="${NODE_VERSION:-20.18.0}"
+
+install_nodejs() {
+ if [ "$ARG_INSTALL_METHOD" != "npm" ]; then
+ echo "ERROR: install_method=${ARG_INSTALL_METHOD} is not supported without the curl installer."
+ echo " Use install_method=\"npm\" in the module or Terraform."
+ exit 1
+ fi
+
+ # Shared tool root across workspaces on this host
+ # Use HOME as fallback since /workspaces may not exist or be writable
+ local dev_root="${DEV_ROOT:-$HOME}"
+ local tool_root="${TOOL_ROOT:-$dev_root/.coder-tools}"
+ local node_distro="linux-x64"
+ local node_tarball="node-v${NODE_VERSION}-${node_distro}.tar.xz"
+ local node_dir="${tool_root}/node-v${NODE_VERSION}-${node_distro}"
+
+ mkdir -p "$tool_root"
+
+ if [ ! -d "$node_dir" ]; then
+ echo "Node.js ${NODE_VERSION} not found in cache. Downloading to ${tool_root}..."
+ curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/${node_tarball}" \
+ -o "${tool_root}/${node_tarball}"
+
+ echo "Extracting Node.js..."
+ tar -xJf "${tool_root}/${node_tarball}" -C "$tool_root"
+ rm -f "${tool_root}/${node_tarball}"
+ else
+ echo "β Node.js ${NODE_VERSION} already cached at ${node_dir}"
+ fi
+
+ # Make Node available now
+ export PATH="${node_dir}/bin:$HOME/.local/bin:$PATH"
+
+ # Shared npm cache to speed up repeated installs
+ local npm_cache_dir="${tool_root}/npm-cache"
+ mkdir -p "$npm_cache_dir"
+ export NPM_CONFIG_CACHE="$npm_cache_dir"
+
+ # Persist PATH + npm cache so start.sh and future shells see it
+ if ! grep -q "node-v${NODE_VERSION}-${node_distro}/bin" "$HOME/.bashrc" 2> /dev/null; then
+ {
+ echo "export PATH=\"${node_dir}/bin:\$HOME/.local/bin:\$PATH\""
+ echo "export NPM_CONFIG_CACHE=\"${npm_cache_dir}\""
+ } >> "$HOME/.bashrc"
+ fi
+
+ if ! command_exists node; then
+ echo "ERROR: Node.js still not on PATH after tarball install"
+ exit 1
+ fi
+
+ echo "β Node.js installed via tarball: $(node --version)"
+}
+
+install_opencode() {
+ mkdir -p "$HOME/.local/bin"
+ export PATH="$HOME/.local/bin:$PATH"
+
+ if ! command_exists opencode; then
+ echo "Installing OpenCode via npm (version: ${ARG_OPENCODE_VERSION})..."
+
+ npm config set prefix "$HOME/.local" > /dev/null 2>&1 || true
+
+ if ! grep -q 'PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc" 2> /dev/null; then
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc"
+ fi
+
+ if [ "$ARG_OPENCODE_VERSION" = "latest" ]; then
+ npm install -g opencode-ai@latest
+ else
+ npm install -g "opencode-ai@${ARG_OPENCODE_VERSION}"
+ fi
+
+ export PATH="$HOME/.local/bin:$PATH"
+
+ if ! command_exists opencode; then
+ echo "ERROR: Failed to install OpenCode"
+ exit 1
+ fi
+
+ echo "β OpenCode installed successfully: $(opencode --version 2>&1 | head -1)"
+ else
+ echo "β OpenCode already installed: $(opencode --version 2>&1 | head -1)"
+ fi
+}
+
+check_github_authentication() {
+ echo "Checking GitHub authentication..."
+
+ if [ -n "${GITHUB_TOKEN:-}" ]; then
+ echo "β GITHUB_TOKEN already set via environment/module"
+ return 0
+ fi
+
+ if command_exists coder; then
+ if coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" > /dev/null 2>&1; then
+ local t
+ t=$(coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" 2> /dev/null || echo "")
+ if [ -n "$t" ] && [ "$t" != "null" ]; then
+ export GITHUB_TOKEN="$t"
+ export GH_TOKEN="$t"
+ echo "β Using Coder external auth token for GitHub"
+ return 0
+ fi
+ fi
+ fi
+
+ if command_exists gh && gh auth status > /dev/null 2>&1; then
+ echo "β GitHub CLI OAuth authentication (gh auth status) is available"
+ return 0
+ fi
+
+ echo "β No GitHub authentication detected."
+ echo " This only affects Git operations / gh; OpenCode can still use other providers."
+ return 0
+}
+
+setup_opencode_configurations() {
+ mkdir -p "$ARG_WORKDIR"
+
+ local module_path="$HOME/.opencode-module"
+ mkdir -p "$module_path"
+
+ setup_opencode_config
+}
+
+setup_opencode_config() {
+ export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
+ local opencode_data_dir="$XDG_DATA_HOME/opencode"
+ local opencode_config_dir="$HOME/.config/opencode"
+
+ mkdir -p "$opencode_data_dir"
+ mkdir -p "$opencode_config_dir"
+
+ # If full custom config provided, use it directly
+ if [ -n "$ARG_OPENCODE_CONFIG" ]; then
+ echo "Setting up OpenCode configuration (opencode.json) from custom config..."
+ echo "$ARG_OPENCODE_CONFIG" > "$opencode_config_dir/opencode.json"
+ return 0
+ fi
+
+ # Otherwise, build config from individual options
+ echo "Building OpenCode configuration..."
+
+ # Start with base config
+ local config='{}'
+
+ # Add model config if specified
+ if [ -n "$ARG_OPENCODE_MODEL" ]; then
+ echo " Adding model configuration: $ARG_OPENCODE_MODEL"
+ config=$(echo "$config" | jq --arg model "$ARG_OPENCODE_MODEL" '. + {
+ "agents": {
+ "coder": {"model": $model},
+ "task": {"model": $model}
+ }
+ }')
+ fi
+
+ # Add MCP servers if specified (OpenCode uses 'mcp' key, not 'mcpServers')
+ if [ -n "$ARG_MCP_SERVERS" ]; then
+ echo " Adding MCP servers configuration..."
+ # Merge MCP servers into config under 'mcp' key
+ local mcp_config
+ mcp_config=$(echo "$ARG_MCP_SERVERS" | jq '.')
+ if [ $? -eq 0 ] && [ -n "$mcp_config" ]; then
+ config=$(echo "$config" | jq --argjson mcp "$mcp_config" '. + {"mcp": $mcp}')
+ else
+ echo " β Warning: Invalid MCP servers JSON, skipping"
+ fi
+ fi
+
+ # Only write config if we have something to configure
+ if [ "$config" != '{}' ]; then
+ echo "$config" | jq '.' > "$opencode_config_dir/opencode.json"
+ echo "β OpenCode config written to $opencode_config_dir/opencode.json"
+ else
+ echo " No custom configuration needed"
+ fi
+}
+
+configure_coder_integration() {
+ if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
+ echo "Configuring OpenCode task reporting..."
+ export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
+ export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
+ echo "β Coder integration configured for task reporting"
+ else
+ echo "Task reporting disabled or no app status slug provided."
+ export CODER_MCP_APP_STATUS_SLUG=""
+ export CODER_MCP_AI_AGENTAPI_URL=""
+ fi
+}
+
+# Main execution
+install_nodejs
+install_opencode
+check_github_authentication
+setup_opencode_configurations
+configure_coder_integration
+
+echo "OpenCode module setup completed."
diff --git a/registry/rothnic/modules/opencode/scripts/start.sh b/registry/rothnic/modules/opencode/scripts/start.sh
new file mode 100644
index 000000000..9f25d4058
--- /dev/null
+++ b/registry/rothnic/modules/opencode/scripts/start.sh
@@ -0,0 +1,108 @@
+#!/bin/bash
+set -euo pipefail
+
+source "$HOME"/.bashrc 2> /dev/null || true
+export PATH="$HOME/.local/bin:$PATH"
+
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
+ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
+ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
+ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
+ARG_RESUME_SESSION=${ARG_RESUME_SESSION:-true}
+ARG_OPENCODE_AUTH_CONFIG=$(echo -n "${ARG_OPENCODE_AUTH_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
+
+validate_opencode_installation() {
+ if ! command_exists opencode; then
+ echo "ERROR: OpenCode not found on PATH. Did install.sh fail?"
+ exit 1
+ fi
+ echo "β OpenCode found: $(opencode --version 2>&1 | head -1 || echo '')"
+}
+
+build_initial_prompt() {
+ local initial_prompt=""
+
+ if [ -n "$ARG_AI_PROMPT" ]; then
+ if [ -n "$ARG_SYSTEM_PROMPT" ]; then
+ initial_prompt="$ARG_SYSTEM_PROMPT
+
+$ARG_AI_PROMPT"
+ else
+ initial_prompt="$ARG_AI_PROMPT"
+ fi
+ fi
+
+ echo "$initial_prompt"
+}
+
+setup_github_authentication() {
+ export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
+ local opencode_data_dir="$XDG_DATA_HOME/opencode"
+ local auth_file="$opencode_data_dir/auth.json"
+
+ echo "Setting up OpenCode / GitHub authentication..."
+ mkdir -p "$opencode_data_dir"
+
+ # 1) If the module is given a full auth.json blob, use it verbatim.
+ if [ -n "$ARG_OPENCODE_AUTH_CONFIG" ]; then
+ echo "β Using pre-configured OpenCode auth.json from module variable"
+ echo "$ARG_OPENCODE_AUTH_CONFIG" > "$auth_file"
+ fi
+
+ # 2) For general GitHub use (git, gh), try to populate GITHUB_TOKEN / GH_TOKEN.
+ # We do NOT derive auth.json from these tokens.
+ if [ -z "${GITHUB_TOKEN:-}" ]; then
+ if command_exists coder; then
+ local t
+ t=$(coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" 2> /dev/null || echo "")
+ if [ -n "$t" ] && [ "$t" != "null" ]; then
+ export GITHUB_TOKEN="$t"
+ export GH_TOKEN="$t"
+ echo "β Using Coder external auth token for GitHub (GITHUB_TOKEN/GH_TOKEN)"
+ fi
+ fi
+ else
+ export GH_TOKEN="$GITHUB_TOKEN"
+ echo "β Using GITHUB_TOKEN from module configuration"
+ fi
+
+ # 3) If still no token env, fall back to gh CLI if it's logged in.
+ if [ -z "${GITHUB_TOKEN:-}" ] && command_exists gh && gh auth status > /dev/null 2>&1; then
+ echo "β GitHub CLI auth is available (gh auth status ok)"
+ fi
+
+ # 4) If we still don't have an auth.json, warn, but don't fabricate one.
+ if [ ! -f "$auth_file" ]; then
+ echo "β No OpenCode auth.json present."
+ echo " Copilot / provider credentials must be set by:"
+ echo " - Running 'opencode auth login' and wiring that auth.json into opencode_auth_config"
+ echo " - Or using another provider via env vars / config"
+ fi
+}
+
+start_agentapi() {
+ echo "Starting in directory: $ARG_WORKDIR"
+ cd "$ARG_WORKDIR"
+
+ echo "Starting OpenCode TUI with agentapi..."
+ local initial_prompt
+ initial_prompt=$(build_initial_prompt)
+
+ # Run opencode with agentapi, backgrounded so start script returns quickly
+ # The agentapi module's wrapper expects the script to exit so it can run its wait loop
+ # Use standard terminal dimensions (120x40) for TUI stability - prevents browser auto-scroll issues
+ if [ -n "$initial_prompt" ]; then
+ echo "Using initial prompt with system context"
+ agentapi server -I="$initial_prompt" --type=opencode --term-width 120 --term-height 40 -- opencode "$ARG_WORKDIR" &
+ else
+ agentapi server --type=opencode --term-width 120 --term-height 40 -- opencode "$ARG_WORKDIR" &
+ fi
+}
+
+setup_github_authentication
+validate_opencode_installation
+start_agentapi