#
tokens: 42376/50000 7/56 files (page 2/6)
lines: on (toggle) GitHub
raw markdown copy reset
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()
```
Page 2/6FirstPrevNextLast