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,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for the hook engine.
|
|
3
|
+
|
|
4
|
+
Defines all data structures used throughout the hook engine with full type
|
|
5
|
+
safety and validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class HookConfig:
|
|
14
|
+
"""
|
|
15
|
+
Configuration for a single hook.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
matcher: Pattern to match against events (e.g., "Edit && .py")
|
|
19
|
+
type: Type of hook action ("command" or "prompt")
|
|
20
|
+
command: Command or prompt text to execute
|
|
21
|
+
timeout: Maximum execution time in milliseconds (default: 5000)
|
|
22
|
+
once: Execute only once per session (default: False)
|
|
23
|
+
enabled: Whether this hook is enabled (default: True)
|
|
24
|
+
id: Optional unique identifier for this hook
|
|
25
|
+
source: Origin of the hook config ("global" or "project")
|
|
26
|
+
trusted: Whether this hook has been explicitly trusted. For global
|
|
27
|
+
hooks this defaults to True. Project hooks default to False.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
matcher: str
|
|
31
|
+
type: Literal["command", "prompt"]
|
|
32
|
+
command: str
|
|
33
|
+
timeout: int = 5000
|
|
34
|
+
once: bool = False
|
|
35
|
+
enabled: bool = True
|
|
36
|
+
id: str | None = None
|
|
37
|
+
source: Literal["global", "project"] = "global"
|
|
38
|
+
trusted: bool = True
|
|
39
|
+
|
|
40
|
+
def __post_init__(self):
|
|
41
|
+
"""Validate hook configuration after initialization."""
|
|
42
|
+
if not self.matcher:
|
|
43
|
+
raise ValueError("Hook matcher cannot be empty")
|
|
44
|
+
|
|
45
|
+
if self.type not in ("command", "prompt"):
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Hook type must be 'command' or 'prompt', got: {self.type}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if not self.command:
|
|
51
|
+
raise ValueError("Hook command cannot be empty")
|
|
52
|
+
|
|
53
|
+
if self.timeout < 100:
|
|
54
|
+
raise ValueError(f"Hook timeout must be >= 100ms, got: {self.timeout}")
|
|
55
|
+
|
|
56
|
+
if self.id is None:
|
|
57
|
+
import hashlib
|
|
58
|
+
|
|
59
|
+
content = f"{self.matcher}:{self.type}:{self.command}"
|
|
60
|
+
self.id = hashlib.sha256(content.encode()).hexdigest()[:12]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class EventData:
|
|
65
|
+
"""
|
|
66
|
+
Input data for hook processing.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
event_type: Type of event (PreToolUse, PostToolUse, etc.)
|
|
70
|
+
tool_name: Name of the tool being called
|
|
71
|
+
tool_args: Arguments passed to the tool
|
|
72
|
+
context: Optional context metadata (result, duration, etc.)
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
event_type: str
|
|
76
|
+
tool_name: str
|
|
77
|
+
tool_args: dict[str, Any] = field(default_factory=dict)
|
|
78
|
+
context: dict[str, Any] = field(default_factory=dict)
|
|
79
|
+
|
|
80
|
+
def __post_init__(self):
|
|
81
|
+
if not self.event_type:
|
|
82
|
+
raise ValueError("Event type cannot be empty")
|
|
83
|
+
if not self.tool_name:
|
|
84
|
+
raise ValueError("Tool name cannot be empty")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class ExecutionResult:
|
|
89
|
+
"""
|
|
90
|
+
Result from executing a hook.
|
|
91
|
+
|
|
92
|
+
Attributes:
|
|
93
|
+
blocked: Whether the hook blocked the operation
|
|
94
|
+
hook_command: The command that was executed
|
|
95
|
+
stdout: Standard output from command
|
|
96
|
+
stderr: Standard error from command
|
|
97
|
+
exit_code: Exit code from command execution
|
|
98
|
+
duration_ms: Execution duration in milliseconds
|
|
99
|
+
error: Error message if execution failed
|
|
100
|
+
hook_id: ID of the hook that was executed
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
blocked: bool
|
|
104
|
+
hook_command: str
|
|
105
|
+
stdout: str = ""
|
|
106
|
+
stderr: str = ""
|
|
107
|
+
exit_code: int = 0
|
|
108
|
+
duration_ms: float = 0.0
|
|
109
|
+
error: str | None = None
|
|
110
|
+
hook_id: str | None = None
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def success(self) -> bool:
|
|
114
|
+
"""Whether the hook executed successfully (exit code 0)."""
|
|
115
|
+
return self.exit_code == 0 and self.error is None
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def output(self) -> str:
|
|
119
|
+
"""Combined stdout and stderr."""
|
|
120
|
+
parts = []
|
|
121
|
+
if self.stdout:
|
|
122
|
+
parts.append(self.stdout)
|
|
123
|
+
if self.stderr:
|
|
124
|
+
parts.append(self.stderr)
|
|
125
|
+
return "\n".join(parts)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class HookGroup:
|
|
130
|
+
"""A group of hooks that share the same matcher."""
|
|
131
|
+
|
|
132
|
+
matcher: str
|
|
133
|
+
hooks: list[HookConfig] = field(default_factory=list)
|
|
134
|
+
|
|
135
|
+
def __post_init__(self):
|
|
136
|
+
if not self.matcher:
|
|
137
|
+
raise ValueError("Hook group matcher cannot be empty")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class HookRegistry:
|
|
142
|
+
"""Registry of all hooks organized by event type."""
|
|
143
|
+
|
|
144
|
+
pre_tool_use: list[HookConfig] = field(default_factory=list)
|
|
145
|
+
post_tool_use: list[HookConfig] = field(default_factory=list)
|
|
146
|
+
session_start: list[HookConfig] = field(default_factory=list)
|
|
147
|
+
session_end: list[HookConfig] = field(default_factory=list)
|
|
148
|
+
pre_compact: list[HookConfig] = field(default_factory=list)
|
|
149
|
+
user_prompt_submit: list[HookConfig] = field(default_factory=list)
|
|
150
|
+
notification: list[HookConfig] = field(default_factory=list)
|
|
151
|
+
stop: list[HookConfig] = field(default_factory=list)
|
|
152
|
+
subagent_stop: list[HookConfig] = field(default_factory=list)
|
|
153
|
+
|
|
154
|
+
_executed_once_hooks: set = field(default_factory=set, repr=False)
|
|
155
|
+
|
|
156
|
+
def get_hooks_for_event(self, event_type: str) -> list[HookConfig]:
|
|
157
|
+
attr_name = self._normalize_event_type(event_type)
|
|
158
|
+
if not hasattr(self, attr_name):
|
|
159
|
+
return []
|
|
160
|
+
all_hooks = getattr(self, attr_name)
|
|
161
|
+
enabled_hooks = []
|
|
162
|
+
for hook in all_hooks:
|
|
163
|
+
if not hook.enabled:
|
|
164
|
+
continue
|
|
165
|
+
if hook.once and hook.id in self._executed_once_hooks:
|
|
166
|
+
continue
|
|
167
|
+
enabled_hooks.append(hook)
|
|
168
|
+
return enabled_hooks
|
|
169
|
+
|
|
170
|
+
def mark_hook_executed(self, hook_id: str) -> None:
|
|
171
|
+
self._executed_once_hooks.add(hook_id)
|
|
172
|
+
|
|
173
|
+
def reset_once_hooks(self) -> None:
|
|
174
|
+
self._executed_once_hooks.clear()
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _normalize_event_type(event_type: str) -> str:
|
|
178
|
+
import re
|
|
179
|
+
|
|
180
|
+
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", event_type)
|
|
181
|
+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
|
182
|
+
|
|
183
|
+
def add_hook(self, event_type: str, hook: HookConfig) -> None:
|
|
184
|
+
attr_name = self._normalize_event_type(event_type)
|
|
185
|
+
if not hasattr(self, attr_name):
|
|
186
|
+
raise ValueError(f"Unknown event type: {event_type}")
|
|
187
|
+
getattr(self, attr_name).append(hook)
|
|
188
|
+
|
|
189
|
+
def remove_hook(self, event_type: str, hook_id: str) -> bool:
|
|
190
|
+
attr_name = self._normalize_event_type(event_type)
|
|
191
|
+
if not hasattr(self, attr_name):
|
|
192
|
+
return False
|
|
193
|
+
hooks_list = getattr(self, attr_name)
|
|
194
|
+
for i, hook in enumerate(hooks_list):
|
|
195
|
+
if hook.id == hook_id:
|
|
196
|
+
hooks_list.pop(i)
|
|
197
|
+
return True
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
def count_hooks(self, event_type: str | None = None) -> int:
|
|
201
|
+
if event_type is None:
|
|
202
|
+
total = 0
|
|
203
|
+
for attr in [
|
|
204
|
+
"pre_tool_use",
|
|
205
|
+
"post_tool_use",
|
|
206
|
+
"session_start",
|
|
207
|
+
"session_end",
|
|
208
|
+
"pre_compact",
|
|
209
|
+
"user_prompt_submit",
|
|
210
|
+
"notification",
|
|
211
|
+
"stop",
|
|
212
|
+
"subagent_stop",
|
|
213
|
+
]:
|
|
214
|
+
total += len(getattr(self, attr))
|
|
215
|
+
return total
|
|
216
|
+
attr_name = self._normalize_event_type(event_type)
|
|
217
|
+
if not hasattr(self, attr_name):
|
|
218
|
+
return 0
|
|
219
|
+
return len(getattr(self, attr_name))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class ProcessEventResult:
|
|
224
|
+
"""Result from processing an event through the hook engine."""
|
|
225
|
+
|
|
226
|
+
blocked: bool
|
|
227
|
+
executed_hooks: int
|
|
228
|
+
results: list[ExecutionResult]
|
|
229
|
+
blocking_reason: str | None = None
|
|
230
|
+
total_duration_ms: float = 0.0
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def all_successful(self) -> bool:
|
|
234
|
+
return all(result.success for result in self.results)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def failed_hooks(self) -> list[ExecutionResult]:
|
|
238
|
+
return [result for result in self.results if not result.success]
|
|
239
|
+
|
|
240
|
+
def get_combined_output(self) -> str:
|
|
241
|
+
outputs = []
|
|
242
|
+
for result in self.results:
|
|
243
|
+
if result.output:
|
|
244
|
+
outputs.append(f"[{result.hook_command}]\n{result.output}")
|
|
245
|
+
return "\n\n".join(outputs)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Registry management for hooks.
|
|
3
|
+
|
|
4
|
+
Builds and manages the HookRegistry from configuration dictionaries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .models import HookConfig, HookRegistry
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Supported event types
|
|
16
|
+
SUPPORTED_EVENT_TYPES = [
|
|
17
|
+
"PreToolUse",
|
|
18
|
+
"PostToolUse",
|
|
19
|
+
"SessionStart",
|
|
20
|
+
"SessionEnd",
|
|
21
|
+
"PreCompact",
|
|
22
|
+
"UserPromptSubmit",
|
|
23
|
+
"Notification",
|
|
24
|
+
"Stop",
|
|
25
|
+
"SubagentStop",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_registry_from_config(
|
|
30
|
+
config: dict[str, Any],
|
|
31
|
+
source: str = "global",
|
|
32
|
+
trusted: bool = True,
|
|
33
|
+
) -> HookRegistry:
|
|
34
|
+
"""
|
|
35
|
+
Build a HookRegistry from a configuration dictionary.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: Hook configuration dictionary.
|
|
39
|
+
source: Origin of this config ("global" or "project").
|
|
40
|
+
trusted: Whether hooks from this config are pre-trusted.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Populated HookRegistry
|
|
44
|
+
"""
|
|
45
|
+
registry = HookRegistry()
|
|
46
|
+
|
|
47
|
+
for event_type, hook_groups in config.items():
|
|
48
|
+
if event_type.startswith("_"):
|
|
49
|
+
continue # skip comment keys
|
|
50
|
+
|
|
51
|
+
if not isinstance(hook_groups, list):
|
|
52
|
+
logger.warning(f"Hook groups for '{event_type}' must be a list, skipping")
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
for group in hook_groups:
|
|
56
|
+
if not isinstance(group, dict):
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
matcher = group.get("matcher", "*")
|
|
60
|
+
hooks_data = group.get("hooks", [])
|
|
61
|
+
|
|
62
|
+
for hook_data in hooks_data:
|
|
63
|
+
if not isinstance(hook_data, dict):
|
|
64
|
+
continue
|
|
65
|
+
if hook_data.get("type") == "command" and not hook_data.get("command"):
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
hook = HookConfig(
|
|
70
|
+
matcher=matcher,
|
|
71
|
+
type=hook_data.get("type", "command"),
|
|
72
|
+
command=hook_data.get("command", hook_data.get("prompt", "")),
|
|
73
|
+
timeout=hook_data.get("timeout", 5000),
|
|
74
|
+
once=hook_data.get("once", False),
|
|
75
|
+
enabled=hook_data.get("enabled", True),
|
|
76
|
+
id=hook_data.get("id"),
|
|
77
|
+
source=source, # type: ignore[arg-type]
|
|
78
|
+
trusted=trusted,
|
|
79
|
+
)
|
|
80
|
+
registry.add_hook(event_type, hook)
|
|
81
|
+
except (ValueError, KeyError) as e:
|
|
82
|
+
logger.warning(f"Skipping invalid hook in {event_type}: {e}")
|
|
83
|
+
|
|
84
|
+
return registry
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_registry_stats(registry: HookRegistry) -> dict[str, Any]:
|
|
88
|
+
"""Get statistics about a registry."""
|
|
89
|
+
stats: dict[str, Any] = {
|
|
90
|
+
"total_hooks": registry.count_hooks(),
|
|
91
|
+
"enabled_hooks": 0,
|
|
92
|
+
"disabled_hooks": 0,
|
|
93
|
+
"by_event": {},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
def _to_attr(event_type: str) -> str:
|
|
97
|
+
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", event_type)
|
|
98
|
+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
|
99
|
+
|
|
100
|
+
for event_type in SUPPORTED_EVENT_TYPES:
|
|
101
|
+
attr = _to_attr(event_type)
|
|
102
|
+
hooks = getattr(registry, attr, [])
|
|
103
|
+
enabled = sum(1 for h in hooks if h.enabled)
|
|
104
|
+
disabled = len(hooks) - enabled
|
|
105
|
+
stats["enabled_hooks"] += enabled
|
|
106
|
+
stats["disabled_hooks"] += disabled
|
|
107
|
+
if hooks:
|
|
108
|
+
stats["by_event"][event_type] = {
|
|
109
|
+
"total": len(hooks),
|
|
110
|
+
"enabled": enabled,
|
|
111
|
+
"disabled": disabled,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return stats
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Hook trust engine for project-level hooks.
|
|
2
|
+
|
|
3
|
+
Requires explicit user trust before executing repository-controlled hooks.
|
|
4
|
+
Trust decisions are stored privately in XDG state and keyed by:
|
|
5
|
+
- project root directory
|
|
6
|
+
- hooks file path
|
|
7
|
+
- content hash (SHA-256 of hooks file)
|
|
8
|
+
|
|
9
|
+
This prevents repo-controlled hooks from executing automatically after
|
|
10
|
+
cloning or when content changes.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import contextlib
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# XDG state directory for private trust storage
|
|
23
|
+
_TRUST_DIR = (
|
|
24
|
+
Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state"))
|
|
25
|
+
/ "code_muse"
|
|
26
|
+
)
|
|
27
|
+
_TRUST_FILE = _TRUST_DIR / "hook_trust.json"
|
|
28
|
+
|
|
29
|
+
# Minimal env vars allowed for project hooks
|
|
30
|
+
_SAFE_ENV_ALLOWLIST = {
|
|
31
|
+
"PATH",
|
|
32
|
+
"HOME",
|
|
33
|
+
"SHELL",
|
|
34
|
+
"PWD",
|
|
35
|
+
"TERM",
|
|
36
|
+
"LANG",
|
|
37
|
+
"LC_ALL",
|
|
38
|
+
"USER",
|
|
39
|
+
"LOGNAME",
|
|
40
|
+
"CLAUDE_PROJECT_DIR",
|
|
41
|
+
"CLAUDE_TOOL_INPUT",
|
|
42
|
+
"CLAUDE_TOOL_NAME",
|
|
43
|
+
"CLAUDE_HOOK_EVENT",
|
|
44
|
+
"CLAUDE_CODE_HOOK",
|
|
45
|
+
"CLAUDE_FILE_PATH",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Patterns that suggest a secret-bearing env var
|
|
49
|
+
_SECRET_ENV_PATTERNS = (
|
|
50
|
+
"token",
|
|
51
|
+
"secret",
|
|
52
|
+
"password",
|
|
53
|
+
"passwd",
|
|
54
|
+
"api_key",
|
|
55
|
+
"apikey",
|
|
56
|
+
"auth",
|
|
57
|
+
"credential",
|
|
58
|
+
"private_key",
|
|
59
|
+
"ssh_key",
|
|
60
|
+
"aws_access",
|
|
61
|
+
"aws_secret",
|
|
62
|
+
"bearer",
|
|
63
|
+
"jwt",
|
|
64
|
+
"client_secret",
|
|
65
|
+
"access_token",
|
|
66
|
+
"refresh_token",
|
|
67
|
+
"id_token",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _ensure_private_dir(path: Path) -> Path:
|
|
72
|
+
"""Ensure directory exists with 0o700 permissions."""
|
|
73
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
with contextlib.suppress(OSError):
|
|
75
|
+
os.chmod(path, 0o700)
|
|
76
|
+
return path
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _atomic_write_private_json(file_path: Path, data: dict) -> None:
|
|
80
|
+
"""Atomically write JSON with 0o600 permissions."""
|
|
81
|
+
tmp_path = file_path.with_suffix(".tmp")
|
|
82
|
+
try:
|
|
83
|
+
fd = os.open(
|
|
84
|
+
str(tmp_path),
|
|
85
|
+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
|
|
86
|
+
0o600,
|
|
87
|
+
)
|
|
88
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
89
|
+
json.dump(data, f, indent=2)
|
|
90
|
+
f.flush()
|
|
91
|
+
os.fsync(f.fileno())
|
|
92
|
+
os.replace(str(tmp_path), str(file_path))
|
|
93
|
+
with contextlib.suppress(OSError):
|
|
94
|
+
os.chmod(file_path, 0o600)
|
|
95
|
+
except Exception:
|
|
96
|
+
with contextlib.suppress(OSError):
|
|
97
|
+
tmp_path.unlink(missing_ok=True)
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _load_trust_db() -> dict[str, dict]:
|
|
102
|
+
"""Load the trust database from private storage."""
|
|
103
|
+
_ensure_private_dir(_TRUST_DIR)
|
|
104
|
+
if not _TRUST_FILE.exists():
|
|
105
|
+
return {}
|
|
106
|
+
try:
|
|
107
|
+
with open(_TRUST_FILE, encoding="utf-8") as f:
|
|
108
|
+
data = json.load(f)
|
|
109
|
+
if isinstance(data, dict):
|
|
110
|
+
return data
|
|
111
|
+
except json.JSONDecodeError, OSError:
|
|
112
|
+
pass
|
|
113
|
+
return {}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _save_trust_db(db: dict[str, dict]) -> None:
|
|
117
|
+
"""Save the trust database to private storage."""
|
|
118
|
+
_ensure_private_dir(_TRUST_DIR)
|
|
119
|
+
_atomic_write_private_json(_TRUST_FILE, db)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _compute_trust_key(
|
|
123
|
+
project_root: str | Path,
|
|
124
|
+
hooks_file_path: str | Path,
|
|
125
|
+
content_hash: str,
|
|
126
|
+
) -> str:
|
|
127
|
+
"""Compute a deterministic trust key."""
|
|
128
|
+
raw = f"{project_root}\n{hooks_file_path}\n{content_hash}"
|
|
129
|
+
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def compute_content_hash(content: str) -> str:
|
|
133
|
+
"""Compute SHA-256 hash of hook file content."""
|
|
134
|
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def is_hook_trusted(
|
|
138
|
+
project_root: str | Path,
|
|
139
|
+
hooks_file_path: str | Path,
|
|
140
|
+
content_hash: str,
|
|
141
|
+
) -> bool:
|
|
142
|
+
"""Check whether a project hook file is explicitly trusted.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
project_root: Absolute path to the project root directory.
|
|
146
|
+
hooks_file_path: Absolute path to the hooks configuration file.
|
|
147
|
+
content_hash: SHA-256 hash of the current hooks file content.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if the user has previously approved this exact content.
|
|
151
|
+
"""
|
|
152
|
+
db = _load_trust_db()
|
|
153
|
+
key = _compute_trust_key(project_root, hooks_file_path, content_hash)
|
|
154
|
+
entry = db.get(key)
|
|
155
|
+
if entry is None:
|
|
156
|
+
return False
|
|
157
|
+
stored_hash = entry.get("content_hash")
|
|
158
|
+
if stored_hash != content_hash:
|
|
159
|
+
return False
|
|
160
|
+
return entry.get("trusted", False)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def approve_hook(
|
|
164
|
+
project_root: str,
|
|
165
|
+
hooks_file_path: str,
|
|
166
|
+
content_hash: str,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Explicitly mark a project hook file as trusted.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
project_root: Absolute path to the project root directory.
|
|
172
|
+
hooks_file_path: Absolute path to the hooks configuration file.
|
|
173
|
+
content_hash: SHA-256 hash of the current hooks file content.
|
|
174
|
+
"""
|
|
175
|
+
db = _load_trust_db()
|
|
176
|
+
key = _compute_trust_key(project_root, hooks_file_path, content_hash)
|
|
177
|
+
db[key] = {
|
|
178
|
+
"project_root": project_root,
|
|
179
|
+
"hooks_file_path": hooks_file_path,
|
|
180
|
+
"content_hash": content_hash,
|
|
181
|
+
"trusted": True,
|
|
182
|
+
}
|
|
183
|
+
_save_trust_db(db)
|
|
184
|
+
logger.info(f"Hook trusted: {hooks_file_path} (hash={content_hash[:16]}...)")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def revoke_hook_trust(
|
|
188
|
+
project_root: str,
|
|
189
|
+
hooks_file_path: str,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Revoke trust for a project hook file regardless of content hash.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
project_root: Absolute path to the project root directory.
|
|
195
|
+
hooks_file_path: Absolute path to the hooks configuration file.
|
|
196
|
+
"""
|
|
197
|
+
db = _load_trust_db()
|
|
198
|
+
keys_to_remove = []
|
|
199
|
+
for key, entry in db.items():
|
|
200
|
+
if (
|
|
201
|
+
entry.get("project_root") == project_root
|
|
202
|
+
and entry.get("hooks_file_path") == hooks_file_path
|
|
203
|
+
):
|
|
204
|
+
keys_to_remove.append(key)
|
|
205
|
+
for key in keys_to_remove:
|
|
206
|
+
del db[key]
|
|
207
|
+
if keys_to_remove:
|
|
208
|
+
_save_trust_db(db)
|
|
209
|
+
logger.info(f"Hook trust revoked: {hooks_file_path}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_minimal_hook_env(base_env: dict[str, str | None] = None) -> dict[str, str]:
|
|
213
|
+
"""Build a minimal environment dict for project hooks.
|
|
214
|
+
|
|
215
|
+
Strips secret-like environment variables while preserving safe
|
|
216
|
+
operational variables and any hook-specific vars.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
base_env: Optional base environment to filter. Defaults to os.environ.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Filtered environment dictionary.
|
|
223
|
+
"""
|
|
224
|
+
env = base_env if base_env is not None else os.environ.copy()
|
|
225
|
+
filtered: dict[str, str] = {}
|
|
226
|
+
for key, value in env.items():
|
|
227
|
+
upper_key = key.upper()
|
|
228
|
+
# Always allow safe vars
|
|
229
|
+
if upper_key in _SAFE_ENV_ALLOWLIST:
|
|
230
|
+
filtered[key] = value
|
|
231
|
+
continue
|
|
232
|
+
# Block anything that looks secret-bearing
|
|
233
|
+
lower_key = key.lower()
|
|
234
|
+
if any(pattern in lower_key for pattern in _SECRET_ENV_PATTERNS):
|
|
235
|
+
continue
|
|
236
|
+
# Block very long values that might be tokens/keys
|
|
237
|
+
if len(value) > 4096 and upper_key not in _SAFE_ENV_ALLOWLIST:
|
|
238
|
+
continue
|
|
239
|
+
filtered[key] = value
|
|
240
|
+
return filtered
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def cap_hook_output(text: str, max_chars: int = 4096, max_lines: int = 256) -> str:
|
|
244
|
+
"""Cap hook stdout/stderr to prevent unbounded output in model context.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
text: Raw output string.
|
|
248
|
+
max_chars: Maximum total characters to retain.
|
|
249
|
+
max_lines: Maximum number of lines to retain.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Capped output with a truncation marker if trimmed.
|
|
253
|
+
"""
|
|
254
|
+
if not text:
|
|
255
|
+
return text
|
|
256
|
+
lines = text.splitlines()
|
|
257
|
+
if len(lines) > max_lines:
|
|
258
|
+
lines = lines[:max_lines]
|
|
259
|
+
truncated = True
|
|
260
|
+
else:
|
|
261
|
+
truncated = False
|
|
262
|
+
result = "\n".join(lines)
|
|
263
|
+
if len(result) > max_chars:
|
|
264
|
+
result = result[:max_chars]
|
|
265
|
+
truncated = True
|
|
266
|
+
if truncated:
|
|
267
|
+
result += "\n... [output truncated]"
|
|
268
|
+
return result
|