code-muse 0.0.1__py3-none-any.whl
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_muse/__init__.py +26 -0
- code_muse/__main__.py +10 -0
- code_muse/agents/__init__.py +31 -0
- code_muse/agents/_builder.py +214 -0
- code_muse/agents/_compaction.py +506 -0
- code_muse/agents/_diagnostics.py +171 -0
- code_muse/agents/_history.py +382 -0
- code_muse/agents/_key_listeners.py +148 -0
- code_muse/agents/_non_streaming_render.py +148 -0
- code_muse/agents/_runtime.py +596 -0
- code_muse/agents/agent_creator_agent.py +603 -0
- code_muse/agents/agent_helios.py +47 -0
- code_muse/agents/agent_manager.py +740 -0
- code_muse/agents/agent_muse.py +78 -0
- code_muse/agents/agent_planning.py +44 -0
- code_muse/agents/agent_qa_melpomene.py +207 -0
- code_muse/agents/base_agent.py +194 -0
- code_muse/agents/event_stream_handler.py +361 -0
- code_muse/agents/json_agent.py +201 -0
- code_muse/agents/prompt_v3.py +521 -0
- code_muse/agents/subagent_stream_handler.py +273 -0
- code_muse/callbacks.py +941 -0
- code_muse/chatgpt_codex_client.py +333 -0
- code_muse/claude_cache_client.py +853 -0
- code_muse/cli_runner/__init__.py +319 -0
- code_muse/cli_runner/args.py +63 -0
- code_muse/cli_runner/loop.py +510 -0
- code_muse/cli_runner/resume.py +72 -0
- code_muse/cli_runner/runner.py +161 -0
- code_muse/command_line/__init__.py +1 -0
- code_muse/command_line/add_model_menu.py +1331 -0
- code_muse/command_line/agent_menu.py +674 -0
- code_muse/command_line/attachments.py +397 -0
- code_muse/command_line/autosave_menu.py +709 -0
- code_muse/command_line/clipboard.py +528 -0
- code_muse/command_line/colors_menu.py +530 -0
- code_muse/command_line/command_handler.py +262 -0
- code_muse/command_line/command_registry.py +150 -0
- code_muse/command_line/config_commands.py +711 -0
- code_muse/command_line/core_commands.py +740 -0
- code_muse/command_line/diff_menu.py +865 -0
- code_muse/command_line/file_path_completion.py +73 -0
- code_muse/command_line/load_context_completion.py +57 -0
- code_muse/command_line/model_picker_completion.py +512 -0
- code_muse/command_line/model_settings_menu.py +983 -0
- code_muse/command_line/onboarding_slides.py +162 -0
- code_muse/command_line/onboarding_wizard.py +337 -0
- code_muse/command_line/pagination.py +41 -0
- code_muse/command_line/pin_command_completion.py +329 -0
- code_muse/command_line/prompt_toolkit_completion.py +886 -0
- code_muse/command_line/session_commands.py +304 -0
- code_muse/command_line/shell_passthrough.py +145 -0
- code_muse/command_line/skills_completion.py +158 -0
- code_muse/command_line/types.py +18 -0
- code_muse/command_line/uc_menu.py +908 -0
- code_muse/command_line/utils.py +105 -0
- code_muse/command_line/wiggum_state.py +77 -0
- code_muse/config.py +1138 -0
- code_muse/config_agent.py +168 -0
- code_muse/config_appearance.py +241 -0
- code_muse/config_model.py +357 -0
- code_muse/config_security.py +73 -0
- code_muse/error_logging.py +132 -0
- code_muse/evals/__init__.py +35 -0
- code_muse/evals/eval_helpers.py +81 -0
- code_muse/evals/eval_runner.py +299 -0
- code_muse/evals/sample_evals/__init__.py +1 -0
- code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
- code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
- code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
- code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
- code_muse/fs_scan_cache/__init__.py +31 -0
- code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
- code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
- code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
- code_muse/fs_scan_cache/tool_integration.py +309 -0
- code_muse/fs_scan_cache/ttl_policy.py +44 -0
- code_muse/gemini_code_assist.py +383 -0
- code_muse/gemini_model.py +838 -0
- code_muse/hook_engine/README.md +105 -0
- code_muse/hook_engine/__init__.py +21 -0
- code_muse/hook_engine/aliases.py +153 -0
- code_muse/hook_engine/engine.py +221 -0
- code_muse/hook_engine/executor.py +347 -0
- code_muse/hook_engine/matcher.py +154 -0
- code_muse/hook_engine/models.py +245 -0
- code_muse/hook_engine/registry.py +114 -0
- code_muse/hook_engine/trust.py +268 -0
- code_muse/hook_engine/validator.py +144 -0
- code_muse/http_utils.py +360 -0
- code_muse/keymap.py +128 -0
- code_muse/list_filtering.py +26 -0
- code_muse/main.py +10 -0
- code_muse/messaging/__init__.py +259 -0
- code_muse/messaging/bus.py +621 -0
- code_muse/messaging/commands.py +166 -0
- code_muse/messaging/markdown_patches.py +57 -0
- code_muse/messaging/message_queue.py +397 -0
- code_muse/messaging/messages.py +591 -0
- code_muse/messaging/queue_console.py +269 -0
- code_muse/messaging/renderers.py +308 -0
- code_muse/messaging/rich_renderer.py +1158 -0
- code_muse/messaging/shimmer.py +154 -0
- code_muse/messaging/spinner/__init__.py +87 -0
- code_muse/messaging/spinner/console_spinner.py +250 -0
- code_muse/messaging/spinner/spinner_base.py +82 -0
- code_muse/messaging/subagent_console.py +458 -0
- code_muse/model_factory.py +1203 -0
- code_muse/model_switching.py +59 -0
- code_muse/model_utils.py +156 -0
- code_muse/models.json +66 -0
- code_muse/models_cache/__init__.py +26 -0
- code_muse/models_cache/blocking_lru_cache.py +98 -0
- code_muse/models_cache/cache_writer.py +86 -0
- code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
- code_muse/models_cache/sha256_hash.pyx +34 -0
- code_muse/models_cache/startup_integration.py +75 -0
- code_muse/models_dev_api.json +1 -0
- code_muse/models_dev_parser.py +590 -0
- code_muse/motion.py +126 -0
- code_muse/plugins/__init__.py +471 -0
- code_muse/plugins/agent_skills/__init__.py +32 -0
- code_muse/plugins/agent_skills/config.py +176 -0
- code_muse/plugins/agent_skills/discovery.py +309 -0
- code_muse/plugins/agent_skills/downloader.py +389 -0
- code_muse/plugins/agent_skills/installer.py +19 -0
- code_muse/plugins/agent_skills/metadata.py +293 -0
- code_muse/plugins/agent_skills/prompt_builder.py +66 -0
- code_muse/plugins/agent_skills/register_callbacks.py +298 -0
- code_muse/plugins/agent_skills/remote_catalog.py +320 -0
- code_muse/plugins/agent_skills/skill_catalog.py +254 -0
- code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
- code_muse/plugins/agent_skills/skills_menu.py +791 -0
- code_muse/plugins/autonomous_memory/__init__.py +39 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
- code_muse/plugins/autonomous_memory/consolidation.py +82 -0
- code_muse/plugins/autonomous_memory/extraction.py +382 -0
- code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
- code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
- code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
- code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
- code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
- code_muse/plugins/aws_bedrock/__init__.py +14 -0
- code_muse/plugins/aws_bedrock/config.py +99 -0
- code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
- code_muse/plugins/aws_bedrock/utils.py +153 -0
- code_muse/plugins/azure_foundry/README.md +238 -0
- code_muse/plugins/azure_foundry/__init__.py +15 -0
- code_muse/plugins/azure_foundry/config.py +125 -0
- code_muse/plugins/azure_foundry/discovery.py +187 -0
- code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
- code_muse/plugins/azure_foundry/token.py +180 -0
- code_muse/plugins/azure_foundry/utils.py +345 -0
- code_muse/plugins/build_filter/__init__.py +1 -0
- code_muse/plugins/build_filter/register_callbacks.py +201 -0
- code_muse/plugins/build_filter/strategies/__init__.py +1 -0
- code_muse/plugins/build_filter/strategies/build.py +397 -0
- code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
- code_muse/plugins/chatgpt_oauth/config.py +52 -0
- code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
- code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
- code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_muse/plugins/chatgpt_oauth/utils.py +538 -0
- code_muse/plugins/checkpointing/__init__.py +29 -0
- code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
- code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
- code_muse/plugins/checkpointing/register_callbacks.py +51 -0
- code_muse/plugins/checkpointing/restore_command.py +263 -0
- code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
- code_muse/plugins/checkpointing/shadow_git.py +90 -0
- code_muse/plugins/claude_code_hooks/__init__.py +1 -0
- code_muse/plugins/claude_code_hooks/config.py +188 -0
- code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
- code_muse/plugins/claude_code_oauth/README.md +167 -0
- code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
- code_muse/plugins/claude_code_oauth/__init__.py +25 -0
- code_muse/plugins/claude_code_oauth/config.py +52 -0
- code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
- code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
- code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
- code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
- code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
- code_muse/plugins/claude_code_oauth/utils.py +664 -0
- code_muse/plugins/copilot_auth/__init__.py +11 -0
- code_muse/plugins/copilot_auth/config.py +91 -0
- code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
- code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
- code_muse/plugins/copilot_auth/utils.py +584 -0
- code_muse/plugins/custom_commands/__init__.py +14 -0
- code_muse/plugins/custom_commands/args_injection.py +82 -0
- code_muse/plugins/custom_commands/command_discovery.py +89 -0
- code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
- code_muse/plugins/custom_commands/register_callbacks.py +176 -0
- code_muse/plugins/customizable_commands/__init__.py +0 -0
- code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
- code_muse/plugins/destructive_command_guard/__init__.py +14 -0
- code_muse/plugins/destructive_command_guard/detector.py +375 -0
- code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
- code_muse/plugins/example_custom_command/README.md +280 -0
- code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
- code_muse/plugins/file_permission_handler/__init__.py +4 -0
- code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
- code_muse/plugins/filter_engine/__init__.py +30 -0
- code_muse/plugins/filter_engine/classifier.py +153 -0
- code_muse/plugins/filter_engine/content_detector.py +184 -0
- code_muse/plugins/filter_engine/dispatcher.py +244 -0
- code_muse/plugins/filter_engine/register_callbacks.py +188 -0
- code_muse/plugins/filter_engine/registry.py +279 -0
- code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
- code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
- code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
- code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
- code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
- code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
- code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
- code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
- code_muse/plugins/filter_engine/verbosity.py +63 -0
- code_muse/plugins/force_push_guard/__init__.py +5 -0
- code_muse/plugins/force_push_guard/detector.py +96 -0
- code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
- code_muse/plugins/force_push_guard/test_detector.py +143 -0
- code_muse/plugins/frontend_emitter/__init__.py +25 -0
- code_muse/plugins/frontend_emitter/emitter.py +121 -0
- code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
- code_muse/plugins/gac/__init__.py +4 -0
- code_muse/plugins/gac/git_ops.py +136 -0
- code_muse/plugins/gac/prompt.py +191 -0
- code_muse/plugins/gac/register_callbacks.py +82 -0
- code_muse/plugins/hook_creator/__init__.py +1 -0
- code_muse/plugins/hook_creator/register_callbacks.py +34 -0
- code_muse/plugins/hook_manager/__init__.py +1 -0
- code_muse/plugins/hook_manager/config.py +289 -0
- code_muse/plugins/hook_manager/hooks_menu.py +563 -0
- code_muse/plugins/hook_manager/register_callbacks.py +227 -0
- code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
- code_muse/plugins/mindpack/__init__.py +0 -0
- code_muse/plugins/mindpack/factory.py +930 -0
- code_muse/plugins/mindpack/judge.py +573 -0
- code_muse/plugins/mindpack/memory.py +100 -0
- code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
- code_muse/plugins/mindpack/orchestration.py +605 -0
- code_muse/plugins/mindpack/register_callbacks.py +175 -0
- code_muse/plugins/mindpack/schemas.py +358 -0
- code_muse/plugins/mindpack/tools.py +387 -0
- code_muse/plugins/oauth_muse_html.py +226 -0
- code_muse/plugins/ollama_setup/__init__.py +5 -0
- code_muse/plugins/ollama_setup/completer.py +36 -0
- code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
- code_muse/plugins/plan_command/__init__.py +0 -0
- code_muse/plugins/plan_command/register_callbacks.py +206 -0
- code_muse/plugins/plan_mode/__init__.py +37 -0
- code_muse/plugins/plan_mode/mode_cycling.py +40 -0
- code_muse/plugins/plan_mode/plan_generation.py +68 -0
- code_muse/plugins/plan_mode/plan_hooks.py +74 -0
- code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
- code_muse/plugins/plan_mode/register_callbacks.py +121 -0
- code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
- code_muse/plugins/policy_engine/__init__.py +46 -0
- code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
- code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
- code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
- code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
- code_muse/plugins/policy_engine/register_callbacks.py +112 -0
- code_muse/plugins/pop_command/__init__.py +1 -0
- code_muse/plugins/pop_command/register_callbacks.py +189 -0
- code_muse/plugins/prompt_newline/__init__.py +13 -0
- code_muse/plugins/prompt_newline/config.py +19 -0
- code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
- code_muse/plugins/safety_status/__init__.py +0 -0
- code_muse/plugins/safety_status/register_callbacks.py +113 -0
- code_muse/plugins/semantic_compression/__init__.py +6 -0
- code_muse/plugins/semantic_compression/compressor.py +295 -0
- code_muse/plugins/semantic_compression/config.py +123 -0
- code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
- code_muse/plugins/shell_minimizer/__init__.py +50 -0
- code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
- code_muse/plugins/shell_minimizer/pipeline.py +556 -0
- code_muse/plugins/shell_minimizer/primitives.py +482 -0
- code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
- code_muse/plugins/shell_safety/__init__.py +6 -0
- code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_muse/plugins/shell_safety/command_cache.py +149 -0
- code_muse/plugins/shell_safety/register_callbacks.py +202 -0
- code_muse/plugins/synthetic_status/__init__.py +1 -0
- code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
- code_muse/plugins/synthetic_status/status_api.py +145 -0
- code_muse/plugins/token_caching/__init__.py +21 -0
- code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
- code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
- code_muse/plugins/token_caching/register_callbacks.py +54 -0
- code_muse/plugins/token_caching/stats_display.py +35 -0
- code_muse/plugins/token_tracking/__init__.py +26 -0
- code_muse/plugins/token_tracking/database.py +381 -0
- code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
- code_muse/plugins/token_tracking/record.py +55 -0
- code_muse/plugins/token_tracking/register_callbacks.py +277 -0
- code_muse/plugins/token_tracking/reports.py +329 -0
- code_muse/plugins/universal_constructor/__init__.py +13 -0
- code_muse/plugins/universal_constructor/models.py +136 -0
- code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
- code_muse/plugins/universal_constructor/registry.py +390 -0
- code_muse/plugins/universal_constructor/runner.py +474 -0
- code_muse/plugins/universal_constructor/safety.py +440 -0
- code_muse/plugins/universal_constructor/sandbox.py +584 -0
- code_muse/provider_identity.py +105 -0
- code_muse/pydantic_patches.py +410 -0
- code_muse/reopenable_async_client.py +233 -0
- code_muse/round_robin_model.py +151 -0
- code_muse/secret_storage.py +74 -0
- code_muse/security/__init__.py +1 -0
- code_muse/security/redaction.cpython-314-darwin.so +0 -0
- code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/security/redaction.pyx +135 -0
- code_muse/session_storage.py +565 -0
- code_muse/status_display.py +261 -0
- code_muse/stream_parser/__init__.py +76 -0
- code_muse/stream_parser/assistant_text_parser.py +90 -0
- code_muse/stream_parser/citation_parser.py +76 -0
- code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
- code_muse/stream_parser/proposed_plan_parser.py +158 -0
- code_muse/stream_parser/stream_text_chunk.py +23 -0
- code_muse/stream_parser/stream_text_parser.py +27 -0
- code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
- code_muse/stream_parser/tagged_line_parser.pyx +251 -0
- code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
- code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
- code_muse/summarization_agent.py +308 -0
- code_muse/terminal_utils.cpython-314-darwin.so +0 -0
- code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/terminal_utils.pyx +483 -0
- code_muse/tools/__init__.py +459 -0
- code_muse/tools/agent_tools.py +613 -0
- code_muse/tools/ask_user_question/__init__.py +26 -0
- code_muse/tools/ask_user_question/constants.py +73 -0
- code_muse/tools/ask_user_question/demo_tui.py +55 -0
- code_muse/tools/ask_user_question/handler.py +232 -0
- code_muse/tools/ask_user_question/models.py +302 -0
- code_muse/tools/ask_user_question/registration.py +37 -0
- code_muse/tools/ask_user_question/renderers.py +336 -0
- code_muse/tools/ask_user_question/terminal_ui.py +327 -0
- code_muse/tools/ask_user_question/theme.py +156 -0
- code_muse/tools/ask_user_question/tui_loop.py +422 -0
- code_muse/tools/background_jobs.py +99 -0
- code_muse/tools/browser/__init__.py +37 -0
- code_muse/tools/browser/browser_control.py +289 -0
- code_muse/tools/browser/browser_interactions.py +545 -0
- code_muse/tools/browser/browser_locators.py +640 -0
- code_muse/tools/browser/browser_manager.py +376 -0
- code_muse/tools/browser/browser_navigation.py +251 -0
- code_muse/tools/browser/browser_screenshot.py +180 -0
- code_muse/tools/browser/browser_scripts.py +462 -0
- code_muse/tools/browser/browser_workflows.py +222 -0
- code_muse/tools/chrome_cdp/__init__.py +1070 -0
- code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
- code_muse/tools/command_runner.py +1401 -0
- code_muse/tools/common.py +1407 -0
- code_muse/tools/display.py +87 -0
- code_muse/tools/file_modifications.py +1099 -0
- code_muse/tools/file_operations.py +860 -0
- code_muse/tools/image_tools.py +185 -0
- code_muse/tools/meetin_proxy/__init__.py +243 -0
- code_muse/tools/meetin_proxy/capture_addon.py +82 -0
- code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
- code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
- code_muse/tools/path_policy.py +219 -0
- code_muse/tools/skills_tools.py +586 -0
- code_muse/tools/subagent_context.py +158 -0
- code_muse/tools/tools_content.py +50 -0
- code_muse/tools/universal_constructor.py +965 -0
- code_muse/uvx_detection.py +241 -0
- code_muse/version_checker.py +86 -0
- code_muse-0.0.1.data/data/code_muse/models.json +66 -0
- code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
- code_muse-0.0.1.dist-info/METADATA +845 -0
- code_muse-0.0.1.dist-info/RECORD +394 -0
- code_muse-0.0.1.dist-info/WHEEL +4 -0
- code_muse-0.0.1.dist-info/entry_points.txt +2 -0
- code_muse-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
"""Chrome CDP — Chrome DevTools Protocol inspection tool.
|
|
2
|
+
|
|
3
|
+
Pure-Python CDP client using websockets. No Node.js dependency.
|
|
4
|
+
Connects directly to Chrome's remote-debugging WebSocket endpoint.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import platform
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
from pydantic_ai import RunContext
|
|
20
|
+
|
|
21
|
+
from code_muse.messaging import emit_info, emit_success
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Module-level caches
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
_CHROME_WS: str | None = None
|
|
29
|
+
_PERSISTENT_SESSIONS: dict[str, CdpSession] = {}
|
|
30
|
+
_ACTIVE_TABS_CACHE: dict[str, str] = {} # prefix -> targetId
|
|
31
|
+
_PAGES_CACHE: list[dict[str, Any]] = []
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Pydantic models
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ChromeCdpResult(BaseModel):
|
|
39
|
+
"""Result of a chrome_cdp command."""
|
|
40
|
+
|
|
41
|
+
success: bool = True
|
|
42
|
+
output: str = ""
|
|
43
|
+
screenshot_file: str = ""
|
|
44
|
+
page_list: list[dict[str, Any]] = []
|
|
45
|
+
error: str = ""
|
|
46
|
+
dpr: float = 1.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ChromeCdpInfo(BaseModel):
|
|
50
|
+
"""Status/info about Chrome CDP availability."""
|
|
51
|
+
|
|
52
|
+
available: bool = False
|
|
53
|
+
ws_url: str = ""
|
|
54
|
+
version: str = ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Port / WS discovery
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _get_devtools_port() -> int:
|
|
63
|
+
"""Read Chrome's DevToolsActivePort file from common locations."""
|
|
64
|
+
env_path = os.environ.get("CDP_PORT_FILE")
|
|
65
|
+
if env_path:
|
|
66
|
+
paths = [Path(env_path)]
|
|
67
|
+
else:
|
|
68
|
+
home = Path.home()
|
|
69
|
+
system = platform.system()
|
|
70
|
+
if system == "Darwin":
|
|
71
|
+
base = home / "Library/Application Support"
|
|
72
|
+
else:
|
|
73
|
+
base = home / ".config"
|
|
74
|
+
|
|
75
|
+
profiles = [
|
|
76
|
+
"Google/Chrome",
|
|
77
|
+
"google-chrome",
|
|
78
|
+
"BraveSoftware/Brave-Browser",
|
|
79
|
+
"brave-browser",
|
|
80
|
+
"microsoft-edge",
|
|
81
|
+
"chromium",
|
|
82
|
+
"vivaldi",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
paths: list[Path] = []
|
|
86
|
+
for profile in profiles:
|
|
87
|
+
paths.append(base / profile / "DevToolsActivePort")
|
|
88
|
+
# Flatpak / snap variants
|
|
89
|
+
flatpak_path = (
|
|
90
|
+
".var/app/com.google.Chrome/config/google-chrome/DevToolsActivePort"
|
|
91
|
+
)
|
|
92
|
+
paths.append(home / flatpak_path)
|
|
93
|
+
paths.append(home / "snap/chromium/common/chromium/DevToolsActivePort")
|
|
94
|
+
|
|
95
|
+
for p in paths:
|
|
96
|
+
try:
|
|
97
|
+
text = p.read_text(encoding="utf-8").strip()
|
|
98
|
+
first_line = text.splitlines()[0]
|
|
99
|
+
return int(first_line)
|
|
100
|
+
except Exception:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
raise FileNotFoundError(
|
|
104
|
+
"Could not find Chrome DevToolsActivePort. "
|
|
105
|
+
"Enable remote debugging (chrome://inspect/#devices) or set CDP_PORT_FILE."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _get_browser_ws_url(port: int) -> str:
|
|
110
|
+
"""Fetch the browser WebSocket URL from Chrome's JSON/version endpoint."""
|
|
111
|
+
import urllib.request
|
|
112
|
+
|
|
113
|
+
url = f"http://localhost:{port}/json/version"
|
|
114
|
+
with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310
|
|
115
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
116
|
+
ws_url = data.get("webSocketDebuggerUrl", "")
|
|
117
|
+
if not ws_url:
|
|
118
|
+
raise RuntimeError("No webSocketDebuggerUrl in /json/version")
|
|
119
|
+
return ws_url
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _discover_browser_ws() -> str:
|
|
123
|
+
"""Discover and cache the browser WebSocket URL."""
|
|
124
|
+
global _CHROME_WS
|
|
125
|
+
if _CHROME_WS:
|
|
126
|
+
return _CHROME_WS
|
|
127
|
+
port = _get_devtools_port()
|
|
128
|
+
ws_url = _get_browser_ws_url(port)
|
|
129
|
+
_CHROME_WS = ws_url
|
|
130
|
+
return ws_url
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# HTTP helpers for page listing
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _fetch_json_list(port: int) -> list[dict[str, Any]]:
|
|
139
|
+
"""Fetch the list of open pages from Chrome's JSON/list endpoint."""
|
|
140
|
+
import urllib.request
|
|
141
|
+
|
|
142
|
+
url = f"http://localhost:{port}/json/list"
|
|
143
|
+
with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310
|
|
144
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# WebSocket helpers
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _is_ws_closed(ws: Any) -> bool:
|
|
153
|
+
"""Check whether a websockets connection is closed.
|
|
154
|
+
Compatible with both old and new websockets APIs.
|
|
155
|
+
"""
|
|
156
|
+
if hasattr(ws, "closed"):
|
|
157
|
+
return bool(ws.closed)
|
|
158
|
+
import websockets
|
|
159
|
+
|
|
160
|
+
return ws.state == websockets.State.CLOSED
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async def _ws_connect(ws_url: str) -> Any:
|
|
164
|
+
"""Open a WebSocket connection to the given URL."""
|
|
165
|
+
try:
|
|
166
|
+
import websockets
|
|
167
|
+
except ImportError as exc:
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
"websockets library not installed. Run: pip install websockets"
|
|
170
|
+
) from exc
|
|
171
|
+
|
|
172
|
+
return await websockets.connect(ws_url, open_timeout=5, close_timeout=5)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def _send_cdp(
|
|
176
|
+
ws,
|
|
177
|
+
method: str,
|
|
178
|
+
params: dict[str, Any] | None = None,
|
|
179
|
+
session_id: str | None = None,
|
|
180
|
+
timeout: float = 10.0,
|
|
181
|
+
) -> Any:
|
|
182
|
+
"""Send a CDP command and wait for the matching response."""
|
|
183
|
+
msg_id = _next_msg_id()
|
|
184
|
+
payload: dict[str, Any] = {"id": msg_id, "method": method}
|
|
185
|
+
if params is not None:
|
|
186
|
+
payload["params"] = params
|
|
187
|
+
if session_id is not None:
|
|
188
|
+
payload["sessionId"] = session_id
|
|
189
|
+
|
|
190
|
+
future: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
|
|
191
|
+
_PENDING[msg_id] = future
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
await ws.send(json.dumps(payload))
|
|
195
|
+
msg = await asyncio.wait_for(future, timeout=timeout)
|
|
196
|
+
finally:
|
|
197
|
+
_PENDING.pop(msg_id, None)
|
|
198
|
+
|
|
199
|
+
if "error" in msg:
|
|
200
|
+
err_msg = msg["error"].get("message", str(msg["error"]))
|
|
201
|
+
raise RuntimeError(f"CDP error: {err_msg}")
|
|
202
|
+
return msg.get("result", {})
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
_MSG_COUNTER = 0
|
|
206
|
+
_PENDING: dict[int, asyncio.Future[Any]] = {}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _next_msg_id() -> int:
|
|
210
|
+
global _MSG_COUNTER
|
|
211
|
+
_MSG_COUNTER += 1
|
|
212
|
+
return _MSG_COUNTER
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def _read_ws_loop(ws, event_buffers: dict[str, list] | None = None) -> None:
|
|
216
|
+
"""Background task: reads WS messages, resolves futures, buffers events."""
|
|
217
|
+
import websockets
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
async for raw in ws:
|
|
221
|
+
try:
|
|
222
|
+
msg = json.loads(raw)
|
|
223
|
+
except json.JSONDecodeError:
|
|
224
|
+
continue
|
|
225
|
+
msg_id = msg.get("id")
|
|
226
|
+
method = msg.get("method")
|
|
227
|
+
params = msg.get("params", {})
|
|
228
|
+
|
|
229
|
+
if msg_id is not None and msg_id in _PENDING:
|
|
230
|
+
future = _PENDING.pop(msg_id)
|
|
231
|
+
if not future.done():
|
|
232
|
+
future.set_result(msg)
|
|
233
|
+
|
|
234
|
+
# Route events to buffers (console logs, network events, etc.)
|
|
235
|
+
if method and event_buffers is not None:
|
|
236
|
+
if method in event_buffers:
|
|
237
|
+
event_buffers[method].append(params)
|
|
238
|
+
# Also store under a wildcard for generic "all events"
|
|
239
|
+
if "*" in event_buffers:
|
|
240
|
+
event_buffers["*"].append({"method": method, "params": params})
|
|
241
|
+
except websockets.exceptions.ConnectionClosed:
|
|
242
|
+
pass
|
|
243
|
+
except Exception:
|
|
244
|
+
logger.exception("chrome_cdp WS read loop error")
|
|
245
|
+
finally:
|
|
246
|
+
for fut in list(_PENDING.values()):
|
|
247
|
+
if not fut.done():
|
|
248
|
+
fut.cancel()
|
|
249
|
+
_PENDING.clear()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
# CdpSession
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class CdpSession:
|
|
258
|
+
"""Persistent CDP session for a single target tab."""
|
|
259
|
+
|
|
260
|
+
def __init__(self, target_id: str):
|
|
261
|
+
self.target_id = target_id
|
|
262
|
+
self.ws: Any | None = None
|
|
263
|
+
self.session_id: str | None = None
|
|
264
|
+
self._read_task: asyncio.Task[Any] | None = None
|
|
265
|
+
# Event buffers for monitoring
|
|
266
|
+
self._event_buffers: dict[str, list[dict]] = {}
|
|
267
|
+
self._console_capturing = False
|
|
268
|
+
self._network_capturing = False
|
|
269
|
+
|
|
270
|
+
async def enable_console_capture(self) -> None:
|
|
271
|
+
"""Start capturing console logs from the page."""
|
|
272
|
+
if self._console_capturing:
|
|
273
|
+
return
|
|
274
|
+
self._event_buffers["Runtime.consoleAPICalled"] = []
|
|
275
|
+
self._event_buffers["Runtime.exceptionThrown"] = []
|
|
276
|
+
await self.send("Runtime.enable")
|
|
277
|
+
self._console_capturing = True
|
|
278
|
+
|
|
279
|
+
async def get_console_logs(self) -> list[dict]:
|
|
280
|
+
"""Get captured console logs and clear the buffer."""
|
|
281
|
+
logs = []
|
|
282
|
+
for entry in self._event_buffers.get("Runtime.consoleAPICalled", []):
|
|
283
|
+
args = entry.get("args", [])
|
|
284
|
+
text = " ".join(
|
|
285
|
+
a.get("value", str(a)) if isinstance(a, dict) else str(a) for a in args
|
|
286
|
+
)
|
|
287
|
+
level = entry.get("type", "log")
|
|
288
|
+
logs.append({"level": level, "text": text})
|
|
289
|
+
self._event_buffers["Runtime.consoleAPICalled"] = []
|
|
290
|
+
|
|
291
|
+
exceptions = []
|
|
292
|
+
for exc in self._event_buffers.get("Runtime.exceptionThrown", []):
|
|
293
|
+
details = exc.get("exceptionDetails", {})
|
|
294
|
+
text = details.get("text", "") or str(
|
|
295
|
+
details.get("exception", {}).get("description", "")
|
|
296
|
+
)
|
|
297
|
+
exceptions.append({"level": "exception", "text": text})
|
|
298
|
+
self._event_buffers["Runtime.exceptionThrown"] = []
|
|
299
|
+
|
|
300
|
+
return logs + exceptions
|
|
301
|
+
|
|
302
|
+
async def enable_network_capture(self) -> None:
|
|
303
|
+
"""Start capturing network requests."""
|
|
304
|
+
if self._network_capturing:
|
|
305
|
+
return
|
|
306
|
+
self._event_buffers["Network.requestWillBeSent"] = []
|
|
307
|
+
self._event_buffers["Network.responseReceived"] = []
|
|
308
|
+
self._event_buffers["Network.loadingFailed"] = []
|
|
309
|
+
await self.send("Network.enable")
|
|
310
|
+
self._network_capturing = True
|
|
311
|
+
|
|
312
|
+
async def disable_network_capture(self) -> None:
|
|
313
|
+
"""Stop capturing network requests."""
|
|
314
|
+
if not self._network_capturing:
|
|
315
|
+
return
|
|
316
|
+
import contextlib
|
|
317
|
+
|
|
318
|
+
with contextlib.suppress(Exception):
|
|
319
|
+
await self.send("Network.disable")
|
|
320
|
+
self._network_capturing = False
|
|
321
|
+
|
|
322
|
+
async def get_network_activity(self) -> list[dict]:
|
|
323
|
+
"""Get captured network activity and clear the buffer."""
|
|
324
|
+
# Combine requests and responses by requestId
|
|
325
|
+
requests = {
|
|
326
|
+
r["requestId"]: {
|
|
327
|
+
"url": r.get("request", {}).get("url", ""),
|
|
328
|
+
"method": r.get("request", {}).get("method", ""),
|
|
329
|
+
"type": r.get("type", ""),
|
|
330
|
+
"timestamp": r.get("timestamp", 0),
|
|
331
|
+
}
|
|
332
|
+
for r in self._event_buffers.get("Network.requestWillBeSent", [])
|
|
333
|
+
}
|
|
334
|
+
responses = {
|
|
335
|
+
r["requestId"]: {
|
|
336
|
+
"status": r.get("response", {}).get("status", 0),
|
|
337
|
+
"status_text": r.get("response", {}).get("statusText", ""),
|
|
338
|
+
"content_type": r.get("response", {}).get("mimeType", ""),
|
|
339
|
+
"size": r.get("response", {}).get("transferSize", 0),
|
|
340
|
+
}
|
|
341
|
+
for r in self._event_buffers.get("Network.responseReceived", [])
|
|
342
|
+
}
|
|
343
|
+
failures = {
|
|
344
|
+
f["requestId"]: f.get("errorText", "Failed")
|
|
345
|
+
for f in self._event_buffers.get("Network.loadingFailed", [])
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# Merge
|
|
349
|
+
all_ids = set(requests) | set(responses) | set(failures)
|
|
350
|
+
merged = []
|
|
351
|
+
for rid in all_ids:
|
|
352
|
+
entry = {"request_id": rid}
|
|
353
|
+
entry.update(requests.get(rid, {}))
|
|
354
|
+
entry.update(responses.get(rid, {}))
|
|
355
|
+
if rid in failures:
|
|
356
|
+
entry["error"] = failures[rid]
|
|
357
|
+
merged.append(entry)
|
|
358
|
+
|
|
359
|
+
# Clear buffers
|
|
360
|
+
for key in (
|
|
361
|
+
"Network.requestWillBeSent",
|
|
362
|
+
"Network.responseReceived",
|
|
363
|
+
"Network.loadingFailed",
|
|
364
|
+
):
|
|
365
|
+
self._event_buffers[key] = []
|
|
366
|
+
|
|
367
|
+
return merged
|
|
368
|
+
|
|
369
|
+
async def get_source(self, url: str) -> str:
|
|
370
|
+
"""Get the source code of a script or style by URL."""
|
|
371
|
+
# Try as script source first
|
|
372
|
+
try:
|
|
373
|
+
result = await self.send("Debugger.getScriptSource", {"scriptId": url})
|
|
374
|
+
return result.get("scriptSource", "")
|
|
375
|
+
except Exception:
|
|
376
|
+
pass
|
|
377
|
+
# Try getting source from page via eval (fetch the URL content)
|
|
378
|
+
result = await self.eval(f"fetch({json.dumps(url)}).then(r => r.text())")
|
|
379
|
+
return result
|
|
380
|
+
|
|
381
|
+
async def ensure_connected(self) -> None:
|
|
382
|
+
"""Connect to the browser WS, attach to target, and store sessionId."""
|
|
383
|
+
if self.ws is not None and not _is_ws_closed(self.ws):
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
browser_ws = _discover_browser_ws()
|
|
387
|
+
self.ws = await _ws_connect(browser_ws)
|
|
388
|
+
self._read_task = asyncio.create_task(
|
|
389
|
+
_read_ws_loop(self.ws, self._event_buffers)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
result = await _send_cdp(
|
|
393
|
+
self.ws,
|
|
394
|
+
"Target.attachToTarget",
|
|
395
|
+
{"targetId": self.target_id, "flatten": True},
|
|
396
|
+
)
|
|
397
|
+
self.session_id = result.get("sessionId")
|
|
398
|
+
if not self.session_id:
|
|
399
|
+
raise RuntimeError("No sessionId returned from attachToTarget")
|
|
400
|
+
|
|
401
|
+
async def send(
|
|
402
|
+
self,
|
|
403
|
+
method: str,
|
|
404
|
+
params: dict[str, Any] | None = None,
|
|
405
|
+
timeout: float = 10.0,
|
|
406
|
+
) -> Any:
|
|
407
|
+
"""Send a CDP command via the target session."""
|
|
408
|
+
if self.ws is None or _is_ws_closed(self.ws):
|
|
409
|
+
await self.ensure_connected()
|
|
410
|
+
return await _send_cdp(
|
|
411
|
+
self.ws, method, params, session_id=self.session_id, timeout=timeout
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
async def disconnect(self) -> None:
|
|
415
|
+
"""Close the WebSocket and clean up."""
|
|
416
|
+
if self._read_task is not None:
|
|
417
|
+
self._read_task.cancel()
|
|
418
|
+
self._read_task = None
|
|
419
|
+
if self.ws is not None:
|
|
420
|
+
import contextlib
|
|
421
|
+
|
|
422
|
+
with contextlib.suppress(Exception):
|
|
423
|
+
await self.ws.close()
|
|
424
|
+
self.ws = None
|
|
425
|
+
self.session_id = None
|
|
426
|
+
|
|
427
|
+
async def eval(self, expression: str) -> str:
|
|
428
|
+
"""Evaluate a JS expression and return the string result."""
|
|
429
|
+
result = await self.send(
|
|
430
|
+
"Runtime.evaluate",
|
|
431
|
+
{"expression": expression, "returnByValue": True},
|
|
432
|
+
)
|
|
433
|
+
res = result.get("result", {})
|
|
434
|
+
if res.get("subtype") == "error":
|
|
435
|
+
desc = res.get("description", res.get("value", "JS error"))
|
|
436
|
+
raise RuntimeError(desc)
|
|
437
|
+
val = res.get("value")
|
|
438
|
+
return str(val) if val is not None else ""
|
|
439
|
+
|
|
440
|
+
async def get_screenshot(self) -> tuple[bytes, float]:
|
|
441
|
+
"""Capture a screenshot and return (bytes, dpr)."""
|
|
442
|
+
result = await self.send("Page.captureScreenshot", {"format": "png"})
|
|
443
|
+
b64 = result.get("data", "")
|
|
444
|
+
if not b64:
|
|
445
|
+
raise RuntimeError("Screenshot data empty")
|
|
446
|
+
import base64
|
|
447
|
+
|
|
448
|
+
raw = base64.b64decode(b64)
|
|
449
|
+
|
|
450
|
+
# Try to fetch DPR
|
|
451
|
+
dpr = 1.0
|
|
452
|
+
try:
|
|
453
|
+
metrics = await self.send("Page.getLayoutMetrics")
|
|
454
|
+
dpr = metrics.get("visualViewport", {}).get("scale", 1.0)
|
|
455
|
+
except Exception:
|
|
456
|
+
pass
|
|
457
|
+
return raw, float(dpr)
|
|
458
|
+
|
|
459
|
+
async def get_snapshot(self) -> str:
|
|
460
|
+
"""Fetch accessibility tree and return formatted text."""
|
|
461
|
+
result = await self.send("Accessibility.getFullAXTree")
|
|
462
|
+
nodes = result.get("nodes", [])
|
|
463
|
+
if not nodes:
|
|
464
|
+
return "(empty accessibility tree)"
|
|
465
|
+
lines: list[str] = []
|
|
466
|
+
for node in nodes:
|
|
467
|
+
role = node.get("role", {}).get("value", "")
|
|
468
|
+
name = node.get("name", {}).get("value", "")
|
|
469
|
+
if role or name:
|
|
470
|
+
lines.append(f"[{role}] {name}")
|
|
471
|
+
return "\n".join(lines) if lines else "(empty accessibility tree)"
|
|
472
|
+
|
|
473
|
+
async def navigate(self, url: str) -> None:
|
|
474
|
+
"""Navigate to a URL and wait for load."""
|
|
475
|
+
# Enable Page events first
|
|
476
|
+
await self.send("Page.enable")
|
|
477
|
+
result = await self.send("Page.navigate", {"url": url})
|
|
478
|
+
frame_id = result.get("frameId")
|
|
479
|
+
if not frame_id:
|
|
480
|
+
raise RuntimeError("Navigation failed: no frameId")
|
|
481
|
+
|
|
482
|
+
# Wait for loadEventFired via event — but our simple loop only resolves
|
|
483
|
+
# responses, not events. Poll document.readyState instead.
|
|
484
|
+
for _ in range(50):
|
|
485
|
+
ready = await self.eval("document.readyState")
|
|
486
|
+
if ready == "complete":
|
|
487
|
+
return
|
|
488
|
+
await asyncio.sleep(0.2)
|
|
489
|
+
raise RuntimeError("Navigation timeout: page did not reach readyState=complete")
|
|
490
|
+
|
|
491
|
+
async def click_selector(self, selector: str) -> None:
|
|
492
|
+
"""Click an element via JS."""
|
|
493
|
+
expression = (
|
|
494
|
+
"(function(){"
|
|
495
|
+
"var el=document.querySelector('" + selector.replace("'", "\\'") + "');"
|
|
496
|
+
"if(!el) throw new Error('"
|
|
497
|
+
"Element not found: " + selector.replace("'", "\\'") + "');"
|
|
498
|
+
"el.click();"
|
|
499
|
+
"return true;"
|
|
500
|
+
"})()"
|
|
501
|
+
)
|
|
502
|
+
await self.eval(expression)
|
|
503
|
+
|
|
504
|
+
async def click_xy(self, x: int, y: int) -> None:
|
|
505
|
+
"""Dispatch a mouse click at (x, y)."""
|
|
506
|
+
await self.send(
|
|
507
|
+
"Input.dispatchMouseEvent",
|
|
508
|
+
{
|
|
509
|
+
"type": "mousePressed",
|
|
510
|
+
"x": x,
|
|
511
|
+
"y": y,
|
|
512
|
+
"button": "left",
|
|
513
|
+
"clickCount": 1,
|
|
514
|
+
},
|
|
515
|
+
)
|
|
516
|
+
await self.send(
|
|
517
|
+
"Input.dispatchMouseEvent",
|
|
518
|
+
{
|
|
519
|
+
"type": "mouseReleased",
|
|
520
|
+
"x": x,
|
|
521
|
+
"y": y,
|
|
522
|
+
"button": "left",
|
|
523
|
+
"clickCount": 1,
|
|
524
|
+
},
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
async def type_text(self, text: str) -> None:
|
|
528
|
+
"""Insert text into the focused element."""
|
|
529
|
+
await self.send("Input.insertText", {"text": text})
|
|
530
|
+
|
|
531
|
+
async def get_html(self, selector: str | None = None) -> str:
|
|
532
|
+
"""Get outerHTML of the page or a matched element."""
|
|
533
|
+
if selector:
|
|
534
|
+
expr = (
|
|
535
|
+
"(function(){"
|
|
536
|
+
"var el=document.querySelector('" + selector.replace("'", "\\'") + "');"
|
|
537
|
+
"if(!el) throw new Error('"
|
|
538
|
+
"Element not found: " + selector.replace("'", "\\'") + "');"
|
|
539
|
+
"return el.outerHTML;"
|
|
540
|
+
"})()"
|
|
541
|
+
)
|
|
542
|
+
return await self.eval(expr)
|
|
543
|
+
return await self.eval("document.documentElement.outerHTML")
|
|
544
|
+
|
|
545
|
+
async def get_network_entries(self) -> list[dict[str, Any]]:
|
|
546
|
+
"""Return performance resource timing entries."""
|
|
547
|
+
raw = await self.eval(
|
|
548
|
+
"JSON.stringify(performance.getEntriesByType('resource').map(e=>{"
|
|
549
|
+
"return {name:e.name,initiatorType:e.initiatorType,duration:e.duration};"
|
|
550
|
+
"}))"
|
|
551
|
+
)
|
|
552
|
+
try:
|
|
553
|
+
return json.loads(raw)
|
|
554
|
+
except json.JSONDecodeError:
|
|
555
|
+
return []
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# ---------------------------------------------------------------------------
|
|
559
|
+
# Page cache helpers
|
|
560
|
+
# ---------------------------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _refresh_page_cache() -> list[dict[str, Any]]:
|
|
564
|
+
"""Fetch all pages from Chrome and update caches."""
|
|
565
|
+
global _PAGES_CACHE, _ACTIVE_TABS_CACHE
|
|
566
|
+
port = _get_devtools_port()
|
|
567
|
+
pages = _fetch_json_list(port)
|
|
568
|
+
_PAGES_CACHE = [p for p in pages if p.get("type") == "page"]
|
|
569
|
+
_ACTIVE_TABS_CACHE.clear()
|
|
570
|
+
for idx, page in enumerate(_PAGES_CACHE, start=1):
|
|
571
|
+
target_id = page.get("id", "")
|
|
572
|
+
prefix = f"{idx}"
|
|
573
|
+
_ACTIVE_TABS_CACHE[prefix] = target_id
|
|
574
|
+
return _PAGES_CACHE
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _resolve_target(prefix: str) -> str:
|
|
578
|
+
"""Resolve a short prefix to a full targetId."""
|
|
579
|
+
if prefix in _ACTIVE_TABS_CACHE:
|
|
580
|
+
return _ACTIVE_TABS_CACHE[prefix]
|
|
581
|
+
# Try refreshing
|
|
582
|
+
_refresh_page_cache()
|
|
583
|
+
if prefix in _ACTIVE_TABS_CACHE:
|
|
584
|
+
return _ACTIVE_TABS_CACHE[prefix]
|
|
585
|
+
raise ValueError(f"Unknown target prefix '{prefix}'. Run list first.")
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _get_or_create_session(target_id: str) -> CdpSession:
|
|
589
|
+
"""Return an existing CdpSession or create a new one."""
|
|
590
|
+
if target_id not in _PERSISTENT_SESSIONS:
|
|
591
|
+
_PERSISTENT_SESSIONS[target_id] = CdpSession(target_id)
|
|
592
|
+
return _PERSISTENT_SESSIONS[target_id]
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
# ---------------------------------------------------------------------------
|
|
596
|
+
# Tool registration
|
|
597
|
+
# ---------------------------------------------------------------------------
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def register_chrome_cdp(agent):
|
|
601
|
+
"""Register the chrome_cdp unified tool on the agent."""
|
|
602
|
+
|
|
603
|
+
@agent.tool
|
|
604
|
+
async def chrome_cdp(
|
|
605
|
+
context: RunContext,
|
|
606
|
+
command: str,
|
|
607
|
+
target: str = "",
|
|
608
|
+
expression: str = "",
|
|
609
|
+
selector: str = "",
|
|
610
|
+
url: str = "",
|
|
611
|
+
x: int = 0,
|
|
612
|
+
y: int = 0,
|
|
613
|
+
text: str = "",
|
|
614
|
+
) -> ChromeCdpResult | ChromeCdpInfo | dict[str, Any]:
|
|
615
|
+
"""Inspect, debug, and interact with web pages via Chrome DevTools Protocol.
|
|
616
|
+
|
|
617
|
+
Connects to your running Chrome browser — like having the DevTools
|
|
618
|
+
inspector programmatically. Use it to see what's happening on any page,
|
|
619
|
+
debug rendering issues, catch console errors, monitor network requests,
|
|
620
|
+
and interact with the page.
|
|
621
|
+
|
|
622
|
+
─── QUICK START ───
|
|
623
|
+
|
|
624
|
+
1. Check Chrome: chrome_cdp(command="status")
|
|
625
|
+
2. List tabs: chrome_cdp(command="list")
|
|
626
|
+
3. Snapshot: chrome_cdp(command="snap", target="1")
|
|
627
|
+
4. Screenshot: chrome_cdp(command="shot", target="1")
|
|
628
|
+
5. Console: chrome_cdp(command="console", target="1")
|
|
629
|
+
6. Evaluate JS: chrome_cdp(command="eval", target="1", expression="document.title")
|
|
630
|
+
|
|
631
|
+
─── FULL CAPABILITY GUIDE ───
|
|
632
|
+
|
|
633
|
+
Call `chrome_cdp(command="guide")` at any time to see the complete
|
|
634
|
+
capability catalog with examples. Or read the command breakdown below.
|
|
635
|
+
|
|
636
|
+
📄 PAGE INSPECTION
|
|
637
|
+
list / ls → List all open tabs with numeric prefixes
|
|
638
|
+
shot / screenshot → Save a PNG screenshot; returns file path + DPR
|
|
639
|
+
snap / snapshot → Accessibility tree (how screen readers see the page)
|
|
640
|
+
html → Get full page HTML or a specific element's outerHTML
|
|
641
|
+
source → Get JS/CSS source code by URL or fetch-page path
|
|
642
|
+
|
|
643
|
+
💻 JAVASCRIPT RUNTIME
|
|
644
|
+
eval → Execute any JavaScript: read state, call functions,
|
|
645
|
+
inspect localStorage, cookies, component props, etc.
|
|
646
|
+
Example: eval "JSON.stringify(window.__INITIAL_STATE__)"
|
|
647
|
+
Example: eval "document.querySelector('h1').textContent"
|
|
648
|
+
console / logs → Capture console.log/warn/error/info output from the page.
|
|
649
|
+
Also catches unhandled JS exceptions. Use AFTER page
|
|
650
|
+
interactions to see if anything broke.
|
|
651
|
+
|
|
652
|
+
🌐 NETWORK
|
|
653
|
+
net / network → Performance API resource entries (duration, size, type)
|
|
654
|
+
network_start → Begin REAL-TIME capture of all HTTP requests/responses
|
|
655
|
+
network_captured→ Show captured requests with method, status, URL, size, errors
|
|
656
|
+
network_stop → Stop network capture
|
|
657
|
+
|
|
658
|
+
🖱️ PAGE INTERACTION
|
|
659
|
+
nav / navigate → Navigate to a URL and wait for page load
|
|
660
|
+
click → Click an element by CSS selector
|
|
661
|
+
clickxy → Click at CSS pixel coordinates (x, y)
|
|
662
|
+
type → Type text into the focused element (works in cross-origin iframes!)
|
|
663
|
+
|
|
664
|
+
🔧 DEBUGGING
|
|
665
|
+
status → Check if Chrome remote debugging is available
|
|
666
|
+
guide → Show this complete capability guide with examples
|
|
667
|
+
|
|
668
|
+
─── ADVANCED WORKFLOWS ───
|
|
669
|
+
|
|
670
|
+
Debug a dev server:
|
|
671
|
+
chrome_cdp(command="nav", target="1", url="http://localhost:5173")
|
|
672
|
+
chrome_cdp(command="shot", target="1")
|
|
673
|
+
chrome_cdp(command="console", target="1")
|
|
674
|
+
|
|
675
|
+
Debug a failed API call:
|
|
676
|
+
chrome_cdp(command="network_start", target="1")
|
|
677
|
+
chrome_cdp(command="click", target="1", selector="button[type=submit]")
|
|
678
|
+
chrome_cdp(command="network_captured", target="1")
|
|
679
|
+
chrome_cdp(command="network_stop", target="1")
|
|
680
|
+
|
|
681
|
+
Check page state:
|
|
682
|
+
chrome_cdp(command="eval", target="1", expression="navigator.userAgent")
|
|
683
|
+
chrome_cdp(command="eval", target="1", expression="JSON.stringify(localStorage)")
|
|
684
|
+
chrome_cdp(command="eval", target="1", expression="document.cookie")
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
command: What to do — see guide above for full list.
|
|
688
|
+
target: Tab prefix from `list` output (e.g. "1", "2").
|
|
689
|
+
expression: JavaScript expression for `eval`.
|
|
690
|
+
selector: CSS selector for `click` or scoped `html`.
|
|
691
|
+
url: URL for `navigate` or `source`.
|
|
692
|
+
x: X coordinate in CSS pixels for `clickxy`.
|
|
693
|
+
y: Y coordinate in CSS pixels for `clickxy`.
|
|
694
|
+
text: Text to type for `type`.
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
ChromeCdpResult with success/error status and output.
|
|
698
|
+
"""
|
|
699
|
+
cmd = command.lower().strip()
|
|
700
|
+
|
|
701
|
+
# --- status ---
|
|
702
|
+
if cmd in ("status",):
|
|
703
|
+
try:
|
|
704
|
+
ws_url = _discover_browser_ws()
|
|
705
|
+
import urllib.request
|
|
706
|
+
|
|
707
|
+
port = _get_devtools_port()
|
|
708
|
+
version_url = f"http://localhost:{port}/json/version"
|
|
709
|
+
with urllib.request.urlopen(version_url, timeout=3) as resp: # noqa: S310
|
|
710
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
711
|
+
version = data.get("Browser", "")
|
|
712
|
+
emit_info(f"chrome_cdp: Chrome available — {version}")
|
|
713
|
+
return ChromeCdpInfo(available=True, ws_url=ws_url, version=version)
|
|
714
|
+
except Exception as exc:
|
|
715
|
+
logger.exception("chrome_cdp status check failed")
|
|
716
|
+
return ChromeCdpInfo(
|
|
717
|
+
available=False, ws_url="", version=f"Not available: {exc}"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
# --- guide ---
|
|
721
|
+
if cmd in ("guide", "help", "commands"):
|
|
722
|
+
guide_text = (
|
|
723
|
+
"╔══════════════════════════════════════════════════════════╗\n"
|
|
724
|
+
"║ chrome_cdp — Complete Capability Guide ║\n"
|
|
725
|
+
"╚══════════════════════════════════════════════════════════╝\n"
|
|
726
|
+
"\n"
|
|
727
|
+
"📄 PAGE INSPECTION\n"
|
|
728
|
+
' chrome_cdp(command="list") — List open tabs\n'
|
|
729
|
+
' chrome_cdp(command="shot", target="1") — Screenshot (temp file)\n'
|
|
730
|
+
' chrome_cdp(command="snap", target="1") — Accessibility tree\n'
|
|
731
|
+
' chrome_cdp(command="html", target="1") — Full page HTML\n'
|
|
732
|
+
' chrome_cdp(command="html", target="1", selector="#main") — Element HTML\n'
|
|
733
|
+
' chrome_cdp(command="source", target="1", url="https://...") — JS/CSS source\n'
|
|
734
|
+
"\n"
|
|
735
|
+
"💻 JAVASCRIPT RUNTIME\n"
|
|
736
|
+
' chrome_cdp(command="eval", target="1", expression="document.title")\n'
|
|
737
|
+
' chrome_cdp(command="eval", target="1", expression="JSON.stringify(localStorage)")\n'
|
|
738
|
+
' chrome_cdp(command="eval", target="1", expression="document.cookie")\n'
|
|
739
|
+
' chrome_cdp(command="eval", target="1", expression="navigator.userAgent")\n'
|
|
740
|
+
' chrome_cdp(command="eval", target="1", expression="window.innerWidth") — viewport size\n'
|
|
741
|
+
' chrome_cdp(command="console", target="1") — Capture console logs/errors\n'
|
|
742
|
+
"\n"
|
|
743
|
+
"🌐 NETWORK DIAGNOSTICS\n"
|
|
744
|
+
' chrome_cdp(command="net", target="1") — Resource timing entries\n'
|
|
745
|
+
' chrome_cdp(command="network_start", target="1") — Begin HTTP monitoring\n'
|
|
746
|
+
' chrome_cdp(command="network_captured", target="1") — View captured requests\n'
|
|
747
|
+
' chrome_cdp(command="network_stop", target="1") — Stop monitoring\n'
|
|
748
|
+
"\n"
|
|
749
|
+
"🖱️ PAGE INTERACTION\n"
|
|
750
|
+
' chrome_cdp(command="nav", target="1", url="https://...") — Navigate\n'
|
|
751
|
+
' chrome_cdp(command="click", target="1", selector="button") — Click element\n'
|
|
752
|
+
' chrome_cdp(command="clickxy", target="1", x=100, y=200) — Click at coords\n'
|
|
753
|
+
' chrome_cdp(command="type", target="1", text="hello") — Type text\n'
|
|
754
|
+
"\n"
|
|
755
|
+
"🔧 SYSTEM\n"
|
|
756
|
+
' chrome_cdp(command="status") — Check Chrome availability\n'
|
|
757
|
+
' chrome_cdp(command="guide") — Show this guide\n'
|
|
758
|
+
"\n"
|
|
759
|
+
"─── TYPICAL DEBUGGING WORKFLOW ───\n"
|
|
760
|
+
" # After starting a dev server:\n"
|
|
761
|
+
' 1. chrome_cdp(command="status")\n'
|
|
762
|
+
' 2. chrome_cdp(command="list")\n'
|
|
763
|
+
' 3. chrome_cdp(command="nav", target="1", url="http://localhost:3000")\n'
|
|
764
|
+
' 4. chrome_cdp(command="shot", target="1")\n'
|
|
765
|
+
' 5. chrome_cdp(command="console", target="1")\n'
|
|
766
|
+
' 6. chrome_cdp(command="snap", target="1")\n'
|
|
767
|
+
"\n"
|
|
768
|
+
" ─ or debug a failing API call ─\n"
|
|
769
|
+
' 1. chrome_cdp(command="network_start", target="1")\n'
|
|
770
|
+
" 2. [interact with the page]\n"
|
|
771
|
+
' 3. chrome_cdp(command="network_captured", target="1")\n'
|
|
772
|
+
' 4. chrome_cdp(command="network_stop", target="1")\n'
|
|
773
|
+
"\n"
|
|
774
|
+
"─── TIPS ───\n"
|
|
775
|
+
" • The first connection to a tab may show 'Allow debugging?' — approve it once.\n"
|
|
776
|
+
" • Screenshots are saved to temp files; the path is returned in output.\n"
|
|
777
|
+
" • DPR (device pixel ratio) affects coordinates: CSS px = screenshot px / DPR.\n"
|
|
778
|
+
" • Use `console` AFTER interactions to catch post-action errors.\n"
|
|
779
|
+
" • Network capture starts fresh when you call network_start.\n"
|
|
780
|
+
)
|
|
781
|
+
return ChromeCdpResult(success=True, output=guide_text)
|
|
782
|
+
|
|
783
|
+
# --- list / ls ---
|
|
784
|
+
if cmd in ("list", "ls"):
|
|
785
|
+
try:
|
|
786
|
+
pages = _refresh_page_cache()
|
|
787
|
+
page_list: list[dict[str, Any]] = []
|
|
788
|
+
for idx, page in enumerate(pages, start=1):
|
|
789
|
+
title = page.get("title", "")
|
|
790
|
+
url = page.get("url", "")
|
|
791
|
+
target_id = page.get("id", "")
|
|
792
|
+
page_list.append(
|
|
793
|
+
{
|
|
794
|
+
"prefix": str(idx),
|
|
795
|
+
"title": title,
|
|
796
|
+
"url": url,
|
|
797
|
+
"target_id": target_id,
|
|
798
|
+
}
|
|
799
|
+
)
|
|
800
|
+
return ChromeCdpResult(
|
|
801
|
+
success=True,
|
|
802
|
+
output=f"{len(page_list)} tab(s) open",
|
|
803
|
+
page_list=page_list,
|
|
804
|
+
)
|
|
805
|
+
except Exception as exc:
|
|
806
|
+
logger.exception("chrome_cdp list failed")
|
|
807
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
808
|
+
|
|
809
|
+
# All remaining commands need a target
|
|
810
|
+
if not target:
|
|
811
|
+
return ChromeCdpResult(
|
|
812
|
+
success=False,
|
|
813
|
+
error="Missing 'target' parameter. Run list first to get a prefix.",
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
try:
|
|
817
|
+
target_id = _resolve_target(target)
|
|
818
|
+
session = _get_or_create_session(target_id)
|
|
819
|
+
await session.ensure_connected()
|
|
820
|
+
except Exception as exc:
|
|
821
|
+
logger.exception("chrome_cdp connect failed for target=%s", target)
|
|
822
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
823
|
+
|
|
824
|
+
# --- shot / screenshot ---
|
|
825
|
+
if cmd in ("shot", "screenshot"):
|
|
826
|
+
try:
|
|
827
|
+
screenshot_bytes, dpr = await session.get_screenshot()
|
|
828
|
+
fd, path = tempfile.mkstemp(prefix="chrome_cdp_shot_", suffix=".png")
|
|
829
|
+
with open(fd, "wb") as f:
|
|
830
|
+
f.write(screenshot_bytes)
|
|
831
|
+
emit_success(f"chrome_cdp screenshot saved: {path}")
|
|
832
|
+
return ChromeCdpResult(
|
|
833
|
+
success=True,
|
|
834
|
+
output=f"Screenshot saved to {path} (DPR={dpr})",
|
|
835
|
+
screenshot_file=path,
|
|
836
|
+
dpr=dpr,
|
|
837
|
+
)
|
|
838
|
+
except Exception as exc:
|
|
839
|
+
logger.exception("chrome_cdp screenshot failed")
|
|
840
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
841
|
+
|
|
842
|
+
# --- snap / snapshot ---
|
|
843
|
+
if cmd in ("snap", "snapshot"):
|
|
844
|
+
try:
|
|
845
|
+
tree = await session.get_snapshot()
|
|
846
|
+
return ChromeCdpResult(success=True, output=tree)
|
|
847
|
+
except Exception as exc:
|
|
848
|
+
logger.exception("chrome_cdp snapshot failed")
|
|
849
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
850
|
+
|
|
851
|
+
# --- eval ---
|
|
852
|
+
if cmd in ("eval",):
|
|
853
|
+
if not expression:
|
|
854
|
+
return ChromeCdpResult(
|
|
855
|
+
success=False, error="Missing 'expression' for eval command."
|
|
856
|
+
)
|
|
857
|
+
try:
|
|
858
|
+
result = await session.eval(expression)
|
|
859
|
+
return ChromeCdpResult(success=True, output=result)
|
|
860
|
+
except Exception as exc:
|
|
861
|
+
logger.exception("chrome_cdp eval failed")
|
|
862
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
863
|
+
|
|
864
|
+
# --- html ---
|
|
865
|
+
if cmd in ("html",):
|
|
866
|
+
try:
|
|
867
|
+
html = await session.get_html(selector if selector else None)
|
|
868
|
+
# Truncate very large HTML
|
|
869
|
+
if len(html) > 100_000:
|
|
870
|
+
html = html[:100_000] + "\n... (truncated)"
|
|
871
|
+
return ChromeCdpResult(success=True, output=html)
|
|
872
|
+
except Exception as exc:
|
|
873
|
+
logger.exception("chrome_cdp html failed")
|
|
874
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
875
|
+
|
|
876
|
+
# --- nav / navigate ---
|
|
877
|
+
if cmd in ("nav", "navigate"):
|
|
878
|
+
if not url:
|
|
879
|
+
return ChromeCdpResult(
|
|
880
|
+
success=False, error="Missing 'url' for navigate command."
|
|
881
|
+
)
|
|
882
|
+
try:
|
|
883
|
+
await session.navigate(url)
|
|
884
|
+
return ChromeCdpResult(success=True, output=f"Navigated to {url}")
|
|
885
|
+
except Exception as exc:
|
|
886
|
+
logger.exception("chrome_cdp navigate failed")
|
|
887
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
888
|
+
|
|
889
|
+
# --- net / network ---
|
|
890
|
+
if cmd in ("net", "network"):
|
|
891
|
+
try:
|
|
892
|
+
entries = await session.get_network_entries()
|
|
893
|
+
lines = [
|
|
894
|
+
f"{e.get('name', '')} [{e.get('initiatorType', '')}]"
|
|
895
|
+
f" {e.get('duration', 0):.1f}ms"
|
|
896
|
+
for e in entries
|
|
897
|
+
]
|
|
898
|
+
return ChromeCdpResult(
|
|
899
|
+
success=True,
|
|
900
|
+
output=f"{len(entries)} resource(s)\n" + "\n".join(lines[:200]),
|
|
901
|
+
)
|
|
902
|
+
except Exception as exc:
|
|
903
|
+
logger.exception("chrome_cdp network failed")
|
|
904
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
905
|
+
|
|
906
|
+
# --- console ---
|
|
907
|
+
if cmd in ("console", "logs"):
|
|
908
|
+
try:
|
|
909
|
+
await session.enable_console_capture()
|
|
910
|
+
# Wait a moment for any existing console events
|
|
911
|
+
await asyncio.sleep(0.5)
|
|
912
|
+
logs = await session.get_console_logs()
|
|
913
|
+
if not logs:
|
|
914
|
+
return ChromeCdpResult(
|
|
915
|
+
success=True, output="(no console output captured)"
|
|
916
|
+
)
|
|
917
|
+
lines = []
|
|
918
|
+
for entry in logs:
|
|
919
|
+
level = entry.get("level", "log")
|
|
920
|
+
text = entry.get("text", "")
|
|
921
|
+
lines.append(f"[{level}] {text}")
|
|
922
|
+
return ChromeCdpResult(success=True, output="\n".join(lines))
|
|
923
|
+
except Exception as exc:
|
|
924
|
+
logger.exception("chrome_cdp console failed")
|
|
925
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
926
|
+
|
|
927
|
+
# --- network_start ---
|
|
928
|
+
if cmd in ("network_start",):
|
|
929
|
+
try:
|
|
930
|
+
await session.enable_network_capture()
|
|
931
|
+
return ChromeCdpResult(success=True, output="Network capture started")
|
|
932
|
+
except Exception as exc:
|
|
933
|
+
logger.exception("chrome_cdp network_start failed")
|
|
934
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
935
|
+
|
|
936
|
+
# --- network_stop ---
|
|
937
|
+
if cmd in ("network_stop",):
|
|
938
|
+
try:
|
|
939
|
+
await session.disable_network_capture()
|
|
940
|
+
return ChromeCdpResult(success=True, output="Network capture stopped")
|
|
941
|
+
except Exception as exc:
|
|
942
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
943
|
+
|
|
944
|
+
# --- network_captured ---
|
|
945
|
+
if cmd in ("network_captured",):
|
|
946
|
+
try:
|
|
947
|
+
activity = await session.get_network_activity()
|
|
948
|
+
if not activity:
|
|
949
|
+
return ChromeCdpResult(
|
|
950
|
+
success=True, output="(no network activity captured)"
|
|
951
|
+
)
|
|
952
|
+
lines = []
|
|
953
|
+
for entry in activity[:100]: # cap at 100 entries
|
|
954
|
+
url = entry.get("url", "")[:80]
|
|
955
|
+
method = entry.get("method", "?")
|
|
956
|
+
status = entry.get("status", "?")
|
|
957
|
+
size = entry.get("size", "?")
|
|
958
|
+
error = entry.get("error", "")
|
|
959
|
+
if error:
|
|
960
|
+
lines.append(f"{method} {status} {size}B FAIL:{error} {url}")
|
|
961
|
+
else:
|
|
962
|
+
lines.append(f"{method} {status} {size}B {url}")
|
|
963
|
+
return ChromeCdpResult(success=True, output="\n".join(lines))
|
|
964
|
+
except Exception as exc:
|
|
965
|
+
logger.exception("chrome_cdp network_captured failed")
|
|
966
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
967
|
+
|
|
968
|
+
# --- source ---
|
|
969
|
+
if cmd in ("source",):
|
|
970
|
+
if not url:
|
|
971
|
+
return ChromeCdpResult(
|
|
972
|
+
success=False,
|
|
973
|
+
error="Missing 'url' for source command. "
|
|
974
|
+
"Pass the script URL or src path.",
|
|
975
|
+
)
|
|
976
|
+
try:
|
|
977
|
+
src = await session.get_source(url)
|
|
978
|
+
return ChromeCdpResult(success=True, output=src)
|
|
979
|
+
except Exception as exc:
|
|
980
|
+
logger.exception("chrome_cdp source failed")
|
|
981
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
982
|
+
|
|
983
|
+
# --- click ---
|
|
984
|
+
if cmd in ("click",):
|
|
985
|
+
if not selector:
|
|
986
|
+
return ChromeCdpResult(
|
|
987
|
+
success=False, error="Missing 'selector' for click command."
|
|
988
|
+
)
|
|
989
|
+
try:
|
|
990
|
+
await session.click_selector(selector)
|
|
991
|
+
return ChromeCdpResult(
|
|
992
|
+
success=True, output=f"Clicked element: {selector}"
|
|
993
|
+
)
|
|
994
|
+
except Exception as exc:
|
|
995
|
+
logger.exception("chrome_cdp click failed")
|
|
996
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
997
|
+
|
|
998
|
+
# --- clickxy ---
|
|
999
|
+
if cmd in ("clickxy",):
|
|
1000
|
+
try:
|
|
1001
|
+
await session.click_xy(x, y)
|
|
1002
|
+
return ChromeCdpResult(success=True, output=f"Clicked at ({x}, {y})")
|
|
1003
|
+
except Exception as exc:
|
|
1004
|
+
logger.exception("chrome_cdp clickxy failed")
|
|
1005
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
1006
|
+
|
|
1007
|
+
# --- type ---
|
|
1008
|
+
if cmd in ("type",):
|
|
1009
|
+
if not text:
|
|
1010
|
+
return ChromeCdpResult(
|
|
1011
|
+
success=False, error="Missing 'text' for type command."
|
|
1012
|
+
)
|
|
1013
|
+
try:
|
|
1014
|
+
await session.type_text(text)
|
|
1015
|
+
return ChromeCdpResult(
|
|
1016
|
+
success=True, output=f"Typed text ({len(text)} chars)"
|
|
1017
|
+
)
|
|
1018
|
+
except Exception as exc:
|
|
1019
|
+
logger.exception("chrome_cdp type failed")
|
|
1020
|
+
return ChromeCdpResult(success=False, error=str(exc))
|
|
1021
|
+
|
|
1022
|
+
# Unknown command
|
|
1023
|
+
return ChromeCdpResult(
|
|
1024
|
+
success=False,
|
|
1025
|
+
error=(
|
|
1026
|
+
f"Unknown command '{command}'. "
|
|
1027
|
+
"Use: status, list, shot, snap, eval, "
|
|
1028
|
+
"html, nav, net, console, network_start, network_captured, "
|
|
1029
|
+
"network_stop, source, click, clickxy, type."
|
|
1030
|
+
),
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
return chrome_cdp
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
# ---------------------------------------------------------------------------
|
|
1037
|
+
# Cleanup
|
|
1038
|
+
# ---------------------------------------------------------------------------
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
async def cleanup_all_sessions() -> None:
|
|
1042
|
+
"""Disconnect all persistent CDP sessions. Called on shutdown."""
|
|
1043
|
+
global _CHROME_WS
|
|
1044
|
+
for session in list(_PERSISTENT_SESSIONS.values()):
|
|
1045
|
+
await session.disconnect()
|
|
1046
|
+
_PERSISTENT_SESSIONS.clear()
|
|
1047
|
+
_ACTIVE_TABS_CACHE.clear()
|
|
1048
|
+
_PAGES_CACHE.clear()
|
|
1049
|
+
_CHROME_WS = None
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _sync_cleanup() -> None:
|
|
1053
|
+
"""Synchronous cleanup wrapper for atexit/shutdown."""
|
|
1054
|
+
try:
|
|
1055
|
+
loop = asyncio.get_running_loop()
|
|
1056
|
+
loop.create_task(cleanup_all_sessions())
|
|
1057
|
+
except RuntimeError:
|
|
1058
|
+
pass # No running loop, connections will be cleaned on process exit
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
# Register startup callback for availability check
|
|
1062
|
+
from code_muse.callbacks import register_callback # noqa: E402
|
|
1063
|
+
from code_muse.tools.chrome_cdp.register_callbacks import ( # noqa: E402
|
|
1064
|
+
register as _register_chrome_cdp_callbacks,
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
_register_chrome_cdp_callbacks()
|
|
1068
|
+
|
|
1069
|
+
# Register shutdown cleanup
|
|
1070
|
+
register_callback("shutdown", _sync_cleanup)
|