code-puppy 0.0.315__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 (168) hide show
  1. {code_puppy-0.0.315 → code_puppy-0.0.323}/PKG-INFO +1 -1
  2. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/base_agent.py +205 -113
  3. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/cli_runner.py +8 -1
  4. code_puppy-0.0.323/code_puppy/command_line/mcp/logs_command.py +235 -0
  5. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/model_settings_menu.py +6 -0
  6. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/keymap.py +8 -2
  7. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/__init__.py +17 -0
  8. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/blocking_startup.py +61 -32
  9. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/managed_server.py +23 -3
  10. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/manager.py +65 -0
  11. code_puppy-0.0.323/code_puppy/mcp_/mcp_logs.py +224 -0
  12. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/__init__.py +9 -0
  13. code_puppy-0.0.323/code_puppy/messaging/markdown_patches.py +57 -0
  14. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/model_factory.py +54 -0
  15. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/models.json +1 -1
  16. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/utils.py +1 -0
  17. code_puppy-0.0.323/code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  18. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
  19. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/command_runner.py +48 -21
  20. {code_puppy-0.0.315 → code_puppy-0.0.323}/pyproject.toml +2 -1
  21. code_puppy-0.0.315/code_puppy/command_line/mcp/logs_command.py +0 -126
  22. code_puppy-0.0.315/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -186
  23. {code_puppy-0.0.315 → code_puppy-0.0.323}/.gitignore +0 -0
  24. {code_puppy-0.0.315 → code_puppy-0.0.323}/LICENSE +0 -0
  25. {code_puppy-0.0.315 → code_puppy-0.0.323}/README.md +0 -0
  26. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/__init__.py +0 -0
  27. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/__main__.py +0 -0
  28. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/__init__.py +0 -0
  29. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_c_reviewer.py +0 -0
  30. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_code_puppy.py +0 -0
  31. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_code_reviewer.py +0 -0
  32. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  33. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_creator_agent.py +0 -0
  34. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  35. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  36. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_manager.py +0 -0
  37. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_planning.py +0 -0
  38. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_python_programmer.py +0 -0
  39. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_python_reviewer.py +0 -0
  40. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_qa_expert.py +0 -0
  41. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_qa_kitten.py +0 -0
  42. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_security_auditor.py +0 -0
  43. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  44. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/json_agent.py +0 -0
  45. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/prompt_reviewer.py +0 -0
  46. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/callbacks.py +0 -0
  47. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/chatgpt_codex_client.py +0 -0
  48. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/claude_cache_client.py +0 -0
  49. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/__init__.py +0 -0
  50. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/add_model_menu.py +0 -0
  51. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/attachments.py +0 -0
  52. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/autosave_menu.py +0 -0
  53. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/colors_menu.py +0 -0
  54. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/command_handler.py +0 -0
  55. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/command_registry.py +0 -0
  56. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/config_commands.py +0 -0
  57. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/core_commands.py +0 -0
  58. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/diff_menu.py +0 -0
  59. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/file_path_completion.py +0 -0
  60. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/load_context_completion.py +0 -0
  61. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/__init__.py +0 -0
  62. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/add_command.py +0 -0
  63. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/base.py +0 -0
  64. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  65. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  66. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  67. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/edit_command.py +0 -0
  68. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/handler.py +0 -0
  69. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/help_command.py +0 -0
  70. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/install_command.py +0 -0
  71. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/install_menu.py +0 -0
  72. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/list_command.py +0 -0
  73. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/remove_command.py +0 -0
  74. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/restart_command.py +0 -0
  75. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/search_command.py +0 -0
  76. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  77. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/start_command.py +0 -0
  78. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/status_command.py +0 -0
  79. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  80. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/stop_command.py +0 -0
  81. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/test_command.py +0 -0
  82. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/utils.py +0 -0
  83. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  84. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp_completion.py +0 -0
  85. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/model_picker_completion.py +0 -0
  86. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/motd.py +0 -0
  87. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/pin_command_completion.py +0 -0
  88. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  89. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/session_commands.py +0 -0
  90. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/utils.py +0 -0
  91. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/config.py +0 -0
  92. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/error_logging.py +0 -0
  93. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/gemini_code_assist.py +0 -0
  94. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/http_utils.py +0 -0
  95. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/main.py +0 -0
  96. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/async_lifecycle.py +0 -0
  97. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  98. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/circuit_breaker.py +0 -0
  99. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/config_wizard.py +0 -0
  100. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/dashboard.py +0 -0
  101. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/error_isolation.py +0 -0
  102. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/examples/retry_example.py +0 -0
  103. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/health_monitor.py +0 -0
  104. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/registry.py +0 -0
  105. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/retry_manager.py +0 -0
  106. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  107. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/status_tracker.py +0 -0
  108. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/system_tools.py +0 -0
  109. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/bus.py +0 -0
  110. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/commands.py +0 -0
  111. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/message_queue.py +0 -0
  112. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/messages.py +0 -0
  113. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/queue_console.py +0 -0
  114. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/renderers.py +0 -0
  115. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/rich_renderer.py +0 -0
  116. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/spinner/__init__.py +0 -0
  117. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  118. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  119. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/model_utils.py +0 -0
  120. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/models_dev_api.json +0 -0
  121. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/models_dev_parser.py +0 -0
  122. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/__init__.py +0 -0
  123. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  124. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  125. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  126. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  127. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  128. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
  129. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  130. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  131. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  132. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  133. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
  134. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  135. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  136. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  137. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/example_custom_command/README.md +0 -0
  138. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  139. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  140. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  141. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  142. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  143. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  144. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/prompts/codex_system_prompt.md +0 -0
  145. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/pydantic_patches.py +0 -0
  146. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/reopenable_async_client.py +0 -0
  147. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/round_robin_model.py +0 -0
  148. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/session_storage.py +0 -0
  149. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/status_display.py +0 -0
  150. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/summarization_agent.py +0 -0
  151. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/terminal_utils.py +0 -0
  152. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/__init__.py +0 -0
  153. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/agent_tools.py +0 -0
  154. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/__init__.py +0 -0
  155. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_control.py +0 -0
  156. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_interactions.py +0 -0
  157. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_locators.py +0 -0
  158. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_navigation.py +0 -0
  159. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  160. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_scripts.py +0 -0
  161. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_workflows.py +0 -0
  162. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  163. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/vqa_agent.py +0 -0
  164. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/common.py +0 -0
  165. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/file_modifications.py +0 -0
  166. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/file_operations.py +0 -0
  167. {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/tools_content.py +0 -0
  168. {code_puppy-0.0.315 → 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.315
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
@@ -47,11 +59,10 @@ from code_puppy.config import (
47
59
  get_protected_token_count,
48
60
  get_use_dbos,
49
61
  get_value,
50
- load_mcp_server_configs,
51
62
  )
52
63
  from code_puppy.error_logging import log_error
53
64
  from code_puppy.keymap import cancel_agent_uses_signal, get_cancel_agent_char_code
54
- from code_puppy.mcp_ import ServerConfig, get_mcp_manager
65
+ from code_puppy.mcp_ import get_mcp_manager
55
66
  from code_puppy.messaging import (
56
67
  emit_error,
57
68
  emit_info,
@@ -90,6 +101,9 @@ class BaseAgent(ABC):
90
101
  # Cache for MCP tool definitions (for token estimation)
91
102
  # This is populated after the first successful run when MCP tools are retrieved
92
103
  self._mcp_tool_definitions_cache: List[Dict[str, Any]] = []
104
+ # Shared console for streaming output - should be set by cli_runner
105
+ # to avoid conflicts between spinner's Live display and response streaming
106
+ self._console: Optional[Any] = None
93
107
 
94
108
  @property
95
109
  @abstractmethod
@@ -989,45 +1003,31 @@ class BaseAgent(ABC):
989
1003
  return self._puppy_rules
990
1004
 
991
1005
  def load_mcp_servers(self, extra_headers: Optional[Dict[str, str]] = None):
992
- """Load MCP servers through the manager and return pydantic-ai compatible servers."""
1006
+ """Load MCP servers through the manager and return pydantic-ai compatible servers.
1007
+
1008
+ Note: The manager automatically syncs from mcp_servers.json during initialization,
1009
+ so we don't need to sync here. Use reload_mcp_servers() to force a re-sync.
1010
+ """
993
1011
 
994
1012
  mcp_disabled = get_value("disable_mcp_servers")
995
1013
  if mcp_disabled and str(mcp_disabled).lower() in ("1", "true", "yes", "on"):
996
1014
  return []
997
1015
 
998
1016
  manager = get_mcp_manager()
999
- configs = load_mcp_server_configs()
1000
- if not configs:
1001
- existing_servers = manager.list_servers()
1002
- if not existing_servers:
1003
- return []
1004
- else:
1005
- for name, conf in configs.items():
1006
- try:
1007
- server_config = ServerConfig(
1008
- id=conf.get("id", f"{name}_{hash(name)}"),
1009
- name=name,
1010
- type=conf.get("type", "sse"),
1011
- enabled=conf.get("enabled", True),
1012
- config=conf,
1013
- )
1014
- existing = manager.get_server_by_name(name)
1015
- if not existing:
1016
- manager.register_server(server_config)
1017
- else:
1018
- if existing.config != server_config.config:
1019
- manager.update_server(existing.id, server_config)
1020
- except Exception:
1021
- continue
1022
-
1023
1017
  return manager.get_servers_for_agent()
1024
1018
 
1025
1019
  def reload_mcp_servers(self):
1026
- """Reload MCP servers and return updated servers."""
1020
+ """Reload MCP servers and return updated servers.
1021
+
1022
+ Forces a re-sync from mcp_servers.json to pick up any configuration changes.
1023
+ """
1027
1024
  # Clear the MCP tool cache when servers are reloaded
1028
1025
  self._mcp_tool_definitions_cache = []
1029
- self.load_mcp_servers()
1026
+
1027
+ # Force re-sync from mcp_servers.json
1030
1028
  manager = get_mcp_manager()
1029
+ manager.sync_from_config()
1030
+
1031
1031
  return manager.get_servers_for_agent()
1032
1032
 
1033
1033
  def _load_model_with_fallback(
@@ -1242,6 +1242,74 @@ class BaseAgent(ABC):
1242
1242
  self._mcp_servers = mcp_servers
1243
1243
  return self._code_generation_agent
1244
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
+
1245
1313
  # It's okay to decorate it with DBOS.step even if not using DBOS; the decorator is a no-op in that case.
1246
1314
  @DBOS.step()
1247
1315
  def message_history_accumulator(self, ctx: RunContext, messages: List[Any]):
@@ -1279,27 +1347,22 @@ class BaseAgent(ABC):
1279
1347
  ctx: The run context.
1280
1348
  events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
1281
1349
  """
1282
- import os
1283
- import time as time_module
1284
-
1285
1350
  from pydantic_ai import PartDeltaEvent, PartStartEvent
1286
1351
  from pydantic_ai.messages import TextPartDelta, ThinkingPartDelta
1287
1352
  from rich.console import Console
1288
- from rich.live import Live
1289
1353
  from rich.markdown import Markdown
1290
1354
  from rich.markup import escape
1291
1355
 
1292
1356
  from code_puppy.messaging.spinner import pause_all_spinners
1293
1357
 
1294
- console = Console()
1295
-
1296
- # Disable Live display in test mode or non-interactive environments
1297
- # This fixes issues with pexpect PTY where Live() hangs
1298
- use_live_display = (
1299
- console.is_terminal
1300
- and os.environ.get("CODE_PUPPY_TEST_FAST", "").lower() not in ("1", "true")
1301
- and os.environ.get("CI", "").lower() not in ("1", "true")
1302
- )
1358
+ # IMPORTANT: Use the shared console (set by cli_runner) to avoid conflicts
1359
+ # with the spinner's Live display. Multiple Console instances with separate
1360
+ # Live displays cause cursor positioning chaos and line duplication.
1361
+ if self._console is not None:
1362
+ console = self._console
1363
+ else:
1364
+ # Fallback if console not set (shouldn't happen in normal use)
1365
+ console = Console()
1303
1366
 
1304
1367
  # Track which part indices we're currently streaming (for Text/Thinking parts)
1305
1368
  streaming_parts: set[int] = set()
@@ -1308,11 +1371,9 @@ class BaseAgent(ABC):
1308
1371
  ) # Track which parts are thinking (for dim style)
1309
1372
  text_parts: set[int] = set() # Track which parts are text
1310
1373
  banner_printed: set[int] = set() # Track if banner was already printed
1311
- text_buffer: dict[int, list[str]] = {} # Buffer text for markdown
1312
- live_displays: dict[int, Live] = {} # Live displays for streaming markdown
1374
+ text_buffer: dict[int, list[str]] = {} # Buffer text for final markdown render
1375
+ token_count: dict[int, int] = {} # Track token count per text part
1313
1376
  did_stream_anything = False # Track if we streamed any content
1314
- last_render_time: dict[int, float] = {} # Track last render time per part
1315
- render_interval = 0.1 # Only re-render markdown every 100ms (throttle)
1316
1377
 
1317
1378
  def _print_thinking_banner() -> None:
1318
1379
  """Print the THINKING banner with spinner pause and line clear."""
@@ -1377,9 +1438,11 @@ class BaseAgent(ABC):
1377
1438
  streaming_parts.add(event.index)
1378
1439
  text_parts.add(event.index)
1379
1440
  text_buffer[event.index] = [] # Initialize buffer
1441
+ token_count[event.index] = 0 # Initialize token counter
1380
1442
  # Buffer initial content if present
1381
1443
  if part.content and part.content.strip():
1382
1444
  text_buffer[event.index].append(part.content)
1445
+ token_count[event.index] += 1
1383
1446
 
1384
1447
  # PartDeltaEvent - stream the content as it arrives
1385
1448
  elif isinstance(event, PartDeltaEvent):
@@ -1387,43 +1450,23 @@ class BaseAgent(ABC):
1387
1450
  delta = event.delta
1388
1451
  if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
1389
1452
  if delta.content_delta:
1390
- # For text parts, stream markdown with Live display
1453
+ # For text parts, show token counter then render at end
1391
1454
  if event.index in text_parts:
1392
- # Print banner and start Live on first content
1455
+ import sys
1456
+
1457
+ # Print banner on first content
1393
1458
  if event.index not in banner_printed:
1394
1459
  _print_response_banner()
1395
1460
  banner_printed.add(event.index)
1396
- # Only use Live display if enabled (disabled in test/CI)
1397
- if use_live_display:
1398
- live = Live(
1399
- Markdown(""),
1400
- console=console,
1401
- refresh_per_second=10,
1402
- vertical_overflow="visible", # Allow scrolling for long content
1403
- )
1404
- live.start()
1405
- live_displays[event.index] = live
1406
- # Accumulate text and throttle markdown rendering
1407
- # (Markdown parsing is O(n), doing it on every token = O(n²) death)
1461
+ # Accumulate text for final markdown render
1408
1462
  text_buffer[event.index].append(delta.content_delta)
1409
- now = time_module.monotonic()
1410
- last_render = last_render_time.get(event.index, 0)
1411
-
1412
- # Only re-render if enough time has passed (throttle)
1413
- # Skip Live updates when not using live display
1414
- if (
1415
- use_live_display
1416
- and now - last_render >= render_interval
1417
- ):
1418
- content = "".join(text_buffer[event.index])
1419
- if event.index in live_displays:
1420
- try:
1421
- live_displays[event.index].update(
1422
- Markdown(content)
1423
- )
1424
- last_render_time[event.index] = now
1425
- except Exception:
1426
- pass
1463
+ token_count[event.index] += 1
1464
+ # Update token counter in place (single line)
1465
+ count = token_count[event.index]
1466
+ sys.stdout.write(
1467
+ f"\r\x1b[K ⏳ Receiving... {count} tokens"
1468
+ )
1469
+ sys.stdout.flush()
1427
1470
  else:
1428
1471
  # For thinking parts, stream immediately (dim)
1429
1472
  if event.index not in banner_printed:
@@ -1435,36 +1478,24 @@ class BaseAgent(ABC):
1435
1478
  # PartEndEvent - finish the streaming with a newline
1436
1479
  elif isinstance(event, PartEndEvent):
1437
1480
  if event.index in streaming_parts:
1438
- # For text parts, do final render then stop the Live display
1481
+ # For text parts, clear counter line and render markdown
1439
1482
  if event.index in text_parts:
1440
- # Final render to ensure we show complete content
1441
- # (throttling may have skipped the last few tokens)
1442
- if event.index in live_displays and event.index in text_buffer:
1443
- try:
1444
- final_content = "".join(text_buffer[event.index])
1445
- live_displays[event.index].update(
1446
- Markdown(final_content)
1447
- )
1448
- except Exception:
1449
- pass
1450
- if event.index in live_displays:
1451
- try:
1452
- live_displays[event.index].stop()
1453
- except Exception:
1454
- pass
1455
- del live_displays[event.index]
1456
- # When not using Live display, print the final content as markdown
1457
- elif event.index in text_buffer:
1483
+ import sys
1484
+
1485
+ # Clear the token counter line
1486
+ sys.stdout.write("\r\x1b[K")
1487
+ sys.stdout.flush()
1488
+ # Render the final markdown nicely
1489
+ if event.index in text_buffer:
1458
1490
  try:
1459
1491
  final_content = "".join(text_buffer[event.index])
1460
1492
  if final_content.strip():
1461
1493
  console.print(Markdown(final_content))
1462
1494
  except Exception:
1463
1495
  pass
1464
- if event.index in text_buffer:
1465
1496
  del text_buffer[event.index]
1466
- # Clean up render time tracking
1467
- last_render_time.pop(event.index, None)
1497
+ # Clean up token count
1498
+ token_count.pop(event.index, None)
1468
1499
  # For thinking parts, just print newline
1469
1500
  elif event.index in banner_printed:
1470
1501
  console.print() # Final newline after streaming
@@ -1639,6 +1670,7 @@ class BaseAgent(ABC):
1639
1670
  *,
1640
1671
  attachments: Optional[Sequence[BinaryContent]] = None,
1641
1672
  link_attachments: Optional[Sequence[Union[ImageUrl, DocumentUrl]]] = None,
1673
+ output_type: Optional[Type[Any]] = None,
1642
1674
  **kwargs,
1643
1675
  ) -> Any:
1644
1676
  """Run the agent with MCP servers, attachments, and full cancellation support.
@@ -1647,10 +1679,13 @@ class BaseAgent(ABC):
1647
1679
  prompt: Primary user prompt text (may be empty when attachments present).
1648
1680
  attachments: Local binary payloads (e.g., dragged images) to include.
1649
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.
1650
1685
  **kwargs: Additional arguments forwarded to `pydantic_ai.Agent.run`.
1651
1686
 
1652
1687
  Returns:
1653
- The agent's response.
1688
+ The agent's response (typed according to output_type if specified).
1654
1689
 
1655
1690
  Raises:
1656
1691
  asyncio.CancelledError: When execution is cancelled by user.
@@ -1674,6 +1709,11 @@ class BaseAgent(ABC):
1674
1709
  pydantic_agent = (
1675
1710
  self._code_generation_agent or self.reload_code_generation_agent()
1676
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
+
1677
1717
  # Handle claude-code and chatgpt-codex models: prepend system prompt to first user message
1678
1718
  from code_puppy.model_utils import is_chatgpt_codex_model, is_claude_code_model
1679
1719
 
@@ -1871,29 +1911,72 @@ class BaseAgent(ABC):
1871
1911
  # When using keyboard-based cancel, SIGINT should be a no-op
1872
1912
  # (just show a hint to user about the configured cancel key)
1873
1913
  from code_puppy.keymap import get_cancel_agent_display_name
1914
+ import sys
1874
1915
 
1875
1916
  cancel_key = get_cancel_agent_display_name()
1876
- 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.")
1877
1923
 
1878
1924
  original_handler = None
1879
1925
  key_listener_stop_event = None
1880
1926
  _key_listener_thread = None
1927
+ _windows_ctrl_handler = None # Store reference to prevent garbage collection
1881
1928
 
1882
1929
  try:
1883
- if cancel_agent_uses_signal():
1884
- # 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
1885
1970
  original_handler = signal.signal(
1886
1971
  signal.SIGINT, keyboard_interrupt_handler
1887
1972
  )
1888
1973
  else:
1889
- # Use keyboard listener for agent cancellation
1890
- # Set a graceful SIGINT handler that shows a hint
1974
+ # Unix with different cancel key: Use keyboard listener
1891
1975
  original_handler = signal.signal(signal.SIGINT, graceful_sigint_handler)
1892
- # Spawn keyboard listener with the cancel agent callback
1893
1976
  key_listener_stop_event = threading.Event()
1894
1977
  _key_listener_thread = self._spawn_ctrl_x_key_listener(
1895
1978
  key_listener_stop_event,
1896
- on_escape=lambda: None, # Ctrl+X handled by command_runner
1979
+ on_escape=lambda: None,
1897
1980
  on_cancel_agent=schedule_agent_cancel,
1898
1981
  )
1899
1982
 
@@ -1918,8 +2001,17 @@ class BaseAgent(ABC):
1918
2001
  # Stop keyboard listener if it was started
1919
2002
  if key_listener_stop_event is not None:
1920
2003
  key_listener_stop_event.set()
1921
- # Restore original signal handler
1922
- if (
1923
- original_handler is not None
1924
- ): # 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:
1925
2017
  signal.signal(signal.SIGINT, original_handler)
@@ -706,6 +706,12 @@ async def run_prompt_with_attachments(
706
706
  attachments = [attachment.content for attachment in processed_prompt.attachments]
707
707
  link_attachments = [link.url_part for link in processed_prompt.link_attachments]
708
708
 
709
+ # IMPORTANT: Set the shared console on the agent so that streaming output
710
+ # uses the same console as the spinner. This prevents Live display conflicts
711
+ # that cause line duplication during markdown streaming.
712
+ if spinner_console is not None:
713
+ agent._console = spinner_console
714
+
709
715
  # Create the agent task first so we can track and cancel it
710
716
  agent_task = asyncio.create_task(
711
717
  agent.run_with_mcp(
@@ -784,5 +790,6 @@ def main_entry():
784
790
  DBOS.destroy()
785
791
  return 0
786
792
  finally:
787
- # 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
788
795
  reset_unix_terminal()