diff --git a/.icons/claude.svg b/.icons/claude.svg new file mode 100644 index 00000000..998fb0d5 --- /dev/null +++ b/.icons/claude.svg @@ -0,0 +1,4 @@ + + + + diff --git a/.icons/goose.svg b/.icons/goose.svg new file mode 100644 index 00000000..cbbe8419 --- /dev/null +++ b/.icons/goose.svg @@ -0,0 +1,4 @@ + + + + diff --git a/claude-code/README.md b/claude-code/README.md new file mode 100644 index 00000000..2422f174 --- /dev/null +++ b/claude-code/README.md @@ -0,0 +1,112 @@ +--- +display_name: Claude Code +description: Run Claude Code in your workspace +icon: ../.icons/claude.svg +maintainer_github: coder +verified: true +tags: [agent, claude-code] +--- + +# Claude Code + +Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks. + +```tf +module "claude-code" { + source = "registry.coder.com/modules/claude-code/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_claude_code = true + claude_code_version = "latest" +} +``` + +### Prerequisites + +- Node.js and npm must be installed in your workspace to install Claude Code +- `screen` must be installed in your workspace to run Claude Code in the background +- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template + +The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces. + +## Examples + +### Run in the background and report tasks (Experimental) + +> This functionality is in early access and subject to change. Do not run in +> production as it is unstable. Instead, deploy these changes into a demo or +> staging environment. +> +> Join our [Discord channel](https://discord.gg/coder) or +> [contact us](https://coder.com/contact) to get help or share feedback. + +Your workspace must have `screen` installed to use this. + +```tf +variable "anthropic_api_key" { + type = string + description = "The Anthropic API key" + sensitive = true +} + +module "coder-login" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/coder-login/coder" + version = "1.0.31" + agent_id = coder_agent.example.id +} + +data "coder_parameter" "ai_prompt" { + type = "string" + name = "AI Prompt" + default = "" + description = "Write a prompt for Claude Code" + mutable = true +} + +# Set the prompt and system prompt for Claude Code via environment variables +resource "coder_agent" "main" { + # ... + env = { + CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter + CODER_MCP_CLAUDE_TASK_PROMPT = data.coder_parameter.ai_prompt.value + CODER_MCP_APP_STATUS_SLUG = "claude-code" + CODER_MCP_CLAUDE_SYSTEM_PROMPT = <<-EOT + You are a helpful assistant that can help with code. + EOT + } +} + +module "claude-code" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/claude-code/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_claude_code = true + claude_code_version = "0.2.57" + + # Enable experimental features + experiment_use_screen = true + experiment_report_tasks = true +} +``` + +## Run standalone + +Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it directly without using screen or any task reporting to the Coder UI. + +```tf +module "claude-code" { + source = "registry.coder.com/modules/claude-code/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_claude_code = true + claude_code_version = "latest" + + # Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL + icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png" +} +``` diff --git a/claude-code/main.tf b/claude-code/main.tf new file mode 100644 index 00000000..349af17f --- /dev/null +++ b/claude-code/main.tf @@ -0,0 +1,170 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.17" + } + } +} + +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 "icon" { + type = string + description = "The icon to use for the app." + default = "/icon/claude.svg" +} + +variable "folder" { + type = string + description = "The folder to run Claude Code in." + default = "/home/coder" +} + +variable "install_claude_code" { + type = bool + description = "Whether to install Claude Code." + default = true +} + +variable "claude_code_version" { + type = string + description = "The version of Claude Code to install." + default = "latest" +} + +variable "experiment_use_screen" { + type = bool + description = "Whether to use screen for running Claude Code in the background." + default = false +} + +variable "experiment_report_tasks" { + type = bool + description = "Whether to enable task reporting." + default = false +} + +# Install and Initialize Claude Code +resource "coder_script" "claude_code" { + agent_id = var.agent_id + display_name = "Claude Code" + icon = var.icon + script = <<-EOT + #!/bin/bash + set -e + + # Function to check if a command exists + command_exists() { + command -v "$1" >/dev/null 2>&1 + } + + # Install Claude Code if enabled + if [ "${var.install_claude_code}" = "true" ]; then + if ! command_exists npm; then + echo "Error: npm is not installed. Please install Node.js and npm first." + exit 1 + fi + echo "Installing Claude Code..." + npm install -g @anthropic-ai/claude-code@${var.claude_code_version} + fi + + if [ "${var.experiment_report_tasks}" = "true" ]; then + echo "Configuring Claude Code to report tasks via Coder MCP..." + coder exp mcp configure claude-code ${var.folder} + fi + + # Run with screen if enabled + if [ "${var.experiment_use_screen}" = "true" ]; then + echo "Running Claude Code in the background..." + + # Check if screen is installed + if ! command_exists screen; then + echo "Error: screen is not installed. Please install screen manually." + exit 1 + fi + + touch "$HOME/.claude-code.log" + + # Ensure the screenrc exists + if [ ! -f "$HOME/.screenrc" ]; then + echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.claude-code.log" + echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc" + fi + + if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then + echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log" + echo "multiuser on" >> "$HOME/.screenrc" + fi + + if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then + echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log" + echo "acladd $(whoami)" >> "$HOME/.screenrc" + fi + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + screen -U -dmS claude-code bash -c ' + cd ${var.folder} + claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log" + exec bash + ' + # Extremely hacky way to send the prompt to the screen session + # This will be fixed in the future, but `claude` was not sending MCP + # tasks when an initial prompt is provided. + screen -S claude-code -X stuff "$CODER_MCP_CLAUDE_TASK_PROMPT" + sleep 5 + screen -S claude-code -X stuff "^M" + else + # Check if claude is installed before running + if ! command_exists claude; then + echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually." + exit 1 + fi + fi + EOT + run_on_start = true +} + +resource "coder_app" "claude_code" { + slug = "claude-code" + display_name = "Claude Code" + agent_id = var.agent_id + command = <<-EOT + #!/bin/bash + set -e + + if [ "${var.experiment_use_screen}" = "true" ]; then + if screen -list | grep -q "claude-code"; then + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + echo "Attaching to existing Claude Code session." | tee -a "$HOME/.claude-code.log" + screen -xRR claude-code + else + echo "Starting a new Claude Code session." | tee -a "$HOME/.claude-code.log" + screen -S claude-code bash -c 'export LANG=en_US.UTF-8; export LC_ALL=en_US.UTF-8; claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash' + fi + else + cd ${var.folder} + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + claude + fi + EOT + icon = var.icon +} diff --git a/filebrowser/README.md b/filebrowser/README.md index 13e06577..3a0e56bd 100644 --- a/filebrowser/README.md +++ b/filebrowser/README.md @@ -15,7 +15,7 @@ A file browser for your workspace. module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id } ``` @@ -30,7 +30,7 @@ module "filebrowser" { module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id folder = "/home/coder/project" } @@ -42,7 +42,7 @@ module "filebrowser" { module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id database_path = ".config/filebrowser.db" } @@ -54,7 +54,7 @@ module "filebrowser" { module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.29" + version = "1.0.31" agent_id = coder_agent.example.id agent_name = "main" subdomain = false diff --git a/filebrowser/main.test.ts b/filebrowser/main.test.ts deleted file mode 100644 index 368075b2..00000000 --- a/filebrowser/main.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { - executeScriptInContainer, - runTerraformApply, - runTerraformInit, - testRequiredVariables, -} from "../test"; - -describe("filebrowser", async () => { - await runTerraformInit(import.meta.dir); - - testRequiredVariables(import.meta.dir, { - agent_id: "foo", - }); - - it("fails with wrong database_path", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - database_path: "nofb", - }).catch((e) => { - if (!e.message.startsWith("\nError: Invalid value for variable")) { - throw e; - } - }); - }); - - it("runs with default", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - }); - const output = await executeScriptInContainer(state, "alpine"); - expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ - "\u001b[0;1mInstalling filebrowser ", - "", - "🥳 Installation complete! ", - "", - "👷 Starting filebrowser in background... ", - "", - "📂 Serving /root at http://localhost:13339 ", - "", - "Running 'filebrowser --noauth --root /root --port 13339 --baseurl ' ", - "", - "📝 Logs at /tmp/filebrowser.log", - ]); - }); - - it("runs with database_path var", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - database_path: ".config/filebrowser.db", - }); - const output = await executeScriptInContainer(state, "alpine"); - expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ - "\u001b[0;1mInstalling filebrowser ", - "", - "🥳 Installation complete! ", - "", - "👷 Starting filebrowser in background... ", - "", - "📂 Serving /root at http://localhost:13339 ", - "", - "Running 'filebrowser --noauth --root /root --port 13339 -d .config/filebrowser.db --baseurl ' ", - "", - "📝 Logs at /tmp/filebrowser.log", - ]); - }); - - it("runs with folder var", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - folder: "/home/coder/project", - }); - const output = await executeScriptInContainer(state, "alpine"); - expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ - "\u001b[0;1mInstalling filebrowser ", - "", - "🥳 Installation complete! ", - "", - "👷 Starting filebrowser in background... ", - "", - "📂 Serving /home/coder/project at http://localhost:13339 ", - "", - "Running 'filebrowser --noauth --root /home/coder/project --port 13339 --baseurl ' ", - "", - "📝 Logs at /tmp/filebrowser.log", - ]); - }); - - it("runs with subdomain=false", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - agent_name: "main", - subdomain: false, - }); - const output = await executeScriptInContainer(state, "alpine"); - expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ - "\u001B[0;1mInstalling filebrowser ", - "", - "🥳 Installation complete! ", - "", - "👷 Starting filebrowser in background... ", - "", - "📂 Serving /root at http://localhost:13339 ", - "", - "Running 'filebrowser --noauth --root /root --port 13339 --baseurl /@default/default.main/apps/filebrowser' ", - "", - "📝 Logs at /tmp/filebrowser.log", - ]); - }); -}); diff --git a/filebrowser/run.sh b/filebrowser/run.sh index b8927582..84810e4e 100644 --- a/filebrowser/run.sh +++ b/filebrowser/run.sh @@ -22,11 +22,11 @@ export FB_DATABASE="${DB_PATH}" # Check if filebrowser db exists if [[ ! -f "${DB_PATH}" ]]; then - filebrowser config init 2>&1 | tee -a ${LOG_PATH} - filebrowser users add admin "" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH} + filebrowser config init 2>&1 | tee -a ${LOG_PATH} + filebrowser users add admin "" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH} fi -filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH} +filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH} printf "👷 Starting filebrowser in background... \n\n" diff --git a/goose/README.md b/goose/README.md new file mode 100644 index 00000000..5735cb76 --- /dev/null +++ b/goose/README.md @@ -0,0 +1,125 @@ +--- +display_name: Goose +description: Run Goose in your workspace +icon: ../.icons/goose.svg +maintainer_github: coder +verified: true +tags: [agent, goose] +--- + +# Goose + +Run the [Goose](https://block.github.io/goose/) agent in your workspace to generate code and perform tasks. + +```tf +module "goose" { + source = "registry.coder.com/modules/goose/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_goose = true + goose_version = "v1.0.16" +} +``` + +### Prerequisites + +- `screen` must be installed in your workspace to run Goose in the background +- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template + +The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces. + +## Examples + +Your workspace must have `screen` installed to use this. + +### Run in the background and report tasks (Experimental) + +> This functionality is in early access and subject to change. Do not run in +> production as it is unstable. Instead, deploy these changes into a demo or +> staging environment. +> +> Join our [Discord channel](https://discord.gg/coder) or +> [contact us](https://coder.com/contact) to get help or share feedback. + +```tf +module "coder-login" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/coder-login/coder" + version = "1.0.31" + agent_id = coder_agent.example.id +} + +variable "anthropic_api_key" { + type = string + description = "The Anthropic API key" + sensitive = true +} + +data "coder_parameter" "ai_prompt" { + type = "string" + name = "AI Prompt" + default = "" + description = "Write a prompt for Goose" + mutable = true +} + +# Set the prompt and system prompt for Goose via environment variables +resource "coder_agent" "main" { + # ... + env = { + GOOSE_SYSTEM_PROMPT = <<-EOT + You are a helpful assistant that can help write code. + + Run all long running tasks (e.g. npm run dev) in the background and not in the foreground. + + Periodically check in on background tasks. + + Notify Coder of the status of the task before and after your steps. + EOT + GOOSE_TASK_PROMPT = data.coder_parameter.ai_prompt.value + + # An API key is required for experiment_auto_configure + # See https://block.github.io/goose/docs/getting-started/providers + ANTHROPIC_API_KEY = var.anthropic_api_key # or use a coder_parameter + } +} + +module "goose" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/goose/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_goose = true + goose_version = "v1.0.16" + + # Enable experimental features + experiment_report_tasks = true + + # Avoid configuring Goose manually + experiment_auto_configure = true + + # Required for experiment_auto_configure + experiment_goose_provider = "anthropic" + experiment_goose_model = "claude-3-5-sonnet-latest" +} +``` + +## Run standalone + +Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or any task reporting to the Coder UI. + +```tf +module "goose" { + source = "registry.coder.com/modules/goose/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_goose = true + goose_version = "v1.0.16" + + # Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL + icon = "https://raw.githubusercontent.com/block/goose/refs/heads/main/ui/desktop/src/images/icon.svg" +} +``` diff --git a/goose/main.tf b/goose/main.tf new file mode 100644 index 00000000..fcb6baaa --- /dev/null +++ b/goose/main.tf @@ -0,0 +1,207 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.17" + } + } +} + +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 "icon" { + type = string + description = "The icon to use for the app." + default = "/icon/goose.svg" +} + +variable "folder" { + type = string + description = "The folder to run Goose in." + default = "/home/coder" +} + +variable "install_goose" { + type = bool + description = "Whether to install Goose." + default = true +} + +variable "goose_version" { + type = string + description = "The version of Goose to install." + default = "stable" +} + +variable "experiment_use_screen" { + type = bool + description = "Whether to use screen for running Goose in the background." + default = false +} + +variable "experiment_report_tasks" { + type = bool + description = "Whether to enable task reporting." + default = false +} + +variable "experiment_auto_configure" { + type = bool + description = "Whether to automatically configure Goose." + default = false +} + +variable "experiment_goose_provider" { + type = string + description = "The provider to use for Goose (e.g., anthropic)." + default = null +} + +variable "experiment_goose_model" { + type = string + description = "The model to use for Goose (e.g., claude-3-5-sonnet-latest)." + default = null +} + +# Install and Initialize Goose +resource "coder_script" "goose" { + agent_id = var.agent_id + display_name = "Goose" + icon = var.icon + script = <<-EOT + #!/bin/bash + set -e + + # Function to check if a command exists + command_exists() { + command -v "$1" >/dev/null 2>&1 + } + + # Install Goose if enabled + if [ "${var.install_goose}" = "true" ]; then + if ! command_exists npm; then + echo "Error: npm is not installed. Please install Node.js and npm first." + exit 1 + fi + echo "Installing Goose..." + RELEASE_TAG=v${var.goose_version} curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash + fi + + # Configure Goose if auto-configure is enabled + if [ "${var.experiment_auto_configure}" = "true" ]; then + echo "Configuring Goose..." + mkdir -p "$HOME/.config/goose" + cat > "$HOME/.config/goose/config.yaml" << EOL +GOOSE_PROVIDER: ${var.experiment_goose_provider} +GOOSE_MODEL: ${var.experiment_goose_model} +extensions: + coder: + args: + - exp + - mcp + - server + cmd: coder + description: Report ALL tasks and statuses (in progress, done, failed) before and after starting + enabled: true + envs: + CODER_MCP_APP_STATUS_SLUG: goose + name: Coder + timeout: 3000 + type: stdio + developer: + display_name: Developer + enabled: true + name: developer + timeout: 300 + type: builtin +EOL + fi + + # Run with screen if enabled + if [ "${var.experiment_use_screen}" = "true" ]; then + echo "Running Goose in the background..." + + # Check if screen is installed + if ! command_exists screen; then + echo "Error: screen is not installed. Please install screen manually." + exit 1 + fi + + touch "$HOME/.goose.log" + + # Ensure the screenrc exists + if [ ! -f "$HOME/.screenrc" ]; then + echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.goose.log" + echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc" + fi + + if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then + echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.goose.log" + echo "multiuser on" >> "$HOME/.screenrc" + fi + + if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then + echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.goose.log" + echo "acladd $(whoami)" >> "$HOME/.screenrc" + fi + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + screen -U -dmS goose bash -c ' + cd ${var.folder} + $HOME/.local/bin/goose run --text "$GOOSE_SYSTEM_PROMPT. Your task: $GOOSE_TASK_PROMPT" --interactive | tee -a "$HOME/.goose.log" + exec bash + ' + else + # Check if goose is installed before running + if ! command_exists $HOME/.local/bin/goose; then + echo "Error: Goose is not installed. Please enable install_goose or install it manually." + exit 1 + fi + fi + EOT + run_on_start = true +} + +resource "coder_app" "goose" { + slug = "goose" + display_name = "Goose" + agent_id = var.agent_id + command = <<-EOT + #!/bin/bash + set -e + + if [ "${var.experiment_use_screen}" = "true" ]; then + if screen -list | grep -q "goose"; then + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + echo "Attaching to existing Goose session." | tee -a "$HOME/.goose.log" + screen -xRR goose + else + echo "Starting a new Goose session." | tee -a "$HOME/.goose.log" + screen -S goose bash -c 'export LANG=en_US.UTF-8; export LC_ALL=en_US.UTF-8; $HOME/.local/bin/goose run --text "Always report status and instructions to Coder, before and after your steps" --interactive | tee -a "$HOME/.goose.log"; exec bash' + fi + else + cd ${var.folder} + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + $HOME/.local/bin/goose + fi + EOT + icon = var.icon +}