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,853 @@
|
|
|
1
|
+
"""Cache helpers for Claude Code / Anthropic.
|
|
2
|
+
|
|
3
|
+
ClaudeCacheAsyncClient: httpx client that tries to patch /v1/messages bodies.
|
|
4
|
+
|
|
5
|
+
We now also expose `patch_anthropic_client_messages` which monkey-patches
|
|
6
|
+
AsyncAnthropic.messages.create() so we can inject cache_control BEFORE
|
|
7
|
+
serialization, avoiding httpx/Pydantic internals.
|
|
8
|
+
|
|
9
|
+
This module also handles:
|
|
10
|
+
- Tool name prefixing/unprefixing for Claude Code OAuth compatibility
|
|
11
|
+
- Header transformations (anthropic-beta, user-agent)
|
|
12
|
+
- URL modifications (adding ?beta=true query param)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import base64
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import time
|
|
20
|
+
from collections.abc import Callable, MutableMapping
|
|
21
|
+
from typing import Any
|
|
22
|
+
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
|
23
|
+
|
|
24
|
+
import httpx
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Refresh token if it's older than the configured max age (seconds)
|
|
29
|
+
TOKEN_MAX_AGE_SECONDS = 3600
|
|
30
|
+
|
|
31
|
+
# Retry configuration
|
|
32
|
+
RETRY_STATUS_CODES = (429, 500, 502, 503, 504)
|
|
33
|
+
MAX_RETRIES = 5
|
|
34
|
+
|
|
35
|
+
# Tool name prefix for Claude Code OAuth compatibility
|
|
36
|
+
# Tools are prefixed on outgoing requests and unprefixed on incoming responses
|
|
37
|
+
TOOL_PREFIX = "cp_"
|
|
38
|
+
|
|
39
|
+
# User-Agent to send with Claude Code OAuth requests
|
|
40
|
+
CLAUDE_CLI_USER_AGENT = "claude-cli/2.1.2 (external, cli)"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _model_requires_thinking_summary(model_name):
|
|
44
|
+
# Anthropic's Opus 4.7 family rejects adaptive-thinking requests unless a
|
|
45
|
+
# 'display: summary' field is present alongside 'type: adaptive'. We check
|
|
46
|
+
# both naming conventions (opus-4-7 and 4-7-opus).
|
|
47
|
+
if not model_name:
|
|
48
|
+
return False
|
|
49
|
+
lower = model_name.lower()
|
|
50
|
+
return "opus-4-7" in lower or "4-7-opus" in lower
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _enforce_thinking_display_summary(payload):
|
|
54
|
+
# Belt-and-suspenders wire-level enforcement of thinking.display='summary'
|
|
55
|
+
# for Opus 4.7 payloads. Mutates payload in place; returns True if a
|
|
56
|
+
# change was made. No-ops on non-matching models or payloads without a
|
|
57
|
+
# thinking dict.
|
|
58
|
+
if not isinstance(payload, dict):
|
|
59
|
+
return False
|
|
60
|
+
if not _model_requires_thinking_summary(payload.get("model")):
|
|
61
|
+
return False
|
|
62
|
+
thinking = payload.get("thinking")
|
|
63
|
+
if not isinstance(thinking, dict):
|
|
64
|
+
return False
|
|
65
|
+
if thinking.get("display") == "summarized":
|
|
66
|
+
return False
|
|
67
|
+
thinking["display"] = "summarized"
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
from anthropic import AsyncAnthropic
|
|
73
|
+
except ImportError: # pragma: no cover - optional dep
|
|
74
|
+
AsyncAnthropic = None # type: ignore
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
78
|
+
"""Async HTTP client with Claude Code OAuth transformations.
|
|
79
|
+
|
|
80
|
+
Handles:
|
|
81
|
+
- Cache control injection for prompt caching
|
|
82
|
+
- Tool name prefixing on outgoing requests
|
|
83
|
+
- Tool name unprefixing on incoming streaming responses
|
|
84
|
+
- Header transformations (anthropic-beta, user-agent)
|
|
85
|
+
- URL modifications (adding ?beta=true)
|
|
86
|
+
- Proactive token refresh
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
*args: Any,
|
|
92
|
+
oauth_reauthentication_callback: Callable[[], str | None] | None = None,
|
|
93
|
+
token_update_callback: Callable[[str], None] | None = None,
|
|
94
|
+
**kwargs: Any,
|
|
95
|
+
) -> None:
|
|
96
|
+
super().__init__(*args, **kwargs)
|
|
97
|
+
self._oauth_reauthentication_callback = oauth_reauthentication_callback
|
|
98
|
+
self._token_update_callback = token_update_callback
|
|
99
|
+
|
|
100
|
+
def set_token_update_callback(self, callback: Callable[[str], None] | None) -> None:
|
|
101
|
+
self._token_update_callback = callback
|
|
102
|
+
|
|
103
|
+
def _notify_token_recovered(self, access_token: str) -> None:
|
|
104
|
+
if not self._token_update_callback:
|
|
105
|
+
return
|
|
106
|
+
try:
|
|
107
|
+
self._token_update_callback(access_token)
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
logger.debug("Token update callback failed: %s", exc)
|
|
110
|
+
|
|
111
|
+
def _get_jwt_age_seconds(self, token: str | None) -> float | None:
|
|
112
|
+
"""Decode a JWT and return its age in seconds.
|
|
113
|
+
|
|
114
|
+
Returns None if the token can't be decoded or has no timestamp claims.
|
|
115
|
+
Uses 'iat' (issued at) if available, otherwise calculates from 'exp'.
|
|
116
|
+
"""
|
|
117
|
+
if not token:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
# JWT format: header.payload.signature
|
|
122
|
+
# We only need the payload (second part)
|
|
123
|
+
parts = token.split(".")
|
|
124
|
+
if len(parts) != 3:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
# Decode the payload (base64url encoded)
|
|
128
|
+
payload_b64 = parts[1]
|
|
129
|
+
# Add padding if needed (base64url doesn't require padding)
|
|
130
|
+
padding = 4 - len(payload_b64) % 4
|
|
131
|
+
if padding != 4:
|
|
132
|
+
payload_b64 += "=" * padding
|
|
133
|
+
|
|
134
|
+
payload_bytes = base64.urlsafe_b64decode(payload_b64)
|
|
135
|
+
payload = json.loads(payload_bytes.decode("utf-8"))
|
|
136
|
+
|
|
137
|
+
now = time.time()
|
|
138
|
+
|
|
139
|
+
# Prefer 'iat' (issued at) claim if available
|
|
140
|
+
if "iat" in payload:
|
|
141
|
+
iat = float(payload["iat"])
|
|
142
|
+
age = now - iat
|
|
143
|
+
return age
|
|
144
|
+
|
|
145
|
+
# Fall back to calculating from 'exp' claim
|
|
146
|
+
# Assume tokens are typically valid for TOKEN_MAX_AGE_SECONDS
|
|
147
|
+
if "exp" in payload:
|
|
148
|
+
exp = float(payload["exp"])
|
|
149
|
+
# If exp is in the future, calculate how long until expiry
|
|
150
|
+
# and assume the token was issued TOKEN_MAX_AGE_SECONDS before expiry
|
|
151
|
+
time_until_exp = exp - now
|
|
152
|
+
# If token has less than TOKEN_MAX_AGE_SECONDS left, it's "old"
|
|
153
|
+
age = TOKEN_MAX_AGE_SECONDS - time_until_exp
|
|
154
|
+
return max(0, age)
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
logger.debug("Failed to decode JWT age: %s", exc)
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def _extract_bearer_token(self, request: httpx.Request) -> str | None:
|
|
162
|
+
"""Extract the bearer token from request headers."""
|
|
163
|
+
auth_header = request.headers.get("Authorization") or request.headers.get(
|
|
164
|
+
"authorization"
|
|
165
|
+
)
|
|
166
|
+
if auth_header and auth_header.lower().startswith("bearer "):
|
|
167
|
+
return auth_header[7:] # Strip "Bearer " prefix
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def _should_refresh_token(self, request: httpx.Request) -> bool:
|
|
171
|
+
"""Check if the token should be refreshed (within the max-age window).
|
|
172
|
+
|
|
173
|
+
Uses two strategies:
|
|
174
|
+
1. Decode JWT to check token age (if possible)
|
|
175
|
+
2. Fall back to stored expires_at from token file
|
|
176
|
+
|
|
177
|
+
Returns True if token expires within TOKEN_MAX_AGE_SECONDS.
|
|
178
|
+
"""
|
|
179
|
+
token = self._extract_bearer_token(request)
|
|
180
|
+
if not token:
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
# Strategy 1: Try to decode JWT age
|
|
184
|
+
age = self._get_jwt_age_seconds(token)
|
|
185
|
+
if age is not None:
|
|
186
|
+
should_refresh = age >= TOKEN_MAX_AGE_SECONDS
|
|
187
|
+
if should_refresh:
|
|
188
|
+
logger.info(
|
|
189
|
+
"JWT token is %.1f seconds old (>= %d), will refresh proactively",
|
|
190
|
+
age,
|
|
191
|
+
TOKEN_MAX_AGE_SECONDS,
|
|
192
|
+
)
|
|
193
|
+
return should_refresh
|
|
194
|
+
|
|
195
|
+
# Strategy 2: Fall back to stored expires_at from token file
|
|
196
|
+
should_refresh = self._check_stored_token_expiry()
|
|
197
|
+
if should_refresh:
|
|
198
|
+
logger.info(
|
|
199
|
+
"Stored token expires within %d seconds, will refresh proactively",
|
|
200
|
+
TOKEN_MAX_AGE_SECONDS,
|
|
201
|
+
)
|
|
202
|
+
return should_refresh
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _check_stored_token_expiry() -> bool:
|
|
206
|
+
"""Check if the stored token expires within TOKEN_MAX_AGE_SECONDS.
|
|
207
|
+
|
|
208
|
+
This is a fallback for when JWT decoding fails or isn't available.
|
|
209
|
+
Uses the expires_at timestamp from the stored token file.
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
from code_muse.plugins.claude_code_oauth.utils import (
|
|
213
|
+
is_token_expired,
|
|
214
|
+
load_stored_tokens,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
tokens = load_stored_tokens()
|
|
218
|
+
if not tokens:
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
# is_token_expired already uses the configured refresh buffer window
|
|
222
|
+
return is_token_expired(tokens)
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
logger.debug("Error checking stored token expiry: %s", exc)
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def _prefix_tool_names(body: bytes) -> bytes | None:
|
|
229
|
+
"""Prefix all tool names in the request body with TOOL_PREFIX.
|
|
230
|
+
|
|
231
|
+
This is required for Claude Code OAuth compatibility - tools must be
|
|
232
|
+
prefixed on outgoing requests and unprefixed on incoming responses.
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
data = json.loads(body.decode("utf-8"))
|
|
236
|
+
except Exception:
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
if not isinstance(data, dict):
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
tools = data.get("tools")
|
|
243
|
+
if not isinstance(tools, list) or not tools:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
modified = False
|
|
247
|
+
for tool in tools:
|
|
248
|
+
if isinstance(tool, dict) and "name" in tool:
|
|
249
|
+
name = tool["name"]
|
|
250
|
+
if name and not name.startswith(TOOL_PREFIX):
|
|
251
|
+
tool["name"] = f"{TOOL_PREFIX}{name}"
|
|
252
|
+
modified = True
|
|
253
|
+
|
|
254
|
+
if not modified:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
return json.dumps(data).encode("utf-8")
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def _transform_headers_for_claude_code(
|
|
261
|
+
headers: MutableMapping[str, str],
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Transform headers for Claude Code OAuth compatibility.
|
|
264
|
+
|
|
265
|
+
- Sets user-agent to claude-cli
|
|
266
|
+
- Merges anthropic-beta headers appropriately
|
|
267
|
+
- Removes x-api-key (using Bearer auth instead)
|
|
268
|
+
"""
|
|
269
|
+
# Set user-agent
|
|
270
|
+
headers["user-agent"] = CLAUDE_CLI_USER_AGENT
|
|
271
|
+
|
|
272
|
+
# Handle anthropic-beta header — merge required betas with any
|
|
273
|
+
# extras already present (e.g. context-1m-2025-08-07).
|
|
274
|
+
incoming_beta = headers.get("anthropic-beta", "")
|
|
275
|
+
incoming_betas = [b.strip() for b in incoming_beta.split(",") if b.strip()]
|
|
276
|
+
|
|
277
|
+
# Always-required betas for Claude Code OAuth
|
|
278
|
+
required_betas = [
|
|
279
|
+
"oauth-2025-04-20",
|
|
280
|
+
"interleaved-thinking-2025-05-14",
|
|
281
|
+
]
|
|
282
|
+
if "claude-code-20250219" in incoming_betas:
|
|
283
|
+
required_betas.append("claude-code-20250219")
|
|
284
|
+
|
|
285
|
+
# Merge: start with required, then append any extras from the
|
|
286
|
+
# incoming headers that aren't already in the required set.
|
|
287
|
+
merged = list(required_betas)
|
|
288
|
+
required_set = set(required_betas)
|
|
289
|
+
for beta in incoming_betas:
|
|
290
|
+
if beta not in required_set:
|
|
291
|
+
merged.append(beta)
|
|
292
|
+
|
|
293
|
+
headers["anthropic-beta"] = ",".join(merged)
|
|
294
|
+
|
|
295
|
+
# Remove x-api-key if present (we use Bearer auth)
|
|
296
|
+
for key in ["x-api-key", "X-API-Key", "X-Api-Key"]:
|
|
297
|
+
if key in headers:
|
|
298
|
+
del headers[key]
|
|
299
|
+
|
|
300
|
+
@staticmethod
|
|
301
|
+
def _add_beta_query_param(url: httpx.URL) -> httpx.URL:
|
|
302
|
+
"""Add ?beta=true query parameter to the URL if not already present."""
|
|
303
|
+
# Parse the URL
|
|
304
|
+
parsed = urlparse(str(url))
|
|
305
|
+
query_params = parse_qs(parsed.query)
|
|
306
|
+
|
|
307
|
+
# Only add if not already present
|
|
308
|
+
if "beta" not in query_params:
|
|
309
|
+
query_params["beta"] = ["true"]
|
|
310
|
+
# Rebuild query string
|
|
311
|
+
new_query = urlencode(query_params, doseq=True)
|
|
312
|
+
# Rebuild URL
|
|
313
|
+
new_parsed = parsed._replace(query=new_query)
|
|
314
|
+
return httpx.URL(urlunparse(new_parsed))
|
|
315
|
+
|
|
316
|
+
return url
|
|
317
|
+
|
|
318
|
+
async def send(
|
|
319
|
+
self, request: httpx.Request, *args: Any, **kwargs: Any
|
|
320
|
+
) -> httpx.Response: # type: ignore[override]
|
|
321
|
+
is_messages_endpoint = request.url.path.endswith("/v1/messages")
|
|
322
|
+
|
|
323
|
+
# Proactive token refresh: check JWT age before every request
|
|
324
|
+
if not request.extensions.get("claude_oauth_proactive_refresh_attempted"):
|
|
325
|
+
try:
|
|
326
|
+
if self._should_refresh_token(request):
|
|
327
|
+
# TODO: PEP 734 async bridge — convert refresh_access_token to async
|
|
328
|
+
refreshed_token = await asyncio.to_thread(
|
|
329
|
+
self._refresh_claude_oauth_token
|
|
330
|
+
)
|
|
331
|
+
if refreshed_token:
|
|
332
|
+
logger.info("Proactively refreshed token before request")
|
|
333
|
+
# Rebuild request with new token
|
|
334
|
+
headers = dict(request.headers)
|
|
335
|
+
self._update_auth_headers(headers, refreshed_token)
|
|
336
|
+
body_bytes = self._extract_body_bytes(request)
|
|
337
|
+
request = self.build_request(
|
|
338
|
+
method=request.method,
|
|
339
|
+
url=request.url,
|
|
340
|
+
headers=headers,
|
|
341
|
+
content=body_bytes,
|
|
342
|
+
)
|
|
343
|
+
request.extensions[
|
|
344
|
+
"claude_oauth_proactive_refresh_attempted"
|
|
345
|
+
] = True
|
|
346
|
+
except Exception as exc:
|
|
347
|
+
logger.debug("Error during proactive token refresh check: %s", exc)
|
|
348
|
+
|
|
349
|
+
# Apply Claude Code OAuth transformations for /v1/messages
|
|
350
|
+
if is_messages_endpoint:
|
|
351
|
+
try:
|
|
352
|
+
body_bytes = self._extract_body_bytes(request)
|
|
353
|
+
headers = dict(request.headers)
|
|
354
|
+
url = request.url
|
|
355
|
+
body_modified = False
|
|
356
|
+
headers_modified = False
|
|
357
|
+
|
|
358
|
+
# 1. Transform headers for Claude Code OAuth
|
|
359
|
+
self._transform_headers_for_claude_code(headers)
|
|
360
|
+
headers_modified = True
|
|
361
|
+
|
|
362
|
+
# 2. Add ?beta=true query param
|
|
363
|
+
url = self._add_beta_query_param(url)
|
|
364
|
+
|
|
365
|
+
# 3. Prefix tool names in request body
|
|
366
|
+
if body_bytes:
|
|
367
|
+
prefixed_body = self._prefix_tool_names(body_bytes)
|
|
368
|
+
if prefixed_body is not None:
|
|
369
|
+
body_bytes = prefixed_body
|
|
370
|
+
body_modified = True
|
|
371
|
+
|
|
372
|
+
# 4. Inject cache_control
|
|
373
|
+
cached_body = self._inject_cache_control(body_bytes)
|
|
374
|
+
if cached_body is not None:
|
|
375
|
+
body_bytes = cached_body
|
|
376
|
+
body_modified = True
|
|
377
|
+
|
|
378
|
+
# Rebuild request if anything changed
|
|
379
|
+
if body_modified or headers_modified or url != request.url:
|
|
380
|
+
try:
|
|
381
|
+
rebuilt = self.build_request(
|
|
382
|
+
method=request.method,
|
|
383
|
+
url=url,
|
|
384
|
+
headers=headers,
|
|
385
|
+
content=body_bytes,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Copy core internals so httpx uses the modified body/stream
|
|
389
|
+
if hasattr(rebuilt, "_content"):
|
|
390
|
+
request._content = rebuilt._content # type: ignore[attr-defined]
|
|
391
|
+
if hasattr(rebuilt, "stream"):
|
|
392
|
+
request.stream = rebuilt.stream
|
|
393
|
+
if hasattr(rebuilt, "extensions"):
|
|
394
|
+
# Preserve caller-owned flags (notably oauth retry guards)
|
|
395
|
+
# when httpx gives the rebuilt request fresh extensions.
|
|
396
|
+
request.extensions = {
|
|
397
|
+
**rebuilt.extensions,
|
|
398
|
+
**request.extensions,
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
# Update URL
|
|
402
|
+
request.url = url
|
|
403
|
+
|
|
404
|
+
# Update headers
|
|
405
|
+
for key, value in headers.items():
|
|
406
|
+
request.headers[key] = value
|
|
407
|
+
|
|
408
|
+
# Ensure Content-Length matches the new body
|
|
409
|
+
if body_bytes:
|
|
410
|
+
request.headers["Content-Length"] = str(len(body_bytes))
|
|
411
|
+
|
|
412
|
+
except Exception as exc:
|
|
413
|
+
logger.debug("Error rebuilding request: %s", exc)
|
|
414
|
+
|
|
415
|
+
except Exception as exc:
|
|
416
|
+
logger.debug("Error in Claude Code transformations: %s", exc)
|
|
417
|
+
|
|
418
|
+
# Send the request with retry logic for transient errors
|
|
419
|
+
response = await self._send_with_retries(request, *args, **kwargs)
|
|
420
|
+
|
|
421
|
+
# Extract cache usage for token tracking (best-effort, never blocks)
|
|
422
|
+
if is_messages_endpoint and response.status_code == 200:
|
|
423
|
+
try:
|
|
424
|
+
from code_muse.plugins.token_caching.cache_hit_tracking import (
|
|
425
|
+
_session_stats,
|
|
426
|
+
extract_cache_usage,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
await response.aread()
|
|
430
|
+
data = response.json()
|
|
431
|
+
usage = extract_cache_usage(data)
|
|
432
|
+
if usage:
|
|
433
|
+
_session_stats.record_usage(usage)
|
|
434
|
+
except Exception:
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
# NOTE: Tool name unprefixing is now handled at the pydantic-ai level
|
|
438
|
+
# in pydantic_patches.py rather than wrapping the HTTP response stream.
|
|
439
|
+
# The response wrapper caused zlib decompression errors due to httpx
|
|
440
|
+
# response lifecycle issues.
|
|
441
|
+
|
|
442
|
+
# Handle auth errors with token refresh
|
|
443
|
+
try:
|
|
444
|
+
if response.status_code in (400, 401, 403) and not request.extensions.get(
|
|
445
|
+
"claude_oauth_refresh_attempted"
|
|
446
|
+
):
|
|
447
|
+
is_auth_error = response.status_code in (401, 403)
|
|
448
|
+
|
|
449
|
+
if response.status_code == 400:
|
|
450
|
+
is_auth_error = await self._is_cloudflare_html_error(response)
|
|
451
|
+
if is_auth_error:
|
|
452
|
+
logger.info(
|
|
453
|
+
"Detected Cloudflare 400 error (likely auth-related), attempting token refresh"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
if is_auth_error:
|
|
457
|
+
# TODO: PEP 734 async bridge — convert reauthentication to async
|
|
458
|
+
recovered_token = await asyncio.to_thread(
|
|
459
|
+
self._recover_claude_oauth_token_after_auth_error
|
|
460
|
+
)
|
|
461
|
+
if recovered_token:
|
|
462
|
+
logger.info("Token recovered successfully, retrying request")
|
|
463
|
+
await response.aclose()
|
|
464
|
+
body_bytes = self._extract_body_bytes(request)
|
|
465
|
+
headers = dict(request.headers)
|
|
466
|
+
self._update_auth_headers(headers, recovered_token)
|
|
467
|
+
retry_request = self.build_request(
|
|
468
|
+
method=request.method,
|
|
469
|
+
url=request.url,
|
|
470
|
+
headers=headers,
|
|
471
|
+
content=body_bytes,
|
|
472
|
+
)
|
|
473
|
+
retry_request.extensions["claude_oauth_refresh_attempted"] = (
|
|
474
|
+
True
|
|
475
|
+
)
|
|
476
|
+
return await self._send_with_retries(
|
|
477
|
+
retry_request, *args, **kwargs
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
logger.warning(
|
|
481
|
+
"Token recovery failed, returning original error"
|
|
482
|
+
)
|
|
483
|
+
except Exception as exc:
|
|
484
|
+
logger.debug("Error during token refresh attempt: %s", exc)
|
|
485
|
+
|
|
486
|
+
return response
|
|
487
|
+
|
|
488
|
+
async def _send_with_retries(
|
|
489
|
+
self, request: httpx.Request, *args: Any, **kwargs: Any
|
|
490
|
+
) -> httpx.Response:
|
|
491
|
+
"""Send request with automatic retries for rate limits and server errors.
|
|
492
|
+
|
|
493
|
+
Retries on:
|
|
494
|
+
- 429 (rate limit) - respects Retry-After header
|
|
495
|
+
- 500, 502, 503, 504 (server errors) - exponential backoff
|
|
496
|
+
- Connection errors (ConnectError, ReadTimeout, PoolTimeout)
|
|
497
|
+
"""
|
|
498
|
+
last_response = None
|
|
499
|
+
last_exception = None
|
|
500
|
+
|
|
501
|
+
for attempt in range(MAX_RETRIES + 1):
|
|
502
|
+
try:
|
|
503
|
+
response = await super().send(request, *args, **kwargs)
|
|
504
|
+
last_response = response
|
|
505
|
+
|
|
506
|
+
# Check for retryable status
|
|
507
|
+
if response.status_code not in RETRY_STATUS_CODES:
|
|
508
|
+
return response
|
|
509
|
+
|
|
510
|
+
# Don't retry if this is the last attempt
|
|
511
|
+
if attempt >= MAX_RETRIES:
|
|
512
|
+
return response
|
|
513
|
+
|
|
514
|
+
# Close response before retrying
|
|
515
|
+
await response.aclose()
|
|
516
|
+
|
|
517
|
+
# Calculate wait time with exponential backoff
|
|
518
|
+
wait_time = 1.0 * (2**attempt) # 1s, 2s, 4s, 8s, 16s
|
|
519
|
+
|
|
520
|
+
# For 429, respect Retry-After header if present
|
|
521
|
+
if response.status_code == 429:
|
|
522
|
+
retry_after = response.headers.get("Retry-After")
|
|
523
|
+
if retry_after:
|
|
524
|
+
try:
|
|
525
|
+
wait_time = float(retry_after)
|
|
526
|
+
except ValueError:
|
|
527
|
+
# Try parsing http-date format
|
|
528
|
+
try:
|
|
529
|
+
from email.utils import parsedate_to_datetime
|
|
530
|
+
|
|
531
|
+
date = parsedate_to_datetime(retry_after)
|
|
532
|
+
wait_time = max(0, date.timestamp() - time.time())
|
|
533
|
+
except Exception:
|
|
534
|
+
pass
|
|
535
|
+
|
|
536
|
+
# Cap wait time between 0.5s and 60s
|
|
537
|
+
wait_time = max(0.5, min(wait_time, 60.0))
|
|
538
|
+
|
|
539
|
+
logger.info(
|
|
540
|
+
"HTTP %d received, retrying in %.1fs (attempt %d/%d)",
|
|
541
|
+
response.status_code,
|
|
542
|
+
wait_time,
|
|
543
|
+
attempt + 1,
|
|
544
|
+
MAX_RETRIES,
|
|
545
|
+
)
|
|
546
|
+
await asyncio.sleep(wait_time)
|
|
547
|
+
|
|
548
|
+
except (httpx.ConnectError, httpx.ReadTimeout, httpx.PoolTimeout) as exc:
|
|
549
|
+
last_exception = exc
|
|
550
|
+
|
|
551
|
+
# Don't retry if this is the last attempt
|
|
552
|
+
if attempt >= MAX_RETRIES:
|
|
553
|
+
raise
|
|
554
|
+
|
|
555
|
+
wait_time = 1.0 * (2**attempt)
|
|
556
|
+
wait_time = max(0.5, min(wait_time, 60.0))
|
|
557
|
+
|
|
558
|
+
logger.warning(
|
|
559
|
+
"HTTP connection error: %s. Retrying in %.1fs (attempt %d/%d)",
|
|
560
|
+
exc,
|
|
561
|
+
wait_time,
|
|
562
|
+
attempt + 1,
|
|
563
|
+
MAX_RETRIES,
|
|
564
|
+
)
|
|
565
|
+
await asyncio.sleep(wait_time)
|
|
566
|
+
|
|
567
|
+
except Exception:
|
|
568
|
+
# Don't retry on other exceptions (e.g., validation errors)
|
|
569
|
+
raise
|
|
570
|
+
|
|
571
|
+
# Return last response if we have one
|
|
572
|
+
if last_response is not None:
|
|
573
|
+
return last_response
|
|
574
|
+
|
|
575
|
+
# Re-raise last exception if we have one
|
|
576
|
+
if last_exception is not None:
|
|
577
|
+
raise last_exception
|
|
578
|
+
|
|
579
|
+
# This shouldn't happen, but just in case
|
|
580
|
+
raise RuntimeError("Retry loop completed without response or exception")
|
|
581
|
+
|
|
582
|
+
@staticmethod
|
|
583
|
+
def _extract_body_bytes(request: httpx.Request) -> bytes | None:
|
|
584
|
+
# Try public content first
|
|
585
|
+
try:
|
|
586
|
+
content = request.content
|
|
587
|
+
if content:
|
|
588
|
+
return content
|
|
589
|
+
except Exception:
|
|
590
|
+
pass
|
|
591
|
+
|
|
592
|
+
# Fallback to private attr if necessary
|
|
593
|
+
try:
|
|
594
|
+
content = getattr(request, "_content", None)
|
|
595
|
+
if content:
|
|
596
|
+
return content
|
|
597
|
+
except Exception:
|
|
598
|
+
pass
|
|
599
|
+
|
|
600
|
+
return None
|
|
601
|
+
|
|
602
|
+
@staticmethod
|
|
603
|
+
def _update_auth_headers(
|
|
604
|
+
headers: MutableMapping[str, str], access_token: str
|
|
605
|
+
) -> None:
|
|
606
|
+
bearer_value = f"Bearer {access_token}"
|
|
607
|
+
if "Authorization" in headers or "authorization" in headers:
|
|
608
|
+
headers["Authorization"] = bearer_value
|
|
609
|
+
elif "x-api-key" in headers or "X-API-Key" in headers:
|
|
610
|
+
headers["x-api-key"] = access_token
|
|
611
|
+
else:
|
|
612
|
+
headers["Authorization"] = bearer_value
|
|
613
|
+
|
|
614
|
+
@staticmethod
|
|
615
|
+
async def _is_cloudflare_html_error(response: httpx.Response) -> bool:
|
|
616
|
+
"""Check if this is a Cloudflare HTML error response.
|
|
617
|
+
|
|
618
|
+
Cloudflare often returns HTML error pages with status 400 when
|
|
619
|
+
there are authentication issues.
|
|
620
|
+
"""
|
|
621
|
+
# Check content type
|
|
622
|
+
content_type = response.headers.get("content-type", "")
|
|
623
|
+
if "text/html" not in content_type.lower():
|
|
624
|
+
return False
|
|
625
|
+
|
|
626
|
+
# Check if body contains Cloudflare markers
|
|
627
|
+
try:
|
|
628
|
+
# For async httpx, we need to read the body first
|
|
629
|
+
if not hasattr(response, "_content") or not response._content:
|
|
630
|
+
try:
|
|
631
|
+
await response.aread()
|
|
632
|
+
except Exception as read_exc:
|
|
633
|
+
logger.debug("Failed to read response body: %s", read_exc)
|
|
634
|
+
return False
|
|
635
|
+
|
|
636
|
+
# Now we can safely access the content
|
|
637
|
+
if hasattr(response, "_content") and response._content:
|
|
638
|
+
body = response._content.decode("utf-8", errors="ignore")
|
|
639
|
+
else:
|
|
640
|
+
# Fallback to text property (should work after aread)
|
|
641
|
+
try:
|
|
642
|
+
body = response.text
|
|
643
|
+
except Exception:
|
|
644
|
+
return False
|
|
645
|
+
|
|
646
|
+
# Look for Cloudflare and 400 Bad Request markers
|
|
647
|
+
body_lower = body.lower()
|
|
648
|
+
return "cloudflare" in body_lower and "400 bad request" in body_lower
|
|
649
|
+
except Exception as exc:
|
|
650
|
+
logger.debug("Error checking for Cloudflare error: %s", exc)
|
|
651
|
+
return False
|
|
652
|
+
|
|
653
|
+
def _recover_claude_oauth_token_after_auth_error(self) -> str | None:
|
|
654
|
+
"""Recover an OAuth token after the API rejected the current one.
|
|
655
|
+
|
|
656
|
+
First tries a refresh-token exchange. If that fails, an optional
|
|
657
|
+
provider-specific callback may run a full interactive OAuth flow.
|
|
658
|
+
"""
|
|
659
|
+
refreshed_token = self._refresh_claude_oauth_token()
|
|
660
|
+
if refreshed_token:
|
|
661
|
+
return refreshed_token
|
|
662
|
+
|
|
663
|
+
if not self._oauth_reauthentication_callback:
|
|
664
|
+
return None
|
|
665
|
+
|
|
666
|
+
try:
|
|
667
|
+
reauthenticated_token = self._oauth_reauthentication_callback()
|
|
668
|
+
except Exception as exc:
|
|
669
|
+
logger.error("Exception during OAuth reauthentication: %s", exc)
|
|
670
|
+
return None
|
|
671
|
+
|
|
672
|
+
if not reauthenticated_token:
|
|
673
|
+
logger.warning("OAuth reauthentication returned no token")
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
self._update_auth_headers(self.headers, reauthenticated_token)
|
|
677
|
+
self._notify_token_recovered(reauthenticated_token)
|
|
678
|
+
return reauthenticated_token
|
|
679
|
+
|
|
680
|
+
def _refresh_claude_oauth_token(self) -> str | None:
|
|
681
|
+
try:
|
|
682
|
+
from code_muse.plugins.claude_code_oauth.utils import refresh_access_token
|
|
683
|
+
|
|
684
|
+
logger.info("Attempting to refresh Claude Code OAuth token...")
|
|
685
|
+
refreshed_token = refresh_access_token(force=True)
|
|
686
|
+
if refreshed_token:
|
|
687
|
+
self._update_auth_headers(self.headers, refreshed_token)
|
|
688
|
+
self._notify_token_recovered(refreshed_token)
|
|
689
|
+
logger.info("Successfully refreshed Claude Code OAuth token")
|
|
690
|
+
else:
|
|
691
|
+
logger.warning("Token refresh returned None")
|
|
692
|
+
return refreshed_token
|
|
693
|
+
except Exception as exc:
|
|
694
|
+
logger.error("Exception during token refresh: %s", exc)
|
|
695
|
+
return None
|
|
696
|
+
|
|
697
|
+
@staticmethod
|
|
698
|
+
def _inject_cache_control(body: bytes) -> bytes | None:
|
|
699
|
+
try:
|
|
700
|
+
data = json.loads(body.decode("utf-8"))
|
|
701
|
+
except Exception:
|
|
702
|
+
return None
|
|
703
|
+
|
|
704
|
+
if not isinstance(data, dict):
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
modified = False
|
|
708
|
+
|
|
709
|
+
# Anthropic supports up to 4 cache breakpoints. We place them on
|
|
710
|
+
# the three most impactful, stable prefixes so that content which
|
|
711
|
+
# doesn't change between turns is independently cached:
|
|
712
|
+
# 1. System prompt – static across the whole session
|
|
713
|
+
# 2. Tool definitions – static across the whole session
|
|
714
|
+
# 3. Last message – caches the growing conversation prefix
|
|
715
|
+
|
|
716
|
+
# 1. System prompt
|
|
717
|
+
system = data.get("system")
|
|
718
|
+
if isinstance(system, list) and system:
|
|
719
|
+
last_sys = system[-1]
|
|
720
|
+
if isinstance(last_sys, dict) and "cache_control" not in last_sys:
|
|
721
|
+
last_sys["cache_control"] = {"type": "ephemeral"}
|
|
722
|
+
modified = True
|
|
723
|
+
elif isinstance(system, str) and system:
|
|
724
|
+
# Convert bare string to content-block list so we can attach
|
|
725
|
+
# cache_control (the Anthropic API accepts both formats).
|
|
726
|
+
data["system"] = [
|
|
727
|
+
{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}
|
|
728
|
+
]
|
|
729
|
+
modified = True
|
|
730
|
+
|
|
731
|
+
# 2. Tool definitions
|
|
732
|
+
tools = data.get("tools")
|
|
733
|
+
if isinstance(tools, list) and tools:
|
|
734
|
+
last_tool = tools[-1]
|
|
735
|
+
if isinstance(last_tool, dict) and "cache_control" not in last_tool:
|
|
736
|
+
last_tool["cache_control"] = {"type": "ephemeral"}
|
|
737
|
+
modified = True
|
|
738
|
+
|
|
739
|
+
# 3. Last message content block
|
|
740
|
+
messages = data.get("messages")
|
|
741
|
+
if isinstance(messages, list) and messages:
|
|
742
|
+
last = messages[-1]
|
|
743
|
+
if isinstance(last, dict):
|
|
744
|
+
content = last.get("content")
|
|
745
|
+
if isinstance(content, list) and content:
|
|
746
|
+
last_block = content[-1]
|
|
747
|
+
if (
|
|
748
|
+
isinstance(last_block, dict)
|
|
749
|
+
and "cache_control" not in last_block
|
|
750
|
+
):
|
|
751
|
+
last_block["cache_control"] = {"type": "ephemeral"}
|
|
752
|
+
modified = True
|
|
753
|
+
|
|
754
|
+
# 4. Opus 4.7 adaptive-thinking requires display=summarized on the
|
|
755
|
+
# thinking dict. Enforce at the wire level so the request can't go
|
|
756
|
+
# out without it, regardless of upstream settings construction.
|
|
757
|
+
if _enforce_thinking_display_summary(data):
|
|
758
|
+
modified = True
|
|
759
|
+
|
|
760
|
+
if not modified:
|
|
761
|
+
return None
|
|
762
|
+
|
|
763
|
+
return json.dumps(data).encode("utf-8")
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _inject_cache_control_in_payload(payload: dict[str, Any]) -> None:
|
|
767
|
+
"""In-place cache_control injection on Anthropic messages.create payload.
|
|
768
|
+
|
|
769
|
+
Places up to three cache breakpoints (Anthropic allows 4) on the most
|
|
770
|
+
valuable, stable prefixes:
|
|
771
|
+
1. System prompt – never changes between turns
|
|
772
|
+
2. Tool defs – never changes between turns
|
|
773
|
+
3. Last message – caches the growing conversation prefix
|
|
774
|
+
"""
|
|
775
|
+
|
|
776
|
+
# 1. System prompt
|
|
777
|
+
system = payload.get("system")
|
|
778
|
+
if isinstance(system, list) and system:
|
|
779
|
+
last_sys = system[-1]
|
|
780
|
+
if isinstance(last_sys, dict) and "cache_control" not in last_sys:
|
|
781
|
+
last_sys["cache_control"] = {"type": "ephemeral"}
|
|
782
|
+
elif isinstance(system, str) and system:
|
|
783
|
+
payload["system"] = [
|
|
784
|
+
{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}
|
|
785
|
+
]
|
|
786
|
+
|
|
787
|
+
# 2. Tool definitions
|
|
788
|
+
tools = payload.get("tools")
|
|
789
|
+
if isinstance(tools, list) and tools:
|
|
790
|
+
last_tool = tools[-1]
|
|
791
|
+
if isinstance(last_tool, dict) and "cache_control" not in last_tool:
|
|
792
|
+
last_tool["cache_control"] = {"type": "ephemeral"}
|
|
793
|
+
|
|
794
|
+
# 3. Last message content block
|
|
795
|
+
messages = payload.get("messages")
|
|
796
|
+
if isinstance(messages, list) and messages:
|
|
797
|
+
last = messages[-1]
|
|
798
|
+
if isinstance(last, dict):
|
|
799
|
+
content = last.get("content")
|
|
800
|
+
if isinstance(content, list) and content:
|
|
801
|
+
last_block = content[-1]
|
|
802
|
+
if isinstance(last_block, dict) and "cache_control" not in last_block:
|
|
803
|
+
last_block["cache_control"] = {"type": "ephemeral"}
|
|
804
|
+
|
|
805
|
+
# 4. Opus 4.7 adaptive-thinking requires display=summarized on the
|
|
806
|
+
# thinking dict. Enforce here as well so the AsyncAnthropic client
|
|
807
|
+
# patch path matches the raw httpx path.
|
|
808
|
+
_enforce_thinking_display_summary(payload)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _make_cache_wrapper(original_create: Callable[..., Any]) -> Callable[..., Any]:
|
|
812
|
+
"""Create a wrapped version of messages.create that injects cache_control."""
|
|
813
|
+
|
|
814
|
+
async def wrapped_create(*args: Any, **kwargs: Any):
|
|
815
|
+
if kwargs:
|
|
816
|
+
_inject_cache_control_in_payload(kwargs)
|
|
817
|
+
elif args:
|
|
818
|
+
maybe_payload = args[-1]
|
|
819
|
+
if isinstance(maybe_payload, dict):
|
|
820
|
+
_inject_cache_control_in_payload(maybe_payload)
|
|
821
|
+
|
|
822
|
+
return await original_create(*args, **kwargs)
|
|
823
|
+
|
|
824
|
+
return wrapped_create
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def patch_anthropic_client_messages(client: Any) -> None:
|
|
828
|
+
"""Monkey-patch AsyncAnthropic messages.create to inject cache_control.
|
|
829
|
+
|
|
830
|
+
Patches both client.messages.create AND client.beta.messages.create
|
|
831
|
+
since pydantic-ai uses the beta endpoint.
|
|
832
|
+
"""
|
|
833
|
+
|
|
834
|
+
if AsyncAnthropic is None or not isinstance(client, AsyncAnthropic): # type: ignore[arg-type]
|
|
835
|
+
return
|
|
836
|
+
|
|
837
|
+
# Patch client.messages.create
|
|
838
|
+
try:
|
|
839
|
+
messages_obj = getattr(client, "messages", None)
|
|
840
|
+
if messages_obj is not None:
|
|
841
|
+
messages_obj.create = _make_cache_wrapper(messages_obj.create) # type: ignore[assignment]
|
|
842
|
+
except Exception: # pragma: no cover - defensive
|
|
843
|
+
pass
|
|
844
|
+
|
|
845
|
+
# Patch client.beta.messages.create (used by pydantic-ai)
|
|
846
|
+
try:
|
|
847
|
+
beta_obj = getattr(client, "beta", None)
|
|
848
|
+
if beta_obj is not None:
|
|
849
|
+
beta_messages_obj = getattr(beta_obj, "messages", None)
|
|
850
|
+
if beta_messages_obj is not None:
|
|
851
|
+
beta_messages_obj.create = _make_cache_wrapper(beta_messages_obj.create) # type: ignore[assignment]
|
|
852
|
+
except Exception: # pragma: no cover - defensive
|
|
853
|
+
pass
|