code-puppy 0.0.373__py3-none-any.whl → 0.0.375__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 (44) 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 +60 -4
  4. code_puppy/agents/base_agent.py +61 -4
  5. code_puppy/agents/json_agent.py +30 -7
  6. code_puppy/callbacks.py +125 -0
  7. code_puppy/command_line/colors_menu.py +2 -0
  8. code_puppy/command_line/command_handler.py +1 -0
  9. code_puppy/command_line/config_commands.py +3 -1
  10. code_puppy/command_line/uc_menu.py +890 -0
  11. code_puppy/config.py +29 -0
  12. code_puppy/messaging/messages.py +18 -0
  13. code_puppy/messaging/rich_renderer.py +48 -7
  14. code_puppy/messaging/subagent_console.py +0 -1
  15. code_puppy/model_factory.py +63 -258
  16. code_puppy/model_utils.py +33 -1
  17. code_puppy/plugins/antigravity_oauth/register_callbacks.py +106 -1
  18. code_puppy/plugins/antigravity_oauth/utils.py +2 -3
  19. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +85 -3
  20. code_puppy/plugins/claude_code_oauth/register_callbacks.py +88 -0
  21. code_puppy/plugins/ralph/__init__.py +13 -0
  22. code_puppy/plugins/ralph/agents.py +433 -0
  23. code_puppy/plugins/ralph/commands.py +208 -0
  24. code_puppy/plugins/ralph/loop_controller.py +285 -0
  25. code_puppy/plugins/ralph/models.py +125 -0
  26. code_puppy/plugins/ralph/register_callbacks.py +133 -0
  27. code_puppy/plugins/ralph/state_manager.py +322 -0
  28. code_puppy/plugins/ralph/tools.py +451 -0
  29. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  30. code_puppy/plugins/universal_constructor/models.py +138 -0
  31. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  32. code_puppy/plugins/universal_constructor/registry.py +304 -0
  33. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  34. code_puppy/tools/__init__.py +169 -1
  35. code_puppy/tools/agent_tools.py +1 -1
  36. code_puppy/tools/command_runner.py +23 -9
  37. code_puppy/tools/universal_constructor.py +889 -0
  38. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/METADATA +1 -1
  39. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/RECORD +44 -28
  40. {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models.json +0 -0
  41. {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models_dev_api.json +0 -0
  42. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/WHEEL +0 -0
  43. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/entry_points.txt +0 -0
  44. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,451 @@
1
+ """Ralph plugin tools - registered via the register_tools callback."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import List
6
+
7
+ from pydantic_ai import RunContext
8
+
9
+ from .models import ProgressEntry
10
+ from .state_manager import get_state_manager
11
+
12
+ # ============================================================================
13
+ # TOOL OUTPUT TYPES
14
+ # ============================================================================
15
+
16
+
17
+ @dataclass
18
+ class RalphStoryOutput:
19
+ """Output for getting the current story."""
20
+
21
+ story_id: str | None
22
+ title: str | None
23
+ description: str | None
24
+ acceptance_criteria: List[str]
25
+ priority: int | None
26
+ requires_ui_verification: bool
27
+ all_complete: bool
28
+ error: str | None = None
29
+
30
+
31
+ @dataclass
32
+ class RalphStatusOutput:
33
+ """Output for status checks."""
34
+
35
+ success: bool
36
+ message: str
37
+ progress_summary: str | None = None
38
+ stories_remaining: int = 0
39
+ all_complete: bool = False
40
+
41
+
42
+ @dataclass
43
+ class RalphPRDOutput:
44
+ """Output for reading the full PRD."""
45
+
46
+ success: bool
47
+ project: str | None = None
48
+ branch_name: str | None = None
49
+ description: str | None = None
50
+ stories: List[dict] | None = None
51
+ progress_summary: str | None = None
52
+ error: str | None = None
53
+
54
+
55
+ @dataclass
56
+ class RalphPatternsOutput:
57
+ """Output for reading codebase patterns."""
58
+
59
+ patterns: str
60
+ has_patterns: bool
61
+
62
+
63
+ # ============================================================================
64
+ # TOOL REGISTRATION FUNCTIONS
65
+ # ============================================================================
66
+
67
+
68
+ def register_ralph_get_current_story(agent) -> None:
69
+ """Register the tool to get the current story to work on."""
70
+
71
+ @agent.tool
72
+ def ralph_get_current_story(context: RunContext) -> RalphStoryOutput:
73
+ """Get the next user story to work on from prd.json.
74
+
75
+ This tool reads the prd.json file and returns the highest-priority
76
+ story that hasn't been completed yet (passes=false).
77
+
78
+ Returns:
79
+ RalphStoryOutput containing:
80
+ - story_id: The story ID (e.g., "US-001")
81
+ - title: Story title
82
+ - description: Full story description
83
+ - acceptance_criteria: List of criteria to satisfy
84
+ - priority: Story priority (lower = higher priority)
85
+ - requires_ui_verification: True if story needs browser testing
86
+ - all_complete: True if ALL stories are done
87
+ - error: Error message if something went wrong
88
+ """
89
+ manager = get_state_manager()
90
+
91
+ if not manager.prd_exists():
92
+ return RalphStoryOutput(
93
+ story_id=None,
94
+ title=None,
95
+ description=None,
96
+ acceptance_criteria=[],
97
+ priority=None,
98
+ requires_ui_verification=False,
99
+ all_complete=False,
100
+ error="No prd.json found in current directory. Create one first with /ralph prd",
101
+ )
102
+
103
+ if manager.all_stories_complete():
104
+ return RalphStoryOutput(
105
+ story_id=None,
106
+ title=None,
107
+ description=None,
108
+ acceptance_criteria=[],
109
+ priority=None,
110
+ requires_ui_verification=False,
111
+ all_complete=True,
112
+ error=None,
113
+ )
114
+
115
+ story = manager.get_next_story()
116
+ if story is None:
117
+ return RalphStoryOutput(
118
+ story_id=None,
119
+ title=None,
120
+ description=None,
121
+ acceptance_criteria=[],
122
+ priority=None,
123
+ requires_ui_verification=False,
124
+ all_complete=True,
125
+ error=None,
126
+ )
127
+
128
+ return RalphStoryOutput(
129
+ story_id=story.id,
130
+ title=story.title,
131
+ description=story.description,
132
+ acceptance_criteria=story.acceptance_criteria,
133
+ priority=story.priority,
134
+ requires_ui_verification=story.has_ui_verification(),
135
+ all_complete=False,
136
+ error=None,
137
+ )
138
+
139
+
140
+ def register_ralph_mark_story_complete(agent) -> None:
141
+ """Register the tool to mark a story as complete."""
142
+
143
+ @agent.tool
144
+ def ralph_mark_story_complete(
145
+ context: RunContext,
146
+ story_id: str,
147
+ notes: str | None = None,
148
+ ) -> RalphStatusOutput:
149
+ """Mark a user story as complete (passes=true) in prd.json.
150
+
151
+ Call this AFTER you have:
152
+ 1. Implemented all the acceptance criteria
153
+ 2. Verified the code compiles/typechecks
154
+ 3. Run any required tests
155
+ 4. Committed the changes
156
+
157
+ For UI stories, also ensure browser verification passed.
158
+
159
+ Args:
160
+ story_id: The story ID to mark complete (e.g., "US-001")
161
+ notes: Optional notes about the implementation
162
+
163
+ Returns:
164
+ RalphStatusOutput with success status and message
165
+ """
166
+ manager = get_state_manager()
167
+
168
+ success, message = manager.mark_story_complete(story_id, notes or "")
169
+
170
+ prd = manager.read_prd()
171
+ remaining = 0
172
+ all_complete = False
173
+ progress = None
174
+
175
+ if prd:
176
+ remaining = sum(1 for s in prd.user_stories if not s.passes)
177
+ all_complete = prd.all_complete()
178
+ progress = prd.get_progress_summary()
179
+
180
+ return RalphStatusOutput(
181
+ success=success,
182
+ message=message,
183
+ progress_summary=progress,
184
+ stories_remaining=remaining,
185
+ all_complete=all_complete,
186
+ )
187
+
188
+
189
+ def register_ralph_log_progress(agent) -> None:
190
+ """Register the tool to log progress to progress.txt."""
191
+
192
+ @agent.tool
193
+ def ralph_log_progress(
194
+ context: RunContext,
195
+ story_id: str,
196
+ summary: str,
197
+ files_changed: List[str] | None = None,
198
+ learnings: List[str] | None = None,
199
+ ) -> RalphStatusOutput:
200
+ """Append a progress entry to progress.txt.
201
+
202
+ Call this after completing a story to record:
203
+ - What was implemented
204
+ - Which files were changed
205
+ - Any learnings for future iterations
206
+
207
+ The learnings are especially important for helping future iterations
208
+ understand patterns and avoid mistakes.
209
+
210
+ Args:
211
+ story_id: The story ID that was completed
212
+ summary: Brief summary of what was implemented
213
+ files_changed: List of files that were modified
214
+ learnings: List of learnings/patterns discovered
215
+
216
+ Returns:
217
+ RalphStatusOutput with success status
218
+ """
219
+ manager = get_state_manager()
220
+
221
+ entry = ProgressEntry(
222
+ timestamp=datetime.now(),
223
+ story_id=story_id,
224
+ summary=summary,
225
+ files_changed=files_changed or [],
226
+ learnings=learnings or [],
227
+ )
228
+
229
+ success = manager.append_progress(entry)
230
+
231
+ return RalphStatusOutput(
232
+ success=success,
233
+ message="Progress logged successfully"
234
+ if success
235
+ else "Failed to log progress",
236
+ )
237
+
238
+
239
+ def register_ralph_check_all_complete(agent) -> None:
240
+ """Register the tool to check if all stories are complete."""
241
+
242
+ @agent.tool
243
+ def ralph_check_all_complete(context: RunContext) -> RalphStatusOutput:
244
+ """Check if all user stories in prd.json are complete.
245
+
246
+ Use this to determine if the Ralph loop should exit.
247
+ When all stories are complete, you should output:
248
+ <promise>COMPLETE</promise>
249
+
250
+ Returns:
251
+ RalphStatusOutput with all_complete flag
252
+ """
253
+ manager = get_state_manager()
254
+
255
+ if not manager.prd_exists():
256
+ return RalphStatusOutput(
257
+ success=False,
258
+ message="No prd.json found",
259
+ all_complete=False,
260
+ )
261
+
262
+ prd = manager.read_prd()
263
+ if prd is None:
264
+ return RalphStatusOutput(
265
+ success=False,
266
+ message="Failed to read prd.json",
267
+ all_complete=False,
268
+ )
269
+
270
+ all_complete = prd.all_complete()
271
+ remaining = sum(1 for s in prd.user_stories if not s.passes)
272
+
273
+ return RalphStatusOutput(
274
+ success=True,
275
+ message="All stories complete!"
276
+ if all_complete
277
+ else f"{remaining} stories remaining",
278
+ progress_summary=prd.get_progress_summary(),
279
+ stories_remaining=remaining,
280
+ all_complete=all_complete,
281
+ )
282
+
283
+
284
+ def register_ralph_read_prd(agent) -> None:
285
+ """Register the tool to read the full PRD."""
286
+
287
+ @agent.tool
288
+ def ralph_read_prd(context: RunContext) -> RalphPRDOutput:
289
+ """Read the full prd.json file and return its contents.
290
+
291
+ Use this to understand the overall project and see all stories.
292
+
293
+ Returns:
294
+ RalphPRDOutput with project details and all stories
295
+ """
296
+ manager = get_state_manager()
297
+
298
+ if not manager.prd_exists():
299
+ return RalphPRDOutput(
300
+ success=False,
301
+ error="No prd.json found in current directory",
302
+ )
303
+
304
+ prd = manager.read_prd()
305
+ if prd is None:
306
+ return RalphPRDOutput(
307
+ success=False,
308
+ error="Failed to parse prd.json",
309
+ )
310
+
311
+ return RalphPRDOutput(
312
+ success=True,
313
+ project=prd.project,
314
+ branch_name=prd.branch_name,
315
+ description=prd.description,
316
+ stories=[s.to_dict() for s in prd.user_stories],
317
+ progress_summary=prd.get_progress_summary(),
318
+ )
319
+
320
+
321
+ def register_ralph_read_patterns(agent) -> None:
322
+ """Register the tool to read codebase patterns from progress.txt."""
323
+
324
+ @agent.tool
325
+ def ralph_read_patterns(context: RunContext) -> RalphPatternsOutput:
326
+ """Read the Codebase Patterns section from progress.txt.
327
+
328
+ These patterns were discovered by previous iterations and contain
329
+ important context about the codebase. Read this BEFORE starting
330
+ work on a new story.
331
+
332
+ Returns:
333
+ RalphPatternsOutput with patterns text
334
+ """
335
+ manager = get_state_manager()
336
+ patterns = manager.read_codebase_patterns()
337
+
338
+ return RalphPatternsOutput(
339
+ patterns=patterns if patterns else "No patterns recorded yet.",
340
+ has_patterns=bool(patterns),
341
+ )
342
+
343
+
344
+ def register_ralph_add_pattern(agent) -> None:
345
+ """Register the tool to add a codebase pattern."""
346
+
347
+ @agent.tool
348
+ def ralph_add_pattern(context: RunContext, pattern: str) -> RalphStatusOutput:
349
+ """Add a reusable pattern to the Codebase Patterns section.
350
+
351
+ Only add patterns that are GENERAL and REUSABLE, not story-specific.
352
+
353
+ Good examples:
354
+ - "Use `sql<number>` template for aggregations"
355
+ - "Always use `IF NOT EXISTS` for migrations"
356
+ - "Export types from actions.ts for UI components"
357
+
358
+ Bad examples (too specific):
359
+ - "Added login button to header" (story-specific)
360
+ - "Fixed bug in user.ts" (not a pattern)
361
+
362
+ Args:
363
+ pattern: The pattern to record
364
+
365
+ Returns:
366
+ RalphStatusOutput with success status
367
+ """
368
+ manager = get_state_manager()
369
+ success = manager.add_codebase_pattern(pattern)
370
+
371
+ return RalphStatusOutput(
372
+ success=success,
373
+ message="Pattern added" if success else "Failed to add pattern",
374
+ )
375
+
376
+
377
+ def register_ralph_run_loop(agent) -> None:
378
+ """Register the tool to run the Ralph autonomous loop."""
379
+
380
+ @agent.tool
381
+ async def ralph_run_loop(
382
+ context: RunContext,
383
+ max_iterations: int = 10,
384
+ ) -> RalphStatusOutput:
385
+ """Start the Ralph autonomous loop to implement all pending stories.
386
+
387
+ This runs the full Ralph loop, executing one story per iteration
388
+ until all stories are complete or max_iterations is reached.
389
+
390
+ Each iteration:
391
+ 1. Gets the next pending story from prd.json
392
+ 2. Invokes the ralph-orchestrator agent with a fresh session
393
+ 3. The orchestrator implements and commits the story
394
+ 4. Checks if all stories are complete
395
+
396
+ Args:
397
+ max_iterations: Maximum number of iterations (default 10)
398
+
399
+ Returns:
400
+ RalphStatusOutput with success status and final progress
401
+ """
402
+ from .loop_controller import run_ralph_loop
403
+
404
+ try:
405
+ result = await run_ralph_loop(max_iterations=max_iterations)
406
+
407
+ return RalphStatusOutput(
408
+ success=result.get("success", False),
409
+ message=result.get("message", "Loop completed"),
410
+ progress_summary=result.get("message"),
411
+ stories_remaining=0 if result.get("all_complete") else -1,
412
+ all_complete=result.get("all_complete", False),
413
+ )
414
+ except Exception as e:
415
+ return RalphStatusOutput(
416
+ success=False,
417
+ message=f"Loop failed: {str(e)}",
418
+ all_complete=False,
419
+ )
420
+
421
+
422
+ # ============================================================================
423
+ # TOOL PROVIDER FOR CALLBACK
424
+ # ============================================================================
425
+
426
+
427
+ def get_ralph_tools() -> List[dict]:
428
+ """Get all Ralph tools for registration via the register_tools callback.
429
+
430
+ Returns:
431
+ List of tool definitions with name and register_func.
432
+ """
433
+ return [
434
+ {
435
+ "name": "ralph_get_current_story",
436
+ "register_func": register_ralph_get_current_story,
437
+ },
438
+ {
439
+ "name": "ralph_mark_story_complete",
440
+ "register_func": register_ralph_mark_story_complete,
441
+ },
442
+ {"name": "ralph_log_progress", "register_func": register_ralph_log_progress},
443
+ {
444
+ "name": "ralph_check_all_complete",
445
+ "register_func": register_ralph_check_all_complete,
446
+ },
447
+ {"name": "ralph_read_prd", "register_func": register_ralph_read_prd},
448
+ {"name": "ralph_read_patterns", "register_func": register_ralph_read_patterns},
449
+ {"name": "ralph_add_pattern", "register_func": register_ralph_add_pattern},
450
+ {"name": "ralph_run_loop", "register_func": register_ralph_run_loop},
451
+ ]
@@ -0,0 +1,13 @@
1
+ """Universal Constructor - Dynamic tool creation and management plugin.
2
+
3
+ This plugin enables users to create, manage, and deploy custom tools
4
+ that extend Code Puppy's capabilities. Tools are stored in the user's
5
+ config directory and can be organized into namespaces via subdirectories.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ # User tools directory - where user-created UC tools live
11
+ USER_UC_DIR = Path.home() / ".code_puppy" / "plugins" / "universal_constructor"
12
+
13
+ __all__ = ["USER_UC_DIR"]
@@ -0,0 +1,138 @@
1
+ """Pydantic models for Universal Constructor tools and responses.
2
+
3
+ This module defines the data structures used throughout the UC plugin
4
+ for representing tool metadata, tool information, and operation responses.
5
+ """
6
+
7
+ from typing import Any, List, Optional
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class ToolMeta(BaseModel):
13
+ """Metadata for a UC tool.
14
+
15
+ This is the structure expected in the TOOL_META dictionary
16
+ at the top of each tool file.
17
+ """
18
+
19
+ name: str = Field(..., description="Human-readable tool name")
20
+ namespace: str = Field(
21
+ default="", description="Namespace for the tool (from subdirectory path)"
22
+ )
23
+ description: str = Field(..., description="What the tool does")
24
+ enabled: bool = Field(default=True, description="Whether the tool is active")
25
+ version: str = Field(default="1.0.0", description="Semantic version of the tool")
26
+ author: str = Field(default="", description="Tool author or creator")
27
+ created_at: Optional[str] = Field(
28
+ default=None, description="When the tool was created (ISO format string)"
29
+ )
30
+
31
+ model_config = {"extra": "allow"} # Allow additional metadata fields
32
+
33
+
34
+ class UCToolInfo(BaseModel):
35
+ """Full information about a UC tool.
36
+
37
+ Combines metadata with runtime information like function signature
38
+ and source file location.
39
+ """
40
+
41
+ meta: ToolMeta = Field(..., description="Tool metadata")
42
+ signature: str = Field(..., description="Function signature string")
43
+ source_path: str = Field(..., description="Path to the tool source file")
44
+ function_name: str = Field(default="", description="Name of the callable function")
45
+ docstring: Optional[str] = Field(default=None, description="Function docstring")
46
+
47
+ model_config = {"arbitrary_types_allowed": True}
48
+
49
+ @property
50
+ def full_name(self) -> str:
51
+ """Get the fully qualified tool name including namespace."""
52
+ if self.meta.namespace:
53
+ return f"{self.meta.namespace}.{self.meta.name}"
54
+ return self.meta.name
55
+
56
+
57
+ # Response models for UC operations
58
+
59
+
60
+ class UCListOutput(BaseModel):
61
+ """Response model for listing UC tools."""
62
+
63
+ tools: List[UCToolInfo] = Field(
64
+ default_factory=list, description="List of available tools"
65
+ )
66
+ total_count: int = Field(default=0, description="Total number of tools")
67
+ enabled_count: int = Field(default=0, description="Number of enabled tools")
68
+ error: Optional[str] = Field(default=None, description="Error message if any")
69
+
70
+ model_config = {"arbitrary_types_allowed": True}
71
+
72
+
73
+ class UCCallOutput(BaseModel):
74
+ """Response model for calling a UC tool."""
75
+
76
+ success: bool = Field(..., description="Whether the call succeeded")
77
+ tool_name: str = Field(..., description="Name of the tool that was called")
78
+ result: Any = Field(default=None, description="Return value from the tool")
79
+ error: Optional[str] = Field(default=None, description="Error message if failed")
80
+ execution_time: Optional[float] = Field(
81
+ default=None, description="Execution time in seconds"
82
+ )
83
+ source_preview: Optional[str] = Field(
84
+ default=None, description="Preview of the tool's source code that was executed"
85
+ )
86
+
87
+
88
+ class UCCreateOutput(BaseModel):
89
+ """Response model for creating a UC tool."""
90
+
91
+ success: bool = Field(..., description="Whether creation succeeded")
92
+ tool_name: str = Field(default="", description="Name of the created tool")
93
+ source_path: Optional[str] = Field(
94
+ default=None, description="Path where tool was saved"
95
+ )
96
+ preview: Optional[str] = Field(
97
+ default=None, description="Preview of the first 10 lines of source code"
98
+ )
99
+ error: Optional[str] = Field(default=None, description="Error message if failed")
100
+ validation_warnings: List[str] = Field(
101
+ default_factory=list, description="Non-fatal validation warnings"
102
+ )
103
+
104
+ model_config = {"arbitrary_types_allowed": True}
105
+
106
+
107
+ class UCUpdateOutput(BaseModel):
108
+ """Response model for updating a UC tool."""
109
+
110
+ success: bool = Field(..., description="Whether update succeeded")
111
+ tool_name: str = Field(default="", description="Name of the updated tool")
112
+ source_path: Optional[str] = Field(
113
+ default=None, description="Path to the updated tool"
114
+ )
115
+ preview: Optional[str] = Field(
116
+ default=None, description="Preview of the first 10 lines of updated source code"
117
+ )
118
+ error: Optional[str] = Field(default=None, description="Error message if failed")
119
+ changes_applied: List[str] = Field(
120
+ default_factory=list, description="List of changes that were applied"
121
+ )
122
+
123
+ model_config = {"arbitrary_types_allowed": True}
124
+
125
+
126
+ class UCInfoOutput(BaseModel):
127
+ """Response model for getting info about a specific UC tool."""
128
+
129
+ success: bool = Field(..., description="Whether lookup succeeded")
130
+ tool: Optional[UCToolInfo] = Field(
131
+ default=None, description="Tool information if found"
132
+ )
133
+ source_code: Optional[str] = Field(
134
+ default=None, description="Source code of the tool"
135
+ )
136
+ error: Optional[str] = Field(default=None, description="Error message if failed")
137
+
138
+ model_config = {"arbitrary_types_allowed": True}
@@ -0,0 +1,47 @@
1
+ """Callback registration for the Universal Constructor plugin.
2
+
3
+ This module registers callbacks to integrate UC with the rest of
4
+ Code Puppy. It ensures the plugin is properly loaded and initialized.
5
+ """
6
+
7
+ import logging
8
+
9
+ from code_puppy.callbacks import register_callback
10
+
11
+ from . import USER_UC_DIR
12
+ from .registry import get_registry
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def _on_startup() -> None:
18
+ """Initialize UC plugin on application startup."""
19
+ from code_puppy.config import get_universal_constructor_enabled
20
+
21
+ # Skip initialization if UC is disabled
22
+ if not get_universal_constructor_enabled():
23
+ logger.debug("Universal Constructor is disabled, skipping initialization")
24
+ return
25
+
26
+ logger.debug("Universal Constructor plugin initializing...")
27
+
28
+ # Ensure the user tools directory exists
29
+ USER_UC_DIR.mkdir(parents=True, exist_ok=True)
30
+
31
+ # Do an initial scan of tools (lazy - will happen on first access)
32
+ registry = get_registry()
33
+ logger.debug(f"UC registry initialized, tools dir: {registry._tools_dir}")
34
+
35
+ # Log plugin info at startup
36
+ tools = registry.list_tools(include_disabled=True)
37
+ enabled = [t for t in tools if t.meta.enabled]
38
+ logger.debug(
39
+ f"UC plugin loaded: {len(enabled)}/{len(tools)} tools enabled "
40
+ f"from {USER_UC_DIR}"
41
+ )
42
+
43
+
44
+ # Register startup callback
45
+ register_callback("startup", _on_startup)
46
+
47
+ logger.debug("Universal Constructor plugin callbacks registered")