From 6c3c2f067ddd914a6ed09d476166d2b1f5a52eeb Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Tue, 24 Jun 2025 13:33:57 +0200 Subject: [PATCH] agentapi and ai task support --- registry/coder/modules/claude-code/main.tf | 183 ++++++++++++++++++--- 1 file changed, 158 insertions(+), 25 deletions(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index d699b4f1..f2affa3d 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.5" + version = ">= 2.7" } } } @@ -96,9 +96,75 @@ variable "experiment_tmux_session_save_interval" { default = "15" } +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.2.2" +} + locals { - encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : "" - encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : "" + # we have to trim the slash because otherwise coder exp mcp will + # set up an invalid claude config + workdir = trimsuffix(var.folder, "/") + encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : "" + encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : "" + agentapi_start_command = <<-EOT + #!/bin/bash + set -e + + # if the first argument is not empty, start claude with the prompt + if [ -n "$1" ]; then + prompt="$(cat ~/.claude-code-prompt)" + cp ~/.claude-code-prompt /tmp/claude-code-prompt + else + rm -f /tmp/claude-code-prompt + fi + + # We need to check if there's a session to use --continue. If there's no session, + # using this flag would cause claude to exit with an error. + # warning: this is a hack and will break if claude changes the format of the .claude.json file. + # Also, this solution is not ideal: a user has to quit claude in order for the session id to appear + # in .claude.json. If they just restart the workspace, the session id will not be available. + continue_flag="" + if grep -q '"lastSessionId":' ~/.claude.json; then + echo "Found a Claude Code session to continue." + continue_flag="--continue" + else + echo "No Claude Code session to continue." + fi + + # use low width to fit in the tasks UI sidebar. height is adjusted so that width x height ~= 80x1000 characters + # visible in the terminal screen by default. + prompt_subshell='"$(cat /tmp/claude-code-prompt)"' + agentapi server --term-width 67 --term-height 1190 -- bash -c "claude $continue_flag --dangerously-skip-permissions $prompt_subshell" + EOT + agentapi_wait_for_start_command = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + echo "Waiting for agentapi server to start on port 3284..." + for i in $(seq 1 15); do + if lsof -i :3284 | grep -q 'LISTEN'; then + echo "agentapi server started on port 3284." + break + fi + echo "Waiting... ($i/15)" + sleep 1 + done + if ! lsof -i :3284 | grep -q 'LISTEN'; then + echo "Error: agentapi server did not start on port 3284 after 15 seconds." + exit 1 + fi + EOT + agentapi_start_command_base64 = base64encode(local.agentapi_start_command) + agentapi_wait_for_start_command_base64 = base64encode(local.agentapi_wait_for_start_command) } # Install and Initialize Claude Code @@ -132,12 +198,12 @@ resource "coder_script" "claude_code" { fi } - if [ ! -d "${var.folder}" ]; then - echo "Warning: The specified folder '${var.folder}' does not exist." + if [ ! -d "${local.workdir}" ]; then + echo "Warning: The specified folder '${local.workdir}' does not exist." echo "Creating the folder..." # The folder must exist before tmux is started or else claude will start # in the home directory. - mkdir -p "${var.folder}" + mkdir -p "${local.workdir}" echo "Folder created successfully." fi if [ -n "${local.encoded_pre_install_script}" ]; then @@ -176,9 +242,38 @@ resource "coder_script" "claude_code" { npm install -g @anthropic-ai/claude-code@${var.claude_code_version} fi + # Install AgentAPI if enabled + if [ "${var.install_agentapi}" = "true" ]; then + echo "Installing AgentAPI..." + arch=$(uname -m) + if [ "$arch" = "x86_64" ]; then + binary_name="agentapi-linux-amd64" + elif [ "$arch" = "aarch64" ]; then + binary_name="agentapi-linux-arm64" + else + echo "Error: Unsupported architecture: $arch" + exit 1 + fi + wget "https://github.com/coder/agentapi/releases/download/${var.agentapi_version}/$binary_name" + chmod +x "$binary_name" + sudo mv "$binary_name" /usr/local/bin/agentapi + fi + if ! command_exists agentapi; then + echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually." + exit 1 + fi + + # save the prompt for the agentapi start command + echo -n "$CODER_MCP_CLAUDE_TASK_PROMPT" > ~/.claude-code-prompt + + echo -n "${local.agentapi_start_command_base64}" | base64 -d > ~/.agentapi-start-command + chmod +x ~/.agentapi-start-command + echo -n "${local.agentapi_wait_for_start_command_base64}" | base64 -d > ~/.agentapi-wait-for-start-command + chmod +x ~/.agentapi-wait-for-start-command + 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} + coder exp mcp configure claude-code ${local.workdir} --ai-agentapi-url http://localhost:3284 fi if [ -n "${local.encoded_post_install_script}" ]; then @@ -257,17 +352,16 @@ EOF export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 + tmux new-session -d -s agentapi-cc -c ${local.workdir} '~/.agentapi-start-command true; exec bash' + ~/.agentapi-wait-for-start-command + if [ "${var.experiment_tmux_session_persistence}" = "true" ]; then sleep 3 + fi - if ! tmux has-session -t claude-code 2>/dev/null; then - # Only create a new session if one doesn't exist - tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions \"$CODER_MCP_CLAUDE_TASK_PROMPT\"" - fi - else - if ! tmux has-session -t claude-code 2>/dev/null; then - tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions \"$CODER_MCP_CLAUDE_TASK_PROMPT\"" - fi + if ! tmux has-session -t claude-code 2>/dev/null; then + # Only create a new session if one doesn't exist + tmux new-session -d -s claude-code -c ${local.workdir} "agentapi attach; exec bash" fi fi @@ -297,9 +391,17 @@ EOF export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 + screen -U -dmS agentapi-cc bash -c ' + cd ${local.workdir} + # setting the first argument will make claude use the prompt + ~/.agentapi-start-command true + exec bash + ' + ~/.agentapi-wait-for-start-command + screen -U -dmS claude-code bash -c ' - cd ${var.folder} - claude --dangerously-skip-permissions "$CODER_MCP_CLAUDE_TASK_PROMPT" | tee -a "$HOME/.claude-code.log" + cd ${local.workdir} + agentapi attach exec bash ' else @@ -312,6 +414,21 @@ EOF run_on_start = true } +resource "coder_app" "claude_code_web" { + # use a short slug to mitigate https://github.com/coder/coder/issues/15178 + slug = "ccw" + display_name = "Claude Code Web" + agent_id = var.agent_id + url = "http://localhost:3284/" + icon = var.icon + subdomain = true + healthcheck { + url = "http://localhost:3284/status" + interval = 5 + threshold = 3 + } +} + resource "coder_app" "claude_code" { slug = "claude-code" display_name = "Claude Code" @@ -324,31 +441,47 @@ resource "coder_app" "claude_code" { export LC_ALL=en_US.UTF-8 if [ "${var.experiment_use_tmux}" = "true" ]; then + if ! tmux has-session -t agentapi-cc 2>/dev/null; then + # start agentapi without claude using the prompt (no argument) + tmux new-session -d -s agentapi-cc -c ${local.workdir} '~/.agentapi-start-command; exec bash' + ~/.agentapi-wait-for-start-command + fi + if tmux has-session -t claude-code 2>/dev/null; then echo "Attaching to existing Claude Code tmux session." | tee -a "$HOME/.claude-code.log" - # If Claude isn't running in the session, start it without the prompt - if ! tmux list-panes -t claude-code -F '#{pane_current_command}' | grep -q "claude"; then - tmux send-keys -t claude-code "cd ${var.folder} && claude -c --dangerously-skip-permissions" C-m - fi tmux attach-session -t claude-code else echo "Starting a new Claude Code tmux session." | tee -a "$HOME/.claude-code.log" - tmux new-session -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions | tee -a \"$HOME/.claude-code.log\"; exec bash" + tmux new-session -s claude-code -c ${local.workdir} "agentapi attach; exec bash" fi elif [ "${var.experiment_use_screen}" = "true" ]; then + if ! screen -list | grep -q "agentapi-cc"; then + screen -S agentapi-cc bash -c ' + cd ${local.workdir} + # start agentapi without claude using the prompt (no argument) + ~/.agentapi-start-command + exec bash + ' + fi if screen -list | grep -q "claude-code"; then echo "Attaching to existing Claude Code screen session." | tee -a "$HOME/.claude-code.log" screen -xRR claude-code else echo "Starting a new Claude Code screen session." | tee -a "$HOME/.claude-code.log" - screen -S claude-code bash -c 'claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash' + screen -S claude-code bash -c 'agentapi attach; exec bash' fi else - cd ${var.folder} - claude + cd ${local.workdir} + agentapi attach fi EOT icon = var.icon order = var.order group = var.group } + +resource "coder_ai_task" "claude_code" { + sidebar_app { + id = coder_app.claude_code_web.id + } +}