From c4931afbf4ec78ab9c934a4cff5089cedab462cd Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Wed, 22 Apr 2026 00:32:27 +0530 Subject: [PATCH] feat(coder/modules/tasks): add tasks module --- .../modules/claude-code/scripts/start.sh | 256 ------------------ .../modules/tasks/MODULE_NAME.tftest.hcl | 21 ++ registry/coder/modules/tasks/README.md | 71 +++++ registry/coder/modules/tasks/main.tf | 227 ++++++++++++++++ registry/coder/modules/tasks/run.sh | 26 ++ .../tasks/scripts/claude_code_start.sh | 202 ++++++++++++++ 6 files changed, 547 insertions(+), 256 deletions(-) delete mode 100644 registry/coder/modules/claude-code/scripts/start.sh create mode 100644 registry/coder/modules/tasks/MODULE_NAME.tftest.hcl create mode 100644 registry/coder/modules/tasks/README.md create mode 100644 registry/coder/modules/tasks/main.tf create mode 100755 registry/coder/modules/tasks/run.sh create mode 100644 registry/coder/modules/tasks/scripts/claude_code_start.sh diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh deleted file mode 100644 index 5ccbc8fa1..000000000 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ /dev/null @@ -1,256 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} -ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH/#\~/$HOME}" -ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}" - -export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH" - -command_exists() { - command -v "$1" > /dev/null 2>&1 -} - -ARG_RESUME_SESSION_ID=${ARG_RESUME_SESSION_ID:-} -ARG_CONTINUE=${ARG_CONTINUE:-false} -ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-} -ARG_PERMISSION_MODE=${ARG_PERMISSION_MODE:-} -ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} -ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d) -ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} -ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false} -ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"latest"} -ARG_COMPILE_FROM_SOURCE=${ARG_COMPILE_FROM_SOURCE:-false} -ARG_USE_BOUNDARY_DIRECTLY=${ARG_USE_BOUNDARY_DIRECTLY:-false} -ARG_CODER_HOST=${ARG_CODER_HOST:-} - -echo "--------------------------------" - -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_USE_BOUNDARY_DIRECTLY: %s\n" "$ARG_USE_BOUNDARY_DIRECTLY" -printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST" - -echo "--------------------------------" - -function install_boundary() { - if [ "$ARG_COMPILE_FROM_SOURCE" = "true" ]; then - # Install boundary by compiling from source - echo "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)" - - echo "Removing existing boundary directory to allow re-running the script safely" - if [ -d boundary ]; then - rm -rf boundary - fi - - echo "Clone boundary repository" - git clone https://github.com/coder/boundary.git - cd boundary - git checkout "$ARG_BOUNDARY_VERSION" - - # Build the binary - make build - - # Install binary - sudo cp boundary /usr/local/bin/ - sudo chmod +x /usr/local/bin/boundary - elif [ "$ARG_USE_BOUNDARY_DIRECTLY" = "true" ]; then - # Install boundary using official install script - echo "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" - else - # Use coder boundary subcommand (default) - no installation needed - echo "Using coder boundary subcommand (provided by Coder)" - fi -} - -function validate_claude_installation() { - if command_exists claude; then - printf "Claude Code is installed\n" - else - printf "Error: Claude Code is not installed. Please enable install_claude_code or install it manually\n" - exit 1 - fi -} - -# Hardcoded task session ID for Coder task reporting -# This ensures all task sessions use a consistent, predictable ID -TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2" - -get_project_dir() { - local workdir_normalized - workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/._' '-') - echo "$HOME/.claude/projects/${workdir_normalized}" -} - -get_task_session_file() { - echo "$(get_project_dir)/${TASK_SESSION_ID}.jsonl" -} - -task_session_exists() { - local session_file - session_file=$(get_task_session_file) - - if [ -f "$session_file" ]; then - printf "Task session file found: %s\n" "$session_file" - return 0 - else - printf "Task session file not found: %s\n" "$session_file" - return 1 - fi -} - -is_valid_session() { - local session_file="$1" - - # 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" - return 1 - fi - - if [ ! -s "$session_file" ]; then - printf "Session validation failed: file is empty, removing stale file\n" - rm -f "$session_file" - return 1 - fi - - # Check for minimum session content - # Valid sessions need at least 2 lines: initial message and first response - 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" - rm -f "$session_file" - return 1 - fi - - # 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" - rm -f "$session_file" - return 1 - fi - - # Verify the session has a valid sessionId field - # 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" - rm -f "$session_file" - return 1 - fi - - printf "Session validation passed: %s\n" "$session_file" - return 0 -} - -has_any_sessions() { - local project_dir - 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" - return 0 - else - printf "No sessions found in: %s\n" "$project_dir" - return 1 - fi -} - -ARGS=() - -function start_agentapi() { - # For Task reporting - export CODER_MCP_ALLOWED_TOOLS="coder_report_task" - - mkdir -p "$ARG_WORKDIR" - cd "$ARG_WORKDIR" - - if [ -n "$ARG_PERMISSION_MODE" ]; then - ARGS+=(--permission-mode "$ARG_PERMISSION_MODE") - fi - - if [ -n "$ARG_RESUME_SESSION_ID" ]; then - echo "Resuming specified session: $ARG_RESUME_SESSION_ID" - ARGS+=(--resume "$ARG_RESUME_SESSION_ID") - [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) - - elif [ "$ARG_CONTINUE" = "true" ]; then - - if [ "$ARG_REPORT_TASKS" = "true" ]; then - local session_file - session_file=$(get_task_session_file) - - if task_session_exists && is_valid_session "$session_file"; then - echo "Resuming task session: $TASK_SESSION_ID" - ARGS+=(--resume "$TASK_SESSION_ID" --dangerously-skip-permissions) - else - echo "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" - ARGS+=(--continue) - [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) - else - echo "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" - [ "$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[@]}")" - - if [ "$ARG_ENABLE_BOUNDARY" = "true" ]; then - install_boundary - - printf "Starting with coder boundary enabled\n" - - BOUNDARY_ARGS+=() - - # Determine which boundary command to use - if [ "$ARG_COMPILE_FROM_SOURCE" = "true" ] || [ "$ARG_USE_BOUNDARY_DIRECTLY" = "true" ]; then - # Use boundary binary directly (from compilation or release installation) - BOUNDARY_CMD=("boundary") - else - # Use coder boundary subcommand (default) - # Copy coder binary to coder-no-caps. Copying strips CAP_NET_ADMIN capabilities - # from the binary, which is necessary because boundary doesn't work with - # privileged binaries (you can't launch privileged binaries inside network - # namespaces unless you have sys_admin). - CODER_NO_CAPS="$(dirname "$(which coder)")/coder-no-caps" - cp "$(which coder)" "$CODER_NO_CAPS" - BOUNDARY_CMD=("$CODER_NO_CAPS" "boundary") - fi - - agentapi server --type claude --term-width 67 --term-height 1190 -- \ - "${BOUNDARY_CMD[@]}" "${BOUNDARY_ARGS[@]}" -- \ - claude "${ARGS[@]}" - else - agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" - fi -} - -validate_claude_installation -start_agentapi diff --git a/registry/coder/modules/tasks/MODULE_NAME.tftest.hcl b/registry/coder/modules/tasks/MODULE_NAME.tftest.hcl new file mode 100644 index 000000000..a6ccc5240 --- /dev/null +++ b/registry/coder/modules/tasks/MODULE_NAME.tftest.hcl @@ -0,0 +1,21 @@ +run "plan_with_required_vars" { + command = plan + + variables { + agent_id = "example-agent-id" + } +} + +run "app_url_uses_port" { + command = plan + + variables { + agent_id = "example-agent-id" + port = 19999 + } + + assert { + condition = resource.coder_app.module_name.url == "http://localhost:19999" + error_message = "Expected module-name app URL to include configured port" + } +} diff --git a/registry/coder/modules/tasks/README.md b/registry/coder/modules/tasks/README.md new file mode 100644 index 000000000..7251fb96f --- /dev/null +++ b/registry/coder/modules/tasks/README.md @@ -0,0 +1,71 @@ +--- +display_name: tasks +description: Describe what this module does +icon: ../../../../.icons/.svg +verified: false +tags: [helper] +--- + +# tasks + + + +```tf +module "tasks" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/NAMESPACE/tasks/coder" + version = "1.0.0" +} +``` + + + +## Examples + +### Example 1 + +Install the Dracula theme from [OpenVSX](https://open-vsx.org/): + +```tf +module "tasks" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/NAMESPACE/tasks/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + extensions = [ + "dracula-theme.theme-dracula" + ] +} +``` + +Enter the `.` into the extensions array and code-server will automatically install on start. + +### Example 2 + +Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file: + +```tf +module "tasks" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/NAMESPACE/tasks/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + extensions = ["dracula-theme.theme-dracula"] + settings = { + "workbench.colorTheme" = "Dracula" + } +} +``` + +### Example 3 + +Run code-server in the background, don't fetch it from GitHub: + +```tf +module "tasks" { + source = "registry.coder.com/NAMESPACE/tasks/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + offline = true +} +``` diff --git a/registry/coder/modules/tasks/main.tf b/registry/coder/modules/tasks/main.tf new file mode 100644 index 000000000..72286b8bc --- /dev/null +++ b/registry/coder/modules/tasks/main.tf @@ -0,0 +1,227 @@ +terraform { + required_version = ">= 1.9" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + aap = { + source = "ansible/aap" + version = "1.3.0" + } + } +} + +locals { + # A built-in icon like "/icon/code.svg" or a full URL of icon + icon_url = "https://raw.githubusercontent.com/coder/coder/main/site/static/icon/code.svg" + # a map of all possible values + options = { + "Option 1" = { + "name" = "Option 1", + "value" = "1" + "icon" = "/emojis/1.png" + } + "Option 2" = { + "name" = "Option 2", + "value" = "2" + "icon" = "/emojis/2.png" + } + } +} + +# Add required variables for your modules and remove any unneeded variables +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "agent_module_ref" { + type = object({ + agent_ref = string + agent_module_dir = string + agent_binary_path = string + }) +} + +variable "agent_parameters" { + type = object({ + claude_code = optional(object({ + resume_session_id = optional(string, "") + continue = optional(bool, false) + dangerously_skip_permissions = optional(bool, false) + permission_mode = optional(string, "") + }), null) + + another_agent = optional(object({ + temperature = optional(number, null) + system_prompt = optional(string, null) + }), null) + }) + default = {} + + validation { + condition = var.agent_parameters.claude_code == null || var.agent_module_ref.agent_name == "claude_code" + error_message = "'claude_code' parameters are only valid when ref is 'claude-code'." + } + + validation { + condition = var.agent_parameters.another_agent == null || var.agent_module_ref.agent_name == "another_agent" + error_message = "'another_agent' parameters are only valid when ref is 'another_agent'." + } +} + +variable "enable_agentapi" { + type = bool + description = "Whether to enable AgentAPI for this agent. If false, the AgentAPI module will not be included, the start script will still run and a cli app will be created which runs the agent in normal terminal mode" +} + +variable "agentapi" { + description = <<-EOT + AgentAPI app configuration: + - `web_app`: Whether to create the web app for Claude Code. When false, AgentAPI still runs but no web UI app icon is shown in the Coder dashboard. This is automatically enabled when using Coder Tasks, regardless of this setting. + - `cli_app`: Whether to create a CLI app for Claude Code. + - `web_app_display_name`: Display name for the web app. + - `cli_app_display_name`: Display name for the CLI app. + - `web_app_icon`: The icon to use for the app. + EOT + type = object({ + version = optional(string, "latest") + web_app = optional(bool, true) + cli_app = optional(bool, false) + web_app_display_name = optional(string, "ClaudeCode") + cli_app_display_name = optional(string, "ClaudeCode CLI") + web_app_icon = optional(string, "/icon/claude.svg") + module_directory = optional(string) + }) + default = {} +} + +variable "enable_boundary" { + type = bool + description = "Whether to enable Boundary for this agent. If false, the Boundary module will not be included and Boundary will not be installed, but the start script will still run." + default = false +} + +variable "cli_app_display_name" { + type = string + description = "Display name for the CLI app. Only applicable if `enable_agentapi` is false." + default = "Agent CLI" + + validation { + condition = var.enable_agentapi == false + error_message = "cli_app_display_name should not be set when enable_agentapi is true." + } +} + +variable "boundary" { + description = <<-EOT + Boundary configuration: + - `version`: Boundary version. When `use_binary_directly` is true, a release version should be provided or 'latest' for the latest release. + - `compile_from_source`: Whether to compile boundary from source instead of using the official install script. + - `use_binary_directly`: Whether to use boundary binary directly instead of coder boundary subcommand. + - `pre_install_script`: Custom script to run before installing Boundary. + - `post_install_script`: Custom script to run after installing Boundary. + - `module_directory`: Directory where the Boundary module files are stored. + EOT + type = object({ + version = optional(string, "latest") + compile_from_source = optional(bool, false) + use_binary_directly = optional(bool, false) + pre_install_script = optional(string, null) + post_install_script = optional(string, null) + module_directory = optional(string, "$HOME/.coder-modules/coder/boundary") + }) +} + +locals { + start_script = file("${path.module}/${var.agent_module_ref.agent_name}_start.sh") + export_variable_prefix = upper(var.agent_module_ref.agent_name) + + export_variables = { + for key, value in var.agent_parameters[var.agent_module_ref.agent_name] : "${local.export_variable_prefix}_${upper(key)}" => value + } + + export_merged_variables = merge(local.export_variables, { + "ARG_ENABLE_AGENTAPI" = var.enable_agentapi + "ARG_ENABLE_BOUNDARY" = var.enable_boundary + }) + + default_app_slugs = { + "claude_code" = "ccw" + } + + app_slug = lookup(local.default_app_slugs, var.agent_module_ref.agent_name) + cli_app_slug = "${local.app_slug}-cli" + +} + +variable "ai_prompt" { + type = string + description = "Initial task prompt for Claude Code." + default = "" +} + +resource "coder_script" "start_script" { + agent_id = var.agent_id + display_name = "Task Statrt Script" + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + echo -n '${base64encode(local.start_script)}' | base64 -d > "${var.agent_module_ref.agent_module_dir}/start.sh" + + # Export variables for the script based on the provided agent parameters + %{for var_name, var_value in local.export_merged_variables~} + export ${var_name}="${var_value}" + %{endfor~} + chmod +x "${var.agent_module_ref.agent_module_dir}/start.sh" + "${var.agent_module_ref.agent_module_dir}/start.sh" + EOT +} + +module "agentapi" { + count = var.enable_agentapi ? 1 : 0 + + source = "git::https://github.com/coder/registry.git//registry/coder/modules/agentapi?ref=35C4n0r/refactor-agentapi-decouple" + agentapi_version = var.agentapi.version + agent_id = var.agent_id + cli_app = var.agentapi.cli_app + cli_app_display_name = var.agentapi.cli_app_display_name + cli_app_slug = local.cli_app_slug + web_app = var.agentapi.web_app + web_app_display_name = var.agentapi.web_app_display_name + web_app_icon = var.agentapi.web_app_icon + web_app_slug = local.app_slug + module_directory = var.agentapi.module_directory +} + +resource "coder_app" "non_agentapi_cli" { + count = var.enable_agentapi ? 0 : 1 + + agent_id = var.agent_id + display_name = var.cli_app_display_name + command = "" + slug = local.cli_app_slug +} + +module "boundary" { + count = var.enable_boundary ? 1 : 0 + + source = "git::https://github.com/coder/registry.git//registry/coder/modules/boundary?ref=35C4n0r/feat-boundary-module" + + agent_id = var.agent_id + compile_boundary_from_source = var.boundary.compile_from_source + use_boundary_directly = var.boundary.use_binary_directly + boundary_version = var.boundary.version + pre_install_script = var.boundary.pre_install_script + post_install_script = var.boundary.post_install_script + module_directory = var.boundary.module_directory +} + +output "task_app_id" { + description = "The app ID for the task's web app, if created." + value = try(module.agentapi[0].task_app_id, coder_app.non_agentapi_cli[0].id) +} diff --git a/registry/coder/modules/tasks/run.sh b/registry/coder/modules/tasks/run.sh new file mode 100755 index 000000000..a15fcf6c0 --- /dev/null +++ b/registry/coder/modules/tasks/run.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env sh + +# Convert templated variables to shell variables +# shellcheck disable=SC2269 +LOG_PATH=${LOG_PATH} + +# shellcheck disable=SC2034 +BOLD='\033[0;1m' + +# shellcheck disable=SC2059 +printf "$${BOLD}Installing MODULE_NAME ...\n\n" + +# Add code here +# Use variables from the templatefile function in main.tf +# e.g. LOG_PATH, PORT, etc. + +printf "🥳 Installation complete!\n\n" + +printf "👷 Starting MODULE_NAME in background...\n\n" +# Start the app in here +# 1. Use & to run it in background +# 2. redirct stdout and stderr to log files + +./app > "$${LOG_PATH}" 2>&1 & + +printf "check logs at %s\n\n" "$${LOG_PATH}" diff --git a/registry/coder/modules/tasks/scripts/claude_code_start.sh b/registry/coder/modules/tasks/scripts/claude_code_start.sh new file mode 100644 index 000000000..437868559 --- /dev/null +++ b/registry/coder/modules/tasks/scripts/claude_code_start.sh @@ -0,0 +1,202 @@ +#!/bin/bash + +set -euo pipefail + +ARG_CLAUDE_BINARY_PATH=${CLAUDE_CODE_ARG_CLAUDE_BINARY_PATH:-"${HOME}/.local/bin"} +ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH/#\~/${HOME}}" +ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH//\$HOME/${HOME}}" + +export PATH="${ARG_CLAUDE_BINARY_PATH}:${PATH}" + +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +ARG_RESUME_SESSION_ID=${CLAUDE_CODE_ARG_RESUME_SESSION_ID:-} +ARG_CONTINUE=${CLAUDE_CODE_ARG_CONTINUE:-false} +ARG_DANGEROUSLY_SKIP_PERMISSIONS=${CLAUDE_CODE_ARG_DANGEROUSLY_SKIP_PERMISSIONS:-} +ARG_PERMISSION_MODE=${CLAUDE_CODE_ARG_PERMISSION_MODE:-} +ARG_WORKDIR=${CLAUDE_CODE_ARG_WORKDIR:-"${HOME}"} +ARG_AI_PROMPT=$(echo -n "${CLAUDE_CODE_ARG_AI_PROMPT:-}" | base64 -d) +ARG_CODER_HOST=${CLAUDE_CODE_ARG_CODER_HOST:-} +ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false} +ARG_ENABLE_AGENTAPI=${ARG_ENABLE_AGENTAPI:-false} + +echo "--------------------------------" + +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_ENABLE_BOUNDARY: %s\n" "${ARG_ENABLE_BOUNDARY}" +printf "ARG_ENABLE_AGENTAPI: %s\n" "${ARG_ENABLE_AGENTAPI}" +printf "ARG_CODER_HOST: %s\n" "${ARG_CODER_HOST}" + +echo "--------------------------------" + +function validate_claude_installation() { + if command_exists claude; then + printf "Claude Code is installed\n" + else + printf "Error: Claude Code is not installed. Please enable install_claude_code or install it manually\n" + exit 1 + fi +} + +# Hardcoded task session ID for Coder task reporting +# This ensures all task sessions use a consistent, predictable ID +TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2" + +get_project_dir() { + local workdir_normalized + workdir_normalized=$(echo "${ARG_WORKDIR}" | tr '/._' '-') + echo "${HOME}/.claude/projects/${workdir_normalized}" +} + +get_task_session_file() { + echo "$(get_project_dir)/${TASK_SESSION_ID}.jsonl" +} + +task_session_exists() { + local session_file + session_file=$(get_task_session_file) + + if [ -f "$session_file" ]; then + printf "Task session file found: %s\n" "$session_file" + return 0 + else + printf "Task session file not found: %s\n" "$session_file" + return 1 + fi +} + +is_valid_session() { + local session_file="$1" + + # 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" + return 1 + fi + + if [ ! -s "$session_file" ]; then + printf "Session validation failed: file is empty, removing stale file\n" + rm -f "$session_file" + return 1 + fi + + # Check for minimum session content + # Valid sessions need at least 2 lines: initial message and first response + 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" + rm -f "$session_file" + return 1 + fi + + # 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" + rm -f "$session_file" + return 1 + fi + + # Verify the session has a valid sessionId field + # 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" + rm -f "$session_file" + return 1 + fi + + printf "Session validation passed: %s\n" "$session_file" + return 0 +} + +has_any_sessions() { + local project_dir + 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" + return 0 + else + printf "No sessions found in: %s\n" "$project_dir" + return 1 + fi +} + +AGENTAPI_CMD=() +BOUNDARY_CMD=("${ARG_BOUNDARY_WRAPPER_PATH}" --) +AGENT_CMD=() + +build_agentapi_cmd() { + AGENTAPI_CMD=(agentapi server --type claude --term-width 67 --term-height 1190 --) +} + +build_agent_cmd() { + local args=() + + if [[ -n "${ARG_PERMISSION_MODE}" ]]; then + args+=(--permission-mode "${ARG_PERMISSION_MODE}") + fi + + if [[ -n "${ARG_RESUME_SESSION_ID}" ]]; then + echo "Resuming specified session: ${ARG_RESUME_SESSION_ID}" + args+=(--resume "${ARG_RESUME_SESSION_ID}") + [[ "${ARG_DANGEROUSLY_SKIP_PERMISSIONS}" = "true" ]] && args+=(--dangerously-skip-permissions) + + elif [[ "${ARG_CONTINUE}" = "true" ]]; then + + local session_file + session_file=$(get_task_session_file) + + if task_session_exists && is_valid_session "${session_file}"; then + echo "Resuming task session: ${TASK_SESSION_ID}" + args+=(--resume "${TASK_SESSION_ID}" --dangerously-skip-permissions) + else + echo "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 + echo "Continue disabled, starting fresh session" + [[ "${ARG_DANGEROUSLY_SKIP_PERMISSIONS}" = "true" ]] && args+=(--dangerously-skip-permissions) + [[ -n "${ARG_AI_PROMPT}" ]] && args+=(-- "${ARG_AI_PROMPT}") + fi + + AGENT_CMD=(claude "${args[@]}") +} + +function start() { + # For Task reporting + export CODER_MCP_ALLOWED_TOOLS="coder_report_task" + + mkdir -p "${ARG_WORKDIR}" + cd "${ARG_WORKDIR}" + + if [[ "${ARG_ENABLE_AGENTAPI}" == "true" ]]; then + build_agentapi_cmd + + fi + build_agent_cmd + + printf "Running claude code with args: %s\n" "$(printf '%q ' "${AGENT_CMD[@]}")" + + if [[ "${ARG_ENABLE_BOUNDARY}" = "true" ]]; then + printf "Starting with coder boundary enabled\n" + "${AGENTAPI_CMD[@]}" "" -- "${AGENT_CMD[@]}" + else + "${AGENTAPI_CMD[@]}" "${AGENT_CMD[@]}" + fi +} + +validate_claude_installation +start