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.
- code_puppy/agents/agent_creator_agent.py +49 -1
- code_puppy/agents/agent_helios.py +122 -0
- code_puppy/agents/agent_manager.py +60 -4
- code_puppy/agents/base_agent.py +61 -4
- code_puppy/agents/json_agent.py +30 -7
- code_puppy/callbacks.py +125 -0
- code_puppy/command_line/colors_menu.py +2 -0
- code_puppy/command_line/command_handler.py +1 -0
- code_puppy/command_line/config_commands.py +3 -1
- code_puppy/command_line/uc_menu.py +890 -0
- code_puppy/config.py +29 -0
- code_puppy/messaging/messages.py +18 -0
- code_puppy/messaging/rich_renderer.py +48 -7
- code_puppy/messaging/subagent_console.py +0 -1
- code_puppy/model_factory.py +63 -258
- code_puppy/model_utils.py +33 -1
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +106 -1
- code_puppy/plugins/antigravity_oauth/utils.py +2 -3
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +85 -3
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +88 -0
- code_puppy/plugins/ralph/__init__.py +13 -0
- code_puppy/plugins/ralph/agents.py +433 -0
- code_puppy/plugins/ralph/commands.py +208 -0
- code_puppy/plugins/ralph/loop_controller.py +285 -0
- code_puppy/plugins/ralph/models.py +125 -0
- code_puppy/plugins/ralph/register_callbacks.py +133 -0
- code_puppy/plugins/ralph/state_manager.py +322 -0
- code_puppy/plugins/ralph/tools.py +451 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +304 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/tools/__init__.py +169 -1
- code_puppy/tools/agent_tools.py +1 -1
- code_puppy/tools/command_runner.py +23 -9
- code_puppy/tools/universal_constructor.py +889 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/METADATA +1 -1
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/RECORD +44 -28
- {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/entry_points.txt +0 -0
- {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")
|