code-puppy 0.0.784__tar.gz → 0.0.786__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.784 → code_puppy-0.0.786}/.gitignore +4 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/PKG-INFO +5 -4
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_run_signals.py +65 -1
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/claude_cache_client.py +21 -16
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/cli_runner.py +82 -28
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/trust_command.py +5 -2
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/config.py +10 -1
- code_puppy-0.0.786/code_puppy/diagnostics.py +124 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/locales/en-US.json +2 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/manager.py +17 -6
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/__init__.py +19 -14
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/session_surrogate_unpickler.py +3 -11
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/file_modifications.py +94 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/pyproject.toml +5 -4
- {code_puppy-0.0.784 → code_puppy-0.0.786}/LICENSE +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/README.md +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/__main__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agent_execution_context.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_builder.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_compaction.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_diagnostics.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_history.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_key_listeners.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_model_message_transform.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_non_streaming_render.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_output_limits.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_runtime.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_steer_processor.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_subagent_recursion.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_code_puppy.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_creator_agent.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_helios.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_manager.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_model_judge.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_planning.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_qa_kitten.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_web_retriever.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/base_agent.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/event_stream_handler.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/json_agent.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/retry_profiles.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/run_stats.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/smooth_stream.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/subagent_stream_handler.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/atomic_io.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/atomic_json.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/callbacks.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/chatgpt_codex_client.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/add_model_details.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/add_model_menu.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/agent_menu.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/attachments.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/autosave_menu.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/autosave_search.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/clipboard.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/colors_menu.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/command_handler.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/command_registry.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/completers.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/completion_cache.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/config_apply.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/config_commands.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/core_commands.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/file_index.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/file_path_completion.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/help_catalog.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/help_overlay.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/image_utils.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/load_context_completion.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/base.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/edit_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/handler.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/help_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/install_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/install_menu.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/list_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/logs_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/mcp_servers_store.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/remove_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/restart_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/search_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/silence_warning_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/start_all_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/start_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/status_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/stop_command.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/utils.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp_binding_menu.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp_completion.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/menu_session.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/model_picker_completion.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/model_settings_defs.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/model_settings_menu.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/onboarding_slides.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/onboarding_wizard.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/pagination.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/pin_command_completion.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/session_browser.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/session_browser_data.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/session_commands.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu_catalog.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu_schema.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu_settings.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu_shims.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu_values.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/shell_passthrough.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/skills_completion.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/tui_style.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/uc_menu.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/utils.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/config_file.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/error_logging.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/gemini_code_assist.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/gemini_model.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/README.md +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/aliases.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/engine.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/executor.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/matcher.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/models.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/registry.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/validator.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/http_utils.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/audit.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/catalog.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/formats.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/locale.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/locales/es.json +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/locales/fr-CA.json +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/plurals.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/pseudo.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/translate.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/kennel_provider.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/keymap.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/list_filtering.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/main.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/agent_bindings.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/async_lifecycle.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/blocking_startup.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/circuit_breaker.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/config_wizard.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/dashboard.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/error_isolation.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/examples/retry_example.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/health_monitor.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/managed_server.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/mcp_logs.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/project_config.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/registry.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/retry_manager.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/server_registry_catalog.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/status_tracker.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/system_tools.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/tool_arg_coercion.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/toolset_utils.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_prompts/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_prompts/hook_creator.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/bar_painters.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/bar_rendering.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/bottom_bar.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/bus.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/chords.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/commands.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_actions.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_completion.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_display.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_history.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_keys.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_paste.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/external_editor.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/inline_bar.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/line_editor.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/markdown_patches.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/message_queue.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/messages.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/pause_controller.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/prompt_prefix_style.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/queue_console.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/renderers.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/rich_renderer.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/run_ui.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/run_ui_wiring.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/spinner/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/subagent_console.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/transcript_guard.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/model_descriptions.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/model_factory.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/model_switching.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/model_utils.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/models.json +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/models_dev_parser.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/observability.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/platform_utils.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/config.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/oauth_pasteback.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/oauth_puppy_html.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/trust.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/trust_notice.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/provider_credentials.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/provider_identity.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/pydantic_patches.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/reopenable_async_client.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/round_robin_model.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/secret_store.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/secret_store_backends.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/session_format_migration.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/session_lifecycle.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/session_migration.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/session_storage.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/skill_provider.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/splash.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/status_display.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/terminal_utils.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/token_usage.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/agent_tools.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/constants.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/demo_tui.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/handler.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/models.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/registration.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/renderers.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/terminal_ui.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/theme.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/tui_loop.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/__init__.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_control.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_interactions.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_locator_resolver.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_locators.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_manager.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_navigation.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_page_snapshot.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_screenshot.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_scripts.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_semantic_interactions.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_workflows.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/tool_registry.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/command_runner.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/common.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/display.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/file_operations.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/file_permission_state.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/fs_access.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/image_tools.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/io_backends.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/model_tools.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/shell_backgrounding.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/skills_tools.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/subagent_context.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/subagent_invocation.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/subagent_usage_metrics.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/tools_content.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/universal_constructor.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/undo_manager.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/universal_constructor_provider.py +0 -0
- {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/version_checker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: code-puppy
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.786
|
|
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
|
|
@@ -16,11 +16,12 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Topic :: Software Development :: Code Generators
|
|
17
17
|
Requires-Python: <3.15,>=3.11
|
|
18
18
|
Requires-Dist: agent-client-protocol<0.12,>=0.11.0
|
|
19
|
-
Requires-Dist: anthropic<1
|
|
19
|
+
Requires-Dist: anthropic<2,>=1.0.0
|
|
20
20
|
Requires-Dist: anyio>=4.7.0
|
|
21
21
|
Requires-Dist: azure-identity>=1.15.0
|
|
22
22
|
Requires-Dist: boto3>=1.43.9
|
|
23
|
-
Requires-Dist: code-puppy-core-plugins>=0.0.
|
|
23
|
+
Requires-Dist: code-puppy-core-plugins>=0.0.27
|
|
24
|
+
Requires-Dist: httpx2>=2.7
|
|
24
25
|
Requires-Dist: httpx[http2]>=0.24.1
|
|
25
26
|
Requires-Dist: json-repair>=0.46.2
|
|
26
27
|
Requires-Dist: keyring>=24.0.0
|
|
@@ -31,7 +32,7 @@ Requires-Dist: packaging>=24.0
|
|
|
31
32
|
Requires-Dist: pillow>=10.0.0
|
|
32
33
|
Requires-Dist: playwright>=1.40.0; sys_platform != 'android'
|
|
33
34
|
Requires-Dist: pydantic-ai-harness==0.23.0
|
|
34
|
-
Requires-Dist: pydantic-ai-slim[anthropic,mcp,openai]==2.
|
|
35
|
+
Requires-Dist: pydantic-ai-slim[anthropic,mcp,openai]==2.35.0
|
|
35
36
|
Requires-Dist: pydantic>=2.4.0
|
|
36
37
|
Requires-Dist: pyfiglet>=0.8.post1
|
|
37
38
|
Requires-Dist: python-dotenv>=1.0.0
|
|
@@ -9,12 +9,51 @@ daemon thread.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
|
+
import time
|
|
12
13
|
from typing import Any, Callable, Optional
|
|
13
14
|
|
|
14
15
|
from code_puppy.command_line.attachments import resolve_steer_content
|
|
15
16
|
from code_puppy.messaging import emit_info, emit_warning
|
|
16
17
|
from code_puppy.tools.agent_tools import _active_subagent_tasks
|
|
17
18
|
|
|
19
|
+
# =============================================================================
|
|
20
|
+
# Run-detach seam — the escape hatch for zombie cancellations
|
|
21
|
+
# =============================================================================
|
|
22
|
+
#
|
|
23
|
+
# A cancelled run with sub-agents/MCP servers can get stuck unwinding (anyio
|
|
24
|
+
# cancel-scope teardown, plugin cancel hooks, ...). The REPL awaits the run
|
|
25
|
+
# task, the persistent prompt stays visually alive, and every quit gesture is
|
|
26
|
+
# gated on ``is_run_active()`` — so a stuck unwind freezes the whole app.
|
|
27
|
+
# The seam below lets repeated cancel gestures escalate to "abandon the run":
|
|
28
|
+
# ``run_prompt_with_attachments`` installs an event and races it against the
|
|
29
|
+
# agent task, so setting it unblocks the REPL even if the task never finishes.
|
|
30
|
+
|
|
31
|
+
#: A second cancel gesture this many seconds after the first one on a run
|
|
32
|
+
#: that STILL hasn't finished unwinding escalates to a detach.
|
|
33
|
+
CANCEL_ESCALATE_AFTER_S = 3.0
|
|
34
|
+
|
|
35
|
+
_detach_event: Optional[asyncio.Event] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def install_detach_event(event: asyncio.Event) -> None:
|
|
39
|
+
"""Register the top-level run's detach event (cli_runner owns it)."""
|
|
40
|
+
global _detach_event
|
|
41
|
+
_detach_event = event
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def clear_detach_event() -> None:
|
|
45
|
+
"""Drop the seam once the run's await has resolved (normal or detached)."""
|
|
46
|
+
global _detach_event
|
|
47
|
+
_detach_event = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def request_run_detach() -> bool:
|
|
51
|
+
"""Fire the detach event. Loop-thread only. Returns True if one was armed."""
|
|
52
|
+
if _detach_event is None:
|
|
53
|
+
return False
|
|
54
|
+
_detach_event.set()
|
|
55
|
+
return True
|
|
56
|
+
|
|
18
57
|
|
|
19
58
|
def sigint_should_cancel() -> bool:
|
|
20
59
|
"""Buffer-first Ctrl+C gate for the run's SIGINT handler.
|
|
@@ -61,6 +100,12 @@ def make_schedule_cancel(
|
|
|
61
100
|
terminal) while letting the cancel actually proceed.
|
|
62
101
|
"""
|
|
63
102
|
|
|
103
|
+
# Closure state: monotonic time of the first cancel request, so a later
|
|
104
|
+
# gesture on a still-stuck unwind can escalate to a detach (list, not a
|
|
105
|
+
# bare float, to stay writable from the nested function without nonlocal
|
|
106
|
+
# gymnastics on multiple call sites).
|
|
107
|
+
first_cancel_at: list = []
|
|
108
|
+
|
|
64
109
|
def schedule_agent_cancel(force: bool = False) -> None:
|
|
65
110
|
from code_puppy.tools.command_runner import (
|
|
66
111
|
_RUNNING_PROCESSES,
|
|
@@ -70,6 +115,21 @@ def make_schedule_cancel(
|
|
|
70
115
|
|
|
71
116
|
if agent_task.done():
|
|
72
117
|
return
|
|
118
|
+
# Escalation: the user already cancelled, waited, and is cancelling
|
|
119
|
+
# again — the unwind is stuck. Abandon the run so the REPL (and
|
|
120
|
+
# every quit gesture it gates) comes back. The zombie task is left
|
|
121
|
+
# to finish — or not — in the background.
|
|
122
|
+
now = time.monotonic()
|
|
123
|
+
if first_cancel_at and now - first_cancel_at[0] >= CANCEL_ESCALATE_AFTER_S:
|
|
124
|
+
_tear_down_live_panels()
|
|
125
|
+
emit_warning(
|
|
126
|
+
"\nRun is stuck cancelling — abandoning it and returning "
|
|
127
|
+
"to the prompt..."
|
|
128
|
+
)
|
|
129
|
+
loop.call_soon_threadsafe(request_run_detach)
|
|
130
|
+
return
|
|
131
|
+
if not first_cancel_at:
|
|
132
|
+
first_cancel_at.append(now)
|
|
73
133
|
if _RUNNING_PROCESSES and not force:
|
|
74
134
|
# Ordering matters (see _shell_sigint_handler): banner BEFORE the
|
|
75
135
|
# kill — the sweep blocks this thread ~2s per process, so the
|
|
@@ -236,11 +296,15 @@ def drain_pause_state_on_cancel() -> None:
|
|
|
236
296
|
)
|
|
237
297
|
|
|
238
298
|
|
|
239
|
-
|
|
299
|
+
__all__ = [
|
|
300
|
+
"CANCEL_ESCALATE_AFTER_S",
|
|
301
|
+
"clear_detach_event",
|
|
240
302
|
"drain_pause_state_on_cancel",
|
|
241
303
|
"inject_interrupted_subagent_notes",
|
|
304
|
+
"install_detach_event",
|
|
242
305
|
"make_schedule_cancel",
|
|
243
306
|
"prepare_queued_steer_injection",
|
|
307
|
+
"request_run_detach",
|
|
244
308
|
"reset_pause_state_at_run_start",
|
|
245
309
|
"sigint_should_cancel",
|
|
246
310
|
]
|
|
@@ -5,6 +5,11 @@ Prompt caching is configured through pydantic-ai's native
|
|
|
5
5
|
markers; it only owns transport concerns that cannot be expressed there:
|
|
6
6
|
OAuth refresh/retry, Claude Code tool-name prefixing, request headers, URL
|
|
7
7
|
parameters, and the Opus summarized-thinking compatibility transform.
|
|
8
|
+
|
|
9
|
+
Built on ``httpx2`` (not ``httpx``): every consumer of this client hands it
|
|
10
|
+
to ``anthropic.AsyncAnthropic``, and the Anthropic SDK moved to httpx2 in
|
|
11
|
+
its 1.0 release (pydantic-ai >= 2.35 followed). The rest of Code Puppy
|
|
12
|
+
(OpenAI providers, http_utils) still rides classic httpx.
|
|
8
13
|
"""
|
|
9
14
|
|
|
10
15
|
from __future__ import annotations
|
|
@@ -17,7 +22,7 @@ import time
|
|
|
17
22
|
from typing import Any, Callable, MutableMapping
|
|
18
23
|
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
|
19
24
|
|
|
20
|
-
import
|
|
25
|
+
import httpx2
|
|
21
26
|
|
|
22
27
|
logger = logging.getLogger(__name__)
|
|
23
28
|
|
|
@@ -61,7 +66,7 @@ def _enforce_thinking_display_summary(payload):
|
|
|
61
66
|
return True
|
|
62
67
|
|
|
63
68
|
|
|
64
|
-
class ClaudeCacheAsyncClient(
|
|
69
|
+
class ClaudeCacheAsyncClient(httpx2.AsyncClient):
|
|
65
70
|
"""Async HTTP client with Claude Code OAuth transformations.
|
|
66
71
|
|
|
67
72
|
Handles:
|
|
@@ -128,7 +133,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
128
133
|
logger.debug("Failed to decode JWT age: %s", exc)
|
|
129
134
|
return None
|
|
130
135
|
|
|
131
|
-
def _extract_bearer_token(self, request:
|
|
136
|
+
def _extract_bearer_token(self, request: httpx2.Request) -> str | None:
|
|
132
137
|
"""Extract the bearer token from request headers."""
|
|
133
138
|
auth_header = request.headers.get("Authorization") or request.headers.get(
|
|
134
139
|
"authorization"
|
|
@@ -137,7 +142,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
137
142
|
return auth_header[7:] # Strip "Bearer " prefix
|
|
138
143
|
return None
|
|
139
144
|
|
|
140
|
-
def _jwt_refresh_decision(self, request:
|
|
145
|
+
def _jwt_refresh_decision(self, request: httpx2.Request) -> bool | None:
|
|
141
146
|
"""Return a JWT-based refresh decision, or ``None`` for stored fallback."""
|
|
142
147
|
token = self._extract_bearer_token(request)
|
|
143
148
|
if not token:
|
|
@@ -163,14 +168,14 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
163
168
|
)
|
|
164
169
|
return should_refresh
|
|
165
170
|
|
|
166
|
-
def _should_refresh_token(self, request:
|
|
171
|
+
def _should_refresh_token(self, request: httpx2.Request) -> bool:
|
|
167
172
|
"""Synchronously check JWT age, then the stored-token callback."""
|
|
168
173
|
decision = self._jwt_refresh_decision(request)
|
|
169
174
|
if decision is not None:
|
|
170
175
|
return decision
|
|
171
176
|
return self._log_stored_token_refresh(self._check_stored_token_expiry())
|
|
172
177
|
|
|
173
|
-
async def _should_refresh_token_async(self, request:
|
|
178
|
+
async def _should_refresh_token_async(self, request: httpx2.Request) -> bool:
|
|
174
179
|
"""Check token expiry while awaiting async providers in ``send()``."""
|
|
175
180
|
decision = self._jwt_refresh_decision(request)
|
|
176
181
|
if decision is not None:
|
|
@@ -320,7 +325,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
320
325
|
del headers[key]
|
|
321
326
|
|
|
322
327
|
@staticmethod
|
|
323
|
-
def _add_beta_query_param(url:
|
|
328
|
+
def _add_beta_query_param(url: httpx2.URL) -> httpx2.URL:
|
|
324
329
|
"""Add ?beta=true query parameter to the URL if not already present."""
|
|
325
330
|
parsed = urlparse(str(url))
|
|
326
331
|
query_params = parse_qs(parsed.query)
|
|
@@ -328,12 +333,12 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
328
333
|
query_params["beta"] = ["true"]
|
|
329
334
|
new_query = urlencode(query_params, doseq=True)
|
|
330
335
|
new_parsed = parsed._replace(query=new_query)
|
|
331
|
-
return
|
|
336
|
+
return httpx2.URL(urlunparse(new_parsed))
|
|
332
337
|
return url
|
|
333
338
|
|
|
334
339
|
async def send(
|
|
335
|
-
self, request:
|
|
336
|
-
) ->
|
|
340
|
+
self, request: httpx2.Request, *args: Any, **kwargs: Any
|
|
341
|
+
) -> httpx2.Response: # type: ignore[override]
|
|
337
342
|
is_messages_endpoint = request.url.path.endswith("/v1/messages")
|
|
338
343
|
if not request.extensions.get("claude_oauth_proactive_refresh_attempted"):
|
|
339
344
|
try:
|
|
@@ -450,10 +455,10 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
450
455
|
return response
|
|
451
456
|
|
|
452
457
|
async def _send_with_retries(
|
|
453
|
-
self, request:
|
|
454
|
-
) ->
|
|
458
|
+
self, request: httpx2.Request, *args: Any, **kwargs: Any
|
|
459
|
+
) -> httpx2.Response:
|
|
455
460
|
"""Retry rate limits, server failures, and transient connections."""
|
|
456
|
-
last_response:
|
|
461
|
+
last_response: httpx2.Response | None = None
|
|
457
462
|
last_exception: Exception | None = None
|
|
458
463
|
for attempt in range(MAX_RETRIES + 1):
|
|
459
464
|
status_code: int | None = None
|
|
@@ -467,7 +472,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
467
472
|
return response
|
|
468
473
|
status_code = response.status_code
|
|
469
474
|
await response.aclose()
|
|
470
|
-
except (
|
|
475
|
+
except (httpx2.ConnectError, httpx2.ReadTimeout, httpx2.PoolTimeout) as exc:
|
|
471
476
|
last_exception = exc
|
|
472
477
|
if attempt >= MAX_RETRIES:
|
|
473
478
|
raise
|
|
@@ -515,7 +520,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
515
520
|
raise RuntimeError("Retry loop completed without response or exception")
|
|
516
521
|
|
|
517
522
|
@staticmethod
|
|
518
|
-
def _extract_body_bytes(request:
|
|
523
|
+
def _extract_body_bytes(request: httpx2.Request) -> bytes | None:
|
|
519
524
|
try:
|
|
520
525
|
content = request.content
|
|
521
526
|
if content:
|
|
@@ -547,7 +552,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
547
552
|
headers["Authorization"] = bearer_value
|
|
548
553
|
|
|
549
554
|
@staticmethod
|
|
550
|
-
async def _is_cloudflare_html_error(response:
|
|
555
|
+
async def _is_cloudflare_html_error(response: httpx2.Response) -> bool:
|
|
551
556
|
"""Return whether a 400 HTML response is a Cloudflare auth failure."""
|
|
552
557
|
if "text/html" not in response.headers.get("content-type", "").lower():
|
|
553
558
|
return False
|
|
@@ -741,6 +741,35 @@ def _interactive_sigint_guard(_sig, _frame):
|
|
|
741
741
|
return
|
|
742
742
|
|
|
743
743
|
|
|
744
|
+
#: How long the quit paths (double Ctrl+C / Ctrl+D / exit) wait for a
|
|
745
|
+
#: lingering agent task to finish cancelling before abandoning it. A stuck
|
|
746
|
+
#: unwind must never freeze the exit — process teardown reaps the zombie.
|
|
747
|
+
_QUIT_CANCEL_TIMEOUT_S = 5.0
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
async def _shutdown_agent_task(agent_task) -> None:
|
|
751
|
+
"""Cancel a lingering agent task on quit, bounded by a timeout.
|
|
752
|
+
|
|
753
|
+
``asyncio.wait`` (not ``wait_for``) on purpose: ``wait_for``'s timeout
|
|
754
|
+
path awaits the inner cancellation, which is exactly the await that can
|
|
755
|
+
wedge. ``wait`` just stops waiting.
|
|
756
|
+
"""
|
|
757
|
+
from code_puppy.messaging import emit_warning
|
|
758
|
+
|
|
759
|
+
if agent_task is None or agent_task.done():
|
|
760
|
+
return
|
|
761
|
+
emit_info(t("cli.agent.cancelling"))
|
|
762
|
+
agent_task.cancel()
|
|
763
|
+
done, _ = await asyncio.wait({agent_task}, timeout=_QUIT_CANCEL_TIMEOUT_S)
|
|
764
|
+
if agent_task not in done:
|
|
765
|
+
emit_warning(t("cli.agent.quit_cancel_timeout", seconds=_QUIT_CANCEL_TIMEOUT_S))
|
|
766
|
+
return
|
|
767
|
+
try:
|
|
768
|
+
agent_task.result()
|
|
769
|
+
except BaseException:
|
|
770
|
+
pass # Expected when cancelling — nothing to do with it on the way out.
|
|
771
|
+
|
|
772
|
+
|
|
744
773
|
async def interactive_mode(message_renderer, initial_command: str = None) -> None:
|
|
745
774
|
"""Run the agent in interactive mode."""
|
|
746
775
|
from code_puppy.command_line.command_handler import handle_command
|
|
@@ -837,6 +866,16 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
837
866
|
except (ValueError, OSError):
|
|
838
867
|
pass
|
|
839
868
|
|
|
869
|
+
# Field diagnostics: `kill -USR2 <pid>` dumps thread + asyncio-task
|
|
870
|
+
# stacks to ~/.code_puppy/stackdumps/ — the conviction kit for wedged
|
|
871
|
+
# cancellations (a frozen REPL can't be introspected any other way).
|
|
872
|
+
try:
|
|
873
|
+
from code_puppy.diagnostics import install_stack_dump_handler
|
|
874
|
+
|
|
875
|
+
install_stack_dump_handler(asyncio.get_running_loop())
|
|
876
|
+
except Exception:
|
|
877
|
+
pass # diagnostics must never block the REPL
|
|
878
|
+
|
|
840
879
|
# ------------------------------------------------------------------
|
|
841
880
|
# Persistent-prompt mode: the bottom-bar editor is THE prompt (idle AND
|
|
842
881
|
# running) — pinned input row, output scrolls above. Classic path behind
|
|
@@ -921,13 +960,7 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
921
960
|
now = time.monotonic()
|
|
922
961
|
if was_plain_ctrl_c and now - last_idle_ctrl_c <= DOUBLE_CTRL_C_WINDOW_S:
|
|
923
962
|
emit_success("\n" + t("cli.goodbye_ctrld"))
|
|
924
|
-
|
|
925
|
-
emit_info(t("cli.agent.cancelling"))
|
|
926
|
-
current_agent_task.cancel()
|
|
927
|
-
try:
|
|
928
|
-
await current_agent_task
|
|
929
|
-
except asyncio.CancelledError:
|
|
930
|
-
pass # Expected when cancelling
|
|
963
|
+
await _shutdown_agent_task(current_agent_task)
|
|
931
964
|
break
|
|
932
965
|
last_idle_ctrl_c = now if was_plain_ctrl_c else 0.0
|
|
933
966
|
|
|
@@ -940,14 +973,9 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
940
973
|
|
|
941
974
|
emit_success("\n" + t("cli.goodbye_ctrld"))
|
|
942
975
|
|
|
943
|
-
# Cancel any running agent task for clean shutdown
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
current_agent_task.cancel()
|
|
947
|
-
try:
|
|
948
|
-
await current_agent_task
|
|
949
|
-
except asyncio.CancelledError:
|
|
950
|
-
pass # Expected when cancelling
|
|
976
|
+
# Cancel any running agent task for clean shutdown (bounded — a
|
|
977
|
+
# stuck unwind must not freeze Ctrl+D).
|
|
978
|
+
await _shutdown_agent_task(current_agent_task)
|
|
951
979
|
|
|
952
980
|
break
|
|
953
981
|
|
|
@@ -975,14 +1003,9 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
975
1003
|
|
|
976
1004
|
emit_success(t("cli.goodbye"))
|
|
977
1005
|
|
|
978
|
-
# Cancel any running agent task for clean shutdown
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
current_agent_task.cancel()
|
|
982
|
-
try:
|
|
983
|
-
await current_agent_task
|
|
984
|
-
except asyncio.CancelledError:
|
|
985
|
-
pass # Expected when cancelling
|
|
1006
|
+
# Cancel any running agent task for clean shutdown (bounded — a
|
|
1007
|
+
# stuck unwind must not freeze `exit`).
|
|
1008
|
+
await _shutdown_agent_task(current_agent_task)
|
|
986
1009
|
|
|
987
1010
|
# The renderer is stopped in the finally block of main().
|
|
988
1011
|
break
|
|
@@ -1365,13 +1388,44 @@ async def run_prompt_with_attachments(
|
|
|
1365
1388
|
)
|
|
1366
1389
|
)
|
|
1367
1390
|
|
|
1391
|
+
# Escape hatch: a cancelled run can wedge mid-unwind (sub-agent/MCP
|
|
1392
|
+
# cancel-scope teardown). Racing the task against a detach event keeps
|
|
1393
|
+
# the REPL recoverable — repeated cancel gestures escalate to a detach
|
|
1394
|
+
# (see _run_signals.make_schedule_cancel) and we abandon the zombie.
|
|
1395
|
+
from code_puppy.agents._run_signals import (
|
|
1396
|
+
clear_detach_event,
|
|
1397
|
+
install_detach_event,
|
|
1398
|
+
)
|
|
1399
|
+
|
|
1400
|
+
detach_event = asyncio.Event()
|
|
1401
|
+
install_detach_event(detach_event)
|
|
1402
|
+
|
|
1368
1403
|
async def _await_agent():
|
|
1404
|
+
detach_wait = asyncio.create_task(detach_event.wait())
|
|
1369
1405
|
try:
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1406
|
+
try:
|
|
1407
|
+
done, _ = await asyncio.wait(
|
|
1408
|
+
{agent_task, detach_wait}, return_when=asyncio.FIRST_COMPLETED
|
|
1409
|
+
)
|
|
1410
|
+
except asyncio.CancelledError:
|
|
1411
|
+
# The waiter itself was cancelled (shutdown teardown) — same
|
|
1412
|
+
# contract as the old bare ``await agent_task`` path.
|
|
1413
|
+
emit_info(t("cli.agent.task_cancelled"))
|
|
1414
|
+
return None, agent_task
|
|
1415
|
+
if agent_task not in done:
|
|
1416
|
+
# Escalated cancel: abandon the stuck unwind, free the REPL.
|
|
1417
|
+
emit_warning(t("cli.agent.detached"))
|
|
1418
|
+
return None, agent_task
|
|
1419
|
+
if agent_task.cancelled():
|
|
1420
|
+
emit_info(t("cli.agent.task_cancelled"))
|
|
1421
|
+
return None, agent_task
|
|
1422
|
+
exc = agent_task.exception()
|
|
1423
|
+
if exc is not None:
|
|
1424
|
+
raise exc
|
|
1425
|
+
return agent_task.result(), agent_task
|
|
1426
|
+
finally:
|
|
1427
|
+
detach_wait.cancel()
|
|
1428
|
+
clear_detach_event()
|
|
1375
1429
|
|
|
1376
1430
|
if use_run_ui:
|
|
1377
1431
|
# Interactive run: bottom bar stays up while the agent works. run_ui() is
|
|
@@ -150,11 +150,14 @@ class TrustCommand(MCPCommandBase):
|
|
|
150
150
|
|
|
151
151
|
def _revoke(self, group_id: str) -> None:
|
|
152
152
|
if revoke_project_mcp():
|
|
153
|
+
try:
|
|
154
|
+
self.manager.sync_from_config()
|
|
155
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
156
|
+
logger.warning("Post-revoke registry sync failed: %s", exc)
|
|
153
157
|
emit_info(
|
|
154
158
|
Text.from_markup(
|
|
155
159
|
"[green]\u2713 Revoked[/green] trust for this project's MCP "
|
|
156
|
-
"config. Its servers will no longer load.
|
|
157
|
-
"to drop already-registered ones."
|
|
160
|
+
"config. Its servers will no longer load."
|
|
158
161
|
),
|
|
159
162
|
message_group=group_id,
|
|
160
163
|
)
|
|
@@ -595,7 +595,7 @@ def _parse_mcp_servers_mapping(raw_text: str) -> dict:
|
|
|
595
595
|
return servers
|
|
596
596
|
|
|
597
597
|
|
|
598
|
-
def load_mcp_server_configs():
|
|
598
|
+
def load_mcp_server_configs(*, raise_on_error: bool = False):
|
|
599
599
|
"""Load MCP server configs, merging user-level and trusted project-level.
|
|
600
600
|
|
|
601
601
|
Sources, in ascending order of precedence:
|
|
@@ -610,6 +610,11 @@ def load_mcp_server_configs():
|
|
|
610
610
|
Project entries win on name collision, matching how project agents, skills,
|
|
611
611
|
and plugins override their user-level counterparts. Returns an empty dict
|
|
612
612
|
when nothing is configured.
|
|
613
|
+
|
|
614
|
+
When *raise_on_error* is true, a parse/IO failure of an existing user-level
|
|
615
|
+
file (or a failure of the project loader) is re-raised after the error is
|
|
616
|
+
emitted, so callers that unregister missing names can skip that drop
|
|
617
|
+
instead of treating ``{}`` as "configure nothing".
|
|
613
618
|
"""
|
|
614
619
|
from code_puppy.messaging.message_queue import emit_error
|
|
615
620
|
|
|
@@ -622,6 +627,8 @@ def load_mcp_server_configs():
|
|
|
622
627
|
configs.update(_parse_mcp_servers_mapping(f.read()))
|
|
623
628
|
except Exception as e:
|
|
624
629
|
emit_error(f"Failed to load MCP servers - {str(e)}")
|
|
630
|
+
if raise_on_error:
|
|
631
|
+
raise
|
|
625
632
|
|
|
626
633
|
# 2. Project-level config (opt-in, trust-gated). A broken or untrusted
|
|
627
634
|
# project file must never break user-level loading.
|
|
@@ -633,6 +640,8 @@ def load_mcp_server_configs():
|
|
|
633
640
|
configs.update(project_configs)
|
|
634
641
|
except Exception as e:
|
|
635
642
|
emit_error(f"Failed to load project MCP servers - {str(e)}")
|
|
643
|
+
if raise_on_error:
|
|
644
|
+
raise
|
|
636
645
|
|
|
637
646
|
return configs
|
|
638
647
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""On-demand stack dumps for wedged runs (``kill -USR2 <pid>``).
|
|
2
|
+
|
|
3
|
+
Field diagnosis for the "cancelled run wedges mid-unwind" class of bug
|
|
4
|
+
(Discord: "death spiral of 2 asyncs waiting for the other to finish").
|
|
5
|
+
``faulthandler`` alone is useless there — the event-loop thread is parked
|
|
6
|
+
in ``select()`` and every interesting frame lives in a *suspended asyncio
|
|
7
|
+
task*, which faulthandler cannot see. This module dumps BOTH:
|
|
8
|
+
|
|
9
|
+
- OS-thread stacks via ``faulthandler.dump_traceback`` (key listener,
|
|
10
|
+
renderer, shell readers, ...); and
|
|
11
|
+
- every asyncio task's coroutine stack via ``Task.print_stack`` (the REPL
|
|
12
|
+
coroutine stuck on ``await agent_task``, the agent task stuck in its
|
|
13
|
+
cancel-scope teardown — the actual smoking gun).
|
|
14
|
+
|
|
15
|
+
Dumps are written to ``~/.code_puppy/stackdumps/`` (never the terminal:
|
|
16
|
+
the bottom-bar TUI owns it, and a wedged app may not repaint anyway) with
|
|
17
|
+
a one-line breadcrumb on stderr. POSIX only — Windows has no SIGUSR2;
|
|
18
|
+
``install_stack_dump_handler`` degrades to a no-op there.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import faulthandler
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
import signal
|
|
28
|
+
import sys
|
|
29
|
+
import time
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Optional, TextIO
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
DUMP_DIR = Path.home() / ".code_puppy" / "stackdumps"
|
|
36
|
+
|
|
37
|
+
#: Frames per task stack. Wedges live near the leaf; unbounded dumps of
|
|
38
|
+
#: deep pydantic-ai graphs just bury the signal.
|
|
39
|
+
TASK_STACK_LIMIT = 40
|
|
40
|
+
|
|
41
|
+
_captured_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def dump_stacks(file: TextIO, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
|
|
45
|
+
"""Write thread stacks + asyncio task stacks to ``file``.
|
|
46
|
+
|
|
47
|
+
Never raises: each section is best-effort so a half-broken process
|
|
48
|
+
can still confess as much as it knows.
|
|
49
|
+
"""
|
|
50
|
+
print(f"=== code-puppy stack dump (pid {os.getpid()}) ===", file=file)
|
|
51
|
+
print("--- OS threads (faulthandler) ---", file=file)
|
|
52
|
+
try:
|
|
53
|
+
file.flush()
|
|
54
|
+
faulthandler.dump_traceback(file=file, all_threads=True)
|
|
55
|
+
except Exception:
|
|
56
|
+
logger.debug("faulthandler dump failed", exc_info=True)
|
|
57
|
+
print("<faulthandler dump failed>", file=file)
|
|
58
|
+
|
|
59
|
+
loop = loop or _captured_loop
|
|
60
|
+
if loop is None:
|
|
61
|
+
print("--- asyncio tasks: no loop captured ---", file=file)
|
|
62
|
+
return
|
|
63
|
+
try:
|
|
64
|
+
tasks = asyncio.all_tasks(loop)
|
|
65
|
+
except Exception:
|
|
66
|
+
logger.debug("asyncio.all_tasks failed", exc_info=True)
|
|
67
|
+
print("--- asyncio tasks: enumeration failed ---", file=file)
|
|
68
|
+
return
|
|
69
|
+
print(f"--- asyncio tasks: {len(tasks)} ---", file=file)
|
|
70
|
+
for task in tasks:
|
|
71
|
+
print(f"\nTask: {task!r}", file=file)
|
|
72
|
+
try:
|
|
73
|
+
task.print_stack(limit=TASK_STACK_LIMIT, file=file)
|
|
74
|
+
except Exception:
|
|
75
|
+
logger.debug("task.print_stack failed", exc_info=True)
|
|
76
|
+
print("<task stack unavailable>", file=file)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def write_stack_dump(
|
|
80
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
81
|
+
) -> Optional[Path]:
|
|
82
|
+
"""Dump to a timestamped file under ``DUMP_DIR``; return its path.
|
|
83
|
+
|
|
84
|
+
Returns ``None`` (and stays silent beyond a debug log) if even the
|
|
85
|
+
file can't be created — diagnostics must never hurt the patient.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
DUMP_DIR.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
path = DUMP_DIR / f"stackdump-{os.getpid()}-{int(time.time())}.txt"
|
|
90
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
91
|
+
dump_stacks(f, loop=loop)
|
|
92
|
+
# stderr, not the message bus: a wedged loop may never render a
|
|
93
|
+
# bus message, and stderr survives a hung TUI.
|
|
94
|
+
print(f"[code-puppy] stack dump written to {path}", file=sys.stderr)
|
|
95
|
+
return path
|
|
96
|
+
except Exception:
|
|
97
|
+
logger.debug("stack dump failed", exc_info=True)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def install_stack_dump_handler(
|
|
102
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
103
|
+
) -> bool:
|
|
104
|
+
"""Register the SIGUSR2 stack-dump handler. Returns True if installed.
|
|
105
|
+
|
|
106
|
+
Safe to call more than once (last loop reference wins). No-op on
|
|
107
|
+
platforms without SIGUSR2 (Windows) and off the main thread, where
|
|
108
|
+
``signal.signal`` raises.
|
|
109
|
+
"""
|
|
110
|
+
global _captured_loop
|
|
111
|
+
if not hasattr(signal, "SIGUSR2"):
|
|
112
|
+
return False
|
|
113
|
+
if loop is not None:
|
|
114
|
+
_captured_loop = loop
|
|
115
|
+
|
|
116
|
+
def _handler(signum, frame) -> None:
|
|
117
|
+
write_stack_dump()
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
signal.signal(signal.SIGUSR2, _handler)
|
|
121
|
+
except (ValueError, OSError):
|
|
122
|
+
logger.debug("SIGUSR2 handler install failed", exc_info=True)
|
|
123
|
+
return False
|
|
124
|
+
return True
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"cli.agent.set_error": "Error setting agent: {error}",
|
|
45
45
|
"cli.agent.cancelling": "Cancelling running agent task...",
|
|
46
46
|
"cli.agent.task_cancelled": "Agent task cancelled",
|
|
47
|
+
"cli.agent.detached": "Run abandoned — it was stuck cancelling. Back at the prompt (the old run may still unwind in the background).",
|
|
48
|
+
"cli.agent.quit_cancel_timeout": "Agent task didn't finish cancelling within {seconds}s — exiting anyway.",
|
|
47
49
|
"cli.resume.quick_searching": "🔍 Quick Resume selected - finding latest session for {scope}",
|
|
48
50
|
"cli.resume.none_found": "No previous session found for this scope; starting fresh.",
|
|
49
51
|
"cli.resume.available_sessions": "Available sessions: {sessions}",
|
|
@@ -161,10 +161,7 @@ class MCPManager:
|
|
|
161
161
|
try:
|
|
162
162
|
from code_puppy.config import load_mcp_server_configs
|
|
163
163
|
|
|
164
|
-
configs = load_mcp_server_configs()
|
|
165
|
-
if not configs:
|
|
166
|
-
logger.debug("No servers found in mcp_servers.json")
|
|
167
|
-
return
|
|
164
|
+
configs = load_mcp_server_configs(raise_on_error=True) or {}
|
|
168
165
|
|
|
169
166
|
synced_count = 0
|
|
170
167
|
updated_count = 0
|
|
@@ -209,9 +206,23 @@ class MCPManager:
|
|
|
209
206
|
logger.warning(f"Failed to sync server '{name}' from config: {e}")
|
|
210
207
|
continue
|
|
211
208
|
|
|
212
|
-
|
|
209
|
+
configured_names = {
|
|
210
|
+
name for name, conf in configs.items() if isinstance(conf, dict)
|
|
211
|
+
}
|
|
212
|
+
dropped_count = 0
|
|
213
|
+
for existing in list(self.registry.list_all()):
|
|
214
|
+
if existing.name not in configured_names:
|
|
215
|
+
if self.remove_server(existing.id):
|
|
216
|
+
dropped_count += 1
|
|
217
|
+
logger.debug(
|
|
218
|
+
"Dropped server no longer in mcp_servers.json: %s",
|
|
219
|
+
existing.name,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if synced_count > 0 or updated_count > 0 or dropped_count > 0:
|
|
213
223
|
logger.info(
|
|
214
|
-
f"Synced {synced_count} new
|
|
224
|
+
f"Synced {synced_count} new, updated {updated_count}, "
|
|
225
|
+
f"dropped {dropped_count} servers from mcp_servers.json"
|
|
215
226
|
)
|
|
216
227
|
|
|
217
228
|
except Exception as e:
|