mirror of
https://github.com/coder/registry.git
synced 2026-06-03 13:08:14 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3b40c08f1 | |||
| 01f5100068 | |||
| 7e42a145fa | |||
| 0ff3dbcc48 | |||
| a327e79bc4 | |||
| bc39c2ee29 | |||
| e3ff43c0a6 |
@@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
|
||||
```tf
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "3.0.0"
|
||||
version = "3.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = var.openai_api_key
|
||||
workdir = "/home/coder/project"
|
||||
@@ -33,7 +33,7 @@ module "codex" {
|
||||
module "codex" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "3.0.0"
|
||||
version = "3.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
workdir = "/home/coder/project"
|
||||
@@ -61,7 +61,7 @@ module "coder-login" {
|
||||
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "3.0.0"
|
||||
version = "3.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
@@ -84,6 +84,7 @@ module "codex" {
|
||||
- **System Prompt**: If `codex_system_prompt` is set, writes the prompt to `AGENTS.md` in the `~/.codex/` directory
|
||||
- **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI
|
||||
- **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided)
|
||||
- **Session Continuity**: When `continue = true` (default), the module automatically tracks task sessions in `~/.codex-module/.codex-task-session`. On workspace restart, it resumes the existing session with full conversation history. Set `continue = false` to always start fresh sessions.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -107,7 +108,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se
|
||||
```tf
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "3.0.0"
|
||||
version = "3.1.0"
|
||||
# ... other variables ...
|
||||
|
||||
# Override default configuration
|
||||
|
||||
@@ -368,4 +368,90 @@ describe("codex", async () => {
|
||||
expect(prompt.exitCode).not.toBe(0);
|
||||
expect(prompt.stderr).toContain("No such file or directory");
|
||||
});
|
||||
|
||||
test("codex-continue-capture-new-session", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
ai_prompt: "test task",
|
||||
},
|
||||
});
|
||||
|
||||
const workdir = "/home/coder";
|
||||
const expectedSessionId = "019a1234-5678-9abc-def0-123456789012";
|
||||
const sessionsDir = "/home/coder/.codex/sessions";
|
||||
const sessionFile = `${sessionsDir}/${expectedSessionId}.jsonl`;
|
||||
|
||||
await execContainer(id, ["mkdir", "-p", sessionsDir]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo '{"id":"${expectedSessionId}","cwd":"${workdir}","created":"2024-10-24T20:00:00Z","model":"gpt-4-turbo"}' > ${sessionFile}`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const trackingFile = "/home/coder/.codex-module/.codex-task-session";
|
||||
const maxAttempts = 30;
|
||||
let trackingFileContents = "";
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const result = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat ${trackingFile} 2>/dev/null || echo ""`,
|
||||
]);
|
||||
if (result.stdout.trim().length > 0) {
|
||||
trackingFileContents = result.stdout;
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
expect(trackingFileContents).toContain(`${workdir}|${expectedSessionId}`);
|
||||
|
||||
const startLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.codex-module/agentapi-start.log",
|
||||
);
|
||||
expect(startLog).toContain("Capturing new session ID");
|
||||
expect(startLog).toContain("Session tracked");
|
||||
expect(startLog).toContain(expectedSessionId);
|
||||
});
|
||||
|
||||
test("codex-continue-resume-existing-session", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const workdir = "/home/coder";
|
||||
const mockSessionId = "019a1234-5678-9abc-def0-123456789012";
|
||||
const trackingFile = "/home/coder/.codex-module/.codex-task-session";
|
||||
|
||||
await execContainer(id, ["mkdir", "-p", "/home/coder/.codex-module"]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo "${workdir}|${mockSessionId}" > ${trackingFile}`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.codex-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("Found existing task session");
|
||||
expect(startLog.stdout).toContain(mockSessionId);
|
||||
expect(startLog.stdout).toContain("Resuming existing session");
|
||||
expect(startLog.stdout).toContain(
|
||||
`Starting Codex with arguments: --model gpt-4-turbo resume ${mockSessionId}`,
|
||||
);
|
||||
expect(startLog.stdout).not.toContain("test prompt");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -137,6 +137,12 @@ variable "ai_prompt" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "continue" {
|
||||
type = bool
|
||||
description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "codex_system_prompt" {
|
||||
type = string
|
||||
description = "System instructions written to AGENTS.md in the ~/.codex directory"
|
||||
@@ -187,8 +193,9 @@ module "agentapi" {
|
||||
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_CODEX_MODEL='${var.codex_model}' \
|
||||
ARG_CODEX_START_DIRECTORY='${var.workdir}' \
|
||||
ARG_CODEX_START_DIRECTORY='${local.workdir}' \
|
||||
ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_CONTINUE='${var.continue}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
@@ -206,7 +213,7 @@ module "agentapi" {
|
||||
ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \
|
||||
ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_CODEX_START_DIRECTORY='${var.workdir}' \
|
||||
ARG_CODEX_START_DIRECTORY='${local.workdir}' \
|
||||
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
source "$HOME"/.bashrc
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
@@ -16,6 +17,7 @@ fi
|
||||
printf "Version: %s\n" "$(codex --version)"
|
||||
set -o nounset
|
||||
ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d)
|
||||
ARG_CONTINUE=${ARG_CONTINUE:-true}
|
||||
|
||||
echo "=== Codex Launch Configuration ==="
|
||||
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
|
||||
@@ -23,53 +25,187 @@ printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}"
|
||||
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
|
||||
printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")"
|
||||
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
|
||||
printf "Continue Sessions: %s\n" "$ARG_CONTINUE"
|
||||
echo "======================================"
|
||||
set +o nounset
|
||||
CODEX_ARGS=()
|
||||
|
||||
if command_exists codex; then
|
||||
printf "Codex is installed\n"
|
||||
else
|
||||
printf "Error: Codex is not installed. Please enable install_codex or install it manually\n"
|
||||
exit 1
|
||||
fi
|
||||
SESSION_TRACKING_FILE="$HOME/.codex-module/.codex-task-session"
|
||||
|
||||
if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then
|
||||
printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
cd "${ARG_CODEX_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
|
||||
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
cd "${ARG_CODEX_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
find_session_for_directory() {
|
||||
local target_dir="$1"
|
||||
|
||||
if [ -n "$ARG_CODEX_MODEL" ]; then
|
||||
CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL")
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
|
||||
printf "Running the task prompt %s\n" "$ARG_CODEX_TASK_PROMPT"
|
||||
if [ "${ARG_REPORT_TASKS}" == "true" ]; then
|
||||
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
|
||||
else
|
||||
PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
|
||||
if [ ! -f "$SESSION_TRACKING_FILE" ]; then
|
||||
return 1
|
||||
fi
|
||||
CODEX_ARGS+=("$PROMPT")
|
||||
else
|
||||
printf "No task prompt given.\n"
|
||||
fi
|
||||
|
||||
# Terminal dimensions optimized for Coder Tasks UI sidebar:
|
||||
# - Width 67: fits comfortably in sidebar
|
||||
# - Height 1190: adjusted due to Codex terminal height bug
|
||||
printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
|
||||
agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}"
|
||||
local session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1)
|
||||
|
||||
if [ -n "$session_id" ]; then
|
||||
echo "$session_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
store_session_mapping() {
|
||||
local dir="$1"
|
||||
local session_id="$2"
|
||||
|
||||
mkdir -p "$(dirname "$SESSION_TRACKING_FILE")"
|
||||
|
||||
if [ -f "$SESSION_TRACKING_FILE" ]; then
|
||||
grep -v "^$dir|" "$SESSION_TRACKING_FILE" > "$SESSION_TRACKING_FILE.tmp" 2> /dev/null || true
|
||||
mv "$SESSION_TRACKING_FILE.tmp" "$SESSION_TRACKING_FILE"
|
||||
fi
|
||||
|
||||
echo "$dir|$session_id" >> "$SESSION_TRACKING_FILE"
|
||||
}
|
||||
|
||||
find_recent_session_file() {
|
||||
local target_dir="$1"
|
||||
local sessions_dir="$HOME/.codex/sessions"
|
||||
|
||||
if [ ! -d "$sessions_dir" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local latest_file=""
|
||||
local latest_time=0
|
||||
|
||||
while IFS= read -r session_file; do
|
||||
local file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0")
|
||||
local first_line=$(head -n 1 "$session_file" 2> /dev/null)
|
||||
local session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ "$session_cwd" = "$target_dir" ] && [ "$file_time" -gt "$latest_time" ]; then
|
||||
latest_file="$session_file"
|
||||
latest_time="$file_time"
|
||||
fi
|
||||
done < <(find "$sessions_dir" -type f -name "*.jsonl" 2> /dev/null)
|
||||
|
||||
if [ -n "$latest_file" ]; then
|
||||
local first_line=$(head -n 1 "$latest_file")
|
||||
local session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
|
||||
if [ -n "$session_id" ]; then
|
||||
echo "$session_id"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_session_file() {
|
||||
local target_dir="$1"
|
||||
local max_attempts=20
|
||||
local attempt=0
|
||||
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
local session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "")
|
||||
if [ -n "$session_id" ]; then
|
||||
echo "$session_id"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.5
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
validate_codex_installation() {
|
||||
if command_exists codex; then
|
||||
printf "Codex is installed\n"
|
||||
else
|
||||
printf "Error: Codex is not installed. Please enable install_codex or install it manually\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
setup_workdir() {
|
||||
if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then
|
||||
printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
cd "${ARG_CODEX_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
|
||||
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
cd "${ARG_CODEX_START_DIRECTORY}" || {
|
||||
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
build_codex_args() {
|
||||
CODEX_ARGS=()
|
||||
|
||||
if [ -n "$ARG_CODEX_MODEL" ]; then
|
||||
CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL")
|
||||
fi
|
||||
|
||||
if [ "$ARG_CONTINUE" = "true" ]; then
|
||||
existing_session=$(find_session_for_directory "$ARG_CODEX_START_DIRECTORY" 2> /dev/null || echo "")
|
||||
|
||||
if [ -n "$existing_session" ]; then
|
||||
printf "Found existing task session for this directory: %s\n" "$existing_session"
|
||||
printf "Resuming existing session...\n"
|
||||
CODEX_ARGS+=("resume" "$existing_session")
|
||||
else
|
||||
printf "No existing task session found for this directory\n"
|
||||
printf "Starting new task session...\n"
|
||||
|
||||
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
|
||||
if [ "${ARG_REPORT_TASKS}" == "true" ]; then
|
||||
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
|
||||
else
|
||||
PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
|
||||
fi
|
||||
CODEX_ARGS+=("$PROMPT")
|
||||
fi
|
||||
fi
|
||||
else
|
||||
printf "Continue disabled, starting fresh session\n"
|
||||
|
||||
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
|
||||
if [ "${ARG_REPORT_TASKS}" == "true" ]; then
|
||||
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
|
||||
else
|
||||
PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
|
||||
fi
|
||||
CODEX_ARGS+=("$PROMPT")
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
capture_session_id() {
|
||||
if [ "$ARG_CONTINUE" = "true" ] && [ -z "$existing_session" ]; then
|
||||
printf "Capturing new session ID...\n"
|
||||
new_session=$(wait_for_session_file "$ARG_CODEX_START_DIRECTORY" || echo "")
|
||||
|
||||
if [ -n "$new_session" ]; then
|
||||
store_session_mapping "$ARG_CODEX_START_DIRECTORY" "$new_session"
|
||||
printf "✓ Session tracked: %s\n" "$new_session"
|
||||
printf "This session will be automatically resumed on next restart\n"
|
||||
else
|
||||
printf "⚠ Could not capture session ID after 10s timeout\n"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
start_codex() {
|
||||
printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
|
||||
agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" &
|
||||
capture_session_id
|
||||
}
|
||||
|
||||
validate_codex_installation
|
||||
setup_workdir
|
||||
build_codex_args
|
||||
start_codex
|
||||
|
||||
+25
-1
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Handle --version flag
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "HELLO: $(bash -c env)"
|
||||
echo "codex version v1.0.0"
|
||||
@@ -8,7 +9,30 @@ fi
|
||||
|
||||
set -e
|
||||
|
||||
SESSION_ID=""
|
||||
IS_RESUME=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
resume)
|
||||
IS_RESUME=true
|
||||
SESSION_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$IS_RESUME" = false ]; then
|
||||
SESSION_ID="019a1234-5678-9abc-def0-123456789012"
|
||||
echo "Created new session: $SESSION_ID"
|
||||
else
|
||||
echo "Resuming session: $SESSION_ID"
|
||||
fi
|
||||
|
||||
while true; do
|
||||
echo "$(date) - codex-mock"
|
||||
echo "$(date) - codex-mock (session: $SESSION_ID)"
|
||||
sleep 15
|
||||
done
|
||||
|
||||
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,8 +239,6 @@ resource "coder_app" "agentapi_cli" {
|
||||
group = var.cli_app_group
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "agentapi" {
|
||||
sidebar_app {
|
||||
id = coder_app.agentapi_web.id
|
||||
}
|
||||
output "task_app_id" {
|
||||
value = coder_app.agentapi_web.id
|
||||
}
|
||||
|
||||
@@ -8,76 +8,58 @@ tags: [agent, ai, aider]
|
||||
|
||||
# Aider
|
||||
|
||||
Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider and provides a persistent session using screen or tmux.
|
||||
Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider with AgentAPI for seamless Coder Tasks Support.
|
||||
|
||||
```tf
|
||||
module "aider" {
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Interactive Parameter Selection**: Choose your AI provider, model, and configuration options when creating the workspace
|
||||
- **Multiple AI Providers**: Supports Anthropic (Claude), OpenAI, DeepSeek, GROQ, and OpenRouter
|
||||
- **Persistent Sessions**: Uses screen (default) or tmux to keep Aider running in the background
|
||||
- **Optional Dependencies**: Install Playwright for web page scraping and PortAudio for voice coding
|
||||
- **Project Integration**: Works with any project directory, including Git repositories
|
||||
- **Browser UI**: Use Aider in your browser with a modern web interface instead of the terminal
|
||||
- **Non-Interactive Mode**: Automatically processes tasks when provided via the `task_prompt` variable
|
||||
|
||||
## Module Parameters
|
||||
|
||||
> [!NOTE]
|
||||
> The `use_screen` and `use_tmux` parameters cannot both be enabled at the same time. By default, `use_screen` is set to `true` and `use_tmux` is set to `false`.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic setup with API key
|
||||
|
||||
```tf
|
||||
variable "anthropic_api_key" {
|
||||
variable "api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key"
|
||||
description = "API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
}
|
||||
```
|
||||
|
||||
This basic setup will:
|
||||
|
||||
- Install Aider in the workspace
|
||||
- Create a persistent screen session named "aider"
|
||||
- Configure Aider to use Anthropic Claude 3.7 Sonnet model
|
||||
- Enable task reporting (configures Aider to report tasks to Coder MCP)
|
||||
|
||||
### Using OpenAI with tmux
|
||||
|
||||
```tf
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
use_tmux = true
|
||||
ai_provider = "openai"
|
||||
ai_model = "4o" # Uses Aider's built-in alias for gpt-4o
|
||||
ai_api_key = var.openai_api_key
|
||||
api_key = var.api_key
|
||||
ai_provider = "google"
|
||||
model = "gemini"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- pipx is automatically installed if not already available
|
||||
|
||||
## Usage Example
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
name = "AI Prompt"
|
||||
description = "Write an initial prompt for Aider to work on."
|
||||
type = "string"
|
||||
default = ""
|
||||
mutable = true
|
||||
}
|
||||
|
||||
variable "gemini_api_key" {
|
||||
type = string
|
||||
description = "Gemini API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
api_key = var.gemini_api_key
|
||||
install_aider = true
|
||||
workdir = "/home/coder"
|
||||
ai_provider = "google"
|
||||
model = "gemini"
|
||||
install_agentapi = true
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
system_prompt = "..."
|
||||
}
|
||||
```
|
||||
|
||||
@@ -93,174 +75,16 @@ variable "custom_api_key" {
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
ai_provider = "custom"
|
||||
custom_env_var_name = "MY_CUSTOM_API_KEY"
|
||||
ai_model = "custom-model"
|
||||
ai_api_key = var.custom_api_key
|
||||
model = "custom-model"
|
||||
api_key = var.custom_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Custom Extensions (Experimental)
|
||||
|
||||
You can extend Aider's capabilities by adding custom extensions:
|
||||
|
||||
```tf
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
|
||||
experiment_pre_install_script = <<-EOT
|
||||
pip install some-custom-dependency
|
||||
EOT
|
||||
|
||||
experiment_additional_extensions = <<-EOT
|
||||
custom-extension:
|
||||
args: []
|
||||
cmd: custom-extension-command
|
||||
description: A custom extension for Aider
|
||||
enabled: true
|
||||
envs: {}
|
||||
name: custom-extension
|
||||
timeout: 300
|
||||
type: stdio
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.
|
||||
|
||||
## Task Reporting (Experimental)
|
||||
|
||||
> This functionality is in early access as of Coder v2.21 and is still evolving.
|
||||
> For now, we recommend testing it in a demo or staging environment,
|
||||
> rather than deploying to production
|
||||
>
|
||||
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
|
||||
>
|
||||
> 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 either `screen` or `tmux` installed to use this.
|
||||
|
||||
Task reporting is **enabled by default** in this module, allowing you to:
|
||||
|
||||
- Send an initial prompt to Aider during workspace creation
|
||||
- Monitor task progress in the Coder UI
|
||||
- Use the `coder_parameter` resource to collect prompts from users
|
||||
|
||||
### Setting up Task Reporting
|
||||
|
||||
To use task reporting effectively:
|
||||
|
||||
1. Add the Coder Login module to your template
|
||||
2. Configure the necessary variables to pass the task prompt
|
||||
3. Optionally add a coder_parameter to collect prompts from users
|
||||
|
||||
Here's a complete example:
|
||||
|
||||
```tf
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/coder-login/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Write a prompt for Aider"
|
||||
mutable = true
|
||||
ephemeral = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
task_prompt = data.coder_parameter.ai_prompt.value
|
||||
|
||||
# Optionally customize the system prompt
|
||||
system_prompt = <<-EOT
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks, you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message.
|
||||
- Be granular. If you are investigating with multiple steps, report each step to coder.
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input.
|
||||
- Use "state": "complete" only when finished with a task.
|
||||
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing.
|
||||
- Include clear and actionable steps for the user.
|
||||
- Be less than 160 characters in length.
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
When a task prompt is provided via the `task_prompt` variable, the module automatically:
|
||||
|
||||
1. Combines the system prompt with the task prompt into a single message in the format:
|
||||
|
||||
```
|
||||
SYSTEM PROMPT:
|
||||
[system_prompt content]
|
||||
|
||||
This is your current task: [task_prompt]
|
||||
```
|
||||
|
||||
2. Executes the task during workspace creation using the `--message` and `--yes-always` flags
|
||||
3. Logs task output to `$HOME/.aider.log` for reference
|
||||
|
||||
If you want to disable task reporting, set `experiment_report_tasks = false` in your module configuration.
|
||||
|
||||
## Using Aider in Your Workspace
|
||||
|
||||
After the workspace starts, Aider will be installed and configured according to your parameters. A persistent session will automatically be started during workspace creation.
|
||||
|
||||
### Session Options
|
||||
|
||||
You can run Aider in three different ways:
|
||||
|
||||
1. **Direct Mode**: Aider starts directly in the specified folder when you click the app button
|
||||
|
||||
- Simple setup without persistent context
|
||||
- Suitable for quick coding sessions
|
||||
|
||||
2. **Screen Mode** (Default): Run Aider in a screen session that persists across connections
|
||||
|
||||
- Session name: "aider" (or configured via `session_name`)
|
||||
|
||||
3. **Tmux Mode**: Run Aider in a tmux session instead of screen
|
||||
|
||||
- Set `use_tmux = true` to enable
|
||||
- Session name: "aider" (or configured via `session_name`)
|
||||
- Configures tmux with mouse support for shared sessions
|
||||
|
||||
Persistent sessions (screen/tmux) allow you to:
|
||||
|
||||
- Disconnect and reconnect without losing context
|
||||
- Run Aider in the background while doing other work
|
||||
- Switch between terminal and browser interfaces
|
||||
|
||||
### Available AI Providers and Models
|
||||
|
||||
Aider supports various providers and models, and this module integrates directly with Aider's built-in model aliases:
|
||||
@@ -280,10 +104,12 @@ For a complete and up-to-date list of supported aliases and models, please refer
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues:
|
||||
- If `aider` is not found, ensure `install_aider = true` and your API key is valid
|
||||
- Logs are written under `/home/coder/.aider-module/` (`install.log`, `agentapi-start.log`) for debugging
|
||||
- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts
|
||||
|
||||
1. **Screen/Tmux issues**: If you can't reconnect to your session, check if the session exists with `screen -list` or `tmux list-sessions`
|
||||
2. **API key issues**: Ensure you've entered the correct API key for your selected provider
|
||||
3. **Browser mode issues**: If the browser interface doesn't open, check that you're accessing it from a machine that can reach your Coder workspace
|
||||
## References
|
||||
|
||||
For more information on using Aider, see the [Aider documentation](https://aider.chat/docs/).
|
||||
- [Aider Documentation](https://aider.chat/docs)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
|
||||
@@ -1,107 +1,138 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
findResourceInstance,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
test,
|
||||
afterEach,
|
||||
describe,
|
||||
setDefaultTimeout,
|
||||
beforeAll,
|
||||
expect,
|
||||
} from "bun:test";
|
||||
import { execContainer, readFileContainer, runTerraformInit } from "~test";
|
||||
import {
|
||||
loadTestFile,
|
||||
writeExecutable,
|
||||
setup as setupUtil,
|
||||
execModuleScript,
|
||||
expectAgentAPIStarted,
|
||||
} from "../../../coder/modules/agentapi/test-util";
|
||||
|
||||
describe("aider", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||
cleanupFunctions.push(cleanup);
|
||||
};
|
||||
afterEach(async () => {
|
||||
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||
cleanupFunctions = [];
|
||||
for (const cleanup of cleanupFnsCopy) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
skipAiderMock?: boolean;
|
||||
moduleVariables?: Record<string, string>;
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
install_aider: props?.skipAiderMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
aider_model: "test-model",
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
agentapiMockScript: props?.agentapiMockScript,
|
||||
});
|
||||
|
||||
it("configures task prompt correctly", async () => {
|
||||
const testPrompt = "Add a hello world function";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
task_prompt: testPrompt,
|
||||
// Place the Aider mock CLI binary inside the container
|
||||
if (!props?.skipAiderMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/aider",
|
||||
content: await loadTestFile(`${import.meta.dir}`, "aider-mock.sh"),
|
||||
});
|
||||
}
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
`This is your current task: ${testPrompt}`,
|
||||
);
|
||||
expect(instance.script).toContain("aider --architect --yes-always");
|
||||
return { id };
|
||||
};
|
||||
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
describe("Aider", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
it("handles custom system prompt", async () => {
|
||||
const customPrompt = "Report all tasks with state: working";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
system_prompt: customPrompt,
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
model: "gemini",
|
||||
},
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(customPrompt);
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
it("handles pre and post install scripts", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
experiment_pre_install_script: "echo 'Pre-install script executed'",
|
||||
experiment_post_install_script: "echo 'Post-install script executed'",
|
||||
test("api-key", async () => {
|
||||
const apiKey = "test-api-key-123";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
api_key: apiKey,
|
||||
model: "gemini",
|
||||
},
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(instance.script).toContain("Running pre-install script");
|
||||
expect(instance.script).toContain("Running post-install script");
|
||||
expect(instance.script).toContain("base64 -d > /tmp/pre_install.sh");
|
||||
expect(instance.script).toContain("base64 -d > /tmp/post_install.sh");
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain("API key provided!");
|
||||
});
|
||||
|
||||
it("validates that use_screen and use_tmux cannot both be true", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
use_screen: true,
|
||||
use_tmux: true,
|
||||
test("custom-folder", async () => {
|
||||
const workdir = "/tmp/aider-test";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
workdir,
|
||||
model: "gemini",
|
||||
},
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(instance.script).toContain(
|
||||
"Error: Both use_screen and use_tmux cannot be enabled at the same time",
|
||||
await execModuleScript(id);
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/install.log",
|
||||
);
|
||||
expect(instance.script).toContain("exit 1");
|
||||
expect(resp).toContain(workdir);
|
||||
});
|
||||
|
||||
it("configures Aider with known provider and model", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
ai_provider: "anthropic",
|
||||
ai_model: "sonnet",
|
||||
ai_api_key: "test-anthropic-key",
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
|
||||
post_install_script: "#!/bin/bash\necho 'post-install-script'",
|
||||
model: "gemini",
|
||||
},
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
'export ANTHROPIC_API_KEY=\\"test-anthropic-key\\"',
|
||||
await execModuleScript(id);
|
||||
const preLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/pre_install.log",
|
||||
);
|
||||
expect(instance.script).toContain("--model sonnet");
|
||||
expect(instance.script).toContain(
|
||||
"Starting Aider using anthropic provider and model: sonnet",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles custom provider with custom env var and API key", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
ai_provider: "custom",
|
||||
custom_env_var_name: "MY_CUSTOM_API_KEY",
|
||||
ai_model: "custom-model",
|
||||
ai_api_key: "test-custom-key",
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
'export MY_CUSTOM_API_KEY=\\"test-custom-key\\"',
|
||||
);
|
||||
expect(instance.script).toContain("--model custom-model");
|
||||
expect(instance.script).toContain(
|
||||
"Starting Aider using custom provider and model: custom-model",
|
||||
expect(preLog).toContain("pre-install-script");
|
||||
const postLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.aider-module/post_install.log",
|
||||
);
|
||||
expect(postLog).toContain("post-install-script");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,87 +36,84 @@ variable "icon" {
|
||||
default = "/icon/aider.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
variable "workdir" {
|
||||
type = string
|
||||
description = "The folder to run Aider in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Aider"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "Aider"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "Aider CLI"
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
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.10.0"
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "Initial task prompt for Aider."
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
variable "install_aider" {
|
||||
type = bool
|
||||
description = "Whether to install Aider."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "aider_version" {
|
||||
type = string
|
||||
description = "The version of Aider to install."
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "use_screen" {
|
||||
type = bool
|
||||
description = "Whether to use screen for running Aider in the background"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Aider in the background"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "session_name" {
|
||||
type = string
|
||||
description = "Name for the persistent session (screen or tmux)"
|
||||
default = "aider"
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "System prompt for instructing Aider on task reporting and behavior"
|
||||
default = <<-EOT
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks, you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message.
|
||||
- Be granular. If you are investigating with multiple steps, report each step to coder.
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input.
|
||||
- Use "state": "complete" only when finished with a task.
|
||||
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing.
|
||||
- Include clear and actionable steps for the user.
|
||||
- Be less than 160 characters in length.
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "task_prompt" {
|
||||
type = string
|
||||
description = "Task prompt to use with Aider"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Aider."
|
||||
default = null
|
||||
default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
|
||||
}
|
||||
|
||||
variable "experiment_additional_extensions" {
|
||||
@@ -128,20 +125,19 @@ variable "experiment_additional_extensions" {
|
||||
variable "ai_provider" {
|
||||
type = string
|
||||
description = "AI provider to use with Aider (openai, anthropic, azure, google, etc.)"
|
||||
default = "anthropic"
|
||||
default = "google"
|
||||
validation {
|
||||
condition = contains(["openai", "anthropic", "azure", "google", "cohere", "mistral", "ollama", "custom"], var.ai_provider)
|
||||
error_message = "ai_provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
|
||||
error_message = "provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
|
||||
}
|
||||
}
|
||||
|
||||
variable "ai_model" {
|
||||
variable "model" {
|
||||
type = string
|
||||
description = "AI model to use with Aider. Can use Aider's built-in aliases like '4o' (gpt-4o), 'sonnet' (claude-3-7-sonnet), 'opus' (claude-3-opus), etc."
|
||||
default = "sonnet"
|
||||
}
|
||||
|
||||
variable "ai_api_key" {
|
||||
variable "api_key" {
|
||||
type = string
|
||||
description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider."
|
||||
default = ""
|
||||
@@ -154,55 +150,66 @@ variable "custom_env_var_name" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "base_aider_config" {
|
||||
type = string
|
||||
description = <<-EOT
|
||||
Base Aider configuration in yaml format. Will be stored in .aider.conf.yml file.
|
||||
|
||||
options include:
|
||||
read:
|
||||
- CONVENTIONS.md
|
||||
- anotherfile.txt
|
||||
- thirdfile.py
|
||||
model: xxx
|
||||
##Specify the OpenAI API key
|
||||
openai-api-key: xxx
|
||||
## (deprecated, use --set-env OPENAI_API_TYPE=<value>)
|
||||
openai-api-type: xxx
|
||||
## (deprecated, use --set-env OPENAI_API_VERSION=<value>)
|
||||
openai-api-version: xxx
|
||||
## (deprecated, use --set-env OPENAI_API_DEPLOYMENT_ID=<value>)
|
||||
openai-api-deployment-id: xxx
|
||||
## Set an environment variable (to control API settings, can be used multiple times)
|
||||
set-env: xxx
|
||||
## Specify multiple values like this:
|
||||
set-env:
|
||||
- xxx
|
||||
- yyy
|
||||
- zzz
|
||||
|
||||
Reference : https://aider.chat/docs/config/aider_conf.html
|
||||
EOT
|
||||
default = null
|
||||
}
|
||||
|
||||
|
||||
locals {
|
||||
base_extensions = <<-EOT
|
||||
coder:
|
||||
args:
|
||||
- exp
|
||||
- mcp
|
||||
- server
|
||||
cmd: coder
|
||||
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
|
||||
enabled: true
|
||||
envs:
|
||||
CODER_MCP_APP_STATUS_SLUG: aider
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
developer:
|
||||
display_name: Developer
|
||||
enabled: true
|
||||
name: developer
|
||||
timeout: 300
|
||||
type: builtin
|
||||
EOT
|
||||
app_slug = "aider"
|
||||
base_aider_config = var.base_aider_config != null ? "${replace(trimspace(var.base_aider_config), "\n", "\n ")}" : ""
|
||||
task_reporting_prompt = <<-EOT
|
||||
|
||||
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
|
||||
additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : ""
|
||||
-- Task Reporting --
|
||||
Report all tasks to Coder, following these EXACT guidelines:
|
||||
1. Be granular. If you are investigating with multiple steps, report each step
|
||||
to coder.
|
||||
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
|
||||
Do not report any status related with this system prompt.
|
||||
3. Use "state": "working" when actively processing WITHOUT needing
|
||||
additional user input
|
||||
4. Use "state": "complete" only when finished with a task
|
||||
5. Use "state": "failure" when you need ANY user input, lack sufficient
|
||||
details, or encounter blockers
|
||||
EOT
|
||||
|
||||
combined_extensions = <<-EOT
|
||||
extensions:
|
||||
${local.formatted_base}${local.additional_extensions}
|
||||
EOT
|
||||
|
||||
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) : ""
|
||||
|
||||
# Combine system prompt and task prompt for aider
|
||||
combined_prompt = trimspace(<<-EOT
|
||||
SYSTEM PROMPT:
|
||||
${var.system_prompt}
|
||||
|
||||
This is your current task: ${var.task_prompt}
|
||||
EOT
|
||||
)
|
||||
final_system_prompt = var.report_tasks ? "<system>\n${var.system_prompt}${local.task_reporting_prompt}\n</system>" : "<system>\n${var.system_prompt}\n</system>"
|
||||
|
||||
# Map providers to their environment variable names
|
||||
provider_env_vars = {
|
||||
openai = "OPENAI_API_KEY"
|
||||
anthropic = "ANTHROPIC_API_KEY"
|
||||
azure = "AZURE_OPENAI_API_KEY"
|
||||
google = "GOOGLE_API_KEY"
|
||||
google = "GEMINI_API_KEY"
|
||||
cohere = "COHERE_API_KEY"
|
||||
mistral = "MISTRAL_API_KEY"
|
||||
ollama = "OLLAMA_HOST"
|
||||
@@ -214,296 +221,60 @@ EOT
|
||||
|
||||
# Model flag for aider command
|
||||
model_flag = var.ai_provider == "ollama" ? "--ollama-model" : "--model"
|
||||
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".aider-module"
|
||||
}
|
||||
|
||||
# Install and Initialize Aider
|
||||
resource "coder_script" "aider" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Aider"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
agentapi_subdomain = var.subdomain
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_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 -e
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
echo "Setting up Aider AI pair programming..."
|
||||
|
||||
if [ "${var.use_screen}" = "true" ] && [ "${var.use_tmux}" = "true" ]; then
|
||||
echo "Error: Both use_screen and use_tmux cannot be enabled at the same time."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${var.folder}"
|
||||
|
||||
if [ "$(uname)" = "Linux" ]; then
|
||||
echo "Checking dependencies for Linux..."
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if ! command_exists tmux; then
|
||||
echo "Installing tmux for persistent sessions..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq tmux
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq tmux || echo "Warning: Cannot install tmux without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q tmux
|
||||
else
|
||||
dnf install -y -q tmux || echo "Warning: Cannot install tmux without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install tmux on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "tmux is already installed, skipping installation."
|
||||
fi
|
||||
elif [ "${var.use_screen}" = "true" ]; then
|
||||
if ! command_exists screen; then
|
||||
echo "Installing screen for persistent sessions..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq screen
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq screen || echo "Warning: Cannot install screen without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q screen
|
||||
else
|
||||
dnf install -y -q screen || echo "Warning: Cannot install screen without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install screen on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "screen is already installed, skipping installation."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "This module currently only supports Linux workspaces."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.install_aider}" = "true" ]; then
|
||||
echo "Installing Aider..."
|
||||
|
||||
if ! command_exists python3 || ! command_exists pip3; then
|
||||
echo "Installing Python dependencies required for Aider..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq python3-pip python3-venv
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq python3-pip python3-venv || echo "Warning: Cannot install Python packages without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q python3-pip python3-virtualenv
|
||||
else
|
||||
dnf install -y -q python3-pip python3-virtualenv || echo "Warning: Cannot install Python packages without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install Python on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "Python is already installed, skipping installation."
|
||||
fi
|
||||
|
||||
if ! command_exists aider; then
|
||||
curl -LsSf https://aider.chat/install.sh | sh
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then
|
||||
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.zshrc" ]; then
|
||||
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then
|
||||
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Aider to report tasks via Coder MCP..."
|
||||
|
||||
mkdir -p "$HOME/.config/aider"
|
||||
|
||||
cat > "$HOME/.config/aider/config.yml" << EOL
|
||||
${trimspace(local.combined_extensions)}
|
||||
EOL
|
||||
echo "Added Coder MCP extension to Aider config.yml"
|
||||
fi
|
||||
|
||||
echo "Starting persistent Aider session..."
|
||||
|
||||
touch "$HOME/.aider.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
export PATH="$HOME/bin:$PATH"
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if [ -n "${var.task_prompt}" ]; then
|
||||
echo "Running Aider with message in tmux session..."
|
||||
|
||||
# Configure tmux for shared sessions
|
||||
if [ ! -f "$HOME/.tmux.conf" ]; then
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\""
|
||||
echo "Aider task started in tmux session '${var.session_name}'. Check the UI for progress."
|
||||
else
|
||||
# Configure tmux for shared sessions
|
||||
if [ ! -f "$HOME/.tmux.conf" ]; then
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${var.system_prompt}\""
|
||||
echo "Tmux session '${var.session_name}' started. Access it by clicking the Aider button."
|
||||
fi
|
||||
else
|
||||
if [ -n "${var.task_prompt}" ]; then
|
||||
echo "Running Aider with message in screen session..."
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..."
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..."
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
|
||||
export ${local.env_var_name}=\"${var.ai_api_key}\"
|
||||
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
|
||||
/bin/bash
|
||||
"
|
||||
|
||||
echo "Aider task started in screen session '${var.session_name}'. Check the UI for progress."
|
||||
else
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..."
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..."
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
|
||||
export ${local.env_var_name}=\"${var.ai_api_key}\"
|
||||
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
|
||||
/bin/bash
|
||||
"
|
||||
echo "Screen session '${var.session_name}' started. Access it by clicking the Aider button."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Aider setup complete!"
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_WORKDIR='${var.workdir}' \
|
||||
ARG_API_KEY='${base64encode(var.api_key)}' \
|
||||
ARG_MODEL='${var.model}' \
|
||||
ARG_PROVIDER='${var.ai_provider}' \
|
||||
ARG_ENV_API_NAME_HOLDER='${local.env_var_name}' \
|
||||
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
|
||||
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
# Aider CLI app
|
||||
resource "coder_app" "aider_cli" {
|
||||
agent_id = var.agent_id
|
||||
slug = "aider"
|
||||
display_name = "Aider"
|
||||
icon = var.icon
|
||||
command = <<-EOT
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t ${var.session_name} 2>/dev/null; then
|
||||
echo "Attaching to existing Aider tmux session..."
|
||||
tmux attach-session -t ${var.session_name}
|
||||
else
|
||||
echo "Starting new Aider tmux session..."
|
||||
tmux new-session -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.use_screen}" = "true" ]; then
|
||||
if ! screen -list | grep -q "${var.session_name}"; then
|
||||
echo "Error: No existing Aider session found. Please wait for the script to start it."
|
||||
exit 1
|
||||
fi
|
||||
screen -xRR ${var.session_name}
|
||||
else
|
||||
cd "${var.folder}"
|
||||
echo "Starting Aider directly..."
|
||||
export ${local.env_var_name}="${var.ai_api_key}"
|
||||
aider ${local.model_flag} ${var.ai_model} --message "${local.combined_prompt}"
|
||||
fi
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_WORKDIR='${var.workdir}' \
|
||||
ARG_INSTALL_AIDER='${var.install_aider}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_AIDER_CONFIG="$(echo -n '${base64encode(local.base_aider_config)}' | base64 -d)" \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
run "test_aider_basic" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-123"
|
||||
workdir = "/home/coder"
|
||||
model = "gemini"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.workdir == "/home/coder"
|
||||
error_message = "Workdir variable should default to /home/coder"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.agent_id == "test-agent-123"
|
||||
error_message = "Agent ID variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_aider == true
|
||||
error_message = "install_aider should default to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_agentapi == true
|
||||
error_message = "install_agentapi should default to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.report_tasks == false
|
||||
error_message = "report_tasks should default to false"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_with_api_key" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-456"
|
||||
workdir = "/home/coder/workspace"
|
||||
api_key = "test-api-key-123"
|
||||
model = "gemini"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.api_key == "test-api-key-123"
|
||||
error_message = "API key value should match the input"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_options" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-789"
|
||||
workdir = "/home/coder/custom"
|
||||
order = 5
|
||||
group = "development"
|
||||
icon = "/icon/custom.svg"
|
||||
model = "4o"
|
||||
ai_prompt = "Help me write better code"
|
||||
install_aider = false
|
||||
install_agentapi = false
|
||||
agentapi_version = "v0.10.0"
|
||||
api_key = ""
|
||||
base_aider_config = "read:\n - CONVENTIONS.md"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.order == 5
|
||||
error_message = "Order variable should be set to 5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.group == "development"
|
||||
error_message = "Group variable should be set to 'development'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.icon == "/icon/custom.svg"
|
||||
error_message = "Icon variable should be set to custom icon"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.model == "4o"
|
||||
error_message = "Model variable should be set to '4o'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.ai_prompt == "Help me write better code"
|
||||
error_message = "AI prompt variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_aider == false
|
||||
error_message = "install_aider should be set to false"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_agentapi == false
|
||||
error_message = "install_agentapi should be set to false"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.agentapi_version == "v0.10.0"
|
||||
error_message = "AgentAPI version should be set to 'v0.10.0'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_with_scripts" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-scripts"
|
||||
workdir = "/home/coder/scripts"
|
||||
model = "gemini"
|
||||
pre_install_script = "echo 'Pre-install script'"
|
||||
post_install_script = "echo 'Post-install script'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.pre_install_script == "echo 'Pre-install script'"
|
||||
error_message = "Pre-install script should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.post_install_script == "echo 'Post-install script'"
|
||||
error_message = "Post-install script should be set correctly"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_ai_provider_env_mapping" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-provider"
|
||||
workdir = "/home/coder/test"
|
||||
ai_provider = "google"
|
||||
model = "gemini"
|
||||
custom_env_var_name = ""
|
||||
}
|
||||
|
||||
# Ensure provider -> env var mapping works as expected (based on locals.provider_env_vars)
|
||||
assert {
|
||||
condition = var.ai_provider == "google"
|
||||
error_message = "AI provider should be set to 'google' for this test"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Inputs
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-/home/coder}
|
||||
ARG_INSTALL_AIDER=${ARG_INSTALL_AIDER:-true}
|
||||
ARG_AIDER_CONFIG=${ARG_AIDER_CONFIG:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "Install flag: $ARG_INSTALL_AIDER"
|
||||
echo "Workspace: $ARG_WORKDIR"
|
||||
echo "--------------------------------"
|
||||
|
||||
function install_aider() {
|
||||
echo "pipx installing..."
|
||||
sudo apt-get install -y pipx
|
||||
echo "pipx installed!"
|
||||
pipx ensurepath
|
||||
mkdir -p "$ARG_WORKDIR/.local/bin"
|
||||
export PATH="$HOME/.local/bin:$ARG_WORKDIR/.local/bin:$PATH"
|
||||
|
||||
if ! command_exists aider; then
|
||||
echo "Installing Aider via pipx..."
|
||||
pipx install --force aider-install
|
||||
aider-install
|
||||
fi
|
||||
echo "Aider installed: $(aider --version || echo 'Aider installation check failed')"
|
||||
}
|
||||
|
||||
function configure_aider_settings() {
|
||||
if [ -n "${ARG_AIDER_CONFIG}" ]; then
|
||||
echo "Configuring Aider environment variables and model"
|
||||
|
||||
mkdir -p "$HOME/.config/aider"
|
||||
|
||||
echo "$ARG_AIDER_CONFIG" > "$HOME/.config/aider/.aider.conf.yml"
|
||||
echo "Aider config created at $HOME/.config/aider/.aider.conf.yml"
|
||||
else
|
||||
printf "No Aider environment variables or model configured\n"
|
||||
fi
|
||||
}
|
||||
|
||||
install_aider
|
||||
configure_aider_settings
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure pipx-installed apps are in PATH
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-/home/coder}
|
||||
ARG_API_KEY=$(echo -n "${ARG_API_KEY:-}" | base64 -d)
|
||||
ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_MODEL=${ARG_MODEL:-}
|
||||
ARG_PROVIDER=${ARG_PROVIDER:-}
|
||||
ARG_ENV_API_NAME_HOLDER=${ARG_ENV_API_NAME_HOLDER:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "Provider: $ARG_PROVIDER"
|
||||
echo "Model: $ARG_MODEL"
|
||||
echo "--------------------------------"
|
||||
|
||||
if [ -n "$ARG_API_KEY" ]; then
|
||||
printf "API key provided!\n"
|
||||
export $ARG_ENV_API_NAME_HOLDER=$ARG_API_KEY
|
||||
else
|
||||
printf "API key not provided.\n"
|
||||
fi
|
||||
|
||||
build_initial_prompt() {
|
||||
local initial_prompt=""
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ -n "$ARG_SYSTEM_PROMPT" ]; then
|
||||
initial_prompt="$ARG_SYSTEM_PROMPT $ARG_AI_PROMPT"
|
||||
else
|
||||
initial_prompt="$ARG_AI_PROMPT"
|
||||
fi
|
||||
fi
|
||||
echo "$initial_prompt"
|
||||
}
|
||||
|
||||
start_agentapi() {
|
||||
echo "Starting in directory: $ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
local initial_prompt
|
||||
initial_prompt=$(build_initial_prompt)
|
||||
if [ -n "$initial_prompt" ]; then
|
||||
echo "Starting agentapi with initial prompt"
|
||||
agentapi server -I="$initial_prompt" --type aider --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always
|
||||
else
|
||||
agentapi server --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always
|
||||
fi
|
||||
}
|
||||
|
||||
# TODO: Implement MCP server for coder when Aider support MCP servers.
|
||||
|
||||
start_agentapi
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "HELLO: $(bash -c env)"
|
||||
echo "aider version v0.86.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - aider-agent-mock"
|
||||
sleep 15
|
||||
done
|
||||
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.3.0"
|
||||
version = "3.3.3"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
@@ -51,7 +51,7 @@ module "claude-code" {
|
||||
boundary_log_level = "WARN"
|
||||
boundary_additional_allowed_urls = ["GET *google.com"]
|
||||
boundary_proxy_port = "8087"
|
||||
version = "3.3.0"
|
||||
version = "3.3.3"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.3.0"
|
||||
version = "3.3.3"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -106,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.3.0"
|
||||
version = "3.3.3"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -129,7 +129,7 @@ variable "claude_code_oauth_token" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.3.0"
|
||||
version = "3.3.3"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
@@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.3.0"
|
||||
version = "3.3.3"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
@@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.3.0"
|
||||
version = "3.3.3"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
|
||||
@@ -91,11 +91,6 @@ function report_tasks() {
|
||||
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure claude-code "$ARG_WORKDIR"
|
||||
else
|
||||
export CODER_MCP_APP_STATUS_SLUG=""
|
||||
export CODER_MCP_AI_AGENTAPI_URL=""
|
||||
echo "Configuring Claude Code with Coder MCP..."
|
||||
coder exp mcp configure claude-code "$ARG_WORKDIR"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ task_session_exists() {
|
||||
ARGS=()
|
||||
|
||||
function start_agentapi() {
|
||||
# For Task reporting
|
||||
export CODER_MCP_ALLOWED_TOOLS="coder_report_task"
|
||||
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
@@ -163,18 +166,10 @@ function start_agentapi() {
|
||||
BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT})
|
||||
fi
|
||||
|
||||
# Remove --dangerously-skip-permissions from ARGS when using boundary (it doesn't work with elevated permissions)
|
||||
# Create a new array without the dangerous permissions flag
|
||||
CLAUDE_ARGS=()
|
||||
for arg in "${ARGS[@]}"; do
|
||||
if [ "$arg" != "--dangerously-skip-permissions" ]; then
|
||||
CLAUDE_ARGS+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
agentapi server --allowed-hosts="*" --type claude --term-width 67 --term-height 1190 -- \
|
||||
sudo -E env PATH=$PATH setpriv --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \
|
||||
claude "${CLAUDE_ARGS[@]}"
|
||||
sudo -E env PATH=$PATH setpriv --reuid=$(id -u) --regid=$(id -g) --clear-groups \
|
||||
--inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \
|
||||
claude "${ARGS[@]}"
|
||||
else
|
||||
agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}"
|
||||
fi
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
|
||||
module "kasmvnc" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kasmvnc/coder"
|
||||
version = "1.2.4"
|
||||
version = "1.2.5"
|
||||
agent_id = coder_agent.example.id
|
||||
desktop_environment = "xfce"
|
||||
subdomain = true
|
||||
|
||||
@@ -23,7 +23,7 @@ variable "port" {
|
||||
variable "kasm_version" {
|
||||
type = string
|
||||
description = "Version of KasmVNC to install."
|
||||
default = "1.3.2"
|
||||
default = "1.4.0"
|
||||
}
|
||||
|
||||
variable "desktop_environment" {
|
||||
|
||||
@@ -8,10 +8,10 @@ error() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Function to check if vncserver is already installed
|
||||
# Function to check if KasmVNC is already installed
|
||||
check_installed() {
|
||||
if command -v vncserver &> /dev/null; then
|
||||
echo "vncserver is already installed."
|
||||
if command -v kasmvncserver &> /dev/null; then
|
||||
echo "KasmVNC is already installed."
|
||||
return 0 # Don't exit, just indicate it's installed
|
||||
else
|
||||
return 1 # Indicates not installed
|
||||
@@ -158,7 +158,7 @@ case "$arch" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check if vncserver is installed, and install if not
|
||||
# Check if KasmVNC is installed, and install if not
|
||||
if ! check_installed; then
|
||||
# Check for NOPASSWD sudo (required)
|
||||
if ! command -v sudo &> /dev/null || ! sudo -n true 2> /dev/null; then
|
||||
@@ -188,7 +188,7 @@ if ! check_installed; then
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "vncserver already installed. Skipping installation."
|
||||
echo "KasmVNC already installed. Skipping installation."
|
||||
fi
|
||||
|
||||
if command -v sudo &> /dev/null && sudo -n true 2> /dev/null; then
|
||||
@@ -227,7 +227,7 @@ EOF
|
||||
# This password is not used since we start the server without auth.
|
||||
# The server is protected via the Coder session token / tunnel
|
||||
# and does not listen publicly
|
||||
echo -e "password\npassword\n" | vncpasswd -wo -u "$USER"
|
||||
echo -e "password\npassword\n" | kasmvncpasswd -wo -u "$USER"
|
||||
|
||||
get_http_dir() {
|
||||
# determine the served file path
|
||||
@@ -290,7 +290,7 @@ VNC_LOG="/tmp/kasmvncserver.log"
|
||||
printf "🚀 Starting KasmVNC server...\n"
|
||||
|
||||
set +e
|
||||
vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1
|
||||
kasmvncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1
|
||||
RETVAL=$?
|
||||
set -e
|
||||
|
||||
|
||||
Reference in New Issue
Block a user