@@ -4,7 +4,7 @@ terraform {
44 required_providers {
55 coder = {
66 source = " coder/coder"
7- version = " >= 2.5 "
7+ version = " >= 2.7 "
88 }
99 }
1010}
@@ -96,9 +96,75 @@ variable "experiment_tmux_session_save_interval" {
9696 default = " 15"
9797}
9898
99+ variable "install_agentapi" {
100+ type = bool
101+ description = " Whether to install AgentAPI."
102+ default = true
103+ }
104+
105+ variable "agentapi_version" {
106+ type = string
107+ description = " The version of AgentAPI to install."
108+ default = " v0.2.2"
109+ }
110+
99111locals {
100- encoded_pre_install_script = var. experiment_pre_install_script != null ? base64encode (var. experiment_pre_install_script ) : " "
101- encoded_post_install_script = var. experiment_post_install_script != null ? base64encode (var. experiment_post_install_script ) : " "
112+ # we have to trim the slash because otherwise coder exp mcp will
113+ # set up an invalid claude config
114+ workdir = trimsuffix (var. folder , " /" )
115+ encoded_pre_install_script = var. experiment_pre_install_script != null ? base64encode (var. experiment_pre_install_script ) : " "
116+ encoded_post_install_script = var. experiment_post_install_script != null ? base64encode (var. experiment_post_install_script ) : " "
117+ agentapi_start_command = <<- EOT
118+ #!/bin/bash
119+ set -e
120+
121+ # if the first argument is not empty, start claude with the prompt
122+ if [ -n "$1" ]; then
123+ prompt="$(cat ~/.claude-code-prompt)"
124+ cp ~/.claude-code-prompt /tmp/claude-code-prompt
125+ else
126+ rm -f /tmp/claude-code-prompt
127+ fi
128+
129+ # We need to check if there's a session to use --continue. If there's no session,
130+ # using this flag would cause claude to exit with an error.
131+ # warning: this is a hack and will break if claude changes the format of the .claude.json file.
132+ # Also, this solution is not ideal: a user has to quit claude in order for the session id to appear
133+ # in .claude.json. If they just restart the workspace, the session id will not be available.
134+ continue_flag=""
135+ if grep -q '"lastSessionId":' ~/.claude.json; then
136+ echo "Found a Claude Code session to continue."
137+ continue_flag="--continue"
138+ else
139+ echo "No Claude Code session to continue."
140+ fi
141+
142+ # use low width to fit in the tasks UI sidebar. height is adjusted so that width x height ~= 80x1000 characters
143+ # visible in the terminal screen by default.
144+ prompt_subshell='"$(cat /tmp/claude-code-prompt)"'
145+ agentapi server --term-width 67 --term-height 1190 -- bash -c "claude $continue_flag --dangerously-skip-permissions $prompt_subshell"
146+ EOT
147+ agentapi_wait_for_start_command = <<- EOT
148+ #!/bin/bash
149+ set -o errexit
150+ set -o pipefail
151+
152+ echo "Waiting for agentapi server to start on port 3284..."
153+ for i in $(seq 1 15); do
154+ if lsof -i :3284 | grep -q 'LISTEN'; then
155+ echo "agentapi server started on port 3284."
156+ break
157+ fi
158+ echo "Waiting... ($i/15)"
159+ sleep 1
160+ done
161+ if ! lsof -i :3284 | grep -q 'LISTEN'; then
162+ echo "Error: agentapi server did not start on port 3284 after 15 seconds."
163+ exit 1
164+ fi
165+ EOT
166+ agentapi_start_command_base64 = base64encode (local. agentapi_start_command )
167+ agentapi_wait_for_start_command_base64 = base64encode (local. agentapi_wait_for_start_command )
102168}
103169
104170# Install and Initialize Claude Code
@@ -132,12 +198,12 @@ resource "coder_script" "claude_code" {
132198 fi
133199 }
134200
135- if [ ! -d "${ var . folder } " ]; then
136- echo "Warning: The specified folder '${ var . folder } ' does not exist."
201+ if [ ! -d "${ local . workdir } " ]; then
202+ echo "Warning: The specified folder '${ local . workdir } ' does not exist."
137203 echo "Creating the folder..."
138204 # The folder must exist before tmux is started or else claude will start
139205 # in the home directory.
140- mkdir -p "${ var . folder } "
206+ mkdir -p "${ local . workdir } "
141207 echo "Folder created successfully."
142208 fi
143209 if [ -n "${ local . encoded_pre_install_script } " ]; then
@@ -176,9 +242,38 @@ resource "coder_script" "claude_code" {
176242 npm install -g @anthropic-ai/claude-code@${ var . claude_code_version }
177243 fi
178244
245+ # Install AgentAPI if enabled
246+ if [ "${ var . install_agentapi } " = "true" ]; then
247+ echo "Installing AgentAPI..."
248+ arch=$(uname -m)
249+ if [ "$arch" = "x86_64" ]; then
250+ binary_name="agentapi-linux-amd64"
251+ elif [ "$arch" = "aarch64" ]; then
252+ binary_name="agentapi-linux-arm64"
253+ else
254+ echo "Error: Unsupported architecture: $arch"
255+ exit 1
256+ fi
257+ wget "https://github.com/coder/agentapi/releases/download/${ var . agentapi_version } /$binary_name"
258+ chmod +x "$binary_name"
259+ sudo mv "$binary_name" /usr/local/bin/agentapi
260+ fi
261+ if ! command_exists agentapi; then
262+ echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
263+ exit 1
264+ fi
265+
266+ # save the prompt for the agentapi start command
267+ echo -n "$CODER_MCP_CLAUDE_TASK_PROMPT" > ~/.claude-code-prompt
268+
269+ echo -n "${ local . agentapi_start_command_base64 } " | base64 -d > ~/.agentapi-start-command
270+ chmod +x ~/.agentapi-start-command
271+ echo -n "${ local . agentapi_wait_for_start_command_base64 } " | base64 -d > ~/.agentapi-wait-for-start-command
272+ chmod +x ~/.agentapi-wait-for-start-command
273+
179274 if [ "${ var . experiment_report_tasks } " = "true" ]; then
180275 echo "Configuring Claude Code to report tasks via Coder MCP..."
181- coder exp mcp configure claude-code ${ var . folder }
276+ coder exp mcp configure claude-code ${ local . workdir } --ai-agentapi-url http://localhost:3284
182277 fi
183278
184279 if [ -n "${ local . encoded_post_install_script } " ]; then
@@ -257,17 +352,16 @@ EOF
257352 export LANG=en_US.UTF-8
258353 export LC_ALL=en_US.UTF-8
259354
355+ tmux new-session -d -s agentapi-cc -c ${ local . workdir } '~/.agentapi-start-command true; exec bash'
356+ ~/.agentapi-wait-for-start-command
357+
260358 if [ "${ var . experiment_tmux_session_persistence } " = "true" ]; then
261359 sleep 3
360+ fi
262361
263- if ! tmux has-session -t claude-code 2>/dev/null; then
264- # Only create a new session if one doesn't exist
265- tmux new-session -d -s claude-code -c ${ var . folder } "claude --dangerously-skip-permissions \"$CODER_MCP_CLAUDE_TASK_PROMPT\""
266- fi
267- else
268- if ! tmux has-session -t claude-code 2>/dev/null; then
269- tmux new-session -d -s claude-code -c ${ var . folder } "claude --dangerously-skip-permissions \"$CODER_MCP_CLAUDE_TASK_PROMPT\""
270- fi
362+ if ! tmux has-session -t claude-code 2>/dev/null; then
363+ # Only create a new session if one doesn't exist
364+ tmux new-session -d -s claude-code -c ${ local . workdir } "agentapi attach; exec bash"
271365 fi
272366 fi
273367
297391 export LANG=en_US.UTF-8
298392 export LC_ALL=en_US.UTF-8
299393
394+ screen -U -dmS agentapi-cc bash -c '
395+ cd ${ local . workdir }
396+ # setting the first argument will make claude use the prompt
397+ ~/.agentapi-start-command true
398+ exec bash
399+ '
400+ ~/.agentapi-wait-for-start-command
401+
300402 screen -U -dmS claude-code bash -c '
301- cd ${ var . folder }
302- claude --dangerously-skip-permissions "$CODER_MCP_CLAUDE_TASK_PROMPT" | tee -a "$HOME/.claude-code.log"
403+ cd ${ local . workdir }
404+ agentapi attach
303405 exec bash
304406 '
305407 else
312414 run_on_start = true
313415}
314416
417+ resource "coder_app" "claude_code_web" {
418+ # use a short slug to mitigate https://github.com/coder/coder/issues/15178
419+ slug = " ccw"
420+ display_name = " Claude Code Web"
421+ agent_id = var. agent_id
422+ url = " http://localhost:3284/"
423+ icon = var. icon
424+ subdomain = true
425+ healthcheck {
426+ url = " http://localhost:3284/status"
427+ interval = 5
428+ threshold = 3
429+ }
430+ }
431+
315432resource "coder_app" "claude_code" {
316433 slug = " claude-code"
317434 display_name = " Claude Code"
@@ -324,31 +441,47 @@ resource "coder_app" "claude_code" {
324441 export LC_ALL=en_US.UTF-8
325442
326443 if [ "${ var . experiment_use_tmux } " = "true" ]; then
444+ if ! tmux has-session -t agentapi-cc 2>/dev/null; then
445+ # start agentapi without claude using the prompt (no argument)
446+ tmux new-session -d -s agentapi-cc -c ${ local . workdir } '~/.agentapi-start-command; exec bash'
447+ ~/.agentapi-wait-for-start-command
448+ fi
449+
327450 if tmux has-session -t claude-code 2>/dev/null; then
328451 echo "Attaching to existing Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
329- # If Claude isn't running in the session, start it without the prompt
330- if ! tmux list-panes -t claude-code -F '#{pane_current_command}' | grep -q "claude"; then
331- tmux send-keys -t claude-code "cd ${ var . folder } && claude -c --dangerously-skip-permissions" C-m
332- fi
333452 tmux attach-session -t claude-code
334453 else
335454 echo "Starting a new Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
336- tmux new-session -s claude-code -c ${ var . folder } "claude --dangerously-skip-permissions | tee -a \"$HOME/.claude-code.log\" ; exec bash"
455+ tmux new-session -s claude-code -c ${ local . workdir } "agentapi attach ; exec bash"
337456 fi
338457 elif [ "${ var . experiment_use_screen } " = "true" ]; then
458+ if ! screen -list | grep -q "agentapi-cc"; then
459+ screen -S agentapi-cc bash -c '
460+ cd ${ local . workdir }
461+ # start agentapi without claude using the prompt (no argument)
462+ ~/.agentapi-start-command
463+ exec bash
464+ '
465+ fi
339466 if screen -list | grep -q "claude-code"; then
340467 echo "Attaching to existing Claude Code screen session." | tee -a "$HOME/.claude-code.log"
341468 screen -xRR claude-code
342469 else
343470 echo "Starting a new Claude Code screen session." | tee -a "$HOME/.claude-code.log"
344- screen -S claude-code bash -c 'claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log" ; exec bash'
471+ screen -S claude-code bash -c 'agentapi attach ; exec bash'
345472 fi
346473 else
347- cd ${ var . folder }
348- claude
474+ cd ${ local . workdir }
475+ agentapi attach
349476 fi
350477 EOT
351478 icon = var. icon
352479 order = var. order
353480 group = var. group
354481}
482+
483+ resource "coder_ai_task" "claude_code" {
484+ sidebar_app {
485+ id = coder_app. claude_code_web . id
486+ }
487+ }
0 commit comments