autopilot-code 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
from .base import BaseAgent, AgentResult
|
|
2
2
|
from .opencode import OpenCodeAgent
|
|
3
|
+
from .claude import ClaudeCodeAgent
|
|
3
4
|
|
|
4
|
-
__all__ = ["BaseAgent", "AgentResult", "OpenCodeAgent"]
|
|
5
|
+
__all__ = ["BaseAgent", "AgentResult", "OpenCodeAgent", "ClaudeCodeAgent"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_agent(agent_type: str, config: dict) -> BaseAgent:
|
|
9
|
+
"""
|
|
10
|
+
Factory function to create the appropriate agent.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
agent_type: "opencode" or "claude"
|
|
14
|
+
config: Agent configuration from autopilot.json
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Configured agent instance
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: If agent_type is unknown
|
|
21
|
+
"""
|
|
22
|
+
agents = {
|
|
23
|
+
"opencode": OpenCodeAgent,
|
|
24
|
+
"claude": ClaudeCodeAgent,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if agent_type not in agents:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"Unknown agent type: {agent_type}. Available: {list(agents.keys())}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return agents[agent_type](config)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import shutil
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .base import BaseAgent, AgentResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClaudeCodeAgent(BaseAgent):
|
|
11
|
+
"""
|
|
12
|
+
Agent implementation for Claude Code CLI (Anthropic's official CLI).
|
|
13
|
+
|
|
14
|
+
Claude Code is invoked via: claude [options] "<prompt>"
|
|
15
|
+
|
|
16
|
+
Session support: Claude Code supports --resume <session_id> to continue sessions.
|
|
17
|
+
Sessions maintain full conversation history and context.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self) -> str:
|
|
22
|
+
return "Claude Code"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def supports_sessions(self) -> bool:
|
|
26
|
+
return True # Claude Code supports --resume
|
|
27
|
+
|
|
28
|
+
def find_binary(self) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Locate the claude binary.
|
|
31
|
+
|
|
32
|
+
Search order:
|
|
33
|
+
1. agentPath from config
|
|
34
|
+
2. PATH
|
|
35
|
+
3. Common npm global locations
|
|
36
|
+
4. Homebrew location (macOS)
|
|
37
|
+
"""
|
|
38
|
+
# 1. Config-specified path
|
|
39
|
+
agent_path = self.config.get("agentPath", "")
|
|
40
|
+
if agent_path and os.path.isfile(agent_path) and os.access(agent_path, os.X_OK):
|
|
41
|
+
return agent_path
|
|
42
|
+
|
|
43
|
+
# 2. Already in PATH
|
|
44
|
+
which_result = shutil.which("claude")
|
|
45
|
+
if which_result:
|
|
46
|
+
return which_result
|
|
47
|
+
|
|
48
|
+
# 3. Common locations
|
|
49
|
+
home = Path.home()
|
|
50
|
+
common_paths = [
|
|
51
|
+
home / ".local" / "bin" / "claude",
|
|
52
|
+
Path("/usr/local/bin/claude"),
|
|
53
|
+
home / ".npm-global" / "bin" / "claude",
|
|
54
|
+
Path("/opt/homebrew/bin/claude"), # macOS Homebrew
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
# Also check nvm locations
|
|
58
|
+
nvm_dir = home / ".nvm" / "versions" / "node"
|
|
59
|
+
if nvm_dir.exists():
|
|
60
|
+
for node_dir in nvm_dir.iterdir():
|
|
61
|
+
claude_path = node_dir / "bin" / "claude"
|
|
62
|
+
if claude_path.exists() and os.access(claude_path, os.X_OK):
|
|
63
|
+
return str(claude_path)
|
|
64
|
+
|
|
65
|
+
for path in common_paths:
|
|
66
|
+
if path.exists() and os.access(path, os.X_OK):
|
|
67
|
+
return str(path)
|
|
68
|
+
|
|
69
|
+
raise FileNotFoundError(
|
|
70
|
+
"claude not found. Install via 'npm install -g @anthropic-ai/claude-code' "
|
|
71
|
+
"or set 'agentPath' in autopilot.json."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def run(
|
|
75
|
+
self, worktree: Path, prompt: str, session_id: Optional[str] = None
|
|
76
|
+
) -> AgentResult:
|
|
77
|
+
"""
|
|
78
|
+
Run Claude Code with the given prompt.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
worktree: Working directory for the agent
|
|
82
|
+
prompt: The task/prompt
|
|
83
|
+
session_id: Previous session ID to resume (if any)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
AgentResult with session_id for future continuation
|
|
87
|
+
"""
|
|
88
|
+
cmd = [self.binary_path]
|
|
89
|
+
|
|
90
|
+
# Add session resume if we have a previous session
|
|
91
|
+
if session_id and self.supports_sessions:
|
|
92
|
+
cmd.extend(["--resume", session_id])
|
|
93
|
+
|
|
94
|
+
# Add print mode for non-interactive execution
|
|
95
|
+
cmd.extend(["--print", "--dangerously-skip-permissions"])
|
|
96
|
+
|
|
97
|
+
# Add the prompt
|
|
98
|
+
cmd.extend(["-p", prompt])
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
result = subprocess.run(
|
|
102
|
+
cmd,
|
|
103
|
+
cwd=worktree,
|
|
104
|
+
capture_output=True,
|
|
105
|
+
text=True,
|
|
106
|
+
timeout=1800, # 30 minute timeout
|
|
107
|
+
env={**os.environ, "CLAUDE_CODE_HEADLESS": "1"},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Extract session ID from output
|
|
111
|
+
# Claude Code outputs the session ID which we can capture
|
|
112
|
+
new_session_id = (
|
|
113
|
+
self._extract_session_id(result.stdout, result.stderr) or session_id
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return AgentResult(
|
|
117
|
+
success=result.returncode == 0,
|
|
118
|
+
session_id=new_session_id,
|
|
119
|
+
output=result.stdout,
|
|
120
|
+
error=result.stderr if result.returncode != 0 else None,
|
|
121
|
+
)
|
|
122
|
+
except subprocess.TimeoutExpired:
|
|
123
|
+
return AgentResult(
|
|
124
|
+
success=False,
|
|
125
|
+
session_id=session_id,
|
|
126
|
+
output="",
|
|
127
|
+
error="Agent timed out after 30 minutes",
|
|
128
|
+
)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
return AgentResult(
|
|
131
|
+
success=False, session_id=session_id, output="", error=str(e)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _extract_session_id(self, stdout: str, stderr: str) -> Optional[str]:
|
|
135
|
+
"""
|
|
136
|
+
Extract session ID from Claude Code output.
|
|
137
|
+
|
|
138
|
+
Claude Code outputs session information that we can parse to get
|
|
139
|
+
the session ID for future --resume calls.
|
|
140
|
+
"""
|
|
141
|
+
import re
|
|
142
|
+
|
|
143
|
+
# Check both stdout and stderr for session info
|
|
144
|
+
for output in [stdout, stderr]:
|
|
145
|
+
# Look for session ID patterns
|
|
146
|
+
# Claude Code may output something like "Session ID: abc123" or similar
|
|
147
|
+
patterns = [
|
|
148
|
+
r"session[_\s]?id[:\s]+([a-zA-Z0-9_-]+)",
|
|
149
|
+
r"resume[:\s]+([a-zA-Z0-9_-]+)",
|
|
150
|
+
r"--resume\s+([a-zA-Z0-9_-]+)",
|
|
151
|
+
]
|
|
152
|
+
for pattern in patterns:
|
|
153
|
+
match = re.search(pattern, output, re.IGNORECASE)
|
|
154
|
+
if match:
|
|
155
|
+
return match.group(1)
|
|
156
|
+
|
|
157
|
+
return None
|