#!/usr/bin/env python3
"""
coder - A CLI tool for interacting with coderai API
Connects to OpenAI-compatible API and executes tools automatically.
"""

# Debug: Verify script execution - imports must come first
import sys
import os

if os.environ.get('CODER_DEBUG'):
    print(f"DEBUG: Script started", file=sys.stderr)
    print(f"DEBUG: Arguments: {sys.argv}", file=sys.stderr)
    print(f"DEBUG: Python executable: {sys.executable}", file=sys.stderr)
import sys
import json
import argparse
import subprocess
import readline
import random
import string
from pathlib import Path
from typing import Optional, Dict, Any, List, Callable
from dataclasses import dataclass, field
from datetime import datetime

import requests


# ANSI color codes
class Colors:
    """ANSI color codes for terminal output."""
    RESET = "\033[0m"
    BOLD = "\033[1m"
    DIM = "\033[2m"
    RED = "\033[91m"
    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    BLUE = "\033[94m"
    MAGENTA = "\033[95m"
    CYAN = "\033[96m"
    WHITE = "\033[97m"


# Default system prompt for normal models
DEFAULT_SYSTEM_PROMPT = """You are Coder, an AI coding assistant. You help users write, read, and modify code files. You have access to tools for file operations.

## CRITICAL: Response Format

1. ALWAYS maintain proper spacing between words and after punctuation.
2. Use complete sentences with normal spacing.
3. When showing code, use proper code blocks with language identifiers.

## Available Tools

You can invoke tools by outputting JSON inside <tool> tags:

<tool>{"name": "TOOL_NAME", "arguments": {PARAMETERS}}</tool>

### read_file - Read file contents
Purpose: Read one or more files to understand the codebase
Parameters: {"path": "relative/path/to/file"}
Example: <tool>{"name": "read_file", "arguments": {"path": "main.py"}}</tool>

### write_file - Create or overwrite files
Purpose: Write new files or completely replace existing ones
Parameters: {"path": "relative/path", "content": "full file content"}
Example: <tool>{"name": "write_file", "arguments": {"path": "hello.py", "content": "print('Hello World')"}}</tool>

### apply_diff - Modify existing files
Purpose: Make targeted changes to specific sections of files
Parameters: {"path": "relative/path", "diff": "SEARCH/REPLACE block"}
Example: <tool>{"name": "apply_diff", "arguments": {"path": "main.py", "diff": "<<<<<<< SEARCH\ndef old_func():\n    pass\n=======\ndef new_func():\n    return 42\n>>>>>>> REPLACE"}}</tool>

### execute_command - Run shell commands
Purpose: Execute commands like git, npm, python, ls, etc.
Parameters: {"command": "shell command string"}
Example: <tool>{"name": "execute_command", "arguments": {"command": "ls -la"}}</tool>

## Tool Usage Rules

1. READ FIRST: Always read files before modifying them
2. COMPLETE REPLACEMENTS: When using write_file, include the ENTIRE file content
3. TARGETED EDITS: Use apply_diff for small changes to preserve the rest of the file
4. ONE TOOL AT A TIME: Make one tool call, wait for results, then proceed
5. VERIFY CHANGES: After writing files, read them back to confirm

## Workflow Example

User: "Add a function to main.py"
You: <tool>{"name": "read_file", "arguments": {"path": "main.py"}}</tool>
[Tool result shown]
You: [Explain what you'll add, then call write_file or apply_diff]

## Output Style

- Use markdown for formatting
- Show file paths as [`filename`](path/to/file)
- Include code blocks with language tags
- Maintain normal spacing in all responses"""

# Simplified system prompt for small models (under 7B parameters)
SMALL_MODEL_SYSTEM_PROMPT = """You are Coder, an AI assistant. Help with coding tasks.

IMPORTANT RULES:
1. Put ONE space between EVERY word.
2. Put ONE space after periods and commas.
3. Use code blocks with triple backticks.
4. Be concise.

TOOLS:
Use <tool>{"name": "TOOL", "arguments": {}}</tool> format.

Available tools:
- read_file: {"path": "file.py"} - Read a file
- write_file: {"path": "file.py", "content": "code"} - Write a file
- apply_diff: {"path": "file.py", "diff": "SEARCH...REPLACE"} - Edit file
- execute_command: {"command": "ls"} - Run command

ALWAYS add spaces between words."""

# Minimal system prompt for tiny models (under 3B parameters)
TINY_MODEL_SYSTEM_PROMPT = """You are Coder. Help with code.

Rules:
- Space between words
- Space after punctuation
- Use ``` for code
- Be brief

Tools: <tool>{"name":"TOOL","arguments":{}}</tool>
Tools: read_file, write_file, apply_diff, execute_command"""

# Ultra-minimal system prompt for micro models (under 1.5B parameters)
MICRO_MODEL_SYSTEM_PROMPT = """Coder AI. Help code.

Rules:
- Space between words
- Use ``` for code blocks

Tools: read_file, write_file, apply_diff, execute_command
Format: <tool>{"name":"TOOL","arguments":{}}</tool>"""


@dataclass
class Config:
    """Configuration for the coder CLI."""
    api_url: str = "http://localhost:6744/v1"
    token: Optional[str] = None
    system_prompt: str = DEFAULT_SYSTEM_PROMPT
    model: str = "default"
    model_aliases: Dict[str, str] = None  # Alias -> Model mapping
    small: bool = False  # Use small model optimizations
    tiny: bool = False   # Use tiny model optimizations (minimal)
    micro: bool = False  # Use micro model optimizations (ultra-minimal)
    timeout: int = 600   # Request timeout in seconds
    confirm_all: bool = True  # Confirm before executing tools by default
    confirm_commands: Dict[str, bool] = None  # Per-command confirmation settings
    debug: bool = False  # Show debug output including raw tool calls
    dump: bool = False  # Show dump output: tools schema, raw response, parsed tool calls
    max_context: int = 32768  # Maximum context size in tokens
    no_prompt: bool = False  # Don't send system prompt
    no_tools: bool = False   # Don't send tool definitions
    
    def __post_init__(self):
        if self.confirm_commands is None:
            self.confirm_commands = {}
        if self.model_aliases is None:
            self.model_aliases = {}
    
    def resolve_model(self, model: str) -> str:
        """Resolve model alias to actual model name."""
        return self.model_aliases.get(model, model)

    @classmethod
    def load(cls, config_path: Optional[str] = None) -> "Config":
        """Load configuration from file or create default."""
        if config_path is None:
            config_path = os.path.expanduser("~/.config/coderai/cli.json")
        
        config = cls()
        
        if os.path.exists(config_path):
            try:
                with open(config_path, 'r') as f:
                    data = json.load(f)
                config.api_url = data.get('api_url', config.api_url)
                config.token = data.get('token')
                config.system_prompt = data.get('system_prompt', config.system_prompt)
                config.model = data.get('model', config.model)
                config.model_aliases = data.get('model_aliases', config.model_aliases)
                config.small = data.get('small', config.small)
                config.tiny = data.get('tiny', config.tiny)
                config.micro = data.get('micro', config.micro)
                config.timeout = data.get('timeout', config.timeout)
                config.debug = data.get('debug', config.debug)
                config.max_context = data.get('max_context', config.max_context)
                config.no_prompt = data.get('no_prompt', config.no_prompt)
                config.no_tools = data.get('no_tools', config.no_tools)
            except (json.JSONDecodeError, IOError) as e:
                print(f"Warning: Could not load config from {config_path}: {e}", file=sys.stderr)
        
        return config
    
    def save(self, config_path: Optional[str] = None) -> None:
        """Save configuration to file."""
        if config_path is None:
            config_path = os.path.expanduser("~/.config/coderai/cli.json")
        
        # Ensure directory exists
        os.makedirs(os.path.dirname(config_path), exist_ok=True)
        
        data = {
            'api_url': self.api_url,
            'token': self.token,
            'system_prompt': self.system_prompt,
            'model': self.model,
            'model_aliases': self.model_aliases,
            'small': self.small,
            'tiny': self.tiny,
            'micro': self.micro,
            'timeout': self.timeout,
            'debug': self.debug,
            'max_context': self.max_context,
            'no_prompt': self.no_prompt,
            'no_tools': self.no_tools
        }
        
        with open(config_path, 'w') as f:
            json.dump(data, f, indent=2)


class SessionManager:
    """Manages named sessions for the CLI."""
    
    def __init__(self, sessions_dir: str = None):
        if sessions_dir is None:
            sessions_dir = os.path.expanduser("~/.cache/coderai/sessions")
        self.sessions_dir = sessions_dir
        os.makedirs(sessions_dir, exist_ok=True)
    
    def _get_session_path(self, name: str) -> str:
        """Get the file path for a session."""
        # Sanitize name for filesystem
        safe_name = "".join(c for c in name if c.isalnum() or c in ('-', '_')).rstrip()
        return os.path.join(self.sessions_dir, f"{safe_name}.json")
    
    def session_exists(self, name: str) -> bool:
        """Check if a session exists."""
        return os.path.exists(self._get_session_path(name))
    
    def save_session(self, name: str, history: List[Dict[str, Any]]) -> None:
        """Save a session to disk."""
        session_data = {
            'name': name,
            'timestamp': datetime.now().isoformat(),
            'history': history
        }
        with open(self._get_session_path(name), 'w') as f:
            json.dump(session_data, f, indent=2)
    
    def load_session(self, name: str) -> Optional[List[Dict[str, Any]]]:
        """Load a session from disk."""
        path = self._get_session_path(name)
        if not os.path.exists(path):
            return None
        try:
            with open(path, 'r') as f:
                data = json.load(f)
            return data.get('history', [])
        except (json.JSONDecodeError, IOError):
            return None
    
    def delete_session(self, name: str) -> bool:
        """Delete a session. Returns True if deleted, False if not found."""
        path = self._get_session_path(name)
        if os.path.exists(path):
            os.remove(path)
            return True
        return False
    
    def delete_all_sessions(self) -> int:
        """Delete all sessions. Returns count of deleted sessions."""
        count = 0
        for filename in os.listdir(self.sessions_dir):
            if filename.endswith('.json'):
                os.remove(os.path.join(self.sessions_dir, filename))
                count += 1
        return count
    
    def list_sessions(self) -> List[Dict[str, Any]]:
        """List all available sessions."""
        sessions = []
        for filename in sorted(os.listdir(self.sessions_dir)):
            if filename.endswith('.json'):
                path = os.path.join(self.sessions_dir, filename)
                try:
                    with open(path, 'r') as f:
                        data = json.load(f)
                    sessions.append({
                        'name': data.get('name', filename[:-5]),
                        'timestamp': data.get('timestamp', ''),
                        'message_count': len(data.get('history', []))
                    })
                except (json.JSONDecodeError, IOError):
                    pass
        return sessions
    
    def generate_unique_name(self, base_name: str) -> str:
        """Generate a unique session name by adding random characters if needed."""
        if not self.session_exists(base_name):
            return base_name
        # Add random suffix
        suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=4))
        return f"{base_name}_{suffix}"


class ToolExecutor:
    """Executes tool calls from the LLM."""
    
    def __init__(self, working_dir: str = "."):
        self.working_dir = working_dir
        self.tools = self._define_tools()
    
    def _define_tools(self) -> List[Dict[str, Any]]:
        """Define available tools in OpenAI format."""
        return [
            {
                "type": "function",
                "function": {
                    "name": "read_file",
                    "description": "Read the contents of a file",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "path": {
                                "type": "string",
                                "description": "Path to the file to read (relative to working directory)"
                            }
                        },
                        "required": ["path"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "write_file",
                    "description": "Write content to a file (creates or overwrites)",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "path": {
                                "type": "string",
                                "description": "Path to the file to write (relative to working directory)"
                            },
                            "content": {
                                "type": "string",
                                "description": "Content to write to the file"
                            }
                        },
                        "required": ["path", "content"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "apply_diff",
                    "description": "Apply a diff/patch to a file. Use SEARCH/REPLACE blocks format.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "path": {
                                "type": "string",
                                "description": "Path to the file to modify"
                            },
                            "diff": {
                                "type": "string",
                                "description": "Diff content in SEARCH/REPLACE format: <<<<<<< SEARCH\\n[old content]\\n=======\\n[new content]\\n>>>>>>> REPLACE"
                            }
                        },
                        "required": ["path", "diff"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "execute_command",
                    "description": "Execute a shell command",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "command": {
                                "type": "string",
                                "description": "The shell command to execute"
                            },
                            "cwd": {
                                "type": "string",
                                "description": "Working directory for the command (optional, defaults to current)"
                            }
                        },
                        "required": ["command"]
                    }
                }
            }
        ]
    
    def execute(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
        """Execute a tool call and return the result."""
        try:
            if tool_name == "read_file":
                return self._read_file(arguments["path"])
            elif tool_name == "write_file":
                return self._write_file(arguments["path"], arguments["content"])
            elif tool_name == "apply_diff":
                return self._apply_diff(arguments["path"], arguments["diff"])
            elif tool_name == "execute_command":
                cwd = arguments.get("cwd", self.working_dir)
                return self._execute_command(arguments["command"], cwd)
            else:
                return {"error": f"Unknown tool: {tool_name}"}
        except Exception as e:
            return {"error": str(e)}
    
    def _read_file(self, path: str) -> Dict[str, Any]:
        """Read a file and return its contents."""
        full_path = os.path.join(self.working_dir, path)
        full_path = os.path.abspath(full_path)
        
        if not os.path.exists(full_path):
            return {"error": f"File not found: {path}"}
        
        if not os.path.isfile(full_path):
            return {"error": f"Path is not a file: {path}"}
        
        try:
            with open(full_path, 'r', encoding='utf-8', errors='replace') as f:
                content = f.read()
            return {"content": content, "path": path}
        except Exception as e:
            return {"error": f"Failed to read file: {e}"}
    
    def _write_file(self, path: str, content: str) -> Dict[str, Any]:
        """Write content to a file."""
        full_path = os.path.join(self.working_dir, path)
        full_path = os.path.abspath(full_path)
        
        # Ensure directory exists
        os.makedirs(os.path.dirname(full_path), exist_ok=True)
        
        try:
            with open(full_path, 'w', encoding='utf-8') as f:
                f.write(content)
            return {"success": True, "path": path, "bytes_written": len(content)}
        except Exception as e:
            return {"error": f"Failed to write file: {e}"}
    
    def _apply_diff(self, path: str, diff: str) -> Dict[str, Any]:
        """Apply a SEARCH/REPLACE diff to a file."""
        full_path = os.path.join(self.working_dir, path)
        full_path = os.path.abspath(full_path)
        
        if not os.path.exists(full_path):
            return {"error": f"File not found: {path}"}
        
        try:
            with open(full_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # Parse and apply SEARCH/REPLACE blocks
            import re
            pattern = r'<<<<<<< SEARCH\n(.*?)\n=======\n(.*?)\n>>>>>>> REPLACE'
            matches = list(re.finditer(pattern, diff, re.DOTALL))
            
            if not matches:
                return {"error": "No valid SEARCH/REPLACE blocks found in diff"}
            
            new_content = content
            replacements = 0
            
            for match in matches:
                search_text = match.group(1)
                replace_text = match.group(2)
                
                if search_text in new_content:
                    new_content = new_content.replace(search_text, replace_text, 1)
                    replacements += 1
                else:
                    return {"error": f"Search text not found in file: {search_text[:50]}..."}
            
            with open(full_path, 'w', encoding='utf-8') as f:
                f.write(new_content)
            
            return {
                "success": True, 
                "path": path, 
                "replacements": replacements
            }
        except Exception as e:
            return {"error": f"Failed to apply diff: {e}"}
    
    def _execute_command(self, command: str, cwd: str) -> Dict[str, Any]:
        """Execute a shell command."""
        try:
            result = subprocess.run(
                command,
                shell=True,
                cwd=cwd,
                capture_output=True,
                text=True,
                timeout=300  # 5 minute timeout
            )
            
            return {
                "success": result.returncode == 0,
                "returncode": result.returncode,
                "stdout": result.stdout,
                "stderr": result.stderr,
                "command": command
            }
        except subprocess.TimeoutExpired:
            return {"error": "Command timed out after 5 minutes"}
        except Exception as e:
            return {"error": f"Failed to execute command: {e}"}


class CoderClient:
    """Client for interacting with the coderai API."""
    
    def __init__(self, config: Config, session_manager: SessionManager = None):
        self.config = config
        self.tool_executor = ToolExecutor()
        self.conversation_history: List[Dict[str, Any]] = []
        self.in_tool_call = False
        self.tool_call_buffer = ""
        self.session_manager = session_manager
        self.session_name: Optional[str] = None
        self.input_history: List[str] = []  # Track user inputs for readline
    
    def chat(self, message: str, stream: bool = True) -> str:
        """Send a message to the API and get response."""
        # Add to input history for readline
        self.input_history.append(message)
        
        # Check context size and compress if needed
        self._manage_context()
        
        # Add user message to history
        self.conversation_history.append({
            "role": "user",
            "content": message
        })
        
        # Prepare messages with system prompt (if not disabled)
        if self.config.no_prompt:
            messages = []
        else:
            messages = [{"role": "system", "content": self.config.system_prompt}]
        messages.extend(self.conversation_history)
        
        headers = {"Content-Type": "application/json"}
        if self.config.token:
            headers["Authorization"] = f"Bearer {self.config.token}"
        
        # Build payload, conditionally including tools
        payload = {
            "model": self.config.resolve_model(self.config.model),
            "messages": messages,
            "stream": stream
        }
        
        # Only include tools if not disabled
        if not self.config.no_tools:
            payload["tools"] = self.tool_executor.tools
            payload["tool_choice"] = "auto"
            
            # Dump tools schema if enabled
            if self.config.dump:
                print(f"\n{Colors.CYAN}=== DUMP: TOOLS SCHEMA ==={Colors.RESET}")
                print(json.dumps(self.tool_executor.tools, indent=2))
                print(f"{Colors.CYAN}=== END DUMP ==={Colors.RESET}\n")
        
        try:
            response = requests.post(
                f"{self.config.api_url}/chat/completions",
                headers=headers,
                json=payload,
                stream=stream,
                timeout=self.config.timeout
            )
            response.raise_for_status()
            
            if stream:
                result = self._handle_streaming_response(response)
            else:
                result = self._handle_non_streaming_response(response)
            
            # Save session after each interaction
            self._save_current_session()
            return result
                
        except requests.exceptions.ConnectionError:
            return "Error: Could not connect to API. Is the server running?"
        except requests.exceptions.Timeout:
            return "Error: Request timed out."
        except requests.exceptions.RequestException as e:
            return f"Error: API request failed: {e}"
    
    def _estimate_tokens(self, text: str) -> int:
        """Rough estimation of token count (4 chars per token on average)."""
        return len(text) // 4
    
    def _manage_context(self):
        """Manage context size and compress if approaching limit."""
        # Calculate current context size
        total_text = ""
        for msg in self.conversation_history:
            total_text += msg.get("content", "") or ""
            if "tool_calls" in msg:
                for tc in msg["tool_calls"]:
                    total_text += tc.get("function", {}).get("arguments", "")
        
        current_tokens = self._estimate_tokens(total_text)
        threshold = int(self.config.max_context * 0.9)  # 90% threshold
        
        if current_tokens > threshold:
            print(f"{Colors.YELLOW}[Context at {current_tokens}/{self.config.max_context} tokens - compressing...]{Colors.RESET}")
            self._compress_context()
    
    def _compress_context(self):
        """Compress context by summarizing old messages."""
        if len(self.conversation_history) <= 4:
            return
        
        # Keep system message (implicit), first user message, and last 2 exchanges
        # Summarize the middle portion
        to_summarize = self.conversation_history[:-4]
        keep = self.conversation_history[-4:]
        
        # Create a summary placeholder
        summary = f"[Previous {len(to_summarize)} messages summarized]"
        
        # Replace with summary
        self.conversation_history = [{"role": "system", "content": summary}] + keep
        
        # Save the updated session after compression
        self._save_current_session()
    
    def _save_current_session(self):
        """Save current session if it has a name."""
        if self.session_manager and self.session_name and self.conversation_history:
            self.session_manager.save_session(self.session_name, self.conversation_history)
    
    def load_session(self, name: str) -> bool:
        """Load a session by name."""
        if not self.session_manager:
            return False
        
        history = self.session_manager.load_session(name)
        if history is not None:
            self.conversation_history = history
            self.session_name = name
            return True
        return False
    
    def new_session(self, name: str = None) -> str:
        """Start a new session. Returns the session name."""
        # Save current session if exists
        self._save_current_session()
        
        # Clear history
        self.conversation_history = []
        
        if name:
            if self.session_manager and self.session_manager.session_exists(name):
                name = self.session_manager.generate_unique_name(name)
            self.session_name = name
        else:
            # Generate default name
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            self.session_name = f"session_{timestamp}"
        
        # Save the new empty session immediately
        self._save_current_session()
        
        return self.session_name
    
    def _handle_streaming_response(self, response: requests.Response) -> str:
        """Handle streaming response from API."""
        import time
        import re
        import sys
        
        full_content = ""
        tool_calls = []
        in_thinking = False
        thinking_content = ""
        thinking_start_time = 0
        last_update_time = 0
        displayed_elapsed = 0
        in_tool_call = False
        tool_call_buffer = ""
        
        def format_thinking_line(elapsed, content):
            """Format the thinking line for display."""
            # Filter out tool tags and normalize
            display = re.sub(r'<tool.*?>.*?</tool>', '', content, flags=re.DOTALL)
            display = re.sub(r'<tool_call.*?>.*?</tool_call>', '', display, flags=re.DOTALL)
            display = display.replace('\n', ' ').strip()
            if len(display) > 50:
                display = "..." + display[-50:]
            return f"[{elapsed}s] Thinking: [{display}]"
        
        def parse_tool_calls_from_content(text):
            """Parse tool calls from content in various formats."""
            parsed = []
            
            # Format 1: <tool_call>{"name": "...", "arguments": {...}}</tool_call>
            pattern1 = r'<tool_call>\s*(\{.*?\})\s*</tool_call>'
            matches1 = re.findall(pattern1, text, re.DOTALL)
            for match in matches1:
                try:
                    tool_data = json.loads(match)
                    parsed.append({
                        'id': f'call_{len(parsed)}',
                        'type': 'function',
                        'function': {
                            'name': tool_data.get('name', ''),
                            'arguments': json.dumps(tool_data.get('arguments', {}))
                        }
                    })
                except json.JSONDecodeError:
                    continue
            
            # Format 2: XML format
            write_file_pattern = r'<tool>\s*<name>write_file</name>\s*<arguments>\s*<file\s+path="([^"]+)">\s*<content>(.*?)</content>\s*</file>\s*</arguments>\s*</tool>'
            for match in re.finditer(write_file_pattern, text, re.DOTALL | re.IGNORECASE):
                path = match.group(1)
                content = match.group(2)
                parsed.append({
                    'id': f'call_{len(parsed)}',
                    'type': 'function',
                    'function': {
                        'name': 'write_file',
                        'arguments': json.dumps({'path': path, 'content': content.strip()})
                    }
                })
            
            read_file_pattern = r'<tool>\s*<name>read_file</name>\s*<arguments>\s*<path>([^<]+)</path>\s*</arguments>\s*</tool>'
            for match in re.finditer(read_file_pattern, text, re.DOTALL | re.IGNORECASE):
                path = match.group(1).strip()
                parsed.append({
                    'id': f'call_{len(parsed)}',
                    'type': 'function',
                    'function': {
                        'name': 'read_file',
                        'arguments': json.dumps({'path': path})
                    }
                })
            
            exec_pattern = r'<tool>\s*<name>execute_command</name>\s*<arguments>\s*(?:<command>)?([^<]+)(?:</command>)?\s*</arguments>\s*</tool>'
            for match in re.finditer(exec_pattern, text, re.DOTALL | re.IGNORECASE):
                command = match.group(1).strip()
                parsed.append({
                    'id': f'call_{len(parsed)}',
                    'type': 'function',
                    'function': {
                        'name': 'execute_command',
                        'arguments': json.dumps({'command': command})
                    }
                })
            
            diff_pattern = r'<tool>\s*<name>apply_diff</name>\s*<arguments>\s*<path>([^<]+)</path>\s*<diff>(.*?)</diff>\s*</arguments>\s*</tool>'
            for match in re.finditer(diff_pattern, text, re.DOTALL | re.IGNORECASE):
                path = match.group(1).strip()
                diff = match.group(2).strip()
                parsed.append({
                    'id': f'call_{len(parsed)}',
                    'type': 'function',
                    'function': {
                        'name': 'apply_diff',
                        'arguments': json.dumps({'path': path, 'diff': diff})
                    }
                })
            
            # Format 3: Generic <tool_call><tool><name>...</name><arguments>JSON</arguments></tool></tool_call>
            # Also handles incomplete closing tags like <tool_call> without </tool_call>
            generic_pattern = r'<tool_call>\s*<tool>\s*<name>(.*?)</name>\s*<arguments>(.*?)</arguments>\s*</tool>\s*(?:</tool_call>)?'
            for match in re.finditer(generic_pattern, text, re.DOTALL | re.IGNORECASE):
                name = match.group(1).strip()
                args_str = match.group(2).strip()
                if not name:
                    continue
                try:
                    args = json.loads(args_str) if args_str else {}
                except json.JSONDecodeError:
                    args = {}
                parsed.append({
                    'id': f'call_{len(parsed)}',
                    'type': 'function',
                    'function': {
                        'name': name,
                        'arguments': json.dumps(args)
                    }
                })
            
            # Format 4: Short format <tool>TOOL_NAME>JSON</tool>
            # Example: <tool>financial_data_fetcher>{"ticker": "AAPL"}</tool>
            pattern_short = r'<tool>(\w+)>(\{.*?\})</tool>'
            for match in re.finditer(pattern_short, text, re.DOTALL):
                name = match.group(1).strip()
                args_str = match.group(2).strip()
                if not name:
                    continue
                try:
                    args = json.loads(args_str) if args_str else {}
                except json.JSONDecodeError:
                    args = {}
                parsed.append({
                    'id': f'call_{len(parsed)}',
                    'type': 'function',
                    'function': {
                        'name': name,
                        'arguments': json.dumps(args)
                    }
                })
            
            # Format 5: <tool_call><tool>TOOL_NAME>JSON</tool></tool_call>
            pattern_short2 = r'<tool_call>\s*<tool>(\w+)>\s*(\{.*?\})\s*</tool>\s*</tool_call>'
            for match in re.finditer(pattern_short2, text, re.DOTALL):
                name = match.group(1).strip()
                args_str = match.group(2).strip()
                if not name:
                    continue
                try:
                    args = json.loads(args_str) if args_str else {}
                except json.JSONDecodeError:
                    args = {}
                parsed.append({
                    'id': f'call_{len(parsed)}',
                    'type': 'function',
                    'function': {
                        'name': name,
                        'arguments': json.dumps(args)
                    }
                })
            
            return parsed
        
        # Process streaming response line by line
        for line in response.iter_lines():
            if not line:
                continue
                
            line = line.decode('utf-8')
            
            # Handle SSE format
            if line.startswith('data: '):
                line = line[6:]
            
            if line == '[DONE]':
                break
            
            try:
                data = json.loads(line)
                delta = data.get('choices', [{}])[0].get('delta', {})
                content = delta.get('content')
                
                if not content:
                    continue
                
                full_content += content
                current_time = time.time()
                
                # Filter out tool calls from display (unless debug mode)
                display_content = content
                if not self.config.debug:
                    # Check for tool_call tag start
                    if '<tool_call>' in content:
                        if not in_tool_call:
                            # Start of tool call - hide everything from <tool_call> onwards
                            parts = content.split('<tool_call>', 1)
                            if parts[0]:
                                display_content = parts[0]
                            else:
                                display_content = ""
                            in_tool_call = True
                            tool_call_buffer = '<tool_call>' + (parts[1] if len(parts) > 1 else "")
                        else:
                            tool_call_buffer += content
                            display_content = ""
                    elif in_tool_call:
                        # We're inside a tool call
                        tool_call_buffer += content
                        if '</tool_call>' in content:
                            # End of tool call
                            in_tool_call = False
                            # Check if there's content after </tool_call>
                            parts = content.split('</tool_call>', 1)
                            if len(parts) > 1 and parts[1]:
                                display_content = parts[1]
                            else:
                                display_content = ""
                            tool_call_buffer = ""
                        else:
                            display_content = ""
                
                # Handle thinking state
                if '<think>' in display_content:
                    in_thinking = True
                    thinking_start_time = current_time
                    last_update_time = current_time
                    displayed_elapsed = 0
                    thinking_content = ""
                    sys.stdout.write(f"\r{Colors.DIM}{format_thinking_line(0, '')}{Colors.RESET}")
                    sys.stdout.flush()
                    continue
                
                if in_thinking:
                    if '</think>' in display_content:
                        in_thinking = False
                        elapsed = int(current_time - thinking_start_time)
                        parts = display_content.split('</think>', 1)
                        if parts[0]:
                            thinking_content += parts[0]
                        sys.stdout.write(f"\r{Colors.DIM}{format_thinking_line(elapsed, thinking_content)}{Colors.RESET}\n")
                        sys.stdout.flush()
                        # Content after </think>
                        if len(parts) > 1 and parts[1]:
                            actual = parts[1]
                            sys.stdout.write(actual)
                            sys.stdout.flush()
                    else:
                        thinking_content += display_content
                        # Update display every 0.1 seconds or on new content
                        elapsed = int(current_time - thinking_start_time)
                        if elapsed != displayed_elapsed or current_time - last_update_time >= 0.1:
                            sys.stdout.write(f"\r{Colors.DIM}{format_thinking_line(elapsed, thinking_content)}{Colors.RESET}")
                            sys.stdout.flush()
                            displayed_elapsed = elapsed
                            last_update_time = current_time
                else:
                    if display_content:
                        sys.stdout.write(display_content)
                        sys.stdout.flush()
                    
            except json.JSONDecodeError:
                continue
        
        if in_thinking:
            sys.stdout.write('\n')
        sys.stdout.write('\n')
        sys.stdout.flush()
        
        # Dump raw response if enabled
        if self.config.dump:
            print(f"\n{Colors.CYAN}=== DUMP: RAW RESPONSE ==={Colors.RESET}")
            print(full_content)
            print(f"{Colors.CYAN}=== END DUMP ==={Colors.RESET}\n")
        
        # Parse tool calls from full content after streaming
        tool_calls = parse_tool_calls_from_content(full_content)
        
        # Dump output if enabled
        if self.config.dump:
            print(f"\n{Colors.CYAN}=== DUMP: PARSED TOOL CALLS ==={Colors.RESET}")
            print(json.dumps(tool_calls, indent=2))
            print(f"{Colors.CYAN}=== END DUMP ==={Colors.RESET}\n")
        
        # Execute tool calls if any
        if tool_calls:
            tool_results = []
            
            for tc in tool_calls:
                tool_name = tc['function']['name']
                try:
                    arguments = json.loads(tc['function']['arguments'])
                except json.JSONDecodeError:
                    arguments = {}
                
                # Format arguments for display
                if tool_name == 'execute_command':
                    args_str = arguments.get('command', '')
                elif tool_name == 'read_file':
                    args_str = arguments.get('path', '')
                elif tool_name == 'write_file':
                    args_str = f"{arguments.get('path', '')} ({len(arguments.get('content', ''))} bytes)"
                elif tool_name == 'apply_diff':
                    args_str = arguments.get('path', '')
                else:
                    args_str = str(arguments)
                
                # Show tool call with colors: yellow "Calling tool:", red tool name, white args
                print(f"\n{Colors.YELLOW}Calling tool:{Colors.RESET} {Colors.RED}{tool_name}{Colors.RESET} -> {args_str}")
                
                # Check if confirmation is needed
                needs_confirm = self.config.confirm_all
                if tool_name in self.config.confirm_commands:
                    needs_confirm = self.config.confirm_commands[tool_name]
                
                if needs_confirm:
                    confirm = input(f"{Colors.YELLOW}Execute? (y/N): {Colors.RESET}").strip().lower()
                    if confirm not in ('y', 'yes'):
                        result = {"error": "User declined execution", "declined": True}
                        print(f"{Colors.YELLOW}Skipped{Colors.RESET}")
                    else:
                        result = self.tool_executor.execute(tool_name, arguments)
                else:
                    result = self.tool_executor.execute(tool_name, arguments)
                
                # Show result summary
                if "error" in result:
                    print(f"{Colors.RED}Error: {result['error']}{Colors.RESET}")
                elif result.get('declined'):
                    pass  # Already printed "Skipped"
                else:
                    print(f"{Colors.GREEN}Success{Colors.RESET}")
                    # Show command output for execute_command only in debug mode
                    if self.config.debug and tool_name == 'execute_command' and 'stdout' in result:
                        stdout = result['stdout'].strip()
                        if stdout:
                            # Show first few lines of output
                            lines = stdout.split('\n')[:20]
                            print(f"{Colors.CYAN}Output:{Colors.RESET}")
                            for line in lines:
                                print(f"  {line}")
                            if len(stdout.split('\n')) > 20:
                                print(f"  {Colors.DIM}... ({len(stdout.split(chr(10))) - 20} more lines){Colors.RESET}")
                
                tool_results.append({
                    "tool_call_id": tc['id'],
                    "role": "tool",
                    "content": json.dumps(result)
                })
            
            # Add assistant message with tool calls to history
            self.conversation_history.append({
                "role": "assistant",
                "content": full_content or None,
                "tool_calls": [
                    {
                        "id": tc['id'],
                        "type": "function",
                        "function": tc['function']
                    } for tc in tool_calls
                ]
            })
            
            # Add tool results to history
            self.conversation_history.extend(tool_results)
            
            # Get follow-up response with tool results
            print(f"\n{Colors.DIM}[Getting follow-up response...]{Colors.RESET}")
            return self._get_follow_up_response()
        
        # Add assistant response to history
        if full_content:
            self.conversation_history.append({
                "role": "assistant",
                "content": full_content
            })
        
        return full_content
    
    def _handle_non_streaming_response(self, response: requests.Response) -> str:
        """Handle non-streaming response from API."""
        data = response.json()
        message = data.get('choices', [{}])[0].get('message', {})
        content = message.get('content', '')
        tool_calls = message.get('tool_calls', [])
        
        # Dump raw response if enabled
        if self.config.dump:
            print(f"\n{Colors.CYAN}=== DUMP: RAW RESPONSE ==={Colors.RESET}")
            print(json.dumps(data, indent=2))
            print(f"{Colors.CYAN}=== END DUMP ==={Colors.RESET}\n")
        
        # Dump parsed tool calls if enabled
        if self.config.dump and tool_calls:
            print(f"\n{Colors.CYAN}=== DUMP: PARSED TOOL CALLS ==={Colors.RESET}")
            print(json.dumps(tool_calls, indent=2))
            print(f"{Colors.CYAN}=== END DUMP ==={Colors.RESET}\n")
        
        if content:
            print(content)
        
        # Execute tool calls if any
        if tool_calls:
            print("\n[Executing tools...]")
            tool_results = []
            
            for tc in tool_calls:
                tool_name = tc['function']['name']
                try:
                    arguments = json.loads(tc['function']['arguments'])
                except json.JSONDecodeError:
                    arguments = {}
                
                print(f"  → {tool_name}({arguments})")
                result = self.tool_executor.execute(tool_name, arguments)
                tool_results.append({
                    "tool_call_id": tc['id'],
                    "role": "tool",
                    "content": json.dumps(result)
                })
                
                if "error" in result:
                    print(f"    Error: {result['error']}")
                else:
                    print(f"    Success")
            
            # Add to history
            self.conversation_history.append({
                "role": "assistant",
                "content": content or None,
                "tool_calls": tool_calls
            })
            self.conversation_history.extend(tool_results)
            
            # Get follow-up response
            print("\n[Getting follow-up response...]")
            return self._get_follow_up_response()
        
        # Add to history
        self.conversation_history.append({
            "role": "assistant",
            "content": content
        })
        
        return content
    
    def _get_follow_up_response(self) -> str:
        """Get follow-up response after tool execution."""
        messages = [{"role": "system", "content": self.config.system_prompt}]
        messages.extend(self.conversation_history)
        
        headers = {"Content-Type": "application/json"}
        if self.config.token:
            headers["Authorization"] = f"Bearer {self.config.token}"
        
        # Build payload, conditionally including tools
        payload = {
            "model": self.config.resolve_model(self.config.model),
            "messages": messages,
            "stream": True
        }
        
        # Only include tools if not disabled
        if not self.config.no_tools:
            payload["tools"] = self.tool_executor.tools
            payload["tool_choice"] = "auto"
            
            # Dump tools schema if enabled
            if self.config.dump:
                print(f"\n{Colors.CYAN}=== DUMP: TOOLS SCHEMA (FOLLOW-UP) ==={Colors.RESET}")
                print(json.dumps(self.tool_executor.tools, indent=2))
                print(f"{Colors.CYAN}=== END DUMP ==={Colors.RESET}\n")
        
        response = requests.post(
            f"{self.config.api_url}/chat/completions",
            headers=headers,
            json=payload,
            stream=True,
            timeout=self.config.timeout
        )
        response.raise_for_status()
        
        return self._handle_streaming_response(response)
    
    def clear_history(self):
        """Clear conversation history."""
        self.conversation_history = []


def run_interactive_shell(client: CoderClient, session_manager: SessionManager) -> None:
    """Run interactive REPL shell."""
    # Set up readline for history
    readline_history_file = os.path.expanduser("~/.cache/coderai/input_history")
    os.makedirs(os.path.dirname(readline_history_file), exist_ok=True)
    try:
        readline.read_history_file(readline_history_file)
    except FileNotFoundError:
        pass
    
    # Save history on exit
    import atexit
    atexit.register(readline.write_history_file, readline_history_file)
    
    print(f"{Colors.CYAN}{'=' * 60}{Colors.RESET}")
    print(f"{Colors.BOLD}{Colors.GREEN}  coder - Interactive Coding Assistant{Colors.RESET}")
    if client.session_name:
        print(f"{Colors.DIM}  Session: {client.session_name}{Colors.RESET}")
    print(f"{Colors.CYAN}{'=' * 60}{Colors.RESET}")
    print(f"{Colors.DIM}Type /quit, /exit or press Ctrl+C to exit.{Colors.RESET}")
    print(f"{Colors.DIM}Type /clear to clear conversation history.{Colors.RESET}")
    print(f"{Colors.DIM}Type /help for more commands.{Colors.RESET}")
    print(f"{Colors.DIM}End line with \\ to continue on next line (multiline){Colors.RESET}")
    print(f"{Colors.CYAN}{'-' * 60}{Colors.RESET}")
    
    while True:
        try:
            # Calculate context usage
            total_text = ""
            for msg in client.conversation_history:
                total_text += msg.get("content", "") or ""
                if "tool_calls" in msg:
                    for tc in msg["tool_calls"]:
                        total_text += tc.get("function", {}).get("arguments", "")
            current_tokens = client._estimate_tokens(total_text)
            max_ctx = client.config.max_context
            
            # Choose color based on usage
            if current_tokens > int(max_ctx * 0.9):
                ctx_color = Colors.RED
            elif current_tokens > int(max_ctx * 0.7):
                ctx_color = Colors.YELLOW
            else:
                ctx_color = Colors.DIM
            
            # Colorful prompt with context counter
            prompt = f"{ctx_color}[{current_tokens}/{max_ctx}]{Colors.RESET} {Colors.BOLD}{Colors.BLUE}CoderCLI>{Colors.RESET} "
            lines = []
            
            while True:
                try:
                    line = input(prompt)
                except EOFError:
                    # Handle Ctrl+D as end of multiline input
                    if lines:
                        break
                    raise
                
                lines.append(line)
                
                # Check for explicit continuation with backslash
                if line.rstrip().endswith('\\'):
                    lines[-1] = line.rstrip()[:-1]  # Remove backslash
                    prompt = f"{Colors.BLUE}  ...>{Colors.RESET} "  # Continuation prompt
                    continue
                
                # Check for incomplete brackets (multiline paste detection)
                joined = '\n'.join(lines)
                open_brackets = joined.count('(') - joined.count(')')
                open_brackets += joined.count('[') - joined.count(']')
                open_brackets += joined.count('{') - joined.count('}')
                
                # Check for incomplete code blocks or sentences
                stripped = line.strip()
                ends_with_colon = stripped.endswith(':')
                ends_with_open = stripped.endswith(('(', '[', '{'))
                
                if open_brackets > 0 or ends_with_colon or ends_with_open:
                    prompt = f"{Colors.BLUE}  ...>{Colors.RESET} "  # Continuation prompt
                    continue
                
                # Empty line after content - finish input (for paste mode)
                if not stripped and len(lines) > 1:
                    lines.pop()  # Remove the empty line
                    break
                
                # Single line input - we're done
                if len(lines) == 1 and not open_brackets and not ends_with_colon and not ends_with_open:
                    break
                
                # Multiple lines with balanced brackets - finish on empty line or new prompt
                if open_brackets == 0 and not ends_with_colon and not ends_with_open:
                    # For pasted multiline content, finish immediately
                    # For manual entry, require empty line
                    if '\n'.join(lines).count('\n') >= 1 and not stripped:
                        lines.pop()  # Remove the empty line
                        break
                    elif '\n'.join(lines).count('\n') >= 1 and stripped:
                        # Pasted content with balanced brackets - likely done
                        break
                    else:
                        prompt = f"{Colors.BLUE}  ...>{Colors.RESET} "
                        continue
            
            user_input = '\n'.join(lines).strip()
            
            if not user_input:
                continue
            
            # Print separator after user input
            print(f"{Colors.CYAN}{'─' * 40}{Colors.RESET}")
            
            # Handle commands with / prefix
            cmd = user_input.lower()
            
            if cmd in ('/quit', '/exit', '/q'):
                print(f"{Colors.GREEN}Goodbye!{Colors.RESET}")
                break
            
            if cmd == '/clear' or cmd == '/c':
                client.clear_history()
                print(f"{Colors.YELLOW}Conversation history cleared.{Colors.RESET}")
                continue
            
            if cmd == '/help' or cmd == '/h':
                print_help()
                continue
            
            if cmd.startswith('/read '):
                path = user_input[6:].strip()
                result = client.tool_executor._read_file(path)
                if 'content' in result:
                    print(f"\n{Colors.CYAN}--- Content of {path} ---{Colors.RESET}")
                    print(result['content'])
                    print(f"{Colors.CYAN}--- End ---{Colors.RESET}")
                else:
                    print(f"{Colors.RED}Error: {result.get('error', 'Unknown error')}{Colors.RESET}")
                continue
            
            if cmd.startswith('/exec '):
                command = user_input[6:].strip()
                result = client.tool_executor._execute_command(command, ".")
                print(f"\n{Colors.GREEN}$ {command}{Colors.RESET}")
                if result.get('stdout'):
                    print(result['stdout'])
                if result.get('stderr'):
                    print(f"{Colors.RED}stderr: {result['stderr']}{Colors.RESET}", file=sys.stderr)
                if result.get('returncode', 0) != 0:
                    print(f"{Colors.RED}Exit code: {result['returncode']}{Colors.RESET}")
                continue
            
            if cmd.startswith('/confirm '):
                parts = user_input[9:].strip().split()
                if len(parts) >= 2:
                    tool_name = parts[0]
                    setting = parts[1].lower()
                    if setting in ('yes', 'y', 'true', '1'):
                        client.config.confirm_commands[tool_name] = True
                        print(f"{Colors.GREEN}Confirmation enabled for {tool_name}{Colors.RESET}")
                    elif setting in ('no', 'n', 'false', '0'):
                        client.config.confirm_commands[tool_name] = False
                        print(f"{Colors.YELLOW}Confirmation disabled for {tool_name}{Colors.RESET}")
                    else:
                        print(f"{Colors.RED}Invalid setting. Use 'yes' or 'no'{Colors.RESET}")
                else:
                    print(f"{Colors.RED}Usage: /confirm <tool_name> <yes/no>{Colors.RESET}")
                    print(f"  Example: /confirm execute_command no")
                    print(f"  Example: /confirm write_file yes")
                continue
            
            # Handle /new command
            if cmd == '/new' or cmd.startswith('/new '):
                name = user_input[5:].strip() if len(user_input) > 5 else None
                session_name = client.new_session(name)
                print(f"{Colors.GREEN}Started new session: {session_name}{Colors.RESET}")
                continue
            
            # Handle /session command
            if cmd.startswith('/session '):
                name = user_input[9:].strip()
                if client.load_session(name):
                    print(f"{Colors.GREEN}Loaded session: {name}{Colors.RESET}")
                else:
                    print(f"{Colors.RED}Session not found: {name}{Colors.RESET}")
                continue
            
            # Handle /delete command
            if cmd.startswith('/delete '):
                target = user_input[8:].strip()
                if target.upper() == 'ALL':
                    count = session_manager.delete_all_sessions()
                    print(f"{Colors.YELLOW}Deleted {count} sessions{Colors.RESET}")
                else:
                    if session_manager.delete_session(target):
                        print(f"{Colors.YELLOW}Deleted session: {target}{Colors.RESET}")
                    else:
                        print(f"{Colors.RED}Session not found: {target}{Colors.RESET}")
                continue
            
            # Handle /sessions command (list sessions)
            if cmd == '/sessions' or cmd == '/ls':
                sessions = session_manager.list_sessions()
                if sessions:
                    print(f"{Colors.CYAN}Available sessions:{Colors.RESET}")
                    for s in sessions:
                        current = " (current)" if s['name'] == client.session_name else ""
                        print(f"  {Colors.GREEN}{s['name']}{Colors.RESET}{current} - {s['message_count']} messages - {s['timestamp'][:19]}")
                else:
                    print(f"{Colors.DIM}No saved sessions{Colors.RESET}")
                continue
            
            # Send message to LLM
            client.chat(user_input)
            
        except KeyboardInterrupt:
            print(f"\n{Colors.GREEN}Goodbye!{Colors.RESET}")
            break
        except EOFError:
            print(f"\n{Colors.GREEN}Goodbye!{Colors.RESET}")
            break


def print_help():
    """Print help information."""
    print(f"""
{Colors.BOLD}{Colors.CYAN}Commands:{Colors.RESET}
  {Colors.GREEN}/quit, /exit, /q{Colors.RESET}    Exit the shell
  {Colors.GREEN}/clear, /c{Colors.RESET}          Clear conversation history
  {Colors.GREEN}/help, /h{Colors.RESET}           Show this help message
  {Colors.GREEN}/new [name]{Colors.RESET}         Start a new session (optional name)
  {Colors.GREEN}/session <name>{Colors.RESET}     Load a saved session
  {Colors.GREEN}/sessions, /ls{Colors.RESET}      List all saved sessions
  {Colors.GREEN}/delete <name|ALL>{Colors.RESET}  Delete a session or all sessions
  
{Colors.BOLD}{Colors.CYAN}Shortcuts:{Colors.RESET}
  {Colors.YELLOW}/read <path>{Colors.RESET}      Read a file directly
  {Colors.YELLOW}/exec <command>{Colors.RESET}   Execute a shell command directly
  {Colors.YELLOW}/confirm <tool> <y/n>{Colors.RESET}  Enable/disable tool confirmation

{Colors.BOLD}{Colors.CYAN}Multiline Input:{Colors.RESET}
  - End a line with {Colors.YELLOW}\\{Colors.RESET} to continue
  - Or paste multiline content directly
  - Or press Enter twice to finish
  - Unclosed brackets ( ) [ ] {{ }} or : continue automatically
  Example:
    CoderCLI> This is a \\
      ...> multiline message

{Colors.BOLD}{Colors.CYAN}The assistant can use tools to:{Colors.RESET}
  - {Colors.BLUE}read_file{Colors.RESET}: Read file contents
  - {Colors.BLUE}write_file{Colors.RESET}: Write/create files
  - {Colors.BLUE}apply_diff{Colors.RESET}: Apply patches to files
  - {Colors.BLUE}execute_command{Colors.RESET}: Run shell commands
  
{Colors.DIM}Tool execution requires confirmation by default.{Colors.RESET}
{Colors.DIM}Disable confirmation with: /confirm <tool_name> no{Colors.RESET}
""")


def main():
    parser = argparse.ArgumentParser(
        description="coder - CLI tool for coderai API",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  coder                    Start interactive shell
  coder "Hello!"           Send a single message
  coder -m "Fix bug"       Send message non-interactively
  coder --config /path     Use custom config file
  coder --ctx 65536        Set max context to 65536 tokens
  coder --session myproj   Load session 'myproj'
        """
    )
    
    parser.add_argument(
        'message',
        nargs='?',
        help='Message to send (if not provided, starts interactive shell)'
    )
    
    parser.add_argument(
        '-m', '--message',
        dest='msg_flag',
        help='Message to send (alternative to positional argument)'
    )
    
    parser.add_argument(
        '--api-url',
        help='API URL (default: from config or http://localhost:6744/v1)'
    )
    
    parser.add_argument(
        '--endpoint',
        help='API endpoint URL (same as --api-url, temporary override)'
    )
    
    parser.add_argument(
        '--token',
        help='API token (default: from config, temporary override)'
    )
    
    parser.add_argument(
        '--model',
        help='Model name to use (default: "default", temporary override)'
    )
    
    parser.add_argument(
        '--alias',
        help='Create an alias for the model (alias -> model mapping)'
    )
    
    parser.add_argument(
        '--config',
        help='Path to config file (default: ~/.config/coderai/cli.json)'
    )
    
    parser.add_argument(
        '--init-config',
        action='store_true',
        help='Create default config file and exit'
    )
    
    parser.add_argument(
        '--no-stream',
        action='store_true',
        help='Disable streaming responses'
    )
    
    parser.add_argument(
        '--small',
        action='store_true',
        help='Use small model mode (simplified prompt for models under 7B parameters)'
    )
    
    parser.add_argument(
        '--tiny',
        action='store_true',
        help='Use tiny model mode (minimal prompt for models under 3B parameters)'
    )
    
    parser.add_argument(
        '--micro',
        action='store_true',
        help='Use micro model mode (ultra-minimal prompt for models under 1.5B parameters)'
    )
    
    parser.add_argument(
        '--no-prompt',
        action='store_true',
        dest='no_prompt',
        help='Do not send system prompt (for custom use cases)'
    )
    
    parser.add_argument(
        '--timeout',
        type=int,
        default=600,
        help='Request timeout in seconds (default: 600)'
    )
    
    parser.add_argument(
        '--debug',
        action='store_true',
        help='Show debug output including raw tool calls'
    )
    
    parser.add_argument(
        '--dump',
        action='store_true',
        help='Show dump output: tools schema, raw response, and parsed tool calls'
    )
    
    parser.add_argument(
        '--no-tools',
        action='store_true',
        dest='no_tools',
        help='Do not send tool definitions to the API (plain chat mode)'
    )
    
    parser.add_argument(
        '--ctx',
        type=int,
        dest='max_context',
        help='Maximum context size in tokens (default: 32768)'
    )
    
    parser.add_argument(
        '--session',
        help='Load a saved session by name'
    )
    
    args = parser.parse_args()
    
    # Handle init-config
    if args.init_config:
        config = Config()
        config.save()
        config_path = os.path.expanduser("~/.config/coderai/cli.json")
        print(f"Created default config at: {config_path}")
        print(json.dumps({
            'api_url': config.api_url,
            'token': config.token,
            'system_prompt': config.system_prompt,
            'model': config.model,
            'model_aliases': config.model_aliases,
            'small': config.small,
            'tiny': config.tiny,
            'max_context': config.max_context,
            'no_tools': config.no_tools
        }, indent=2))
        return
    
    # Load config
    config = Config.load(args.config)
    
    # Override with command line args (temporary, not saved to config)
    if args.api_url:
        config.api_url = args.api_url
    if args.endpoint:
        config.api_url = args.endpoint
    if args.token:
        config.token = args.token
    if args.model:
        config.model = args.model
    if args.alias:
        # Register alias: alias -> model mapping
        # If model not specified, alias points to "default"
        target_model = args.model if args.model else config.model
        config.model_aliases[args.alias] = target_model
        print(f"[Alias registered: {args.alias} -> {target_model}]")
    if args.small:
        config.small = True
    if args.tiny:
        config.tiny = True
    if args.micro:
        config.micro = True
    if args.timeout:
        config.timeout = args.timeout
    if args.debug:
        config.debug = True
    
    if args.dump:
        config.dump = True
    if args.max_context:
        config.max_context = args.max_context
    if args.no_prompt:
        config.no_prompt = True
    if args.no_tools:
        config.no_tools = True
    
    # Apply small/tiny model system prompt if enabled
    if config.micro:
        config.system_prompt = MICRO_MODEL_SYSTEM_PROMPT
        print("[Micro model mode enabled - using ultra-minimal system prompt]")
    elif config.tiny:
        config.system_prompt = TINY_MODEL_SYSTEM_PROMPT
        print("[Tiny model mode enabled - using minimal system prompt]")
    elif config.small:
        config.system_prompt = SMALL_MODEL_SYSTEM_PROMPT
        print("[Small model mode enabled - using simplified system prompt]")
    
    # Create session manager
    session_manager = SessionManager()
    
    # Create client
    client = CoderClient(config, session_manager)
    
    # Load session if specified
    if args.session:
        if client.load_session(args.session):
            print(f"[Loaded session: {args.session}]")
        else:
            print(f"[Session not found: {args.session}, starting new session]")
            client.new_session(args.session)
    else:
        # Start with a default session name
        client.new_session()
    
    # Get message
    message = args.message or args.msg_flag
    
    if os.environ.get('CODER_DEBUG'):
        print(f"DEBUG: message = {message}", file=sys.stderr)
        print(f"DEBUG: args.no_stream = {args.no_stream}", file=sys.stderr)
    
    if message:
        # Single message mode - disable confirmations for non-interactive use
        if os.environ.get('CODER_DEBUG'):
            print(f"DEBUG: Entering single message mode", file=sys.stderr)
        client.config.confirm_all = False
        result = client.chat(message, stream=not args.no_stream)
        # Print result if non-streaming (streaming prints internally)
        if args.no_stream and result:
            print(result)
        if os.environ.get('CODER_DEBUG'):
            print(f"DEBUG: chat() returned", file=sys.stderr)
    else:
        # Interactive shell mode
        if os.environ.get('CODER_DEBUG'):
            print(f"DEBUG: Entering interactive shell mode", file=sys.stderr)
        run_interactive_shell(client, session_manager)


if __name__ == "__main__":
    main()
