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,117 @@
|
|
|
1
|
+
"""Conversation snapshot serialization for checkpointing."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from code_muse.agents import get_current_agent
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_snapshot(
|
|
15
|
+
agent: Any, tool_name: str, tool_call_id: str | None = None
|
|
16
|
+
) -> Path | None:
|
|
17
|
+
"""Serialize the current conversation state to a JSON snapshot."""
|
|
18
|
+
try:
|
|
19
|
+
if agent is None:
|
|
20
|
+
agent = get_current_agent()
|
|
21
|
+
|
|
22
|
+
messages = list(agent.get_message_history())
|
|
23
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
24
|
+
|
|
25
|
+
snapshot: dict[str, Any] = {
|
|
26
|
+
"turn_id": f"{tool_name}_{timestamp}",
|
|
27
|
+
"timestamp": timestamp,
|
|
28
|
+
"tool_name": tool_name,
|
|
29
|
+
"tool_call_id": tool_call_id or "",
|
|
30
|
+
"messages": _serialize_messages(messages),
|
|
31
|
+
"agent_state": {
|
|
32
|
+
"name": getattr(agent, "name", "unknown"),
|
|
33
|
+
"model": getattr(agent, "model", "unknown"),
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Store in shadow git repo area
|
|
38
|
+
project_root = Path.cwd()
|
|
39
|
+
project_hash = _hash_project_root(str(project_root))
|
|
40
|
+
repo_path = Path.home() / ".muse" / "history" / project_hash
|
|
41
|
+
snapshot_dir = repo_path / "snapshots"
|
|
42
|
+
snapshot_dir.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
snapshot_path = snapshot_dir / f"snapshot_{timestamp.replace(':', '_')}.json"
|
|
45
|
+
snapshot_path.write_text(json.dumps(snapshot, indent=2), encoding="utf-8")
|
|
46
|
+
|
|
47
|
+
logger.info(f"Conversation snapshot saved to {snapshot_path}")
|
|
48
|
+
return snapshot_path
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
logger.error(f"Failed to create snapshot: {exc}")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_snapshot(path: Path) -> dict[str, Any] | None:
|
|
55
|
+
"""Load and validate a snapshot JSON file."""
|
|
56
|
+
try:
|
|
57
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
58
|
+
required_keys = {"turn_id", "timestamp", "tool_name", "messages", "agent_state"}
|
|
59
|
+
if not required_keys.issubset(data.keys()):
|
|
60
|
+
logger.warning(f"Snapshot at {path} is missing required keys")
|
|
61
|
+
return None
|
|
62
|
+
return data
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
logger.error(f"Failed to load snapshot {path}: {exc}")
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def list_snapshots(repo_path: Path) -> list[dict[str, Any]]:
|
|
69
|
+
"""Scan snapshot directory and return metadata sorted newest-first."""
|
|
70
|
+
snapshot_dir = repo_path / "snapshots"
|
|
71
|
+
if not snapshot_dir.exists():
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
results: list[dict[str, Any]] = []
|
|
75
|
+
for path in snapshot_dir.glob("snapshot_*.json"):
|
|
76
|
+
data = load_snapshot(path)
|
|
77
|
+
if data is None:
|
|
78
|
+
continue
|
|
79
|
+
results.append(
|
|
80
|
+
{
|
|
81
|
+
"timestamp": data.get("timestamp", ""),
|
|
82
|
+
"tool_name": data.get("tool_name", ""),
|
|
83
|
+
"tool_call_id": data.get("tool_call_id", ""),
|
|
84
|
+
"path": str(path),
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
results.sort(key=lambda x: x["timestamp"], reverse=True)
|
|
89
|
+
return results
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _hash_project_root(project_root: str) -> str:
|
|
93
|
+
import hashlib
|
|
94
|
+
|
|
95
|
+
return hashlib.sha256(project_root.encode()).hexdigest()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _serialize_messages(messages: list[Any]) -> list[dict[str, Any]]:
|
|
99
|
+
"""Best-effort serialization of message history to JSON-safe dicts."""
|
|
100
|
+
serialized: list[dict[str, Any]] = []
|
|
101
|
+
for msg in messages:
|
|
102
|
+
try:
|
|
103
|
+
if hasattr(msg, "model_dump"):
|
|
104
|
+
serialized.append(msg.model_dump())
|
|
105
|
+
elif hasattr(msg, "dict"):
|
|
106
|
+
serialized.append(msg.dict())
|
|
107
|
+
else:
|
|
108
|
+
serialized.append(
|
|
109
|
+
{
|
|
110
|
+
"type": type(msg).__name__,
|
|
111
|
+
"repr": repr(msg),
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
logger.warning(f"Could not serialize message: {exc}")
|
|
116
|
+
serialized.append({"type": "unserializable", "error": str(exc)})
|
|
117
|
+
return serialized
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Register checkpointing callbacks and commands."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from code_muse.callbacks import register_callback
|
|
6
|
+
from code_muse.plugins.checkpointing.checkpoint_hook import (
|
|
7
|
+
on_pre_tool_call_checkpoint,
|
|
8
|
+
)
|
|
9
|
+
from code_muse.plugins.checkpointing.restore_command import (
|
|
10
|
+
_custom_help,
|
|
11
|
+
_handle_custom_command,
|
|
12
|
+
)
|
|
13
|
+
from code_muse.plugins.checkpointing.rewind_shortcut import RewindKeyListener
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_rewind_listener: RewindKeyListener | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _start_rewind_listener() -> None:
|
|
21
|
+
global _rewind_listener
|
|
22
|
+
if _rewind_listener is not None:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
def _on_double_esc() -> None:
|
|
26
|
+
from code_muse.plugins.checkpointing.restore_command import (
|
|
27
|
+
_handle_restore_command,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_handle_restore_command("/restore")
|
|
31
|
+
|
|
32
|
+
_rewind_listener = RewindKeyListener(_on_double_esc)
|
|
33
|
+
_rewind_listener.start()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _stop_rewind_listener() -> None:
|
|
37
|
+
global _rewind_listener
|
|
38
|
+
if _rewind_listener is not None:
|
|
39
|
+
_rewind_listener.stop()
|
|
40
|
+
_rewind_listener = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _on_shutdown() -> None:
|
|
44
|
+
_stop_rewind_listener()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Register callbacks
|
|
48
|
+
register_callback("pre_tool_call", on_pre_tool_call_checkpoint)
|
|
49
|
+
register_callback("custom_command_help", _custom_help)
|
|
50
|
+
register_callback("custom_command", _handle_custom_command)
|
|
51
|
+
register_callback("shutdown", _on_shutdown)
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""/restore slash command for checkpointing rewind."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from code_muse.tools.common import get_user_approval_async
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _custom_help() -> list[tuple[str, str]]:
|
|
14
|
+
return [
|
|
15
|
+
(
|
|
16
|
+
"restore",
|
|
17
|
+
"List checkpoints or revert to a previous checkpoint (files and/or conversation)",
|
|
18
|
+
)
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _handle_restore_command(command: str) -> bool:
|
|
23
|
+
from code_muse.messaging import emit_error, emit_info
|
|
24
|
+
from code_muse.plugins.checkpointing.conversation_snapshots import (
|
|
25
|
+
list_snapshots,
|
|
26
|
+
load_snapshot,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
tokens = command.split()
|
|
30
|
+
if len(tokens) < 2:
|
|
31
|
+
# List all checkpoints
|
|
32
|
+
project_root = Path.cwd()
|
|
33
|
+
project_hash = _hash_project_root(str(project_root))
|
|
34
|
+
repo_path = Path.home() / ".muse" / "history" / project_hash
|
|
35
|
+
snapshots = list_snapshots(repo_path)
|
|
36
|
+
if not snapshots:
|
|
37
|
+
emit_info("No checkpoints available yet.")
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
lines = [":rewind: Available checkpoints:"]
|
|
41
|
+
for idx, snap in enumerate(snapshots, start=1):
|
|
42
|
+
ts = snap.get("timestamp", "")
|
|
43
|
+
tool = snap.get("tool_name", "")
|
|
44
|
+
lines.append(f" {idx}. [{ts}] {tool}")
|
|
45
|
+
|
|
46
|
+
emit_info("\n".join(lines))
|
|
47
|
+
emit_info("Usage: /restore <index> [full|files|conversation]")
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
index = int(tokens[1])
|
|
52
|
+
except ValueError:
|
|
53
|
+
emit_error("/restore: index must be an integer")
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
project_root = Path.cwd()
|
|
57
|
+
project_hash = _hash_project_root(str(project_root))
|
|
58
|
+
repo_path = Path.home() / ".muse" / "history" / project_hash
|
|
59
|
+
snapshots = list_snapshots(repo_path)
|
|
60
|
+
if not snapshots:
|
|
61
|
+
emit_info("No checkpoints available yet.")
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
if index < 1 or index > len(snapshots):
|
|
65
|
+
emit_error(f"/restore: index {index} out of range (1–{len(snapshots)})")
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
selected = snapshots[index - 1]
|
|
69
|
+
snapshot = load_snapshot(Path(selected["path"]))
|
|
70
|
+
if snapshot is None:
|
|
71
|
+
emit_error("/restore: could not load selected snapshot")
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
scope = "preview"
|
|
75
|
+
if len(tokens) >= 3:
|
|
76
|
+
scope = tokens[2].lower()
|
|
77
|
+
if scope not in ("full", "files", "conversation", "preview"):
|
|
78
|
+
emit_error(
|
|
79
|
+
"/restore: scope must be one of full, files, conversation, preview"
|
|
80
|
+
)
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
if scope == "preview":
|
|
84
|
+
_preview_checkpoint(selected, snapshot, project_root)
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
# Confirmation dialog before destructive restore
|
|
88
|
+
_run_restore(scope, selected, snapshot, project_root)
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _preview_checkpoint(
|
|
93
|
+
selected: dict[str, Any], snapshot: dict[str, Any], project_root: Path
|
|
94
|
+
) -> None:
|
|
95
|
+
from code_muse.messaging import emit_info
|
|
96
|
+
|
|
97
|
+
commit_hash = _get_commit_hash_for_snapshot(selected, project_root)
|
|
98
|
+
lines = [":mag: Checkpoint preview:"]
|
|
99
|
+
lines.append(f" Timestamp: {selected.get('timestamp', '')}")
|
|
100
|
+
lines.append(f" Tool: {selected.get('tool_name', '')}")
|
|
101
|
+
lines.append(f" Messages: {len(snapshot.get('messages', []))} at checkpoint")
|
|
102
|
+
|
|
103
|
+
if commit_hash:
|
|
104
|
+
try:
|
|
105
|
+
diff_result = subprocess.run(
|
|
106
|
+
[
|
|
107
|
+
"git",
|
|
108
|
+
"-C",
|
|
109
|
+
str(project_root),
|
|
110
|
+
"diff",
|
|
111
|
+
f"{commit_hash}..HEAD",
|
|
112
|
+
"--stat",
|
|
113
|
+
],
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
)
|
|
117
|
+
if diff_result.returncode == 0 and diff_result.stdout.strip():
|
|
118
|
+
lines.append(" Diff since checkpoint:")
|
|
119
|
+
for line in diff_result.stdout.strip().splitlines():
|
|
120
|
+
lines.append(f" {line}")
|
|
121
|
+
else:
|
|
122
|
+
lines.append(" No file changes since checkpoint.")
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
lines.append(f" Could not compute diff: {exc}")
|
|
125
|
+
else:
|
|
126
|
+
lines.append(" Commit hash not available.")
|
|
127
|
+
|
|
128
|
+
lines.append("")
|
|
129
|
+
lines.append("Usage: /restore <index> [full|files|conversation]")
|
|
130
|
+
emit_info("\n".join(lines))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _run_restore(
|
|
134
|
+
scope: str,
|
|
135
|
+
selected: dict[str, Any],
|
|
136
|
+
snapshot: dict[str, Any],
|
|
137
|
+
project_root: Path,
|
|
138
|
+
) -> None:
|
|
139
|
+
import asyncio
|
|
140
|
+
|
|
141
|
+
from code_muse.messaging import emit_error, emit_info, emit_success
|
|
142
|
+
|
|
143
|
+
async def _do_restore() -> bool:
|
|
144
|
+
# TODO: PEP 734 async bridge — _get_commit_hash_for_snapshot uses sync subprocess
|
|
145
|
+
commit_hash = await asyncio.to_thread(
|
|
146
|
+
_get_commit_hash_for_snapshot, selected, project_root
|
|
147
|
+
)
|
|
148
|
+
restore_parts: list[str] = []
|
|
149
|
+
if scope in ("full", "files"):
|
|
150
|
+
restore_parts.append("files")
|
|
151
|
+
if scope in ("full", "conversation"):
|
|
152
|
+
restore_parts.append("conversation")
|
|
153
|
+
|
|
154
|
+
preview_text = f"Revert {', '.join(restore_parts)} to checkpoint [{selected.get('timestamp', '')}]?"
|
|
155
|
+
|
|
156
|
+
confirmed, feedback = await get_user_approval_async(
|
|
157
|
+
title="Restore Checkpoint",
|
|
158
|
+
content=preview_text,
|
|
159
|
+
preview=None,
|
|
160
|
+
border_style="dim white",
|
|
161
|
+
agent_name="Muse",
|
|
162
|
+
)
|
|
163
|
+
if not confirmed:
|
|
164
|
+
emit_info("Restore cancelled.")
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
if scope in ("full", "files") and commit_hash:
|
|
168
|
+
try:
|
|
169
|
+
checkout_proc = await asyncio.create_subprocess_exec(
|
|
170
|
+
"git",
|
|
171
|
+
"-C",
|
|
172
|
+
str(project_root),
|
|
173
|
+
"checkout",
|
|
174
|
+
commit_hash,
|
|
175
|
+
"--",
|
|
176
|
+
".",
|
|
177
|
+
stdout=asyncio.subprocess.PIPE,
|
|
178
|
+
stderr=asyncio.subprocess.PIPE,
|
|
179
|
+
)
|
|
180
|
+
checkout_stdout, checkout_stderr = await checkout_proc.communicate()
|
|
181
|
+
if checkout_proc.returncode != 0:
|
|
182
|
+
emit_error(f"git checkout failed: {checkout_stderr.decode()}")
|
|
183
|
+
return False
|
|
184
|
+
emit_success(":white_check_mark: Files restored.")
|
|
185
|
+
except Exception as exc:
|
|
186
|
+
emit_error(f"File restore failed: {exc}")
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
if scope in ("full", "conversation"):
|
|
190
|
+
try:
|
|
191
|
+
from code_muse.agents import get_current_agent
|
|
192
|
+
|
|
193
|
+
agent = get_current_agent()
|
|
194
|
+
messages = snapshot.get("messages", [])
|
|
195
|
+
if messages:
|
|
196
|
+
agent.set_message_history(messages)
|
|
197
|
+
emit_success(":white_check_mark: Conversation restored.")
|
|
198
|
+
else:
|
|
199
|
+
emit_info("No conversation state in snapshot.")
|
|
200
|
+
except Exception as exc:
|
|
201
|
+
emit_error(f"Conversation restore failed: {exc}")
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
asyncio.get_running_loop()
|
|
208
|
+
asyncio.create_task(_do_restore())
|
|
209
|
+
except RuntimeError:
|
|
210
|
+
asyncio.run(_do_restore())
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _get_commit_hash_for_snapshot(
|
|
214
|
+
selected: dict[str, Any], project_root: Path
|
|
215
|
+
) -> str | None:
|
|
216
|
+
"""Find the commit hash that matches this snapshot's timestamp/tool."""
|
|
217
|
+
try:
|
|
218
|
+
timestamp = selected.get("timestamp", "")
|
|
219
|
+
tool_name = selected.get("tool_name", "")
|
|
220
|
+
log_result = subprocess.run(
|
|
221
|
+
[
|
|
222
|
+
"git",
|
|
223
|
+
"-C",
|
|
224
|
+
str(project_root),
|
|
225
|
+
"log",
|
|
226
|
+
"--oneline",
|
|
227
|
+
"--format=%H %s",
|
|
228
|
+
],
|
|
229
|
+
capture_output=True,
|
|
230
|
+
text=True,
|
|
231
|
+
)
|
|
232
|
+
if log_result.returncode != 0:
|
|
233
|
+
return None
|
|
234
|
+
for line in log_result.stdout.strip().splitlines():
|
|
235
|
+
parts = line.split(None, 1)
|
|
236
|
+
if len(parts) < 2:
|
|
237
|
+
continue
|
|
238
|
+
commit_hash, message = parts
|
|
239
|
+
if f"checkpoint: {tool_name}" in message and timestamp in message:
|
|
240
|
+
return commit_hash
|
|
241
|
+
# Fallback: return HEAD if no exact match
|
|
242
|
+
head_result = subprocess.run(
|
|
243
|
+
["git", "-C", str(project_root), "rev-parse", "HEAD"],
|
|
244
|
+
capture_output=True,
|
|
245
|
+
text=True,
|
|
246
|
+
)
|
|
247
|
+
if head_result.returncode == 0:
|
|
248
|
+
return head_result.stdout.strip()
|
|
249
|
+
except Exception as exc:
|
|
250
|
+
logger.warning(f"Could not resolve commit hash: {exc}")
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _hash_project_root(project_root: str) -> str:
|
|
255
|
+
import hashlib
|
|
256
|
+
|
|
257
|
+
return hashlib.sha256(project_root.encode()).hexdigest()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _handle_custom_command(command: str, name: str) -> bool | None:
|
|
261
|
+
if name != "restore":
|
|
262
|
+
return None
|
|
263
|
+
return _handle_restore_command(command)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Esc×2 rewind keyboard shortcut."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DoublePressDetector:
|
|
13
|
+
"""Detects double-press of a key within a time window."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, window_ms: int = 500) -> None:
|
|
16
|
+
self._window_ms = window_ms
|
|
17
|
+
self._last_press: float = 0.0
|
|
18
|
+
|
|
19
|
+
def press(self) -> bool:
|
|
20
|
+
now = time.monotonic()
|
|
21
|
+
delta_ms = (now - self._last_press) * 1000
|
|
22
|
+
self._last_press = now
|
|
23
|
+
return 0 < delta_ms < self._window_ms
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RewindKeyListener:
|
|
27
|
+
"""Daemon thread that listens for raw ESC keycodes and triggers rewind on double-press."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, on_double_esc: Callable[[], None]) -> None:
|
|
30
|
+
self._detector = DoublePressDetector()
|
|
31
|
+
self._on_double_esc = on_double_esc
|
|
32
|
+
self._stop = threading.Event()
|
|
33
|
+
self._thread = threading.Thread(target=self._listen, daemon=True)
|
|
34
|
+
|
|
35
|
+
def start(self) -> None:
|
|
36
|
+
self._thread.start()
|
|
37
|
+
logger.info("RewindKeyListener started")
|
|
38
|
+
|
|
39
|
+
def stop(self) -> None:
|
|
40
|
+
self._stop.set()
|
|
41
|
+
logger.info("RewindKeyListener stopped")
|
|
42
|
+
|
|
43
|
+
def _listen(self) -> None:
|
|
44
|
+
if sys.platform == "win32":
|
|
45
|
+
self._listen_windows()
|
|
46
|
+
else:
|
|
47
|
+
self._listen_posix()
|
|
48
|
+
|
|
49
|
+
def _listen_posix(self) -> None:
|
|
50
|
+
try:
|
|
51
|
+
import select
|
|
52
|
+
import termios
|
|
53
|
+
import tty
|
|
54
|
+
|
|
55
|
+
fd = sys.stdin.fileno()
|
|
56
|
+
old_settings = termios.tcgetattr(fd)
|
|
57
|
+
tty.setcbreak(fd)
|
|
58
|
+
try:
|
|
59
|
+
while not self._stop.is_set():
|
|
60
|
+
ready, _, _ = select.select([sys.stdin], [], [], 0.1)
|
|
61
|
+
if ready:
|
|
62
|
+
char = sys.stdin.read(1)
|
|
63
|
+
if char == "\x1b":
|
|
64
|
+
if self._detector.press():
|
|
65
|
+
try:
|
|
66
|
+
self._on_double_esc()
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
logger.error(f"Double-esc handler failed: {exc}")
|
|
69
|
+
finally:
|
|
70
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
logger.debug(f"POSIX rewind listener could not start: {exc}")
|
|
73
|
+
|
|
74
|
+
def _listen_windows(self) -> None:
|
|
75
|
+
try:
|
|
76
|
+
import msvcrt
|
|
77
|
+
|
|
78
|
+
while not self._stop.is_set():
|
|
79
|
+
if msvcrt.kbhit():
|
|
80
|
+
char = msvcrt.getch()
|
|
81
|
+
if char == b"\x1b" and self._detector.press():
|
|
82
|
+
try:
|
|
83
|
+
self._on_double_esc()
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
logger.error(f"Double-esc handler failed: {exc}")
|
|
86
|
+
time.sleep(0.05)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
logger.debug(f"Windows rewind listener could not start: {exc}")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Shadow git repository for checkpointing file snapshots."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
import subprocess
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ShadowGit:
|
|
13
|
+
"""Manages a bare shadow git repository for project checkpointing."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, project_root: str) -> None:
|
|
16
|
+
self.project_root = Path(project_root).resolve()
|
|
17
|
+
project_hash = hashlib.sha256(str(self.project_root).encode()).hexdigest()
|
|
18
|
+
self.repo_path = Path.home() / ".muse" / "history" / project_hash
|
|
19
|
+
self._init_repo()
|
|
20
|
+
|
|
21
|
+
def _init_repo(self) -> None:
|
|
22
|
+
if not self.repo_path.exists():
|
|
23
|
+
try:
|
|
24
|
+
self.repo_path.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
subprocess.run(
|
|
26
|
+
["git", "init", "--bare", str(self.repo_path)],
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
check=True,
|
|
30
|
+
)
|
|
31
|
+
logger.info(f"Initialized shadow git repo at {self.repo_path}")
|
|
32
|
+
except Exception as exc:
|
|
33
|
+
logger.warning(f"Failed to init shadow git repo: {exc}")
|
|
34
|
+
|
|
35
|
+
def create_checkpoint(
|
|
36
|
+
self, tool_name: str, affected_files: list[str] | None = None
|
|
37
|
+
) -> str | None:
|
|
38
|
+
"""Create a git checkpoint and return the commit hash."""
|
|
39
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
40
|
+
message = f"checkpoint: {tool_name} {timestamp}"
|
|
41
|
+
if affected_files:
|
|
42
|
+
files_str = ", ".join(affected_files)
|
|
43
|
+
message += f" ({files_str})"
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Stage everything in the project root
|
|
47
|
+
add_result = subprocess.run(
|
|
48
|
+
["git", "-C", str(self.project_root), "add", "-A"],
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
)
|
|
52
|
+
if add_result.returncode != 0:
|
|
53
|
+
logger.warning(f"git add failed: {add_result.stderr}")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
# Commit
|
|
57
|
+
commit_result = subprocess.run(
|
|
58
|
+
[
|
|
59
|
+
"git",
|
|
60
|
+
"-C",
|
|
61
|
+
str(self.project_root),
|
|
62
|
+
"commit",
|
|
63
|
+
"--allow-empty",
|
|
64
|
+
"-m",
|
|
65
|
+
message,
|
|
66
|
+
],
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
)
|
|
70
|
+
if commit_result.returncode != 0:
|
|
71
|
+
# No changes to commit is acceptable
|
|
72
|
+
if "nothing to commit" in commit_result.stdout.lower():
|
|
73
|
+
pass
|
|
74
|
+
else:
|
|
75
|
+
logger.warning(f"git commit failed: {commit_result.stderr}")
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Get commit hash
|
|
79
|
+
hash_result = subprocess.run(
|
|
80
|
+
["git", "-C", str(self.project_root), "rev-parse", "HEAD"],
|
|
81
|
+
capture_output=True,
|
|
82
|
+
text=True,
|
|
83
|
+
check=True,
|
|
84
|
+
)
|
|
85
|
+
commit_hash = hash_result.stdout.strip()
|
|
86
|
+
logger.info(f"Created checkpoint {commit_hash} for {tool_name}")
|
|
87
|
+
return commit_hash
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
logger.warning(f"Checkpoint creation failed: {exc}")
|
|
90
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Claude Code hooks plugin."""
|