diff --git a/roles/evaluator.py b/roles/evaluator.py index f575f7c..340a936 100755 --- a/roles/evaluator.py +++ b/roles/evaluator.py @@ -41,6 +41,23 @@ def _call_llm(prompt: str, cfg: dict) -> tuple[str, int, float]: tokens = resp.usage.total_tokens return resp.choices[0].message.content, tokens, tokens * 3e-6 + if provider == "claude_cli": + import subprocess + proc = subprocess.run( + ["claude", "-p", "--model", model, "--output-format", "json"], + input=prompt, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}") + data = json.loads(proc.stdout) + text = data.get("result", "") + usage = data.get("usage", {}) + tokens = usage.get("input_tokens", 0) + usage.get("output_tokens", 0) + cost = float(data.get("total_cost_usd", 0.0)) + return text, tokens, cost + raise ValueError(f"unknown llm.provider: {provider!r}") diff --git a/roles/goal_evaluator.py b/roles/goal_evaluator.py index 3085a1c..e816a85 100644 --- a/roles/goal_evaluator.py +++ b/roles/goal_evaluator.py @@ -38,6 +38,21 @@ def _call_openai(prompt: str, model: str, api_key_env: str) -> str: return resp.choices[0].message.content +def _call_claude_cli(prompt: str, model: str) -> str: + """Call Claude via the local Claude Code CLI (no API key required).""" + import subprocess + proc = subprocess.run( + ["claude", "-p", "--model", model, "--output-format", "json"], + input=prompt, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}") + data = json.loads(proc.stdout) + return data.get("result", "") + + def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--input", required=True) @@ -81,6 +96,8 @@ def main() -> None: text = _call_anthropic(prompt, model, api_key_env) elif provider == "openai": text = _call_openai(prompt, model, api_key_env) + elif provider == "claude_cli": + text = _call_claude_cli(prompt, model) else: print(f"error: unknown llm.provider: {provider!r}", file=sys.stderr) sys.exit(1) diff --git a/roles/planner.py b/roles/planner.py index ec2f14f..06e2308 100755 --- a/roles/planner.py +++ b/roles/planner.py @@ -45,6 +45,29 @@ def _call_openai(prompt: str, model: str, api_key_env: str) -> tuple[str, int, f return resp.choices[0].message.content, tokens, cost +def _call_claude_cli(prompt: str, model: str) -> tuple[str, int, float]: + """Call Claude via the local Claude Code CLI (no API key required). + + Uses --output-format json to recover the response text plus real + token / cost numbers, so kernel-level cost guards keep working. + """ + import subprocess + proc = subprocess.run( + ["claude", "-p", "--model", model, "--output-format", "json"], + input=prompt, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}") + data = json.loads(proc.stdout) + text = data.get("result", "") + usage = data.get("usage", {}) + tokens = usage.get("input_tokens", 0) + usage.get("output_tokens", 0) + cost = float(data.get("total_cost_usd", 0.0)) + return text, tokens, cost + + def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--input", required=True) @@ -104,6 +127,8 @@ def main() -> None: text, tokens, cost = _call_anthropic(prompt, model, api_key_env) elif provider == "openai": text, tokens, cost = _call_openai(prompt, model, api_key_env) + elif provider == "claude_cli": + text, tokens, cost = _call_claude_cli(prompt, model) else: print(f"error: unknown llm.provider: {provider!r}", file=sys.stderr) sys.exit(1) diff --git a/roles/strategist.py b/roles/strategist.py index 74d6af9..02db832 100644 --- a/roles/strategist.py +++ b/roles/strategist.py @@ -38,6 +38,21 @@ def _call_openai(prompt: str, model: str, api_key_env: str) -> str: return resp.choices[0].message.content +def _call_claude_cli(prompt: str, model: str) -> str: + """Call Claude via the local Claude Code CLI (no API key required).""" + import subprocess + proc = subprocess.run( + ["claude", "-p", "--model", model, "--output-format", "json"], + input=prompt, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}") + data = json.loads(proc.stdout) + return data.get("result", "") + + def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--input", required=True) @@ -80,6 +95,8 @@ def main() -> None: text = _call_anthropic(prompt, model, api_key_env) elif provider == "openai": text = _call_openai(prompt, model, api_key_env) + elif provider == "claude_cli": + text = _call_claude_cli(prompt, model) else: print(f"error: unknown llm.provider: {provider!r}", file=sys.stderr) sys.exit(1)