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
code_muse/motion.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Centralized motion primitives for animated terminal output.
|
|
2
|
+
|
|
3
|
+
All animated effects (shimmer, spinners, indicators) should go through this
|
|
4
|
+
module so callers get a consistent reduced-motion fallback automatically.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from code_muse.motion import MotionMode, activity_indicator, shimmer_text
|
|
8
|
+
|
|
9
|
+
mode = MotionMode.from_animations_enabled(True)
|
|
10
|
+
|
|
11
|
+
# A shimmering bullet as an activity indicator
|
|
12
|
+
bullet = activity_indicator(start_time, mode)
|
|
13
|
+
|
|
14
|
+
# Shimmer text like "Thinking..."
|
|
15
|
+
spans = shimmer_text("Thinking...", mode)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
from enum import Enum
|
|
20
|
+
|
|
21
|
+
from rich.style import Style
|
|
22
|
+
from rich.text import Span
|
|
23
|
+
|
|
24
|
+
from code_muse.messaging.shimmer import shimmer_spans
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MotionMode(Enum):
|
|
28
|
+
"""Animation mode: full animation or reduced-motion fallback."""
|
|
29
|
+
|
|
30
|
+
ANIMATED = "animated"
|
|
31
|
+
REDUCED = "reduced"
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_animations_enabled(cls, enabled: bool) -> MotionMode:
|
|
35
|
+
"""Convert a boolean flag to the corresponding mode."""
|
|
36
|
+
return cls.ANIMATED if enabled else cls.REDUCED
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ReducedMotionIndicator(Enum):
|
|
40
|
+
"""Fallback indicator style when animations are disabled."""
|
|
41
|
+
|
|
42
|
+
HIDDEN = "hidden" # nothing shown
|
|
43
|
+
STATIC_BULLET = "static_bullet" # a plain dimmed bullet
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def activity_indicator(
|
|
47
|
+
start_time: float | None = None,
|
|
48
|
+
motion_mode: MotionMode = MotionMode.ANIMATED,
|
|
49
|
+
reduced_fallback: ReducedMotionIndicator = ReducedMotionIndicator.STATIC_BULLET,
|
|
50
|
+
) -> Span | None:
|
|
51
|
+
"""Return an activity-indicator ``Span`` appropriate for *motion_mode*.
|
|
52
|
+
|
|
53
|
+
In ``ANIMATED`` mode this is a shimmering bullet ``•`` (truecolor
|
|
54
|
+
terminal) or a blinking alternate-character effect (basic terminal).
|
|
55
|
+
|
|
56
|
+
In ``REDUCED`` mode it follows *reduced_fallback*:
|
|
57
|
+
- ``HIDDEN`` → ``None`` (invisible)
|
|
58
|
+
- ``STATIC_BULLET`` → a dimmed ``•``
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
start_time: ``time.monotonic()`` value captured when the activity
|
|
62
|
+
began, or ``None``.
|
|
63
|
+
motion_mode: Current animation mode.
|
|
64
|
+
reduced_fallback: What to show in reduced-motion mode.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
A ``Span`` or ``None``.
|
|
68
|
+
"""
|
|
69
|
+
if motion_mode == MotionMode.REDUCED:
|
|
70
|
+
if reduced_fallback == ReducedMotionIndicator.HIDDEN:
|
|
71
|
+
return None
|
|
72
|
+
# StaticBullet
|
|
73
|
+
return Span(0, 1, Style(dim=True))
|
|
74
|
+
|
|
75
|
+
# Animated mode
|
|
76
|
+
elapsed = time.monotonic() - start_time if start_time else 0.0
|
|
77
|
+
|
|
78
|
+
has_truecolor = _has_truecolor()
|
|
79
|
+
if has_truecolor:
|
|
80
|
+
# Use a shimmer bullet — single character through the shimmer pipeline.
|
|
81
|
+
spans = shimmer_spans("•")
|
|
82
|
+
return spans[0] if spans else Span(0, 1, Style())
|
|
83
|
+
else:
|
|
84
|
+
# 600ms blink period: alternate "•" / "◦".
|
|
85
|
+
blink_on = int(elapsed * 1000 / 600) % 2 == 0
|
|
86
|
+
if blink_on:
|
|
87
|
+
return Span(0, 1, Style())
|
|
88
|
+
else:
|
|
89
|
+
return Span(0, 1, Style(dim=True))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def shimmer_text(
|
|
93
|
+
text: str,
|
|
94
|
+
motion_mode: MotionMode = MotionMode.ANIMATED,
|
|
95
|
+
) -> list[Span]:
|
|
96
|
+
"""Return shimmer-styled spans for *text*, or plain text in reduced mode.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
text: The text to style.
|
|
100
|
+
motion_mode: Current animation mode.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
List of ``Span`` objects.
|
|
104
|
+
"""
|
|
105
|
+
if motion_mode == MotionMode.REDUCED:
|
|
106
|
+
if not text:
|
|
107
|
+
return []
|
|
108
|
+
return [Span(0, len(text), Style())]
|
|
109
|
+
return shimmer_spans(text)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# Internal helpers
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
_truecolor_cache: bool | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _has_truecolor() -> bool:
|
|
120
|
+
"""Cached truecolor detection."""
|
|
121
|
+
global _truecolor_cache
|
|
122
|
+
if _truecolor_cache is None:
|
|
123
|
+
from code_muse.terminal_utils import detect_truecolor_support
|
|
124
|
+
|
|
125
|
+
_truecolor_cache = detect_truecolor_support()
|
|
126
|
+
return _truecolor_cache
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""Plugin loading with trust model for user plugins.
|
|
2
|
+
|
|
3
|
+
Built-in plugins (under code_muse/plugins/) load unconditionally.
|
|
4
|
+
User plugins (under ~/.muse/plugins/) require explicit trust
|
|
5
|
+
recorded in a manifest keyed by content hash; fail closed by default.
|
|
6
|
+
No sys.path insertion — user plugins are loaded via importlib with
|
|
7
|
+
unique module names to prevent stdlib/project shadowing.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
import hashlib
|
|
12
|
+
import importlib
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from code_muse.secret_storage import atomic_write_private_json, ensure_private_dir
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# User plugins directory
|
|
26
|
+
USER_PLUGINS_DIR = Path.home() / ".muse" / "plugins"
|
|
27
|
+
|
|
28
|
+
# Track if plugins have already been loaded to prevent duplicate registration
|
|
29
|
+
_PLUGINS_LOADED = False
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Trust manifest helpers
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
# Env var / monkeypatch-friendly override for trust manifest path.
|
|
36
|
+
# Set MUSE_PLUGIN_TRUST_MANIFEST to a file path to redirect the DB.
|
|
37
|
+
_TRUST_MANIFEST_ENV = "MUSE_PLUGIN_TRUST_MANIFEST"
|
|
38
|
+
|
|
39
|
+
# Safe plugin name pattern: only alphanumeric + underscore + hyphen
|
|
40
|
+
_SAFE_NAME_RE = re.compile(r"^[a-zA-Z0-9_-]+$")
|
|
41
|
+
|
|
42
|
+
# Max SKILL.md content size (bytes) before we refuse to read
|
|
43
|
+
_MAX_SKILL_MD_BYTES = 256 * 1024 # 256 KiB
|
|
44
|
+
|
|
45
|
+
# Cap for skill content injected into model context (chars)
|
|
46
|
+
_SKILL_CONTEXT_CAP = 64_000 # ~64k chars
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _default_trust_manifest_path() -> Path:
|
|
50
|
+
"""Return default path for the plugin trust manifest."""
|
|
51
|
+
return Path.home() / ".muse" / "plugin_trust.json"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_trust_manifest_path() -> Path:
|
|
55
|
+
"""Return the trust manifest path (env-override aware)."""
|
|
56
|
+
env_val = os.environ.get(_TRUST_MANIFEST_ENV)
|
|
57
|
+
if env_val:
|
|
58
|
+
return Path(env_val)
|
|
59
|
+
return _default_trust_manifest_path()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _load_trust_manifest() -> dict:
|
|
63
|
+
"""Load the trust manifest from disk. Returns {} on any error."""
|
|
64
|
+
path = get_trust_manifest_path()
|
|
65
|
+
if not path.exists():
|
|
66
|
+
return {}
|
|
67
|
+
try:
|
|
68
|
+
text = path.read_text(encoding="utf-8")
|
|
69
|
+
data = json.loads(text)
|
|
70
|
+
if isinstance(data, dict):
|
|
71
|
+
return data
|
|
72
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
73
|
+
logger.warning("Failed to read plugin trust manifest at %s: %s", path, exc)
|
|
74
|
+
return {}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _save_trust_manifest(manifest: dict) -> None:
|
|
78
|
+
"""Persist the trust manifest to disk atomically with private perms."""
|
|
79
|
+
path = get_trust_manifest_path()
|
|
80
|
+
try:
|
|
81
|
+
ensure_private_dir(path.parent)
|
|
82
|
+
atomic_write_private_json(path, manifest)
|
|
83
|
+
except OSError as exc:
|
|
84
|
+
logger.warning("Failed to save plugin trust manifest: %s", exc)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def compute_plugin_hash(plugin_dir: Path) -> str:
|
|
88
|
+
"""Compute a SHA-256 hash over all .py files in a plugin directory.
|
|
89
|
+
|
|
90
|
+
Hashes register_callbacks.py, __init__.py, and every other .py file
|
|
91
|
+
found recursively under *plugin_dir*. Files are sorted by relative
|
|
92
|
+
path for deterministic ordering. The hash covers file *contents*, not
|
|
93
|
+
just file names.
|
|
94
|
+
|
|
95
|
+
Returns the hex digest string.
|
|
96
|
+
"""
|
|
97
|
+
h = hashlib.sha256()
|
|
98
|
+
py_files: list[Path] = []
|
|
99
|
+
try:
|
|
100
|
+
for child in sorted(plugin_dir.rglob("*.py")):
|
|
101
|
+
# Skip hidden files / dirs
|
|
102
|
+
if any(
|
|
103
|
+
part.startswith(".") for part in child.relative_to(plugin_dir).parts
|
|
104
|
+
):
|
|
105
|
+
continue
|
|
106
|
+
# Skip symlink escapes
|
|
107
|
+
try:
|
|
108
|
+
child.resolve().relative_to(plugin_dir.resolve())
|
|
109
|
+
except ValueError:
|
|
110
|
+
continue
|
|
111
|
+
py_files.append(child)
|
|
112
|
+
except OSError:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
for fpath in sorted(py_files):
|
|
116
|
+
rel = fpath.relative_to(plugin_dir)
|
|
117
|
+
h.update(str(rel).encode())
|
|
118
|
+
h.update(b"\0")
|
|
119
|
+
with contextlib.suppress(OSError):
|
|
120
|
+
h.update(fpath.read_bytes())
|
|
121
|
+
h.update(b"\0")
|
|
122
|
+
|
|
123
|
+
return h.hexdigest()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def is_plugin_trusted(plugin_name: str, content_hash: str) -> bool:
|
|
127
|
+
"""Check if a user plugin is trusted (manifest contains matching hash)."""
|
|
128
|
+
manifest = _load_trust_manifest()
|
|
129
|
+
entry = manifest.get(plugin_name)
|
|
130
|
+
if not isinstance(entry, dict):
|
|
131
|
+
return False
|
|
132
|
+
return entry.get("hash") == content_hash
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def record_plugin_trust(plugin_name: str, content_hash: str, plugin_dir: str) -> None:
|
|
136
|
+
"""Record trust for a user plugin in the manifest."""
|
|
137
|
+
manifest = _load_trust_manifest()
|
|
138
|
+
manifest[plugin_name] = {
|
|
139
|
+
"hash": content_hash,
|
|
140
|
+
"path": plugin_dir,
|
|
141
|
+
"trusted_at": _utc_now_iso(),
|
|
142
|
+
}
|
|
143
|
+
_save_trust_manifest(manifest)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def revoke_plugin_trust(plugin_name: str) -> None:
|
|
147
|
+
"""Remove a plugin from the trust manifest."""
|
|
148
|
+
manifest = _load_trust_manifest()
|
|
149
|
+
manifest.pop(plugin_name, None)
|
|
150
|
+
_save_trust_manifest(manifest)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _utc_now_iso() -> str:
|
|
154
|
+
"""Return current UTC time as ISO string (no heavy deps)."""
|
|
155
|
+
import datetime
|
|
156
|
+
|
|
157
|
+
return datetime.datetime.now(datetime.UTC).isoformat()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# Symlink / hidden-directory safety checks
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _is_symlink_escape(child: Path, parent: Path) -> bool:
|
|
166
|
+
"""Return True if *child* resolves outside *parent* (symlink escape)."""
|
|
167
|
+
try:
|
|
168
|
+
child.resolve().relative_to(parent.resolve())
|
|
169
|
+
return False
|
|
170
|
+
except ValueError:
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _should_skip_entry(item: Path, parent: Path) -> bool:
|
|
175
|
+
"""Return True if *item* should be skipped during plugin/skill discovery.
|
|
176
|
+
|
|
177
|
+
Skips:
|
|
178
|
+
- hidden dirs (name starts with '.' or '_')
|
|
179
|
+
- symlink escapes outside *parent*
|
|
180
|
+
"""
|
|
181
|
+
if item.name.startswith(".") or item.name.startswith("_"):
|
|
182
|
+
return True
|
|
183
|
+
if _is_symlink_escape(item, parent):
|
|
184
|
+
logger.warning(
|
|
185
|
+
"Skipping %s: resolves outside parent directory (symlink escape)", item
|
|
186
|
+
)
|
|
187
|
+
return True
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Built-in plugin loading
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _load_builtin_plugins(
|
|
197
|
+
plugins_dir: Path, failed_names: list[str] | None = None
|
|
198
|
+
) -> list[str]:
|
|
199
|
+
"""Load built-in plugins from the package plugins directory.
|
|
200
|
+
|
|
201
|
+
Returns list of successfully loaded plugin names.
|
|
202
|
+
"""
|
|
203
|
+
loaded = []
|
|
204
|
+
|
|
205
|
+
for item in plugins_dir.iterdir():
|
|
206
|
+
if item.is_dir() and not item.name.startswith("_"):
|
|
207
|
+
plugin_name = item.name
|
|
208
|
+
callbacks_file = item / "register_callbacks.py"
|
|
209
|
+
|
|
210
|
+
if callbacks_file.exists():
|
|
211
|
+
try:
|
|
212
|
+
module_name = f"code_muse.plugins.{plugin_name}.register_callbacks"
|
|
213
|
+
importlib.import_module(module_name)
|
|
214
|
+
loaded.append(plugin_name)
|
|
215
|
+
except ImportError as e:
|
|
216
|
+
logger.warning(
|
|
217
|
+
"Failed to import callbacks from built-in plugin %s: %s",
|
|
218
|
+
plugin_name,
|
|
219
|
+
e,
|
|
220
|
+
)
|
|
221
|
+
if failed_names is not None:
|
|
222
|
+
failed_names.append(plugin_name)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error(
|
|
225
|
+
"Unexpected error loading built-in plugin %s: %s",
|
|
226
|
+
plugin_name,
|
|
227
|
+
e,
|
|
228
|
+
)
|
|
229
|
+
if failed_names is not None:
|
|
230
|
+
failed_names.append(plugin_name)
|
|
231
|
+
|
|
232
|
+
return loaded
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# User plugin loading (trust-gated)
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _make_user_module_name(plugin_name: str, content_hash: str) -> str:
|
|
241
|
+
"""Build a unique, safe module name for a user plugin.
|
|
242
|
+
|
|
243
|
+
Format: ``code_muse_user_plugin_{safe_name}_{hash_prefix}``
|
|
244
|
+
The hash prefix (first 12 chars) avoids name collisions while keeping
|
|
245
|
+
the module name readable.
|
|
246
|
+
"""
|
|
247
|
+
safe = re.sub(r"[^a-zA-Z0-9_]", "_", plugin_name)
|
|
248
|
+
return f"code_muse_user_plugin_{safe}_{content_hash[:12]}"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _load_single_user_plugin(
|
|
252
|
+
plugin_dir: Path,
|
|
253
|
+
plugin_name: str,
|
|
254
|
+
user_plugins_dir: Path,
|
|
255
|
+
failed_names: list[str] | None = None,
|
|
256
|
+
) -> str | None:
|
|
257
|
+
"""Attempt to load a single user plugin directory.
|
|
258
|
+
|
|
259
|
+
Returns the plugin name on success, None on failure/skip.
|
|
260
|
+
"""
|
|
261
|
+
# Safety checks
|
|
262
|
+
if _should_skip_entry(plugin_dir, user_plugins_dir):
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
# Validate plugin name
|
|
266
|
+
if not _SAFE_NAME_RE.match(plugin_name):
|
|
267
|
+
logger.warning(
|
|
268
|
+
"Skipping user plugin '%s': name contains unsafe characters", plugin_name
|
|
269
|
+
)
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
callbacks_file = plugin_dir / "register_callbacks.py"
|
|
273
|
+
init_file = plugin_dir / "__init__.py"
|
|
274
|
+
|
|
275
|
+
# Pick the file to load (prefer register_callbacks.py)
|
|
276
|
+
load_file = None
|
|
277
|
+
if callbacks_file.exists():
|
|
278
|
+
if _is_symlink_escape(callbacks_file, plugin_dir):
|
|
279
|
+
logger.warning(
|
|
280
|
+
"Skipping user plugin '%s': register_callbacks.py is a symlink escape",
|
|
281
|
+
plugin_name,
|
|
282
|
+
)
|
|
283
|
+
return None
|
|
284
|
+
load_file = callbacks_file
|
|
285
|
+
elif init_file.exists():
|
|
286
|
+
if _is_symlink_escape(init_file, plugin_dir):
|
|
287
|
+
logger.warning(
|
|
288
|
+
"Skipping user plugin '%s': __init__.py is a symlink escape",
|
|
289
|
+
plugin_name,
|
|
290
|
+
)
|
|
291
|
+
return None
|
|
292
|
+
load_file = init_file
|
|
293
|
+
else:
|
|
294
|
+
# No entry point file
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
# Compute content hash for trust check
|
|
298
|
+
content_hash = compute_plugin_hash(plugin_dir)
|
|
299
|
+
|
|
300
|
+
# Fail closed: untrusted plugins are NOT imported
|
|
301
|
+
if not is_plugin_trusted(plugin_name, content_hash):
|
|
302
|
+
logger.warning(
|
|
303
|
+
"User plugin '%s' is not trusted (hash: %s…). "
|
|
304
|
+
"To trust it, run: /plugin trust %s "
|
|
305
|
+
"or set MUSE_TRUST_ALL_USER_PLUGINS=1 (dangerous).",
|
|
306
|
+
plugin_name,
|
|
307
|
+
content_hash[:12],
|
|
308
|
+
plugin_name,
|
|
309
|
+
)
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
# Build unique module name to avoid import shadowing
|
|
313
|
+
module_name = _make_user_module_name(plugin_name, content_hash)
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
spec = importlib.util.spec_from_file_location(module_name, load_file)
|
|
317
|
+
if spec is None or spec.loader is None:
|
|
318
|
+
logger.warning(
|
|
319
|
+
"Could not create module spec for user plugin: %s", plugin_name
|
|
320
|
+
)
|
|
321
|
+
if failed_names is not None:
|
|
322
|
+
failed_names.append(plugin_name)
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
module = importlib.util.module_from_spec(spec)
|
|
326
|
+
sys.modules[module_name] = module
|
|
327
|
+
spec.loader.exec_module(module)
|
|
328
|
+
return plugin_name
|
|
329
|
+
|
|
330
|
+
except ImportError as e:
|
|
331
|
+
logger.warning(
|
|
332
|
+
"Failed to import callbacks from user plugin %s: %s", plugin_name, e
|
|
333
|
+
)
|
|
334
|
+
if failed_names is not None:
|
|
335
|
+
failed_names.append(plugin_name)
|
|
336
|
+
except Exception as e:
|
|
337
|
+
logger.error(
|
|
338
|
+
"Unexpected error loading user plugin %s: %s", plugin_name, e, exc_info=True
|
|
339
|
+
)
|
|
340
|
+
if failed_names is not None:
|
|
341
|
+
failed_names.append(plugin_name)
|
|
342
|
+
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _load_user_plugins(
|
|
347
|
+
user_plugins_dir: Path, failed_names: list[str] | None = None
|
|
348
|
+
) -> list[str]:
|
|
349
|
+
"""Load user plugins from ~/.muse/plugins/.
|
|
350
|
+
|
|
351
|
+
Each plugin should be a directory containing a register_callbacks.py file.
|
|
352
|
+
Plugins are loaded via importlib with unique module names — no sys.path
|
|
353
|
+
insertion. Untrusted plugins are skipped with a clear warning.
|
|
354
|
+
|
|
355
|
+
Returns list of successfully loaded plugin names.
|
|
356
|
+
"""
|
|
357
|
+
loaded = []
|
|
358
|
+
|
|
359
|
+
if not user_plugins_dir.exists():
|
|
360
|
+
return loaded
|
|
361
|
+
|
|
362
|
+
if not user_plugins_dir.is_dir():
|
|
363
|
+
logger.warning("User plugins path is not a directory: %s", user_plugins_dir)
|
|
364
|
+
return loaded
|
|
365
|
+
|
|
366
|
+
# Allow trusting all user plugins via env var (for development / CI)
|
|
367
|
+
trust_all = os.environ.get("MUSE_TRUST_ALL_USER_PLUGINS", "") == "1"
|
|
368
|
+
|
|
369
|
+
for item in user_plugins_dir.iterdir():
|
|
370
|
+
if not item.is_dir():
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
plugin_name = item.name
|
|
374
|
+
|
|
375
|
+
# Safety checks
|
|
376
|
+
if _should_skip_entry(item, user_plugins_dir):
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
# Validate plugin name
|
|
380
|
+
if not _SAFE_NAME_RE.match(plugin_name):
|
|
381
|
+
logger.warning(
|
|
382
|
+
"Skipping user plugin '%s': name contains unsafe characters",
|
|
383
|
+
plugin_name,
|
|
384
|
+
)
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
# Dev override: auto-trust everything
|
|
388
|
+
if trust_all:
|
|
389
|
+
content_hash = compute_plugin_hash(item)
|
|
390
|
+
if not is_plugin_trusted(plugin_name, content_hash):
|
|
391
|
+
record_plugin_trust(plugin_name, content_hash, str(item))
|
|
392
|
+
|
|
393
|
+
result = _load_single_user_plugin(
|
|
394
|
+
item, plugin_name, user_plugins_dir, failed_names
|
|
395
|
+
)
|
|
396
|
+
if result is not None:
|
|
397
|
+
loaded.append(result)
|
|
398
|
+
|
|
399
|
+
return loaded
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
# Public API
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def load_plugin_callbacks() -> dict[str, list[str]]:
|
|
408
|
+
"""Dynamically load register_callbacks.py from all plugin sources.
|
|
409
|
+
|
|
410
|
+
Loads plugins from:
|
|
411
|
+
1. Built-in plugins in the code_muse/plugins/ directory
|
|
412
|
+
2. User plugins in ~/.muse/plugins/
|
|
413
|
+
|
|
414
|
+
User plugins require trust (content-hash match in manifest).
|
|
415
|
+
No sys.path manipulation is performed.
|
|
416
|
+
|
|
417
|
+
Returns dict with 'builtin' and 'user' keys containing lists of loaded
|
|
418
|
+
plugin names.
|
|
419
|
+
|
|
420
|
+
NOTE: This function is idempotent - calling it multiple times will only
|
|
421
|
+
load plugins once. Subsequent calls return empty lists.
|
|
422
|
+
"""
|
|
423
|
+
global _PLUGINS_LOADED
|
|
424
|
+
|
|
425
|
+
# Prevent duplicate loading - plugins register callbacks at import time,
|
|
426
|
+
# so re-importing would cause duplicate registrations
|
|
427
|
+
if _PLUGINS_LOADED:
|
|
428
|
+
logger.debug("Plugins already loaded, skipping duplicate load")
|
|
429
|
+
return {"builtin": [], "user": []}
|
|
430
|
+
|
|
431
|
+
plugins_dir = Path(__file__).parent
|
|
432
|
+
builtin_failed: list[str] = []
|
|
433
|
+
user_failed: list[str] = []
|
|
434
|
+
|
|
435
|
+
result = {
|
|
436
|
+
"builtin": _load_builtin_plugins(plugins_dir, builtin_failed),
|
|
437
|
+
"user": _load_user_plugins(USER_PLUGINS_DIR, user_failed),
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
total_loaded = len(result["builtin"]) + len(result["user"])
|
|
441
|
+
total_failed = len(builtin_failed) + len(user_failed)
|
|
442
|
+
if total_failed:
|
|
443
|
+
all_failed = builtin_failed + user_failed
|
|
444
|
+
names_str = ", ".join(all_failed)
|
|
445
|
+
logger.warning("Plugin load failures: %s", names_str)
|
|
446
|
+
from code_muse.messaging import emit_warning
|
|
447
|
+
|
|
448
|
+
emit_warning(
|
|
449
|
+
f"⚠️ {total_failed}/{total_loaded + total_failed} plugins failed to load"
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
_PLUGINS_LOADED = True
|
|
453
|
+
logger.debug(
|
|
454
|
+
"Loaded plugins: builtin=%s, user=%s", result["builtin"], result["user"]
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
return result
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def get_user_plugins_dir() -> Path:
|
|
461
|
+
"""Return the path to the user plugins directory."""
|
|
462
|
+
return USER_PLUGINS_DIR
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def ensure_user_plugins_dir() -> Path:
|
|
466
|
+
"""Create the user plugins directory if it doesn't exist.
|
|
467
|
+
|
|
468
|
+
Returns the path to the directory.
|
|
469
|
+
"""
|
|
470
|
+
ensure_private_dir(USER_PLUGINS_DIR)
|
|
471
|
+
return USER_PLUGINS_DIR
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Agent Skills plugin - dynamic skill loading and discovery.
|
|
2
|
+
|
|
3
|
+
This plugin enables code_muse to discover, load, and use custom skills
|
|
4
|
+
defined in SKILL.md files. Skills can be placed in user-specific or
|
|
5
|
+
project-specific directories for easy sharing and organization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .discovery import (
|
|
9
|
+
MAX_SKILL_MD_BYTES,
|
|
10
|
+
SKILL_CONTEXT_CAP,
|
|
11
|
+
SkillInfo,
|
|
12
|
+
cap_skill_content,
|
|
13
|
+
)
|
|
14
|
+
from .metadata import (
|
|
15
|
+
SkillMetadata,
|
|
16
|
+
get_skill_resources,
|
|
17
|
+
load_full_skill_content,
|
|
18
|
+
parse_skill_metadata,
|
|
19
|
+
parse_yaml_frontmatter,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"SkillInfo",
|
|
24
|
+
"SkillMetadata",
|
|
25
|
+
"MAX_SKILL_MD_BYTES",
|
|
26
|
+
"SKILL_CONTEXT_CAP",
|
|
27
|
+
"cap_skill_content",
|
|
28
|
+
"parse_yaml_frontmatter",
|
|
29
|
+
"parse_skill_metadata",
|
|
30
|
+
"load_full_skill_content",
|
|
31
|
+
"get_skill_resources",
|
|
32
|
+
]
|