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,6 +1,6 @@
1
1
  {
2
2
  "name": "autopilot-code",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "description": "Repo-issue–driven autopilot runner",
6
6
  "license": "MIT",
@@ -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