diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index dd5402790..3ac18d778 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -244,6 +244,12 @@ variable "enable_aibridge" { } } +variable "cli_command" { + type = string + description = "The command to run for the Claude Code CLI app when tasks are disabled." + default = "" +} + resource "coder_env" "claude_code_md_path" { count = var.claude_md_path == "" ? 0 : 1 agent_id = var.agent_id @@ -344,12 +350,89 @@ locals { var.report_tasks ? format("\n%s\n", local.report_tasks_system_prompt) : "", local.custom_system_prompt != "" ? format("\n%s\n", local.custom_system_prompt) : "" ) + + # Common environment variables for install script + install_env_vars = <<-EOT + export ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}' + export ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' + export ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' + export ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' + export ARG_INSTALL_VIA_NPM='${var.install_via_npm}' + export ARG_REPORT_TASKS='${var.report_tasks}' + export ARG_WORKDIR='${local.workdir}' + export ARG_ALLOWED_TOOLS='${var.allowed_tools}' + export ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' + export ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' + export ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' + EOT + + # Common environment variables for start script + start_env_vars = <<-EOT + export ARG_RESUME_SESSION_ID='${var.resume_session_id}' + export ARG_CONTINUE='${var.continue}' + export ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}' + export ARG_PERMISSION_MODE='${var.permission_mode}' + export ARG_WORKDIR='${local.workdir}' + export ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' + export ARG_REPORT_TASKS='${var.report_tasks}' + export ARG_ENABLE_BOUNDARY='${var.enable_boundary}' + export ARG_BOUNDARY_VERSION='${var.boundary_version}' + export ARG_COMPILE_FROM_SOURCE='${var.compile_boundary_from_source}' + export ARG_CODER_HOST='${local.coder_host}' + export ARG_NON_AGENTAPI_CLI='${!var.report_tasks && var.cli_app ? true : false}' + EOT + + # Reusable install script command + install_command = <<-EOT + #!/bin/bash + set -o pipefail + + echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh + + chmod +x /tmp/install.sh + ${local.install_env_vars} + /tmp/install.sh + EOT + + # Reusable start script command for agentapi module + agentapi_start_command = <<-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 + + ${local.start_env_vars} + /tmp/start.sh + EOT +} + +resource "coder_script" "install_agent" { + count = !var.report_tasks ? 1 : 0 + + agent_id = var.agent_id + display_name = "Install agent" + run_on_start = true + log_path = "/home/coder/install.log" + script = local.install_command +} + +resource "coder_app" "agent_cli" { + count = (!var.report_tasks && var.cli_app) ? 1 : 0 + + agent_id = var.agent_id + slug = local.app_slug + display_name = var.cli_app_display_name + + command = length(trimprefix(var.cli_command, " ")) > 0 ? var.cli_command : local.agentapi_start_command } + module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" version = "2.0.0" + count = var.report_tasks ? 1 : 0 agent_id = var.agent_id web_app_slug = local.app_slug web_app_order = var.order @@ -366,49 +449,10 @@ module "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_RESUME_SESSION_ID='${var.resume_session_id}' \ - ARG_CONTINUE='${var.continue}' \ - ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}' \ - ARG_PERMISSION_MODE='${var.permission_mode}' \ - ARG_WORKDIR='${local.workdir}' \ - ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \ - ARG_REPORT_TASKS='${var.report_tasks}' \ - ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \ - ARG_BOUNDARY_VERSION='${var.boundary_version}' \ - ARG_COMPILE_FROM_SOURCE='${var.compile_boundary_from_source}' \ - ARG_CODER_HOST='${local.coder_host}' \ - /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_CLAUDE_CODE_VERSION='${var.claude_code_version}' \ - ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \ - ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \ - ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \ - ARG_INSTALL_VIA_NPM='${var.install_via_npm}' \ - ARG_REPORT_TASKS='${var.report_tasks}' \ - ARG_WORKDIR='${local.workdir}' \ - ARG_ALLOWED_TOOLS='${var.allowed_tools}' \ - ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \ - ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \ - ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \ - /tmp/install.sh - EOT + start_script = local.agentapi_start_command + install_script = local.install_command } output "task_app_id" { - value = module.agentapi.task_app_id + value = try(module.agentapi[0].task_app_id, null) } diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index c3c320209..55f17c936 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -2,6 +2,8 @@ set -euo pipefail +true > "$HOME/start.log" + command_exists() { command -v "$1" > /dev/null 2>&1 } @@ -17,34 +19,41 @@ ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false} ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"} ARG_COMPILE_FROM_SOURCE=${ARG_COMPILE_FROM_SOURCE:-false} ARG_CODER_HOST=${ARG_CODER_HOST:-} +ARG_NON_AGENTAPI_CLI=${ARG_NON_AGENTAPI_CLI:-false} -echo "--------------------------------" +log() { + if [[ "${ARG_NON_AGENTAPI_CLI}" = "true" ]]; then + printf -- "$@" >> "$HOME/start.log" + else + printf -- "$@" + fi +} -printf "ARG_RESUME: %s\n" "$ARG_RESUME_SESSION_ID" -printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE" -printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" -printf "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE" -printf "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT" -printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" -printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" -printf "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY" -printf "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION" -printf "ARG_COMPILE_FROM_SOURCE: %s\n" "$ARG_COMPILE_FROM_SOURCE" -printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST" +log "ARG_RESUME: %s\n" "$ARG_RESUME_SESSION_ID" +log "ARG_CONTINUE: %s\n" "$ARG_CONTINUE" +log "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" +log "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE" +log "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT" +log "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" +log "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" +log "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY" +log "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION" +log "ARG_COMPILE_FROM_SOURCE: %s\n" "$ARG_COMPILE_FROM_SOURCE" +log "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST" -echo "--------------------------------" +log "--------------------------------\n" function install_boundary() { if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then # Install boundary by compiling from source - echo "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)" + log "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)\n" - echo "Removing existing boundary directory to allow re-running the script safely" + log "Removing existing boundary directory to allow re-running the script safely\n" if [ -d boundary ]; then rm -rf boundary fi - echo "Clone boundary repository" + log "Clone boundary repository\n" git clone https://github.com/coder/boundary.git cd boundary git checkout "$ARG_BOUNDARY_VERSION" @@ -58,16 +67,16 @@ function install_boundary() { sudo chmod +x /usr/local/bin/boundary-run else # Install boundary using official install script - echo "Installing boundary using official install script (version: $ARG_BOUNDARY_VERSION)" + log "Installing boundary using official install script (version: $ARG_BOUNDARY_VERSION)\n" curl -fsSL https://raw.githubusercontent.com/coder/boundary/main/install.sh | bash -s -- --version "$ARG_BOUNDARY_VERSION" fi } function validate_claude_installation() { if command_exists claude; then - printf "Claude Code is installed\n" + log "Claude Code is installed\n" else - printf "Error: Claude Code is not installed. Please enable install_claude_code or install it manually\n" + log "Error: Claude Code is not installed. Please enable install_claude_code or install it manually\n" exit 1 fi } @@ -91,10 +100,10 @@ task_session_exists() { session_file=$(get_task_session_file) if [ -f "$session_file" ]; then - printf "Task session file found: %s\n" "$session_file" + log "Task session file found: %s\n" "$session_file" return 0 else - printf "Task session file not found: %s\n" "$session_file" + log "Task session file not found: %s\n" "$session_file" return 1 fi } @@ -105,12 +114,12 @@ is_valid_session() { # Check if file exists and is not empty # Empty files indicate the session was created but never used so they need to be removed if [ ! -f "$session_file" ]; then - printf "Session validation failed: file does not exist\n" + log "Session validation failed: file does not exist\n" return 1 fi if [ ! -s "$session_file" ]; then - printf "Session validation failed: file is empty, removing stale file\n" + log "Session validation failed: file is empty, removing stale file\n" rm -f "$session_file" return 1 fi @@ -120,7 +129,7 @@ is_valid_session() { local line_count line_count=$(wc -l < "$session_file") if [ "$line_count" -lt 2 ]; then - printf "Session validation failed: incomplete (only %s lines), removing incomplete file\n" "$line_count" + log "Session validation failed: incomplete (only %s lines), removing incomplete file\n" "$line_count" rm -f "$session_file" return 1 fi @@ -128,7 +137,7 @@ is_valid_session() { # Validate JSONL format by checking first 3 lines # Claude session files use JSONL (JSON Lines) format where each line is valid JSON if ! head -3 "$session_file" | jq empty 2> /dev/null; then - printf "Session validation failed: invalid JSONL format, removing corrupt file\n" + log "Session validation failed: invalid JSONL format, removing corrupt file\n" rm -f "$session_file" return 1 fi @@ -137,12 +146,12 @@ is_valid_session() { # This ensures the file structure matches Claude's session format if ! grep -q '"sessionId"' "$session_file" \ || ! grep -m 1 '"sessionId"' "$session_file" | jq -e '.sessionId' > /dev/null 2>&1; then - printf "Session validation failed: no valid sessionId found, removing malformed file\n" + log "Session validation failed: no valid sessionId found, removing malformed file\n" rm -f "$session_file" return 1 fi - printf "Session validation passed: %s\n" "$session_file" + log "Session validation passed: %s\n" "$session_file" return 0 } @@ -151,16 +160,21 @@ has_any_sessions() { project_dir=$(get_project_dir) if [ -d "$project_dir" ] && find "$project_dir" -maxdepth 1 -name "*.jsonl" -size +0c 2> /dev/null | grep -q .; then - printf "Sessions found in: %s\n" "$project_dir" + log "Sessions found in: %s\n" "$project_dir" return 0 else - printf "No sessions found in: %s\n" "$project_dir" + log "No sessions found in: %s\n" "$project_dir" return 1 fi } ARGS=() +CORE_COMMAND=() +if [[ "${ARG_REPORT_TASKS}" == "true" ]]; then + CORE_COMMAND+=(agentapi server --type claude --term-width 67 --term-height 1190 --) +fi + function start_agentapi() { # For Task reporting export CODER_MCP_ALLOWED_TOOLS="coder_report_task" @@ -173,7 +187,7 @@ function start_agentapi() { fi if [ -n "$ARG_RESUME_SESSION_ID" ]; then - echo "Resuming specified session: $ARG_RESUME_SESSION_ID" + log "Resuming specified session: $ARG_RESUME_SESSION_ID" ARGS+=(--resume "$ARG_RESUME_SESSION_ID") [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) @@ -184,46 +198,45 @@ function start_agentapi() { session_file=$(get_task_session_file) if task_session_exists && is_valid_session "$session_file"; then - echo "Resuming task session: $TASK_SESSION_ID" + log "Resuming task session: $TASK_SESSION_ID" ARGS+=(--resume "$TASK_SESSION_ID" --dangerously-skip-permissions) else - echo "Starting new task session: $TASK_SESSION_ID" + log "Starting new task session: $TASK_SESSION_ID" ARGS+=(--session-id "$TASK_SESSION_ID" --dangerously-skip-permissions) [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") fi else if has_any_sessions; then - echo "Continuing most recent standalone session" + log "Continuing most recent standalone session" ARGS+=(--continue) [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) else - echo "No sessions found, starting fresh standalone session" + log "No sessions found, starting fresh standalone session" [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") fi fi else - echo "Continue disabled, starting fresh session" + log "Continue disabled, starting fresh session" [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") fi - printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")" + log "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")" if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then install_boundary - printf "Starting with coder boundary enabled\n" + log "Starting with coder boundary enabled\n" BOUNDARY_ARGS+=() - agentapi server --type claude --term-width 67 --term-height 1190 -- \ - boundary-run "${BOUNDARY_ARGS[@]}" -- \ + "${CORE_COMMAND[@]}" boundary-run "${BOUNDARY_ARGS[@]}" -- \ claude "${ARGS[@]}" else - agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" + "${CORE_COMMAND[@]}" claude "${ARGS[@]}" fi }