code-puppy 0.0.316__tar.gz → 0.0.318__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.316 → code_puppy-0.0.318}/PKG-INFO +1 -1
  2. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/base_agent.py +11 -1
  3. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/cli_runner.py +6 -0
  4. code_puppy-0.0.318/code_puppy/command_line/mcp/logs_command.py +235 -0
  5. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/__init__.py +17 -0
  6. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/blocking_startup.py +50 -29
  7. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/managed_server.py +1 -1
  8. code_puppy-0.0.318/code_puppy/mcp_/mcp_logs.py +224 -0
  9. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/messaging/__init__.py +9 -0
  10. code_puppy-0.0.318/code_puppy/messaging/markdown_patches.py +57 -0
  11. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/model_factory.py +6 -0
  12. {code_puppy-0.0.316 → code_puppy-0.0.318}/pyproject.toml +2 -1
  13. code_puppy-0.0.316/code_puppy/command_line/mcp/logs_command.py +0 -126
  14. {code_puppy-0.0.316 → code_puppy-0.0.318}/.gitignore +0 -0
  15. {code_puppy-0.0.316 → code_puppy-0.0.318}/LICENSE +0 -0
  16. {code_puppy-0.0.316 → code_puppy-0.0.318}/README.md +0 -0
  17. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/__init__.py +0 -0
  18. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/__main__.py +0 -0
  19. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/__init__.py +0 -0
  20. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_c_reviewer.py +0 -0
  21. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_code_puppy.py +0 -0
  22. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_code_reviewer.py +0 -0
  23. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  24. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_creator_agent.py +0 -0
  25. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  26. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  27. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_manager.py +0 -0
  28. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_planning.py +0 -0
  29. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_python_programmer.py +0 -0
  30. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_python_reviewer.py +0 -0
  31. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_qa_expert.py +0 -0
  32. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_qa_kitten.py +0 -0
  33. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_security_auditor.py +0 -0
  34. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  35. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/json_agent.py +0 -0
  36. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/agents/prompt_reviewer.py +0 -0
  37. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/callbacks.py +0 -0
  38. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/chatgpt_codex_client.py +0 -0
  39. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/claude_cache_client.py +0 -0
  40. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/__init__.py +0 -0
  41. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/add_model_menu.py +0 -0
  42. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/attachments.py +0 -0
  43. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/autosave_menu.py +0 -0
  44. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/colors_menu.py +0 -0
  45. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/command_handler.py +0 -0
  46. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/command_registry.py +0 -0
  47. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/config_commands.py +0 -0
  48. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/core_commands.py +0 -0
  49. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/diff_menu.py +0 -0
  50. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/file_path_completion.py +0 -0
  51. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/load_context_completion.py +0 -0
  52. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/__init__.py +0 -0
  53. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/add_command.py +0 -0
  54. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/base.py +0 -0
  55. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  56. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  57. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  58. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/edit_command.py +0 -0
  59. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/handler.py +0 -0
  60. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/help_command.py +0 -0
  61. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/install_command.py +0 -0
  62. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/install_menu.py +0 -0
  63. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/list_command.py +0 -0
  64. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/remove_command.py +0 -0
  65. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/restart_command.py +0 -0
  66. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/search_command.py +0 -0
  67. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  68. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/start_command.py +0 -0
  69. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/status_command.py +0 -0
  70. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  71. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/stop_command.py +0 -0
  72. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/test_command.py +0 -0
  73. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/utils.py +0 -0
  74. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  75. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/mcp_completion.py +0 -0
  76. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/model_picker_completion.py +0 -0
  77. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/model_settings_menu.py +0 -0
  78. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/motd.py +0 -0
  79. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/pin_command_completion.py +0 -0
  80. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  81. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/session_commands.py +0 -0
  82. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/command_line/utils.py +0 -0
  83. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/config.py +0 -0
  84. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/error_logging.py +0 -0
  85. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/gemini_code_assist.py +0 -0
  86. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/http_utils.py +0 -0
  87. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/keymap.py +0 -0
  88. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/main.py +0 -0
  89. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/async_lifecycle.py +0 -0
  90. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  91. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/circuit_breaker.py +0 -0
  92. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/config_wizard.py +0 -0
  93. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/dashboard.py +0 -0
  94. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/error_isolation.py +0 -0
  95. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/examples/retry_example.py +0 -0
  96. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/health_monitor.py +0 -0
  97. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/manager.py +0 -0
  98. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/registry.py +0 -0
  99. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/retry_manager.py +0 -0
  100. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  101. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/status_tracker.py +0 -0
  102. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/mcp_/system_tools.py +0 -0
  103. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/messaging/bus.py +0 -0
  104. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/messaging/commands.py +0 -0
  105. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/messaging/message_queue.py +0 -0
  106. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/messaging/messages.py +0 -0
  107. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/messaging/queue_console.py +0 -0
  108. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/messaging/renderers.py +0 -0
  109. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/messaging/rich_renderer.py +0 -0
  110. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/messaging/spinner/__init__.py +0 -0
  111. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  112. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  113. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/model_utils.py +0 -0
  114. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/models.json +0 -0
  115. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/models_dev_api.json +0 -0
  116. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/models_dev_parser.py +0 -0
  117. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/__init__.py +0 -0
  118. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  119. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  120. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  121. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  122. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  123. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
  124. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  125. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  126. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  127. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  128. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
  129. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  130. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/claude_code_oauth/utils.py +0 -0
  131. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  132. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  133. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/example_custom_command/README.md +0 -0
  134. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  135. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  136. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  137. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  138. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  139. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -0
  140. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  141. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/plugins/shell_safety/register_callbacks.py +0 -0
  142. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/prompts/codex_system_prompt.md +0 -0
  143. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/pydantic_patches.py +0 -0
  144. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/reopenable_async_client.py +0 -0
  145. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/round_robin_model.py +0 -0
  146. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/session_storage.py +0 -0
  147. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/status_display.py +0 -0
  148. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/summarization_agent.py +0 -0
  149. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/terminal_utils.py +0 -0
  150. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/__init__.py +0 -0
  151. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/agent_tools.py +0 -0
  152. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/browser/__init__.py +0 -0
  153. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/browser/browser_control.py +0 -0
  154. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/browser/browser_interactions.py +0 -0
  155. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/browser/browser_locators.py +0 -0
  156. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/browser/browser_navigation.py +0 -0
  157. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  158. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/browser/browser_scripts.py +0 -0
  159. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/browser/browser_workflows.py +0 -0
  160. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  161. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/browser/vqa_agent.py +0 -0
  162. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/command_runner.py +0 -0
  163. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/common.py +0 -0
  164. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/file_modifications.py +0 -0
  165. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/file_operations.py +0 -0
  166. {code_puppy-0.0.316 → code_puppy-0.0.318}/code_puppy/tools/tools_content.py +0 -0
  167. {code_puppy-0.0.316 → code_puppy-0.0.318}/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.316
3
+ Version: 0.0.318
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
@@ -89,6 +89,9 @@ class BaseAgent(ABC):
89
89
  # Cache for MCP tool definitions (for token estimation)
90
90
  # This is populated after the first successful run when MCP tools are retrieved
91
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
92
95
 
93
96
  @property
94
97
  @abstractmethod
@@ -1276,7 +1279,14 @@ class BaseAgent(ABC):
1276
1279
 
1277
1280
  from code_puppy.messaging.spinner import pause_all_spinners
1278
1281
 
1279
- console = Console()
1282
+ # IMPORTANT: Use the shared console (set by cli_runner) to avoid conflicts
1283
+ # with the spinner's Live display. Multiple Console instances with separate
1284
+ # Live displays cause cursor positioning chaos and line duplication.
1285
+ if self._console is not None:
1286
+ console = self._console
1287
+ else:
1288
+ # Fallback if console not set (shouldn't happen in normal use)
1289
+ console = Console()
1280
1290
 
1281
1291
  # Disable Live display in test mode or non-interactive environments
1282
1292
  # This fixes issues with pexpect PTY where Live() hangs
@@ -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)
@@ -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
  ]
@@ -2,14 +2,13 @@
2
2
  MCP Server with blocking startup capability and stderr capture.
3
3
 
4
4
  This module provides MCP servers that:
5
- 1. Capture stderr output from stdio servers
5
+ 1. Capture stderr output from stdio servers to persistent log files
6
6
  2. Block until fully initialized before allowing operations
7
- 3. Emit stderr to users via emit_info with message groups
7
+ 3. Optionally emit stderr to users (disabled by default to reduce console noise)
8
8
  """
9
9
 
10
10
  import asyncio
11
11
  import os
12
- import tempfile
13
12
  import threading
14
13
  import uuid
15
14
  from contextlib import asynccontextmanager
@@ -18,56 +17,73 @@ from typing import List, Optional
18
17
  from mcp.client.stdio import StdioServerParameters, stdio_client
19
18
  from pydantic_ai.mcp import MCPServerStdio
20
19
 
20
+ from code_puppy.mcp_.mcp_logs import get_log_file_path, rotate_log_if_needed, write_log
21
21
  from code_puppy.messaging import emit_info
22
22
 
23
23
 
24
24
  class StderrFileCapture:
25
- """Captures stderr to a file and monitors it in a background thread."""
25
+ """
26
+ Captures stderr to a persistent log file and optionally monitors it.
27
+
28
+ Logs are written to ~/.code_puppy/mcp_logs/<server_name>.log
29
+ """
26
30
 
27
31
  def __init__(
28
32
  self,
29
33
  server_name: str,
30
- emit_to_user: bool = True,
34
+ emit_to_user: bool = False, # Disabled by default to reduce console noise
31
35
  message_group: Optional[uuid.UUID] = None,
32
36
  ):
33
37
  self.server_name = server_name
34
38
  self.emit_to_user = emit_to_user
35
39
  self.message_group = message_group or uuid.uuid4()
36
- self.temp_file = None
37
- self.temp_path = None
40
+ self.log_file = None
41
+ self.log_path = None
38
42
  self.monitor_thread = None
39
43
  self.stop_monitoring = threading.Event()
40
44
  self.captured_lines = []
45
+ self._last_read_pos = 0
41
46
 
42
47
  def start(self):
43
- """Start capture by creating temp file and monitor thread."""
44
- # Create temp file
45
- self.temp_file = tempfile.NamedTemporaryFile(
46
- mode="w+", delete=False, suffix=".err"
47
- )
48
- self.temp_path = self.temp_file.name
48
+ """Start capture by opening persistent log file and monitor thread."""
49
+ # Rotate log if needed
50
+ rotate_log_if_needed(self.server_name)
51
+
52
+ # Get persistent log path
53
+ self.log_path = get_log_file_path(self.server_name)
54
+
55
+ # Write startup marker
56
+ write_log(self.server_name, "--- Server starting ---", "INFO")
49
57
 
50
- # Start monitoring thread
58
+ # Open log file for appending stderr
59
+ self.log_file = open(self.log_path, "a", encoding="utf-8")
60
+
61
+ # Start monitoring thread only if we need to emit to user or capture lines
51
62
  self.stop_monitoring.clear()
52
63
  self.monitor_thread = threading.Thread(target=self._monitor_file)
53
64
  self.monitor_thread.daemon = True
54
65
  self.monitor_thread.start()
55
66
 
56
- return self.temp_file
67
+ return self.log_file
57
68
 
58
69
  def _monitor_file(self):
59
- """Monitor the temp file for new content."""
60
- if not self.temp_path:
70
+ """Monitor the log file for new content."""
71
+ if not self.log_path:
61
72
  return
62
73
 
63
- last_pos = 0
74
+ # Start reading from current position (end of file before we started)
75
+ try:
76
+ self._last_read_pos = os.path.getsize(self.log_path)
77
+ except OSError:
78
+ self._last_read_pos = 0
79
+
64
80
  while not self.stop_monitoring.is_set():
65
81
  try:
66
- with open(self.temp_path, "r") as f:
67
- f.seek(last_pos)
82
+ with open(self.log_path, "r", encoding="utf-8", errors="replace") as f:
83
+ f.seek(self._last_read_pos)
68
84
  new_content = f.read()
69
85
  if new_content:
70
- last_pos = f.tell()
86
+ self._last_read_pos = f.tell()
71
87
  # Process new lines
72
88
  for line in new_content.splitlines():
73
89
  if line.strip():
@@ -89,16 +105,21 @@ class StderrFileCapture:
89
105
  if self.monitor_thread:
90
106
  self.monitor_thread.join(timeout=1)
91
107
 
92
- if self.temp_file:
108
+ if self.log_file:
93
109
  try:
94
- self.temp_file.close()
110
+ self.log_file.flush()
111
+ self.log_file.close()
95
112
  except Exception:
96
113
  pass
97
114
 
98
- if self.temp_path and os.path.exists(self.temp_path):
115
+ # Write shutdown marker
116
+ write_log(self.server_name, "--- Server stopped ---", "INFO")
117
+
118
+ # Read any remaining content for in-memory capture
119
+ if self.log_path and os.path.exists(self.log_path):
99
120
  try:
100
- # Read any remaining content
101
- with open(self.temp_path, "r") as f:
121
+ with open(self.log_path, "r", encoding="utf-8", errors="replace") as f:
122
+ f.seek(self._last_read_pos)
102
123
  content = f.read()
103
124
  for line in content.splitlines():
104
125
  if line.strip() and line not in self.captured_lines:
@@ -108,13 +129,13 @@ class StderrFileCapture:
108
129
  f"MCP {self.server_name}: {line}",
109
130
  message_group=self.message_group,
110
131
  )
111
-
112
- os.unlink(self.temp_path)
113
132
  except Exception:
114
133
  pass
115
134
 
135
+ # Note: We do NOT delete the log file - it's persistent now!
136
+
116
137
  def get_captured_lines(self) -> List[str]:
117
- """Get all captured lines."""
138
+ """Get all captured lines from this session."""
118
139
  return self.captured_lines.copy()
119
140
 
120
141
 
@@ -204,7 +204,7 @@ class ManagedMCPServer:
204
204
  **stdio_kwargs,
205
205
  process_tool_call=process_tool_call,
206
206
  tool_prefix=self.config.name,
207
- emit_stderr=True, # Always emit stderr for now
207
+ emit_stderr=False, # Logs go to file, not console (use /mcp logs to view)
208
208
  message_group=message_group,
209
209
  )
210
210