code-puppy 0.0.153__tar.gz → 0.0.155__tar.gz

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 (133) hide show
  1. {code_puppy-0.0.153 → code_puppy-0.0.155}/PKG-INFO +2 -2
  2. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/agent.py +4 -3
  3. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/agents/agent_creator_agent.py +9 -2
  4. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/agents/runtime_manager.py +12 -4
  5. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/install_command.py +50 -1
  6. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/wizard_utils.py +88 -17
  7. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/config.py +8 -2
  8. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/main.py +17 -4
  9. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/__init__.py +2 -2
  10. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/config_wizard.py +1 -1
  11. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/message_history_processor.py +1 -9
  12. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/messaging/spinner/console_spinner.py +1 -1
  13. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/model_factory.py +13 -12
  14. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/models.json +26 -0
  15. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/round_robin_model.py +35 -18
  16. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/summarization_agent.py +1 -3
  17. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tools/agent_tools.py +41 -138
  18. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tools/file_operations.py +116 -96
  19. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/app.py +1 -1
  20. {code_puppy-0.0.153 → code_puppy-0.0.155}/pyproject.toml +2 -2
  21. code_puppy-0.0.153/code_puppy/token_utils.py +0 -67
  22. code_puppy-0.0.153/code_puppy/tools/token_check.py +0 -32
  23. code_puppy-0.0.153/code_puppy/tui/tests/__init__.py +0 -1
  24. code_puppy-0.0.153/code_puppy/tui/tests/test_agent_command.py +0 -79
  25. code_puppy-0.0.153/code_puppy/tui/tests/test_chat_message.py +0 -28
  26. code_puppy-0.0.153/code_puppy/tui/tests/test_chat_view.py +0 -88
  27. code_puppy-0.0.153/code_puppy/tui/tests/test_command_history.py +0 -89
  28. code_puppy-0.0.153/code_puppy/tui/tests/test_copy_button.py +0 -191
  29. code_puppy-0.0.153/code_puppy/tui/tests/test_custom_widgets.py +0 -27
  30. code_puppy-0.0.153/code_puppy/tui/tests/test_disclaimer.py +0 -27
  31. code_puppy-0.0.153/code_puppy/tui/tests/test_enums.py +0 -15
  32. code_puppy-0.0.153/code_puppy/tui/tests/test_file_browser.py +0 -60
  33. code_puppy-0.0.153/code_puppy/tui/tests/test_help.py +0 -38
  34. code_puppy-0.0.153/code_puppy/tui/tests/test_history_file_reader.py +0 -107
  35. code_puppy-0.0.153/code_puppy/tui/tests/test_input_area.py +0 -33
  36. code_puppy-0.0.153/code_puppy/tui/tests/test_settings.py +0 -44
  37. code_puppy-0.0.153/code_puppy/tui/tests/test_sidebar.py +0 -33
  38. code_puppy-0.0.153/code_puppy/tui/tests/test_sidebar_history.py +0 -153
  39. code_puppy-0.0.153/code_puppy/tui/tests/test_sidebar_history_navigation.py +0 -132
  40. code_puppy-0.0.153/code_puppy/tui/tests/test_status_bar.py +0 -54
  41. code_puppy-0.0.153/code_puppy/tui/tests/test_timestamped_history.py +0 -52
  42. code_puppy-0.0.153/code_puppy/tui/tests/test_tools.py +0 -82
  43. {code_puppy-0.0.153 → code_puppy-0.0.155}/.gitignore +0 -0
  44. {code_puppy-0.0.153 → code_puppy-0.0.155}/LICENSE +0 -0
  45. {code_puppy-0.0.153 → code_puppy-0.0.155}/README.md +0 -0
  46. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/__init__.py +0 -0
  47. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/__main__.py +0 -0
  48. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/agents/__init__.py +0 -0
  49. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/agents/agent_code_puppy.py +0 -0
  50. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/agents/agent_manager.py +0 -0
  51. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/agents/agent_orchestrator.json +0 -0
  52. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/agents/base_agent.py +0 -0
  53. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/agents/json_agent.py +0 -0
  54. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/callbacks.py +0 -0
  55. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/__init__.py +0 -0
  56. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/command_handler.py +0 -0
  57. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/file_path_completion.py +0 -0
  58. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/load_context_completion.py +0 -0
  59. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/__init__.py +0 -0
  60. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/add_command.py +0 -0
  61. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/base.py +0 -0
  62. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/handler.py +0 -0
  63. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/help_command.py +0 -0
  64. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/list_command.py +0 -0
  65. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/logs_command.py +0 -0
  66. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/remove_command.py +0 -0
  67. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/restart_command.py +0 -0
  68. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/search_command.py +0 -0
  69. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  70. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/start_command.py +0 -0
  71. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/status_command.py +0 -0
  72. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  73. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/stop_command.py +0 -0
  74. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/test_command.py +0 -0
  75. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/mcp/utils.py +0 -0
  76. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/meta_command_handler.py +0 -0
  77. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/model_picker_completion.py +0 -0
  78. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/motd.py +0 -0
  79. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  80. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/command_line/utils.py +0 -0
  81. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/http_utils.py +0 -0
  82. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/async_lifecycle.py +0 -0
  83. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/blocking_startup.py +0 -0
  84. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/captured_stdio_server.py +0 -0
  85. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/circuit_breaker.py +0 -0
  86. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/dashboard.py +0 -0
  87. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/error_isolation.py +0 -0
  88. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/examples/retry_example.py +0 -0
  89. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/health_monitor.py +0 -0
  90. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/managed_server.py +0 -0
  91. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/manager.py +0 -0
  92. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/registry.py +0 -0
  93. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/retry_manager.py +0 -0
  94. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/server_registry_catalog.py +0 -0
  95. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/status_tracker.py +0 -0
  96. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/mcp/system_tools.py +0 -0
  97. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/messaging/__init__.py +0 -0
  98. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/messaging/message_queue.py +0 -0
  99. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/messaging/queue_console.py +0 -0
  100. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/messaging/renderers.py +0 -0
  101. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/messaging/spinner/__init__.py +0 -0
  102. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  103. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
  104. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/plugins/__init__.py +0 -0
  105. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/reopenable_async_client.py +0 -0
  106. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/state_management.py +0 -0
  107. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/status_display.py +0 -0
  108. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tools/__init__.py +0 -0
  109. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tools/command_runner.py +0 -0
  110. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tools/common.py +0 -0
  111. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tools/file_modifications.py +0 -0
  112. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tools/tools_content.py +0 -0
  113. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/__init__.py +0 -0
  114. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/components/__init__.py +0 -0
  115. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/components/chat_view.py +0 -0
  116. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/components/command_history_modal.py +0 -0
  117. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/components/copy_button.py +0 -0
  118. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/components/custom_widgets.py +0 -0
  119. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/components/human_input_modal.py +0 -0
  120. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/components/input_area.py +0 -0
  121. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/components/sidebar.py +0 -0
  122. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/components/status_bar.py +0 -0
  123. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/messages.py +0 -0
  124. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/models/__init__.py +0 -0
  125. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/models/chat_message.py +0 -0
  126. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/models/command_history.py +0 -0
  127. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/models/enums.py +0 -0
  128. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/screens/__init__.py +0 -0
  129. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/screens/help.py +0 -0
  130. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
  131. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/screens/settings.py +0 -0
  132. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/tui/screens/tools.py +0 -0
  133. {code_puppy-0.0.153 → code_puppy-0.0.155}/code_puppy/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.153
3
+ Version: 0.0.155
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Topic :: Software Development :: Code Generators
16
- Requires-Python: >=3.10
16
+ Requires-Python: >=3.11
17
17
  Requires-Dist: bs4>=0.0.2
18
18
  Requires-Dist: fastapi>=0.110.0
19
19
  Requires-Dist: httpx-limiter>=0.3.0
@@ -24,17 +24,17 @@ from code_puppy.tools.common import console
24
24
 
25
25
  def load_puppy_rules():
26
26
  global PUPPY_RULES
27
-
27
+
28
28
  # Check for all 4 combinations of the rules file
29
29
  possible_paths = ["AGENTS.md", "AGENT.md", "agents.md", "agent.md"]
30
-
30
+
31
31
  for path_str in possible_paths:
32
32
  puppy_rules_path = Path(path_str)
33
33
  if puppy_rules_path.exists():
34
34
  with open(puppy_rules_path, "r") as f:
35
35
  puppy_rules = f.read()
36
36
  return puppy_rules
37
-
37
+
38
38
  # If none of the files exist, return None
39
39
  return None
40
40
 
@@ -206,4 +206,5 @@ def get_custom_usage_limits():
206
206
  Default pydantic-ai limit is 50, this increases it to the configured value (default 100).
207
207
  """
208
208
  from code_puppy.config import get_message_limit
209
+
209
210
  return UsageLimits(request_limit=get_message_limit())
@@ -390,7 +390,14 @@ Your goal is to take users from idea to working agent in one smooth conversation
390
390
 
391
391
  def get_available_tools(self) -> List[str]:
392
392
  """Get all tools needed for agent creation."""
393
- return ["list_files", "read_file", "edit_file", "agent_share_your_reasoning", "list_agents", "invoke_agent"]
393
+ return [
394
+ "list_files",
395
+ "read_file",
396
+ "edit_file",
397
+ "agent_share_your_reasoning",
398
+ "list_agents",
399
+ "invoke_agent",
400
+ ]
394
401
 
395
402
  def validate_agent_json(self, agent_config: Dict) -> List[str]:
396
403
  """Validate a JSON agent configuration.
@@ -485,4 +492,4 @@ Your goal is to take users from idea to working agent in one smooth conversation
485
492
 
486
493
  def get_user_prompt(self) -> Optional[str]:
487
494
  """Get the initial user prompt."""
488
- return "Hi! I'm the Agent Creator 🏗️ Let's build an awesome agent together!"
495
+ return "Hi! I'm the Agent Creator 🏗️ Let's build an awesome agent together!"
@@ -27,7 +27,7 @@ from pydantic_ai import Agent
27
27
  from pydantic_ai.exceptions import UsageLimitExceeded
28
28
  from pydantic_ai.usage import UsageLimits
29
29
 
30
- from code_puppy.messaging.message_queue import emit_info, emit_warning
30
+ from code_puppy.messaging.message_queue import emit_info
31
31
 
32
32
 
33
33
  class RuntimeAgentManager:
@@ -113,7 +113,10 @@ class RuntimeAgentManager:
113
113
  return await agent.run(prompt, usage_limits=usage_limits, **kwargs)
114
114
  except* UsageLimitExceeded as ule:
115
115
  emit_info(f"Usage limit exceeded: {str(ule)}", group_id=group_id)
116
- emit_info("The agent has reached its usage limit. You can ask it to continue by saying 'please continue' or similar.", group_id=group_id)
116
+ emit_info(
117
+ "The agent has reached its usage limit. You can ask it to continue by saying 'please continue' or similar.",
118
+ group_id=group_id,
119
+ )
117
120
  except* mcp.shared.exceptions.McpError as mcp_error:
118
121
  emit_info(f"MCP server error: {str(mcp_error)}", group_id=group_id)
119
122
  emit_info(f"{str(mcp_error)}", group_id=group_id)
@@ -132,7 +135,9 @@ class RuntimeAgentManager:
132
135
  if isinstance(exc, ExceptionGroup):
133
136
  for sub_exc in exc.exceptions:
134
137
  collect_non_cancelled_exceptions(sub_exc)
135
- elif not isinstance(exc, (asyncio.CancelledError, UsageLimitExceeded)):
138
+ elif not isinstance(
139
+ exc, (asyncio.CancelledError, UsageLimitExceeded)
140
+ ):
136
141
  remaining_exceptions.append(exc)
137
142
  emit_info(f"Unexpected error: {str(exc)}", group_id=group_id)
138
143
  emit_info(f"{str(exc.args)}", group_id=group_id)
@@ -226,7 +231,10 @@ class RuntimeAgentManager:
226
231
  except UsageLimitExceeded as ule:
227
232
  group_id = str(uuid.uuid4())
228
233
  emit_info(f"Usage limit exceeded: {str(ule)}", group_id=group_id)
229
- emit_info("The agent has reached its usage limit. You can ask it to continue by saying 'please continue' or similar.", group_id=group_id)
234
+ emit_info(
235
+ "The agent has reached its usage limit. You can ask it to continue by saying 'please continue' or similar.",
236
+ group_id=group_id,
237
+ )
230
238
  # Return None or some default value to indicate the limit was reached
231
239
  return None
232
240
 
@@ -158,10 +158,59 @@ class InstallCommand(MCPCommandBase):
158
158
  emit_info("Installation cancelled", message_group=group_id)
159
159
  return False
160
160
 
161
- # Install with default configuration (simplified)
161
+ # Collect environment variables and command line arguments
162
162
  env_vars = {}
163
163
  cmd_args = {}
164
164
 
165
+ # Get environment variables
166
+ required_env_vars = selected_server.get_environment_vars()
167
+ if required_env_vars:
168
+ emit_info(
169
+ "\n[yellow]Required Environment Variables:[/yellow]",
170
+ message_group=group_id,
171
+ )
172
+ for var in required_env_vars:
173
+ # Check if already set in environment
174
+ import os
175
+
176
+ current_value = os.environ.get(var, "")
177
+ if current_value:
178
+ emit_info(
179
+ f" {var}: [green]Already set[/green]",
180
+ message_group=group_id,
181
+ )
182
+ env_vars[var] = current_value
183
+ else:
184
+ value = emit_prompt(f" Enter value for {var}: ").strip()
185
+ if value:
186
+ env_vars[var] = value
187
+
188
+ # Get command line arguments
189
+ required_cmd_args = selected_server.get_command_line_args()
190
+ if required_cmd_args:
191
+ emit_info(
192
+ "\n[yellow]Command Line Arguments:[/yellow]", message_group=group_id
193
+ )
194
+ for arg_config in required_cmd_args:
195
+ name = arg_config.get("name", "")
196
+ prompt = arg_config.get("prompt", name)
197
+ default = arg_config.get("default", "")
198
+ required = arg_config.get("required", True)
199
+
200
+ # If required or has default, prompt user
201
+ if required or default:
202
+ arg_prompt = f" {prompt}"
203
+ if default:
204
+ arg_prompt += f" [{default}]"
205
+ if not required:
206
+ arg_prompt += " (optional)"
207
+
208
+ value = emit_prompt(f"{arg_prompt}: ").strip()
209
+ if value:
210
+ cmd_args[name] = value
211
+ elif default:
212
+ cmd_args[name] = default
213
+
165
214
  # Install the server
166
215
  return install_server_from_catalog(
167
216
  self.manager, selected_server, server_name, env_vars, cmd_args, group_id
@@ -43,9 +43,61 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
43
43
  if not server_name:
44
44
  return False
45
45
 
46
+ # Collect environment variables and command line arguments
47
+ env_vars = {}
48
+ cmd_args = {}
49
+
50
+ # Get environment variables
51
+ required_env_vars = selected_server.get_environment_vars()
52
+ if required_env_vars:
53
+ emit_info(
54
+ "\n[yellow]Required Environment Variables:[/yellow]",
55
+ message_group=group_id,
56
+ )
57
+ for var in required_env_vars:
58
+ # Check if already set in environment
59
+ import os
60
+
61
+ current_value = os.environ.get(var, "")
62
+ if current_value:
63
+ emit_info(
64
+ f" {var}: [green]Already set[/green]", message_group=group_id
65
+ )
66
+ env_vars[var] = current_value
67
+ else:
68
+ value = emit_prompt(f" Enter value for {var}: ").strip()
69
+ if value:
70
+ env_vars[var] = value
71
+
72
+ # Get command line arguments
73
+ required_cmd_args = selected_server.get_command_line_args()
74
+ if required_cmd_args:
75
+ emit_info(
76
+ "\n[yellow]Command Line Arguments:[/yellow]", message_group=group_id
77
+ )
78
+ for arg_config in required_cmd_args:
79
+ name = arg_config.get("name", "")
80
+ prompt = arg_config.get("prompt", name)
81
+ default = arg_config.get("default", "")
82
+ required = arg_config.get("required", True)
83
+
84
+ # If required or has default, prompt user
85
+ if required or default:
86
+ arg_prompt = f" {prompt}"
87
+ if default:
88
+ arg_prompt += f" [{default}]"
89
+ if not required:
90
+ arg_prompt += " (optional)"
91
+
92
+ value = emit_prompt(f"{arg_prompt}: ").strip()
93
+ if value:
94
+ cmd_args[name] = value
95
+ elif default:
96
+ cmd_args[name] = default
97
+
46
98
  # Configure the server
47
99
  return interactive_configure_server(
48
- manager, selected_server, server_name, group_id
100
+ manager, selected_server, server_name, group_id, env_vars, cmd_args
49
101
  )
50
102
 
51
103
  except ImportError:
@@ -131,7 +183,12 @@ def interactive_get_server_name(selected_server, group_id: str) -> Optional[str]
131
183
 
132
184
 
133
185
  def interactive_configure_server(
134
- manager, selected_server, server_name: str, group_id: str
186
+ manager,
187
+ selected_server,
188
+ server_name: str,
189
+ group_id: str,
190
+ env_vars: Dict[str, Any],
191
+ cmd_args: Dict[str, Any],
135
192
  ) -> bool:
136
193
  """
137
194
  Configure and install the selected server.
@@ -151,15 +208,20 @@ def interactive_configure_server(
151
208
  emit_info("Installation cancelled", message_group=group_id)
152
209
  return False
153
210
 
154
- # For now, use defaults - a full implementation would collect env vars, etc.
155
- # requirements = selected_server.get_requirements() # TODO: Use for validation
156
- env_vars = {}
157
- cmd_args = {}
158
-
159
211
  # Show confirmation
160
212
  emit_info(f"Installing: {selected_server.display_name}", message_group=group_id)
161
213
  emit_info(f"Name: {server_name}", message_group=group_id)
162
214
 
215
+ if env_vars:
216
+ emit_info("Environment Variables:", message_group=group_id)
217
+ for var, value in env_vars.items():
218
+ emit_info(f" {var}: [hidden]{value}[/hidden]", message_group=group_id)
219
+
220
+ if cmd_args:
221
+ emit_info("Command Line Arguments:", message_group=group_id)
222
+ for arg, value in cmd_args.items():
223
+ emit_info(f" {arg}: {value}", message_group=group_id)
224
+
163
225
  confirm = emit_prompt("Proceed with installation? [Y/n]: ")
164
226
  if confirm.lower().startswith("n"):
165
227
  emit_info("Installation cancelled", message_group=group_id)
@@ -196,18 +258,25 @@ def install_server_from_catalog(
196
258
  from code_puppy.config import MCP_SERVERS_FILE
197
259
  from code_puppy.mcp.managed_server import ServerConfig
198
260
 
199
- # Create server configuration
200
- config_dict = selected_server.get_config_template()
261
+ # Set environment variables in the current environment
262
+ for var, value in env_vars.items():
263
+ os.environ[var] = value
201
264
 
202
- # Apply environment variables and command args
203
- if env_vars:
204
- config_dict.update(env_vars)
205
- if cmd_args:
206
- config_dict.update(cmd_args)
265
+ # Get server config with command line argument overrides
266
+ config_dict = selected_server.to_server_config(server_name, **cmd_args)
267
+
268
+ # Update the config with actual environment variable values
269
+ if "env" in config_dict:
270
+ for env_key, env_value in config_dict["env"].items():
271
+ # If it's a placeholder like $GITHUB_TOKEN, replace with actual value
272
+ if env_value.startswith("$"):
273
+ var_name = env_value[1:] # Remove the $
274
+ if var_name in env_vars:
275
+ config_dict["env"][env_key] = env_vars[var_name]
207
276
 
208
277
  # Create ServerConfig
209
278
  server_config = ServerConfig(
210
- id=f"{server_name}_{hash(server_name)}",
279
+ id=server_name,
211
280
  name=server_name,
212
281
  type=selected_server.type,
213
282
  enabled=True,
@@ -234,8 +303,10 @@ def install_server_from_catalog(
234
303
  data = {"mcp_servers": servers}
235
304
 
236
305
  # Add new server
237
- servers[server_name] = config_dict.copy()
238
- servers[server_name]["type"] = selected_server.type
306
+ # Copy the config dict and add type before saving
307
+ save_config = config_dict.copy()
308
+ save_config["type"] = selected_server.type
309
+ servers[server_name] = save_config
239
310
 
240
311
  # Save back
241
312
  os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
@@ -106,7 +106,13 @@ def get_config_keys():
106
106
  Returns the list of all config keys currently in puppy.cfg,
107
107
  plus certain preset expected keys (e.g. "yolo_mode", "model", "compaction_strategy", "message_limit", "allow_recursion").
108
108
  """
109
- default_keys = ["yolo_mode", "model", "compaction_strategy", "message_limit", "allow_recursion"]
109
+ default_keys = [
110
+ "yolo_mode",
111
+ "model",
112
+ "compaction_strategy",
113
+ "message_limit",
114
+ "allow_recursion",
115
+ ]
110
116
  config = configparser.ConfigParser()
111
117
  config.read(CONFIG_FILE)
112
118
  keys = set(config[DEFAULT_SECTION].keys()) if DEFAULT_SECTION in config else set()
@@ -173,7 +179,7 @@ def _default_model_from_models_json():
173
179
  first_key = next(iter(models_config)) # Raises StopIteration if empty
174
180
  _default_model_cache = first_key
175
181
  return first_key
176
- except Exception as e:
182
+ except Exception:
177
183
  # Any problem (network, file missing, empty dict, etc.) => fall back
178
184
  _default_model_cache = "gpt-5"
179
185
  return "gpt-5"
@@ -290,7 +290,9 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
290
290
  if awaiting_input:
291
291
  # No spinner - use agent_manager's run_with_mcp method
292
292
  response = await agent_manager.run_with_mcp(
293
- initial_command, usage_limits=get_custom_usage_limits()
293
+ initial_command,
294
+ message_history=get_message_history(),
295
+ usage_limits=get_custom_usage_limits(),
294
296
  )
295
297
  else:
296
298
  # Use our custom spinner for better compatibility with user input
@@ -299,7 +301,11 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
299
301
  with ConsoleSpinner(console=display_console):
300
302
  # Use agent_manager's run_with_mcp method
301
303
  response = await agent_manager.run_with_mcp(
302
- initial_command, usage_limits=get_custom_usage_limits()
304
+ initial_command,
305
+ message_history=prune_interrupted_tool_calls(
306
+ get_message_history()
307
+ ),
308
+ usage_limits=get_custom_usage_limits(),
303
309
  )
304
310
  set_message_history(
305
311
  prune_interrupted_tool_calls(get_message_history())
@@ -426,7 +432,13 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
426
432
 
427
433
  runtime_manager = get_runtime_agent_manager()
428
434
  with ConsoleSpinner(console=message_renderer.console):
429
- result = await runtime_manager.run_with_mcp(task, get_custom_usage_limits())
435
+ result = await runtime_manager.run_with_mcp(
436
+ task,
437
+ get_custom_usage_limits(),
438
+ message_history=prune_interrupted_tool_calls(
439
+ get_message_history()
440
+ ),
441
+ )
430
442
  # Check if the task was cancelled (but don't show message if we just killed processes)
431
443
  if result is None:
432
444
  continue
@@ -495,7 +507,8 @@ async def execute_single_prompt(prompt: str, message_renderer) -> None:
495
507
 
496
508
  with ConsoleSpinner(console=message_renderer.console):
497
509
  response = await agent_manager.run_with_mcp(
498
- prompt, usage_limits=get_custom_usage_limits()
510
+ prompt,
511
+ usage_limits=get_custom_usage_limits(),
499
512
  )
500
513
 
501
514
  agent_response = response.output
@@ -1,7 +1,7 @@
1
1
  """MCP (Model Context Protocol) management system for Code Puppy.
2
2
 
3
- Note: Be careful not to create circular imports with config_wizard.py.
4
- config_wizard.py imports ServerConfig and get_mcp_manager directly from
3
+ Note: Be careful not to create circular imports with config_wizard.py.
4
+ config_wizard.py imports ServerConfig and get_mcp_manager directly from
5
5
  .manager to avoid circular dependencies with this package __init__.py
6
6
  """
7
7
 
@@ -1,7 +1,7 @@
1
1
  """
2
2
  MCP Configuration Wizard - Interactive setup for MCP servers.
3
3
 
4
- Note: This module imports ServerConfig and get_mcp_manager directly from
4
+ Note: This module imports ServerConfig and get_mcp_manager directly from
5
5
  .code_puppy.mcp.manager to avoid circular imports with the package __init__.py
6
6
  """
7
7
 
@@ -26,14 +26,6 @@ from code_puppy.summarization_agent import run_summarization_sync
26
26
  # Default is 50000 but can be customized in ~/.code_puppy/puppy.cfg
27
27
 
28
28
 
29
- def estimate_token_count(text: str) -> int:
30
- """
31
- Simple token estimation using len(message) - 4.
32
- This replaces tiktoken with a much simpler approach.
33
- """
34
- return int(max(1, len(text)) / 4)
35
-
36
-
37
29
  def stringify_message_part(part) -> str:
38
30
  """
39
31
  Convert a message part to a string representation for token estimation or other uses.
@@ -84,7 +76,7 @@ def estimate_tokens_for_message(message: ModelMessage) -> int:
84
76
  for part in message.parts:
85
77
  part_str = stringify_message_part(part)
86
78
  if part_str:
87
- total_tokens += estimate_token_count(part_str)
79
+ total_tokens += len(part_str)
88
80
 
89
81
  return int(max(1, total_tokens) / 4)
90
82
 
@@ -125,7 +125,7 @@ class ConsoleSpinner(SpinnerBase):
125
125
  self._live.refresh()
126
126
 
127
127
  # Short sleep to control animation speed
128
- time.sleep(0.1)
128
+ time.sleep(0.05)
129
129
  except Exception as e:
130
130
  print(f"\nSpinner error: {e}")
131
131
  self._is_spinning = False
@@ -6,9 +6,8 @@ from typing import Any, Dict
6
6
 
7
7
  import httpx
8
8
  from anthropic import AsyncAnthropic
9
- from openai import AsyncAzureOpenAI # For Azure OpenAI client
9
+ from openai import AsyncAzureOpenAI
10
10
  from pydantic_ai.models.anthropic import AnthropicModel
11
- from pydantic_ai.models.fallback import infer_model
12
11
  from pydantic_ai.models.gemini import GeminiModel
13
12
  from pydantic_ai.models.openai import OpenAIChatModel
14
13
  from pydantic_ai.providers.anthropic import AnthropicProvider
@@ -87,13 +86,13 @@ class ModelFactory:
87
86
  else:
88
87
  from code_puppy.config import MODELS_FILE
89
88
 
90
- if not pathlib.Path(MODELS_FILE).exists():
91
- with open(pathlib.Path(__file__).parent / "models.json", "r") as src:
92
- with open(pathlib.Path(MODELS_FILE), "w") as target:
93
- target.write(src.read())
89
+ with open(pathlib.Path(__file__).parent / "models.json", "r") as src:
90
+ with open(pathlib.Path(MODELS_FILE), "w") as target:
91
+ target.write(src.read())
94
92
 
95
93
  with open(MODELS_FILE, "r") as f:
96
94
  config = json.load(f)
95
+
97
96
  if pathlib.Path(EXTRA_MODELS_FILE).exists():
98
97
  with open(EXTRA_MODELS_FILE, "r") as f:
99
98
  extra_config = json.load(f)
@@ -248,25 +247,27 @@ class ModelFactory:
248
247
  model = OpenAIChatModel(model_name=model_config["name"], provider=provider)
249
248
  setattr(model, "provider", provider)
250
249
  return model
251
-
250
+
252
251
  elif model_type == "round_robin":
253
252
  # Get the list of model names to use in the round-robin
254
253
  model_names = model_config.get("models")
255
254
  if not model_names or not isinstance(model_names, list):
256
- raise ValueError(f"Round-robin model '{model_name}' requires a 'models' list in its configuration.")
257
-
255
+ raise ValueError(
256
+ f"Round-robin model '{model_name}' requires a 'models' list in its configuration."
257
+ )
258
+
258
259
  # Get the rotate_every parameter (default: 1)
259
260
  rotate_every = model_config.get("rotate_every", 1)
260
-
261
+
261
262
  # Resolve each model name to an actual model instance
262
263
  models = []
263
264
  for name in model_names:
264
265
  # Recursively get each model using the factory
265
266
  model = ModelFactory.get_model(name, config)
266
267
  models.append(model)
267
-
268
+
268
269
  # Create and return the round-robin model
269
270
  return RoundRobinModel(*models, rotate_every=rotate_every)
270
-
271
+
271
272
  else:
272
273
  raise ValueError(f"Unsupported model type: {model_type}")
@@ -1,4 +1,30 @@
1
1
  {
2
+ "openrouter-sonoma-dusk-alpha": {
3
+ "type": "custom_openai",
4
+ "name": "openrouter/sonoma-dusk-alpha",
5
+ "custom_endpoint": {
6
+ "url": "https://openrouter.ai/api/v1",
7
+ "api_key": "$OPENROUTER_API_KEY",
8
+ "headers": {
9
+ "HTTP-Referer": "https://github.com/mpfaffenberger/code_puppy",
10
+ "X-Title": "Code Puppy"
11
+ }
12
+ },
13
+ "context_length": 2000000
14
+ },
15
+ "openrouter-sonoma-sky-alpha": {
16
+ "type": "custom_openai",
17
+ "name": "openrouter/sonoma-sky-alpha",
18
+ "custom_endpoint": {
19
+ "url": "https://openrouter.ai/api/v1",
20
+ "api_key": "$OPENROUTER_API_KEY",
21
+ "headers": {
22
+ "HTTP-Referer": "https://github.com/mpfaffenberger/code_puppy",
23
+ "X-Title": "Code Puppy"
24
+ }
25
+ },
26
+ "context_length": 2000000
27
+ },
2
28
  "gpt-5": {
3
29
  "type": "openai",
4
30
  "name": "gpt-5",
@@ -1,10 +1,18 @@
1
-
2
1
  from contextlib import asynccontextmanager, suppress
3
2
  from dataclasses import dataclass, field
4
- from typing import Any, Callable, AsyncIterator, List
3
+ from typing import Any, AsyncIterator, List
5
4
 
6
- from pydantic_ai.models import Model, ModelMessage, ModelSettings, ModelRequestParameters, ModelResponse, StreamedResponse
7
- from pydantic_ai.models.fallback import KnownModelName, infer_model, merge_model_settings
5
+ from pydantic_ai.models import (
6
+ Model,
7
+ ModelMessage,
8
+ ModelSettings,
9
+ ModelRequestParameters,
10
+ ModelResponse,
11
+ StreamedResponse,
12
+ )
13
+ from pydantic_ai.models.fallback import (
14
+ merge_model_settings,
15
+ )
8
16
  from pydantic_ai.result import RunContext
9
17
 
10
18
  try:
@@ -15,18 +23,21 @@ except ImportError:
15
23
  class DummySpan:
16
24
  def is_recording(self):
17
25
  return False
26
+
18
27
  def set_attributes(self, attributes):
19
28
  pass
29
+
20
30
  return DummySpan()
21
31
 
32
+
22
33
  @dataclass(init=False)
23
34
  class RoundRobinModel(Model):
24
35
  """A model that cycles through multiple models in a round-robin fashion.
25
-
36
+
26
37
  This model distributes requests across multiple candidate models to help
27
38
  overcome rate limits or distribute load.
28
39
  """
29
-
40
+
30
41
  models: List[Model]
31
42
  _current_index: int = field(default=0, repr=False)
32
43
  _model_name: str = field(repr=False)
@@ -37,10 +48,10 @@ class RoundRobinModel(Model):
37
48
  self,
38
49
  *models: Model,
39
50
  rotate_every: int = 1,
40
- settings: ModelSettings | None = None
51
+ settings: ModelSettings | None = None,
41
52
  ):
42
53
  """Initialize a round-robin model instance.
43
-
54
+
44
55
  Args:
45
56
  models: The model instances to cycle through.
46
57
  rotate_every: Number of requests before rotating to the next model (default: 1).
@@ -59,9 +70,9 @@ class RoundRobinModel(Model):
59
70
  @property
60
71
  def model_name(self) -> str:
61
72
  """The model name showing this is a round-robin model with its candidates."""
62
- base_name = f'round_robin:{",".join(model.model_name for model in self.models)}'
73
+ base_name = f"round_robin:{','.join(model.model_name for model in self.models)}"
63
74
  if self._rotate_every != 1:
64
- return f'{base_name}:rotate_every={self._rotate_every}'
75
+ return f"{base_name}:rotate_every={self._rotate_every}"
65
76
  return base_name
66
77
 
67
78
  @property
@@ -93,10 +104,14 @@ class RoundRobinModel(Model):
93
104
  current_model = self._get_next_model()
94
105
  # Use the current model's settings as base, then merge with provided settings
95
106
  merged_settings = merge_model_settings(current_model.settings, model_settings)
96
- customized_model_request_parameters = current_model.customize_request_parameters(model_request_parameters)
97
-
107
+ customized_model_request_parameters = (
108
+ current_model.customize_request_parameters(model_request_parameters)
109
+ )
110
+
98
111
  try:
99
- response = await current_model.request(messages, merged_settings, customized_model_request_parameters)
112
+ response = await current_model.request(
113
+ messages, merged_settings, customized_model_request_parameters
114
+ )
100
115
  self._set_span_attributes(current_model)
101
116
  return response
102
117
  except Exception as exc:
@@ -116,8 +131,10 @@ class RoundRobinModel(Model):
116
131
  current_model = self._get_next_model()
117
132
  # Use the current model's settings as base, then merge with provided settings
118
133
  merged_settings = merge_model_settings(current_model.settings, model_settings)
119
- customized_model_request_parameters = current_model.customize_request_parameters(model_request_parameters)
120
-
134
+ customized_model_request_parameters = (
135
+ current_model.customize_request_parameters(model_request_parameters)
136
+ )
137
+
121
138
  async with current_model.request_stream(
122
139
  messages, merged_settings, customized_model_request_parameters, run_context
123
140
  ) as response:
@@ -129,6 +146,6 @@ class RoundRobinModel(Model):
129
146
  with suppress(Exception):
130
147
  span = get_current_span()
131
148
  if span.is_recording():
132
- attributes = getattr(span, 'attributes', {})
133
- if attributes.get('gen_ai.request.model') == self.model_name:
134
- span.set_attributes(model.model_attributes(model))
149
+ attributes = getattr(span, "attributes", {})
150
+ if attributes.get("gen_ai.request.model") == self.model_name:
151
+ span.set_attributes(model.model_attributes(model))