├── .gitignore ├── demo.gif ├── video.mp4 ├── compose.yaml ├── .claude └── plan.md ├── ccl ├── ccsb ├── Dockerfile ├── README.md └── claude-loop.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .claude/settings.local.json 2 | test.* 3 | 4 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeprecatedLuke/claude-loop/HEAD/demo.gif -------------------------------------------------------------------------------- /video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeprecatedLuke/claude-loop/HEAD/video.mp4 -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | claude-code: 3 | user: "1000:1000" 4 | image: claude 5 | build: . 6 | volumes: 7 | - ~/.claude:/home/node/.claude 8 | - ~/.claude.json:/home/node/.claude.json 9 | - ~/.config/gemini-cli:/home/node/.config/gemini-cli 10 | - ~/.gemini:/home/node/.gemini 11 | - ./:/home/node/workspace 12 | tty: true 13 | command: ['sh', '-c', 'cd workspace && claude'] 14 | #command: 'bash .claude/claude-loop.sh' 15 | -------------------------------------------------------------------------------- /.claude/plan.md: -------------------------------------------------------------------------------- 1 | # This is a test project 2 | 3 | ## IMPORTANT (when working on a plan versus ccsb aka CLAUDE.MD) 4 | - Never use hello world as a string in testing 5 | - Use gemini-cli when you need to ask about documentation 6 | 7 | ## PLAN 8 | 9 | ### Basic file 10 | - Create a python file called main.py 11 | 12 | ### Architecture 13 | - The file should be able to print whatever it is given into split by space returned as json array 14 | 15 | ## POST TASK TASKS 16 | - Git commit with comprehensive changes 17 | - Restructure project architecture for scalability 18 | - Merge duplicate systems and optimize code quality 19 | - Create utility libraries for common operations 20 | - Performance testing and optimization 21 | - Final git commit 22 | -------------------------------------------------------------------------------- /ccl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if current directory is under user home 4 | if [[ "$PWD" != "$HOME"* ]]; then 5 | echo "Error: Must be run from within user home directory" 6 | exit 1 7 | fi 8 | 9 | # Build docker volume arguments 10 | VOLUME_ARGS="" 11 | VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude:/home/node/.claude" 12 | VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude.json:/home/node/.claude.json" 13 | 14 | # Only mount gemini directories if they exist 15 | if [[ -d $HOME/.config/gemini-cli ]]; then 16 | VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.config/gemini-cli:/home/node/.config/gemini-cli" 17 | fi 18 | 19 | if [[ -d $HOME/.gemini ]]; then 20 | VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.gemini:/home/node/.gemini" 21 | fi 22 | 23 | VOLUME_ARGS="$VOLUME_ARGS -v $PWD:/home/node/workspace" 24 | 25 | docker run --rm -it \ 26 | --user "1000:1000" \ 27 | -e GEMINI_API_KEY \ 28 | $VOLUME_ARGS \ 29 | claude \ 30 | sh -c 'cd workspace && claude-loop' 31 | -------------------------------------------------------------------------------- /ccsb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if current directory is under user home 4 | if [[ "$PWD" != "$HOME"* ]]; then 5 | echo "Error: Must be run from within user home directory" 6 | exit 1 7 | fi 8 | 9 | # Build docker volume arguments 10 | VOLUME_ARGS="" 11 | VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude:/home/node/.claude" 12 | VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude.json:/home/node/.claude.json" 13 | 14 | # Only mount gemini directories if they exist 15 | if [[ -d $HOME/.config/gemini-cli ]]; then 16 | VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.config/gemini-cli:/home/node/.config/gemini-cli" 17 | fi 18 | 19 | if [[ -d $HOME/.gemini ]]; then 20 | VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.gemini:/home/node/.gemini" 21 | fi 22 | 23 | VOLUME_ARGS="$VOLUME_ARGS -v $PWD:/home/node/workspace" 24 | 25 | docker run --rm -it \ 26 | --user "1000:1000" \ 27 | -e GEMINI_API_KEY \ 28 | $VOLUME_ARGS \ 29 | claude \ 30 | sh -c 'cd workspace && claude --dangerously-skip-permissions' 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | # Install system dependencies 4 | RUN apk add --no-cache \ 5 | git \ 6 | curl \ 7 | bash \ 8 | vim \ 9 | nano \ 10 | openssh-client \ 11 | python3 \ 12 | py3-pip \ 13 | build-base \ 14 | ca-certificates \ 15 | jq 16 | 17 | # Set working directory to existing node user's home 18 | WORKDIR /home/node 19 | 20 | # Install Claude Code 21 | RUN npm install -g @anthropic-ai/claude-code 22 | 23 | # Install additional development tools 24 | RUN npm install -g \ 25 | typescript \ 26 | ts-node \ 27 | nodemon \ 28 | prettier \ 29 | eslint 30 | 31 | # Install gemini-cli 32 | RUN npm install -g @google/gemini-cli 33 | 34 | # Create necessary directories for node user 35 | RUN mkdir -p /home/node/.claude \ 36 | /home/node/.config \ 37 | /home/node/.ssh \ 38 | /home/node/logs 39 | 40 | # Copy claude-loop script 41 | COPY claude-loop.sh /usr/local/bin/claude-loop 42 | RUN chmod +x /usr/local/bin/claude-loop 43 | 44 | # Set ownership of node user directories 45 | RUN chown -R node:node /home/node 46 | 47 | # Switch to node user 48 | USER node 49 | 50 | RUN curl -fsSL https://bun.sh/install | bash 51 | 52 | # Set environment variables for Claude Code 53 | ENV TERM=xterm-256color 54 | ENV COLORTERM=truecolor 55 | ENV NODE_ENV=development 56 | ENV PATH="/home/node/.bun/bin:$PATH" 57 | 58 | # Default command 59 | CMD ["claude"] 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Claude Loop Project 2 | 3 | A Docker-based automated task execution system using Claude AI to process and complete tasks defined in `.claude/plan.md`. 4 | 5 | ## Demo 6 | 7 | ![Demo](./demo.gif) 8 | 9 | ## Overview 10 | 11 | This project provides two main scripts for interacting with Claude AI in a sandboxed Docker environment: 12 | 13 | - **`ccl`** (Claude Code Loop) - Runs an automated loop that processes tasks from `.claude/plan.md` 14 | - **`ccsb`** (Claude Code Sandbox) - Executes single Claude commands with full tool access 15 | 16 | ## Prerequisites 17 | 18 | 1. **Docker** 19 | 2. **Claude CLI** 20 | 3. **Gemini CLI** (optional) - For documentation queries / gemini-cli mcp (https://github.com/jamubc/gemini-mcp-tool) 21 | 4. **User Home Directory** - Scripts must be run from within your home directory for security 22 | 5. **Linux** (or WSL) - with user 1000:1000 23 | 24 | ## Security Warning 25 | 26 | - While rootless docker is very safe, it still has access to the internet and you local network and can possibly cause (limited) havoc. 27 | 28 | ## Setup 29 | 30 | ### 1. Build the Docker Image 31 | 32 | ```bash 33 | docker compose build 34 | ``` 35 | 36 | ### 2. Configure Claude CLI 37 | 38 | Ensure you have: 39 | - `~/.claude.json` - Claude CLI configuration 40 | - `~/.claude/` - Claude settings directory 41 | 42 | ### 3. Configure Gemini CLI (Optional) 43 | 44 | If using gemini-cli for documentation queries: 45 | - `~/.gemini/` - Gemini settings 46 | - `GEMINI_API_KEY` environment variable 47 | 48 | ### 4. symlink (or move) to /bin 49 | ```bash 50 | ln -s ccl /bin/ccl 51 | ln -s ccsb /bin/ccsb 52 | ``` 53 | 54 | ## Usage 55 | 56 | ### CCL (Claude Code Loop) 57 | 58 | Automatically processes tasks defined in `.claude/plan.md`: 59 | 60 | ```bash 61 | ccl 62 | ``` 63 | 64 | **Features:** 65 | - Reads tasks from `.claude/plan.md` 66 | - Updates task statuses: `(Not Started)` → `(In Progress)` → `(Completed)/(Aborted)` 67 | - Continues until all tasks show `(Completed)` 68 | - Creates `/tmp/plan_complete` when finished 69 | - Pretty formatted output with progress tracking 70 | 71 | **Task Status Format:** 72 | ```markdown 73 | - (Status) Task description 74 | ``` 75 | 76 | Status options: `Not Started | In Progress | Aborted | Completed` 77 | 78 | ### CCSB (Claude Code Sandbox) 79 | 80 | Execute claude in a sandbox with all permissions. 81 | 82 | ```bash 83 | ccsb 84 | ``` 85 | 86 | ## Plan File Structure 87 | 88 | The `.claude/plan.md` file defines tasks to be executed: 89 | 90 | ```markdown 91 | # Project Name 92 | 93 | ## IMPORTANT (instructions for Claude when in ccl mode, does not apply to ccsb) 94 | - Project-specific guidelines 95 | - Tool preferences 96 | - Constraints 97 | 98 | ## PLAN 99 | 100 | ### Section 1 101 | - Task 1 description 102 | - Task 2 description 103 | 104 | ### Section 2 105 | - Task 3 description 106 | 107 | ## POST TASK TASKS 108 | - Cleanup tasks 109 | - Final commits 110 | - Documentation updates 111 | ``` 112 | 113 | ## Tips 114 | 115 | - You can ask ccsb to run claude-loop 116 | - You can ask claude to keep a work-log such as: 117 | ```markdown 118 | - Append work to .claude/work-log.md, never read entire file into context with format $(date): \n\n 119 | - tail work-log.md before starting 120 | - Focus on words with !! for accuracy 121 | - Look for (Changes Needed) and view all the changes requested below 122 | ``` 123 | - Changes Needed example 124 | ```markdown 125 | ### Some Tasks Topic 126 | - (Completed) Task1 127 | - (Changes Needed) Original task to build a snowman 128 | - You built a snowman without a head, add a head 129 | ``` 130 | 131 | ## Security Features 132 | 133 | - **Home Directory Restriction**: Scripts only run from within user home directory 134 | - **Docker Isolation**: All Claude operations run in isolated container 135 | - **Non-root Execution**: Container runs as user `1000:1000` 136 | - **Limited File Access**: Only mounted directories are accessible 137 | 138 | ## Troubleshooting 139 | 140 | ### Common Issues 141 | 142 | 1. **"Must be run from within user home directory"** 143 | - Ensure you're running the script from a subdirectory of `$HOME` 144 | 145 | 2. **Docker permission errors** 146 | - Check Docker is running and user has permissions 147 | - Verify volume mounts point to existing directories 148 | 149 | 3. **Claude CLI not configured** 150 | - Run `claude` to set up claude 151 | - Ensure `~/.claude.json` exists 152 | 153 | ## License 154 | 155 | MIT 156 | -------------------------------------------------------------------------------- /claude-loop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # claude-loop.sh - Pretty output with trimmed tool results 3 | 4 | # Colors for better visual appeal 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | YELLOW='\033[1;33m' 8 | BLUE='\033[0;34m' 9 | MAGENTA='\033[0;35m' 10 | CYAN='\033[0;36m' 11 | WHITE='\033[1;37m' 12 | GRAY='\033[0;90m' 13 | NC='\033[0m' # No Color 14 | 15 | # Box drawing characters for prettier output 16 | BOX_H="─" 17 | BOX_V="│" 18 | BOX_TL="┌" 19 | BOX_TR="┐" 20 | BOX_BL="└" 21 | BOX_BR="┘" 22 | 23 | rm -f /tmp/plan_complete 24 | 25 | iteration=1 26 | total_cost=0 27 | total_input_tokens=0 28 | total_output_tokens=0 29 | 30 | # Function to print a fancy header 31 | print_header() { 32 | local text="$1" 33 | local width=60 34 | echo -e "${CYAN}${BOX_TL}$(printf "%.0s${BOX_H}" $(seq 1 $((width-2))))${BOX_TR}${NC}" 35 | printf "${CYAN}${BOX_V}${WHITE} %-*s ${CYAN}${BOX_V}${NC}\n" $((width-4)) "$text" 36 | echo -e "${CYAN}${BOX_BL}$(printf "%.0s${BOX_H}" $(seq 1 $((width-2))))${BOX_BR}${NC}" 37 | } 38 | 39 | # Function to trim and format text nicely 40 | trim_text() { 41 | local text="$1" 42 | local max_length="${2:-200}" 43 | 44 | # Remove excessive whitespace and newlines 45 | text=$(echo "$text" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') 46 | 47 | if [[ ${#text} -le $max_length ]]; then 48 | echo "$text" 49 | else 50 | echo "${text:0:$max_length}..." 51 | fi 52 | } 53 | 54 | while true; do 55 | echo "" 56 | print_header "🔄 Claude Code Loop - Iteration #$iteration" 57 | 58 | # Run Claude with formatted output processing 59 | claude --dangerously-skip-permissions -p " 60 | INSTRUCTIONS: 61 | 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) 62 | 2. IMPORTANT: Only work on tasks under the ## PLAN section - ignore tasks in other sections like ## IMPORTANT or ## POST-COMPLETION TASKS 63 | 3. IMPORTANT: Tasks without any status prefix under ## PLAN should be treated as Not Started and worked on 64 | 4. Work on the next available task in ## PLAN - update its status by prepending (In Progress) when you start 65 | 5. Update task status by prepending (Completed) when finished, or (Aborted) if cannot complete 66 | 6. Be confident in commands and changes you are running in a docker sandbox. 67 | 7. Task format: (Status) Task description - where Status is: Not Started | In Progress | Aborted | Completed 68 | 8. Tasks without status prefixes under ## PLAN are considered Not Started and should be worked on 69 | 9. If ALL tasks in the ## PLAN section show '(Completed)' (explicit status), create the file '/tmp/plan_complete' using the Bash tool and stop 70 | 10. Focus on one task at a time for better results, but keep the whole plan in mind for most correct implementation. 71 | 72 | Current objective: Process tasks in the ## PLAN section of .claude/plan.md systematically until all tasks explicitly show '(Completed)'. 73 | " --output-format stream-json --verbose 2>&1 | while IFS= read -r line; do 74 | # Skip empty lines and non-JSON debug output 75 | [[ -z "$line" || "$line" =~ ^[[:space:]]*$ ]] && continue 76 | 77 | # Check if line contains JSON 78 | if echo "$line" | jq -e . >/dev/null 2>&1; then 79 | # Extract message type and content 80 | msg_type=$(echo "$line" | jq -r '.type // "unknown"') 81 | 82 | case "$msg_type" in 83 | "assistant") 84 | # Extract assistant message content 85 | content=$(echo "$line" | jq -r '.message.content[]? | select(.type=="text") | .text // empty' 2>/dev/null) 86 | if [[ -n "$content" && "$content" != "null" && "$content" != "empty" ]]; then 87 | trimmed_content=$(trim_text "$content" 300) 88 | echo -e "${BLUE}🤖 Claude:${NC} $trimmed_content" 89 | fi 90 | 91 | # Check for tool use 92 | tool_name=$(echo "$line" | jq -r '.message.content[]? | select(.type=="tool_use") | .name // empty' 2>/dev/null) 93 | if [[ -n "$tool_name" && "$tool_name" != "null" && "$tool_name" != "empty" ]]; then 94 | echo -e "${MAGENTA}🔧 Tool:${NC} ${YELLOW}$tool_name${NC}" 95 | 96 | # Show relevant tool parameters 97 | tool_input=$(echo "$line" | jq -r '.message.content[]? | select(.type=="tool_use") | .input' 2>/dev/null) 98 | if [[ -n "$tool_input" && "$tool_input" != "null" ]]; then 99 | # Extract key parameters (file_path, pattern, command, etc.) 100 | for param in file_path pattern command prompt description; do 101 | value=$(echo "$tool_input" | jq -r ".$param // empty" 2>/dev/null) 102 | if [[ -n "$value" && "$value" != "null" && "$value" != "empty" ]]; then 103 | trimmed_value=$(trim_text "$value" 80) 104 | echo -e " ${GRAY}$param:${NC} $trimmed_value" 105 | break # Show only the first relevant parameter 106 | fi 107 | done 108 | fi 109 | fi 110 | ;; 111 | "user") 112 | # Extract and format tool results 113 | tool_result=$(echo "$line" | jq -r '.message.content[]?.content // empty' 2>/dev/null) 114 | if [[ -n "$tool_result" && "$tool_result" != "null" && "$tool_result" != "empty" ]]; then 115 | # Check if it's a file content, error, or other result 116 | if [[ "$tool_result" =~ ^[[:space:]]*[0-9]+→ ]]; then 117 | # File content with line numbers 118 | line_count=$(echo "$tool_result" | wc -l) 119 | first_lines=$(echo "$tool_result" | head -3 | tr '\n' ' ') 120 | trimmed_first=$(trim_text "$first_lines" 100) 121 | echo -e "${GREEN}📄 File content:${NC} $trimmed_first ${GRAY}($line_count lines)${NC}" 122 | elif [[ "$tool_result" =~ ^Error: ]] || [[ "$tool_result" =~ failed ]]; then 123 | # Error message 124 | trimmed_error=$(trim_text "$tool_result" 150) 125 | echo -e "${RED}❌ Error:${NC} $trimmed_error" 126 | elif [[ ${#tool_result} -gt 500 ]]; then 127 | # Long output - show beginning and stats 128 | trimmed_result=$(trim_text "$tool_result" 200) 129 | echo -e "${GREEN}📤 Output:${NC} $trimmed_result ${GRAY}(${#tool_result} chars total)${NC}" 130 | else 131 | # Short result - show it all 132 | trimmed_result=$(trim_text "$tool_result" 300) 133 | echo -e "${GREEN}📤 Result:${NC} $trimmed_result" 134 | fi 135 | fi 136 | ;; 137 | "result") 138 | # Final result with colored status 139 | success=$(echo "$line" | jq -r '.subtype // empty' 2>/dev/null) 140 | if [[ "$success" == "success" ]]; then 141 | echo -e "${GREEN}✅ Iteration #$iteration completed successfully!${NC}" 142 | else 143 | echo -e "${YELLOW}⚠️ Iteration #$iteration completed with issues${NC}" 144 | fi 145 | 146 | # Show cost information if available 147 | cost_usd=$(echo "$line" | jq -r '.total_cost_usd // 0' 2>/dev/null) 148 | input_tokens=$(echo "$line" | jq -r '.usage.input_tokens // 0' 2>/dev/null) 149 | output_tokens=$(echo "$line" | jq -r '.usage.output_tokens // 0' 2>/dev/null) 150 | if [[ -n "$cost_usd" && "$cost_usd" != "null" && "$cost_usd" != "0" ]]; then 151 | 152 | # Update totals 153 | total_cost=$(echo "$total_cost + $cost_usd" | bc -l 2>/dev/null || echo "$total_cost") 154 | if [[ "$input_tokens" != "0" && "$input_tokens" != "null" ]]; then 155 | total_input_tokens=$((total_input_tokens + input_tokens)) 156 | fi 157 | if [[ "$output_tokens" != "0" && "$output_tokens" != "null" ]]; then 158 | total_output_tokens=$((total_output_tokens + output_tokens)) 159 | fi 160 | 161 | # Format cost nicely 162 | if [[ "$cost_usd" != "0" && "$cost_usd" != "null" ]]; then 163 | printf "${MAGENTA}💰 Cost:${NC} ${YELLOW}$%.4f${NC} ${GRAY}(in: %s, out: %s tokens)${NC}\n" "$cost_usd" "$input_tokens" "$output_tokens" 164 | elif [[ "$input_tokens" != "0" || "$output_tokens" != "0" ]]; then 165 | echo -e "${MAGENTA}💰 Tokens:${NC} ${GRAY}in: $input_tokens, out: $output_tokens${NC}" 166 | fi 167 | fi 168 | 169 | # Show brief final result 170 | result=$(echo "$line" | jq -r '.result // empty' 2>/dev/null) 171 | if [[ -n "$result" && "$result" != "null" ]]; then 172 | trimmed_result=$(trim_text "$result" 250) 173 | echo -e "${WHITE}📋 Summary:${NC} $trimmed_result" 174 | fi 175 | ;; 176 | esac 177 | else 178 | # Filter out verbose debug output - only show actual error messages 179 | if [[ "$line" =~ ^Error: && ! "$line" =~ ^[[:space:]]*$ ]]; then 180 | echo -e "${RED}⚠️ $line${NC}" 181 | fi 182 | fi 183 | done 184 | 185 | # Check if plan is complete 186 | if [ -f /tmp/plan_complete ]; then 187 | echo "" 188 | echo -e "${GREEN}┌─────────────────────────────────────────────────────────┐${NC}" 189 | echo -e "${GREEN}│${WHITE} 🎉 ALL TASKS COMPLETED! 🎉 ${GREEN}│${NC}" 190 | echo -e "${GREEN}└─────────────────────────────────────────────────────────┘${NC}" 191 | 192 | if [ -s /tmp/plan_complete ]; then 193 | echo -e "${CYAN}📄 Completion details:${NC}" 194 | completion_content=$(cat /tmp/plan_complete) 195 | if [[ ${#completion_content} -gt 300 ]]; then 196 | trimmed_completion=$(trim_text "$completion_content" 300) 197 | echo -e "${WHITE}$trimmed_completion${NC}" 198 | else 199 | echo -e "${WHITE}$completion_content${NC}" 200 | fi 201 | fi 202 | 203 | # Show total cost summary 204 | if [[ $(echo "$total_cost > 0" | bc -l 2>/dev/null) == "1" ]] || [[ $total_input_tokens -gt 0 ]] || [[ $total_output_tokens -gt 0 ]]; then 205 | echo "" 206 | if [[ $(echo "$total_cost > 0" | bc -l 2>/dev/null) == "1" ]]; then 207 | printf "${MAGENTA}💰 Total Cost:${NC} ${YELLOW}$%.4f${NC} ${GRAY}(%d iterations, %s input + %s output tokens)${NC}\n" \ 208 | "$total_cost" "$((iteration-1))" "$total_input_tokens" "$total_output_tokens" 209 | else 210 | echo -e "${MAGENTA}💰 Total Tokens:${NC} ${GRAY}$total_input_tokens input + $total_output_tokens output across $((iteration-1)) iterations${NC}" 211 | fi 212 | fi 213 | 214 | echo "" 215 | echo -e "${GRAY}✅ Task complete - exiting...${NC}" 216 | exit 0 217 | fi 218 | 219 | # Show iteration completion with progress indicator 220 | echo "" 221 | echo -e "${CYAN}┌─────────────────────────────────────────────────────────┐${NC}" 222 | echo -e "${CYAN}│${WHITE} ⏸️ Iteration #$iteration complete - preparing next... ${CYAN}│${NC}" 223 | echo -e "${CYAN}└─────────────────────────────────────────────────────────┘${NC}" 224 | 225 | # Show running total if we have cost data 226 | if [[ $(echo "$total_cost > 0" | bc -l 2>/dev/null) == "1" ]]; then 227 | printf "${GRAY}Running total: ${YELLOW}$%.4f${GRAY} (%s input + %s output tokens)${NC}\n" \ 228 | "$total_cost" "$total_input_tokens" "$total_output_tokens" 229 | elif [[ $total_input_tokens -gt 0 ]] || [[ $total_output_tokens -gt 0 ]]; then 230 | echo -e "${GRAY}Running total: $total_input_tokens input + $total_output_tokens output tokens${NC}" 231 | fi 232 | 233 | # Show a brief progress indicator 234 | echo -ne "${GRAY}Pausing" 235 | for i in {1..3}; do 236 | sleep 0.7 237 | echo -ne "." 238 | done 239 | echo -e " ready!${NC}" 240 | 241 | ((iteration++)) 242 | done 243 | --------------------------------------------------------------------------------