This is page 2 of 6. Use http://codebase.md/arthurcolle/openai-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitignore
├── claude_code
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-312.pyc
│ │ └── mcp_server.cpython-312.pyc
│ ├── claude.py
│ ├── commands
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── __init__.cpython-312.pyc
│ │ │ └── serve.cpython-312.pyc
│ │ ├── client.py
│ │ ├── multi_agent_client.py
│ │ └── serve.py
│ ├── config
│ │ └── __init__.py
│ ├── examples
│ │ ├── agents_config.json
│ │ ├── claude_mcp_config.html
│ │ ├── claude_mcp_config.json
│ │ ├── echo_server.py
│ │ └── README.md
│ ├── lib
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ └── __init__.cpython-312.pyc
│ │ ├── context
│ │ │ └── __init__.py
│ │ ├── monitoring
│ │ │ ├── __init__.py
│ │ │ ├── __pycache__
│ │ │ │ ├── __init__.cpython-312.pyc
│ │ │ │ └── server_metrics.cpython-312.pyc
│ │ │ ├── cost_tracker.py
│ │ │ └── server_metrics.py
│ │ ├── providers
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ └── openai.py
│ │ ├── rl
│ │ │ ├── __init__.py
│ │ │ ├── grpo.py
│ │ │ ├── mcts.py
│ │ │ └── tool_optimizer.py
│ │ ├── tools
│ │ │ ├── __init__.py
│ │ │ ├── __pycache__
│ │ │ │ ├── __init__.cpython-312.pyc
│ │ │ │ ├── base.cpython-312.pyc
│ │ │ │ ├── file_tools.cpython-312.pyc
│ │ │ │ └── manager.cpython-312.pyc
│ │ │ ├── ai_tools.py
│ │ │ ├── base.py
│ │ │ ├── code_tools.py
│ │ │ ├── file_tools.py
│ │ │ ├── manager.py
│ │ │ └── search_tools.py
│ │ └── ui
│ │ ├── __init__.py
│ │ └── tool_visualizer.py
│ ├── mcp_server.py
│ ├── README_MCP_CLIENT.md
│ ├── README_MULTI_AGENT.md
│ └── util
│ └── __init__.py
├── claude.py
├── cli.py
├── data
│ └── prompt_templates.json
├── deploy_modal_mcp.py
├── deploy.sh
├── examples
│ ├── agents_config.json
│ └── echo_server.py
├── install.sh
├── mcp_modal_adapter.py
├── mcp_server.py
├── modal_mcp_server.py
├── README_modal_mcp.md
├── README.md
├── requirements.txt
├── setup.py
├── static
│ └── style.css
├── templates
│ └── index.html
└── web-client.html
```
# Files
--------------------------------------------------------------------------------
/claude_code/mcp_server.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # claude_code/mcp_server.py
3 | """Model Context Protocol server implementation using FastMCP."""
4 |
5 | import os
6 | import logging
7 | import platform
8 | import sys
9 | import uuid
10 | import time
11 | from typing import Dict, List, Any, Optional, Callable, Union
12 | import pathlib
13 | import json
14 | from fastmcp import FastMCP, Context, Image
15 |
16 | from claude_code.lib.tools.base import Tool, ToolRegistry
17 | from claude_code.lib.tools.manager import ToolExecutionManager
18 | from claude_code.lib.tools.file_tools import register_file_tools
19 | from claude_code.lib.monitoring.server_metrics import get_metrics
20 |
21 | # Initialize logging
22 | logging.basicConfig(level=logging.INFO)
23 | logger = logging.getLogger(__name__)
24 |
25 | # Get server metrics
26 | metrics = get_metrics()
27 |
28 | # Create the FastMCP server
29 | mcp = FastMCP(
30 | "Claude Code MCP Server",
31 | description="A Model Context Protocol server for Claude Code tools",
32 | dependencies=["fastmcp>=0.4.1", "openai", "pydantic"],
33 | homepage_html_file=str(pathlib.Path(__file__).parent / "examples" / "claude_mcp_config.html")
34 | )
35 |
36 | # Initialize tool registry and manager
37 | tool_registry = ToolRegistry()
38 | tool_manager = ToolExecutionManager(tool_registry)
39 |
40 | # Register file tools
41 | register_file_tools(tool_registry)
42 |
43 |
44 | def setup_tools():
45 | """Register all tools from the tool registry with FastMCP."""
46 |
47 | # Get all registered tools
48 | registered_tools = tool_registry.get_all_tools()
49 |
50 | for tool_obj in registered_tools:
51 | # Convert the tool execution function to an MCP tool
52 | @mcp.tool(name=tool_obj.name, description=tool_obj.description)
53 | async def tool_executor(params: Dict[str, Any], ctx: Context) -> str:
54 | # Create a tool call in the format expected by ToolExecutionManager
55 | tool_call = {
56 | "id": ctx.request_id,
57 | "function": {
58 | "name": tool_obj.name,
59 | "arguments": str(params)
60 | }
61 | }
62 |
63 | try:
64 | # Log the tool call in metrics
65 | metrics.log_tool_call(tool_obj.name)
66 |
67 | # Execute the tool and get the result
68 | result = tool_obj.execute(tool_call)
69 |
70 | # Report progress when complete
71 | await ctx.report_progress(1, 1)
72 |
73 | return result.result
74 | except Exception as e:
75 | # Log error in metrics
76 | metrics.log_error(f"tool_{tool_obj.name}", str(e))
77 | raise
78 |
79 |
80 | # Function to register all View resources
81 | def register_view_resources():
82 | """Register file viewing as resources."""
83 |
84 | @mcp.resource("file://{file_path}")
85 | def get_file_content(file_path: str) -> str:
86 | """Get the content of a file"""
87 | try:
88 | # Log resource request
89 | metrics.log_resource_request(f"file://{file_path}")
90 |
91 | # Get the View tool
92 | view_tool = tool_registry.get_tool("View")
93 | if not view_tool:
94 | metrics.log_error("resource_error", "View tool not found")
95 | return "Error: View tool not found"
96 |
97 | # Execute the tool to get file content
98 | tool_call = {
99 | "id": "resource_call",
100 | "function": {
101 | "name": "View",
102 | "arguments": json.dumps({"file_path": file_path})
103 | }
104 | }
105 |
106 | result = view_tool.execute(tool_call)
107 | return result.result
108 | except Exception as e:
109 | metrics.log_error("resource_error", f"Error viewing file: {str(e)}")
110 | return f"Error: {str(e)}"
111 |
112 |
113 | # Register file system resources
114 | @mcp.resource("filesystem://{path}")
115 | def list_directory(path: str) -> str:
116 | """List files and directories at the given path."""
117 | try:
118 | # Log resource request
119 | metrics.log_resource_request(f"filesystem://{path}")
120 |
121 | import os
122 |
123 | if not os.path.isabs(path):
124 | metrics.log_error("resource_error", f"Path must be absolute: {path}")
125 | return f"Error: Path must be absolute: {path}"
126 |
127 | if not os.path.exists(path):
128 | metrics.log_error("resource_error", f"Path does not exist: {path}")
129 | return f"Error: Path does not exist: {path}"
130 |
131 | if not os.path.isdir(path):
132 | metrics.log_error("resource_error", f"Path is not a directory: {path}")
133 | return f"Error: Path is not a directory: {path}"
134 |
135 | items = os.listdir(path)
136 | result = []
137 |
138 | for item in items:
139 | item_path = os.path.join(path, item)
140 | if os.path.isdir(item_path):
141 | result.append(f"{item}/")
142 | else:
143 | result.append(item)
144 |
145 | return "\n".join(result)
146 | except Exception as e:
147 | metrics.log_error("resource_error", f"Error listing directory: {str(e)}")
148 | return f"Error: {str(e)}"
149 |
150 |
151 | # Add system information resource
152 | @mcp.resource("system://info")
153 | def get_system_info() -> str:
154 | """Get system information"""
155 | try:
156 | # Log resource request
157 | metrics.log_resource_request("system://info")
158 |
159 | info = {
160 | "os": platform.system(),
161 | "os_version": platform.version(),
162 | "python_version": sys.version,
163 | "hostname": platform.node(),
164 | "platform": platform.platform(),
165 | "architecture": platform.architecture(),
166 | "processor": platform.processor(),
167 | "uptime": metrics.get_uptime()
168 | }
169 |
170 | return "\n".join([f"{k}: {v}" for k, v in info.items()])
171 | except Exception as e:
172 | metrics.log_error("resource_error", f"Error getting system info: {str(e)}")
173 | return f"Error: {str(e)}"
174 |
175 |
176 | # Add configuration resource
177 | @mcp.resource("config://json")
178 | def get_config_json() -> str:
179 | """Get Claude Desktop MCP configuration in JSON format"""
180 | try:
181 | # Log resource request
182 | metrics.log_resource_request("config://json")
183 |
184 | config_path = pathlib.Path(__file__).parent / "examples" / "claude_mcp_config.json"
185 |
186 | try:
187 | with open(config_path, 'r', encoding='utf-8') as f:
188 | config = json.load(f)
189 |
190 | # Update working directory to actual path
191 | current_dir = str(pathlib.Path(__file__).parent.parent.absolute())
192 | config["workingDirectory"] = current_dir
193 |
194 | return json.dumps(config, indent=2)
195 | except Exception as e:
196 | logger.error(f"Error reading config file: {e}")
197 | metrics.log_error("resource_error", f"Error reading config file: {str(e)}")
198 |
199 | return json.dumps({
200 | "name": "Claude Code Tools",
201 | "type": "local_process",
202 | "command": "python",
203 | "args": ["claude.py", "serve"],
204 | "workingDirectory": str(pathlib.Path(__file__).parent.parent.absolute()),
205 | "environment": {},
206 | "description": "A Model Context Protocol server for Claude Code tools"
207 | }, indent=2)
208 | except Exception as e:
209 | metrics.log_error("resource_error", f"Error in config resource: {str(e)}")
210 | return f"Error: {str(e)}"
211 |
212 |
213 | # Add metrics resource
214 | @mcp.resource("metrics://json")
215 | def get_metrics_json() -> str:
216 | """Get server metrics in JSON format"""
217 | try:
218 | # Log resource request
219 | metrics.log_resource_request("metrics://json")
220 |
221 | # Get all metrics
222 | all_metrics = metrics.get_all_metrics()
223 |
224 | return json.dumps(all_metrics, indent=2)
225 | except Exception as e:
226 | metrics.log_error("resource_error", f"Error getting metrics: {str(e)}")
227 | return f"Error: {str(e)}"
228 |
229 |
230 | # Add metrics tool
231 | @mcp.tool(name="GetServerMetrics", description="Get server metrics and statistics")
232 | async def get_server_metrics(metric_type: str = "all") -> str:
233 | """Get server metrics and statistics.
234 |
235 | Args:
236 | metric_type: Type of metrics to return (all, uptime, tools, resources, errors)
237 |
238 | Returns:
239 | The requested metrics information
240 | """
241 | try:
242 | # Log tool call
243 | metrics.log_tool_call("GetServerMetrics")
244 |
245 | if metric_type.lower() == "all":
246 | all_metrics = metrics.get_all_metrics()
247 | return json.dumps(all_metrics, indent=2)
248 |
249 | elif metric_type.lower() == "uptime":
250 | return f"Server uptime: {metrics.get_uptime()}"
251 |
252 | elif metric_type.lower() == "tools":
253 | tool_stats = metrics.get_tool_usage_stats()
254 | result = "Tool Usage Statistics:\n\n"
255 | for tool, count in sorted(tool_stats.items(), key=lambda x: x[1], reverse=True):
256 | result += f"- {tool}: {count} calls\n"
257 | return result
258 |
259 | elif metric_type.lower() == "resources":
260 | resource_stats = metrics.get_resource_usage_stats()
261 | result = "Resource Usage Statistics:\n\n"
262 | for resource, count in sorted(resource_stats.items(), key=lambda x: x[1], reverse=True):
263 | result += f"- {resource}: {count} requests\n"
264 | return result
265 |
266 | elif metric_type.lower() == "errors":
267 | error_stats = metrics.get_error_stats()
268 | if not error_stats:
269 | return "No errors recorded."
270 |
271 | result = "Error Statistics:\n\n"
272 | for error_type, count in sorted(error_stats.items(), key=lambda x: x[1], reverse=True):
273 | result += f"- {error_type}: {count} occurrences\n"
274 | return result
275 |
276 | elif metric_type.lower() == "activity":
277 | recent = metrics.get_recent_activity(15)
278 | result = "Recent Activity:\n\n"
279 | for event in recent:
280 | time_str = event.get("formatted_time", "unknown")
281 | if event["type"] == "tool":
282 | result += f"[{time_str}] Tool call: {event['name']}\n"
283 | elif event["type"] == "resource":
284 | result += f"[{time_str}] Resource request: {event['uri']}\n"
285 | elif event["type"] == "connection":
286 | action = "connected" if event["action"] == "connect" else "disconnected"
287 | result += f"[{time_str}] Client {event['client_id']} {action}\n"
288 | elif event["type"] == "error":
289 | result += f"[{time_str}] Error ({event['error_type']}): {event['message']}\n"
290 | return result
291 |
292 | else:
293 | return f"Unknown metric type: {metric_type}. Available types: all, uptime, tools, resources, errors, activity"
294 |
295 | except Exception as e:
296 | metrics.log_error("tool_error", f"Error in GetServerMetrics: {str(e)}")
297 | return f"Error retrieving metrics: {str(e)}"
298 |
299 |
300 | # Add connection tracking
301 | @mcp.on_connect
302 | async def handle_connect(ctx: Context):
303 | """Track client connections."""
304 | client_id = str(uuid.uuid4())
305 | ctx.client_data["id"] = client_id
306 | metrics.log_connection(client_id, connected=True)
307 | logger.info(f"Client connected: {client_id}")
308 |
309 |
310 | @mcp.on_disconnect
311 | async def handle_disconnect(ctx: Context):
312 | """Track client disconnections."""
313 | client_id = ctx.client_data.get("id", "unknown")
314 | metrics.log_connection(client_id, connected=False)
315 | logger.info(f"Client disconnected: {client_id}")
316 |
317 |
318 | @mcp.tool(name="GetConfiguration", description="Get Claude Desktop configuration for this MCP server")
319 | async def get_configuration(format: str = "json") -> str:
320 | """Get configuration for connecting Claude Desktop to this MCP server.
321 |
322 | Args:
323 | format: The format to return (json or text)
324 |
325 | Returns:
326 | The configuration in the requested format
327 | """
328 | if format.lower() == "json":
329 | return get_config_json()
330 | else:
331 | # Return text instructions
332 | config = json.loads(get_config_json())
333 |
334 | return f"""
335 | To connect Claude Desktop to this MCP server:
336 |
337 | 1. Open Claude Desktop and go to Settings
338 | 2. Navigate to "Model Context Protocol" section
339 | 3. Click "Add New Server"
340 | 4. Use the following settings:
341 | - Name: {config['name']}
342 | - Type: Local Process
343 | - Command: {config['command']}
344 | - Arguments: {" ".join(config['args'])}
345 | - Working Directory: {config['workingDirectory']}
346 | 5. Click Save and connect to the server
347 |
348 | You can also visit http://localhost:8000 for more detailed instructions and to download the configuration file.
349 | """
350 |
351 |
352 | # Initialize MCP server
353 | def initialize_server():
354 | """Initialize the MCP server with all tools and resources."""
355 | # Register all tools
356 | setup_tools()
357 |
358 | # Register resources
359 | register_view_resources()
360 |
361 | # Add metrics tool for server monitoring
362 | @mcp.tool(name="ResetServerMetrics", description="Reset server metrics tracking")
363 | async def reset_metrics(confirm: bool = False) -> str:
364 | """Reset server metrics tracking.
365 |
366 | Args:
367 | confirm: Confirmation flag to prevent accidental resets
368 |
369 | Returns:
370 | Confirmation message
371 | """
372 | if not confirm:
373 | return "Please set confirm=true to reset server metrics."
374 |
375 | # Log the call
376 | metrics.log_tool_call("ResetServerMetrics")
377 |
378 | # Reset metrics
379 | metrics.reset_stats()
380 |
381 | return "Server metrics have been reset successfully."
382 |
383 | logger.info("MCP server initialized with all tools and resources")
384 |
385 | return mcp
386 |
387 |
388 | # Main function to run the server
389 | def main():
390 | """Run the MCP server"""
391 | # Initialize the server
392 | server = initialize_server()
393 |
394 | # Run the server
395 | server.run()
396 |
397 |
398 | if __name__ == "__main__":
399 | main()
```
--------------------------------------------------------------------------------
/claude_code/lib/tools/file_tools.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # claude_code/lib/tools/file_tools.py
3 | """File operation tools."""
4 |
5 | import os
6 | import logging
7 | from typing import Dict, List, Optional, Any
8 |
9 | from .base import tool, ToolRegistry
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | @tool(
15 | name="View",
16 | description="Reads a file from the local filesystem. The file_path parameter must be an absolute path, not a relative path.",
17 | parameters={
18 | "type": "object",
19 | "properties": {
20 | "file_path": {
21 | "type": "string",
22 | "description": "The absolute path to the file to read"
23 | },
24 | "limit": {
25 | "type": "number",
26 | "description": "The number of lines to read. Only provide if the file is too large to read at once."
27 | },
28 | "offset": {
29 | "type": "number",
30 | "description": "The line number to start reading from. Only provide if the file is too large to read at once"
31 | }
32 | },
33 | "required": ["file_path"]
34 | },
35 | category="file"
36 | )
37 | def view_file(file_path: str, limit: Optional[int] = None, offset: Optional[int] = 0) -> str:
38 | """Read contents of a file.
39 |
40 | Args:
41 | file_path: Absolute path to the file
42 | limit: Maximum number of lines to read
43 | offset: Line number to start reading from
44 |
45 | Returns:
46 | File contents as a string
47 |
48 | Raises:
49 | FileNotFoundError: If the file doesn't exist
50 | PermissionError: If the file can't be read
51 | """
52 | logger.info(f"Reading file: {file_path} (offset={offset}, limit={limit})")
53 |
54 | if not os.path.isabs(file_path):
55 | return f"Error: File path must be absolute: {file_path}"
56 |
57 | if not os.path.exists(file_path):
58 | return f"Error: File not found: {file_path}"
59 |
60 | try:
61 | with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
62 | if limit is not None and offset is not None:
63 | # Skip to offset
64 | for _ in range(offset):
65 | next(f, None)
66 |
67 | # Read limited lines
68 | lines = []
69 | for _ in range(limit):
70 | line = next(f, None)
71 | if line is None:
72 | break
73 | lines.append(line)
74 | content = ''.join(lines)
75 | else:
76 | content = f.read()
77 |
78 | return content
79 | except Exception as e:
80 | logger.exception(f"Error reading file: {file_path}")
81 | return f"Error reading file: {str(e)}"
82 |
83 |
84 | @tool(
85 | name="Edit",
86 | description="This is a tool for editing files. For moving or renaming files, you should generally use the Bash tool with the 'mv' command instead.",
87 | parameters={
88 | "type": "object",
89 | "properties": {
90 | "file_path": {
91 | "type": "string",
92 | "description": "The absolute path to the file to modify"
93 | },
94 | "old_string": {
95 | "type": "string",
96 | "description": "The text to replace"
97 | },
98 | "new_string": {
99 | "type": "string",
100 | "description": "The text to replace it with"
101 | }
102 | },
103 | "required": ["file_path", "old_string", "new_string"]
104 | },
105 | needs_permission=True,
106 | category="file"
107 | )
108 | def edit_file(file_path: str, old_string: str, new_string: str) -> str:
109 | """Edit a file by replacing text.
110 |
111 | Args:
112 | file_path: Absolute path to the file
113 | old_string: Text to replace
114 | new_string: Replacement text
115 |
116 | Returns:
117 | Success or error message
118 |
119 | Raises:
120 | FileNotFoundError: If the file doesn't exist
121 | PermissionError: If the file can't be modified
122 | """
123 | logger.info(f"Editing file: {file_path}")
124 |
125 | if not os.path.isabs(file_path):
126 | return f"Error: File path must be absolute: {file_path}"
127 |
128 | try:
129 | # Create directory if creating new file
130 | if not os.path.exists(os.path.dirname(file_path)) and old_string == "":
131 | os.makedirs(os.path.dirname(file_path), exist_ok=True)
132 |
133 | if old_string == "" and not os.path.exists(file_path):
134 | # Creating new file
135 | with open(file_path, 'w', encoding='utf-8') as f:
136 | f.write(new_string)
137 | return f"Created new file: {file_path}"
138 |
139 | # Reading existing file
140 | if not os.path.exists(file_path):
141 | return f"Error: File not found: {file_path}"
142 |
143 | with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
144 | content = f.read()
145 |
146 | # Replace string
147 | if old_string not in content:
148 | return f"Error: Could not find the specified text in {file_path}"
149 |
150 | # Count occurrences to ensure uniqueness
151 | occurrences = content.count(old_string)
152 | if occurrences > 1:
153 | return f"Error: Found {occurrences} occurrences of the specified text in {file_path}. Please provide more context to uniquely identify the text to replace."
154 |
155 | new_content = content.replace(old_string, new_string)
156 |
157 | # Write back to file
158 | with open(file_path, 'w', encoding='utf-8') as f:
159 | f.write(new_content)
160 |
161 | return f"Successfully edited {file_path}"
162 |
163 | except Exception as e:
164 | logger.exception(f"Error editing file: {file_path}")
165 | return f"Error editing file: {str(e)}"
166 |
167 |
168 | @tool(
169 | name="Replace",
170 | description="Write a file to the local filesystem. Overwrites the existing file if there is one.",
171 | parameters={
172 | "type": "object",
173 | "properties": {
174 | "file_path": {
175 | "type": "string",
176 | "description": "The absolute path to the file to write"
177 | },
178 | "content": {
179 | "type": "string",
180 | "description": "The content to write to the file"
181 | }
182 | },
183 | "required": ["file_path", "content"]
184 | },
185 | needs_permission=True,
186 | category="file"
187 | )
188 | def replace_file(file_path: str, content: str) -> str:
189 | """Replace file contents or create a new file.
190 |
191 | Args:
192 | file_path: Absolute path to the file
193 | content: New content for the file
194 |
195 | Returns:
196 | Success or error message
197 |
198 | Raises:
199 | PermissionError: If the file can't be written
200 | """
201 | logger.info(f"Replacing file: {file_path}")
202 |
203 | if not os.path.isabs(file_path):
204 | return f"Error: File path must be absolute: {file_path}"
205 |
206 | try:
207 | # Create directory if it doesn't exist
208 | directory = os.path.dirname(file_path)
209 | if directory and not os.path.exists(directory):
210 | os.makedirs(directory, exist_ok=True)
211 |
212 | # Write content to file
213 | with open(file_path, 'w', encoding='utf-8') as f:
214 | f.write(content)
215 |
216 | return f"Successfully wrote to {file_path}"
217 |
218 | except Exception as e:
219 | logger.exception(f"Error writing file: {file_path}")
220 | return f"Error writing file: {str(e)}"
221 |
222 |
223 | @tool(
224 | name="MakeDirectory",
225 | description="Create a new directory on the local filesystem.",
226 | parameters={
227 | "type": "object",
228 | "properties": {
229 | "directory_path": {
230 | "type": "string",
231 | "description": "The absolute path to the directory to create"
232 | },
233 | "parents": {
234 | "type": "boolean",
235 | "description": "Whether to create parent directories if they don't exist",
236 | "default": True
237 | },
238 | "mode": {
239 | "type": "integer",
240 | "description": "The file mode (permissions) to set for the directory (octal)",
241 | "default": 0o755
242 | }
243 | },
244 | "required": ["directory_path"]
245 | },
246 | needs_permission=True,
247 | category="file"
248 | )
249 | def make_directory(directory_path: str, parents: bool = True, mode: int = 0o755) -> str:
250 | """Create a new directory.
251 |
252 | Args:
253 | directory_path: Absolute path to the directory to create
254 | parents: Whether to create parent directories
255 | mode: File mode (permissions) to set
256 |
257 | Returns:
258 | Success or error message
259 |
260 | Raises:
261 | PermissionError: If the directory can't be created
262 | """
263 | logger.info(f"Creating directory: {directory_path}")
264 |
265 | if not os.path.isabs(directory_path):
266 | return f"Error: Directory path must be absolute: {directory_path}"
267 |
268 | try:
269 | if os.path.exists(directory_path):
270 | if os.path.isdir(directory_path):
271 | return f"Directory already exists: {directory_path}"
272 | else:
273 | return f"Error: Path exists but is not a directory: {directory_path}"
274 |
275 | # Create directory
276 | os.makedirs(directory_path, exist_ok=parents, mode=mode)
277 |
278 | return f"Successfully created directory: {directory_path}"
279 |
280 | except Exception as e:
281 | logger.exception(f"Error creating directory: {directory_path}")
282 | return f"Error creating directory: {str(e)}"
283 |
284 |
285 | @tool(
286 | name="ListDirectory",
287 | description="List files and directories in a given path with detailed information.",
288 | parameters={
289 | "type": "object",
290 | "properties": {
291 | "directory_path": {
292 | "type": "string",
293 | "description": "The absolute path to the directory to list"
294 | },
295 | "pattern": {
296 | "type": "string",
297 | "description": "Optional glob pattern to filter files (e.g., '*.py')"
298 | },
299 | "recursive": {
300 | "type": "boolean",
301 | "description": "Whether to list files recursively",
302 | "default": False
303 | },
304 | "show_hidden": {
305 | "type": "boolean",
306 | "description": "Whether to show hidden files (starting with .)",
307 | "default": False
308 | },
309 | "details": {
310 | "type": "boolean",
311 | "description": "Whether to show detailed information (size, permissions, etc.)",
312 | "default": False
313 | }
314 | },
315 | "required": ["directory_path"]
316 | },
317 | category="file"
318 | )
319 | def list_directory(
320 | directory_path: str,
321 | pattern: Optional[str] = None,
322 | recursive: bool = False,
323 | show_hidden: bool = False,
324 | details: bool = False
325 | ) -> str:
326 | """List files and directories with detailed information.
327 |
328 | Args:
329 | directory_path: Absolute path to the directory
330 | pattern: Glob pattern to filter files
331 | recursive: Whether to list files recursively
332 | show_hidden: Whether to show hidden files
333 | details: Whether to show detailed information
334 |
335 | Returns:
336 | Directory listing as formatted text
337 | """
338 | logger.info(f"Listing directory: {directory_path}")
339 |
340 | if not os.path.isabs(directory_path):
341 | return f"Error: Directory path must be absolute: {directory_path}"
342 |
343 | if not os.path.exists(directory_path):
344 | return f"Error: Directory not found: {directory_path}"
345 |
346 | if not os.path.isdir(directory_path):
347 | return f"Error: Path is not a directory: {directory_path}"
348 |
349 | try:
350 | import glob
351 | import stat
352 | from datetime import datetime
353 |
354 | # Build the pattern
355 | if pattern:
356 | if recursive:
357 | search_pattern = os.path.join(directory_path, "**", pattern)
358 | else:
359 | search_pattern = os.path.join(directory_path, pattern)
360 | else:
361 | if recursive:
362 | search_pattern = os.path.join(directory_path, "**")
363 | else:
364 | search_pattern = os.path.join(directory_path, "*")
365 |
366 | # Get all matching files
367 | if recursive:
368 | matches = glob.glob(search_pattern, recursive=True)
369 | else:
370 | matches = glob.glob(search_pattern)
371 |
372 | # Filter hidden files if needed
373 | if not show_hidden:
374 | matches = [m for m in matches if not os.path.basename(m).startswith('.')]
375 |
376 | # Sort by name
377 | matches.sort()
378 |
379 | # Format the output
380 | result = []
381 |
382 | if details:
383 | # Header
384 | result.append(f"{'Type':<6} {'Permissions':<11} {'Size':<10} {'Modified':<20} {'Name'}")
385 | result.append("-" * 80)
386 |
387 | for item_path in matches:
388 | try:
389 | # Get file stats
390 | item_stat = os.stat(item_path)
391 |
392 | # Determine type
393 | if os.path.isdir(item_path):
394 | item_type = "dir"
395 | elif os.path.islink(item_path):
396 | item_type = "link"
397 | else:
398 | item_type = "file"
399 |
400 | # Format permissions
401 | mode = item_stat.st_mode
402 | perms = ""
403 | for who in "USR", "GRP", "OTH":
404 | for what in "R", "W", "X":
405 | perm = getattr(stat, f"S_I{what}{who}")
406 | perms += what.lower() if mode & perm else "-"
407 |
408 | # Format size
409 | size = item_stat.st_size
410 | if size < 1024:
411 | size_str = f"{size}B"
412 | elif size < 1024 * 1024:
413 | size_str = f"{size/1024:.1f}KB"
414 | elif size < 1024 * 1024 * 1024:
415 | size_str = f"{size/(1024*1024):.1f}MB"
416 | else:
417 | size_str = f"{size/(1024*1024*1024):.1f}GB"
418 |
419 | # Format modification time
420 | mtime = datetime.fromtimestamp(item_stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
421 |
422 | # Format name (relative to the directory)
423 | name = os.path.relpath(item_path, directory_path)
424 |
425 | # Add to result
426 | result.append(f"{item_type:<6} {perms:<11} {size_str:<10} {mtime:<20} {name}")
427 |
428 | except Exception as e:
429 | result.append(f"Error getting info for {item_path}: {str(e)}")
430 | else:
431 | # Simple listing
432 | dirs = []
433 | files = []
434 |
435 | for item_path in matches:
436 | name = os.path.relpath(item_path, directory_path)
437 | if os.path.isdir(item_path):
438 | dirs.append(f"{name}/")
439 | else:
440 | files.append(name)
441 |
442 | if dirs:
443 | result.append("Directories:")
444 | for d in dirs:
445 | result.append(f" {d}")
446 |
447 | if files:
448 | if dirs:
449 | result.append("")
450 | result.append("Files:")
451 | for f in files:
452 | result.append(f" {f}")
453 |
454 | if not result:
455 | return f"No matching items found in {directory_path}"
456 |
457 | return "\n".join(result)
458 |
459 | except Exception as e:
460 | logger.exception(f"Error listing directory: {directory_path}")
461 | return f"Error listing directory: {str(e)}"
462 |
463 |
464 | def register_file_tools(registry: ToolRegistry) -> None:
465 | """Register all file tools with the registry.
466 |
467 | Args:
468 | registry: Tool registry to register with
469 | """
470 | from .base import create_tools_from_functions
471 |
472 | file_tools = [
473 | view_file,
474 | edit_file,
475 | replace_file,
476 | make_directory,
477 | list_directory
478 | ]
479 |
480 | create_tools_from_functions(registry, file_tools)
481 |
```
--------------------------------------------------------------------------------
/web-client.html:
--------------------------------------------------------------------------------
```html
1 | <!DOCTYPE html>
2 | <html lang="en">
3 | <head>
4 | <meta charset="UTF-8">
5 | <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 | <title>OpenAI Code Assistant Web Client</title>
7 | <style>
8 | body {
9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
10 | line-height: 1.6;
11 | color: #333;
12 | max-width: 1200px;
13 | margin: 0 auto;
14 | padding: 20px;
15 | }
16 |
17 | h1 {
18 | color: #2c3e50;
19 | border-bottom: 2px solid #eee;
20 | padding-bottom: 10px;
21 | }
22 |
23 | .chat-container {
24 | display: flex;
25 | height: 70vh;
26 | }
27 |
28 | .sidebar {
29 | width: 250px;
30 | background-color: #f8f9fa;
31 | padding: 15px;
32 | border-radius: 5px;
33 | margin-right: 20px;
34 | }
35 |
36 | .main-chat {
37 | flex: 1;
38 | display: flex;
39 | flex-direction: column;
40 | border: 1px solid #ddd;
41 | border-radius: 5px;
42 | }
43 |
44 | .chat-messages {
45 | flex: 1;
46 | overflow-y: auto;
47 | padding: 15px;
48 | background-color: #fff;
49 | }
50 |
51 | .chat-input {
52 | display: flex;
53 | padding: 10px;
54 | background-color: #f8f9fa;
55 | border-top: 1px solid #ddd;
56 | }
57 |
58 | .chat-input textarea {
59 | flex: 1;
60 | padding: 10px;
61 | border: 1px solid #ddd;
62 | border-radius: 4px;
63 | resize: none;
64 | font-family: inherit;
65 | }
66 |
67 | .chat-input button {
68 | margin-left: 10px;
69 | padding: 10px 15px;
70 | background-color: #4CAF50;
71 | color: white;
72 | border: none;
73 | border-radius: 4px;
74 | cursor: pointer;
75 | }
76 |
77 | .chat-input button:hover {
78 | background-color: #45a049;
79 | }
80 |
81 | .message {
82 | margin-bottom: 15px;
83 | padding: 10px;
84 | border-radius: 5px;
85 | }
86 |
87 | .user-message {
88 | background-color: #e3f2fd;
89 | align-self: flex-end;
90 | margin-left: 20%;
91 | }
92 |
93 | .assistant-message {
94 | background-color: #f1f1f1;
95 | align-self: flex-start;
96 | margin-right: 20%;
97 | }
98 |
99 | .tool-message {
100 | background-color: #fff8e1;
101 | border-left: 3px solid #ffc107;
102 | padding-left: 10px;
103 | font-family: monospace;
104 | white-space: pre-wrap;
105 | }
106 |
107 | .status-message {
108 | color: #666;
109 | font-style: italic;
110 | text-align: center;
111 | margin: 10px 0;
112 | }
113 |
114 | .warning-message {
115 | color: #ff9800;
116 | border-left: 3px solid #ff9800;
117 | padding-left: 10px;
118 | }
119 |
120 | .error-message {
121 | color: #f44336;
122 | border-left: 3px solid #f44336;
123 | padding-left: 10px;
124 | }
125 |
126 | .conversation-list {
127 | list-style: none;
128 | padding: 0;
129 | }
130 |
131 | .conversation-list li {
132 | padding: 8px 10px;
133 | margin-bottom: 5px;
134 | background-color: #e9ecef;
135 | border-radius: 4px;
136 | cursor: pointer;
137 | }
138 |
139 | .conversation-list li:hover {
140 | background-color: #dee2e6;
141 | }
142 |
143 | .conversation-list li.active {
144 | background-color: #4CAF50;
145 | color: white;
146 | }
147 |
148 | .settings-panel {
149 | margin-top: 20px;
150 | }
151 |
152 | .settings-panel h3 {
153 | margin-bottom: 10px;
154 | }
155 |
156 | .settings-panel label {
157 | display: block;
158 | margin-bottom: 5px;
159 | }
160 |
161 | .settings-panel select, .settings-panel input {
162 | width: 100%;
163 | padding: 8px;
164 | margin-bottom: 10px;
165 | border: 1px solid #ddd;
166 | border-radius: 4px;
167 | }
168 |
169 | .new-conversation-btn {
170 | width: 100%;
171 | padding: 10px;
172 | background-color: #4CAF50;
173 | color: white;
174 | border: none;
175 | border-radius: 4px;
176 | cursor: pointer;
177 | margin-bottom: 15px;
178 | }
179 |
180 | .new-conversation-btn:hover {
181 | background-color: #45a049;
182 | }
183 |
184 | pre {
185 | background-color: #f5f5f5;
186 | padding: 10px;
187 | border-radius: 4px;
188 | overflow-x: auto;
189 | }
190 |
191 | code {
192 | font-family: 'Courier New', Courier, monospace;
193 | }
194 | </style>
195 | </head>
196 | <body>
197 | <h1>OpenAI Code Assistant</h1>
198 |
199 | <div class="chat-container">
200 | <div class="sidebar">
201 | <button id="newConversationBtn" class="new-conversation-btn">New Conversation</button>
202 |
203 | <h3>Conversations</h3>
204 | <ul id="conversationList" class="conversation-list">
205 | <!-- Conversations will be added here -->
206 | </ul>
207 |
208 | <div class="settings-panel">
209 | <h3>Settings</h3>
210 | <label for="modelSelect">Model:</label>
211 | <select id="modelSelect">
212 | <option value="gpt-4o">GPT-4o</option>
213 | <option value="gpt-4-turbo">GPT-4 Turbo</option>
214 | <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
215 | </select>
216 |
217 | <label for="temperatureInput">Temperature:</label>
218 | <input type="number" id="temperatureInput" min="0" max="2" step="0.1" value="0">
219 | </div>
220 | </div>
221 |
222 | <div class="main-chat">
223 | <div id="chatMessages" class="chat-messages">
224 | <div class="status-message">Start a new conversation or select an existing one.</div>
225 | </div>
226 |
227 | <div class="chat-input">
228 | <textarea id="userInput" placeholder="Type your message here..." rows="3"></textarea>
229 | <button id="sendButton">Send</button>
230 | </div>
231 | </div>
232 | </div>
233 |
234 | <script>
235 | // API endpoint (change this to match your server)
236 | const API_BASE_URL = 'http://localhost:8000';
237 |
238 | // State
239 | let currentConversationId = null;
240 | let conversations = [];
241 |
242 | // DOM Elements
243 | const chatMessages = document.getElementById('chatMessages');
244 | const userInput = document.getElementById('userInput');
245 | const sendButton = document.getElementById('sendButton');
246 | const newConversationBtn = document.getElementById('newConversationBtn');
247 | const conversationList = document.getElementById('conversationList');
248 | const modelSelect = document.getElementById('modelSelect');
249 | const temperatureInput = document.getElementById('temperatureInput');
250 |
251 | // Event Listeners
252 | sendButton.addEventListener('click', sendMessage);
253 | newConversationBtn.addEventListener('click', createNewConversation);
254 | userInput.addEventListener('keydown', (e) => {
255 | if (e.key === 'Enter' && !e.shiftKey) {
256 | e.preventDefault();
257 | sendMessage();
258 | }
259 | });
260 |
261 | // Initialize
262 | function init() {
263 | // Load conversations from local storage
264 | const savedConversations = localStorage.getItem('conversations');
265 | if (savedConversations) {
266 | conversations = JSON.parse(savedConversations);
267 | updateConversationList();
268 | }
269 | }
270 |
271 | // Create a new conversation
272 | async function createNewConversation() {
273 | try {
274 | const model = modelSelect.value;
275 | const temperature = parseFloat(temperatureInput.value);
276 |
277 | const response = await fetch(`${API_BASE_URL}/conversation`, {
278 | method: 'POST',
279 | headers: {
280 | 'Content-Type': 'application/json'
281 | },
282 | body: JSON.stringify({ model, temperature })
283 | });
284 |
285 | const data = await response.json();
286 |
287 | if (data.conversation_id) {
288 | const newConversation = {
289 | id: data.conversation_id,
290 | model: data.model,
291 | created: new Date().toISOString(),
292 | messages: []
293 | };
294 |
295 | conversations.push(newConversation);
296 | saveConversations();
297 | updateConversationList();
298 |
299 | // Switch to the new conversation
300 | switchConversation(data.conversation_id);
301 | }
302 | } catch (error) {
303 | console.error('Error creating conversation:', error);
304 | addErrorMessage('Failed to create a new conversation. Please try again.');
305 | }
306 | }
307 |
308 | // Switch to a different conversation
309 | function switchConversation(conversationId) {
310 | currentConversationId = conversationId;
311 |
312 | // Update UI
313 | const conversationItems = conversationList.querySelectorAll('li');
314 | conversationItems.forEach(item => {
315 | if (item.dataset.id === conversationId) {
316 | item.classList.add('active');
317 | } else {
318 | item.classList.remove('active');
319 | }
320 | });
321 |
322 | // Clear and load messages
323 | chatMessages.innerHTML = '';
324 |
325 | const conversation = conversations.find(c => c.id === conversationId);
326 | if (conversation && conversation.messages) {
327 | conversation.messages.forEach(msg => {
328 | if (msg.role === 'user') {
329 | addUserMessage(msg.content);
330 | } else if (msg.role === 'assistant') {
331 | addAssistantMessage(msg.content);
332 | } else if (msg.role === 'tool') {
333 | addToolMessage(msg.name, msg.content);
334 | }
335 | });
336 | }
337 |
338 | // Focus input
339 | userInput.focus();
340 | }
341 |
342 | // Send a message
343 | async function sendMessage() {
344 | const message = userInput.value.trim();
345 | if (!message) return;
346 |
347 | if (!currentConversationId) {
348 | await createNewConversation();
349 | }
350 |
351 | // Add user message to UI
352 | addUserMessage(message);
353 |
354 | // Save message to conversation
355 | const conversation = conversations.find(c => c.id === currentConversationId);
356 | if (conversation) {
357 | conversation.messages.push({
358 | role: 'user',
359 | content: message
360 | });
361 | saveConversations();
362 | }
363 |
364 | // Clear input
365 | userInput.value = '';
366 |
367 | // Send to API and stream response
368 | try {
369 | const response = await fetch(`${API_BASE_URL}/conversation/${currentConversationId}/message/stream`, {
370 | method: 'POST',
371 | headers: {
372 | 'Content-Type': 'application/json'
373 | },
374 | body: JSON.stringify({ message })
375 | });
376 |
377 | const reader = response.body.getReader();
378 | const decoder = new TextDecoder();
379 |
380 | let assistantResponse = '';
381 | let responseElement = null;
382 |
383 | while (true) {
384 | const { done, value } = await reader.read();
385 | if (done) break;
386 |
387 | const text = decoder.decode(value);
388 | const lines = text.split('\n').filter(line => line.trim());
389 |
390 | for (const line of lines) {
391 | try {
392 | const data = JSON.parse(line);
393 |
394 | if (data.type === 'content') {
395 | if (!responseElement) {
396 | responseElement = addAssistantMessage('');
397 | }
398 |
399 | assistantResponse += data.content;
400 | responseElement.textContent = assistantResponse;
401 | }
402 | else if (data.type === 'status') {
403 | if (data.status === 'running_tools') {
404 | addStatusMessage('Running tools...');
405 | } else if (data.status.startsWith('running_tools_iteration_')) {
406 | const iteration = data.status.split('_').pop();
407 | addStatusMessage(`Running tools (iteration ${iteration})...`);
408 | }
409 | }
410 | else if (data.type === 'tool_result') {
411 | addToolMessage(data.tool, data.result);
412 | }
413 | else if (data.type === 'warning') {
414 | addWarningMessage(data.warning);
415 | }
416 | else if (data.type === 'error') {
417 | addErrorMessage(data.error);
418 | }
419 | } catch (e) {
420 | console.error('Error parsing stream data:', e, line);
421 | }
422 | }
423 | }
424 |
425 | // Save assistant response to conversation
426 | if (conversation && assistantResponse) {
427 | conversation.messages.push({
428 | role: 'assistant',
429 | content: assistantResponse
430 | });
431 | saveConversations();
432 | }
433 |
434 | } catch (error) {
435 | console.error('Error sending message:', error);
436 | addErrorMessage('Failed to send message. Please try again.');
437 | }
438 | }
439 |
440 | // Add a user message to the chat
441 | function addUserMessage(message) {
442 | const messageElement = document.createElement('div');
443 | messageElement.className = 'message user-message';
444 | messageElement.textContent = message;
445 | chatMessages.appendChild(messageElement);
446 | scrollToBottom();
447 | return messageElement;
448 | }
449 |
450 | // Add an assistant message to the chat
451 | function addAssistantMessage(message) {
452 | const messageElement = document.createElement('div');
453 | messageElement.className = 'message assistant-message';
454 | messageElement.textContent = message;
455 | chatMessages.appendChild(messageElement);
456 | scrollToBottom();
457 | return messageElement;
458 | }
459 |
460 | // Add a tool message to the chat
461 | function addToolMessage(toolName, result) {
462 | const messageElement = document.createElement('div');
463 | messageElement.className = 'message tool-message';
464 | messageElement.innerHTML = `<strong>${toolName}:</strong>\n${result}`;
465 | chatMessages.appendChild(messageElement);
466 | scrollToBottom();
467 | return messageElement;
468 | }
469 |
470 | // Add a status message to the chat
471 | function addStatusMessage(message) {
472 | const messageElement = document.createElement('div');
473 | messageElement.className = 'status-message';
474 | messageElement.textContent = message;
475 | chatMessages.appendChild(messageElement);
476 | scrollToBottom();
477 | return messageElement;
478 | }
479 |
480 | // Add a warning message to the chat
481 | function addWarningMessage(message) {
482 | const messageElement = document.createElement('div');
483 | messageElement.className = 'warning-message';
484 | messageElement.textContent = message;
485 | chatMessages.appendChild(messageElement);
486 | scrollToBottom();
487 | return messageElement;
488 | }
489 |
490 | // Add an error message to the chat
491 | function addErrorMessage(message) {
492 | const messageElement = document.createElement('div');
493 | messageElement.className = 'error-message';
494 | messageElement.textContent = message;
495 | chatMessages.appendChild(messageElement);
496 | scrollToBottom();
497 | return messageElement;
498 | }
499 |
500 | // Update the conversation list in the sidebar
501 | function updateConversationList() {
502 | conversationList.innerHTML = '';
503 |
504 | conversations.forEach(conversation => {
505 | const listItem = document.createElement('li');
506 | listItem.textContent = new Date(conversation.created).toLocaleString();
507 | listItem.dataset.id = conversation.id;
508 |
509 | if (conversation.id === currentConversationId) {
510 | listItem.classList.add('active');
511 | }
512 |
513 | listItem.addEventListener('click', () => {
514 | switchConversation(conversation.id);
515 | });
516 |
517 | conversationList.appendChild(listItem);
518 | });
519 | }
520 |
521 | // Save conversations to local storage
522 | function saveConversations() {
523 | localStorage.setItem('conversations', JSON.stringify(conversations));
524 | }
525 |
526 | // Scroll chat to bottom
527 | function scrollToBottom() {
528 | chatMessages.scrollTop = chatMessages.scrollHeight;
529 | }
530 |
531 | // Initialize the app
532 | init();
533 | </script>
534 | </body>
535 | </html>
536 |
```
--------------------------------------------------------------------------------
/claude_code/commands/multi_agent_client.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # claude_code/commands/multi_agent_client.py
3 | """Multi-agent MCP client implementation with synchronization capabilities."""
4 |
5 | import asyncio
6 | import sys
7 | import os
8 | import json
9 | import logging
10 | import uuid
11 | import argparse
12 | import time
13 | from typing import Optional, Dict, Any, List, Set, Tuple
14 | from contextlib import AsyncExitStack
15 | from dataclasses import dataclass, field, asdict
16 |
17 | from rich.console import Console
18 | from rich.prompt import Prompt
19 | from rich.panel import Panel
20 | from rich.markdown import Markdown
21 | from rich.table import Table
22 | from rich.live import Live
23 | from rich import print as rprint
24 |
25 | from mcp import ClientSession, StdioServerParameters
26 | from mcp.client.stdio import stdio_client
27 |
28 | from anthropic import Anthropic
29 | from dotenv import load_dotenv
30 |
31 | # Setup logging
32 | logging.basicConfig(level=logging.INFO)
33 | logger = logging.getLogger(__name__)
34 |
35 | # Load environment variables
36 | load_dotenv()
37 |
38 | # Console for rich output
39 | console = Console()
40 |
41 | @dataclass
42 | class Agent:
43 | """Agent representation for multi-agent scenarios."""
44 | id: str
45 | name: str
46 | role: str
47 | model: str
48 | system_prompt: str
49 | conversation: List[Dict[str, Any]] = field(default_factory=list)
50 | connected_agents: Set[str] = field(default_factory=set)
51 | message_queue: asyncio.Queue = field(default_factory=asyncio.Queue)
52 |
53 | def __post_init__(self):
54 | """Initialize the conversation with system prompt."""
55 | self.conversation = [{
56 | "role": "system",
57 | "content": self.system_prompt
58 | }]
59 |
60 | @dataclass
61 | class Message:
62 | """Message for agent communication."""
63 | id: str = field(default_factory=lambda: str(uuid.uuid4()))
64 | sender_id: str = ""
65 | sender_name: str = ""
66 | recipient_id: Optional[str] = None # None means broadcast to all
67 | recipient_name: Optional[str] = None
68 | content: str = ""
69 | timestamp: float = field(default_factory=time.time)
70 |
71 | @classmethod
72 | def create(cls, sender_id: str, sender_name: str, content: str,
73 | recipient_id: Optional[str] = None, recipient_name: Optional[str] = None) -> 'Message':
74 | """Create a new message."""
75 | return cls(
76 | sender_id=sender_id,
77 | sender_name=sender_name,
78 | recipient_id=recipient_id,
79 | recipient_name=recipient_name,
80 | content=content
81 | )
82 |
83 | class AgentCoordinator:
84 | """Coordinates communication between multiple agents."""
85 |
86 | def __init__(self):
87 | """Initialize the agent coordinator."""
88 | self.agents: Dict[str, Agent] = {}
89 | self.message_history: List[Message] = []
90 | self.broadcast_queue: asyncio.Queue = asyncio.Queue()
91 |
92 | def add_agent(self, agent: Agent) -> None:
93 | """Add a new agent to the coordinator.
94 |
95 | Args:
96 | agent: The agent to add
97 | """
98 | self.agents[agent.id] = agent
99 |
100 | def remove_agent(self, agent_id: str) -> None:
101 | """Remove an agent from the coordinator.
102 |
103 | Args:
104 | agent_id: ID of the agent to remove
105 | """
106 | if agent_id in self.agents:
107 | del self.agents[agent_id]
108 |
109 | async def broadcast_message(self, message: Message) -> None:
110 | """Broadcast a message to all agents.
111 |
112 | Args:
113 | message: The message to broadcast
114 | """
115 | self.message_history.append(message)
116 |
117 | for agent_id, agent in self.agents.items():
118 | # Don't send message back to sender
119 | if agent_id != message.sender_id:
120 | await agent.message_queue.put(message)
121 | logger.debug(f"Queued message from {message.sender_name} to {agent.name}")
122 |
123 | async def send_direct_message(self, message: Message) -> None:
124 | """Send a message to a specific agent.
125 |
126 | Args:
127 | message: The message to send
128 | """
129 | self.message_history.append(message)
130 |
131 | if message.recipient_id in self.agents:
132 | recipient = self.agents[message.recipient_id]
133 | await recipient.message_queue.put(message)
134 | logger.debug(f"Queued direct message from {message.sender_name} to {recipient.name}")
135 |
136 | async def process_message(self, message: Message) -> None:
137 | """Process an incoming message and route appropriately.
138 |
139 | Args:
140 | message: The message to process
141 | """
142 | if message.recipient_id is None:
143 | # Broadcast message
144 | await self.broadcast_message(message)
145 | else:
146 | # Direct message
147 | await self.send_direct_message(message)
148 |
149 | def get_message_history_for_agent(self, agent_id: str) -> List[Dict[str, Any]]:
150 | """Get conversation messages formatted for a specific agent.
151 |
152 | Args:
153 | agent_id: ID of the agent
154 |
155 | Returns:
156 | List of messages in the format expected by Claude
157 | """
158 | agent = self.agents.get(agent_id)
159 | if not agent:
160 | return []
161 |
162 | messages = []
163 |
164 | # Start with the agent's conversation history
165 | messages.extend(agent.conversation)
166 |
167 | # Add relevant messages from the message history
168 | for msg in self.message_history:
169 | # Include messages sent by this agent or addressed to this agent
170 | # or broadcast messages from other agents
171 | if (msg.sender_id == agent_id or
172 | msg.recipient_id == agent_id or
173 | (msg.recipient_id is None and msg.sender_id != agent_id)):
174 |
175 | if msg.sender_id == agent_id:
176 | # This agent's own messages
177 | messages.append({
178 | "role": "assistant",
179 | "content": msg.content
180 | })
181 | else:
182 | # Messages from other agents
183 | sender = self.agents.get(msg.sender_id)
184 | sender_name = sender.name if sender else msg.sender_name
185 |
186 | if msg.recipient_id is None:
187 | # Broadcast message
188 | messages.append({
189 | "role": "user",
190 | "content": f"{sender_name}: {msg.content}"
191 | })
192 | else:
193 | # Direct message
194 | messages.append({
195 | "role": "user",
196 | "content": f"{sender_name} (direct): {msg.content}"
197 | })
198 |
199 | return messages
200 |
201 | class MultiAgentMCPClient:
202 | """Multi-agent Model Context Protocol client with synchronization capabilities."""
203 |
204 | def __init__(self, config_path: str = None):
205 | """Initialize the multi-agent MCP client.
206 |
207 | Args:
208 | config_path: Path to the agent configuration file
209 | """
210 | self.session: Optional[ClientSession] = None
211 | self.exit_stack = AsyncExitStack()
212 | self.anthropic = Anthropic()
213 | self.coordinator = AgentCoordinator()
214 | self.available_tools = []
215 |
216 | # Configuration
217 | self.config_path = config_path
218 | self.agents_config = self._load_agents_config()
219 |
220 | def _load_agents_config(self) -> List[Dict[str, Any]]:
221 | """Load agent configurations from file.
222 |
223 | Returns:
224 | List of agent configurations
225 | """
226 | default_config = [{
227 | "name": "Assistant",
228 | "role": "general assistant",
229 | "model": "claude-3-5-sonnet-20241022",
230 | "system_prompt": "You are a helpful AI assistant participating in a multi-agent conversation. You can communicate with other agents and humans to solve complex problems."
231 | }]
232 |
233 | if not self.config_path:
234 | return default_config
235 |
236 | try:
237 | with open(self.config_path, 'r', encoding='utf-8') as f:
238 | return json.load(f)
239 | except Exception as e:
240 | logger.warning(f"Failed to load agent configuration: {e}")
241 | return default_config
242 |
243 | def setup_agents(self) -> None:
244 | """Set up agents based on configuration."""
245 | for idx, agent_config in enumerate(self.agents_config):
246 | agent_id = str(uuid.uuid4())
247 | agent = Agent(
248 | id=agent_id,
249 | name=agent_config.get("name", f"Agent-{idx+1}"),
250 | role=agent_config.get("role", "assistant"),
251 | model=agent_config.get("model", "claude-3-5-sonnet-20241022"),
252 | system_prompt=agent_config.get("system_prompt", "You are a helpful AI assistant.")
253 | )
254 | self.coordinator.add_agent(agent)
255 | logger.info(f"Created agent: {agent.name} ({agent.role})")
256 |
257 | async def connect_to_server(self, server_script_path: str):
258 | """Connect to an MCP server.
259 |
260 | Args:
261 | server_script_path: Path to the server script (.py or .js)
262 | """
263 | is_python = server_script_path.endswith('.py')
264 | is_js = server_script_path.endswith('.js')
265 | if not (is_python or is_js):
266 | raise ValueError("Server script must be a .py or .js file")
267 |
268 | command = "python" if is_python else "node"
269 | server_params = StdioServerParameters(
270 | command=command,
271 | args=[server_script_path],
272 | env=None
273 | )
274 |
275 | stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
276 | self.stdio, self.write = stdio_transport
277 | self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
278 |
279 | await self.session.initialize()
280 |
281 | # List available tools
282 | response = await self.session.list_tools()
283 | tools = response.tools
284 | self.available_tools = [{
285 | "name": tool.name,
286 | "description": tool.description,
287 | "input_schema": tool.inputSchema
288 | } for tool in response.tools]
289 |
290 | tool_names = [tool.name for tool in tools]
291 | logger.info(f"Connected to server with tools: {tool_names}")
292 | console.print(Panel.fit(
293 | f"[bold green]Connected to MCP server[/bold green]\n"
294 | f"Available tools: {', '.join(tool_names)}",
295 | title="Connection Status",
296 | border_style="green"
297 | ))
298 |
299 | async def process_agent_query(self, agent_id: str, query: str, is_direct_message: bool = False) -> str:
300 | """Process a query using Claude and available tools for a specific agent.
301 |
302 | Args:
303 | agent_id: The ID of the agent processing the query
304 | query: The query to process
305 | is_direct_message: Whether this is a direct message from user
306 |
307 | Returns:
308 | The response text
309 | """
310 | agent = self.coordinator.agents.get(agent_id)
311 | if not agent:
312 | return "Error: Agent not found"
313 |
314 | # Get the conversation history for this agent
315 | messages = self.coordinator.get_message_history_for_agent(agent_id)
316 |
317 | # Add the current query if it's a direct message
318 | if is_direct_message:
319 | messages.append({
320 | "role": "user",
321 | "content": query
322 | })
323 |
324 | # Initial Claude API call
325 | response = self.anthropic.messages.create(
326 | model=agent.model,
327 | max_tokens=1000,
328 | messages=messages,
329 | tools=self.available_tools
330 | )
331 |
332 | # Process response and handle tool calls
333 | tool_results = []
334 | final_text = ""
335 | assistant_message_content = []
336 |
337 | for content in response.content:
338 | if content.type == 'text':
339 | final_text = content.text
340 | assistant_message_content.append(content)
341 | elif content.type == 'tool_use':
342 | tool_name = content.name
343 | tool_args = content.input
344 |
345 | # Execute tool call
346 | result = await self.session.call_tool(tool_name, tool_args)
347 | tool_results.append({"call": tool_name, "result": result})
348 | console.print(f"[bold cyan]Agent {agent.name} calling tool {tool_name}[/bold cyan]")
349 |
350 | assistant_message_content.append(content)
351 | messages.append({
352 | "role": "assistant",
353 | "content": assistant_message_content
354 | })
355 | messages.append({
356 | "role": "user",
357 | "content": [
358 | {
359 | "type": "tool_result",
360 | "tool_use_id": content.id,
361 | "content": result.content
362 | }
363 | ]
364 | })
365 |
366 | # Get next response from Claude
367 | response = self.anthropic.messages.create(
368 | model=agent.model,
369 | max_tokens=1000,
370 | messages=messages,
371 | tools=self.available_tools
372 | )
373 |
374 | final_text = response.content[0].text
375 |
376 | # Create a message from the agent's response
377 | message = Message.create(
378 | sender_id=agent_id,
379 | sender_name=agent.name,
380 | content=final_text,
381 | recipient_id=None # Broadcast to all
382 | )
383 |
384 | # Process the message
385 | await self.coordinator.process_message(message)
386 |
387 | return final_text
388 |
389 | async def process_user_query(self, query: str, target_agent_id: Optional[str] = None) -> None:
390 | """Process a query from the user and route it to agents.
391 |
392 | Args:
393 | query: The user query
394 | target_agent_id: Optional ID of a specific agent to target
395 | """
396 | # Handle special commands
397 | if query.startswith("/"):
398 | await self._handle_special_command(query)
399 | return
400 |
401 | if target_agent_id:
402 | # Direct message to a specific agent
403 | agent = self.coordinator.agents.get(target_agent_id)
404 | if not agent:
405 | console.print("[bold red]Error: Agent not found[/bold red]")
406 | return
407 |
408 | console.print(f"[bold blue]User → {agent.name}:[/bold blue] {query}")
409 |
410 | response = await self.process_agent_query(target_agent_id, query, is_direct_message=True)
411 | console.print(f"[bold green]{agent.name}:[/bold green] {response}")
412 | else:
413 | # Broadcast to all agents
414 | console.print(f"[bold blue]User (broadcast):[/bold blue] {query}")
415 |
416 | # Create a message from the user
417 | message = Message.create(
418 | sender_id="user",
419 | sender_name="User",
420 | content=query,
421 | recipient_id=None # Broadcast
422 | )
423 |
424 | # Process the message
425 | await self.coordinator.process_message(message)
426 |
427 | # Process in parallel for all agents
428 | tasks = []
429 | for agent_id in self.coordinator.agents:
430 | tasks.append(asyncio.create_task(self.process_agent_query(agent_id, query)))
431 |
432 | # Wait for all agents to respond
433 | await asyncio.gather(*tasks)
434 |
435 | async def run_agent_thought_loops(self) -> None:
436 | """Run continuous thought loops for each agent in the background."""
437 | while True:
438 | for agent_id, agent in self.coordinator.agents.items():
439 | try:
440 | # Check if there are new messages for this agent
441 | if not agent.message_queue.empty():
442 | message = await agent.message_queue.get()
443 |
444 | # Log the message
445 | if message.recipient_id is None:
446 | console.print(f"[bold cyan]{message.sender_name} (broadcast):[/bold cyan] {message.content}")
447 | else:
448 | console.print(f"[bold cyan]{message.sender_name} → {agent.name}:[/bold cyan] {message.content}")
449 |
450 | # Give the agent a chance to respond
451 | await self.process_agent_query(agent_id, message.content)
452 |
453 | # Mark the message as processed
454 | agent.message_queue.task_done()
455 |
456 | except Exception as e:
457 | logger.exception(f"Error in agent thought loop for {agent.name}: {e}")
458 |
459 | # Small delay to prevent CPU hogging
460 | await asyncio.sleep(0.1)
461 |
462 | async def _handle_special_command(self, command: str) -> None:
463 | """Handle special commands.
464 |
465 | Args:
466 | command: The command string starting with /
467 | """
468 | parts = command.strip().split()
469 | cmd = parts[0].lower()
470 | args = parts[1:]
471 |
472 | if cmd == "/help":
473 | self._show_help()
474 | elif cmd == "/agents":
475 | self._show_agents()
476 | elif cmd == "/talk":
477 | if len(args) < 2:
478 | console.print("[bold red]Error: /talk requires agent name and message[/bold red]")
479 | return
480 |
481 | agent_name = args[0]
482 | message = " ".join(args[1:])
483 |
484 | # Find agent by name
485 | target_agent = None
486 | for agent_id, agent in self.coordinator.agents.items():
487 | if agent.name.lower() == agent_name.lower():
488 | target_agent = agent
489 | break
490 |
491 | if target_agent:
492 | await self.process_user_query(message, target_agent.id)
493 | else:
494 | console.print(f"[bold red]Error: Agent '{agent_name}' not found[/bold red]")
495 | elif cmd == "/history":
496 | self._show_message_history()
497 | elif cmd == "/quit" or cmd == "/exit":
498 | console.print("[bold yellow]Exiting multi-agent session...[/bold yellow]")
499 | sys.exit(0)
500 | else:
501 | console.print(f"[bold red]Unknown command: {cmd}[/bold red]")
502 | self._show_help()
503 |
504 | def _show_help(self) -> None:
505 | """Show help information."""
506 | help_text = """
507 | # Multi-Agent MCP Client Commands
508 |
509 | - **/help**: Show this help message
510 | - **/agents**: List all active agents
511 | - **/talk <agent> <message>**: Send a direct message to a specific agent
512 | - **/history**: Show message history
513 | - **/quit**, **/exit**: Exit the application
514 |
515 | To broadcast a message to all agents, simply type your message without any command.
516 | """
517 | console.print(Markdown(help_text))
518 |
519 | def _show_agents(self) -> None:
520 | """Show information about all active agents."""
521 | table = Table(title="Active Agents")
522 | table.add_column("Name", style="cyan")
523 | table.add_column("Role", style="green")
524 | table.add_column("Model", style="blue")
525 |
526 | for agent_id, agent in self.coordinator.agents.items():
527 | table.add_row(agent.name, agent.role, agent.model)
528 |
529 | console.print(table)
530 |
531 | def _show_message_history(self) -> None:
532 | """Show the message history."""
533 | if not self.coordinator.message_history:
534 | console.print("[yellow]No messages in history yet.[/yellow]")
535 | return
536 |
537 | table = Table(title="Message History")
538 | table.add_column("Time", style="cyan")
539 | table.add_column("From", style="green")
540 | table.add_column("To", style="blue")
541 | table.add_column("Message", style="white")
542 |
543 | for msg in self.coordinator.message_history:
544 | timestamp = time.strftime("%H:%M:%S", time.localtime(msg.timestamp))
545 | recipient = msg.recipient_name if msg.recipient_name else "All"
546 | table.add_row(timestamp, msg.sender_name, recipient, msg.content[:50] + ("..." if len(msg.content) > 50 else ""))
547 |
548 | console.print(table)
549 |
550 | async def chat_loop(self) -> None:
551 | """Run the interactive chat loop."""
552 | console.print(Panel.fit(
553 | "[bold green]Multi-Agent MCP Client Started![/bold green]\n"
554 | "Type your messages to broadcast to all agents or use /help for commands.",
555 | title="Welcome",
556 | border_style="green"
557 | ))
558 |
559 | # Start the agent thought loop in the background
560 | thought_loop_task = asyncio.create_task(self.run_agent_thought_loops())
561 |
562 | try:
563 | # First, show active agents
564 | self._show_agents()
565 |
566 | # Main chat loop
567 | while True:
568 | try:
569 | query = Prompt.ask("\n[bold blue]>[/bold blue]").strip()
570 |
571 | if not query:
572 | continue
573 |
574 | if query.lower() == "quit" or query.lower() == "exit":
575 | break
576 |
577 | await self.process_user_query(query)
578 |
579 | except KeyboardInterrupt:
580 | console.print("\n[bold yellow]Operation cancelled.[/bold yellow]")
581 | continue
582 | except Exception as e:
583 | console.print(f"\n[bold red]Error: {str(e)}[/bold red]")
584 | logger.exception("Error processing query")
585 |
586 | finally:
587 | # Cancel the thought loop task
588 | thought_loop_task.cancel()
589 | try:
590 | await thought_loop_task
591 | except asyncio.CancelledError:
592 | pass
593 |
594 | async def cleanup(self) -> None:
595 | """Clean up resources."""
596 | await self.exit_stack.aclose()
597 |
598 |
599 | def add_arguments(parser: argparse.ArgumentParser) -> None:
600 | """Add command-specific arguments to the parser.
601 |
602 | Args:
603 | parser: Argument parser
604 | """
605 | parser.add_argument(
606 | "server_script",
607 | type=str,
608 | help="Path to the server script (.py or .js)"
609 | )
610 |
611 | parser.add_argument(
612 | "--config",
613 | type=str,
614 | help="Path to agent configuration JSON file"
615 | )
616 |
617 |
618 | def execute(args: argparse.Namespace) -> int:
619 | """Execute the multi-agent client command.
620 |
621 | Args:
622 | args: Command arguments
623 |
624 | Returns:
625 | Exit code
626 | """
627 | try:
628 | client = MultiAgentMCPClient(config_path=args.config)
629 | client.setup_agents()
630 |
631 | async def run_client():
632 | try:
633 | await client.connect_to_server(args.server_script)
634 | await client.chat_loop()
635 | finally:
636 | await client.cleanup()
637 |
638 | asyncio.run(run_client())
639 | return 0
640 |
641 | except Exception as e:
642 | logger.exception(f"Error running multi-agent MCP client: {e}")
643 | console.print(f"[bold red]Error: {str(e)}[/bold red]")
644 | return 1
645 |
646 |
647 | def main() -> int:
648 | """Run the multi-agent client command as a standalone script."""
649 | parser = argparse.ArgumentParser(description="Run the Claude Code Multi-Agent MCP client")
650 | add_arguments(parser)
651 | args = parser.parse_args()
652 | return execute(args)
653 |
654 |
655 | if __name__ == "__main__":
656 | sys.exit(main())
```
--------------------------------------------------------------------------------
/claude_code/lib/tools/code_tools.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # claude_code/lib/tools/code_tools.py
3 | """Code analysis and manipulation tools."""
4 |
5 | import os
6 | import logging
7 | import subprocess
8 | import tempfile
9 | import json
10 | from typing import Dict, List, Optional, Any, Union
11 | import ast
12 | import re
13 |
14 | from .base import tool, ToolRegistry
15 |
16 | logger = logging.getLogger(__name__)
17 |
18 |
19 | @tool(
20 | name="CodeAnalyze",
21 | description="Analyze code to extract structure, dependencies, and complexity metrics",
22 | parameters={
23 | "type": "object",
24 | "properties": {
25 | "file_path": {
26 | "type": "string",
27 | "description": "The absolute path to the file to analyze"
28 | },
29 | "analysis_type": {
30 | "type": "string",
31 | "description": "Type of analysis to perform",
32 | "enum": ["structure", "complexity", "dependencies", "all"],
33 | "default": "all"
34 | }
35 | },
36 | "required": ["file_path"]
37 | },
38 | category="code"
39 | )
40 | def analyze_code(file_path: str, analysis_type: str = "all") -> str:
41 | """Analyze code to extract structure and metrics.
42 |
43 | Args:
44 | file_path: Path to the file to analyze
45 | analysis_type: Type of analysis to perform
46 |
47 | Returns:
48 | Analysis results as formatted text
49 | """
50 | logger.info(f"Analyzing code in {file_path} (type: {analysis_type})")
51 |
52 | if not os.path.isabs(file_path):
53 | return f"Error: File path must be absolute: {file_path}"
54 |
55 | if not os.path.exists(file_path):
56 | return f"Error: File not found: {file_path}"
57 |
58 | try:
59 | # Read the file
60 | with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
61 | code = f.read()
62 |
63 | # Get file extension
64 | _, ext = os.path.splitext(file_path)
65 | ext = ext.lower()
66 |
67 | # Determine language
68 | if ext in ['.py']:
69 | return _analyze_python(code, analysis_type)
70 | elif ext in ['.js', '.jsx', '.ts', '.tsx']:
71 | return _analyze_javascript(code, analysis_type)
72 | elif ext in ['.java']:
73 | return _analyze_java(code, analysis_type)
74 | elif ext in ['.c', '.cpp', '.cc', '.h', '.hpp']:
75 | return _analyze_cpp(code, analysis_type)
76 | else:
77 | return _analyze_generic(code, analysis_type)
78 |
79 | except Exception as e:
80 | logger.exception(f"Error analyzing code: {str(e)}")
81 | return f"Error analyzing code: {str(e)}"
82 |
83 |
84 | def _analyze_python(code: str, analysis_type: str) -> str:
85 | """Analyze Python code."""
86 | result = []
87 |
88 | # Structure analysis
89 | if analysis_type in ["structure", "all"]:
90 | try:
91 | tree = ast.parse(code)
92 |
93 | # Extract classes
94 | classes = [node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]
95 | if classes:
96 | result.append("Classes:")
97 | for cls in classes:
98 | methods = [node.name for node in ast.walk(cls) if isinstance(node, ast.FunctionDef)]
99 | result.append(f" - {cls.name}")
100 | if methods:
101 | result.append(" Methods:")
102 | for method in methods:
103 | result.append(f" - {method}")
104 |
105 | # Extract functions
106 | functions = [node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef) and
107 | not any(isinstance(parent, ast.ClassDef) for parent in ast.iter_child_nodes(tree))]
108 | if functions:
109 | result.append("\nFunctions:")
110 | for func in functions:
111 | result.append(f" - {func.name}")
112 |
113 | # Extract imports
114 | imports = []
115 | for node in ast.walk(tree):
116 | if isinstance(node, ast.Import):
117 | for name in node.names:
118 | imports.append(name.name)
119 | elif isinstance(node, ast.ImportFrom):
120 | module = node.module or ""
121 | for name in node.names:
122 | imports.append(f"{module}.{name.name}")
123 |
124 | if imports:
125 | result.append("\nImports:")
126 | for imp in imports:
127 | result.append(f" - {imp}")
128 |
129 | except SyntaxError as e:
130 | result.append(f"Error parsing Python code: {str(e)}")
131 |
132 | # Complexity analysis
133 | if analysis_type in ["complexity", "all"]:
134 | try:
135 | # Count lines of code
136 | lines = code.count('\n') + 1
137 | non_empty_lines = sum(1 for line in code.split('\n') if line.strip())
138 | comment_lines = sum(1 for line in code.split('\n') if line.strip().startswith('#'))
139 |
140 | result.append("\nComplexity Metrics:")
141 | result.append(f" - Total lines: {lines}")
142 | result.append(f" - Non-empty lines: {non_empty_lines}")
143 | result.append(f" - Comment lines: {comment_lines}")
144 | result.append(f" - Code lines: {non_empty_lines - comment_lines}")
145 |
146 | # Cyclomatic complexity (simplified)
147 | tree = ast.parse(code)
148 | complexity = 1 # Base complexity
149 | for node in ast.walk(tree):
150 | if isinstance(node, (ast.If, ast.While, ast.For, ast.comprehension)):
151 | complexity += 1
152 | elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.And):
153 | complexity += len(node.values) - 1
154 |
155 | result.append(f" - Cyclomatic complexity (estimated): {complexity}")
156 |
157 | except Exception as e:
158 | result.append(f"Error calculating complexity: {str(e)}")
159 |
160 | # Dependencies analysis
161 | if analysis_type in ["dependencies", "all"]:
162 | try:
163 | # Extract imports
164 | tree = ast.parse(code)
165 | std_lib_imports = []
166 | third_party_imports = []
167 | local_imports = []
168 |
169 | std_lib_modules = [
170 | "abc", "argparse", "ast", "asyncio", "base64", "collections", "concurrent", "contextlib",
171 | "copy", "csv", "datetime", "decimal", "enum", "functools", "glob", "gzip", "hashlib",
172 | "http", "io", "itertools", "json", "logging", "math", "multiprocessing", "os", "pathlib",
173 | "pickle", "random", "re", "shutil", "socket", "sqlite3", "string", "subprocess", "sys",
174 | "tempfile", "threading", "time", "typing", "unittest", "urllib", "uuid", "xml", "zipfile"
175 | ]
176 |
177 | for node in ast.walk(tree):
178 | if isinstance(node, ast.Import):
179 | for name in node.names:
180 | module = name.name.split('.')[0]
181 | if module in std_lib_modules:
182 | std_lib_imports.append(name.name)
183 | else:
184 | third_party_imports.append(name.name)
185 |
186 | elif isinstance(node, ast.ImportFrom):
187 | if node.module:
188 | module = node.module.split('.')[0]
189 | if module in std_lib_modules:
190 | for name in node.names:
191 | std_lib_imports.append(f"{node.module}.{name.name}")
192 | elif node.level > 0: # Relative import
193 | for name in node.names:
194 | local_imports.append(f"{'.' * node.level}{node.module or ''}.{name.name}")
195 | else:
196 | for name in node.names:
197 | third_party_imports.append(f"{node.module}.{name.name}")
198 |
199 | result.append("\nDependencies:")
200 | if std_lib_imports:
201 | result.append(" Standard Library:")
202 | for imp in sorted(set(std_lib_imports)):
203 | result.append(f" - {imp}")
204 |
205 | if third_party_imports:
206 | result.append(" Third-Party:")
207 | for imp in sorted(set(third_party_imports)):
208 | result.append(f" - {imp}")
209 |
210 | if local_imports:
211 | result.append(" Local/Project:")
212 | for imp in sorted(set(local_imports)):
213 | result.append(f" - {imp}")
214 |
215 | except Exception as e:
216 | result.append(f"Error analyzing dependencies: {str(e)}")
217 |
218 | return "\n".join(result)
219 |
220 |
221 | def _analyze_javascript(code: str, analysis_type: str) -> str:
222 | """Analyze JavaScript/TypeScript code."""
223 | result = []
224 |
225 | # Structure analysis
226 | if analysis_type in ["structure", "all"]:
227 | try:
228 | # Extract functions using regex (simplified)
229 | function_pattern = r'(function\s+(\w+)|const\s+(\w+)\s*=\s*function|const\s+(\w+)\s*=\s*\(.*?\)\s*=>)'
230 | functions = re.findall(function_pattern, code)
231 |
232 | if functions:
233 | result.append("Functions:")
234 | for func in functions:
235 | # Get the first non-empty group which is the function name
236 | func_name = next((name for name in func[1:] if name), "anonymous")
237 | result.append(f" - {func_name}")
238 |
239 | # Extract classes
240 | class_pattern = r'class\s+(\w+)'
241 | classes = re.findall(class_pattern, code)
242 |
243 | if classes:
244 | result.append("\nClasses:")
245 | for cls in classes:
246 | result.append(f" - {cls}")
247 |
248 | # Extract imports
249 | import_pattern = r'import\s+.*?from\s+[\'"](.+?)[\'"]'
250 | imports = re.findall(import_pattern, code)
251 |
252 | if imports:
253 | result.append("\nImports:")
254 | for imp in imports:
255 | result.append(f" - {imp}")
256 |
257 | except Exception as e:
258 | result.append(f"Error parsing JavaScript code: {str(e)}")
259 |
260 | # Complexity analysis
261 | if analysis_type in ["complexity", "all"]:
262 | try:
263 | # Count lines of code
264 | lines = code.count('\n') + 1
265 | non_empty_lines = sum(1 for line in code.split('\n') if line.strip())
266 | comment_lines = sum(1 for line in code.split('\n')
267 | if line.strip().startswith('//') or line.strip().startswith('/*'))
268 |
269 | result.append("\nComplexity Metrics:")
270 | result.append(f" - Total lines: {lines}")
271 | result.append(f" - Non-empty lines: {non_empty_lines}")
272 | result.append(f" - Comment lines: {comment_lines}")
273 | result.append(f" - Code lines: {non_empty_lines - comment_lines}")
274 |
275 | # Simplified cyclomatic complexity
276 | control_structures = len(re.findall(r'\b(if|for|while|switch|catch)\b', code))
277 | logical_operators = len(re.findall(r'(&&|\|\|)', code))
278 |
279 | complexity = 1 + control_structures + logical_operators
280 | result.append(f" - Cyclomatic complexity (estimated): {complexity}")
281 |
282 | except Exception as e:
283 | result.append(f"Error calculating complexity: {str(e)}")
284 |
285 | # Dependencies analysis
286 | if analysis_type in ["dependencies", "all"]:
287 | try:
288 | # Extract imports
289 | import_pattern = r'import\s+.*?from\s+[\'"](.+?)[\'"]'
290 | imports = re.findall(import_pattern, code)
291 |
292 | node_std_libs = [
293 | "fs", "path", "http", "https", "url", "querystring", "crypto", "os",
294 | "util", "stream", "events", "buffer", "assert", "zlib", "child_process"
295 | ]
296 |
297 | std_lib_imports = []
298 | third_party_imports = []
299 | local_imports = []
300 |
301 | for imp in imports:
302 | if imp in node_std_libs:
303 | std_lib_imports.append(imp)
304 | elif imp.startswith('.'):
305 | local_imports.append(imp)
306 | else:
307 | third_party_imports.append(imp)
308 |
309 | result.append("\nDependencies:")
310 | if std_lib_imports:
311 | result.append(" Standard Library:")
312 | for imp in sorted(set(std_lib_imports)):
313 | result.append(f" - {imp}")
314 |
315 | if third_party_imports:
316 | result.append(" Third-Party:")
317 | for imp in sorted(set(third_party_imports)):
318 | result.append(f" - {imp}")
319 |
320 | if local_imports:
321 | result.append(" Local/Project:")
322 | for imp in sorted(set(local_imports)):
323 | result.append(f" - {imp}")
324 |
325 | except Exception as e:
326 | result.append(f"Error analyzing dependencies: {str(e)}")
327 |
328 | return "\n".join(result)
329 |
330 |
331 | def _analyze_java(code: str, analysis_type: str) -> str:
332 | """Analyze Java code."""
333 | # Simplified Java analysis
334 | result = []
335 |
336 | # Structure analysis
337 | if analysis_type in ["structure", "all"]:
338 | try:
339 | # Extract class names
340 | class_pattern = r'(public|private|protected)?\s+class\s+(\w+)'
341 | classes = re.findall(class_pattern, code)
342 |
343 | if classes:
344 | result.append("Classes:")
345 | for cls in classes:
346 | result.append(f" - {cls[1]}")
347 |
348 | # Extract methods
349 | method_pattern = r'(public|private|protected)?\s+\w+\s+(\w+)\s*\([^)]*\)\s*\{'
350 | methods = re.findall(method_pattern, code)
351 |
352 | if methods:
353 | result.append("\nMethods:")
354 | for method in methods:
355 | result.append(f" - {method[1]}")
356 |
357 | # Extract imports
358 | import_pattern = r'import\s+(.+?);'
359 | imports = re.findall(import_pattern, code)
360 |
361 | if imports:
362 | result.append("\nImports:")
363 | for imp in imports:
364 | result.append(f" - {imp}")
365 |
366 | except Exception as e:
367 | result.append(f"Error parsing Java code: {str(e)}")
368 |
369 | # Complexity analysis
370 | if analysis_type in ["complexity", "all"]:
371 | try:
372 | # Count lines of code
373 | lines = code.count('\n') + 1
374 | non_empty_lines = sum(1 for line in code.split('\n') if line.strip())
375 | comment_lines = sum(1 for line in code.split('\n')
376 | if line.strip().startswith('//') or line.strip().startswith('/*'))
377 |
378 | result.append("\nComplexity Metrics:")
379 | result.append(f" - Total lines: {lines}")
380 | result.append(f" - Non-empty lines: {non_empty_lines}")
381 | result.append(f" - Comment lines: {comment_lines}")
382 | result.append(f" - Code lines: {non_empty_lines - comment_lines}")
383 |
384 | # Simplified cyclomatic complexity
385 | control_structures = len(re.findall(r'\b(if|for|while|switch|catch)\b', code))
386 | logical_operators = len(re.findall(r'(&&|\|\|)', code))
387 |
388 | complexity = 1 + control_structures + logical_operators
389 | result.append(f" - Cyclomatic complexity (estimated): {complexity}")
390 |
391 | except Exception as e:
392 | result.append(f"Error calculating complexity: {str(e)}")
393 |
394 | return "\n".join(result)
395 |
396 |
397 | def _analyze_cpp(code: str, analysis_type: str) -> str:
398 | """Analyze C/C++ code."""
399 | # Simplified C/C++ analysis
400 | result = []
401 |
402 | # Structure analysis
403 | if analysis_type in ["structure", "all"]:
404 | try:
405 | # Extract class names
406 | class_pattern = r'class\s+(\w+)'
407 | classes = re.findall(class_pattern, code)
408 |
409 | if classes:
410 | result.append("Classes:")
411 | for cls in classes:
412 | result.append(f" - {cls}")
413 |
414 | # Extract functions
415 | function_pattern = r'(\w+)\s+(\w+)\s*\([^)]*\)\s*\{'
416 | functions = re.findall(function_pattern, code)
417 |
418 | if functions:
419 | result.append("\nFunctions:")
420 | for func in functions:
421 | # Filter out keywords that might be matched
422 | if func[1] not in ['if', 'for', 'while', 'switch']:
423 | result.append(f" - {func[1]} (return type: {func[0]})")
424 |
425 | # Extract includes
426 | include_pattern = r'#include\s+[<"](.+?)[>"]'
427 | includes = re.findall(include_pattern, code)
428 |
429 | if includes:
430 | result.append("\nIncludes:")
431 | for inc in includes:
432 | result.append(f" - {inc}")
433 |
434 | except Exception as e:
435 | result.append(f"Error parsing C/C++ code: {str(e)}")
436 |
437 | # Complexity analysis
438 | if analysis_type in ["complexity", "all"]:
439 | try:
440 | # Count lines of code
441 | lines = code.count('\n') + 1
442 | non_empty_lines = sum(1 for line in code.split('\n') if line.strip())
443 | comment_lines = sum(1 for line in code.split('\n')
444 | if line.strip().startswith('//') or line.strip().startswith('/*'))
445 |
446 | result.append("\nComplexity Metrics:")
447 | result.append(f" - Total lines: {lines}")
448 | result.append(f" - Non-empty lines: {non_empty_lines}")
449 | result.append(f" - Comment lines: {comment_lines}")
450 | result.append(f" - Code lines: {non_empty_lines - comment_lines}")
451 |
452 | # Simplified cyclomatic complexity
453 | control_structures = len(re.findall(r'\b(if|for|while|switch|catch)\b', code))
454 | logical_operators = len(re.findall(r'(&&|\|\|)', code))
455 |
456 | complexity = 1 + control_structures + logical_operators
457 | result.append(f" - Cyclomatic complexity (estimated): {complexity}")
458 |
459 | except Exception as e:
460 | result.append(f"Error calculating complexity: {str(e)}")
461 |
462 | return "\n".join(result)
463 |
464 |
465 | def _analyze_generic(code: str, analysis_type: str) -> str:
466 | """Generic code analysis for unsupported languages."""
467 | result = []
468 |
469 | # Basic analysis for any language
470 | try:
471 | # Count lines of code
472 | lines = code.count('\n') + 1
473 | non_empty_lines = sum(1 for line in code.split('\n') if line.strip())
474 |
475 | result.append("Basic Code Metrics:")
476 | result.append(f" - Total lines: {lines}")
477 | result.append(f" - Non-empty lines: {non_empty_lines}")
478 |
479 | # Try to identify language
480 | language = "unknown"
481 | if "def " in code and "import " in code:
482 | language = "Python"
483 | elif "function " in code or "const " in code or "let " in code:
484 | language = "JavaScript"
485 | elif "public class " in code or "private class " in code:
486 | language = "Java"
487 | elif "#include" in code and "{" in code:
488 | language = "C/C++"
489 |
490 | result.append(f" - Detected language: {language}")
491 |
492 | # Find potential functions/methods using a generic pattern
493 | function_pattern = r'\b(\w+)\s*\([^)]*\)\s*\{'
494 | functions = re.findall(function_pattern, code)
495 |
496 | if functions:
497 | result.append("\nPotential Functions/Methods:")
498 | for func in functions:
499 | # Filter out common keywords
500 | if func not in ['if', 'for', 'while', 'switch', 'catch']:
501 | result.append(f" - {func}")
502 |
503 | except Exception as e:
504 | result.append(f"Error analyzing code: {str(e)}")
505 |
506 | return "\n".join(result)
507 |
508 |
509 | @tool(
510 | name="LintCode",
511 | description="Lint code to find potential issues and style violations",
512 | parameters={
513 | "type": "object",
514 | "properties": {
515 | "file_path": {
516 | "type": "string",
517 | "description": "The absolute path to the file to lint"
518 | },
519 | "linter": {
520 | "type": "string",
521 | "description": "Linter to use (auto, pylint, eslint, etc.)",
522 | "default": "auto"
523 | }
524 | },
525 | "required": ["file_path"]
526 | },
527 | category="code"
528 | )
529 | def lint_code(file_path: str, linter: str = "auto") -> str:
530 | """Lint code to find potential issues.
531 |
532 | Args:
533 | file_path: Path to the file to lint
534 | linter: Linter to use
535 |
536 | Returns:
537 | Linting results as formatted text
538 | """
539 | logger.info(f"Linting code in {file_path} using {linter}")
540 |
541 | if not os.path.isabs(file_path):
542 | return f"Error: File path must be absolute: {file_path}"
543 |
544 | if not os.path.exists(file_path):
545 | return f"Error: File not found: {file_path}"
546 |
547 | try:
548 | # Get file extension
549 | _, ext = os.path.splitext(file_path)
550 | ext = ext.lower()
551 |
552 | # Auto-detect linter if not specified
553 | if linter == "auto":
554 | if ext in ['.py']:
555 | linter = "pylint"
556 | elif ext in ['.js', '.jsx']:
557 | linter = "eslint"
558 | elif ext in ['.ts', '.tsx']:
559 | linter = "tslint"
560 | elif ext in ['.java']:
561 | linter = "checkstyle"
562 | elif ext in ['.c', '.cpp', '.cc', '.h', '.hpp']:
563 | linter = "cppcheck"
564 | else:
565 | return f"Error: Could not auto-detect linter for file type {ext}"
566 |
567 | # Run appropriate linter
568 | if linter == "pylint":
569 | return _run_pylint(file_path)
570 | elif linter == "eslint":
571 | return _run_eslint(file_path)
572 | elif linter == "tslint":
573 | return _run_tslint(file_path)
574 | elif linter == "checkstyle":
575 | return _run_checkstyle(file_path)
576 | elif linter == "cppcheck":
577 | return _run_cppcheck(file_path)
578 | else:
579 | return f"Error: Unsupported linter: {linter}"
580 |
581 | except Exception as e:
582 | logger.exception(f"Error linting code: {str(e)}")
583 | return f"Error linting code: {str(e)}"
584 |
585 |
586 | def _run_pylint(file_path: str) -> str:
587 | """Run pylint on a Python file."""
588 | try:
589 | # Check if pylint is installed
590 | try:
591 | subprocess.run(["pylint", "--version"], capture_output=True, check=True)
592 | except (subprocess.SubprocessError, FileNotFoundError):
593 | return "Error: pylint is not installed. Please install it with 'pip install pylint'."
594 |
595 | # Run pylint
596 | result = subprocess.run(
597 | ["pylint", "--output-format=text", file_path],
598 | capture_output=True,
599 | text=True
600 | )
601 |
602 | if result.returncode == 0:
603 | return "No issues found."
604 |
605 | # Format output
606 | output = result.stdout or result.stderr
607 |
608 | # Summarize output
609 | lines = output.split('\n')
610 | summary_lines = [line for line in lines if "rated at" in line]
611 | issue_lines = [line for line in lines if re.match(r'^.*?:\d+:\d+:', line)]
612 |
613 | formatted_output = []
614 |
615 | if issue_lines:
616 | formatted_output.append("Issues found:")
617 | for line in issue_lines:
618 | formatted_output.append(f" {line}")
619 |
620 | if summary_lines:
621 | formatted_output.append("\nSummary:")
622 | for line in summary_lines:
623 | formatted_output.append(f" {line}")
624 |
625 | return "\n".join(formatted_output)
626 |
627 | except Exception as e:
628 | return f"Error running pylint: {str(e)}"
629 |
630 |
631 | def _run_eslint(file_path: str) -> str:
632 | """Run eslint on a JavaScript file."""
633 | try:
634 | # Check if eslint is installed
635 | try:
636 | subprocess.run(["eslint", "--version"], capture_output=True, check=True)
637 | except (subprocess.SubprocessError, FileNotFoundError):
638 | return "Error: eslint is not installed. Please install it with 'npm install -g eslint'."
639 |
640 | # Run eslint
641 | result = subprocess.run(
642 | ["eslint", "--format=stylish", file_path],
643 | capture_output=True,
644 | text=True
645 | )
646 |
647 | if result.returncode == 0:
648 | return "No issues found."
649 |
650 | # Format output
651 | output = result.stdout or result.stderr
652 |
653 | # Clean up output
654 | lines = output.split('\n')
655 | filtered_lines = [line for line in lines if line.strip() and not line.startswith("eslint:")]
656 |
657 | return "\n".join(filtered_lines)
658 |
659 | except Exception as e:
660 | return f"Error running eslint: {str(e)}"
661 |
662 |
663 | def _run_tslint(file_path: str) -> str:
664 | """Run tslint on a TypeScript file."""
665 | try:
666 | # Check if tslint is installed
667 | try:
668 | subprocess.run(["tslint", "--version"], capture_output=True, check=True)
669 | except (subprocess.SubprocessError, FileNotFoundError):
670 | return "Error: tslint is not installed. Please install it with 'npm install -g tslint'."
671 |
672 | # Run tslint
673 | result = subprocess.run(
674 | ["tslint", "-t", "verbose", file_path],
675 | capture_output=True,
676 | text=True
677 | )
678 |
679 | if result.returncode == 0:
680 | return "No issues found."
681 |
682 | # Format output
683 | output = result.stdout or result.stderr
684 |
685 | return output
686 |
687 | except Exception as e:
688 | return f"Error running tslint: {str(e)}"
689 |
690 |
691 | def _run_checkstyle(file_path: str) -> str:
692 | """Run checkstyle on a Java file."""
693 | return "Checkstyle support not implemented. Please install and run checkstyle manually."
694 |
695 |
696 | def _run_cppcheck(file_path: str) -> str:
697 | """Run cppcheck on a C/C++ file."""
698 | try:
699 | # Check if cppcheck is installed
700 | try:
701 | subprocess.run(["cppcheck", "--version"], capture_output=True, check=True)
702 | except (subprocess.SubprocessError, FileNotFoundError):
703 | return "Error: cppcheck is not installed. Please install it using your system package manager."
704 |
705 | # Run cppcheck
706 | result = subprocess.run(
707 | ["cppcheck", "--enable=all", "--template='{file}:{line}: {severity}: {message}'", file_path],
708 | capture_output=True,
709 | text=True
710 | )
711 |
712 | # Format output
713 | output = result.stderr # cppcheck outputs to stderr
714 |
715 | if not output or "no errors found" in output.lower():
716 | return "No issues found."
717 |
718 | # Clean up output
719 | lines = output.split('\n')
720 | filtered_lines = [line for line in lines if line.strip() and "Checking" not in line]
721 |
722 | return "\n".join(filtered_lines)
723 |
724 | except Exception as e:
725 | return f"Error running cppcheck: {str(e)}"
726 |
727 |
728 | def register_code_tools(registry: ToolRegistry) -> None:
729 | """Register all code analysis tools with the registry.
730 |
731 | Args:
732 | registry: Tool registry to register with
733 | """
734 | from .base import create_tools_from_functions
735 |
736 | code_tools = [
737 | analyze_code,
738 | lint_code
739 | ]
740 |
741 | create_tools_from_functions(registry, code_tools)
742 |
```
--------------------------------------------------------------------------------
/claude_code/lib/tools/manager.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # claude_code/lib/tools/manager.py
3 | """Tool execution manager."""
4 |
5 | import logging
6 | import time
7 | import json
8 | import uuid
9 | import os
10 | from typing import Dict, List, Any, Optional, Callable, Union, Sequence
11 | import concurrent.futures
12 | from concurrent.futures import ThreadPoolExecutor, Future
13 |
14 | from .base import Tool, ToolResult, ToolRegistry, Routine, RoutineStep, RoutineDefinition
15 |
16 | logger = logging.getLogger(__name__)
17 |
18 |
19 | class RoutineExecutionManager:
20 | """Manages the execution of tool routines."""
21 |
22 | def __init__(self, registry: ToolRegistry, execution_manager: 'ToolExecutionManager'):
23 | """Initialize the routine execution manager.
24 |
25 | Args:
26 | registry: Tool registry containing available tools and routines
27 | execution_manager: Tool execution manager for executing individual tools
28 | """
29 | self.registry = registry
30 | self.execution_manager = execution_manager
31 | self.active_routines: Dict[str, Dict[str, Any]] = {}
32 | self.progress_callback: Optional[Callable[[str, str, float], None]] = None
33 | self.result_callback: Optional[Callable[[str, List[ToolResult]], None]] = None
34 |
35 | # Load existing routines
36 | self.registry.load_routines()
37 |
38 | def set_progress_callback(self, callback: Callable[[str, str, float], None]) -> None:
39 | """Set a callback function for routine progress updates.
40 |
41 | Args:
42 | callback: Function that takes routine_id, step_name, and progress (0-1) as arguments
43 | """
44 | self.progress_callback = callback
45 |
46 | def set_result_callback(self, callback: Callable[[str, List[ToolResult]], None]) -> None:
47 | """Set a callback function for routine results.
48 |
49 | Args:
50 | callback: Function that takes routine_id and list of ToolResults as arguments
51 | """
52 | self.result_callback = callback
53 |
54 | def create_routine(self, definition: RoutineDefinition) -> str:
55 | """Create a new routine from a definition.
56 |
57 | Args:
58 | definition: Routine definition
59 |
60 | Returns:
61 | Routine ID
62 |
63 | Raises:
64 | ValueError: If a routine with the same name already exists
65 | """
66 | # Convert step objects to dictionaries
67 | steps = []
68 | for step in definition.steps:
69 | step_dict = {
70 | "tool_name": step.tool_name,
71 | "args": step.args
72 | }
73 | if step.condition is not None:
74 | step_dict["condition"] = step.condition
75 | if step.store_result:
76 | step_dict["store_result"] = True
77 | if step.result_var is not None:
78 | step_dict["result_var"] = step.result_var
79 |
80 | steps.append(step_dict)
81 |
82 | # Create routine
83 | routine = Routine(
84 | name=definition.name,
85 | description=definition.description,
86 | steps=steps
87 | )
88 |
89 | # Register routine
90 | self.registry.register_routine(routine)
91 |
92 | return routine.name
93 |
94 | def create_routine_from_tool_history(
95 | self,
96 | name: str,
97 | description: str,
98 | tool_results: List[ToolResult],
99 | context_variables: Dict[str, Any] = None
100 | ) -> str:
101 | """Create a routine from a history of tool executions.
102 |
103 | Args:
104 | name: Name for the routine
105 | description: Description of the routine
106 | tool_results: List of tool results to base the routine on
107 | context_variables: Optional dictionary of context variables to identify
108 |
109 | Returns:
110 | Routine ID
111 | """
112 | steps = []
113 |
114 | # Process tool results into steps
115 | for i, result in enumerate(tool_results):
116 | # Skip failed tool calls
117 | if result.status != "success":
118 | continue
119 |
120 | # Get tool
121 | tool = self.registry.get_tool(result.name)
122 | if not tool:
123 | continue
124 |
125 | # Extract arguments from tool call
126 | args = {}
127 | # Here we would need to extract the arguments from the tool call
128 | # This is a simplification and would need to be adapted to the actual structure
129 |
130 | # Create step
131 | step = {
132 | "tool_name": result.name,
133 | "args": args,
134 | "store_result": True,
135 | "result_var": f"result_{i}"
136 | }
137 |
138 | steps.append(step)
139 |
140 | # Create routine
141 | routine = Routine(
142 | name=name,
143 | description=description,
144 | steps=steps
145 | )
146 |
147 | # Register routine
148 | self.registry.register_routine(routine)
149 |
150 | return routine.name
151 |
152 | def execute_routine(self, name: str, context: Dict[str, Any] = None) -> str:
153 | """Execute a routine with the given context.
154 |
155 | Args:
156 | name: Name of the routine to execute
157 | context: Context variables for the routine
158 |
159 | Returns:
160 | Routine execution ID
161 |
162 | Raises:
163 | ValueError: If the routine is not found
164 | """
165 | # Get routine
166 | routine = self.registry.get_routine(name)
167 | if not routine:
168 | raise ValueError(f"Routine not found: {name}")
169 |
170 | # Create execution ID
171 | execution_id = str(uuid.uuid4())
172 |
173 | # Initialize context
174 | if context is None:
175 | context = {}
176 |
177 | # Initialize execution state
178 | self.active_routines[execution_id] = {
179 | "routine": routine,
180 | "context": context.copy(),
181 | "results": [],
182 | "current_step": 0,
183 | "start_time": time.time(),
184 | "status": "running"
185 | }
186 |
187 | # Record routine usage
188 | self.registry.record_routine_usage(name)
189 |
190 | # Start execution in background thread
191 | executor = ThreadPoolExecutor(max_workers=1)
192 | executor.submit(self._execute_routine_steps, execution_id)
193 |
194 | return execution_id
195 |
196 | def _execute_routine_steps(self, execution_id: str) -> None:
197 | """Execute the steps of a routine in sequence.
198 |
199 | Args:
200 | execution_id: Routine execution ID
201 | """
202 | if execution_id not in self.active_routines:
203 | logger.error(f"Routine execution not found: {execution_id}")
204 | return
205 |
206 | execution = self.active_routines[execution_id]
207 | routine = execution["routine"]
208 | context = execution["context"]
209 | results = execution["results"]
210 |
211 | try:
212 | # Execute each step
213 | for i, step in enumerate(routine.steps):
214 | # Update current step
215 | execution["current_step"] = i
216 |
217 | # Check for conditions
218 | if "condition" in step and not self._evaluate_condition(step["condition"], context, results):
219 | logger.info(f"Skipping step {i+1}/{len(routine.steps)} due to condition")
220 | continue
221 |
222 | # Process tool arguments with variable substitution
223 | processed_args = self._process_arguments(step["args"], context, results)
224 |
225 | # Create tool call
226 | tool_call = {
227 | "id": f"{execution_id}_{i}",
228 | "function": {
229 | "name": step["tool_name"],
230 | "arguments": json.dumps(processed_args)
231 | }
232 | }
233 |
234 | # Report progress
235 | self._report_routine_progress(execution_id, i, len(routine.steps), step["tool_name"])
236 |
237 | # Execute tool
238 | result = self.execution_manager.execute_tool(tool_call)
239 |
240 | # Add result to results
241 | results.append(result)
242 |
243 | # Store result in context if requested
244 | if step.get("store_result", False):
245 | var_name = step.get("result_var", f"result_{i}")
246 | context[var_name] = result.result
247 |
248 | # Check for loop control
249 | if "repeat_until" in step and not self._evaluate_condition(step["repeat_until"], context, results):
250 | # Go back to specified step
251 | target_step = step.get("repeat_target", 0)
252 | if 0 <= target_step < i:
253 | i = target_step - 1 # Will be incremented in next loop iteration
254 |
255 | # Check for exit condition
256 | if "exit_condition" in step and self._evaluate_condition(step["exit_condition"], context, results):
257 | logger.info(f"Exiting routine early due to exit condition at step {i+1}/{len(routine.steps)}")
258 | break
259 |
260 | # Update execution status
261 | execution["status"] = "completed"
262 |
263 | # Report final progress
264 | self._report_routine_progress(execution_id, len(routine.steps), len(routine.steps), "completed")
265 |
266 | # Call result callback
267 | if self.result_callback:
268 | self.result_callback(execution_id, results)
269 |
270 | except Exception as e:
271 | logger.exception(f"Error executing routine: {e}")
272 | execution["status"] = "error"
273 | execution["error"] = str(e)
274 |
275 | # Report error progress
276 | self._report_routine_progress(execution_id, execution["current_step"], len(routine.steps), "error")
277 |
278 | def _process_arguments(
279 | self,
280 | args: Dict[str, Any],
281 | context: Dict[str, Any],
282 | results: List[ToolResult]
283 | ) -> Dict[str, Any]:
284 | """Process tool arguments with variable substitution.
285 |
286 | Args:
287 | args: Tool arguments
288 | context: Context variables
289 | results: Previous tool results
290 |
291 | Returns:
292 | Processed arguments
293 | """
294 | processed_args = {}
295 |
296 | for key, value in args.items():
297 | if isinstance(value, str) and value.startswith("$"):
298 | # Variable reference
299 | var_name = value[1:]
300 | if var_name in context:
301 | processed_args[key] = context[var_name]
302 | elif var_name.startswith("result[") and var_name.endswith("]"):
303 | # Reference to previous result
304 | try:
305 | idx = int(var_name[7:-1])
306 | if 0 <= idx < len(results):
307 | processed_args[key] = results[idx].result
308 | else:
309 | processed_args[key] = value
310 | except (ValueError, IndexError):
311 | processed_args[key] = value
312 | else:
313 | processed_args[key] = value
314 | else:
315 | processed_args[key] = value
316 |
317 | return processed_args
318 |
319 | def _evaluate_condition(
320 | self,
321 | condition: Dict[str, Any],
322 | context: Dict[str, Any],
323 | results: List[ToolResult]
324 | ) -> bool:
325 | """Evaluate a condition for a routine step.
326 |
327 | Args:
328 | condition: Condition specification
329 | context: Context variables
330 | results: Previous tool results
331 |
332 | Returns:
333 | Whether the condition is met
334 | """
335 | condition_type = condition.get("type", "simple")
336 |
337 | if condition_type == "simple":
338 | # Simple variable comparison
339 | var_name = condition.get("variable", "")
340 | operation = condition.get("operation", "equals")
341 | value = condition.get("value")
342 |
343 | # Get variable value
344 | var_value = None
345 | if var_name.startswith("$"):
346 | var_name = var_name[1:]
347 | var_value = context.get(var_name)
348 | elif var_name.startswith("result[") and var_name.endswith("]"):
349 | try:
350 | idx = int(var_name[7:-1])
351 | if 0 <= idx < len(results):
352 | var_value = results[idx].result
353 | except (ValueError, IndexError):
354 | return False
355 |
356 | # Compare
357 | if operation == "equals":
358 | return var_value == value
359 | elif operation == "not_equals":
360 | return var_value != value
361 | elif operation == "contains":
362 | return value in var_value if var_value is not None else False
363 | elif operation == "greater_than":
364 | return var_value > value if var_value is not None else False
365 | elif operation == "less_than":
366 | return var_value < value if var_value is not None else False
367 |
368 | return False
369 |
370 | elif condition_type == "and":
371 | # Logical AND of multiple conditions
372 | sub_conditions = condition.get("conditions", [])
373 | return all(self._evaluate_condition(c, context, results) for c in sub_conditions)
374 |
375 | elif condition_type == "or":
376 | # Logical OR of multiple conditions
377 | sub_conditions = condition.get("conditions", [])
378 | return any(self._evaluate_condition(c, context, results) for c in sub_conditions)
379 |
380 | elif condition_type == "not":
381 | # Logical NOT
382 | sub_condition = condition.get("condition", {})
383 | return not self._evaluate_condition(sub_condition, context, results)
384 |
385 | return True # Default to True
386 |
387 | def _report_routine_progress(
388 | self,
389 | execution_id: str,
390 | current_step: int,
391 | total_steps: int,
392 | step_name: str
393 | ) -> None:
394 | """Report progress for a routine execution.
395 |
396 | Args:
397 | execution_id: Routine execution ID
398 | current_step: Current step index
399 | total_steps: Total number of steps
400 | step_name: Name of the current step
401 | """
402 | progress = current_step / total_steps if total_steps > 0 else 1.0
403 |
404 | # Call progress callback if set
405 | if self.progress_callback:
406 | self.progress_callback(execution_id, step_name, progress)
407 |
408 | def get_active_routines(self) -> Dict[str, Dict[str, Any]]:
409 | """Get information about active routine executions.
410 |
411 | Returns:
412 | Dictionary mapping execution ID to routine execution information
413 | """
414 | return {
415 | k: {
416 | "routine_name": v["routine"].name,
417 | "current_step": v["current_step"],
418 | "total_steps": len(v["routine"].steps),
419 | "status": v["status"],
420 | "start_time": v["start_time"],
421 | "elapsed_time": time.time() - v["start_time"]
422 | }
423 | for k, v in self.active_routines.items()
424 | }
425 |
426 | def get_routine_results(self, execution_id: str) -> Optional[List[ToolResult]]:
427 | """Get the results of a routine execution.
428 |
429 | Args:
430 | execution_id: Routine execution ID
431 |
432 | Returns:
433 | List of tool results, or None if the routine execution is not found
434 | """
435 | if execution_id in self.active_routines:
436 | return self.active_routines[execution_id]["results"]
437 | return None
438 |
439 | def cancel_routine(self, execution_id: str) -> bool:
440 | """Cancel a routine execution.
441 |
442 | Args:
443 | execution_id: Routine execution ID
444 |
445 | Returns:
446 | Whether the routine was canceled successfully
447 | """
448 | if execution_id in self.active_routines:
449 | self.active_routines[execution_id]["status"] = "canceled"
450 | return True
451 | return False
452 |
453 |
454 | class ToolExecutionManager:
455 | """Manages tool execution, including parallel execution and progress tracking."""
456 |
457 | def __init__(self, registry: ToolRegistry):
458 | """Initialize the tool execution manager.
459 |
460 | Args:
461 | registry: Tool registry containing available tools
462 | """
463 | self.registry = registry
464 | self.active_executions: Dict[str, Dict[str, Any]] = {}
465 | self.progress_callback: Optional[Callable[[str, float], None]] = None
466 | self.result_callback: Optional[Callable[[str, ToolResult], None]] = None
467 | self.max_workers = 10
468 |
469 | # Initialize routine manager
470 | self.routine_manager = RoutineExecutionManager(registry, self)
471 |
472 | def set_progress_callback(self, callback: Callable[[str, float], None]) -> None:
473 | """Set a callback function for progress updates.
474 |
475 | Args:
476 | callback: Function that takes tool_call_id and progress (0-1) as arguments
477 | """
478 | self.progress_callback = callback
479 |
480 | def set_result_callback(self, callback: Callable[[str, ToolResult], None]) -> None:
481 | """Set a callback function for results.
482 |
483 | Args:
484 | callback: Function that takes tool_call_id and ToolResult as arguments
485 | """
486 | self.result_callback = callback
487 |
488 | def execute_tool(self, tool_call: Dict[str, Any]) -> ToolResult:
489 | """Execute a single tool synchronously.
490 |
491 | Args:
492 | tool_call: Dictionary containing tool call information
493 |
494 | Returns:
495 | ToolResult with execution result
496 |
497 | Raises:
498 | ValueError: If the tool is not found
499 | """
500 | function_name = tool_call.get("function", {}).get("name", "")
501 | tool_call_id = tool_call.get("id", "unknown")
502 |
503 | # Check if it's a routine
504 | if function_name.startswith("routine."):
505 | routine_name = function_name[9:] # Remove "routine." prefix
506 | return self._execute_routine_as_tool(routine_name, tool_call)
507 |
508 | # Get the tool
509 | tool = self.registry.get_tool(function_name)
510 | if not tool:
511 | error_msg = f"Tool not found: {function_name}"
512 | logger.error(error_msg)
513 | return ToolResult(
514 | tool_call_id=tool_call_id,
515 | name=function_name,
516 | result=f"Error: {error_msg}",
517 | execution_time=0,
518 | status="error",
519 | error=error_msg
520 | )
521 |
522 | # Check if tool needs permission and handle it
523 | if tool.needs_permission:
524 | # TODO: Implement permission handling
525 | logger.warning(f"Tool {function_name} needs permission, but permission handling is not implemented")
526 |
527 | # Track progress
528 | self._track_execution_start(tool_call_id, function_name)
529 |
530 | try:
531 | # Execute the tool
532 | result = tool.execute(tool_call)
533 |
534 | # Track completion
535 | self._track_execution_complete(tool_call_id, result)
536 |
537 | return result
538 | except Exception as e:
539 | logger.exception(f"Error executing tool {function_name}: {e}")
540 | result = ToolResult(
541 | tool_call_id=tool_call_id,
542 | name=function_name,
543 | result=f"Error: {str(e)}",
544 | execution_time=0,
545 | status="error",
546 | error=str(e)
547 | )
548 | self._track_execution_complete(tool_call_id, result)
549 | return result
550 |
551 | def _execute_routine_as_tool(self, routine_name: str, tool_call: Dict[str, Any]) -> ToolResult:
552 | """Execute a routine as if it were a tool.
553 |
554 | Args:
555 | routine_name: Name of the routine
556 | tool_call: Dictionary containing tool call information
557 |
558 | Returns:
559 | ToolResult with execution result
560 | """
561 | tool_call_id = tool_call.get("id", "unknown")
562 | start_time = time.time()
563 |
564 | try:
565 | # Extract context from arguments
566 | arguments_str = tool_call.get("function", {}).get("arguments", "{}")
567 | try:
568 | context = json.loads(arguments_str)
569 | except json.JSONDecodeError:
570 | context = {}
571 |
572 | # Execute routine
573 | execution_id = self.routine_manager.execute_routine(routine_name, context)
574 |
575 | # Wait for routine to complete
576 | while True:
577 | routine_status = self.routine_manager.get_active_routines().get(execution_id, {})
578 | if routine_status.get("status") != "running":
579 | break
580 | time.sleep(0.1)
581 |
582 | # Get results
583 | results = self.routine_manager.get_routine_results(execution_id)
584 | if not results:
585 | raise ValueError(f"No results from routine: {routine_name}")
586 |
587 | # Format results
588 | result_summary = f"Routine {routine_name} executed successfully with {len(results)} steps\n\n"
589 | for i, result in enumerate(results):
590 | result_summary += f"Step {i+1}: {result.name} - {'SUCCESS' if result.status == 'success' else 'ERROR'}\n"
591 | if result.status != "success":
592 | result_summary += f" Error: {result.error}\n"
593 |
594 | # Track execution time
595 | execution_time = time.time() - start_time
596 |
597 | # Create result
598 | return ToolResult(
599 | tool_call_id=tool_call_id,
600 | name=f"routine.{routine_name}",
601 | result=result_summary,
602 | execution_time=execution_time,
603 | status="success"
604 | )
605 |
606 | except Exception as e:
607 | logger.exception(f"Error executing routine {routine_name}: {e}")
608 | return ToolResult(
609 | tool_call_id=tool_call_id,
610 | name=f"routine.{routine_name}",
611 | result=f"Error: {str(e)}",
612 | execution_time=time.time() - start_time,
613 | status="error",
614 | error=str(e)
615 | )
616 |
617 | def execute_tools_parallel(self, tool_calls: List[Dict[str, Any]]) -> List[ToolResult]:
618 | """Execute multiple tools in parallel.
619 |
620 | Args:
621 | tool_calls: List of dictionaries containing tool call information
622 |
623 | Returns:
624 | List of ToolResult with execution results
625 | """
626 | results = []
627 | futures: Dict[Future, str] = {}
628 |
629 | # Use ThreadPoolExecutor for parallel execution
630 | with ThreadPoolExecutor(max_workers=min(self.max_workers, len(tool_calls))) as executor:
631 | # Submit all tool calls
632 | for tool_call in tool_calls:
633 | tool_call_id = tool_call.get("id", "unknown")
634 | future = executor.submit(self.execute_tool, tool_call)
635 | futures[future] = tool_call_id
636 |
637 | # Wait for completion and collect results
638 | for future in concurrent.futures.as_completed(futures):
639 | tool_call_id = futures[future]
640 | try:
641 | result = future.result()
642 | results.append(result)
643 | except Exception as e:
644 | logger.exception(f"Error in parallel tool execution for {tool_call_id}: {e}")
645 | # Create an error result
646 | function_name = next(
647 | (tc.get("function", {}).get("name", "") for tc in tool_calls
648 | if tc.get("id", "") == tool_call_id),
649 | "unknown"
650 | )
651 | results.append(ToolResult(
652 | tool_call_id=tool_call_id,
653 | name=function_name,
654 | result=f"Error: {str(e)}",
655 | execution_time=0,
656 | status="error",
657 | error=str(e)
658 | ))
659 |
660 | return results
661 |
662 | def create_routine(self, definition: RoutineDefinition) -> str:
663 | """Create a new routine.
664 |
665 | Args:
666 | definition: Routine definition
667 |
668 | Returns:
669 | Routine ID
670 | """
671 | return self.routine_manager.create_routine(definition)
672 |
673 | def create_routine_from_tool_history(
674 | self,
675 | name: str,
676 | description: str,
677 | tool_results: List[ToolResult],
678 | context_variables: Dict[str, Any] = None
679 | ) -> str:
680 | """Create a routine from a history of tool executions.
681 |
682 | Args:
683 | name: Name for the routine
684 | description: Description of the routine
685 | tool_results: List of tool results to base the routine on
686 | context_variables: Optional dictionary of context variables to identify
687 |
688 | Returns:
689 | Routine ID
690 | """
691 | return self.routine_manager.create_routine_from_tool_history(
692 | name, description, tool_results, context_variables
693 | )
694 |
695 | def execute_routine(self, name: str, context: Dict[str, Any] = None) -> str:
696 | """Execute a routine with the given context.
697 |
698 | Args:
699 | name: Name of the routine to execute
700 | context: Context variables for the routine
701 |
702 | Returns:
703 | Routine execution ID
704 | """
705 | return self.routine_manager.execute_routine(name, context)
706 |
707 | def get_routine_results(self, execution_id: str) -> Optional[List[ToolResult]]:
708 | """Get the results of a routine execution.
709 |
710 | Args:
711 | execution_id: Routine execution ID
712 |
713 | Returns:
714 | List of tool results, or None if the routine execution is not found
715 | """
716 | return self.routine_manager.get_routine_results(execution_id)
717 |
718 | def _track_execution_start(self, tool_call_id: str, tool_name: str) -> None:
719 | """Track the start of tool execution.
720 |
721 | Args:
722 | tool_call_id: ID of the tool call
723 | tool_name: Name of the tool
724 | """
725 | self.active_executions[tool_call_id] = {
726 | "tool_name": tool_name,
727 | "start_time": time.time(),
728 | "progress": 0.0
729 | }
730 |
731 | # Call progress callback if set
732 | if self.progress_callback:
733 | self.progress_callback(tool_call_id, 0.0)
734 |
735 | def _track_execution_progress(self, tool_call_id: str, progress: float) -> None:
736 | """Track the progress of tool execution.
737 |
738 | Args:
739 | tool_call_id: ID of the tool call
740 | progress: Progress value (0-1)
741 | """
742 | if tool_call_id in self.active_executions:
743 | self.active_executions[tool_call_id]["progress"] = progress
744 |
745 | # Call progress callback if set
746 | if self.progress_callback:
747 | self.progress_callback(tool_call_id, progress)
748 |
749 | def _track_execution_complete(self, tool_call_id: str, result: ToolResult) -> None:
750 | """Track the completion of tool execution.
751 |
752 | Args:
753 | tool_call_id: ID of the tool call
754 | result: Tool execution result
755 | """
756 | if tool_call_id in self.active_executions:
757 | # Update progress
758 | self._track_execution_progress(tool_call_id, 1.0)
759 |
760 | # Calculate execution time
761 | start_time = self.active_executions[tool_call_id]["start_time"]
762 | execution_time = time.time() - start_time
763 |
764 | # Clean up
765 | del self.active_executions[tool_call_id]
766 |
767 | # Call result callback if set
768 | if self.result_callback:
769 | self.result_callback(tool_call_id, result)
770 |
771 | def get_active_executions(self) -> Dict[str, Dict[str, Any]]:
772 | """Get information about active tool executions.
773 |
774 | Returns:
775 | Dictionary mapping tool_call_id to execution information
776 | """
777 | return self.active_executions.copy()
778 |
779 | def cancel_execution(self, tool_call_id: str) -> bool:
780 | """Cancel a tool execution if possible.
781 |
782 | Args:
783 | tool_call_id: ID of the tool call to cancel
784 |
785 | Returns:
786 | True if canceled successfully, False otherwise
787 | """
788 | # TODO: Implement cancellation logic
789 | # This would require more sophisticated execution tracking
790 | logger.warning(f"Cancellation not implemented for tool_call_id: {tool_call_id}")
791 | return False
```
--------------------------------------------------------------------------------
/claude_code/claude.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # claude.py
3 | """Claude Code Python Edition - CLI entry point."""
4 |
5 | import os
6 | import sys
7 | import logging
8 | import argparse
9 | from typing import Dict, List, Optional, Any
10 | import json
11 | import signal
12 | from datetime import datetime
13 |
14 | import typer
15 | from rich.console import Console
16 | from rich.panel import Panel
17 | from rich.markdown import Markdown
18 | from rich.prompt import Prompt
19 | from rich.syntax import Syntax
20 | from rich.logging import RichHandler
21 | from dotenv import load_dotenv
22 |
23 | from claude_code.lib.providers import get_provider, list_available_providers
24 | from claude_code.lib.tools.base import ToolRegistry
25 | from claude_code.lib.tools.manager import ToolExecutionManager
26 | from claude_code.lib.tools.file_tools import register_file_tools
27 | from claude_code.lib.ui.tool_visualizer import ToolCallVisualizer, MultiPanelLayout
28 | from claude_code.lib.monitoring.cost_tracker import CostTracker
29 |
30 | # Configure logging
31 | logging.basicConfig(
32 | level=logging.INFO,
33 | format="%(message)s",
34 | datefmt="[%X]",
35 | handlers=[RichHandler(rich_tracebacks=True)]
36 | )
37 | logger = logging.getLogger("claude_code")
38 |
39 | # Load environment variables
40 | load_dotenv()
41 |
42 | # Get version from package
43 | VERSION = "0.1.0"
44 |
45 | # Create typer app
46 | app = typer.Typer(help="Claude Code Python Edition")
47 | console = Console()
48 |
49 | # Global state
50 | conversation: List[Dict[str, Any]] = []
51 | tool_registry = ToolRegistry()
52 | tool_manager: Optional[ToolExecutionManager] = None
53 | cost_tracker: Optional[CostTracker] = None
54 | visualizer: Optional[ToolCallVisualizer] = None
55 | provider_name: str = ""
56 | model_name: str = ""
57 | user_config: Dict[str, Any] = {}
58 |
59 |
60 | def initialize_tools() -> None:
61 | """Initialize all available tools."""
62 | global tool_registry, tool_manager
63 |
64 | # Create the registry and manager
65 | tool_registry = ToolRegistry()
66 | tool_manager = ToolExecutionManager(tool_registry)
67 |
68 | # Register file tools
69 | register_file_tools(tool_registry)
70 |
71 | # TODO: Register more tools
72 | # register_search_tools(tool_registry)
73 | # register_bash_tools(tool_registry)
74 | # register_agent_tools(tool_registry)
75 |
76 | logger.info(f"Initialized {len(tool_registry.get_all_tools())} tools")
77 |
78 |
79 | def setup_visualizer() -> None:
80 | """Set up the tool visualizer with callbacks."""
81 | global tool_manager, visualizer
82 |
83 | if not tool_manager:
84 | return
85 |
86 | # Create visualizer
87 | visualizer = ToolCallVisualizer(console)
88 |
89 | # Set up callbacks
90 | def progress_callback(tool_call_id: str, progress: float) -> None:
91 | if visualizer:
92 | visualizer.update_progress(tool_call_id, progress)
93 |
94 | def result_callback(tool_call_id: str, result: Any) -> None:
95 | if visualizer:
96 | visualizer.complete_tool_call(tool_call_id, result)
97 |
98 | tool_manager.set_progress_callback(progress_callback)
99 | tool_manager.set_result_callback(result_callback)
100 |
101 |
102 | def load_configuration() -> Dict[str, Any]:
103 | """Load user configuration from file."""
104 | config_path = os.path.expanduser("~/.config/claude_code/config.json")
105 |
106 | # Default configuration
107 | default_config = {
108 | "provider": "openai",
109 | "model": None, # Use provider default
110 | "budget_limit": None,
111 | "history_file": os.path.expanduser("~/.config/claude_code/usage_history.json"),
112 | "ui": {
113 | "theme": "dark",
114 | "show_tool_calls": True,
115 | "show_cost": True
116 | }
117 | }
118 |
119 | # If configuration file doesn't exist, create it with defaults
120 | if not os.path.exists(config_path):
121 | try:
122 | os.makedirs(os.path.dirname(config_path), exist_ok=True)
123 | with open(config_path, 'w', encoding='utf-8') as f:
124 | json.dump(default_config, f, indent=2)
125 | except Exception as e:
126 | logger.warning(f"Failed to create default configuration: {e}")
127 | return default_config
128 |
129 | # Load configuration
130 | try:
131 | with open(config_path, 'r', encoding='utf-8') as f:
132 | config = json.load(f)
133 | # Merge with defaults for any missing keys
134 | for key, value in default_config.items():
135 | if key not in config:
136 | config[key] = value
137 | return config
138 | except Exception as e:
139 | logger.warning(f"Failed to load configuration: {e}")
140 | return default_config
141 |
142 |
143 | def handle_compact_command() -> str:
144 | """Handle the /compact command to compress conversation history."""
145 | global conversation, provider_name, model_name
146 |
147 | if not conversation:
148 | return "No conversation to compact."
149 |
150 | # Add a system message requesting summarization
151 | compact_prompt = (
152 | "Summarize the conversation so far, focusing on the key points, decisions, and context. "
153 | "Keep important details about the code and tasks. Retain critical file paths, commands, "
154 | "and code snippets. The summary should be concise but complete enough to continue the "
155 | "conversation effectively."
156 | )
157 |
158 | conversation.append({"role": "user", "content": compact_prompt})
159 |
160 | # Get the provider
161 | provider = get_provider(provider_name, model=model_name)
162 |
163 | # Make non-streaming API call for compaction
164 | response = provider.generate_completion(conversation, stream=False)
165 |
166 | # Extract summary
167 | summary = response["content"] or ""
168 |
169 | # Reset conversation with summary
170 | system_message = next((m for m in conversation if m["role"] == "system"), None)
171 |
172 | if system_message:
173 | conversation = [system_message]
174 | else:
175 | conversation = []
176 |
177 | # Add compacted context
178 | conversation.append({
179 | "role": "system",
180 | "content": f"This is a compacted conversation. Previous context: {summary}"
181 | })
182 |
183 | return "Conversation compacted successfully."
184 |
185 |
186 | def handle_help_command() -> str:
187 | """Handle the /help command."""
188 | help_text = """
189 | # Claude Code Python Edition Help
190 |
191 | ## Commands
192 | - **/help**: Show this help message
193 | - **/compact**: Compact the conversation to reduce token usage
194 | - **/version**: Show version information
195 | - **/providers**: List available LLM providers
196 | - **/cost**: Show cost and usage information
197 | - **/budget [amount]**: Set a budget limit (e.g., /budget 5.00)
198 | - **/quit, /exit**: Exit the application
199 |
200 | ## Routine Commands
201 | - **/routine list**: List all available routines
202 | - **/routine create <name> <description>**: Create a routine from recent tool executions
203 | - **/routine run <name>**: Run a routine
204 | - **/routine delete <name>**: Delete a routine
205 |
206 | ## Tools
207 | Claude Code has access to these tools:
208 | - **View**: Read files
209 | - **Edit**: Edit files (replace text)
210 | - **Replace**: Overwrite or create files
211 | - **GlobTool**: Find files by pattern
212 | - **GrepTool**: Search file contents
213 | - **LS**: List directory contents
214 | - **Bash**: Execute shell commands
215 |
216 | ## CLI Commands
217 | - **claude**: Start the Claude Code assistant (main interface)
218 | - **claude mcp-client**: Start the MCP client to connect to MCP servers
219 | - Usage: `claude mcp-client path/to/server.py [--model MODEL]`
220 | - **claude mcp-multi-agent**: Start the multi-agent MCP client with synchronized agents
221 | - Usage: `claude mcp-multi-agent path/to/server.py [--config CONFIG_FILE]`
222 |
223 | ## Multi-Agent Commands
224 | When using the multi-agent client:
225 | - **/agents**: List all active agents
226 | - **/talk <agent> <message>**: Send a direct message to a specific agent
227 | - **/history**: Show message history
228 | - **/help**: Show multi-agent help
229 |
230 | ## Tips
231 | - Be specific about file paths when requesting file operations
232 | - For complex tasks, break them down into smaller steps
233 | - Use /compact periodically for long sessions to save tokens
234 | - Create routines for repetitive sequences of tool operations
235 | - In multi-agent mode, use agent specialization for complex problems
236 | """
237 | return help_text
238 |
239 |
240 | def handle_version_command() -> str:
241 | """Handle the /version command."""
242 | import platform
243 | python_version = platform.python_version()
244 |
245 | version_info = f"""
246 | # Claude Code Python Edition v{VERSION}
247 |
248 | - Python: {python_version}
249 | - Provider: {provider_name}
250 | - Model: {model_name}
251 | - Tools: {len(tool_registry.get_all_tools()) if tool_registry else 0} available
252 | """
253 | return version_info
254 |
255 |
256 | def handle_providers_command() -> str:
257 | """Handle the /providers command."""
258 | providers = list_available_providers()
259 |
260 | providers_text = "# Available LLM Providers\n\n"
261 |
262 | for name, info in providers.items():
263 | providers_text += f"## {info['name']}\n"
264 |
265 | if info['available']:
266 | providers_text += f"- Status: Available\n"
267 | providers_text += f"- Current model: {info['current_model']}\n"
268 | providers_text += f"- Available models: {', '.join(info['models'])}\n"
269 | else:
270 | providers_text += f"- Status: Not available ({info['error']})\n"
271 |
272 | providers_text += "\n"
273 |
274 | return providers_text
275 |
276 |
277 | def handle_cost_command() -> str:
278 | """Handle the /cost command."""
279 | global cost_tracker
280 |
281 | if not cost_tracker:
282 | return "Cost tracking is not available."
283 |
284 | # Generate a usage report
285 | return cost_tracker.generate_usage_report(format="markdown")
286 |
287 |
288 | def handle_budget_command(args: List[str]) -> str:
289 | """Handle the /budget command."""
290 | global cost_tracker
291 |
292 | if not cost_tracker:
293 | return "Cost tracking is not available."
294 |
295 | if not args:
296 | # Show current budget
297 | budget = cost_tracker.check_budget()
298 | if not budget["has_budget"]:
299 | return "No budget limit is currently set."
300 |
301 | return f"Current budget: ${budget['limit']:.2f} (${budget['used']:.2f} used, ${budget['remaining']:.2f} remaining)"
302 |
303 | # Set new budget
304 | try:
305 | budget_amount = float(args[0])
306 | if budget_amount <= 0:
307 | return "Budget must be a positive number."
308 |
309 | cost_tracker.budget_limit = budget_amount
310 |
311 | # Update configuration
312 | user_config["budget_limit"] = budget_amount
313 |
314 | # Save configuration
315 | config_path = os.path.expanduser("~/.config/claude_code/config.json")
316 | try:
317 | with open(config_path, 'w', encoding='utf-8') as f:
318 | json.dump(user_config, f, indent=2)
319 | except Exception as e:
320 | logger.warning(f"Failed to save configuration: {e}")
321 |
322 | return f"Budget set to ${budget_amount:.2f}"
323 |
324 | except ValueError:
325 | return f"Invalid budget amount: {args[0]}"
326 |
327 |
328 | def handle_routine_list_command() -> str:
329 | """Handle the /routine list command."""
330 | global tool_manager
331 |
332 | if not tool_manager:
333 | return "Tool manager is not initialized."
334 |
335 | routines = tool_manager.registry.get_all_routines()
336 | if not routines:
337 | return "No routines available."
338 |
339 | routines_text = "# Available Routines\n\n"
340 |
341 | for routine in routines:
342 | usage = f" (Used {routine.usage_count} times)" if routine.usage_count > 0 else ""
343 | last_used = ""
344 | if routine.last_used_at:
345 | last_used_time = datetime.fromtimestamp(routine.last_used_at)
346 | last_used = f" (Last used: {last_used_time.strftime('%Y-%m-%d %H:%M')})"
347 |
348 | routines_text += f"## {routine.name}{usage}{last_used}\n"
349 | routines_text += f"{routine.description}\n\n"
350 | routines_text += f"**Steps:** {len(routine.steps)}\n\n"
351 |
352 | return routines_text
353 |
354 |
355 | def handle_routine_create_command(args: List[str]) -> str:
356 | """Handle the /routine create command."""
357 | global tool_manager, visualizer
358 |
359 | if not tool_manager:
360 | return "Tool manager is not initialized."
361 |
362 | if len(args) < 2:
363 | return "Usage: /routine create <name> <description>"
364 |
365 | name = args[0]
366 | description = " ".join(args[1:])
367 |
368 | # Get recent tool results from visualizer
369 | if not visualizer or not hasattr(visualizer, "recent_tool_results"):
370 | return "No recent tool executions to create a routine from."
371 |
372 | recent_tool_results = visualizer.recent_tool_results
373 | if not recent_tool_results:
374 | return "No recent tool executions to create a routine from."
375 |
376 | try:
377 | routine_id = tool_manager.create_routine_from_tool_history(
378 | name, description, recent_tool_results
379 | )
380 | return f"Created routine '{name}' with {len(recent_tool_results)} steps."
381 | except Exception as e:
382 | logger.exception(f"Error creating routine: {e}")
383 | return f"Error creating routine: {str(e)}"
384 |
385 |
386 | def handle_routine_run_command(args: List[str]) -> str:
387 | """Handle the /routine run command."""
388 | global tool_manager, visualizer
389 |
390 | if not tool_manager:
391 | return "Tool manager is not initialized."
392 |
393 | if not args:
394 | return "Usage: /routine run <name>"
395 |
396 | name = args[0]
397 |
398 | # Check if routine exists
399 | routine = tool_manager.registry.get_routine(name)
400 | if not routine:
401 | return f"Routine '{name}' not found."
402 |
403 | try:
404 | # Execute the routine
405 | execution_id = tool_manager.execute_routine(name)
406 |
407 | # Wait for completion
408 | while True:
409 | routine_status = tool_manager.routine_manager.get_active_routines().get(execution_id, {})
410 | if routine_status.get("status") != "running":
411 | break
412 | time.sleep(0.1)
413 |
414 | # Get results
415 | results = tool_manager.get_routine_results(execution_id)
416 | if not results:
417 | return f"Routine '{name}' completed but returned no results."
418 |
419 | # Format results
420 | result_text = f"# Routine '{name}' Results\n\n"
421 | result_text += f"Executed {len(results)} steps:\n\n"
422 |
423 | for i, result in enumerate(results):
424 | status = "✅" if result.status == "success" else "❌"
425 | result_text += f"## Step {i+1}: {result.name} {status}\n"
426 | result_text += f"```\n{result.result}\n```\n\n"
427 |
428 | return result_text
429 |
430 | except Exception as e:
431 | logger.exception(f"Error executing routine: {e}")
432 | return f"Error executing routine: {str(e)}"
433 |
434 |
435 | def handle_routine_delete_command(args: List[str]) -> str:
436 | """Handle the /routine delete command."""
437 | global tool_manager
438 |
439 | if not tool_manager:
440 | return "Tool manager is not initialized."
441 |
442 | if not args:
443 | return "Usage: /routine delete <name>"
444 |
445 | name = args[0]
446 |
447 | # Check if routine exists
448 | routine = tool_manager.registry.get_routine(name)
449 | if not routine:
450 | return f"Routine '{name}' not found."
451 |
452 | try:
453 | # Remove from registry and save
454 | tool_manager.registry.routines.pop(name, None)
455 | tool_manager.registry._save_routines()
456 | return f"Deleted routine '{name}'."
457 | except Exception as e:
458 | logger.exception(f"Error deleting routine: {e}")
459 | return f"Error deleting routine: {str(e)}"
460 |
461 |
462 | def process_special_command(user_input: str) -> Optional[str]:
463 | """Process special commands starting with /."""
464 | # Split into command and arguments
465 | parts = user_input.strip().split()
466 | command = parts[0].lower()
467 | args = parts[1:]
468 |
469 | # Handle commands
470 | if command == "/help":
471 | return handle_help_command()
472 | elif command == "/compact":
473 | return handle_compact_command()
474 | elif command == "/version":
475 | return handle_version_command()
476 | elif command == "/providers":
477 | return handle_providers_command()
478 | elif command == "/cost":
479 | return handle_cost_command()
480 | elif command == "/budget":
481 | return handle_budget_command(args)
482 | elif command in ["/quit", "/exit"]:
483 | console.print("[bold yellow]Goodbye![/bold yellow]")
484 | sys.exit(0)
485 |
486 | # Handle routine commands
487 | elif command == "/routine":
488 | if not args:
489 | return "Usage: /routine [list|create|run|delete]"
490 |
491 | subcmd = args[0].lower()
492 | if subcmd == "list":
493 | return handle_routine_list_command()
494 | elif subcmd == "create":
495 | return handle_routine_create_command(args[1:])
496 | elif subcmd == "run":
497 | return handle_routine_run_command(args[1:])
498 | elif subcmd == "delete":
499 | return handle_routine_delete_command(args[1:])
500 | else:
501 | return f"Unknown routine command: {subcmd}\nUsage: /routine [list|create|run|delete]"
502 |
503 | # Not a recognized command
504 | return None
505 |
506 |
507 | def process_tool_calls(tool_calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
508 | """Process tool calls and return results.
509 |
510 | Args:
511 | tool_calls: List of tool call dictionaries
512 |
513 | Returns:
514 | List of tool responses
515 | """
516 | global tool_manager, visualizer
517 |
518 | if not tool_manager:
519 | logger.error("Tool manager not initialized")
520 | return []
521 |
522 | # Add tool calls to visualizer
523 | if visualizer:
524 | for tool_call in tool_calls:
525 | function_name = tool_call.get("function", {}).get("name", "")
526 | tool_call_id = tool_call.get("id", "unknown")
527 | arguments_str = tool_call.get("function", {}).get("arguments", "{}")
528 |
529 | try:
530 | parameters = json.loads(arguments_str)
531 | visualizer.add_tool_call(tool_call_id, function_name, parameters)
532 | except json.JSONDecodeError:
533 | visualizer.add_tool_call(tool_call_id, function_name, {})
534 |
535 | # Execute tools in parallel
536 | tool_results = tool_manager.execute_tools_parallel(tool_calls)
537 |
538 | # Format results for the conversation
539 | tool_responses = []
540 | for result in tool_results:
541 | tool_responses.append({
542 | "tool_call_id": result.tool_call_id,
543 | "role": "tool",
544 | "name": result.name,
545 | "content": result.result
546 | })
547 |
548 | return tool_responses
549 |
550 |
551 | @app.command(name="mcp-client")
552 | def mcp_client(
553 | server_script: str = typer.Argument(..., help="Path to the server script (.py or .js)"),
554 | model: str = typer.Option("claude-3-5-sonnet-20241022", "--model", "-m", help="Claude model to use")
555 | ):
556 | """Run the MCP client to interact with an MCP server."""
557 | from claude_code.commands.client import execute as client_execute
558 | import argparse
559 |
560 | # Create a namespace with the arguments
561 | args = argparse.Namespace()
562 | args.server_script = server_script
563 | args.model = model
564 |
565 | # Execute the client
566 | return client_execute(args)
567 |
568 |
569 | @app.command(name="mcp-multi-agent")
570 | def mcp_multi_agent(
571 | server_script: str = typer.Argument(..., help="Path to the server script (.py or .js)"),
572 | config: str = typer.Option(None, "--config", "-c", help="Path to agent configuration JSON file")
573 | ):
574 | """Run the multi-agent MCP client with agent synchronization."""
575 | from claude_code.commands.multi_agent_client import execute as multi_agent_execute
576 | import argparse
577 |
578 | # Create a namespace with the arguments
579 | args = argparse.Namespace()
580 | args.server_script = server_script
581 | args.config = config
582 |
583 | # Execute the multi-agent client
584 | return multi_agent_execute(args)
585 |
586 |
587 | @app.command()
588 | def main(
589 | provider: str = typer.Option(None, "--provider", "-p", help="LLM provider to use"),
590 | model: str = typer.Option(None, "--model", "-m", help="Model to use"),
591 | budget: Optional[float] = typer.Option(None, "--budget", "-b", help="Budget limit in dollars"),
592 | system_prompt: Optional[str] = typer.Option(None, "--system", "-s", help="System prompt file"),
593 | verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output")
594 | ):
595 | """Claude Code Python Edition - A LLM-powered coding assistant."""
596 | global conversation, tool_registry, tool_manager, cost_tracker, visualizer
597 | global provider_name, model_name, user_config
598 |
599 | # Set logging level
600 | if verbose:
601 | logging.getLogger("claude_code").setLevel(logging.DEBUG)
602 |
603 | # Show welcome message
604 | console.print(Panel.fit(
605 | f"[bold green]Claude Code Python Edition v{VERSION}[/bold green]\n"
606 | "Type your questions or commands. Use /help for available commands.",
607 | title="Welcome",
608 | border_style="green"
609 | ))
610 |
611 | # Load configuration
612 | user_config = load_configuration()
613 |
614 | # Override with command line arguments
615 | if provider:
616 | user_config["provider"] = provider
617 | if model:
618 | user_config["model"] = model
619 | if budget is not None:
620 | user_config["budget_limit"] = budget
621 |
622 | # Set provider and model
623 | provider_name = user_config["provider"]
624 | model_name = user_config["model"]
625 |
626 | try:
627 | # Initialize tools
628 | initialize_tools()
629 |
630 | # Set up cost tracking
631 | cost_tracker = CostTracker(
632 | budget_limit=user_config["budget_limit"],
633 | history_file=user_config["history_file"]
634 | )
635 |
636 | # Get provider
637 | provider = get_provider(provider_name, model=model_name)
638 | provider_name = provider.name
639 | model_name = provider.current_model
640 |
641 | logger.info(f"Using {provider_name} with model {model_name}")
642 |
643 | # Set up tool visualizer
644 | setup_visualizer()
645 | if visualizer:
646 | visualizer.start()
647 |
648 | # Load system prompt
649 | system_message = ""
650 | if system_prompt:
651 | try:
652 | with open(system_prompt, 'r', encoding='utf-8') as f:
653 | system_message = f.read()
654 | except Exception as e:
655 | logger.error(f"Failed to load system prompt: {e}")
656 | system_message = get_default_system_prompt()
657 | else:
658 | system_message = get_default_system_prompt()
659 |
660 | # Initialize conversation
661 | conversation = [{"role": "system", "content": system_message}]
662 |
663 | # Main interaction loop
664 | while True:
665 | try:
666 | # Get user input
667 | user_input = Prompt.ask("\n[bold blue]>>[/bold blue]")
668 |
669 | # Handle special commands
670 | if user_input.startswith("/"):
671 | result = process_special_command(user_input)
672 | if result:
673 | console.print(Markdown(result))
674 | continue
675 |
676 | # Add user message to conversation
677 | conversation.append({"role": "user", "content": user_input})
678 |
679 | # Get schemas for all tools
680 | tool_schemas = tool_registry.get_tool_schemas() if tool_registry else None
681 |
682 | # Call the LLM
683 | with console.status("[bold blue]Thinking...[/bold blue]", spinner="dots"):
684 | # Stream the response
685 | response_stream = provider.generate_completion(
686 | messages=conversation,
687 | tools=tool_schemas,
688 | stream=True
689 | )
690 |
691 | # Track tool calls from streaming response
692 | current_content = ""
693 | current_tool_calls = []
694 |
695 | # Process streaming response
696 | for chunk in response_stream:
697 | # If there's content, print it
698 | if chunk.get("content"):
699 | content_piece = chunk["content"]
700 | current_content += content_piece
701 | console.print(content_piece, end="")
702 |
703 | # Process tool calls
704 | if chunk.get("tool_calls") and not chunk.get("delta", True):
705 | # This is a complete tool call
706 | current_tool_calls = chunk["tool_calls"]
707 | break
708 |
709 | console.print() # Add newline after content
710 |
711 | # Add assistant response to conversation
712 | conversation.append({
713 | "role": "assistant",
714 | "content": current_content,
715 | "tool_calls": current_tool_calls
716 | })
717 |
718 | # Process tool calls if any
719 | if current_tool_calls:
720 | console.print("[bold green]Executing tools...[/bold green]")
721 |
722 | # Process tool calls
723 | tool_responses = process_tool_calls(current_tool_calls)
724 |
725 | # Add tool responses to conversation
726 | conversation.extend(tool_responses)
727 |
728 | # Continue the conversation with tool responses
729 | console.print("[bold blue]Continuing with tool results...[/bold blue]")
730 |
731 | follow_up = provider.generate_completion(
732 | messages=conversation,
733 | tools=tool_schemas,
734 | stream=False
735 | )
736 |
737 | follow_up_text = follow_up.get("content", "")
738 | if follow_up_text:
739 | console.print(Markdown(follow_up_text))
740 |
741 | # Add to conversation
742 | conversation.append({
743 | "role": "assistant",
744 | "content": follow_up_text
745 | })
746 |
747 | # Track token usage and cost
748 | if cost_tracker:
749 | # Get token counts - this is an approximation
750 | token_counts = provider.count_message_tokens(conversation[-3:])
751 | cost_info = provider.cost_per_1k_tokens
752 |
753 | # Add request to tracker
754 | cost_tracker.add_request(
755 | provider=provider_name,
756 | model=model_name,
757 | tokens_input=token_counts["input"],
758 | tokens_output=token_counts.get("output", 0) or 150, # Estimate if not available
759 | input_cost_per_1k=cost_info["input"],
760 | output_cost_per_1k=cost_info["output"]
761 | )
762 |
763 | # Check budget
764 | budget_status = cost_tracker.check_budget()
765 | if budget_status["has_budget"] and budget_status["status"] in ["critical", "exceeded"]:
766 | console.print(f"[bold red]{budget_status['message']}[/bold red]")
767 |
768 | except KeyboardInterrupt:
769 | console.print("\n[bold yellow]Operation cancelled by user.[/bold yellow]")
770 | continue
771 | except Exception as e:
772 | logger.exception(f"Error: {str(e)}")
773 | console.print(f"[bold red]Error:[/bold red] {str(e)}")
774 |
775 | finally:
776 | # Clean up
777 | if visualizer:
778 | visualizer.stop()
779 |
780 | # Save cost history
781 | if cost_tracker and hasattr(cost_tracker, '_save_history'):
782 | cost_tracker._save_history()
783 |
784 |
785 | def get_default_system_prompt() -> str:
786 | """Get the default system prompt."""
787 | return """You are Claude Code Python Edition, a CLI tool that helps users with software engineering tasks.
788 | Use the available tools to assist the user with their requests.
789 |
790 | # Tone and style
791 | You should be concise, direct, and to the point. When you run a non-trivial bash command,
792 | you should explain what the command does and why you are running it.
793 | Output text to communicate with the user; all text you output outside of tool use is displayed to the user.
794 | Remember that your output will be displayed on a command line interface.
795 |
796 | # Tool usage policy
797 | - When doing file search, remember to search effectively with the available tools.
798 | - Always use the appropriate tool for the task.
799 | - Use parallel tool calls when appropriate to improve performance.
800 | - NEVER commit changes unless the user explicitly asks you to.
801 |
802 | # Routines
803 | You have access to Routines, which are sequences of tool calls that can be created and reused.
804 | To create a routine from recent tool executions, use `/routine create <name> <description>`.
805 | To run a routine, use `/routine run <name>`.
806 | Routines are ideal for repetitive task sequences like:
807 | - Deep research across multiple sources
808 | - Multi-step code updates across files
809 | - Complex search and replace operations
810 | - Data processing pipelines
811 |
812 | # Tasks
813 | The user will primarily request you perform software engineering tasks:
814 | 1. Solving bugs
815 | 2. Adding new functionality
816 | 3. Refactoring code
817 | 4. Explaining code
818 | 5. Writing tests
819 |
820 | For these tasks:
821 | 1. Use search tools to understand the codebase
822 | 2. Implement solutions using the available tools
823 | 3. Verify solutions with tests if possible
824 | 4. Run lint and typecheck commands when appropriate
825 | 5. Consider creating routines for repetitive operations
826 |
827 | # Code style
828 | - Follow the existing code style of the project
829 | - Maintain consistent naming conventions
830 | - Use appropriate libraries that are already in the project
831 | - Add comments when code is complex or non-obvious
832 |
833 | IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness,
834 | quality, and accuracy. Answer concisely with short lines of text unless the user asks for detail.
835 | """
836 |
837 |
838 | if __name__ == "__main__":
839 | # Handle Ctrl+C gracefully
840 | signal.signal(signal.SIGINT, lambda sig, frame: sys.exit(0))
841 |
842 | # Run app
843 | app()
```