diff --git a/CLAUDE.md b/CLAUDE.md index d71e6dee..531588a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -381,17 +381,6 @@ npx agent-browser close | POST | `/api/cases` | Create new case | | GET | `/api/screens` | List screen sessions with stats | -## CLI Commands (when using `claudeman` globally) - -```bash -claudeman web [-p PORT] # Start web interface -claudeman start [--dir PATH] # Start Claude session -claudeman list # List sessions -claudeman task add "PROMPT" # Add task to queue -claudeman ralph start [--min-hours N] # Start autonomous loop -claudeman status # Overall status -``` - ## Keyboard Shortcuts | Shortcut | Action | @@ -405,12 +394,6 @@ claudeman status # Overall status | `Ctrl+?` | Show keyboard shortcuts help | | `Escape` | Close panels and modals | -## UI Behaviors - -- **Auto-focus**: When only one session exists and none is active, it auto-selects -- **Scroll preservation**: Expanding/collapsing Ralph panel preserves terminal scroll position -- **Consistent icons**: Ralph and Monitor panels use same detach/attach icons (⧉/⊞) - ## State Files | File | Purpose | @@ -421,6 +404,36 @@ claudeman status # Overall status Cases created in `~/claudeman-cases/` by default. +## Screen Session Manager (CLI Tool) + +Interactive bash tool for managing claudeman screen sessions directly from the terminal. + +```bash +./scripts/screen-manager.sh # Interactive mode +./scripts/screen-manager.sh list # List all sessions +./scripts/screen-manager.sh attach 1 # Attach to session #1 +./scripts/screen-manager.sh kill 2,3 # Kill sessions 2 and 3 +./scripts/screen-manager.sh kill-all # Kill all sessions +./scripts/screen-manager.sh info 1 # Show session #1 details +``` + +**Interactive Controls:** + +| Key | Action | +|-----|--------| +| `↑`/`↓` or `j`/`k` | Navigate sessions | +| `Enter` | Attach to selected session | +| `d` | Delete selected session | +| `D` | Delete ALL sessions | +| `i` | Show session info | +| `q`/`Esc` | Quit | + +**Features:** +- Reads from `~/.claudeman/screens.json` (claudeman's authoritative source) +- Shows session name, running time, alive/dead status, mode +- Flicker-free navigation (only updates changed rows) +- Requires `jq` and `screen` to be installed + ## Documentation Extended documentation is available in the `docs/` directory: @@ -432,8 +445,6 @@ Extended documentation is available in the `docs/` directory: ### Quick Reference: Ralph Wiggum Loops -Ralph Wiggum is an autonomous loop technique that lets Claude work iteratively until completion criteria are met. - **Core Pattern**: `PHRASE` - The completion signal that tells the loop to stop. **Skill Commands**: @@ -443,31 +454,6 @@ Ralph Wiggum is an autonomous loop technique that lets Claude work iteratively u /ralph-loop:help # Show help and usage ``` -**Best Practices** (see full guide for details): -1. **Always set `--max-iterations`** - Safety limit to prevent runaway costs -2. **Define clear success criteria** - Tests pass, lint clean, specific outputs -3. **Use test-driven verification** - Built-in feedback loop -4. **Include escape hatches** - "If stuck after N iterations, document and stop" -5. **Commit frequently** - Recovery points in git history - **Claudeman Implementation**: The `InnerLoopTracker` class (`src/inner-loop-tracker.ts`) detects Ralph patterns in Claude output and tracks loop state, todos, and completion phrases. It auto-enables when Ralph-related patterns are detected. -**API**: -- `GET /api/sessions/:id/inner-state` - Loop state and todos -- `POST /api/sessions/:id/inner-config` - Configure tracker - -**SSE Events**: -- `session:innerLoopUpdate` - Loop state changes -- `session:innerTodoUpdate` - Todo list updates -- `session:innerCompletionDetected` - Completion phrase detected - -### External References - -**Official Anthropic Documentation**: -- [Claude Code Hooks](https://code.claude.com/docs/en/hooks) -- [Claude Code Best Practices](https://www.anthropic.com/engineering/claude-code-best-practices) -- [Ralph Wiggum Plugin](https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum) - -**Community Resources**: -- [Awesome Claude - Ralph Wiggum](https://awesomeclaude.ai/ralph-wiggum) -- [Claude Fast - Autonomous Loops](https://claudefa.st/blog/guide/mechanics/autonomous-agent-loops) +See [`docs/ralph-wiggum-guide.md`](docs/ralph-wiggum-guide.md) for full documentation on best practices, prompt templates, and troubleshooting. diff --git a/README.md b/README.md index be96c65f..9eca0e9f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Claudeman transforms Claude Code into an autonomous development powerhouse. Spaw - [Token Management](#token-management) - [Web Interface](#web-interface) - [CLI Commands](#cli-commands) +- [Screen Manager TUI](#screen-manager-interactive-tui) - [API Reference](#api-reference) - [Long-Running Sessions](#long-running-sessions) - [Troubleshooting](#troubleshooting) @@ -44,6 +45,7 @@ Claudeman transforms Claude Code into an autonomous development powerhouse. Spaw | **Real-time Monitoring** | Track tokens, costs, memory, background tasks | | **Session Persistence** | Screen sessions survive server restarts | | **Inner Loop Tracking** | Detect Ralph loops running inside Claude Code | +| **Screen Manager TUI** | Interactive terminal tool for managing screen sessions | --- @@ -423,6 +425,28 @@ claudeman status claudeman reset --force ``` +### Screen Manager (Interactive TUI) + +```bash +./scripts/screen-manager.sh # Interactive mode with arrow navigation +./scripts/screen-manager.sh list # List all sessions +./scripts/screen-manager.sh attach 1 # Attach to session #1 +./scripts/screen-manager.sh kill 2,3 # Kill sessions 2 and 3 +./scripts/screen-manager.sh kill 1-5 # Kill sessions 1 through 5 +./scripts/screen-manager.sh kill-all # Kill all sessions +./scripts/screen-manager.sh info 1 # Show session #1 details +``` + +**Interactive Controls:** +- `↑`/`↓` or `j`/`k` - Navigate sessions +- `Enter` - Attach to selected session (Ctrl+A D to detach) +- `d` - Delete selected session +- `D` - Delete ALL sessions +- `i` - Show session info +- `q`/`Esc` - Quit + +Requires `jq` and `screen` packages. + --- ## API Reference @@ -540,6 +564,16 @@ curl -X POST localhost:3000/api/sessions/:id/auto-clear \ ### Screen Issues +Use the interactive screen manager for easy session management: + +```bash +./scripts/screen-manager.sh # Interactive TUI +./scripts/screen-manager.sh list # List all sessions +./scripts/screen-manager.sh kill-all # Kill all sessions +``` + +Or use raw commands: + ```bash screen -ls | grep claudeman # List screens screen -X -S claudeman- quit # Kill specific diff --git a/scripts/screen-manager.sh b/scripts/screen-manager.sh new file mode 100755 index 00000000..abc67bbd --- /dev/null +++ b/scripts/screen-manager.sh @@ -0,0 +1,494 @@ +#!/bin/bash +# +# Claudeman Screen Session Manager +# Interactive tool with arrow key navigation +# Reads from ~/.claudeman/screens.json +# + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' +REVERSE='\033[7m' + +# Use the same path as claudeman (src/screen-manager.ts) +SCREENS_FILE="${HOME}/.claudeman/screens.json" + +# Cached data +CACHED_JSON="" +CACHED_COUNT=0 +LAST_REFRESH=0 +REFRESH_INTERVAL=5 # Refresh data every 5 seconds + +# Cleanup on exit +cleanup() { + tput cnorm 2>/dev/null + stty echo 2>/dev/null + echo "" +} +trap cleanup EXIT + +# Check dependencies +check_dependencies() { + if ! command -v jq &> /dev/null; then + echo -e "${RED}Error: 'jq' is required. Install with: sudo apt install jq${NC}" + exit 1 + fi + if ! command -v screen &> /dev/null; then + echo -e "${RED}Error: 'screen' is required. Install with: sudo apt install screen${NC}" + exit 1 + fi +} + +# Refresh cached data if needed +refresh_cache() { + local now=$(date +%s) + if [[ $((now - LAST_REFRESH)) -ge $REFRESH_INTERVAL ]] || [[ -z "$CACHED_JSON" ]]; then + if [[ -f "$SCREENS_FILE" ]]; then + CACHED_JSON=$(cat "$SCREENS_FILE") + else + CACHED_JSON="[]" + fi + CACHED_COUNT=$(echo "$CACHED_JSON" | jq 'length') + LAST_REFRESH=$now + fi +} + +# Force refresh +force_refresh() { + LAST_REFRESH=0 + refresh_cache +} + +get_session_field() { + local index=$1 field=$2 + echo "$CACHED_JSON" | jq -r ".[$index].$field // \"unknown\"" +} + +# Format duration +format_duration() { + local s=$1 d=$((s/86400)) h=$(((s%86400)/3600)) m=$(((s%3600)/60)) + if [[ $d -gt 0 ]]; then printf "%dd %dh" $d $h + elif [[ $h -gt 0 ]]; then printf "%dh %dm" $h $m + elif [[ $m -gt 0 ]]; then printf "%dm" $m + else printf "%ds" $s; fi +} + +# Check if screen alive (cached per draw cycle) +declare -A ALIVE_CACHE +check_alive() { + local screen_name=$1 + if [[ -z "${ALIVE_CACHE[$screen_name]+x}" ]]; then + if screen -ls 2>/dev/null | grep -q "$screen_name"; then + ALIVE_CACHE[$screen_name]=1 + else + ALIVE_CACHE[$screen_name]=0 + fi + fi + [[ "${ALIVE_CACHE[$screen_name]}" -eq 1 ]] +} + +clear_alive_cache() { + ALIVE_CACHE=() +} + +# Kill session by index +kill_session() { + local idx=$1 + local screen_name=$(get_session_field $idx "screenName") + local pid=$(get_session_field $idx "pid") + + pkill -TERM -P $pid 2>/dev/null + kill -TERM -$pid 2>/dev/null + screen -S "$screen_name" -X quit 2>/dev/null + kill -KILL $pid 2>/dev/null + + # Remove from JSON + CACHED_JSON=$(echo "$CACHED_JSON" | jq "del(.[$idx])") + echo "$CACHED_JSON" > "$SCREENS_FILE" + CACHED_COUNT=$(echo "$CACHED_JSON" | jq 'length') + clear_alive_cache +} + +# Draw header (only once) +draw_header() { + echo -e "${BOLD}${CYAN}╔══════════════════════════════════════════════════════════╗${NC}" + echo -e "${BOLD}${CYAN}║ Claudeman Screen Session Manager ║${NC}" + echo -e "${BOLD}${CYAN}║ ${DIM}Press q or Esc to exit${CYAN} ║${NC}" + echo -e "${BOLD}${CYAN}╚══════════════════════════════════════════════════════════╝${NC}" + echo "" +} + +# Draw a single session row (no_newline=1 for partial updates) +draw_row() { + local i=$1 + local selected=$2 + local no_newline=${3:-0} + local now=$(date +%s) + + local name=$(echo "$CACHED_JSON" | jq -r ".[$i].name // \"unnamed\"") + local screen_name=$(echo "$CACHED_JSON" | jq -r ".[$i].screenName") + local created_at=$(echo "$CACHED_JSON" | jq -r ".[$i].createdAt") + local mode=$(echo "$CACHED_JSON" | jq -r ".[$i].mode") + + local created_s=$((created_at / 1000)) + local elapsed=$((now - created_s)) + local running=$(format_duration $elapsed) + + local status status_color + if check_alive "$screen_name"; then + status="● alive" + status_color="${GREEN}" + else + status="○ dead" + status_color="${RED}" + fi + + [[ ${#name} -gt 20 ]] && name="${name:0:17}..." + + local line_end=$'\n' + [[ $no_newline -eq 1 ]] && line_end="" + + if [[ $i -eq $selected ]]; then + printf " ${REVERSE}${BOLD}▶ %-20s %-14s ${status_color}%-12s${NC}${REVERSE} %-8s ${NC}%s" \ + "$name" "$running" "$status" "$mode" "$line_end" + else + printf " %-20s %-14s ${status_color}%-12s${NC} %-8s%s" \ + "$name" "$running" "$status" "$mode" "$line_end" + fi +} + +# Draw footer +draw_footer() { + echo "" + echo -e " ${DIM}──────────────────────────────────────────────────────────────${NC}" + echo -e " ${GREEN}↑/↓${NC} Navigate ${GREEN}Enter${NC} Attach ${YELLOW}d${NC} Delete ${RED}D${NC} Delete All ${BLUE}i${NC} Info ${DIM}q${NC} Quit" +} + +# Full redraw +full_redraw() { + local selected=$1 + clear + draw_header + + if [[ $CACHED_COUNT -eq 0 ]]; then + echo -e " ${YELLOW}No sessions found${NC}" + echo -e " ${DIM}(checking: $SCREENS_FILE)${NC}" + echo "" + echo -e " ${DIM}Press q to quit${NC}" + return + fi + + printf " ${BOLD}%-22s %-14s %-12s %-8s${NC}\n" "NAME" "RUNNING" "STATUS" "MODE" + echo -e " ${DIM}──────────────────────────────────────────────────────────────${NC}" + + for ((i=0; i/dev/null || echo "unknown") + + clear + echo -e "${BOLD}${CYAN}Session Details${NC}" + echo -e "${DIM}────────────────────────────────────────${NC}" + echo -e " ${BOLD}Name:${NC} $name" + echo -e " ${BOLD}Session ID:${NC} $session_id" + echo -e " ${BOLD}Screen:${NC} $screen_name" + echo -e " ${BOLD}PID:${NC} $pid" + echo -e " ${BOLD}Mode:${NC} $mode" + echo -e " ${BOLD}Directory:${NC} $working_dir" + echo -e " ${BOLD}Started:${NC} $started" + echo -e " ${BOLD}Running:${NC} $(format_duration $elapsed)" + + if check_alive "$screen_name"; then + echo -e " ${BOLD}Status:${NC} ${GREEN}● alive${NC}" + local mem=$(ps -o rss= -p $pid 2>/dev/null | tr -d ' ') + [[ -n "$mem" ]] && echo -e " ${BOLD}Memory:${NC} $(echo "scale=1; $mem/1024" | bc) MB" + else + echo -e " ${BOLD}Status:${NC} ${RED}○ dead${NC}" + fi + + echo "" + echo -e "${DIM}Press any key to continue...${NC}" + read -rsn1 +} + +# Confirm dialog +confirm() { + local msg=$1 + tput sc # Save cursor + tput cup $((CACHED_COUNT + 12)) 0 + echo -en " ${YELLOW}$msg (y/n):${NC} " + read -rsn1 answer + tput rc # Restore cursor + [[ "$answer" == "y" || "$answer" == "Y" ]] +} + +# Read single keypress (handles arrow keys) +read_key() { + local key + IFS= read -rsn1 key + + if [[ "$key" == $'\x1b' ]]; then + read -rsn2 -t 0.1 key + case "$key" in + '[A') echo "UP" ;; + '[B') echo "DOWN" ;; + *) echo "ESC" ;; + esac + elif [[ "$key" == "" ]]; then + echo "ENTER" + else + echo "$key" + fi +} + +# Main interactive loop +interactive_menu() { + local selected=0 + local need_full_redraw=1 + + tput civis # Hide cursor + force_refresh + clear_alive_cache + full_redraw $selected + need_full_redraw=0 + + while true; do + # Adjust selection if out of bounds + [[ $selected -ge $CACHED_COUNT ]] && selected=$((CACHED_COUNT > 0 ? CACHED_COUNT - 1 : 0)) + [[ $selected -lt 0 ]] && selected=0 + + # Redraw if needed + if [[ $need_full_redraw -eq 1 ]]; then + clear_alive_cache + full_redraw $selected + need_full_redraw=0 + fi + + local key=$(read_key) + local old_selected=$selected + + case "$key" in + UP|k) + if [[ $selected -gt 0 ]]; then + ((selected--)) + [[ $CACHED_COUNT -gt 0 ]] && update_selection $old_selected $selected + fi + ;; + DOWN|j) + if [[ $selected -lt $((CACHED_COUNT - 1)) ]]; then + ((selected++)) + [[ $CACHED_COUNT -gt 0 ]] && update_selection $old_selected $selected + fi + ;; + ENTER) + if [[ $CACHED_COUNT -gt 0 ]]; then + local screen_name=$(get_session_field $selected "screenName") + if check_alive "$screen_name"; then + tput cnorm + clear + echo -e "${CYAN}Attaching... (Ctrl+A D to detach)${NC}" + sleep 0.3 + screen -r "$screen_name" + tput civis + need_full_redraw=1 + force_refresh + else + tput cup $((CACHED_COUNT + 12)) 0 + echo -e " ${RED}Session is dead${NC} " + sleep 1 + need_full_redraw=1 + fi + fi + ;; + d|x) + if [[ $CACHED_COUNT -gt 0 ]]; then + local name=$(get_session_field $selected "name") + if confirm "Kill '$name'?"; then + kill_session $selected + need_full_redraw=1 + else + need_full_redraw=1 + fi + fi + ;; + D|X) + if [[ $CACHED_COUNT -gt 0 ]]; then + if confirm "Kill ALL $CACHED_COUNT sessions?"; then + for ((i=CACHED_COUNT-1; i>=0; i--)); do + kill_session $i + done + fi + need_full_redraw=1 + fi + ;; + i) + if [[ $CACHED_COUNT -gt 0 ]]; then + show_info $selected + need_full_redraw=1 + fi + ;; + q|Q|ESC) + clear + echo -e "${DIM}Goodbye!${NC}" + break + ;; + esac + done + + tput cnorm +} + +# Quick list (non-interactive) +quick_list() { + force_refresh + local now=$(date +%s) + + if [[ $CACHED_COUNT -eq 0 ]]; then + echo "No claudeman sessions found." + exit 0 + fi + + echo "Claudeman Sessions ($SCREENS_FILE):" + echo "" + printf "%-4s %-22s %-14s %-10s %-8s\n" "#" "NAME" "RUNNING" "STATUS" "MODE" + echo "─────────────────────────────────────────────────────────────────" + + for ((i=0; i"; exit 1; } + force_refresh + local screen_name=$(get_session_field $(($2-1)) "screenName") + check_alive "$screen_name" && screen -r "$screen_name" || echo "Session dead or not found" + ;; + kill) + [[ -z "${2:-}" ]] && { echo "Usage: $0 kill "; exit 1; } + force_refresh + if [[ "$2" =~ ^[0-9]+-[0-9]+$ ]]; then + local start="${2%-*}" end="${2#*-}" + for ((i=end-1; i>=start-1; i--)); do kill_session $i 2>/dev/null; done + else + IFS=',' read -ra nums <<< "$2" + for num in $(echo "${nums[*]}" | tr ' ' '\n' | sort -rn); do + kill_session $((num-1)) 2>/dev/null + done + fi + echo "Done" + ;; + kill-all) + force_refresh + for ((i=CACHED_COUNT-1; i>=0; i--)); do kill_session $i; done + echo "All sessions killed" + ;; + info) + [[ -z "${2:-}" ]] && { echo "Usage: $0 info "; exit 1; } + force_refresh + show_info $(($2-1)) + ;; + help|--help|-h) usage ;; + *) echo "Unknown: $1"; usage; exit 1 ;; + esac +} + +main "$@"