commit d1c56355442b85f7a473385460a5a11351dd4edc Author: Luke Date: Wed Jul 30 20:04:13 2025 +0300 initial diff --git a/.claude/plan.md b/.claude/plan.md new file mode 100644 index 0000000..34fca97 --- /dev/null +++ b/.claude/plan.md @@ -0,0 +1,21 @@ +# This is a test project + +## IMPORTANT (when working on a plan versus ccsb aka CLAUDE.MD) +- Never use hello world as a string in testing +- Use gemini-cli when you need to ask about documentation + +## PLAN + +### Basic file +- Create a python file called main.py + +### Architecture +- The file should be able to print whatever it is given into split by space returned as json array + +## POST TASK TASKS +- Git commit with comprehensive changes +- Restructure project architecture for scalability +- Merge duplicate systems and optimize code quality +- Create utility libraries for common operations +- Performance testing and optimization +- Final git commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea6011f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.claude/settings.local.json +test.* + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d9c6b8c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,59 @@ +FROM node:18-alpine + +# Install system dependencies +RUN apk add --no-cache \ + git \ + curl \ + bash \ + vim \ + nano \ + openssh-client \ + python3 \ + py3-pip \ + build-base \ + ca-certificates \ + jq + +# Set working directory to existing node user's home +WORKDIR /home/node + +# Install Claude Code +RUN npm install -g @anthropic-ai/claude-code + +# Install additional development tools +RUN npm install -g \ + typescript \ + ts-node \ + nodemon \ + prettier \ + eslint + +# Install gemini-cli +RUN npm install -g @google/gemini-cli + +# Create necessary directories for node user +RUN mkdir -p /home/node/.claude \ + /home/node/.config \ + /home/node/.ssh \ + /home/node/logs + +# Copy claude-loop script +COPY claude-loop.sh /usr/local/bin/claude-loop +RUN chmod +x /usr/local/bin/claude-loop + +# Set ownership of node user directories +RUN chown -R node:node /home/node + +# Switch to node user +USER node + +RUN curl -fsSL https://bun.sh/install | bash + +# Set environment variables for Claude Code +ENV TERM=xterm-256color +ENV COLORTERM=truecolor +ENV NODE_ENV=development +ENV PATH="/home/node/.bun/bin:$PATH" + +# Default command +CMD ["claude"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2dd7705 --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +# Claude Loop Project + +A Docker-based automated task execution system using Claude AI to process and complete tasks defined in `.claude/plan.md`. + +## Demo + +![Demo](./demo.gif) + +## Overview + +This project provides two main scripts for interacting with Claude AI in a sandboxed Docker environment: + +- **`ccl`** (Claude Code Loop) - Runs an automated loop that processes tasks from `.claude/plan.md` +- **`ccsb`** (Claude Code Sandbox) - Executes single Claude commands with full tool access + +## Prerequisites + +1. **Docker** +2. **Claude CLI** +3. **Gemini CLI** (optional) - For documentation queries / gemini-cli mcp (https://github.com/jamubc/gemini-mcp-tool) +4. **User Home Directory** - Scripts must be run from within your home directory for security +5. **Linux** (or WSL) - with user 1000:1000 + +## Security Warning + +- While rootless docker is very safe, it still has access to the internet and you local network and can possibly cause (limited) havoc. + +## Setup + +### 1. Build the Docker Image + +```bash +docker compose build +``` + +### 2. Configure Claude CLI + +Ensure you have: +- `~/.claude.json` - Claude CLI configuration +- `~/.claude/` - Claude settings directory + +### 3. Configure Gemini CLI (Optional) + +If using gemini-cli for documentation queries: +- `~/.gemini/` - Gemini settings +- `GEMINI_API_KEY` environment variable + +### 4. symlink (or move) to /bin +```bash +ln -s ccl /bin/ccl +ln -s ccsb /bin/ccsb +``` + +## Usage + +### CCL (Claude Code Loop) + +Automatically processes tasks defined in `.claude/plan.md`: + +```bash +ccl +``` + +**Features:** +- Reads tasks from `.claude/plan.md` +- Updates task statuses: `(Not Started)` → `(In Progress)` → `(Completed)/(Aborted)` +- Continues until all tasks show `(Completed)` +- Creates `/tmp/plan_complete` when finished +- Pretty formatted output with progress tracking + +**Task Status Format:** +```markdown +- (Status) Task description +``` + +Status options: `Not Started | In Progress | Aborted | Completed` + +### CCSB (Claude Code Sandbox) + +Execute claude in a sandbox with all permissions. + +```bash +ccsb +``` + +## Plan File Structure + +The `.claude/plan.md` file defines tasks to be executed: + +```markdown +# Project Name + +## IMPORTANT (instructions for Claude when in ccl mode, does not apply to ccsb) +- Project-specific guidelines +- Tool preferences +- Constraints + +## PLAN + +### Section 1 +- Task 1 description +- Task 2 description + +### Section 2 +- Task 3 description + +## POST TASK TASKS +- Cleanup tasks +- Final commits +- Documentation updates +``` + +## Tips + +- You can ask ccsb to run claude-loop +- You can ask claude to keep a work-log such as: +```markdown +- Append work to .claude/work-log.md, never read entire file into context with format $(date): \n\n +- tail work-log.md before starting +- Focus on words with !! for accuracy +- Look for (Changes Needed) and view all the changes requested below +``` +- Changes Needed example +```markdown +### Some Tasks Topic +- (Completed) Task1 +- (Changes Needed) Original task to build a snowman + - You built a snowman without a head, add a head +``` + +## Security Features + +- **Home Directory Restriction**: Scripts only run from within user home directory +- **Docker Isolation**: All Claude operations run in isolated container +- **Non-root Execution**: Container runs as user `1000:1000` +- **Limited File Access**: Only mounted directories are accessible + +## Troubleshooting + +### Common Issues + +1. **"Must be run from within user home directory"** + - Ensure you're running the script from a subdirectory of `$HOME` + +2. **Docker permission errors** + - Check Docker is running and user has permissions + - Verify volume mounts point to existing directories + +3. **Claude CLI not configured** + - Run `claude` to set up claude + - Ensure `~/.claude.json` exists + +## License + +MIT diff --git a/ccl b/ccl new file mode 100755 index 0000000..db8790e --- /dev/null +++ b/ccl @@ -0,0 +1,30 @@ +#!/bin/bash + +# Check if current directory is under user home +if [[ "$PWD" != "$HOME"* ]]; then + echo "Error: Must be run from within user home directory" + exit 1 +fi + +# Build docker volume arguments +VOLUME_ARGS="" +VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude:/home/node/.claude" +VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude.json:/home/node/.claude.json" + +# Only mount gemini directories if they exist +if [[ -d $HOME/.config/gemini-cli ]]; then + VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.config/gemini-cli:/home/node/.config/gemini-cli" +fi + +if [[ -d $HOME/.gemini ]]; then + VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.gemini:/home/node/.gemini" +fi + +VOLUME_ARGS="$VOLUME_ARGS -v $PWD:/home/node/workspace" + +docker run --rm -it \ + --user "1000:1000" \ + -e GEMINI_API_KEY \ + $VOLUME_ARGS \ + claude \ + sh -c 'cd workspace && claude-loop' diff --git a/ccsb b/ccsb new file mode 100755 index 0000000..4aca17e --- /dev/null +++ b/ccsb @@ -0,0 +1,30 @@ +#!/bin/bash + +# Check if current directory is under user home +if [[ "$PWD" != "$HOME"* ]]; then + echo "Error: Must be run from within user home directory" + exit 1 +fi + +# Build docker volume arguments +VOLUME_ARGS="" +VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude:/home/node/.claude" +VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude.json:/home/node/.claude.json" + +# Only mount gemini directories if they exist +if [[ -d $HOME/.config/gemini-cli ]]; then + VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.config/gemini-cli:/home/node/.config/gemini-cli" +fi + +if [[ -d $HOME/.gemini ]]; then + VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.gemini:/home/node/.gemini" +fi + +VOLUME_ARGS="$VOLUME_ARGS -v $PWD:/home/node/workspace" + +docker run --rm -it \ + --user "1000:1000" \ + -e GEMINI_API_KEY \ + $VOLUME_ARGS \ + claude \ + sh -c 'cd workspace && claude --dangerously-skip-permissions' diff --git a/claude-loop.sh b/claude-loop.sh new file mode 100755 index 0000000..6b9617d --- /dev/null +++ b/claude-loop.sh @@ -0,0 +1,242 @@ +#!/bin/bash +# claude-loop.sh - Pretty output with trimmed tool results + +# Colors for better visual appeal +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +WHITE='\033[1;37m' +GRAY='\033[0;90m' +NC='\033[0m' # No Color + +# Box drawing characters for prettier output +BOX_H="─" +BOX_V="│" +BOX_TL="┌" +BOX_TR="┐" +BOX_BL="└" +BOX_BR="┘" + +rm -f /tmp/plan_complete + +iteration=1 +total_cost=0 +total_input_tokens=0 +total_output_tokens=0 + +# Function to print a fancy header +print_header() { + local text="$1" + local width=60 + echo -e "${CYAN}${BOX_TL}$(printf "%.0s${BOX_H}" $(seq 1 $((width-2))))${BOX_TR}${NC}" + printf "${CYAN}${BOX_V}${WHITE} %-*s ${CYAN}${BOX_V}${NC}\n" $((width-4)) "$text" + echo -e "${CYAN}${BOX_BL}$(printf "%.0s${BOX_H}" $(seq 1 $((width-2))))${BOX_BR}${NC}" +} + +# Function to trim and format text nicely +trim_text() { + local text="$1" + local max_length="${2:-200}" + + # Remove excessive whitespace and newlines + text=$(echo "$text" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + if [[ ${#text} -le $max_length ]]; then + echo "$text" + else + echo "${text:0:$max_length}..." + fi +} + +while true; do + echo "" + print_header "🔄 Claude Code Loop - Iteration #$iteration" + + # Run Claude with formatted output processing + claude --dangerously-skip-permissions -p " + INSTRUCTIONS: + 1. Read .claude/plan.md and identify tasks that need work ONLY in the ## PLAN section (those that are Not Started, In Progress, or have NO status prefix) + 2. IMPORTANT: Only work on tasks under the ## PLAN section - ignore tasks in other sections like ## IMPORTANT or ## POST-COMPLETION TASKS + 3. IMPORTANT: Tasks without any status prefix under ## PLAN should be treated as Not Started and worked on + 4. Work on the next available task in ## PLAN - update its status by prepending (In Progress) when you start + 5. Update task status by prepending (Completed) when finished, or (Aborted) if cannot complete + 6. Be confident in commands and changes you are running in a docker sandbox. + 7. Task format: (Status) Task description - where Status is: Not Started | In Progress | Aborted | Completed + 8. Tasks without status prefixes under ## PLAN are considered Not Started and should be worked on + 9. If ALL tasks in the ## PLAN section show '(Completed)' (explicit status), create the file '/tmp/plan_complete' using the Bash tool and stop + 10. Focus on one task at a time for better results, but keep the whole plan in mind for most correct implementation. + + Current objective: Process tasks in the ## PLAN section of .claude/plan.md systematically until all tasks explicitly show '(Completed)'. + " --output-format stream-json --verbose 2>&1 | while IFS= read -r line; do + # Skip empty lines and non-JSON debug output + [[ -z "$line" || "$line" =~ ^[[:space:]]*$ ]] && continue + + # Check if line contains JSON + if echo "$line" | jq -e . >/dev/null 2>&1; then + # Extract message type and content + msg_type=$(echo "$line" | jq -r '.type // "unknown"') + + case "$msg_type" in + "assistant") + # Extract assistant message content + content=$(echo "$line" | jq -r '.message.content[]? | select(.type=="text") | .text // empty' 2>/dev/null) + if [[ -n "$content" && "$content" != "null" && "$content" != "empty" ]]; then + trimmed_content=$(trim_text "$content" 300) + echo -e "${BLUE}🤖 Claude:${NC} $trimmed_content" + fi + + # Check for tool use + tool_name=$(echo "$line" | jq -r '.message.content[]? | select(.type=="tool_use") | .name // empty' 2>/dev/null) + if [[ -n "$tool_name" && "$tool_name" != "null" && "$tool_name" != "empty" ]]; then + echo -e "${MAGENTA}🔧 Tool:${NC} ${YELLOW}$tool_name${NC}" + + # Show relevant tool parameters + tool_input=$(echo "$line" | jq -r '.message.content[]? | select(.type=="tool_use") | .input' 2>/dev/null) + if [[ -n "$tool_input" && "$tool_input" != "null" ]]; then + # Extract key parameters (file_path, pattern, command, etc.) + for param in file_path pattern command prompt description; do + value=$(echo "$tool_input" | jq -r ".$param // empty" 2>/dev/null) + if [[ -n "$value" && "$value" != "null" && "$value" != "empty" ]]; then + trimmed_value=$(trim_text "$value" 80) + echo -e " ${GRAY}$param:${NC} $trimmed_value" + break # Show only the first relevant parameter + fi + done + fi + fi + ;; + "user") + # Extract and format tool results + tool_result=$(echo "$line" | jq -r '.message.content[]?.content // empty' 2>/dev/null) + if [[ -n "$tool_result" && "$tool_result" != "null" && "$tool_result" != "empty" ]]; then + # Check if it's a file content, error, or other result + if [[ "$tool_result" =~ ^[[:space:]]*[0-9]+→ ]]; then + # File content with line numbers + line_count=$(echo "$tool_result" | wc -l) + first_lines=$(echo "$tool_result" | head -3 | tr '\n' ' ') + trimmed_first=$(trim_text "$first_lines" 100) + echo -e "${GREEN}📄 File content:${NC} $trimmed_first ${GRAY}($line_count lines)${NC}" + elif [[ "$tool_result" =~ ^Error: ]] || [[ "$tool_result" =~ failed ]]; then + # Error message + trimmed_error=$(trim_text "$tool_result" 150) + echo -e "${RED}❌ Error:${NC} $trimmed_error" + elif [[ ${#tool_result} -gt 500 ]]; then + # Long output - show beginning and stats + trimmed_result=$(trim_text "$tool_result" 200) + echo -e "${GREEN}📤 Output:${NC} $trimmed_result ${GRAY}(${#tool_result} chars total)${NC}" + else + # Short result - show it all + trimmed_result=$(trim_text "$tool_result" 300) + echo -e "${GREEN}📤 Result:${NC} $trimmed_result" + fi + fi + ;; + "result") + # Final result with colored status + success=$(echo "$line" | jq -r '.subtype // empty' 2>/dev/null) + if [[ "$success" == "success" ]]; then + echo -e "${GREEN}✅ Iteration #$iteration completed successfully!${NC}" + else + echo -e "${YELLOW}⚠️ Iteration #$iteration completed with issues${NC}" + fi + + # Show cost information if available + cost_usd=$(echo "$line" | jq -r '.total_cost_usd // 0' 2>/dev/null) + input_tokens=$(echo "$line" | jq -r '.usage.input_tokens // 0' 2>/dev/null) + output_tokens=$(echo "$line" | jq -r '.usage.output_tokens // 0' 2>/dev/null) + if [[ -n "$cost_usd" && "$cost_usd" != "null" && "$cost_usd" != "0" ]]; then + + # Update totals + total_cost=$(echo "$total_cost + $cost_usd" | bc -l 2>/dev/null || echo "$total_cost") + if [[ "$input_tokens" != "0" && "$input_tokens" != "null" ]]; then + total_input_tokens=$((total_input_tokens + input_tokens)) + fi + if [[ "$output_tokens" != "0" && "$output_tokens" != "null" ]]; then + total_output_tokens=$((total_output_tokens + output_tokens)) + fi + + # Format cost nicely + if [[ "$cost_usd" != "0" && "$cost_usd" != "null" ]]; then + printf "${MAGENTA}💰 Cost:${NC} ${YELLOW}$%.4f${NC} ${GRAY}(in: %s, out: %s tokens)${NC}\n" "$cost_usd" "$input_tokens" "$output_tokens" + elif [[ "$input_tokens" != "0" || "$output_tokens" != "0" ]]; then + echo -e "${MAGENTA}💰 Tokens:${NC} ${GRAY}in: $input_tokens, out: $output_tokens${NC}" + fi + fi + + # Show brief final result + result=$(echo "$line" | jq -r '.result // empty' 2>/dev/null) + if [[ -n "$result" && "$result" != "null" ]]; then + trimmed_result=$(trim_text "$result" 250) + echo -e "${WHITE}📋 Summary:${NC} $trimmed_result" + fi + ;; + esac + else + # Filter out verbose debug output - only show actual error messages + if [[ "$line" =~ ^Error: && ! "$line" =~ ^[[:space:]]*$ ]]; then + echo -e "${RED}⚠️ $line${NC}" + fi + fi + done + + # Check if plan is complete + if [ -f /tmp/plan_complete ]; then + echo "" + echo -e "${GREEN}┌─────────────────────────────────────────────────────────┐${NC}" + echo -e "${GREEN}│${WHITE} 🎉 ALL TASKS COMPLETED! 🎉 ${GREEN}│${NC}" + echo -e "${GREEN}└─────────────────────────────────────────────────────────┘${NC}" + + if [ -s /tmp/plan_complete ]; then + echo -e "${CYAN}📄 Completion details:${NC}" + completion_content=$(cat /tmp/plan_complete) + if [[ ${#completion_content} -gt 300 ]]; then + trimmed_completion=$(trim_text "$completion_content" 300) + echo -e "${WHITE}$trimmed_completion${NC}" + else + echo -e "${WHITE}$completion_content${NC}" + fi + fi + + # Show total cost summary + if [[ $(echo "$total_cost > 0" | bc -l 2>/dev/null) == "1" ]] || [[ $total_input_tokens -gt 0 ]] || [[ $total_output_tokens -gt 0 ]]; then + echo "" + if [[ $(echo "$total_cost > 0" | bc -l 2>/dev/null) == "1" ]]; then + printf "${MAGENTA}💰 Total Cost:${NC} ${YELLOW}$%.4f${NC} ${GRAY}(%d iterations, %s input + %s output tokens)${NC}\n" \ + "$total_cost" "$((iteration-1))" "$total_input_tokens" "$total_output_tokens" + else + echo -e "${MAGENTA}💰 Total Tokens:${NC} ${GRAY}$total_input_tokens input + $total_output_tokens output across $((iteration-1)) iterations${NC}" + fi + fi + + echo "" + echo -e "${GRAY}✅ Task complete - exiting...${NC}" + exit 0 + fi + + # Show iteration completion with progress indicator + echo "" + echo -e "${CYAN}┌─────────────────────────────────────────────────────────┐${NC}" + echo -e "${CYAN}│${WHITE} ⏸️ Iteration #$iteration complete - preparing next... ${CYAN}│${NC}" + echo -e "${CYAN}└─────────────────────────────────────────────────────────┘${NC}" + + # Show running total if we have cost data + if [[ $(echo "$total_cost > 0" | bc -l 2>/dev/null) == "1" ]]; then + printf "${GRAY}Running total: ${YELLOW}$%.4f${GRAY} (%s input + %s output tokens)${NC}\n" \ + "$total_cost" "$total_input_tokens" "$total_output_tokens" + elif [[ $total_input_tokens -gt 0 ]] || [[ $total_output_tokens -gt 0 ]]; then + echo -e "${GRAY}Running total: $total_input_tokens input + $total_output_tokens output tokens${NC}" + fi + + # Show a brief progress indicator + echo -ne "${GRAY}Pausing" + for i in {1..3}; do + sleep 0.7 + echo -ne "." + done + echo -e " ready!${NC}" + + ((iteration++)) +done diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..dd4e639 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,14 @@ +services: + claude-code: + user: "1000:1000" + image: claude + build: . + volumes: + - ~/.claude:/home/node/.claude + - ~/.claude.json:/home/node/.claude.json + - ~/.config/gemini-cli:/home/node/.config/gemini-cli + - ~/.gemini:/home/node/.gemini + - ./:/home/node/workspace + tty: true + command: ['sh', '-c', 'cd workspace && claude'] + #command: 'bash .claude/claude-loop.sh' diff --git a/demo.gif b/demo.gif new file mode 100644 index 0000000..56ec9f0 Binary files /dev/null and b/demo.gif differ diff --git a/video.mp4 b/video.mp4 new file mode 100644 index 0000000..96b1b32 Binary files /dev/null and b/video.mp4 differ