code-puppy 0.0.894__tar.gz → 0.0.896__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 (325) hide show
  1. {code_puppy-0.0.894 → code_puppy-0.0.896}/PKG-INFO +1 -1
  2. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/managed_server.py +31 -0
  3. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/command_runner.py +87 -207
  4. code_puppy-0.0.896/code_puppy/tools/win_shell_io.py +161 -0
  5. {code_puppy-0.0.894 → code_puppy-0.0.896}/pyproject.toml +1 -1
  6. {code_puppy-0.0.894 → code_puppy-0.0.896}/.gitignore +0 -0
  7. {code_puppy-0.0.894 → code_puppy-0.0.896}/LICENSE +0 -0
  8. {code_puppy-0.0.894 → code_puppy-0.0.896}/README.md +0 -0
  9. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/__init__.py +0 -0
  10. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/__main__.py +0 -0
  11. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/_plugin_i18n_lifecycle.py +0 -0
  12. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/_pydantic_tool_helpers.py +0 -0
  13. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/_pydantic_video_patch.py +0 -0
  14. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agent_completion_inbox.py +0 -0
  15. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agent_execution_context.py +0 -0
  16. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/__init__.py +0 -0
  17. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_builder.py +0 -0
  18. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_code_mode.py +0 -0
  19. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_code_mode_guidance.py +0 -0
  20. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_compaction.py +0 -0
  21. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_diagnostics.py +0 -0
  22. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_foreign_thinking.py +0 -0
  23. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_history.py +0 -0
  24. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_key_listeners.py +0 -0
  25. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_model_message_transform.py +0 -0
  26. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_non_streaming_render.py +0 -0
  27. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_output_limits.py +0 -0
  28. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_run_signals.py +0 -0
  29. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_runtime.py +0 -0
  30. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_steer_processor.py +0 -0
  31. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_subagent_recursion.py +0 -0
  32. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_windows_console.py +0 -0
  33. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_wire_tool_names.py +0 -0
  34. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_code_puppy.py +0 -0
  35. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_creator_agent.py +0 -0
  36. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_helios.py +0 -0
  37. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_manager.py +0 -0
  38. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_model_judge.py +0 -0
  39. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_planning.py +0 -0
  40. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_qa_kitten.py +0 -0
  41. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_web_retriever.py +0 -0
  42. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/base_agent.py +0 -0
  43. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/event_stream_handler.py +0 -0
  44. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/json_agent.py +0 -0
  45. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/retry_checkpoint.py +0 -0
  46. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/retry_profiles.py +0 -0
  47. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/run_stats.py +0 -0
  48. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/smooth_stream.py +0 -0
  49. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/stream_status.py +0 -0
  50. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/subagent_stream_handler.py +0 -0
  51. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/thinking_output.py +0 -0
  52. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/asyncio_cleanup.py +0 -0
  53. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/atomic_io.py +0 -0
  54. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/atomic_json.py +0 -0
  55. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/ca_bundle.py +0 -0
  56. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/callbacks.py +0 -0
  57. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/capabilities/__init__.py +0 -0
  58. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/capabilities/compaction.py +0 -0
  59. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/capabilities/eager_timing.py +0 -0
  60. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/chatgpt_codex_client.py +0 -0
  61. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/claude_cache_client.py +0 -0
  62. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/claude_oauth_transport.py +0 -0
  63. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/cli_runner.py +0 -0
  64. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/__init__.py +0 -0
  65. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/add_model_details.py +0 -0
  66. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/add_model_menu.py +0 -0
  67. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/add_model_menus.py +0 -0
  68. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/add_model_vllm.py +0 -0
  69. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/agent_menu.py +0 -0
  70. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/attachments.py +0 -0
  71. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/autosave_menu.py +0 -0
  72. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/autosave_search.py +0 -0
  73. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/clipboard.py +0 -0
  74. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/colors_menu.py +0 -0
  75. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/command_handler.py +0 -0
  76. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/command_registry.py +0 -0
  77. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/completers.py +0 -0
  78. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/completion_cache.py +0 -0
  79. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/config_apply.py +0 -0
  80. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/config_commands.py +0 -0
  81. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/core_commands.py +0 -0
  82. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/file_index.py +0 -0
  83. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/file_path_completion.py +0 -0
  84. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/help_catalog.py +0 -0
  85. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/help_overlay.py +0 -0
  86. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/image_utils.py +0 -0
  87. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/load_context_completion.py +0 -0
  88. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/__init__.py +0 -0
  89. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/base.py +0 -0
  90. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  91. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  92. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  93. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/edit_command.py +0 -0
  94. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/handler.py +0 -0
  95. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/help_command.py +0 -0
  96. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/install_command.py +0 -0
  97. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/install_menu.py +0 -0
  98. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/list_command.py +0 -0
  99. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/logs_command.py +0 -0
  100. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/mcp_servers_store.py +0 -0
  101. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/remove_command.py +0 -0
  102. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/restart_command.py +0 -0
  103. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/search_command.py +0 -0
  104. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/silence_warning_command.py +0 -0
  105. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  106. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/start_command.py +0 -0
  107. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/status_command.py +0 -0
  108. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  109. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/stop_command.py +0 -0
  110. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/trust_command.py +0 -0
  111. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/utils.py +0 -0
  112. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  113. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp_binding_menu.py +0 -0
  114. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp_completion.py +0 -0
  115. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/menu_session.py +0 -0
  116. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/model_picker_completion.py +0 -0
  117. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/model_settings_defs.py +0 -0
  118. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/model_settings_menu.py +0 -0
  119. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/onboarding_slides.py +0 -0
  120. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/onboarding_wizard.py +0 -0
  121. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/pagination.py +0 -0
  122. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/pin_command_completion.py +0 -0
  123. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/refresh_models.py +0 -0
  124. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/session_browser.py +0 -0
  125. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/session_browser_data.py +0 -0
  126. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/session_commands.py +0 -0
  127. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu.py +0 -0
  128. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu_catalog.py +0 -0
  129. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu_schema.py +0 -0
  130. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu_settings.py +0 -0
  131. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu_shims.py +0 -0
  132. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu_values.py +0 -0
  133. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/shell_passthrough.py +0 -0
  134. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/skills_completion.py +0 -0
  135. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/tui_style.py +0 -0
  136. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/uc_menu.py +0 -0
  137. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/utils.py +0 -0
  138. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/config.py +0 -0
  139. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/config_file.py +0 -0
  140. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/diagnostics.py +0 -0
  141. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/error_logging.py +0 -0
  142. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/events/__init__.py +0 -0
  143. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/events/bridge.py +0 -0
  144. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/file_completion_io.py +0 -0
  145. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/file_completion_tokens.py +0 -0
  146. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/gemini_code_assist.py +0 -0
  147. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/gemini_common.py +0 -0
  148. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/gemini_model.py +0 -0
  149. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/README.md +0 -0
  150. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/__init__.py +0 -0
  151. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/aliases.py +0 -0
  152. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/engine.py +0 -0
  153. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/executor.py +0 -0
  154. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/matcher.py +0 -0
  155. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/models.py +0 -0
  156. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/registry.py +0 -0
  157. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/validator.py +0 -0
  158. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/http_retry.py +0 -0
  159. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/http_utils.py +0 -0
  160. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/httpx2_utils.py +0 -0
  161. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/__init__.py +0 -0
  162. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/audit.py +0 -0
  163. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/catalog.py +0 -0
  164. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/formats.py +0 -0
  165. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/interpolation.py +0 -0
  166. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/locale.py +0 -0
  167. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/locales/en-US.json +0 -0
  168. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/locales/es.json +0 -0
  169. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/locales/fr-CA.json +0 -0
  170. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/plugin_catalog.py +0 -0
  171. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/plurals.py +0 -0
  172. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/pseudo.py +0 -0
  173. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/translate.py +0 -0
  174. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/kennel_provider.py +0 -0
  175. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/keymap.py +0 -0
  176. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/list_filtering.py +0 -0
  177. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/main.py +0 -0
  178. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/__init__.py +0 -0
  179. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/agent_bindings.py +0 -0
  180. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/async_lifecycle.py +0 -0
  181. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/blocking_startup.py +0 -0
  182. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/circuit_breaker.py +0 -0
  183. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/config_wizard.py +0 -0
  184. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/dashboard.py +0 -0
  185. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/error_isolation.py +0 -0
  186. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/examples/retry_example.py +0 -0
  187. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/health_monitor.py +0 -0
  188. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/http_auth.py +0 -0
  189. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/manager.py +0 -0
  190. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/manager_lifecycle.py +0 -0
  191. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/mcp_logs.py +0 -0
  192. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/project_config.py +0 -0
  193. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/registry.py +0 -0
  194. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/retry_manager.py +0 -0
  195. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  196. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/status_tracker.py +0 -0
  197. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/system_tools.py +0 -0
  198. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/tool_arg_coercion.py +0 -0
  199. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/toolset_utils.py +0 -0
  200. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_prompts/__init__.py +0 -0
  201. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_prompts/hook_creator.py +0 -0
  202. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/__init__.py +0 -0
  203. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/bar_painters.py +0 -0
  204. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/bar_rendering.py +0 -0
  205. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/bottom_bar.py +0 -0
  206. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/bus.py +0 -0
  207. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/chords.py +0 -0
  208. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/commands.py +0 -0
  209. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_actions.py +0 -0
  210. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_completion.py +0 -0
  211. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_display.py +0 -0
  212. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_history.py +0 -0
  213. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_keys.py +0 -0
  214. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_paste.py +0 -0
  215. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_queue.py +0 -0
  216. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_submission.py +0 -0
  217. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/external_editor.py +0 -0
  218. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/identity_line.py +0 -0
  219. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/idle_status.py +0 -0
  220. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/inline_bar.py +0 -0
  221. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/line_editor.py +0 -0
  222. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/markdown_patches.py +0 -0
  223. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/menu_lifecycle.py +0 -0
  224. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/message_queue.py +0 -0
  225. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/messages.py +0 -0
  226. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/pause_controller.py +0 -0
  227. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/prompt_prefix_style.py +0 -0
  228. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/queue_console.py +0 -0
  229. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/renderers.py +0 -0
  230. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/rich_renderer.py +0 -0
  231. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/run_ui.py +0 -0
  232. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/run_ui_wiring.py +0 -0
  233. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/speculation_line.py +0 -0
  234. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/speculation_stats.py +0 -0
  235. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/speculation_toggle.py +0 -0
  236. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/spinner/__init__.py +0 -0
  237. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/status_line.py +0 -0
  238. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/subagent_console.py +0 -0
  239. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/theme_accent.py +0 -0
  240. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/tool_output.py +0 -0
  241. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/transcript_guard.py +0 -0
  242. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/model_descriptions.py +0 -0
  243. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/model_factory.py +0 -0
  244. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/model_switching.py +0 -0
  245. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/model_utils.py +0 -0
  246. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/models.json +0 -0
  247. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/models_dev_api.json +0 -0
  248. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/models_dev_parser.py +0 -0
  249. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/observability.py +0 -0
  250. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/platform_utils.py +0 -0
  251. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/__init__.py +0 -0
  252. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/config.py +0 -0
  253. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/oauth_pasteback.py +0 -0
  254. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  255. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/trust.py +0 -0
  256. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/trust_notice.py +0 -0
  257. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/private_inference.py +0 -0
  258. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/provider_credentials.py +0 -0
  259. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/provider_identity.py +0 -0
  260. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/pydantic_patches.py +0 -0
  261. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/reopenable_async_client.py +0 -0
  262. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/round_robin_model.py +0 -0
  263. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/secret_store.py +0 -0
  264. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/secret_store_backends.py +0 -0
  265. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/session_format_migration.py +0 -0
  266. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/session_lifecycle.py +0 -0
  267. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/session_migration.py +0 -0
  268. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/session_storage.py +0 -0
  269. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/session_surrogate_unpickler.py +0 -0
  270. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/shared_credentials.py +0 -0
  271. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/skill_provider.py +0 -0
  272. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/splash.py +0 -0
  273. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/status_display.py +0 -0
  274. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/steer_metadata.py +0 -0
  275. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/terminal_utils.py +0 -0
  276. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/token_usage.py +0 -0
  277. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/__init__.py +0 -0
  278. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/_browser_registry.py +0 -0
  279. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/_lazy.py +0 -0
  280. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/agent_tools.py +0 -0
  281. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/approval_feedback.py +0 -0
  282. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/__init__.py +0 -0
  283. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/constants.py +0 -0
  284. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/demo_tui.py +0 -0
  285. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/handler.py +0 -0
  286. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/models.py +0 -0
  287. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/registration.py +0 -0
  288. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/renderers.py +0 -0
  289. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/terminal_ui.py +0 -0
  290. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/theme.py +0 -0
  291. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/tui_loop.py +0 -0
  292. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/__init__.py +0 -0
  293. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_control.py +0 -0
  294. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_interactions.py +0 -0
  295. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_locator_resolver.py +0 -0
  296. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_locators.py +0 -0
  297. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_manager.py +0 -0
  298. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_navigation.py +0 -0
  299. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_page_snapshot.py +0 -0
  300. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  301. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_scripts.py +0 -0
  302. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_semantic_interactions.py +0 -0
  303. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_workflows.py +0 -0
  304. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/tool_registry.py +0 -0
  305. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/common.py +0 -0
  306. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/display.py +0 -0
  307. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/file_modifications.py +0 -0
  308. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/file_operations.py +0 -0
  309. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/file_permission_state.py +0 -0
  310. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/fs_access.py +0 -0
  311. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/image_tools.py +0 -0
  312. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/io_backends.py +0 -0
  313. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/model_tools.py +0 -0
  314. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ripgrep.py +0 -0
  315. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/shell_backgrounding.py +0 -0
  316. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/skills_tools.py +0 -0
  317. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/subagent_context.py +0 -0
  318. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/subagent_invocation.py +0 -0
  319. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/subagent_usage_metrics.py +0 -0
  320. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/tools_content.py +0 -0
  321. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/universal_constructor.py +0 -0
  322. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/undo_manager.py +0 -0
  323. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/universal_constructor_provider.py +0 -0
  324. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/version_checker.py +0 -0
  325. {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/zai_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code-puppy
3
- Version: 0.0.894
3
+ Version: 0.0.896
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
@@ -77,6 +77,36 @@ def _httpx_client_factory(
77
77
  return factory
78
78
 
79
79
 
80
+ def _with_inherited_windows_system_paths(
81
+ env: Optional[Dict[str, str]],
82
+ ) -> Optional[Dict[str, str]]:
83
+ """Propagate Windows system paths required by some stdio launchers.
84
+
85
+ The MCP SDK gives stdio children a deliberately restricted environment.
86
+ Its Windows allowlist omits ``PROGRAMW6432`` and ``ProgramData``. Docker's
87
+ CLI needs the former to discover CLI plugins, while Docker MCP Gateway
88
+ needs the latter to locate Docker Desktop state. Missing either can cause
89
+ misleading argument errors or a startup panic.
90
+
91
+ Preserve the SDK's restricted-environment behavior on other platforms and
92
+ let explicit server configuration override inherited values.
93
+ """
94
+ if os.name != "nt":
95
+ return env
96
+
97
+ inherited = {
98
+ key: value
99
+ for key in ("PROGRAMW6432", "ProgramData")
100
+ if (value := os.environ.get(key))
101
+ }
102
+ if not inherited:
103
+ return env
104
+
105
+ out: Dict[str, str] = inherited
106
+ out.update(env or {})
107
+ return out
108
+
109
+
80
110
  def _with_inherited_ca_bundle(
81
111
  env: Optional[Dict[str, str]],
82
112
  ) -> Optional[Dict[str, str]]:
@@ -336,6 +366,7 @@ class ManagedMCPServer:
336
366
  args = _expand_env_vars(args)
337
367
 
338
368
  env = _expand_env_vars(config["env"]) if "env" in config else None
369
+ env = _with_inherited_windows_system_paths(env)
339
370
  # Always run (even with no configured env) so the child trusts
340
371
  # our CA bundle; see _with_inherited_ca_bundle for why.
341
372
  env = _with_inherited_ca_bundle(env)
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import ctypes
3
2
  import os
4
3
  import select
5
4
  import signal
@@ -37,6 +36,13 @@ from code_puppy.tools.shell_backgrounding import (
37
36
  request_background_all,
38
37
  )
39
38
  from code_puppy.tools.subagent_context import is_subagent
39
+ from code_puppy.tools.win_shell_io import (
40
+ IS_WINDOWS,
41
+ NonBlockingPipeReader,
42
+ attach_job,
43
+ release_job,
44
+ terminate_job,
45
+ )
40
46
 
41
47
  # Maximum line length for shell command output to prevent massive token usage
42
48
  # This helps avoid exceeding model context limits when commands produce very long lines
@@ -50,52 +56,23 @@ def _truncate_line(line: str) -> str:
50
56
  return line
51
57
 
52
58
 
53
- # Windows-specific: Check if pipe has data available without blocking
54
- # This is needed because select() doesn't work on pipes on Windows
55
- if sys.platform.startswith("win"):
56
- import msvcrt
57
-
58
- # Load kernel32 for PeekNamedPipe
59
- _kernel32 = ctypes.windll.kernel32
60
-
61
- def _win32_pipe_has_data(pipe) -> bool:
62
- """Check if a Windows pipe has data available without blocking.
63
-
64
- Uses PeekNamedPipe from kernel32.dll to check if there's data
65
- in the pipe buffer without actually reading it.
66
-
67
- Args:
68
- pipe: A file object with a fileno() method (e.g., process.stdout)
69
-
70
- Returns:
71
- True if data is available, False otherwise (including on error)
72
- """
73
- try:
74
- # Get the Windows handle from the file descriptor
75
- handle = msvcrt.get_osfhandle(pipe.fileno())
76
-
77
- # PeekNamedPipe: NULL buffer/0 size = peek only; grab lpTotalBytesAvail.
78
- bytes_available = ctypes.c_ulong(0)
79
-
80
- result = _kernel32.PeekNamedPipe(
81
- handle,
82
- None, # Don't read data
83
- 0, # Buffer size 0
84
- None, # Don't care about bytes read
85
- ctypes.byref(bytes_available), # Get bytes available
86
- None, # Don't care about bytes left in message
87
- )
59
+ def _close_pipes(process: subprocess.Popen, readers=()) -> bool:
60
+ """Close the child's pipes, unless a reader thread is still alive.
88
61
 
89
- if result:
90
- return bytes_available.value > 0
91
- return False
92
- except (ValueError, OSError, ctypes.ArgumentError):
93
- # Handle closed, invalid, or other errors
94
- return False
95
- else:
96
- # POSIX stub - not used, but keeps the code clean
97
- def _win32_pipe_has_data(pipe) -> bool:
62
+ Closing under a live reader can deadlock: on Windows ``close()`` waits
63
+ for the CRT fd lock a blocked read holds, and buffered streams wait on
64
+ their own lock everywhere. A leaked fd beats a bricked puppy, so if a
65
+ reader is alive we skip the close and leave cleanup to GC.
66
+ """
67
+ if any(t is not None and t.is_alive() for t in readers):
98
68
  return False
69
+ for pipe in (process.stdout, process.stderr, process.stdin):
70
+ try:
71
+ if pipe and not pipe.closed:
72
+ pipe.close()
73
+ except (OSError, ValueError):
74
+ pass
75
+ return True
99
76
 
100
77
 
101
78
  _AWAITING_USER_INPUT = threading.Event()
@@ -153,11 +130,13 @@ def _unregister_process(proc: subprocess.Popen) -> None:
153
130
  def _kill_process_group(proc: subprocess.Popen) -> None:
154
131
  """Attempt to aggressively terminate a process and its group.
155
132
 
156
- Cross-platform best-effort. On POSIX, uses process groups. On Windows, tries taskkill with /T flag for tree kill.
133
+ Cross-platform best-effort. On POSIX, uses process groups. On Windows,
134
+ terminates the shell's Job Object (this reaches orphans whose parent
135
+ already exited), then runs taskkill /T for anything outside the job.
157
136
  """
158
137
  try:
159
- if sys.platform.startswith("win"):
160
- # On Windows, use taskkill to kill the process tree
138
+ if IS_WINDOWS:
139
+ terminate_job(proc)
161
140
  # /F = force, /T = kill tree (children), /PID = process ID
162
141
  try:
163
142
  import subprocess as sp
@@ -238,17 +217,11 @@ def kill_all_running_shell_processes() -> int:
238
217
  count = 0
239
218
  for p in procs:
240
219
  try:
241
- # Close pipes first to unblock readline()
242
- try:
243
- if p.stdout and not p.stdout.closed:
244
- p.stdout.close()
245
- if p.stderr and not p.stderr.closed:
246
- p.stderr.close()
247
- if p.stdin and not p.stdin.closed:
248
- p.stdin.close()
249
- except (OSError, ValueError):
250
- pass
251
-
220
+ # Deliberately NO pipe closing here: a reader may be mid-read,
221
+ # and close() then blocks this (Ctrl+C) thread forever, which is
222
+ # exactly how a stray `start` used to brick cancellation. The
223
+ # stop events above end the readers, and the streaming pump
224
+ # closes the pipes once they're gone.
252
225
  if p.poll() is None:
253
226
  _kill_process_group(p)
254
227
  count += 1
@@ -707,134 +680,51 @@ def run_shell_command_streaming(
707
680
  if not silent:
708
681
  emit_shell_line(line, stream=stream)
709
682
 
710
- def read_stdout():
711
- try:
712
- fd = process.stdout.fileno()
713
- except (ValueError, OSError):
714
- return
715
-
716
- try:
717
- while True:
718
- # Check stop event first
719
- if stop_event.is_set():
683
+ def _emit(line, lines_list, stream):
684
+ # Strip CR/LF: a stray \r would retrigger the renderer's redraw bypass.
685
+ _sink(_truncate_line(line.rstrip("\r\n")), lines_list, stream)
686
+ last_output_time[0] = time.time()
687
+
688
+ def _pump_windows(pipe, lines_list, stream):
689
+ # Never blocks (see win_shell_io), so stop_event is always honoured
690
+ # and nobody ends up holding the CRT fd lock that close() needs.
691
+ reader = NonBlockingPipeReader(pipe)
692
+ while not stop_event.is_set():
693
+ # Check exit BEFORE reading, so the read that follows sees
694
+ # everything the shell wrote before it died.
695
+ exited = process.poll() is not None
696
+ for line in reader.read_available():
697
+ _emit(line, lines_list, stream)
698
+ # Once the shell is gone and the pipe is empty we're done. Don't
699
+ # wait for EOF: a `start`ed orphan may hold the write-end forever.
700
+ if reader.eof or (exited and reader.drained):
701
+ break
702
+ if reader.drained:
703
+ time.sleep(0.1)
704
+ for line in reader.finish():
705
+ _emit(line, lines_list, stream)
706
+
707
+ def _pump_posix(pipe, lines_list, stream):
708
+ fd = pipe.fileno()
709
+ while not stop_event.is_set():
710
+ try:
711
+ ready, _, _ = select.select([fd], [], [], 0.1)
712
+ except (ValueError, OSError, select.error):
713
+ break
714
+ if ready:
715
+ line = pipe.readline()
716
+ if not line: # EOF
720
717
  break
718
+ _emit(line, lines_list, stream)
721
719
 
722
- # Use select to check if data is available (with timeout)
723
- if sys.platform.startswith("win"):
724
- # Windows: no select on pipes — PeekNamedPipe to check availability
725
- try:
726
- if _win32_pipe_has_data(process.stdout):
727
- line = process.stdout.readline()
728
- if not line: # EOF
729
- break
730
- line = line.rstrip("\r\n")
731
- line = _truncate_line(line)
732
- _sink(line, stdout_lines, "stdout")
733
- last_output_time[0] = time.time()
734
- else:
735
- # No data available, check if process has exited
736
- if process.poll() is not None:
737
- # Process exited, do one final drain
738
- try:
739
- remaining = process.stdout.read()
740
- if remaining:
741
- for line in remaining.split("\n"):
742
- # Strip CR/LF like the readline path —
743
- # Windows CRLF's stray \r would retrigger
744
- # the renderer's redraw bypass.
745
- line = line.rstrip("\r\n")
746
- line = _truncate_line(line)
747
- _sink(line, stdout_lines, "stdout")
748
- except (ValueError, OSError):
749
- pass
750
- break
751
- # Sleep briefly to avoid busy-waiting (100ms like POSIX)
752
- time.sleep(0.1)
753
- except (ValueError, OSError):
754
- break
755
- else:
756
- # POSIX: use select with timeout
757
- try:
758
- ready, _, _ = select.select([fd], [], [], 0.1) # 100ms timeout
759
- except (ValueError, OSError, select.error):
760
- break
761
-
762
- if ready:
763
- line = process.stdout.readline()
764
- if not line: # EOF
765
- break
766
- line = line.rstrip("\r\n")
767
- line = _truncate_line(line)
768
- _sink(line, stdout_lines, "stdout")
769
- last_output_time[0] = time.time()
770
- # If not ready, loop continues and checks stop event again
771
- except (ValueError, OSError):
772
- pass
773
- except Exception:
774
- pass
775
-
776
- def read_stderr():
777
- try:
778
- fd = process.stderr.fileno()
779
- except (ValueError, OSError):
780
- return
781
-
720
+ def pump(pipe, lines_list, stream):
782
721
  try:
783
- while True:
784
- # Check stop event first
785
- if stop_event.is_set():
786
- break
787
-
788
- if sys.platform.startswith("win"):
789
- # Windows: no select on pipes — PeekNamedPipe to check availability
790
- try:
791
- if _win32_pipe_has_data(process.stderr):
792
- line = process.stderr.readline()
793
- if not line: # EOF
794
- break
795
- line = line.rstrip("\r\n")
796
- line = _truncate_line(line)
797
- _sink(line, stderr_lines, "stderr")
798
- last_output_time[0] = time.time()
799
- else:
800
- # No data available, check if process has exited
801
- if process.poll() is not None:
802
- # Process exited, do one final drain
803
- try:
804
- remaining = process.stderr.read()
805
- if remaining:
806
- for line in remaining.split("\n"):
807
- # Strip CR/LF like the readline path —
808
- # Windows CRLF's stray \r would retrigger
809
- # the renderer's redraw bypass.
810
- line = line.rstrip("\r\n")
811
- line = _truncate_line(line)
812
- _sink(line, stderr_lines, "stderr")
813
- except (ValueError, OSError):
814
- pass
815
- break
816
- # Sleep briefly to avoid busy-waiting (100ms like POSIX)
817
- time.sleep(0.1)
818
- except (ValueError, OSError):
819
- break
820
- else:
821
- try:
822
- ready, _, _ = select.select([fd], [], [], 0.1)
823
- except (ValueError, OSError, select.error):
824
- break
825
-
826
- if ready:
827
- line = process.stderr.readline()
828
- if not line: # EOF
829
- break
830
- line = line.rstrip("\r\n")
831
- line = _truncate_line(line)
832
- _sink(line, stderr_lines, "stderr")
833
- last_output_time[0] = time.time()
834
- except (ValueError, OSError):
835
- pass
722
+ if IS_WINDOWS:
723
+ _pump_windows(pipe, lines_list, stream)
724
+ else:
725
+ _pump_posix(pipe, lines_list, stream)
836
726
  except Exception:
837
- pass
727
+ pass # closed/invalid pipe: nothing left to read
838
728
 
839
729
  def cleanup_process_and_threads(timeout_type: str = "unknown"):
840
730
  nonlocal stdout_thread, stderr_thread
@@ -849,17 +739,6 @@ def run_shell_command_streaming(
849
739
  if process.poll() is None:
850
740
  nuclear_kill(process)
851
741
 
852
- try:
853
- if process.stdout and not process.stdout.closed:
854
- process.stdout.close()
855
- if process.stderr and not process.stderr.closed:
856
- process.stderr.close()
857
- if process.stdin and not process.stdin.closed:
858
- process.stdin.close()
859
- except (OSError, ValueError):
860
- pass
861
-
862
- # Unregister once we're done cleaning up
863
742
  _unregister_process(process)
864
743
 
865
744
  if stdout_thread and stdout_thread.is_alive():
@@ -878,6 +757,9 @@ def run_shell_command_streaming(
878
757
  message_group=group_id,
879
758
  )
880
759
 
760
+ # Only after the joins: closing under a live reader can deadlock.
761
+ _close_pipes(process, (stdout_thread, stderr_thread))
762
+
881
763
  except Exception as e:
882
764
  if not silent:
883
765
  emit_warning(
@@ -945,8 +827,12 @@ def run_shell_command_streaming(
945
827
  )
946
828
 
947
829
  try:
948
- stdout_thread = threading.Thread(target=read_stdout, daemon=True)
949
- stderr_thread = threading.Thread(target=read_stderr, daemon=True)
830
+ stdout_thread = threading.Thread(
831
+ target=pump, args=(process.stdout, stdout_lines, "stdout"), daemon=True
832
+ )
833
+ stderr_thread = threading.Thread(
834
+ target=pump, args=(process.stderr, stderr_lines, "stderr"), daemon=True
835
+ )
950
836
 
951
837
  stdout_thread.start()
952
838
  stderr_thread.start()
@@ -978,17 +864,8 @@ def run_shell_command_streaming(
978
864
  exit_code = process.returncode
979
865
  execution_time = time.time() - start_time
980
866
 
981
- try:
982
- if process.stdout and not process.stdout.closed:
983
- process.stdout.close()
984
- if process.stderr and not process.stderr.closed:
985
- process.stderr.close()
986
- if process.stdin and not process.stdin.closed:
987
- process.stdin.close()
988
- except (OSError, ValueError):
989
- pass
990
-
991
867
  _unregister_process(process)
868
+ _close_pipes(process, (stdout_thread, stderr_thread))
992
869
 
993
870
  # Apply line length limits to stdout/stderr before returning
994
871
  truncated_stdout = stdout_lines[-256:]
@@ -1431,7 +1308,7 @@ def _run_command_sync(
1431
1308
  """
1432
1309
  creationflags = 0
1433
1310
  preexec_fn = None
1434
- if sys.platform.startswith("win"):
1311
+ if IS_WINDOWS:
1435
1312
  try:
1436
1313
  creationflags = (
1437
1314
  subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
@@ -1456,6 +1333,8 @@ def _run_command_sync(
1456
1333
  preexec_fn=preexec_fn,
1457
1334
  creationflags=creationflags,
1458
1335
  )
1336
+ # Windows: job-wrap the shell so a kill reaches orphaned grandchildren.
1337
+ attach_job(process)
1459
1338
 
1460
1339
  # Wrap pipes with TextIOWrapper that preserves \r (newline='' disables translation)
1461
1340
  process.stdout = io.TextIOWrapper(
@@ -1472,6 +1351,7 @@ def _run_command_sync(
1472
1351
  finally:
1473
1352
  # Ensure unregistration in case streaming returned early or raised
1474
1353
  _unregister_process(process)
1354
+ release_job(process) # no KILL_ON_JOB_CLOSE: survivors keep running
1475
1355
 
1476
1356
 
1477
1357
  async def _run_command_inner(
@@ -0,0 +1,161 @@
1
+ """Windows shell-pipe I/O and process-tree control that cannot wedge the puppy.
2
+
3
+ Split from ``command_runner`` (600-line cap). It handles two Windows hazards:
4
+
5
+ 1. **Inherited pipe handles.** ``cmd /c "build && start server"`` exits at
6
+ once, but ``start``'s child inherits our pipe write-ends, so EOF never
7
+ arrives. A blocking ``read()``/``readline()`` then hangs forever. Worse,
8
+ ``close()`` on that fd blocks too: it needs the CRT per-fd lock that the
9
+ stuck read holds. That wedges both the tool call and the Ctrl+C kill
10
+ path. ``NonBlockingPipeReader`` only ``ReadFile``s the bytes that
11
+ ``PeekNamedPipe`` already reports as available, and goes through
12
+ ``_winapi`` on the raw OS handle, so it never blocks and never takes the
13
+ CRT lock.
14
+
15
+ 2. **Orphaned grandchildren.** ``taskkill /T`` walks parent PIDs, so once an
16
+ intermediate shell exits, its children can't be reached. The ``*_job``
17
+ helpers put each shell in a Job Object so a kill reaches the whole tree.
18
+ There is deliberately no ``KILL_ON_JOB_CLOSE``: releasing the job after a
19
+ normal finish must leave processes the user started on purpose alone.
20
+ Small race: anything the shell spawns before ``attach_job`` runs (a few
21
+ ms after ``CreateProcess``) stays outside the job, and ``taskkill /T``
22
+ still covers those.
23
+
24
+ Every helper is a safe no-op off Windows, so callers don't need to branch.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import codecs
30
+ import io
31
+ import subprocess
32
+ import sys
33
+ import threading
34
+
35
+ IS_WINDOWS = sys.platform.startswith("win")
36
+
37
+ if IS_WINDOWS:
38
+ import ctypes
39
+ import msvcrt
40
+ from ctypes import wintypes
41
+
42
+ import _winapi
43
+
44
+ # Private WinDLL: setting argtypes on the shared ctypes.windll would leak
45
+ # into every other module that uses kernel32.
46
+ _kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
47
+ _kernel32.CreateJobObjectW.argtypes = (wintypes.LPVOID, wintypes.LPCWSTR)
48
+ _kernel32.CreateJobObjectW.restype = wintypes.HANDLE
49
+ _kernel32.AssignProcessToJobObject.argtypes = (wintypes.HANDLE, wintypes.HANDLE)
50
+ _kernel32.AssignProcessToJobObject.restype = wintypes.BOOL
51
+ _kernel32.TerminateJobObject.argtypes = (wintypes.HANDLE, wintypes.UINT)
52
+ _kernel32.TerminateJobObject.restype = wintypes.BOOL
53
+ _kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
54
+ _kernel32.CloseHandle.restype = wintypes.BOOL
55
+
56
+
57
+ class NonBlockingPipeReader:
58
+ """Line reader over a Windows anonymous pipe that never blocks.
59
+
60
+ Splits on ``\\r\\n``, ``\\r`` and ``\\n``, the same as the
61
+ ``TextIOWrapper(newline="")`` readline it replaces, and keeps a trailing
62
+ ``\\r`` that lands at a chunk boundary until the next chunk arrives.
63
+ Decodes as UTF-8 with replacement characters.
64
+ """
65
+
66
+ _CHUNK = 64 * 1024
67
+ #: Bounded so a firehose writer can't keep us from rechecking stop_event.
68
+ _MAX_CHUNKS_PER_CALL = 16
69
+
70
+ def __init__(self, pipe) -> None:
71
+ self._handle = msvcrt.get_osfhandle(pipe.fileno())
72
+ utf8 = codecs.getincrementaldecoder("utf-8")(errors="replace")
73
+ self._decoder = io.IncrementalNewlineDecoder(utf8, translate=True)
74
+ self._partial = ""
75
+ self._finished = False
76
+ #: Every write-end is closed (or our handle is gone).
77
+ self.eof = False
78
+ #: The last ``read_available`` call emptied the pipe.
79
+ self.drained = False
80
+
81
+ def read_available(self) -> list[str]:
82
+ """Return the complete lines in the pipe right now. Never blocks."""
83
+ chunks: list[str] = []
84
+ self.drained = False
85
+ for _ in range(self._MAX_CHUNKS_PER_CALL):
86
+ try:
87
+ available, _left = _winapi.PeekNamedPipe(self._handle)
88
+ if not available:
89
+ self.drained = True
90
+ break
91
+ data, _err = _winapi.ReadFile(self._handle, min(available, self._CHUNK))
92
+ except OSError: # ERROR_BROKEN_PIPE: no writers left. Real EOF.
93
+ self.eof = self.drained = True
94
+ break
95
+ chunks.append(self._decoder.decode(data))
96
+ lines = self._split("".join(chunks))
97
+ return lines + self.finish() if self.eof else lines
98
+
99
+ def finish(self) -> list[str]:
100
+ """Flush the decoder and the trailing partial line. Idempotent."""
101
+ if self._finished:
102
+ return []
103
+ self._finished = True
104
+ lines = self._split(self._decoder.decode(b"", final=True))
105
+ if self._partial:
106
+ lines.append(self._partial)
107
+ self._partial = ""
108
+ return lines
109
+
110
+ def _split(self, text: str) -> list[str]:
111
+ lines = (self._partial + text).split("\n")
112
+ self._partial = lines.pop()
113
+ return lines
114
+
115
+
116
+ # Job handles keyed by Popen, held only while the shell is in the foreground.
117
+ _JOBS: dict[subprocess.Popen, int] = {}
118
+ _JOBS_LOCK = threading.Lock()
119
+
120
+
121
+ def attach_job(proc: subprocess.Popen) -> None:
122
+ """Put ``proc`` (and everything it spawns afterwards) in a fresh Job Object."""
123
+ if not IS_WINDOWS:
124
+ return
125
+ job = _kernel32.CreateJobObjectW(None, None)
126
+ if not job:
127
+ return
128
+ # Popen._handle is the real process handle, which avoids any PID-reuse race.
129
+ if not _kernel32.AssignProcessToJobObject(job, int(proc._handle)):
130
+ _kernel32.CloseHandle(job) # e.g. the process already exited
131
+ return
132
+ with _JOBS_LOCK:
133
+ _JOBS[proc] = job
134
+
135
+
136
+ def terminate_job(proc: subprocess.Popen) -> bool:
137
+ """Kill every process in ``proc``'s job, orphans included."""
138
+ if not IS_WINDOWS:
139
+ return False
140
+ with _JOBS_LOCK:
141
+ job = _JOBS.get(proc)
142
+ return bool(job) and bool(_kernel32.TerminateJobObject(job, 1))
143
+
144
+
145
+ def release_job(proc: subprocess.Popen) -> None:
146
+ """Drop our job handle. Doesn't kill anything (no KILL_ON_JOB_CLOSE)."""
147
+ if not IS_WINDOWS:
148
+ return
149
+ with _JOBS_LOCK:
150
+ job = _JOBS.pop(proc, None)
151
+ if job:
152
+ _kernel32.CloseHandle(job)
153
+
154
+
155
+ __all__ = [
156
+ "IS_WINDOWS",
157
+ "NonBlockingPipeReader",
158
+ "attach_job",
159
+ "release_job",
160
+ "terminate_job",
161
+ ]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.894"
7
+ version = "0.0.896"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.15"
File without changes
File without changes
File without changes