""" Claude Code CLI adapter — runs the ``claude`` CLI as a subprocess. """ import subprocess from typing import Optional from markitect.prompts.execution.llm_adapter import LLMAdapter from markitect.prompts.execution.models import RunConfig, LLMResponse from markitect.llm.config import LLMConfig from markitect.llm._token_estimator import estimate_tokens from markitect.llm.exceptions import ( LLMSubprocessError, LLMTimeoutError, ) class ClaudeCodeAdapter(LLMAdapter): """LLM adapter that shells out to the ``claude`` CLI with ``--print``. The compiled prompt is piped via **stdin** to avoid shell argument length limits (compiled prompts can exceed 30 KB). """ def __init__( self, cli_path: str = "claude", model: Optional[str] = None, config: Optional[LLMConfig] = None, ): self._config = config or LLMConfig(provider="claude-code") self._cli_path = cli_path or self._config.claude_cli_path self._model = model # ── LLMAdapter interface ──────────────────────────────────────── def execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse: cmd = [self._cli_path, "--print"] if self._model: cmd.extend(["--model", self._model]) timeout = config.timeout_seconds or self._config.timeout_seconds try: result = subprocess.run( cmd, input=prompt, capture_output=True, text=True, timeout=timeout, ) except subprocess.TimeoutExpired as exc: raise LLMTimeoutError( f"claude CLI timed out after {timeout}s", cause=exc, ) from exc if result.returncode != 0: raise LLMSubprocessError( f"claude CLI exited with code {result.returncode}", return_code=result.returncode, stderr=result.stderr, ) content = result.stdout prompt_tokens = estimate_tokens(prompt) completion_tokens = estimate_tokens(content) return LLMResponse( content=content, model=self._model or "claude-code-cli", usage={ "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": prompt_tokens + completion_tokens, }, finish_reason="stop", metadata={ "provider": "claude-code", "cli_path": self._cli_path, }, ) def validate_config(self, config: RunConfig) -> bool: try: result = subprocess.run( [self._cli_path, "--version"], capture_output=True, text=True, timeout=10, ) return result.returncode == 0 except (subprocess.TimeoutExpired, FileNotFoundError, OSError): return False