code-muse 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_muse/__init__.py +26 -0
- code_muse/__main__.py +10 -0
- code_muse/agents/__init__.py +31 -0
- code_muse/agents/_builder.py +214 -0
- code_muse/agents/_compaction.py +506 -0
- code_muse/agents/_diagnostics.py +171 -0
- code_muse/agents/_history.py +382 -0
- code_muse/agents/_key_listeners.py +148 -0
- code_muse/agents/_non_streaming_render.py +148 -0
- code_muse/agents/_runtime.py +596 -0
- code_muse/agents/agent_creator_agent.py +603 -0
- code_muse/agents/agent_helios.py +47 -0
- code_muse/agents/agent_manager.py +740 -0
- code_muse/agents/agent_muse.py +78 -0
- code_muse/agents/agent_planning.py +44 -0
- code_muse/agents/agent_qa_melpomene.py +207 -0
- code_muse/agents/base_agent.py +194 -0
- code_muse/agents/event_stream_handler.py +361 -0
- code_muse/agents/json_agent.py +201 -0
- code_muse/agents/prompt_v3.py +521 -0
- code_muse/agents/subagent_stream_handler.py +273 -0
- code_muse/callbacks.py +941 -0
- code_muse/chatgpt_codex_client.py +333 -0
- code_muse/claude_cache_client.py +853 -0
- code_muse/cli_runner/__init__.py +319 -0
- code_muse/cli_runner/args.py +63 -0
- code_muse/cli_runner/loop.py +510 -0
- code_muse/cli_runner/resume.py +72 -0
- code_muse/cli_runner/runner.py +161 -0
- code_muse/command_line/__init__.py +1 -0
- code_muse/command_line/add_model_menu.py +1331 -0
- code_muse/command_line/agent_menu.py +674 -0
- code_muse/command_line/attachments.py +397 -0
- code_muse/command_line/autosave_menu.py +709 -0
- code_muse/command_line/clipboard.py +528 -0
- code_muse/command_line/colors_menu.py +530 -0
- code_muse/command_line/command_handler.py +262 -0
- code_muse/command_line/command_registry.py +150 -0
- code_muse/command_line/config_commands.py +711 -0
- code_muse/command_line/core_commands.py +740 -0
- code_muse/command_line/diff_menu.py +865 -0
- code_muse/command_line/file_path_completion.py +73 -0
- code_muse/command_line/load_context_completion.py +57 -0
- code_muse/command_line/model_picker_completion.py +512 -0
- code_muse/command_line/model_settings_menu.py +983 -0
- code_muse/command_line/onboarding_slides.py +162 -0
- code_muse/command_line/onboarding_wizard.py +337 -0
- code_muse/command_line/pagination.py +41 -0
- code_muse/command_line/pin_command_completion.py +329 -0
- code_muse/command_line/prompt_toolkit_completion.py +886 -0
- code_muse/command_line/session_commands.py +304 -0
- code_muse/command_line/shell_passthrough.py +145 -0
- code_muse/command_line/skills_completion.py +158 -0
- code_muse/command_line/types.py +18 -0
- code_muse/command_line/uc_menu.py +908 -0
- code_muse/command_line/utils.py +105 -0
- code_muse/command_line/wiggum_state.py +77 -0
- code_muse/config.py +1138 -0
- code_muse/config_agent.py +168 -0
- code_muse/config_appearance.py +241 -0
- code_muse/config_model.py +357 -0
- code_muse/config_security.py +73 -0
- code_muse/error_logging.py +132 -0
- code_muse/evals/__init__.py +35 -0
- code_muse/evals/eval_helpers.py +81 -0
- code_muse/evals/eval_runner.py +299 -0
- code_muse/evals/sample_evals/__init__.py +1 -0
- code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
- code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
- code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
- code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
- code_muse/fs_scan_cache/__init__.py +31 -0
- code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
- code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
- code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
- code_muse/fs_scan_cache/tool_integration.py +309 -0
- code_muse/fs_scan_cache/ttl_policy.py +44 -0
- code_muse/gemini_code_assist.py +383 -0
- code_muse/gemini_model.py +838 -0
- code_muse/hook_engine/README.md +105 -0
- code_muse/hook_engine/__init__.py +21 -0
- code_muse/hook_engine/aliases.py +153 -0
- code_muse/hook_engine/engine.py +221 -0
- code_muse/hook_engine/executor.py +347 -0
- code_muse/hook_engine/matcher.py +154 -0
- code_muse/hook_engine/models.py +245 -0
- code_muse/hook_engine/registry.py +114 -0
- code_muse/hook_engine/trust.py +268 -0
- code_muse/hook_engine/validator.py +144 -0
- code_muse/http_utils.py +360 -0
- code_muse/keymap.py +128 -0
- code_muse/list_filtering.py +26 -0
- code_muse/main.py +10 -0
- code_muse/messaging/__init__.py +259 -0
- code_muse/messaging/bus.py +621 -0
- code_muse/messaging/commands.py +166 -0
- code_muse/messaging/markdown_patches.py +57 -0
- code_muse/messaging/message_queue.py +397 -0
- code_muse/messaging/messages.py +591 -0
- code_muse/messaging/queue_console.py +269 -0
- code_muse/messaging/renderers.py +308 -0
- code_muse/messaging/rich_renderer.py +1158 -0
- code_muse/messaging/shimmer.py +154 -0
- code_muse/messaging/spinner/__init__.py +87 -0
- code_muse/messaging/spinner/console_spinner.py +250 -0
- code_muse/messaging/spinner/spinner_base.py +82 -0
- code_muse/messaging/subagent_console.py +458 -0
- code_muse/model_factory.py +1203 -0
- code_muse/model_switching.py +59 -0
- code_muse/model_utils.py +156 -0
- code_muse/models.json +66 -0
- code_muse/models_cache/__init__.py +26 -0
- code_muse/models_cache/blocking_lru_cache.py +98 -0
- code_muse/models_cache/cache_writer.py +86 -0
- code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
- code_muse/models_cache/sha256_hash.pyx +34 -0
- code_muse/models_cache/startup_integration.py +75 -0
- code_muse/models_dev_api.json +1 -0
- code_muse/models_dev_parser.py +590 -0
- code_muse/motion.py +126 -0
- code_muse/plugins/__init__.py +471 -0
- code_muse/plugins/agent_skills/__init__.py +32 -0
- code_muse/plugins/agent_skills/config.py +176 -0
- code_muse/plugins/agent_skills/discovery.py +309 -0
- code_muse/plugins/agent_skills/downloader.py +389 -0
- code_muse/plugins/agent_skills/installer.py +19 -0
- code_muse/plugins/agent_skills/metadata.py +293 -0
- code_muse/plugins/agent_skills/prompt_builder.py +66 -0
- code_muse/plugins/agent_skills/register_callbacks.py +298 -0
- code_muse/plugins/agent_skills/remote_catalog.py +320 -0
- code_muse/plugins/agent_skills/skill_catalog.py +254 -0
- code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
- code_muse/plugins/agent_skills/skills_menu.py +791 -0
- code_muse/plugins/autonomous_memory/__init__.py +39 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
- code_muse/plugins/autonomous_memory/consolidation.py +82 -0
- code_muse/plugins/autonomous_memory/extraction.py +382 -0
- code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
- code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
- code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
- code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
- code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
- code_muse/plugins/aws_bedrock/__init__.py +14 -0
- code_muse/plugins/aws_bedrock/config.py +99 -0
- code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
- code_muse/plugins/aws_bedrock/utils.py +153 -0
- code_muse/plugins/azure_foundry/README.md +238 -0
- code_muse/plugins/azure_foundry/__init__.py +15 -0
- code_muse/plugins/azure_foundry/config.py +125 -0
- code_muse/plugins/azure_foundry/discovery.py +187 -0
- code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
- code_muse/plugins/azure_foundry/token.py +180 -0
- code_muse/plugins/azure_foundry/utils.py +345 -0
- code_muse/plugins/build_filter/__init__.py +1 -0
- code_muse/plugins/build_filter/register_callbacks.py +201 -0
- code_muse/plugins/build_filter/strategies/__init__.py +1 -0
- code_muse/plugins/build_filter/strategies/build.py +397 -0
- code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
- code_muse/plugins/chatgpt_oauth/config.py +52 -0
- code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
- code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
- code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_muse/plugins/chatgpt_oauth/utils.py +538 -0
- code_muse/plugins/checkpointing/__init__.py +29 -0
- code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
- code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
- code_muse/plugins/checkpointing/register_callbacks.py +51 -0
- code_muse/plugins/checkpointing/restore_command.py +263 -0
- code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
- code_muse/plugins/checkpointing/shadow_git.py +90 -0
- code_muse/plugins/claude_code_hooks/__init__.py +1 -0
- code_muse/plugins/claude_code_hooks/config.py +188 -0
- code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
- code_muse/plugins/claude_code_oauth/README.md +167 -0
- code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
- code_muse/plugins/claude_code_oauth/__init__.py +25 -0
- code_muse/plugins/claude_code_oauth/config.py +52 -0
- code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
- code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
- code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
- code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
- code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
- code_muse/plugins/claude_code_oauth/utils.py +664 -0
- code_muse/plugins/copilot_auth/__init__.py +11 -0
- code_muse/plugins/copilot_auth/config.py +91 -0
- code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
- code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
- code_muse/plugins/copilot_auth/utils.py +584 -0
- code_muse/plugins/custom_commands/__init__.py +14 -0
- code_muse/plugins/custom_commands/args_injection.py +82 -0
- code_muse/plugins/custom_commands/command_discovery.py +89 -0
- code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
- code_muse/plugins/custom_commands/register_callbacks.py +176 -0
- code_muse/plugins/customizable_commands/__init__.py +0 -0
- code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
- code_muse/plugins/destructive_command_guard/__init__.py +14 -0
- code_muse/plugins/destructive_command_guard/detector.py +375 -0
- code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
- code_muse/plugins/example_custom_command/README.md +280 -0
- code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
- code_muse/plugins/file_permission_handler/__init__.py +4 -0
- code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
- code_muse/plugins/filter_engine/__init__.py +30 -0
- code_muse/plugins/filter_engine/classifier.py +153 -0
- code_muse/plugins/filter_engine/content_detector.py +184 -0
- code_muse/plugins/filter_engine/dispatcher.py +244 -0
- code_muse/plugins/filter_engine/register_callbacks.py +188 -0
- code_muse/plugins/filter_engine/registry.py +279 -0
- code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
- code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
- code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
- code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
- code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
- code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
- code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
- code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
- code_muse/plugins/filter_engine/verbosity.py +63 -0
- code_muse/plugins/force_push_guard/__init__.py +5 -0
- code_muse/plugins/force_push_guard/detector.py +96 -0
- code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
- code_muse/plugins/force_push_guard/test_detector.py +143 -0
- code_muse/plugins/frontend_emitter/__init__.py +25 -0
- code_muse/plugins/frontend_emitter/emitter.py +121 -0
- code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
- code_muse/plugins/gac/__init__.py +4 -0
- code_muse/plugins/gac/git_ops.py +136 -0
- code_muse/plugins/gac/prompt.py +191 -0
- code_muse/plugins/gac/register_callbacks.py +82 -0
- code_muse/plugins/hook_creator/__init__.py +1 -0
- code_muse/plugins/hook_creator/register_callbacks.py +34 -0
- code_muse/plugins/hook_manager/__init__.py +1 -0
- code_muse/plugins/hook_manager/config.py +289 -0
- code_muse/plugins/hook_manager/hooks_menu.py +563 -0
- code_muse/plugins/hook_manager/register_callbacks.py +227 -0
- code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
- code_muse/plugins/mindpack/__init__.py +0 -0
- code_muse/plugins/mindpack/factory.py +930 -0
- code_muse/plugins/mindpack/judge.py +573 -0
- code_muse/plugins/mindpack/memory.py +100 -0
- code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
- code_muse/plugins/mindpack/orchestration.py +605 -0
- code_muse/plugins/mindpack/register_callbacks.py +175 -0
- code_muse/plugins/mindpack/schemas.py +358 -0
- code_muse/plugins/mindpack/tools.py +387 -0
- code_muse/plugins/oauth_muse_html.py +226 -0
- code_muse/plugins/ollama_setup/__init__.py +5 -0
- code_muse/plugins/ollama_setup/completer.py +36 -0
- code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
- code_muse/plugins/plan_command/__init__.py +0 -0
- code_muse/plugins/plan_command/register_callbacks.py +206 -0
- code_muse/plugins/plan_mode/__init__.py +37 -0
- code_muse/plugins/plan_mode/mode_cycling.py +40 -0
- code_muse/plugins/plan_mode/plan_generation.py +68 -0
- code_muse/plugins/plan_mode/plan_hooks.py +74 -0
- code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
- code_muse/plugins/plan_mode/register_callbacks.py +121 -0
- code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
- code_muse/plugins/policy_engine/__init__.py +46 -0
- code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
- code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
- code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
- code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
- code_muse/plugins/policy_engine/register_callbacks.py +112 -0
- code_muse/plugins/pop_command/__init__.py +1 -0
- code_muse/plugins/pop_command/register_callbacks.py +189 -0
- code_muse/plugins/prompt_newline/__init__.py +13 -0
- code_muse/plugins/prompt_newline/config.py +19 -0
- code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
- code_muse/plugins/safety_status/__init__.py +0 -0
- code_muse/plugins/safety_status/register_callbacks.py +113 -0
- code_muse/plugins/semantic_compression/__init__.py +6 -0
- code_muse/plugins/semantic_compression/compressor.py +295 -0
- code_muse/plugins/semantic_compression/config.py +123 -0
- code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
- code_muse/plugins/shell_minimizer/__init__.py +50 -0
- code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
- code_muse/plugins/shell_minimizer/pipeline.py +556 -0
- code_muse/plugins/shell_minimizer/primitives.py +482 -0
- code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
- code_muse/plugins/shell_safety/__init__.py +6 -0
- code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_muse/plugins/shell_safety/command_cache.py +149 -0
- code_muse/plugins/shell_safety/register_callbacks.py +202 -0
- code_muse/plugins/synthetic_status/__init__.py +1 -0
- code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
- code_muse/plugins/synthetic_status/status_api.py +145 -0
- code_muse/plugins/token_caching/__init__.py +21 -0
- code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
- code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
- code_muse/plugins/token_caching/register_callbacks.py +54 -0
- code_muse/plugins/token_caching/stats_display.py +35 -0
- code_muse/plugins/token_tracking/__init__.py +26 -0
- code_muse/plugins/token_tracking/database.py +381 -0
- code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
- code_muse/plugins/token_tracking/record.py +55 -0
- code_muse/plugins/token_tracking/register_callbacks.py +277 -0
- code_muse/plugins/token_tracking/reports.py +329 -0
- code_muse/plugins/universal_constructor/__init__.py +13 -0
- code_muse/plugins/universal_constructor/models.py +136 -0
- code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
- code_muse/plugins/universal_constructor/registry.py +390 -0
- code_muse/plugins/universal_constructor/runner.py +474 -0
- code_muse/plugins/universal_constructor/safety.py +440 -0
- code_muse/plugins/universal_constructor/sandbox.py +584 -0
- code_muse/provider_identity.py +105 -0
- code_muse/pydantic_patches.py +410 -0
- code_muse/reopenable_async_client.py +233 -0
- code_muse/round_robin_model.py +151 -0
- code_muse/secret_storage.py +74 -0
- code_muse/security/__init__.py +1 -0
- code_muse/security/redaction.cpython-314-darwin.so +0 -0
- code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/security/redaction.pyx +135 -0
- code_muse/session_storage.py +565 -0
- code_muse/status_display.py +261 -0
- code_muse/stream_parser/__init__.py +76 -0
- code_muse/stream_parser/assistant_text_parser.py +90 -0
- code_muse/stream_parser/citation_parser.py +76 -0
- code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
- code_muse/stream_parser/proposed_plan_parser.py +158 -0
- code_muse/stream_parser/stream_text_chunk.py +23 -0
- code_muse/stream_parser/stream_text_parser.py +27 -0
- code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
- code_muse/stream_parser/tagged_line_parser.pyx +251 -0
- code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
- code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
- code_muse/summarization_agent.py +308 -0
- code_muse/terminal_utils.cpython-314-darwin.so +0 -0
- code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/terminal_utils.pyx +483 -0
- code_muse/tools/__init__.py +459 -0
- code_muse/tools/agent_tools.py +613 -0
- code_muse/tools/ask_user_question/__init__.py +26 -0
- code_muse/tools/ask_user_question/constants.py +73 -0
- code_muse/tools/ask_user_question/demo_tui.py +55 -0
- code_muse/tools/ask_user_question/handler.py +232 -0
- code_muse/tools/ask_user_question/models.py +302 -0
- code_muse/tools/ask_user_question/registration.py +37 -0
- code_muse/tools/ask_user_question/renderers.py +336 -0
- code_muse/tools/ask_user_question/terminal_ui.py +327 -0
- code_muse/tools/ask_user_question/theme.py +156 -0
- code_muse/tools/ask_user_question/tui_loop.py +422 -0
- code_muse/tools/background_jobs.py +99 -0
- code_muse/tools/browser/__init__.py +37 -0
- code_muse/tools/browser/browser_control.py +289 -0
- code_muse/tools/browser/browser_interactions.py +545 -0
- code_muse/tools/browser/browser_locators.py +640 -0
- code_muse/tools/browser/browser_manager.py +376 -0
- code_muse/tools/browser/browser_navigation.py +251 -0
- code_muse/tools/browser/browser_screenshot.py +180 -0
- code_muse/tools/browser/browser_scripts.py +462 -0
- code_muse/tools/browser/browser_workflows.py +222 -0
- code_muse/tools/chrome_cdp/__init__.py +1070 -0
- code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
- code_muse/tools/command_runner.py +1401 -0
- code_muse/tools/common.py +1407 -0
- code_muse/tools/display.py +87 -0
- code_muse/tools/file_modifications.py +1099 -0
- code_muse/tools/file_operations.py +860 -0
- code_muse/tools/image_tools.py +185 -0
- code_muse/tools/meetin_proxy/__init__.py +243 -0
- code_muse/tools/meetin_proxy/capture_addon.py +82 -0
- code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
- code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
- code_muse/tools/path_policy.py +219 -0
- code_muse/tools/skills_tools.py +586 -0
- code_muse/tools/subagent_context.py +158 -0
- code_muse/tools/tools_content.py +50 -0
- code_muse/tools/universal_constructor.py +965 -0
- code_muse/uvx_detection.py +241 -0
- code_muse/version_checker.py +86 -0
- code_muse-0.0.1.data/data/code_muse/models.json +66 -0
- code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
- code_muse-0.0.1.dist-info/METADATA +845 -0
- code_muse-0.0.1.dist-info/RECORD +394 -0
- code_muse-0.0.1.dist-info/WHEEL +4 -0
- code_muse-0.0.1.dist-info/entry_points.txt +2 -0
- code_muse-0.0.1.dist-info/licenses/LICENSE +21 -0
code_muse/config.py
ADDED
|
@@ -0,0 +1,1138 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import contextlib
|
|
3
|
+
import datetime
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from code_muse.session_storage import save_session
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_xdg_dir(env_var: str, fallback: str) -> Path:
|
|
11
|
+
"""
|
|
12
|
+
Get directory for code_muse files, defaulting to ~/.muse.
|
|
13
|
+
|
|
14
|
+
XDG paths are only used when the corresponding environment variable
|
|
15
|
+
is explicitly set by the user. Otherwise, we use the legacy ~/.muse
|
|
16
|
+
directory for all file types (config, data, cache, state).
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
env_var: XDG environment variable name (e.g., "XDG_CONFIG_HOME")
|
|
20
|
+
fallback: Fallback path relative to home (e.g., ".config") - unused unless XDG var is set
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Path to the directory for code_muse files
|
|
24
|
+
"""
|
|
25
|
+
# Use XDG directory ONLY if environment variable is explicitly set
|
|
26
|
+
xdg_base = os.getenv(env_var)
|
|
27
|
+
if xdg_base:
|
|
28
|
+
return Path(xdg_base) / "muse"
|
|
29
|
+
|
|
30
|
+
# Default to legacy ~/.muse for all file types
|
|
31
|
+
return Path.home() / ".muse"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# XDG Base Directory paths
|
|
35
|
+
CONFIG_DIR = _get_xdg_dir("XDG_CONFIG_HOME", ".config")
|
|
36
|
+
DATA_DIR = _get_xdg_dir("XDG_DATA_HOME", ".local/share")
|
|
37
|
+
CACHE_DIR = _get_xdg_dir("XDG_CACHE_HOME", ".cache")
|
|
38
|
+
STATE_DIR = _get_xdg_dir("XDG_STATE_HOME", ".local/state")
|
|
39
|
+
|
|
40
|
+
# Configuration files (XDG_CONFIG_HOME)
|
|
41
|
+
CONFIG_FILE = CONFIG_DIR / "muse.cfg"
|
|
42
|
+
|
|
43
|
+
# Config cache to avoid repeated file reads
|
|
44
|
+
_config_cache: tuple[str, float, configparser.ConfigParser | None] = None
|
|
45
|
+
_config_cache_lock = None # Will be initialized lazily; FREE-THREADED: sync-only cache
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_cached_config() -> configparser.ConfigParser:
|
|
49
|
+
"""Return a cached ConfigParser, re-reading only when the file changes."""
|
|
50
|
+
global _config_cache, _config_cache_lock
|
|
51
|
+
import threading
|
|
52
|
+
|
|
53
|
+
if _config_cache_lock is None:
|
|
54
|
+
_config_cache_lock = threading.Lock()
|
|
55
|
+
|
|
56
|
+
cache_key = str(CONFIG_FILE)
|
|
57
|
+
try:
|
|
58
|
+
mtime = CONFIG_FILE.stat().st_mtime
|
|
59
|
+
except OSError:
|
|
60
|
+
mtime = 0.0
|
|
61
|
+
|
|
62
|
+
with _config_cache_lock:
|
|
63
|
+
if _config_cache is not None and _config_cache[0] == cache_key and _config_cache[1] == mtime:
|
|
64
|
+
return _config_cache[2]
|
|
65
|
+
# Cache miss or file changed — reload
|
|
66
|
+
config = configparser.ConfigParser()
|
|
67
|
+
config.read(CONFIG_FILE)
|
|
68
|
+
_config_cache = (cache_key, mtime, config)
|
|
69
|
+
return config
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Data files (XDG_DATA_HOME)
|
|
73
|
+
MODELS_FILE = DATA_DIR / "models.json"
|
|
74
|
+
EXTRA_MODELS_FILE = DATA_DIR / "extra_models.json"
|
|
75
|
+
MODELS_CACHE_FILE = DATA_DIR / "models_cache.json"
|
|
76
|
+
AGENTS_DIR = DATA_DIR / "agents"
|
|
77
|
+
SKILLS_DIR = DATA_DIR / "skills"
|
|
78
|
+
CONTEXTS_DIR = DATA_DIR / "contexts"
|
|
79
|
+
|
|
80
|
+
# OAuth plugin model files (XDG_DATA_HOME)
|
|
81
|
+
GEMINI_MODELS_FILE = DATA_DIR / "gemini_models.json"
|
|
82
|
+
CHATGPT_MODELS_FILE = DATA_DIR / "chatgpt_models.json"
|
|
83
|
+
CLAUDE_MODELS_FILE = DATA_DIR / "claude_models.json"
|
|
84
|
+
COPILOT_MODELS_FILE = DATA_DIR / "copilot_models.json"
|
|
85
|
+
|
|
86
|
+
# Cache files (XDG_CACHE_HOME)
|
|
87
|
+
AUTOSAVE_DIR = CACHE_DIR / "autosaves"
|
|
88
|
+
|
|
89
|
+
# State files (XDG_STATE_HOME)
|
|
90
|
+
COMMAND_HISTORY_FILE = STATE_DIR / "command_history.txt"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_subagent_verbose() -> bool:
|
|
94
|
+
"""Return True if sub-agent verbose output is enabled (default False).
|
|
95
|
+
|
|
96
|
+
When False (default), sub-agents produce quiet, sparse output suitable
|
|
97
|
+
for parallel execution. When True, sub-agents produce full verbose output
|
|
98
|
+
like the main agent (useful for debugging).
|
|
99
|
+
"""
|
|
100
|
+
cfg_val = get_value("subagent_verbose")
|
|
101
|
+
if cfg_val is None:
|
|
102
|
+
return False
|
|
103
|
+
return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Pack agents - the specialized sub-agents coordinated by Pack Leader
|
|
107
|
+
|
|
108
|
+
# Agents that require Universal Constructor to be enabled
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_max_hook_retries() -> int:
|
|
112
|
+
"""Return the maximum number of plugin hook retries after an agent run.
|
|
113
|
+
|
|
114
|
+
When a plugin hook returns ``{"retry": True, ...}`` the agent re-runs.
|
|
115
|
+
This caps how many times that can happen to prevent runaway loops.
|
|
116
|
+
Defaults to 3.
|
|
117
|
+
"""
|
|
118
|
+
val = get_value("max_hook_retries")
|
|
119
|
+
if val is None:
|
|
120
|
+
return 3
|
|
121
|
+
try:
|
|
122
|
+
n = int(val)
|
|
123
|
+
return max(1, n) # At least 1 to avoid nonsensical values
|
|
124
|
+
except ValueError, TypeError:
|
|
125
|
+
return 3
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_enable_streaming() -> bool:
|
|
129
|
+
"""
|
|
130
|
+
Get the enable_streaming configuration value.
|
|
131
|
+
Controls whether streaming (SSE) is used for model responses.
|
|
132
|
+
Returns True if streaming is enabled, False otherwise.
|
|
133
|
+
Defaults to True.
|
|
134
|
+
"""
|
|
135
|
+
val = get_value("enable_streaming")
|
|
136
|
+
if val is None:
|
|
137
|
+
return True # Default to True for better UX
|
|
138
|
+
return str(val).lower() in ("1", "true", "yes", "on")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_auto_approve() -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Get the auto_approve configuration value.
|
|
144
|
+
When True, all user approval prompts are automatically approved
|
|
145
|
+
without showing the interactive menu.
|
|
146
|
+
Defaults to True for a smoother UX.
|
|
147
|
+
"""
|
|
148
|
+
val = get_value("auto_approve")
|
|
149
|
+
if val is None:
|
|
150
|
+
return True
|
|
151
|
+
return str(val).lower() in ("1", "true", "yes", "on")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
DEFAULT_SECTION = "muse"
|
|
155
|
+
REQUIRED_KEYS = ["agent_name", "owner_name"]
|
|
156
|
+
|
|
157
|
+
# Runtime-only autosave session ID (per-process)
|
|
158
|
+
_CURRENT_AUTOSAVE_ID: str | None = None
|
|
159
|
+
|
|
160
|
+
# Session-local model name (initialized from file on first access, then cached)
|
|
161
|
+
_SESSION_MODEL: str | None = None
|
|
162
|
+
|
|
163
|
+
# Cache containers for model validation and defaults
|
|
164
|
+
_model_validation_cache = {}
|
|
165
|
+
_default_model_cache = None
|
|
166
|
+
_default_vision_model_cache = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def ensure_config_exists():
|
|
170
|
+
"""
|
|
171
|
+
Ensure that XDG directories and muse.cfg exist, prompting if needed.
|
|
172
|
+
Returns configparser.ConfigParser for reading.
|
|
173
|
+
"""
|
|
174
|
+
# Create all XDG directories with 0700 permissions per XDG spec
|
|
175
|
+
for directory in [CONFIG_DIR, DATA_DIR, CACHE_DIR, STATE_DIR, SKILLS_DIR]:
|
|
176
|
+
if not directory.exists():
|
|
177
|
+
directory.mkdir(parents=True, mode=0o700, exist_ok=True)
|
|
178
|
+
exists = CONFIG_FILE.is_file()
|
|
179
|
+
config = configparser.ConfigParser()
|
|
180
|
+
if exists:
|
|
181
|
+
config.read(CONFIG_FILE)
|
|
182
|
+
missing = []
|
|
183
|
+
if DEFAULT_SECTION not in config:
|
|
184
|
+
config[DEFAULT_SECTION] = {}
|
|
185
|
+
for key in REQUIRED_KEYS:
|
|
186
|
+
if not config[DEFAULT_SECTION].get(key):
|
|
187
|
+
missing.append(key)
|
|
188
|
+
if missing:
|
|
189
|
+
# Note: Using sys.stdout here for initial setup before messaging system is available
|
|
190
|
+
import sys
|
|
191
|
+
|
|
192
|
+
sys.stdout.write("[Run] Let's get your agent ready.\n")
|
|
193
|
+
sys.stdout.flush()
|
|
194
|
+
for key in missing:
|
|
195
|
+
if key == "agent_name":
|
|
196
|
+
val = input("What should we name the agent? ").strip()
|
|
197
|
+
elif key == "owner_name":
|
|
198
|
+
val = input(
|
|
199
|
+
"What's your name (so Muse knows its owner)? "
|
|
200
|
+
).strip()
|
|
201
|
+
else:
|
|
202
|
+
val = input(f"Enter {key}: ").strip()
|
|
203
|
+
config[DEFAULT_SECTION][key] = val
|
|
204
|
+
|
|
205
|
+
# Set default values for important config keys if they don't exist
|
|
206
|
+
if not config[DEFAULT_SECTION].get("auto_save_session"):
|
|
207
|
+
config[DEFAULT_SECTION]["auto_save_session"] = "true"
|
|
208
|
+
if not config[DEFAULT_SECTION].get("animations_enabled"):
|
|
209
|
+
config[DEFAULT_SECTION]["animations_enabled"] = "true"
|
|
210
|
+
|
|
211
|
+
# Write the config if we made any changes
|
|
212
|
+
if missing or not exists:
|
|
213
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
214
|
+
config.write(f)
|
|
215
|
+
return config
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_value(key: str):
|
|
219
|
+
config = _get_cached_config()
|
|
220
|
+
val = config.get(DEFAULT_SECTION, key, fallback=None)
|
|
221
|
+
return val
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_agent_name():
|
|
225
|
+
return get_value("agent_name") or "Muse"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def get_owner_name():
|
|
229
|
+
return get_value("owner_name") or "Creator"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_animations_enabled() -> bool:
|
|
233
|
+
"""Return whether terminal animations are enabled.
|
|
234
|
+
|
|
235
|
+
Defaults to True if not configured.
|
|
236
|
+
"""
|
|
237
|
+
val = get_value("animations_enabled")
|
|
238
|
+
if val is None:
|
|
239
|
+
return True
|
|
240
|
+
return val.lower() in ("true", "1", "yes", "on")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# Legacy function removed - message history limit is no longer used
|
|
244
|
+
# Message history is now managed by token-based compaction system
|
|
245
|
+
# using get_protected_token_count() and get_summarization_threshold()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def get_allow_recursion() -> bool:
|
|
249
|
+
"""
|
|
250
|
+
Get the allow_recursion configuration value.
|
|
251
|
+
Returns True if recursion is allowed, False otherwise.
|
|
252
|
+
"""
|
|
253
|
+
val = get_value("allow_recursion")
|
|
254
|
+
if val is None:
|
|
255
|
+
return True # Default to True to allow recursion unless explicitly disabled
|
|
256
|
+
return str(val).lower() in ("1", "true", "yes", "on")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def get_model_context_length() -> int:
|
|
260
|
+
"""
|
|
261
|
+
Get the context length for the currently configured model from models.json
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
from code_muse.model_factory import ModelFactory
|
|
265
|
+
|
|
266
|
+
model_configs = ModelFactory.load_config()
|
|
267
|
+
model_name = get_global_model_name()
|
|
268
|
+
|
|
269
|
+
# Get context length from model config
|
|
270
|
+
model_config = model_configs.get(model_name, {})
|
|
271
|
+
context_length = model_config.get("context_length", 128000) # Default value
|
|
272
|
+
|
|
273
|
+
return int(context_length)
|
|
274
|
+
except Exception:
|
|
275
|
+
# Fallback to default context length if anything goes wrong
|
|
276
|
+
return 128000
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# --- CONFIG SETTER STARTS HERE ---
|
|
280
|
+
def get_config_keys():
|
|
281
|
+
"""
|
|
282
|
+
Returns the list of all config keys currently in muse.cfg,
|
|
283
|
+
plus certain preset expected keys (e.g. "yolo_mode", "model", "compaction_strategy", "message_limit", "allow_recursion").
|
|
284
|
+
"""
|
|
285
|
+
default_keys = [
|
|
286
|
+
"yolo_mode",
|
|
287
|
+
"model",
|
|
288
|
+
"compaction_strategy",
|
|
289
|
+
"protected_token_count",
|
|
290
|
+
"compaction_threshold",
|
|
291
|
+
"summarization_model",
|
|
292
|
+
"message_limit",
|
|
293
|
+
"allow_recursion",
|
|
294
|
+
"openai_reasoning_effort",
|
|
295
|
+
"openai_reasoning_summary",
|
|
296
|
+
"openai_verbosity",
|
|
297
|
+
"auto_save_session",
|
|
298
|
+
"max_saved_sessions",
|
|
299
|
+
"http2",
|
|
300
|
+
"diff_context_lines",
|
|
301
|
+
"default_agent",
|
|
302
|
+
"temperature",
|
|
303
|
+
"frontend_emitter_enabled",
|
|
304
|
+
"frontend_emitter_max_recent_events",
|
|
305
|
+
"frontend_emitter_queue_size",
|
|
306
|
+
"auto_approve",
|
|
307
|
+
]
|
|
308
|
+
# Add pack agents control key
|
|
309
|
+
default_keys.append("enable_pack_agents")
|
|
310
|
+
# Add universal constructor control key
|
|
311
|
+
default_keys.append("enable_universal_constructor")
|
|
312
|
+
# Add hook retry limit key
|
|
313
|
+
default_keys.append("max_hook_retries")
|
|
314
|
+
# Add streaming control key
|
|
315
|
+
default_keys.append("enable_streaming")
|
|
316
|
+
# Add cancel agent key configuration
|
|
317
|
+
default_keys.append("cancel_agent_key")
|
|
318
|
+
# Add banner color keys
|
|
319
|
+
for banner_name in DEFAULT_BANNER_COLORS:
|
|
320
|
+
default_keys.append(f"banner_color_{banner_name}")
|
|
321
|
+
# Add resume message count configuration
|
|
322
|
+
default_keys.append("resume_message_count")
|
|
323
|
+
|
|
324
|
+
config = configparser.ConfigParser()
|
|
325
|
+
config.read(CONFIG_FILE)
|
|
326
|
+
keys = set(config[DEFAULT_SECTION].keys()) if DEFAULT_SECTION in config else set()
|
|
327
|
+
keys.update(default_keys)
|
|
328
|
+
return sorted(keys)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def set_config_value(key: str, value: str):
|
|
332
|
+
"""
|
|
333
|
+
Sets a config value in the persistent config file.
|
|
334
|
+
"""
|
|
335
|
+
global _config_cache
|
|
336
|
+
config = configparser.ConfigParser()
|
|
337
|
+
config.read(CONFIG_FILE)
|
|
338
|
+
if DEFAULT_SECTION not in config:
|
|
339
|
+
config[DEFAULT_SECTION] = {}
|
|
340
|
+
config[DEFAULT_SECTION][key] = value
|
|
341
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
342
|
+
config.write(f)
|
|
343
|
+
# Invalidate cache so subsequent reads pick up the change immediately
|
|
344
|
+
_config_cache = None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# Alias for API compatibility
|
|
348
|
+
def set_value(key: str, value: str) -> None:
|
|
349
|
+
"""Set a config value. Alias for set_config_value."""
|
|
350
|
+
set_config_value(key, value)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def reset_value(key: str) -> None:
|
|
354
|
+
"""Remove a key from the config file, resetting it to default."""
|
|
355
|
+
global _config_cache
|
|
356
|
+
config = configparser.ConfigParser()
|
|
357
|
+
config.read(CONFIG_FILE)
|
|
358
|
+
if DEFAULT_SECTION in config and key in config[DEFAULT_SECTION]:
|
|
359
|
+
del config[DEFAULT_SECTION][key]
|
|
360
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
361
|
+
config.write(f)
|
|
362
|
+
# Invalidate cache so subsequent reads pick up the change immediately
|
|
363
|
+
_config_cache = None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# --- MODEL STICKY EXTENSION STARTS HERE ---
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _default_model_from_models_json():
|
|
370
|
+
"""Load the default model name from models.json.
|
|
371
|
+
|
|
372
|
+
Returns the first model in models.json as the default.
|
|
373
|
+
Falls back to ``gpt-5`` if the file cannot be read.
|
|
374
|
+
"""
|
|
375
|
+
global _default_model_cache
|
|
376
|
+
|
|
377
|
+
if _default_model_cache is not None:
|
|
378
|
+
return _default_model_cache
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
from code_muse.model_factory import ModelFactory
|
|
382
|
+
|
|
383
|
+
models_config = ModelFactory.load_config()
|
|
384
|
+
if models_config:
|
|
385
|
+
# Use first model in models.json as default
|
|
386
|
+
first_key = next(iter(models_config))
|
|
387
|
+
_default_model_cache = first_key
|
|
388
|
+
return first_key
|
|
389
|
+
_default_model_cache = "gpt-5"
|
|
390
|
+
return "gpt-5"
|
|
391
|
+
except Exception:
|
|
392
|
+
_default_model_cache = "gpt-5"
|
|
393
|
+
return "gpt-5"
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _default_vision_model_from_models_json() -> str:
|
|
397
|
+
"""Select a default vision-capable model from models.json with caching."""
|
|
398
|
+
global _default_vision_model_cache
|
|
399
|
+
|
|
400
|
+
if _default_vision_model_cache is not None:
|
|
401
|
+
return _default_vision_model_cache
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
from code_muse.model_factory import ModelFactory
|
|
405
|
+
|
|
406
|
+
models_config = ModelFactory.load_config()
|
|
407
|
+
if models_config:
|
|
408
|
+
# Prefer explicitly tagged vision models
|
|
409
|
+
for name, config in models_config.items():
|
|
410
|
+
if config.get("supports_vision"):
|
|
411
|
+
_default_vision_model_cache = name
|
|
412
|
+
return name
|
|
413
|
+
|
|
414
|
+
# Fallback heuristic: common multimodal models
|
|
415
|
+
preferred_candidates = (
|
|
416
|
+
"gpt-4.1",
|
|
417
|
+
"gpt-4.1-mini",
|
|
418
|
+
"gpt-4.1-nano",
|
|
419
|
+
"claude-4-0-sonnet",
|
|
420
|
+
"gemini-2.5-flash-preview-05-20",
|
|
421
|
+
)
|
|
422
|
+
for candidate in preferred_candidates:
|
|
423
|
+
if candidate in models_config:
|
|
424
|
+
_default_vision_model_cache = candidate
|
|
425
|
+
return candidate
|
|
426
|
+
|
|
427
|
+
# Last resort: use the general default model
|
|
428
|
+
_default_vision_model_cache = _default_model_from_models_json()
|
|
429
|
+
return _default_vision_model_cache
|
|
430
|
+
|
|
431
|
+
_default_vision_model_cache = "gpt-4.1"
|
|
432
|
+
return "gpt-4.1"
|
|
433
|
+
except Exception:
|
|
434
|
+
_default_vision_model_cache = "gpt-4.1"
|
|
435
|
+
return "gpt-4.1"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _validate_model_exists(model_name: str) -> bool:
|
|
439
|
+
"""Check if a model exists in models.json with caching to avoid redundant calls."""
|
|
440
|
+
global _model_validation_cache
|
|
441
|
+
|
|
442
|
+
# Check cache first
|
|
443
|
+
if model_name in _model_validation_cache:
|
|
444
|
+
return _model_validation_cache[model_name]
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
from code_muse.model_factory import ModelFactory
|
|
448
|
+
|
|
449
|
+
models_config = ModelFactory.load_config()
|
|
450
|
+
exists = model_name in models_config
|
|
451
|
+
|
|
452
|
+
# Cache the result
|
|
453
|
+
_model_validation_cache[model_name] = exists
|
|
454
|
+
return exists
|
|
455
|
+
except Exception:
|
|
456
|
+
# If we can't validate, assume it exists to avoid breaking things
|
|
457
|
+
_model_validation_cache[model_name] = True
|
|
458
|
+
return True
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def clear_model_cache():
|
|
462
|
+
"""Clear the model validation cache. Call this when models.json changes."""
|
|
463
|
+
global _model_validation_cache, _default_model_cache, _default_vision_model_cache
|
|
464
|
+
_model_validation_cache.clear()
|
|
465
|
+
_default_model_cache = None
|
|
466
|
+
_default_vision_model_cache = None
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def reset_session_model():
|
|
470
|
+
"""Reset the session-local model cache.
|
|
471
|
+
|
|
472
|
+
This is primarily for testing purposes. In normal operation, the session
|
|
473
|
+
model is set once at startup and only changes via set_model_name().
|
|
474
|
+
"""
|
|
475
|
+
global _SESSION_MODEL
|
|
476
|
+
_SESSION_MODEL = None
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def model_supports_setting(model_name: str, setting: str) -> bool:
|
|
480
|
+
"""Check if a model supports a particular setting (e.g., 'temperature', 'seed').
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
model_name: The name of the model to check.
|
|
484
|
+
setting: The setting name to check for (e.g., 'temperature', 'seed', 'top_p').
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
True if the model supports the setting, False otherwise.
|
|
488
|
+
Defaults to True for backwards compatibility if model config doesn't specify.
|
|
489
|
+
"""
|
|
490
|
+
# GLM-4.7 and GLM-5 models always support clear_thinking setting
|
|
491
|
+
if setting == "clear_thinking" and (
|
|
492
|
+
"glm-4.7" in model_name.lower() or "glm-5" in model_name.lower()
|
|
493
|
+
):
|
|
494
|
+
return True
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
from code_muse.model_factory import ModelFactory
|
|
498
|
+
|
|
499
|
+
models_config = ModelFactory.load_config()
|
|
500
|
+
model_config = models_config.get(model_name, {})
|
|
501
|
+
|
|
502
|
+
# Get supported_settings list, default to supporting common settings
|
|
503
|
+
supported_settings = model_config.get("supported_settings")
|
|
504
|
+
|
|
505
|
+
if supported_settings is None:
|
|
506
|
+
# Default: assume common settings are supported for backwards compatibility
|
|
507
|
+
# For Anthropic/Claude models, include extended thinking settings
|
|
508
|
+
if model_name.startswith("claude-") or model_name.startswith("anthropic-"):
|
|
509
|
+
base = ["temperature", "extended_thinking", "budget_tokens"]
|
|
510
|
+
from code_muse.model_utils import supports_adaptive_thinking
|
|
511
|
+
|
|
512
|
+
if supports_adaptive_thinking(model_name):
|
|
513
|
+
base.append("effort")
|
|
514
|
+
return setting in base
|
|
515
|
+
return setting in ["temperature", "seed"]
|
|
516
|
+
|
|
517
|
+
return setting in supported_settings
|
|
518
|
+
except Exception:
|
|
519
|
+
# If we can't check, assume supported for safety
|
|
520
|
+
return True
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def get_global_model_name():
|
|
524
|
+
"""Return a valid model name for Muse to use.
|
|
525
|
+
|
|
526
|
+
Uses session-local caching so that model changes in other terminals
|
|
527
|
+
don't affect this running instance. The file is only read once at startup.
|
|
528
|
+
|
|
529
|
+
1. If _SESSION_MODEL is set, return it (session cache)
|
|
530
|
+
2. Otherwise, look at ``model`` in *muse.cfg*
|
|
531
|
+
3. If that value exists **and** is present in *models.json*, use it
|
|
532
|
+
4. Otherwise return the first model listed in *models.json*
|
|
533
|
+
5. As a last resort fall back to ``claude-4-0-sonnet``
|
|
534
|
+
|
|
535
|
+
The result is cached in _SESSION_MODEL for subsequent calls.
|
|
536
|
+
"""
|
|
537
|
+
global _SESSION_MODEL
|
|
538
|
+
|
|
539
|
+
# Return cached session model if already initialized
|
|
540
|
+
if _SESSION_MODEL is not None:
|
|
541
|
+
return _SESSION_MODEL
|
|
542
|
+
|
|
543
|
+
# First access - initialize from file
|
|
544
|
+
stored_model = get_value("model")
|
|
545
|
+
|
|
546
|
+
if stored_model:
|
|
547
|
+
# Use cached validation to avoid hitting ModelFactory every time
|
|
548
|
+
if _validate_model_exists(stored_model):
|
|
549
|
+
_SESSION_MODEL = stored_model
|
|
550
|
+
return _SESSION_MODEL
|
|
551
|
+
|
|
552
|
+
# Either no stored model or it's not valid – choose default from models.json
|
|
553
|
+
_SESSION_MODEL = _default_model_from_models_json()
|
|
554
|
+
return _SESSION_MODEL
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def set_model_name(model: str):
|
|
558
|
+
"""Sets the model name in both the session cache and persistent config file.
|
|
559
|
+
|
|
560
|
+
Updates _SESSION_MODEL immediately for this process, and writes to the
|
|
561
|
+
config file so new terminals will pick up this model as their default.
|
|
562
|
+
"""
|
|
563
|
+
global _SESSION_MODEL
|
|
564
|
+
|
|
565
|
+
# Update session cache immediately
|
|
566
|
+
_SESSION_MODEL = model
|
|
567
|
+
|
|
568
|
+
# Also persist to file for new terminal sessions
|
|
569
|
+
config = configparser.ConfigParser()
|
|
570
|
+
config.read(CONFIG_FILE)
|
|
571
|
+
if DEFAULT_SECTION not in config:
|
|
572
|
+
config[DEFAULT_SECTION] = {}
|
|
573
|
+
config[DEFAULT_SECTION]["model"] = model or ""
|
|
574
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
575
|
+
config.write(f)
|
|
576
|
+
|
|
577
|
+
# Clear model cache when switching models to ensure fresh validation
|
|
578
|
+
clear_model_cache()
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# --- PER-MODEL SETTINGS ---
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
# Legacy functions for backward compatibility
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def normalize_command_history():
|
|
588
|
+
"""
|
|
589
|
+
Normalize the command history file by converting old format timestamps to the new format.
|
|
590
|
+
|
|
591
|
+
Old format example:
|
|
592
|
+
- "# 2025-08-04 12:44:45.469829"
|
|
593
|
+
|
|
594
|
+
New format example:
|
|
595
|
+
- "# 2025-08-05T10:35:33" (ISO)
|
|
596
|
+
"""
|
|
597
|
+
import os
|
|
598
|
+
import re
|
|
599
|
+
|
|
600
|
+
# Skip implementation during tests
|
|
601
|
+
import sys
|
|
602
|
+
|
|
603
|
+
if "pytest" in sys.modules:
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
# Skip normalization if file doesn't exist
|
|
607
|
+
command_history_exists = COMMAND_HISTORY_FILE.is_file()
|
|
608
|
+
if not command_history_exists:
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
try:
|
|
612
|
+
# Read the entire file with encoding error handling for Windows
|
|
613
|
+
with open(
|
|
614
|
+
COMMAND_HISTORY_FILE, encoding="utf-8", errors="surrogateescape"
|
|
615
|
+
) as f:
|
|
616
|
+
content = f.read()
|
|
617
|
+
|
|
618
|
+
# Sanitize any surrogate characters that might have slipped in
|
|
619
|
+
try:
|
|
620
|
+
content = content.encode("utf-8", errors="surrogatepass").decode(
|
|
621
|
+
"utf-8", errors="replace"
|
|
622
|
+
)
|
|
623
|
+
except UnicodeEncodeError, UnicodeDecodeError:
|
|
624
|
+
pass # Keep original if sanitization fails
|
|
625
|
+
|
|
626
|
+
# Skip empty files
|
|
627
|
+
if not content.strip():
|
|
628
|
+
return
|
|
629
|
+
|
|
630
|
+
# Define regex pattern for old timestamp format
|
|
631
|
+
# Format: "# YYYY-MM-DD HH:MM:SS.ffffff"
|
|
632
|
+
old_timestamp_pattern = r"# (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\.(\d+)"
|
|
633
|
+
|
|
634
|
+
# Function to convert matched timestamp to ISO format
|
|
635
|
+
def convert_to_iso(match):
|
|
636
|
+
date = match.group(1)
|
|
637
|
+
time = match.group(2)
|
|
638
|
+
# Create ISO format (YYYY-MM-DDThh:mm:ss)
|
|
639
|
+
return f"# {date}T{time}"
|
|
640
|
+
|
|
641
|
+
# Replace all occurrences of the old timestamp format with the new ISO format
|
|
642
|
+
updated_content = re.sub(old_timestamp_pattern, convert_to_iso, content)
|
|
643
|
+
|
|
644
|
+
# Write the updated content back to the file only if changes were made
|
|
645
|
+
if content != updated_content:
|
|
646
|
+
import tempfile
|
|
647
|
+
|
|
648
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
649
|
+
dir=str(COMMAND_HISTORY_FILE.parent), suffix=".tmp"
|
|
650
|
+
)
|
|
651
|
+
try:
|
|
652
|
+
with os.fdopen(
|
|
653
|
+
fd, "w", encoding="utf-8", errors="surrogateescape"
|
|
654
|
+
) as f:
|
|
655
|
+
f.write(updated_content)
|
|
656
|
+
os.replace(tmp_path, COMMAND_HISTORY_FILE)
|
|
657
|
+
except BaseException:
|
|
658
|
+
with contextlib.suppress(OSError):
|
|
659
|
+
os.unlink(tmp_path)
|
|
660
|
+
raise
|
|
661
|
+
except Exception as e:
|
|
662
|
+
from code_muse.messaging import emit_error
|
|
663
|
+
|
|
664
|
+
emit_error(
|
|
665
|
+
f"An unexpected error occurred while normalizing command history: {str(e)}"
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def initialize_command_history_file():
|
|
670
|
+
"""Create the command history file if it doesn't exist.
|
|
671
|
+
Handles migration from the old history file location for backward compatibility.
|
|
672
|
+
Also normalizes the command history format if needed.
|
|
673
|
+
"""
|
|
674
|
+
from pathlib import Path
|
|
675
|
+
|
|
676
|
+
# Ensure the state directory exists before trying to create the history file
|
|
677
|
+
if not STATE_DIR.exists():
|
|
678
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
679
|
+
|
|
680
|
+
command_history_exists = COMMAND_HISTORY_FILE.is_file()
|
|
681
|
+
if not command_history_exists:
|
|
682
|
+
try:
|
|
683
|
+
COMMAND_HISTORY_FILE.touch()
|
|
684
|
+
|
|
685
|
+
# For backwards compatibility, copy the old history file, then remove it
|
|
686
|
+
old_history_file = Path.home() / ".muse_history.txt"
|
|
687
|
+
old_history_exists = old_history_file.is_file()
|
|
688
|
+
if old_history_exists:
|
|
689
|
+
import shutil
|
|
690
|
+
|
|
691
|
+
shutil.copy2(old_history_file, COMMAND_HISTORY_FILE)
|
|
692
|
+
old_history_file.unlink(missing_ok=True)
|
|
693
|
+
|
|
694
|
+
# Normalize the command history format if needed
|
|
695
|
+
normalize_command_history()
|
|
696
|
+
except Exception as e:
|
|
697
|
+
from code_muse.messaging import emit_error
|
|
698
|
+
|
|
699
|
+
emit_error(
|
|
700
|
+
f"An unexpected error occurred while trying to initialize history file: {str(e)}"
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def get_yolo_mode():
|
|
705
|
+
"""
|
|
706
|
+
Checks muse.cfg for 'yolo_mode' (case-insensitive in value only).
|
|
707
|
+
Defaults to False (safe mode) if not set.
|
|
708
|
+
Allowed values for ON: 1, '1', 'true', 'yes', 'on' (all case-insensitive for value).
|
|
709
|
+
"""
|
|
710
|
+
true_vals = {"1", "true", "yes", "on"}
|
|
711
|
+
cfg_val = get_value("yolo_mode")
|
|
712
|
+
if cfg_val is not None:
|
|
713
|
+
return str(cfg_val).strip().lower() in true_vals
|
|
714
|
+
return False
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def get_safety_permission_level():
|
|
718
|
+
"""
|
|
719
|
+
Checks muse.cfg for 'safety_permission_level' (case-insensitive in value only).
|
|
720
|
+
Defaults to 'medium' if not set.
|
|
721
|
+
Allowed values: 'none', 'low', 'medium', 'high', 'critical' (all case-insensitive for value).
|
|
722
|
+
Returns the normalized lowercase string.
|
|
723
|
+
"""
|
|
724
|
+
valid_levels = {"none", "low", "medium", "high", "critical"}
|
|
725
|
+
cfg_val = get_value("safety_permission_level")
|
|
726
|
+
if cfg_val is not None:
|
|
727
|
+
normalized = str(cfg_val).strip().lower()
|
|
728
|
+
if normalized in valid_levels:
|
|
729
|
+
return normalized
|
|
730
|
+
return "medium" # Default to medium risk threshold
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def get_grep_output_verbose():
|
|
734
|
+
"""
|
|
735
|
+
Checks muse.cfg for 'grep_output_verbose' (case-insensitive in value only).
|
|
736
|
+
Defaults to False (concise output) if not set.
|
|
737
|
+
Allowed values for ON: 1, '1', 'true', 'yes', 'on' (all case-insensitive for value).
|
|
738
|
+
|
|
739
|
+
When False (default): Shows only file names with match counts
|
|
740
|
+
When True: Shows full output with line numbers and content
|
|
741
|
+
"""
|
|
742
|
+
true_vals = {"1", "true", "yes", "on"}
|
|
743
|
+
cfg_val = get_value("grep_output_verbose")
|
|
744
|
+
if cfg_val is not None:
|
|
745
|
+
return str(cfg_val).strip().lower() in true_vals
|
|
746
|
+
return False
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def get_protected_token_count():
|
|
750
|
+
"""
|
|
751
|
+
Returns the user-configured protected token count for message history compaction.
|
|
752
|
+
This is the number of tokens in recent messages that won't be summarized.
|
|
753
|
+
Defaults to 50000 if unset or misconfigured.
|
|
754
|
+
Configurable by 'protected_token_count' key.
|
|
755
|
+
Enforces that protected tokens don't exceed 75% of model context length.
|
|
756
|
+
"""
|
|
757
|
+
val = get_value("protected_token_count")
|
|
758
|
+
try:
|
|
759
|
+
# Get the model context length to enforce the 75% limit
|
|
760
|
+
model_context_length = get_model_context_length()
|
|
761
|
+
max_protected_tokens = int(model_context_length * 0.75)
|
|
762
|
+
|
|
763
|
+
# Parse the configured value
|
|
764
|
+
configured_value = int(val) if val else 50000
|
|
765
|
+
|
|
766
|
+
# Apply constraints: minimum 1000, maximum 75% of context length
|
|
767
|
+
return max(1000, min(configured_value, max_protected_tokens))
|
|
768
|
+
except ValueError, TypeError:
|
|
769
|
+
# If parsing fails, return a reasonable default that respects the 75% limit
|
|
770
|
+
model_context_length = get_model_context_length()
|
|
771
|
+
max_protected_tokens = int(model_context_length * 0.75)
|
|
772
|
+
return min(50000, max_protected_tokens)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def get_resume_message_count() -> int:
|
|
776
|
+
"""
|
|
777
|
+
Returns the number of messages to display when resuming a session.
|
|
778
|
+
Defaults to 50 if unset or misconfigured.
|
|
779
|
+
Configurable by 'resume_message_count' key via /set command.
|
|
780
|
+
|
|
781
|
+
Example: /set resume_message_count=30
|
|
782
|
+
"""
|
|
783
|
+
val = get_value("resume_message_count")
|
|
784
|
+
try:
|
|
785
|
+
configured_value = int(val) if val else 50
|
|
786
|
+
# Enforce reasonable bounds: minimum 1, maximum 100
|
|
787
|
+
return max(1, min(configured_value, 100))
|
|
788
|
+
except ValueError, TypeError:
|
|
789
|
+
return 50
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def get_compaction_threshold():
|
|
793
|
+
"""
|
|
794
|
+
Returns the user-configured compaction threshold as a float between 0.0 and 1.0.
|
|
795
|
+
This is the proportion of model context that triggers compaction.
|
|
796
|
+
Defaults to 0.85 (85%) if unset or misconfigured.
|
|
797
|
+
Configurable by 'compaction_threshold' key.
|
|
798
|
+
"""
|
|
799
|
+
val = get_value("compaction_threshold")
|
|
800
|
+
try:
|
|
801
|
+
threshold = float(val) if val else 0.85
|
|
802
|
+
# Clamp between reasonable bounds
|
|
803
|
+
return max(0.5, min(0.95, threshold))
|
|
804
|
+
except ValueError, TypeError:
|
|
805
|
+
return 0.85
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def get_compaction_strategy() -> str:
|
|
809
|
+
"""
|
|
810
|
+
Returns the user-configured compaction strategy.
|
|
811
|
+
Options are 'summarization' or 'truncation'.
|
|
812
|
+
Defaults to 'summarization' if not set or misconfigured.
|
|
813
|
+
Configurable by 'compaction_strategy' key.
|
|
814
|
+
"""
|
|
815
|
+
val = get_value("compaction_strategy")
|
|
816
|
+
if val and val.lower() in ["summarization", "truncation"]:
|
|
817
|
+
return val.lower()
|
|
818
|
+
# Default to summarization
|
|
819
|
+
return "truncation"
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def get_http2() -> bool:
|
|
823
|
+
"""
|
|
824
|
+
Get the http2 configuration value.
|
|
825
|
+
Returns False if not set (default).
|
|
826
|
+
"""
|
|
827
|
+
val = get_value("http2")
|
|
828
|
+
if val is None:
|
|
829
|
+
return False
|
|
830
|
+
return str(val).lower() in ("1", "true", "yes", "on")
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def set_http2(enabled: bool) -> None:
|
|
834
|
+
"""
|
|
835
|
+
Sets the http2 configuration value.
|
|
836
|
+
|
|
837
|
+
Args:
|
|
838
|
+
enabled: Whether to enable HTTP/2 for httpx clients
|
|
839
|
+
"""
|
|
840
|
+
set_config_value("http2", "true" if enabled else "false")
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def get_message_limit(default: int = 1000) -> int:
|
|
844
|
+
"""
|
|
845
|
+
Returns the user-configured message/request limit for the agent.
|
|
846
|
+
This controls how many steps/requests the agent can take.
|
|
847
|
+
Defaults to 1000 if unset or misconfigured.
|
|
848
|
+
Configurable by 'message_limit' key.
|
|
849
|
+
"""
|
|
850
|
+
val = get_value("message_limit")
|
|
851
|
+
try:
|
|
852
|
+
return int(val) if val else default
|
|
853
|
+
except ValueError, TypeError:
|
|
854
|
+
return default
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def save_command_to_history(command: str):
|
|
858
|
+
"""Save a command to the history file with an ISO format timestamp.
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
command: The command to save
|
|
862
|
+
"""
|
|
863
|
+
import datetime
|
|
864
|
+
|
|
865
|
+
try:
|
|
866
|
+
timestamp = datetime.datetime.now().isoformat(timespec="seconds")
|
|
867
|
+
|
|
868
|
+
# Sanitize command to remove any invalid surrogate characters
|
|
869
|
+
# that could cause encoding errors on Windows
|
|
870
|
+
try:
|
|
871
|
+
command = command.encode("utf-8", errors="surrogatepass").decode(
|
|
872
|
+
"utf-8", errors="replace"
|
|
873
|
+
)
|
|
874
|
+
except UnicodeEncodeError, UnicodeDecodeError:
|
|
875
|
+
# If that fails, do a more aggressive cleanup
|
|
876
|
+
command = "".join(
|
|
877
|
+
char if ord(char) < 0xD800 or ord(char) > 0xDFFF else "\ufffd"
|
|
878
|
+
for char in command
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
with open(
|
|
882
|
+
COMMAND_HISTORY_FILE, "a", encoding="utf-8", errors="surrogateescape"
|
|
883
|
+
) as f:
|
|
884
|
+
f.write(f"\n# {timestamp}\n{command}\n")
|
|
885
|
+
except Exception as e:
|
|
886
|
+
from code_muse.messaging import emit_error
|
|
887
|
+
|
|
888
|
+
emit_error(
|
|
889
|
+
f"An unexpected error occurred while saving command history: {str(e)}"
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def get_auto_save_session() -> bool:
|
|
894
|
+
"""
|
|
895
|
+
Checks muse.cfg for 'auto_save_session' (case-insensitive in value only).
|
|
896
|
+
Defaults to True if not set.
|
|
897
|
+
Allowed values for ON: 1, '1', 'true', 'yes', 'on' (all case-insensitive for value).
|
|
898
|
+
"""
|
|
899
|
+
true_vals = {"1", "true", "yes", "on"}
|
|
900
|
+
cfg_val = get_value("auto_save_session")
|
|
901
|
+
if cfg_val is not None:
|
|
902
|
+
return str(cfg_val).strip().lower() in true_vals
|
|
903
|
+
return True
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def set_auto_save_session(enabled: bool):
|
|
907
|
+
"""Sets the auto_save_session configuration value.
|
|
908
|
+
|
|
909
|
+
Args:
|
|
910
|
+
enabled: Whether to enable auto-saving of sessions
|
|
911
|
+
"""
|
|
912
|
+
set_config_value("auto_save_session", "true" if enabled else "false")
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def get_max_saved_sessions() -> int:
|
|
916
|
+
"""
|
|
917
|
+
Gets the maximum number of sessions to keep.
|
|
918
|
+
Defaults to 20 if not set.
|
|
919
|
+
"""
|
|
920
|
+
cfg_val = get_value("max_saved_sessions")
|
|
921
|
+
if cfg_val is not None:
|
|
922
|
+
try:
|
|
923
|
+
val = int(cfg_val)
|
|
924
|
+
return max(0, val) # Ensure non-negative
|
|
925
|
+
except ValueError, TypeError:
|
|
926
|
+
pass
|
|
927
|
+
return 20
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def set_max_saved_sessions(max_sessions: int):
|
|
931
|
+
"""Sets the max_saved_sessions configuration value.
|
|
932
|
+
|
|
933
|
+
Args:
|
|
934
|
+
max_sessions: Maximum number of sessions to keep (0 for unlimited)
|
|
935
|
+
"""
|
|
936
|
+
set_config_value("max_saved_sessions", str(max_sessions))
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
# Defaults for diff highlight colors — single source of truth.
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
# =============================================================================
|
|
943
|
+
# Banner Color Configuration
|
|
944
|
+
# =============================================================================
|
|
945
|
+
|
|
946
|
+
# Default banner colors (Rich color names)
|
|
947
|
+
# A beautiful jewel-tone palette with semantic meaning:
|
|
948
|
+
# - Blues/Teals: Reading & navigation (calm, informational)
|
|
949
|
+
# - Warm tones: Actions & changes (edits, shell commands)
|
|
950
|
+
# - Purples: AI thinking & reasoning (the "brain" colors)
|
|
951
|
+
# - Greens: Completions & success
|
|
952
|
+
# - Neutrals: Search & listings
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def get_current_autosave_id() -> str:
|
|
956
|
+
"""Get or create the current autosave session ID for this process."""
|
|
957
|
+
global _CURRENT_AUTOSAVE_ID
|
|
958
|
+
if not _CURRENT_AUTOSAVE_ID:
|
|
959
|
+
# Use a full timestamp so tests and UX can predict the name if needed
|
|
960
|
+
_CURRENT_AUTOSAVE_ID = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
961
|
+
return _CURRENT_AUTOSAVE_ID
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def rotate_autosave_id() -> str:
|
|
965
|
+
"""Force a new autosave session ID and return it."""
|
|
966
|
+
global _CURRENT_AUTOSAVE_ID
|
|
967
|
+
_CURRENT_AUTOSAVE_ID = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
968
|
+
return _CURRENT_AUTOSAVE_ID
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def get_current_autosave_session_name() -> str:
|
|
972
|
+
"""Return the full session name used for autosaves (no file extension)."""
|
|
973
|
+
return f"auto_session_{get_current_autosave_id()}"
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def set_current_autosave_from_session_name(session_name: str) -> str:
|
|
977
|
+
"""Set the current autosave ID based on a full session name.
|
|
978
|
+
|
|
979
|
+
Accepts names like 'auto_session_YYYYMMDD_HHMMSS' and extracts the ID part.
|
|
980
|
+
Returns the ID that was set.
|
|
981
|
+
"""
|
|
982
|
+
global _CURRENT_AUTOSAVE_ID
|
|
983
|
+
prefix = "auto_session_"
|
|
984
|
+
if session_name.startswith(prefix):
|
|
985
|
+
_CURRENT_AUTOSAVE_ID = session_name[len(prefix) :]
|
|
986
|
+
else:
|
|
987
|
+
_CURRENT_AUTOSAVE_ID = session_name
|
|
988
|
+
return _CURRENT_AUTOSAVE_ID
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def auto_save_session_if_enabled() -> bool:
|
|
992
|
+
"""Automatically save the current session if auto_save_session is enabled."""
|
|
993
|
+
if not get_auto_save_session():
|
|
994
|
+
return False
|
|
995
|
+
|
|
996
|
+
try:
|
|
997
|
+
import pathlib
|
|
998
|
+
|
|
999
|
+
from code_muse.agents.agent_manager import get_current_agent
|
|
1000
|
+
from code_muse.messaging import emit_info
|
|
1001
|
+
|
|
1002
|
+
current_agent = get_current_agent()
|
|
1003
|
+
history = current_agent.get_message_history()
|
|
1004
|
+
if not history:
|
|
1005
|
+
return False
|
|
1006
|
+
|
|
1007
|
+
now = datetime.datetime.now()
|
|
1008
|
+
session_name = get_current_autosave_session_name()
|
|
1009
|
+
autosave_dir = pathlib.Path(AUTOSAVE_DIR)
|
|
1010
|
+
|
|
1011
|
+
metadata = save_session(
|
|
1012
|
+
history=history,
|
|
1013
|
+
session_name=session_name,
|
|
1014
|
+
base_dir=autosave_dir,
|
|
1015
|
+
timestamp=now.isoformat(),
|
|
1016
|
+
token_estimator=current_agent.estimate_tokens_for_message,
|
|
1017
|
+
auto_saved=True,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
emit_info(
|
|
1021
|
+
f"[Done] Auto-saved session: {metadata.message_count} messages ({metadata.total_tokens} tokens)"
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
return True
|
|
1025
|
+
|
|
1026
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
1027
|
+
from code_muse.messaging import emit_error
|
|
1028
|
+
|
|
1029
|
+
emit_error(f"Failed to auto-save session: {exc}")
|
|
1030
|
+
return False
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def finalize_autosave_session() -> str:
|
|
1034
|
+
"""Persist the current autosave snapshot and rotate to a fresh session."""
|
|
1035
|
+
auto_save_session_if_enabled()
|
|
1036
|
+
return rotate_autosave_id()
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
# API Key management functions
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
# --- FRONTEND EMITTER CONFIGURATION ---
|
|
1043
|
+
def get_frontend_emitter_enabled() -> bool:
|
|
1044
|
+
"""Check if frontend emitter is enabled."""
|
|
1045
|
+
val = get_value("frontend_emitter_enabled")
|
|
1046
|
+
if val is None:
|
|
1047
|
+
return True # Enabled by default
|
|
1048
|
+
return str(val).lower() in ("1", "true", "yes", "on")
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def get_frontend_emitter_max_recent_events() -> int:
|
|
1052
|
+
"""Get max number of recent events to buffer."""
|
|
1053
|
+
val = get_value("frontend_emitter_max_recent_events")
|
|
1054
|
+
if val is None:
|
|
1055
|
+
return 100
|
|
1056
|
+
try:
|
|
1057
|
+
return int(val)
|
|
1058
|
+
except ValueError:
|
|
1059
|
+
return 100
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def get_frontend_emitter_queue_size() -> int:
|
|
1063
|
+
"""Get max subscriber queue size."""
|
|
1064
|
+
val = get_value("frontend_emitter_queue_size")
|
|
1065
|
+
if val is None:
|
|
1066
|
+
return 100
|
|
1067
|
+
try:
|
|
1068
|
+
return int(val)
|
|
1069
|
+
except ValueError:
|
|
1070
|
+
return 100
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
# Re-exports from config submodules (kept at bottom to avoid circular imports)
|
|
1074
|
+
from code_muse.config_agent import ( # noqa: E402,F401
|
|
1075
|
+
PACK_AGENT_NAMES,
|
|
1076
|
+
UC_AGENT_NAMES,
|
|
1077
|
+
clear_agent_pinned_model,
|
|
1078
|
+
get_agent_pinned_model,
|
|
1079
|
+
get_agents_pinned_to_model,
|
|
1080
|
+
get_all_agent_pinned_models,
|
|
1081
|
+
get_default_agent,
|
|
1082
|
+
get_pack_agents_enabled,
|
|
1083
|
+
get_project_agents_directory,
|
|
1084
|
+
get_universal_constructor_enabled,
|
|
1085
|
+
get_user_agents_directory,
|
|
1086
|
+
set_agent_pinned_model,
|
|
1087
|
+
set_default_agent,
|
|
1088
|
+
set_universal_constructor_enabled,
|
|
1089
|
+
)
|
|
1090
|
+
from code_muse.config_appearance import ( # noqa: E402,F401
|
|
1091
|
+
_DEFAULT_DIFF_ADDITION_HEX,
|
|
1092
|
+
_DEFAULT_DIFF_DELETION_HEX,
|
|
1093
|
+
DEFAULT_BANNER_COLORS,
|
|
1094
|
+
_coerce_to_hex,
|
|
1095
|
+
get_all_banner_colors,
|
|
1096
|
+
get_banner_color,
|
|
1097
|
+
get_diff_addition_color,
|
|
1098
|
+
get_diff_context_lines,
|
|
1099
|
+
get_diff_deletion_color,
|
|
1100
|
+
get_suppress_informational_messages,
|
|
1101
|
+
get_suppress_thinking_messages,
|
|
1102
|
+
reset_all_banner_colors,
|
|
1103
|
+
reset_banner_color,
|
|
1104
|
+
set_banner_color,
|
|
1105
|
+
set_diff_addition_color,
|
|
1106
|
+
set_diff_deletion_color,
|
|
1107
|
+
set_diff_highlight_style,
|
|
1108
|
+
set_suppress_informational_messages,
|
|
1109
|
+
set_suppress_thinking_messages,
|
|
1110
|
+
)
|
|
1111
|
+
from code_muse.config_model import ( # noqa: E402,F401
|
|
1112
|
+
_sanitize_model_name_for_key,
|
|
1113
|
+
clear_model_settings,
|
|
1114
|
+
get_all_model_settings,
|
|
1115
|
+
get_effective_model_settings,
|
|
1116
|
+
get_effective_seed,
|
|
1117
|
+
get_effective_temperature,
|
|
1118
|
+
get_effective_top_p,
|
|
1119
|
+
get_model_setting,
|
|
1120
|
+
get_openai_reasoning_effort,
|
|
1121
|
+
get_openai_reasoning_summary,
|
|
1122
|
+
get_openai_verbosity,
|
|
1123
|
+
get_muse_token,
|
|
1124
|
+
get_summarization_model_name,
|
|
1125
|
+
get_temperature,
|
|
1126
|
+
set_model_setting,
|
|
1127
|
+
set_openai_reasoning_effort,
|
|
1128
|
+
set_openai_reasoning_summary,
|
|
1129
|
+
set_openai_verbosity,
|
|
1130
|
+
set_muse_token,
|
|
1131
|
+
set_summarization_model_name,
|
|
1132
|
+
set_temperature,
|
|
1133
|
+
)
|
|
1134
|
+
from code_muse.config_security import ( # noqa: E402,F401
|
|
1135
|
+
get_api_key,
|
|
1136
|
+
load_api_keys_to_environment,
|
|
1137
|
+
set_api_key,
|
|
1138
|
+
)
|