code-puppy 0.0.315__tar.gz → 0.0.320__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.315 → code_puppy-0.0.320}/PKG-INFO +1 -1
  2. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/base_agent.py +51 -100
  3. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/cli_runner.py +6 -0
  4. code_puppy-0.0.320/code_puppy/command_line/mcp/logs_command.py +235 -0
  5. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/model_settings_menu.py +6 -0
  6. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/__init__.py +17 -0
  7. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/blocking_startup.py +61 -32
  8. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/managed_server.py +23 -3
  9. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/manager.py +65 -0
  10. code_puppy-0.0.320/code_puppy/mcp_/mcp_logs.py +224 -0
  11. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/messaging/__init__.py +9 -0
  12. code_puppy-0.0.320/code_puppy/messaging/markdown_patches.py +57 -0
  13. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/model_factory.py +54 -0
  14. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/models.json +1 -1
  15. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/claude_code_oauth/utils.py +1 -0
  16. {code_puppy-0.0.315 → code_puppy-0.0.320}/pyproject.toml +2 -1
  17. code_puppy-0.0.315/code_puppy/command_line/mcp/logs_command.py +0 -126
  18. {code_puppy-0.0.315 → code_puppy-0.0.320}/.gitignore +0 -0
  19. {code_puppy-0.0.315 → code_puppy-0.0.320}/LICENSE +0 -0
  20. {code_puppy-0.0.315 → code_puppy-0.0.320}/README.md +0 -0
  21. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/__init__.py +0 -0
  22. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/__main__.py +0 -0
  23. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/__init__.py +0 -0
  24. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_c_reviewer.py +0 -0
  25. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_code_puppy.py +0 -0
  26. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_code_reviewer.py +0 -0
  27. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  28. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_creator_agent.py +0 -0
  29. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  30. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  31. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_manager.py +0 -0
  32. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_planning.py +0 -0
  33. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_python_programmer.py +0 -0
  34. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_python_reviewer.py +0 -0
  35. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_qa_expert.py +0 -0
  36. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_qa_kitten.py +0 -0
  37. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_security_auditor.py +0 -0
  38. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  39. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/json_agent.py +0 -0
  40. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/agents/prompt_reviewer.py +0 -0
  41. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/callbacks.py +0 -0
  42. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/chatgpt_codex_client.py +0 -0
  43. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/claude_cache_client.py +0 -0
  44. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/__init__.py +0 -0
  45. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/add_model_menu.py +0 -0
  46. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/attachments.py +0 -0
  47. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/autosave_menu.py +0 -0
  48. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/colors_menu.py +0 -0
  49. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/command_handler.py +0 -0
  50. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/command_registry.py +0 -0
  51. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/config_commands.py +0 -0
  52. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/core_commands.py +0 -0
  53. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/diff_menu.py +0 -0
  54. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/file_path_completion.py +0 -0
  55. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/load_context_completion.py +0 -0
  56. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/__init__.py +0 -0
  57. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/add_command.py +0 -0
  58. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/base.py +0 -0
  59. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  60. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  61. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  62. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/edit_command.py +0 -0
  63. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/handler.py +0 -0
  64. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/help_command.py +0 -0
  65. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/install_command.py +0 -0
  66. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/install_menu.py +0 -0
  67. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/list_command.py +0 -0
  68. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/remove_command.py +0 -0
  69. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/restart_command.py +0 -0
  70. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/search_command.py +0 -0
  71. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  72. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/start_command.py +0 -0
  73. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/status_command.py +0 -0
  74. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  75. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/stop_command.py +0 -0
  76. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/test_command.py +0 -0
  77. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/utils.py +0 -0
  78. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  79. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/mcp_completion.py +0 -0
  80. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/model_picker_completion.py +0 -0
  81. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/motd.py +0 -0
  82. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/pin_command_completion.py +0 -0
  83. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  84. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/session_commands.py +0 -0
  85. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/command_line/utils.py +0 -0
  86. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/config.py +0 -0
  87. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/error_logging.py +0 -0
  88. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/gemini_code_assist.py +0 -0
  89. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/http_utils.py +0 -0
  90. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/keymap.py +0 -0
  91. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/main.py +0 -0
  92. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/async_lifecycle.py +0 -0
  93. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  94. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/circuit_breaker.py +0 -0
  95. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/config_wizard.py +0 -0
  96. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/dashboard.py +0 -0
  97. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/error_isolation.py +0 -0
  98. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/examples/retry_example.py +0 -0
  99. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/health_monitor.py +0 -0
  100. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/registry.py +0 -0
  101. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/retry_manager.py +0 -0
  102. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  103. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/status_tracker.py +0 -0
  104. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/mcp_/system_tools.py +0 -0
  105. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/messaging/bus.py +0 -0
  106. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/messaging/commands.py +0 -0
  107. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/messaging/message_queue.py +0 -0
  108. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/messaging/messages.py +0 -0
  109. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/messaging/queue_console.py +0 -0
  110. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/messaging/renderers.py +0 -0
  111. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/messaging/rich_renderer.py +0 -0
  112. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/messaging/spinner/__init__.py +0 -0
  113. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  114. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  115. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/model_utils.py +0 -0
  116. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/models_dev_api.json +0 -0
  117. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/models_dev_parser.py +0 -0
  118. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/__init__.py +0 -0
  119. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  120. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  121. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  122. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  123. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  124. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
  125. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  126. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  127. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  128. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  129. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
  130. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  131. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  132. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  133. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/example_custom_command/README.md +0 -0
  134. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  135. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  136. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  137. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  138. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  139. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -0
  140. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  141. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/plugins/shell_safety/register_callbacks.py +0 -0
  142. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/prompts/codex_system_prompt.md +0 -0
  143. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/pydantic_patches.py +0 -0
  144. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/reopenable_async_client.py +0 -0
  145. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/round_robin_model.py +0 -0
  146. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/session_storage.py +0 -0
  147. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/status_display.py +0 -0
  148. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/summarization_agent.py +0 -0
  149. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/terminal_utils.py +0 -0
  150. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/__init__.py +0 -0
  151. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/agent_tools.py +0 -0
  152. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/browser/__init__.py +0 -0
  153. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/browser/browser_control.py +0 -0
  154. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/browser/browser_interactions.py +0 -0
  155. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/browser/browser_locators.py +0 -0
  156. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/browser/browser_navigation.py +0 -0
  157. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  158. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/browser/browser_scripts.py +0 -0
  159. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/browser/browser_workflows.py +0 -0
  160. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  161. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/browser/vqa_agent.py +0 -0
  162. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/command_runner.py +0 -0
  163. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/common.py +0 -0
  164. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/file_modifications.py +0 -0
  165. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/file_operations.py +0 -0
  166. {code_puppy-0.0.315 → code_puppy-0.0.320}/code_puppy/tools/tools_content.py +0 -0
  167. {code_puppy-0.0.315 → code_puppy-0.0.320}/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.320
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
@@ -47,11 +47,10 @@ from code_puppy.config import (
47
47
  get_protected_token_count,
48
48
  get_use_dbos,
49
49
  get_value,
50
- load_mcp_server_configs,
51
50
  )
52
51
  from code_puppy.error_logging import log_error
53
52
  from code_puppy.keymap import cancel_agent_uses_signal, get_cancel_agent_char_code
54
- from code_puppy.mcp_ import ServerConfig, get_mcp_manager
53
+ from code_puppy.mcp_ import get_mcp_manager
55
54
  from code_puppy.messaging import (
56
55
  emit_error,
57
56
  emit_info,
@@ -90,6 +89,9 @@ class BaseAgent(ABC):
90
89
  # Cache for MCP tool definitions (for token estimation)
91
90
  # This is populated after the first successful run when MCP tools are retrieved
92
91
  self._mcp_tool_definitions_cache: List[Dict[str, Any]] = []
92
+ # Shared console for streaming output - should be set by cli_runner
93
+ # to avoid conflicts between spinner's Live display and response streaming
94
+ self._console: Optional[Any] = None
93
95
 
94
96
  @property
95
97
  @abstractmethod
@@ -989,45 +991,31 @@ class BaseAgent(ABC):
989
991
  return self._puppy_rules
990
992
 
991
993
  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."""
994
+ """Load MCP servers through the manager and return pydantic-ai compatible servers.
995
+
996
+ Note: The manager automatically syncs from mcp_servers.json during initialization,
997
+ so we don't need to sync here. Use reload_mcp_servers() to force a re-sync.
998
+ """
993
999
 
994
1000
  mcp_disabled = get_value("disable_mcp_servers")
995
1001
  if mcp_disabled and str(mcp_disabled).lower() in ("1", "true", "yes", "on"):
996
1002
  return []
997
1003
 
998
1004
  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
1005
  return manager.get_servers_for_agent()
1024
1006
 
1025
1007
  def reload_mcp_servers(self):
1026
- """Reload MCP servers and return updated servers."""
1008
+ """Reload MCP servers and return updated servers.
1009
+
1010
+ Forces a re-sync from mcp_servers.json to pick up any configuration changes.
1011
+ """
1027
1012
  # Clear the MCP tool cache when servers are reloaded
1028
1013
  self._mcp_tool_definitions_cache = []
1029
- self.load_mcp_servers()
1014
+
1015
+ # Force re-sync from mcp_servers.json
1030
1016
  manager = get_mcp_manager()
1017
+ manager.sync_from_config()
1018
+
1031
1019
  return manager.get_servers_for_agent()
1032
1020
 
1033
1021
  def _load_model_with_fallback(
@@ -1279,27 +1267,22 @@ class BaseAgent(ABC):
1279
1267
  ctx: The run context.
1280
1268
  events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
1281
1269
  """
1282
- import os
1283
- import time as time_module
1284
-
1285
1270
  from pydantic_ai import PartDeltaEvent, PartStartEvent
1286
1271
  from pydantic_ai.messages import TextPartDelta, ThinkingPartDelta
1287
1272
  from rich.console import Console
1288
- from rich.live import Live
1289
1273
  from rich.markdown import Markdown
1290
1274
  from rich.markup import escape
1291
1275
 
1292
1276
  from code_puppy.messaging.spinner import pause_all_spinners
1293
1277
 
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
- )
1278
+ # IMPORTANT: Use the shared console (set by cli_runner) to avoid conflicts
1279
+ # with the spinner's Live display. Multiple Console instances with separate
1280
+ # Live displays cause cursor positioning chaos and line duplication.
1281
+ if self._console is not None:
1282
+ console = self._console
1283
+ else:
1284
+ # Fallback if console not set (shouldn't happen in normal use)
1285
+ console = Console()
1303
1286
 
1304
1287
  # Track which part indices we're currently streaming (for Text/Thinking parts)
1305
1288
  streaming_parts: set[int] = set()
@@ -1308,11 +1291,9 @@ class BaseAgent(ABC):
1308
1291
  ) # Track which parts are thinking (for dim style)
1309
1292
  text_parts: set[int] = set() # Track which parts are text
1310
1293
  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
1294
+ text_buffer: dict[int, list[str]] = {} # Buffer text for final markdown render
1295
+ token_count: dict[int, int] = {} # Track token count per text part
1313
1296
  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
1297
 
1317
1298
  def _print_thinking_banner() -> None:
1318
1299
  """Print the THINKING banner with spinner pause and line clear."""
@@ -1377,9 +1358,11 @@ class BaseAgent(ABC):
1377
1358
  streaming_parts.add(event.index)
1378
1359
  text_parts.add(event.index)
1379
1360
  text_buffer[event.index] = [] # Initialize buffer
1361
+ token_count[event.index] = 0 # Initialize token counter
1380
1362
  # Buffer initial content if present
1381
1363
  if part.content and part.content.strip():
1382
1364
  text_buffer[event.index].append(part.content)
1365
+ token_count[event.index] += 1
1383
1366
 
1384
1367
  # PartDeltaEvent - stream the content as it arrives
1385
1368
  elif isinstance(event, PartDeltaEvent):
@@ -1387,43 +1370,23 @@ class BaseAgent(ABC):
1387
1370
  delta = event.delta
1388
1371
  if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
1389
1372
  if delta.content_delta:
1390
- # For text parts, stream markdown with Live display
1373
+ # For text parts, show token counter then render at end
1391
1374
  if event.index in text_parts:
1392
- # Print banner and start Live on first content
1375
+ import sys
1376
+
1377
+ # Print banner on first content
1393
1378
  if event.index not in banner_printed:
1394
1379
  _print_response_banner()
1395
1380
  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)
1381
+ # Accumulate text for final markdown render
1408
1382
  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
1383
+ token_count[event.index] += 1
1384
+ # Update token counter in place (single line)
1385
+ count = token_count[event.index]
1386
+ sys.stdout.write(
1387
+ f"\r\x1b[K ⏳ Receiving... {count} tokens"
1388
+ )
1389
+ sys.stdout.flush()
1427
1390
  else:
1428
1391
  # For thinking parts, stream immediately (dim)
1429
1392
  if event.index not in banner_printed:
@@ -1435,36 +1398,24 @@ class BaseAgent(ABC):
1435
1398
  # PartEndEvent - finish the streaming with a newline
1436
1399
  elif isinstance(event, PartEndEvent):
1437
1400
  if event.index in streaming_parts:
1438
- # For text parts, do final render then stop the Live display
1401
+ # For text parts, clear counter line and render markdown
1439
1402
  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:
1403
+ import sys
1404
+
1405
+ # Clear the token counter line
1406
+ sys.stdout.write("\r\x1b[K")
1407
+ sys.stdout.flush()
1408
+ # Render the final markdown nicely
1409
+ if event.index in text_buffer:
1458
1410
  try:
1459
1411
  final_content = "".join(text_buffer[event.index])
1460
1412
  if final_content.strip():
1461
1413
  console.print(Markdown(final_content))
1462
1414
  except Exception:
1463
1415
  pass
1464
- if event.index in text_buffer:
1465
1416
  del text_buffer[event.index]
1466
- # Clean up render time tracking
1467
- last_render_time.pop(event.index, None)
1417
+ # Clean up token count
1418
+ token_count.pop(event.index, None)
1468
1419
  # For thinking parts, just print newline
1469
1420
  elif event.index in banner_printed:
1470
1421
  console.print() # Final newline after streaming
@@ -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(
@@ -0,0 +1,235 @@
1
+ """
2
+ MCP Logs Command - Shows server logs from persistent log files.
3
+ """
4
+
5
+ import logging
6
+ from typing import List, Optional
7
+
8
+ from rich.panel import Panel
9
+ from rich.syntax import Syntax
10
+ from rich.text import Text
11
+
12
+ from code_puppy.mcp_.mcp_logs import (
13
+ clear_logs,
14
+ get_log_file_path,
15
+ get_log_stats,
16
+ list_servers_with_logs,
17
+ read_logs,
18
+ )
19
+ from code_puppy.messaging import emit_error, emit_info
20
+
21
+ from .base import MCPCommandBase
22
+ from .utils import find_server_id_by_name, suggest_similar_servers
23
+
24
+ # Configure logging
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class LogsCommand(MCPCommandBase):
29
+ """
30
+ Command handler for showing MCP server logs.
31
+
32
+ Shows logs from persistent log files stored in ~/.code_puppy/mcp_logs/.
33
+ """
34
+
35
+ def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
36
+ """
37
+ Show logs for a server.
38
+
39
+ Usage:
40
+ /mcp logs - List servers with logs
41
+ /mcp logs <server_name> - Show last 50 lines
42
+ /mcp logs <server_name> 100 - Show last 100 lines
43
+ /mcp logs <server_name> all - Show all logs
44
+ /mcp logs <server_name> --clear - Clear logs for server
45
+
46
+ Args:
47
+ args: Command arguments
48
+ group_id: Optional message group ID for grouping related messages
49
+ """
50
+ if group_id is None:
51
+ group_id = self.generate_group_id()
52
+
53
+ # No args - list servers with logs
54
+ if not args:
55
+ self._list_servers_with_logs(group_id)
56
+ return
57
+
58
+ server_name = args[0]
59
+
60
+ # Check for --clear flag
61
+ if len(args) > 1 and args[1] == "--clear":
62
+ self._clear_logs(server_name, group_id)
63
+ return
64
+
65
+ # Determine number of lines
66
+ lines = 50 # Default
67
+ show_all = False
68
+
69
+ if len(args) > 1:
70
+ if args[1].lower() == "all":
71
+ show_all = True
72
+ else:
73
+ try:
74
+ lines = int(args[1])
75
+ if lines <= 0:
76
+ emit_info(
77
+ "Lines must be positive, using default: 50",
78
+ message_group=group_id,
79
+ )
80
+ lines = 50
81
+ except ValueError:
82
+ emit_info(
83
+ f"Invalid number '{args[1]}', using default: 50",
84
+ message_group=group_id,
85
+ )
86
+
87
+ self._show_logs(server_name, lines if not show_all else None, group_id)
88
+
89
+ def _list_servers_with_logs(self, group_id: str) -> None:
90
+ """List all servers that have log files."""
91
+ servers = list_servers_with_logs()
92
+
93
+ if not servers:
94
+ emit_info(
95
+ "📋 No MCP server logs found.\n"
96
+ "Logs are created when servers are started.",
97
+ message_group=group_id,
98
+ )
99
+ return
100
+
101
+ lines = ["📋 **Servers with logs:**\n"]
102
+
103
+ for server in servers:
104
+ stats = get_log_stats(server)
105
+ size_kb = stats["total_size_bytes"] / 1024
106
+ size_str = (
107
+ f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb / 1024:.1f} MB"
108
+ )
109
+ rotated = (
110
+ f" (+{stats['rotated_count']} rotated)"
111
+ if stats["rotated_count"]
112
+ else ""
113
+ )
114
+ lines.append(
115
+ f" • **{server}** - {stats['line_count']} lines, {size_str}{rotated}"
116
+ )
117
+
118
+ lines.append("\n**Usage:** `/mcp logs <server_name> [lines|all]`")
119
+
120
+ emit_info("\n".join(lines), message_group=group_id)
121
+
122
+ def _show_logs(self, server_name: str, lines: Optional[int], group_id: str) -> None:
123
+ """
124
+ Show logs for a specific server.
125
+
126
+ Args:
127
+ server_name: Name of the server
128
+ lines: Number of lines to show, or None for all
129
+ group_id: Message group ID
130
+ """
131
+ try:
132
+ # Verify server exists in manager
133
+ server_id = find_server_id_by_name(self.manager, server_name)
134
+ if not server_id:
135
+ # Server not configured, but might have logs from before
136
+ stats = get_log_stats(server_name)
137
+ if not stats["exists"]:
138
+ emit_info(
139
+ f"Server '{server_name}' not found and has no logs.",
140
+ message_group=group_id,
141
+ )
142
+ suggest_similar_servers(
143
+ self.manager, server_name, group_id=group_id
144
+ )
145
+ return
146
+
147
+ # Read logs
148
+ log_lines = read_logs(server_name, lines=lines)
149
+
150
+ if not log_lines:
151
+ emit_info(
152
+ f"📋 No logs found for server: **{server_name}**\n"
153
+ f"Log file: `{get_log_file_path(server_name)}`",
154
+ message_group=group_id,
155
+ )
156
+ return
157
+
158
+ # Get stats for header
159
+ stats = get_log_stats(server_name)
160
+ total_lines = stats["line_count"]
161
+ showing = len(log_lines)
162
+
163
+ # Format header
164
+ if lines is None:
165
+ header = f"📋 Logs for {server_name} (all {total_lines} lines)"
166
+ else:
167
+ header = (
168
+ f"📋 Logs for {server_name} (last {showing} of {total_lines} lines)"
169
+ )
170
+
171
+ # Format log content with syntax highlighting
172
+ log_content = "\n".join(log_lines)
173
+
174
+ # Create a panel with the logs
175
+ syntax = Syntax(
176
+ log_content,
177
+ "log",
178
+ theme="monokai",
179
+ word_wrap=True,
180
+ line_numbers=False,
181
+ )
182
+
183
+ panel = Panel(
184
+ syntax,
185
+ title=header,
186
+ subtitle=f"Log file: {get_log_file_path(server_name)}",
187
+ border_style="dim",
188
+ )
189
+
190
+ emit_info(panel, message_group=group_id)
191
+
192
+ # Show hint for more options
193
+ if lines is not None and showing < total_lines:
194
+ emit_info(
195
+ Text.from_markup(
196
+ f"[dim]💡 Use `/mcp logs {server_name} all` to see all logs, "
197
+ f"or `/mcp logs {server_name} <number>` for specific count[/dim]"
198
+ ),
199
+ message_group=group_id,
200
+ )
201
+
202
+ except Exception as e:
203
+ logger.error(f"Error getting logs for server '{server_name}': {e}")
204
+ emit_error(f"Error getting logs: {e}", message_group=group_id)
205
+
206
+ def _clear_logs(self, server_name: str, group_id: str) -> None:
207
+ """
208
+ Clear logs for a specific server.
209
+
210
+ Args:
211
+ server_name: Name of the server
212
+ group_id: Message group ID
213
+ """
214
+ try:
215
+ stats = get_log_stats(server_name)
216
+
217
+ if not stats["exists"] and stats["rotated_count"] == 0:
218
+ emit_info(
219
+ f"No logs to clear for server: {server_name}",
220
+ message_group=group_id,
221
+ )
222
+ return
223
+
224
+ # Clear the logs
225
+ clear_logs(server_name, include_rotated=True)
226
+
227
+ cleared_count = 1 + stats["rotated_count"]
228
+ emit_info(
229
+ f"🗑️ Cleared {cleared_count} log file(s) for **{server_name}**",
230
+ message_group=group_id,
231
+ )
232
+
233
+ except Exception as e:
234
+ logger.error(f"Error clearing logs for server '{server_name}': {e}")
235
+ emit_error(f"Error clearing logs: {e}", message_group=group_id)
@@ -84,6 +84,12 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
84
84
  "default": 10000,
85
85
  "format": "{:.0f}",
86
86
  },
87
+ "interleaved_thinking": {
88
+ "name": "Interleaved Thinking",
89
+ "description": "Enable thinking between tool calls (Claude 4 only: Opus 4.5, Opus 4.1, Opus 4, Sonnet 4). Adds beta header. WARNING: On Vertex/Bedrock, this FAILS for non-Claude 4 models!",
90
+ "type": "boolean",
91
+ "default": False,
92
+ },
87
93
  }
88
94
 
89
95
 
@@ -17,6 +17,15 @@ from .error_isolation import (
17
17
  )
18
18
  from .managed_server import ManagedMCPServer, ServerConfig, ServerState
19
19
  from .manager import MCPManager, ServerInfo, get_mcp_manager
20
+ from .mcp_logs import (
21
+ clear_logs,
22
+ get_log_file_path,
23
+ get_log_stats,
24
+ get_mcp_logs_dir,
25
+ list_servers_with_logs,
26
+ read_logs,
27
+ write_log,
28
+ )
20
29
  from .registry import ServerRegistry
21
30
  from .retry_manager import RetryManager, RetryStats, get_retry_manager, retry_mcp_call
22
31
  from .status_tracker import Event, ServerStatusTracker
@@ -46,4 +55,12 @@ __all__ = [
46
55
  "MCPDashboard",
47
56
  "MCPConfigWizard",
48
57
  "run_add_wizard",
58
+ # Log management
59
+ "get_mcp_logs_dir",
60
+ "get_log_file_path",
61
+ "read_logs",
62
+ "write_log",
63
+ "clear_logs",
64
+ "list_servers_with_logs",
65
+ "get_log_stats",
49
66
  ]