mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-05-22 11:09:37 +00:00
97 lines
3.1 KiB
Python
97 lines
3.1 KiB
Python
import asyncio
|
|
import os
|
|
import json
|
|
import logging
|
|
from typing import AsyncGenerator, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CLISession:
|
|
"""Manages a single persistent Claude Code CLI subprocess."""
|
|
|
|
def __init__(
|
|
self,
|
|
workspace_path: str,
|
|
api_url: str,
|
|
allowed_dirs: Optional[list[str]] = None,
|
|
):
|
|
self.workspace = workspace_path
|
|
self.api_url = api_url
|
|
self.allowed_dirs = allowed_dirs or []
|
|
self.process: Optional[asyncio.subprocess.Process] = None
|
|
|
|
async def start_task(self, prompt: str) -> AsyncGenerator[dict, None]:
|
|
"""Runs a single prompt and yields JSON events from the CLI."""
|
|
env = os.environ.copy()
|
|
|
|
# FIX for 404:
|
|
# ANTHROPIC_API_URL usually wants the full path to the API endpoint or version root
|
|
env["ANTHROPIC_API_URL"] = self.api_url
|
|
|
|
# ANTHROPIC_BASE_URL usually wants the server root, and the client appends /v1/messages
|
|
# If self.api_url is "http://localhost:8082/v1", base should be "http://localhost:8082"
|
|
if self.api_url.endswith("/v1"):
|
|
env["ANTHROPIC_BASE_URL"] = self.api_url[:-3]
|
|
else:
|
|
env["ANTHROPIC_BASE_URL"] = self.api_url
|
|
|
|
# Ensure we don't try to use interactive TTY features
|
|
env["TERM"] = "dumb"
|
|
# Ensure path is normalized for Windows to avoid \a (bell) character issues
|
|
normalized_workspace = os.path.normpath(self.workspace)
|
|
|
|
cmd = [
|
|
"claude",
|
|
"-p",
|
|
prompt,
|
|
"--output-format",
|
|
"stream-json",
|
|
"--dangerously-skip-permissions",
|
|
"--verbose",
|
|
]
|
|
|
|
if self.allowed_dirs:
|
|
for d in self.allowed_dirs:
|
|
cmd.extend(["--add-dir", os.path.normpath(d)])
|
|
|
|
logger.info(
|
|
f"Launching Claude CLI in {normalized_workspace} with API {self.api_url}"
|
|
)
|
|
|
|
self.process = await asyncio.create_subprocess_exec(
|
|
*cmd,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
cwd=normalized_workspace,
|
|
env=env,
|
|
)
|
|
|
|
# Read stdout line by line
|
|
while True:
|
|
line = await self.process.stdout.readline()
|
|
if not line:
|
|
break
|
|
|
|
line_str = line.decode("utf-8").strip()
|
|
if not line_str:
|
|
continue
|
|
|
|
try:
|
|
event = json.loads(line_str)
|
|
yield event
|
|
except json.JSONDecodeError:
|
|
# Log non-JSON lines for debugging but don't crash
|
|
logger.debug(f"Non-JSON output: {line_str}")
|
|
yield {"type": "raw", "content": line_str}
|
|
|
|
# Capture remaining stderr if the process crashed
|
|
stderr_output = await self.process.stderr.read()
|
|
if stderr_output:
|
|
logger.error(
|
|
f"Claude CLI Stderr: {stderr_output.decode('utf-8', errors='replace')}"
|
|
)
|
|
|
|
return_code = await self.process.wait()
|
|
logger.info(f"Claude CLI exited with code {return_code}")
|
|
yield {"type": "exit", "code": return_code}
|