#!/bin/bash set -euo pipefail # Constants PROJECT_DIR="$PWD" AGENT_SESSIONS_DIR="$PROJECT_DIR/.cli-agent-runner/agent-sessions" AGENTS_DIR="$PROJECT_DIR/.cli-agent-runner/agents" MAX_NAME_LENGTH=30 # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Show help message show_help() { cat << EOF Usage: cli-agent-runner.sh new [--agent ] [-p ] cli-agent-runner.sh resume [-p ] cli-agent-runner.sh list cli-agent-runner.sh list-agents cli-agent-runner.sh clean Commands: new Create a new session (optionally with an agent) resume Resume an existing session list List all sessions with metadata list-agents List all available agent definitions clean Remove all sessions Arguments: Name of the session (alphanumeric, dash, underscore only; max 30 chars) Name of the agent definition to use (optional for new command) Options: -p Session prompt (can be combined with stdin; -p content comes first) --agent Use a specific agent definition (only for new command) Examples: # Create new session (generic, no agent) ./cli-agent-runner.sh new architect -p "Design user auth system" # Create new session with agent ./cli-agent-runner.sh new architect --agent system-architect -p "Design user auth system" # Create new session from file cat prompt.md | ./cli-agent-runner.sh new architect --agent system-architect # Resume session (agent association remembered) ./cli-agent-runner.sh resume architect -p "Continue with API design" # Resume from file cat continue.md | ./cli-agent-runner.sh resume architect # Combine -p and stdin (concatenated) cat requirements.md | ./cli-agent-runner.sh new architect -p "Create architecture based on:" # List all sessions ./cli-agent-runner.sh list # List all agent definitions ./cli-agent-runner.sh list-agents # Remove all sessions ./cli-agent-runner.sh clean EOF } # Error message helper error() { echo -e "${RED}Error: $1${NC}" >&2 exit 1 } # Ensure required directories exist ensure_directories() { mkdir -p "$AGENT_SESSIONS_DIR" mkdir -p "$AGENTS_DIR" } # Validate session name validate_session_name() { local name="$1" # Check if empty if [ -z "$name" ]; then error "Session name cannot be empty" fi # Check length if [ ${#name} -gt $MAX_NAME_LENGTH ]; then error "Session name too long (max $MAX_NAME_LENGTH characters): $name" fi # Check for valid characters (alphanumeric, dash, underscore only) if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then error "Session name contains invalid characters. Only alphanumeric, dash (-), and underscore (_) are allowed: $name" fi } # Get prompt from -p flag and/or stdin get_prompt() { local prompt_arg="$1" local final_prompt="" # Add -p content first if provided if [ -n "$prompt_arg" ]; then final_prompt="$prompt_arg" fi # Check if stdin has data if [ ! -t 0 ]; then # Read from stdin local stdin_content stdin_content=$(cat) if [ -n "$stdin_content" ]; then # If we already have prompt from -p, add newline separator if [ -n "$final_prompt" ]; then final_prompt="${final_prompt}"$'\n'"${stdin_content}" else final_prompt="$stdin_content" fi fi fi # Check if we got any prompt at all if [ -z "$final_prompt" ]; then error "No prompt provided. Use -p flag or pipe prompt via stdin" fi echo "$final_prompt" } # Extract result from last line of agent session file extract_result() { local session_file="$1" if [ ! -f "$session_file" ]; then error "Session file not found: $session_file" fi local result result=$(tail -n 1 "$session_file" | jq -r '.result // empty' 2>/dev/null) if [ -z "$result" ]; then error "Could not extract result from session file" fi echo "$result" } # Extract session_id from first line of agent session file extract_session_id() { local session_file="$1" if [ ! -f "$session_file" ]; then error "Session file not found: $session_file" fi local session_id session_id=$(head -n 1 "$session_file" | jq -r '.session_id // empty' 2>/dev/null) if [ -z "$session_id" ]; then error "Could not extract session_id from session file" fi echo "$session_id" } # Load agent configuration from agent directory # Args: $1 - Agent name (must match folder name) # Sets global vars: AGENT_NAME, AGENT_DESCRIPTION, SYSTEM_PROMPT_FILE (full path), MCP_CONFIG (full path) load_agent_config() { local agent_name="$1" local agent_dir="$AGENTS_DIR/${agent_name}" local agent_file="$agent_dir/agent.json" if [ ! -d "$agent_dir" ]; then error "Agent not found: $agent_name (expected directory: $agent_dir)" fi if [ ! -f "$agent_file" ]; then error "Agent configuration not found: $agent_file" fi # Validate JSON if ! jq empty "$agent_file" 2>/dev/null; then error "Invalid JSON in agent configuration: $agent_file" fi # Extract fields AGENT_NAME=$(jq -r '.name' "$agent_file") AGENT_DESCRIPTION=$(jq -r '.description' "$agent_file") # Validate name matches folder name if [ "$AGENT_NAME" != "$agent_name" ]; then error "Agent name mismatch: folder=$agent_name, config name=$AGENT_NAME" fi # Check for optional files by convention SYSTEM_PROMPT_FILE="" if [ -f "$agent_dir/agent.system-prompt.md" ]; then SYSTEM_PROMPT_FILE="$agent_dir/agent.system-prompt.md" fi MCP_CONFIG="" if [ -f "$agent_dir/agent.mcp.json" ]; then MCP_CONFIG="$agent_dir/agent.mcp.json" fi } # Load system prompt from file and return its content # Args: $1 - Full path to prompt file (already resolved by load_agent_config) # Returns: File content via stdout, or empty string if path is empty load_system_prompt() { local prompt_file="$1" if [ -z "$prompt_file" ]; then echo "" return fi if [ ! -f "$prompt_file" ]; then error "System prompt file not found: $prompt_file" fi cat "$prompt_file" } # Save session metadata save_session_metadata() { local session_name="$1" local agent_name="$2" # Can be empty for generic sessions local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") local meta_file="$AGENT_SESSIONS_DIR/${session_name}.meta.json" cat > "$meta_file" < "${meta_file}.tmp" mv "${meta_file}.tmp" "$meta_file" else # Create meta.json if it doesn't exist (backward compatibility) save_session_metadata "$session_name" "$agent_name" fi } # Build MCP config argument for Claude CLI # Args: $1 - Full path to MCP config file (already resolved by load_agent_config) # Returns: Claude CLI argument string "--mcp-config ", or empty string if path is empty build_mcp_arg() { local mcp_config="$1" if [ -z "$mcp_config" ]; then echo "" return fi if [ ! -f "$mcp_config" ]; then error "MCP config file not found: $mcp_config" fi echo "--mcp-config $mcp_config" } # Command: new cmd_new() { local session_name="$1" local prompt_arg="$2" local agent_name="$3" # Optional validate_session_name "$session_name" local session_file="$AGENT_SESSIONS_DIR/${session_name}.jsonl" # Check if session already exists if [ -f "$session_file" ]; then error "Session '$session_name' already exists. Use 'resume' command to continue or choose a different name" fi # Get user prompt local user_prompt user_prompt=$(get_prompt "$prompt_arg") # Load agent configuration if specified local final_prompt="$user_prompt" local mcp_arg="" if [ -n "$agent_name" ]; then load_agent_config "$agent_name" # Load and prepend system prompt if [ -n "$SYSTEM_PROMPT_FILE" ]; then local system_prompt system_prompt=$(load_system_prompt "$SYSTEM_PROMPT_FILE") final_prompt="${system_prompt}"$'\n\n---\n\n'"${user_prompt}" fi # Build MCP argument mcp_arg=$(build_mcp_arg "$MCP_CONFIG") fi # Ensure required directories exist ensure_directories # Save session metadata immediately save_session_metadata "$session_name" "$agent_name" # Run claude command if ! claude -p "$final_prompt" $mcp_arg --output-format stream-json --permission-mode bypassPermissions >> "$session_file" 2>&1; then error "Claude command failed" fi # Extract and output result extract_result "$session_file" } # Command: resume cmd_resume() { local session_name="$1" local prompt_arg="$2" validate_session_name "$session_name" local session_file="$AGENT_SESSIONS_DIR/${session_name}.jsonl" # Check if session exists if [ ! -f "$session_file" ]; then error "Session '$session_name' does not exist. Use 'new' command to create it" fi # Load session metadata to get agent load_session_metadata "$session_name" # Extract session_id local session_id session_id=$(extract_session_id "$session_file") # Get prompt local prompt prompt=$(get_prompt "$prompt_arg") # Load agent configuration if session has an agent local mcp_arg="" if [ -n "$SESSION_AGENT" ]; then load_agent_config "$SESSION_AGENT" mcp_arg=$(build_mcp_arg "$MCP_CONFIG") fi # Run claude command with resume if ! claude -r "$session_id" -p "$prompt" $mcp_arg --output-format stream-json --permission-mode bypassPermissions >> "$session_file" 2>&1; then error "Claude resume command failed" fi # Update session metadata timestamp (or create if missing) update_session_metadata "$session_name" "$SESSION_AGENT" # Extract and output result extract_result "$session_file" } # Command: list cmd_list() { # Ensure required directories exist ensure_directories # Check if there are any sessions local session_files=("$AGENT_SESSIONS_DIR"/*.jsonl) if [ ! -f "${session_files[0]}" ]; then echo "No sessions found" return fi # List all sessions with metadata for session_file in "$AGENT_SESSIONS_DIR"/*.jsonl; do local session_name session_name=$(basename "$session_file" .jsonl) local session_id # Extract session_id without calling error function (for empty/initializing sessions) if [ -s "$session_file" ]; then session_id=$(head -n 1 "$session_file" 2>/dev/null | jq -r '.session_id // "unknown"' 2>/dev/null || echo "unknown") else session_id="initializing" fi echo "$session_name (session: $session_id)" done } # Command: list-agents - List all available agent definitions from agent directories # Scans AGENTS_DIR for subdirectories containing agent.json files # Outputs: Agent name and description in formatted list cmd_list_agents() { # Ensure required directories exist ensure_directories # Check if there are any agent directories local found_agents=false for agent_dir in "$AGENTS_DIR"/*; do if [ -d "$agent_dir" ] && [ -f "$agent_dir/agent.json" ]; then found_agents=true break fi done if [ "$found_agents" = false ]; then echo "No agent definitions found" return fi # List all agent definitions local first=true for agent_dir in "$AGENTS_DIR"/*; do # Skip if not a directory or doesn't have agent.json if [ ! -d "$agent_dir" ] || [ ! -f "$agent_dir/agent.json" ]; then continue fi local agent_name local agent_description local agent_file="$agent_dir/agent.json" # Extract name and description from JSON agent_name=$(jq -r '.name // "unknown"' "$agent_file" 2>/dev/null) agent_description=$(jq -r '.description // "No description available"' "$agent_file" 2>/dev/null) # Add separator before each agent (except the first) if [ "$first" = true ]; then first=false else echo "---" echo "" fi # Display in requested format echo "${agent_name}:" echo "${agent_description}" echo "" done } # Command: clean cmd_clean() { # Remove the entire agent-sessions directory if [ -d "$AGENT_SESSIONS_DIR" ]; then rm -rf "$AGENT_SESSIONS_DIR" echo "All sessions removed" else echo "No sessions to remove" fi } # Main script logic main() { # Check if no arguments provided if [ $# -eq 0 ]; then show_help exit 1 fi local command="$1" shift case "$command" in new) # Parse arguments if [ $# -eq 0 ]; then error "Session name required for 'new' command" fi local session_name="$1" shift local prompt_arg="" local agent_name="" while [ $# -gt 0 ]; do case "$1" in -p) if [ $# -lt 2 ]; then error "-p flag requires a prompt argument" fi prompt_arg="$2" shift 2 ;; --agent) if [ $# -lt 2 ]; then error "--agent flag requires an agent name" fi agent_name="$2" shift 2 ;; *) error "Unknown option: $1" ;; esac done cmd_new "$session_name" "$prompt_arg" "$agent_name" ;; resume) # Parse arguments if [ $# -eq 0 ]; then error "Session name required for 'resume' command" fi local session_name="$1" shift local prompt_arg="" while [ $# -gt 0 ]; do case "$1" in -p) if [ $# -lt 2 ]; then error "-p flag requires a prompt argument" fi prompt_arg="$2" shift 2 ;; *) error "Unknown option: $1" ;; esac done cmd_resume "$session_name" "$prompt_arg" ;; list) cmd_list ;; list-agents) cmd_list_agents ;; clean) cmd_clean ;; -h|--help) show_help exit 0 ;; *) error "Unknown command: $command\n\nRun './cli-agent-runner.sh' for usage information" ;; esac } # Run main function main "$@"