"""
Claude Code CLI backend.
Implements the AgentCLI protocol for Claude Code CLI.
This preserves the existing behavior from iterm_utils.py.
"""
from typing import Literal
from .base import AgentCLI
from ..utils.env_vars import get_env_with_fallback
# Built-in default command.
_DEFAULT_COMMAND = "claude"
# Environment variables for command override (takes highest precedence).
_ENV_VAR = "MANIPLE_COMMAND"
_ENV_VAR_FALLBACK = "CLAUDE_TEAM_COMMAND"
def get_claude_command() -> str:
"""
Get the Claude CLI command with precedence: env var > config > default.
Resolution order:
1. MANIPLE_COMMAND environment variable (for override)
2. Config file commands.claude setting
3. Built-in default "claude"
Returns:
The command to use for Claude CLI
"""
# Environment variable takes highest precedence (for override).
env_val = get_env_with_fallback(_ENV_VAR, _ENV_VAR_FALLBACK)
if env_val:
return env_val
# Try config file next.
# Import here to avoid circular imports and lazy-load config.
try:
from ..config import ConfigError, load_config
config = load_config()
except ConfigError:
return _DEFAULT_COMMAND
if config.commands.claude:
return config.commands.claude
# Fall back to built-in default.
return _DEFAULT_COMMAND
class ClaudeCLI(AgentCLI):
"""
Claude Code CLI implementation.
Supports:
- --dangerously-skip-permissions flag
- --settings flag for Stop hook injection
- Ready detection via TUI patterns (robot banner, '>' prompt, 'tokens' status)
- Idle detection via Stop hook markers in JSONL
"""
@property
def engine_id(self) -> str:
"""Return 'claude' as the engine identifier."""
return "claude"
def command(self) -> str:
"""
Return the Claude CLI command.
Resolution order:
1. MANIPLE_COMMAND environment variable (for override)
2. Config file commands.claude setting
3. Built-in default "claude"
"""
return get_claude_command()
def build_args(
self,
*,
dangerously_skip_permissions: bool = False,
settings_file: str | None = None,
) -> list[str]:
"""
Build Claude CLI arguments.
Args:
dangerously_skip_permissions: Add --dangerously-skip-permissions
settings_file: Path to settings JSON for Stop hook injection
Returns:
List of CLI arguments
"""
args: list[str] = []
if dangerously_skip_permissions:
args.append("--dangerously-skip-permissions")
# Only add --settings for the default 'claude' command.
# Custom commands like 'happy' have their own session tracking mechanisms.
# See HAPPY_INTEGRATION_RESEARCH.md for full analysis.
if settings_file and self._is_default_command():
args.append("--settings")
args.append(settings_file)
return args
def ready_patterns(self) -> list[str]:
"""
Return patterns indicating Claude TUI is ready.
These patterns appear in Claude's startup:
- '>' prompt indicates input ready
- 'tokens' in status bar
- Parts of the robot ASCII art banner
"""
return [
">", # Input prompt
"tokens", # Status bar
"Claude Code v", # Version line in banner
"▐▛███▜▌", # Top of robot head
"▝▜█████▛▘", # Middle of robot
]
def idle_detection_method(self) -> Literal["stop_hook", "jsonl_stream", "none"]:
"""
Claude uses Stop hook for idle detection.
A Stop hook writes a marker to the JSONL when Claude finishes responding.
"""
return "stop_hook"
def supports_settings_file(self) -> bool:
"""
Claude supports --settings for hook injection.
Only returns True for the default 'claude' command.
Custom wrappers may have their own settings mechanisms.
"""
return self._is_default_command()
def _is_default_command(self) -> bool:
"""Check if using the default 'claude' command (not a custom wrapper)."""
return get_claude_command() == _DEFAULT_COMMAND
# Singleton instance for convenience
claude_cli = ClaudeCLI()