diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index bef2a219e..afcf9df3f 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -252,6 +252,12 @@ variable "compile_boundary_from_source" { default = false } +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 @@ -329,12 +335,97 @@ 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_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, "'", "'\\''")) : ""}' + EOT + + # Common environment variables for start script + start_env_vars = <<-EOT + export ARG_MODEL='${var.model}' + 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_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' + export ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' + export ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join("|", var.boundary_additional_allowed_urls)}' + export ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' + export ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' + export ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' + 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 + set -x + + echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh + echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh + + chmod +x /tmp/install.sh + chmod +x /tmp/start.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 + chmod +x /tmp/remove-last-session-id.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.start_env_vars}\n/tmp/start.sh" } + 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 @@ -351,53 +442,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_MODEL='${var.model}' \ - 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_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' \ - ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \ - ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join("|", var.boundary_additional_allowed_urls)}' \ - ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \ - ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' \ - ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' \ - 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_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, "'", "'\\''")) : ""}' \ - /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/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index ba4420fa3..0bca66755 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -1,9 +1,5 @@ #!/bin/bash -if [ -f "$HOME/.bashrc" ]; then - source "$HOME"/.bashrc -fi - # Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles set -euo pipefail diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 93ff4f723..f2353de7a 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -1,13 +1,9 @@ #!/bin/bash -if [ -f "$HOME/.bashrc" ]; then - source "$HOME"/.bashrc -fi - # Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles set -euo pipefail -export PATH="$HOME/.local/bin:$PATH" +true > "$HOME/start.log" command_exists() { command -v "$1" > /dev/null 2>&1 @@ -30,31 +26,41 @@ ARG_ENABLE_BOUNDARY_PPROF=${ARG_ENABLE_BOUNDARY_PPROF:-false} ARG_BOUNDARY_PPROF_PORT=${ARG_BOUNDARY_PPROF_PORT:-"6067"} ARG_COMPILE_FROM_SOURCE=${ARG_COMPILE_FROM_SOURCE:-false} ARG_CODER_HOST=${ARG_CODER_HOST:-} +ARG_NON_AGENTAPI_CLI=${ARG_NON_AGENTAPI_CLI:-false} + +log() { + if [[ "${ARG_NON_AGENTAPI_CLI}" = "true" ]]; then + printf -- "$@" >> "$HOME/start.log" + else + printf -- "$@" + fi +} -echo "--------------------------------" - -printf "ARG_MODEL: %s\n" "$ARG_MODEL" -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_BOUNDARY_LOG_DIR: %s\n" "$ARG_BOUNDARY_LOG_DIR" -printf "ARG_BOUNDARY_LOG_LEVEL: %s\n" "$ARG_BOUNDARY_LOG_LEVEL" -printf "ARG_BOUNDARY_PROXY_PORT: %s\n" "$ARG_BOUNDARY_PROXY_PORT" -printf "ARG_COMPILE_FROM_SOURCE: %s\n" "$ARG_COMPILE_FROM_SOURCE" -printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST" - -echo "--------------------------------" +log "--------------------------------\n" + +log "ARG_MODEL: %s\n" "$ARG_MODEL" +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_BOUNDARY_LOG_DIR: %s\n" "$ARG_BOUNDARY_LOG_DIR" +log "ARG_BOUNDARY_LOG_LEVEL: %s\n" "$ARG_BOUNDARY_LOG_LEVEL" +log "ARG_BOUNDARY_PROXY_PORT: %s\n" "$ARG_BOUNDARY_PROXY_PORT" +log "ARG_COMPILE_FROM_SOURCE: %s\n" "$ARG_COMPILE_FROM_SOURCE" +log "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST" +log "ARG_NON_AGENTAPI_CLI: %s\n" "$ARG_NON_AGENTAPI_CLI" + +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)" git clone https://github.com/coder/boundary.git cd boundary git checkout "$ARG_BOUNDARY_VERSION" @@ -68,16 +74,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)" 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 } @@ -101,10 +107,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 } @@ -115,12 +121,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 @@ -130,7 +136,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 @@ -138,7 +144,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 @@ -147,12 +153,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 } @@ -161,10 +167,10 @@ 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 } @@ -187,7 +193,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) @@ -198,39 +204,39 @@ 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 mkdir -p "$ARG_BOUNDARY_LOG_DIR" - printf "Starting with coder boundary enabled\n" + log "Starting with coder boundary enabled\n" # Build boundary args with conditional --unprivileged flag BOUNDARY_ARGS=(--log-dir "$ARG_BOUNDARY_LOG_DIR") @@ -238,31 +244,35 @@ function start_agentapi() { BOUNDARY_ARGS+=(--allow "domain=anthropic.com" --allow "domain=registry.npmjs.org" --allow "domain=sentry.io" --allow "domain=claude.ai" --allow "domain=$ARG_CODER_HOST") # Add any additional allowed URLs from the variable - if [ -n "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" ]; then - IFS='|' read -ra ADDITIONAL_URLS <<< "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" + if [[ -n "${ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS}" ]]; then + IFS='|' read -ra ADDITIONAL_URLS <<< "${ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS}" for url in "${ADDITIONAL_URLS[@]}"; do # Quote the URL to preserve spaces within the allow rule - BOUNDARY_ARGS+=(--allow "$url") + BOUNDARY_ARGS+=(--allow "${url}") done fi # Set HTTP Proxy port used by Boundary - BOUNDARY_ARGS+=(--proxy-port "$ARG_BOUNDARY_PROXY_PORT") + BOUNDARY_ARGS+=(--proxy-port "${ARG_BOUNDARY_PROXY_PORT}") # Set log level for boundary - BOUNDARY_ARGS+=(--log-level "$ARG_BOUNDARY_LOG_LEVEL") + BOUNDARY_ARGS+=(--log-level "${ARG_BOUNDARY_LOG_LEVEL}") - if [ "${ARG_ENABLE_BOUNDARY_PPROF:-false}" = "true" ]; then + if [[ "${ARG_ENABLE_BOUNDARY_PPROF:-false}" = "true" ]]; then # Enable boundary pprof server on specified port BOUNDARY_ARGS+=(--pprof) - BOUNDARY_ARGS+=(--pprof-port "$ARG_BOUNDARY_PPROF_PORT") + BOUNDARY_ARGS+=(--pprof-port "${ARG_BOUNDARY_PPROF_PORT}") fi - agentapi server --type claude --term-width 67 --term-height 1190 -- \ - boundary-run "${BOUNDARY_ARGS[@]}" -- \ + # if [[ "${ARG_REPORT_TASKS}" == "true" ]]; then + # boundary-run "${BOUNDARY_ARGS[@]}" -- \ + # claude "${ARGS[@]}" + # else + "${CORE_COMMAND[@]}" boundary-run "${BOUNDARY_ARGS[@]}" -- \ claude "${ARGS[@]}" + # fi else - agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" + "${CORE_COMMAND[@]}" claude "${ARGS[@]}" fi }