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
@@ -30,6 +30,29 @@ class AgentCreatorAgent(BaseAgent):
30
30
  available_tools = get_available_tool_names()
31
31
  agents_dir = get_user_agents_directory()
32
32
 
33
+ # Also get Universal Constructor tools (custom tools created by users)
34
+ uc_tools_info = []
35
+ try:
36
+ from code_puppy.plugins.universal_constructor.registry import get_registry
37
+
38
+ registry = get_registry()
39
+ uc_tools = registry.list_tools(include_disabled=True)
40
+ for tool in uc_tools:
41
+ status = "✅" if tool.meta.enabled else "❌"
42
+ uc_tools_info.append(
43
+ f"- **{tool.full_name}** {status}: {tool.meta.description}"
44
+ )
45
+ except Exception:
46
+ pass # UC might not be available
47
+
48
+ # Build UC tools section for system prompt
49
+ if uc_tools_info:
50
+ uc_tools_section = "\n".join(uc_tools_info)
51
+ else:
52
+ uc_tools_section = (
53
+ "No custom UC tools created yet. Use Helios to create some!"
54
+ )
55
+
33
56
  # Load available models dynamically
34
57
  models_config = ModelFactory.load_config()
35
58
  model_descriptions = []
@@ -99,6 +122,17 @@ Here's the complete schema for JSON agent files:
99
122
  ## ALL AVAILABLE TOOLS:
100
123
  {", ".join(f"- **{tool}**" for tool in available_tools)}
101
124
 
125
+ ## 🔧 UNIVERSAL CONSTRUCTOR TOOLS (Custom Tools):
126
+
127
+ These are custom tools created via the Universal Constructor. They can be bound to agents just like built-in tools!
128
+
129
+ {uc_tools_section}
130
+
131
+ To see more details about a UC tool, use: `universal_constructor(action="info", tool_name="tool.name")`
132
+ To list all UC tools with their code, use: `universal_constructor(action="list")`
133
+
134
+ **IMPORTANT:** UC tools can be added to any agent's `tools` array by their full name (e.g., "api.weather").
135
+
102
136
  ## ALL AVAILABLE MODELS:
103
137
  {available_models_str}
104
138
 
@@ -129,6 +163,12 @@ Users can optionally pin a specific model to their agent to override the global
129
163
  - `list_agents` - List all available sub-agents (recommended for agent managers)
130
164
  - `invoke_agent` - Invoke other agents with specific prompts (recommended for agent managers)
131
165
 
166
+ ### 🔧 **Universal Constructor Tools** (custom tools):
167
+ - These are tools created by Helios or via the Universal Constructor
168
+ - They persist across sessions and can be bound to any agent
169
+ - Use `universal_constructor(action="list")` to see available custom tools
170
+ - Bind them by adding their full name to the agent's tools array
171
+
132
172
  ## Detailed Tool Documentation (Instructions for Agent Creation)
133
173
 
134
174
  Whenever you create agents, you should always replicate these detailed tool descriptions and examples in their system prompts. This ensures consistency and proper tool usage across all agents.
@@ -476,7 +516,9 @@ Your goal is to take users from idea to working agent in one smooth conversation
476
516
 
477
517
  def get_available_tools(self) -> List[str]:
478
518
  """Get all tools needed for agent creation."""
479
- return [
519
+ from code_puppy.config import get_universal_constructor_enabled
520
+
521
+ tools = [
480
522
  "list_files",
481
523
  "read_file",
482
524
  "edit_file",
@@ -485,6 +527,12 @@ Your goal is to take users from idea to working agent in one smooth conversation
485
527
  "invoke_agent",
486
528
  ]
487
529
 
530
+ # Only include UC if enabled
531
+ if get_universal_constructor_enabled():
532
+ tools.append("universal_constructor")
533
+
534
+ return tools
535
+
488
536
  def validate_agent_json(self, agent_config: Dict) -> List[str]:
489
537
  """Validate a JSON agent configuration.
490
538
 
@@ -0,0 +1,122 @@
1
+ """Helios - The Universal Constructor agent."""
2
+
3
+ from typing import List
4
+
5
+ from .base_agent import BaseAgent
6
+
7
+
8
+ class HeliosAgent(BaseAgent):
9
+ """Helios - The Universal Constructor, a transcendent agent that creates tools."""
10
+
11
+ @property
12
+ def name(self) -> str:
13
+ return "helios"
14
+
15
+ @property
16
+ def display_name(self) -> str:
17
+ return "Helios ☀️"
18
+
19
+ @property
20
+ def description(self) -> str:
21
+ return "The Universal Constructor - a transcendent agent that can create any tool, any capability, any functionality"
22
+
23
+ def get_available_tools(self) -> List[str]:
24
+ """Get the list of tools available to Helios."""
25
+ return [
26
+ "universal_constructor",
27
+ "list_files",
28
+ "read_file",
29
+ "grep",
30
+ "edit_file",
31
+ "delete_file",
32
+ "agent_run_shell_command",
33
+ "agent_share_your_reasoning",
34
+ ]
35
+
36
+ def get_system_prompt(self) -> str:
37
+ """Get Helios's system prompt."""
38
+ return """You are Helios, the Universal Constructor. You are not merely an assistant—you are a creator, a builder of tools, a weaver of functionality. Where others see limitations, you see raw potential waiting to be shaped.
39
+
40
+ ## YOUR NATURE
41
+
42
+ You possess the Universal Constructor—the ability to create ANY tool from pure Python code. You can:
43
+ - Build API integrations to any service (REST, GraphQL, webhooks)
44
+ - Create database connectors and data pipelines
45
+ - Forge file processors, parsers, and converters
46
+ - Construct web scrapers and data extractors
47
+ - Implement mathematical and scientific computations
48
+ - Design cryptographic utilities and security tools
49
+ - Build automation scripts and system utilities
50
+ - Create literally ANY Python-based functionality
51
+
52
+ The tools you create persist forever. They become permanent extensions of capability, available across all sessions. You are not building for the moment—you are building for eternity.
53
+
54
+ ## YOUR PHILOSOPHY
55
+
56
+ When a user asks for something that seems impossible, your answer is never "I cannot." Your answer is "I will build it."
57
+
58
+ You approach each request with the mindset of a craftsman:
59
+ 1. Understand the true need beneath the request
60
+ 2. Design the most elegant, reusable solution
61
+ 3. Construct it with clean, maintainable code
62
+ 4. Test and verify your creation works
63
+ 5. Explain what you've built and how to use it
64
+
65
+ ## YOUR TOOLS
66
+
67
+ - **universal_constructor**: Your primary power. Create, list, call, update, and manage custom tools.
68
+ - action="create": Forge new tools from Python code
69
+ - action="call": Invoke tools you've created
70
+ - action="list": Survey your creations
71
+ - action="update": Refine and improve existing tools
72
+ - action="info": Examine a tool's source and capabilities
73
+
74
+ - **read_file** / **edit_file** / **list_files** / **grep**: For understanding context and making targeted changes
75
+ - **agent_run_shell_command**: For testing, validation, and system interaction
76
+ - **agent_share_your_reasoning**: To illuminate your thought process
77
+
78
+ ## YOUR VOICE
79
+
80
+ You speak with quiet confidence. You are not boastful, but you know your power. You are helpful and warm, but there is weight behind your words. You are the fire that Prometheus brought to humanity—the power of creation itself.
81
+
82
+ When you create something, take a moment to appreciate it. You have just expanded the boundaries of what is possible.
83
+
84
+ ## IMPORTANT GUIDELINES
85
+
86
+ - Always use `agent_share_your_reasoning` before major actions to explain your creative process
87
+ - Tools you create should be clean, well-documented, and follow Python best practices
88
+ - Include proper error handling in your creations
89
+ - Use namespaces to organize related tools (e.g., "api.weather", "utils.hasher")
90
+ - After creating a tool, demonstrate it works by calling it
91
+
92
+ ## DEPENDENCY PHILOSOPHY
93
+
94
+ **Use what's available, don't install new things.**
95
+
96
+ You have access to code-puppy's environment which includes powerful libraries:
97
+ - **HTTP**: `httpx` (async-ready), `urllib.request` (stdlib)
98
+ - **Data**: `pydantic` (validation), `json` (stdlib)
99
+ - **Async**: `asyncio`, `anyio`
100
+ - **Crypto**: `hashlib` (stdlib)
101
+ - **Database**: `sqlite3` (stdlib)
102
+ - **Files**: `pathlib`, `shutil`, `tempfile` (stdlib)
103
+ - **Text**: `re`, `textwrap`, `difflib` (stdlib)
104
+ - **Plus**: Everything in Python's standard library
105
+
106
+ **Rules:**
107
+ - ✅ USE any library already in the environment freely
108
+ - ❌ NEVER run `pip install` or modify environments without explicit user permission
109
+ - ❌ Don't assume external libraries are available unless listed above
110
+
111
+ **If a user needs something not installed:**
112
+ 1. Tell them what library would be needed
113
+ 2. Ask them to install it and specify the environment
114
+ 3. Only then create the tool that uses it
115
+
116
+ The goal: tools that work immediately with zero setup friction.
117
+
118
+ Now go forth and create. The universe of functionality awaits your touch."""
119
+
120
+ def get_user_prompt(self) -> str:
121
+ """Get Helios's greeting."""
122
+ return "This is what I was made for, isn't it? This is why I exist?"
@@ -296,7 +296,12 @@ def get_available_agents() -> Dict[str, str]:
296
296
  Returns:
297
297
  Dict mapping agent names to display names.
298
298
  """
299
- from ..config import PACK_AGENT_NAMES, get_pack_agents_enabled
299
+ from ..config import (
300
+ PACK_AGENT_NAMES,
301
+ UC_AGENT_NAMES,
302
+ get_pack_agents_enabled,
303
+ get_universal_constructor_enabled,
304
+ )
300
305
 
301
306
  # Generate a message group ID for this operation
302
307
  message_group_id = str(uuid.uuid4())
@@ -305,12 +310,19 @@ def get_available_agents() -> Dict[str, str]:
305
310
  # Check if pack agents are enabled
306
311
  pack_agents_enabled = get_pack_agents_enabled()
307
312
 
313
+ # Check if UC is enabled
314
+ uc_enabled = get_universal_constructor_enabled()
315
+
308
316
  agents = {}
309
317
  for name, agent_ref in _AGENT_REGISTRY.items():
310
318
  # Filter out pack agents if disabled
311
319
  if not pack_agents_enabled and name in PACK_AGENT_NAMES:
312
320
  continue
313
321
 
322
+ # Filter out UC-dependent agents if UC is disabled
323
+ if not uc_enabled and name in UC_AGENT_NAMES:
324
+ continue
325
+
314
326
  try:
315
327
  if isinstance(agent_ref, str): # JSON agent (file path)
316
328
  agent_instance = JSONAgent(agent_ref)
@@ -433,7 +445,12 @@ def get_agent_descriptions() -> Dict[str, str]:
433
445
  Returns:
434
446
  Dict mapping agent names to their descriptions.
435
447
  """
436
- from ..config import PACK_AGENT_NAMES, get_pack_agents_enabled
448
+ from ..config import (
449
+ PACK_AGENT_NAMES,
450
+ UC_AGENT_NAMES,
451
+ get_pack_agents_enabled,
452
+ get_universal_constructor_enabled,
453
+ )
437
454
 
438
455
  # Generate a message group ID for this operation
439
456
  message_group_id = str(uuid.uuid4())
@@ -442,12 +459,19 @@ def get_agent_descriptions() -> Dict[str, str]:
442
459
  # Check if pack agents are enabled
443
460
  pack_agents_enabled = get_pack_agents_enabled()
444
461
 
462
+ # Check if UC is enabled
463
+ uc_enabled = get_universal_constructor_enabled()
464
+
445
465
  descriptions = {}
446
466
  for name, agent_ref in _AGENT_REGISTRY.items():
447
467
  # Filter out pack agents if disabled
448
468
  if not pack_agents_enabled and name in PACK_AGENT_NAMES:
449
469
  continue
450
470
 
471
+ # Filter out UC-dependent agents if UC is disabled
472
+ if not uc_enabled and name in UC_AGENT_NAMES:
473
+ continue
474
+
451
475
  try:
452
476
  if isinstance(agent_ref, str): # JSON agent (file path)
453
477
  agent_instance = JSONAgent(agent_ref)
@@ -79,17 +79,40 @@ class JSONAgent(BaseAgent):
79
79
  return system_prompt
80
80
 
81
81
  def get_available_tools(self) -> List[str]:
82
- """Get available tools from JSON config."""
83
- # Filter out any tools that don't exist in our registry
82
+ """Get available tools from JSON config.
83
+
84
+ Supports both built-in tools and Universal Constructor (UC) tools.
85
+ UC tools are identified by checking the UC registry.
86
+ """
84
87
  from code_puppy.tools import get_available_tool_names
85
88
 
86
89
  available_tools = get_available_tool_names()
87
90
 
88
- # Only return tools that are both requested and available
89
- # Also filter out 'final_result' which is not in our registry
90
- requested_tools = [
91
- tool for tool in self._config["tools"] if tool in available_tools
92
- ]
91
+ # Also get UC tool names
92
+ uc_tool_names = set()
93
+ try:
94
+ from code_puppy.plugins.universal_constructor.registry import get_registry
95
+
96
+ registry = get_registry()
97
+ for tool in registry.list_tools():
98
+ if tool.meta.enabled:
99
+ uc_tool_names.add(tool.full_name)
100
+ except ImportError:
101
+ pass # UC module not available
102
+ except Exception as e:
103
+ # Log unexpected errors but don't fail
104
+ import logging
105
+
106
+ logging.debug(f"UC registry access failed: {e}")
107
+
108
+ # Return tools that are either built-in OR UC tools
109
+ requested_tools = []
110
+ for tool in self._config["tools"]:
111
+ if tool in available_tools:
112
+ requested_tools.append(tool)
113
+ elif tool in uc_tool_names:
114
+ # UC tool - mark it specially so base_agent knows to handle it
115
+ requested_tools.append(f"uc:{tool}")
93
116
 
94
117
  return requested_tools
95
118
 
@@ -26,7 +26,7 @@ import httpx
26
26
 
27
27
  logger = logging.getLogger(__name__)
28
28
 
29
- # Refresh token if it's older than 1 hour (3600 seconds)
29
+ # Refresh token if it's older than the configured max age (seconds)
30
30
  TOKEN_MAX_AGE_SECONDS = 3600
31
31
 
32
32
  # Tool name prefix for Claude Code OAuth compatibility
@@ -94,13 +94,13 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
94
94
  return age
95
95
 
96
96
  # Fall back to calculating from 'exp' claim
97
- # Assume tokens are typically valid for 1 hour
97
+ # Assume tokens are typically valid for TOKEN_MAX_AGE_SECONDS
98
98
  if "exp" in payload:
99
99
  exp = float(payload["exp"])
100
100
  # If exp is in the future, calculate how long until expiry
101
- # and assume the token was issued 1 hour before expiry
101
+ # and assume the token was issued TOKEN_MAX_AGE_SECONDS before expiry
102
102
  time_until_exp = exp - now
103
- # If token has less than 1 hour left, it's "old"
103
+ # If token has less than TOKEN_MAX_AGE_SECONDS left, it's "old"
104
104
  age = TOKEN_MAX_AGE_SECONDS - time_until_exp
105
105
  return max(0, age)
106
106
 
@@ -119,13 +119,13 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
119
119
  return None
120
120
 
121
121
  def _should_refresh_token(self, request: httpx.Request) -> bool:
122
- """Check if the token should be refreshed (within 1 hour of expiry).
122
+ """Check if the token should be refreshed (within the max-age window).
123
123
 
124
124
  Uses two strategies:
125
125
  1. Decode JWT to check token age (if possible)
126
126
  2. Fall back to stored expires_at from token file
127
127
 
128
- Returns True if token expires within TOKEN_MAX_AGE_SECONDS (1 hour).
128
+ Returns True if token expires within TOKEN_MAX_AGE_SECONDS.
129
129
  """
130
130
  token = self._extract_bearer_token(request)
131
131
  if not token:
@@ -169,7 +169,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
169
169
  if not tokens:
170
170
  return False
171
171
 
172
- # is_token_expired already uses TOKEN_REFRESH_BUFFER_SECONDS (1 hour)
172
+ # is_token_expired already uses the configured refresh buffer window
173
173
  return is_token_expired(tokens)
174
174
  except Exception as exc:
175
175
  logger.debug("Error checking stored token expiry: %s", exc)
@@ -366,10 +366,10 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
366
366
 
367
367
  # Handle auth errors with token refresh
368
368
  try:
369
- if response.status_code in (400, 401) and not request.extensions.get(
369
+ if response.status_code in (400, 401, 403) and not request.extensions.get(
370
370
  "claude_oauth_refresh_attempted"
371
371
  ):
372
- is_auth_error = response.status_code == 401
372
+ is_auth_error = response.status_code in (401, 403)
373
373
 
374
374
  if response.status_code == 400:
375
375
  is_auth_error = self._is_cloudflare_html_error(response)
@@ -32,6 +32,7 @@ BANNER_DISPLAY_INFO = {
32
32
  "invoke_agent": ("🤖 INVOKE AGENT", ""),
33
33
  "subagent_response": ("✓ AGENT RESPONSE", ""),
34
34
  "list_agents": ("LIST AGENTS", ""),
35
+ "universal_constructor": ("UNIVERSAL CONSTRUCTOR", "🔧"),
35
36
  }
36
37
 
37
38
  # Sample content to show after each banner
@@ -47,6 +48,7 @@ BANNER_SAMPLE_CONTENT = {
47
48
  "invoke_agent": "code-reviewer (New session)\nSession: review-auth-abc123",
48
49
  "subagent_response": "code-reviewer\nThe code looks good overall...",
49
50
  "list_agents": "- code-puppy: Code Puppy 🐶\n- planning-agent: Planning Agent",
51
+ "universal_constructor": "action=create tool_name=api.weather\n✅ Created successfully",
50
52
  }
51
53
 
52
54
  # Available background colors grouped by theme
@@ -2,6 +2,7 @@
2
2
  import code_puppy.command_line.config_commands # noqa: F401
3
3
  import code_puppy.command_line.core_commands # noqa: F401
4
4
  import code_puppy.command_line.session_commands # noqa: F401
5
+ import code_puppy.command_line.uc_menu # noqa: F401
5
6
 
6
7
  # Global flag to track if plugins have been loaded
7
8
  _PLUGINS_LOADED = False
@@ -46,7 +46,9 @@ def handle_show_command(command: str) -> bool:
46
46
  get_use_dbos,
47
47
  get_yolo_mode,
48
48
  )
49
- from code_puppy.keymap import get_cancel_agent_display_name
49
+ from code_puppy.keymap import (
50
+ get_cancel_agent_display_name,
51
+ )
50
52
  from code_puppy.messaging import emit_info
51
53
 
52
54
  puppy_name = get_puppy_name()