diff --git a/.icons/opencode.svg b/.icons/opencode.svg
new file mode 100644
index 000000000..b79c7332e
--- /dev/null
+++ b/.icons/opencode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/registry/coder-labs/modules/opencode/README.md b/registry/coder-labs/modules/opencode/README.md
new file mode 100644
index 000000000..c06c12715
--- /dev/null
+++ b/registry/coder-labs/modules/opencode/README.md
@@ -0,0 +1,108 @@
+---
+display_name: OpenCode
+icon: ../../../../.icons/opencode.svg
+description: Run OpenCode AI coding assistant for AI-powered terminal assistance
+verified: false
+tags: [agent, opencode, ai, tasks]
+---
+
+# OpenCode
+
+Run [OpenCode](https://opencode.ai) AI coding assistant in your workspace for intelligent code generation, analysis, and development assistance. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for seamless task reporting in the Coder UI.
+
+```tf
+module "opencode" {
+ source = "registry.coder.com/coder-labs/opencode/coder"
+ version = "0.1.0"
+ agent_id = coder_agent.example.id
+ workdir = "/home/coder/project"
+}
+```
+
+## Prerequisites
+
+- **Authentication credentials** - OpenCode auth.json file is required for non-interactive authentication, you can find this file on your system: `$HOME/.local/share/opencode/auth.json`
+
+## Examples
+
+### Basic Usage with Tasks
+
+```tf
+resource "coder_ai_task" "task" {
+ app_id = module.opencode.task_app_id
+}
+
+module "opencode" {
+ source = "registry.coder.com/coder-labs/opencode/coder"
+ version = "0.1.0"
+ agent_id = coder_agent.example.id
+ workdir = "/home/coder/project"
+
+ ai_prompt = coder_ai_task.task.prompt
+
+ auth_json = <<-EOT
+{
+ "google": {
+ "type": "api",
+ "key": "gem-xxx-xxxx"
+ },
+ "anthropic": {
+ "type": "api",
+ "key": "sk-ant-api03-xxx-xxxxxxx"
+ }
+}
+EOT
+
+ config_json = jsonencode({
+ "$schema" = "https://opencode.ai/config.json"
+ mcp = {
+ filesystem = {
+ command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/home/coder/projects"]
+ enabled = true
+ type = "local"
+ environment = {
+ SOME_VARIABLE_X = "value"
+ }
+ }
+ playwright = {
+ command = ["npx", "-y", "@playwright/mcp@latest", "--headless", "--isolated"]
+ enabled = true
+ type = "local"
+ }
+ }
+ model = "anthropic/claude-sonnet-4-20250514"
+ })
+
+ pre_install_script = <<-EOT
+ #!/bin/bash
+ curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
+ sudo apt-get install -y nodejs
+ EOT
+}
+```
+
+### Standalone CLI Mode
+
+Run OpenCode as a command-line tool without web interface or task reporting:
+
+```tf
+module "opencode" {
+ source = "registry.coder.com/coder-labs/opencode/coder"
+ version = "0.1.0"
+ agent_id = coder_agent.example.id
+ workdir = "/home/coder"
+ report_tasks = false
+ cli_app = true
+}
+```
+
+## Troubleshooting
+
+If you encounter any issues, check the log files in the `~/.opencode-module` directory within your workspace for detailed information.
+
+## References
+
+- [Opencode JSON Config](https://opencode.ai/docs/config/)
+- [OpenCode Documentation](https://opencode.ai/docs)
+- [AgentAPI Documentation](https://github.com/coder/agentapi)
+- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
diff --git a/registry/coder-labs/modules/opencode/main.test.ts b/registry/coder-labs/modules/opencode/main.test.ts
new file mode 100644
index 000000000..dec42a592
--- /dev/null
+++ b/registry/coder-labs/modules/opencode/main.test.ts
@@ -0,0 +1,362 @@
+import {
+ test,
+ afterEach,
+ describe,
+ setDefaultTimeout,
+ beforeAll,
+ expect,
+} from "bun:test";
+import { execContainer, readFileContainer, runTerraformInit } from "~test";
+import {
+ loadTestFile,
+ writeExecutable,
+ setup as setupUtil,
+ execModuleScript,
+ expectAgentAPIStarted,
+} from "../../../coder/modules/agentapi/test-util";
+import dedent from "dedent";
+
+let cleanupFunctions: (() => Promise)[] = [];
+const registerCleanup = (cleanup: () => Promise) => {
+ cleanupFunctions.push(cleanup);
+};
+afterEach(async () => {
+ const cleanupFnsCopy = cleanupFunctions.slice().reverse();
+ cleanupFunctions = [];
+ for (const cleanup of cleanupFnsCopy) {
+ try {
+ await cleanup();
+ } catch (error) {
+ console.error("Error during cleanup:", error);
+ }
+ }
+});
+
+interface SetupProps {
+ skipAgentAPIMock?: boolean;
+ skipOpencodeMock?: boolean;
+ moduleVariables?: Record;
+ agentapiMockScript?: string;
+}
+
+const setup = async (props?: SetupProps): Promise<{ id: string }> => {
+ const projectDir = "/home/coder/project";
+ const { id } = await setupUtil({
+ moduleDir: import.meta.dir,
+ moduleVariables: {
+ install_opencode: props?.skipOpencodeMock ? "true" : "false",
+ install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
+ workdir: projectDir,
+ ...props?.moduleVariables,
+ },
+ registerCleanup,
+ projectDir,
+ skipAgentAPIMock: props?.skipAgentAPIMock,
+ agentapiMockScript: props?.agentapiMockScript,
+ });
+ if (!props?.skipOpencodeMock) {
+ await writeExecutable({
+ containerId: id,
+ filePath: "/usr/bin/opencode",
+ content: await loadTestFile(import.meta.dir, "opencode-mock.sh"),
+ });
+ }
+ return { id };
+};
+
+setDefaultTimeout(60 * 1000);
+
+describe("opencode", async () => {
+ beforeAll(async () => {
+ await runTerraformInit(import.meta.dir);
+ });
+
+ test("happy-path", async () => {
+ const { id } = await setup();
+ await execModuleScript(id);
+ await expectAgentAPIStarted(id);
+ });
+
+ test("install-opencode-version", async () => {
+ const version_to_install = "0.1.0";
+ const { id } = await setup({
+ skipOpencodeMock: true,
+ moduleVariables: {
+ install_opencode: "true",
+ opencode_version: version_to_install,
+ pre_install_script: dedent`
+ #!/usr/bin/env bash
+ set -euo pipefail
+
+ # Mock the opencode install for testing
+ mkdir -p /home/coder/.opencode/bin
+ echo '#!/bin/bash\necho "opencode mock version ${version_to_install}"' > /home/coder/.opencode/bin/opencode
+ chmod +x /home/coder/.opencode/bin/opencode
+ `,
+ },
+ });
+ await execModuleScript(id);
+ const resp = await execContainer(id, [
+ "bash",
+ "-c",
+ `cat /home/coder/.opencode-module/install.log`,
+ ]);
+ expect(resp.stdout).toContain(version_to_install);
+ });
+
+ test("check-latest-opencode-version-works", async () => {
+ const { id } = await setup({
+ skipOpencodeMock: true,
+ skipAgentAPIMock: true,
+ moduleVariables: {
+ install_opencode: "true",
+ pre_install_script: dedent`
+ #!/usr/bin/env bash
+ set -euo pipefail
+
+ # Mock the opencode install for testing
+ mkdir -p /home/coder/.opencode/bin
+ echo '#!/bin/bash\necho "opencode mock latest version"' > /home/coder/.opencode/bin/opencode
+ chmod +x /home/coder/.opencode/bin/opencode
+ `,
+ },
+ });
+ await execModuleScript(id);
+ await expectAgentAPIStarted(id);
+ });
+
+ test("opencode-auth-json", async () => {
+ const authJson = JSON.stringify({
+ token: "test-auth-token-123",
+ user: "test-user",
+ });
+ const { id } = await setup({
+ moduleVariables: {
+ auth_json: authJson,
+ },
+ });
+ await execModuleScript(id);
+
+ const authFile = await readFileContainer(
+ id,
+ "/home/coder/.local/share/opencode/auth.json",
+ );
+
+ expect(authFile).toContain("test-auth-token-123");
+ expect(authFile).toContain("test-user");
+ });
+
+ test("opencode-config-json", async () => {
+ const configJson = JSON.stringify({
+ $schema: "https://opencode.ai/config.json",
+ mcp: {
+ test: {
+ command: ["test-cmd"],
+ type: "local",
+ },
+ },
+ model: "anthropic/claude-sonnet-4-20250514",
+ });
+ const { id } = await setup({
+ moduleVariables: {
+ config_json: configJson,
+ },
+ });
+ await execModuleScript(id);
+
+ const configFile = await readFileContainer(
+ id,
+ "/home/coder/.config/opencode/opencode.json",
+ );
+ expect(configFile).toContain("test-cmd");
+ expect(configFile).toContain("anthropic/claude-sonnet-4-20250514");
+ });
+
+ test("opencode-ai-prompt", async () => {
+ const prompt = "This is a task prompt for OpenCode.";
+ const { id } = await setup({
+ moduleVariables: {
+ ai_prompt: prompt,
+ },
+ });
+ await execModuleScript(id);
+
+ const resp = await execContainer(id, [
+ "bash",
+ "-c",
+ `cat /home/coder/.opencode-module/agentapi-start.log`,
+ ]);
+ expect(resp.stdout).toContain(prompt);
+ });
+
+ test("opencode-continue-flag", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ continue: "true",
+ ai_prompt: "test prompt",
+ },
+ });
+ await execModuleScript(id);
+
+ const startLog = await execContainer(id, [
+ "bash",
+ "-c",
+ "cat /home/coder/.opencode-module/agentapi-start.log",
+ ]);
+ expect(startLog.stdout).toContain("--continue");
+ });
+
+ test("opencode-continue-with-session-id", async () => {
+ const sessionId = "session-123";
+ const { id } = await setup({
+ moduleVariables: {
+ continue: "true",
+ session_id: sessionId,
+ ai_prompt: "test prompt",
+ },
+ });
+ await execModuleScript(id);
+
+ const startLog = await execContainer(id, [
+ "bash",
+ "-c",
+ "cat /home/coder/.opencode-module/agentapi-start.log",
+ ]);
+ expect(startLog.stdout).toContain("--continue");
+ expect(startLog.stdout).toContain(`--session ${sessionId}`);
+ });
+
+ test("opencode-session-id", async () => {
+ const sessionId = "session-123";
+ const { id } = await setup({
+ moduleVariables: {
+ session_id: sessionId,
+ ai_prompt: "test prompt",
+ },
+ });
+ await execModuleScript(id);
+
+ const startLog = await execContainer(id, [
+ "bash",
+ "-c",
+ "cat /home/coder/.opencode-module/agentapi-start.log",
+ ]);
+ expect(startLog.stdout).toContain(`--session ${sessionId}`);
+ });
+
+ test("opencode-report-tasks-enabled", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ report_tasks: "true",
+ ai_prompt: "test prompt",
+ },
+ });
+ await execModuleScript(id);
+
+ const startLog = await execContainer(id, [
+ "bash",
+ "-c",
+ "cat /home/coder/.opencode-module/agentapi-start.log",
+ ]);
+ expect(startLog.stdout).toContain(
+ "report your progress using coder_report_task",
+ );
+ });
+
+ test("opencode-report-tasks-disabled", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ report_tasks: "false",
+ ai_prompt: "test prompt",
+ },
+ });
+ await execModuleScript(id);
+
+ const startLog = await execContainer(id, [
+ "bash",
+ "-c",
+ "cat /home/coder/.opencode-module/agentapi-start.log",
+ ]);
+ expect(startLog.stdout).not.toContain(
+ "report your progress using coder_report_task",
+ );
+ });
+
+ test("cli-app-creation", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ cli_app: "true",
+ cli_app_display_name: "OpenCode Terminal",
+ },
+ });
+ await execModuleScript(id);
+ // CLI app creation is handled by the agentapi module
+ // We just verify the setup completed successfully
+ await expectAgentAPIStarted(id);
+ });
+
+ test("pre-post-install-scripts", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ pre_install_script: "#!/bin/bash\necho 'opencode-pre-install-script'",
+ post_install_script: "#!/bin/bash\necho 'opencode-post-install-script'",
+ },
+ });
+ await execModuleScript(id);
+
+ const preInstallLog = await readFileContainer(
+ id,
+ "/home/coder/.opencode-module/pre_install.log",
+ );
+ expect(preInstallLog).toContain("opencode-pre-install-script");
+
+ const postInstallLog = await readFileContainer(
+ id,
+ "/home/coder/.opencode-module/post_install.log",
+ );
+ expect(postInstallLog).toContain("opencode-post-install-script");
+ });
+
+ test("workdir-variable", async () => {
+ const workdir = "/home/coder/opencode-test-folder";
+ const { id } = await setup({
+ skipOpencodeMock: false,
+ moduleVariables: {
+ workdir,
+ },
+ });
+ await execModuleScript(id);
+
+ const resp = await readFileContainer(
+ id,
+ "/home/coder/.opencode-module/agentapi-start.log",
+ );
+ expect(resp).toContain(workdir);
+ });
+
+ test("subdomain-enabled", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ subdomain: "true",
+ },
+ });
+ await execModuleScript(id);
+ // Subdomain configuration is handled by the agentapi module
+ // We just verify the setup completed successfully
+ await expectAgentAPIStarted(id);
+ });
+
+ test("custom-display-names", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ web_app_display_name: "Custom OpenCode Web",
+ cli_app_display_name: "Custom OpenCode CLI",
+ cli_app: "true",
+ },
+ });
+ await execModuleScript(id);
+ // Display names are handled by the agentapi module
+ // We just verify the setup completed successfully
+ await expectAgentAPIStarted(id);
+ });
+});
diff --git a/registry/coder-labs/modules/opencode/main.tf b/registry/coder-labs/modules/opencode/main.tf
new file mode 100644
index 000000000..df7f946f2
--- /dev/null
+++ b/registry/coder-labs/modules/opencode/main.tf
@@ -0,0 +1,203 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 2.12"
+ }
+ }
+}
+
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+data "coder_workspace" "me" {}
+
+data "coder_workspace_owner" "me" {}
+
+variable "order" {
+ type = number
+ description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
+ 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.svg"
+}
+
+variable "workdir" {
+ type = string
+ description = "The folder to run OpenCode in."
+}
+
+variable "report_tasks" {
+ type = bool
+ description = "Whether to enable task reporting to Coder UI via AgentAPI"
+ default = true
+}
+
+variable "cli_app" {
+ type = bool
+ description = "Whether to create a CLI app for OpenCode"
+ default = false
+}
+
+variable "web_app_display_name" {
+ type = string
+ description = "Display name for the web app"
+ default = "OpenCode"
+}
+
+variable "cli_app_display_name" {
+ type = string
+ description = "Display name for the CLI app"
+ default = "OpenCode CLI"
+}
+
+variable "pre_install_script" {
+ type = string
+ description = "Custom script to run before installing OpenCode."
+ default = null
+}
+
+variable "post_install_script" {
+ type = string
+ description = "Custom script to run after installing OpenCode."
+ default = null
+}
+
+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.11.2"
+}
+
+variable "ai_prompt" {
+ type = string
+ description = "Initial task prompt for OpenCode."
+ default = ""
+}
+
+variable "subdomain" {
+ type = bool
+ description = "Whether to use a subdomain for AgentAPI."
+ default = false
+}
+
+variable "install_opencode" {
+ type = bool
+ description = "Whether to install OpenCode."
+ default = true
+}
+
+variable "opencode_version" {
+ type = string
+ description = "The version of OpenCode to install."
+ default = "latest"
+}
+
+variable "continue" {
+ type = bool
+ description = "continue the last session. Uses the --continue flag"
+ default = false
+}
+
+variable "session_id" {
+ type = string
+ description = "Session id to continue. Passed via --session"
+ default = ""
+}
+
+variable "auth_json" {
+ type = string
+ description = "Your auth.json from $HOME/.local/share/opencode/auth.json, Required for non-interactive authentication"
+ default = ""
+}
+
+variable "config_json" {
+ type = string
+ description = "OpenCode JSON config. https://opencode.ai/docs/config/"
+ default = ""
+}
+
+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"
+}
+
+module "agentapi" {
+ source = "registry.coder.com/coder/agentapi/coder"
+ version = "2.0.0"
+
+ agent_id = var.agent_id
+ 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_display_name = var.cli_app ? var.cli_app_display_name : null
+ agentapi_subdomain = var.subdomain
+ folder = local.workdir
+ 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_SESSION_ID='${var.session_id}' \
+ ARG_REPORT_TASKS='${var.report_tasks}' \
+ ARG_CONTINUE='${var.continue}' \
+ /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_OPENCODE_VERSION='${var.opencode_version}' \
+ ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
+ ARG_INSTALL_OPENCODE='${var.install_opencode}' \
+ ARG_REPORT_TASKS='${var.report_tasks}' \
+ ARG_WORKDIR='${local.workdir}' \
+ ARG_AUTH_JSON='${var.auth_json != null ? base64encode(replace(var.auth_json, "'", "'\\''")) : ""}' \
+ ARG_OPENCODE_CONFIG='${var.config_json != null ? base64encode(replace(var.config_json, "'", "'\\''")) : ""}' \
+ /tmp/install.sh
+ EOT
+}
+
+output "task_app_id" {
+ value = module.agentapi.task_app_id
+}
diff --git a/registry/coder-labs/modules/opencode/main.tftest.hcl b/registry/coder-labs/modules/opencode/main.tftest.hcl
new file mode 100644
index 000000000..e8c7954b7
--- /dev/null
+++ b/registry/coder-labs/modules/opencode/main.tftest.hcl
@@ -0,0 +1,374 @@
+run "defaults_are_correct" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ }
+
+ assert {
+ condition = var.install_opencode == true
+ error_message = "OpenCode installation should be enabled by default"
+ }
+
+ assert {
+ condition = var.install_agentapi == true
+ error_message = "AgentAPI installation should be enabled by default"
+ }
+
+ assert {
+ condition = var.agentapi_version == "v0.11.2"
+ error_message = "Default AgentAPI version should be 'v0.11.2'"
+ }
+
+ assert {
+ condition = var.opencode_version == "latest"
+ error_message = "Default OpenCode version should be 'latest'"
+ }
+
+ assert {
+ condition = var.report_tasks == true
+ error_message = "Task reporting should be enabled by default"
+ }
+
+ assert {
+ condition = var.cli_app == false
+ error_message = "CLI app should be disabled by default"
+ }
+
+ assert {
+ condition = var.subdomain == false
+ error_message = "Subdomain should be disabled by default"
+ }
+
+ assert {
+ condition = var.web_app_display_name == "OpenCode"
+ error_message = "Default web app display name should be 'OpenCode'"
+ }
+
+ assert {
+ condition = var.cli_app_display_name == "OpenCode CLI"
+ error_message = "Default CLI app display name should be 'OpenCode CLI'"
+ }
+
+ 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'"
+ }
+
+ assert {
+ condition = local.workdir == "/home/coder/project"
+ error_message = "Workdir should be trimmed of trailing slash"
+ }
+
+ assert {
+ condition = var.continue == false
+ error_message = "Continue flag should be disabled by default"
+ }
+}
+
+run "workdir_trailing_slash_trimmed" {
+ 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 "opencode_version_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ opencode_version = "v1.0.0"
+ }
+
+ assert {
+ condition = var.opencode_version == "v1.0.0"
+ error_message = "OpenCode version should be set correctly"
+ }
+}
+
+run "agentapi_version_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ agentapi_version = "v0.9.0"
+ }
+
+ assert {
+ condition = var.agentapi_version == "v0.9.0"
+ error_message = "AgentAPI version should be set correctly"
+ }
+}
+
+run "cli_app_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ cli_app = true
+ cli_app_display_name = "Custom OpenCode CLI"
+ }
+
+ assert {
+ condition = var.cli_app == true
+ error_message = "CLI app should be enabled when specified"
+ }
+
+ assert {
+ condition = var.cli_app_display_name == "Custom OpenCode CLI"
+ error_message = "Custom CLI app display name should be set"
+ }
+}
+
+run "web_app_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ web_app_display_name = "Custom OpenCode Web"
+ order = 5
+ group = "AI Tools"
+ icon = "/custom/icon.svg"
+ }
+
+ assert {
+ condition = var.web_app_display_name == "Custom OpenCode Web"
+ error_message = "Custom web app display name should be set"
+ }
+
+ assert {
+ condition = var.order == 5
+ error_message = "Custom order should be set"
+ }
+
+ assert {
+ condition = var.group == "AI Tools"
+ error_message = "Custom group should be set"
+ }
+
+ assert {
+ condition = var.icon == "/custom/icon.svg"
+ error_message = "Custom icon should be set"
+ }
+}
+
+run "ai_configuration_variables" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ ai_prompt = "This is a test prompt"
+ session_id = "session-123"
+ continue = true
+ }
+
+ assert {
+ condition = var.ai_prompt == "This is a test prompt"
+ error_message = "AI prompt should be set correctly"
+ }
+
+ assert {
+ condition = var.session_id == "session-123"
+ error_message = "Session ID should be set correctly"
+ }
+
+ assert {
+ condition = var.continue == true
+ error_message = "Continue flag should be set correctly"
+ }
+}
+
+run "auth_json_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ auth_json = "{\"token\": \"test-token\", \"user\": \"test-user\"}"
+ }
+
+ assert {
+ condition = var.auth_json != ""
+ error_message = "Auth JSON should be set"
+ }
+
+ assert {
+ condition = can(jsondecode(var.auth_json))
+ error_message = "Auth JSON should be valid JSON"
+ }
+}
+
+run "config_json_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ config_json = "{\"$schema\": \"https://opencode.ai/config.json\", \"mcp\": {\"test\": {\"command\": [\"test-cmd\"], \"type\": \"local\"}}, \"model\": \"anthropic/claude-sonnet-4-20250514\"}"
+ }
+
+ assert {
+ condition = var.config_json != ""
+ error_message = "OpenCode JSON configuration should be set"
+ }
+
+ assert {
+ condition = can(jsondecode(var.config_json))
+ error_message = "OpenCode JSON configuration should be valid JSON"
+ }
+}
+
+run "task_reporting_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ report_tasks = false
+ }
+
+ assert {
+ condition = var.report_tasks == false
+ error_message = "Task reporting should be disabled when specified"
+ }
+}
+
+run "subdomain_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ subdomain = true
+ }
+
+ assert {
+ condition = var.subdomain == true
+ error_message = "Subdomain should be enabled when specified"
+ }
+}
+
+run "install_flags_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ install_opencode = false
+ install_agentapi = false
+ }
+
+ assert {
+ condition = var.install_opencode == false
+ error_message = "OpenCode installation should be disabled when specified"
+ }
+
+ assert {
+ condition = var.install_agentapi == false
+ error_message = "AgentAPI installation should be disabled when specified"
+ }
+}
+
+run "custom_scripts_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ pre_install_script = "#!/bin/bash\necho 'pre-install'"
+ post_install_script = "#!/bin/bash\necho 'post-install'"
+ }
+
+ assert {
+ condition = var.pre_install_script != null
+ error_message = "Pre-install script should be set"
+ }
+
+ assert {
+ condition = var.post_install_script != null
+ error_message = "Post-install script should be set"
+ }
+
+ assert {
+ condition = can(regex("pre-install", var.pre_install_script))
+ error_message = "Pre-install script should contain expected content"
+ }
+
+ assert {
+ condition = can(regex("post-install", var.post_install_script))
+ error_message = "Post-install script should contain expected content"
+ }
+}
+
+run "empty_variables_handled_correctly" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ ai_prompt = ""
+ session_id = ""
+ auth_json = ""
+ config_json = ""
+ continue = false
+ }
+
+ assert {
+ condition = var.ai_prompt == ""
+ error_message = "Empty AI prompt should be handled correctly"
+ }
+
+ assert {
+ condition = var.session_id == ""
+ error_message = "Empty session ID should be handled correctly"
+ }
+
+ assert {
+ condition = var.auth_json == ""
+ error_message = "Empty auth JSON should be handled correctly"
+ }
+
+ assert {
+ condition = var.config_json == ""
+ error_message = "Empty config JSON should be handled correctly"
+ }
+
+ assert {
+ condition = var.continue == false
+ error_message = "Continue flag default should be handled correctly"
+ }
+}
+
+run "continue_flag_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ continue = true
+ }
+
+ assert {
+ condition = var.continue == true
+ error_message = "Continue flag should be enabled when specified"
+ }
+}
\ No newline at end of file
diff --git a/registry/coder-labs/modules/opencode/scripts/install.sh b/registry/coder-labs/modules/opencode/scripts/install.sh
new file mode 100755
index 000000000..6d5531082
--- /dev/null
+++ b/registry/coder-labs/modules/opencode/scripts/install.sh
@@ -0,0 +1,131 @@
+#!/bin/bash
+set -euo pipefail
+
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
+ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
+ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
+ARG_OPENCODE_VERSION=${ARG_OPENCODE_VERSION:-latest}
+ARG_INSTALL_OPENCODE=${ARG_INSTALL_OPENCODE:-true}
+ARG_AUTH_JSON=$(echo -n "$ARG_AUTH_JSON" | base64 -d 2> /dev/null || echo "")
+ARG_OPENCODE_CONFIG=$(echo -n "$ARG_OPENCODE_CONFIG" | base64 -d 2> /dev/null || echo "")
+
+# Print all received environment variables
+printf "=== INSTALL CONFIG ===\n"
+printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
+printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
+printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG"
+printf "ARG_OPENCODE_VERSION: %s\n" "$ARG_OPENCODE_VERSION"
+printf "ARG_INSTALL_OPENCODE: %s\n" "$ARG_INSTALL_OPENCODE"
+if [ -n "$ARG_AUTH_JSON" ]; then
+ printf "ARG_AUTH_JSON: [AUTH DATA RECEIVED]\n"
+else
+ printf "ARG_AUTH_JSON: [NOT PROVIDED]\n"
+fi
+if [ -n "$ARG_OPENCODE_CONFIG" ]; then
+ printf "ARG_OPENCODE_CONFIG: [RECEIVED]\n"
+else
+ printf "ARG_OPENCODE_CONFIG: [NOT PROVIDED]\n"
+fi
+printf "==================================\n"
+
+install_opencode() {
+ if [ "$ARG_INSTALL_OPENCODE" = "true" ]; then
+ if ! command_exists opencode; then
+ echo "Installing OpenCode (version: ${ARG_OPENCODE_VERSION})..."
+ if [ "$ARG_OPENCODE_VERSION" = "latest" ]; then
+ curl -fsSL https://opencode.ai/install | bash
+ else
+ VERSION=$ARG_OPENCODE_VERSION curl -fsSL https://opencode.ai/install | bash
+ fi
+ export PATH=/home/coder/.opencode/bin:$PATH
+ printf "Opencode location: %s\n" "$(which opencode)"
+ if ! command_exists opencode; then
+ echo "ERROR: Failed to install OpenCode"
+ exit 1
+ fi
+ echo "OpenCode installed successfully"
+ else
+ echo "OpenCode already installed"
+ fi
+ else
+ echo "OpenCode installation skipped (ARG_INSTALL_OPENCODE=false)"
+ fi
+}
+
+setup_opencode_config() {
+ local opencode_config_file="$HOME/.config/opencode/opencode.json"
+ local auth_json_file="$HOME/.local/share/opencode/auth.json"
+
+ mkdir -p "$(dirname "$auth_json_file")"
+ mkdir -p "$(dirname "$opencode_config_file")"
+
+ setup_opencode_auth "$auth_json_file"
+
+ if [ -n "$ARG_OPENCODE_CONFIG" ]; then
+ echo "Writing to the config file"
+ echo "$ARG_OPENCODE_CONFIG" > "$opencode_config_file"
+ fi
+
+ if [ "$ARG_REPORT_TASKS" = "true" ]; then
+ setup_coder_mcp_server "$opencode_config_file"
+ fi
+
+ echo "MCP configuration completed: $opencode_config_file"
+}
+
+setup_opencode_auth() {
+ local auth_json_file="$1"
+
+ if [ -n "$ARG_AUTH_JSON" ]; then
+ echo "$ARG_AUTH_JSON" > "$auth_json_file"
+ printf "added auth json to %s" "$auth_json_file"
+ else
+ printf "auth json not provided"
+ fi
+}
+
+setup_coder_mcp_server() {
+ local opencode_config_file="$1"
+
+ # Set environment variables based on task reporting setting
+ 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"
+
+ # Add coder MCP server configuration to the JSON file
+ echo "Adding Coder MCP server configuration"
+
+ # Create the coder server configuration JSON
+ coder_config=$(
+ cat << EOF
+{
+ "type": "local",
+ "command": ["coder", "exp", "mcp", "server"],
+ "enabled": true,
+ "environment": {
+ "CODER_MCP_APP_STATUS_SLUG": "${CODER_MCP_APP_STATUS_SLUG:-}",
+ "CODER_MCP_AI_AGENTAPI_URL": "${CODER_MCP_AI_AGENTAPI_URL:-}",
+ "CODER_AGENT_URL": "${CODER_AGENT_URL:-}",
+ "CODER_AGENT_TOKEN": "${CODER_AGENT_TOKEN:-}",
+ "CODER_MCP_ALLOWED_TOOLS": "coder_report_task"
+ }
+}
+EOF
+ )
+
+ temp_file=$(mktemp)
+ jq --argjson coder_config "$coder_config" '.mcp.coder = $coder_config' "$opencode_config_file" > "$temp_file"
+ mv "$temp_file" "$opencode_config_file"
+ echo "Coder MCP server configuration added"
+
+}
+
+install_opencode
+setup_opencode_config
+
+echo "OpenCode module setup completed."
diff --git a/registry/coder-labs/modules/opencode/scripts/start.sh b/registry/coder-labs/modules/opencode/scripts/start.sh
new file mode 100755
index 000000000..c3c51beb3
--- /dev/null
+++ b/registry/coder-labs/modules/opencode/scripts/start.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+set -euo pipefail
+
+export PATH=/home/coder/.opencode/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_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
+ARG_SESSION_ID=${ARG_SESSION_ID:-}
+ARG_CONTINUE=${ARG_CONTINUE:-false}
+
+# Print all received environment variables
+printf "=== START CONFIG ===\n"
+printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
+printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
+printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE"
+printf "ARG_SESSION_ID: %s\n" "$ARG_SESSION_ID"
+if [ -n "$ARG_AI_PROMPT" ]; then
+ printf "ARG_AI_PROMPT: [AI PROMPT RECEIVED]\n"
+else
+ printf "ARG_AI_PROMPT: [NOT PROVIDED]\n"
+fi
+printf "==================================\n"
+
+OPENCODE_ARGS=()
+AGENTAPI_ARGS=()
+
+validate_opencode_installation() {
+ if ! command_exists opencode; then
+ printf "ERROR: OpenCode not installed. Set install_opencode to true\n"
+ exit 1
+ fi
+}
+
+build_opencode_args() {
+
+ if [ -n "$ARG_SESSION_ID" ]; then
+ OPENCODE_ARGS+=(--session "$ARG_SESSION_ID")
+ fi
+
+ if [ "$ARG_CONTINUE" = "true" ]; then
+ OPENCODE_ARGS+=(--continue)
+ fi
+
+ if [ -n "$ARG_AI_PROMPT" ]; then
+ if [ "$ARG_REPORT_TASKS" = "true" ]; then
+ PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT"
+ else
+ PROMPT="$ARG_AI_PROMPT"
+ fi
+ AGENTAPI_ARGS+=(-I "$PROMPT")
+ fi
+}
+
+start_agentapi() {
+ printf "Starting in directory: %s\n" "$ARG_WORKDIR"
+ cd "$ARG_WORKDIR"
+
+ build_opencode_args
+
+ printf "Running OpenCode with args: %s\n" "${OPENCODE_ARGS[*]}"
+ echo agentapi server "${AGENTAPI_ARGS[@]}" --type opencode --term-width 67 --term-height 1190 -- opencode "${OPENCODE_ARGS[@]}"
+ agentapi server "${AGENTAPI_ARGS[@]}" --type opencode --term-width 67 --term-height 1190 -- opencode "${OPENCODE_ARGS[@]}"
+}
+
+validate_opencode_installation
+start_agentapi
diff --git a/registry/coder-labs/modules/opencode/testdata/opencode-mock.sh b/registry/coder-labs/modules/opencode/testdata/opencode-mock.sh
new file mode 100644
index 000000000..dcde8756c
--- /dev/null
+++ b/registry/coder-labs/modules/opencode/testdata/opencode-mock.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Mock OpenCode CLI for testing purposes
+# This script simulates the OpenCode command-line interface
+
+echo "OpenCode Mock CLI - Test Version"
+echo "Args received: $*"
+
+# Simulate opencode behavior based on arguments
+case "$1" in
+ --version | -v)
+ echo "opencode mock version 0.1.0-test"
+ ;;
+ --help | -h)
+ echo "OpenCode Mock Help"
+ echo "Usage: opencode [options] [command]"
+ echo "This is a mock version for testing"
+ ;;
+ *)
+ echo "Running OpenCode mock with arguments: $*"
+ echo "Mock execution completed successfully"
+ ;;
+esac
+
+exit 0