#!/usr/bin/env bash
# crabcode - Generic tmux-based workspace manager for multi-repo development
# https://github.com/your-org/crabcode
#
# Usage: crabcode [command] [arguments]
#
# Commands:
#   (no args)         List all workspaces
#   <N>               Open/create workspace N
#   <N> --separate    Open workspace N in separate terminal window
#   <N> cleanup       Kill window + reset to origin/main
#   <N> restart       Reset git + restart panes in place
#   <N> continue      Resume session with --continue flag
#   restart           Auto-detect workspace from cwd + restart
#   continue          Auto-detect workspace from cwd + resume
#   init              Setup config (auto-detects project type)
#   config            Show current configuration
#   config scan       Auto-detect .env files and ports
#   doctor            Diagnose common issues
#   cheat             Show cheat sheet / help
#   ports             Show port usage across workspaces
#   wip save          Save work in progress
#   wip ls            List saved WIP states
#   wip --continue    Restore most recent WIP
#   wip --resume      Interactive WIP selection
#   wip delete <name> Delete a WIP state

set -e

VERSION="0.14.0" # x-release-please-version
CONFIG_DIR="$HOME/.crabcode"
CONFIG_FILE="$CONFIG_DIR/config.yaml"
WIP_BASE="$CONFIG_DIR/wip"
PROJECTS_DIR="$CONFIG_DIR/projects"
GLOBAL_CONFIG="$CONFIG_DIR/config.yaml"
PROJECT_ALIAS=""

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
GRAY='\033[0;90m'
BOLD='\033[1m'
NC='\033[0m' # No Color

# =============================================================================
# Utility Functions
# =============================================================================

error() {
  echo -e "${RED}Error: $*${NC}" >&2
}

warn() {
  echo -e "${YELLOW}Warning: $*${NC}" >&2
}

info() {
  echo -e "${CYAN}$*${NC}"
}

success() {
  echo -e "${GREEN}$*${NC}"
}

# Check if a command exists
command_exists() {
  command -v "$1" &>/dev/null
}

# Treat "n" and "no" as explicit negative responses.
is_negative_response() {
  [[ "$1" =~ ^[Nn]([Oo])?$ ]]
}

# =============================================================================
# Agent Abstraction Layer
# =============================================================================
# Crabcode supports multiple coding agents (codex, claude, etc.)
# The agent is configured per-project via the `agent:` field in project YAML.

# Get configured agent type: "codex" or "claude"
get_agent_type() {
  local agent=""

  if [ -f "$CONFIG_FILE" ]; then
    agent=$(yq -r '.agent // ""' "$CONFIG_FILE" 2>/dev/null)
    [ "$agent" = "null" ] && agent=""

    case "${agent:-}" in
      codex|claude)
        echo "$agent"
        return
        ;;
    esac

    # Backward compat: infer the agent from an explicitly configured main pane command.
    local main_cmd=$(yq -r '.layout.panes[]? | select(.name == "main") | .command // ""' "$CONFIG_FILE" 2>/dev/null | head -1)
    case "$main_cmd" in
      *claude*) echo "claude" ; return ;;
      *codex*) echo "codex" ; return ;;
    esac
  fi

  echo "codex"
}

agent_base_cmd_for_type() {
  local agent="${1:-codex}"
  case "$agent" in
    claude) echo "claude --dangerously-skip-permissions" ;;
    codex|*) echo "codex --full-auto --skip-git-repo-check" ;;
  esac
}

agent_safe_cmd_for_type() {
  local agent="${1:-codex}"
  case "$agent" in
    claude) echo "claude" ;;
    codex|*) echo "codex --skip-git-repo-check" ;;
  esac
}

agent_cmd_continue_for_type() {
  local agent="${1:-codex}"
  local cmd="$2"
  case "$agent" in
    claude) echo "$cmd --continue" ;;
    codex|*) echo "codex resume --last --full-auto" ;;
  esac
}

agent_cmd_resume_for_type() {
  local agent="${1:-codex}"
  local cmd="$2"
  local session_id="$3"
  case "$agent" in
    claude) echo "$cmd --resume $session_id" ;;
    codex|*) echo "codex resume $session_id --full-auto" ;;
  esac
}

agent_print_cmd_for_type() {
  local agent="${1:-codex}"
  case "$agent" in
    claude) echo "claude --print" ;;
    codex|*) echo "codex exec" ;;
  esac
}

agent_display_name_for_type() {
  local agent="${1:-codex}"
  case "$agent" in
    claude) echo "Claude" ;;
    codex|*) echo "Codex" ;;
  esac
}

agent_resume_file_for_type() {
  local agent="${1:-codex}"
  local dir="$2"
  case "$agent" in
    claude) echo "$dir/.claude-resume-session" ;;
    codex|*) echo "$dir/.codex-resume-session" ;;
  esac
}

agent_session_dir_for_type() {
  local agent="${1:-codex}"
  local workspace_dir="$2"
  case "$agent" in
    claude) echo "$HOME/.claude/projects/$(echo "$workspace_dir" | tr '/.' '--')" ;;
    codex|*) echo "$HOME/.codex" ;;
  esac
}

agent_type_from_metadata() {
  local metadata="$1"
  local saved_agent=$(grep -o '"agent"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"agent"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')

  case "${saved_agent:-}" in
    codex|claude)
      echo "$saved_agent"
      ;;
    *)
      if grep -q '"claude_session"' "$metadata" 2>/dev/null; then
        echo "claude"
      else
        echo "codex"
      fi
      ;;
  esac
}

# Get the base agent command (interactive mode, autonomous permissions)
get_agent_base_cmd() {
  agent_base_cmd_for_type "$(get_agent_type)"
}

# Get the agent command in safe/interactive mode (user approves each action)
get_agent_safe_cmd() {
  agent_safe_cmd_for_type "$(get_agent_type)"
}

# Build agent command with "continue last session" semantics
agent_cmd_continue() {
  agent_cmd_continue_for_type "$(get_agent_type)" "$1"
}

# Build agent command with "resume specific session" semantics
agent_cmd_resume() {
  agent_cmd_resume_for_type "$(get_agent_type)" "$1" "$2"
}

# Append prompt to agent command
agent_cmd_with_prompt() {
  local cmd="$1"
  local prompt="$2"
  local agent=$(get_agent_type)
  # Both claude and codex accept positional prompts
  echo "$cmd $prompt"
}

# Get the non-interactive print command for the agent
agent_print_cmd() {
  agent_print_cmd_for_type "$(get_agent_type)"
}

# Run agent non-interactively with a prompt (stdin)
agent_run_print() {
  local prompt="$1"
  local agent=$(get_agent_type)
  case "$agent" in
    claude) echo "$prompt" | timeout 30 claude --print 2>/dev/null || echo "" ;;
    codex|*) echo "$prompt" | timeout 30 codex exec --skip-git-repo-check -C "$(pwd)" 2>/dev/null || echo "" ;;
  esac
}

# Check if the configured agent CLI is installed
agent_cli_exists() {
  local agent=$(get_agent_type)
  case "$agent" in
    claude) command_exists claude ;;
    codex|*) command_exists codex ;;
  esac
}

# Get agent display name (for user-facing messages)
agent_display_name() {
  agent_display_name_for_type "$(get_agent_type)"
}

# Get agent resume file name (per-workspace)
agent_resume_file() {
  agent_resume_file_for_type "$(get_agent_type)" "$1"
}

# Get agent session history directory
agent_session_dir() {
  agent_session_dir_for_type "$(get_agent_type)" "$1"
}

# Get the agent system prompt file path and ensure it exists
agent_ensure_system_prompt() {
  local dir="$1"
  local agent=$(get_agent_type)
  case "$agent" in
    claude)
      local team_file="$dir/.claude/CLAUDE.md"
      mkdir -p "$dir/.claude"
      if ! grep -q "^## Team Mode$" "$team_file" 2>/dev/null; then
        cat >> "$team_file" << 'CLAUDEEOF'

## Team Mode

You can spawn agent teammates for complex tasks. Use the Task tool to create specialized agents (researcher, implementer, reviewer, debugger) that work in parallel. Coordinate the team, assign tasks, and synthesize results. Only spawn teams when the task benefits from parallel work.
CLAUDEEOF
      fi
      ;;
    codex|*)
      local agents_file="$dir/AGENTS.md"
      if ! grep -q "^## Team Mode$" "$agents_file" 2>/dev/null; then
        cat >> "$agents_file" << 'AGENTSEOF'

## Team Mode

You can spawn agent teammates for complex tasks. Create specialized agents (researcher, implementer, reviewer, debugger) that work in parallel. Coordinate the team, assign tasks, and synthesize results. Only spawn teams when the task benefits from parallel work.
AGENTSEOF
      fi
      ;;
  esac
}

# Capture agent session ID for WIP save
agent_capture_session_id() {
  local dir="$1"
  local agent=$(get_agent_type)
  case "$agent" in
    claude)
      local claude_project_dir="$HOME/.claude/projects/$(echo "$dir" | tr '/.' '--')"
      if [ -d "$claude_project_dir" ]; then
        ls -t "$claude_project_dir"/*.jsonl 2>/dev/null | head -1 | xargs -I{} basename {} .jsonl 2>/dev/null || echo ""
      else
        echo ""
      fi
      ;;
    codex|*)
      # Codex stores sessions in ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl
      local codex_sessions="$HOME/.codex/sessions"
      if [ -d "$codex_sessions" ]; then
        local latest_file
        latest_file=$(find "$codex_sessions" -name "*.jsonl" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
        if [ -n "$latest_file" ]; then
          basename "$latest_file" .jsonl | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' || echo ""
        else
          echo ""
        fi
      else
        echo ""
      fi
      ;;
  esac
}

# Write agent resume file from session ID
agent_write_resume_file() {
  local dir="$1"
  local session_id="$2"
  local resume_file=$(agent_resume_file "$dir")
  if [ -n "$session_id" ] && [ "$session_id" != "" ]; then
    echo "$session_id" > "$resume_file"
    echo -e "  ${BLUE}$(agent_display_name) conversation will resume automatically${NC}"
  fi
}

# Build agent command with context file
agent_cmd_with_context() {
  local cmd="$1"
  local context_file="$2"
  local agent=$(get_agent_type)
  case "$agent" in
    claude) echo "$cmd \"$context_file\"" ;;
    codex|*) echo "$cmd \"$(cat "$context_file" 2>/dev/null | head -c 4000)\"" ;;
  esac
}

# Generate auto-summary using agent in non-interactive mode (for review sessions)
agent_generate_summary() {
  local prompt="$1"
  local agent=$(get_agent_type)
  case "$agent" in
    claude)
      claude --continue --print -p "$prompt" 2>/dev/null | tail -1 || echo ""
      ;;
    codex|*)
      codex exec --skip-git-repo-check -C "$(pwd)" "$prompt" 2>/dev/null | tail -1 || echo ""
      ;;
  esac
}

# =============================================================================
# Agent Sync — sync user-level MCP, agents/skills between Claude & Codex
# =============================================================================

# Show what agent configs exist on each side
agent_sync_status() {
  echo -e "${CYAN}Agent Sync Status${NC}"
  echo ""

  # --- MCP Servers ---
  echo -e "${BOLD}MCP Servers:${NC}"

  # Claude MCP: parse from settings.json
  local claude_settings="$HOME/.claude/settings.json"
  local claude_mcps=""
  if [ -f "$claude_settings" ]; then
    claude_mcps=$(python3 -c "
import json, sys
d = json.load(open('$claude_settings'))
servers = set()
for path, cfg in d.get('projects', {}).items():
    for name in cfg.get('mcpServers', {}):
        servers.add(name)
for s in sorted(servers):
    print(s)
" 2>/dev/null)
  fi

  # Codex MCP: parse from config.toml
  local codex_config="$HOME/.codex/config.toml"
  local codex_mcps=""
  if [ -f "$codex_config" ]; then
    codex_mcps=$(grep '^\[mcp_servers\.' "$codex_config" 2>/dev/null | sed 's/\[mcp_servers\.\(.*\)\]/\1/' | grep -v '\.env$' | sort -u)
  fi

  if [ -n "$claude_mcps" ]; then
    echo -e "  Claude:  ${GREEN}$(echo "$claude_mcps" | tr '\n' ', ' | sed 's/,$//')${NC}"
  else
    echo -e "  Claude:  ${GRAY}(none)${NC}"
  fi
  if [ -n "$codex_mcps" ]; then
    echo -e "  Codex:   ${GREEN}$(echo "$codex_mcps" | tr '\n' ', ' | sed 's/,$//')${NC}"
  else
    echo -e "  Codex:   ${GRAY}(none)${NC}"
  fi

  # Show what needs syncing
  local needs_to_codex=""
  local needs_to_claude=""
  if [ -n "$claude_mcps" ]; then
    while IFS= read -r server; do
      if ! echo "$codex_mcps" | grep -qx "$server" 2>/dev/null; then
        needs_to_codex="${needs_to_codex}${server} "
      fi
    done <<< "$claude_mcps"
  fi
  if [ -n "$codex_mcps" ]; then
    while IFS= read -r server; do
      if ! echo "$claude_mcps" | grep -qx "$server" 2>/dev/null; then
        needs_to_claude="${needs_to_claude}${server} "
      fi
    done <<< "$codex_mcps"
  fi
  [ -n "$needs_to_codex" ] && echo -e "  ${YELLOW}→ Needs sync to Codex: $needs_to_codex${NC}"
  [ -n "$needs_to_claude" ] && echo -e "  ${YELLOW}→ Needs sync to Claude: $needs_to_claude${NC}"
  [ -z "$needs_to_codex" ] && [ -z "$needs_to_claude" ] && [ -n "$claude_mcps$codex_mcps" ] && echo -e "  ${GREEN}✓ In sync${NC}"
  echo ""

  # --- Custom Agents/Skills ---
  echo -e "${BOLD}Custom Agents / Skills:${NC}"

  local claude_agents=""
  if [ -d "$HOME/.claude/agents" ]; then
    claude_agents=$(ls "$HOME/.claude/agents/"*.md 2>/dev/null | xargs -I{} basename {} .md | sort)
  fi

  local codex_skills=""
  if [ -d "$HOME/.codex/skills" ]; then
    codex_skills=$(ls -d "$HOME/.codex/skills"/*/ 2>/dev/null | grep -v '\.system' | xargs -I{} basename {} | sort)
  fi

  if [ -n "$claude_agents" ]; then
    echo -e "  Claude agents:  ${GREEN}$(echo "$claude_agents" | tr '\n' ', ' | sed 's/,$//')${NC}"
  else
    echo -e "  Claude agents:  ${GRAY}(none)${NC}"
  fi
  if [ -n "$codex_skills" ]; then
    echo -e "  Codex skills:   ${GREEN}$(echo "$codex_skills" | tr '\n' ', ' | sed 's/,$//')${NC}"
  else
    echo -e "  Codex skills:   ${GRAY}(none)${NC}"
  fi

  # Show what needs syncing
  local agents_to_codex=""
  if [ -n "$claude_agents" ]; then
    while IFS= read -r agent; do
      [ -z "$agent" ] && continue
      if [ ! -d "$HOME/.codex/skills/$agent" ]; then
        agents_to_codex="${agents_to_codex}${agent} "
      fi
    done <<< "$claude_agents"
  fi
  local skills_to_claude=""
  if [ -n "$codex_skills" ]; then
    while IFS= read -r skill; do
      [ -z "$skill" ] && continue
      if [ ! -f "$HOME/.claude/agents/${skill}.md" ]; then
        skills_to_claude="${skills_to_claude}${skill} "
      fi
    done <<< "$codex_skills"
  fi
  [ -n "$agents_to_codex" ] && echo -e "  ${YELLOW}→ Claude agents not in Codex: $agents_to_codex${NC}"
  [ -n "$skills_to_claude" ] && echo -e "  ${YELLOW}→ Codex skills not in Claude: $skills_to_claude${NC}"
  [ -z "$agents_to_codex" ] && [ -z "$skills_to_claude" ] && [ -n "$claude_agents$codex_skills" ] && echo -e "  ${GREEN}✓ In sync${NC}"
  echo ""
}

# Sync MCP servers from Claude to Codex
_sync_mcp_claude_to_codex() {
  local apply="${1:-false}"
  local claude_settings="$HOME/.claude/settings.json"

  if [ ! -f "$claude_settings" ]; then
    echo "No Claude settings found."
    return 0
  fi

  # Extract unique MCP servers from all Claude projects
  local servers_json
  servers_json=$(python3 -c "
import json
d = json.load(open('$claude_settings'))
merged = {}
for path, cfg in d.get('projects', {}).items():
    for name, srv in cfg.get('mcpServers', {}).items():
        if name not in merged:
            merged[name] = srv
print(json.dumps(merged))
" 2>/dev/null)

  if [ "$servers_json" = "{}" ] || [ -z "$servers_json" ]; then
    echo "No MCP servers found in Claude config."
    return 0
  fi

  # Check which ones already exist in Codex
  local codex_config="$HOME/.codex/config.toml"
  local servers_to_add
  servers_to_add=$(python3 -c "
import json, sys
servers = json.loads('$servers_json')
existing = set()
try:
    with open('$codex_config') as f:
        for line in f:
            if line.startswith('[mcp_servers.'):
                name = line.strip().strip('[]').split('.', 1)[1].split(']')[0]
                if '.' not in name:
                    existing.add(name)
except FileNotFoundError:
    pass
for name in sorted(servers):
    if name not in existing:
        print(name)
" 2>/dev/null)

  if [ -z "$servers_to_add" ]; then
    echo -e "${GREEN}All Claude MCP servers already exist in Codex.${NC}"
    return 0
  fi

  echo -e "${CYAN}MCP servers to sync (Claude → Codex):${NC}"
  echo ""
  while IFS= read -r server_name; do
    [ -z "$server_name" ] && continue
    local cmd args env_vars
    cmd=$(python3 -c "import json; s=json.loads('$servers_json'); print(s['$server_name'].get('command',''))" 2>/dev/null)
    args=$(python3 -c "import json; s=json.loads('$servers_json'); print(' '.join(s['$server_name'].get('args',[])))" 2>/dev/null)
    env_vars=$(python3 -c "
import json
s = json.loads('$(echo "$servers_json" | sed "s/'/'\\\\''/g")')
env = s['$server_name'].get('env', {})
for k, v in env.items():
    print(f'{k}={v}')
" 2>/dev/null)

    echo -e "  ${BOLD}$server_name${NC}"
    echo "    command: $cmd $args"
    if [ -n "$env_vars" ]; then
      while IFS= read -r ev; do
        local key="${ev%%=*}"
        echo "    env: $key=***"
      done <<< "$env_vars"
    fi
    echo ""

    if [ "$apply" = "true" ]; then
      # Build codex mcp add command
      local add_cmd="codex mcp add $server_name"
      if [ -n "$env_vars" ]; then
        while IFS= read -r ev; do
          add_cmd="$add_cmd --env \"$ev\""
        done <<< "$env_vars"
      fi
      add_cmd="$add_cmd -- $cmd $args"

      echo -e "  ${CYAN}Adding $server_name to Codex...${NC}"
      eval "$add_cmd" 2>&1 | sed 's/^/    /'
      echo ""
    fi
  done <<< "$servers_to_add"

  if [ "$apply" != "true" ]; then
    echo -e "${YELLOW}Dry run — use 'crab agent sync mcp --apply' to write changes.${NC}"
  fi
}

# Sync MCP servers from Codex to Claude
_sync_mcp_codex_to_claude() {
  local apply="${1:-false}"
  local codex_config="$HOME/.codex/config.toml"
  local claude_settings="$HOME/.claude/settings.json"

  if [ ! -f "$codex_config" ]; then
    echo "No Codex config found."
    return 0
  fi

  # Parse Codex MCP servers from TOML
  local servers_json
  servers_json=$(python3 -c "
import re, json

config_text = open('$codex_config').read()
servers = {}
current_server = None
current_section = None  # 'main' or 'env'

for line in config_text.split('\n'):
    line = line.strip()
    # Match [mcp_servers.NAME]
    m = re.match(r'^\[mcp_servers\.([^.\]]+)\]$', line)
    if m:
        current_server = m.group(1)
        servers[current_server] = {'command': '', 'args': [], 'env': {}}
        current_section = 'main'
        continue
    # Match [mcp_servers.NAME.env]
    m = re.match(r'^\[mcp_servers\.([^.\]]+)\.env\]$', line)
    if m:
        current_server = m.group(1)
        current_section = 'env'
        continue
    # Match other section headers (end current server)
    if line.startswith('['):
        current_server = None
        current_section = None
        continue
    if current_server and '=' in line:
        key, val = line.split('=', 1)
        key = key.strip()
        val = val.strip().strip('\"')
        if current_section == 'env':
            servers[current_server]['env'][key] = val
        elif key == 'command':
            servers[current_server]['command'] = val
        elif key == 'args':
            # Parse TOML array
            try:
                servers[current_server]['args'] = json.loads(val.replace(\"'\", '\"'))
            except:
                pass

# Filter out servers with no command (HTTP servers not supported in Claude)
result = {k: v for k, v in servers.items() if v.get('command')}
print(json.dumps(result))
" 2>/dev/null)

  if [ "$servers_json" = "{}" ] || [ -z "$servers_json" ]; then
    echo "No STDIO MCP servers found in Codex config."
    return 0
  fi

  # Check which already exist in Claude
  local servers_to_add
  servers_to_add=$(python3 -c "
import json
servers = json.loads('$(echo "$servers_json" | sed "s/'/'\\\\''/g")')
existing = set()
try:
    d = json.load(open('$claude_settings'))
    for path, cfg in d.get('projects', {}).items():
        for name in cfg.get('mcpServers', {}):
            existing.add(name)
except (FileNotFoundError, json.JSONDecodeError):
    pass
for name in sorted(servers):
    if name not in existing:
        print(name)
" 2>/dev/null)

  if [ -z "$servers_to_add" ]; then
    echo -e "${GREEN}All Codex MCP servers already exist in Claude.${NC}"
    return 0
  fi

  echo -e "${CYAN}MCP servers to sync (Codex → Claude):${NC}"
  echo ""
  while IFS= read -r server_name; do
    [ -z "$server_name" ] && continue
    echo -e "  ${BOLD}$server_name${NC}"
    python3 -c "
import json
s = json.loads('$(echo "$servers_json" | sed "s/'/'\\\\''/g")')
srv = s['$server_name']
print(f\"    command: {srv['command']} {' '.join(srv.get('args',[]))}\")
for k in srv.get('env', {}):
    print(f'    env: {k}=***')
" 2>/dev/null
    echo ""
  done <<< "$servers_to_add"

  if [ "$apply" = "true" ]; then
    # Add to Claude settings.json — inject into the first project that has mcpServers
    python3 -c "
import json
servers = json.loads('$(echo "$servers_json" | sed "s/'/'\\\\''/g")')
to_add = '$(echo "$servers_to_add" | tr '\n' ',')'
to_add = [s for s in to_add.strip(',').split(',') if s]

d = json.load(open('$claude_settings'))
# Add to all existing project configs that have mcpServers
added_to = 0
for path, cfg in d.get('projects', {}).items():
    if 'mcpServers' in cfg:
        for name in to_add:
            if name not in cfg['mcpServers']:
                cfg['mcpServers'][name] = servers[name]
        added_to += 1

# If no projects had mcpServers, we can't add blindly
if added_to == 0:
    print('No Claude projects with MCP config found. Add manually with: claude mcp add')
else:
    with open('$claude_settings', 'w') as f:
        json.dump(d, f, indent=2)
    print(f'Added to {added_to} Claude project configs.')
" 2>/dev/null
  else
    echo -e "${YELLOW}Dry run — use 'crab agent sync mcp --apply' to write changes.${NC}"
  fi
}

# Sync MCP servers bidirectionally based on --from flag
agent_sync_mcp() {
  local direction="${1:-auto}"
  local apply="${2:-false}"

  case "$direction" in
    claude)
      _sync_mcp_claude_to_codex "$apply"
      ;;
    codex)
      _sync_mcp_codex_to_claude "$apply"
      ;;
    auto|*)
      _sync_mcp_claude_to_codex "$apply"
      echo ""
      _sync_mcp_codex_to_claude "$apply"
      ;;
  esac
}

# Rewrite a Claude agent as a Codex skill using an LLM
_rewrite_claude_agent_to_codex_skill() {
  local agent_file="$1"
  local apply="${2:-false}"
  local agent_name=$(basename "$agent_file" .md)
  local skill_dir="$HOME/.codex/skills/$agent_name"

  if [ -d "$skill_dir" ] && [ "$apply" = "true" ]; then
    echo -e "  ${YELLOW}Codex skill '$agent_name' already exists.${NC}"
    read -p "  Overwrite? [y/N]: " overwrite
    if [ "$overwrite" != "y" ] && [ "$overwrite" != "Y" ]; then
      echo "  Skipped."
      return 0
    fi
  fi

  local agent_content
  agent_content=$(cat "$agent_file")

  local prompt="You are converting a Claude Code custom agent definition into an OpenAI Codex CLI skill.

INPUT (Claude Code agent — markdown with YAML frontmatter):
---
$agent_content
---

OUTPUT REQUIREMENTS:
- Produce a Codex SKILL.md file with YAML frontmatter containing only 'name' and 'description' fields
- The description should explain when this skill triggers (this is how Codex matches skills to user requests)
- Convert the body instructions to work with Codex's tool set (Codex uses shell commands, file I/O, not Claude's named tools like Read/Write/Grep)
- Remove any Claude-specific references (Task tool, Agent tool, claude commands)
- Keep the core logic and workflow intact
- Output ONLY the SKILL.md content, nothing else — no explanation, no code fences"

  echo -e "  ${CYAN}Translating $agent_name...${NC}"

  local skill_content
  if command -v codex &>/dev/null; then
    skill_content=$(codex exec "$prompt" 2>/dev/null)
  elif command -v claude &>/dev/null; then
    skill_content=$(claude --print -p "$prompt" 2>/dev/null)
  else
    error "No LLM CLI available (need codex or claude)"
    return 1
  fi

  if [ -z "$skill_content" ]; then
    error "LLM returned empty output for $agent_name"
    return 1
  fi

  echo ""
  echo -e "  ${BOLD}Preview: $skill_dir/SKILL.md${NC}"
  echo "  ─────────────────────────────────────"
  echo "$skill_content" | head -30 | sed 's/^/  /'
  if [ "$(echo "$skill_content" | wc -l)" -gt 30 ]; then
    echo "  ... ($(echo "$skill_content" | wc -l) lines total)"
  fi
  echo "  ─────────────────────────────────────"
  echo ""

  if [ "$apply" = "true" ]; then
    mkdir -p "$skill_dir"
    echo "$skill_content" > "$skill_dir/SKILL.md"
    echo -e "  ${GREEN}Created $skill_dir/SKILL.md${NC}"
  else
    echo -e "  ${YELLOW}Dry run — use --apply to write.${NC}"
  fi
}

# Rewrite a Codex skill as a Claude agent using an LLM
_rewrite_codex_skill_to_claude_agent() {
  local skill_dir="$1"
  local apply="${2:-false}"
  local skill_name=$(basename "$skill_dir")
  local agent_file="$HOME/.claude/agents/${skill_name}.md"

  if [ -f "$agent_file" ] && [ "$apply" = "true" ]; then
    echo -e "  ${YELLOW}Claude agent '$skill_name' already exists.${NC}"
    read -p "  Overwrite? [y/N]: " overwrite
    if [ "$overwrite" != "y" ] && [ "$overwrite" != "Y" ]; then
      echo "  Skipped."
      return 0
    fi
  fi

  local skill_content
  skill_content=$(cat "$skill_dir/SKILL.md")

  local prompt="You are converting an OpenAI Codex CLI skill into a Claude Code custom agent definition.

INPUT (Codex skill — SKILL.md with YAML frontmatter):
---
$skill_content
---

OUTPUT REQUIREMENTS:
- Produce a Claude Code agent .md file with YAML frontmatter containing: name, description, tools, model
- For 'tools', choose from: Bash, Read, Write, Edit, Grep, Glob, WebFetch, WebSearch (pick what the skill needs)
- For 'model', use 'sonnet' (good default)
- The description should explain when to use this agent
- Convert any Codex-specific references to Claude Code equivalents
- Keep the core logic and workflow intact
- Output ONLY the agent .md content, nothing else — no explanation, no code fences"

  echo -e "  ${CYAN}Translating $skill_name...${NC}"

  local agent_content
  if command -v codex &>/dev/null; then
    agent_content=$(codex exec "$prompt" 2>/dev/null)
  elif command -v claude &>/dev/null; then
    agent_content=$(claude --print -p "$prompt" 2>/dev/null)
  else
    error "No LLM CLI available (need codex or claude)"
    return 1
  fi

  if [ -z "$agent_content" ]; then
    error "LLM returned empty output for $skill_name"
    return 1
  fi

  echo ""
  echo -e "  ${BOLD}Preview: $agent_file${NC}"
  echo "  ─────────────────────────────────────"
  echo "$agent_content" | head -30 | sed 's/^/  /'
  if [ "$(echo "$agent_content" | wc -l)" -gt 30 ]; then
    echo "  ... ($(echo "$agent_content" | wc -l) lines total)"
  fi
  echo "  ─────────────────────────────────────"
  echo ""

  if [ "$apply" = "true" ]; then
    mkdir -p "$HOME/.claude/agents"
    echo "$agent_content" > "$agent_file"
    echo -e "  ${GREEN}Created $agent_file${NC}"
  else
    echo -e "  ${YELLOW}Dry run — use --apply to write.${NC}"
  fi
}

# Sync agents/skills between Claude and Codex
agent_sync_agents() {
  local direction="${1:-auto}"
  local apply="${2:-false}"

  case "$direction" in
    claude)
      echo -e "${CYAN}Syncing Claude agents → Codex skills${NC}"
      echo ""
      local found=false
      for agent_file in "$HOME/.claude/agents/"*.md; do
        [ ! -f "$agent_file" ] && continue
        local name=$(basename "$agent_file" .md)
        if [ ! -d "$HOME/.codex/skills/$name" ]; then
          found=true
          _rewrite_claude_agent_to_codex_skill "$agent_file" "$apply"
          echo ""
        fi
      done
      if [ "$found" = "false" ]; then
        echo -e "${GREEN}All Claude agents already have Codex skill equivalents.${NC}"
      fi
      ;;
    codex)
      echo -e "${CYAN}Syncing Codex skills → Claude agents${NC}"
      echo ""
      local found=false
      for skill_dir in "$HOME/.codex/skills"/*/; do
        [ ! -d "$skill_dir" ] && continue
        echo "$skill_dir" | grep -q '\.system' && continue
        [ ! -f "$skill_dir/SKILL.md" ] && continue
        local name=$(basename "$skill_dir")
        if [ ! -f "$HOME/.claude/agents/${name}.md" ]; then
          found=true
          _rewrite_codex_skill_to_claude_agent "$skill_dir" "$apply"
          echo ""
        fi
      done
      if [ "$found" = "false" ]; then
        echo -e "${GREEN}All Codex skills already have Claude agent equivalents.${NC}"
      fi
      ;;
    auto|*)
      agent_sync_agents "claude" "$apply"
      echo ""
      agent_sync_agents "codex" "$apply"
      ;;
  esac
}

# Main agent sync handler
handle_agent_sync_command() {
  local subcmd="${1:-status}"
  shift 2>/dev/null || true

  # Parse flags
  local direction="auto"
  local apply="false"
  local remaining_args=()
  while [ $# -gt 0 ]; do
    case "$1" in
      --from)
        direction="${2:-auto}"
        shift 2
        ;;
      --apply)
        apply="true"
        shift
        ;;
      --dry-run)
        apply="false"
        shift
        ;;
      *)
        remaining_args+=("$1")
        shift
        ;;
    esac
  done

  case "$subcmd" in
    "status"|"st")
      agent_sync_status
      ;;
    "mcp")
      agent_sync_mcp "$direction" "$apply"
      ;;
    "agents"|"skills")
      agent_sync_agents "$direction" "$apply"
      ;;
    "all"|"sync")
      echo -e "${BOLD}=== MCP Servers ===${NC}"
      echo ""
      agent_sync_mcp "$direction" "$apply"
      echo ""
      echo -e "${BOLD}=== Agents / Skills ===${NC}"
      echo ""
      agent_sync_agents "$direction" "$apply"
      ;;
    *)
      echo -e "${CYAN}Agent Sync Commands:${NC}"
      echo ""
      echo "  crab agent status                  Show what's configured on each side"
      echo "  crab agent sync mcp                Sync MCP servers between Claude & Codex"
      echo "  crab agent sync agents             Sync custom agents/skills (LLM-assisted)"
      echo "  crab agent sync all                Sync everything"
      echo ""
      echo "  Options:"
      echo "    --from claude|codex              Sync from specific agent (default: both directions)"
      echo "    --apply                          Apply changes (default: dry run)"
      echo ""
      echo "  Examples:"
      echo "    crab agent sync mcp --from claude --apply"
      echo "    crab agent sync agents --from claude --apply"
      echo "    crab agent status"
      echo ""
      ;;
  esac
}

# =============================================================================
# Config Loading (using yq for YAML parsing)
# =============================================================================

# Check if yq is installed
check_yq() {
  if ! command_exists yq; then
    error "yq is required but not installed."
    echo "Install with:"
    echo "  brew install yq       # macOS"
    echo "  apt install yq        # Ubuntu/Debian"
    echo "  go install github.com/mikefarah/yq/v4@latest  # Go"
    exit 1
  fi
}

# Load a config value, with optional default
# Usage: config_get "path.to.value" "default"
config_get() {
  local path="$1"
  local default="${2:-}"
  local value

  if [ ! -f "$CONFIG_FILE" ]; then
    echo "$default"
    return
  fi

  value=$(yq -r ".$path // \"\"" "$CONFIG_FILE" 2>/dev/null)
  if [ -z "$value" ] || [ "$value" = "null" ]; then
    echo "$default"
  else
    echo "$value"
  fi
}

# Check if config exists and is valid
config_exists() {
  [ -f "$CONFIG_FILE" ]
}

# Validate required config fields
validate_config() {
  local errors=0

  if ! config_exists; then
    if [ -n "${PROJECT_ALIAS:-}" ]; then
      error "Config not found for project @$PROJECT_ALIAS at $CONFIG_FILE"
    else
      error "No config file found at $CONFIG_FILE"
    fi
    echo "Run 'crabcode init' to create one."
    exit 1
  fi

  local session_name=$(config_get "session_name")
  local workspace_base=$(config_get "workspace_base")
  local main_repo=$(config_get "main_repo")

  if [ -z "$session_name" ]; then
    error "Missing required config: session_name"
    errors=$((errors + 1))
  fi

  if [ -z "$workspace_base" ]; then
    error "Missing required config: workspace_base"
    errors=$((errors + 1))
  fi

  if [ -z "$main_repo" ]; then
    error "Missing required config: main_repo"
    errors=$((errors + 1))
  fi

  if [ $errors -gt 0 ]; then
    echo ""
    echo "Run 'crabcode doctor' to diagnose issues."
    exit 1
  fi
}

# Sync MCP config from main repo to workspace
# Claude Code reads MCP servers from .mcp.json in the project directory
sync_mcp_servers() {
  local workspace_dir=$1
  local main_repo=$(config_get "main_repo" "")

  [ -z "$main_repo" ] && return

  # Copy .mcp.json if it exists in main repo (this is where Claude Code reads MCP config)
  if [ -f "$main_repo/.mcp.json" ]; then
    cp "$main_repo/.mcp.json" "$workspace_dir/.mcp.json"
    echo -e "${GREEN}Copied .mcp.json from main repo${NC}"
  fi
}

# Expand ~ and environment variables in paths
expand_path() {
  local path="$1"
  # Expand ~ to $HOME
  path="${path/#\~/$HOME}"
  # Expand environment variables
  eval echo "$path"
}

# =============================================================================
# Multi-Project Resolution
# =============================================================================

# Resolve project from @alias argument or fall back to default
# Sets CONFIG_FILE and PROJECT_ALIAS
resolve_project() {
  local arg="${1:-}"

  if [[ "$arg" == @* ]]; then
    local alias="${arg#@}"
    local project_file="$PROJECTS_DIR/${alias}.yaml"
    if [ ! -f "$project_file" ]; then
      error "No project registered with alias: @$alias"
      echo "Run 'crab projects' to see registered projects."
      exit 1
    fi
    CONFIG_FILE="$project_file"
    PROJECT_ALIAS="$alias"
    return
  fi

  # No @alias given — try to resolve default
  resolve_default_project
}

# Resolve the default project when no @alias is given
# Falls back to legacy config.yaml if no projects/ dir
resolve_default_project() {
  # If projects/ dir doesn't exist or is empty, keep legacy CONFIG_FILE
  if [ ! -d "$PROJECTS_DIR" ] || [ -z "$(ls -A "$PROJECTS_DIR" 2>/dev/null)" ]; then
    return
  fi

  # Check GLOBAL_CONFIG for default_project
  local default_alias=""
  if [ -f "$GLOBAL_CONFIG" ]; then
    default_alias=$(yq -r '.default_project // ""' "$GLOBAL_CONFIG" 2>/dev/null)
    [ "$default_alias" = "null" ] && default_alias=""
  fi

  if [ -n "$default_alias" ]; then
    local project_file="$PROJECTS_DIR/${default_alias}.yaml"
    if [ -f "$project_file" ]; then
      CONFIG_FILE="$project_file"
      PROJECT_ALIAS="$default_alias"
      return
    fi
    warn "Default project '@$default_alias' not found, ignoring."
  fi

  # If exactly 1 project exists, auto-select it
  local count=0
  local single_file=""
  for f in "$PROJECTS_DIR"/*.yaml; do
    [ -f "$f" ] || continue
    count=$((count + 1))
    single_file="$f"
  done

  if [ "$count" -eq 1 ]; then
    local alias=$(basename "$single_file" .yaml)
    CONFIG_FILE="$single_file"
    PROJECT_ALIAS="$alias"
    return
  fi

  # Multiple projects, no default — keep legacy CONFIG_FILE (will fail on validate if needed)
}

# List all registered project aliases
list_project_aliases() {
  if [ ! -d "$PROJECTS_DIR" ]; then
    return
  fi
  for f in "$PROJECTS_DIR"/*.yaml; do
    [ -f "$f" ] || continue
    local alias=$(basename "$f" .yaml)
    local repo=$(yq -r '.main_repo // ""' "$f" 2>/dev/null)
    echo "@$alias ($repo)"
  done
}

# Try to resolve the correct project from the current working directory
# Scans all project configs to find one whose workspace_base matches cwd
resolve_project_from_cwd() {
  local cwd=$(pwd)

  if [ ! -d "$PROJECTS_DIR" ]; then
    return 1
  fi

  for f in "$PROJECTS_DIR"/*.yaml; do
    [ -f "$f" ] || continue
    local ws_base=$(yq -r '.workspace_base // ""' "$f" 2>/dev/null)
    [ -z "$ws_base" ] || [ "$ws_base" = "null" ] && continue
    # Expand ~ in workspace_base
    ws_base="${ws_base/#\~/$HOME}"

    if [[ "$cwd" == "$ws_base"* ]]; then
      local alias=$(basename "$f" .yaml)
      CONFIG_FILE="$f"
      PROJECT_ALIAS="$alias"
      return 0
    fi

    # Also check main_repo (user might be in the main repo dir)
    local repo=$(yq -r '.main_repo // ""' "$f" 2>/dev/null)
    [ -z "$repo" ] || [ "$repo" = "null" ] && continue
    repo="${repo/#\~/$HOME}"

    if [[ "$cwd" == "$repo"* ]]; then
      local alias=$(basename "$f" .yaml)
      CONFIG_FILE="$f"
      PROJECT_ALIAS="$alias"
      return 0
    fi
  done

  return 1
}

# Resolve user-configured command aliases from global config
# Expands alias to one or more words, prepending to remaining args
resolve_command_aliases() {
  local cmd="${1:-}"
  [ -z "$cmd" ] && return 1

  if [ ! -f "$GLOBAL_CONFIG" ]; then
    return 1
  fi

  local resolved
  resolved=$(cmd="$cmd" yq -r '.aliases.[strenv(cmd)] // ""' "$GLOBAL_CONFIG" 2>/dev/null)
  if [ -n "$resolved" ] && [ "$resolved" != "null" ]; then
    echo "$resolved"
    return 0
  fi
  return 1
}

# Check if we're in legacy single-project mode
is_legacy_config() {
  # Legacy = config.yaml has main_repo AND projects/ dir is empty/missing
  if [ ! -f "$CONFIG_DIR/config.yaml" ]; then
    return 1
  fi
  local has_main_repo=$(yq -r '.main_repo // ""' "$CONFIG_DIR/config.yaml" 2>/dev/null)
  if [ -z "$has_main_repo" ] || [ "$has_main_repo" = "null" ]; then
    return 1
  fi
  # Check projects dir
  if [ -d "$PROJECTS_DIR" ] && [ -n "$(ls -A "$PROJECTS_DIR" 2>/dev/null)" ]; then
    return 1
  fi
  return 0
}

# Prompt user to migrate legacy config.yaml → projects/<alias>.yaml
check_legacy_migration() {
  echo -e "${CYAN}Multi-project support detected.${NC}"
  echo ""
  echo "Your config at ~/.crabcode/config.yaml can be migrated to the new"
  echo "per-project format. This enables managing multiple repos with crab."
  echo ""
  read -p "Migrate now? [Y/n]: " migrate
  if [ "$migrate" = "n" ] || [ "$migrate" = "N" ]; then
    return
  fi

  # Ask for alias
  local repo_path=$(yq -r '.main_repo // ""' "$CONFIG_DIR/config.yaml" 2>/dev/null)
  local default_alias=$(basename "$repo_path" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g')
  echo ""
  read -p "Project alias [$default_alias]: " alias_input
  alias_input=${alias_input:-$default_alias}

  # Validate alias
  if ! [[ "$alias_input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
    error "Alias must be alphanumeric (dashes and underscores allowed)"
    return 1
  fi

  # Create projects dir and copy config
  mkdir -p "$PROJECTS_DIR"
  cp "$CONFIG_DIR/config.yaml" "$PROJECTS_DIR/${alias_input}.yaml"

  # Rename tmux session if it's running under the old name
  local old_session=$(yq -r '.session_name // ""' "$PROJECTS_DIR/${alias_input}.yaml" 2>/dev/null)
  if [ -n "$old_session" ] && [ "$old_session" != "null" ] && [ "$old_session" != "$alias_input" ]; then
    if tmux has-session -t "$old_session" 2>/dev/null; then
      tmux rename-session -t "$old_session" "$alias_input" 2>/dev/null && \
        echo -e "  ${GREEN}Renamed tmux session: $old_session → $alias_input${NC}"
    fi
  fi

  # Update session_name in the project config to use the alias
  yq -i ".session_name = \"$alias_input\"" "$PROJECTS_DIR/${alias_input}.yaml"

  # Backup old config and create new global config
  cp "$CONFIG_DIR/config.yaml" "$CONFIG_DIR/config.yaml.bak"

  # Write new global config with just default_project
  cat > "$GLOBAL_CONFIG" << EOF
# Crabcode Global Configuration
default_project: $alias_input
EOF

  CONFIG_FILE="$PROJECTS_DIR/${alias_input}.yaml"
  PROJECT_ALIAS="$alias_input"

  echo ""
  success "Migrated to @$alias_input"
  echo "  Project config: $PROJECTS_DIR/${alias_input}.yaml"
  echo "  Backup: ~/.crabcode/config.yaml.bak"
  echo "  Default set to: @$alias_input"
  echo ""
}

# =============================================================================
# Config Variables (loaded lazily)
# =============================================================================

_config_loaded=false

load_config() {
  if [ "$_config_loaded" = true ]; then
    return
  fi

  check_yq

  if ! config_exists; then
    return
  fi

  SESSION_NAME=$(config_get "session_name" "crab")
  WORKSPACE_BASE=$(expand_path "$(config_get "workspace_base")")
  MAIN_REPO=$(expand_path "$(config_get "main_repo")")

  WORKSPACE_COUNT=$(config_get "workspaces.count" "5")
  WORKSPACE_PREFIX=$(config_get "workspaces.prefix" "workspace")
  BRANCH_PATTERN=$(config_get "workspaces.branch_pattern" "workspace-{N}")

  API_PORT_BASE=$(config_get "ports.api_base" "3200")
  APP_PORT_BASE=$(config_get "ports.app_base" "3000")

  # Override from env_sync.port_bases if available (new config format)
  local _pb_api=$(yq -r '.env_sync.port_bases.api // ""' "$CONFIG_FILE" 2>/dev/null)
  local _pb_app=$(yq -r '.env_sync.port_bases.app // ""' "$CONFIG_FILE" 2>/dev/null)
  [ -n "$_pb_api" ] && [ "$_pb_api" != "null" ] && API_PORT_BASE="$_pb_api"
  [ -n "$_pb_app" ] && [ "$_pb_app" != "null" ] && APP_PORT_BASE="$_pb_app"

  # Shared volume settings
  SHARED_VOLUME_PATH=$(expand_path "$(config_get "shared_volume.path" "$CONFIG_DIR/shared")")
  SHARED_VOLUME_LINK=$(config_get "shared_volume.link_as" ".local")
  SHARED_VOLUME_ENABLED=$(config_get "shared_volume.enabled" "true")

  _config_loaded=true

  # WIP isolation: per-project WIP directories
  if [ -n "${PROJECT_ALIAS:-}" ]; then
    WIP_BASE="$CONFIG_DIR/wip/$PROJECT_ALIAS"
  fi
}

# =============================================================================
# Tmux Pane Helpers
# =============================================================================

# Get tmux pane indices based on pane-base-index setting
# Layout: terminal (top-left), main (right), server (bottom-left)
# Returns: "terminal_pane:main_pane:server_pane"
get_pane_indices() {
  local pane_base=$(tmux show-options -gv pane-base-index 2>/dev/null || echo "0")
  local p_terminal=$pane_base
  local p_server=$((pane_base + 1))
  local p_main=$((pane_base + 2))
  echo "$p_terminal:$p_main:$p_server"
}

# =============================================================================
# Port Management
# =============================================================================

# Find an available port starting from a base
find_available_port() {
  local base_port=$1
  local port=$base_port
  while lsof -i ":$port" &>/dev/null; do
    port=$((port + 1))
    # Safety limit - don't scan forever
    if [ $port -gt $((base_port + 100)) ]; then
      echo "$base_port"  # Give up, return original
      return
    fi
  done
  echo "$port"
}

# Get base port for a port type from env_sync.port_bases config
get_port_base() {
  local port_type=$1
  local val=$(yq -r ".env_sync.port_bases.${port_type} // \"\"" "$CONFIG_FILE" 2>/dev/null)
  if [ -n "$val" ] && [ "$val" != "null" ]; then
    echo "$val"
    return
  fi
  # Backward compat: fall back to globals
  case "$port_type" in
    api) echo "${API_PORT_BASE:-3200}" ;;
    app) echo "${APP_PORT_BASE:-3000}" ;;
    *) echo "" ;;
  esac
}

# Get all port type keys from env_sync.port_bases
get_port_types() {
  local types=$(yq -r '.env_sync.port_bases // {} | keys | .[]' "$CONFIG_FILE" 2>/dev/null)
  if [ -n "$types" ]; then
    echo "$types"
  else
    echo -e "api\napp"
  fi
}

# Get workspace ports (api and app) for workspace N
# Returns: api_port:app_port:need_override
get_workspace_ports() {
  local num=$1
  local dir=$2

  # Use port_spacing from config (default 10) for proper spacing between workspaces
  local port_spacing=$(yq -r '.env_sync.port_spacing // 10' "$CONFIG_FILE" 2>/dev/null)
  [ "$port_spacing" = "null" ] && port_spacing=10

  local default_api=$(($(get_port_base "api") + (num * port_spacing)))
  local default_app=$(($(get_port_base "app") + (num * port_spacing)))

  # Read from .env if env_sync is configured
  local env_api=$(read_env_port "$dir" "api")
  local env_app=$(read_env_port "$dir" "app")

  [ -z "$env_api" ] && env_api="$default_api"
  [ -z "$env_app" ] && env_app="$default_app"

  local actual_api=$(find_available_port "$env_api")
  local actual_app=$(find_available_port "$env_app")

  local need_override="false"
  if [ "$actual_api" != "$env_api" ] || [ "$actual_app" != "$env_app" ]; then
    need_override="true"
  fi

  echo "$actual_api:$actual_app:$need_override:$env_api:$env_app"
}

# Read port from .env file based on env_sync config
read_env_port() {
  local dir=$1
  local port_type=$2  # "api", "app", "admin", etc.

  local env_files_count=$(yq -r '.env_sync.files | length // 0' "$CONFIG_FILE" 2>/dev/null)

  for ((i=0; i<env_files_count; i++)); do
    local env_path=$(yq -r ".env_sync.files[$i].path" "$CONFIG_FILE" 2>/dev/null)
    [ -z "$env_path" ] || [ "$env_path" = "null" ] && continue

    local full_path="$dir/$env_path"
    [ ! -f "$full_path" ] && continue

    local ports_type=$(yq -r ".env_sync.files[$i].ports | type" "$CONFIG_FILE" 2>/dev/null)

    local port_var=""
    if [ "$ports_type" = "!!map" ]; then
      # New format: find key whose value matches requested type
      port_var=$(yq -r ".env_sync.files[$i].ports | to_entries | .[] | select(.value == \"$port_type\") | .key" "$CONFIG_FILE" 2>/dev/null)
    elif [ "$ports_type" = "!!seq" ]; then
      # Old format: positional — index 0=api, index 1=app
      local port_idx
      case "$port_type" in
        api) port_idx=0 ;;
        app) port_idx=1 ;;
        *) continue ;;
      esac
      port_var=$(yq -r ".env_sync.files[$i].ports[$port_idx] // \"\"" "$CONFIG_FILE" 2>/dev/null)
    fi

    [ -z "$port_var" ] || [ "$port_var" = "null" ] && continue

    local value=$(grep "^${port_var}=" "$full_path" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]')
    if [ -n "$value" ]; then
      if [[ "$value" =~ :([0-9]+) ]]; then
        echo "${BASH_REMATCH[1]}"
        return
      elif [[ "$value" =~ ^[0-9]+$ ]]; then
        echo "$value"
        return
      fi
    fi
  done
}

# Sync .env files for a workspace
# New format supports:
#   - copy_from: template file to copy from
#   - ports: array of port vars to manage (increment per workspace)
#   - refs: map of URL vars that reference managed ports
sync_env_files() {
  local dir=$1
  local workspace_num=$2
  local quiet=${3:-false}

  local env_files_count=$(yq -r '.env_sync.files | length // 0' "$CONFIG_FILE" 2>/dev/null)
  [ "$env_files_count" = "0" ] && return

  # Temp file to store resolved ports (bash 3.x compatible)
  local ports_file=$(mktemp)
  trap "rm -f '$ports_file'" RETURN

  # First pass: resolve all managed ports
  for ((i=0; i<env_files_count; i++)); do
    local env_path=$(yq -r ".env_sync.files[$i].path" "$CONFIG_FILE" 2>/dev/null)
    local copy_from=$(yq -r ".env_sync.files[$i].copy_from // \"\"" "$CONFIG_FILE" 2>/dev/null)

    [ -z "$env_path" ] || [ "$env_path" = "null" ] && continue

    local full_path="$dir/$env_path"
    local template_path=""

    # Determine template path
    if [ -n "$copy_from" ] && [ "$copy_from" != "null" ]; then
      template_path="$dir/$copy_from"
    else
      # Fallback: look for .env.example
      template_path="$dir/${env_path}.example"
      [ ! -f "$template_path" ] && template_path="$dir/${env_path%.env}.env.example"
    fi

    # Create from template if target doesn't exist
    if [ ! -f "$full_path" ] && [ -f "$template_path" ]; then
      mkdir -p "$(dirname "$full_path")"
      cp "$template_path" "$full_path"
      [ "$quiet" != "true" ] && echo -e "  ${YELLOW}Created $env_path from template${NC}"
    fi

    # Create empty file if no template but has managed ports/refs
    if [ ! -f "$full_path" ]; then
      local has_ports=$(yq -r ".env_sync.files[$i].ports | type" "$CONFIG_FILE" 2>/dev/null)
      local has_refs=$(yq -r ".env_sync.files[$i].refs | type" "$CONFIG_FILE" 2>/dev/null)
      if [ "$has_ports" != "!!null" ] || [ "$has_refs" != "!!null" ]; then
        mkdir -p "$(dirname "$full_path")"
        touch "$full_path"
        [ "$quiet" != "true" ] && echo -e "  ${YELLOW}Created empty $env_path${NC}"
      fi
    fi

    # Skip if file still doesn't exist
    [ ! -f "$full_path" ] && continue

    # Get port spacing
    local port_spacing=$(yq -r '.env_sync.port_spacing // 10' "$CONFIG_FILE" 2>/dev/null)
    [ "$port_spacing" = "null" ] && port_spacing=10

    # Detect ports format: !!map (new) or !!seq (old) or !!null (none)
    local ports_type=$(yq -r ".env_sync.files[$i].ports | type" "$CONFIG_FILE" 2>/dev/null)

    if [ "$ports_type" = "!!map" ]; then
      # NEW FORMAT: ports is a map {VAR_NAME: port_type}
      # Base ports come from config, NOT from .env files — this prevents compounding
      local port_entries=$(yq -r ".env_sync.files[$i].ports | to_entries | .[] | \"\(.key)=\(.value)\"" "$CONFIG_FILE" 2>/dev/null)
      echo "$port_entries" | while IFS='=' read -r port_var port_type; do
        [ -z "$port_var" ] || [ -z "$port_type" ] && continue
        local base_port=$(get_port_base "$port_type")
        [ -z "$base_port" ] && continue

        local new_port=$((base_port + (workspace_num * port_spacing)))

        # Store resolved port for refs lookup
        echo "$port_var=$new_port" >> "$ports_file"

        # Read current value from .env
        local current_value=$(grep "^${port_var}=" "$full_path" 2>/dev/null | cut -d= -f2-)

        if [ -z "$current_value" ]; then
          # Var doesn't exist — append with appropriate format
          local new_value
          if [[ "$port_var" == *"_URL"* ]] || [[ "$port_var" == *"_HOST"* ]]; then
            new_value="http://localhost:${new_port}"
          else
            new_value="$new_port"
          fi
          echo "${port_var}=${new_value}" >> "$full_path"
          [ "$quiet" != "true" ] && echo -e "  ${GREEN}$env_path: $port_var → $new_port (added)${NC}"
        else
          # Build new value preserving existing format (URL vs number)
          local new_value
          if echo "$current_value" | grep -qE '^[0-9]+$'; then
            new_value="$new_port"
          else
            local current_port=$(echo "$current_value" | grep -oE ':[0-9]+' | tail -1 | tr -d ':')
            if [ -n "$current_port" ]; then
              new_value=$(echo "$current_value" | sed "s/:${current_port}/:${new_port}/g")
            else
              new_value="$current_value"
            fi
          fi

          if [ "$current_value" != "$new_value" ]; then
            if [[ "$OSTYPE" == "darwin"* ]]; then
              sed -i '' "s|^${port_var}=.*|${port_var}=$new_value|" "$full_path"
            else
              sed -i "s|^${port_var}=.*|${port_var}=$new_value|" "$full_path"
            fi
            [ "$quiet" != "true" ] && echo -e "  ${GREEN}$env_path: $port_var → $new_port${NC}"
          fi
        fi
      done

    elif [ "$ports_type" = "!!seq" ]; then
      # OLD FORMAT: backward compat — ports is an array of var names
      local old_port_var=$(yq -r ".env_sync.files[$i].port_var // \"\"" "$CONFIG_FILE" 2>/dev/null)
      local ports_json=$(yq -r ".env_sync.files[$i].ports // []" "$CONFIG_FILE" 2>/dev/null)

      if [ -n "$old_port_var" ] && [ "$old_port_var" != "null" ]; then
        ports_json="[\"$old_port_var\"]"
      fi

      local ports_count=$(echo "$ports_json" | yq -r 'length // 0' 2>/dev/null)
      for ((p=0; p<ports_count; p++)); do
        local port_var=$(echo "$ports_json" | yq -r ".[$p]" 2>/dev/null)
        [ -z "$port_var" ] || [ "$port_var" = "null" ] && continue

        local base_value=""
        local main_env="$MAIN_REPO/$env_path"
        [ -f "$main_env" ] && base_value=$(grep "^${port_var}=" "$main_env" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]"'"'")

        if [ -z "$base_value" ]; then
          base_value=$(grep "^${port_var}=" "$full_path" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]"'"'")
        fi

        local current_value=$(grep "^${port_var}=" "$full_path" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]"'"'")
        [ -z "$current_value" ] && current_value="$base_value"

        local base_port=""
        if echo "$base_value" | grep -qE '^[0-9]+$'; then
          base_port="$base_value"
        elif echo "$base_value" | grep -qE ':[0-9]+'; then
          base_port=$(echo "$base_value" | grep -oE ':[0-9]+' | tail -1 | tr -d ':')
        fi

        [ -z "$base_port" ] && continue

        local new_port=$((base_port + (workspace_num * port_spacing)))

        while lsof -i ":$new_port" &>/dev/null; do
          new_port=$((new_port + 1))
          if [ $new_port -gt $((base_port + (workspace_num * port_spacing) + port_spacing - 1)) ]; then
            new_port=$((base_port + (workspace_num * port_spacing)))
            break
          fi
        done

        echo "$port_var=$new_port" >> "$ports_file"

        local new_value
        if echo "$current_value" | grep -qE '^[0-9]+$'; then
          new_value="$new_port"
        else
          new_value=$(echo "$current_value" | sed "s/:${base_port}/:${new_port}/g")
        fi

        if [ "$current_value" != "$new_value" ]; then
          if [[ "$OSTYPE" == "darwin"* ]]; then
            sed -i '' "s|^${port_var}=.*|${port_var}=$new_value|" "$full_path"
          else
            sed -i "s|^${port_var}=.*|${port_var}=$new_value|" "$full_path"
          fi
          [ "$quiet" != "true" ] && echo -e "  ${GREEN}$env_path: $port_var → $new_port${NC}"
        fi
      done
    fi
  done

  # Second pass: process refs (URL vars that reference managed ports)
  for ((i=0; i<env_files_count; i++)); do
    local env_path=$(yq -r ".env_sync.files[$i].path" "$CONFIG_FILE" 2>/dev/null)
    [ -z "$env_path" ] || [ "$env_path" = "null" ] && continue

    local full_path="$dir/$env_path"
    [ ! -f "$full_path" ] && continue

    # Get refs map
    local refs_keys=$(yq -r ".env_sync.files[$i].refs // {} | keys | .[]" "$CONFIG_FILE" 2>/dev/null)
    [ -z "$refs_keys" ] && continue

    echo "$refs_keys" | while read -r ref_var; do
      [ -z "$ref_var" ] && continue

      local ref_source=$(yq -r ".env_sync.files[$i].refs[\"$ref_var\"]" "$CONFIG_FILE" 2>/dev/null)
      [ -z "$ref_source" ] || [ "$ref_source" = "null" ] && continue

      # Check for :port suffix (e.g., API_URL:port extracts just the port number)
      local extract_port_only=false
      local ref_port_var="$ref_source"
      if [[ "$ref_source" == *":port" ]]; then
        extract_port_only=true
        ref_port_var="${ref_source%:port}"
      fi

      # Look up the resolved port
      local resolved_port=$(grep "^${ref_port_var}=" "$ports_file" 2>/dev/null | cut -d= -f2)
      [ -z "$resolved_port" ] && continue

      # Read current value
      local current_value=$(grep "^${ref_var}=" "$full_path" 2>/dev/null | cut -d= -f2-)

      if [ "$extract_port_only" = true ]; then
        # Set target to plain port number
        if [ -z "$current_value" ]; then
          # Var doesn't exist — append plain port number
          echo "${ref_var}=$resolved_port" >> "$full_path"
          [ "$quiet" != "true" ] && echo -e "  ${GREEN}$env_path: $ref_var → $resolved_port (added)${NC}"
        elif [ "$current_value" != "$resolved_port" ]; then
          if [[ "$OSTYPE" == "darwin"* ]]; then
            sed -i '' "s|^${ref_var}=.*|${ref_var}=$resolved_port|" "$full_path"
          else
            sed -i "s|^${ref_var}=.*|${ref_var}=$resolved_port|" "$full_path"
          fi
          [ "$quiet" != "true" ] && echo -e "  ${GREEN}$env_path: $ref_var → $resolved_port${NC}"
        fi
      else
        # Replace port in URL (existing behavior)
        [ -z "$current_value" ] && continue

        # Extract current port from URL
        local current_port=$(echo "$current_value" | grep -oE ':[0-9]+' | tail -1 | tr -d ':')
        [ -z "$current_port" ] && continue

        if [ "$current_port" != "$resolved_port" ]; then
          local new_value=$(echo "$current_value" | sed "s/:${current_port}/:${resolved_port}/g")

          if [[ "$OSTYPE" == "darwin"* ]]; then
            sed -i '' "s|^${ref_var}=.*|${ref_var}=$new_value|" "$full_path"
          else
            sed -i "s|^${ref_var}=.*|${ref_var}=$new_value|" "$full_path"
          fi
          [ "$quiet" != "true" ] && echo -e "  ${GREEN}$env_path: $ref_var → :$resolved_port${NC}"
        fi
      fi
    done
  done
}

# Show port usage across all workspaces
show_ports() {
  load_config
  validate_config

  local port_spacing=$(yq -r '.env_sync.port_spacing // 10' "$CONFIG_FILE" 2>/dev/null)
  [ "$port_spacing" = "null" ] && port_spacing=10

  # Get port types dynamically
  local types=($(get_port_types))

  echo -e "${CYAN}Port Usage Across Workspaces${NC}"
  echo ""

  # Build header
  local header="  %-20s"
  for t in "${types[@]}"; do
    header+=" %-8s"
  done
  header+=" %-20s\n"
  printf "$header" "WORKSPACE" "${types[@]^^}" "STATUS"
  local sep="  "
  local sep_len=$((20 + ${#types[@]} * 9 + 20))
  for ((s=0; s<sep_len; s++)); do sep+="─"; done
  echo "$sep"

  for ((i=1; i<=WORKSPACE_COUNT; i++)); do
    local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$i"
    [ ! -d "$dir" ] && continue

    # Calculate ports for each type
    local ports=()
    local statuses=()
    for t in "${types[@]}"; do
      local base=$(get_port_base "$t")
      local default_port=$((base + (i * port_spacing)))
      local port=$(read_env_port "$dir" "$t")
      [ -z "$port" ] && port="$default_port"
      ports+=("$port")

      # Check port status
      local status="free"
      if lsof -i ":$port" &>/dev/null; then
        local pid=$(lsof -t -i ":$port" 2>/dev/null | head -1)
        local cwd=$(lsof -p "$pid" 2>/dev/null | grep cwd | awk '{print $NF}')
        if [[ "$cwd" == *"$WORKSPACE_PREFIX-$i"* ]]; then
          status="running"
        elif [[ "$cwd" == *"$WORKSPACE_PREFIX-"* ]]; then
          local other_ws=$(echo "$cwd" | grep -oE "${WORKSPACE_PREFIX}-[0-9]+" | sed "s/${WORKSPACE_PREFIX}-/ws/")
          status="TAKEN:$other_ws"
        else
          status="in-use"
        fi
      fi
      statuses+=("$status")
    done

    # Print port columns
    printf "  %-20s" "$WORKSPACE_PREFIX-$i"
    for p in "${ports[@]}"; do
      printf " %-8s" "$p"
    done

    # Print status for each type
    local status_str=""
    for ((ti=0; ti<${#types[@]}; ti++)); do
      local t_upper="${types[$ti]^^}"
      local s="${statuses[$ti]}"
      local display
      case "$s" in
        "free") display="${YELLOW}free${NC}" ;;
        "running") display="${GREEN}●${NC}" ;;
        TAKEN:*) display="${RED}${s}${NC}" ;;
        *) display="${YELLOW}busy${NC}" ;;
      esac
      [ -n "$status_str" ] && status_str+="  "
      status_str+="${t_upper}: $display"
    done
    echo -e " $status_str"
  done
  echo ""
  echo -e "  Legend: ${GREEN}●${NC}=running  ${YELLOW}free${NC}=available  ${RED}TAKEN:wsN${NC}=conflict"
}

# =============================================================================
# Git Operations
# =============================================================================

# Get the branch name for workspace N
get_branch_name() {
  local num=$1
  echo "${BRANCH_PATTERN//\{N\}/$num}"
}

# Create workspace directory with git worktree
create_workspace() {
  local num=$1
  local workspace_dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
  local branch_name=$(get_branch_name "$num")

  if [ -d "$workspace_dir" ]; then
    echo -e "  ${YELLOW}Workspace $num already exists${NC}"
    return 0
  fi

  echo -e "${CYAN}Creating workspace $num...${NC}"

  # Check if main repo exists
  if [ ! -d "$MAIN_REPO" ]; then
    error "Main repo not found at $MAIN_REPO"
    exit 1
  fi

  # Ensure workspace base exists
  mkdir -p "$WORKSPACE_BASE"

  # Create git worktree
  echo "  Creating git worktree..."
  cd "$MAIN_REPO"
  git fetch origin 2>/dev/null || true  # May fail if no remote

  # Check if branch exists
  if git show-ref --verify --quiet "refs/heads/$branch_name"; then
    git worktree add "$workspace_dir" "$branch_name"
  elif git show-ref --verify --quiet "refs/remotes/origin/main"; then
    git worktree add -b "$branch_name" "$workspace_dir" origin/main
  else
    # No origin/main - use current HEAD
    git worktree add -b "$branch_name" "$workspace_dir" HEAD
  fi

  # Initialize submodules if configured
  init_submodules "$workspace_dir"

  # Setup shared volume
  setup_shared_volume "$workspace_dir"

  # Sync .env files (pass workspace number for port calculation)
  sync_env_files "$workspace_dir" "$num"

  # Install dependencies (force install for new workspaces)
  local install_cmd=$(config_get "install_command" "")
  if [ -n "$install_cmd" ]; then
    echo -e "${YELLOW}Installing dependencies...${NC}"
    cd "$workspace_dir"
    # Get env vars from install_env config
    local install_env=$(config_get "install_env" "")
    local run_cmd="$install_cmd"
    if [ -n "$install_env" ] && [ "$install_env" != "null" ]; then
      # Prepend env var to command so it's passed to subprocesses
      run_cmd="$install_env $install_cmd"
    fi
    if bash -c "$run_cmd"; then
      touch "$workspace_dir/node_modules/.crabcode-installed"
      echo -e "${GREEN}Dependencies installed${NC}"
    else
      warn "Failed to install dependencies - run manually: $install_cmd"
    fi

    # Install submodule dependencies
    local submodules_count=$(yq -r '.submodules | length // 0' "$CONFIG_FILE" 2>/dev/null)
    for ((i=0; i<submodules_count; i++)); do
      local sub_path=$(yq -r ".submodules[$i].path" "$CONFIG_FILE" 2>/dev/null)
      local sub_install=$(yq -r ".submodules[$i].install_command // \"\"" "$CONFIG_FILE" 2>/dev/null)
      [ -z "$sub_path" ] || [ "$sub_path" = "null" ] && continue
      [ -z "$sub_install" ] || [ "$sub_install" = "null" ] && continue

      if [ -d "$workspace_dir/$sub_path" ]; then
        echo -e "${YELLOW}Installing $sub_path dependencies...${NC}"
        cd "$workspace_dir/$sub_path"
        # Prepend env var to submodule install command too
        local sub_run_cmd="$sub_install"
        if [ -n "$install_env" ] && [ "$install_env" != "null" ]; then
          sub_run_cmd="$install_env $sub_install"
        fi
        if bash -c "$sub_run_cmd"; then
          # Only touch marker if node_modules exists
          [ -d "node_modules" ] && touch "node_modules/.crabcode-installed"
        fi
        cd "$workspace_dir"
      fi
    done
  fi

  # Sync MCP servers from main repo to new workspace
  sync_mcp_servers "$workspace_dir"

  success "Workspace $num created at $workspace_dir"
}

# Initialize submodules in workspace
init_submodules() {
  local dir=$1

  local submodules_count=$(yq -r '.submodules | length // 0' "$CONFIG_FILE" 2>/dev/null)
  [ "$submodules_count" = "0" ] && return

  cd "$dir"

  # Find the main git dir (worktrees share .git/modules with main repo)
  local git_dir=$(git rev-parse --git-common-dir 2>/dev/null)

  for ((i=0; i<submodules_count; i++)); do
    local sub_path=$(yq -r ".submodules[$i].path" "$CONFIG_FILE" 2>/dev/null)
    local reset_to=$(yq -r ".submodules[$i].reset_to" "$CONFIG_FILE" 2>/dev/null)

    [ -z "$sub_path" ] || [ "$sub_path" = "null" ] && continue

    # Check if submodule already exists in this worktree
    if [ -d "$dir/$sub_path/.git" ] || [ -f "$dir/$sub_path/.git" ]; then
      echo "  Submodule $sub_path already initialized"
      continue
    fi

    # FAST PATH: Copy submodule from main repo if it exists there
    # This is MUCH faster than git submodule update which re-checksout all files
    local main_sub="$MAIN_REPO/$sub_path"
    if [ -d "$main_sub" ] && { [ -d "$main_sub/.git" ] || [ -f "$main_sub/.git" ]; }; then
      echo "  Copying submodule: $sub_path (fast copy from main repo)"

      # Remove empty directory if exists
      [ -d "$dir/$sub_path" ] && rm -rf "$dir/$sub_path"

      # Copy the submodule directory (uses copy-on-write on APFS/modern filesystems)
      cp -R "$MAIN_REPO/$sub_path" "$dir/$sub_path"

      # Fix the .git file to point to the shared modules dir
      # Submodules use a .git file (not directory) pointing to ../.git/modules/name
      if [ -f "$dir/$sub_path/.git" ]; then
        # Update gitdir path to point to shared modules
        local modules_dir="$git_dir/modules/$sub_path"
        if [ -d "$modules_dir" ]; then
          echo "gitdir: $modules_dir" > "$dir/$sub_path/.git"
        fi
      fi
    else
      # SLOW PATH: Fall back to git submodule update
      local modules_dir="$git_dir/modules/$sub_path"
      if [ -d "$modules_dir" ]; then
        echo "  Checking out submodule: $sub_path (using cached git data)"
        git submodule update "$sub_path" 2>/dev/null || git submodule update --init "$sub_path" 2>/dev/null || true
      else
        echo "  Fetching submodule: $sub_path (first time, may take a moment)"
        git submodule update --init "$sub_path" 2>/dev/null || true
      fi
    fi

    # Reset to configured ref if specified
    if [ -n "$reset_to" ] && [ "$reset_to" != "null" ] && [ -d "$dir/$sub_path" ]; then
      cd "$dir/$sub_path"
      # Only fetch if the ref doesn't exist locally
      if ! git rev-parse --verify "$reset_to" &>/dev/null; then
        echo "  Fetching $reset_to for $sub_path..."
        git fetch origin --quiet 2>/dev/null || true
      fi
      git checkout main 2>/dev/null || git checkout -b main 2>/dev/null || true
      git reset --hard "$reset_to" 2>/dev/null || true
      cd "$dir"
    fi
  done
}

# Reset submodules to configured state
reset_submodules() {
  local dir=$1

  local submodules_count=$(yq -r '.submodules | length // 0' "$CONFIG_FILE" 2>/dev/null)
  [ "$submodules_count" = "0" ] && return

  for ((i=0; i<submodules_count; i++)); do
    local sub_path=$(yq -r ".submodules[$i].path" "$CONFIG_FILE" 2>/dev/null)
    local reset_to=$(yq -r ".submodules[$i].reset_to" "$CONFIG_FILE" 2>/dev/null)

    [ -z "$sub_path" ] || [ "$sub_path" = "null" ] && continue

    if [ -d "$dir/$sub_path" ]; then
      echo "  Resetting submodule: $sub_path"
      cd "$dir/$sub_path"
      git checkout -- . 2>/dev/null || true
      git clean -fd 2>/dev/null || true
      git checkout main 2>/dev/null || git checkout -b main origin/main 2>/dev/null || true
      git fetch origin main --quiet 2>/dev/null || true
      if [ -n "$reset_to" ] && [ "$reset_to" != "null" ]; then
        git reset --hard "$reset_to"
      else
        git reset --hard origin/main
      fi
      cd "$dir"
      echo -e "  ${GREEN}Submodule $sub_path reset${NC}"
    fi
  done
}

# =============================================================================
# Shared Volume Management
# =============================================================================

# Setup shared volume symlink in a workspace
# Handles migration of existing .local directories
setup_shared_volume() {
  local dir=$1

  # Skip if shared volume is disabled
  if [ "$SHARED_VOLUME_ENABLED" != "true" ]; then
    return 0
  fi

  local link_name="$SHARED_VOLUME_LINK"
  local link_path="$dir/$link_name"
  local shared_path="$SHARED_VOLUME_PATH"

  # Create shared volume directory if it doesn't exist
  if [ ! -d "$shared_path" ]; then
    echo "  Creating shared volume at $shared_path..."
    mkdir -p "$shared_path"
  fi

  # Check current state of link path
  if [ -L "$link_path" ]; then
    # Already a symlink - check if it points to the right place
    local current_target=$(readlink "$link_path")
    if [ "$current_target" = "$shared_path" ]; then
      # Already correctly linked
      return 0
    else
      # Wrong target - remove and recreate
      echo "  Updating $link_name symlink..."
      rm "$link_path"
    fi
  elif [ -d "$link_path" ]; then
    # Existing directory - migrate contents to shared volume
    echo "  Migrating existing $link_name to shared volume..."

    # Copy contents to shared volume (preserving existing shared files)
    if [ "$(ls -A "$link_path" 2>/dev/null)" ]; then
      # Directory has contents - migrate them
      local migrated=0
      for item in "$link_path"/*; do
        [ ! -e "$item" ] && continue
        local basename=$(basename "$item")
        if [ -e "$shared_path/$basename" ]; then
          # File/dir exists in shared - create workspace-specific backup
          local ws_name=$(basename "$dir")
          local backup_name="${basename}.from-${ws_name}"
          echo "    $basename exists in shared, saving as $backup_name"
          mv "$item" "$shared_path/$backup_name"
        else
          echo "    Moving $basename to shared volume"
          mv "$item" "$shared_path/"
        fi
        migrated=$((migrated + 1))
      done
      if [ $migrated -gt 0 ]; then
        echo -e "  ${GREEN}Migrated $migrated item(s) to shared volume${NC}"
      fi
    fi

    # Remove the now-empty directory
    rmdir "$link_path" 2>/dev/null || rm -rf "$link_path"
  elif [ -e "$link_path" ]; then
    # Some other file type - backup and remove
    echo "  Backing up existing $link_name..."
    mv "$link_path" "$link_path.backup.$(date +%Y%m%d%H%M%S)"
  fi

  # Create symlink
  ln -s "$shared_path" "$link_path"
  echo -e "  ${GREEN}Linked $link_name → $shared_path${NC}"

  # Add to .gitignore if not already there
  local gitignore="$dir/.gitignore"
  if [ -f "$gitignore" ]; then
    if ! grep -q "^${link_name}$" "$gitignore" 2>/dev/null; then
      echo "" >> "$gitignore"
      echo "# Shared local volume (crabcode)" >> "$gitignore"
      echo "$link_name" >> "$gitignore"
      echo "  Added $link_name to .gitignore"
    fi
  else
    # Create .gitignore with the link name
    echo "# Shared local volume (crabcode)" > "$gitignore"
    echo "$link_name" >> "$gitignore"
    echo "  Created .gitignore with $link_name"
  fi
}

# =============================================================================
# Workspace Operations
# =============================================================================

# List all workspaces
list_workspaces() {
  load_config

  echo -e "${CYAN}Crabcode Workspaces${NC}"
  echo ""

  if ! config_exists; then
    echo -e "${YELLOW}No config file found.${NC}"
    echo ""
    echo "Run 'crabcode init' to set up crabcode."
    return
  fi

  validate_config

  # Check if main session exists
  if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
    echo -e "${GREEN}Active session: $SESSION_NAME${NC}"
    echo "Windows:"
    tmux list-windows -t "$SESSION_NAME" 2>/dev/null | while read -r line; do
      echo "  $line"
    done
    echo ""
  else
    echo -e "${YELLOW}No active $SESSION_NAME session.${NC}"
    echo ""
  fi

  echo -e "${YELLOW}Available workspaces:${NC}"
  local found=false

  # Find all existing workspaces (may be more than configured count)
  local max_ws=$WORKSPACE_COUNT
  for dir in "$WORKSPACE_BASE/$WORKSPACE_PREFIX-"*; do
    if [ -d "$dir" ]; then
      local num=$(basename "$dir" | sed "s/${WORKSPACE_PREFIX}-//")
      if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -gt "$max_ws" ]; then
        max_ws=$num
      fi
    fi
  done

  for ((i=1; i<=max_ws; i++)); do
    local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$i"
    if [ -d "$dir" ]; then
      found=true
      local branch=$(cd "$dir" && git branch --show-current 2>/dev/null || echo "unknown")
      local port_spacing=$(yq -r '.env_sync.port_spacing // 10' "$CONFIG_FILE" 2>/dev/null)
      [ "$port_spacing" = "null" ] && port_spacing=10
      local api_port=$(($(get_port_base "api") + (i * port_spacing)))
      local env_port=$(read_env_port "$dir" "api")
      [ -n "$env_port" ] && api_port="$env_port"

      # Check if this workspace has a window in the session
      local status="${YELLOW}[available]${NC}"
      if tmux has-session -t "$SESSION_NAME" 2>/dev/null && \
         tmux list-windows -t "$SESSION_NAME" -F "#{window_name}" 2>/dev/null | grep -q "^ws$i$"; then
        status="${GREEN}[active]${NC}"
      fi
      echo -e "  $i: $WORKSPACE_PREFIX-$i ($branch) :$api_port $status"
    fi
  done

  if [ "$found" = false ]; then
    echo "  No workspaces found."
    echo ""
    echo "Run 'crab ws new' or 'crab ws 1' to create a workspace."
  fi
}

# Interactive workspace menu
interactive_workspace_menu() {
  load_config

  if ! config_exists; then
    echo -e "${YELLOW}No config file found.${NC}"
    echo ""
    echo "Run 'crab init' to set up crabcode."
    return
  fi

  validate_config

  # Show workspaces
  echo -e "${CYAN}Crabcode Workspaces${NC}"
  echo ""

  # Check if main session exists
  if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
    echo -e "${GREEN}Active session: $SESSION_NAME${NC}"
    echo "Windows:"
    tmux list-windows -t "$SESSION_NAME" 2>/dev/null | while read -r line; do
      echo "  $line"
    done
    echo ""
  else
    echo -e "${YELLOW}No active $SESSION_NAME session.${NC}"
    echo ""
  fi

  # Find all existing workspaces
  local max_ws=$WORKSPACE_COUNT
  local existing_workspaces=""
  for dir in "$WORKSPACE_BASE/$WORKSPACE_PREFIX-"*; do
    if [ -d "$dir" ]; then
      local num=$(basename "$dir" | sed "s/${WORKSPACE_PREFIX}-//")
      if [[ "$num" =~ ^[0-9]+$ ]]; then
        existing_workspaces="$existing_workspaces $num"
        [ "$num" -gt "$max_ws" ] && max_ws=$num
      fi
    fi
  done

  echo -e "${YELLOW}Workspaces:${NC}"
  local found=false
  for ((i=1; i<=max_ws; i++)); do
    local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$i"
    if [ -d "$dir" ]; then
      found=true
      local branch=$(cd "$dir" && git branch --show-current 2>/dev/null || echo "unknown")
      local port_spacing=$(yq -r '.env_sync.port_spacing // 10' "$CONFIG_FILE" 2>/dev/null)
      [ "$port_spacing" = "null" ] && port_spacing=10
      local api_port=$(($(get_port_base "api") + (i * port_spacing)))
      local env_port=$(read_env_port "$dir" "api")
      [ -n "$env_port" ] && api_port="$env_port"

      local status="${YELLOW}[available]${NC}"
      if tmux has-session -t "$SESSION_NAME" 2>/dev/null && \
         tmux list-windows -t "$SESSION_NAME" -F "#{window_name}" 2>/dev/null | grep -q "^ws$i$"; then
        status="${GREEN}[active]${NC}"
      fi
      echo -e "  $i: $WORKSPACE_PREFIX-$i ($branch) :$api_port $status"
    fi
  done

  if [ "$found" = false ]; then
    echo "  (none created yet)"
  fi

  # Show menu
  echo ""
  echo -e "${CYAN}Actions:${NC}"
  echo "  [1-9] Open workspace    [n] New    [r] Restart    [c] Cleanup    [q] Quit"
  echo ""
  printf "  > "

  read -r choice

  case "$choice" in
    q|Q|quit|exit|"")
      return 0
      ;;
    n|N|new)
      create_new_workspace
      ;;
    r|R|restart)
      echo -n "  Restart which workspace? [1-9]: "
      read -r ws_num
      if [[ "$ws_num" =~ ^[0-9]+$ ]]; then
        restart_workspace "$ws_num"
      else
        error "Invalid workspace number"
      fi
      ;;
    c|C|cleanup)
      echo -n "  Cleanup which workspace? [1-9]: "
      read -r ws_num
      if [[ "$ws_num" =~ ^[0-9]+$ ]]; then
        cleanup_workspace "$ws_num"
      else
        error "Invalid workspace number"
      fi
      ;;
    *)
      if [[ "$choice" =~ ^[0-9]+$ ]]; then
        open_workspace "$choice"
      else
        error "Unknown option: $choice"
      fi
      ;;
  esac
}

# Open or create workspace
open_workspace() {
  local num=$1
  local initial_prompt="${2:-}"
  local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
  local window_name="ws$num"

  # Create if doesn't exist
  if [ ! -d "$dir" ]; then
    create_workspace "$num"
  fi

  # Get port info
  local port_info=$(get_workspace_ports "$num" "$dir")
  local api_port=$(echo "$port_info" | cut -d: -f1)
  local app_port=$(echo "$port_info" | cut -d: -f2)
  local need_override=$(echo "$port_info" | cut -d: -f3)
  local env_api_port=$(echo "$port_info" | cut -d: -f4)

  # Check dependencies
  check_and_setup_workspace "$dir" "$num"

  # Build commands from config
  local dev_cmd=$(get_pane_command "server")
  local main_cmd=$(get_pane_command "main")

  # Check for agent session resume
  local resume_file=$(agent_resume_file "$dir")
  if [ -f "$resume_file" ]; then
    local resume_id=$(cat "$resume_file")
    if [ -n "$resume_id" ]; then
      main_cmd=$(agent_cmd_resume "$main_cmd" "$resume_id")
      rm -f "$resume_file"
    fi
  fi

  # Ensure agent system prompt exists (CLAUDE.md or AGENTS.md)
  agent_ensure_system_prompt "$dir"

  # Port override if needed
  if [ "$need_override" = "true" ]; then
    dev_cmd="PORT=$api_port APP_PORT=$app_port $dev_cmd"
    echo -e "${YELLOW}  Port $env_api_port in use, using $api_port instead${NC}"
  fi

  local port_msg="Using port $env_api_port"
  [ "$need_override" = "true" ] && port_msg="Port $env_api_port in use → using $api_port"

  # Append initial prompt to agent command if provided
  if [ -n "$initial_prompt" ]; then
    local escaped_prompt
    escaped_prompt=$(printf '%q' "$initial_prompt")
    main_cmd=$(agent_cmd_with_prompt "$main_cmd" "$escaped_prompt")
  fi

  # Check if session exists
  if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
    # Create new session
    if [ -n "$initial_prompt" ]; then
      echo -e "${CYAN}Starting $SESSION_NAME session with workspace $num (with ticket prompt)...${NC}"
    else
      echo -e "${CYAN}Starting $SESSION_NAME session with workspace $num...${NC}"
    fi
    echo "  Directory: $dir"
    echo -e "  ${YELLOW}$port_msg${NC}"

    create_workspace_layout "$window_name" "$dir" "$dev_cmd" "$main_cmd" "$port_msg" "new"
    tmux attach -t "$SESSION_NAME"
  else
    # Session exists
    if tmux list-windows -t "$SESSION_NAME" -F "#{window_name}" 2>/dev/null | grep -q "^$window_name$"; then
      if [ -n "$initial_prompt" ]; then
        # Ticket mode: restart the main pane with the new prompt
        # Check if something is running in the main pane before killing it
        local pane_indices=$(get_pane_indices)
        local p_main=$(echo "$pane_indices" | cut -d: -f2)
        local p_server=$(echo "$pane_indices" | cut -d: -f3)

        local current_cmd
        current_cmd=$(tmux display-message -t "$SESSION_NAME:$window_name.$p_main" -p '#{pane_current_command}' 2>/dev/null || echo "")
        if [[ -n "$current_cmd" && ! "$current_cmd" =~ ^(bash|zsh|sh|fish)$ ]]; then
          echo -e "${YELLOW}Warning: Workspace $num main pane is running: $current_cmd${NC}"
          read -p "Kill it and restart with ticket prompt? [y/N]: " confirm
          if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
            echo "Aborted."
            return 0
          fi
        fi

        echo "  Window exists, restarting with ticket prompt..."

        tmux send-keys -t "$SESSION_NAME:$window_name.$p_server" C-c
        sleep 0.3
        [ -n "$dev_cmd" ] && tmux send-keys -t "$SESSION_NAME:$window_name.$p_server" "$dev_cmd" C-m

        if [ -n "$main_cmd" ]; then
          # Run command via respawn-pane shell arg to avoid zsh init prompts eating keystrokes
          tmux respawn-pane -k -t "$SESSION_NAME:$window_name.$p_main" -c "$dir" "$main_cmd; exec $SHELL"
        else
          tmux respawn-pane -k -t "$SESSION_NAME:$window_name.$p_main" -c "$dir"
        fi

        success "Workspace $num started with ticket prompt"
      else
        echo -e "${CYAN}Switching to workspace $num...${NC}"
        if [ -n "$TMUX" ]; then
          tmux select-window -t "$SESSION_NAME:$window_name"
        else
          tmux attach -t "$SESSION_NAME" \; select-window -t "$window_name"
        fi
      fi
    else
      echo -e "${CYAN}Adding workspace $num to session...${NC}"
      echo "  Directory: $dir"
      echo -e "  ${YELLOW}$port_msg${NC}"

      create_workspace_layout "$window_name" "$dir" "$dev_cmd" "$main_cmd" "$port_msg" "add"

      if [ -n "$TMUX" ]; then
        tmux select-window -t "$SESSION_NAME:$window_name"
      else
        tmux attach -t "$SESSION_NAME" \; select-window -t "$window_name"
      fi
    fi
  fi
}

# Create the tmux layout for a workspace
create_workspace_layout() {
  local window_name=$1
  local dir=$2
  local dev_cmd=$3
  local main_cmd=$4
  local port_msg=$5
  local mode=$6  # "new" or "add"

  if [ "$mode" = "new" ]; then
    tmux new-session -d -s "$SESSION_NAME" -n "$window_name" -c "$dir"
  else
    tmux new-window -t "$SESSION_NAME" -n "$window_name" -c "$dir"
  fi

  # Create 3-pane layout:
  # ┌─────────┬─────────┐
  # │terminal │         │
  # ├─────────┤ main    │
  # │ server  │         │
  # └─────────┴─────────┘

  # Get pane indices (handles pane-base-index setting)
  local pane_indices=$(get_pane_indices)
  local p_terminal=$(echo "$pane_indices" | cut -d: -f1)
  local p_main=$(echo "$pane_indices" | cut -d: -f2)
  local p_server=$(echo "$pane_indices" | cut -d: -f3)

  # Create layout: start with one pane, split right for main, then split left for server
  tmux split-window -h -t "$SESSION_NAME:$window_name" -c "$dir"
  tmux select-pane -t "$SESSION_NAME:$window_name.$p_terminal"
  tmux split-window -v -t "$SESSION_NAME:$window_name" -c "$dir"

  # After splits: terminal=base, main=base+1, server=base+2
  [ -n "$dev_cmd" ] && tmux send-keys -t "$SESSION_NAME:$window_name.$p_server" "echo '$port_msg' && $dev_cmd" C-m
  [ -n "$main_cmd" ] && tmux send-keys -t "$SESSION_NAME:$window_name.$p_main" "$main_cmd" C-m
  tmux select-pane -t "$SESSION_NAME:$window_name.$p_main"
}

# Get command for a pane from config
get_pane_command() {
  local pane_name=$1
  local panes_count=$(yq -r '.layout.panes | length // 0' "$CONFIG_FILE" 2>/dev/null)

  for ((i=0; i<panes_count; i++)); do
    local name=$(yq -r ".layout.panes[$i].name" "$CONFIG_FILE" 2>/dev/null)
    if [ "$name" = "$pane_name" ]; then
      local cmd=$(yq -r ".layout.panes[$i].command // \"\"" "$CONFIG_FILE" 2>/dev/null)
      if [ "$pane_name" = "main" ] && { [ -z "$cmd" ] || [ "$cmd" = "null" ]; }; then
        get_agent_base_cmd
      else
        echo "$cmd"
      fi
      return
    fi
  done

  if [ "$pane_name" = "main" ]; then
    get_agent_base_cmd
  else
    echo ""
  fi
}

# Check and setup workspace (dependencies, .env sync, shared volume)
check_and_setup_workspace() {
  local dir=$1
  local num=$2

  check_and_install_deps "$dir"

  # Setup shared volume (handles migration if needed)
  setup_shared_volume "$dir"

  echo -e "${BLUE}Syncing .env files for workspace $num...${NC}"
  sync_env_files "$dir" "$num"
}

# Check if dependencies need installing
check_and_install_deps() {
  local dir=$1
  local install_cmd=$(config_get "install_command" "")

  [ -z "$install_cmd" ] && return

  cd "$dir"

  local marker="$dir/node_modules/.crabcode-installed"
  local need_install="false"

  if [ ! -d "$dir/node_modules" ]; then
    need_install="true"
  elif [ ! -f "$marker" ]; then
    need_install="true"
  elif [ -f "$dir/pnpm-lock.yaml" ] && [ "$dir/pnpm-lock.yaml" -nt "$marker" ]; then
    need_install="true"
  elif [ -f "$dir/package-lock.json" ] && [ "$dir/package-lock.json" -nt "$marker" ]; then
    need_install="true"
  elif [ -f "$dir/yarn.lock" ] && [ "$dir/yarn.lock" -nt "$marker" ]; then
    need_install="true"
  fi

  if [ "$need_install" = "true" ]; then
    echo -e "${YELLOW}Installing dependencies...${NC}"
    # Get env vars from install_env config
    local install_env=$(config_get "install_env" "")
    local run_cmd="$install_cmd"
    if [ -n "$install_env" ] && [ "$install_env" != "null" ]; then
      # Prepend env var to command so it's passed to subprocesses
      run_cmd="$install_env $install_cmd"
    fi
    if bash -c "$run_cmd"; then
      touch "$marker"
      echo -e "${GREEN}Dependencies installed${NC}"
    else
      error "Failed to install dependencies"
      return 1
    fi
  fi

  # Check submodule dependencies
  local submodules_count=$(yq -r '.submodules | length // 0' "$CONFIG_FILE" 2>/dev/null)
  for ((i=0; i<submodules_count; i++)); do
    local sub_path=$(yq -r ".submodules[$i].path" "$CONFIG_FILE" 2>/dev/null)
    local sub_install=$(yq -r ".submodules[$i].install_command // \"\"" "$CONFIG_FILE" 2>/dev/null)

    [ -z "$sub_path" ] || [ "$sub_path" = "null" ] && continue
    [ -z "$sub_install" ] || [ "$sub_install" = "null" ] && continue

    local sub_dir="$dir/$sub_path"
    if [ -d "$sub_dir" ]; then
      local sub_marker="$sub_dir/node_modules/.crabcode-installed"
      local sub_need_install="false"

      if [ ! -d "$sub_dir/node_modules" ]; then
        sub_need_install="true"
      elif [ ! -f "$sub_marker" ]; then
        sub_need_install="true"
      fi

      if [ "$sub_need_install" = "true" ]; then
        echo -e "${YELLOW}Installing $sub_path dependencies...${NC}"
        cd "$sub_dir"
        # Prepend env var to submodule install command too
        local sub_run_cmd="$sub_install"
        if [ -n "$install_env" ] && [ "$install_env" != "null" ]; then
          sub_run_cmd="$install_env $sub_install"
        fi
        if bash -c "$sub_run_cmd"; then
          # Only touch marker if node_modules exists
          [ -d "node_modules" ] && touch "$sub_marker"
          echo -e "${GREEN}$sub_path dependencies installed${NC}"
        else
          warn "Failed to install $sub_path dependencies"
        fi
        cd "$dir"
      fi
    fi
  done
}

# =============================================================================
# Cleanup / Restart / Continue
# =============================================================================

cleanup_workspace() {
  local num=$1
  local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
  local window_name="ws$num"

  if [ ! -d "$dir" ]; then
    error "Workspace $num does not exist at $dir"
    exit 1
  fi

  echo -e "${YELLOW}Cleaning up workspace $num...${NC}"

  # Kill the window in the session if it exists
  if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
    tmux kill-window -t "$SESSION_NAME:$window_name" 2>/dev/null || true
  fi

  # Kill any running processes for this workspace
  local kill_pattern=$(config_get "cleanup.kill_pattern" "")
  if [ -n "$kill_pattern" ]; then
    kill_pattern="${kill_pattern//\{N\}/$num}"
    kill_pattern="${kill_pattern//\{PREFIX\}/$WORKSPACE_PREFIX}"
    pkill -f "$kill_pattern" 2>/dev/null || true
  fi

  cd "$dir"

  echo "  Fetching origin..."
  git fetch origin

  echo "  Resetting to origin/main..."
  git checkout -f main 2>/dev/null || git checkout -f -B main origin/main
  git reset --hard origin/main

  echo "  Cleaning untracked files..."
  local exclude_pattern=$(config_get "cleanup.preserve_files" ".env")
  git clean -fd --exclude="$exclude_pattern"

  reset_submodules "$dir"

  # Record mood event
  mood_record_event "cleanup" "workspace $num"

  success "Workspace $num cleaned and reset to origin/main"
}

# Completely destroy a workspace (remove worktree and all files)
destroy_workspace() {
  local num=$1
  local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
  local window_name="ws$num"
  local branch_name=$(get_branch_name "$num")

  if [ ! -d "$dir" ]; then
    error "Workspace $num does not exist at $dir"
    exit 1
  fi

  echo -e "${RED}Destroying workspace $num...${NC}"
  echo -e "${YELLOW}This will permanently delete all files in the workspace!${NC}"
  echo ""

  # Confirm unless --force is passed
  if [ "${2:-}" != "--force" ] && [ "${2:-}" != "-f" ]; then
    echo -n "Are you sure? (y/N) "
    read -r confirm
    if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
      echo "Aborted."
      return 1
    fi
  fi

  # Kill the tmux window if it exists
  if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
    echo "  Killing tmux window..."
    tmux kill-window -t "$SESSION_NAME:$window_name" 2>/dev/null || true
  fi

  # Kill any running processes for this workspace
  local kill_pattern=$(config_get "cleanup.kill_pattern" "")
  if [ -n "$kill_pattern" ]; then
    kill_pattern="${kill_pattern//\{N\}/$num}"
    kill_pattern="${kill_pattern//\{PREFIX\}/$WORKSPACE_PREFIX}"
    echo "  Killing processes..."
    pkill -f "$kill_pattern" 2>/dev/null || true
  fi

  # Remove the git worktree properly
  echo "  Removing git worktree..."
  cd "$MAIN_REPO"
  git worktree remove "$dir" --force 2>/dev/null || true

  # If worktree remove failed, force delete
  if [ -d "$dir" ]; then
    echo "  Force removing directory..."
    rm -rf "$dir"
  fi

  # Clean up the branch if it exists and is not checked out elsewhere
  if git show-ref --verify --quiet "refs/heads/$branch_name"; then
    # Check if branch is used by another worktree
    local branch_in_use=$(git worktree list | grep "\[$branch_name\]" | wc -l | tr -d ' ')
    if [ "$branch_in_use" = "0" ]; then
      echo "  Deleting branch $branch_name..."
      git branch -D "$branch_name" 2>/dev/null || true
    fi
  fi

  # Clean up snapshots for this workspace
  if [ -d "$SNAPSHOT_DIR/ws-$num" ]; then
    echo "  Removing snapshots..."
    rm -rf "$SNAPSHOT_DIR/ws-$num"
  fi

  success "Workspace $num destroyed"
}

restart_workspace() {
  local num=$1
  local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
  local window_name="ws$num"
  local branch_name=$(get_branch_name "$num")

  if [ ! -d "$dir" ]; then
    error "Workspace $num does not exist at $dir"
    exit 1
  fi

  echo -e "${YELLOW}Restarting workspace $num...${NC}"

  cd "$dir"

  local current_branch=$(git branch --show-current)

  echo "  Fetching origin..."
  git fetch origin

  if [ "$current_branch" != "$branch_name" ]; then
    echo "  Switching from $current_branch to $branch_name..."
    git checkout -f "$branch_name" 2>/dev/null || git checkout -f -B "$branch_name"
  fi

  echo "  Resetting $branch_name to origin/main content..."
  git reset --hard origin/main

  echo "  Cleaning untracked files..."
  local exclude_pattern=$(config_get "cleanup.preserve_files" ".env")
  git clean -fd --exclude="$exclude_pattern"

  reset_submodules "$dir"

  # Ensure shared volume is set up (migrates existing .local if needed)
  setup_shared_volume "$dir"

  # Reinstall dependencies (lockfile may have changed after reset)
  check_and_install_deps "$dir"

  success "Git reset complete (on branch: $branch_name)"

  # Rebuild the tmux window with full layout
  if [ -n "$TMUX" ]; then
    local port_info=$(get_workspace_ports "$num" "$dir")
    local api_port=$(echo "$port_info" | cut -d: -f1)
    local app_port=$(echo "$port_info" | cut -d: -f2)
    local need_override=$(echo "$port_info" | cut -d: -f3)
    local env_api_port=$(echo "$port_info" | cut -d: -f4)

    local dev_cmd=$(get_pane_command "server")
    local main_cmd=$(get_pane_command "main")

    if [ "$need_override" = "true" ]; then
      dev_cmd="PORT=$api_port APP_PORT=$app_port $dev_cmd"
    fi

    local port_msg="Using port $env_api_port"
    [ "$need_override" = "true" ] && port_msg="Port $env_api_port in use → using $api_port"

    local current_window=$(tmux display-message -p '#{window_name}' 2>/dev/null)
    local old_window=""

    # Determine which window to replace
    if tmux list-windows -t "$SESSION_NAME" -F "#{window_name}" 2>/dev/null | grep -q "^$window_name$"; then
      old_window="$window_name"
    elif [[ "$(pwd)" == "$dir"* ]] && [ -n "$current_window" ]; then
      old_window="$current_window"
    fi

    if [ -n "$old_window" ]; then
      echo "  Recreating workspace layout..."

      # Create new window with correct name directly
      # First, rename old window to temp so we can use the proper name
      local temp_old="_old_$$"
      tmux rename-window -t "$SESSION_NAME:$old_window" "$temp_old" 2>/dev/null || true

      # Now create new window with correct name
      create_workspace_layout "$window_name" "$dir" "$dev_cmd" "$main_cmd" "$port_msg" "add"

      # Switch to new window
      tmux select-window -t "$SESSION_NAME:$window_name"

      # Kill old window (now named temp_old) via run-shell so it doesn't kill us
      tmux run-shell -t "$SESSION_NAME" "tmux kill-window -t '$SESSION_NAME:$temp_old' 2>/dev/null || true"

      # Record mood event
      mood_record_event "restart" "workspace $num"

      success "Workspace $num restarted with fresh layout!"
    else
      echo -e "${YELLOW}  No existing window found - creating new...${NC}"
      create_workspace_layout "$window_name" "$dir" "$dev_cmd" "$main_cmd" "$port_msg" "add"
      tmux select-window -t "$SESSION_NAME:$window_name"
    fi
  else
    echo -e "${YELLOW}  Not in tmux session - run 'crabcode $num' to start${NC}"
  fi
}

continue_workspace() {
  local num=$1
  local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
  local window_name="ws$num"

  if [ ! -d "$dir" ]; then
    error "Workspace $num does not exist at $dir"
    exit 1
  fi

  echo -e "${CYAN}Continuing workspace $num...${NC}"

  local port_info=$(get_workspace_ports "$num" "$dir")
  local api_port=$(echo "$port_info" | cut -d: -f1)
  local app_port=$(echo "$port_info" | cut -d: -f2)
  local need_override=$(echo "$port_info" | cut -d: -f3)

  local dev_cmd=$(get_pane_command "server")
  local main_cmd=$(get_pane_command "main")

  # Add session resume or --continue to agent command
  local resume_file=$(agent_resume_file "$dir")
  if [ -f "$resume_file" ]; then
    local resume_id=$(cat "$resume_file")
    if [ -n "$resume_id" ]; then
      main_cmd=$(agent_cmd_resume "$main_cmd" "$resume_id")
      rm -f "$resume_file"
    else
      main_cmd=$(agent_cmd_continue "$main_cmd")
    fi
  else
    main_cmd=$(agent_cmd_continue "$main_cmd")
  fi

  if [ "$need_override" = "true" ]; then
    dev_cmd="PORT=$api_port APP_PORT=$app_port $dev_cmd"
  fi

  if [ -n "$TMUX" ] && tmux list-windows -t "$SESSION_NAME" -F "#{window_name}" 2>/dev/null | grep -q "^$window_name$"; then
    echo "  Window exists, restarting with --continue..."

    # Get pane indices
    local pane_indices=$(get_pane_indices)
    local p_main=$(echo "$pane_indices" | cut -d: -f2)
    local p_server=$(echo "$pane_indices" | cut -d: -f3)

    tmux send-keys -t "$SESSION_NAME:$window_name.$p_server" C-c
    sleep 0.3
    [ -n "$dev_cmd" ] && tmux send-keys -t "$SESSION_NAME:$window_name.$p_server" "$dev_cmd" C-m

    if [ -n "$main_cmd" ]; then
      # Run command via respawn-pane shell arg to avoid zsh init prompts eating keystrokes
      tmux respawn-pane -k -t "$SESSION_NAME:$window_name.$p_main" -c "$dir" "$main_cmd; exec $SHELL"
    else
      tmux respawn-pane -k -t "$SESSION_NAME:$window_name.$p_main" -c "$dir"
    fi

    success "Workspace $num continued with previous session"
  else
    echo "  Creating window with --continue..."
    create_workspace_layout "$window_name" "$dir" "$dev_cmd" "$main_cmd" "" "add"

    if [ -n "$TMUX" ]; then
      tmux select-window -t "$SESSION_NAME:$window_name"
    fi

    success "Workspace $num started with previous session"
  fi
}

# Open workspace in separate terminal
open_workspace_separate() {
  local num=$1
  local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
  local session="$SESSION_NAME-$num"

  # Create if doesn't exist
  if [ ! -d "$dir" ]; then
    create_workspace "$num"
  fi

  local port_info=$(get_workspace_ports "$num" "$dir")
  local api_port=$(echo "$port_info" | cut -d: -f1)
  local app_port=$(echo "$port_info" | cut -d: -f2)
  local need_override=$(echo "$port_info" | cut -d: -f3)
  local env_api_port=$(echo "$port_info" | cut -d: -f4)

  echo -e "${CYAN}Starting workspace $num in separate window...${NC}"

  check_and_setup_workspace "$dir" "$num"

  local dev_cmd=$(get_pane_command "server")
  local main_cmd=$(get_pane_command "main")

  if [ "$need_override" = "true" ]; then
    dev_cmd="PORT=$api_port APP_PORT=$app_port $dev_cmd"
    echo -e "${YELLOW}  Port $env_api_port in use, using $api_port instead${NC}"
  fi

  if tmux has-session -t "$session" 2>/dev/null; then
    if command_exists osascript; then
      osascript -e "tell application \"Terminal\" to do script \"tmux attach -t $session\""
    else
      echo "Open a new terminal and run: tmux attach -t $session"
    fi
  else
    tmux new-session -d -s "$session" -n "dev" -c "$dir"
    tmux split-window -h -t "$session" -c "$dir"
    tmux select-pane -t "$session:dev.1"
    tmux split-window -v -t "$session" -c "$dir"

    [ -n "$dev_cmd" ] && tmux send-keys -t "$session:dev.2" "$dev_cmd" C-m
    [ -n "$main_cmd" ] && tmux send-keys -t "$session:dev.3" "$main_cmd" C-m
    tmux select-pane -t "$session:dev.3"

    if command_exists osascript; then
      osascript -e "tell application \"Terminal\" to do script \"tmux attach -t $session\""
    else
      echo "Open a new terminal and run: tmux attach -t $session"
    fi
  fi
}

# Detect workspace number from current directory only (not tmux)
detect_workspace_from_dir() {
  local cwd=$(pwd)
  if [[ "$cwd" =~ $WORKSPACE_PREFIX-([0-9]+) ]]; then
    echo "${BASH_REMATCH[1]}"
    return
  fi
  echo ""
}

# Detect workspace number from current directory or tmux window
detect_workspace() {
  # First try: current directory
  local cwd=$(pwd)
  if [[ "$cwd" =~ $WORKSPACE_PREFIX-([0-9]+) ]]; then
    echo "${BASH_REMATCH[1]}"
    return
  fi

  # Second try: tmux window name (ws1, ws2, etc.)
  if [ -n "$TMUX" ]; then
    local window_name=$(tmux display-message -p '#{window_name}' 2>/dev/null)
    if [[ "$window_name" =~ ^ws([0-9]+)$ ]]; then
      echo "${BASH_REMATCH[1]}"
      return
    fi
  fi

  echo ""
}

# Find the next available workspace number
find_next_workspace() {
  local num=1
  while true; do
    local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
    if [ ! -d "$dir" ]; then
      echo "$num"
      return
    fi
    num=$((num + 1))
    # Safety limit
    if [ $num -gt 100 ]; then
      echo ""
      return
    fi
  done
}

# Build a prompt for Claude to handle a Linear ticket
build_ticket_prompt() {
  local identifier="$1"
  local default_prompt='Fetch the Linear ticket {identifier} using your Linear MCP tools. Read the ticket title, description, and any relevant comments. Then:
1. Create and checkout a git branch using the ticket'\''s suggested branch name from Linear
2. Analyze the requirements from the ticket
3. Create an implementation plan and enter plan mode for my approval before writing any code
If you need clarification on the requirements, ask me before proceeding.'

  local template
  template=$(config_get "ticket.prompt_template" "$default_prompt")
  echo "${template//\{identifier\}/$identifier}"
}

# Create a new workspace with the next available number
create_new_workspace() {
  local next_num=$(find_next_workspace)

  if [ -z "$next_num" ]; then
    error "Could not find an available workspace number"
    exit 1
  fi

  echo -e "${CYAN}Creating new workspace $next_num...${NC}"
  open_workspace "$next_num"
}

# =============================================================================
# WIP Management
# =============================================================================

get_wip_dir() {
  local num=$1
  echo "$WIP_BASE/$WORKSPACE_PREFIX-$num"
}

wip_save() {
  local num=$1
  local do_restart=$2
  local custom_name="${3:-}"
  local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
  local wip_dir=$(get_wip_dir "$num")
  local timestamp=$(date +%Y%m%d-%H%M%S)
  local branch_name=$(get_branch_name "$num")

  if [ ! -d "$dir" ]; then
    error "Workspace $num does not exist"
    return 1
  fi

  echo -e "${CYAN}Saving WIP state for workspace $num...${NC}"

  cd "$dir"
  local diff=$(git diff HEAD 2>/dev/null || echo "")
  local staged=$(git diff --cached HEAD 2>/dev/null || echo "")
  local status=$(git status --porcelain 2>/dev/null || echo "")
  local branch=$(git branch --show-current 2>/dev/null || echo "unknown")

  # Get untracked files
  local untracked_files=$(git ls-files --others --exclude-standard 2>/dev/null || echo "")

  git fetch origin main --quiet 2>/dev/null || true
  local commits_ahead=$(git rev-list --count origin/main..HEAD 2>/dev/null || echo "0")
  local commit_log=""
  if [ "$commits_ahead" -gt 0 ] 2>/dev/null; then
    commit_log=$(git log --oneline origin/main..HEAD 2>/dev/null || echo "")
  fi

  # Collect submodule changes
  local submodule_diffs=""
  local submodules_count=$(yq -r '.submodules | length // 0' "$CONFIG_FILE" 2>/dev/null)
  for ((i=0; i<submodules_count; i++)); do
    local sub_path=$(yq -r ".submodules[$i].path" "$CONFIG_FILE" 2>/dev/null)
    [ -z "$sub_path" ] || [ "$sub_path" = "null" ] && continue

    if [ -d "$dir/$sub_path" ]; then
      cd "$dir/$sub_path"
      local sub_diff=$(git diff HEAD 2>/dev/null || echo "")
      local sub_staged=$(git diff --cached HEAD 2>/dev/null || echo "")
      [ -n "$sub_diff" ] && submodule_diffs="$submodule_diffs$sub_diff"
      [ -n "$sub_staged" ] && submodule_diffs="$submodule_diffs$sub_staged"
      cd "$dir"
    fi
  done

  # Check if there's anything to save
  local has_changes="false"
  if [ -n "$diff" ] || [ -n "$staged" ] || [ -n "$status" ] || [ -n "$submodule_diffs" ] || [ -n "$untracked_files" ]; then
    has_changes="true"
  elif [ "$commits_ahead" -gt 0 ] 2>/dev/null; then
    has_changes="true"
  elif [ "$branch" != "$branch_name" ] && [ "$branch" != "main" ]; then
    has_changes="true"
  fi

  if [ "$has_changes" = "false" ]; then
    echo -e "${YELLOW}No changes to save.${NC}"
    return 0
  fi

  local slug=""
  local summary=""

  # Use custom name if provided
  if [ -n "$custom_name" ]; then
    slug=$(echo "$custom_name" | tr ' ' '-' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]//g')
    summary="$custom_name"
  else
    # Try to generate AI summary if agent is available
    slug="wip-$(date +%H%M)"
    summary="Work in progress"
    if agent_cli_exists; then
      local all_diffs="$diff$staged$submodule_diffs"
      local summary_json=$(generate_wip_summary "$all_diffs")
      local parsed_slug=$(echo "$summary_json" | sed -n 's/.*"slug"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
      local parsed_summary=$(echo "$summary_json" | sed -n 's/.*"summary"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
      [ -n "$parsed_slug" ] && slug="$parsed_slug"
      [ -n "$parsed_summary" ] && summary="$parsed_summary"
    fi
  fi

  local wip_path="$wip_dir/$timestamp-$slug"
  mkdir -p "$wip_path"

  local saved_count=0

  # Save patches
  if [ -n "$diff" ]; then
    echo "$diff" > "$wip_path/main-unstaged.patch"
    ((saved_count++))
  fi
  if [ -n "$staged" ]; then
    echo "$staged" > "$wip_path/main-staged.patch"
    ((saved_count++))
  fi
  [ -n "$status" ] && echo "$status" > "$wip_path/main-status.txt"
  [ -n "$commit_log" ] && echo "$commit_log" > "$wip_path/main-commits.txt"

  # Save untracked files (copy actual content)
  if [ -n "$untracked_files" ]; then
    local untracked_dir="$wip_path/untracked"
    mkdir -p "$untracked_dir"
    echo "$untracked_files" | while read -r file; do
      [ -z "$file" ] && continue
      local file_dir=$(dirname "$file")
      mkdir -p "$untracked_dir/$file_dir"
      cp "$dir/$file" "$untracked_dir/$file" 2>/dev/null && ((saved_count++))
    done
    echo "$untracked_files" > "$wip_path/untracked-files.txt"
  fi

  # Save submodule patches
  for ((i=0; i<submodules_count; i++)); do
    local sub_path=$(yq -r ".submodules[$i].path" "$CONFIG_FILE" 2>/dev/null)
    [ -z "$sub_path" ] || [ "$sub_path" = "null" ] && continue

    if [ -d "$dir/$sub_path" ]; then
      cd "$dir/$sub_path"
      local sub_diff=$(git diff HEAD 2>/dev/null || echo "")
      local sub_staged=$(git diff --cached HEAD 2>/dev/null || echo "")
      local sub_status=$(git status --porcelain 2>/dev/null || echo "")
      local safe_name=$(echo "$sub_path" | tr '/' '-')
      [ -n "$sub_diff" ] && echo "$sub_diff" > "$wip_path/${safe_name}-unstaged.patch"
      [ -n "$sub_staged" ] && echo "$sub_staged" > "$wip_path/${safe_name}-staged.patch"
      [ -n "$sub_status" ] && echo "$sub_status" > "$wip_path/${safe_name}-status.txt"
      cd "$dir"
    fi
  done

  # Capture agent session ID for conversation resume
  local agent_session=$(agent_capture_session_id "$dir")

  cat > "$wip_path/metadata.json" << EOF
{
  "timestamp": "$timestamp",
  "slug": "$slug",
  "summary": "$summary",
  "workspace": $num,
  "branch": "$branch",
  "commits_ahead": $commits_ahead,
  "agent": "$(get_agent_type)",
  "agent_session": "$agent_session",
  "claude_session": "$agent_session",
  "created_at": "$(date -Iseconds)"
}
EOF

  # Record mood event
  mood_record_event "wip_save" "$slug"

  success "WIP saved: $slug"
  echo -e "   ${GRAY}$summary${NC}"

  if [ "$do_restart" = "true" ]; then
    echo ""
    restart_workspace "$num"
  fi
}

generate_wip_summary() {
  local diff_content="$1"
  local max_lines=100
  local truncated_diff=$(echo "$diff_content" | head -n $max_lines)

  if agent_cli_exists; then
    local prompt="Based on this git diff, provide a JSON response with exactly this format (no markdown, just raw JSON):
{\"slug\": \"short-kebab-case-name-max-30-chars\", \"summary\": \"One sentence describing the changes\"}

Git diff:
$truncated_diff"

    local result=$(agent_run_print "$prompt")

    if [ -n "$result" ]; then
      local json=$(echo "$result" | grep -o '{[^}]*}' | head -1)
      if [ -n "$json" ]; then
        echo "$json"
        return
      fi
    fi
  fi

  echo "{\"slug\": \"wip-$(date +%H%M)\", \"summary\": \"Work in progress\"}"
}

wip_list() {
  local num=$1
  local wip_dir=$(get_wip_dir "$num")

  if [ ! -d "$wip_dir" ]; then
    echo -e "${YELLOW}No WIP states saved for workspace $num${NC}"
    return 0
  fi

  local wips=($(ls -1d "$wip_dir"/*/ 2>/dev/null | sort -r))

  if [ ${#wips[@]} -eq 0 ]; then
    echo -e "${YELLOW}No WIP states saved for workspace $num${NC}"
    return 0
  fi

  echo -e "${CYAN}WIP States for Workspace $num${NC}"
  echo ""

  local i=1
  for wip in "${wips[@]}"; do
    local name=$(basename "$wip")
    local metadata="$wip/metadata.json"

    if [ -f "$metadata" ]; then
      local summary=$(grep -o '"summary"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"summary"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
      local created=$(grep -o '"created_at"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"created_at"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')

      # Handle both new format (branch) and old format (cloud_branch)
      local branch=$(grep -o '"branch"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"branch"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
      if [ -z "$branch" ]; then
        branch=$(grep -o '"cloud_branch"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"cloud_branch"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
      fi

      # Format date nicely
      local display_date=$(echo "$created" | cut -d'T' -f1)
      local display_time=$(echo "$created" | cut -d'T' -f2 | cut -d'+' -f1 | cut -d'-' -f1 | cut -c1-5)

      # Check for agent session
      local session_tag=""
      local cs=$(grep -o '"agent_session"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"agent_session"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
      # Backward compat: also check claude_session
      [ -z "$cs" ] && cs=$(grep -o '"claude_session"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"claude_session"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
      local saved_agent=$(agent_type_from_metadata "$metadata")
      local agent_label=$(agent_display_name_for_type "$saved_agent")
      [ -n "$cs" ] && session_tag="  ${CYAN}${agent_label}: saved${NC}"

      echo -e "  ${GREEN}[$i]${NC} $name"
      echo -e "      ${BLUE}$summary${NC}"
      echo -e "      Branch: $branch | Created: $display_date $display_time${session_tag}"
      echo ""
    else
      echo -e "  ${GREEN}[$i]${NC} $name (no metadata)"
      echo ""
    fi

    ((i++))
  done
}

# List all WIPs globally across all workspaces
wip_list_global() {
  if [ ! -d "$WIP_BASE" ]; then
    echo -e "${YELLOW}No WIP states saved${NC}"
    echo "  Save your work with: crab wip save"
    return 0
  fi

  # Collect all WIPs with their metadata
  local all_wips=()
  local wip_data=()

  # Scan all workspace WIP directories
  for ws_dir in "$WIP_BASE"/*/; do
    [ ! -d "$ws_dir" ] && continue
    local ws_name=$(basename "$ws_dir")

    for wip in "$ws_dir"*/; do
      [ ! -d "$wip" ] && continue
      local metadata="$wip/metadata.json"
      if [ -f "$metadata" ]; then
        # Get timestamp for sorting
        local timestamp=$(grep -o '"timestamp"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"timestamp"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
        all_wips+=("$timestamp|$ws_name|$wip")
      else
        all_wips+=("00000000-000000|$ws_name|$wip")
      fi
    done
  done

  if [ ${#all_wips[@]} -eq 0 ]; then
    echo -e "${YELLOW}No WIP states saved${NC}"
    echo "  Save your work with: crab wip save"
    return 0
  fi

  # Sort by timestamp (newest first)
  IFS=$'\n' sorted_wips=($(printf '%s\n' "${all_wips[@]}" | sort -r))
  unset IFS

  echo -e "${CYAN}${BOLD}All WIP States${NC}"
  echo ""

  local i=1
  for entry in "${sorted_wips[@]}"; do
    local ws_name=$(echo "$entry" | cut -d'|' -f2)
    local wip_path=$(echo "$entry" | cut -d'|' -f3)
    local wip_name=$(basename "$wip_path")
    local metadata="$wip_path/metadata.json"

    # Extract workspace number from ws_name (e.g., cloud-workspace-1 -> 1)
    local ws_num=$(echo "$ws_name" | grep -o '[0-9]*$')

    # Count patch files
    local file_count=$(ls -1 "$wip_path"/*.patch 2>/dev/null | wc -l | tr -d ' ')
    [ -z "$file_count" ] && file_count=0

    if [ -f "$metadata" ]; then
      local summary=$(grep -o '"summary"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"summary"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
      local created=$(grep -o '"created_at"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"created_at"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')

      # Handle both new format (branch) and old format (cloud_branch)
      local branch=$(grep -o '"branch"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"branch"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
      if [ -z "$branch" ]; then
        branch=$(grep -o '"cloud_branch"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"cloud_branch"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
      fi

      # Handle both new format (commits_ahead) and old format (cloud_commits_ahead)
      local commits_ahead=$(grep -o '"commits_ahead"[[:space:]]*:[[:space:]]*[0-9]*' "$metadata" | sed 's/"commits_ahead"[[:space:]]*:[[:space:]]*//')
      if [ -z "$commits_ahead" ]; then
        commits_ahead=$(grep -o '"cloud_commits_ahead"[[:space:]]*:[[:space:]]*[0-9]*' "$metadata" | sed 's/"cloud_commits_ahead"[[:space:]]*:[[:space:]]*//')
      fi

      # Format the date nicely
      local display_date=$(echo "$created" | cut -d'T' -f1)
      local display_time=$(echo "$created" | cut -d'T' -f2 | cut -d'+' -f1 | cut -d'-' -f1 | cut -c1-5)

      echo -e "  ${GREEN}[$i]${NC} ${BOLD}$wip_name${NC}"
      echo -e "      ${BLUE}$summary${NC}"
      # Check for agent session
      local session_tag=""
      local agent_sess=$(grep -o '"agent_session"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"agent_session"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
      [ -z "$agent_sess" ] && agent_sess=$(grep -o '"claude_session"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"claude_session"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
      local saved_agent=$(agent_type_from_metadata "$metadata")
      local agent_label=$(agent_display_name_for_type "$saved_agent")
      [ -n "$agent_sess" ] && session_tag="  ${CYAN}${agent_label}: saved${NC}"

      echo -e "      ${GRAY}Workspace: ${NC}$ws_num  ${GRAY}Branch: ${NC}$branch  ${GRAY}Files: ${NC}$file_count patches${session_tag}"
      if [ -n "$commits_ahead" ] && [ "$commits_ahead" != "0" ]; then
        echo -e "      ${GRAY}Commits ahead: ${NC}$commits_ahead  ${GRAY}Created: ${NC}$display_date $display_time"
      else
        echo -e "      ${GRAY}Created: ${NC}$display_date $display_time"
      fi
      echo ""
    else
      echo -e "  ${GREEN}[$i]${NC} ${BOLD}$wip_name${NC} ${GRAY}(no metadata)${NC}"
      echo -e "      ${GRAY}Workspace: ${NC}$ws_num  ${GRAY}Files: ${NC}$file_count patches"
      echo ""
    fi

    ((i++))
  done

  echo -e "${GRAY}Restore with: crab wip restore <number> [--to <workspace>]${NC}"
}

# Interactive restore from global WIP list
wip_restore_global() {
  local target_ws="${1:-}"
  local open_after="${2:-true}"

  if [ ! -d "$WIP_BASE" ]; then
    error "No WIP states found"
    return 1
  fi

  # Collect all WIPs
  local all_wips=()
  for ws_dir in "$WIP_BASE"/*/; do
    [ ! -d "$ws_dir" ] && continue
    local ws_name=$(basename "$ws_dir")

    for wip in "$ws_dir"*/; do
      [ ! -d "$wip" ] && continue
      local metadata="$wip/metadata.json"
      if [ -f "$metadata" ]; then
        local timestamp=$(grep -o '"timestamp"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"timestamp"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
        all_wips+=("$timestamp|$ws_name|$wip")
      else
        all_wips+=("00000000-000000|$ws_name|$wip")
      fi
    done
  done

  if [ ${#all_wips[@]} -eq 0 ]; then
    error "No WIP states found"
    return 1
  fi

  # Sort by timestamp (newest first)
  IFS=$'\n' sorted_wips=($(printf '%s\n' "${all_wips[@]}" | sort -r))
  unset IFS

  echo -e "${CYAN}Select WIP to restore:${NC}"
  echo ""

  local i=1
  for entry in "${sorted_wips[@]}"; do
    local ws_name=$(echo "$entry" | cut -d'|' -f2)
    local wip_path=$(echo "$entry" | cut -d'|' -f3)
    local wip_name=$(basename "$wip_path")
    local metadata="$wip_path/metadata.json"
    local ws_num=$(echo "$ws_name" | grep -o '[0-9]*$')
    local summary=""

    if [ -f "$metadata" ]; then
      summary=$(grep -o '"summary"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"summary"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
    fi

    echo -e "  ${GREEN}[$i]${NC} $wip_name ${GRAY}(ws $ws_num)${NC}"
    [ -n "$summary" ] && echo -e "      ${BLUE}$summary${NC}"
    ((i++))
  done

  echo ""
  echo -e "  ${YELLOW}[0]${NC} Cancel"
  echo ""

  read -p "Select WIP [1-${#sorted_wips[@]}]: " selection

  if [ "$selection" = "0" ] || [ -z "$selection" ]; then
    echo "Cancelled."
    return 0
  fi

  if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt ${#sorted_wips[@]} ]; then
    error "Invalid selection"
    return 1
  fi

  local selected_entry="${sorted_wips[$((selection-1))]}"
  local selected_ws=$(echo "$selected_entry" | cut -d'|' -f2)
  local selected_wip=$(echo "$selected_entry" | cut -d'|' -f3)
  local original_ws_num=$(echo "$selected_ws" | grep -o '[0-9]*$')

  # Determine target workspace
  local restore_to_ws="$original_ws_num"
  if [ -n "$target_ws" ]; then
    restore_to_ws="$target_ws"
  fi

  # Check if target workspace exists
  local target_dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$restore_to_ws"
  if [ ! -d "$target_dir" ]; then
    echo -e "${YELLOW}Workspace $restore_to_ws does not exist.${NC}"
    read -p "Create it? [y/N]: " create_ws
    if [ "$create_ws" = "y" ] || [ "$create_ws" = "Y" ]; then
      create_workspace "$restore_to_ws"
    else
      echo "Cancelled."
      return 0
    fi
  else
    # Check if target workspace has uncommitted changes
    if _workspace_has_changes "$target_dir"; then
      echo -e "${YELLOW}Workspace $restore_to_ws has uncommitted changes.${NC}"
      echo ""
      echo "  [1] Restore anyway (may conflict)"
      echo "  [2] Create a new workspace"
      echo "  [0] Cancel"
      echo ""
      read -p "Choice [0-2]: " conflict_choice
      case "$conflict_choice" in
        1) ;; # Continue with restore
        2)
          # Find next available workspace
          local next_ws=$(find_next_workspace)
          echo -e "${CYAN}Creating workspace $next_ws...${NC}"
          create_workspace "$next_ws"
          restore_to_ws="$next_ws"
          ;;
        *)
          echo "Cancelled."
          return 0
          ;;
      esac
    fi
  fi

  local wip_name=$(basename "$selected_wip")
  echo ""
  echo -e "${CYAN}Restoring WIP: $wip_name -> workspace $restore_to_ws${NC}"

  _restore_wip "$restore_to_ws" "$selected_wip" "true"
}

# Check if workspace directory has uncommitted changes
_workspace_has_changes() {
  local dir="$1"
  [ ! -d "$dir" ] && return 1
  # Check for any changes (staged, unstaged, or untracked)
  # Use git -C to avoid changing the caller's working directory
  if ! git -C "$dir" diff --quiet 2>/dev/null || ! git -C "$dir" diff --cached --quiet 2>/dev/null; then
    return 0
  fi
  # Check for untracked files
  if [ -n "$(git -C "$dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then
    return 0
  fi
  return 1
}

# Restore by index from global list
wip_restore_by_index() {
  local index="$1"
  local target_ws="$2"
  local open_after="${3:-true}"

  if [ ! -d "$WIP_BASE" ]; then
    error "No WIP states found"
    return 1
  fi

  # Collect all WIPs
  local all_wips=()
  for ws_dir in "$WIP_BASE"/*/; do
    [ ! -d "$ws_dir" ] && continue
    local ws_name=$(basename "$ws_dir")

    for wip in "$ws_dir"*/; do
      [ ! -d "$wip" ] && continue
      local metadata="$wip/metadata.json"
      if [ -f "$metadata" ]; then
        local timestamp=$(grep -o '"timestamp"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"timestamp"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
        all_wips+=("$timestamp|$ws_name|$wip")
      else
        all_wips+=("00000000-000000|$ws_name|$wip")
      fi
    done
  done

  if [ ${#all_wips[@]} -eq 0 ]; then
    error "No WIP states found"
    return 1
  fi

  # Sort by timestamp (newest first)
  IFS=$'\n' sorted_wips=($(printf '%s\n' "${all_wips[@]}" | sort -r))
  unset IFS

  if ! [[ "$index" =~ ^[0-9]+$ ]] || [ "$index" -lt 1 ] || [ "$index" -gt ${#sorted_wips[@]} ]; then
    error "Invalid WIP index: $index (valid range: 1-${#sorted_wips[@]})"
    return 1
  fi

  local selected_entry="${sorted_wips[$((index-1))]}"
  local selected_ws=$(echo "$selected_entry" | cut -d'|' -f2)
  local selected_wip=$(echo "$selected_entry" | cut -d'|' -f3)
  local original_ws_num=$(echo "$selected_ws" | grep -o '[0-9]*$')

  # Determine target workspace
  local restore_to_ws="$original_ws_num"
  if [ -n "$target_ws" ]; then
    restore_to_ws="$target_ws"
  fi

  # Check if target workspace exists
  local target_dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$restore_to_ws"
  if [ ! -d "$target_dir" ]; then
    echo -e "${YELLOW}Workspace $restore_to_ws does not exist.${NC}"
    read -p "Create it? [y/N]: " create_ws
    if [ "$create_ws" = "y" ] || [ "$create_ws" = "Y" ]; then
      create_workspace "$restore_to_ws"
    else
      echo "Cancelled."
      return 0
    fi
  else
    # Check if target workspace has uncommitted changes
    if _workspace_has_changes "$target_dir"; then
      echo -e "${YELLOW}Workspace $restore_to_ws has uncommitted changes.${NC}"
      echo ""
      echo "  [1] Restore anyway (may conflict)"
      echo "  [2] Create a new workspace"
      echo "  [0] Cancel"
      echo ""
      read -p "Choice [0-2]: " conflict_choice
      case "$conflict_choice" in
        1) ;; # Continue with restore
        2)
          # Find next available workspace
          local next_ws=$(find_next_workspace)
          echo -e "${CYAN}Creating workspace $next_ws...${NC}"
          create_workspace "$next_ws"
          restore_to_ws="$next_ws"
          ;;
        *)
          echo "Cancelled."
          return 0
          ;;
      esac
    fi
  fi

  local wip_name=$(basename "$selected_wip")
  echo -e "${CYAN}Restoring WIP: $wip_name -> workspace $restore_to_ws${NC}"

  _restore_wip "$restore_to_ws" "$selected_wip" "true"
}

wip_continue() {
  local num=$1
  local open_after="${2:-true}"
  local wip_dir=$(get_wip_dir "$num")

  if [ ! -d "$wip_dir" ]; then
    error "No WIP states found for workspace $num"
    return 1
  fi

  local latest_wip=$(ls -1d "$wip_dir"/*/ 2>/dev/null | sort -r | head -1)

  if [ -z "$latest_wip" ]; then
    error "No WIP states found for workspace $num"
    return 1
  fi

  local wip_name=$(basename "$latest_wip")
  echo -e "${CYAN}Restoring WIP: $wip_name${NC}"

  _restore_wip "$num" "$latest_wip" "$open_after"
}

wip_resume() {
  local num=$1
  local wip_dir=$(get_wip_dir "$num")

  if [ ! -d "$wip_dir" ]; then
    error "No WIP states found for workspace $num"
    return 1
  fi

  local wips=($(ls -1d "$wip_dir"/*/ 2>/dev/null | sort -r))

  if [ ${#wips[@]} -eq 0 ]; then
    error "No WIP states found for workspace $num"
    return 1
  fi

  echo -e "${CYAN}Select WIP to restore:${NC}"
  echo ""

  local i=1
  for wip in "${wips[@]}"; do
    local name=$(basename "$wip")
    local metadata="$wip/metadata.json"
    local summary=""

    if [ -f "$metadata" ]; then
      summary=$(grep -o '"summary"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"summary"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
    fi

    echo -e "  ${GREEN}[$i]${NC} $name"
    [ -n "$summary" ] && echo -e "      ${BLUE}$summary${NC}"
    ((i++))
  done

  echo ""
  echo -e "  ${YELLOW}[0]${NC} Cancel"
  echo ""

  read -p "Select WIP [1-${#wips[@]}]: " selection

  if [ "$selection" = "0" ] || [ -z "$selection" ]; then
    echo "Cancelled."
    return 0
  fi

  if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt ${#wips[@]} ]; then
    error "Invalid selection"
    return 1
  fi

  local selected_wip="${wips[$((selection-1))]}"
  local wip_name=$(basename "$selected_wip")

  echo ""
  echo -e "${CYAN}Restoring WIP: $wip_name${NC}"

  _restore_wip "$num" "$selected_wip"
}

_restore_wip() {
  local num=$1
  local wip_path=$2
  local open_after="${3:-true}"
  local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
  local wip_name=$(basename "$wip_path")
  local branch_name=$(get_branch_name "$num")
  local original_dir=$(pwd)  # Save original directory before cd'ing

  local metadata="$wip_path/metadata.json"
  local saved_branch="$branch_name"
  if [ -f "$metadata" ]; then
    # Handle both new format (branch) and old format (cloud_branch)
    saved_branch=$(grep -o '"branch"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"branch"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
    if [ -z "$saved_branch" ]; then
      saved_branch=$(grep -o '"cloud_branch"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"cloud_branch"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
    fi
    # If still empty, use default branch name
    if [ -z "$saved_branch" ]; then
      saved_branch="$branch_name"
    fi
  fi

  cd "$dir"

  # Use workspace's own branch when restoring cross-workspace, otherwise
  # use the saved branch so we land on the exact same branch as before.
  local current_branch=$(git branch --show-current)
  local target_branch="$saved_branch"
  if [ "$saved_branch" != "$branch_name" ]; then
    target_branch="$branch_name"
  fi
  if [ "$current_branch" != "$target_branch" ] && [ -n "$target_branch" ]; then
    echo "  Switching to branch $target_branch..."
    git checkout "$target_branch" 2>/dev/null || git checkout -b "$target_branch"
  fi

  # Cherry-pick saved commits (handle both main-commits.txt and cloud-commits.txt)
  local commits_file=""
  if [ -f "$wip_path/main-commits.txt" ] && [ -s "$wip_path/main-commits.txt" ]; then
    commits_file="$wip_path/main-commits.txt"
  elif [ -f "$wip_path/cloud-commits.txt" ] && [ -s "$wip_path/cloud-commits.txt" ]; then
    commits_file="$wip_path/cloud-commits.txt"
  fi

  if [ -n "$commits_file" ]; then
    echo "  Restoring commits..."
    # Read commit hashes in reverse order (oldest first) and cherry-pick
    local commit_hashes=$(awk '{print $1}' "$commits_file" | tac)
    local cherry_pick_failed=false
    for hash in $commit_hashes; do
      if git cat-file -e "$hash" 2>/dev/null; then
        if ! git cherry-pick "$hash" --no-commit 2>/dev/null; then
          echo "    (commit $hash may need manual resolution)"
          cherry_pick_failed=true
        fi
      else
        echo "    (commit $hash not found in repo - may need to fetch)"
      fi
    done
    if [ "$cherry_pick_failed" = "false" ] && [ -n "$commit_hashes" ]; then
      # Commit the cherry-picked changes
      git commit --no-edit 2>/dev/null || true
    fi
  fi

  # Apply main patches
  if [ -f "$wip_path/main-staged.patch" ] && [ -s "$wip_path/main-staged.patch" ]; then
    echo "  Applying staged changes..."
    git apply "$wip_path/main-staged.patch" 2>/dev/null && git add -A || echo "    (some hunks may have failed)"
  fi

  if [ -f "$wip_path/main-unstaged.patch" ] && [ -s "$wip_path/main-unstaged.patch" ]; then
    echo "  Applying unstaged changes..."
    git apply "$wip_path/main-unstaged.patch" 2>/dev/null || echo "    (some hunks may have failed)"
  fi

  # Apply submodule patches
  local submodules_count=$(yq -r '.submodules | length // 0' "$CONFIG_FILE" 2>/dev/null)
  for ((i=0; i<submodules_count; i++)); do
    local sub_path=$(yq -r ".submodules[$i].path" "$CONFIG_FILE" 2>/dev/null)
    [ -z "$sub_path" ] || [ "$sub_path" = "null" ] && continue

    if [ -d "$dir/$sub_path" ]; then
      cd "$dir/$sub_path"
      local safe_name=$(echo "$sub_path" | tr '/' '-')

      # Check for saved submodule branch in metadata
      local sub_branch=""
      if [ -f "$metadata" ]; then
        sub_branch=$(grep -o "\"${safe_name}_branch\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$metadata" | sed "s/\"${safe_name}_branch\"[[:space:]]*:[[:space:]]*\"//" | sed 's/"$//')
        # Also try promptfoo_branch for backwards compatibility
        if [ -z "$sub_branch" ] && [ "$sub_path" = "promptfoo" ]; then
          sub_branch=$(grep -o '"promptfoo_branch"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"promptfoo_branch"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
        fi
      fi

      # Switch submodule to saved branch if specified
      if [ -n "$sub_branch" ] && [ "$sub_branch" != "null" ]; then
        local current_sub_branch=$(git branch --show-current 2>/dev/null)
        if [ "$current_sub_branch" != "$sub_branch" ]; then
          echo "  Switching $sub_path to branch $sub_branch..."
          git checkout "$sub_branch" 2>/dev/null || git checkout -b "$sub_branch" 2>/dev/null || true
        fi
      fi

      # Cherry-pick submodule commits
      local sub_commits_file=""
      if [ -f "$wip_path/${safe_name}-commits.txt" ] && [ -s "$wip_path/${safe_name}-commits.txt" ]; then
        sub_commits_file="$wip_path/${safe_name}-commits.txt"
      fi

      if [ -n "$sub_commits_file" ]; then
        echo "  Restoring $sub_path commits..."
        local sub_hashes=$(awk '{print $1}' "$sub_commits_file" | tac)
        for hash in $sub_hashes; do
          if git cat-file -e "$hash" 2>/dev/null; then
            git cherry-pick "$hash" --no-commit 2>/dev/null || echo "    (commit $hash may need manual resolution)"
          fi
        done
        git commit --no-edit 2>/dev/null || true
      fi

      if [ -f "$wip_path/${safe_name}-staged.patch" ] && [ -s "$wip_path/${safe_name}-staged.patch" ]; then
        echo "  Applying $sub_path staged changes..."
        git apply "$wip_path/${safe_name}-staged.patch" 2>/dev/null && git add -A || echo "    (some hunks may have failed)"
      fi

      if [ -f "$wip_path/${safe_name}-unstaged.patch" ] && [ -s "$wip_path/${safe_name}-unstaged.patch" ]; then
        echo "  Applying $sub_path unstaged changes..."
        git apply "$wip_path/${safe_name}-unstaged.patch" 2>/dev/null || echo "    (some hunks may have failed)"
      fi

      cd "$dir"
    fi
  done

  # Restore untracked files
  if [ -d "$wip_path/untracked" ]; then
    echo "  Restoring untracked files..."
    cp -r "$wip_path/untracked/." "$dir/" 2>/dev/null || true
  fi

  success "WIP restored: $wip_name"

  local restored_saved_agent=""

  # Write agent session resume file if available
  if [ -f "$metadata" ]; then
    local agent_sess=$(grep -o '"agent_session"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"agent_session"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
    # Backward compat: also check claude_session
    [ -z "$agent_sess" ] && agent_sess=$(grep -o '"claude_session"[[:space:]]*:[[:space:]]*"[^"]*"' "$metadata" | sed 's/"claude_session"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
    restored_saved_agent=$(agent_type_from_metadata "$metadata")

    if [ -n "$agent_sess" ] && [ "$agent_sess" != "" ]; then
      if [ "$restored_saved_agent" = "$(get_agent_type)" ]; then
        agent_write_resume_file "$dir" "$agent_sess"
      else
        echo "$agent_sess" > "$(agent_resume_file_for_type "$restored_saved_agent" "$dir")"
        echo -e "  ${YELLOW}Saved conversation belongs to $(agent_display_name_for_type "$restored_saved_agent").${NC}"
        echo -e "  ${YELLOW}Current project agent is $(agent_display_name), so it will only resume after switching the project agent.${NC}"
      fi
    fi
  fi

  # Relaunch the workspace tmux window with agent session resume
  if [ "$open_after" = "true" ]; then
    echo ""
    echo "  Relaunching workspace $num..."
    if [ -n "$restored_saved_agent" ] && [ "$restored_saved_agent" != "$(get_agent_type)" ]; then
      open_workspace "$num"
    else
      continue_workspace "$num"
    fi
  else
    echo ""
    local resume_file=$(agent_resume_file "$dir")
    if [ -f "$resume_file" ]; then
      echo -e "  Run ${BOLD}crab $num continue${NC} to launch with the saved $(agent_display_name) conversation"
    else
      echo -e "  Run 'crab $num' to open the workspace"
    fi
  fi
}

wip_delete() {
  local num=$1
  local wip_name=$2
  local wip_dir=$(get_wip_dir "$num")

  if [ -z "$wip_name" ]; then
    error "Usage: crabcode wip delete <wip-name>"
    return 1
  fi

  local wip_path="$wip_dir/$wip_name"

  if [ ! -d "$wip_path" ]; then
    wip_path=$(ls -1d "$wip_dir"/*"$wip_name"*/ 2>/dev/null | head -1)
  fi

  if [ ! -d "$wip_path" ]; then
    error "WIP not found: $wip_name"
    return 1
  fi

  local actual_name=$(basename "$wip_path")
  read -p "Delete WIP '$actual_name'? [y/N]: " confirm

  if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
    rm -rf "$wip_path"
    success "Deleted: $actual_name"
  else
    echo "Cancelled."
  fi
}

# =============================================================================
# Toolkit - Share
# =============================================================================

# Default exclude patterns for sharing (dependencies, build artifacts)
SHARE_EXCLUDE_PATTERNS=(
  "node_modules"
  ".git"
  "vendor"
  "venv"
  ".venv"
  "__pycache__"
  ".pytest_cache"
  "dist"
  "build"
  ".next"
  ".nuxt"
  "target"
  ".cache"
  "*.pyc"
  "*.pyo"
  ".DS_Store"
  "Thumbs.db"
)

# Create a clean archive of a path, excluding dependencies
# Returns the path to the created archive
tk_create_archive() {
  local source_path="$1"
  local include_git="${2:-false}"
  local include_deps="${3:-false}"

  local abs_path=$(cd "$source_path" 2>/dev/null && pwd || echo "$source_path")
  local base_name=$(basename "$abs_path")
  local timestamp=$(date +%Y%m%d-%H%M%S)
  local archive_name="${base_name}-${timestamp}.zip"
  local archive_path="/tmp/$archive_name"

  echo -e "${CYAN}Creating archive...${NC}"

  # Build exclude arguments for zip
  local exclude_args=""

  if [ "$include_deps" != "true" ]; then
    for pattern in "${SHARE_EXCLUDE_PATTERNS[@]}"; do
      # Skip .git if include_git is true
      if [ "$pattern" = ".git" ] && [ "$include_git" = "true" ]; then
        continue
      fi
      exclude_args="$exclude_args -x '$base_name/$pattern/*' -x '$base_name/$pattern'"
    done
    echo "  Excluding: dependencies and build artifacts"
  fi

  if [ "$include_git" != "true" ]; then
    echo "  Excluding: .git/"
  fi

  # Check if it's a git repo and respect .gitignore
  if [ -d "$abs_path/.git" ] || [ -f "$abs_path/.git" ]; then
    # Get list of gitignored files and add to excludes
    local gitignored=$(cd "$abs_path" && git ls-files --others --ignored --exclude-standard 2>/dev/null | head -100)
    if [ -n "$gitignored" ]; then
      echo "  Respecting: .gitignore patterns"
    fi
  fi

  # Create the archive
  cd "$(dirname "$abs_path")"

  # Use eval to properly handle the exclude args
  if eval "zip -rq '$archive_path' '$base_name' $exclude_args" 2>/dev/null; then
    local size=$(du -h "$archive_path" | cut -f1)
    echo -e "  ${GREEN}Created: $archive_name ($size)${NC}"
    echo "$archive_path"
  else
    error "Failed to create archive"
    return 1
  fi
}

# Upload to transfer.sh
tk_upload_transfersh() {
  local file_path="$1"
  local filename=$(basename "$file_path")
  local filesize=$(du -h "$file_path" | cut -f1)

  echo -e "${CYAN}Uploading to transfer.sh ($filesize)...${NC}"

  # Create temp file for error output
  local error_file=$(mktemp)

  # Use -f to fail silently on HTTP errors, capture response
  local response
  response=$(curl -f --progress-bar --upload-file "$file_path" "https://transfer.sh/$filename" 2>"$error_file")
  local curl_exit=$?

  # Read any error output
  local error_output=$(cat "$error_file" 2>/dev/null)
  rm -f "$error_file"

  if [ $curl_exit -eq 0 ] && [ -n "$response" ] && [[ "$response" == http* ]]; then
    echo ""
    echo -e "${GREEN}Upload complete!${NC}"
    echo ""
    echo -e "  URL: ${BOLD}$response${NC}"
    echo "  Expires: 14 days"
    echo ""

    # Copy to clipboard if possible
    if command_exists pbcopy; then
      echo -n "$response" | pbcopy
      echo "  (Copied to clipboard)"
    elif command_exists xclip; then
      echo -n "$response" | xclip -selection clipboard
      echo "  (Copied to clipboard)"
    fi

    return 0
  else
    # Show error details for debugging
    if [ -n "$error_output" ]; then
      echo -e "${YELLOW}transfer.sh error: $error_output${NC}" >&2
    fi
    if [ -n "$response" ] && [[ "$response" != http* ]]; then
      echo -e "${YELLOW}Unexpected response: $response${NC}" >&2
    fi
    return 1
  fi
}

# Upload to 0x0.st (alternative service)
tk_upload_0x0() {
  local file_path="$1"
  local filesize=$(du -h "$file_path" | cut -f1)

  echo -e "${CYAN}Uploading to 0x0.st ($filesize)...${NC}"

  # Create temp file for error output
  local error_file=$(mktemp)

  local response
  response=$(curl -f -F "file=@$file_path" https://0x0.st 2>"$error_file")
  local curl_exit=$?

  local error_output=$(cat "$error_file" 2>/dev/null)
  rm -f "$error_file"

  if [ $curl_exit -eq 0 ] && [ -n "$response" ] && [[ "$response" == http* ]]; then
    echo ""
    echo -e "${GREEN}Upload complete!${NC}"
    echo ""
    echo -e "  URL: ${BOLD}$response${NC}"
    echo ""

    # Copy to clipboard
    if command_exists pbcopy; then
      echo -n "$response" | pbcopy
      echo "  (Copied to clipboard)"
    elif command_exists xclip; then
      echo -n "$response" | xclip -selection clipboard
      echo "  (Copied to clipboard)"
    fi

    return 0
  else
    if [ -n "$error_output" ]; then
      echo -e "${YELLOW}0x0.st error: $error_output${NC}" >&2
    fi
    return 1
  fi
}

# Upload with automatic fallback between services
tk_upload_with_fallback() {
  local file_path="$1"

  # Try transfer.sh first
  if tk_upload_transfersh "$file_path"; then
    return 0
  fi

  echo ""
  echo -e "${YELLOW}transfer.sh failed, trying 0x0.st...${NC}"
  echo ""

  # Fallback to 0x0.st
  if tk_upload_0x0 "$file_path"; then
    return 0
  fi

  echo ""
  error "All upload services failed"
  echo "Try using SSH transfer instead: crab tk share <path> --to ssh:user@host"
  return 1
}

# Transfer via SSH/rsync
tk_transfer_ssh() {
  local file_path="$1"
  local destination="$2"

  # Parse destination: user@host:/path or just host
  local user_host="${destination%%:*}"
  local remote_path="${destination#*:}"

  # Default to ~/Downloads if no path specified
  if [ "$remote_path" = "$destination" ]; then
    remote_path="~/Downloads/"
  fi

  echo -e "${CYAN}Transferring via SSH...${NC}"
  echo "  Destination: $user_host:$remote_path"

  if command_exists rsync; then
    rsync -avz --progress "$file_path" "$user_host:$remote_path"
  else
    scp "$file_path" "$user_host:$remote_path"
  fi

  if [ $? -eq 0 ]; then
    echo ""
    success "Transfer complete!"
  else
    error "Transfer failed"
    return 1
  fi
}

# Upload to Slack
tk_upload_slack() {
  local file_path="$1"
  local target="${2:-}"

  # Use same token as crab slack commands
  local slack_token=$(config_get "slack.bot_token" "")

  # Check for slackcat first (easier)
  if command_exists slackcat && [ -n "$target" ]; then
    echo -e "${CYAN}Uploading to Slack via slackcat...${NC}"
    slackcat --channel "$target" "$file_path"
    return $?
  fi

  # Otherwise use curl with Slack API
  if [ -z "$slack_token" ]; then
    error "Slack not configured. Set CRAB_SLACK_BOT_TOKEN env var:"
    echo ""
    echo "  export CRAB_SLACK_BOT_TOKEN=xoxb-your-bot-token"
    return 1
  fi

  if [ -z "$target" ]; then
    error "No target specified. Use --to slack:@user or --to slack:#channel"
    return 1
  fi

  # Resolve target to channel ID (supports @user and #channel)
  local channel_id
  if [[ "$target" == "@"* ]]; then
    local username="${target#@}"
    echo -e "${CYAN}Looking up user: $username...${NC}"
    local user_id=$(slack_get_user_id "$username")
    if [ -z "$user_id" ]; then
      error "User not found: $target"
      return 1
    fi
    channel_id=$(slack_open_dm "$user_id")
    if [ -z "$channel_id" ] || [ "$channel_id" = "null" ]; then
      error "Failed to open DM with $target"
      return 1
    fi
    echo -e "${CYAN}Uploading file to @$username...${NC}"
  else
    # Remove # prefix if present
    local channel_name="${target#\#}"
    channel_id=$(slack_get_channel_id "$channel_name")
    if [ -z "$channel_id" ]; then
      error "Channel not found: $target"
      return 1
    fi
    echo -e "${CYAN}Uploading file to #$channel_name...${NC}"
  fi

  # Step 1: Get upload URL (new Slack API)
  local filename=$(basename "$file_path")
  local filesize=$(stat -f%z "$file_path" 2>/dev/null || stat -c%s "$file_path" 2>/dev/null)

  local upload_response=$(curl -s -X POST "https://slack.com/api/files.getUploadURLExternal" \
    -H "Authorization: Bearer $slack_token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "filename=$filename&length=$filesize")

  local upload_url=$(echo "$upload_response" | jq -r '.upload_url // empty')
  local file_id=$(echo "$upload_response" | jq -r '.file_id // empty')

  if [ -z "$upload_url" ] || [ -z "$file_id" ]; then
    error "Failed to get upload URL from Slack"
    echo "$upload_response" | jq -r '.error // .'
    return 1
  fi

  # Step 2: Upload file to the URL
  curl -s -X POST "$upload_url" \
    -F "file=@$file_path" > /dev/null

  # Step 3: Complete upload and share to channel/DM
  local complete_response=$(curl -s -X POST "https://slack.com/api/files.completeUploadExternal" \
    -H "Authorization: Bearer $slack_token" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg file_id "$file_id" --arg channel_id "$channel_id" \
      '{files: [{id: $file_id}], channel_id: $channel_id}')")

  local ok=$(echo "$complete_response" | jq -r '.ok')
  if [ "$ok" = "true" ]; then
    echo ""
    success "File uploaded to $target!"
  else
    error "Failed to complete Slack upload"
    echo "$complete_response" | jq -r '.error // .'
    return 1
  fi
}

# Send via email
tk_send_email() {
  local file_path="$1"
  local recipient="$2"
  local filename=$(basename "$file_path")

  echo -e "${CYAN}Sending via email...${NC}"

  # Check for mutt (preferred for attachments)
  if command_exists mutt; then
    local subject="Shared file: $filename"
    local body="Shared via crabcode toolkit.\n\nFile: $filename"

    echo -e "$body" | mutt -s "$subject" -a "$file_path" -- "$recipient"

    if [ $? -eq 0 ]; then
      success "Email sent to $recipient"
      return 0
    else
      error "Failed to send email"
      return 1
    fi
  fi

  # macOS: Open in Mail.app
  if [[ "$OSTYPE" == "darwin"* ]]; then
    echo "Opening Mail.app..."
    open -a Mail "$file_path"
    echo -e "${YELLOW}Please compose and send the email manually${NC}"
    return 0
  fi

  # Fallback: check for mail/mailx
  if command_exists mail; then
    echo -e "${YELLOW}Note: mail command may not support attachments on all systems${NC}"
    echo "File: $file_path"
    echo ""
    echo "To send manually:"
    echo "  echo 'See attached' | mutt -s 'Shared file' -a '$file_path' -- $recipient"
    return 1
  fi

  error "No email client found. Install mutt:"
  echo "  brew install mutt    # macOS"
  echo "  apt install mutt     # Linux"
  return 1
}

# Start local HTTP server
tk_serve() {
  local source_path="$1"
  local port="${2:-8080}"

  local abs_path=$(cd "$source_path" 2>/dev/null && pwd || echo "$source_path")

  # If it's a directory, create an archive first
  local file_to_serve="$abs_path"
  if [ -d "$abs_path" ]; then
    file_to_serve=$(tk_create_archive "$abs_path")
  fi

  local filename=$(basename "$file_to_serve")
  local filesize=$(du -h "$file_to_serve" | cut -f1)

  # Get local IP
  local local_ip=""
  if command_exists ipconfig; then
    # macOS
    local_ip=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null)
  else
    # Linux
    local_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
  fi

  echo ""
  echo -e "${CYAN}Starting file server...${NC}"
  echo ""
  echo -e "  File:    ${BOLD}$filename${NC} ($filesize)"
  echo -e "  Local:   ${BOLD}http://localhost:$port/$filename${NC}"
  [ -n "$local_ip" ] && echo -e "  Network: ${BOLD}http://$local_ip:$port/$filename${NC}"
  echo ""

  # Generate QR code if qrencode is available
  if command_exists qrencode && [ -n "$local_ip" ]; then
    echo "QR Code (scan to download):"
    qrencode -t ANSI256 "http://$local_ip:$port/$filename" 2>/dev/null || true
    echo ""
  fi

  echo -e "${YELLOW}Press Ctrl+C to stop${NC}"
  echo ""

  # Create a temporary directory for serving
  local serve_dir=$(mktemp -d)
  cp "$file_to_serve" "$serve_dir/"

  # Use Python's built-in HTTP server
  if command_exists python3; then
    cd "$serve_dir" && python3 -m http.server "$port"
  elif command_exists python; then
    cd "$serve_dir" && python -m SimpleHTTPServer "$port"
  else
    error "Python not found. Cannot start HTTP server."
    rm -rf "$serve_dir"
    return 1
  fi

  # Cleanup
  rm -rf "$serve_dir"
}

# Main toolkit share handler
handle_tk_share() {
  local path="${1:-.}"
  shift || true

  # Parse options
  local to=""
  local include_git="false"
  local include_deps="false"
  local zip_only="false"
  local serve_mode="false"
  local port="8080"

  while [ $# -gt 0 ]; do
    case "$1" in
      --to)
        to="$2"
        shift 2
        ;;
      --to=*)
        to="${1#--to=}"
        shift
        ;;
      --include-git)
        include_git="true"
        shift
        ;;
      --include-deps)
        include_deps="true"
        shift
        ;;
      --zip)
        zip_only="true"
        shift
        ;;
      --serve)
        serve_mode="true"
        shift
        ;;
      --port)
        port="$2"
        shift 2
        ;;
      --port=*)
        port="${1#--port=}"
        shift
        ;;
      *)
        shift
        ;;
    esac
  done

  # Validate path
  if [ ! -e "$path" ]; then
    error "Path not found: $path"
    return 1
  fi

  # Handle serve mode
  if [ "$serve_mode" = "true" ]; then
    tk_serve "$path" "$port"
    return $?
  fi

  # Create archive if it's a directory
  local file_to_share="$path"
  if [ -d "$path" ]; then
    file_to_share=$(tk_create_archive "$path" "$include_git" "$include_deps")
    if [ $? -ne 0 ]; then
      return 1
    fi
  fi

  # Just create zip
  if [ "$zip_only" = "true" ]; then
    local filename=$(basename "$file_to_share")
    local dest="./$filename"
    cp "$file_to_share" "$dest"
    success "Archive created: $dest"
    return 0
  fi

  # Handle different destinations
  case "$to" in
    ""|url)
      # Default: use auto-fallback between transfer.sh and 0x0.st
      tk_upload_with_fallback "$file_to_share"
      ;;
    transfer.sh)
      # Explicit transfer.sh (no fallback)
      tk_upload_transfersh "$file_to_share"
      ;;
    0x0|0x0.st)
      tk_upload_0x0 "$file_to_share"
      ;;
    ssh:*|scp:*|rsync:*)
      local dest="${to#*:}"
      tk_transfer_ssh "$file_to_share" "$dest"
      ;;
    slack:*|slack)
      local channel="${to#slack:}"
      [ "$channel" = "slack" ] && channel=""
      tk_upload_slack "$file_to_share" "$channel"
      ;;
    email:*|mail:*)
      local recipient="${to#*:}"
      tk_send_email "$file_to_share" "$recipient"
      ;;
    *)
      # Assume it's an SSH destination if it contains @
      if [[ "$to" == *"@"* ]]; then
        tk_transfer_ssh "$file_to_share" "$to"
      else
        error "Unknown destination: $to"
        echo "Use: --to=url, --to=ssh:user@host, --to=slack:#channel, --to=email:addr"
        return 1
      fi
      ;;
  esac
}

# Show toolkit help
show_tk_help() {
  echo -e "${CYAN}Toolkit Commands${NC}"
  echo ""
  echo "Share files and folders:"
  echo "  crab tk share <path>                    Upload and get temp URL"
  echo "  crab tk share <path> --to ssh:user@host Transfer via SSH"
  echo "  crab tk share <path> --to slack:#chan   Upload to Slack"
  echo "  crab tk share <path> --to email:addr    Send via email"
  echo "  crab tk share <path> --zip              Just create archive"
  echo "  crab tk share <path> --serve            Start local HTTP server"
  echo ""
  echo "Options:"
  echo "  --include-git     Include .git folder"
  echo "  --include-deps    Include dependencies (node_modules, etc.)"
  echo "  --port=8080       Port for --serve mode"
  echo ""
  echo "For git repos, automatically excludes:"
  echo "  node_modules, vendor, venv, dist, build, .git, etc."
}

# Handle toolkit subcommand
handle_tk_command() {
  local cmd="${1:-}"
  shift || true

  case "$cmd" in
    "share")
      handle_tk_share "$@"
      ;;
    "serve")
      tk_serve "${1:-.}" "${2:-8080}"
      ;;
    ""|"help"|"-h"|"--help")
      show_tk_help
      ;;
    *)
      error "Unknown toolkit command: $cmd"
      echo "Run 'crab tk help' for usage"
      return 1
      ;;
  esac
}

# =============================================================================
# Slack Integration
# =============================================================================

# Get Slack bot token from env var (preferred) or config
slack_get_token() {
  if [ -n "${CRAB_SLACK_BOT_TOKEN:-}" ]; then
    echo "$CRAB_SLACK_BOT_TOKEN"
  else
    config_get "slack.bot_token" ""
  fi
}

# Get display name for messages (git config or override)
slack_get_display_name() {
  local configured=$(config_get "slack.display_name" "")
  if [ -n "$configured" ] && [ "$configured" != "null" ]; then
    echo "$configured"
  else
    git config user.name 2>/dev/null || whoami
  fi
}

# Lookup Slack user ID from @username
slack_get_user_id() {
  local username="$1"
  local token=$(slack_get_token)

  # Remove @ prefix if present
  username="${username#@}"

  # Get user list and find matching username
  local response=$(curl -s -H "Authorization: Bearer $token" \
    "https://slack.com/api/users.list?limit=500")

  # Extract user ID where name or display_name matches
  echo "$response" | jq -r --arg name "$username" \
    '.members[] | select(.name == $name or .profile.display_name == $name or .real_name == $name) | .id' | head -1
}

# Lookup Slack channel ID from #channel
slack_get_channel_id() {
  local channel="$1"
  local token=$(slack_get_token)

  # Remove # prefix if present
  channel="${channel#\#}"

  # Get channel list
  local response=$(curl -s -H "Authorization: Bearer $token" \
    "https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=500")

  echo "$response" | jq -r --arg name "$channel" \
    '.channels[] | select(.name == $name) | .id' | head -1
}

# Open a DM channel with a user
slack_open_dm() {
  local user_id="$1"
  local token=$(slack_get_token)

  local response=$(curl -s -X POST -H "Authorization: Bearer $token" \
    -H "Content-Type: application/json" \
    -d "{\"users\":\"$user_id\"}" \
    "https://slack.com/api/conversations.open")

  echo "$response" | jq -r '.channel.id'
}

# Send a message to Slack
slack_post_message() {
  local channel_id="$1"
  local message="$2"
  local token=$(slack_get_token)
  local display_name=$(slack_get_display_name)

  # Format: 🦀 [Name] message - with crab icon as bot avatar
  local full_message="*[$display_name]* $message"

  local response=$(curl -s -X POST -H "Authorization: Bearer $token" \
    -H "Content-Type: application/json" \
    -d "$(jq -n \
      --arg channel "$channel_id" \
      --arg text "$full_message" \
      --arg username "crab ($display_name)" \
      '{channel: $channel, text: $text, icon_emoji: ":crab:", username: $username}')" \
    "https://slack.com/api/chat.postMessage")

  local ok=$(echo "$response" | jq -r '.ok')
  if [ "$ok" = "true" ]; then
    return 0
  else
    local err=$(echo "$response" | jq -r '.error')
    error "Slack API error: $err"
    return 1
  fi
}

# Read message history from a channel/DM
slack_read_history() {
  local channel_id="$1"
  local limit="${2:-10}"
  local token=$(slack_get_token)

  local response=$(curl -s -H "Authorization: Bearer $token" \
    "https://slack.com/api/conversations.history?channel=$channel_id&limit=$limit")

  local ok=$(echo "$response" | jq -r '.ok')
  if [ "$ok" != "true" ]; then
    local err=$(echo "$response" | jq -r '.error')
    error "Failed to read history: $err"
    return 1
  fi

  # Parse and display messages (oldest first)
  echo "$response" | jq -r '.messages | reverse | .[] |
    "\(.ts | tonumber | strftime("%H:%M")) \(.user // "bot"): \(.text)"' 2>/dev/null
}

# Get username from user ID (for display)
slack_get_username() {
  local user_id="$1"
  local token=$(slack_get_token)

  local response=$(curl -s -H "Authorization: Bearer $token" \
    "https://slack.com/api/users.info?user=$user_id")

  echo "$response" | jq -r '.user.profile.display_name // .user.real_name // .user.name'
}

# Cache for user ID to name mapping (file-based for bash 3.x compatibility)
SLACK_USER_CACHE_DIR="/tmp/crab-slack-cache-$$"

slack_get_username_cached() {
  local user_id="$1"
  local cache_file="$SLACK_USER_CACHE_DIR/$user_id"

  mkdir -p "$SLACK_USER_CACHE_DIR" 2>/dev/null

  if [ -f "$cache_file" ]; then
    cat "$cache_file"
  else
    local name=$(slack_get_username "$user_id")
    echo "$name" > "$cache_file"
    echo "$name"
  fi
}

# Format and display messages with usernames
slack_display_messages() {
  local channel_id="$1"
  local limit="${2:-10}"
  local token=$(slack_get_token)

  local response=$(curl -s -H "Authorization: Bearer $token" \
    "https://slack.com/api/conversations.history?channel=$channel_id&limit=$limit")

  local ok=$(echo "$response" | jq -r '.ok')
  if [ "$ok" != "true" ]; then
    return 1
  fi

  # Get messages as JSON array (reversed for chronological order)
  local messages=$(echo "$response" | jq -c '.messages | reverse | .[]')

  while IFS= read -r msg; do
    local ts=$(echo "$msg" | jq -r '.ts | tonumber | strftime("%H:%M")')
    local user_id=$(echo "$msg" | jq -r '.user // empty')
    local text=$(echo "$msg" | jq -r '.text')
    local bot_name=$(echo "$msg" | jq -r '.bot_profile.name // empty')

    local sender
    if [ -n "$bot_name" ]; then
      sender="$bot_name"
    elif [ -n "$user_id" ]; then
      sender=$(slack_get_username_cached "$user_id")
    else
      sender="unknown"
    fi

    echo -e "${GRAY}[$ts]${NC} ${CYAN}$sender${NC}: $text"
  done <<< "$messages"
}

# Interactive chat mode
slack_chat() {
  local target="$1"
  local token=$(slack_get_token)

  if [ -z "$token" ]; then
    error "Slack not configured. Set CRAB_SLACK_BOT_TOKEN env var"
    return 1
  fi

  # Resolve target to channel ID
  local channel_id
  local target_name

  if [[ "$target" == "#"* ]]; then
    target_name="${target#\#}"
    channel_id=$(slack_get_channel_id "$target_name")
    if [ -z "$channel_id" ]; then
      error "Channel not found: $target"
      return 1
    fi
  elif [[ "$target" == "@"* ]]; then
    target_name="${target#@}"
    local user_id=$(slack_get_user_id "$target_name")
    if [ -z "$user_id" ]; then
      error "User not found: $target"
      return 1
    fi
    channel_id=$(slack_open_dm "$user_id")
    if [ -z "$channel_id" ] || [ "$channel_id" = "null" ]; then
      error "Failed to open DM with $target"
      return 1
    fi
  else
    error "Target must start with @ (user) or # (channel)"
    return 1
  fi

  # Display chat header
  echo ""
  echo -e "${CYAN}━━ Chat with $target ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  echo -e "${GRAY}Type messages and press Enter. Ctrl+C to exit.${NC}"
  echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  echo ""

  # Show recent history
  slack_display_messages "$channel_id" 10

  echo ""
  echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"

  # Chat loop
  local last_check=$(date +%s)

  while true; do
    # Non-blocking read with timeout for polling
    echo -ne "${GREEN}>${NC} "

    if read -t 5 -r message; then
      if [ -n "$message" ]; then
        if slack_post_message "$channel_id" "$message"; then
          echo -e "${GRAY}[sent]${NC}"
        fi
      fi
    fi

    # Poll for new messages every 5 seconds
    local now=$(date +%s)
    if [ $((now - last_check)) -ge 5 ]; then
      # Check for new messages (would need to track last seen timestamp for proper implementation)
      last_check=$now
    fi
  done
}

# Log file for sent messages
SLACK_SENT_LOG="$CONFIG_DIR/slack_sent.log"

# Log a sent message
slack_log_sent() {
  local target="$1"
  local message="$2"
  local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
  local display_name=$(slack_get_display_name)

  mkdir -p "$CONFIG_DIR"
  echo "[$timestamp] $display_name → $target: $message" >> "$SLACK_SENT_LOG"
}

# View sent messages log
slack_view_sent() {
  local count="${1:-20}"

  if [ ! -f "$SLACK_SENT_LOG" ]; then
    echo "No sent messages yet."
    return
  fi

  echo -e "${CYAN}━━ Sent Messages ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  echo ""
  tail -n "$count" "$SLACK_SENT_LOG" | while IFS= read -r line; do
    echo -e "  ${GRAY}$line${NC}"
  done
  echo ""
}

# Quick send message
slack_send() {
  local target="$1"
  local message="$2"
  local token=$(slack_get_token)

  if [ -z "$token" ]; then
    error "Slack not configured. Set CRAB_SLACK_BOT_TOKEN env var"
    return 1
  fi

  if [ -z "$target" ] || [ -z "$message" ]; then
    error "Usage: crab slack @user \"message\" or crab slack #channel \"message\""
    return 1
  fi

  # Resolve target to channel ID
  local channel_id

  if [[ "$target" == "#"* ]]; then
    local channel_name="${target#\#}"
    channel_id=$(slack_get_channel_id "$channel_name")
    if [ -z "$channel_id" ]; then
      error "Channel not found: $target"
      return 1
    fi
  elif [[ "$target" == "@"* ]]; then
    local username="${target#@}"
    local user_id=$(slack_get_user_id "$username")
    if [ -z "$user_id" ]; then
      error "User not found: $target"
      return 1
    fi
    channel_id=$(slack_open_dm "$user_id")
    if [ -z "$channel_id" ] || [ "$channel_id" = "null" ]; then
      error "Failed to open DM with $target"
      return 1
    fi
  else
    error "Target must start with @ (user) or # (channel)"
    return 1
  fi

  if slack_post_message "$channel_id" "$message"; then
    slack_log_sent "$target" "$message"
    success "Message sent to $target"
  fi
}

# Read recent messages from a conversation
slack_read() {
  local target="$1"
  local limit="${2:-10}"
  local token=$(slack_get_token)

  if [ -z "$token" ]; then
    error "Slack not configured. Set CRAB_SLACK_BOT_TOKEN env var"
    return 1
  fi

  if [ -z "$target" ]; then
    error "Usage: crab slack read @user [count]"
    return 1
  fi

  # Resolve target to channel ID
  local channel_id
  local target_name

  if [[ "$target" == "#"* ]]; then
    target_name="${target#\#}"
    channel_id=$(slack_get_channel_id "$target_name")
    if [ -z "$channel_id" ]; then
      error "Channel not found: $target"
      return 1
    fi
  elif [[ "$target" == "@"* ]]; then
    target_name="${target#@}"
    local user_id=$(slack_get_user_id "$target_name")
    if [ -z "$user_id" ]; then
      error "User not found: $target"
      return 1
    fi
    channel_id=$(slack_open_dm "$user_id")
    if [ -z "$channel_id" ] || [ "$channel_id" = "null" ]; then
      error "Failed to open DM with $target"
      return 1
    fi
  else
    error "Target must start with @ (user) or # (channel)"
    return 1
  fi

  echo ""
  echo -e "${CYAN}━━ Messages with $target ━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  echo ""
  slack_display_messages "$channel_id" "$limit"
  echo ""
}

# List available users
slack_list_users() {
  local token=$(slack_get_token)
  local filter="${1:-}"

  echo -e "${CYAN}Slack Users:${NC}"
  echo ""

  local response=$(curl -s -H "Authorization: Bearer $token" \
    "https://slack.com/api/users.list?limit=200")

  if [ -n "$filter" ]; then
    echo "$response" | jq -r --arg filter "$filter" \
      '.members[] | select(.deleted == false and .is_bot == false) | select(.name | contains($filter)) | "  @\(.name)  \(.profile.display_name // .real_name // "-")"'
  else
    echo "$response" | jq -r \
      '.members[] | select(.deleted == false and .is_bot == false) | "  @\(.name)  \(.profile.display_name // .real_name // "-")"'
  fi
}

# Handle slack command
handle_slack_command() {
  local first="${1:-}"

  if [ -z "$first" ]; then
    show_slack_help
    return
  fi

  case "$first" in
    "chat")
      shift
      slack_chat "$@"
      ;;
    "read"|"history")
      shift
      slack_read "$@"
      ;;
    "sent"|"log")
      shift
      slack_view_sent "$@"
      ;;
    "users"|"list")
      shift
      slack_list_users "$@"
      ;;
    "help"|"-h"|"--help")
      show_slack_help
      ;;
    "@"*|"#"*)
      # Quick send: crab slack @user "message" or crab slack #channel "message"
      local target="$1"
      shift
      local message="$*"
      slack_send "$target" "$message"
      ;;
    *)
      error "Unknown slack command: $first"
      show_slack_help
      return 1
      ;;
  esac
}

show_slack_help() {
  echo -e "${CYAN}Slack Commands${NC}"
  echo ""
  echo "Quick send:"
  echo "  crab slack @user \"message\"      Send DM to a user"
  echo "  crab slack #channel \"message\"   Post to a channel"
  echo ""
  echo "Read messages:"
  echo "  crab slack read @user           Show recent DMs with user"
  echo "  crab slack read #channel        Show recent channel messages"
  echo "  crab slack read @user 20        Show last 20 messages"
  echo ""
  echo "Interactive chat:"
  echo "  crab slack chat @user           Open chat session with user"
  echo "  crab slack chat #channel        Open chat session in channel"
  echo ""
  echo "Utilities:"
  echo "  crab slack sent                 View your sent messages log"
  echo "  crab slack sent 50              Show last 50 sent messages"
  echo "  crab slack users                List all users"
  echo "  crab slack users mike           Search users by name"
  echo ""
  echo "Examples:"
  echo "  crab slack @michael \"hey, can you review my PR?\""
  echo "  crab slack read @michael"
  echo "  crab slack chat @michael"
  echo ""
  echo "Configuration:"
  echo "  export CRAB_SLACK_BOT_TOKEN=xoxb-your-token  # add to shell profile"
  echo ""
  echo "Optional (~/.crabcode/config.yaml):"
  echo "  slack:"
  echo "    display_name: \"Your Name\"  # defaults to git config"
}

# =============================================================================
# Init / Config / Doctor
# =============================================================================

# -- Template System ----------------------------------------------------------

template_promptfoo_cloud() {
  cat << 'TEMPLATE_EOF'
# Crabcode Configuration - Promptfoo Cloud Template
# Generated by 'crabcode init --template promptfoo-cloud'

session_name: ALIAS_PLACEHOLDER
workspace_base: WORKSPACE_BASE_PLACEHOLDER
main_repo: MAIN_REPO_PLACEHOLDER

workspaces:
  prefix: cloud-workspace
  branch_pattern: workspace-{N}

layout:
  panes:
    - name: terminal
      command: ""
    - name: server
      command: pnpm dev
    - name: main
      command: codex --full-auto

install_command: pnpm install
install_env: PROMPTFOO_NODE_MODULES_CACHED=true

submodules:
  - path: promptfoo
    reset_to: origin/main
    install_command: pnpm install

env_sync:
  port_spacing: 10
  port_bases:
    api: 3200
    app: 3000
    admin: 3100
  files:
    - path: server/.env
      copy_from: server/.env.example
      ports:
        API_URL: api
        APP_URL: app
        ADMIN_APP_URL: admin
      refs:
        API_PORT: API_URL:port
    - path: app/.env
      copy_from: app/.env.example
      refs:
        VITE_API_BASE_URL: API_URL
        PORT: APP_URL:port
    - path: admin-app/.env
      ports:
        PORT: admin
      refs:
        API_PORT: API_URL:port
    - path: promptfoo/.env
      refs:
        PROMPTFOO_REMOTE_GENERATION_URL: API_URL
        PROMPTFOO_UNALIGNED_INFERENCE_ENDPOINT: API_URL
        API_HOST: API_URL

cleanup:
  preserve_files: ".env"
  kill_pattern: "cloud-workspace-{N}.*pnpm|cloud-workspace-{N}.*node"

shared_volume:
  enabled: true
  path: ~/.crabcode/shared
  link_as: .local
TEMPLATE_EOF
}

detect_template() {
  local repo_path="${1:-.}"

  # Check for promptfoo-cloud: has .gitmodules with "promptfoo" AND server/.env.example
  if [ -f "$repo_path/.gitmodules" ] && grep -q "promptfoo" "$repo_path/.gitmodules" 2>/dev/null \
     && [ -f "$repo_path/server/.env.example" ]; then
    echo "promptfoo-cloud"
    return
  fi

  # Fallback: directory name contains "promptfoo-cloud"
  if [[ "$(basename "$repo_path")" == *"promptfoo-cloud"* ]]; then
    echo "promptfoo-cloud"
    return
  fi

  echo ""
}

apply_template() {
  local template_name="$1"
  local main_repo="$2"
  local workspace_base="$3"
  local alias="${4:-crab}"

  local template_content=""
  case "$template_name" in
    "promptfoo-cloud")
      template_content="$(template_promptfoo_cloud)"
      ;;
    *)
      error "Unknown template: $template_name"
      return 1
      ;;
  esac

  mkdir -p "$PROJECTS_DIR"
  echo "$template_content" \
    | sed "s|MAIN_REPO_PLACEHOLDER|$main_repo|g" \
    | sed "s|WORKSPACE_BASE_PLACEHOLDER|$workspace_base|g" \
    | sed "s|ALIAS_PLACEHOLDER|$alias|g" \
    > "$CONFIG_FILE"
}

show_templates() {
  echo -e "${CYAN}Available Templates${NC}"
  echo ""
  echo -e "  ${GREEN}promptfoo-cloud${NC}  Full config for promptfoo-cloud repos"
  echo "                    Includes env_sync, submodules, port_spacing, install_env"
  echo ""
  echo "Usage:"
  echo "  crab init --template promptfoo-cloud"
  echo "  crab init -t promptfoo-cloud"
}

# -- End Template System ------------------------------------------------------

show_init() {
  # Parse arguments
  local template_name=""
  local alias_input=""
  while [ $# -gt 0 ]; do
    case "$1" in
      --template|-t)
        template_name="$2"
        shift 2
        ;;
      --alias|-a)
        alias_input="$2"
        shift 2
        ;;
      --list-templates)
        show_templates
        return
        ;;
      *)
        shift
        ;;
    esac
  done

  echo -e "${CYAN}Crabcode Setup${NC}"
  echo ""

  # Main repo
  local default_repo=$(pwd)
  echo -e "Path to your main repo"
  read -p "  [$default_repo]: " main_repo
  main_repo=${main_repo:-$default_repo}

  # Expand ~
  main_repo="${main_repo/#\~/$HOME}"

  if [ ! -d "$main_repo" ]; then
    error "Directory does not exist: $main_repo"
    return 1
  fi

  if [ ! -d "$main_repo/.git" ]; then
    warn "Not a git repository: $main_repo"
    read -p "Continue anyway? [y/N]: " confirm
    if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
      return 1
    fi
  fi

  # Project alias
  if [ -z "$alias_input" ]; then
    local default_alias=$(basename "$main_repo" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g')
    echo ""
    echo -e "Project alias (used in ${GREEN}crab @alias ws 1${NC})"
    read -p "  [$default_alias]: " alias_input
    alias_input=${alias_input:-$default_alias}
  fi

  # Validate alias
  if ! [[ "$alias_input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
    error "Alias must be alphanumeric (dashes and underscores allowed)"
    return 1
  fi

  # Check for existing project with same alias
  if [ -f "$PROJECTS_DIR/${alias_input}.yaml" ]; then
    echo -e "${YELLOW}Project @$alias_input already exists.${NC}"
    read -p "Overwrite? [y/N]: " confirm
    if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
      echo "Cancelled."
      return
    fi
  fi

  # Set CONFIG_FILE to the project-specific path
  CONFIG_FILE="$PROJECTS_DIR/${alias_input}.yaml"
  PROJECT_ALIAS="$alias_input"

  # Workspace base - derive from main repo
  local repo_name=$(basename "$main_repo")
  local repo_parent=$(dirname "$main_repo")
  local default_base="$repo_parent/${repo_name}-workspaces"

  echo ""
  echo -e "Where should workspaces be created?"
  echo -e "  (Each workspace is a git worktree, e.g., ${repo_name}-workspaces/ws-1)"
  read -p "  [$default_base]: " workspace_base
  workspace_base=${workspace_base:-$default_base}

  # Expand ~
  workspace_base="${workspace_base/#\~/$HOME}"

  # Check for project-level config in repo directory
  local project_config="$main_repo/.crabcode.yaml"
  if [ -f "$project_config" ]; then
    echo ""
    echo -e "${GREEN}Found project config: .crabcode.yaml${NC}"
    read -p "Import settings from it? [Y/n]: " use_project
    if [ "$use_project" != "n" ] && [ "$use_project" != "N" ]; then
      mkdir -p "$PROJECTS_DIR"
      cp "$project_config" "$CONFIG_FILE"

      # Update paths to absolute if they use relative paths
      local main_repo_val=$(yq -r '.main_repo // ""' "$CONFIG_FILE" 2>/dev/null)
      if [ -z "$main_repo_val" ] || [ "$main_repo_val" = "null" ] || [[ "$main_repo_val" == "~/"* ]] || [[ "$main_repo_val" == "./"* ]]; then
        yq -i ".main_repo = \"$main_repo\"" "$CONFIG_FILE"
      fi

      local ws_base_val=$(yq -r '.workspace_base // ""' "$CONFIG_FILE" 2>/dev/null)
      if [ -z "$ws_base_val" ] || [ "$ws_base_val" = "null" ]; then
        yq -i ".workspace_base = \"$workspace_base\"" "$CONFIG_FILE"
      fi

      # Set session_name to alias
      yq -i ".session_name = \"$alias_input\"" "$CONFIG_FILE"

      # Set as default if first project
      _init_set_default_if_first "$alias_input"

      echo ""
      success "Project @$alias_input registered at $CONFIG_FILE"
      echo ""
      echo -e "${CYAN}Next steps:${NC}"
      echo "  1. Review config: crab @$alias_input config"
      echo "  2. Run 'crab @$alias_input config scan' to detect ports"
      echo "  3. Run 'crab @$alias_input ws 1' to create your first workspace"
      return
    fi
    echo ""
  fi

  # Template handling: explicit flag or auto-detection
  if [ -n "$template_name" ]; then
    case "$template_name" in
      "promptfoo-cloud"|"pf")
        template_name="promptfoo-cloud"
        ;;
      *)
        error "Unknown template: $template_name"
        echo "Run 'crab init --list-templates' to see available templates."
        return 1
        ;;
    esac
    echo ""
    echo -e "Applying ${GREEN}$template_name${NC} template..."
    apply_template "$template_name" "$main_repo" "$workspace_base" "$alias_input"
    _init_set_default_if_first "$alias_input"
    echo ""
    success "Project @$alias_input registered at $CONFIG_FILE (template: $template_name)"
    echo ""
    echo -e "${CYAN}Next steps:${NC}"
    echo "  1. Review config: crab @$alias_input config"
    echo "  2. Run 'crab @$alias_input ws 1' to create your first workspace"
    return
  fi

  # Auto-detect template from repo contents
  local detected
  detected=$(detect_template "$main_repo")
  if [ -n "$detected" ]; then
    echo ""
    echo -e "Detected ${GREEN}$detected${NC} project."
    read -p "Use template? [Y/n]: " use_tpl
    if [ "$use_tpl" != "n" ] && [ "$use_tpl" != "N" ]; then
      echo ""
      echo -e "Applying ${GREEN}$detected${NC} template..."
      apply_template "$detected" "$main_repo" "$workspace_base" "$alias_input"
      _init_set_default_if_first "$alias_input"
      echo ""
      success "Project @$alias_input registered at $CONFIG_FILE (template: $detected)"
      echo ""
      echo -e "${CYAN}Next steps:${NC}"
      echo "  1. Review config: crab @$alias_input config"
      echo "  2. Run 'crab @$alias_input ws 1' to create your first workspace"
      return
    fi
    echo ""
  fi

  # No template - generate minimal config
  mkdir -p "$PROJECTS_DIR"

  cat > "$CONFIG_FILE" << EOF
# Crabcode Configuration
# Generated by 'crabcode init'
# Project: @$alias_input
# Run 'crab config scan' to auto-detect env files and ports

session_name: $alias_input
workspace_base: $workspace_base
main_repo: $main_repo

workspaces:
  prefix: ws
  branch_pattern: workspace-{N}

layout:
  panes:
    - name: terminal
      command: ""
    - name: server
      command: ""
    - name: main
      command: ""

# Run 'crab config scan' to detect .env files and generate env_sync config
# Then edit this file to customize:
#
# env_sync:
#   files:
#     - path: .env
#       ports: [PORT, API_PORT]
#
# layout:
#   panes:
#     - name: terminal
#       command: ""
#     - name: server
#       command: "npm run dev"
#     - name: main
#       command: ""  # Set via 'agent:' field, or override directly
#
# agent: codex  # or "claude" - defaults to codex if omitted
#
# shared_volume:
#   enabled: true
#   path: ~/.crabcode/shared
#   link_as: .local
#
# submodules:
#   - path: my-submodule
#     reset_to: origin/main
EOF

  _init_set_default_if_first "$alias_input"

  echo ""
  success "Project @$alias_input registered at $CONFIG_FILE"
  echo ""
  echo -e "${CYAN}Next steps:${NC}"
  echo ""
  echo "  1. Run 'crab @$alias_input config scan' to detect .env files and ports"
  echo "  2. Edit $CONFIG_FILE to set your layout commands:"
  echo "     - server pane: your dev server command (e.g., pnpm dev)"
  echo "     - main pane: your coding agent (e.g., codex, claude)"
  echo "  3. Run 'crab @$alias_input ws 1' to create your first workspace"
  echo ""
}

# Helper: set as default project if it's the first one registered
_init_set_default_if_first() {
  local alias="$1"
  local count=0
  for f in "$PROJECTS_DIR"/*.yaml; do
    [ -f "$f" ] || continue
    count=$((count + 1))
  done

  if [ "$count" -le 1 ]; then
    mkdir -p "$CONFIG_DIR"
    # Use yq to set default_project if global config exists, else create it
    if [ -f "$GLOBAL_CONFIG" ] && yq -r '.default_project' "$GLOBAL_CONFIG" &>/dev/null; then
      yq -i ".default_project = \"$alias\"" "$GLOBAL_CONFIG"
    else
      cat > "$GLOBAL_CONFIG" << EOF
# Crabcode Global Configuration
default_project: $alias
EOF
    fi
    echo -e "  ${GREEN}Set as default project${NC} (use 'crab default' to change)"
  fi
}

# =============================================================================
# Config Scan - Discover env files and provide template
# =============================================================================

config_scan() {
  echo -e "${CYAN}Scanning for .env files...${NC}"
  echo ""

  if ! config_exists; then
    error "No config file found. Run 'crab init' first."
    return 1
  fi

  load_config

  local repo_path=$(expand_path "$MAIN_REPO")
  if [ ! -d "$repo_path" ]; then
    error "Main repo not found: $repo_path"
    return 1
  fi

  # Find all .env and .env.example files (bash 3.x compatible)
  local env_files=""
  local file_count=0

  while IFS= read -r file; do
    [ -z "$file" ] && continue
    env_files="$env_files$file"$'\n'
    file_count=$((file_count + 1))
  done < <(find "$repo_path" -maxdepth 3 -type f \( -name ".env" -o -name ".env.example" \) ! -path "*/node_modules/*" ! -path "*/.git/*" 2>/dev/null | sort)

  if [ $file_count -eq 0 ]; then
    echo "  No .env files found in $repo_path"
    echo ""
    echo "  You can manually configure env_sync in $CONFIG_FILE"
    return 0
  fi

  echo -e "Found ${GREEN}${file_count}${NC} env file(s):"
  echo ""

  # Group files by directory, prioritize .env over .env.example
  local dirs_seen=""
  local files_to_show=""

  echo "$env_files" | while IFS= read -r file; do
    [ -z "$file" ] && continue
    local rel_path="${file#$repo_path/}"
    local dir_path=$(dirname "$rel_path")
    local file_name=$(basename "$file")

    # Determine the target .env path (without .example)
    local target_path="$rel_path"
    local copy_from=""

    case "$file_name" in
      .env.example)
        target_path="${rel_path%.example}"
        # Check if .env exists (prioritize it)
        if [ -f "$repo_path/$target_path" ]; then
          # .env exists, skip .env.example
          continue
        fi
        copy_from="$rel_path"
        ;;
      .env)
        # Check if .env.example also exists
        if [ -f "$file.example" ]; then
          copy_from="${rel_path}.example"
        fi
        ;;
    esac

    echo -e "  ${BOLD}$target_path${NC}"
    if [ -n "$copy_from" ]; then
      echo -e "    └─ copy from: ${CYAN}$copy_from${NC}"
    fi
  done

  echo ""
  echo -e "${CYAN}Add to $CONFIG_FILE:${NC}"
  echo ""
  echo "env_sync:"
  echo "  files:"

  # Generate template
  local shown_paths=""
  echo "$env_files" | while IFS= read -r file; do
    [ -z "$file" ] && continue
    local rel_path="${file#$repo_path/}"
    local file_name=$(basename "$file")

    local target_path="$rel_path"
    local copy_from=""

    case "$file_name" in
      .env.example)
        target_path="${rel_path%.example}"
        if [ -f "$repo_path/$target_path" ]; then
          continue
        fi
        copy_from="$rel_path"
        ;;
      .env)
        if [ -f "$file.example" ]; then
          copy_from="${rel_path}.example"
        fi
        ;;
    esac

    # Skip if already shown
    case "$shown_paths" in
      *"$target_path"*) continue ;;
    esac
    shown_paths="$shown_paths $target_path"

    echo "    - path: $target_path"
    if [ -n "$copy_from" ]; then
      echo "      copy_from: $copy_from"
    fi
    echo "      ports: []           # e.g., [API_PORT, ADMIN_PORT]"
    echo "      # refs:             # optional: URL vars that reference ports"
    echo "      #   VITE_API_URL: API_PORT"
  done

  echo ""
  echo -e "${CYAN}How it works:${NC}"
  echo "  - ports: vars crab manages (increments per workspace, checks availability)"
  echo "  - refs: URL vars that reference a managed port (auto-rewritten)"
  echo "  - other vars: just copied from copy_from template"
  echo ""
  echo -e "${CYAN}Example for promptfoo-cloud:${NC}"
  echo ""
  echo "env_sync:"
  echo "  files:"
  echo "    - path: server/.env"
  echo "      copy_from: server/.env.example"
  echo "      ports: [API_PORT, ADMIN_PORT]"
  echo "    - path: app/.env"
  echo "      copy_from: app/.env.example"
  echo "      ports: [VITE_PORT]"
  echo "      refs:"
  echo "        VITE_API_URL: API_PORT"
}

show_config() {
  if ! config_exists; then
    echo -e "${YELLOW}No config file found.${NC}"
    echo "Run 'crabcode init' to create one."
    return
  fi

  echo -e "${CYAN}Crabcode Configuration${NC}"
  if [ -n "${PROJECT_ALIAS:-}" ]; then
    echo "Project: @$PROJECT_ALIAS"
  fi
  echo "File: $CONFIG_FILE"
  echo ""
  cat "$CONFIG_FILE"
}

show_doctor() {
  echo -e "${CYAN}    \\___/${NC}"
  echo -e "${CYAN}   ( •_•)  Crabcode Doctor${NC}"
  echo -e "${CYAN}  /)🦀(\\${NC}"
  echo ""

  if [ -n "${PROJECT_ALIAS:-}" ]; then
    echo -e "  project: ${GREEN}@$PROJECT_ALIAS${NC}"
  fi

  local issues=0

  # Check yq
  echo -n "  yq: "
  if command_exists yq; then
    echo -e "${GREEN}installed${NC}"
  else
    echo -e "${RED}not installed${NC}"
    issues=$((issues + 1))
  fi

  # Check tmux
  echo -n "  tmux: "
  if command_exists tmux; then
    echo -e "${GREEN}installed${NC}"
  else
    echo -e "${RED}not installed${NC}"
    issues=$((issues + 1))
  fi

  # Check git
  echo -n "  git: "
  if command_exists git; then
    echo -e "${GREEN}installed${NC}"
  else
    echo -e "${RED}not installed${NC}"
    issues=$((issues + 1))
  fi

  # Check config
  echo -n "  config: "
  if config_exists; then
    echo -e "${GREEN}exists${NC}"

    # Validate config
    load_config

    echo -n "  session_name: "
    if [ -n "$SESSION_NAME" ]; then
      echo -e "${GREEN}$SESSION_NAME${NC}"
    else
      echo -e "${RED}not set${NC}"
      issues=$((issues + 1))
    fi

    echo -n "  workspace_base: "
    if [ -n "$WORKSPACE_BASE" ]; then
      if [ -d "$WORKSPACE_BASE" ] || [ -d "$(dirname "$WORKSPACE_BASE")" ]; then
        echo -e "${GREEN}$WORKSPACE_BASE${NC}"
      else
        echo -e "${YELLOW}$WORKSPACE_BASE (will be created)${NC}"
      fi
    else
      echo -e "${RED}not set${NC}"
      issues=$((issues + 1))
    fi

    echo -n "  main_repo: "
    if [ -n "$MAIN_REPO" ]; then
      if [ -d "$MAIN_REPO" ]; then
        echo -e "${GREEN}$MAIN_REPO${NC}"
      else
        echo -e "${RED}$MAIN_REPO (not found)${NC}"
        issues=$((issues + 1))
      fi
    else
      echo -e "${RED}not set${NC}"
      issues=$((issues + 1))
    fi

  else
    echo -e "${YELLOW}not found${NC}"
    echo ""
    echo "Run 'crabcode init' to create a config file."
    return
  fi

  # Check shared volume
  echo -n "  shared_volume: "
  if [ "$SHARED_VOLUME_ENABLED" = "true" ]; then
    if [ -d "$SHARED_VOLUME_PATH" ]; then
      echo -e "${GREEN}$SHARED_VOLUME_PATH${NC}"
    else
      echo -e "${YELLOW}$SHARED_VOLUME_PATH (will be created)${NC}"
    fi
  else
    echo -e "${YELLOW}disabled${NC}"
  fi

  echo ""
  if [ $issues -eq 0 ]; then
    echo -e "${GREEN}   \\o/${NC}"
    echo -e "${GREEN}  ( ˆᴗˆ )  All checks passed!${NC}"
    echo -e "${GREEN}  /)🦀(\\${NC}"
  else
    echo -e "${RED}   \\__/${NC}"
    echo -e "${RED}  ( •̩̩̩-•̩̩̩ )  $issues issue(s) found${NC}"
    echo -e "${RED}  /)🦀(\\${NC}"
  fi
}

# =============================================================================
# Crab Mood - Fun ASCII crab that reacts to your work
# =============================================================================

MOOD_FILE="$CONFIG_DIR/mood.json"

# ASCII art for different moods
crab_art_happy() {
  cat << 'EOF'
    \o/
   ( ˆ ᴗ ˆ )
  /)🦀(\

EOF
}

crab_art_excited() {
  cat << 'EOF'
   \(★ω★)/
  /)  🎉  (\
     🦀

EOF
}

crab_art_working() {
  cat << 'EOF'
      💻
   ( •_•)
  /)🦀(\

EOF
}

crab_art_nervous() {
  cat << 'EOF'
      😰
   (°△°;)
  /)🦀(\

EOF
}

crab_art_sleepy() {
  cat << 'EOF'
     💤
   ( -_-)
  /)🦀(\

EOF
}

crab_art_chill() {
  cat << 'EOF'
     🌴
   ( ˘ω˘)
  /)🦀(\  🍹

EOF
}

crab_art_celebrating() {
  cat << 'EOF'
   🎊\(^o^)/🎊
  /)  🦀  (\
   🎉 🎉 🎉

EOF
}

# Record a mood event
mood_record_event() {
  local event_type="$1"
  local details="${2:-}"
  local timestamp=$(date +%s)

  mkdir -p "$CONFIG_DIR"

  # Initialize mood file if doesn't exist
  if [ ! -f "$MOOD_FILE" ]; then
    echo '{"events":[],"stats":{"commits":0,"cleanups":0,"restarts":0,"wip_saves":0,"prs_merged":0}}' > "$MOOD_FILE"
  fi

  # Add event (keep last 50 events)
  if command_exists jq; then
    local tmp_file="/tmp/mood_$$"
    jq --arg type "$event_type" --arg details "$details" --arg ts "$timestamp" \
      '.events = ([{type: $type, details: $details, timestamp: ($ts | tonumber)}] + .events)[:50] |
       if $type == "commit" then .stats.commits += 1
       elif $type == "cleanup" then .stats.cleanups += 1
       elif $type == "restart" then .stats.restarts += 1
       elif $type == "wip_save" then .stats.wip_saves += 1
       elif $type == "pr_merged" then .stats.prs_merged += 1
       else . end' "$MOOD_FILE" > "$tmp_file" && mv "$tmp_file" "$MOOD_FILE"
  fi
}

# Calculate current mood based on events and context
mood_calculate() {
  local now=$(date +%s)
  local hour=$(date +%H)
  local day_of_week=$(date +%u)  # 1=Monday, 7=Sunday

  # Default mood
  local mood="working"
  local message="Keep coding!"

  # Check if we have jq for JSON parsing
  if ! command_exists jq; then
    echo "working:No jq installed - but keep up the great work!"
    return
  fi

  # Check if mood file exists
  if [ ! -f "$MOOD_FILE" ]; then
    echo "working:Fresh start - let's build something awesome!"
    return
  fi

  # Get recent events (last hour)
  local recent_commits=$(jq "[.events[] | select(.type == \"commit\" and .timestamp > ($now - 3600))] | length" "$MOOD_FILE" 2>/dev/null || echo 0)
  local recent_prs=$(jq "[.events[] | select(.type == \"pr_merged\" and .timestamp > ($now - 86400))] | length" "$MOOD_FILE" 2>/dev/null || echo 0)
  local total_commits=$(jq ".stats.commits // 0" "$MOOD_FILE" 2>/dev/null || echo 0)
  local last_event_time=$(jq ".events[0].timestamp // 0" "$MOOD_FILE" 2>/dev/null || echo 0)
  local last_event_type=$(jq -r ".events[0].type // \"none\"" "$MOOD_FILE" 2>/dev/null || echo "none")

  # Calculate time since last event
  local time_since_last=$((now - last_event_time))

  # Check workspace state
  local has_dirty_workspace=false
  if [ -n "${WORKSPACE_BASE:-}" ] && [ -d "$WORKSPACE_BASE" ]; then
    for ws_dir in "$WORKSPACE_BASE"/"${WORKSPACE_PREFIX:-workspace}"-*; do
      if [ -d "$ws_dir/.git" ]; then
        if [ -n "$(git -C "$ws_dir" status --porcelain 2>/dev/null)" ]; then
          has_dirty_workspace=true
          break
        fi
      fi
    done
  fi

  # Determine mood
  if [ "$recent_prs" -ge 2 ]; then
    mood="celebrating"
    message="$recent_prs PRs merged today! You're on fire! 🔥"
  elif [ "$recent_prs" -ge 1 ]; then
    mood="excited"
    message="PR merged! Time to celebrate! 🎉"
  elif [ "$recent_commits" -ge 5 ]; then
    mood="excited"
    message="$recent_commits commits in the last hour! Crushing it!"
  elif [ "$last_event_type" = "cleanup" ] && [ "$time_since_last" -lt 300 ]; then
    mood="happy"
    message="Fresh workspace, fresh start! ✨"
  elif [ "$has_dirty_workspace" = true ]; then
    mood="nervous"
    message="You have uncommitted changes... don't forget to commit!"
  elif [ "$time_since_last" -gt 7200 ]; then  # More than 2 hours since last event
    mood="sleepy"
    message="Been quiet... taking a break? 😴"
  elif [ "$day_of_week" -ge 6 ] && [ "$hour" -ge 18 ]; then  # Weekend evening
    mood="chill"
    message="Weekend vibes... maybe time to relax? 🌴"
  elif [ "$hour" -ge 22 ] || [ "$hour" -lt 6 ]; then  # Late night / early morning
    mood="sleepy"
    message="It's late... get some rest! 🌙"
  elif [ "$recent_commits" -ge 1 ]; then
    mood="happy"
    message="$recent_commits commits recently - nice progress!"
  else
    mood="working"
    message="In the zone... keep it up! 💪"
  fi

  echo "$mood:$message"
}

# Show current crab mood
show_mood() {
  load_config 2>/dev/null || true

  local result=$(mood_calculate)
  local mood=$(echo "$result" | cut -d: -f1)
  local message=$(echo "$result" | cut -d: -f2-)

  echo ""
  echo -e "${CYAN}╭──────────────────────────────────────╮${NC}"

  # Show the ASCII art
  case "$mood" in
    "happy")
      echo -e "${GREEN}"
      crab_art_happy
      echo -e "${NC}"
      ;;
    "excited")
      echo -e "${YELLOW}"
      crab_art_excited
      echo -e "${NC}"
      ;;
    "celebrating")
      echo -e "${MAGENTA}"
      crab_art_celebrating
      echo -e "${NC}"
      ;;
    "nervous")
      echo -e "${YELLOW}"
      crab_art_nervous
      echo -e "${NC}"
      ;;
    "sleepy")
      echo -e "${GRAY}"
      crab_art_sleepy
      echo -e "${NC}"
      ;;
    "chill")
      echo -e "${CYAN}"
      crab_art_chill
      echo -e "${NC}"
      ;;
    *)
      echo -e "${BLUE}"
      crab_art_working
      echo -e "${NC}"
      ;;
  esac

  echo -e "${CYAN}╰──────────────────────────────────────╯${NC}"
  echo ""
  echo -e "  ${BOLD}Mood:${NC} $mood"
  echo -e "  ${message}"
  echo ""

  # Show quick stats if available
  if command_exists jq && [ -f "$MOOD_FILE" ]; then
    local total_commits=$(jq ".stats.commits // 0" "$MOOD_FILE" 2>/dev/null || echo 0)
    local total_cleanups=$(jq ".stats.cleanups // 0" "$MOOD_FILE" 2>/dev/null || echo 0)
    local total_wip=$(jq ".stats.wip_saves // 0" "$MOOD_FILE" 2>/dev/null || echo 0)

    if [ "$total_commits" -gt 0 ] || [ "$total_cleanups" -gt 0 ]; then
      echo -e "  ${GRAY}Stats: $total_commits commits, $total_cleanups cleanups, $total_wip WIPs saved${NC}"
      echo ""
    fi
  fi
}

# Check for git events in a workspace and record them
mood_check_git_activity() {
  local workspace_dir="$1"

  if [ ! -d "$workspace_dir/.git" ]; then
    return
  fi

  # Check for new commits in last 5 minutes
  local recent_commits=$(git -C "$workspace_dir" log --oneline --since="5 minutes ago" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$recent_commits" -gt 0 ]; then
    mood_record_event "commit" "$recent_commits new commits"
  fi
}

# =============================================================================
# Mobile Companion - Check status and get notifications from your phone
# =============================================================================

MOBILE_CONFIG_FILE="$CONFIG_DIR/mobile.json"

# Initialize mobile config
mobile_init() {
  if [ ! -f "$MOBILE_CONFIG_FILE" ]; then
    # Generate a random topic name for ntfy.sh
    local random_topic="crab-$(openssl rand -hex 4 2>/dev/null || echo $RANDOM$RANDOM)"
    echo "{\"ntfy_topic\":\"$random_topic\",\"enabled\":true}" > "$MOBILE_CONFIG_FILE"
  fi
}

# Get ntfy topic
mobile_get_topic() {
  mobile_init
  if command_exists jq && [ -f "$MOBILE_CONFIG_FILE" ]; then
    jq -r ".ntfy_topic // \"\"" "$MOBILE_CONFIG_FILE" 2>/dev/null
  else
    echo ""
  fi
}

# Send push notification via ntfy.sh
crab_notify() {
  local title="${1:-Crab Update}"
  local message="${2:-}"
  local priority="${3:-default}"  # min, low, default, high, urgent

  mobile_init
  local topic=$(mobile_get_topic)

  if [ -z "$topic" ]; then
    error "Mobile notifications not configured. Run 'crab mobile setup'"
    return 1
  fi

  # Map priority to ntfy format
  local ntfy_priority="3"
  case "$priority" in
    "urgent") ntfy_priority="5" ;;
    "high") ntfy_priority="4" ;;
    "default") ntfy_priority="3" ;;
    "low") ntfy_priority="2" ;;
    "min") ntfy_priority="1" ;;
  esac

  # Send notification
  if curl -s \
    -H "Title: 🦀 $title" \
    -H "Priority: $ntfy_priority" \
    -H "Tags: crab" \
    -d "$message" \
    "https://ntfy.sh/$topic" > /dev/null 2>&1; then
    success "Notification sent!"
  else
    error "Failed to send notification"
    return 1
  fi
}

# Generate status JSON for mobile API
generate_status_json() {
  load_config 2>/dev/null || true

  local workspaces_json="[]"

  if [ -n "${WORKSPACE_BASE:-}" ] && [ -d "$WORKSPACE_BASE" ]; then
    local ws_array="["
    local first=true

    for ws_dir in "$WORKSPACE_BASE"/"${WORKSPACE_PREFIX:-workspace}"-*; do
      [ -d "$ws_dir" ] || continue

      local ws_name=$(basename "$ws_dir")
      local num=$(echo "$ws_name" | sed 's/.*-//')
      local branch=""
      local status="clean"
      local changes=0

      if [ -d "$ws_dir/.git" ]; then
        branch=$(git -C "$ws_dir" branch --show-current 2>/dev/null || echo "unknown")
        local git_status=$(git -C "$ws_dir" status --porcelain 2>/dev/null || echo "")
        if [ -n "$git_status" ]; then
          status="dirty"
          changes=$(echo "$git_status" | wc -l | tr -d ' ')
        fi
      fi

      [ "$first" = true ] || ws_array+=","
      first=false
      ws_array+="{\"number\":$num,\"branch\":\"$branch\",\"status\":\"$status\",\"changes\":$changes}"
    done

    ws_array+="]"
    workspaces_json="$ws_array"
  fi

  # Get mood
  local mood_result=$(mood_calculate 2>/dev/null || echo "working:Keep coding!")
  local mood=$(echo "$mood_result" | cut -d: -f1)
  local mood_message=$(echo "$mood_result" | cut -d: -f2-)

  # Get stats
  local stats_json="{}"
  if command_exists jq && [ -f "$MOOD_FILE" ]; then
    stats_json=$(jq ".stats // {}" "$MOOD_FILE" 2>/dev/null || echo "{}")
  fi

  cat << EOF
{
  "timestamp": "$(date -Iseconds)",
  "hostname": "$(hostname)",
  "mood": "$mood",
  "mood_message": "$mood_message",
  "workspaces": $workspaces_json,
  "stats": $stats_json
}
EOF
}

# Show status in terminal or JSON format
show_status() {
  local format="${1:-terminal}"

  if [ "$format" = "json" ] || [ "$format" = "--json" ]; then
    generate_status_json
    return
  fi

  load_config 2>/dev/null || true

  echo ""
  echo -e "${CYAN}╭────────────────────────────────────────────────────╮${NC}"
  if [ -n "${PROJECT_ALIAS:-}" ]; then
    echo -e "${CYAN}│${NC}         ${BOLD}🦀 CRAB STATUS (@$PROJECT_ALIAS)${NC}$(printf '%*s' $((18 - ${#PROJECT_ALIAS})) '')${CYAN}│${NC}"
  else
    echo -e "${CYAN}│${NC}              ${BOLD}🦀 CRAB STATUS${NC}                       ${CYAN}│${NC}"
  fi
  echo -e "${CYAN}╰────────────────────────────────────────────────────╯${NC}"
  echo ""

  # Show mood summary
  local mood_result=$(mood_calculate 2>/dev/null || echo "working:Keep coding!")
  local mood=$(echo "$mood_result" | cut -d: -f1)
  echo -e "  ${BOLD}Mood:${NC} $mood"
  echo ""

  # Show workspaces
  if [ -n "${WORKSPACE_BASE:-}" ] && [ -d "$WORKSPACE_BASE" ]; then
    echo -e "  ${BOLD}Workspaces:${NC}"
    for ws_dir in "$WORKSPACE_BASE"/"${WORKSPACE_PREFIX:-workspace}"-*; do
      [ -d "$ws_dir" ] || continue

      local ws_name=$(basename "$ws_dir")
      local num=$(echo "$ws_name" | sed 's/.*-//')
      local branch=""
      local status_icon="✓"
      local status_color="${GREEN}"

      if [ -d "$ws_dir/.git" ]; then
        branch=$(git -C "$ws_dir" branch --show-current 2>/dev/null || echo "unknown")
        local git_status=$(git -C "$ws_dir" status --porcelain 2>/dev/null || echo "")
        if [ -n "$git_status" ]; then
          local changes=$(echo "$git_status" | wc -l | tr -d ' ')
          status_icon="●"
          status_color="${YELLOW}"
          echo -e "    ${status_color}${status_icon}${NC} ws-$num: $branch (${YELLOW}$changes changes${NC})"
        else
          echo -e "    ${status_color}${status_icon}${NC} ws-$num: $branch"
        fi
      fi
    done
  fi
  echo ""
}

# Start simple HTTP server for mobile dashboard
start_mobile_server() {
  local port="${1:-8888}"

  echo -e "${CYAN}Starting mobile dashboard server...${NC}"
  echo ""
  echo -e "  ${BOLD}Local:${NC}   http://localhost:$port"
  echo -e "  ${BOLD}Network:${NC} http://$(ipconfig getifaddr en0 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}' || echo 'localhost'):$port"
  echo ""
  echo -e "${GRAY}Press Ctrl+C to stop${NC}"
  echo ""

  # Check if python is available
  if command_exists python3; then
    # Create a simple HTML dashboard
    local tmp_dir="/tmp/crab-mobile-$$"
    mkdir -p "$tmp_dir"

    # Generate dashboard HTML
    cat > "$tmp_dir/index.html" << 'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>🦀 Crab Dashboard</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
      color: #eee;
      min-height: 100vh;
      padding: 20px;
    }
    .container { max-width: 600px; margin: 0 auto; }
    .header {
      text-align: center;
      padding: 30px 0;
    }
    .header h1 { font-size: 2.5em; }
    .mood-card {
      background: rgba(255,255,255,0.1);
      border-radius: 20px;
      padding: 30px;
      text-align: center;
      margin-bottom: 20px;
    }
    .mood-emoji { font-size: 4em; }
    .mood-text { font-size: 1.2em; color: #aaa; margin-top: 10px; }
    .workspace-card {
      background: rgba(255,255,255,0.05);
      border-radius: 15px;
      padding: 15px 20px;
      margin-bottom: 10px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .ws-name { font-weight: bold; }
    .ws-branch { color: #888; font-size: 0.9em; }
    .ws-status { font-size: 1.5em; }
    .ws-status.clean { color: #4ade80; }
    .ws-status.dirty { color: #fbbf24; }
    .stats {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 15px;
      margin-top: 20px;
    }
    .stat-card {
      background: rgba(255,255,255,0.05);
      border-radius: 15px;
      padding: 20px;
      text-align: center;
    }
    .stat-number { font-size: 2em; font-weight: bold; color: #60a5fa; }
    .stat-label { color: #888; font-size: 0.85em; margin-top: 5px; }
    .actions {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 10px;
      margin-top: 20px;
    }
    .action-btn {
      background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
      border: none;
      border-radius: 12px;
      padding: 15px;
      color: white;
      font-size: 1em;
      font-weight: bold;
      cursor: pointer;
    }
    .action-btn:active { transform: scale(0.98); }
    .action-btn.danger { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); }
    .refresh-note { text-align: center; color: #666; margin-top: 20px; font-size: 0.85em; }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <h1>🦀 Crab</h1>
    </div>

    <div class="mood-card">
      <div class="mood-emoji" id="mood-emoji">💻</div>
      <div class="mood-text" id="mood-text">Loading...</div>
    </div>

    <h3 style="margin-bottom: 15px;">Workspaces</h3>
    <div id="workspaces"></div>

    <div class="stats">
      <div class="stat-card">
        <div class="stat-number" id="stat-commits">-</div>
        <div class="stat-label">Commits</div>
      </div>
      <div class="stat-card">
        <div class="stat-number" id="stat-cleanups">-</div>
        <div class="stat-label">Cleanups</div>
      </div>
      <div class="stat-card">
        <div class="stat-number" id="stat-wips">-</div>
        <div class="stat-label">WIPs</div>
      </div>
    </div>

    <div class="refresh-note">Pull down to refresh</div>
  </div>

  <script>
    const moodEmojis = {
      happy: '😊', excited: '🎉', celebrating: '🎊',
      working: '💻', nervous: '😰', sleepy: '😴', chill: '🌴'
    };

    async function loadStatus() {
      try {
        const res = await fetch('/api/status');
        const data = await res.json();

        document.getElementById('mood-emoji').textContent = moodEmojis[data.mood] || '🦀';
        document.getElementById('mood-text').textContent = data.mood_message;

        const wsContainer = document.getElementById('workspaces');
        wsContainer.innerHTML = data.workspaces.map(ws => `
          <div class="workspace-card">
            <div>
              <div class="ws-name">ws-${ws.number}</div>
              <div class="ws-branch">${ws.branch}</div>
            </div>
            <div class="ws-status ${ws.status}">${ws.status === 'clean' ? '✓' : '●'}</div>
          </div>
        `).join('');

        if (data.stats) {
          document.getElementById('stat-commits').textContent = data.stats.commits || 0;
          document.getElementById('stat-cleanups').textContent = data.stats.cleanups || 0;
          document.getElementById('stat-wips').textContent = data.stats.wip_saves || 0;
        }
      } catch (e) {
        console.error('Failed to load status:', e);
      }
    }

    loadStatus();
    setInterval(loadStatus, 5000);
  </script>
</body>
</html>
HTMLEOF

    # Create a simple Python server that serves both static files and API
    cat > "$tmp_dir/server.py" << PYEOF
import http.server
import json
import subprocess
import os

class CrabHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/api/status':
            self.send_response(200)
            self.send_header('Content-type', 'application/json')
            self.send_header('Access-Control-Allow-Origin', '*')
            self.end_headers()
            try:
                result = subprocess.run(
                    ['$0', 'status', '--json'],
                    capture_output=True, text=True, timeout=10
                )
                self.wfile.write(result.stdout.encode())
            except Exception as e:
                self.wfile.write(json.dumps({"error": str(e)}).encode())
        else:
            super().do_GET()

os.chdir('$tmp_dir')
server = http.server.HTTPServer(('0.0.0.0', $port), CrabHandler)
server.serve_forever()
PYEOF

    # Run the server
    python3 "$tmp_dir/server.py"

    # Cleanup on exit
    rm -rf "$tmp_dir"
  else
    error "Python 3 is required for the mobile dashboard"
    echo "Install with: brew install python3"
    exit 1
  fi
}

# Mobile setup wizard
mobile_setup() {
  echo -e "${CYAN}╭────────────────────────────────────────────────────╮${NC}"
  echo -e "${CYAN}│${NC}         ${BOLD}🦀 Mobile Companion Setup${NC}                  ${CYAN}│${NC}"
  echo -e "${CYAN}╰────────────────────────────────────────────────────╯${NC}"
  echo ""

  mobile_init
  local topic=$(mobile_get_topic)

  echo -e "${BOLD}Push Notifications (via ntfy.sh)${NC}"
  echo ""
  echo "  1. Install the ntfy app on your phone:"
  echo "     - iOS: https://apps.apple.com/app/ntfy/id1625396347"
  echo "     - Android: https://play.google.com/store/apps/details?id=io.heckel.ntfy"
  echo ""
  echo "  2. Subscribe to your personal topic:"
  echo -e "     ${GREEN}$topic${NC}"
  echo ""
  echo "  3. Test it:"
  echo -e "     ${CYAN}crab notify \"Hello\" \"Testing from crab!\"${NC}"
  echo ""
  echo -e "${BOLD}Mobile Dashboard${NC}"
  echo ""
  echo "  Start the dashboard server:"
  echo -e "  ${CYAN}crab mobile serve${NC}"
  echo ""
  echo "  Then open the URL on your phone (same WiFi network)"
  echo ""

  # Show QR code for ntfy topic if qrencode is available
  if command_exists qrencode; then
    echo -e "${BOLD}Scan to subscribe:${NC}"
    qrencode -t ANSI "ntfy://$topic" 2>/dev/null || true
    echo ""
  fi
}

# Handle mobile commands
handle_mobile_command() {
  case "${1:-}" in
    "setup"|"init")
      mobile_setup
      ;;
    "serve"|"server"|"dashboard")
      start_mobile_server "${2:-8888}"
      ;;
    "notify"|"push")
      shift
      crab_notify "$@"
      ;;
    "topic")
      mobile_init
      echo "Your ntfy topic: $(mobile_get_topic)"
      echo "Subscribe URL: https://ntfy.sh/$(mobile_get_topic)"
      ;;
    "status")
      show_status "${2:-terminal}"
      ;;
    *)
      echo -e "${CYAN}Mobile Companion Commands:${NC}"
      echo ""
      echo "  crab mobile setup       Setup push notifications"
      echo "  crab mobile serve       Start mobile dashboard (default: port 8888)"
      echo "  crab mobile serve 9999  Start on custom port"
      echo "  crab mobile notify \"Title\" \"Message\""
      echo "  crab mobile topic       Show your ntfy topic"
      echo "  crab mobile status      Show status"
      echo "  crab mobile status --json  JSON output for API"
      echo ""
      ;;
  esac
}

# =============================================================================
# P2P Messaging - Self-hosted relay for direct messaging with coworkers
# =============================================================================

MSG_CONFIG_FILE="$CONFIG_DIR/msg.json"
MSG_RELAY_DIR="$CONFIG_DIR/msg-relay"
MSG_LOG_DIR="$CONFIG_DIR/messages"
MSG_PID_FILE="/tmp/crab-msg-relay.pid"
MSG_TUNNEL_PID_FILE="/tmp/crab-msg-tunnel.pid"
MSG_PORT=9119

# Read a field from msg config
msg_get() {
  local field="$1"
  [ -f "$MSG_CONFIG_FILE" ] && jq -r ".$field // \"\"" "$MSG_CONFIG_FILE" 2>/dev/null || echo ""
}

# Write a field to msg config
msg_set() {
  local field="$1"
  local value="$2"
  if [ ! -f "$MSG_CONFIG_FILE" ]; then
    echo '{}' > "$MSG_CONFIG_FILE"
  fi
  local tmp="${MSG_CONFIG_FILE}.tmp"
  jq --arg v "$value" ".$field = \$v" "$MSG_CONFIG_FILE" > "$tmp" && mv "$tmp" "$MSG_CONFIG_FILE"
}

# Speak a received message via macOS say (runs in background)
msg_say() {
  local from="$1"
  local text="$2"
  local say_enabled=$(msg_get "say")
  # Default to enabled if not set
  if [ "$say_enabled" != "false" ] && command_exists say; then
    say "$from said $text" &
  fi
}

# Append to local message log
msg_log() {
  local direction="$1"  # > or <
  local contact="$2"
  local message="$3"
  mkdir -p "$MSG_LOG_DIR"
  echo "[$(date '+%Y-%m-%d %H:%M')] $direction @$contact: $message" >> "$MSG_LOG_DIR/log"
}

# Start the relay server (Python3 HTTP)
msg_relay_start() {
  local expose="${1:-}"

  if ! command_exists python3; then
    error "Python3 is required for the relay server"
    return 1
  fi

  # Check if already running
  if [ -f "$MSG_PID_FILE" ] && kill -0 "$(cat "$MSG_PID_FILE")" 2>/dev/null; then
    echo -e "${YELLOW}Relay already running (PID $(cat "$MSG_PID_FILE"))${NC}"
    msg_relay_info
    return 0
  fi

  # Get or set host name
  local name=$(msg_get "name")
  if [ -z "$name" ]; then
    local default_name=$(git config user.name 2>/dev/null || whoami)
    echo -ne "Your display name [${default_name}]: "
    read -r input_name
    name="${input_name:-$default_name}"
    msg_set "name" "$name"
  fi
  msg_set "role" "host"

  # Create relay data dir
  mkdir -p "$MSG_RELAY_DIR"

  # Write the Python relay server
  local server_script="/tmp/crab-msg-relay.py"
  cat > "$server_script" << 'PYEOF'
import os, sys, json, time, urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler

DATA_DIR = os.path.expanduser("~/.crabcode/msg-relay")
HOST_NAME = sys.argv[2] if len(sys.argv) > 2 else "unknown"
os.makedirs(DATA_DIR, exist_ok=True)

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.path == "/send":
            length = int(self.headers.get("Content-Length", 0))
            body = json.loads(self.rfile.read(length))
            to_name = body.get("to", "")
            from_name = body.get("from", "")
            text = body.get("text", "")
            if not to_name or not from_name or not text:
                self._respond(400, {"error": "missing fields"})
                return
            inbox = os.path.join(DATA_DIR, f"{to_name}.jsonl")
            with open(inbox, "a") as f:
                f.write(json.dumps({"from": from_name, "text": text, "ts": time.time()}) + "\n")
            self._respond(200, {"ok": True})
        else:
            self._respond(404, {"error": "not found"})

    def do_GET(self):
        parsed = urllib.parse.urlparse(self.path)
        path = parsed.path
        params = dict(urllib.parse.parse_qsl(parsed.query))

        if path.startswith("/read/"):
            name = path[6:]
            since = float(params.get("since", 0))
            inbox = os.path.join(DATA_DIR, f"{name}.jsonl")
            msgs = []
            if os.path.exists(inbox):
                with open(inbox) as f:
                    for line in f:
                        line = line.strip()
                        if not line:
                            continue
                        msg = json.loads(line)
                        if msg.get("ts", 0) > since:
                            msgs.append(msg)
            self._respond(200, msgs)
        elif path == "/info":
            self._respond(200, {"host": HOST_NAME})
        elif path == "/health":
            self.send_response(200)
            self.send_header("Content-Type", "text/plain")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            self.wfile.write(b"ok")
        else:
            self._respond(404, {"error": "not found"})

    def _respond(self, code, data):
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(json.dumps(data).encode())

    def log_message(self, format, *args):
        pass  # suppress request logs

port = int(sys.argv[1]) if len(sys.argv) > 1 else 9119
HTTPServer(("0.0.0.0", port), Handler).serve_forever()
PYEOF

  # Start relay in background
  python3 "$server_script" "$MSG_PORT" "$name" &
  local relay_pid=$!
  echo "$relay_pid" > "$MSG_PID_FILE"

  sleep 0.5
  if ! kill -0 "$relay_pid" 2>/dev/null; then
    error "Relay failed to start"
    rm -f "$MSG_PID_FILE"
    return 1
  fi

  local local_ip=$(ipconfig getifaddr en0 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}' || echo 'localhost')
  local local_hostname=$(hostname 2>/dev/null || echo 'localhost')

  msg_set "relay" "http://localhost:$MSG_PORT"

  echo -e "${CYAN}━━ Relay Started ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  echo ""
  echo -e "  ${GREEN}Relay running on port $MSG_PORT (PID $relay_pid)${NC}"
  echo ""
  echo -e "  ${BOLD}Same network:${NC}"
  echo -e "    crab msg join http://${local_hostname}.local:$MSG_PORT"
  echo -e "    crab msg join http://${local_ip}:$MSG_PORT"
  echo ""

  # Expose via cloudflared if requested
  if [ "$expose" = "--expose" ]; then
    msg_relay_expose
  else
    echo -e "  ${BOLD}Remote access:${NC}"
    echo "    crab msg start --expose"
    echo "    (requires: brew install cloudflared)"
    echo ""
  fi
}

# Expose relay via cloudflared tunnel
msg_relay_expose() {
  if ! command_exists cloudflared; then
    error "cloudflared not installed. Run: brew install cloudflared"
    return 1
  fi

  # Kill existing tunnel
  if [ -f "$MSG_TUNNEL_PID_FILE" ] && kill -0 "$(cat "$MSG_TUNNEL_PID_FILE")" 2>/dev/null; then
    kill "$(cat "$MSG_TUNNEL_PID_FILE")" 2>/dev/null
  fi

  # Start cloudflared in background, capture URL
  local tunnel_log="/tmp/crab-msg-tunnel.log"
  cloudflared tunnel --url "http://localhost:$MSG_PORT" > "$tunnel_log" 2>&1 &
  local tunnel_pid=$!
  echo "$tunnel_pid" > "$MSG_TUNNEL_PID_FILE"

  echo -e "  ${BOLD}Starting tunnel...${NC}"

  # Wait for the tunnel URL to appear (up to 10 seconds)
  local tunnel_url=""
  for i in $(seq 1 20); do
    sleep 0.5
    tunnel_url=$(grep -o 'https://[a-z0-9-]*\.trycloudflare\.com' "$tunnel_log" 2>/dev/null | head -1)
    if [ -n "$tunnel_url" ]; then
      break
    fi
  done

  if [ -z "$tunnel_url" ]; then
    warn "Could not detect tunnel URL. Check $tunnel_log"
    return 1
  fi

  msg_set "relay" "$tunnel_url"

  echo ""
  echo -e "  ${GREEN}Tunnel active!${NC}"
  echo -e "  ${BOLD}Public URL:${NC} $tunnel_url"
  echo ""
  echo -e "  ${BOLD}Share with your coworker:${NC}"
  echo -e "    ${CYAN}crab msg join $tunnel_url${NC}"
  echo ""
}

# Show relay status and connection info
msg_relay_info() {
  local name=$(msg_get "name")
  local relay=$(msg_get "relay")
  local role=$(msg_get "role")

  echo -e "${CYAN}━━ Message Status ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  echo ""
  echo -e "  ${BOLD}Name:${NC}  $name"
  echo -e "  ${BOLD}Role:${NC}  $role"
  echo -e "  ${BOLD}Relay:${NC} $relay"

  if [ -f "$MSG_PID_FILE" ] && kill -0 "$(cat "$MSG_PID_FILE")" 2>/dev/null; then
    echo -e "  ${BOLD}Relay:${NC} ${GREEN}running${NC} (PID $(cat "$MSG_PID_FILE"))"
  fi
  if [ -f "$MSG_TUNNEL_PID_FILE" ] && kill -0 "$(cat "$MSG_TUNNEL_PID_FILE")" 2>/dev/null; then
    echo -e "  ${BOLD}Tunnel:${NC} ${GREEN}running${NC} (PID $(cat "$MSG_TUNNEL_PID_FILE"))"
  fi
  echo ""
}

# Stop the relay and tunnel
msg_relay_stop() {
  local stopped=false

  if [ -f "$MSG_PID_FILE" ]; then
    local pid=$(cat "$MSG_PID_FILE")
    if kill -0 "$pid" 2>/dev/null; then
      kill "$pid" 2>/dev/null
      stopped=true
    fi
    rm -f "$MSG_PID_FILE"
  fi

  if [ -f "$MSG_TUNNEL_PID_FILE" ]; then
    local pid=$(cat "$MSG_TUNNEL_PID_FILE")
    if kill -0 "$pid" 2>/dev/null; then
      kill "$pid" 2>/dev/null
      stopped=true
    fi
    rm -f "$MSG_TUNNEL_PID_FILE"
  fi

  if [ "$stopped" = true ]; then
    success "Relay stopped"
  else
    echo "No relay running"
  fi
}

# Join a relay (coworker runs this)
msg_join() {
  local relay_url="${1:-}"

  if [ -z "$relay_url" ]; then
    error "Usage: crab msg join <relay-url>"
    return 1
  fi

  # Strip trailing slash
  relay_url="${relay_url%/}"

  # Query relay for host info
  local info
  info=$(curl -s --connect-timeout 5 "${relay_url}/info" 2>/dev/null)
  if [ -z "$info" ]; then
    error "Cannot reach relay at $relay_url"
    return 1
  fi

  local host_name=$(echo "$info" | jq -r '.host // "unknown"')

  # Get coworker's name
  local current_name=$(msg_get "name")
  local default_name="${current_name:-$(git config user.name 2>/dev/null || whoami)}"
  echo -ne "Your name [${default_name}]: "
  read -r input_name
  local name="${input_name:-$default_name}"

  msg_set "name" "$name"
  msg_set "relay" "$relay_url"
  msg_set "role" "client"
  mkdir -p "$MSG_LOG_DIR"

  echo ""
  success "Connected to ${host_name}'s relay!"
  echo ""
  echo -e "  Send a message:"
  echo -e "    ${CYAN}crab msg @${host_name} \"hello\"${NC}"
  echo ""
  echo -e "  Check inbox:"
  echo -e "    ${CYAN}crab msg read${NC}"
  echo ""
}

# Send a message
msg_send() {
  local target="${1#@}"
  shift
  local message="$*"

  if [ -z "$target" ] || [ -z "$message" ]; then
    error "Usage: crab msg @name \"message\""
    return 1
  fi

  local relay=$(msg_get "relay")
  local sender=$(msg_get "name")

  if [ -z "$relay" ] || [ -z "$sender" ]; then
    error "Not configured. Run: crab msg start (host) or crab msg join <url> (client)"
    return 1
  fi

  local response
  response=$(curl -s --connect-timeout 5 -X POST \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg to "$target" --arg from "$sender" --arg text "$message" \
      '{to: $to, from: $from, text: $text}')" \
    "${relay}/send" 2>/dev/null)

  if echo "$response" | jq -e '.ok' > /dev/null 2>&1; then
    msg_log ">" "$target" "$message"
    success "Sent to @${target}"
  else
    error "Failed to send (relay: ${relay})"
    return 1
  fi
}

# Read inbox
msg_read() {
  local since="${1:-0}"

  local relay=$(msg_get "relay")
  local name=$(msg_get "name")

  if [ -z "$relay" ] || [ -z "$name" ]; then
    error "Not configured. Run: crab msg start (host) or crab msg join <url> (client)"
    return 1
  fi

  # Calculate since timestamp (default: last 30 minutes)
  local since_ts
  if [ "$since" = "0" ]; then
    since_ts=$(( $(date +%s) - 1800 ))
  else
    # Parse duration like 1h, 30m, 2d
    local num="${since%[hmdHMD]}"
    local unit="${since##*[0-9]}"
    case "$unit" in
      h|H) since_ts=$(( $(date +%s) - num * 3600 )) ;;
      m|M) since_ts=$(( $(date +%s) - num * 60 )) ;;
      d|D) since_ts=$(( $(date +%s) - num * 86400 )) ;;
      *)   since_ts=$(( $(date +%s) - 1800 )) ;;
    esac
  fi

  echo -e "${CYAN}━━ Inbox ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  echo ""

  local response
  response=$(curl -s --connect-timeout 5 "${relay}/read/${name}?since=${since_ts}" 2>/dev/null)

  if [ -z "$response" ] || [ "$response" = "[]" ]; then
    echo "  No new messages"
    echo ""
    return
  fi

  echo "$response" | jq -r '.[] | "\(.ts)|\(.from)|\(.text)"' 2>/dev/null | while IFS='|' read -r ts from text; do
    local time_str=$(date -r "${ts%.*}" '+%H:%M' 2>/dev/null || echo "??:??")
    echo -e "  ${GRAY}[${time_str}]${NC} ${CYAN}@${from}${NC}: ${text}"
    msg_log "<" "$from" "$text"
    msg_say "$from" "$text"
  done
  echo ""
}

# Listen for messages in real-time (poll every 2 seconds)
msg_listen() {
  local relay=$(msg_get "relay")
  local name=$(msg_get "name")

  if [ -z "$relay" ] || [ -z "$name" ]; then
    error "Not configured. Run: crab msg start (host) or crab msg join <url> (client)"
    return 1
  fi

  echo -e "${CYAN}━━ Listening for messages (Ctrl+C to stop) ━━━━━━━${NC}"
  echo ""

  local last_ts=$(date +%s)

  while true; do
    local response
    response=$(curl -s --connect-timeout 5 "${relay}/read/${name}?since=${last_ts}" 2>/dev/null)

    if [ -n "$response" ] && [ "$response" != "[]" ]; then
      echo "$response" | jq -r '.[] | "\(.ts)|\(.from)|\(.text)"' 2>/dev/null | while IFS='|' read -r ts from text; do
        local time_str=$(date -r "${ts%.*}" '+%H:%M' 2>/dev/null || echo "??:??")
        echo -e "  ${GRAY}[${time_str}]${NC} ${CYAN}@${from}${NC}: ${text}"
        msg_log "<" "$from" "$text"
        msg_say "$from" "$text"
      done
      # Update last_ts to the latest message timestamp (keep full float precision
      # so the relay's strict > comparison excludes already-seen messages)
      local new_ts
      new_ts=$(echo "$response" | jq -r '.[-1].ts // 0' 2>/dev/null)
      if [ -n "$new_ts" ] && [ "$new_ts" != "0" ]; then
        last_ts="$new_ts"
      fi
    fi

    sleep 2
  done
}

# View local message history
msg_history() {
  local count="${1:-20}"
  local log_file="$MSG_LOG_DIR/log"

  if [ ! -f "$log_file" ]; then
    echo "No message history yet."
    return
  fi

  echo -e "${CYAN}━━ History ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  echo ""
  tail -n "$count" "$log_file" | while IFS= read -r line; do
    echo -e "  ${GRAY}$line${NC}"
  done
  echo ""
}

# Help text
show_msg_help() {
  echo -e "${CYAN}P2P Messaging Commands${NC}"
  echo ""
  echo "Host (run relay on your machine):"
  echo "  crab msg start                  Start relay server"
  echo "  crab msg start --expose         Start relay + cloudflared tunnel"
  echo "  crab msg stop                   Stop relay and tunnel"
  echo "  crab msg status                 Show relay status"
  echo ""
  echo "Client (connect to a relay):"
  echo "  crab msg join <url>             Connect to a coworker's relay"
  echo ""
  echo "Messaging:"
  echo "  crab msg @name \"message\"        Send a message"
  echo "  crab msg read                   Check inbox (last 30m)"
  echo "  crab msg read 2h                Check inbox (last 2 hours)"
  echo "  crab msg listen                 Live stream (Ctrl+C to stop)"
  echo "  crab msg history                View local message log"
  echo "  crab msg say on/off             Toggle text-to-speech (default: on)"
  echo ""
}

# Main dispatcher
handle_msg_command() {
  local first="${1:-}"

  case "$first" in
    "start")
      msg_relay_start "${2:-}"
      ;;
    "stop")
      msg_relay_stop
      ;;
    "join"|"connect")
      msg_join "${2:-}"
      ;;
    "read"|"inbox")
      msg_read "${2:-0}"
      ;;
    "listen"|"watch")
      msg_listen
      ;;
    "history"|"log")
      msg_history "${2:-20}"
      ;;
    "say")
      local toggle="${2:-}"
      case "$toggle" in
        "on")  msg_set "say" "true"; success "Text-to-speech enabled" ;;
        "off") msg_set "say" "false"; success "Text-to-speech disabled" ;;
        *)
          local current=$(msg_get "say")
          if [ "$current" = "false" ]; then
            echo "Text-to-speech: off"
          else
            echo "Text-to-speech: on (default)"
          fi
          echo "  crab msg say on/off"
          ;;
      esac
      ;;
    "status"|"info")
      msg_relay_info
      ;;
    "help"|"-h"|"--help"|"")
      show_msg_help
      ;;
    "@"*)
      msg_send "$@"
      ;;
    *)
      error "Unknown msg command: $first"
      echo ""
      show_msg_help
      return 1
      ;;
  esac
}

# =============================================================================
# Workspace Handoff - Package and transfer entire context to teammate
# =============================================================================

HANDOFF_DIR="$CONFIG_DIR/handoffs"

# Create a handoff package
create_handoff() {
  local num="$1"
  local recipient="${2:-}"
  local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"

  if [ ! -d "$dir" ]; then
    error "Workspace $num does not exist"
    exit 1
  fi

  mkdir -p "$HANDOFF_DIR"

  local timestamp=$(date +%Y%m%d-%H%M%S)
  local handoff_name="handoff-ws$num-$timestamp"
  local handoff_path="$HANDOFF_DIR/$handoff_name"

  mkdir -p "$handoff_path"

  echo -e "${CYAN}Creating handoff package for workspace $num...${NC}"
  echo ""

  cd "$dir"

  # 1. Save git state
  echo "  📦 Saving git state..."
  local branch=$(git branch --show-current 2>/dev/null || echo "main")
  local commit=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
  local diff=$(git diff HEAD 2>/dev/null || echo "")
  local staged=$(git diff --cached HEAD 2>/dev/null || echo "")
  local status=$(git status --porcelain 2>/dev/null || echo "")
  local log=$(git log --oneline -20 origin/main..HEAD 2>/dev/null || echo "")

  echo "$branch" > "$handoff_path/branch.txt"
  echo "$commit" > "$handoff_path/commit.txt"
  [ -n "$diff" ] && echo "$diff" > "$handoff_path/unstaged.patch"
  [ -n "$staged" ] && echo "$staged" > "$handoff_path/staged.patch"
  [ -n "$status" ] && echo "$status" > "$handoff_path/status.txt"
  [ -n "$log" ] && echo "$log" > "$handoff_path/commits.txt"

  # 2. Save terminal history (last 100 commands from this session)
  echo "  📜 Saving terminal history..."
  if [ -n "$TMUX" ]; then
    local window_name="ws$num"
    # Capture pane content
    tmux capture-pane -t "$SESSION_NAME:$window_name.0" -p -S -100 2>/dev/null > "$handoff_path/terminal-history.txt" || true
    tmux capture-pane -t "$SESSION_NAME:$window_name.1" -p -S -100 2>/dev/null > "$handoff_path/server-output.txt" || true
    tmux capture-pane -t "$SESSION_NAME:$window_name.2" -p -S -100 2>/dev/null > "$handoff_path/main-output.txt" || true
  fi

  # 3. Save agent conversation if available
  local agent_name=$(agent_display_name)
  echo "  🤖 Looking for $agent_name context..."
  local agent=$(get_agent_type)
  case "$agent" in
    codex)
      local codex_sessions="$HOME/.codex/sessions"
      if [ -d "$codex_sessions" ]; then
        local recent_session=$(ls -t "$codex_sessions" 2>/dev/null | head -1)
        if [ -n "$recent_session" ]; then
          cp -r "$codex_sessions/$recent_session" "$handoff_path/codex-session" 2>/dev/null
          echo "    Found Codex session"
        fi
      fi
      ;;
    claude)
      local claude_history="$HOME/.claude/projects"
      if [ -d "$claude_history" ]; then
        local project_dir=$(find "$claude_history" -type d -name "*$(basename "$dir")*" 2>/dev/null | head -1)
        if [ -n "$project_dir" ] && [ -d "$project_dir" ]; then
          local recent_conv=$(ls -t "$project_dir"/*.jsonl 2>/dev/null | head -1)
          if [ -n "$recent_conv" ] && [ -f "$recent_conv" ]; then
            cp "$recent_conv" "$handoff_path/claude-conversation.jsonl"
            echo "    Found Claude conversation"
          fi
        fi
      fi
      ;;
  esac

  # 4. Save environment hints
  echo "  ⚙️  Saving environment info..."
  cat > "$handoff_path/environment.json" << EOF
{
  "workspace": $num,
  "branch": "$branch",
  "created_at": "$(date -Iseconds)",
  "created_by": "$(git config user.name 2>/dev/null || echo "$USER")",
  "recipient": "$recipient",
  "node_version": "$(node --version 2>/dev/null || echo 'unknown')",
  "working_directory": "$dir"
}
EOF

  # 5. Create summary
  echo "  📝 Generating summary..."
  local file_count=$(echo "$status" | grep -c "^" 2>/dev/null || echo "0")
  local commit_count=$(echo "$log" | grep -c "^" 2>/dev/null || echo "0")

  cat > "$handoff_path/README.md" << EOF
# Workspace Handoff: ws-$num

**Created by:** $(git config user.name 2>/dev/null || echo "$USER")
**Date:** $(date)
**Branch:** $branch

## Summary
- $file_count uncommitted changes
- $commit_count commits ahead of main

## To receive this handoff:
\`\`\`bash
crab receive $handoff_name
# or
crab receive $handoff_name --to <workspace-number>
\`\`\`

## Contents
- \`branch.txt\` - Current branch name
- \`commit.txt\` - Current commit hash
- \`unstaged.patch\` - Uncommitted changes
- \`staged.patch\` - Staged changes
- \`commits.txt\` - Commit history
- \`terminal-history.txt\` - Terminal pane history
- \`server-output.txt\` - Server pane output
- \`main-output.txt\` - Main pane output
- \`claude-conversation.jsonl\` - Claude Code context (if available)
EOF

  # 6. Create archive
  echo "  📦 Creating archive..."
  local archive_path="$HANDOFF_DIR/$handoff_name.tar.gz"
  tar -czf "$archive_path" -C "$HANDOFF_DIR" "$handoff_name"

  # Calculate size
  local size=$(du -h "$archive_path" | cut -f1)

  echo ""
  success "Handoff package created!"
  echo ""
  echo -e "  ${BOLD}Package:${NC} $handoff_name"
  echo -e "  ${BOLD}Size:${NC} $size"
  echo -e "  ${BOLD}Location:${NC} $archive_path"
  echo ""

  # 7. Share options
  if [ -n "$recipient" ]; then
    echo -e "${CYAN}Share with $recipient:${NC}"
    echo ""

    # Check if it's a Slack user
    if [[ "$recipient" == @* ]]; then
      echo "  Slack: crab slack $recipient \"Handoff ready: $handoff_name\""
      echo "         crab tk share $archive_path --to slack:$recipient"
    else
      echo "  Upload: crab tk share $archive_path"
      echo "  SSH:    crab tk share $archive_path --to ssh:$recipient"
    fi
    echo ""
  fi

  echo -e "${GRAY}Recipient runs: crab receive $handoff_name${NC}"

  # Cleanup temp directory
  rm -rf "$handoff_path"
}

# Receive and apply a handoff package
receive_handoff() {
  local handoff_name="$1"
  local target_ws="${2:-}"

  # Find the handoff archive
  local archive_path=""
  if [ -f "$handoff_name" ]; then
    archive_path="$handoff_name"
  elif [ -f "$HANDOFF_DIR/$handoff_name.tar.gz" ]; then
    archive_path="$HANDOFF_DIR/$handoff_name.tar.gz"
  elif [ -f "$HANDOFF_DIR/$handoff_name" ]; then
    archive_path="$HANDOFF_DIR/$handoff_name"
  else
    error "Handoff package not found: $handoff_name"
    echo ""
    echo "Looking in: $HANDOFF_DIR"
    echo ""
    echo "Available handoffs:"
    ls -1 "$HANDOFF_DIR"/*.tar.gz 2>/dev/null | while read f; do
      echo "  $(basename "$f" .tar.gz)"
    done
    exit 1
  fi

  echo -e "${CYAN}Receiving handoff package...${NC}"
  echo ""

  # Extract to temp directory
  local tmp_dir="/tmp/crab-handoff-$$"
  mkdir -p "$tmp_dir"
  tar -xzf "$archive_path" -C "$tmp_dir"

  # Find the handoff directory inside
  local handoff_dir=$(ls -d "$tmp_dir"/*/ 2>/dev/null | head -1)
  if [ -z "$handoff_dir" ]; then
    error "Invalid handoff package"
    rm -rf "$tmp_dir"
    exit 1
  fi

  # Read metadata
  local original_ws=$(jq -r ".workspace // 1" "$handoff_dir/environment.json" 2>/dev/null || echo "1")
  local branch=$(cat "$handoff_dir/branch.txt" 2>/dev/null || echo "main")
  local created_by=$(jq -r ".created_by // \"unknown\"" "$handoff_dir/environment.json" 2>/dev/null || echo "unknown")

  echo -e "  ${BOLD}From:${NC} $created_by"
  echo -e "  ${BOLD}Original workspace:${NC} $original_ws"
  echo -e "  ${BOLD}Branch:${NC} $branch"
  echo ""

  # Determine target workspace
  if [ -z "$target_ws" ]; then
    target_ws="$original_ws"
  fi

  local target_dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$target_ws"

  # Create workspace if needed
  if [ ! -d "$target_dir" ]; then
    echo "  Creating workspace $target_ws..."
    create_workspace "$target_ws"
  fi

  cd "$target_dir"

  # Apply git state
  echo "  📦 Applying git state..."
  git fetch origin 2>/dev/null || true

  # Checkout the branch
  if [ "$branch" != "main" ]; then
    git checkout -B "$branch" 2>/dev/null || git checkout "$branch" 2>/dev/null || true
  fi

  # Cherry-pick commits if we have them
  if [ -f "$handoff_dir/commits.txt" ]; then
    local commit_count=$(wc -l < "$handoff_dir/commits.txt" | tr -d ' ')
    if [ "$commit_count" -gt 0 ]; then
      echo "    Found $commit_count commits to apply"
      # Read commit hashes and cherry-pick
      local base_commit=$(cat "$handoff_dir/commit.txt" 2>/dev/null)
      if [ -n "$base_commit" ] && git cat-file -t "$base_commit" >/dev/null 2>&1; then
        echo "    Cherry-picking from handoff..."
        while read -r commit_line; do
          local commit_hash=$(echo "$commit_line" | awk '{print $1}')
          git cherry-pick "$commit_hash" 2>/dev/null || true
        done < <(tac "$handoff_dir/commits.txt")
      fi
    fi
  fi

  # Apply patches
  if [ -f "$handoff_dir/staged.patch" ] && [ -s "$handoff_dir/staged.patch" ]; then
    echo "    Applying staged changes..."
    git apply --index "$handoff_dir/staged.patch" 2>/dev/null || true
  fi

  if [ -f "$handoff_dir/unstaged.patch" ] && [ -s "$handoff_dir/unstaged.patch" ]; then
    echo "    Applying unstaged changes..."
    git apply "$handoff_dir/unstaged.patch" 2>/dev/null || true
  fi

  # Copy agent conversation if available
  local agent_name=$(agent_display_name)
  if [ -f "$handoff_dir/claude-conversation.jsonl" ]; then
    echo "  🤖 Restoring Claude context..."
    local claude_project_dir="$HOME/.claude/projects/-$(echo "$target_dir" | tr '/' '-')"
    mkdir -p "$claude_project_dir"
    cp "$handoff_dir/claude-conversation.jsonl" "$claude_project_dir/handoff-$(date +%s).jsonl"
    echo "    Claude conversation saved (use --continue to resume)"
  elif [ -d "$handoff_dir/codex-session" ]; then
    echo "  🤖 Restoring Codex context..."
    local codex_sessions="$HOME/.codex/sessions"
    mkdir -p "$codex_sessions"
    cp -r "$handoff_dir/codex-session" "$codex_sessions/handoff-$(date +%s)" 2>/dev/null
    echo "    Codex session saved"
  fi

  # Show terminal history context
  if [ -f "$handoff_dir/terminal-history.txt" ]; then
    echo "  📜 Terminal context available"
  fi

  echo ""
  success "Handoff received to workspace $target_ws!"
  echo ""
  echo "  Next steps:"
  echo "    crab ws $target_ws              Open the workspace"
  echo "    crab ws $target_ws continue     Open with $agent_name --continue"
  echo ""

  # Show README
  if [ -f "$handoff_dir/README.md" ]; then
    echo -e "${GRAY}--- Handoff Notes ---${NC}"
    cat "$handoff_dir/README.md" | head -20
    echo -e "${GRAY}---${NC}"
  fi

  # Cleanup
  rm -rf "$tmp_dir"
}

# List available handoffs
list_handoffs() {
  echo -e "${CYAN}Available Handoffs:${NC}"
  echo ""

  if [ ! -d "$HANDOFF_DIR" ] || [ -z "$(ls -A "$HANDOFF_DIR" 2>/dev/null)" ]; then
    echo "  No handoffs found."
    echo ""
    echo "  Create one with: crab handoff [workspace-number] [@recipient]"
    return
  fi

  local count=0
  for archive in "$HANDOFF_DIR"/*.tar.gz; do
    [ -f "$archive" ] || continue
    count=$((count + 1))

    local name=$(basename "$archive" .tar.gz)
    local size=$(du -h "$archive" | cut -f1)
    local date=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M" "$archive" 2>/dev/null || stat -c "%y" "$archive" 2>/dev/null | cut -d' ' -f1-2)

    echo -e "  ${BOLD}$count.${NC} $name"
    echo -e "     Size: $size | Created: $date"
    echo ""
  done
}

# Handle handoff commands
handle_handoff_command() {
  load_config
  validate_config

  case "${1:-}" in
    "ls"|"list")
      list_handoffs
      ;;
    "receive"|"apply")
      if [ -z "${2:-}" ]; then
        error "Usage: crab handoff receive <handoff-name> [--to <workspace>]"
        exit 1
      fi
      local handoff_name="$2"
      local target_ws=""
      if [ "${3:-}" = "--to" ]; then
        target_ws="${4:-}"
      fi
      receive_handoff "$handoff_name" "$target_ws"
      ;;
    "")
      # Auto-detect workspace and create handoff
      local num=$(detect_workspace)
      if [ -z "$num" ]; then
        error "Not in a workspace. Use: crab handoff <workspace-number>"
        exit 1
      fi
      create_handoff "$num"
      ;;
    *)
      # First arg might be workspace number or recipient
      if [[ "$1" =~ ^[0-9]+$ ]]; then
        create_handoff "$1" "${2:-}"
      elif [[ "$1" == @* ]]; then
        # Recipient specified, auto-detect workspace
        local num=$(detect_workspace)
        if [ -z "$num" ]; then
          error "Not in a workspace. Use: crab handoff <workspace-number> $1"
          exit 1
        fi
        create_handoff "$num" "$1"
      else
        error "Unknown handoff command: $1"
        echo ""
        echo "Usage:"
        echo "  crab handoff                    Create from current workspace"
        echo "  crab handoff <N>                Create from workspace N"
        echo "  crab handoff <N> @recipient     Create and note recipient"
        echo "  crab handoff ls                 List available handoffs"
        echo "  crab handoff receive <name>     Apply a handoff"
        echo "  crab handoff receive <name> --to <N>  Apply to specific workspace"
        exit 1
      fi
      ;;
  esac
}

# =============================================================================
# Time Travel - Automatic snapshots and workspace rewind
# =============================================================================

SNAPSHOT_DIR="$CONFIG_DIR/snapshots"
SNAPSHOT_INTERVAL="${CRAB_SNAPSHOT_INTERVAL:-900}"  # Default: 15 minutes

# Create a snapshot of workspace state
create_snapshot() {
  local num="$1"
  local label="${2:-auto}"
  local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"

  if [ ! -d "$dir" ]; then
    return 1
  fi

  mkdir -p "$SNAPSHOT_DIR/ws-$num"

  local timestamp=$(date +%s)
  local snapshot_name="$timestamp-$label"
  local snapshot_path="$SNAPSHOT_DIR/ws-$num/$snapshot_name"

  mkdir -p "$snapshot_path"

  cd "$dir"

  # Save git state
  local branch=$(git branch --show-current 2>/dev/null || echo "main")
  local commit=$(git rev-parse HEAD 2>/dev/null || echo "")
  local diff=$(git diff HEAD 2>/dev/null || echo "")
  local staged=$(git diff --cached HEAD 2>/dev/null || echo "")
  local stash_count=$(git stash list 2>/dev/null | wc -l | tr -d ' ')

  echo "$branch" > "$snapshot_path/branch"
  echo "$commit" > "$snapshot_path/commit"
  [ -n "$diff" ] && echo "$diff" > "$snapshot_path/unstaged.patch"
  [ -n "$staged" ] && echo "$staged" > "$snapshot_path/staged.patch"

  # Save metadata
  cat > "$snapshot_path/metadata.json" << EOF
{
  "timestamp": $timestamp,
  "label": "$label",
  "branch": "$branch",
  "commit": "$commit",
  "has_unstaged": $([ -n "$diff" ] && echo "true" || echo "false"),
  "has_staged": $([ -n "$staged" ] && echo "true" || echo "false"),
  "stash_count": $stash_count,
  "created_at": "$(date -Iseconds)"
}
EOF

  # Prune old snapshots (keep last 50)
  local snapshot_count=$(ls -1d "$SNAPSHOT_DIR/ws-$num"/*/ 2>/dev/null | wc -l | tr -d ' ')
  if [ "$snapshot_count" -gt 50 ]; then
    ls -1d "$SNAPSHOT_DIR/ws-$num"/*/ 2>/dev/null | head -n -50 | xargs rm -rf 2>/dev/null || true
  fi

  echo "$snapshot_name"
}

# Manual snapshot with label
save_snapshot() {
  local num="$1"
  local label="${2:-manual}"

  echo -e "${CYAN}Creating snapshot...${NC}"
  local snapshot_name=$(create_snapshot "$num" "$label")

  if [ -n "$snapshot_name" ]; then
    success "Snapshot saved: $label"
    echo -e "  ${GRAY}Restore with: crab rewind $num $snapshot_name${NC}"
  else
    error "Failed to create snapshot"
  fi
}

# List snapshots for a workspace
list_snapshots() {
  local num="$1"
  local limit="${2:-20}"
  local snapshot_base="$SNAPSHOT_DIR/ws-$num"

  if [ ! -d "$snapshot_base" ]; then
    echo -e "${YELLOW}No snapshots for workspace $num${NC}"
    return
  fi

  echo -e "${CYAN}╭────────────────────────────────────────────────────╮${NC}"
  echo -e "${CYAN}│${NC}         ${BOLD}🕐 Time Travel - Workspace $num${NC}              ${CYAN}│${NC}"
  echo -e "${CYAN}╰────────────────────────────────────────────────────╯${NC}"
  echo ""

  local count=0
  for snapshot_dir in $(ls -1dr "$snapshot_base"/*/ 2>/dev/null); do
    [ -d "$snapshot_dir" ] || continue
    count=$((count + 1))
    [ $count -gt $limit ] && break

    local snapshot_name=$(basename "$snapshot_dir")
    local timestamp=$(echo "$snapshot_name" | cut -d'-' -f1)
    local label=$(echo "$snapshot_name" | cut -d'-' -f2-)

    # Format time relative to now
    local now=$(date +%s)
    local diff=$((now - timestamp))
    local time_ago=""

    if [ $diff -lt 60 ]; then
      time_ago="${diff}s ago"
    elif [ $diff -lt 3600 ]; then
      time_ago="$((diff / 60))m ago"
    elif [ $diff -lt 86400 ]; then
      time_ago="$((diff / 3600))h ago"
    else
      time_ago="$((diff / 86400))d ago"
    fi

    # Read metadata
    local branch=""
    local has_changes="clean"
    if [ -f "$snapshot_dir/metadata.json" ] && command_exists jq; then
      branch=$(jq -r ".branch // \"\"" "$snapshot_dir/metadata.json" 2>/dev/null)
      local has_unstaged=$(jq -r ".has_unstaged // false" "$snapshot_dir/metadata.json" 2>/dev/null)
      local has_staged=$(jq -r ".has_staged // false" "$snapshot_dir/metadata.json" 2>/dev/null)
      [ "$has_unstaged" = "true" ] || [ "$has_staged" = "true" ] && has_changes="changes"
    else
      [ -f "$snapshot_dir/branch" ] && branch=$(cat "$snapshot_dir/branch")
      [ -f "$snapshot_dir/unstaged.patch" ] || [ -f "$snapshot_dir/staged.patch" ] && has_changes="changes"
    fi

    local status_icon="✓"
    local status_color="${GREEN}"
    if [ "$has_changes" = "changes" ]; then
      status_icon="●"
      status_color="${YELLOW}"
    fi

    printf "  ${BOLD}%2d.${NC} %-12s ${GRAY}%-10s${NC} ${status_color}%s${NC} %s\n" \
      "$count" "$time_ago" "$label" "$status_icon" "$branch"
  done

  echo ""
  echo -e "${GRAY}Usage: crab rewind $num <N>  (restore snapshot #N)${NC}"
  echo -e "${GRAY}       crab rewind $num 1h   (restore from ~1 hour ago)${NC}"
  echo ""
}

# Rewind to a specific snapshot
rewind_to_snapshot() {
  local num="$1"
  local target="$2"
  local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
  local snapshot_base="$SNAPSHOT_DIR/ws-$num"

  if [ ! -d "$dir" ]; then
    error "Workspace $num does not exist"
    exit 1
  fi

  if [ ! -d "$snapshot_base" ]; then
    error "No snapshots for workspace $num"
    exit 1
  fi

  local snapshot_dir=""

  # Handle different target formats
  if [[ "$target" =~ ^[0-9]+$ ]]; then
    # Target is a snapshot number (1, 2, 3...)
    snapshot_dir=$(ls -1dr "$snapshot_base"/*/ 2>/dev/null | sed -n "${target}p")
  elif [[ "$target" =~ ^[0-9]+[mhd]$ ]]; then
    # Target is a time offset (30m, 2h, 1d)
    local amount=${target%?}
    local unit=${target: -1}
    local seconds=0

    case "$unit" in
      "m") seconds=$((amount * 60)) ;;
      "h") seconds=$((amount * 3600)) ;;
      "d") seconds=$((amount * 86400)) ;;
    esac

    local target_time=$(($(date +%s) - seconds))

    # Find snapshot closest to target time
    for sdir in $(ls -1dr "$snapshot_base"/*/ 2>/dev/null); do
      local snap_time=$(basename "$sdir" | cut -d'-' -f1)
      if [ "$snap_time" -le "$target_time" ]; then
        snapshot_dir="$sdir"
        break
      fi
    done
  else
    # Target is a snapshot name
    snapshot_dir="$snapshot_base/$target"
  fi

  if [ -z "$snapshot_dir" ] || [ ! -d "$snapshot_dir" ]; then
    error "Snapshot not found: $target"
    echo ""
    echo "Use 'crab rewind $num' to see available snapshots"
    exit 1
  fi

  local snapshot_name=$(basename "$snapshot_dir")
  local timestamp=$(echo "$snapshot_name" | cut -d'-' -f1)
  local label=$(echo "$snapshot_name" | cut -d'-' -f2-)
  local time_str=$(date -r "$timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -d "@$timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "unknown")

  echo -e "${CYAN}Rewinding workspace $num to: $label ($time_str)${NC}"
  echo ""

  # First, save current state as a snapshot
  echo "  💾 Saving current state before rewind..."
  create_snapshot "$num" "pre-rewind" >/dev/null

  cd "$dir"

  # Restore branch
  if [ -f "$snapshot_dir/branch" ]; then
    local branch=$(cat "$snapshot_dir/branch")
    echo "  🔀 Switching to branch: $branch"
    git checkout "$branch" 2>/dev/null || git checkout -b "$branch" 2>/dev/null || true
  fi

  # Restore commit
  if [ -f "$snapshot_dir/commit" ]; then
    local commit=$(cat "$snapshot_dir/commit")
    echo "  📌 Resetting to commit: ${commit:0:8}"
    git reset --hard "$commit" 2>/dev/null || true
  fi

  # Apply patches
  if [ -f "$snapshot_dir/staged.patch" ] && [ -s "$snapshot_dir/staged.patch" ]; then
    echo "  📄 Applying staged changes..."
    git apply --index "$snapshot_dir/staged.patch" 2>/dev/null || true
  fi

  if [ -f "$snapshot_dir/unstaged.patch" ] && [ -s "$snapshot_dir/unstaged.patch" ]; then
    echo "  📄 Applying unstaged changes..."
    git apply "$snapshot_dir/unstaged.patch" 2>/dev/null || true
  fi

  echo ""
  success "Rewound to: $label"
  echo ""
  echo -e "  ${GRAY}Pre-rewind state saved. Undo with: crab rewind $num 1${NC}"
}

# Auto-snapshot daemon (background)
start_snapshot_daemon() {
  local num="$1"
  local interval="${2:-$SNAPSHOT_INTERVAL}"

  echo -e "${CYAN}Starting snapshot daemon for workspace $num...${NC}"
  echo "  Interval: ${interval}s"
  echo ""

  while true; do
    create_snapshot "$num" "auto" >/dev/null 2>&1
    sleep "$interval"
  done
}

# Handle time travel commands
handle_rewind_command() {
  load_config
  validate_config

  local num="${1:-}"

  # Auto-detect workspace if not specified
  if [ -z "$num" ] || ! [[ "$num" =~ ^[0-9]+$ ]]; then
    num=$(detect_workspace)
    if [ -z "$num" ]; then
      error "Specify workspace number: crab rewind <N>"
      exit 1
    fi
    # First arg might be the target, shift it
    if [ -n "$1" ] && ! [[ "$1" =~ ^[0-9]+$ ]]; then
      set -- "$num" "$@"
    fi
  fi

  local action="${2:-}"

  case "$action" in
    "")
      list_snapshots "$num"
      ;;
    "save"|"snapshot")
      save_snapshot "$num" "${3:-manual}"
      ;;
    "daemon"|"auto")
      start_snapshot_daemon "$num" "${3:-$SNAPSHOT_INTERVAL}"
      ;;
    *)
      rewind_to_snapshot "$num" "$action"
      ;;
  esac
}

# =============================================================================
# Live Pairing - Real-time session sharing with teammates
# =============================================================================

PAIR_SOCKET_DIR="$CONFIG_DIR/pair"

# Start a pairing session (host)
start_pair_session() {
  local num="$1"
  local mode="${2:-pair}"  # pair or spectate

  mkdir -p "$PAIR_SOCKET_DIR"

  local socket_name="crab-pair-$num"
  local socket_path="$PAIR_SOCKET_DIR/$socket_name"

  # Check if session already exists
  if [ -S "$socket_path" ]; then
    echo -e "${YELLOW}Pairing session already active for workspace $num${NC}"
    echo ""
    echo "Share this command with your teammate:"
    echo -e "  ${GREEN}crab join $num${NC}"
    echo ""
    echo "Or for spectate-only mode:"
    echo -e "  ${GREEN}crab spectate $num${NC}"
    return
  fi

  echo -e "${CYAN}╭────────────────────────────────────────────────────╮${NC}"
  echo -e "${CYAN}│${NC}       ${BOLD}🦀 Live Pairing - Workspace $num${NC}              ${CYAN}│${NC}"
  echo -e "${CYAN}╰────────────────────────────────────────────────────╯${NC}"
  echo ""

  # Create shared tmux session with socket
  local window_name="ws$num"

  if ! tmux -S "$socket_path" has-session -t "$SESSION_NAME" 2>/dev/null; then
    echo "  🔌 Creating shared session..."
    # Create a new session with the socket
    tmux -S "$socket_path" new-session -d -s "$SESSION_NAME" -n "$window_name"

    # Set permissions so others can join
    chmod 777 "$socket_path" 2>/dev/null || true
  fi

  echo ""
  echo -e "${GREEN}✓ Pairing session active!${NC}"
  echo ""
  echo -e "${BOLD}Share with teammates:${NC}"
  echo ""
  echo -e "  ${CYAN}Same machine:${NC}"
  echo "    crab join $num"
  echo "    crab spectate $num     (view only)"
  echo ""
  echo -e "  ${CYAN}Remote (via SSH):${NC}"
  echo "    ssh $(whoami)@$(hostname) -t 'crab join $num'"
  echo ""
  echo -e "  ${CYAN}Share via tmate (public URL):${NC}"
  if command_exists tmate; then
    echo "    crab pair $num --tmate"
  else
    echo "    Install tmate: brew install tmate"
  fi
  echo ""

  # Attach to the session
  tmux -S "$socket_path" attach-session -t "$SESSION_NAME"
}

# Join a pairing session
join_pair_session() {
  local num="$1"
  local mode="${2:-pair}"  # pair or spectate

  local socket_name="crab-pair-$num"
  local socket_path="$PAIR_SOCKET_DIR/$socket_name"

  if [ ! -S "$socket_path" ]; then
    error "No active pairing session for workspace $num"
    echo ""
    echo "Ask the host to start one with: crab pair $num"
    exit 1
  fi

  echo -e "${CYAN}Joining workspace $num...${NC}"

  if [ "$mode" = "spectate" ]; then
    echo -e "${YELLOW}Spectate mode: view only, no input${NC}"
    tmux -S "$socket_path" attach-session -t "$SESSION_NAME" -r
  else
    tmux -S "$socket_path" attach-session -t "$SESSION_NAME"
  fi
}

# Start tmate session for public sharing
start_tmate_session() {
  local num="$1"

  if ! command_exists tmate; then
    error "tmate is not installed"
    echo ""
    echo "Install with: brew install tmate"
    exit 1
  fi

  echo -e "${CYAN}╭────────────────────────────────────────────────────╮${NC}"
  echo -e "${CYAN}│${NC}     ${BOLD}🦀 Live Pairing via tmate - Workspace $num${NC}     ${CYAN}│${NC}"
  echo -e "${CYAN}╰────────────────────────────────────────────────────╯${NC}"
  echo ""
  echo "  Starting tmate session..."
  echo ""

  # Start tmate
  tmate -F -v -S "/tmp/tmate-crab-$num.sock" new-session -d -s "crab-$num" 2>/dev/null &
  local tmate_pid=$!

  sleep 2

  # Get connection info
  local ssh_url=$(tmate -S "/tmp/tmate-crab-$num.sock" display -p '#{tmate_ssh}' 2>/dev/null)
  local web_url=$(tmate -S "/tmp/tmate-crab-$num.sock" display -p '#{tmate_web}' 2>/dev/null)
  local ssh_ro_url=$(tmate -S "/tmp/tmate-crab-$num.sock" display -p '#{tmate_ssh_ro}' 2>/dev/null)
  local web_ro_url=$(tmate -S "/tmp/tmate-crab-$num.sock" display -p '#{tmate_web_ro}' 2>/dev/null)

  echo -e "${GREEN}✓ tmate session ready!${NC}"
  echo ""
  echo -e "${BOLD}Share these links with your teammate:${NC}"
  echo ""
  echo -e "  ${CYAN}Full access (pair):${NC}"
  [ -n "$ssh_url" ] && echo "    SSH: $ssh_url"
  [ -n "$web_url" ] && echo "    Web: $web_url"
  echo ""
  echo -e "  ${CYAN}View only (spectate):${NC}"
  [ -n "$ssh_ro_url" ] && echo "    SSH: $ssh_ro_url"
  [ -n "$web_ro_url" ] && echo "    Web: $web_ro_url"
  echo ""

  # Attach
  tmate -S "/tmp/tmate-crab-$num.sock" attach-session -t "crab-$num"
}

# List active pairing sessions
list_pair_sessions() {
  echo -e "${CYAN}Active Pairing Sessions:${NC}"
  echo ""

  local found=false
  for socket in "$PAIR_SOCKET_DIR"/crab-pair-*; do
    [ -S "$socket" ] || continue
    found=true

    local num=$(basename "$socket" | sed 's/crab-pair-//')
    local clients=$(tmux -S "$socket" list-clients 2>/dev/null | wc -l | tr -d ' ')

    echo -e "  ${BOLD}Workspace $num${NC} - $clients connected"
    echo "    Join: crab join $num"
    echo "    Spectate: crab spectate $num"
    echo ""
  done

  if [ "$found" = false ]; then
    echo "  No active sessions."
    echo ""
    echo "  Start one with: crab pair <workspace-number>"
  fi
}

# End a pairing session
end_pair_session() {
  local num="$1"
  local socket_path="$PAIR_SOCKET_DIR/crab-pair-$num"

  if [ ! -S "$socket_path" ]; then
    echo "No active session for workspace $num"
    return
  fi

  echo -e "${YELLOW}Ending pairing session for workspace $num...${NC}"
  tmux -S "$socket_path" kill-session -t "$SESSION_NAME" 2>/dev/null || true
  rm -f "$socket_path"
  success "Session ended"
}

# Handle pair commands
handle_pair_command() {
  load_config 2>/dev/null || true

  case "${1:-}" in
    "ls"|"list")
      list_pair_sessions
      ;;
    "end"|"stop")
      if [ -z "${2:-}" ]; then
        error "Usage: crab pair end <workspace-number>"
        exit 1
      fi
      end_pair_session "$2"
      ;;
    "")
      # Auto-detect workspace
      load_config
      validate_config
      local num=$(detect_workspace)
      if [ -z "$num" ]; then
        error "Specify workspace: crab pair <N>"
        exit 1
      fi
      start_pair_session "$num"
      ;;
    *)
      if [[ "$1" =~ ^[0-9]+$ ]]; then
        local num="$1"
        if [ "${2:-}" = "--tmate" ]; then
          start_tmate_session "$num"
        else
          start_pair_session "$num"
        fi
      else
        error "Unknown pair command: $1"
        echo ""
        echo "Usage:"
        echo "  crab pair <N>            Start pairing session"
        echo "  crab pair <N> --tmate    Start with public tmate URLs"
        echo "  crab join <N>            Join a pairing session"
        echo "  crab spectate <N>        Join as spectator (view only)"
        echo "  crab pair ls             List active sessions"
        echo "  crab pair end <N>        End a session"
        exit 1
      fi
      ;;
  esac
}

# =============================================================================
# Help / Cheat Sheet
# =============================================================================

# Show shared volume info
show_shared() {
  load_config

  if [ "$SHARED_VOLUME_ENABLED" != "true" ]; then
    echo -e "${YELLOW}Shared volume is disabled.${NC}"
    echo ""
    echo "To enable, add to your config:"
    echo "  shared_volume:"
    echo "    enabled: true"
    return
  fi

  echo -e "${CYAN}Shared Volume${NC}"
  echo ""
  echo -e "  Path: ${GREEN}$SHARED_VOLUME_PATH${NC}"
  echo -e "  Link name: $SHARED_VOLUME_LINK"
  echo ""

  if [ -d "$SHARED_VOLUME_PATH" ]; then
    local count=$(ls -1 "$SHARED_VOLUME_PATH" 2>/dev/null | wc -l | tr -d ' ')
    echo "  Contents ($count items):"
    ls -la "$SHARED_VOLUME_PATH" 2>/dev/null | tail -n +2 | while read -r line; do
      echo "    $line"
    done
  else
    echo -e "  ${YELLOW}Not yet created (will be created on first workspace open)${NC}"
  fi
}

show_cheat() {
  cat << 'EOF'
╔═══════════════════════════════════════════════════════════════════════════════╗
║                                                                               ║
║      \___/                                                                    ║
║     ( •_•)         CRABCODE CHEAT SHEET                                       ║
║    /)🦀(\          Workspace manager for multi-repo development               ║
║   <      >                                                                    ║
║                    Tip: Use 'crab' as shorthand for 'crabcode'                ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  MULTI-PROJECT (crab @alias ...)                                             ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab init              Register a new project (asks for alias)               ║
║  crab init -t <tpl>     Register with template (e.g., promptfoo-cloud)        ║
║  crab @pf ws 1          Open workspace 1 for project "pf"                    ║
║  crab @cb config        Show config for project "cb"                          ║
║  crab ws 1              Uses default project (or detects from cwd)            ║
║  crab projects          List all registered projects                          ║
║  crab projects rm <a>   Remove a project registration                         ║
║  crab default pf        Set default project                                   ║
║  crab default           Show current default                                  ║
║                                                                               ║
║  Config: ~/.crabcode/projects/<alias>.yaml                                   ║
║  Auto-detect: crab figures out which project from your cwd                    ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  WORKSPACE COMMANDS (crab ws ...)                                             ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab ws               Interactive menu (list + actions)                      ║
║  crab ws ls            List workspaces (non-interactive)                      ║
║  crab ws new           Create next available workspace                        ║
║  crab ws <N>           Open/create workspace N                                ║
║  crab ws <N> --separate   Open in new terminal window                         ║
║  crab ws <N> restart   Reset git + restart panes (recreates full layout)      ║
║  crab ws <N> cleanup   Kill window + reset to origin/main                     ║
║  crab ws <N> destroy   Completely remove workspace (worktree + files)         ║
║  crab destroy <N>      Shorthand for above                                    ║
║  crab ws <N> continue  Resume with --continue flag                            ║
║                                                                               ║
║  SHORTCUTS (auto-detect workspace from cwd or tmux window)                    ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab restart          Restart current workspace                              ║
║  crab cleanup          Cleanup current workspace                              ║
║  crab continue         Continue current workspace                             ║
║  crab <N>              Shorthand for: crab ws <N>                             ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  SESSION COMMANDS (crab session ...)                                          ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab session ls           List sessions with summaries                       ║
║  crab session start "name" Start new named session                            ║
║  crab session resume "name" Resume existing session                           ║
║  crab session delete "name" Delete a session                                  ║
║  crab session summary "name" "text"  Update session summary                   ║
║                                                                               ║
║  REVIEW COMMANDS (crab review ...)                                            ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab review <PR>          Quick review (number, repo#num, or URL)            ║
║  crab review new           Interactive mode (multiple PRs + context)          ║
║  crab review ls            List review sessions                               ║
║  crab review resume <PR>   Resume a review session                            ║
║                                                                               ║
║  PR formats: 3230, promptfoo#456, https://github.com/.../pull/123            ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  TICKET COMMANDS (crab ticket ...)                                            ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab ticket ENG-123       Auto-create workspace for ticket                   ║
║  crab ws 3 ticket ENG-123  Use workspace 3 for ticket                         ║
║  crab @pf ticket ENG-123   Ticket for specific project                        ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  WIP COMMANDS (crab wip ...)                                                  ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab wip save              Save current changes (branch, commits, patches)   ║
║  crab wip save --restart    Save then restart workspace                       ║
║  crab wip ls                List all WIPs globally with metadata              ║
║  crab wip restore           Interactive restore from all WIPs                 ║
║  crab wip restore <N>       Restore WIP #N to original workspace              ║
║  crab wip restore <N> --to <ws>  Restore to different workspace               ║
║  crab wip restore <N> --open     Restore and open workspace with configured agent ║
║  crab wip --continue        Restore most recent WIP (current workspace)       ║
║  crab wip delete <name>     Delete a saved WIP state                          ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  TOOLKIT COMMANDS (crab tk ...)                                               ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab tk share <path>              Upload → temp URL (transfer.sh)            ║
║  crab tk share <path> --to ssh:... Transfer via SSH/rsync                     ║
║  crab tk share <path> --to slack:# Upload to Slack channel                    ║
║  crab tk share <path> --to email:@ Send via email                             ║
║  crab tk share <path> --serve      Start local HTTP server + QR code          ║
║  crab tk share <path> --zip        Just create local archive                  ║
║                                                                               ║
║  Options: --include-git, --include-deps, --port=8080                          ║
║  Auto-excludes: node_modules, .git, vendor, venv, dist, build...              ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  SLACK COMMANDS (crab slack ...)                                              ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab slack @user "msg"       Send DM to a user                               ║
║  crab slack #channel "msg"    Post to a channel                               ║
║  crab slack read @user        Show recent DMs with user                       ║
║  crab slack chat @user        Interactive terminal chat                       ║
║  crab slack sent              View your sent messages log                     ║
║  crab slack users             List all workspace users                        ║
║  crab slack users mike        Search users by name                            ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  OTHER COMMANDS                                                               ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab config scan      Auto-detect .env files and ports                       ║
║  crab config           Show current configuration                             ║
║  crab doctor           Diagnose common issues                                 ║
║  crab ports            Show port usage across workspaces                      ║
║  crab shared           Show shared volume info                                ║
║  crab mood             Show your crab's current mood 🦀                       ║
║  crab status           Show workspace status overview                         ║
║  crab update           Update crabcode to latest version                      ║
║  crab cheat            Show this cheat sheet                                  ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  COMMAND ALIASES (crab alias ...)                                            ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab alias              List all aliases                                     ║
║  crab alias set cu cleanup          Set alias: cu → cleanup                  ║
║  crab alias set wn "ws new"         Multi-word: wn → ws new                  ║
║  crab alias rm cu        Remove an alias                                      ║
║                                                                               ║
║  Config: ~/.crabcode/config.yaml (aliases section)                           ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  MOBILE COMPANION (crab mobile ...)                                           ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab mobile setup     Setup push notifications (ntfy.sh)                     ║
║  crab mobile serve     Start mobile dashboard on port 8888                    ║
║  crab mobile notify "Title" "Message"   Send push notification                ║
║  crab notify "Title" "Message"          Shorthand for above                   ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  P2P MESSAGING (crab msg ...)                                                ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab msg start              Start relay on your machine                     ║
║  crab msg start --expose     Start relay + cloudflared tunnel                ║
║  crab msg stop               Stop relay and tunnel                           ║
║  crab msg join <url>         Connect to a coworker's relay                   ║
║  crab msg @name "message"    Send a message                                  ║
║  crab msg read               Check inbox (last 30m)                          ║
║  crab msg listen             Live message stream                             ║
║  crab msg history            View local message log                          ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  WORKSPACE HANDOFF (crab handoff ...)                                         ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab handoff              Package current workspace for teammate             ║
║  crab handoff <N>          Package workspace N                                ║
║  crab handoff <N> @mike    Package with recipient noted                       ║
║  crab handoff ls           List available handoff packages                    ║
║  crab receive <name>       Apply a handoff to original workspace              ║
║  crab receive <name> --to <N>   Apply to different workspace                  ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  TIME TRAVEL (crab rewind ...)                                                ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab rewind <N>           List snapshots for workspace N                     ║
║  crab rewind <N> 3         Restore 3rd most recent snapshot                   ║
║  crab rewind <N> 2h        Restore from ~2 hours ago                          ║
║  crab rewind <N> save      Create manual snapshot                             ║
║  crab snapshot             Create snapshot of current workspace               ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  LIVE PAIRING (crab pair ...)                                                 ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab pair <N>             Start pairing session for workspace N              ║
║  crab pair <N> --tmate     Start with public tmate URLs                       ║
║  crab join <N>             Join a pairing session (full access)               ║
║  crab spectate <N>         Join as spectator (view only)                      ║
║  crab pair ls              List active pairing sessions                       ║
║  crab pair end <N>         End a pairing session                              ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  MARKDOWN RENDERER (crab md ...)                                              ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  crab md <file>              Render markdown in browser (GFM + dark mode)    ║
║  crab md --update-assets     Re-download rendering libraries                  ║
║                                                                               ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  TMUX KEYBINDINGS (Prefix = Ctrl+a)                                           ║
║  ────────────────────────────────────────────────────────────────────────     ║
║  Option+1,2,3...       Switch to workspace 1, 2, 3...                         ║
║  Ctrl+a  n/p           Next/previous window                                   ║
║  Ctrl+a  d             Detach from session                                    ║
║  Ctrl+a  z             Toggle pane zoom                                       ║
║  Option+←/→/↑/↓        Navigate panes                                         ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝
EOF
}

show_help() {
  echo '    \___/'
  echo '   ( •_•)  crabcode'
  echo '  /)🦀(\   Workspace manager for multi-repo development'
  echo ' <      >'
  echo "         v$VERSION"
  echo ""
  echo "Usage: crab [command] [arguments]"
  echo "       (crab is an alias for crabcode)"
  echo ""
  echo "Workspace Commands:"
  echo "  ws                List all workspaces"
  echo "  ws new            Create next available workspace"
  echo "  ws <N>            Open/create workspace N"
  echo "  ws <N> restart    Reset git + restart panes (recreates layout)"
  echo "  ws <N> cleanup    Kill window + reset to origin/main"
  echo "  ws <N> continue   Resume with --continue flag"
  echo "  ws <N> --separate Open in new terminal window"
  echo ""
  echo "Shortcuts (auto-detect workspace):"
  echo "  <N>               Shorthand for: ws <N>"
  echo "  restart           Restart current workspace"
  echo "  cleanup           Cleanup current workspace"
  echo "  continue          Continue current workspace"
  echo ""
  echo "Session Commands:"
  echo "  session ls        List sessions with summaries"
  echo "  session start     Start new named session"
  echo "  session resume    Resume existing session"
  echo "  session delete    Delete a session"
  echo ""
  echo "Review Commands:"
  echo "  review <PR>       Quick review (number, repo#num, or URL)"
  echo "  review new        Interactive (multiple PRs + context)"
  echo "  review ls         List review sessions"
  echo "  review resume     Resume a review"
  echo ""
  echo "Ticket Commands:"
  echo "  ticket <ID>       Create workspace for a Linear ticket"
  echo "  ws <N> ticket <ID> Use specific workspace for ticket"
  echo ""
  echo "WIP Commands:"
  echo "  wip save          Save current changes"
  echo "  wip ls            List all WIPs globally"
  echo "  wip restore       Interactive restore from all WIPs"
  echo "  wip restore <N>   Restore WIP #N"
  echo "  wip restore <N> --to <ws>  Restore to different workspace"
  echo "  wip --continue    Restore most recent WIP (current workspace)"
  echo ""
  echo "Toolkit Commands:"
  echo "  tk share <path>   Share file/folder (temp URL)"
  echo "  tk share --to ssh:user@host   SSH transfer"
  echo "  tk share --to slack:#channel  Slack upload"
  echo "  tk share --serve  Local HTTP server"
  echo ""
  echo "Markdown:"
  echo "  md <file>         Render markdown in browser (GFM + syntax highlighting)"
  echo ""
  echo "Multi-Project Commands:"
  echo "  @alias <cmd>      Run command against a specific project"
  echo "  projects          List registered projects"
  echo "  default [alias]   Show/set default project"
  echo "  init              Register a new project (asks for alias)"
  echo ""
  echo "Alias Commands:"
  echo "  alias             List all aliases"
  echo "  alias set <name> <cmd>  Set a command alias"
  echo "  alias rm <name>   Remove an alias"
  echo ""
  echo "Other Commands:"
  echo "  init              Setup config (auto-detects project type)"
  echo "  init -t <name>    Setup with template (e.g., promptfoo-cloud)"
  echo "  init --list-templates  Show available templates"
  echo "  config            Show configuration"
  echo "  config scan       Auto-detect .env files and ports"
  echo "  doctor            Diagnose issues"
  echo "  ports             Show port usage"
  echo "  shared            Show shared volume info"
  echo "  cheat             Show cheat sheet"
  echo ""
  echo "Config: $CONFIG_FILE"
}

# =============================================================================
# Workspace Subcommand Handler
# =============================================================================

handle_ws_command() {
  local arg="${1:-}"
  shift || true

  case "$arg" in
    "")
      interactive_workspace_menu
      ;;
    "ls"|"list")
      list_workspaces
      ;;
    "new"|"create")
      create_new_workspace
      ;;
    *)
      if [[ "$arg" =~ ^[0-9]+$ ]]; then
        local num="$arg"

        case "${1:-}" in
          "cleanup"|"clean")
            cleanup_workspace "$num"
            ;;
          "destroy"|"rm"|"remove")
            destroy_workspace "$num" "${2:-}"
            ;;
          "restart"|"reset"|"refresh")
            restart_workspace "$num"
            ;;
          "continue"|"resume")
            continue_workspace "$num"
            ;;
          "--separate"|"-s"|"separate")
            open_workspace_separate "$num"
            ;;
          "wip")
            handle_wip_for_workspace "$num" "${@:2}"
            ;;
          "ticket"|"tkt")
            local ticket_id="${2:-}"
            if [ -z "$ticket_id" ]; then
              error "Ticket identifier required: crab ws $num ticket <identifier>"
              exit 1
            fi
            if ! [[ "$ticket_id" =~ ^[A-Za-z0-9_-]+$ ]]; then
              error "Invalid ticket identifier: $ticket_id"
              echo "Identifiers must be alphanumeric (dashes and underscores allowed)"
              exit 1
            fi
            open_workspace "$num" "$(build_ticket_prompt "$ticket_id")"
            ;;
          "")
            open_workspace "$num"
            ;;
          *)
            error "Unknown command: crab ws $num $1"
            echo "Try: crab ws $num restart|cleanup|continue|wip|ticket"
            exit 1
            ;;
        esac
      else
        error "Invalid workspace argument: $arg"
        echo "Usage: crab ws <N> or crab ws new"
        exit 1
      fi
      ;;
  esac
}

# Handle top-level ticket command
handle_ticket_command() {
  local identifier="${1:-}"

  if [ -z "$identifier" ]; then
    error "Ticket identifier required"
    echo ""
    echo "Usage: crab ticket <identifier>"
    echo "       crab ws <N> ticket <identifier>"
    echo ""
    echo "Examples:"
    echo "  crab ticket ENG-123"
    echo "  crab ws 3 ticket ENG-123"
    exit 1
  fi

  if ! [[ "$identifier" =~ ^[A-Za-z0-9_-]+$ ]]; then
    error "Invalid ticket identifier: $identifier"
    echo "Identifiers must be alphanumeric (dashes and underscores allowed)"
    exit 1
  fi

  local num
  num=$(find_next_workspace)
  if [ -z "$num" ]; then
    error "No available workspace slots (max 100)"
    exit 1
  fi

  echo -e "${CYAN}Using next available workspace: $num${NC}"
  open_workspace "$num" "$(build_ticket_prompt "$identifier")"
}

# Handle WIP commands for a specific workspace
handle_wip_for_workspace() {
  local num=$1
  shift || true

  case "${1:-}" in
    "save")
      shift  # Remove "save"
      local do_restart="false"
      local custom_name=""
      while [ $# -gt 0 ]; do
        case "$1" in
          --restart|-r)
            do_restart="true"
            shift
            ;;
          *)
            custom_name="$1"
            shift
            ;;
        esac
      done
      wip_save "$num" "$do_restart" "$custom_name"
      ;;
    "ls"|"list")
      wip_list "$num"
      ;;
    "--continue"|"-c")
      wip_continue "$num"
      ;;
    "--resume"|"-r"|"resume")
      wip_resume "$num"
      ;;
    "delete"|"rm")
      wip_delete "$num" "${2:-}"
      ;;
    *)
      echo -e "${CYAN}WIP Commands for workspace $num:${NC}"
      echo "  crab ws $num wip save [\"name\"] [--restart]"
      echo "  crab ws $num wip ls"
      echo "  crab ws $num wip --continue"
      echo "  crab ws $num wip --resume"
      echo "  crab ws $num wip delete <name>"
      ;;
  esac
}

# =============================================================================
# Promptfoo Plugin Commands
# =============================================================================

handle_pf_command() {
  local PLUGIN_DIR="$CONFIG_DIR/plugins/promptfoo"

  case "${1:-}" in
    "install")
      pf_install
      ;;
    "uninstall"|"remove")
      pf_uninstall
      ;;
    "update"|"upgrade")
      pf_uninstall
      pf_install
      ;;
    "help"|"-h"|"--help"|"")
      pf_help
      ;;
    *)
      # Check if plugin is installed before running
      if [ ! -d "$PLUGIN_DIR" ] || [ ! -f "$PLUGIN_DIR/dist/cli.js" ]; then
        echo -e "${CYAN}Promptfoo Target Discovery Agent${NC}"
        echo ""
        echo "Plugin not installed. Install with:"
        echo ""
        echo -e "  ${GREEN}crab pf install${NC}"
        echo ""
        return 1
      fi
      # Pass all args to the Node.js CLI
      node "$PLUGIN_DIR/dist/cli.js" "$@"
      ;;
  esac
}

pf_install() {
  local PLUGIN_DIR="$CONFIG_DIR/plugins/promptfoo"
  local REPO_URL="https://github.com/promptfoo/crabcode.git"
  local BRANCH="main"

  echo -e "${CYAN}Installing Promptfoo Target Discovery Agent...${NC}"

  # Check for Node.js
  if ! command_exists node; then
    error "Node.js is required. Install with: brew install node"
    return 1
  fi

  # Create plugins directory
  mkdir -p "$CONFIG_DIR/plugins"

  # Clone just the plugin directory using sparse checkout
  local TEMP_DIR=$(mktemp -d)
  echo "Cloning plugin from $REPO_URL..."

  git clone --depth 1 --branch "$BRANCH" --filter=blob:none --sparse "$REPO_URL" "$TEMP_DIR" 2>/dev/null
  cd "$TEMP_DIR"
  git sparse-checkout set plugins/promptfoo

  # Move to final location
  rm -rf "$PLUGIN_DIR"
  mv plugins/promptfoo "$PLUGIN_DIR"
  cd - >/dev/null
  rm -rf "$TEMP_DIR"

  # Install dependencies and build
  echo "Installing dependencies..."
  cd "$PLUGIN_DIR"
  npm install --silent
  npm run build --silent
  cd - >/dev/null

  success "✓ Installed to $PLUGIN_DIR"
  echo ""
  echo "Usage:"
  echo -e "  ${GREEN}crab pf --file target.txt${NC}           # Analyze from file"
  echo -e "  ${GREEN}crab pf \"curl http://...\"${NC}           # Analyze curl command"
  echo -e "  ${GREEN}crab pf --help${NC}                      # Show all options"
}

pf_uninstall() {
  local PLUGIN_DIR="$CONFIG_DIR/plugins/promptfoo"

  if [ -d "$PLUGIN_DIR" ]; then
    rm -rf "$PLUGIN_DIR"
    success "✓ Uninstalled promptfoo plugin"
  else
    echo "Plugin not installed"
  fi
}

pf_help() {
  echo -e "${CYAN}Promptfoo Target Discovery Agent${NC}"
  echo ""
  echo "Analyze target specifications and generate working promptfoo configs."
  echo ""
  echo -e "${BOLD}Installation:${NC}"
  echo "  crab pf install              Install the plugin"
  echo "  crab pf uninstall            Remove the plugin"
  echo "  crab pf update               Update to latest version"
  echo ""
  echo -e "${BOLD}Usage:${NC}"
  echo "  crab pf --file <path>        Analyze target from file"
  echo "  crab pf \"curl ...\"           Analyze curl command"
  echo "  crab pf --url <url>          Probe a URL directly"
  echo ""
  echo -e "${BOLD}Options:${NC}"
  echo "  --output, -o <dir>           Output directory (default: current dir)"
  echo "  --provider <provider>        LLM provider (default: openai:gpt-5)"
  echo "  --verbose, -v                Show detailed output"
  echo ""
  echo -e "${BOLD}Supported formats:${NC}"
  echo "  - Curl commands"
  echo "  - OpenAPI/Swagger specs"
  echo "  - Postman collections"
  echo "  - Burp Suite exports"
  echo "  - Plain text descriptions"
  echo ""
  echo -e "${BOLD}Environment:${NC}"
  echo "  OPENAI_API_KEY               OpenAI API key"
  echo "  ANTHROPIC_API_KEY            Anthropic API key"
}

# =============================================================================
# Tax Plugin Commands
# =============================================================================

handle_tax_command() {
  local PLUGIN_DIR="$CONFIG_DIR/plugins/tax"

  case "${1:-}" in
    "install")
      tax_install
      ;;
    "uninstall"|"remove")
      tax_uninstall
      ;;
    "update"|"upgrade")
      tax_uninstall
      tax_install
      ;;
    "help"|"-h"|"--help"|"")
      tax_help
      ;;
    *)
      if [ ! -d "$PLUGIN_DIR" ] || [ ! -f "$PLUGIN_DIR/dist/cli.js" ]; then
        echo -e "${CYAN}Crab Tax - Tax document organizer${NC}"
        echo ""
        echo "Plugin not installed. Install with:"
        echo ""
        echo -e "  ${GREEN}crab tax install${NC}"
        echo ""
        return 1
      fi
      node "$PLUGIN_DIR/dist/cli.js" "$@"
      ;;
  esac
}

tax_install() {
  local PLUGIN_DIR="$CONFIG_DIR/plugins/tax"
  local REPO_URL="https://github.com/promptfoo/crabcode.git"
  local BRANCH="main"

  echo -e "${CYAN}Installing Crab Tax...${NC}"

  if ! command_exists node; then
    error "Node.js is required. Install with: brew install node"
    return 1
  fi

  mkdir -p "$CONFIG_DIR/plugins"

  local TEMP_DIR=$(mktemp -d)
  echo "Cloning plugin from $REPO_URL..."

  git clone --depth 1 --branch "$BRANCH" --filter=blob:none --sparse "$REPO_URL" "$TEMP_DIR" 2>/dev/null
  cd "$TEMP_DIR"
  git sparse-checkout set plugins/tax

  rm -rf "$PLUGIN_DIR"
  mv plugins/tax "$PLUGIN_DIR"
  cd - >/dev/null
  rm -rf "$TEMP_DIR"

  echo "Installing dependencies..."
  cd "$PLUGIN_DIR"
  npm install --silent
  npm run build --silent
  cd - >/dev/null

  success "✓ Installed to $PLUGIN_DIR"
  echo ""
  echo "Usage:"
  echo -e "  ${GREEN}crab tax ./my-tax-docs${NC}                  # Inventory tax documents"
  echo -e "  ${GREEN}crab tax ./my-tax-docs --output ./out${NC}    # Write outputs to a directory"
  echo -e "  ${GREEN}crab tax --help${NC}                          # Show all options"
}

tax_uninstall() {
  local PLUGIN_DIR="$CONFIG_DIR/plugins/tax"

  if [ -d "$PLUGIN_DIR" ]; then
    rm -rf "$PLUGIN_DIR"
    success "✓ Uninstalled tax plugin"
  else
    echo "Plugin not installed"
  fi
}

tax_help() {
  echo -e "${CYAN}Crab Tax - Tax document organizer${NC}"
  echo ""
  echo "Organize tax documents and generate filing handoff outputs."
  echo ""
  echo -e "${BOLD}Installation:${NC}"
  echo "  crab tax install               Install the plugin"
  echo "  crab tax uninstall             Remove the plugin"
  echo "  crab tax update                Update to latest version"
  echo ""
  echo -e "${BOLD}Usage:${NC}"
  echo "  crab tax <input-dir>           Process a folder of tax documents"
  echo "  crab tax <input-dir> --output ./out"
  echo "  crab tax <input-dir> --profile ./profile.json"
  echo ""
  echo -e "${BOLD}Options:${NC}"
  echo "  --output <dir>                 Output directory"
  echo "  --profile <path>               Taxpayer profile JSON"
  echo "  --tax-year <year>              Tax year (default: 2025)"
  echo "  --filing-status <single|mfj>   Filing status"
  echo "  --preview                      Allow preview outputs"
  echo "  --verbose                      Show additional output"
}

# =============================================================================
# Draw Plugin Commands
# =============================================================================

handle_draw_command() {
  local PLUGIN_DIR="$CONFIG_DIR/plugins/draw"

  case "${1:-}" in
    "install")
      draw_install
      ;;
    "uninstall"|"remove")
      draw_uninstall
      ;;
    "update"|"upgrade")
      draw_uninstall
      draw_install
      ;;
    "help"|"-h"|"--help"|"")
      if [ ! -d "$PLUGIN_DIR" ] || [ ! -f "$PLUGIN_DIR/dist/cli.js" ]; then
        draw_help
      else
        # No args = start new session
        CRAB_PROJECT_ROOT="${REPO_PATH:-$(pwd)}" CRAB_CONFIG_DIR="$CONFIG_DIR" node "$PLUGIN_DIR/dist/cli.js" "$@"
      fi
      ;;
    *)
      # Check if plugin is installed before running
      if [ ! -d "$PLUGIN_DIR" ] || [ ! -f "$PLUGIN_DIR/dist/cli.js" ]; then
        echo -e "${CYAN}Crab Draw - Collaborative Excalidraw${NC}"
        echo ""
        echo "Plugin not installed. Install with:"
        echo ""
        echo -e "  ${GREEN}crab draw install${NC}"
        echo ""
        return 1
      fi
      # Pass all args to the Node.js CLI
      CRAB_PROJECT_ROOT="${REPO_PATH:-$(pwd)}" CRAB_CONFIG_DIR="$CONFIG_DIR" node "$PLUGIN_DIR/dist/cli.js" "$@"
      ;;
  esac
}

draw_install() {
  local PLUGIN_DIR="$CONFIG_DIR/plugins/draw"
  local REPO_URL="https://github.com/promptfoo/crabcode.git"
  local BRANCH="main"

  echo -e "${CYAN}Installing Crab Draw...${NC}"

  # Check for Node.js
  if ! command_exists node; then
    error "Node.js is required. Install with: brew install node"
    return 1
  fi

  # Create plugins directory
  mkdir -p "$CONFIG_DIR/plugins"

  # Clone just the plugin directory using sparse checkout
  local TEMP_DIR=$(mktemp -d)
  echo "Cloning plugin from $REPO_URL..."

  git clone --depth 1 --branch "$BRANCH" --filter=blob:none --sparse "$REPO_URL" "$TEMP_DIR" 2>/dev/null
  cd "$TEMP_DIR"
  git sparse-checkout set plugins/draw

  # Move to final location
  rm -rf "$PLUGIN_DIR"
  mv plugins/draw "$PLUGIN_DIR"
  cd - >/dev/null
  rm -rf "$TEMP_DIR"

  # Install dependencies and build (server + UI)
  echo "Installing dependencies..."
  cd "$PLUGIN_DIR"
  npm install --silent
  echo "Building server..."
  npm run build:server --silent
  echo "Building UI (this may take a moment)..."
  npm run build:ui --silent
  cd - >/dev/null

  success "✓ Installed to $PLUGIN_DIR"
  echo ""
  echo "Usage:"
  echo -e "  ${GREEN}crab draw${NC}                     Start a new drawing session"
  echo -e "  ${GREEN}crab draw --title \"design\"${NC}    Start with a title"
  echo -e "  ${GREEN}crab draw ls${NC}                   List saved sessions"
  echo -e "  ${GREEN}crab draw --help${NC}               Show all options"
}

draw_uninstall() {
  local PLUGIN_DIR="$CONFIG_DIR/plugins/draw"

  if [ -d "$PLUGIN_DIR" ]; then
    rm -rf "$PLUGIN_DIR"
    success "✓ Uninstalled draw plugin"
  else
    echo "Plugin not installed"
  fi
}

draw_help() {
  echo -e "${CYAN}Crab Draw - Collaborative Excalidraw${NC}"
  echo ""
  echo "Open an Excalidraw canvas with real-time collaboration."
  echo "Sessions are saved to your project for team sharing."
  echo ""
  echo -e "${BOLD}Installation:${NC}"
  echo "  crab draw install              Install the plugin"
  echo "  crab draw uninstall            Remove the plugin"
  echo "  crab draw update               Update to latest version"
  echo ""
  echo -e "${BOLD}Usage:${NC}"
  echo "  crab draw                      Start a new session"
  echo "  crab draw --title \"design\"     Start with a title"
  echo "  crab draw <session-id>         Reopen a saved session"
  echo "  crab draw <session-id> --collab  Reopen with sharing"
  echo "  crab draw ls                   List all sessions"
  echo "  crab draw delete <session-id>  Delete a session"
  echo ""
  echo -e "${BOLD}Options:${NC}"
  echo "  --title, -t <name>     Name the session"
  echo "  --collab               Enable sharing tunnel"
  echo "  --no-tunnel            Local only (no sharing)"
  echo "  --tunnel <tool>        Use specific tunnel (cloudflared, ngrok, bore)"
  echo ""
  echo "Sessions are saved to .crab/draw/ and shared via git."
}

# =============================================================================
# Multi-Project Commands
# =============================================================================

# List all registered projects
show_projects() {
  local subcmd="${1:-}"

  if [ "$subcmd" = "rm" ] || [ "$subcmd" = "remove" ] || [ "$subcmd" = "delete" ]; then
    remove_project "${2:-}"
    return
  fi

  echo -e "${CYAN}Registered Projects${NC}"
  echo ""

  if [ ! -d "$PROJECTS_DIR" ] || [ -z "$(ls -A "$PROJECTS_DIR" 2>/dev/null)" ]; then
    echo "  No projects registered."
    echo ""
    echo "  Run 'crab init' to register your first project."
    return
  fi

  # Get default project
  local default_alias=""
  if [ -f "$GLOBAL_CONFIG" ]; then
    default_alias=$(yq -r '.default_project // ""' "$GLOBAL_CONFIG" 2>/dev/null)
    [ "$default_alias" = "null" ] && default_alias=""
  fi

  for f in "$PROJECTS_DIR"/*.yaml; do
    [ -f "$f" ] || continue
    local alias=$(basename "$f" .yaml)
    local repo=$(yq -r '.main_repo // ""' "$f" 2>/dev/null)
    local session=$(yq -r '.session_name // ""' "$f" 2>/dev/null)

    # Check if tmux session is running
    local status_icon="  "
    if tmux has-session -t "$session" 2>/dev/null; then
      status_icon="${GREEN}●${NC} "
    fi

    # Check if default
    local default_marker=""
    if [ "$alias" = "$default_alias" ]; then
      default_marker=" ${YELLOW}(default)${NC}"
    fi

    echo -e "  ${status_icon}${BOLD}@$alias${NC}${default_marker}"
    echo -e "    ${GRAY}$repo${NC}"
  done
  echo ""
}

# Remove a project registration
remove_project() {
  local alias="${1:-}"

  if [ -z "$alias" ]; then
    error "Usage: crab projects rm <alias>"
    return 1
  fi

  # Strip @ prefix if provided
  alias="${alias#@}"

  local project_file="$PROJECTS_DIR/${alias}.yaml"
  if [ ! -f "$project_file" ]; then
    error "No project registered with alias: @$alias"
    return 1
  fi

  echo -e "Remove project registration for ${BOLD}@$alias${NC}?"
  echo -e "  ${GRAY}(This only removes the config, not your workspaces or repo)${NC}"
  read -p "  [y/N]: " confirm
  if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
    echo "Cancelled."
    return
  fi

  rm "$project_file"

  # Clear default if it was the default
  if [ -f "$GLOBAL_CONFIG" ]; then
    local current_default=$(yq -r '.default_project // ""' "$GLOBAL_CONFIG" 2>/dev/null)
    if [ "$current_default" = "$alias" ]; then
      yq -i '.default_project = ""' "$GLOBAL_CONFIG"
      echo -e "  ${YELLOW}Cleared default project (was @$alias)${NC}"
    fi
  fi

  success "Removed project @$alias"
}

# Set or show the default project
set_default_project() {
  local alias="${1:-}"

  if [ -z "$alias" ]; then
    # Show current default
    local current=""
    if [ -f "$GLOBAL_CONFIG" ]; then
      current=$(yq -r '.default_project // ""' "$GLOBAL_CONFIG" 2>/dev/null)
      [ "$current" = "null" ] && current=""
    fi
    if [ -n "$current" ]; then
      echo -e "Default project: ${BOLD}@$current${NC}"
    else
      echo "No default project set."
      echo "Usage: crab default <alias>"
    fi
    return
  fi

  # Strip @ prefix if provided
  alias="${alias#@}"

  local project_file="$PROJECTS_DIR/${alias}.yaml"
  if [ ! -f "$project_file" ]; then
    error "No project registered with alias: @$alias"
    echo "Run 'crab projects' to see registered projects."
    return 1
  fi

  mkdir -p "$CONFIG_DIR"
  if [ -f "$GLOBAL_CONFIG" ]; then
    yq -i ".default_project = \"$alias\"" "$GLOBAL_CONFIG"
  else
    cat > "$GLOBAL_CONFIG" << EOF
# Crabcode Global Configuration
default_project: $alias
EOF
  fi

  success "Default project set to @$alias"
}

# =============================================================================
# Session Management
# =============================================================================

SESSIONS_DIR="$CONFIG_DIR/sessions"

# Get sessions directory for current project
get_sessions_dir() {
  local project="${PROJECT_ALIAS:-default}"
  echo "$SESSIONS_DIR/$project"
}

# Create a new session
# Usage: session_create "name" ["context content"]
session_create() {
  local name="$1"
  local context="${2:-}"
  local sessions_dir=$(get_sessions_dir)
  local session_dir="$sessions_dir/$name"

  if [ -d "$session_dir" ]; then
    error "Session '$name' already exists"
    echo "Use 'crab session resume $name' to continue it"
    return 1
  fi

  mkdir -p "$session_dir"

  # Create session.yaml
  cat > "$session_dir/session.yaml" << EOF
name: $name
project: ${PROJECT_ALIAS:-default}
created: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
last_accessed: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
agent: $(get_agent_type)
agent_session_id: ""
summary: ""
type: general
EOF

  # Write context if provided
  if [ -n "$context" ]; then
    echo "$context" > "$session_dir/context.md"
  fi

  echo "$session_dir"
}

# Update session metadata
# Usage: session_update "name" "field" "value"
session_update() {
  local name="$1"
  local field="$2"
  local value="$3"
  local sessions_dir=$(get_sessions_dir)
  local session_file="$sessions_dir/$name/session.yaml"

  if [ ! -f "$session_file" ]; then
    error "Session '$name' not found"
    return 1
  fi

  yq -i ".$field = \"$value\"" "$session_file"
}

# Get session field
# Usage: session_get "name" "field"
session_get() {
  local name="$1"
  local field="$2"
  local sessions_dir=$(get_sessions_dir)
  local session_file="$sessions_dir/$name/session.yaml"

  if [ ! -f "$session_file" ]; then
    return 1
  fi

  yq -r ".$field // \"\"" "$session_file"
}

# List all sessions with summaries
session_list() {
  local sessions_dir=$(get_sessions_dir)
  local filter="${1:-}"  # Optional filter like "review"

  if [ ! -d "$sessions_dir" ]; then
    echo -e "${GRAY}No sessions yet.${NC}"
    echo "Start one with: crab session start \"name\""
    return
  fi

  local found=false
  local project_label=""
  [ -n "$PROJECT_ALIAS" ] && project_label="@$PROJECT_ALIAS "

  # Header based on filter
  local header="Sessions"
  [[ "$filter" == "review" ]] && header="Reviews"
  echo -e "${BOLD}${project_label}${header}:${NC}"
  echo ""

  for session_dir in "$sessions_dir"/*/; do
    [ -d "$session_dir" ] || continue
    local name=$(basename "$session_dir")
    local session_file="$session_dir/session.yaml"

    [ -f "$session_file" ] || continue

    # Apply filter if provided
    if [ -n "$filter" ]; then
      local type=$(yq -r '.type // "general"' "$session_file")
      [[ "$name" == "$filter"* ]] || [[ "$type" == "$filter" ]] || continue
    fi

    found=true
    local summary=$(yq -r '.summary // ""' "$session_file")
    local last_accessed=$(yq -r '.last_accessed // ""' "$session_file")
    local type=$(yq -r '.type // "general"' "$session_file")

    # Format time ago
    local time_ago=""
    if [ -n "$last_accessed" ] && [ "$last_accessed" != "null" ]; then
      local timestamp=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$last_accessed" "+%s" 2>/dev/null || echo "")
      if [ -n "$timestamp" ]; then
        local now=$(date "+%s")
        local diff=$((now - timestamp))
        if [ $diff -lt 60 ]; then
          time_ago="just now"
        elif [ $diff -lt 3600 ]; then
          time_ago="$((diff / 60))m ago"
        elif [ $diff -lt 86400 ]; then
          time_ago="$((diff / 3600))h ago"
        else
          time_ago="$((diff / 86400))d ago"
        fi
      fi
    fi

    # Format summary
    local summary_display=""
    if [ -n "$summary" ] && [ "$summary" != "null" ]; then
      [ ${#summary} -gt 50 ] && summary="${summary:0:47}..."
      summary_display="\"$summary\""
    else
      summary_display="${GRAY}(no summary)${NC}"
    fi

    # Color based on type
    local type_color="$CYAN"
    [[ "$type" == "general" ]] && type_color="$GRAY"

    # Check if review output exists
    local output_marker=""
    if [ -f "$session_dir/review-output.md" ]; then
      output_marker=" ${GREEN}[saved]${NC}"
    fi

    printf "  ${BOLD}%-20s${NC} ${type_color}%-50b${NC}%b ${GRAY}%s${NC}\n" "$name" "$summary_display" "$output_marker" "$time_ago"
  done

  if [ "$found" = false ]; then
    echo -e "  ${GRAY}(none)${NC}"
  fi
  echo ""
}

# Start a new session and launch agent
session_start() {
  local name="$1"
  local context="${2:-}"

  local session_dir=$(session_create "$name" "$context")
  [ $? -eq 0 ] || return 1

  local agent_name=$(agent_display_name)
  echo -e "${CYAN}Starting session: $name ($agent_name)${NC}"

  local context_file="$session_dir/context.md"
  local base_cmd=$(get_agent_base_cmd)

  if [ -f "$context_file" ]; then
    echo -e "  Context loaded from: $context_file"
  fi

  # Update last accessed
  session_update "$name" "last_accessed" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"

  # Run agent interactively with context
  cd "$(pwd)"
  local agent=$(get_agent_type)
  if [ -f "$context_file" ]; then
    case "$agent" in
      claude) $base_cmd "$context_file" ;;
      codex|*) $base_cmd "$(cat "$context_file" | head -c 4000)" ;;
    esac
  else
    $base_cmd
  fi

  # After agent exits, prompt for summary if empty
  local current_summary=$(session_get "$name" "summary")
  if [ -z "$current_summary" ] || [ "$current_summary" = "null" ]; then
    echo ""
    read -p "Session summary (optional): " summary
    if [ -n "$summary" ]; then
      session_update "$name" "summary" "$summary"
    fi
  fi
}

# Resume an existing session
session_resume() {
  local name="$1"
  local sessions_dir=$(get_sessions_dir)
  local session_dir="$sessions_dir/$name"
  local session_file="$session_dir/session.yaml"

  if [ ! -f "$session_file" ]; then
    error "Session '$name' not found"
    echo "Use 'crab session ls' to see available sessions"
    return 1
  fi

  local session_agent=$(yq -r '.agent // ""' "$session_file" 2>/dev/null)
  case "${session_agent:-}" in
    codex|claude) ;;
    *) session_agent="$(get_agent_type)" ;;
  esac

  echo -e "${CYAN}Resuming session: $name ($(agent_display_name_for_type "$session_agent"))${NC}"

  local summary=$(session_get "$name" "summary")
  [ -n "$summary" ] && [ "$summary" != "null" ] && echo -e "  ${GRAY}$summary${NC}"

  # Show if review output exists
  if [ -f "$session_dir/review-output.md" ]; then
    echo -e "  ${GREEN}Review output saved${NC}"
  fi

  # Update last accessed
  session_update "$name" "last_accessed" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"

  # Resume agent from session directory
  cd "$session_dir"
  local base_cmd=$(agent_base_cmd_for_type "$session_agent")
  local resume_cmd=$(agent_cmd_continue_for_type "$session_agent" "$base_cmd")
  $resume_cmd
}

# Delete a session
session_delete() {
  local name="$1"
  local sessions_dir=$(get_sessions_dir)
  local session_dir="$sessions_dir/$name"

  if [ ! -d "$session_dir" ]; then
    error "Session '$name' not found"
    return 1
  fi

  read -p "Delete session '$name'? [y/N] " confirm
  if [[ "$confirm" =~ ^[Yy]$ ]]; then
    rm -rf "$session_dir"
    success "Deleted session: $name"
  else
    echo "Cancelled."
  fi
}

# Delete all sessions matching a prefix
_delete_all_sessions() {
  local prefix="${1:-}"
  local sessions_dir=$(get_sessions_dir)

  if [ ! -d "$sessions_dir" ]; then
    echo "No sessions found."
    return
  fi

  # Count matching sessions
  local count=0
  local sessions=()
  for session_dir in "$sessions_dir"/*/; do
    [ -d "$session_dir" ] || continue
    local name=$(basename "$session_dir")
    if [ -z "$prefix" ] || [[ "$name" == "$prefix"* ]]; then
      sessions+=("$name")
      ((count++))
    fi
  done

  if [ $count -eq 0 ]; then
    echo "No ${prefix:-}sessions found."
    return
  fi

  echo "Found $count ${prefix}session(s):"
  for name in "${sessions[@]}"; do
    echo "  - $name"
  done
  echo ""

  read -p "Delete all $count session(s)? [y/N] " confirm
  if [[ "$confirm" =~ ^[Yy]$ ]]; then
    for name in "${sessions[@]}"; do
      rm -rf "$sessions_dir/$name"
      echo "  Deleted: $name"
    done
    success "Deleted $count session(s)"
  else
    echo "Cancelled."
  fi
}

# Handle session commands
handle_session_command() {
  local cmd="${1:-ls}"
  shift || true

  case "$cmd" in
    "ls"|"list")
      session_list "$@"
      ;;
    "start"|"new")
      local name="${1:-}"
      if [ -z "$name" ]; then
        read -p "Session name: " name
        [ -z "$name" ] && { error "Name required"; return 1; }
      fi
      session_start "$name"
      ;;
    "resume"|"continue")
      local name="${1:-}"
      if [ -z "$name" ]; then
        error "Specify session name: crab session resume <name>"
        return 1
      fi
      session_resume "$name"
      ;;
    "delete"|"rm")
      local name="${1:-}"
      if [ -z "$name" ]; then
        error "Specify session name: crab session delete <name>"
        return 1
      fi
      session_delete "$name"
      ;;
    "summary")
      local name="${1:-}"
      local summary="${2:-}"
      if [ -z "$name" ]; then
        error "Usage: crab session summary <name> \"summary text\""
        return 1
      fi
      if [ -z "$summary" ]; then
        read -p "Summary: " summary
      fi
      session_update "$name" "summary" "$summary"
      success "Updated summary for: $name"
      ;;
    *)
      error "Unknown session command: $cmd"
      echo "Usage: crab session [ls|start|resume|delete|summary]"
      return 1
      ;;
  esac
}

# =============================================================================
# Review Commands (Sugar for Sessions)
# =============================================================================

# Parse PR identifier: number, repo#number, or full URL
# Returns: owner repo number
parse_pr_identifier() {
  local pr_id="$1"
  local owner=""
  local repo=""
  local number=""

  if [[ "$pr_id" =~ ^https://github\.com/([^/]+)/([^/]+)/pull/([0-9]+) ]]; then
    # Full URL
    owner="${BASH_REMATCH[1]}"
    repo="${BASH_REMATCH[2]}"
    number="${BASH_REMATCH[3]}"
  elif [[ "$pr_id" =~ ^([^#]+)#([0-9]+)$ ]]; then
    # repo#number format (submodule)
    repo="${BASH_REMATCH[1]}"
    number="${BASH_REMATCH[2]}"
    # Try to get owner from git remote
    owner=$(git remote get-url origin 2>/dev/null | sed -n 's|.*github\.com[:/]\([^/]*\)/.*|\1|p')
  elif [[ "$pr_id" =~ ^[0-9]+$ ]]; then
    # Just a number - use current project's main repo
    number="$pr_id"
    if [ -n "$MAIN_REPO" ]; then
      local remote_url=$(cd "$MAIN_REPO" && git remote get-url origin 2>/dev/null)
      if [[ "$remote_url" =~ github\.com[:/]([^/]+)/([^/.]+) ]]; then
        owner="${BASH_REMATCH[1]}"
        repo="${BASH_REMATCH[2]}"
      fi
    fi
  fi

  echo "$owner $repo $number"
}

# Get court review instructions for the judge pattern
get_court_instructions() {
  cat << 'COURT_EOF'

## Court Review Protocol

You are **Codex acting as the JUDGE** in a code review court. You will orchestrate two independent review passes and deliver a final verdict.

### Your Role as Judge:
- **Orchestrate** the review process
- **Verify** all findings by tracing to actual code
- **Investigate** when reviewers disagree
- **Synthesize** a final verdict with zero false positives

### Phase 1: Empanel the Reviewers

Run two independent review passes:

**Reviewer A (Teammate Review):**
```
Use your available teammate/subagent capability if present. If you do not have one, do a separate first-pass review yourself and label it Reviewer A.

Review brief:
You are Reviewer A. Review this PR for bugs, security issues, and code quality. Be thorough but avoid false positives. Structure findings as Critical/Warning/Suggestion with file:line references.

Beyond standard review, pay special attention to these high-risk patterns:

1. **Called-but-not-changed code**: When the diff introduces a new call to existing code (especially data mutation), use the Read tool to trace INTO the called function. Verify its return value and side effects match what the caller assumes. Do not trust functions you haven't read.

2. **Mutations on read paths**: Flag any write operation (DB update, cache mutation, queue publish) triggered by a GET/read endpoint. Fire-and-forget patterns that silently mutate data are high risk — the failure mode is silent data corruption with no error signal.

3. **Blast radius from initial state**: When new code processes existing records conditionally (e.g. 'refresh if stale'), check the initial state of existing data. If a column starts as null/default for all existing rows, the new path hits EVERY record on first access — that is a mass data rewrite disguised as normal traffic.

4. **Data contract completeness at merge boundaries**: When code spreads/merges one data source onto another (e.g. {...existing, ...fresh}), verify the fresh source returns ALL expected fields. Missing fields silently zero out or null existing data.

Here is the context: [include PR diff]
```

**Reviewer B (Secondary Codex Pass):**
```
Run a separate Codex pass via shell from the session directory:

codex exec --skip-git-repo-check -C . "Read ./context.md, then review this PR for bugs, security issues, and quality problems. Structure findings as Critical/Warning/Suggestion with file:line references.

Beyond standard review, check for these high-risk patterns:
1. Called-but-not-changed code: If the diff calls existing functions (especially data mutation), verify those functions return/do what the caller assumes.
2. Mutations on read paths: Flag any DB write or cache mutation triggered by a GET/read endpoint. Fire-and-forget data rewrites are high risk.
3. Blast radius from initial state: If new code processes records conditionally (refresh if stale), check if null/default initial state means ALL existing records get hit at once.
4. Data contract completeness: When code spreads/merges data sources ({...existing, ...fresh}), verify the fresh source returns ALL expected fields. Missing fields silently zero out existing data.
```

Wait for BOTH review passes to complete before proceeding.

### Phase 2: Collect Testimony

Document what each reviewer found:
- List Reviewer A's findings
- List Reviewer B's findings
- Note agreements and disagreements

### Phase 3: Verify & Investigate

For EACH finding from either reviewer:

1. **Trace to code**: Use Read tool to examine the actual file and line
2. **Verify the issue**: Is this a real problem or false positive?
3. **Check context**: Does surrounding code explain/mitigate it?
4. **When reviewers disagree**: Investigate deeper, examine edge cases

**Judge's Own Investigation** (do this even if neither reviewer flagged it):
- For any new call path to existing code that mutates data, Read the called function and verify its contract
- If you see a write/update triggered by a read endpoint, flag it
- If you see conditional processing of existing records (e.g. "refresh if stale"), check the initial state — does null/default mean all records get hit at once?
- If you see object spread/merge of data sources, verify field completeness of the source

**Critical Rule**: Do NOT include any finding you haven't personally verified in the code.

### Phase 4: Deliberate

For each potential issue, document:
- What was claimed
- What you found when you traced the code
- Your ruling: CONFIRMED / DISMISSED / NEEDS-INVESTIGATION
- Reasoning

### Phase 5: Final Verdict

Write `review-output.md` with:

```markdown
# Court Review: PR #XXXX

## Verdict Summary
[1-2 sentence overall assessment]

## Confirmed Issues
[Issues verified by tracing code - include file:line and evidence]

## Dismissed Claims
[What reviewers flagged but you determined were false positives - explain why]

## Recommendations
[Actionable next steps]

## Court Record
[Summary of the review process, who found what, how disagreements were resolved]
```

### Rules of the Court:
1. **No false positives**: Only confirm what you've verified in the code
2. **Show your work**: Document how you verified each finding
3. **Be thorough**: Check edge cases, error paths, security implications
4. **Cite evidence**: Every confirmed issue needs file:line proof
5. **Explain dismissals**: If you reject a reviewer's finding, say why
COURT_EOF
}

# Fetch PR data using gh CLI
# mode: "standard" (default) or "court"
fetch_pr_data() {
  local owner="$1"
  local repo="$2"
  local number="$3"
  local mode="${4:-standard}"

  if ! command_exists gh; then
    error "gh CLI required for PR reviews"
    echo "Install: brew install gh"
    return 1
  fi

  local full_repo="$owner/$repo"

  echo -e "${CYAN}Fetching PR #$number from $full_repo...${NC}"

  # Get PR metadata
  local pr_json=$(gh pr view "$number" --repo "$full_repo" --json title,body,headRefName,baseRefName,additions,deletions,changedFiles,url 2>/dev/null)
  if [ -z "$pr_json" ]; then
    error "Could not fetch PR #$number from $full_repo"
    return 1
  fi

  local title=$(echo "$pr_json" | jq -r '.title')
  local additions=$(echo "$pr_json" | jq -r '.additions')
  local deletions=$(echo "$pr_json" | jq -r '.deletions')
  local files=$(echo "$pr_json" | jq -r '.changedFiles')
  local url=$(echo "$pr_json" | jq -r '.url')

  echo -e "  Title: ${BOLD}$title${NC}"
  echo -e "  Files: $files (+$additions / -$deletions)"
  echo ""

  # Get diff and file list
  local diff=$(gh pr diff "$number" --repo "$full_repo" 2>/dev/null)
  local file_list=$(gh pr diff "$number" --repo "$full_repo" --name-only 2>/dev/null)

  # Get instructions based on mode
  local instructions=""
  if [ "$mode" = "court" ]; then
    instructions=$(get_court_instructions)
  else
    instructions="## Instructions
Review this PR. Analyze the changes and:
1. First, propose logical review areas (group related files)
2. Wait for my approval on the areas
3. Then review each area, providing specific findings with file paths and line numbers
4. Generate a summary with actionable feedback
5. **IMPORTANT:** When done, save your complete review findings to \`review-output.md\` in the current directory. Include all issues found, suggestions, and your overall assessment."
  fi

  # Return as structured output
  cat << EOF
# PR Review Context

## PR Information
- **Number:** #$number
- **Repository:** $full_repo
- **Title:** $title
- **URL:** $url
- **Changes:** $files files (+$additions / -$deletions)

## Files Changed
\`\`\`
$file_list
\`\`\`

$instructions

## Diff
\`\`\`diff
$diff
\`\`\`
EOF
}

# Start a new review - interactive mode
review_new_interactive() {
  echo -e "${BOLD}Creating new review session${NC}"
  echo ""

  # Get session name
  local name=""
  read -p "Session name: " name
  [ -z "$name" ] && { error "Name required"; return 1; }

  # Collect PRs
  local prs=()
  echo ""
  echo "Add PRs (empty line when done):"
  while true; do
    read -p "> " pr_input
    [ -z "$pr_input" ] && break
    prs+=("$pr_input")
  done

  if [ ${#prs[@]} -eq 0 ]; then
    error "At least one PR required"
    return 1
  fi

  # Collect context
  echo ""
  echo "Context (optional, empty line when done):"
  local context_lines=()
  while true; do
    read -p "> " context_line
    [ -z "$context_line" ] && break
    context_lines+=("$context_line")
  done

  # Build context document
  local context="# Review Session: $name"$'\n\n'

  # Add user context if provided
  if [ ${#context_lines[@]} -gt 0 ]; then
    context+="## Context"$'\n'
    for line in "${context_lines[@]}"; do
      context+="$line"$'\n'
    done
    context+=$'\n'
  fi

  # Fetch and append PR data
  context+="## Pull Requests"$'\n\n'

  for pr_id in "${prs[@]}"; do
    local parsed=($(parse_pr_identifier "$pr_id"))
    local owner="${parsed[0]}"
    local repo="${parsed[1]}"
    local number="${parsed[2]}"

    if [ -z "$number" ]; then
      warn "Could not parse PR: $pr_id"
      continue
    fi

    local pr_data=$(fetch_pr_data "$owner" "$repo" "$number")
    context+="$pr_data"$'\n\n'
    context+="---"$'\n\n'
  done

  # Create session
  local sessions_dir=$(get_sessions_dir)
  local session_dir="$sessions_dir/$name"
  mkdir -p "$session_dir"

  # Write session metadata
  cat > "$session_dir/session.yaml" << EOF
name: $name
project: ${PROJECT_ALIAS:-default}
created: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
last_accessed: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
agent: $(get_agent_type)
agent_session_id: ""
summary: ""
type: review
prs:
$(for pr in "${prs[@]}"; do echo "  - $pr"; done)
EOF

  # Write context
  echo "$context" > "$session_dir/context.md"

  echo ""
  echo -e "${GREEN}Review session created: $name${NC}"
  echo -e "Context written to: $session_dir/context.md"
  echo ""

  # Start agent from session directory so it can write review-output.md there
  local agent_name=$(agent_display_name)
  read -p "Start $agent_name now? [Y/n] " start_now
  if [[ ! "$start_now" =~ ^[Nn]$ ]]; then
    session_update "$name" "last_accessed" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
    cd "$session_dir"
    local base_cmd=$(get_agent_base_cmd)
    local agent=$(get_agent_type)
    case "$agent" in
      claude) $base_cmd "context.md" ;;
      codex|*) $base_cmd "$(cat context.md | head -c 4000)" ;;
    esac

    # Prompt for summary after agent exits
    _prompt_review_summary "$name"
  fi
}

# Prompt for review summary with auto-generate option
_prompt_review_summary() {
  local name="$1"
  echo ""
  local agent_name=$(agent_display_name)
  echo "Save a summary for this review?"
  echo "  [a] Auto-generate ($agent_name summarizes)"
  echo "  [m] Manual (type your own)"
  echo "  [s] Skip"
  read -p "Choice [a/m/s]: " choice

  case "$choice" in
    a|A|"")
      echo -e "${CYAN}Generating summary...${NC}"
      local summary=$(agent_generate_summary "Summarize this review session in ONE short line (under 60 chars). Format: '<main finding/status> - <key detail>'. Example: 'Found 3 issues - N+1 query, missing index, race condition'. Just output the summary, nothing else.")
      if [ -n "$summary" ]; then
        # Clean up the summary (remove quotes if present)
        summary="${summary#\"}"
        summary="${summary%\"}"
        session_update "$name" "summary" "$summary"
        echo -e "Summary: ${GREEN}$summary${NC}"
      else
        echo -e "${YELLOW}Could not generate summary${NC}"
      fi
      ;;
    m|M)
      read -p "Summary: " summary
      if [ -n "$summary" ]; then
        session_update "$name" "summary" "$summary"
        success "Summary saved"
      fi
      ;;
    *)
      echo "Skipped"
      ;;
  esac
}

# Quick review - single PR (standard single-agent review)
review_quick() {
  local pr_id="$1"

  local parsed=($(parse_pr_identifier "$pr_id"))
  local owner="${parsed[0]}"
  local repo="${parsed[1]}"
  local number="${parsed[2]}"

  if [ -z "$number" ]; then
    error "Could not parse PR identifier: $pr_id"
    echo "Formats: 3230, repo#456, https://github.com/.../pull/123"
    return 1
  fi

  local name="review-${repo:-pr}-$number"

  # Check if session already exists
  local sessions_dir=$(get_sessions_dir)
  if [ -d "$sessions_dir/$name" ]; then
    echo -e "${YELLOW}Review session already exists: $name${NC}"
    read -r -p "Resume it? [Y/n] " resume
    if ! is_negative_response "$resume"; then
      session_resume "$name"
      return
    fi
    read -r -p "Start fresh? (deletes existing session) [Y/n] " fresh
    if is_negative_response "$fresh"; then
      return 1
    fi
    rm -rf "$sessions_dir/$name"
    echo -e "${GREEN}Deleted existing session. Starting fresh...${NC}"
  fi

  # Fetch PR data
  local context=$(fetch_pr_data "$owner" "$repo" "$number" "standard")
  [ $? -eq 0 ] || return 1

  # Create session
  local session_dir="$sessions_dir/$name"
  mkdir -p "$session_dir"

  cat > "$session_dir/session.yaml" << EOF
name: $name
project: ${PROJECT_ALIAS:-default}
created: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
last_accessed: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
agent: $(get_agent_type)
agent_session_id: ""
summary: ""
type: review
prs:
  - $pr_id
EOF

  echo "$context" > "$session_dir/context.md"

  echo -e "${GREEN}Review session created: $name${NC}"
  echo ""

  # Start agent from session directory so it can write review-output.md there
  session_update "$name" "last_accessed" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
  cd "$session_dir"
  local base_cmd=$(get_agent_base_cmd)
  local agent=$(get_agent_type)
  case "$agent" in
    claude) $base_cmd "context.md" ;;
    codex|*) $base_cmd "$(cat context.md | head -c 4000)" ;;
  esac

  # Prompt for summary after agent exits
  _prompt_review_summary "$name"
}

# Show fun court intro animation
_show_court_intro() {
  # 256-color orange/coral gradient tones
  local C1='\033[38;5;209m'  # coral
  local C2='\033[38;5;208m'  # orange
  local C3='\033[38;5;214m'  # light orange
  local C4='\033[38;5;215m'  # peach
  local DIM='\033[2m'
  local RST='\033[0m'

  echo ""
  # FIGlet-style banner - "CRAB COURT" (no box - cleaner look)
  echo -e "${C1}    ██████╗██████╗  █████╗ ██████╗      ██████╗ ██████╗ ██╗   ██╗██████╗ ████████╗${RST}"
  echo -e "${C2}   ██╔════╝██╔══██╗██╔══██╗██╔══██╗    ██╔════╝██╔═══██╗██║   ██║██╔══██╗╚══██╔══╝${RST}"
  echo -e "${C2}   ██║     ██████╔╝███████║██████╔╝    ██║     ██║   ██║██║   ██║██████╔╝   ██║   ${RST}"
  echo -e "${C3}   ██║     ██╔══██╗██╔══██║██╔══██╗    ██║     ██║   ██║██║   ██║██╔══██╗   ██║   ${RST}"
  echo -e "${C3}   ╚██████╗██║  ██║██║  ██║██████╔╝    ╚██████╗╚██████╔╝╚██████╔╝██║  ██║   ██║   ${RST}"
  echo -e "${C4}    ╚═════╝╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝      ╚═════╝ ╚═════╝  ╚═════╝ ╚═╝  ╚═╝   ╚═╝   ${RST}"
  echo ""
  # Crab judge with scales
  echo -e "                                      ${C2}\\___/${RST}"
  echo -e "                                     ${C2}( °_°)${RST}"
  echo -e "                                    ${C3}/)" " "${C1}⚖️" " "${C3}(\\\\${RST}"
  echo -e "                                   ${C4}<       >${RST}"
  echo ""
  echo -e "                              ${DIM}\"Order in the court!\"${RST}"
  echo ""
  echo -e "                  ${C2}┌─────────────┐${RST}           ${C3}┌─────────────┐${RST}"
  echo -e "                  ${C2}│  Teammate   │${RST}    vs     ${C3}│    Codex    │${RST}"
  echo -e "                  ${C2}│  Reviewer A │${RST}           ${C3}│  Reviewer B │${RST}"
  echo -e "                  ${C2}└─────────────┘${RST}           ${C3}└─────────────┘${RST}"
  echo ""
  echo -e "  ${DIM}How this works:${RST}"
  echo ""
  echo -e "    ${C1}1.${RST} Empanel      ${DIM}Judge spawns two independent reviewers${RST}"
  echo -e "    ${C2}2.${RST} Review       ${DIM}Both reviewers analyze the PR separately${RST}"
  echo -e "    ${C3}3.${RST} Collect      ${DIM}Judge gathers findings from both${RST}"
  echo -e "    ${C4}4.${RST} Verify       ${DIM}Judge traces each claim to actual code${RST}"
  echo -e "    ${C1}5.${RST} Deliberate   ${DIM}Resolve disagreements, rule on each issue${RST}"
  echo -e "    ${C2}6.${RST} Verdict      ${DIM}Final ruling with zero false positives${RST}"
  echo ""
  echo -e "  ${CYAN}Fetching PR data...${NC}"
  echo ""
}

# Court review - Codex judge plus independent reviewer passes
review_court() {
  local pr_id="$1"

  # Immediate feedback
  echo -e "${CYAN}Preparing court review for: $pr_id${NC}"
  echo ""

  # Check if codex CLI is available
  if ! command_exists codex; then
    error "Codex CLI required for crab court. Install with: npm install -g @openai/codex"
    return 1
  fi

  local parsed=($(parse_pr_identifier "$pr_id"))
  local owner="${parsed[0]}"
  local repo="${parsed[1]}"
  local number="${parsed[2]}"

  if [ -z "$number" ]; then
    error "Could not parse PR identifier: $pr_id"
    echo "Formats: 3230, repo#456, https://github.com/.../pull/123"
    return 1
  fi

  local name="court-${repo:-pr}-$number"

  # Check if session already exists
  local sessions_dir=$(get_sessions_dir)
  if [ -d "$sessions_dir/$name" ]; then
    echo -e "${YELLOW}Court review session already exists: $name${NC}"
    read -r -p "Resume it? [Y/n] " resume
    if ! is_negative_response "$resume"; then
      session_resume "$name"
      return
    fi
    read -r -p "Start fresh? (deletes existing session) [Y/n] " fresh
    if is_negative_response "$fresh"; then
      return 1
    fi
    rm -rf "$sessions_dir/$name"
    echo -e "${GREEN}Deleted existing session. Starting fresh...${NC}"
  fi

  # Show the court intro while fetching
  _show_court_intro

  # Fetch PR data with court instructions (this can take a moment for large PRs)
  local context=$(fetch_pr_data "$owner" "$repo" "$number" "court")
  [ $? -eq 0 ] || return 1

  # Create session
  local session_dir="$sessions_dir/$name"
  mkdir -p "$session_dir"

  cat > "$session_dir/session.yaml" << EOF
name: $name
project: ${PROJECT_ALIAS:-default}
created: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
last_accessed: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
agent: codex
agent_session_id: ""
summary: ""
type: court
prs:
  - $pr_id
EOF

  echo "$context" > "$session_dir/context.md"

  echo -e "${GREEN}Court review session created: $name${NC}"
  echo ""

  # Start Codex as the judge
  session_update "$name" "last_accessed" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
  cd "$session_dir"
  local judge_cmd=$(agent_base_cmd_for_type "codex")
  $judge_cmd "$(cat context.md | head -c 4000)"

  # Prompt for summary after the judge exits
  _prompt_review_summary "$name"
}

# Handle review commands
handle_review_command() {
  local cmd="${1:-}"
  shift || true

  case "$cmd" in
    "")
      error "Usage: crab review <PR> or crab court <PR>"
      echo ""
      echo "Commands:"
      echo "  crab review <PR>       Quick single-agent review"
      echo "  crab court <PR>        Court review (Judge + 2 reviewers, thorough)"
      echo "  crab review new        Interactive mode (multiple PRs + context)"
      echo "  crab review ls         List review sessions"
      echo "  crab review resume     Resume a review"
      echo "  crab review show       View saved review output"
      echo "  crab review delete     Delete a review session"
      return 1
      ;;
    "new")
      review_new_interactive
      ;;
    "ls"|"list")
      session_list "review"
      ;;
    "resume"|"continue")
      local name="${1:-}"
      if [ -z "$name" ]; then
        error "Specify review to resume"
        echo "Use 'crab review ls' to see reviews"
        return 1
      fi
      # Handle both "review-xxx" and just the PR number/name
      if [[ ! "$name" == review-* ]]; then
        local parsed=($(parse_pr_identifier "$name"))
        local repo="${parsed[1]}"
        local number="${parsed[2]}"
        name="review-${repo:-pr}-$number"
      fi
      session_resume "$name"
      ;;
    "delete"|"rm")
      local name="${1:-}"
      if [ -z "$name" ]; then
        error "Specify review to delete (or 'all' to delete all)"
        return 1
      fi
      if [ "$name" = "all" ]; then
        _delete_all_sessions "review"
      else
        if [[ ! "$name" == review-* ]] && [[ ! "$name" == court-* ]]; then
          local parsed=($(parse_pr_identifier "$name"))
          local repo="${parsed[1]}"
          local number="${parsed[2]}"
          name="review-${repo:-pr}-$number"
        fi
        session_delete "$name"
      fi
      ;;
    "show"|"view"|"output")
      local name="${1:-}"
      if [ -z "$name" ]; then
        error "Specify review to view"
        echo "Usage: crab review show <name>"
        return 1
      fi
      if [[ ! "$name" == review-* ]]; then
        local parsed=($(parse_pr_identifier "$name"))
        local repo="${parsed[1]}"
        local number="${parsed[2]}"
        name="review-${repo:-pr}-$number"
      fi
      local sessions_dir=$(get_sessions_dir)
      local output_file="$sessions_dir/$name/review-output.md"
      if [ -f "$output_file" ]; then
        echo -e "${BOLD}Review output for: $name${NC}"
        echo ""
        cat "$output_file"
      else
        error "No saved review output for '$name'"
        echo "The review hasn't been saved yet. Resume the review and ask the active agent to save findings."
      fi
      ;;
    *)
      # Assume it's a PR identifier
      review_quick "$cmd"
      ;;
  esac
}

# Handle court command (shortcut for crab court <PR>)
# Court review - interactive mode for multiple PRs
court_new_interactive() {
  echo "Creating new court review session"
  echo ""

  # Get session name
  local name=""
  read -p "Session name: " name
  [ -z "$name" ] && { error "Name required"; return 1; }
  name="court-$name"

  # Check if codex CLI is available
  if ! command_exists codex; then
    error "Codex CLI required for crab court. Install with: npm install -g @openai/codex"
    return 1
  fi

  # Collect PRs
  local prs=()
  echo ""
  echo "Add PRs (empty line when done):"
  while true; do
    read -p "> " pr_input
    [ -z "$pr_input" ] && break
    prs+=("$pr_input")
  done

  if [ ${#prs[@]} -eq 0 ]; then
    error "At least one PR required"
    return 1
  fi

  # Collect context
  echo ""
  echo "Additional context (optional, empty line when done):"
  local context_lines=()
  while true; do
    read -p "> " context_line
    [ -z "$context_line" ] && break
    context_lines+=("$context_line")
  done

  # Show the court intro
  _show_court_intro

  # Build context document with court instructions
  local court_instructions=$(get_court_instructions)
  local context="# Court Review Session: $name"$'\n\n'
  context+="$court_instructions"$'\n\n'

  # Add user context if provided
  if [ ${#context_lines[@]} -gt 0 ]; then
    context+="## Additional Context"$'\n'
    for line in "${context_lines[@]}"; do
      context+="$line"$'\n'
    done
    context+=$'\n'
  fi

  # Fetch and append PR data
  context+="## Pull Requests"$'\n\n'

  for pr_id in "${prs[@]}"; do
    local parsed=($(parse_pr_identifier "$pr_id"))
    local owner="${parsed[0]}"
    local repo="${parsed[1]}"
    local number="${parsed[2]}"

    if [ -z "$number" ]; then
      warn "Could not parse PR: $pr_id"
      continue
    fi

    # Fetch PR data without court instructions (we already added them above)
    local pr_data=$(fetch_pr_data "$owner" "$repo" "$number" "standard")
    context+="$pr_data"$'\n\n'
    context+="---"$'\n\n'
  done

  # Create session
  local sessions_dir=$(get_sessions_dir)
  local session_dir="$sessions_dir/$name"
  mkdir -p "$session_dir"

  # Write session metadata
  cat > "$session_dir/session.yaml" << EOF
name: $name
project: ${PROJECT_ALIAS:-default}
created: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
last_accessed: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
agent: codex
agent_session_id: ""
summary: ""
type: court
prs:
$(for pr in "${prs[@]}"; do echo "  - $pr"; done)
EOF

  # Write context
  echo "$context" > "$session_dir/context.md"

  echo ""
  echo -e "${GREEN}Court review session created: $name${NC}"
  echo ""

  # Start Codex as the judge
  read -p "Start court session now? [Y/n] " start_now
  if [[ ! "$start_now" =~ ^[Nn]$ ]]; then
    session_update "$name" "last_accessed" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
    cd "$session_dir"
    local judge_cmd=$(agent_base_cmd_for_type "codex")
    $judge_cmd "$(cat context.md | head -c 4000)"

    # Prompt for summary after the judge exits
    _prompt_review_summary "$name"
  fi
}

# Handle court command
handle_court_command() {
  local cmd="${1:-}"
  shift 2>/dev/null || true

  case "$cmd" in
    "")
      error "Usage: crab court <PR> or crab court new"
      echo ""
      echo "Court review: thorough multi-agent review with:"
      echo "  - Judge (Codex) - orchestrates, verifies, delivers verdict"
      echo "  - Reviewer A (teammate/independent pass) - parallel review"
      echo "  - Reviewer B (Codex) - secondary Codex review pass"
      echo ""
      echo "Commands:"
      echo "  crab court <PR>        Quick court review for single PR"
      echo "  crab court new         Interactive mode (multiple PRs + context)"
      echo "  crab court ls          List court sessions"
      echo "  crab court resume      Resume a court session"
      echo "  crab court show        View saved court output"
      echo "  crab court delete      Delete a court session"
      return 1
      ;;
    "new")
      court_new_interactive
      ;;
    "ls"|"list")
      session_list "court"
      ;;
    "resume"|"continue")
      local name="${1:-}"
      if [ -z "$name" ]; then
        error "Specify court session to resume"
        echo "Use 'crab court ls' to see sessions"
        return 1
      fi
      # Handle both "court-xxx" and just the name
      if [[ ! "$name" == court-* ]]; then
        name="court-$name"
      fi
      session_resume "$name"
      ;;
    "show"|"view"|"output")
      local name="${1:-}"
      if [ -z "$name" ]; then
        error "Specify court session to view"
        echo "Usage: crab court show <name>"
        return 1
      fi
      if [[ ! "$name" == court-* ]]; then
        name="court-$name"
      fi
      local sessions_dir=$(get_sessions_dir)
      local output_file="$sessions_dir/$name/review-output.md"
      if [ -f "$output_file" ]; then
        echo "Court verdict for: $name"
        echo ""
        cat "$output_file"
      else
        error "No saved verdict for '$name'"
        echo "Resume the court session and have the judge deliver a verdict."
      fi
      ;;
    "delete"|"rm")
      local name="${1:-}"
      if [ -z "$name" ]; then
        error "Specify court session to delete (or 'all' to delete all)"
        return 1
      fi
      if [ "$name" = "all" ]; then
        _delete_all_sessions "court"
      else
        if [[ ! "$name" == court-* ]]; then
          name="court-$name"
        fi
        session_delete "$name"
      fi
      ;;
    *)
      # Assume it's a PR identifier
      review_court "$cmd"
      ;;
  esac
}

# =============================================================================
# Markdown Renderer
# =============================================================================

MD_ASSETS_VERSION="1"
MD_ASSETS_DIR="$CONFIG_DIR/assets/md"

COMPARE_ASSETS_VERSION="1"
COMPARE_ASSETS_DIR="$CONFIG_DIR/assets/compare"

compare_show_help() {
  echo -e "${CYAN}File Compare:${NC}"
  echo ""
  echo "Usage:"
  echo "  crab compare <file1> <file2>    Compare two files in browser"
  echo "  crab compare --update-assets    Re-download rendering libraries"
  echo ""
  echo "Opens a beautiful side-by-side diff view in your default browser."
  echo "Supports syntax highlighting and both unified and side-by-side modes."
  echo "Supports PDF files (requires pdftotext from poppler)."
  echo ""
  echo -e "${GRAY}Assets stored in: $COMPARE_ASSETS_DIR${NC}"
}

compare_is_pdf() {
  local file="$1"
  local ext="${file##*.}"
  [[ "$ext" =~ ^[Pp][Dd][Ff]$ ]]
}

compare_pdf_to_text() {
  local pdf_file="$1"
  local txt_file="$2"

  if command_exists pdftotext; then
    pdftotext -layout "$pdf_file" "$txt_file" 2>/dev/null
    return $?
  else
    error "PDF comparison requires 'pdftotext' (from poppler)."
    echo ""
    if [[ "$OSTYPE" == "darwin"* ]]; then
      echo -e "  Install with: ${BOLD}brew install poppler${NC}"
    elif command_exists apt-get; then
      echo -e "  Install with: ${BOLD}sudo apt-get install poppler-utils${NC}"
    elif command_exists dnf; then
      echo -e "  Install with: ${BOLD}sudo dnf install poppler-utils${NC}"
    else
      echo -e "  Install the 'poppler-utils' package for your system."
    fi
    return 1
  fi
}

compare_ensure_poppler() {
  if command_exists pdftotext; then
    return 0
  fi

  echo -e "${CYAN}Installing poppler (for PDF comparison support)...${NC}"
  if [[ "$OSTYPE" == "darwin"* ]] && command_exists brew; then
    brew install poppler || { error "Failed to install poppler via brew."; return 1; }
  elif command_exists apt-get; then
    sudo apt-get install -y poppler-utils || { error "Failed to install poppler-utils via apt."; return 1; }
  elif command_exists dnf; then
    sudo dnf install -y poppler-utils || { error "Failed to install poppler-utils via dnf."; return 1; }
  else
    error "Could not auto-install poppler. Please install 'poppler-utils' manually."
    return 1
  fi
  echo -e "${GREEN}poppler installed${NC}"
}

compare_ensure_assets() {
  local version_file="$COMPARE_ASSETS_DIR/.version"

  if [ -f "$version_file" ] && [ "$(cat "$version_file")" = "$COMPARE_ASSETS_VERSION" ]; then
    return 0
  fi

  echo -e "${CYAN}Downloading compare rendering assets (one-time setup)...${NC}"
  mkdir -p "$COMPARE_ASSETS_DIR"

  local base="https://cdn.jsdelivr.net/npm"
  local failed=0

  curl -fsSL "$base/diff2html@3/bundles/css/diff2html.min.css" -o "$COMPARE_ASSETS_DIR/diff2html.min.css" || failed=1
  curl -fsSL "$base/diff2html@3/bundles/js/diff2html-ui.min.js" -o "$COMPARE_ASSETS_DIR/diff2html-ui.min.js" || failed=1

  if [ "$failed" -eq 1 ]; then
    error "Failed to download assets. Check your internet connection."
    rm -rf "$COMPARE_ASSETS_DIR"
    return 1
  fi

  # Ensure poppler is available for PDF support
  compare_ensure_poppler

  echo "$COMPARE_ASSETS_VERSION" > "$version_file"
  echo -e "${GREEN}Assets downloaded${NC}"
}

cmd_compare() {
  local file1="${1:-}"
  local file2="${2:-}"

  case "$file1" in
    ""|"-h"|"--help"|"help")
      compare_show_help
      return 0
      ;;
    "--update-assets"|"--update")
      rm -rf "$COMPARE_ASSETS_DIR"
      compare_ensure_assets
      local rc=$?
      compare_ensure_poppler
      return $rc
      ;;
  esac

  if [ -z "$file2" ]; then
    error "Two file paths required."
    echo ""
    compare_show_help
    return 1
  fi

  # Resolve to absolute paths
  if [[ "$file1" != /* ]]; then
    file1="$(pwd)/$file1"
  fi
  if [[ "$file2" != /* ]]; then
    file2="$(pwd)/$file2"
  fi

  if [ ! -f "$file1" ]; then
    error "File not found: $file1"
    return 1
  fi
  if [ ! -f "$file2" ]; then
    error "File not found: $file2"
    return 1
  fi

  # PDF handling
  local pdf1=false pdf2=false
  local cleanup_files=()
  compare_is_pdf "$file1" && pdf1=true
  compare_is_pdf "$file2" && pdf2=true

  if [ "$pdf1" = true ] && [ "$pdf2" = false ] || [ "$pdf1" = false ] && [ "$pdf2" = true ]; then
    error "Cannot compare a PDF with a non-PDF file. Both files must be the same type."
    return 1
  fi

  local diff_file1="$file1"
  local diff_file2="$file2"

  if [ "$pdf1" = true ] && [ "$pdf2" = true ]; then
    local tmp_txt1="/tmp/crab-pdf1-$$.txt"
    local tmp_txt2="/tmp/crab-pdf2-$$.txt"
    cleanup_files+=("$tmp_txt1" "$tmp_txt2")

    echo -e "${CYAN}Extracting text from PDFs...${NC}"
    compare_pdf_to_text "$file1" "$tmp_txt1" || { rm -f "${cleanup_files[@]}"; return 1; }
    compare_pdf_to_text "$file2" "$tmp_txt2" || { rm -f "${cleanup_files[@]}"; return 1; }

    if [ ! -s "$tmp_txt1" ] && [ ! -s "$tmp_txt2" ]; then
      echo -e "${YELLOW}Warning: Both PDFs produced empty text. They may be image-based (scanned) PDFs.${NC}"
      rm -f "${cleanup_files[@]}"
      return 1
    fi

    diff_file1="$tmp_txt1"
    diff_file2="$tmp_txt2"
  fi

  compare_ensure_assets || { rm -f "${cleanup_files[@]}"; return 1; }

  local name1 name2
  name1=$(basename "$file1")
  name2=$(basename "$file2")
  local tmp_html="/tmp/crab-compare-${name1%.*}-${name2%.*}-$$.html"

  # Generate unified diff (don't fail on differences)
  local diff_output
  diff_output=$(diff -u --label "$name1" --label "$name2" "$diff_file1" "$diff_file2" 2>/dev/null || true)

  # Clean up temp PDF text files
  [ ${#cleanup_files[@]} -gt 0 ] && rm -f "${cleanup_files[@]}"

  # If files are identical
  if [ -z "$diff_output" ]; then
    echo -e "${GREEN}Files are identical.${NC}"
    return 0
  fi

  # Base64-encode the diff for safe embedding
  local diff_b64
  diff_b64=$(printf '%s' "$diff_output" | base64)

  # Generate self-contained HTML
  {
    cat << 'CMPHEAD'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
CMPHEAD
    echo "  <title>Compare: ${name1} ↔ ${name2}</title>"
    echo "  <style>"
    cat "$COMPARE_ASSETS_DIR/diff2html.min.css"
    cat << 'CMPCSS'

    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
      background: #ffffff;
      color: #24292f;
    }
    #cmp-toolbar {
      position: sticky; top: 0; z-index: 100;
      display: flex; justify-content: space-between; align-items: center;
      padding: 10px 20px;
      background: #f6f8fa; border-bottom: 1px solid #d0d7de;
      font-size: 14px;
    }
    #cmp-title {
      font-weight: 600; color: #24292f; font-size: 15px;
    }
    #cmp-title .arrow { color: #57606a; margin: 0 6px; }
    #cmp-toolbar .controls { display: flex; gap: 6px; align-items: center; }
    #cmp-toolbar button {
      padding: 5px 16px; font-size: 13px; border-radius: 6px;
      border: 1px solid rgba(27,31,36,0.15); cursor: pointer;
      font-weight: 500; background: #f6f8fa; color: #24292f;
      transition: background 0.15s;
    }
    #cmp-toolbar button:hover { background: #e1e4e8; }
    #cmp-toolbar button.active {
      background: #0969da; color: #fff; border-color: #0969da;
    }
    #cmp-toolbar button.active:hover { background: #0860ca; }
    .d2h-wrapper { padding: 0; }
    .d2h-file-header { display: none; }
    #cmp-stats {
      font-size: 12px; color: #57606a; padding: 8px 20px;
      background: #f6f8fa; border-bottom: 1px solid #d0d7de;
    }
    #cmp-stats .added { color: #1a7f37; font-weight: 600; }
    #cmp-stats .removed { color: #cf222e; font-weight: 600; }

    /*
     * Eye-friendly diff palette — override diff2html CSS variables.
     * Low saturation (~15%), minimal luminance shift for line backgrounds,
     * gentle word highlights. Based on Solarized perceptual uniformity
     * and WCAG guidelines for reducing visual fatigue.
     */
    :root {
      /* Light mode: pastel tints, barely-there backgrounds */
      --d2h-ins-bg-color: #eef6ee;
      --d2h-ins-border-color: #d8e8d8;
      --d2h-ins-highlight-bg-color: #d0e4d0;
      --d2h-ins-label-color: #5a7a5a;
      --d2h-del-bg-color: #f6eeee;
      --d2h-del-border-color: #e8d8d8;
      --d2h-del-highlight-bg-color: #e4d0d0;
      --d2h-del-label-color: #7a5a5a;
      --d2h-change-ins-color: #e8f0e8;
      --d2h-change-del-color: #f0e8e4;
      --d2h-info-bg-color: #eef2f8;
      --d2h-info-border-color: #dce2ed;
      --d2h-line-border-color: #e8eaed;

      /* Dark mode */
      --d2h-dark-bg-color: #1c2028;
      --d2h-dark-color: #c8cdd5;
      --d2h-dark-border-color: #2c3040;
      --d2h-dark-dim-color: #5a6270;
      --d2h-dark-line-border-color: #262a35;
      --d2h-dark-file-header-bg-color: #20242e;
      --d2h-dark-file-header-border-color: #2c3040;
      --d2h-dark-empty-placeholder-bg-color: rgba(100,110,130,0.08);
      --d2h-dark-empty-placeholder-border-color: #2c3040;
      --d2h-dark-selected-color: rgba(80,130,190,0.10);
      /* Additions: clearly green, not harsh */
      --d2h-dark-ins-bg-color: rgba(50,200,80,0.18);
      --d2h-dark-ins-border-color: rgba(50,200,80,0.25);
      --d2h-dark-ins-highlight-bg-color: rgba(50,210,90,0.38);
      --d2h-dark-ins-label-color: #4cc764;
      /* Deletions: clearly red, not harsh */
      --d2h-dark-del-bg-color: rgba(230,70,60,0.16);
      --d2h-dark-del-border-color: rgba(230,70,60,0.25);
      --d2h-dark-del-highlight-bg-color: rgba(235,80,70,0.35);
      --d2h-dark-del-label-color: #e06060;
      --d2h-dark-change-ins-color: rgba(50,200,80,0.20);
      --d2h-dark-change-del-color: rgba(220,150,50,0.16);
      --d2h-dark-info-bg-color: rgba(70,130,200,0.08);
      --d2h-dark-info-border-color: rgba(70,130,200,0.15);
      --d2h-dark-change-label-color: #a08a50;
      --d2h-dark-moved-label-color: #5a8ab0;
    }

    /* Force dark scheme class based on system preference */
    .d2h-file-header { display: none; }
    .d2h-wrapper { padding: 0; }

    #cmp-stats {
      font-size: 12px; color: #57606a; padding: 8px 20px;
      background: #f6f8fa; border-bottom: 1px solid #d0d7de;
    }
    #cmp-stats .added { color: #5a7a5a; font-weight: 600; }
    #cmp-stats .removed { color: #7a5a5a; font-weight: 600; }

    @media (prefers-color-scheme: dark) {
      body { background: #1c2028; color: #c8cdd5; }
      #cmp-toolbar { background: #20242e; border-bottom-color: #2c3040; }
      #cmp-title { color: #c8cdd5; }
      #cmp-title .arrow { color: #5a6270; }
      #cmp-toolbar button { background: #282d38; color: #a0a8b5; border-color: #2c3040; }
      #cmp-toolbar button:hover { background: #303848; }
      #cmp-toolbar button.active { background: #3a5580; border-color: #3a5580; color: #d0d8e0; }
      #cmp-stats { background: #20242e; border-bottom-color: #2c3040; color: #5a6270; }
      #cmp-stats .added { color: #4cc764; }
      #cmp-stats .removed { color: #e06060; }
    }
    </style>
</head>
<body>
  <div id="cmp-toolbar">
    <span id="cmp-title"></span>
    <div class="controls">
      <button id="btn-side" class="active" title="Side-by-side view">Side by Side</button>
      <button id="btn-line" title="Unified line view">Unified</button>
    </div>
  </div>
  <div id="cmp-stats"></div>
  <div id="cmp-diff"></div>
  <script>
CMPCSS
    cat "$COMPARE_ASSETS_DIR/diff2html-ui.min.js"
    cat << CMPSCRIPT
  </script>
  <script>
    var bin = atob('${diff_b64}');
    var bytes = new Uint8Array(bin.length);
    for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
    var diffString = new TextDecoder('utf-8').decode(bytes);

    var name1 = '$(printf '%s' "$name1" | sed "s/'/\\\\'/g")';
    var name2 = '$(printf '%s' "$name2" | sed "s/'/\\\\'/g")';

    document.getElementById('cmp-title').innerHTML =
      '<span>' + name1 + '</span><span class="arrow">↔</span><span>' + name2 + '</span>';

    // Count additions/removals
    var lines = diffString.split('\\n');
    var added = 0, removed = 0;
    for (var i = 0; i < lines.length; i++) {
      if (lines[i].charAt(0) === '+' && !lines[i].startsWith('+++')) added++;
      else if (lines[i].charAt(0) === '-' && !lines[i].startsWith('---')) removed++;
    }
    document.getElementById('cmp-stats').innerHTML =
      '<span class="added">+' + added + ' additions</span>&nbsp;&nbsp;' +
      '<span class="removed">-' + removed + ' removals</span>';

    function renderDiff(outputFormat) {
      var targetElement = document.getElementById('cmp-diff');
      var configuration = {
        drawFileList: false,
        matching: 'lines',
        outputFormat: outputFormat,
        highlight: true,
        colorScheme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
        renderNothingWhenEmpty: false
      };
      var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
      diff2htmlUi.draw();
      diff2htmlUi.highlightCode();
    }

    // Re-render on system theme change
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
      var active = document.querySelector('#btn-side.active') ? 'side-by-side' : 'line-by-line';
      renderDiff(active);
    });

    renderDiff('side-by-side');

    var btnSide = document.getElementById('btn-side');
    var btnLine = document.getElementById('btn-line');

    btnSide.addEventListener('click', function() {
      btnSide.classList.add('active');
      btnLine.classList.remove('active');
      renderDiff('side-by-side');
    });
    btnLine.addEventListener('click', function() {
      btnLine.classList.add('active');
      btnSide.classList.remove('active');
      renderDiff('line-by-line');
    });
  </script>
</body>
</html>
CMPSCRIPT
  } > "$tmp_html"

  # Open in browser
  if [[ "$OSTYPE" == "darwin"* ]]; then
    open "$tmp_html"
  elif command_exists xdg-open; then
    xdg-open "$tmp_html"
  elif command_exists wslview; then
    wslview "$tmp_html"
  else
    echo -e "Open in browser: ${BOLD}file://$tmp_html${NC}"
    return 0
  fi

  echo -e "${GREEN}Comparing:${NC} $name1 ↔ $name2"
  echo -e "${GRAY}$tmp_html${NC}"
}

md_show_help() {
  echo -e "${CYAN}Markdown Renderer:${NC}"
  echo ""
  echo "Usage:"
  echo "  crab md <file>              Render markdown file in browser"
  echo "  crab md --update-assets     Re-download rendering libraries"
  echo ""
  echo "Renders any markdown file as a beautiful, self-contained HTML page"
  echo "and opens it in your default browser. Supports GitHub-Flavored Markdown"
  echo "with syntax-highlighted code blocks, tables, task lists, and dark mode."
  echo ""
  echo -e "${GRAY}Assets stored in: $MD_ASSETS_DIR${NC}"
}

md_ensure_assets() {
  local version_file="$MD_ASSETS_DIR/.version"

  # Check if assets exist and are current version
  if [ -f "$version_file" ] && [ "$(cat "$version_file")" = "$MD_ASSETS_VERSION" ]; then
    return 0
  fi

  echo -e "${CYAN}Downloading markdown rendering assets (one-time setup)...${NC}"
  mkdir -p "$MD_ASSETS_DIR"

  local base="https://cdn.jsdelivr.net/npm"
  local failed=0

  curl -fsSL "$base/marked@15/lib/marked.umd.min.js" -o "$MD_ASSETS_DIR/marked.min.js" || failed=1
  curl -fsSL "$base/@highlightjs/cdn-assets@11/highlight.min.js" -o "$MD_ASSETS_DIR/highlight.min.js" || failed=1
  curl -fsSL "$base/github-markdown-css@5/github-markdown.css" -o "$MD_ASSETS_DIR/github-markdown.css" || failed=1
  curl -fsSL "$base/@highlightjs/cdn-assets@11/styles/github.min.css" -o "$MD_ASSETS_DIR/highlight-light.min.css" || failed=1
  curl -fsSL "$base/@highlightjs/cdn-assets@11/styles/github-dark-dimmed.min.css" -o "$MD_ASSETS_DIR/highlight-dark.min.css" || failed=1

  if [ "$failed" -eq 1 ]; then
    error "Failed to download assets. Check your internet connection."
    rm -rf "$MD_ASSETS_DIR"
    return 1
  fi

  echo "$MD_ASSETS_VERSION" > "$version_file"
  echo -e "${GREEN}Assets downloaded${NC}"
}

cmd_md() {
  local file="${1:-}"

  case "$file" in
    ""|"-h"|"--help"|"help")
      md_show_help
      return 0
      ;;
    "--update-assets"|"--update")
      rm -rf "$MD_ASSETS_DIR"
      md_ensure_assets
      return $?
      ;;
  esac

  # Resolve to absolute path
  if [[ "$file" != /* ]]; then
    file="$(pwd)/$file"
  fi

  if [ ! -f "$file" ]; then
    error "File not found: $file"
    return 1
  fi

  md_ensure_assets || return 1

  local filename
  filename=$(basename "$file")
  local tmp_html="/tmp/crab-md-${filename%.*}-$$.html"

  # Base64-encode markdown content; decoded via atob + TextDecoder for UTF-8 support
  local md_b64
  md_b64=$(base64 < "$file")

  # Generate self-contained HTML
  # Uses: quoted heredocs for static parts, cat for asset files, echo for dynamic values
  {
    cat << 'MDHEAD'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
MDHEAD
    echo "  <title>${filename}</title>"
    echo "  <style>"
    cat "$MD_ASSETS_DIR/github-markdown.css"
    cat "$MD_ASSETS_DIR/highlight-light.min.css"
    echo "@media (prefers-color-scheme: dark) {"
    cat "$MD_ASSETS_DIR/highlight-dark.min.css"
    echo "}"
    cat << 'MDCSS'
    body {
      max-width: 980px;
      margin: 0 auto;
      padding: 0;
      background: #ffffff;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
    }
    .markdown-body {
      min-width: 200px;
      max-width: 980px;
      padding: 24px 45px 80px;
    }
    @media (prefers-color-scheme: dark) {
      body { background: #0d1117; }
      .markdown-body {
        --bgColor-default: #0d1117;
        --fgColor-default: #e6edf3;
      }
    }
    @media (max-width: 767px) {
      .markdown-body { padding: 15px 15px 60px; }
    }
    @media print {
      body { max-width: none; padding: 0; }
      #md-toolbar { display: none; }
    }
    /* Toolbar */
    #md-toolbar {
      position: sticky; top: 0; z-index: 100;
      display: flex; justify-content: space-between; align-items: center;
      padding: 8px 16px;
      background: #f6f8fa; border-bottom: 1px solid #d0d7de;
      font-size: 14px;
    }
    #md-filename { font-weight: 600; color: #57606a; }
    #md-toolbar button {
      padding: 5px 16px; font-size: 14px; border-radius: 6px;
      border: 1px solid rgba(27,31,36,0.15); cursor: pointer; margin-left: 8px;
      font-weight: 500;
    }
    .btn-primary { background: #2ea44f !important; color: #fff !important; border-color: rgba(27,31,36,0.15) !important; }
    .btn-primary:hover { background: #2c974b !important; }
    .btn-default { background: #f6f8fa; color: #24292f; }
    .btn-default:hover { background: #e1e4e8; }
    @media (prefers-color-scheme: dark) {
      #md-toolbar { background: #161b22; border-bottom-color: #30363d; }
      #md-filename { color: #8b949e; }
      .btn-default { background: #21262d; color: #c9d1d9; border-color: #30363d; }
      .btn-default:hover { background: #30363d; }
    }
    /* Edit mode layout */
    body.edit-mode { max-width: none; }
    body.edit-mode #md-container { display: flex; height: calc(100vh - 45px); }
    body.edit-mode #md-editor { display: flex !important; flex: 1; min-width: 0; border-right: 1px solid #d0d7de; }
    body.edit-mode .markdown-body { flex: 1; min-width: 0; overflow-y: auto; padding: 24px 45px 80px; max-width: none; }
    #md-source {
      width: 100%; height: 100%; border: none; outline: none; resize: none;
      padding: 24px; font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace;
      font-size: 14px; line-height: 1.6; background: #fff; color: #24292f; tab-size: 4;
    }
    @media (prefers-color-scheme: dark) {
      body.edit-mode #md-editor { border-right-color: #30363d; }
      #md-source { background: #0d1117; color: #e6edf3; }
    }
    /* Diff highlights */
    .diff-added {
      background: rgba(46,160,67,0.15); border-left: 3px solid #2ea44f;
      padding-left: 8px; margin-left: -11px;
    }
    .diff-changed {
      background: rgba(210,153,34,0.15); border-left: 3px solid #d29922;
      padding-left: 8px; margin-left: -11px;
    }
    @media (prefers-color-scheme: dark) {
      .diff-added { background: rgba(46,160,67,0.1); }
      .diff-changed { background: rgba(210,153,34,0.1); }
    }
    /* Diff legend */
    #diff-legend {
      display: none; padding: 8px 16px; font-size: 12px; color: #57606a;
      border-bottom: 1px solid #d0d7de; background: #f6f8fa;
    }
    #diff-legend.visible { display: flex; gap: 16px; align-items: center; }
    #diff-legend span { display: inline-flex; align-items: center; gap: 4px; }
    #diff-legend .swatch { width: 12px; height: 12px; border-radius: 2px; display: inline-block; }
    @media (prefers-color-scheme: dark) {
      #diff-legend { background: #161b22; border-bottom-color: #30363d; color: #8b949e; }
    }
    </style>
</head>
<body>
  <div id="md-toolbar">
    <span id="md-filename"></span>
    <div>
      <button id="btn-edit" class="btn-primary" title="Edit markdown">Edit</button>
      <button id="btn-save" class="btn-primary" style="display:none" title="Download edited file">Save</button>
      <button id="btn-cancel" class="btn-default" style="display:none" title="Exit edit mode">Cancel</button>
    </div>
  </div>
  <div id="diff-legend">
    <span><span class="swatch" style="background:#d29922"></span> Changed</span>
    <span><span class="swatch" style="background:#2ea44f"></span> Added</span>
  </div>
  <div id="md-container">
    <article class="markdown-body" id="md-preview"></article>
    <div id="md-editor" style="display:none">
      <textarea id="md-source" spellcheck="false"></textarea>
    </div>
  </div>
  <script>
MDCSS
    cat "$MD_ASSETS_DIR/marked.min.js"
    echo "</script>"
    echo "<script>"
    cat "$MD_ASSETS_DIR/highlight.min.js"
    echo "</script>"
    echo "<script>"
    cat << MDSCRIPT
    marked.setOptions({
      highlight:function(code,lang){
        if(lang&&hljs.getLanguage(lang))return hljs.highlight(code,{language:lang}).value;
        return hljs.highlightAuto(code).value;
      },
      gfm:true
    });
    var bin=atob('${md_b64}');
    var bytes=new Uint8Array(bin.length);
    for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);
    var src=new TextDecoder('utf-8').decode(bytes);
    window.__originalMd=src;
    window.__currentMd=src;
    document.getElementById('md-preview').innerHTML=marked.parse(src);
    document.querySelectorAll('#md-preview pre code').forEach(function(b){hljs.highlightElement(b);});
    document.getElementById('md-filename').textContent=document.title;
MDSCRIPT
    echo "</script>"
    echo "<script>"
    cat << 'MDEDITJS'
(function(){
  var preview=document.getElementById('md-preview');
  var editor=document.getElementById('md-editor');
  var source=document.getElementById('md-source');
  var btnEdit=document.getElementById('btn-edit');
  var btnSave=document.getElementById('btn-save');
  var btnCancel=document.getElementById('btn-cancel');
  var legend=document.getElementById('diff-legend');

  function renderPreview(md,showDiff){
    preview.innerHTML=marked.parse(md);
    document.querySelectorAll('#md-preview pre code').forEach(function(b){hljs.highlightElement(b);});
    if(showDiff) applyDiffHighlights();
  }

  function splitBlocks(text){
    return text.split(/\n{2,}/).filter(function(b){return b.trim().length>0;});
  }

  function applyDiffHighlights(){
    var origBlocks=splitBlocks(window.__originalMd);
    var currBlocks=splitBlocks(window.__currentMd);
    var children=preview.children;
    var hasChanges=false;
    // Walk through rendered children and mark changed/added blocks
    var ci=0;
    for(var i=0;i<children.length&&ci<Math.max(origBlocks.length,currBlocks.length);i++){
      // Skip non-element nodes
      if(children[i].nodeType!==1)continue;
      if(ci>=origBlocks.length){
        children[i].classList.add('diff-added');
        hasChanges=true;
      }else if(ci<currBlocks.length&&origBlocks[ci].trim()!==currBlocks[ci].trim()){
        children[i].classList.add('diff-changed');
        hasChanges=true;
      }
      ci++;
    }
    legend.className=hasChanges?'visible':'';
  }

  function enterEditMode(){
    document.body.classList.add('edit-mode');
    source.value=window.__currentMd;
    btnEdit.style.display='none';
    btnSave.style.display='';
    btnCancel.style.display='';
    legend.className='';
    // Clear diff highlights for split view
    Array.from(preview.children).forEach(function(el){
      el.classList.remove('diff-added','diff-changed');
    });
    source.focus();
  }

  function exitEditMode(){
    document.body.classList.remove('edit-mode');
    btnEdit.style.display='';
    btnSave.style.display='none';
    btnCancel.style.display='none';
    var hasChanges=window.__currentMd!==window.__originalMd;
    renderPreview(window.__currentMd,hasChanges);
  }

  btnEdit.addEventListener('click',enterEditMode);
  btnCancel.addEventListener('click',exitEditMode);

  // Live preview with debounce
  var timer;
  source.addEventListener('input',function(){
    clearTimeout(timer);
    timer=setTimeout(function(){
      window.__currentMd=source.value;
      renderPreview(window.__currentMd,true);
    },150);
  });

  // Save/download
  btnSave.addEventListener('click',function(){
    var content=window.__currentMd;
    var blob=new Blob([content],{type:'text/markdown;charset=utf-8'});
    var url=URL.createObjectURL(blob);
    var a=document.createElement('a');
    var origName=document.title.replace(/\.[^.]+$/,'');
    a.href=url;
    a.download=origName+'.edited.md';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  });

  // Keyboard shortcuts
  document.addEventListener('keydown',function(e){
    if((e.metaKey||e.ctrlKey)&&e.key==='s'){
      if(document.body.classList.contains('edit-mode')){
        e.preventDefault();
        btnSave.click();
      }
    }
    if(e.key==='Escape'&&document.body.classList.contains('edit-mode')){
      exitEditMode();
    }
  });
})();
MDEDITJS
    echo "</script>"
    echo "</body>"
    echo "</html>"
  } > "$tmp_html"

  # Open in browser
  if [[ "$OSTYPE" == "darwin"* ]]; then
    open "$tmp_html"
  elif command_exists xdg-open; then
    xdg-open "$tmp_html"
  elif command_exists wslview; then
    wslview "$tmp_html"
  else
    echo -e "Open in browser: ${BOLD}file://$tmp_html${NC}"
    return 0
  fi

  echo -e "${GREEN}Rendered:${NC} $filename"
  echo -e "${GRAY}$tmp_html${NC}"
}

# =============================================================================
# Alias Management
# =============================================================================

handle_alias_command() {
  local subcmd="${1:-}"

  case "$subcmd" in
    "")
      # List all aliases
      if [ ! -f "$GLOBAL_CONFIG" ]; then
        echo -e "${YELLOW}No aliases configured.${NC}"
        echo ""
        echo "Set one with: crab alias set <name> <command...>"
        return
      fi

      local aliases
      aliases=$(yq -r '.aliases // {} | to_entries[] | .key + " → " + (.value | tostring)' "$GLOBAL_CONFIG" 2>/dev/null)
      if [ -z "$aliases" ]; then
        echo -e "${YELLOW}No aliases configured.${NC}"
        echo ""
        echo "Set one with: crab alias set <name> <command...>"
        return
      fi

      echo -e "${CYAN}Command Aliases${NC}"
      echo ""
      while IFS= read -r line; do
        local name="${line%% →*}"
        local value="${line#*→ }"
        echo -e "  ${GREEN}$name${NC} → $value"
      done <<< "$aliases"
      echo ""
      echo -e "${GRAY}Config: $GLOBAL_CONFIG${NC}"
      ;;
    "set"|"add")
      local name="${2:-}"
      if [ -z "$name" ]; then
        error "Usage: crab alias set <name> <command...>"
        exit 1
      fi
      # Validate alias name
      if ! [[ "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
        error "Invalid alias name '$name'. Use only letters, numbers, hyphens, and underscores."
        exit 1
      fi

      shift 2
      local value="$*"
      if [ -z "$value" ]; then
        error "Usage: crab alias set <name> <command...>"
        exit 1
      fi

      # Ensure global config exists
      mkdir -p "$CONFIG_DIR"
      if [ ! -f "$GLOBAL_CONFIG" ]; then
        echo "{}" > "$GLOBAL_CONFIG"
      fi

      NAME="$name" VALUE="$value" yq -i '.aliases.[strenv(NAME)] = strenv(VALUE)' "$GLOBAL_CONFIG"
      echo -e "${GREEN}Alias set:${NC} $name → $value"
      ;;
    "rm"|"remove"|"delete")
      local name="${2:-}"
      if [ -z "$name" ]; then
        error "Usage: crab alias rm <name>"
        exit 1
      fi

      if [ ! -f "$GLOBAL_CONFIG" ]; then
        error "No aliases configured"
        exit 1
      fi

      local existing
      existing=$(NAME="$name" yq -r '.aliases.[strenv(NAME)] // ""' "$GLOBAL_CONFIG" 2>/dev/null)
      if [ -z "$existing" ] || [ "$existing" = "null" ]; then
        error "Alias '$name' not found"
        exit 1
      fi

      NAME="$name" yq -i 'del(.aliases.[strenv(NAME)])' "$GLOBAL_CONFIG"

      # Clean up empty aliases map
      local remaining
      remaining=$(yq -r '.aliases // {} | length' "$GLOBAL_CONFIG" 2>/dev/null)
      if [ "$remaining" = "0" ]; then
        yq -i 'del(.aliases)' "$GLOBAL_CONFIG"
      fi

      echo -e "${GREEN}Removed alias:${NC} $name"
      ;;
    *)
      error "Unknown alias subcommand: $subcmd"
      echo ""
      echo "Usage:"
      echo "  crab alias              List all aliases"
      echo "  crab alias set <name> <command...>  Set an alias"
      echo "  crab alias rm <name>    Remove an alias"
      exit 1
      ;;
  esac
}

# =============================================================================
# Environment Snapshot & Restore
# =============================================================================

ENV_SNAPSHOT_DIR="$CONFIG_DIR/snapshots"

# Quick pre-scan of the machine — gives the agent a head start
_env_prescan() {
  local info=""

  # OS
  info+="OS: $(uname -s) $(uname -r) ($(uname -m))\n"
  info+="Hostname: $(hostname)\n"
  info+="User: $(whoami)\n"
  info+="Shell: ${SHELL:-unknown}\n"
  info+="Home: $HOME\n"

  # Package manager
  if command_exists brew; then
    info+="Package manager: Homebrew $(brew --version 2>/dev/null | head -1)\n"
    info+="Brew formulas: $(brew list --formula 2>/dev/null | tr '\n' ', ')\n"
    info+="Brew casks: $(brew list --cask 2>/dev/null | tr '\n' ', ')\n"
    info+="Brew taps: $(brew tap 2>/dev/null | tr '\n' ', ')\n"
    info+="Brew services: $(brew services list 2>/dev/null | tail -n +2 | tr '\n' '; ')\n"
  fi
  if command_exists apt; then
    info+="Package manager: apt\n"
  fi
  if command_exists pacman; then
    info+="Package manager: pacman\n"
  fi

  # Node / JS
  if command_exists node; then
    info+="Node: $(node --version 2>/dev/null)\n"
  fi
  if command_exists nvm; then
    info+="nvm versions: $(ls "$HOME/.nvm/versions/node/" 2>/dev/null | tr '\n' ', ')\n"
  elif [ -d "$HOME/.nvm/versions/node" ]; then
    info+="nvm versions: $(ls "$HOME/.nvm/versions/node/" 2>/dev/null | tr '\n' ', ')\n"
  fi
  if command_exists npm; then
    info+="npm globals: $(npm list -g --depth=0 2>/dev/null | tail -n +2 | sed 's/[├└─│ ]//g' | tr '\n' ', ')\n"
    local linked=$(npm ls -g --link --depth=0 2>/dev/null | tail -n +2)
    if [ -n "$linked" ]; then
      info+="npm linked: $linked\n"
    fi
  fi
  if command_exists pnpm; then
    info+="pnpm: $(pnpm --version 2>/dev/null)\n"
  fi
  if command_exists yarn; then
    info+="yarn: $(yarn --version 2>/dev/null)\n"
  fi
  if command_exists bun; then
    info+="bun: $(bun --version 2>/dev/null)\n"
  fi

  # Python
  if command_exists python3; then
    info+="Python: $(python3 --version 2>/dev/null) ($(which python3))\n"
  fi
  if command_exists pyenv; then
    info+="pyenv versions: $(pyenv versions --bare 2>/dev/null | tr '\n' ', ')\n"
  fi
  if command_exists pip3; then
    info+="pip packages: $(pip3 list 2>/dev/null | tail -n +3 | awk '{print $1}' | tr '\n' ', ')\n"
  fi

  # Go, Rust, Ruby, Java
  if command_exists go; then
    info+="Go: $(go version 2>/dev/null)\n"
  fi
  if command_exists rustc; then
    info+="Rust: $(rustc --version 2>/dev/null)\n"
    if [ -d "$HOME/.cargo/bin" ]; then
      info+="Cargo binaries: $(ls "$HOME/.cargo/bin/" 2>/dev/null | tr '\n' ', ')\n"
    fi
  fi
  if command_exists ruby && [[ "$(which ruby)" != "/usr/bin/ruby" ]]; then
    info+="Ruby: $(ruby --version 2>/dev/null)\n"
  fi
  if command_exists java; then
    info+="Java: $(java --version 2>/dev/null | head -1)\n"
  fi

  # Databases
  if command_exists psql; then
    info+="PostgreSQL: $(psql --version 2>/dev/null)\n"
    if pg_isready &>/dev/null; then
      info+="PostgreSQL status: running\n"
      info+="PostgreSQL databases: $(psql -l -t 2>/dev/null | awk -F'|' '{print $1}' | sed 's/^ *//' | grep -v '^$' | grep -v '^template' | tr '\n' ', ')\n"
    else
      info+="PostgreSQL status: not running\n"
    fi
  fi
  if command_exists mysql; then
    info+="MySQL: $(mysql --version 2>/dev/null)\n"
  fi
  if command_exists redis-cli; then
    info+="Redis: $(redis-cli --version 2>/dev/null)\n"
    if redis-cli ping &>/dev/null; then
      info+="Redis status: running\n"
    fi
  fi
  if command_exists mongosh; then
    info+="MongoDB: $(mongosh --version 2>/dev/null)\n"
  fi

  # Docker
  if command_exists docker; then
    info+="Docker: $(docker --version 2>/dev/null)\n"
    if docker info &>/dev/null; then
      info+="Docker status: running\n"
      local images=$(docker images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | head -20 | tr '\n' ', ')
      if [ -n "$images" ]; then
        info+="Docker images: $images\n"
      fi
    fi
  fi

  # Cloud / infra
  if command_exists gcloud; then
    info+="gcloud: $(gcloud --version 2>/dev/null | head -1)\n"
    info+="gcloud account: $(gcloud config get-value account 2>/dev/null)\n"
    info+="gcloud project: $(gcloud config get-value project 2>/dev/null)\n"
  fi
  if command_exists aws; then
    info+="AWS CLI: $(aws --version 2>/dev/null)\n"
  fi
  if command_exists kubectl; then
    info+="kubectl: $(kubectl version --client --short 2>/dev/null || kubectl version --client 2>/dev/null | head -1)\n"
  fi
  if command_exists terraform; then
    info+="Terraform: $(terraform --version 2>/dev/null | head -1)\n"
  fi

  # Git
  if command_exists git; then
    local git_user=$(git config --global user.name 2>/dev/null)
    local git_email=$(git config --global user.email 2>/dev/null)
    info+="Git user: $git_user <$git_email>\n"
  fi
  if command_exists gh; then
    info+="GitHub CLI: $(gh --version 2>/dev/null | head -1)\n"
    info+="GitHub auth: $(gh auth status 2>&1 | grep 'Logged in' | sed 's/^[ ]*//' || echo 'not authenticated')\n"
  fi

  # SSH
  if [ -d "$HOME/.ssh" ]; then
    info+="SSH dir contents: $(ls "$HOME/.ssh/" 2>/dev/null | tr '\n' ', ')\n"
  fi

  # Editors
  if command_exists cursor; then
    info+="Cursor: $(cursor --version 2>/dev/null | head -1)\n"
  fi
  if command_exists code; then
    info+="VS Code: $(code --version 2>/dev/null | head -1)\n"
  fi

  # Terminals
  if [ -f "$HOME/.config/ghostty/config" ]; then
    info+="Ghostty: config exists\n"
  fi
  if [ -d "/Applications/iTerm.app" ]; then
    info+="iTerm2: installed\n"
  fi

  # Security tools
  if [ -d "/Applications/Burp Suite Professional.app" ]; then
    info+="Burp Suite Professional: installed (requires license)\n"
  elif [ -d "/Applications/Burp Suite Community Edition.app" ]; then
    info+="Burp Suite Community: installed\n"
  fi

  # Tunneling
  if command_exists ngrok; then
    info+="ngrok: $(ngrok --version 2>/dev/null)\n"
  fi
  if command_exists cloudflared; then
    info+="cloudflared: $(cloudflared --version 2>/dev/null | head -1)\n"
  fi

  # Shell extras
  if [ -d "$HOME/.oh-my-zsh" ]; then
    info+="oh-my-zsh: installed\n"
    local plugins=$(grep '^plugins=' "$HOME/.zshrc" 2>/dev/null)
    if [ -n "$plugins" ]; then
      info+="oh-my-zsh config: $plugins\n"
    fi
    local theme=$(grep '^ZSH_THEME=' "$HOME/.zshrc" 2>/dev/null)
    if [ -n "$theme" ]; then
      info+="oh-my-zsh config: $theme\n"
    fi
  fi

  # Applications (macOS)
  if [ -d "/Applications" ]; then
    info+="Applications: $(ls /Applications/ 2>/dev/null | sed 's/\.app$//' | tr '\n' ', ')\n"
  fi

  # Git repos
  local repos=$(find "$HOME" -maxdepth 3 -name ".git" -type d 2>/dev/null | head -30 | sed "s|$HOME/||g; s|/\.git||g" | tr '\n' ', ')
  if [ -n "$repos" ]; then
    info+="Git repos found: $repos\n"
  fi

  # .env files
  local envfiles=$(find "$HOME" -maxdepth 4 -name ".env" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -20 | sed "s|$HOME/||g" | tr '\n' ', ')
  if [ -n "$envfiles" ]; then
    info+=".env files: $envfiles\n"
  fi

  # Crabcode
  if [ -d "$CONFIG_DIR" ]; then
    info+="Crabcode config dir: $CONFIG_DIR\n"
    if [ -d "$PROJECTS_DIR" ]; then
      info+="Crabcode projects: $(ls "$PROJECTS_DIR" 2>/dev/null | tr '\n' ', ')\n"
    fi
  fi

  # Claude
  if command_exists claude; then
    info+="Claude Code: $(claude --version 2>/dev/null)\n"
  fi
  if [ -d "$HOME/.claude" ]; then
    info+="Claude config dir: ~/.claude\n"
  fi

  echo -e "$info"
}

handle_env_command() {
  local cmd="${1:-}"
  shift || true

  case "$cmd" in
    "snapshot")
      env_snapshot "$@"
      ;;
    "restore")
      env_restore "$@"
      ;;
    ""|"help"|"-h"|"--help")
      echo -e "${BOLD}crab env${NC} - Environment snapshot & restore"
      echo ""
      echo -e "  ${CYAN}crab env snapshot${NC}              Capture environment recipe"
      echo -e "  ${CYAN}crab env snapshot --dry-run${NC}    Preview what would be captured"
      echo -e "  ${CYAN}crab env restore --from FILE${NC}   Restore from snapshot"
      echo ""
      echo "Snapshot launches an AI agent to explore your machine and build"
      echo "a comprehensive environment recipe. The recipe + supporting files"
      echo "are encrypted into a portable bundle for setting up a new machine."
      ;;
    *)
      error "Unknown env command: $cmd"
      echo "Run 'crab env help' for usage"
      return 1
      ;;
  esac
}

env_snapshot() {
  local dry_run="false"
  local output_path=""

  while [ $# -gt 0 ]; do
    case "$1" in
      --dry-run|-n)
        dry_run="true"
        shift
        ;;
      --output|-o)
        output_path="${2:-}"
        shift 2
        ;;
      *)
        shift
        ;;
    esac
  done

  echo -e "${CYAN}Scanning machine...${NC}"

  # Quick pre-scan
  local prescan
  prescan=$(_env_prescan)

  if [ "$dry_run" = "true" ]; then
    echo -e "${BOLD}Pre-scan results:${NC}"
    echo ""
    echo "$prescan"
    echo ""
    echo -e "${GRAY}In a real snapshot, an AI agent would explore each of these"
    echo -e "in depth and build a comprehensive recipe.${NC}"
    return 0
  fi

  # Create staging directory
  local timestamp=$(date +%Y%m%d-%H%M%S)
  local staging_dir="$ENV_SNAPSHOT_DIR/staging-$timestamp"
  mkdir -p "$staging_dir"

  echo -e "${CYAN}Staging directory: $staging_dir${NC}"
  echo -e "${CYAN}Launching agent to explore and build recipe...${NC}"
  echo ""

  # Write prescan to staging dir for the agent to reference
  echo "$prescan" > "$staging_dir/prescan.txt"

  # Write the agent prompt to a file
  local prompt_file="$staging_dir/_prompt.md"
  cat > "$prompt_file" <<PROMPT_EOF
You are building an environment snapshot recipe for migrating to a new computer.
Your job is to thoroughly explore this machine and produce a comprehensive recipe
that a coding agent on a NEW machine can follow to recreate this dev environment.

## What you have been given

A pre-scan of this machine is at: $staging_dir/prescan.txt
Read it first. It tells you what is installed and gives you starting points.

## Your task

1. **Explore deeply** - the pre-scan is surface-level. For each tool/service found:
   - Check configs, versions, plugins, extensions, auth state
   - Capture relevant config files into $staging_dir/configs/
   - Check for custom settings that differ from defaults

2. **Capture database schemas** - if databases are running:
   - Dump roles: pg_dumpall --roles-only > $staging_dir/databases/postgres-roles.sql
   - Dump schemas (no data): pg_dump -s <db> > $staging_dir/databases/<db>.sql
   - Do this for every non-template database

3. **Capture config files** - copy important dotfiles/configs into $staging_dir/configs/:
   - Shell configs (.zshrc, .zprofile, .bashrc, etc.)
   - Git config (.gitconfig, .gitignore_global if exists)
   - SSH config (.ssh/config if exists - NOT private keys, those go in secrets)
   - Terminal configs (ghostty, iterm, etc.)
   - Editor settings (VS Code/Cursor settings.json, keybindings.json)
   - Docker daemon.json
   - Cloud tool configs (gcloud, aws, kube)
   - Tool-specific configs (ngrok, cloudflared, etc.)
   - Crabcode configs (~/.crabcode/projects/*.yaml, ~/.crabcode/config.yaml)
   - Claude configs (~/.claude/)

4. **Capture editor extensions** - save extension lists:
   - code --list-extensions > $staging_dir/configs/vscode-extensions.txt (if installed)
   - cursor --list-extensions > $staging_dir/configs/cursor-extensions.txt (if installed)

5. **Build project inventory** - for each git repo found:
   - Get the remote URL (git -C <path> remote get-url origin)
   - Get current branch
   - Note if it has .env files (do not copy them yet - those are secrets)
   - Note worktree relationships (repos that share a common parent)
   - Save to $staging_dir/projects/inventory.md

6. **Collect secrets into $staging_dir/secrets/** (these will be encrypted):
   - SSH private keys (~/.ssh/id_* or similar)
   - .env files from projects (preserve directory structure info)
   - API tokens from tool configs (ngrok authtoken, etc.)
   - Cloud credentials (~/.aws/credentials, gcloud tokens)
   - Docker registry auth (~/.docker/config.json if it has auths)
   - Any other auth tokens you find in config files

7. **Write the recipe** - create $staging_dir/recipe.md with:
   - Phased setup order (foundation, runtimes, shell, git/auth, databases, projects, editors, manual steps)
   - For each phase: what to install/configure, referencing the captured config files
   - Directional guidance, not literal scripts - the agent on the new machine adapts
   - Manual steps section: licensed software, app sign-ins, things that cannot be automated
   - Any notes about non-obvious setup (worktree structures, symlinks, custom PATH entries, brew services that auto-start)

## CRITICAL: Completeness rules

The pre-scan below lists specific items found on this machine. You MUST handle EVERY item:

- **Every .env file listed**: Copy ALL of them to $staging_dir/secrets/. Do not skip any.
  Use the filename format: path_with_underscores_.env (e.g., Dev-Promptfoo_cloud2_.env)
  Write a mapping file at $staging_dir/secrets/env-file-mapping.txt showing original paths.
- **Every database listed**: Dump the schema for ALL of them. Do not skip any.
- **Every git repo listed**: Include ALL of them in the project inventory. Do not skip any.
- **Every tool/service listed**: Check its config and capture it. Do not skip any.

After completing your work, review the pre-scan list one more time and confirm you handled every item. If you find you missed something, go back and capture it.

## Important guidelines

- Be thorough but smart - explore things the pre-scan surfaced, AND look for things it missed
- The recipe.md is the PRIMARY artifact - it should be self-sufficient for an agent to follow
- Config files in $staging_dir/configs/ are SUPPORTING evidence the new machine agent can reference
- Secrets in $staging_dir/secrets/ will be encrypted separately - include them
- Do not copy node_modules, build artifacts, caches, or .git directories
- Do not copy entire applications - just note them for reinstallation
- For databases, capture schemas only - not data (unless it is tiny seed data)
- If a service is not running (e.g., postgres is installed but stopped), note that
- If you find something unexpected or noteworthy, include it in the recipe

## Pre-scan results from this machine

$(cat "$staging_dir/prescan.txt")
PROMPT_EOF

  # Launch agent interactively with the prompt
  cd "$HOME"
  local agent_cmd=$(get_agent_base_cmd)
  local agent=$(get_agent_type)
  case "$agent" in
    claude) $agent_cmd "$prompt_file" ;;
    codex|*) $agent_cmd -C "$staging_dir" "$(cat "$prompt_file")" ;;
  esac
  rm -f "$prompt_file"

  # Check if recipe was created
  if [ ! -f "$staging_dir/recipe.md" ]; then
    warn "Agent did not create recipe.md — check $staging_dir for partial results"
    echo -e "You can review what was captured and run the agent again."
    return 1
  fi

  success "Recipe created at $staging_dir/recipe.md"
  echo ""

  # Let user review/edit the recipe before packaging
  echo -e "${YELLOW}Review the recipe before packaging?${NC}"
  read -p "Open recipe.md in editor? [Y/n]: " review_choice
  if [[ ! "$review_choice" =~ ^[Nn]$ ]]; then
    ${EDITOR:-vim} "$staging_dir/recipe.md"
  fi

  # Package and encrypt
  echo ""
  echo -e "${CYAN}Encrypting snapshot...${NC}"
  echo -e "${GRAY}Choose a password to protect this snapshot.${NC}"
  echo -e "${GRAY}You'll need this password on your new machine.${NC}"
  echo ""

  # Determine output path
  if [ -z "$output_path" ]; then
    output_path="$ENV_SNAPSHOT_DIR/env-snapshot-$timestamp.enc"
  fi
  mkdir -p "$(dirname "$output_path")"

  # Encrypt with openssl (use pipefail to catch tar or openssl failures)
  if ( set -o pipefail; tar czf - -C "$(dirname "$staging_dir")" "$(basename "$staging_dir")" \
    | openssl enc -aes-256-cbc -pbkdf2 -salt -out "$output_path" ); then
    # Clean up staging
    rm -rf "$staging_dir"

    local filesize=$(du -h "$output_path" | awk '{print $1}')
    echo ""
    success "Snapshot saved: $output_path ($filesize)"
    echo ""
    echo -e "  ${BOLD}To restore on a new machine:${NC}"
    echo -e "  1. Transfer this file to the new machine"
    echo -e "  2. Install crabcode: ${CYAN}curl -fsSL <install-url> | bash${NC}"
    echo -e "  3. Run: ${CYAN}crab env restore --from $output_path${NC}"
  else
    error "Encryption failed"
    echo "Unencrypted snapshot remains at: $staging_dir"
    return 1
  fi
}

env_restore() {
  local from_path=""

  while [ $# -gt 0 ]; do
    case "$1" in
      --from|-f)
        from_path="${2:-}"
        shift 2
        ;;
      *)
        # Treat bare argument as the path
        if [ -z "$from_path" ]; then
          from_path="$1"
        fi
        shift
        ;;
    esac
  done

  if [ -z "$from_path" ]; then
    error "Usage: crab env restore --from <path-or-url>"
    return 1
  fi

  # Download if URL
  local local_file="$from_path"
  if [[ "$from_path" == http* ]]; then
    echo -e "${CYAN}Downloading snapshot...${NC}"
    local_file="/tmp/crab-env-snapshot-$$.enc"
    curl -fsSL "$from_path" -o "$local_file" || {
      error "Failed to download: $from_path"
      return 1
    }
  fi

  if [ ! -f "$local_file" ]; then
    error "File not found: $local_file"
    return 1
  fi

  # Decrypt
  echo -e "${CYAN}Decrypting snapshot...${NC}"
  local restore_dir="/tmp/crab-env-restore-$$"
  mkdir -p "$restore_dir"

  if ! ( set -o pipefail; openssl enc -d -aes-256-cbc -pbkdf2 -in "$local_file" \
    | tar xzf - -C "$restore_dir" --strip-components=1 ); then
    error "Decryption failed — wrong password?"
    rm -rf "$restore_dir"
    return 1
  fi

  # Clean up downloaded file
  if [[ "$from_path" == http* ]]; then
    rm -f "$local_file"
  fi

  # Verify recipe exists
  if [ ! -f "$restore_dir/recipe.md" ]; then
    error "Invalid snapshot — recipe.md not found"
    echo "Contents of snapshot:"
    ls -la "$restore_dir/"
    return 1
  fi

  # Show recipe summary
  echo ""
  success "Snapshot decrypted to $restore_dir"
  echo ""
  echo -e "${BOLD}Recipe summary:${NC}"
  head -40 "$restore_dir/recipe.md"
  echo -e "${GRAY}...${NC}"
  echo ""

  # Ask to launch agent
  echo -e "${YELLOW}Launch coding agent to set up this machine?${NC}"
  read -p "Start setup? [Y/n]: " start_choice
  if [[ "$start_choice" =~ ^[Nn]$ ]]; then
    echo ""
    echo "Recipe available at: $restore_dir/recipe.md"
    echo "Run manually: $(get_agent_safe_cmd) $restore_dir/recipe.md"
    return 0
  fi

  # Write restore prompt to file
  local prompt_file="$restore_dir/_restore-prompt.md"
  cat > "$prompt_file" <<RESTORE_EOF
You are setting up a new development machine from an environment recipe.

The recipe is at: $restore_dir/recipe.md
Supporting config files are in: $restore_dir/configs/
Database schemas are in: $restore_dir/databases/ (if any)
Secrets are in: $restore_dir/secrets/ (SSH keys, .env files, tokens)
Project inventory is at: $restore_dir/projects/inventory.md (if any)

## Instructions

1. Read recipe.md thoroughly first
2. Work through each phase in order
3. For each step:
   - Tell me what you are about to do
   - Ask for confirmation before major installs or config changes
   - Use the supporting config files as reference (copy them to the right locations)
   - Adapt commands to this machine OS and architecture
4. For secrets:
   - Place SSH keys in ~/.ssh/ with correct permissions (600 for keys, 700 for dir)
   - Place .env files in their respective project directories
   - Restore API tokens to their config files
5. For manual steps - tell me what I need to do myself (app sign-ins, licenses, etc.)
6. After setup, run a quick verification - check that key tools work

Take it phase by phase.
RESTORE_EOF

  # Launch agent with the restore context (safe mode — no auto-approve
  # because restore actually modifies the system — user should approve changes)
  cd "$HOME"
  local agent_cmd=$(get_agent_safe_cmd)
  local agent=$(get_agent_type)
  case "$agent" in
    claude) $agent_cmd "$prompt_file" ;;
    codex|*) $agent_cmd -C "$restore_dir" "$(cat "$prompt_file")" ;;
  esac
  rm -f "$prompt_file"

  echo ""
  echo -e "${GRAY}Restore session complete. Snapshot files remain at: $restore_dir${NC}"
  echo -e "${GRAY}You can delete them with: rm -rf $restore_dir${NC}"
}

# =============================================================================
# Main Entry Point
# =============================================================================

main() {
  # @alias stripping: crab @pf ws 1 → resolve project, then route "ws 1"
  PROJECT_ALIAS=""
  if [[ "${1:-}" == @* ]]; then
    resolve_project "$1"
    shift
  fi

  # Resolve user-configured command aliases (before project + command dispatch)
  if resolved=$(resolve_command_aliases "${1:-}"); then
    shift
    # Split resolved alias into words without glob expansion
    read -r -a resolved_parts <<<"$resolved"
    set -- "${resolved_parts[@]}" "$@"
  fi

  # For project-aware commands, resolve project (cwd-first, then default)
  case "${1:-}" in
    ""|"ws"|"workspace"|"restart"|"reset"|"refresh"|"continue"|"resume"|"cleanup"|"clean"|"destroy"|"rm"|"remove"|"new"|"create"|"wip"|"save"|"config"|"doctor"|"ports"|"shared"|"status"|"snapshot"|"receive"|"handoff"|"rewind"|"timetravel"|"tt"|"pair"|"join"|"spectate"|"watch"|"mood"|"mobile"|"msg"|"message"|"slack"|"tk"|"toolkit"|"pf"|"promptfoo"|"draw"|"compare"|"diff"|"session"|"review"|"court"|"ticket"|"tkt"|"agent")
      if [ -z "$PROJECT_ALIAS" ]; then
        # Legacy migration check
        if is_legacy_config; then
          check_legacy_migration
        fi
        # Try cwd-based detection first (handles: crab restart from a workspace dir)
        resolve_project_from_cwd 2>/dev/null || resolve_project ""
      fi
      ;;
  esac

  case "${1:-}" in
    "")
      list_workspaces
      ;;
    "ws"|"workspace")
      load_config
      validate_config
      handle_ws_command "${@:2}"
      ;;
    "init")
      show_init "${@:2}"
      ;;
    "config")
      if [ "${2:-}" = "scan" ]; then
        config_scan
      else
        show_config
      fi
      ;;
    "doctor")
      show_doctor
      ;;
    "cheat"|"help"|"-h"|"--help")
      if [ "${1:-}" = "cheat" ]; then
        show_cheat
      else
        show_help
      fi
      ;;
    "ports")
      show_ports
      ;;
    "shared")
      show_shared
      ;;
    "tk"|"toolkit")
      handle_tk_command "${@:2}"
      ;;
    "slack")
      handle_slack_command "${@:2}"
      ;;
    "pf"|"promptfoo")
      # Promptfoo target discovery agent
      handle_pf_command "${@:2}"
      ;;
    "tax")
      # Tax document organizer plugin
      handle_tax_command "${@:2}"
      ;;
    "draw")
      # Collaborative Excalidraw sessions
      handle_draw_command "${@:2}"
      ;;
    "md"|"markdown")
      # Render markdown file in browser
      cmd_md "${@:2}"
      ;;
    "compare"|"diff")
      # Compare two files in browser
      cmd_compare "${@:2}"
      ;;
    "env")
      # Environment snapshot & restore
      handle_env_command "${@:2}"
      ;;
    "session"|"sessions")
      # Session management
      handle_session_command "${@:2}"
      ;;
    "review")
      # PR review sessions
      load_config 2>/dev/null || true
      handle_review_command "${@:2}"
      ;;
    "court")
      # Court review - thorough multi-agent review
      load_config 2>/dev/null || true
      handle_court_command "${@:2}"
      ;;
    "ticket"|"tkt")
      # Linear ticket workspace
      load_config
      validate_config
      handle_ticket_command "${@:2}"
      ;;
    "projects"|"project"|"proj")
      show_projects "${@:2}"
      ;;
    "default")
      set_default_project "${2:-}"
      ;;
    "alias"|"aliases")
      handle_alias_command "${@:2}"
      ;;
    "new"|"create")
      # Shorthand: crab new -> crab ws new
      load_config
      validate_config
      create_new_workspace
      ;;
    "restart"|"reset"|"refresh")
      load_config
      validate_config
      num=$(detect_workspace)
      if [ -z "$num" ]; then
        error "Cannot detect workspace. Run from a workspace dir or use: crab ws <N> restart"
        exit 1
      fi
      restart_workspace "$num"
      ;;
    "continue"|"resume")
      load_config
      validate_config
      num=$(detect_workspace)
      if [ -z "$num" ]; then
        error "Cannot detect workspace. Run from a workspace dir or use: crab ws <N> continue"
        exit 1
      fi
      continue_workspace "$num"
      ;;
    "cleanup"|"clean")
      load_config
      validate_config
      num=$(detect_workspace)
      if [ -z "$num" ]; then
        error "Cannot detect workspace. Run from a workspace dir or use: crab ws <N> cleanup"
        exit 1
      fi
      cleanup_workspace "$num"
      ;;
    "save")
      # Shortcut: crab save ["name"] [--restart]
      load_config
      validate_config
      num=$(detect_workspace)
      if [ -z "$num" ]; then
        error "Not in a workspace directory. Run from inside a workspace."
        exit 1
      fi
      local do_restart="false"
      local custom_name=""
      shift  # Remove "save"
      while [ $# -gt 0 ]; do
        case "$1" in
          --restart|-r)
            do_restart="true"
            shift
            ;;
          *)
            custom_name="$1"
            shift
            ;;
        esac
      done
      wip_save "$num" "$do_restart" "$custom_name"
      ;;
    "destroy"|"rm"|"remove")
      load_config
      validate_config
      if [ -n "${2:-}" ] && [[ "${2:-}" =~ ^[0-9]+$ ]]; then
        destroy_workspace "$2" "${3:-}"
      else
        num=$(detect_workspace)
        if [ -z "$num" ]; then
          error "Specify workspace: crab destroy <N>"
          exit 1
        fi
        destroy_workspace "$num" "${2:-}"
      fi
      ;;
    "wip")
      load_config
      validate_config
      num=$(detect_workspace)

      case "${2:-}" in
        "save")
          # Save requires being in a workspace
          if [ -z "$num" ]; then
            error "Not in a workspace directory. Use: crab ws <N> wip save"
            exit 1
          fi
          # Parse: crab wip save ["name"] [--restart]
          local do_restart="false"
          local custom_name=""
          shift 2  # Remove "wip" and "save"
          while [ $# -gt 0 ]; do
            case "$1" in
              --restart|-r)
                do_restart="true"
                shift
                ;;
              *)
                # Anything else is the custom name
                custom_name="$1"
                shift
                ;;
            esac
          done
          wip_save "$num" "$do_restart" "$custom_name"
          ;;
        "ls"|"list")
          # List is project-level by default, unless --ws flag
          local dir_ws=$(detect_workspace_from_dir)
          if [ "${3:-}" = "--ws" ] && [ -n "$num" ]; then
            # Explicitly request workspace-specific list
            wip_list "$num"
          elif [ "${3:-}" = "--ws" ] && [ -n "$dir_ws" ]; then
            wip_list "$dir_ws"
          else
            # Project-level view (default)
            wip_list_global
          fi
          ;;
        "restore")
          # Restore by index with optional target workspace
          # Usage: crab wip restore [<N>] [--to <ws>]
          local target_ws=""
          local index=""
          local open_after="true"
          shift 2  # Remove "wip" and "restore"

          # Parse arguments
          while [ $# -gt 0 ]; do
            case "$1" in
              --to)
                target_ws="${2:-}"
                shift 2
                ;;
              --open|-o)
                open_after="true"
                shift
                ;;
              *)
                # First non-flag argument is the index
                if [ -z "$index" ] && [[ "$1" =~ ^[0-9]+$ ]]; then
                  index="$1"
                fi
                shift
                ;;
            esac
          done

          # Default to current workspace if no --to specified
          if [ -z "$target_ws" ] && [ -n "$num" ]; then
            target_ws="$num"
          fi

          if [ -z "$index" ]; then
            # Interactive restore
            wip_restore_global "$target_ws" "$open_after"
          else
            wip_restore_by_index "$index" "$target_ws" "$open_after"
          fi
          ;;
        "--continue"|"-c")
          if [ -z "$num" ]; then
            error "Not in a workspace directory. Use: crab wip restore"
            exit 1
          fi
          # Check for --open flag
          local open_flag="false"
          if [ "${3:-}" = "--open" ] || [ "${3:-}" = "-o" ]; then
            open_flag="true"
          fi
          wip_continue "$num" "$open_flag"
          ;;
        "--resume"|"-r"|"resume")
          if [ -z "$num" ]; then
            # Global interactive restore
            wip_restore_global
          else
            wip_resume "$num"
          fi
          ;;
        "delete"|"rm")
          if [ -z "$num" ]; then
            error "Not in a workspace directory. Use: crab ws <N> wip delete <name>"
            exit 1
          fi
          wip_delete "$num" "${3:-}"
          ;;
        *)
          echo -e "${CYAN}WIP Commands:${NC}"
          echo ""
          if [ -n "$num" ]; then
            echo -e "  ${BOLD}In workspace $num:${NC}"
            echo "    crab wip save [--restart]  Save current changes"
            echo "    crab wip ls                List WIPs for this workspace"
            echo "    crab wip --continue        Restore most recent WIP"
            echo "    crab wip --resume          Interactive WIP selection"
            echo "    crab wip delete <name>     Delete a WIP state"
            echo ""
          fi
          echo -e "  ${BOLD}Global commands:${NC}"
          echo "    crab wip ls                List all WIPs (global view)"
          echo "    crab wip restore           Interactive restore from all WIPs"
          echo "    crab wip restore <N>       Restore WIP #N to its original workspace"
          echo "    crab wip restore <N> --to <ws>  Restore WIP #N to workspace <ws>"
          echo "    crab wip restore <N> --open     Restore and open workspace"
          echo ""
          echo "  Restores: branch, commits, staged/unstaged changes, submodules"
          ;;
      esac
      ;;
    "agent")
      case "${2:-}" in
        "sync")
          handle_agent_sync_command "${@:3}"
          ;;
        "status"|"st")
          agent_sync_status
          ;;
        *)
          handle_agent_sync_command "help"
          ;;
      esac
      ;;
    "mood")
      show_mood
      ;;
    "mobile")
      handle_mobile_command "${@:2}"
      ;;
    "msg"|"message")
      handle_msg_command "${@:2}"
      ;;
    "handoff")
      handle_handoff_command "${@:2}"
      ;;
    "rewind"|"timetravel"|"tt")
      handle_rewind_command "${@:2}"
      ;;
    "pair")
      handle_pair_command "${@:2}"
      ;;
    "join")
      if [ -z "${2:-}" ]; then
        error "Usage: crab join <workspace-number>"
        exit 1
      fi
      join_pair_session "$2" "pair"
      ;;
    "spectate"|"watch")
      if [ -z "${2:-}" ]; then
        error "Usage: crab spectate <workspace-number>"
        exit 1
      fi
      join_pair_session "$2" "spectate"
      ;;
    "snapshot")
      load_config
      validate_config
      local num=$(detect_workspace)
      if [ -z "$num" ]; then
        error "Not in a workspace. Use: crab snapshot from workspace directory"
        exit 1
      fi
      save_snapshot "$num" "${2:-manual}"
      ;;
    "receive")
      load_config
      validate_config
      receive_handoff "${2:-}" "${4:-}"
      ;;
    "notify")
      crab_notify "${2:-Crab Update}" "${3:-}" "${4:-default}"
      ;;
    "status")
      show_status "${2:-terminal}"
      ;;
    "update"|"upgrade")
      # Self-update from GitHub
      echo -e "${CYAN}Updating crabcode...${NC}"
      local install_dir=$(dirname "$(realpath "$0")")
      local tmp_file="/tmp/crabcode-update-$$"

      # Cache-bust to bypass GitHub raw CDN cache (max-age=300)
      if curl -fsSL -H 'Cache-Control: no-cache' "https://raw.githubusercontent.com/promptfoo/crabcode/main/src/crabcode?t=$(date +%s)" -o "$tmp_file" 2>/dev/null; then
        local new_version=$(grep '^VERSION=' "$tmp_file" | head -1 | cut -d'"' -f2)
        local current_version="$VERSION"

        if [ -z "$new_version" ]; then
          error "Downloaded file is invalid (no version found)"
          rm -f "$tmp_file"
          exit 1
        fi

        if [ "$new_version" = "$current_version" ]; then
          echo -e "${GREEN}Already up to date (v$VERSION)${NC}"
        else
          chmod +x "$tmp_file"
          mv "$tmp_file" "$install_dir/crabcode"
          echo -e "${GREEN}Updated: v$current_version → v$new_version${NC}"
        fi
      else
        error "Failed to download update"
        rm -f "$tmp_file"
        exit 1
      fi
      rm -f "$tmp_file"
      ;;
    "--version"|"-v")
      echo '  /)🦀(\'
      echo "crabcode v$VERSION"
      ;;
    *)
      # Backwards compatibility: crab <N> -> crab ws <N>
      if [[ "$1" =~ ^[0-9]+$ ]]; then
        if [ -z "$PROJECT_ALIAS" ]; then
          if is_legacy_config; then
            check_legacy_migration
          fi
          resolve_project_from_cwd 2>/dev/null || resolve_project ""
        fi
        load_config
        validate_config
        handle_ws_command "$@"
      else
        error "Unknown command: $1"
        echo ""
        echo "Usage: crab ws <N>       Open workspace N"
        echo "       crab @alias ws <N> Open workspace for specific project"
        echo "       crab ws new       Create new workspace"
        echo "       crab restart      Restart current workspace"
        echo "       crab projects     List registered projects"
        echo "       crab cheat        Show all commands"
        exit 1
      fi
      ;;
  esac
}

# Source guard: allow this script to be sourced for testing without executing main
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi
