You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
"""Claude Code CLI subprocess wrapper."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from loguru import logger
|
|
|
|
from gimme_job.models.runtime import ClaudeRunResult
|
|
|
|
DEFAULT_LEARN_TOOLS = ["Read", "Edit", "Write", "Bash", "Grep", "Glob"]
|
|
DEFAULT_REPAIR_TOOLS = ["Read", "Edit", "Write", "Bash", "Grep", "Glob"]
|
|
DEFAULT_TIMEOUT_SECONDS = 600
|
|
|
|
|
|
class ClaudeCodeClient:
|
|
def __init__(self, cwd: Optional[Path] = None, timeout: int = DEFAULT_TIMEOUT_SECONDS):
|
|
from gimme_job.utils.paths import project_root
|
|
self.cwd = str(cwd or project_root())
|
|
self.timeout = timeout
|
|
|
|
def run_prompt(
|
|
self,
|
|
prompt: str,
|
|
cwd: Optional[Path] = None,
|
|
allowed_tools: Optional[list[str]] = None,
|
|
max_turns: int = 30,
|
|
) -> ClaudeRunResult:
|
|
"""Run a prompt via `claude -p`. Returns parsed ClaudeRunResult."""
|
|
tools = allowed_tools or DEFAULT_LEARN_TOOLS
|
|
cmd = [
|
|
"claude",
|
|
"-p", prompt,
|
|
"--output-format", "json",
|
|
"--max-turns", str(max_turns),
|
|
"--allowedTools", ",".join(tools),
|
|
]
|
|
|
|
work_dir = str(cwd) if cwd else self.cwd
|
|
start = time.time()
|
|
|
|
logger.info(f"Calling Claude Code CLI (cwd={work_dir})")
|
|
logger.debug(f"Command: {' '.join(cmd[:4])}...")
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=work_dir,
|
|
timeout=self.timeout,
|
|
)
|
|
duration_ms = int((time.time() - start) * 1000)
|
|
|
|
if result.returncode != 0:
|
|
logger.error(f"Claude Code returned non-zero: {result.returncode}")
|
|
logger.debug(f"stderr: {result.stderr[:500]}")
|
|
return ClaudeRunResult(
|
|
success=False,
|
|
output=result.stdout,
|
|
error=result.stderr[:500],
|
|
duration_ms=duration_ms,
|
|
)
|
|
|
|
# Parse JSON output
|
|
parsed = self._parse_output(result.stdout)
|
|
return ClaudeRunResult(
|
|
success=True,
|
|
session_id=parsed.get("session_id"),
|
|
output=parsed.get("result", result.stdout),
|
|
cost_usd=parsed.get("cost_usd"),
|
|
duration_ms=duration_ms,
|
|
)
|
|
|
|
except subprocess.TimeoutExpired:
|
|
logger.error(f"Claude Code timed out after {self.timeout}s")
|
|
return ClaudeRunResult(
|
|
success=False,
|
|
error=f"Timed out after {self.timeout}s",
|
|
)
|
|
except FileNotFoundError:
|
|
logger.error("Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code")
|
|
return ClaudeRunResult(success=False, error="claude CLI not found")
|
|
|
|
def resume(
|
|
self,
|
|
session_id: str,
|
|
prompt: str,
|
|
cwd: Optional[Path] = None,
|
|
) -> ClaudeRunResult:
|
|
"""Resume a previous session via `--resume`."""
|
|
cmd = [
|
|
"claude",
|
|
"-p", prompt,
|
|
"--resume", session_id,
|
|
"--output-format", "json",
|
|
]
|
|
|
|
work_dir = str(cwd) if cwd else self.cwd
|
|
start = time.time()
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
cmd, capture_output=True, text=True, cwd=work_dir, timeout=self.timeout
|
|
)
|
|
duration_ms = int((time.time() - start) * 1000)
|
|
parsed = self._parse_output(result.stdout)
|
|
return ClaudeRunResult(
|
|
success=result.returncode == 0,
|
|
session_id=parsed.get("session_id"),
|
|
output=parsed.get("result", result.stdout),
|
|
duration_ms=duration_ms,
|
|
error=result.stderr[:500] if result.returncode != 0 else None,
|
|
)
|
|
except Exception as e:
|
|
return ClaudeRunResult(success=False, error=str(e))
|
|
|
|
def _parse_output(self, raw: str) -> dict:
|
|
"""Parse Claude Code JSON output, handling streaming line format."""
|
|
try:
|
|
return json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
# Try last non-empty line (streaming format)
|
|
lines = [l.strip() for l in raw.strip().splitlines() if l.strip()]
|
|
for line in reversed(lines):
|
|
try:
|
|
return json.loads(line)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
|
|
return {"result": raw}
|