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,860 @@
|
|
|
1
|
+
# file_operations.py
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections import deque
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, conint
|
|
12
|
+
from pydantic_ai import RunContext
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Module-level helper functions (exposed for unit tests _and_ used as tools)
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
from code_muse.messaging import ( # New structured messaging types
|
|
18
|
+
FileContentMessage,
|
|
19
|
+
FileEntry,
|
|
20
|
+
FileListingMessage,
|
|
21
|
+
GrepMatch,
|
|
22
|
+
GrepResultMessage,
|
|
23
|
+
get_message_bus,
|
|
24
|
+
)
|
|
25
|
+
from code_muse.tools.path_policy import Operation, check_path_allowed
|
|
26
|
+
|
|
27
|
+
# Caps for listing / reading to avoid unbounded memory and model-context blowup
|
|
28
|
+
MAX_LIST_FILES_UI_ENTRIES = 5_000
|
|
29
|
+
MAX_LIST_FILES_LLM_ENTRIES = 1_000
|
|
30
|
+
MAX_READ_FILE_BYTES = 128_000
|
|
31
|
+
MAX_GREP_MATCHES = 50
|
|
32
|
+
MAX_GREP_LINE_LENGTH = 512
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Pydantic models for tool return types
|
|
36
|
+
class ListedFile(BaseModel):
|
|
37
|
+
path: str | None
|
|
38
|
+
type: str | None
|
|
39
|
+
size: int = 0
|
|
40
|
+
full_path: str | None
|
|
41
|
+
depth: int | None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ListFileOutput(BaseModel):
|
|
45
|
+
content: str
|
|
46
|
+
error: str | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ReadFileOutput(BaseModel):
|
|
50
|
+
content: str | None
|
|
51
|
+
num_tokens: conint(lt=10000)
|
|
52
|
+
error: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MatchInfo(BaseModel):
|
|
56
|
+
file_path: str | None
|
|
57
|
+
line_number: int | None
|
|
58
|
+
line_content: str | None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class GrepOutput(BaseModel):
|
|
62
|
+
matches: list[MatchInfo]
|
|
63
|
+
error: str | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_likely_home_directory(directory):
|
|
67
|
+
"""Detect if directory is likely a user's home directory or common home subdirectory"""
|
|
68
|
+
abs_dir = Path(directory).resolve()
|
|
69
|
+
home_dir = Path.home()
|
|
70
|
+
|
|
71
|
+
# Exact home directory match
|
|
72
|
+
if abs_dir == home_dir:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
# Check for common home directory subdirectories
|
|
76
|
+
common_home_subdirs = {
|
|
77
|
+
"Documents",
|
|
78
|
+
"Desktop",
|
|
79
|
+
"Downloads",
|
|
80
|
+
"Pictures",
|
|
81
|
+
"Music",
|
|
82
|
+
"Videos",
|
|
83
|
+
"Movies",
|
|
84
|
+
"Public",
|
|
85
|
+
"Library",
|
|
86
|
+
"Applications", # Cover macOS/Linux
|
|
87
|
+
}
|
|
88
|
+
return bool(abs_dir.name in common_home_subdirs and abs_dir.parent == home_dir)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_project_directory(directory):
|
|
92
|
+
"""Quick heuristic to detect if this looks like a project directory"""
|
|
93
|
+
project_indicators = {
|
|
94
|
+
"package.json",
|
|
95
|
+
"pyproject.toml",
|
|
96
|
+
"Cargo.toml",
|
|
97
|
+
"pom.xml",
|
|
98
|
+
"build.gradle",
|
|
99
|
+
"CMakeLists.txt",
|
|
100
|
+
".git",
|
|
101
|
+
"requirements.txt",
|
|
102
|
+
"composer.json",
|
|
103
|
+
"Gemfile",
|
|
104
|
+
"go.mod",
|
|
105
|
+
"Makefile",
|
|
106
|
+
"setup.py",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
contents = os.listdir(directory)
|
|
111
|
+
return any(indicator in contents for indicator in project_indicators)
|
|
112
|
+
except OSError:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def would_match_directory(pattern: str, directory: str) -> bool:
|
|
117
|
+
"""Check if a glob pattern would match the given directory path.
|
|
118
|
+
|
|
119
|
+
This is used to avoid adding ignore patterns that would inadvertently
|
|
120
|
+
exclude the directory we're actually trying to search in.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
pattern: A glob pattern like '**/tmp/**' or 'node_modules'
|
|
124
|
+
directory: The directory path to check against
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if the pattern would match the directory, False otherwise
|
|
128
|
+
"""
|
|
129
|
+
import fnmatch
|
|
130
|
+
|
|
131
|
+
# Normalize the directory path
|
|
132
|
+
abs_dir = Path(directory).resolve()
|
|
133
|
+
dir_name = abs_dir.name
|
|
134
|
+
|
|
135
|
+
# Strip leading/trailing wildcards and slashes for simpler matching
|
|
136
|
+
clean_pattern = pattern.strip("*").strip("/")
|
|
137
|
+
|
|
138
|
+
# Check if the directory name matches the pattern
|
|
139
|
+
if fnmatch.fnmatch(dir_name, clean_pattern):
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
# Check if the full path contains the pattern
|
|
143
|
+
if fnmatch.fnmatch(abs_dir, pattern):
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
# Check if any part of the path matches
|
|
147
|
+
path_parts = str(abs_dir).split(os.sep)
|
|
148
|
+
return any(fnmatch.fnmatch(part, clean_pattern) for part in path_parts)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _list_files(
|
|
152
|
+
context: RunContext, directory: str = ".", recursive: bool = True
|
|
153
|
+
) -> ListFileOutput:
|
|
154
|
+
import sys
|
|
155
|
+
|
|
156
|
+
directory = Path(directory).expanduser().resolve()
|
|
157
|
+
|
|
158
|
+
# Enforce workspace / sensitive directory policy before listing
|
|
159
|
+
policy = check_path_allowed(str(directory), Operation.LIST)
|
|
160
|
+
if not policy.allowed:
|
|
161
|
+
error_msg = policy.reason or "Directory listing blocked by path policy."
|
|
162
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
163
|
+
|
|
164
|
+
# Plain text output for LLM consumption
|
|
165
|
+
output_lines = []
|
|
166
|
+
output_lines.append(f"DIRECTORY LISTING: {directory} (recursive={recursive})")
|
|
167
|
+
|
|
168
|
+
if not directory.exists():
|
|
169
|
+
error_msg = f"Error: Directory '{directory}' does not exist"
|
|
170
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
171
|
+
if not directory.is_dir():
|
|
172
|
+
error_msg = f"Error: '{directory}' is not a directory"
|
|
173
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
174
|
+
|
|
175
|
+
results = []
|
|
176
|
+
|
|
177
|
+
# Smart home directory detection - auto-limit recursion for performance
|
|
178
|
+
# But allow recursion in tests (when context=None) or when explicitly requested
|
|
179
|
+
if context is not None and is_likely_home_directory(str(directory)) and recursive:
|
|
180
|
+
if not is_project_directory(str(directory)):
|
|
181
|
+
output_lines.append(
|
|
182
|
+
"Warning: Detected home directory - limiting to non-recursive listing for performance"
|
|
183
|
+
)
|
|
184
|
+
recursive = False
|
|
185
|
+
|
|
186
|
+
# Create a temporary ignore file with our ignore patterns
|
|
187
|
+
ignore_file = None
|
|
188
|
+
try:
|
|
189
|
+
# Find ripgrep executable - first check system PATH, then virtual environment
|
|
190
|
+
rg_path = shutil.which("rg")
|
|
191
|
+
if not rg_path:
|
|
192
|
+
# Try to find it in the virtual environment
|
|
193
|
+
# Use sys.executable to determine the Python environment path
|
|
194
|
+
python_dir = Path(sys.executable).parent
|
|
195
|
+
# python_dir is already bin/ (Unix) or Scripts/ (Windows)
|
|
196
|
+
for name in ["rg", "rg.exe"]:
|
|
197
|
+
candidate = python_dir / name
|
|
198
|
+
if candidate.exists():
|
|
199
|
+
rg_path = str(candidate)
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
if not rg_path and recursive:
|
|
203
|
+
# Only need ripgrep for recursive listings
|
|
204
|
+
error_msg = "Error: ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
205
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
206
|
+
|
|
207
|
+
# Only use ripgrep for recursive listings
|
|
208
|
+
if recursive:
|
|
209
|
+
# Build command for ripgrep --files
|
|
210
|
+
cmd = [rg_path, "--files"]
|
|
211
|
+
|
|
212
|
+
# Add ignore patterns to the command via a temporary file
|
|
213
|
+
from code_muse.tools.common import (
|
|
214
|
+
DIR_IGNORE_PATTERNS,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
with tempfile.NamedTemporaryFile(
|
|
218
|
+
mode="w", delete=False, suffix=".ignore"
|
|
219
|
+
) as f:
|
|
220
|
+
ignore_file = f.name
|
|
221
|
+
for pattern in DIR_IGNORE_PATTERNS:
|
|
222
|
+
# Skip patterns that would match the search directory itself
|
|
223
|
+
# For example, if searching in /tmp/test-dir, skip **/tmp/**
|
|
224
|
+
if would_match_directory(pattern, directory):
|
|
225
|
+
continue
|
|
226
|
+
f.write(f"{pattern}\n")
|
|
227
|
+
|
|
228
|
+
cmd.extend(["--ignore-file", ignore_file])
|
|
229
|
+
cmd.append(str(directory))
|
|
230
|
+
|
|
231
|
+
# Run ripgrep to get file listing
|
|
232
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
233
|
+
|
|
234
|
+
# Process the output lines
|
|
235
|
+
files = result.stdout.strip().split("\n") if result.stdout.strip() else []
|
|
236
|
+
|
|
237
|
+
# Create ListedFile objects with metadata
|
|
238
|
+
for full_path in files:
|
|
239
|
+
if not full_path: # Skip empty lines
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
fp = Path(full_path)
|
|
243
|
+
# Skip if file doesn't exist (though it should)
|
|
244
|
+
if not fp.exists():
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
# Extract relative path from the full path
|
|
248
|
+
if full_path.startswith(str(directory)):
|
|
249
|
+
file_path = full_path[len(str(directory)) :].lstrip(os.sep)
|
|
250
|
+
else:
|
|
251
|
+
file_path = full_path
|
|
252
|
+
|
|
253
|
+
# Check if path is a file or directory
|
|
254
|
+
if fp.is_file():
|
|
255
|
+
entry_type = "file"
|
|
256
|
+
size = fp.stat().st_size
|
|
257
|
+
elif fp.is_dir():
|
|
258
|
+
entry_type = "directory"
|
|
259
|
+
size = 0
|
|
260
|
+
else:
|
|
261
|
+
# Skip if it's neither a file nor directory
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
# Calculate depth based on the relative path
|
|
266
|
+
depth = file_path.count(os.sep)
|
|
267
|
+
|
|
268
|
+
# Add directory entries if needed for files
|
|
269
|
+
if entry_type == "file":
|
|
270
|
+
p = Path(file_path).parent
|
|
271
|
+
dir_path = str(p) if p != Path(".") else ""
|
|
272
|
+
if dir_path:
|
|
273
|
+
# Add directory path components if they don't exist
|
|
274
|
+
path_parts = dir_path.split(os.sep)
|
|
275
|
+
for i in range(len(path_parts)):
|
|
276
|
+
partial_path = os.sep.join(path_parts[: i + 1])
|
|
277
|
+
# Check if we already added this directory
|
|
278
|
+
if not any(
|
|
279
|
+
f.path == partial_path and f.type == "directory"
|
|
280
|
+
for f in results
|
|
281
|
+
):
|
|
282
|
+
results.append(
|
|
283
|
+
ListedFile(
|
|
284
|
+
path=partial_path,
|
|
285
|
+
type="directory",
|
|
286
|
+
size=0,
|
|
287
|
+
full_path=str(directory / partial_path),
|
|
288
|
+
depth=partial_path.count(os.sep),
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Add the entry (file or directory)
|
|
293
|
+
results.append(
|
|
294
|
+
ListedFile(
|
|
295
|
+
path=file_path,
|
|
296
|
+
type=entry_type,
|
|
297
|
+
size=size,
|
|
298
|
+
full_path=full_path,
|
|
299
|
+
depth=depth,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
except OSError:
|
|
303
|
+
# Skip files we can't access
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
# In non-recursive mode, we also need to explicitly list immediate entries
|
|
307
|
+
# ripgrep's --files option only returns files; we add directories and files ourselves
|
|
308
|
+
if not recursive:
|
|
309
|
+
try:
|
|
310
|
+
entries = os.listdir(directory)
|
|
311
|
+
for entry in sorted(entries):
|
|
312
|
+
full_entry_path = directory / entry
|
|
313
|
+
if not full_entry_path.exists():
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
if full_entry_path.is_dir():
|
|
317
|
+
# In non-recursive mode, only skip obviously system/hidden directories
|
|
318
|
+
# Don't use the full should_ignore_dir_path which is too aggressive
|
|
319
|
+
if entry.startswith("."):
|
|
320
|
+
continue
|
|
321
|
+
results.append(
|
|
322
|
+
ListedFile(
|
|
323
|
+
path=entry,
|
|
324
|
+
type="directory",
|
|
325
|
+
size=0,
|
|
326
|
+
full_path=str(full_entry_path),
|
|
327
|
+
depth=0,
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
elif full_entry_path.is_file():
|
|
331
|
+
# Include top-level files (including binaries)
|
|
332
|
+
try:
|
|
333
|
+
size = full_entry_path.stat().st_size
|
|
334
|
+
except OSError:
|
|
335
|
+
size = 0
|
|
336
|
+
results.append(
|
|
337
|
+
ListedFile(
|
|
338
|
+
path=entry,
|
|
339
|
+
type="file",
|
|
340
|
+
size=size,
|
|
341
|
+
full_path=str(full_entry_path),
|
|
342
|
+
depth=0,
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
except OSError:
|
|
346
|
+
# Skip entries we can't access
|
|
347
|
+
pass
|
|
348
|
+
except subprocess.TimeoutExpired:
|
|
349
|
+
error_msg = "Error: List files command timed out after 30 seconds"
|
|
350
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
351
|
+
except Exception as e:
|
|
352
|
+
error_msg = f"Error: Error during list files operation: {e}"
|
|
353
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
354
|
+
finally:
|
|
355
|
+
# Clean up the temporary ignore file
|
|
356
|
+
if ignore_file and Path(ignore_file).exists():
|
|
357
|
+
os.unlink(ignore_file)
|
|
358
|
+
|
|
359
|
+
def format_size(size_bytes):
|
|
360
|
+
if size_bytes < 1024:
|
|
361
|
+
return f"{size_bytes} B"
|
|
362
|
+
elif size_bytes < 1024 * 1024:
|
|
363
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
364
|
+
elif size_bytes < 1024 * 1024 * 1024:
|
|
365
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
366
|
+
else:
|
|
367
|
+
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
|
368
|
+
|
|
369
|
+
def get_file_icon(file_path):
|
|
370
|
+
ext = Path(file_path).suffix.lower()
|
|
371
|
+
if ext in [".py", ".pyw"]:
|
|
372
|
+
return "\U0001f40d"
|
|
373
|
+
elif ext in [".js", ".jsx", ".ts", ".tsx"]:
|
|
374
|
+
return "\U0001f4dc"
|
|
375
|
+
elif ext in [".html", ".htm", ".xml"]:
|
|
376
|
+
return "\U0001f310"
|
|
377
|
+
elif ext in [".css", ".scss", ".sass"]:
|
|
378
|
+
return "\U0001f3a8"
|
|
379
|
+
elif ext in [".md", ".markdown", ".rst"]:
|
|
380
|
+
return "\U0001f4dd"
|
|
381
|
+
elif ext in [".json", ".yaml", ".yml", ".toml"]:
|
|
382
|
+
return "\u2699\ufe0f"
|
|
383
|
+
elif ext in [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp"]:
|
|
384
|
+
return "\U0001f5bc\ufe0f"
|
|
385
|
+
elif ext in [".mp3", ".wav", ".ogg", ".flac"]:
|
|
386
|
+
return "\U0001f3b5"
|
|
387
|
+
elif ext in [".mp4", ".avi", ".mov", ".webm"]:
|
|
388
|
+
return "\U0001f3ac"
|
|
389
|
+
elif ext in [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"]:
|
|
390
|
+
return "\U0001f4c4"
|
|
391
|
+
elif ext in [".zip", ".tar", ".gz", ".rar", ".7z"]:
|
|
392
|
+
return "\U0001f4e6"
|
|
393
|
+
elif ext in [".exe", ".dll", ".so", ".dylib"]:
|
|
394
|
+
return "\u26a1"
|
|
395
|
+
else:
|
|
396
|
+
return "\U0001f4c4"
|
|
397
|
+
|
|
398
|
+
# Count items in results
|
|
399
|
+
dir_count = sum(1 for item in results if item.type == "directory")
|
|
400
|
+
file_count = sum(1 for item in results if item.type == "file")
|
|
401
|
+
total_size = sum(item.size for item in results if item.type == "file")
|
|
402
|
+
|
|
403
|
+
# Build structured FileEntry objects for the UI
|
|
404
|
+
file_entries = []
|
|
405
|
+
|
|
406
|
+
def _sort_key(item):
|
|
407
|
+
"""Sort by path components to keep children grouped under parents.
|
|
408
|
+
|
|
409
|
+
Splitting on os.sep ensures 'src/foo' always sorts right after 'src'
|
|
410
|
+
rather than letting 'src-tauri' (with '-' < '/') slip in between.
|
|
411
|
+
Directories sort before files at the same level.
|
|
412
|
+
"""
|
|
413
|
+
parts = item.path.split(os.sep)
|
|
414
|
+
return (parts, item.type != "directory")
|
|
415
|
+
|
|
416
|
+
sorted_results = sorted(results, key=_sort_key)
|
|
417
|
+
|
|
418
|
+
for item in sorted_results:
|
|
419
|
+
if item.type == "directory" and not item.path:
|
|
420
|
+
continue
|
|
421
|
+
file_entries.append(
|
|
422
|
+
FileEntry(
|
|
423
|
+
path=item.path,
|
|
424
|
+
type="dir" if item.type == "directory" else "file",
|
|
425
|
+
size=item.size,
|
|
426
|
+
depth=item.depth or 0,
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Cap UI structured entries
|
|
431
|
+
ui_truncated = False
|
|
432
|
+
if len(file_entries) > MAX_LIST_FILES_UI_ENTRIES:
|
|
433
|
+
file_entries = file_entries[:MAX_LIST_FILES_UI_ENTRIES]
|
|
434
|
+
ui_truncated = True
|
|
435
|
+
|
|
436
|
+
# Emit structured message for the UI
|
|
437
|
+
file_listing_msg = FileListingMessage(
|
|
438
|
+
directory=str(directory),
|
|
439
|
+
files=file_entries,
|
|
440
|
+
recursive=recursive,
|
|
441
|
+
total_size=total_size,
|
|
442
|
+
dir_count=dir_count,
|
|
443
|
+
file_count=file_count,
|
|
444
|
+
)
|
|
445
|
+
get_message_bus().emit(file_listing_msg)
|
|
446
|
+
|
|
447
|
+
# Build plain text output for LLM consumption
|
|
448
|
+
llm_lines: list[str] = []
|
|
449
|
+
for item in sorted_results:
|
|
450
|
+
if item.type == "directory" and not item.path:
|
|
451
|
+
continue
|
|
452
|
+
name = Path(item.path).name or item.path
|
|
453
|
+
indent = " " * (item.depth or 0)
|
|
454
|
+
if item.type == "directory":
|
|
455
|
+
llm_lines.append(f"{indent}{name}/")
|
|
456
|
+
else:
|
|
457
|
+
size_str = format_size(item.size)
|
|
458
|
+
llm_lines.append(f"{indent}{name} ({size_str})")
|
|
459
|
+
|
|
460
|
+
llm_truncated = False
|
|
461
|
+
if len(llm_lines) > MAX_LIST_FILES_LLM_ENTRIES:
|
|
462
|
+
llm_lines = llm_lines[:MAX_LIST_FILES_LLM_ENTRIES]
|
|
463
|
+
llm_truncated = True
|
|
464
|
+
|
|
465
|
+
output_lines.extend(llm_lines)
|
|
466
|
+
|
|
467
|
+
# Add summary
|
|
468
|
+
output_lines.append(
|
|
469
|
+
f"\nSummary: {dir_count} directories, {file_count} files ({format_size(total_size)} total)"
|
|
470
|
+
)
|
|
471
|
+
if ui_truncated or llm_truncated:
|
|
472
|
+
output_lines.append(
|
|
473
|
+
f"\n[Truncated: shown {MAX_LIST_FILES_LLM_ENTRIES} of {len(sorted_results)} entries]"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return ListFileOutput(content="\n".join(output_lines))
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _read_file(
|
|
480
|
+
context: RunContext,
|
|
481
|
+
file_path: str,
|
|
482
|
+
start_line: int | None = None,
|
|
483
|
+
num_lines: int | None = None,
|
|
484
|
+
) -> ReadFileOutput:
|
|
485
|
+
file_path = Path(file_path).expanduser().resolve()
|
|
486
|
+
|
|
487
|
+
# Enforce path policy before reading
|
|
488
|
+
policy = check_path_allowed(str(file_path), Operation.READ)
|
|
489
|
+
if not policy.allowed:
|
|
490
|
+
error_msg = policy.reason or "File read blocked by path policy."
|
|
491
|
+
return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
|
|
492
|
+
|
|
493
|
+
if not file_path.exists():
|
|
494
|
+
error_msg = f"File {file_path} does not exist"
|
|
495
|
+
return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
|
|
496
|
+
if not file_path.is_file():
|
|
497
|
+
error_msg = f"{file_path} is not a file"
|
|
498
|
+
return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
|
|
499
|
+
|
|
500
|
+
# Huge-file gate: reject full reads of files larger than cap unless chunked
|
|
501
|
+
try:
|
|
502
|
+
file_size = file_path.stat().st_size
|
|
503
|
+
except OSError:
|
|
504
|
+
file_size = 0
|
|
505
|
+
if start_line is None and num_lines is None and file_size > MAX_READ_FILE_BYTES:
|
|
506
|
+
return ReadFileOutput(
|
|
507
|
+
content=None,
|
|
508
|
+
error=(
|
|
509
|
+
f"File is too large ({file_size} bytes > {MAX_READ_FILE_BYTES} bytes). "
|
|
510
|
+
"Please read this file in chunks using start_line and num_lines."
|
|
511
|
+
),
|
|
512
|
+
num_tokens=0,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
# Use errors="surrogateescape" to handle files with invalid UTF-8 sequences
|
|
517
|
+
# This is common on Windows when files contain emojis or were created by
|
|
518
|
+
# applications that don't properly encode Unicode
|
|
519
|
+
with open(file_path, encoding="utf-8", errors="surrogateescape") as f:
|
|
520
|
+
if start_line is not None and start_line < 1:
|
|
521
|
+
error_msg = "start_line must be >= 1 (1-based indexing)"
|
|
522
|
+
return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
|
|
523
|
+
if num_lines is not None and num_lines < 1:
|
|
524
|
+
error_msg = "num_lines must be >= 1"
|
|
525
|
+
return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
|
|
526
|
+
if start_line is not None and num_lines is not None:
|
|
527
|
+
# Read only the specified lines efficiently using itertools.islice
|
|
528
|
+
# to avoid loading the entire file into memory
|
|
529
|
+
import itertools
|
|
530
|
+
|
|
531
|
+
start_idx = start_line - 1
|
|
532
|
+
selected_lines = list(
|
|
533
|
+
itertools.islice(f, start_idx, start_idx + num_lines)
|
|
534
|
+
)
|
|
535
|
+
content = "".join(selected_lines)
|
|
536
|
+
else:
|
|
537
|
+
# Read the entire file
|
|
538
|
+
content = f.read()
|
|
539
|
+
|
|
540
|
+
# Sanitize the content to remove any surrogate characters that could
|
|
541
|
+
# cause issues when the content is later serialized or displayed
|
|
542
|
+
# This re-encodes with surrogatepass then decodes with replace to
|
|
543
|
+
# convert lone surrogates to replacement characters
|
|
544
|
+
try:
|
|
545
|
+
content = content.encode("utf-8", errors="surrogatepass").decode(
|
|
546
|
+
"utf-8", errors="replace"
|
|
547
|
+
)
|
|
548
|
+
except UnicodeEncodeError, UnicodeDecodeError:
|
|
549
|
+
# If that fails, do a more aggressive cleanup
|
|
550
|
+
content = "".join(
|
|
551
|
+
char if ord(char) < 0xD800 or ord(char) > 0xDFFF else "\ufffd"
|
|
552
|
+
for char in content
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Simple approximation: ~4 characters per token
|
|
556
|
+
num_tokens = len(content) // 4
|
|
557
|
+
if num_tokens > 10000:
|
|
558
|
+
return ReadFileOutput(
|
|
559
|
+
content=None,
|
|
560
|
+
error="The file is massive, greater than 10,000 tokens which is dangerous to read entirely. Please read this file in chunks.",
|
|
561
|
+
num_tokens=0,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Count total lines for the message
|
|
565
|
+
total_lines = content.count("\n") + (
|
|
566
|
+
1 if content and not content.endswith("\n") else 0
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Emit structured message for the UI
|
|
570
|
+
# Only include start_line/num_lines if they are valid positive integers
|
|
571
|
+
emit_start_line = (
|
|
572
|
+
start_line if start_line is not None and start_line >= 1 else None
|
|
573
|
+
)
|
|
574
|
+
emit_num_lines = (
|
|
575
|
+
num_lines if num_lines is not None and num_lines >= 1 else None
|
|
576
|
+
)
|
|
577
|
+
file_content_msg = FileContentMessage(
|
|
578
|
+
path=str(file_path),
|
|
579
|
+
content=content,
|
|
580
|
+
start_line=emit_start_line,
|
|
581
|
+
num_lines=emit_num_lines,
|
|
582
|
+
total_lines=total_lines,
|
|
583
|
+
num_tokens=num_tokens,
|
|
584
|
+
)
|
|
585
|
+
get_message_bus().emit(file_content_msg)
|
|
586
|
+
|
|
587
|
+
return ReadFileOutput(content=content, num_tokens=num_tokens)
|
|
588
|
+
except FileNotFoundError:
|
|
589
|
+
error_msg = "FILE NOT FOUND"
|
|
590
|
+
return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
|
|
591
|
+
except PermissionError:
|
|
592
|
+
error_msg = "PERMISSION DENIED"
|
|
593
|
+
return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
|
|
594
|
+
except Exception as e:
|
|
595
|
+
message = f"An error occurred trying to read the file: {e}"
|
|
596
|
+
return ReadFileOutput(content=message, num_tokens=0, error=message)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _sanitize_string(text: str) -> str:
|
|
600
|
+
"""Sanitize a string to remove invalid Unicode surrogates.
|
|
601
|
+
|
|
602
|
+
This handles encoding issues common on Windows with copy-paste operations.
|
|
603
|
+
"""
|
|
604
|
+
if not text:
|
|
605
|
+
return text
|
|
606
|
+
try:
|
|
607
|
+
# Try encoding - if it works, string is clean
|
|
608
|
+
text.encode("utf-8")
|
|
609
|
+
return text
|
|
610
|
+
except UnicodeEncodeError:
|
|
611
|
+
pass
|
|
612
|
+
|
|
613
|
+
try:
|
|
614
|
+
# Encode allowing surrogates, then decode replacing them
|
|
615
|
+
return text.encode("utf-8", errors="surrogatepass").decode(
|
|
616
|
+
"utf-8", errors="replace"
|
|
617
|
+
)
|
|
618
|
+
except UnicodeEncodeError, UnicodeDecodeError:
|
|
619
|
+
# Last resort: filter out surrogate characters
|
|
620
|
+
return "".join(
|
|
621
|
+
char if ord(char) < 0xD800 or ord(char) > 0xDFFF else "\ufffd"
|
|
622
|
+
for char in text
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _grep(context: RunContext, search_string: str, directory: str = ".") -> GrepOutput:
|
|
627
|
+
import json
|
|
628
|
+
import os
|
|
629
|
+
import shutil
|
|
630
|
+
import subprocess
|
|
631
|
+
import sys
|
|
632
|
+
|
|
633
|
+
# Sanitize search string to handle any surrogates from copy-paste
|
|
634
|
+
search_string = _sanitize_string(search_string)
|
|
635
|
+
|
|
636
|
+
directory = Path(directory).expanduser().resolve()
|
|
637
|
+
|
|
638
|
+
# Enforce workspace / sensitive directory policy before searching
|
|
639
|
+
policy = check_path_allowed(str(directory), Operation.SEARCH)
|
|
640
|
+
if not policy.allowed:
|
|
641
|
+
error_message = policy.reason or "Search blocked by path policy."
|
|
642
|
+
return GrepOutput(matches=[], error=error_message)
|
|
643
|
+
|
|
644
|
+
matches: deque[MatchInfo] = deque(maxlen=MAX_GREP_MATCHES)
|
|
645
|
+
error_message: str | None = None
|
|
646
|
+
|
|
647
|
+
# Create a temporary ignore file with our ignore patterns
|
|
648
|
+
ignore_file = None
|
|
649
|
+
try:
|
|
650
|
+
# Find ripgrep executable - first check system PATH, then virtual environment
|
|
651
|
+
rg_path = shutil.which("rg")
|
|
652
|
+
if not rg_path:
|
|
653
|
+
python_dir = Path(sys.executable).parent
|
|
654
|
+
for name in ["rg", "rg.exe"]:
|
|
655
|
+
candidate = python_dir / name
|
|
656
|
+
if candidate.exists():
|
|
657
|
+
rg_path = str(candidate)
|
|
658
|
+
break
|
|
659
|
+
|
|
660
|
+
if not rg_path:
|
|
661
|
+
error_message = (
|
|
662
|
+
"ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
663
|
+
)
|
|
664
|
+
return GrepOutput(matches=[], error=error_message)
|
|
665
|
+
|
|
666
|
+
# Prevent option injection: treat search_string as data/regex only.
|
|
667
|
+
# Use '--' before the pattern so ripgrep stops parsing flags.
|
|
668
|
+
cmd = [
|
|
669
|
+
rg_path,
|
|
670
|
+
"--json",
|
|
671
|
+
"--max-count",
|
|
672
|
+
str(MAX_GREP_MATCHES),
|
|
673
|
+
"--max-filesize",
|
|
674
|
+
"5M",
|
|
675
|
+
"--type=all",
|
|
676
|
+
"--",
|
|
677
|
+
search_string,
|
|
678
|
+
directory,
|
|
679
|
+
]
|
|
680
|
+
|
|
681
|
+
# Add ignore patterns to the command via a temporary file
|
|
682
|
+
from code_muse.tools.common import DIR_IGNORE_PATTERNS
|
|
683
|
+
|
|
684
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".ignore") as f:
|
|
685
|
+
ignore_file = f.name
|
|
686
|
+
for pattern in DIR_IGNORE_PATTERNS:
|
|
687
|
+
# Skip patterns that would match the search directory itself
|
|
688
|
+
if would_match_directory(pattern, directory):
|
|
689
|
+
continue
|
|
690
|
+
f.write(f"{pattern}\n")
|
|
691
|
+
|
|
692
|
+
# Insert ignore-file arg after the base flags and before '--'
|
|
693
|
+
cmd.insert(1, "--ignore-file")
|
|
694
|
+
cmd.insert(2, ignore_file)
|
|
695
|
+
|
|
696
|
+
# Stream JSON output via Popen to avoid buffering huge results
|
|
697
|
+
process = subprocess.Popen(
|
|
698
|
+
cmd,
|
|
699
|
+
stdout=subprocess.PIPE,
|
|
700
|
+
stderr=subprocess.PIPE,
|
|
701
|
+
text=True,
|
|
702
|
+
encoding="utf-8",
|
|
703
|
+
errors="replace",
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
timed_out = False
|
|
707
|
+
import threading
|
|
708
|
+
|
|
709
|
+
def _kill_on_timeout():
|
|
710
|
+
nonlocal timed_out
|
|
711
|
+
timed_out = True
|
|
712
|
+
with contextlib.suppress(Exception):
|
|
713
|
+
process.kill()
|
|
714
|
+
|
|
715
|
+
timer = threading.Timer(30.0, _kill_on_timeout)
|
|
716
|
+
timer.start()
|
|
717
|
+
|
|
718
|
+
try:
|
|
719
|
+
for raw_line in process.stdout:
|
|
720
|
+
if timed_out:
|
|
721
|
+
raise subprocess.TimeoutExpired("rg", 30)
|
|
722
|
+
line = raw_line.rstrip("\n")
|
|
723
|
+
if not line:
|
|
724
|
+
continue
|
|
725
|
+
try:
|
|
726
|
+
match_data = json.loads(line)
|
|
727
|
+
except json.JSONDecodeError:
|
|
728
|
+
continue
|
|
729
|
+
if match_data.get("type") != "match":
|
|
730
|
+
continue
|
|
731
|
+
data = match_data.get("data", {})
|
|
732
|
+
path_data = data.get("path", {})
|
|
733
|
+
file_path = path_data.get("text", "") if path_data.get("text") else ""
|
|
734
|
+
line_number = data.get("line_number", None)
|
|
735
|
+
line_content = (
|
|
736
|
+
data.get("lines", {}).get("text", "")
|
|
737
|
+
if data.get("lines", {}).get("text")
|
|
738
|
+
else ""
|
|
739
|
+
)
|
|
740
|
+
if len(line_content) > MAX_GREP_LINE_LENGTH:
|
|
741
|
+
line_content = line_content[:MAX_GREP_LINE_LENGTH]
|
|
742
|
+
if file_path and line_number:
|
|
743
|
+
matches.append(
|
|
744
|
+
MatchInfo(
|
|
745
|
+
file_path=_sanitize_string(file_path),
|
|
746
|
+
line_number=line_number,
|
|
747
|
+
line_content=_sanitize_string(line_content.strip()),
|
|
748
|
+
)
|
|
749
|
+
)
|
|
750
|
+
if len(matches) >= MAX_GREP_MATCHES:
|
|
751
|
+
break
|
|
752
|
+
finally:
|
|
753
|
+
timer.cancel()
|
|
754
|
+
# Ensure ripgrep is killed even if we stopped early
|
|
755
|
+
try:
|
|
756
|
+
if process.poll() is None:
|
|
757
|
+
process.kill()
|
|
758
|
+
except Exception:
|
|
759
|
+
pass
|
|
760
|
+
with contextlib.suppress(Exception):
|
|
761
|
+
process.wait(timeout=2)
|
|
762
|
+
|
|
763
|
+
except subprocess.TimeoutExpired:
|
|
764
|
+
error_message = "Grep command timed out after 30 seconds"
|
|
765
|
+
except FileNotFoundError:
|
|
766
|
+
error_message = (
|
|
767
|
+
"ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
768
|
+
)
|
|
769
|
+
except Exception as e:
|
|
770
|
+
error_message = f"Error during grep operation: {e}"
|
|
771
|
+
finally:
|
|
772
|
+
if ignore_file and Path(ignore_file).exists():
|
|
773
|
+
with contextlib.suppress(OSError):
|
|
774
|
+
os.unlink(ignore_file)
|
|
775
|
+
|
|
776
|
+
match_list = list(matches)
|
|
777
|
+
|
|
778
|
+
# Build structured GrepMatch objects for the UI
|
|
779
|
+
grep_matches = [
|
|
780
|
+
GrepMatch(
|
|
781
|
+
file_path=m.file_path or "",
|
|
782
|
+
line_number=m.line_number or 1,
|
|
783
|
+
line_content=m.line_content or "",
|
|
784
|
+
)
|
|
785
|
+
for m in match_list
|
|
786
|
+
]
|
|
787
|
+
|
|
788
|
+
unique_files = len(set(m.file_path for m in match_list)) if match_list else 0
|
|
789
|
+
|
|
790
|
+
grep_result_msg = GrepResultMessage(
|
|
791
|
+
search_term=search_string,
|
|
792
|
+
directory=str(directory),
|
|
793
|
+
matches=grep_matches,
|
|
794
|
+
total_matches=len(match_list),
|
|
795
|
+
files_searched=unique_files,
|
|
796
|
+
)
|
|
797
|
+
get_message_bus().emit(grep_result_msg)
|
|
798
|
+
|
|
799
|
+
return GrepOutput(matches=match_list, error=error_message)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def register_list_files(agent):
|
|
803
|
+
"""Register only the list_files tool."""
|
|
804
|
+
from code_muse.config import get_allow_recursion
|
|
805
|
+
|
|
806
|
+
@agent.tool
|
|
807
|
+
def list_files(
|
|
808
|
+
context: RunContext, directory: str = ".", recursive: bool = True
|
|
809
|
+
) -> ListFileOutput:
|
|
810
|
+
"""List files and directories with intelligent filtering and safety features.
|
|
811
|
+
|
|
812
|
+
Automatically ignores build artifacts, caches, and common noise.
|
|
813
|
+
"""
|
|
814
|
+
warning = None
|
|
815
|
+
if recursive and not get_allow_recursion():
|
|
816
|
+
warning = "Recursion disabled globally for list_files - returning non-recursive results"
|
|
817
|
+
recursive = False
|
|
818
|
+
result = _list_files(context, directory, recursive)
|
|
819
|
+
|
|
820
|
+
# The structured FileListingMessage is already emitted by _list_files
|
|
821
|
+
# No need to emit again here
|
|
822
|
+
if warning:
|
|
823
|
+
result.error = warning
|
|
824
|
+
if (len(result.content)) > 200000:
|
|
825
|
+
result.content = result.content[0:200000]
|
|
826
|
+
result.error = "Results truncated. This is a massive directory tree, recommend non-recursive calls to list_files"
|
|
827
|
+
return result
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def register_read_file(agent):
|
|
831
|
+
"""Register only the read_file tool."""
|
|
832
|
+
|
|
833
|
+
@agent.tool
|
|
834
|
+
def read_file(
|
|
835
|
+
context: RunContext,
|
|
836
|
+
file_path: str = "",
|
|
837
|
+
start_line: int | None = None,
|
|
838
|
+
num_lines: int | None = None,
|
|
839
|
+
) -> ReadFileOutput:
|
|
840
|
+
"""Read file contents with optional line-range selection and token safety.
|
|
841
|
+
|
|
842
|
+
Use start_line/num_lines for large files to avoid overwhelming context.
|
|
843
|
+
"""
|
|
844
|
+
return _read_file(context, file_path, start_line, num_lines)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def register_grep(agent):
|
|
848
|
+
"""Register only the grep tool."""
|
|
849
|
+
|
|
850
|
+
@agent.tool
|
|
851
|
+
def grep(
|
|
852
|
+
context: RunContext, search_string: str = "", directory: str = "."
|
|
853
|
+
) -> GrepOutput:
|
|
854
|
+
"""Recursively search for text patterns across files using ripgrep (rg).
|
|
855
|
+
|
|
856
|
+
search_string is treated as a regex pattern, not CLI flags.
|
|
857
|
+
Use plain text or regex syntax; option injection is blocked.
|
|
858
|
+
Output is capped to 50 matches and 512 characters per line.
|
|
859
|
+
"""
|
|
860
|
+
return _grep(context, search_string, directory)
|