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.
- {code_puppy-0.0.894 → code_puppy-0.0.896}/PKG-INFO +1 -1
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/managed_server.py +31 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/command_runner.py +87 -207
- code_puppy-0.0.896/code_puppy/tools/win_shell_io.py +161 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/pyproject.toml +1 -1
- {code_puppy-0.0.894 → code_puppy-0.0.896}/.gitignore +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/LICENSE +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/README.md +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/__main__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/_plugin_i18n_lifecycle.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/_pydantic_tool_helpers.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/_pydantic_video_patch.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agent_completion_inbox.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agent_execution_context.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_builder.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_code_mode.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_code_mode_guidance.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_compaction.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_diagnostics.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_foreign_thinking.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_history.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_key_listeners.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_model_message_transform.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_non_streaming_render.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_output_limits.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_run_signals.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_runtime.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_steer_processor.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_subagent_recursion.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_windows_console.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/_wire_tool_names.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_code_puppy.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_creator_agent.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_helios.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_manager.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_model_judge.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_planning.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_qa_kitten.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/agent_web_retriever.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/base_agent.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/event_stream_handler.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/json_agent.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/retry_checkpoint.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/retry_profiles.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/run_stats.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/smooth_stream.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/stream_status.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/subagent_stream_handler.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/agents/thinking_output.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/asyncio_cleanup.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/atomic_io.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/atomic_json.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/ca_bundle.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/callbacks.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/capabilities/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/capabilities/compaction.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/capabilities/eager_timing.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/chatgpt_codex_client.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/claude_cache_client.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/claude_oauth_transport.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/cli_runner.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/add_model_details.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/add_model_menu.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/add_model_menus.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/add_model_vllm.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/agent_menu.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/attachments.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/autosave_menu.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/autosave_search.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/clipboard.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/colors_menu.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/command_handler.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/command_registry.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/completers.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/completion_cache.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/config_apply.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/config_commands.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/core_commands.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/file_index.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/file_path_completion.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/help_catalog.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/help_overlay.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/image_utils.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/load_context_completion.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/base.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/edit_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/handler.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/help_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/install_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/install_menu.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/list_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/logs_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/mcp_servers_store.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/remove_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/restart_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/search_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/silence_warning_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/start_all_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/start_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/status_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/stop_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/trust_command.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/utils.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp_binding_menu.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/mcp_completion.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/menu_session.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/model_picker_completion.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/model_settings_defs.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/model_settings_menu.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/onboarding_slides.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/onboarding_wizard.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/pagination.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/pin_command_completion.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/refresh_models.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/session_browser.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/session_browser_data.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/session_commands.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu_catalog.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu_schema.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu_settings.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu_shims.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/set_menu_values.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/shell_passthrough.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/skills_completion.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/tui_style.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/uc_menu.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/command_line/utils.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/config.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/config_file.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/diagnostics.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/error_logging.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/events/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/events/bridge.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/file_completion_io.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/file_completion_tokens.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/gemini_code_assist.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/gemini_common.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/gemini_model.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/README.md +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/aliases.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/engine.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/executor.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/matcher.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/models.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/registry.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/hook_engine/validator.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/http_retry.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/http_utils.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/httpx2_utils.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/audit.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/catalog.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/formats.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/interpolation.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/locale.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/locales/en-US.json +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/locales/es.json +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/locales/fr-CA.json +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/plugin_catalog.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/plurals.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/pseudo.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/i18n/translate.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/kennel_provider.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/keymap.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/list_filtering.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/main.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/agent_bindings.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/async_lifecycle.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/blocking_startup.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/circuit_breaker.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/config_wizard.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/dashboard.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/error_isolation.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/examples/retry_example.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/health_monitor.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/http_auth.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/manager.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/manager_lifecycle.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/mcp_logs.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/project_config.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/registry.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/retry_manager.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/server_registry_catalog.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/status_tracker.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/system_tools.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/tool_arg_coercion.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_/toolset_utils.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_prompts/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/mcp_prompts/hook_creator.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/bar_painters.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/bar_rendering.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/bottom_bar.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/bus.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/chords.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/commands.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_actions.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_completion.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_display.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_history.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_keys.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_paste.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_queue.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/editor_submission.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/external_editor.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/identity_line.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/idle_status.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/inline_bar.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/line_editor.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/markdown_patches.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/menu_lifecycle.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/message_queue.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/messages.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/pause_controller.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/prompt_prefix_style.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/queue_console.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/renderers.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/rich_renderer.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/run_ui.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/run_ui_wiring.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/speculation_line.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/speculation_stats.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/speculation_toggle.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/spinner/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/status_line.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/subagent_console.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/theme_accent.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/tool_output.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/messaging/transcript_guard.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/model_descriptions.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/model_factory.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/model_switching.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/model_utils.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/models.json +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/models_dev_parser.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/observability.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/platform_utils.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/config.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/oauth_pasteback.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/oauth_puppy_html.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/trust.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/plugins/trust_notice.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/private_inference.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/provider_credentials.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/provider_identity.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/pydantic_patches.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/reopenable_async_client.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/round_robin_model.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/secret_store.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/secret_store_backends.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/session_format_migration.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/session_lifecycle.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/session_migration.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/session_storage.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/session_surrogate_unpickler.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/shared_credentials.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/skill_provider.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/splash.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/status_display.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/steer_metadata.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/terminal_utils.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/token_usage.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/_browser_registry.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/_lazy.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/agent_tools.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/approval_feedback.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/constants.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/demo_tui.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/handler.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/models.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/registration.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/renderers.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/terminal_ui.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/theme.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ask_user_question/tui_loop.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/__init__.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_control.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_interactions.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_locator_resolver.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_locators.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_manager.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_navigation.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_page_snapshot.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_screenshot.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_scripts.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_semantic_interactions.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/browser_workflows.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/browser/tool_registry.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/common.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/display.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/file_modifications.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/file_operations.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/file_permission_state.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/fs_access.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/image_tools.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/io_backends.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/model_tools.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/ripgrep.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/shell_backgrounding.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/skills_tools.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/subagent_context.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/subagent_invocation.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/subagent_usage_metrics.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/tools_content.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/tools/universal_constructor.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/undo_manager.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/universal_constructor_provider.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/version_checker.py +0 -0
- {code_puppy-0.0.894 → code_puppy-0.0.896}/code_puppy/zai_model.py +0 -0
|
@@ -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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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,
|
|
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
|
|
160
|
-
|
|
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
|
-
#
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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(
|
|
949
|
-
|
|
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
|
|
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
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|