code-puppy 0.0.372__py3-none-any.whl → 0.0.374__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 (30) hide show
  1. code_puppy/agents/agent_creator_agent.py +49 -1
  2. code_puppy/agents/agent_helios.py +122 -0
  3. code_puppy/agents/agent_manager.py +26 -2
  4. code_puppy/agents/json_agent.py +30 -7
  5. code_puppy/claude_cache_client.py +9 -9
  6. code_puppy/command_line/colors_menu.py +2 -0
  7. code_puppy/command_line/command_handler.py +1 -0
  8. code_puppy/command_line/config_commands.py +3 -1
  9. code_puppy/command_line/uc_menu.py +890 -0
  10. code_puppy/config.py +29 -0
  11. code_puppy/messaging/messages.py +18 -0
  12. code_puppy/messaging/rich_renderer.py +35 -0
  13. code_puppy/messaging/subagent_console.py +0 -1
  14. code_puppy/plugins/claude_code_oauth/README.md +1 -1
  15. code_puppy/plugins/claude_code_oauth/SETUP.md +1 -1
  16. code_puppy/plugins/claude_code_oauth/utils.py +44 -13
  17. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  18. code_puppy/plugins/universal_constructor/models.py +138 -0
  19. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  20. code_puppy/plugins/universal_constructor/registry.py +304 -0
  21. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  22. code_puppy/tools/__init__.py +138 -1
  23. code_puppy/tools/universal_constructor.py +889 -0
  24. {code_puppy-0.0.372.dist-info → code_puppy-0.0.374.dist-info}/METADATA +1 -1
  25. {code_puppy-0.0.372.dist-info → code_puppy-0.0.374.dist-info}/RECORD +30 -22
  26. {code_puppy-0.0.372.data → code_puppy-0.0.374.data}/data/code_puppy/models.json +0 -0
  27. {code_puppy-0.0.372.data → code_puppy-0.0.374.data}/data/code_puppy/models_dev_api.json +0 -0
  28. {code_puppy-0.0.372.dist-info → code_puppy-0.0.374.dist-info}/WHEEL +0 -0
  29. {code_puppy-0.0.372.dist-info → code_puppy-0.0.374.dist-info}/entry_points.txt +0 -0
  30. {code_puppy-0.0.372.dist-info → code_puppy-0.0.374.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,304 @@
1
+ """UC Tool Registry - discovers and manages user-created tools.
2
+
3
+ This module provides the core registry that scans the user's UC directory,
4
+ loads tool metadata, extracts function signatures, and provides access
5
+ to enabled tools for the LLM.
6
+ """
7
+
8
+ import importlib.util
9
+ import inspect
10
+ import logging
11
+ import sys
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from types import ModuleType
15
+ from typing import Callable, Dict, List, Optional, Tuple
16
+
17
+ from . import USER_UC_DIR
18
+ from .models import ToolMeta, UCToolInfo
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class UCRegistry:
24
+ """Registry for discovering and managing UC tools.
25
+
26
+ Scans the user's UC directory recursively, loading tool metadata
27
+ and providing access to enabled tools. Supports namespacing via
28
+ subdirectories (e.g., api/weather.py → "api.weather").
29
+ """
30
+
31
+ def __init__(self, tools_dir: Optional[Path] = None):
32
+ """Initialize the registry.
33
+
34
+ Args:
35
+ tools_dir: Directory to scan for tools. Defaults to USER_UC_DIR.
36
+ """
37
+ self._tools_dir = tools_dir or USER_UC_DIR
38
+ self._tools: Dict[str, UCToolInfo] = {}
39
+ self._modules: Dict[str, ModuleType] = {}
40
+ self._last_scan: Optional[datetime] = None
41
+
42
+ def ensure_tools_dir(self) -> Path:
43
+ """Ensure the tools directory exists.
44
+
45
+ Returns:
46
+ Path to the tools directory.
47
+ """
48
+ self._tools_dir.mkdir(parents=True, exist_ok=True)
49
+ return self._tools_dir
50
+
51
+ def scan(self) -> int:
52
+ """Scan the tools directory and load all tools.
53
+
54
+ Returns:
55
+ Number of tools found.
56
+ """
57
+ self._tools.clear()
58
+ self._modules.clear()
59
+
60
+ if not self._tools_dir.exists():
61
+ logger.debug(f"Tools directory does not exist: {self._tools_dir}")
62
+ return 0
63
+
64
+ # Find all Python files recursively
65
+ tool_files = list(self._tools_dir.rglob("*.py"))
66
+
67
+ for tool_file in tool_files:
68
+ # Skip __init__.py and hidden files
69
+ if tool_file.name.startswith("_") or tool_file.name.startswith("."):
70
+ continue
71
+
72
+ try:
73
+ tool_info = self._load_tool_file(tool_file)
74
+ if tool_info:
75
+ self._tools[tool_info.full_name] = tool_info
76
+ logger.debug(f"Loaded tool: {tool_info.full_name}")
77
+ except Exception as e:
78
+ logger.warning(f"Failed to load tool from {tool_file}: {e}")
79
+
80
+ self._last_scan = datetime.now()
81
+ logger.info(f"Scanned {len(self._tools)} tools from {self._tools_dir}")
82
+ return len(self._tools)
83
+
84
+ def _load_tool_file(self, file_path: Path) -> Optional[UCToolInfo]:
85
+ """Load a tool from a Python file.
86
+
87
+ Args:
88
+ file_path: Path to the Python file.
89
+
90
+ Returns:
91
+ UCToolInfo if valid tool, None otherwise.
92
+ """
93
+ # Calculate namespace from relative path
94
+ try:
95
+ rel_path = file_path.relative_to(self._tools_dir)
96
+ namespace_parts = list(rel_path.parent.parts)
97
+ namespace = ".".join(namespace_parts) if namespace_parts else ""
98
+ except ValueError:
99
+ namespace = ""
100
+
101
+ # Load the module
102
+ module = self._load_module(file_path)
103
+ if module is None:
104
+ return None
105
+
106
+ # Check for TOOL_META
107
+ if not hasattr(module, "TOOL_META"):
108
+ logger.debug(f"No TOOL_META found in {file_path}")
109
+ return None
110
+
111
+ raw_meta = dict(
112
+ getattr(module, "TOOL_META")
113
+ ) # Copy to avoid mutating module constant
114
+ if not isinstance(raw_meta, dict):
115
+ logger.warning(f"TOOL_META is not a dict in {file_path}")
116
+ return None
117
+
118
+ # Set namespace from directory structure
119
+ raw_meta["namespace"] = namespace
120
+
121
+ # Parse metadata
122
+ try:
123
+ meta = ToolMeta(**raw_meta)
124
+ except Exception as e:
125
+ logger.warning(f"Invalid TOOL_META in {file_path}: {e}")
126
+ return None
127
+
128
+ # Find the callable function
129
+ func, func_name = self._find_tool_function(module, meta.name)
130
+ if func is None:
131
+ logger.warning(f"No callable function found in {file_path}")
132
+ return None
133
+
134
+ # Extract signature
135
+ try:
136
+ sig = inspect.signature(func)
137
+ signature_str = f"{func_name}{sig}"
138
+ except (ValueError, TypeError):
139
+ signature_str = f"{func_name}(...)"
140
+
141
+ # Extract docstring
142
+ docstring = inspect.getdoc(func)
143
+
144
+ # Store module reference for later calls
145
+ full_name = f"{namespace}.{meta.name}" if namespace else meta.name
146
+ self._modules[full_name] = module
147
+
148
+ return UCToolInfo(
149
+ meta=meta,
150
+ signature=signature_str,
151
+ source_path=str(file_path),
152
+ function_name=func_name,
153
+ docstring=docstring,
154
+ )
155
+
156
+ def _load_module(self, file_path: Path) -> Optional[ModuleType]:
157
+ """Load a Python module from a file path.
158
+
159
+ Args:
160
+ file_path: Path to the Python file.
161
+
162
+ Returns:
163
+ Loaded module or None if failed.
164
+ """
165
+ module_name = f"uc_tool_{file_path.stem}_{hash(str(file_path))}"
166
+
167
+ try:
168
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
169
+ if spec is None or spec.loader is None:
170
+ return None
171
+
172
+ module = importlib.util.module_from_spec(spec)
173
+ sys.modules[module_name] = module
174
+ spec.loader.exec_module(module)
175
+ return module
176
+ except Exception:
177
+ return None
178
+
179
+ def _find_tool_function(
180
+ self, module: ModuleType, tool_name: str
181
+ ) -> Tuple[Optional[Callable], str]:
182
+ """Find the callable function in a tool module.
183
+
184
+ Looks for:
185
+ 1. A function with the same name as the tool
186
+ 2. A function named 'run'
187
+ 3. A function named 'execute'
188
+ 4. Any public function (not starting with _)
189
+
190
+ Args:
191
+ module: The loaded module.
192
+ tool_name: The tool name from metadata.
193
+
194
+ Returns:
195
+ Tuple of (function, function_name) or (None, "") if not found.
196
+ """
197
+ # Priority order for finding the function
198
+ candidates = [tool_name, "run", "execute"]
199
+
200
+ for name in candidates:
201
+ if hasattr(module, name):
202
+ func = getattr(module, name)
203
+ if callable(func) and not isinstance(func, type):
204
+ return func, name
205
+
206
+ # Fall back to first public callable
207
+ for name in dir(module):
208
+ if name.startswith("_"):
209
+ continue
210
+ obj = getattr(module, name)
211
+ if callable(obj) and not isinstance(obj, type):
212
+ return obj, name
213
+
214
+ return None, ""
215
+
216
+ def list_tools(self, include_disabled: bool = False) -> List[UCToolInfo]:
217
+ """List all discovered tools.
218
+
219
+ Args:
220
+ include_disabled: Whether to include disabled tools.
221
+
222
+ Returns:
223
+ List of tool info objects.
224
+ """
225
+ if not self._tools:
226
+ self.scan()
227
+
228
+ tools = list(self._tools.values())
229
+ if not include_disabled:
230
+ tools = [t for t in tools if t.meta.enabled]
231
+
232
+ return sorted(tools, key=lambda t: t.full_name)
233
+
234
+ def get_tool(self, name: str) -> Optional[UCToolInfo]:
235
+ """Get a specific tool by name.
236
+
237
+ Args:
238
+ name: Full tool name (including namespace).
239
+
240
+ Returns:
241
+ Tool info or None if not found.
242
+ """
243
+ if not self._tools:
244
+ self.scan()
245
+
246
+ return self._tools.get(name)
247
+
248
+ def get_tool_function(self, name: str) -> Optional[Callable]:
249
+ """Get the callable function for a tool.
250
+
251
+ Args:
252
+ name: Full tool name (including namespace).
253
+
254
+ Returns:
255
+ Callable function or None if not found.
256
+ """
257
+ tool = self.get_tool(name)
258
+ if tool is None:
259
+ return None
260
+
261
+ module = self._modules.get(name)
262
+ if module is None:
263
+ return None
264
+
265
+ func, _ = self._find_tool_function(module, tool.meta.name)
266
+ return func
267
+
268
+ def load_tool_module(self, name: str) -> Optional[ModuleType]:
269
+ """Get the loaded module for a tool.
270
+
271
+ Args:
272
+ name: Full tool name (including namespace).
273
+
274
+ Returns:
275
+ Module or None if not found.
276
+ """
277
+ if not self._tools:
278
+ self.scan()
279
+
280
+ return self._modules.get(name)
281
+
282
+ def reload(self) -> int:
283
+ """Force a rescan of all tools.
284
+
285
+ Returns:
286
+ Number of tools found.
287
+ """
288
+ return self.scan()
289
+
290
+
291
+ # Global registry instance
292
+ _registry: Optional[UCRegistry] = None
293
+
294
+
295
+ def get_registry() -> UCRegistry:
296
+ """Get the global UC registry instance.
297
+
298
+ Returns:
299
+ The global UCRegistry instance.
300
+ """
301
+ global _registry
302
+ if _registry is None:
303
+ _registry = UCRegistry()
304
+ return _registry