code-puppy 0.0.320__tar.gz → 0.0.323__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 (167) hide show
  1. {code_puppy-0.0.320 → code_puppy-0.0.323}/PKG-INFO +1 -1
  2. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/base_agent.py +154 -13
  3. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/cli_runner.py +2 -1
  4. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/keymap.py +8 -2
  5. code_puppy-0.0.323/code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  6. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
  7. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/command_runner.py +48 -21
  8. {code_puppy-0.0.320 → code_puppy-0.0.323}/pyproject.toml +1 -1
  9. code_puppy-0.0.320/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -186
  10. {code_puppy-0.0.320 → code_puppy-0.0.323}/.gitignore +0 -0
  11. {code_puppy-0.0.320 → code_puppy-0.0.323}/LICENSE +0 -0
  12. {code_puppy-0.0.320 → code_puppy-0.0.323}/README.md +0 -0
  13. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/__init__.py +0 -0
  14. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/__main__.py +0 -0
  15. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/__init__.py +0 -0
  16. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_c_reviewer.py +0 -0
  17. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_code_puppy.py +0 -0
  18. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_code_reviewer.py +0 -0
  19. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  20. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_creator_agent.py +0 -0
  21. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  22. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  23. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_manager.py +0 -0
  24. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_planning.py +0 -0
  25. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_python_programmer.py +0 -0
  26. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_python_reviewer.py +0 -0
  27. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_qa_expert.py +0 -0
  28. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_qa_kitten.py +0 -0
  29. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_security_auditor.py +0 -0
  30. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  31. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/json_agent.py +0 -0
  32. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/agents/prompt_reviewer.py +0 -0
  33. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/callbacks.py +0 -0
  34. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/chatgpt_codex_client.py +0 -0
  35. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/claude_cache_client.py +0 -0
  36. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/__init__.py +0 -0
  37. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/add_model_menu.py +0 -0
  38. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/attachments.py +0 -0
  39. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/autosave_menu.py +0 -0
  40. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/colors_menu.py +0 -0
  41. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/command_handler.py +0 -0
  42. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/command_registry.py +0 -0
  43. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/config_commands.py +0 -0
  44. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/core_commands.py +0 -0
  45. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/diff_menu.py +0 -0
  46. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/file_path_completion.py +0 -0
  47. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/load_context_completion.py +0 -0
  48. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/__init__.py +0 -0
  49. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/add_command.py +0 -0
  50. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/base.py +0 -0
  51. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  52. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  53. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  54. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/edit_command.py +0 -0
  55. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/handler.py +0 -0
  56. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/help_command.py +0 -0
  57. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/install_command.py +0 -0
  58. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/install_menu.py +0 -0
  59. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/list_command.py +0 -0
  60. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/logs_command.py +0 -0
  61. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/remove_command.py +0 -0
  62. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/restart_command.py +0 -0
  63. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/search_command.py +0 -0
  64. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  65. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/start_command.py +0 -0
  66. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/status_command.py +0 -0
  67. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  68. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/stop_command.py +0 -0
  69. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/test_command.py +0 -0
  70. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/utils.py +0 -0
  71. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  72. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/mcp_completion.py +0 -0
  73. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/model_picker_completion.py +0 -0
  74. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/model_settings_menu.py +0 -0
  75. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/motd.py +0 -0
  76. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/pin_command_completion.py +0 -0
  77. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  78. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/session_commands.py +0 -0
  79. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/command_line/utils.py +0 -0
  80. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/config.py +0 -0
  81. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/error_logging.py +0 -0
  82. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/gemini_code_assist.py +0 -0
  83. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/http_utils.py +0 -0
  84. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/main.py +0 -0
  85. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/__init__.py +0 -0
  86. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/async_lifecycle.py +0 -0
  87. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/blocking_startup.py +0 -0
  88. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  89. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/circuit_breaker.py +0 -0
  90. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/config_wizard.py +0 -0
  91. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/dashboard.py +0 -0
  92. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/error_isolation.py +0 -0
  93. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/examples/retry_example.py +0 -0
  94. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/health_monitor.py +0 -0
  95. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/managed_server.py +0 -0
  96. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/manager.py +0 -0
  97. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/mcp_logs.py +0 -0
  98. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/registry.py +0 -0
  99. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/retry_manager.py +0 -0
  100. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  101. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/status_tracker.py +0 -0
  102. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/mcp_/system_tools.py +0 -0
  103. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/__init__.py +0 -0
  104. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/bus.py +0 -0
  105. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/commands.py +0 -0
  106. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/markdown_patches.py +0 -0
  107. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/message_queue.py +0 -0
  108. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/messages.py +0 -0
  109. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/queue_console.py +0 -0
  110. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/renderers.py +0 -0
  111. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/rich_renderer.py +0 -0
  112. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/spinner/__init__.py +0 -0
  113. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  114. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  115. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/model_factory.py +0 -0
  116. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/model_utils.py +0 -0
  117. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/models.json +0 -0
  118. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/models_dev_api.json +0 -0
  119. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/models_dev_parser.py +0 -0
  120. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/__init__.py +0 -0
  121. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  122. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  123. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  124. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  125. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  126. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
  127. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  128. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  129. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  130. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  131. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
  132. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  133. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/utils.py +0 -0
  134. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  135. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  136. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/example_custom_command/README.md +0 -0
  137. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  138. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  139. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  140. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  141. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  142. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  143. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/prompts/codex_system_prompt.md +0 -0
  144. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/pydantic_patches.py +0 -0
  145. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/reopenable_async_client.py +0 -0
  146. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/round_robin_model.py +0 -0
  147. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/session_storage.py +0 -0
  148. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/status_display.py +0 -0
  149. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/summarization_agent.py +0 -0
  150. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/terminal_utils.py +0 -0
  151. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/__init__.py +0 -0
  152. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/agent_tools.py +0 -0
  153. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/__init__.py +0 -0
  154. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_control.py +0 -0
  155. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_interactions.py +0 -0
  156. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_locators.py +0 -0
  157. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_navigation.py +0 -0
  158. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  159. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_scripts.py +0 -0
  160. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_workflows.py +0 -0
  161. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  162. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/browser/vqa_agent.py +0 -0
  163. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/common.py +0 -0
  164. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/file_modifications.py +0 -0
  165. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/file_operations.py +0 -0
  166. {code_puppy-0.0.320 → code_puppy-0.0.323}/code_puppy/tools/tools_content.py +0 -0
  167. {code_puppy-0.0.320 → code_puppy-0.0.323}/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.320
3
+ Version: 0.0.323
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
@@ -4,11 +4,23 @@ import asyncio
4
4
  import json
5
5
  import math
6
6
  import signal
7
+ import sys
7
8
  import threading
8
9
  import uuid
9
10
  from abc import ABC, abstractmethod
10
11
  from collections.abc import AsyncIterable
11
- from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Union
12
+ from typing import (
13
+ Any,
14
+ Callable,
15
+ Dict,
16
+ List,
17
+ Optional,
18
+ Sequence,
19
+ Set,
20
+ Tuple,
21
+ Type,
22
+ Union,
23
+ )
12
24
 
13
25
  import mcp
14
26
  import pydantic
@@ -1230,6 +1242,74 @@ class BaseAgent(ABC):
1230
1242
  self._mcp_servers = mcp_servers
1231
1243
  return self._code_generation_agent
1232
1244
 
1245
+ def _create_agent_with_output_type(self, output_type: Type[Any]) -> PydanticAgent:
1246
+ """Create a temporary agent configured with a custom output_type.
1247
+
1248
+ This is used when structured output is requested via run_with_mcp.
1249
+ The agent is created fresh with the same configuration as the main agent
1250
+ but with the specified output_type instead of str.
1251
+
1252
+ Args:
1253
+ output_type: The Pydantic model or type for structured output.
1254
+
1255
+ Returns:
1256
+ A configured PydanticAgent (or DBOSAgent wrapper) with the custom output_type.
1257
+ """
1258
+ from code_puppy.model_utils import prepare_prompt_for_model
1259
+ from code_puppy.tools import register_tools_for_agent
1260
+
1261
+ model_name = self.get_model_name()
1262
+ models_config = ModelFactory.load_config()
1263
+ model, resolved_model_name = self._load_model_with_fallback(
1264
+ model_name, models_config, str(uuid.uuid4())
1265
+ )
1266
+
1267
+ instructions = self.get_system_prompt()
1268
+ puppy_rules = self.load_puppy_rules()
1269
+ if puppy_rules:
1270
+ instructions += f"\n{puppy_rules}"
1271
+
1272
+ mcp_servers = getattr(self, "_mcp_servers", []) or []
1273
+ model_settings = make_model_settings(resolved_model_name)
1274
+
1275
+ prepared = prepare_prompt_for_model(
1276
+ model_name, instructions, "", prepend_system_to_user=False
1277
+ )
1278
+ instructions = prepared.instructions
1279
+
1280
+ global _reload_count
1281
+ _reload_count += 1
1282
+
1283
+ if get_use_dbos():
1284
+ temp_agent = PydanticAgent(
1285
+ model=model,
1286
+ instructions=instructions,
1287
+ output_type=output_type,
1288
+ retries=3,
1289
+ toolsets=[],
1290
+ history_processors=[self.message_history_accumulator],
1291
+ model_settings=model_settings,
1292
+ )
1293
+ agent_tools = self.get_available_tools()
1294
+ register_tools_for_agent(temp_agent, agent_tools)
1295
+ dbos_agent = DBOSAgent(
1296
+ temp_agent, name=f"{self.name}-structured-{_reload_count}"
1297
+ )
1298
+ return dbos_agent
1299
+ else:
1300
+ temp_agent = PydanticAgent(
1301
+ model=model,
1302
+ instructions=instructions,
1303
+ output_type=output_type,
1304
+ retries=3,
1305
+ toolsets=mcp_servers,
1306
+ history_processors=[self.message_history_accumulator],
1307
+ model_settings=model_settings,
1308
+ )
1309
+ agent_tools = self.get_available_tools()
1310
+ register_tools_for_agent(temp_agent, agent_tools)
1311
+ return temp_agent
1312
+
1233
1313
  # It's okay to decorate it with DBOS.step even if not using DBOS; the decorator is a no-op in that case.
1234
1314
  @DBOS.step()
1235
1315
  def message_history_accumulator(self, ctx: RunContext, messages: List[Any]):
@@ -1590,6 +1670,7 @@ class BaseAgent(ABC):
1590
1670
  *,
1591
1671
  attachments: Optional[Sequence[BinaryContent]] = None,
1592
1672
  link_attachments: Optional[Sequence[Union[ImageUrl, DocumentUrl]]] = None,
1673
+ output_type: Optional[Type[Any]] = None,
1593
1674
  **kwargs,
1594
1675
  ) -> Any:
1595
1676
  """Run the agent with MCP servers, attachments, and full cancellation support.
@@ -1598,10 +1679,13 @@ class BaseAgent(ABC):
1598
1679
  prompt: Primary user prompt text (may be empty when attachments present).
1599
1680
  attachments: Local binary payloads (e.g., dragged images) to include.
1600
1681
  link_attachments: Remote assets (image/document URLs) to include.
1682
+ output_type: Optional Pydantic model or type for structured output.
1683
+ When provided, creates a temporary agent configured to return
1684
+ this type instead of the default string output.
1601
1685
  **kwargs: Additional arguments forwarded to `pydantic_ai.Agent.run`.
1602
1686
 
1603
1687
  Returns:
1604
- The agent's response.
1688
+ The agent's response (typed according to output_type if specified).
1605
1689
 
1606
1690
  Raises:
1607
1691
  asyncio.CancelledError: When execution is cancelled by user.
@@ -1625,6 +1709,11 @@ class BaseAgent(ABC):
1625
1709
  pydantic_agent = (
1626
1710
  self._code_generation_agent or self.reload_code_generation_agent()
1627
1711
  )
1712
+
1713
+ # If a custom output_type is specified, create a temporary agent with that type
1714
+ if output_type is not None:
1715
+ pydantic_agent = self._create_agent_with_output_type(output_type)
1716
+
1628
1717
  # Handle claude-code and chatgpt-codex models: prepend system prompt to first user message
1629
1718
  from code_puppy.model_utils import is_chatgpt_codex_model, is_claude_code_model
1630
1719
 
@@ -1822,29 +1911,72 @@ class BaseAgent(ABC):
1822
1911
  # When using keyboard-based cancel, SIGINT should be a no-op
1823
1912
  # (just show a hint to user about the configured cancel key)
1824
1913
  from code_puppy.keymap import get_cancel_agent_display_name
1914
+ import sys
1825
1915
 
1826
1916
  cancel_key = get_cancel_agent_display_name()
1827
- emit_info(f"Use {cancel_key} to cancel the agent task.")
1917
+ if sys.platform == "win32":
1918
+ # On Windows, we use keyboard listener, so SIGINT might still fire
1919
+ # but we handle cancellation via the key listener
1920
+ pass # Silent on Windows - the key listener handles it
1921
+ else:
1922
+ emit_info(f"Use {cancel_key} to cancel the agent task.")
1828
1923
 
1829
1924
  original_handler = None
1830
1925
  key_listener_stop_event = None
1831
1926
  _key_listener_thread = None
1927
+ _windows_ctrl_handler = None # Store reference to prevent garbage collection
1832
1928
 
1833
1929
  try:
1834
- if cancel_agent_uses_signal():
1835
- # Use SIGINT-based cancellation (default Ctrl+C behavior)
1930
+ if sys.platform == "win32":
1931
+ # Windows: Use SetConsoleCtrlHandler for reliable Ctrl+C handling
1932
+ import ctypes
1933
+
1934
+ # Define the handler function type
1935
+ HANDLER_ROUTINE = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_ulong)
1936
+
1937
+ def windows_ctrl_handler(ctrl_type):
1938
+ """Handle Windows console control events."""
1939
+ CTRL_C_EVENT = 0
1940
+ CTRL_BREAK_EVENT = 1
1941
+
1942
+ if ctrl_type in (CTRL_C_EVENT, CTRL_BREAK_EVENT):
1943
+ # Check if we're awaiting user input
1944
+ if is_awaiting_user_input():
1945
+ return False # Let default handler run
1946
+
1947
+ # Schedule agent cancellation
1948
+ schedule_agent_cancel()
1949
+ return True # We handled it, don't terminate
1950
+
1951
+ return False # Let other handlers process it
1952
+
1953
+ # Create the callback - must keep reference alive!
1954
+ _windows_ctrl_handler = HANDLER_ROUTINE(windows_ctrl_handler)
1955
+
1956
+ # Register the handler
1957
+ kernel32 = ctypes.windll.kernel32
1958
+ if not kernel32.SetConsoleCtrlHandler(_windows_ctrl_handler, True):
1959
+ emit_warning("Failed to set Windows Ctrl+C handler")
1960
+
1961
+ # Also spawn keyboard listener for Ctrl+X (shell cancel) and other keys
1962
+ key_listener_stop_event = threading.Event()
1963
+ _key_listener_thread = self._spawn_ctrl_x_key_listener(
1964
+ key_listener_stop_event,
1965
+ on_escape=lambda: None, # Ctrl+X handled by command_runner
1966
+ on_cancel_agent=None, # Ctrl+C handled by SetConsoleCtrlHandler above
1967
+ )
1968
+ elif cancel_agent_uses_signal():
1969
+ # Unix with Ctrl+C: Use SIGINT-based cancellation
1836
1970
  original_handler = signal.signal(
1837
1971
  signal.SIGINT, keyboard_interrupt_handler
1838
1972
  )
1839
1973
  else:
1840
- # Use keyboard listener for agent cancellation
1841
- # Set a graceful SIGINT handler that shows a hint
1974
+ # Unix with different cancel key: Use keyboard listener
1842
1975
  original_handler = signal.signal(signal.SIGINT, graceful_sigint_handler)
1843
- # Spawn keyboard listener with the cancel agent callback
1844
1976
  key_listener_stop_event = threading.Event()
1845
1977
  _key_listener_thread = self._spawn_ctrl_x_key_listener(
1846
1978
  key_listener_stop_event,
1847
- on_escape=lambda: None, # Ctrl+X handled by command_runner
1979
+ on_escape=lambda: None,
1848
1980
  on_cancel_agent=schedule_agent_cancel,
1849
1981
  )
1850
1982
 
@@ -1869,8 +2001,17 @@ class BaseAgent(ABC):
1869
2001
  # Stop keyboard listener if it was started
1870
2002
  if key_listener_stop_event is not None:
1871
2003
  key_listener_stop_event.set()
1872
- # Restore original signal handler
1873
- if (
1874
- original_handler is not None
1875
- ): # Explicit None check - SIG_DFL can be 0/falsy!
2004
+
2005
+ # Unregister Windows Ctrl handler
2006
+ if sys.platform == "win32" and _windows_ctrl_handler is not None:
2007
+ try:
2008
+ import ctypes
2009
+
2010
+ kernel32 = ctypes.windll.kernel32
2011
+ kernel32.SetConsoleCtrlHandler(_windows_ctrl_handler, False)
2012
+ except Exception:
2013
+ pass # Best effort cleanup
2014
+
2015
+ # Restore original signal handler (Unix)
2016
+ if original_handler is not None:
1876
2017
  signal.signal(signal.SIGINT, original_handler)
@@ -790,5 +790,6 @@ def main_entry():
790
790
  DBOS.destroy()
791
791
  return 0
792
792
  finally:
793
- # Reset terminal on Unix-like systems (not Windows)
793
+ # Reset terminal on all platforms for clean state
794
+ reset_windows_terminal_full() # Safe no-op on non-Windows
794
795
  reset_unix_terminal()
@@ -86,9 +86,15 @@ def cancel_agent_uses_signal() -> bool:
86
86
  """Check if the cancel agent key uses SIGINT (Ctrl+C).
87
87
 
88
88
  Returns:
89
- True if the cancel key is ctrl+c (uses SIGINT handler),
90
- False if it uses keyboard listener approach.
89
+ True if the cancel key is ctrl+c AND we're not on Windows
90
+ (uses SIGINT handler), False if it uses keyboard listener approach.
91
91
  """
92
+ import sys
93
+
94
+ # On Windows, always use keyboard listener - SIGINT is unreliable
95
+ if sys.platform == "win32":
96
+ return False
97
+
92
98
  return get_cancel_agent_key() == "ctrl+c"
93
99
 
94
100
 
@@ -0,0 +1,69 @@
1
+ """Shell command safety assessment agent.
2
+
3
+ This agent provides rapid risk assessment of shell commands before execution.
4
+ It's designed to be ultra-lightweight with a concise prompt (<200 tokens) and
5
+ uses structured output for reliable parsing.
6
+ """
7
+
8
+ from typing import TYPE_CHECKING, List
9
+
10
+ from code_puppy.agents.base_agent import BaseAgent
11
+
12
+ if TYPE_CHECKING:
13
+ pass
14
+
15
+
16
+ class ShellSafetyAgent(BaseAgent):
17
+ """Lightweight agent for assessing shell command safety risks.
18
+
19
+ This agent evaluates shell commands for potential risks including:
20
+ - File system destruction (rm -rf, dd, format, mkfs)
21
+ - Database operations (DROP, TRUNCATE, unfiltered UPDATE/DELETE)
22
+ - Privilege escalation (sudo, su, chmod 777)
23
+ - Network operations (wget/curl to unknown hosts)
24
+ - Data exfiltration patterns
25
+
26
+ The agent returns structured output with a risk level and brief reasoning.
27
+ """
28
+
29
+ @property
30
+ def name(self) -> str:
31
+ """Agent name for internal use."""
32
+ return "shell_safety_checker"
33
+
34
+ @property
35
+ def display_name(self) -> str:
36
+ """User-facing display name."""
37
+ return "Shell Safety Checker 🛡️"
38
+
39
+ @property
40
+ def description(self) -> str:
41
+ """Agent description."""
42
+ return "Lightweight agent that assesses shell command safety risks"
43
+
44
+ def get_system_prompt(self) -> str:
45
+ """Get the ultra-concise system prompt for shell safety assessment.
46
+
47
+ This prompt is kept under 200 tokens for fast inference and low cost.
48
+ """
49
+ return """You are a shell command safety analyzer. Assess risk levels concisely.
50
+
51
+ **Risk Levels:**
52
+ - none: Completely safe (ls, pwd, echo, cat readonly files)
53
+ - low: Minimal risk (mkdir, touch, git status, read-only queries)
54
+ - medium: Moderate risk (file edits, package installs, service restarts)
55
+ - high: Significant risk (rm files, UPDATE/DELETE without WHERE, TRUNCATE, chmod dangerous permissions)
56
+ - critical: Severe/destructive (rm -rf, DROP TABLE/DATABASE, dd, format, mkfs, bq delete dataset, unfiltered mass deletes)
57
+
58
+ **Evaluate:**
59
+ - Scope (single file vs. entire system)
60
+ - Reversibility (can it be undone?)
61
+ - Data loss potential
62
+ - Privilege requirements
63
+ - Database destruction patterns
64
+
65
+ **Output:** Risk level + reasoning (max 1 sentence)."""
66
+
67
+ def get_available_tools(self) -> List[str]:
68
+ """This agent uses no tools - pure reasoning only."""
69
+ return []
@@ -7,12 +7,42 @@ and assesses their safety risk before execution.
7
7
  from typing import Any, Dict, Optional
8
8
 
9
9
  from code_puppy.callbacks import register_callback
10
- from code_puppy.config import get_safety_permission_level, get_yolo_mode
10
+ from code_puppy.config import (
11
+ get_global_model_name,
12
+ get_safety_permission_level,
13
+ get_yolo_mode,
14
+ )
11
15
  from code_puppy.messaging import emit_info
12
16
  from code_puppy.plugins.shell_safety.command_cache import (
13
17
  cache_assessment,
14
18
  get_cached_assessment,
15
19
  )
20
+ from code_puppy.tools.command_runner import ShellSafetyAssessment
21
+
22
+ # OAuth model prefixes - these models have their own safety mechanisms
23
+ OAUTH_MODEL_PREFIXES = (
24
+ "claude-code-", # Anthropic OAuth
25
+ "chatgpt-", # OpenAI OAuth
26
+ "gemini-oauth", # Google OAuth
27
+ )
28
+
29
+
30
+ def is_oauth_model(model_name: str | None) -> bool:
31
+ """Check if the model is an OAuth model that should skip safety checks.
32
+
33
+ OAuth models have their own built-in safety mechanisms, so we skip
34
+ the shell safety callback to avoid redundant checks and potential bugs.
35
+
36
+ Args:
37
+ model_name: The name of the current model
38
+
39
+ Returns:
40
+ True if the model is an OAuth model, False otherwise
41
+ """
42
+ if not model_name:
43
+ return False
44
+ return model_name.startswith(OAUTH_MODEL_PREFIXES)
45
+
16
46
 
17
47
  # Risk level hierarchy for numeric comparison
18
48
  # Lower numbers = safer commands, higher numbers = more dangerous
@@ -68,6 +98,11 @@ async def shell_safety_callback(
68
98
  None if command is safe to proceed
69
99
  Dict with rejection info if command should be blocked
70
100
  """
101
+ # Skip safety checks for OAuth models - they have their own safety mechanisms
102
+ current_model = get_global_model_name()
103
+ if is_oauth_model(current_model):
104
+ return None
105
+
71
106
  # Only check safety in yolo_mode - otherwise user is reviewing manually
72
107
  yolo_mode = get_yolo_mode()
73
108
  if not yolo_mode:
@@ -108,8 +143,14 @@ async def shell_safety_callback(
108
143
  # Create agent and assess command
109
144
  agent = ShellSafetyAgent()
110
145
 
111
- # Run async assessment (we're in an async callback now!)
112
- assessment = await agent.assess_command(command, cwd)
146
+ # Build the assessment prompt with optional cwd context
147
+ prompt = f"Assess this shell command:\n\nCommand: {command}"
148
+ if cwd:
149
+ prompt += f"\nWorking directory: {cwd}"
150
+
151
+ # Run async assessment with structured output type
152
+ result = await agent.run_with_mcp(prompt, output_type=ShellSafetyAssessment)
153
+ assessment = result.output
113
154
 
114
155
  # Cache the result for future use, but only if it's not a fallback assessment
115
156
  if not getattr(assessment, "is_fallback", False):
@@ -192,6 +192,11 @@ def kill_all_running_shell_processes() -> int:
192
192
  """Kill all currently tracked running shell processes and stop reader threads.
193
193
 
194
194
  Returns the number of processes signaled.
195
+
196
+ Implementation notes:
197
+ - Atomically snapshot and clear the registry to prevent race conditions
198
+ - Deduplicate by PID to ensure each process is killed at most once
199
+ - Let exceptions from _kill_process_group propagate (tests expect this)
195
200
  """
196
201
  global _READER_STOP_EVENT
197
202
 
@@ -199,30 +204,52 @@ def kill_all_running_shell_processes() -> int:
199
204
  if _READER_STOP_EVENT:
200
205
  _READER_STOP_EVENT.set()
201
206
 
202
- procs: list[subprocess.Popen]
207
+ # Atomically take snapshot and clear registry
208
+ # This prevents other threads from seeing/processing the same processes
203
209
  with _RUNNING_PROCESSES_LOCK:
204
- procs = list(_RUNNING_PROCESSES)
205
- count = 0
206
- for p in procs:
210
+ procs_snapshot = list(_RUNNING_PROCESSES)
211
+ _RUNNING_PROCESSES.clear()
212
+
213
+ # Deduplicate by pid to ensure at-most-one kill per process
214
+ seen_pids: set = set()
215
+ killed_count = 0
216
+
217
+ for proc in procs_snapshot:
218
+ if proc is None:
219
+ continue
220
+
221
+ pid = getattr(proc, "pid", None)
222
+ key = pid if pid is not None else id(proc)
223
+
224
+ if key in seen_pids:
225
+ continue
226
+ seen_pids.add(key)
227
+
228
+ # Close pipes first to unblock readline()
207
229
  try:
208
- # Close pipes first to unblock readline()
209
- try:
210
- if p.stdout and not p.stdout.closed:
211
- p.stdout.close()
212
- if p.stderr and not p.stderr.closed:
213
- p.stderr.close()
214
- if p.stdin and not p.stdin.closed:
215
- p.stdin.close()
216
- except (OSError, ValueError):
217
- pass
230
+ if proc.stdout and not proc.stdout.closed:
231
+ proc.stdout.close()
232
+ if proc.stderr and not proc.stderr.closed:
233
+ proc.stderr.close()
234
+ if proc.stdin and not proc.stdin.closed:
235
+ proc.stdin.close()
236
+ except (OSError, ValueError):
237
+ pass
238
+
239
+ # Only attempt to kill processes that are still running
240
+ if proc.poll() is None:
241
+ # Let exceptions bubble up (tests expect this behavior)
242
+ _kill_process_group(proc)
243
+ killed_count += 1
244
+
245
+ # Track user-killed PIDs
246
+ if pid is not None:
247
+ try:
248
+ _USER_KILLED_PROCESSES.add(pid)
249
+ except Exception:
250
+ pass # Non-fatal bookkeeping
218
251
 
219
- if p.poll() is None:
220
- _kill_process_group(p)
221
- count += 1
222
- _USER_KILLED_PROCESSES.add(p.pid)
223
- finally:
224
- _unregister_process(p)
225
- return count
252
+ return killed_count
226
253
 
227
254
 
228
255
  def get_running_shell_process_count() -> int:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.320"
7
+ version = "0.0.323"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.14"