ctrlcode 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. ctrlcode/__init__.py +8 -0
  2. ctrlcode/agents/__init__.py +29 -0
  3. ctrlcode/agents/cleanup.py +388 -0
  4. ctrlcode/agents/communication.py +439 -0
  5. ctrlcode/agents/observability.py +421 -0
  6. ctrlcode/agents/react_loop.py +297 -0
  7. ctrlcode/agents/registry.py +211 -0
  8. ctrlcode/agents/result_parser.py +242 -0
  9. ctrlcode/agents/workflow.py +723 -0
  10. ctrlcode/analysis/__init__.py +28 -0
  11. ctrlcode/analysis/ast_diff.py +163 -0
  12. ctrlcode/analysis/bug_detector.py +149 -0
  13. ctrlcode/analysis/code_graphs.py +329 -0
  14. ctrlcode/analysis/semantic.py +205 -0
  15. ctrlcode/analysis/static.py +183 -0
  16. ctrlcode/analysis/synthesizer.py +281 -0
  17. ctrlcode/analysis/tests.py +189 -0
  18. ctrlcode/cleanup/__init__.py +16 -0
  19. ctrlcode/cleanup/auto_merge.py +350 -0
  20. ctrlcode/cleanup/doc_gardening.py +388 -0
  21. ctrlcode/cleanup/pr_automation.py +330 -0
  22. ctrlcode/cleanup/scheduler.py +356 -0
  23. ctrlcode/config.py +380 -0
  24. ctrlcode/embeddings/__init__.py +6 -0
  25. ctrlcode/embeddings/embedder.py +192 -0
  26. ctrlcode/embeddings/vector_store.py +213 -0
  27. ctrlcode/fuzzing/__init__.py +24 -0
  28. ctrlcode/fuzzing/analyzer.py +280 -0
  29. ctrlcode/fuzzing/budget.py +112 -0
  30. ctrlcode/fuzzing/context.py +665 -0
  31. ctrlcode/fuzzing/context_fuzzer.py +506 -0
  32. ctrlcode/fuzzing/derived_orchestrator.py +732 -0
  33. ctrlcode/fuzzing/oracle_adapter.py +135 -0
  34. ctrlcode/linters/__init__.py +11 -0
  35. ctrlcode/linters/hand_rolled_utils.py +221 -0
  36. ctrlcode/linters/yolo_parsing.py +217 -0
  37. ctrlcode/metrics/__init__.py +6 -0
  38. ctrlcode/metrics/dashboard.py +283 -0
  39. ctrlcode/metrics/tech_debt.py +663 -0
  40. ctrlcode/paths.py +68 -0
  41. ctrlcode/permissions.py +179 -0
  42. ctrlcode/providers/__init__.py +15 -0
  43. ctrlcode/providers/anthropic.py +138 -0
  44. ctrlcode/providers/base.py +77 -0
  45. ctrlcode/providers/openai.py +197 -0
  46. ctrlcode/providers/parallel.py +104 -0
  47. ctrlcode/server.py +871 -0
  48. ctrlcode/session/__init__.py +6 -0
  49. ctrlcode/session/baseline.py +57 -0
  50. ctrlcode/session/manager.py +967 -0
  51. ctrlcode/skills/__init__.py +10 -0
  52. ctrlcode/skills/builtin/commit.toml +29 -0
  53. ctrlcode/skills/builtin/docs.toml +25 -0
  54. ctrlcode/skills/builtin/refactor.toml +33 -0
  55. ctrlcode/skills/builtin/review.toml +28 -0
  56. ctrlcode/skills/builtin/test.toml +28 -0
  57. ctrlcode/skills/loader.py +111 -0
  58. ctrlcode/skills/registry.py +139 -0
  59. ctrlcode/storage/__init__.py +19 -0
  60. ctrlcode/storage/history_db.py +708 -0
  61. ctrlcode/tools/__init__.py +220 -0
  62. ctrlcode/tools/bash.py +112 -0
  63. ctrlcode/tools/browser.py +352 -0
  64. ctrlcode/tools/executor.py +153 -0
  65. ctrlcode/tools/explore.py +486 -0
  66. ctrlcode/tools/mcp.py +108 -0
  67. ctrlcode/tools/observability.py +561 -0
  68. ctrlcode/tools/registry.py +193 -0
  69. ctrlcode/tools/todo.py +291 -0
  70. ctrlcode/tools/update.py +266 -0
  71. ctrlcode/tools/webfetch.py +147 -0
  72. ctrlcode-0.1.0.dist-info/METADATA +93 -0
  73. ctrlcode-0.1.0.dist-info/RECORD +75 -0
  74. ctrlcode-0.1.0.dist-info/WHEEL +4 -0
  75. ctrlcode-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,193 @@
1
+ """Tool registry for managing MCP servers and tools."""
2
+
3
+ from typing import Any, Optional, Callable
4
+ import logging
5
+ from dataclasses import dataclass
6
+
7
+ from .mcp import MCPClient, MCPTool
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @dataclass
13
+ class BuiltinTool:
14
+ """Built-in tool definition."""
15
+
16
+ name: str
17
+ description: str
18
+ input_schema: dict[str, Any]
19
+ function: Callable
20
+
21
+
22
+ class ToolRegistry:
23
+ """Registry for MCP and built-in tools."""
24
+
25
+ def __init__(self):
26
+ """Initialize tool registry."""
27
+ self.clients: dict[str, MCPClient] = {}
28
+ self.mcp_tools: dict[str, MCPTool] = {} # tool_name -> MCPTool
29
+ self.builtin_tools: dict[str, BuiltinTool] = {} # tool_name -> BuiltinTool
30
+
31
+ async def add_server(self, server_config: dict[str, Any]) -> None:
32
+ """
33
+ Add and start an MCP server.
34
+
35
+ Args:
36
+ server_config: Server configuration
37
+ """
38
+ server_name = server_config.get("name", "unknown")
39
+
40
+ try:
41
+ client = MCPClient(server_config)
42
+ await client.start()
43
+
44
+ # List tools
45
+ tools = await client.list_tools()
46
+ logger.info(f"Server '{server_name}' provides {len(tools)} tools")
47
+
48
+ # Register tools
49
+ for tool in tools:
50
+ if tool.name in self.mcp_tools or tool.name in self.builtin_tools:
51
+ logger.warning(f"Tool '{tool.name}' already registered, skipping")
52
+ else:
53
+ self.mcp_tools[tool.name] = tool
54
+
55
+ # Store client
56
+ self.clients[server_name] = client
57
+
58
+ except Exception as e:
59
+ logger.error(f"Failed to start server '{server_name}': {e}")
60
+
61
+ def register_builtin(
62
+ self,
63
+ name: str,
64
+ description: str,
65
+ input_schema: dict[str, Any],
66
+ function: Callable,
67
+ ) -> None:
68
+ """
69
+ Register a built-in tool.
70
+
71
+ Args:
72
+ name: Tool name
73
+ description: Tool description
74
+ input_schema: JSON schema for tool inputs
75
+ function: Callable that implements the tool
76
+ """
77
+ if name in self.mcp_tools or name in self.builtin_tools:
78
+ logger.warning(f"Tool '{name}' already registered, skipping")
79
+ return
80
+
81
+ tool = BuiltinTool(
82
+ name=name,
83
+ description=description,
84
+ input_schema=input_schema,
85
+ function=function,
86
+ )
87
+
88
+ self.builtin_tools[name] = tool
89
+ logger.info(f"Registered built-in tool: {name}")
90
+
91
+ async def remove_server(self, server_name: str) -> None:
92
+ """
93
+ Remove and stop an MCP server.
94
+
95
+ Args:
96
+ server_name: Name of server to remove
97
+ """
98
+ if server_name in self.clients:
99
+ client = self.clients[server_name]
100
+ await client.stop()
101
+ del self.clients[server_name]
102
+
103
+ # Remove tools from this server
104
+ self.mcp_tools = {
105
+ name: tool
106
+ for name, tool in self.mcp_tools.items()
107
+ if tool.server_name != server_name
108
+ }
109
+
110
+ def get_tool(self, tool_name: str) -> Optional[MCPTool | BuiltinTool]:
111
+ """
112
+ Get tool by name (MCP or built-in).
113
+
114
+ Args:
115
+ tool_name: Name of tool
116
+
117
+ Returns:
118
+ Tool or None if not found
119
+ """
120
+ return self.mcp_tools.get(tool_name) or self.builtin_tools.get(tool_name)
121
+
122
+ def is_builtin(self, tool_name: str) -> bool:
123
+ """Check if tool is built-in."""
124
+ return tool_name in self.builtin_tools
125
+
126
+ def list_tools(self) -> list[MCPTool | BuiltinTool]:
127
+ """
128
+ List all available tools (MCP and built-in).
129
+
130
+ Returns:
131
+ List of all registered tools
132
+ """
133
+ return list(self.mcp_tools.values()) + list(self.builtin_tools.values())
134
+
135
+ def get_client(self, server_name: str) -> Optional[MCPClient]:
136
+ """
137
+ Get MCP client by server name.
138
+
139
+ Args:
140
+ server_name: Server name
141
+
142
+ Returns:
143
+ Client or None if not found
144
+ """
145
+ return self.clients.get(server_name)
146
+
147
+ def get_tool_definitions(self) -> list[dict[str, Any]]:
148
+ """
149
+ Get tool definitions in provider format (both MCP and built-in).
150
+
151
+ Returns:
152
+ List of tool definitions for LLM providers
153
+ """
154
+ definitions = []
155
+
156
+ # MCP tools
157
+ for tool in self.mcp_tools.values():
158
+ definitions.append({
159
+ "name": tool.name,
160
+ "description": tool.description,
161
+ "input_schema": tool.input_schema,
162
+ })
163
+
164
+ # Built-in tools
165
+ for tool in self.builtin_tools.values():
166
+ definitions.append({
167
+ "name": tool.name,
168
+ "description": tool.description,
169
+ "input_schema": tool.input_schema,
170
+ })
171
+
172
+ return definitions
173
+
174
+ def get_tool_definitions_filtered(self, allowed_tools: list[str]) -> list[dict[str, Any]]:
175
+ """
176
+ Get tool definitions filtered by allowed tool names.
177
+
178
+ Args:
179
+ allowed_tools: List of tool names to include
180
+
181
+ Returns:
182
+ Filtered list of tool definitions
183
+ """
184
+ all_tools = self.get_tool_definitions()
185
+ return [tool for tool in all_tools if tool["name"] in allowed_tools]
186
+
187
+ async def close_all(self) -> None:
188
+ """Close all MCP clients."""
189
+ for client in self.clients.values():
190
+ await client.stop()
191
+ self.clients.clear()
192
+ self.mcp_tools.clear()
193
+ self.builtin_tools.clear()
ctrlcode/tools/todo.py ADDED
@@ -0,0 +1,291 @@
1
+ """Built-in todo list tools for task management."""
2
+
3
+ import json
4
+ import fcntl
5
+ import uuid
6
+ from pathlib import Path
7
+ from time import time
8
+ from typing import Any
9
+
10
+
11
+ class TodoTools:
12
+ """Built-in tools for managing todo lists."""
13
+
14
+ def __init__(self, data_dir: str | Path):
15
+ """
16
+ Initialize todo tools.
17
+
18
+ Args:
19
+ data_dir: Data directory for storing todos
20
+ """
21
+ self.data_dir = Path(data_dir)
22
+ self.todos_dir = self.data_dir / "todos"
23
+ self.todos_dir.mkdir(parents=True, exist_ok=True)
24
+
25
+ def _get_todo_file(self, session_id: str | None) -> Path:
26
+ """Get path to todo file for session."""
27
+ if session_id:
28
+ return self.todos_dir / f"{session_id}.json"
29
+ return self.todos_dir / "global.json"
30
+
31
+ def _read_todos(self, file_path: Path) -> list[dict]:
32
+ """Read todos from file with locking."""
33
+ if not file_path.exists():
34
+ return []
35
+
36
+ try:
37
+ with open(file_path, "r") as f:
38
+ fcntl.flock(f.fileno(), fcntl.LOCK_SH)
39
+ try:
40
+ data = json.load(f)
41
+ return data if isinstance(data, list) else []
42
+ finally:
43
+ fcntl.flock(f.fileno(), fcntl.LOCK_UN)
44
+ except (json.JSONDecodeError, IOError):
45
+ return []
46
+
47
+ def _write_todos(self, file_path: Path, todos: list[dict]) -> None:
48
+ """Write todos to file atomically with locking."""
49
+ temp_path = file_path.with_suffix(".tmp")
50
+
51
+ try:
52
+ # Write to temp file with exclusive lock
53
+ with open(temp_path, "w") as f:
54
+ fcntl.flock(f.fileno(), fcntl.LOCK_EX)
55
+ try:
56
+ json.dump(todos, f, indent=2)
57
+ f.flush()
58
+ finally:
59
+ fcntl.flock(f.fileno(), fcntl.LOCK_UN)
60
+
61
+ # Atomic rename
62
+ temp_path.replace(file_path)
63
+ except Exception as e:
64
+ if temp_path.exists():
65
+ temp_path.unlink()
66
+ raise e
67
+
68
+ def todo_write(
69
+ self, items: list[dict[str, Any]], session_id: str | None = None
70
+ ) -> dict[str, Any]:
71
+ """
72
+ Create or append todo items.
73
+
74
+ Args:
75
+ items: List of todo items with 'content' and optional 'activeForm', 'priority', 'status'
76
+ session_id: Optional session ID for session-specific todos
77
+
78
+ Returns:
79
+ Dict with success status and created todos
80
+ """
81
+ try:
82
+ file_path = self._get_todo_file(session_id)
83
+ todos = self._read_todos(file_path)
84
+ current_time = time()
85
+
86
+ created = []
87
+ for item in items:
88
+ if not item.get("content"):
89
+ continue
90
+
91
+ todo = {
92
+ "id": str(uuid.uuid4()),
93
+ "content": item["content"],
94
+ "status": item.get("status", "pending"),
95
+ "activeForm": item.get("activeForm", item["content"]),
96
+ "priority": item.get("priority", "medium"),
97
+ "created_at": current_time,
98
+ "updated_at": current_time,
99
+ }
100
+ todos.append(todo)
101
+ created.append(todo)
102
+
103
+ self._write_todos(file_path, todos)
104
+ return {"success": True, "todos": created, "error": None}
105
+
106
+ except Exception as e:
107
+ return {"success": False, "todos": [], "error": str(e)}
108
+
109
+ def todo_list(
110
+ self, session_id: str | None = None, status: str | None = None
111
+ ) -> dict[str, Any]:
112
+ """
113
+ List todo items with optional filtering.
114
+
115
+ Args:
116
+ session_id: Optional session ID for session-specific todos
117
+ status: Optional status filter (pending, in_progress, completed)
118
+
119
+ Returns:
120
+ Dict with success status and todos list
121
+ """
122
+ try:
123
+ file_path = self._get_todo_file(session_id)
124
+ todos = self._read_todos(file_path)
125
+
126
+ if status:
127
+ todos = [t for t in todos if t.get("status") == status]
128
+
129
+ return {"success": True, "todos": todos, "error": None}
130
+
131
+ except Exception as e:
132
+ return {"success": False, "todos": [], "error": str(e)}
133
+
134
+ def todo_update(
135
+ self,
136
+ id: str,
137
+ status: str | None = None,
138
+ content: str | None = None,
139
+ activeForm: str | None = None,
140
+ priority: str | None = None,
141
+ session_id: str | None = None,
142
+ ) -> dict[str, Any]:
143
+ """
144
+ Update an existing todo item.
145
+
146
+ Args:
147
+ id: UUID of the todo to update
148
+ status: New status (pending, in_progress, completed)
149
+ content: New content
150
+ activeForm: Present continuous form shown when in progress
151
+ priority: New priority (high, medium, low)
152
+ session_id: Optional session ID for session-specific todos
153
+
154
+ Returns:
155
+ Dict with success status and updated todo
156
+ """
157
+ try:
158
+ file_path = self._get_todo_file(session_id)
159
+ todos = self._read_todos(file_path)
160
+
161
+ todo_found = None
162
+ for todo in todos:
163
+ if todo.get("id") == id:
164
+ todo_found = todo
165
+ if status is not None:
166
+ todo["status"] = status
167
+ if content is not None:
168
+ todo["content"] = content
169
+ if activeForm is not None:
170
+ todo["activeForm"] = activeForm
171
+ if priority is not None:
172
+ todo["priority"] = priority
173
+ todo["updated_at"] = time()
174
+ break
175
+
176
+ if not todo_found:
177
+ return {
178
+ "success": False,
179
+ "todos": [],
180
+ "error": f"Todo with id '{id}' not found",
181
+ }
182
+
183
+ self._write_todos(file_path, todos)
184
+ return {"success": True, "todos": [todo_found], "error": None}
185
+
186
+ except Exception as e:
187
+ return {"success": False, "todos": [], "error": str(e)}
188
+
189
+
190
+ # Tool schemas for LLM providers (Anthropic/OpenAI format)
191
+ TODO_TOOL_SCHEMAS = [
192
+ {
193
+ "name": "todo_write",
194
+ "description": "Create or append todo items for task tracking. Use this when planning work or breaking down complex tasks.",
195
+ "input_schema": {
196
+ "type": "object",
197
+ "properties": {
198
+ "items": {
199
+ "type": "array",
200
+ "description": "List of todo items to create",
201
+ "items": {
202
+ "type": "object",
203
+ "properties": {
204
+ "content": {
205
+ "type": "string",
206
+ "description": "Task description (brief, actionable)",
207
+ },
208
+ "activeForm": {
209
+ "type": "string",
210
+ "description": "Present continuous form shown when in progress (e.g., 'Running tests')",
211
+ },
212
+ "priority": {
213
+ "type": "string",
214
+ "description": "Task priority",
215
+ "enum": ["high", "medium", "low"],
216
+ "default": "medium",
217
+ },
218
+ "status": {
219
+ "type": "string",
220
+ "description": "Initial status",
221
+ "enum": ["pending", "in_progress", "completed"],
222
+ "default": "pending",
223
+ },
224
+ },
225
+ "required": ["content"],
226
+ },
227
+ },
228
+ "session_id": {
229
+ "type": "string",
230
+ "description": "Optional session ID for session-specific todos",
231
+ },
232
+ },
233
+ "required": ["items"],
234
+ },
235
+ },
236
+ {
237
+ "name": "todo_list",
238
+ "description": "List todos with optional filtering by status. Use this to check what tasks are pending or track progress.",
239
+ "input_schema": {
240
+ "type": "object",
241
+ "properties": {
242
+ "session_id": {
243
+ "type": "string",
244
+ "description": "Optional session ID for session-specific todos",
245
+ },
246
+ "status": {
247
+ "type": "string",
248
+ "description": "Filter by status",
249
+ "enum": ["pending", "in_progress", "completed"],
250
+ },
251
+ },
252
+ "required": [],
253
+ },
254
+ },
255
+ {
256
+ "name": "todo_update",
257
+ "description": "Update an existing todo item. Use this to mark tasks as in progress or completed, or modify task details.",
258
+ "input_schema": {
259
+ "type": "object",
260
+ "properties": {
261
+ "id": {
262
+ "type": "string",
263
+ "description": "UUID of the todo to update",
264
+ },
265
+ "status": {
266
+ "type": "string",
267
+ "description": "New status",
268
+ "enum": ["pending", "in_progress", "completed"],
269
+ },
270
+ "content": {
271
+ "type": "string",
272
+ "description": "New task description",
273
+ },
274
+ "activeForm": {
275
+ "type": "string",
276
+ "description": "Present continuous form shown when in progress (e.g., 'Running tests')",
277
+ },
278
+ "priority": {
279
+ "type": "string",
280
+ "description": "New priority",
281
+ "enum": ["high", "medium", "low"],
282
+ },
283
+ "session_id": {
284
+ "type": "string",
285
+ "description": "Optional session ID for session-specific todos",
286
+ },
287
+ },
288
+ "required": ["id"],
289
+ },
290
+ },
291
+ ]