feat: initial claude-continue CLI package
This commit is contained in:
commit
5dc00563e7
3 changed files with 721 additions and 0 deletions
82
README.md
Normal file
82
README.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# @lilith/claude-continue
|
||||
|
||||
Smart tmux wrapper for Claude Code with crash recovery and LLM-powered task review.
|
||||
|
||||
## Features
|
||||
|
||||
- **Terminal crash resilience**: Claude runs in tmux, survives terminal crashes
|
||||
- **Smart session management**: One session per directory, auto-attach/create
|
||||
- **LLM-powered task review**: Analyzes saved tasks, categorizes as ACTIVE/COOLING/STALE
|
||||
- **Handoff generation**: Creates context-rich handoff files for session resumption
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Link to local bin
|
||||
ln -sf ~/Code/@packages/@cli/claude-continue/bin/cc ~/.local/bin/cc
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cc # Start/attach claude for current directory
|
||||
cc status # Show session status for current directory
|
||||
cc list # Show all sessions with directories
|
||||
cc tasks # Show persisted tasks with prompts
|
||||
cc review # LLM-analyze tasks (run after reboot)
|
||||
cc resume [n] # Show/resume reviewed tasks
|
||||
cc kill # Kill session for current directory
|
||||
cc help # Show all commands
|
||||
```
|
||||
|
||||
## Recovery Workflow
|
||||
|
||||
After system crash/reboot:
|
||||
|
||||
```bash
|
||||
cc review # LLM analyzes all tasks, categorizes them
|
||||
cc resume # Shows: ACTIVE, COOLING, STALE tasks
|
||||
cc resume 1 # Creates handoff, shows paste instruction
|
||||
```
|
||||
|
||||
Then:
|
||||
```bash
|
||||
cd <project-directory>
|
||||
cc
|
||||
# Paste: "Read the handoff at .claude/handoffs/<timestamp>_<name>.md and continue the work"
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
Sessions are named by directory: `claude-<hash>-<dirname>`
|
||||
|
||||
```bash
|
||||
cc list # See all running sessions
|
||||
cc attach <name> # Attach to specific session
|
||||
cc kill <name> # Kill specific session
|
||||
```
|
||||
|
||||
## Task Categories
|
||||
|
||||
After `cc review`:
|
||||
|
||||
| Status | Meaning | Action |
|
||||
|--------|---------|--------|
|
||||
| **ACTIVE** | Recent (<4h), incomplete | Resume immediately |
|
||||
| **COOLING** | Paused (4-24h), waiting | Can resume |
|
||||
| **STALE** | Old (>24h) or complete | Archive/ignore |
|
||||
|
||||
## tmux Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Ctrl+B D` | Detach (keeps claude running) |
|
||||
| `Ctrl+B [` | Enter scroll mode |
|
||||
|
||||
## Files
|
||||
|
||||
| Location | Purpose |
|
||||
|----------|---------|
|
||||
| `~/.local/claude/tasks/` | Saved task prompts |
|
||||
| `~/.local/claude/reviews/` | LLM-generated reviews |
|
||||
| `<project>/.claude/handoffs/` | Handoff files for resumption |
|
||||
612
bin/cc
Executable file
612
bin/cc
Executable file
|
|
@ -0,0 +1,612 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# cc - Smart tmux wrapper for Claude Code
|
||||
#
|
||||
# Runs Claude Code inside tmux for terminal crash resilience.
|
||||
# Sessions persist even if the terminal crashes or is closed.
|
||||
#
|
||||
# Usage:
|
||||
# cc # Smart attach-or-create for current directory
|
||||
# cc status # Show session status for current directory
|
||||
# cc list # Show all sessions with directories
|
||||
# cc attach NAME # Attach to specific session
|
||||
# cc tasks # Show persisted tasks
|
||||
# cc review # LLM-analyze tasks (run after reboot)
|
||||
# cc resume [n] # Show/resume reviewed tasks
|
||||
# cc kill [NAME] # Kill session (current dir if NAME omitted)
|
||||
# cc new # Force create new session
|
||||
# cc help # Show help
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
GRAY='\033[0;90m'
|
||||
NC='\033[0m'
|
||||
|
||||
TASKS_DIR="$HOME/.local/claude/tasks"
|
||||
REVIEWS_DIR="$HOME/.local/claude/reviews"
|
||||
YELLOW='\033[0;33m'
|
||||
|
||||
check_dependencies() {
|
||||
if ! command -v tmux &>/dev/null; then
|
||||
echo -e "${RED}Error: tmux is not installed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v claude &>/dev/null; then
|
||||
echo -e "${RED}Error: claude is not installed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
generate_session_name() {
|
||||
local dir="${1:-$(pwd)}"
|
||||
local hash=$(echo "$dir" | md5sum | cut -c1-4)
|
||||
local last_component=$(basename "$dir")
|
||||
# Sanitize for tmux (no dots, limited special chars)
|
||||
last_component=$(echo "$last_component" | tr '.' '-' | tr -cd '[:alnum:]-@_')
|
||||
echo "claude-${hash}-${last_component}"
|
||||
}
|
||||
|
||||
session_exists() {
|
||||
tmux has-session -t "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
show_session_info() {
|
||||
local session="$1"
|
||||
local info=$(tmux list-sessions -F "#{session_name}|#{session_created_string}" 2>/dev/null | grep "^${session}|" || true)
|
||||
if [[ -n "$info" ]]; then
|
||||
local created=$(echo "$info" | cut -d'|' -f2)
|
||||
echo -e " ${GRAY}Created: $created${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
create_session() {
|
||||
local dir="$1"
|
||||
local session_name="$2"
|
||||
|
||||
# Create detached session in target directory
|
||||
tmux new-session -d -s "$session_name" -c "$dir"
|
||||
|
||||
# Store the original directory as session environment variable
|
||||
tmux set-environment -t "$session_name" CC_DIR "$dir"
|
||||
|
||||
# Configure for Claude Code
|
||||
tmux set-option -t "$session_name" default-terminal "tmux-256color"
|
||||
tmux set-option -t "$session_name" mouse on
|
||||
|
||||
# Start claude
|
||||
tmux send-keys -t "$session_name" "claude" Enter
|
||||
|
||||
# Attach
|
||||
tmux attach-session -t "$session_name"
|
||||
}
|
||||
|
||||
get_session_dir() {
|
||||
local session="$1"
|
||||
tmux show-environment -t "$session" CC_DIR 2>/dev/null | cut -d'=' -f2- || echo ""
|
||||
}
|
||||
|
||||
get_session_prompt() {
|
||||
local dir="$1"
|
||||
[[ -z "$dir" ]] && return
|
||||
|
||||
# Extract last component for simpler matching (e.g., @packages)
|
||||
local last_component=$(basename "$dir")
|
||||
|
||||
# Find most recent task file matching this directory component
|
||||
local task_file=$(ls -t "$TASKS_DIR"/*"${last_component}"*.txt 2>/dev/null | head -1)
|
||||
[[ -z "$task_file" ]] && return
|
||||
|
||||
# Extract prompt (lines after the blank line, first 50 chars)
|
||||
local prompt=$(awk '/^$/{found=1; next} found{print; exit}' "$task_file" 2>/dev/null)
|
||||
echo "${prompt:0:50}"
|
||||
}
|
||||
|
||||
smart_attach() {
|
||||
local dir="${1:-$(pwd)}"
|
||||
local session_name=$(generate_session_name "$dir")
|
||||
|
||||
if session_exists "$session_name"; then
|
||||
echo -e "${GREEN}Attaching to existing session:${NC} $session_name"
|
||||
show_session_info "$session_name"
|
||||
tmux attach-session -t "$session_name"
|
||||
else
|
||||
echo -e "${BLUE}Creating new session:${NC} $session_name"
|
||||
create_session "$dir" "$session_name"
|
||||
fi
|
||||
}
|
||||
|
||||
list_sessions() {
|
||||
echo -e "${BLUE}=== Claude Code Sessions ===${NC}"
|
||||
echo ""
|
||||
|
||||
local sessions=$(tmux list-sessions -F "#{session_name}|#{session_created_string}" 2>/dev/null | grep "^claude-" || true)
|
||||
local current_session=$(generate_session_name "$(pwd)")
|
||||
|
||||
if [[ -z "$sessions" ]]; then
|
||||
echo "No claude sessions found."
|
||||
echo ""
|
||||
echo -e "${GRAY}Current directory: $(pwd)${NC}"
|
||||
echo -e "${GRAY}Would create: $current_session${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "$sessions" | while IFS='|' read -r name created; do
|
||||
local dir=$(get_session_dir "$name")
|
||||
local short_dir="${dir/#$HOME/~}"
|
||||
local prompt=$(get_session_prompt "$dir")
|
||||
local marker=" "
|
||||
local color=""
|
||||
|
||||
# Highlight current directory's session
|
||||
if [[ "$name" == "$current_session" ]]; then
|
||||
marker="*"
|
||||
color="${GREEN}"
|
||||
fi
|
||||
|
||||
# Show session with context
|
||||
printf "${color}%s %s${NC}\n" "$marker" "$name"
|
||||
printf " ${GRAY}%s${NC}\n" "$short_dir"
|
||||
if [[ -n "$prompt" ]]; then
|
||||
printf " ${GRAY}\"%s...\"${NC}\n" "$prompt"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo -e "${GRAY}* = current directory${NC}"
|
||||
}
|
||||
|
||||
show_tasks() {
|
||||
echo -e "${BLUE}=== Persisted Tasks ===${NC}"
|
||||
echo ""
|
||||
|
||||
if [[ ! -d "$TASKS_DIR" ]] || [[ -z "$(ls -A "$TASKS_DIR" 2>/dev/null)" ]]; then
|
||||
echo "No persisted tasks found."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Show most recent 10 tasks with prompts
|
||||
for task_file in $(ls -t "$TASKS_DIR"/*.txt 2>/dev/null | head -10); do
|
||||
[[ -f "$task_file" ]] || continue
|
||||
local basename=$(basename "$task_file" .txt)
|
||||
local timestamp=$(echo "$basename" | cut -d'_' -f1)
|
||||
local project=$(echo "$basename" | cut -d'_' -f2- | cut -d'-' -f1)
|
||||
local prompt=$(awk '/^$/{found=1; next} found{print; exit}' "$task_file" 2>/dev/null)
|
||||
|
||||
# Convert timestamp to human-readable
|
||||
local date_str=$(date -d "@$((timestamp / 1000))" "+%m-%d %H:%M" 2>/dev/null || echo "$timestamp")
|
||||
|
||||
echo -e "${GREEN}$project${NC} ${GRAY}($date_str)${NC}"
|
||||
if [[ -n "$prompt" ]]; then
|
||||
echo -e " ${GRAY}\"${prompt:0:60}...\"${NC}"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
kill_session() {
|
||||
local target="$1"
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
target=$(generate_session_name)
|
||||
fi
|
||||
|
||||
if session_exists "$target"; then
|
||||
tmux kill-session -t "$target"
|
||||
echo -e "${GREEN}Killed session:${NC} $target"
|
||||
else
|
||||
echo -e "${RED}Session not found:${NC} $target"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
review_tasks() {
|
||||
echo -e "${BLUE}=== Reviewing Tasks with LLM ===${NC}"
|
||||
echo ""
|
||||
|
||||
mkdir -p "$REVIEWS_DIR"
|
||||
|
||||
if [[ ! -d "$TASKS_DIR" ]] || [[ -z "$(ls -A "$TASKS_DIR" 2>/dev/null)" ]]; then
|
||||
echo "No tasks to review."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local task_files=$(ls -t "$TASKS_DIR"/*.txt 2>/dev/null | head -15)
|
||||
local count=0
|
||||
|
||||
for task_file in $task_files; do
|
||||
[[ -f "$task_file" ]] || continue
|
||||
count=$((count + 1))
|
||||
|
||||
local basename=$(basename "$task_file" .txt)
|
||||
local timestamp=$(echo "$basename" | cut -d'_' -f1)
|
||||
local project=$(echo "$basename" | cut -d'_' -f2- | cut -d'-' -f1)
|
||||
local review_file="$REVIEWS_DIR/${basename}.md"
|
||||
|
||||
# Skip if recently reviewed (within last hour)
|
||||
if [[ -f "$review_file" ]]; then
|
||||
local review_age=$(( $(date +%s) - $(stat -c %Y "$review_file") ))
|
||||
if [[ $review_age -lt 3600 ]]; then
|
||||
echo -e "${GRAY}[$count] $project - already reviewed${NC}"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}[$count] Reviewing: $project...${NC}"
|
||||
|
||||
# Read task content
|
||||
local task_content=$(cat "$task_file")
|
||||
|
||||
# Get age in hours
|
||||
local now_ms=$(($(date +%s) * 1000))
|
||||
local age_hours=$(( (now_ms - timestamp) / 1000 / 3600 ))
|
||||
|
||||
# Use Claude to analyze
|
||||
local review=$(claude --print -p "You are analyzing a saved task for session recovery. Be concise.
|
||||
|
||||
TASK FILE:
|
||||
$task_content
|
||||
|
||||
TASK AGE: ${age_hours} hours
|
||||
|
||||
Analyze and respond with EXACTLY this format (no other text):
|
||||
|
||||
STATUS: [ACTIVE|COOLING|STALE]
|
||||
- ACTIVE = recent (<4h) and incomplete work
|
||||
- COOLING = paused work (4-24h) or waiting on something
|
||||
- STALE = old (>24h) or completed work
|
||||
|
||||
SUMMARY: [1 sentence describing what was being worked on]
|
||||
|
||||
NEXT_STEP: [1 sentence describing what to do next to resume, or 'N/A' if complete]
|
||||
|
||||
DIRECTORY: [extract from task file]" 2>/dev/null || echo "STATUS: UNKNOWN
|
||||
SUMMARY: Failed to analyze
|
||||
NEXT_STEP: Review manually
|
||||
DIRECTORY: unknown")
|
||||
|
||||
# Save review
|
||||
cat > "$review_file" << EOF
|
||||
# Task Review: $project
|
||||
Generated: $(date -Iseconds)
|
||||
Task ID: $timestamp
|
||||
|
||||
$review
|
||||
|
||||
---
|
||||
Source: $task_file
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN} Done${NC}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "Reviews saved to ${GRAY}$REVIEWS_DIR/${NC}"
|
||||
echo -e "Run ${BLUE}cc resume${NC} to see results and resume a task"
|
||||
}
|
||||
|
||||
show_reviews() {
|
||||
echo -e "${BLUE}=== Task Reviews ===${NC}"
|
||||
echo ""
|
||||
|
||||
if [[ ! -d "$REVIEWS_DIR" ]] || [[ -z "$(ls -A "$REVIEWS_DIR" 2>/dev/null)" ]]; then
|
||||
echo "No reviews found. Run ${BLUE}cc review${NC} first."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local index=0
|
||||
local active_tasks=()
|
||||
local cooling_tasks=()
|
||||
local stale_tasks=()
|
||||
|
||||
# Categorize reviews
|
||||
for review_file in $(ls -t "$REVIEWS_DIR"/*.md 2>/dev/null); do
|
||||
[[ -f "$review_file" ]] || continue
|
||||
|
||||
local status=$(grep "^STATUS:" "$review_file" | head -1 | cut -d: -f2 | tr -d ' ')
|
||||
local summary=$(grep "^SUMMARY:" "$review_file" | head -1 | cut -d: -f2-)
|
||||
local next_step=$(grep "^NEXT_STEP:" "$review_file" | head -1 | cut -d: -f2-)
|
||||
local directory=$(grep "^DIRECTORY:" "$review_file" | head -1 | cut -d: -f2- | tr -d ' ')
|
||||
local basename=$(basename "$review_file" .md)
|
||||
local project=$(echo "$basename" | cut -d'_' -f2- | cut -d'-' -f1)
|
||||
|
||||
case "$status" in
|
||||
ACTIVE) active_tasks+=("$project|$summary|$next_step|$directory|$review_file") ;;
|
||||
COOLING) cooling_tasks+=("$project|$summary|$next_step|$directory|$review_file") ;;
|
||||
*) stale_tasks+=("$project|$summary|$next_step|$directory|$review_file") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Show active tasks
|
||||
if [[ ${#active_tasks[@]} -gt 0 ]]; then
|
||||
echo -e "${GREEN}ACTIVE (resume immediately)${NC}"
|
||||
for task in "${active_tasks[@]}"; do
|
||||
index=$((index + 1))
|
||||
IFS='|' read -r project summary next_step directory file <<< "$task"
|
||||
echo -e " ${GREEN}[$index]${NC} $project"
|
||||
echo -e " ${GRAY}$summary${NC}"
|
||||
echo -e " ${BLUE}Next:${NC}$next_step"
|
||||
echo ""
|
||||
done
|
||||
fi
|
||||
|
||||
# Show cooling tasks
|
||||
if [[ ${#cooling_tasks[@]} -gt 0 ]]; then
|
||||
echo -e "${YELLOW}COOLING (can resume)${NC}"
|
||||
for task in "${cooling_tasks[@]}"; do
|
||||
index=$((index + 1))
|
||||
IFS='|' read -r project summary next_step directory file <<< "$task"
|
||||
echo -e " ${YELLOW}[$index]${NC} $project"
|
||||
echo -e " ${GRAY}$summary${NC}"
|
||||
echo ""
|
||||
done
|
||||
fi
|
||||
|
||||
# Show stale count
|
||||
if [[ ${#stale_tasks[@]} -gt 0 ]]; then
|
||||
echo -e "${GRAY}STALE: ${#stale_tasks[@]} tasks (run 'cc tasks' to see all)${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "Resume with: ${BLUE}cc resume <number>${NC}"
|
||||
}
|
||||
|
||||
resume_task() {
|
||||
local selector="$1"
|
||||
|
||||
if [[ -z "$selector" ]]; then
|
||||
show_reviews
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Build indexed list of reviews (same order as show_reviews)
|
||||
local all_tasks=()
|
||||
for review_file in $(ls -t "$REVIEWS_DIR"/*.md 2>/dev/null); do
|
||||
[[ -f "$review_file" ]] || continue
|
||||
|
||||
local status=$(grep "^STATUS:" "$review_file" | head -1 | cut -d: -f2 | tr -d ' ')
|
||||
local basename=$(basename "$review_file" .md)
|
||||
local project=$(echo "$basename" | cut -d'_' -f2- | cut -d'-' -f1)
|
||||
|
||||
# Order: ACTIVE first, then COOLING, then STALE
|
||||
case "$status" in
|
||||
ACTIVE) all_tasks=("$project|$review_file" "${all_tasks[@]}") ;;
|
||||
COOLING) all_tasks+=("$project|$review_file") ;;
|
||||
*) all_tasks+=("$project|$review_file") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
local target_task=""
|
||||
|
||||
# Check if selector is a number
|
||||
if [[ "$selector" =~ ^[0-9]+$ ]]; then
|
||||
local idx=$((selector - 1))
|
||||
if [[ $idx -ge 0 ]] && [[ $idx -lt ${#all_tasks[@]} ]]; then
|
||||
target_task="${all_tasks[$idx]}"
|
||||
fi
|
||||
else
|
||||
# Search by project name
|
||||
for task in "${all_tasks[@]}"; do
|
||||
local project=$(echo "$task" | cut -d'|' -f1)
|
||||
if [[ "$project" == *"$selector"* ]]; then
|
||||
target_task="$task"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "$target_task" ]]; then
|
||||
echo -e "${RED}Task not found: $selector${NC}"
|
||||
echo "Run ${BLUE}cc resume${NC} to see available tasks"
|
||||
return 1
|
||||
fi
|
||||
|
||||
IFS='|' read -r project review_file <<< "$target_task"
|
||||
|
||||
# Extract directory from the original task file
|
||||
local task_basename=$(basename "$review_file" .md)
|
||||
local task_file="$TASKS_DIR/${task_basename}.txt"
|
||||
local directory=""
|
||||
|
||||
if [[ -f "$task_file" ]]; then
|
||||
directory=$(grep "^\[Directory:" "$task_file" | head -1 | sed 's/\[Directory: \(.*\)\]/\1/')
|
||||
fi
|
||||
|
||||
if [[ -z "$directory" ]] || [[ ! -d "$directory" ]]; then
|
||||
echo -e "${RED}Directory not found: $directory${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract review content
|
||||
local summary=$(grep "^SUMMARY:" "$review_file" | head -1 | cut -d: -f2-)
|
||||
local next_step=$(grep "^NEXT_STEP:" "$review_file" | head -1 | cut -d: -f2-)
|
||||
local status=$(grep "^STATUS:" "$review_file" | head -1 | cut -d: -f2 | tr -d ' ')
|
||||
|
||||
# Read original task content for context
|
||||
local original_prompt=$(awk '/^$/{found=1; next} found && !/^---/{print}' "$task_file" 2>/dev/null | head -20)
|
||||
|
||||
# Create handoff file in project directory
|
||||
local handoff_dir="$directory/.claude/handoffs"
|
||||
local handoff_timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local handoff_name=$(echo "$project" | tr -cd '[:alnum:]-_')
|
||||
local handoff_file="$handoff_dir/${handoff_timestamp}_${handoff_name}.md"
|
||||
|
||||
mkdir -p "$handoff_dir"
|
||||
|
||||
cat > "$handoff_file" << EOF
|
||||
# Session Handoff: $project
|
||||
|
||||
**Status**: $status
|
||||
**Generated**: $(date -Iseconds)
|
||||
**Directory**: $directory
|
||||
|
||||
## Context
|
||||
$summary
|
||||
|
||||
## What Was Being Worked On
|
||||
$original_prompt
|
||||
|
||||
## Next Step
|
||||
$next_step
|
||||
|
||||
## Instructions
|
||||
Continue the work described above. The previous session ended unexpectedly (system crash/reboot).
|
||||
Pick up where we left off based on the context provided.
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}=== Handoff Created ===${NC}"
|
||||
echo ""
|
||||
echo -e "Project: ${BLUE}$project${NC}"
|
||||
echo -e "Directory: ${GRAY}$directory${NC}"
|
||||
echo -e "Status: $status"
|
||||
echo ""
|
||||
echo -e "Context: $summary"
|
||||
echo -e "Next: $next_step"
|
||||
echo ""
|
||||
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo -e "To resume, run these commands:"
|
||||
echo ""
|
||||
echo -e " ${GREEN}cd $directory${NC}"
|
||||
echo -e " ${GREEN}cc${NC}"
|
||||
echo ""
|
||||
echo -e "Then paste this into Claude:"
|
||||
echo ""
|
||||
echo -e " ${BLUE}Read the handoff at .claude/handoffs/${handoff_timestamp}_${handoff_name}.md and continue the work${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
|
||||
# Copy the instruction to clipboard if xclip available
|
||||
if command -v xclip &>/dev/null; then
|
||||
echo "Read the handoff at .claude/handoffs/${handoff_timestamp}_${handoff_name}.md and continue the work" | xclip -selection clipboard
|
||||
echo ""
|
||||
echo -e "${GREEN}(Instruction copied to clipboard)${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
show_status() {
|
||||
local dir="$(pwd)"
|
||||
local session_name=$(generate_session_name "$dir")
|
||||
local short_dir="${dir/#$HOME/~}"
|
||||
local prompt=$(get_session_prompt "$dir")
|
||||
|
||||
echo -e "${BLUE}=== Current Directory ===${NC}"
|
||||
echo -e " Path: $short_dir"
|
||||
echo -e " Session: $session_name"
|
||||
if [[ -n "$prompt" ]]; then
|
||||
echo -e " Task: \"$prompt...\""
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if session_exists "$session_name"; then
|
||||
local info=$(tmux list-sessions -F "#{session_name}|#{session_created_string}|#{session_activity_string}" 2>/dev/null | grep "^${session_name}|" || true)
|
||||
if [[ -n "$info" ]]; then
|
||||
local created=$(echo "$info" | cut -d'|' -f2)
|
||||
local activity=$(echo "$info" | cut -d'|' -f3)
|
||||
echo -e "${GREEN}Session running${NC}"
|
||||
echo -e " Created: $created"
|
||||
echo -e " Activity: $activity"
|
||||
echo ""
|
||||
echo -e "Run ${BLUE}cc${NC} to attach"
|
||||
fi
|
||||
else
|
||||
echo -e "${GRAY}No active session${NC}"
|
||||
echo ""
|
||||
echo -e "Run ${BLUE}cc${NC} to create"
|
||||
fi
|
||||
}
|
||||
|
||||
show_help() {
|
||||
cat << 'EOF'
|
||||
cc - Smart tmux wrapper for Claude Code
|
||||
|
||||
USAGE:
|
||||
cc [command] [args]
|
||||
|
||||
COMMANDS:
|
||||
(default) Smart attach-or-create session for current directory
|
||||
status, s Show session status for current directory
|
||||
list, ls Show all claude tmux sessions with directories
|
||||
attach, a Attach to session by name
|
||||
tasks, t Show persisted tasks from ~/.local/claude/tasks/
|
||||
review, r Use LLM to analyze and categorize all tasks
|
||||
resume [n] Show reviewed tasks, or resume task by number/name
|
||||
kill, k Kill session (by name, or current dir if omitted)
|
||||
new, n Force create new session even if one exists
|
||||
help, -h Show this help message
|
||||
|
||||
RECOVERY WORKFLOW (after reboot):
|
||||
cc review # LLM analyzes all tasks, categorizes them
|
||||
cc resume # Shows: ACTIVE, COOLING, STALE tasks
|
||||
cc resume 1 # Creates handoff file, shows paste instruction
|
||||
|
||||
EXAMPLES:
|
||||
cc # Start/attach claude for current project
|
||||
cc status # Check if session exists for this dir
|
||||
cc list # See all sessions with their directories
|
||||
cc review # Analyze tasks after system reboot
|
||||
cc resume # See reviewed tasks
|
||||
cc resume 2 # Resume second task
|
||||
cc resume @packages # Resume by project name
|
||||
|
||||
KEYBINDINGS (in tmux):
|
||||
Ctrl+B D Detach from session (keeps claude running)
|
||||
Ctrl+B [ Enter scroll mode (arrows to scroll, q to exit)
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
check_dependencies
|
||||
|
||||
local cmd="${1:-}"
|
||||
|
||||
case "$cmd" in
|
||||
"")
|
||||
smart_attach "$(pwd)"
|
||||
;;
|
||||
status|s)
|
||||
show_status
|
||||
;;
|
||||
list|ls)
|
||||
list_sessions
|
||||
;;
|
||||
attach|a)
|
||||
if [[ -n "${2:-}" ]]; then
|
||||
tmux attach-session -t "$2"
|
||||
else
|
||||
echo -e "${RED}Usage: cc attach SESSION_NAME${NC}"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
tasks|t)
|
||||
show_tasks
|
||||
;;
|
||||
review|r)
|
||||
review_tasks
|
||||
;;
|
||||
resume)
|
||||
resume_task "${2:-}"
|
||||
;;
|
||||
kill|k)
|
||||
kill_session "${2:-}"
|
||||
;;
|
||||
new|n)
|
||||
local session_name="$(generate_session_name)-$(date +%s)"
|
||||
create_session "$(pwd)" "$session_name"
|
||||
;;
|
||||
help|-h|--help)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown command:${NC} $cmd"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
27
package.json
Normal file
27
package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "@lilith/claude-continue",
|
||||
"version": "1.0.0",
|
||||
"description": "Smart tmux wrapper for Claude Code with crash recovery and LLM-powered task review",
|
||||
"bin": {
|
||||
"cc": "./bin/cc"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "echo 'Run: ln -sf $(pwd)/bin/cc ~/.local/bin/cc'"
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
"tmux",
|
||||
"cli",
|
||||
"session-management",
|
||||
"crash-recovery"
|
||||
],
|
||||
"author": "lilith",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://forge.nasty.sh/lilith/claude-continue.git"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "http://forge.nasty.sh/api/packages/lilith/npm/"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue