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.
Files changed (394) hide show
  1. code_muse/__init__.py +26 -0
  2. code_muse/__main__.py +10 -0
  3. code_muse/agents/__init__.py +31 -0
  4. code_muse/agents/_builder.py +214 -0
  5. code_muse/agents/_compaction.py +506 -0
  6. code_muse/agents/_diagnostics.py +171 -0
  7. code_muse/agents/_history.py +382 -0
  8. code_muse/agents/_key_listeners.py +148 -0
  9. code_muse/agents/_non_streaming_render.py +148 -0
  10. code_muse/agents/_runtime.py +596 -0
  11. code_muse/agents/agent_creator_agent.py +603 -0
  12. code_muse/agents/agent_helios.py +47 -0
  13. code_muse/agents/agent_manager.py +740 -0
  14. code_muse/agents/agent_muse.py +78 -0
  15. code_muse/agents/agent_planning.py +44 -0
  16. code_muse/agents/agent_qa_melpomene.py +207 -0
  17. code_muse/agents/base_agent.py +194 -0
  18. code_muse/agents/event_stream_handler.py +361 -0
  19. code_muse/agents/json_agent.py +201 -0
  20. code_muse/agents/prompt_v3.py +521 -0
  21. code_muse/agents/subagent_stream_handler.py +273 -0
  22. code_muse/callbacks.py +941 -0
  23. code_muse/chatgpt_codex_client.py +333 -0
  24. code_muse/claude_cache_client.py +853 -0
  25. code_muse/cli_runner/__init__.py +319 -0
  26. code_muse/cli_runner/args.py +63 -0
  27. code_muse/cli_runner/loop.py +510 -0
  28. code_muse/cli_runner/resume.py +72 -0
  29. code_muse/cli_runner/runner.py +161 -0
  30. code_muse/command_line/__init__.py +1 -0
  31. code_muse/command_line/add_model_menu.py +1331 -0
  32. code_muse/command_line/agent_menu.py +674 -0
  33. code_muse/command_line/attachments.py +397 -0
  34. code_muse/command_line/autosave_menu.py +709 -0
  35. code_muse/command_line/clipboard.py +528 -0
  36. code_muse/command_line/colors_menu.py +530 -0
  37. code_muse/command_line/command_handler.py +262 -0
  38. code_muse/command_line/command_registry.py +150 -0
  39. code_muse/command_line/config_commands.py +711 -0
  40. code_muse/command_line/core_commands.py +740 -0
  41. code_muse/command_line/diff_menu.py +865 -0
  42. code_muse/command_line/file_path_completion.py +73 -0
  43. code_muse/command_line/load_context_completion.py +57 -0
  44. code_muse/command_line/model_picker_completion.py +512 -0
  45. code_muse/command_line/model_settings_menu.py +983 -0
  46. code_muse/command_line/onboarding_slides.py +162 -0
  47. code_muse/command_line/onboarding_wizard.py +337 -0
  48. code_muse/command_line/pagination.py +41 -0
  49. code_muse/command_line/pin_command_completion.py +329 -0
  50. code_muse/command_line/prompt_toolkit_completion.py +886 -0
  51. code_muse/command_line/session_commands.py +304 -0
  52. code_muse/command_line/shell_passthrough.py +145 -0
  53. code_muse/command_line/skills_completion.py +158 -0
  54. code_muse/command_line/types.py +18 -0
  55. code_muse/command_line/uc_menu.py +908 -0
  56. code_muse/command_line/utils.py +105 -0
  57. code_muse/command_line/wiggum_state.py +77 -0
  58. code_muse/config.py +1138 -0
  59. code_muse/config_agent.py +168 -0
  60. code_muse/config_appearance.py +241 -0
  61. code_muse/config_model.py +357 -0
  62. code_muse/config_security.py +73 -0
  63. code_muse/error_logging.py +132 -0
  64. code_muse/evals/__init__.py +35 -0
  65. code_muse/evals/eval_helpers.py +81 -0
  66. code_muse/evals/eval_runner.py +299 -0
  67. code_muse/evals/sample_evals/__init__.py +1 -0
  68. code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
  69. code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
  70. code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
  71. code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
  72. code_muse/fs_scan_cache/__init__.py +31 -0
  73. code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
  74. code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
  75. code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
  76. code_muse/fs_scan_cache/tool_integration.py +309 -0
  77. code_muse/fs_scan_cache/ttl_policy.py +44 -0
  78. code_muse/gemini_code_assist.py +383 -0
  79. code_muse/gemini_model.py +838 -0
  80. code_muse/hook_engine/README.md +105 -0
  81. code_muse/hook_engine/__init__.py +21 -0
  82. code_muse/hook_engine/aliases.py +153 -0
  83. code_muse/hook_engine/engine.py +221 -0
  84. code_muse/hook_engine/executor.py +347 -0
  85. code_muse/hook_engine/matcher.py +154 -0
  86. code_muse/hook_engine/models.py +245 -0
  87. code_muse/hook_engine/registry.py +114 -0
  88. code_muse/hook_engine/trust.py +268 -0
  89. code_muse/hook_engine/validator.py +144 -0
  90. code_muse/http_utils.py +360 -0
  91. code_muse/keymap.py +128 -0
  92. code_muse/list_filtering.py +26 -0
  93. code_muse/main.py +10 -0
  94. code_muse/messaging/__init__.py +259 -0
  95. code_muse/messaging/bus.py +621 -0
  96. code_muse/messaging/commands.py +166 -0
  97. code_muse/messaging/markdown_patches.py +57 -0
  98. code_muse/messaging/message_queue.py +397 -0
  99. code_muse/messaging/messages.py +591 -0
  100. code_muse/messaging/queue_console.py +269 -0
  101. code_muse/messaging/renderers.py +308 -0
  102. code_muse/messaging/rich_renderer.py +1158 -0
  103. code_muse/messaging/shimmer.py +154 -0
  104. code_muse/messaging/spinner/__init__.py +87 -0
  105. code_muse/messaging/spinner/console_spinner.py +250 -0
  106. code_muse/messaging/spinner/spinner_base.py +82 -0
  107. code_muse/messaging/subagent_console.py +458 -0
  108. code_muse/model_factory.py +1203 -0
  109. code_muse/model_switching.py +59 -0
  110. code_muse/model_utils.py +156 -0
  111. code_muse/models.json +66 -0
  112. code_muse/models_cache/__init__.py +26 -0
  113. code_muse/models_cache/blocking_lru_cache.py +98 -0
  114. code_muse/models_cache/cache_writer.py +86 -0
  115. code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
  116. code_muse/models_cache/sha256_hash.pyx +34 -0
  117. code_muse/models_cache/startup_integration.py +75 -0
  118. code_muse/models_dev_api.json +1 -0
  119. code_muse/models_dev_parser.py +590 -0
  120. code_muse/motion.py +126 -0
  121. code_muse/plugins/__init__.py +471 -0
  122. code_muse/plugins/agent_skills/__init__.py +32 -0
  123. code_muse/plugins/agent_skills/config.py +176 -0
  124. code_muse/plugins/agent_skills/discovery.py +309 -0
  125. code_muse/plugins/agent_skills/downloader.py +389 -0
  126. code_muse/plugins/agent_skills/installer.py +19 -0
  127. code_muse/plugins/agent_skills/metadata.py +293 -0
  128. code_muse/plugins/agent_skills/prompt_builder.py +66 -0
  129. code_muse/plugins/agent_skills/register_callbacks.py +298 -0
  130. code_muse/plugins/agent_skills/remote_catalog.py +320 -0
  131. code_muse/plugins/agent_skills/skill_catalog.py +254 -0
  132. code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
  133. code_muse/plugins/agent_skills/skills_menu.py +791 -0
  134. code_muse/plugins/autonomous_memory/__init__.py +39 -0
  135. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
  136. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
  137. code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
  138. code_muse/plugins/autonomous_memory/consolidation.py +82 -0
  139. code_muse/plugins/autonomous_memory/extraction.py +382 -0
  140. code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
  141. code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
  142. code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
  143. code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
  144. code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
  145. code_muse/plugins/aws_bedrock/__init__.py +14 -0
  146. code_muse/plugins/aws_bedrock/config.py +99 -0
  147. code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
  148. code_muse/plugins/aws_bedrock/utils.py +153 -0
  149. code_muse/plugins/azure_foundry/README.md +238 -0
  150. code_muse/plugins/azure_foundry/__init__.py +15 -0
  151. code_muse/plugins/azure_foundry/config.py +125 -0
  152. code_muse/plugins/azure_foundry/discovery.py +187 -0
  153. code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
  154. code_muse/plugins/azure_foundry/token.py +180 -0
  155. code_muse/plugins/azure_foundry/utils.py +345 -0
  156. code_muse/plugins/build_filter/__init__.py +1 -0
  157. code_muse/plugins/build_filter/register_callbacks.py +201 -0
  158. code_muse/plugins/build_filter/strategies/__init__.py +1 -0
  159. code_muse/plugins/build_filter/strategies/build.py +397 -0
  160. code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
  161. code_muse/plugins/chatgpt_oauth/config.py +52 -0
  162. code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
  163. code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
  164. code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
  165. code_muse/plugins/chatgpt_oauth/utils.py +538 -0
  166. code_muse/plugins/checkpointing/__init__.py +29 -0
  167. code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
  168. code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
  169. code_muse/plugins/checkpointing/register_callbacks.py +51 -0
  170. code_muse/plugins/checkpointing/restore_command.py +263 -0
  171. code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
  172. code_muse/plugins/checkpointing/shadow_git.py +90 -0
  173. code_muse/plugins/claude_code_hooks/__init__.py +1 -0
  174. code_muse/plugins/claude_code_hooks/config.py +188 -0
  175. code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
  176. code_muse/plugins/claude_code_oauth/README.md +167 -0
  177. code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
  178. code_muse/plugins/claude_code_oauth/__init__.py +25 -0
  179. code_muse/plugins/claude_code_oauth/config.py +52 -0
  180. code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
  181. code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
  182. code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
  183. code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
  184. code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
  185. code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
  186. code_muse/plugins/claude_code_oauth/utils.py +664 -0
  187. code_muse/plugins/copilot_auth/__init__.py +11 -0
  188. code_muse/plugins/copilot_auth/config.py +91 -0
  189. code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
  190. code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
  191. code_muse/plugins/copilot_auth/utils.py +584 -0
  192. code_muse/plugins/custom_commands/__init__.py +14 -0
  193. code_muse/plugins/custom_commands/args_injection.py +82 -0
  194. code_muse/plugins/custom_commands/command_discovery.py +89 -0
  195. code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
  196. code_muse/plugins/custom_commands/register_callbacks.py +176 -0
  197. code_muse/plugins/customizable_commands/__init__.py +0 -0
  198. code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
  199. code_muse/plugins/destructive_command_guard/__init__.py +14 -0
  200. code_muse/plugins/destructive_command_guard/detector.py +375 -0
  201. code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
  202. code_muse/plugins/example_custom_command/README.md +280 -0
  203. code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
  204. code_muse/plugins/file_permission_handler/__init__.py +4 -0
  205. code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
  206. code_muse/plugins/filter_engine/__init__.py +30 -0
  207. code_muse/plugins/filter_engine/classifier.py +153 -0
  208. code_muse/plugins/filter_engine/content_detector.py +184 -0
  209. code_muse/plugins/filter_engine/dispatcher.py +244 -0
  210. code_muse/plugins/filter_engine/register_callbacks.py +188 -0
  211. code_muse/plugins/filter_engine/registry.py +279 -0
  212. code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
  213. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
  214. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
  215. code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
  216. code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
  217. code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
  218. code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
  219. code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
  220. code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
  221. code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
  222. code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
  223. code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
  224. code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
  225. code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
  226. code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
  227. code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
  228. code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
  229. code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
  230. code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
  231. code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
  232. code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
  233. code_muse/plugins/filter_engine/verbosity.py +63 -0
  234. code_muse/plugins/force_push_guard/__init__.py +5 -0
  235. code_muse/plugins/force_push_guard/detector.py +96 -0
  236. code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
  237. code_muse/plugins/force_push_guard/test_detector.py +143 -0
  238. code_muse/plugins/frontend_emitter/__init__.py +25 -0
  239. code_muse/plugins/frontend_emitter/emitter.py +121 -0
  240. code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
  241. code_muse/plugins/gac/__init__.py +4 -0
  242. code_muse/plugins/gac/git_ops.py +136 -0
  243. code_muse/plugins/gac/prompt.py +191 -0
  244. code_muse/plugins/gac/register_callbacks.py +82 -0
  245. code_muse/plugins/hook_creator/__init__.py +1 -0
  246. code_muse/plugins/hook_creator/register_callbacks.py +34 -0
  247. code_muse/plugins/hook_manager/__init__.py +1 -0
  248. code_muse/plugins/hook_manager/config.py +289 -0
  249. code_muse/plugins/hook_manager/hooks_menu.py +563 -0
  250. code_muse/plugins/hook_manager/register_callbacks.py +227 -0
  251. code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
  252. code_muse/plugins/mindpack/__init__.py +0 -0
  253. code_muse/plugins/mindpack/factory.py +930 -0
  254. code_muse/plugins/mindpack/judge.py +573 -0
  255. code_muse/plugins/mindpack/memory.py +100 -0
  256. code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
  257. code_muse/plugins/mindpack/orchestration.py +605 -0
  258. code_muse/plugins/mindpack/register_callbacks.py +175 -0
  259. code_muse/plugins/mindpack/schemas.py +358 -0
  260. code_muse/plugins/mindpack/tools.py +387 -0
  261. code_muse/plugins/oauth_muse_html.py +226 -0
  262. code_muse/plugins/ollama_setup/__init__.py +5 -0
  263. code_muse/plugins/ollama_setup/completer.py +36 -0
  264. code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
  265. code_muse/plugins/plan_command/__init__.py +0 -0
  266. code_muse/plugins/plan_command/register_callbacks.py +206 -0
  267. code_muse/plugins/plan_mode/__init__.py +37 -0
  268. code_muse/plugins/plan_mode/mode_cycling.py +40 -0
  269. code_muse/plugins/plan_mode/plan_generation.py +68 -0
  270. code_muse/plugins/plan_mode/plan_hooks.py +74 -0
  271. code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
  272. code_muse/plugins/plan_mode/register_callbacks.py +121 -0
  273. code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
  274. code_muse/plugins/policy_engine/__init__.py +46 -0
  275. code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
  276. code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
  277. code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
  278. code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
  279. code_muse/plugins/policy_engine/register_callbacks.py +112 -0
  280. code_muse/plugins/pop_command/__init__.py +1 -0
  281. code_muse/plugins/pop_command/register_callbacks.py +189 -0
  282. code_muse/plugins/prompt_newline/__init__.py +13 -0
  283. code_muse/plugins/prompt_newline/config.py +19 -0
  284. code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
  285. code_muse/plugins/safety_status/__init__.py +0 -0
  286. code_muse/plugins/safety_status/register_callbacks.py +113 -0
  287. code_muse/plugins/semantic_compression/__init__.py +6 -0
  288. code_muse/plugins/semantic_compression/compressor.py +295 -0
  289. code_muse/plugins/semantic_compression/config.py +123 -0
  290. code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
  291. code_muse/plugins/shell_minimizer/__init__.py +50 -0
  292. code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
  293. code_muse/plugins/shell_minimizer/pipeline.py +556 -0
  294. code_muse/plugins/shell_minimizer/primitives.py +482 -0
  295. code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
  296. code_muse/plugins/shell_safety/__init__.py +6 -0
  297. code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
  298. code_muse/plugins/shell_safety/command_cache.py +149 -0
  299. code_muse/plugins/shell_safety/register_callbacks.py +202 -0
  300. code_muse/plugins/synthetic_status/__init__.py +1 -0
  301. code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
  302. code_muse/plugins/synthetic_status/status_api.py +145 -0
  303. code_muse/plugins/token_caching/__init__.py +21 -0
  304. code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
  305. code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
  306. code_muse/plugins/token_caching/register_callbacks.py +54 -0
  307. code_muse/plugins/token_caching/stats_display.py +35 -0
  308. code_muse/plugins/token_tracking/__init__.py +26 -0
  309. code_muse/plugins/token_tracking/database.py +381 -0
  310. code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
  311. code_muse/plugins/token_tracking/record.py +55 -0
  312. code_muse/plugins/token_tracking/register_callbacks.py +277 -0
  313. code_muse/plugins/token_tracking/reports.py +329 -0
  314. code_muse/plugins/universal_constructor/__init__.py +13 -0
  315. code_muse/plugins/universal_constructor/models.py +136 -0
  316. code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
  317. code_muse/plugins/universal_constructor/registry.py +390 -0
  318. code_muse/plugins/universal_constructor/runner.py +474 -0
  319. code_muse/plugins/universal_constructor/safety.py +440 -0
  320. code_muse/plugins/universal_constructor/sandbox.py +584 -0
  321. code_muse/provider_identity.py +105 -0
  322. code_muse/pydantic_patches.py +410 -0
  323. code_muse/reopenable_async_client.py +233 -0
  324. code_muse/round_robin_model.py +151 -0
  325. code_muse/secret_storage.py +74 -0
  326. code_muse/security/__init__.py +1 -0
  327. code_muse/security/redaction.cpython-314-darwin.so +0 -0
  328. code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
  329. code_muse/security/redaction.pyx +135 -0
  330. code_muse/session_storage.py +565 -0
  331. code_muse/status_display.py +261 -0
  332. code_muse/stream_parser/__init__.py +76 -0
  333. code_muse/stream_parser/assistant_text_parser.py +90 -0
  334. code_muse/stream_parser/citation_parser.py +76 -0
  335. code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
  336. code_muse/stream_parser/proposed_plan_parser.py +158 -0
  337. code_muse/stream_parser/stream_text_chunk.py +23 -0
  338. code_muse/stream_parser/stream_text_parser.py +27 -0
  339. code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
  340. code_muse/stream_parser/tagged_line_parser.pyx +251 -0
  341. code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
  342. code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
  343. code_muse/summarization_agent.py +308 -0
  344. code_muse/terminal_utils.cpython-314-darwin.so +0 -0
  345. code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
  346. code_muse/terminal_utils.pyx +483 -0
  347. code_muse/tools/__init__.py +459 -0
  348. code_muse/tools/agent_tools.py +613 -0
  349. code_muse/tools/ask_user_question/__init__.py +26 -0
  350. code_muse/tools/ask_user_question/constants.py +73 -0
  351. code_muse/tools/ask_user_question/demo_tui.py +55 -0
  352. code_muse/tools/ask_user_question/handler.py +232 -0
  353. code_muse/tools/ask_user_question/models.py +302 -0
  354. code_muse/tools/ask_user_question/registration.py +37 -0
  355. code_muse/tools/ask_user_question/renderers.py +336 -0
  356. code_muse/tools/ask_user_question/terminal_ui.py +327 -0
  357. code_muse/tools/ask_user_question/theme.py +156 -0
  358. code_muse/tools/ask_user_question/tui_loop.py +422 -0
  359. code_muse/tools/background_jobs.py +99 -0
  360. code_muse/tools/browser/__init__.py +37 -0
  361. code_muse/tools/browser/browser_control.py +289 -0
  362. code_muse/tools/browser/browser_interactions.py +545 -0
  363. code_muse/tools/browser/browser_locators.py +640 -0
  364. code_muse/tools/browser/browser_manager.py +376 -0
  365. code_muse/tools/browser/browser_navigation.py +251 -0
  366. code_muse/tools/browser/browser_screenshot.py +180 -0
  367. code_muse/tools/browser/browser_scripts.py +462 -0
  368. code_muse/tools/browser/browser_workflows.py +222 -0
  369. code_muse/tools/chrome_cdp/__init__.py +1070 -0
  370. code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
  371. code_muse/tools/command_runner.py +1401 -0
  372. code_muse/tools/common.py +1407 -0
  373. code_muse/tools/display.py +87 -0
  374. code_muse/tools/file_modifications.py +1099 -0
  375. code_muse/tools/file_operations.py +860 -0
  376. code_muse/tools/image_tools.py +185 -0
  377. code_muse/tools/meetin_proxy/__init__.py +243 -0
  378. code_muse/tools/meetin_proxy/capture_addon.py +82 -0
  379. code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
  380. code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
  381. code_muse/tools/path_policy.py +219 -0
  382. code_muse/tools/skills_tools.py +586 -0
  383. code_muse/tools/subagent_context.py +158 -0
  384. code_muse/tools/tools_content.py +50 -0
  385. code_muse/tools/universal_constructor.py +965 -0
  386. code_muse/uvx_detection.py +241 -0
  387. code_muse/version_checker.py +86 -0
  388. code_muse-0.0.1.data/data/code_muse/models.json +66 -0
  389. code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
  390. code_muse-0.0.1.dist-info/METADATA +845 -0
  391. code_muse-0.0.1.dist-info/RECORD +394 -0
  392. code_muse-0.0.1.dist-info/WHEEL +4 -0
  393. code_muse-0.0.1.dist-info/entry_points.txt +2 -0
  394. code_muse-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,740 @@
1
+ """Agent manager for handling different agent configurations."""
2
+
3
+ import importlib
4
+ import json
5
+ import os
6
+ import pkgutil
7
+ import re
8
+ import threading
9
+ import uuid
10
+ from pathlib import Path
11
+
12
+ from pydantic_ai.messages import ModelMessage
13
+
14
+ from code_muse.agents.base_agent import BaseAgent
15
+ from code_muse.agents.json_agent import JSONAgent, discover_json_agents
16
+ from code_muse.callbacks import on_agent_reload, on_register_agents
17
+ from code_muse.messaging import emit_success, emit_warning
18
+
19
+ # Registry of available agents (Python classes and JSON file paths)
20
+ _AGENT_REGISTRY: dict[str, type[BaseAgent] | str] = {}
21
+ _AGENT_HISTORIES: dict[str, list[ModelMessage]] = {}
22
+ _CURRENT_AGENT: BaseAgent | None = None
23
+
24
+ # Terminal session-based agent selection
25
+ _SESSION_AGENTS_CACHE: dict[str, str] = {}
26
+ _SESSION_FILE_LOADED: bool = False
27
+ # FREE-THREADED: _SESSION_LOCK guards sync-only session cache access.
28
+ _SESSION_LOCK = threading.Lock()
29
+
30
+
31
+ # Session persistence file path
32
+ def _get_session_file_path() -> Path:
33
+ """Get the path to the terminal sessions file."""
34
+ from ..config import STATE_DIR
35
+
36
+ return Path(STATE_DIR) / "terminal_sessions.json"
37
+
38
+
39
+ def get_terminal_session_id() -> str:
40
+ """Get a unique identifier for the current terminal session.
41
+
42
+ Uses parent process ID (PPID) as the session identifier.
43
+ This works across all platforms and provides session isolation.
44
+
45
+ Returns:
46
+ str: Unique session identifier (e.g., "session_12345")
47
+ """
48
+ try:
49
+ ppid = os.getppid()
50
+ return f"session_{ppid}"
51
+ except OSError, AttributeError:
52
+ # Fallback to current process ID if PPID unavailable
53
+ return f"fallback_{os.getpid()}"
54
+
55
+
56
+ def _is_process_alive(pid: int) -> bool:
57
+ """Check if a process with the given PID is still alive, cross-platform.
58
+
59
+ Args:
60
+ pid: Process ID to check
61
+
62
+ Returns:
63
+ bool: True if process likely exists, False otherwise
64
+ """
65
+ try:
66
+ if os.name == "nt":
67
+ # Windows: use OpenProcess to probe liveness safely
68
+ import ctypes
69
+ from ctypes import wintypes
70
+
71
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
72
+ kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
73
+ kernel32.OpenProcess.argtypes = [
74
+ wintypes.DWORD,
75
+ wintypes.BOOL,
76
+ wintypes.DWORD,
77
+ ]
78
+ kernel32.OpenProcess.restype = wintypes.HANDLE
79
+ handle = kernel32.OpenProcess(
80
+ PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid)
81
+ )
82
+ if handle:
83
+ kernel32.CloseHandle(handle)
84
+ return True
85
+ # If access denied, process likely exists but we can't query it
86
+ last_error = kernel32.GetLastError()
87
+ # ERROR_ACCESS_DENIED = 5
88
+ return last_error == 5
89
+ else:
90
+ # Unix-like: signal 0 does not deliver a signal but checks existence
91
+ os.kill(int(pid), 0)
92
+ return True
93
+ except PermissionError:
94
+ # No permission to signal -> process exists
95
+ return True
96
+ except OSError:
97
+ # Process does not exist
98
+ return False
99
+ except ValueError:
100
+ # Invalid signal or pid format
101
+ return False
102
+ except Exception:
103
+ # Be conservative – don't crash session cleanup due to platform quirks
104
+ return True
105
+
106
+
107
+ def _cleanup_dead_sessions(sessions: dict[str, str]) -> dict[str, str]:
108
+ """Remove sessions for processes that no longer exist.
109
+
110
+ Args:
111
+ sessions: Dictionary of session_id -> agent_name
112
+
113
+ Returns:
114
+ dict: Cleaned sessions dictionary
115
+ """
116
+ cleaned = {}
117
+ for session_id, agent_name in sessions.items():
118
+ if session_id.startswith("session_"):
119
+ try:
120
+ pid_str = session_id.replace("session_", "")
121
+ pid = int(pid_str)
122
+ if _is_process_alive(pid):
123
+ cleaned[session_id] = agent_name
124
+ # else: skip dead session
125
+ except ValueError, TypeError:
126
+ # Invalid session ID format, keep it anyway
127
+ cleaned[session_id] = agent_name
128
+ else:
129
+ # Non-standard session ID (like "fallback_"), keep it
130
+ cleaned[session_id] = agent_name
131
+ return cleaned
132
+
133
+
134
+ def _load_session_data() -> dict[str, str]:
135
+ """Load terminal session data from the JSON file.
136
+
137
+ Returns:
138
+ dict: Session ID to agent name mapping
139
+ """
140
+ session_file = _get_session_file_path()
141
+ try:
142
+ if session_file.exists():
143
+ with open(session_file, encoding="utf-8") as f:
144
+ data = json.load(f)
145
+ # Clean up dead sessions while loading
146
+ return _cleanup_dead_sessions(data)
147
+ return {}
148
+ except json.JSONDecodeError, OSError:
149
+ # File corrupted or permission issues, start fresh
150
+ return {}
151
+
152
+
153
+ def _save_session_data(sessions: dict[str, str]) -> None:
154
+ """Save terminal session data to the JSON file.
155
+
156
+ Args:
157
+ sessions: Session ID to agent name mapping
158
+ """
159
+ session_file = _get_session_file_path()
160
+ try:
161
+ # Ensure the config directory exists
162
+ session_file.parent.mkdir(parents=True, exist_ok=True)
163
+
164
+ # Clean up dead sessions before saving
165
+ cleaned_sessions = _cleanup_dead_sessions(sessions)
166
+
167
+ # Write to file atomically (write to temp file, then rename)
168
+ temp_file = session_file.with_suffix(".tmp")
169
+ with open(temp_file, "w", encoding="utf-8") as f:
170
+ json.dump(cleaned_sessions, f, indent=2)
171
+
172
+ # Atomic rename (works on all platforms)
173
+ temp_file.replace(session_file)
174
+
175
+ except OSError:
176
+ # File permission issues, etc. - just continue without persistence
177
+ pass
178
+
179
+
180
+ def _ensure_session_cache_loaded() -> None:
181
+ """Ensure the session cache is loaded from disk."""
182
+ global _SESSION_AGENTS_CACHE, _SESSION_FILE_LOADED
183
+ with _SESSION_LOCK:
184
+ if not _SESSION_FILE_LOADED:
185
+ _SESSION_AGENTS_CACHE.update(_load_session_data())
186
+ _SESSION_FILE_LOADED = True
187
+
188
+
189
+ def _discover_agents(message_group_id: str | None = None):
190
+ """Dynamically discover all agent classes and JSON agents."""
191
+ # Always clear the registry to force refresh
192
+ _AGENT_REGISTRY.clear()
193
+
194
+ # 1. Discover Python agent classes in the agents package
195
+ import code_muse.agents as agents_package
196
+
197
+ # Iterate through all modules in the agents package
198
+ for _, modname, _ in pkgutil.iter_modules(agents_package.__path__):
199
+ if modname.startswith("_") or modname in [
200
+ "base_agent",
201
+ "json_agent",
202
+ "agent_manager",
203
+ ]:
204
+ continue
205
+
206
+ try:
207
+ # Import the module
208
+ module = importlib.import_module(f"code_muse.agents.{modname}")
209
+
210
+ # Look for BaseAgent subclasses
211
+ for attr_name in dir(module):
212
+ attr = getattr(module, attr_name)
213
+ if (
214
+ isinstance(attr, type)
215
+ and issubclass(attr, BaseAgent)
216
+ and attr not in [BaseAgent, JSONAgent]
217
+ ):
218
+ # Create an instance to get the name
219
+ agent_instance = attr()
220
+ _AGENT_REGISTRY[agent_instance.name] = attr
221
+
222
+ except Exception as e:
223
+ # Skip problematic modules
224
+ emit_warning(
225
+ f"Warning: Could not load agent module {modname}: {e}",
226
+ message_group=message_group_id,
227
+ )
228
+ continue
229
+
230
+ # 1b. Discover agents in sub-packages (like 'pack')
231
+ for _, subpkg_name, ispkg in pkgutil.iter_modules(agents_package.__path__):
232
+ if not ispkg or subpkg_name.startswith("_"):
233
+ continue
234
+
235
+ try:
236
+ # Import the sub-package
237
+ subpkg = importlib.import_module(f"code_muse.agents.{subpkg_name}")
238
+
239
+ # Iterate through modules in the sub-package
240
+ if not hasattr(subpkg, "__path__"):
241
+ continue
242
+
243
+ for _, modname, _ in pkgutil.iter_modules(subpkg.__path__):
244
+ if modname.startswith("_"):
245
+ continue
246
+
247
+ try:
248
+ # Import the submodule
249
+ module = importlib.import_module(
250
+ f"code_muse.agents.{subpkg_name}.{modname}"
251
+ )
252
+
253
+ # Look for BaseAgent subclasses
254
+ for attr_name in dir(module):
255
+ attr = getattr(module, attr_name)
256
+ if (
257
+ isinstance(attr, type)
258
+ and issubclass(attr, BaseAgent)
259
+ and attr not in [BaseAgent, JSONAgent]
260
+ ):
261
+ # Create an instance to get the name
262
+ agent_instance = attr()
263
+ _AGENT_REGISTRY[agent_instance.name] = attr
264
+
265
+ except Exception as e:
266
+ emit_warning(
267
+ f"Warning: Could not load agent {subpkg_name}.{modname}: {e}",
268
+ message_group=message_group_id,
269
+ )
270
+ continue
271
+
272
+ except Exception as e:
273
+ emit_warning(
274
+ f"Warning: Could not load agent sub-package {subpkg_name}: {e}",
275
+ message_group=message_group_id,
276
+ )
277
+ continue
278
+
279
+ # 2. Discover JSON agents in user directory
280
+ try:
281
+ json_agents = discover_json_agents()
282
+
283
+ # Add JSON agents to registry (store file path instead of class)
284
+ # Python (builtin) agents take precedence over JSON agents.
285
+ for agent_name, json_path in json_agents.items():
286
+ if agent_name in _AGENT_REGISTRY:
287
+ emit_warning(
288
+ f"JSON agent '{agent_name}' skipped: builtin Python agent with the same name takes precedence.",
289
+ message_group=message_group_id,
290
+ )
291
+ continue
292
+ _AGENT_REGISTRY[agent_name] = json_path
293
+
294
+ except Exception as e:
295
+ emit_warning(
296
+ f"Warning: Could not discover JSON agents: {e}",
297
+ message_group=message_group_id,
298
+ )
299
+
300
+ # 3. Discover agents registered by plugins
301
+ try:
302
+ results = on_register_agents()
303
+ for result in results:
304
+ if result is None:
305
+ continue
306
+ # Each result should be a list of agent definitions
307
+ agents_list = result if isinstance(result, list) else [result]
308
+ for agent_def in agents_list:
309
+ if not isinstance(agent_def, dict) or "name" not in agent_def:
310
+ continue
311
+
312
+ agent_name = agent_def["name"]
313
+
314
+ # Support both class-based and JSON path-based registration
315
+ if "class" in agent_def:
316
+ agent_class = agent_def["class"]
317
+ if isinstance(agent_class, type) and issubclass(
318
+ agent_class, BaseAgent
319
+ ):
320
+ _AGENT_REGISTRY[agent_name] = agent_class
321
+ elif "json_path" in agent_def:
322
+ json_path = agent_def["json_path"]
323
+ if isinstance(json_path, str):
324
+ _AGENT_REGISTRY[agent_name] = json_path
325
+
326
+ except Exception as e:
327
+ emit_warning(
328
+ f"Warning: Could not load plugin agents: {e}",
329
+ message_group=message_group_id,
330
+ )
331
+
332
+
333
+ def get_available_agents() -> dict[str, str]:
334
+ """Get a dictionary of available agents with their display names.
335
+
336
+ Returns:
337
+ Dict mapping agent names to display names.
338
+ """
339
+ from ..config import (
340
+ PACK_AGENT_NAMES,
341
+ UC_AGENT_NAMES,
342
+ get_pack_agents_enabled,
343
+ get_universal_constructor_enabled,
344
+ )
345
+
346
+ # Generate a message group ID for this operation
347
+ message_group_id = str(uuid.uuid4())
348
+ _discover_agents(message_group_id=message_group_id)
349
+
350
+ # Check if pack agents are enabled
351
+ pack_agents_enabled = get_pack_agents_enabled()
352
+
353
+ # Check if UC is enabled
354
+ uc_enabled = get_universal_constructor_enabled()
355
+
356
+ agents = {}
357
+ for name, agent_ref in _AGENT_REGISTRY.items():
358
+ # Filter out pack agents if disabled
359
+ if not pack_agents_enabled and name in PACK_AGENT_NAMES:
360
+ continue
361
+
362
+ # Filter out UC-dependent agents if UC is disabled
363
+ if not uc_enabled and name in UC_AGENT_NAMES:
364
+ continue
365
+
366
+ try:
367
+ if isinstance(agent_ref, str): # JSON agent (file path)
368
+ agent_instance = JSONAgent(agent_ref)
369
+ else: # Python agent (class)
370
+ agent_instance = agent_ref()
371
+ agents[name] = agent_instance.display_name
372
+ except Exception:
373
+ agents[name] = name.title() # Fallback
374
+
375
+ return agents
376
+
377
+
378
+ def get_current_agent_name() -> str:
379
+ """Get the name of the currently active agent for this terminal session.
380
+
381
+ Returns:
382
+ The name of the current agent for this session.
383
+ Priority: session agent > config default > 'muse'.
384
+ """
385
+ _ensure_session_cache_loaded()
386
+ session_id = get_terminal_session_id()
387
+
388
+ # First check for session-specific agent
389
+ with _SESSION_LOCK:
390
+ session_agent = _SESSION_AGENTS_CACHE.get(session_id)
391
+ if session_agent:
392
+ return session_agent
393
+
394
+ # Fall back to config default
395
+ from ..config import get_default_agent
396
+
397
+ return get_default_agent()
398
+
399
+
400
+ def set_current_agent(agent_name: str) -> bool:
401
+ """Set the current agent by name.
402
+
403
+ Args:
404
+ agent_name: The name of the agent to set as current.
405
+
406
+ Returns:
407
+ True if the agent was set successfully, False if agent not found.
408
+ """
409
+ global _CURRENT_AGENT
410
+ curr_agent = get_current_agent()
411
+ if curr_agent is not None:
412
+ # Store a shallow copy so future mutations don't affect saved history
413
+ _AGENT_HISTORIES[curr_agent.name] = list(curr_agent.get_message_history())
414
+ # Generate a message group ID for agent switching
415
+ message_group_id = str(uuid.uuid4())
416
+ _discover_agents(message_group_id=message_group_id)
417
+
418
+ # Save current agent's history before switching
419
+
420
+ # Clear the cached config when switching agents
421
+ agent_obj = load_agent(agent_name)
422
+ _CURRENT_AGENT = agent_obj
423
+
424
+ # Update session-based agent selection and persist to disk
425
+ _ensure_session_cache_loaded()
426
+ session_id = get_terminal_session_id()
427
+ with _SESSION_LOCK:
428
+ _SESSION_AGENTS_CACHE[session_id] = agent_name
429
+ cache_snapshot = dict(_SESSION_AGENTS_CACHE)
430
+ _save_session_data(cache_snapshot)
431
+ if agent_obj.name in _AGENT_HISTORIES:
432
+ # Restore a copy to avoid sharing the same list instance
433
+ agent_obj.set_message_history(list(_AGENT_HISTORIES[agent_obj.name]))
434
+ on_agent_reload(agent_obj.id, agent_name)
435
+ return True
436
+
437
+
438
+ def get_current_agent() -> BaseAgent:
439
+ """Get the current agent configuration.
440
+
441
+ Returns:
442
+ The current agent configuration instance.
443
+ """
444
+ global _CURRENT_AGENT
445
+
446
+ if _CURRENT_AGENT is None:
447
+ agent_name = get_current_agent_name()
448
+ _CURRENT_AGENT = load_agent(agent_name)
449
+
450
+ return _CURRENT_AGENT
451
+
452
+
453
+ def load_agent(agent_name: str) -> BaseAgent:
454
+ """Load an agent configuration by name.
455
+
456
+ Args:
457
+ agent_name: The name of the agent to load.
458
+
459
+ Returns:
460
+ The agent configuration instance.
461
+
462
+ Raises:
463
+ ValueError: If the agent is not found.
464
+ """
465
+ # Generate a message group ID for agent loading
466
+ message_group_id = str(uuid.uuid4())
467
+ _discover_agents(message_group_id=message_group_id)
468
+
469
+ if agent_name not in _AGENT_REGISTRY:
470
+ # Fallback to muse if agent not found
471
+ if "muse" in _AGENT_REGISTRY:
472
+ agent_name = "muse"
473
+ else:
474
+ raise ValueError(
475
+ f"Agent '{agent_name}' not found and no fallback available"
476
+ )
477
+
478
+ agent_ref = _AGENT_REGISTRY[agent_name]
479
+ if isinstance(agent_ref, str): # JSON agent (file path)
480
+ return JSONAgent(agent_ref)
481
+ else: # Python agent (class)
482
+ return agent_ref()
483
+
484
+
485
+ def get_agent_descriptions() -> dict[str, str]:
486
+ """Get descriptions for all available agents.
487
+
488
+ Returns:
489
+ Dict mapping agent names to their descriptions.
490
+ """
491
+ from ..config import (
492
+ PACK_AGENT_NAMES,
493
+ UC_AGENT_NAMES,
494
+ get_pack_agents_enabled,
495
+ get_universal_constructor_enabled,
496
+ )
497
+
498
+ # Generate a message group ID for this operation
499
+ message_group_id = str(uuid.uuid4())
500
+ _discover_agents(message_group_id=message_group_id)
501
+
502
+ # Check if pack agents are enabled
503
+ pack_agents_enabled = get_pack_agents_enabled()
504
+
505
+ # Check if UC is enabled
506
+ uc_enabled = get_universal_constructor_enabled()
507
+
508
+ descriptions = {}
509
+ for name, agent_ref in _AGENT_REGISTRY.items():
510
+ # Filter out pack agents if disabled
511
+ if not pack_agents_enabled and name in PACK_AGENT_NAMES:
512
+ continue
513
+
514
+ # Filter out UC-dependent agents if UC is disabled
515
+ if not uc_enabled and name in UC_AGENT_NAMES:
516
+ continue
517
+
518
+ try:
519
+ if isinstance(agent_ref, str): # JSON agent (file path)
520
+ agent_instance = JSONAgent(agent_ref)
521
+ else: # Python agent (class)
522
+ agent_instance = agent_ref()
523
+ descriptions[name] = agent_instance.description
524
+ except Exception:
525
+ descriptions[name] = "No description available"
526
+
527
+ return descriptions
528
+
529
+
530
+ def refresh_agents():
531
+ """Refresh the agent discovery to pick up newly created agents.
532
+
533
+ This clears the agent registry cache and forces a rediscovery of all agents.
534
+ """
535
+ # Generate a message group ID for agent refreshing
536
+ message_group_id = str(uuid.uuid4())
537
+ _discover_agents(message_group_id=message_group_id)
538
+
539
+
540
+ _CLONE_NAME_PATTERN = re.compile(r"^(?P<base>.+)-clone-(?P<index>\d+)$")
541
+ _CLONE_DISPLAY_PATTERN = re.compile(r"\s*\(Clone\s+\d+\)$", re.IGNORECASE)
542
+
543
+
544
+ def _strip_clone_suffix(agent_name: str) -> str:
545
+ """Strip a trailing -clone-N suffix from a name if present."""
546
+ match = _CLONE_NAME_PATTERN.match(agent_name)
547
+ return match.group("base") if match else agent_name
548
+
549
+
550
+ def _strip_clone_display_suffix(display_name: str) -> str:
551
+ """Remove a trailing "(Clone N)" suffix from display names."""
552
+ cleaned = _CLONE_DISPLAY_PATTERN.sub("", display_name).strip()
553
+ return cleaned or display_name
554
+
555
+
556
+ def is_clone_agent_name(agent_name: str) -> bool:
557
+ """Return True if the agent name looks like a clone."""
558
+ return bool(_CLONE_NAME_PATTERN.match(agent_name))
559
+
560
+
561
+ def _default_display_name(agent_name: str) -> str:
562
+ """Build a default display name from an agent name."""
563
+ title = agent_name.title()
564
+ return f"{title} 🤖"
565
+
566
+
567
+ def _build_clone_display_name(display_name: str, clone_index: int) -> str:
568
+ """Build a clone display name based on the source display name."""
569
+ base_name = _strip_clone_display_suffix(display_name)
570
+ return f"{base_name} (Clone {clone_index})"
571
+
572
+
573
+ def _filter_available_tools(tool_names: list[str]) -> list[str]:
574
+ """Filter a tool list to only available tool names."""
575
+ from code_muse.tools import get_available_tool_names
576
+
577
+ available_tools = set(get_available_tool_names())
578
+ return [tool for tool in tool_names if tool in available_tools]
579
+
580
+
581
+ def _next_clone_index(
582
+ base_name: str, existing_names: set[str], agents_dir: Path
583
+ ) -> int:
584
+ """Compute the next clone index for a base name."""
585
+ clone_pattern = re.compile(rf"^{re.escape(base_name)}-clone-(\\d+)$")
586
+ indices = []
587
+ for name in existing_names:
588
+ match = clone_pattern.match(name)
589
+ if match:
590
+ indices.append(int(match.group(1)))
591
+
592
+ next_index = max(indices, default=0) + 1
593
+ while True:
594
+ clone_name = f"{base_name}-clone-{next_index}"
595
+ clone_path = agents_dir / f"{clone_name}.json"
596
+ if clone_name not in existing_names and not clone_path.exists():
597
+ return next_index
598
+ next_index += 1
599
+
600
+
601
+ def clone_agent(agent_name: str) -> str | None:
602
+ """Clone an agent definition into the user agents directory.
603
+
604
+ Args:
605
+ agent_name: Source agent name to clone.
606
+
607
+ Returns:
608
+ The cloned agent name, or None if cloning failed.
609
+ """
610
+ # Generate a message group ID for agent cloning
611
+ message_group_id = str(uuid.uuid4())
612
+ _discover_agents(message_group_id=message_group_id)
613
+
614
+ agent_ref = _AGENT_REGISTRY.get(agent_name)
615
+ if agent_ref is None:
616
+ emit_warning(f"Agent '{agent_name}' not found for cloning.")
617
+ return None
618
+
619
+ from ..config import get_agent_pinned_model, get_user_agents_directory
620
+
621
+ agents_dir = Path(get_user_agents_directory())
622
+ base_name = _strip_clone_suffix(agent_name)
623
+ existing_names = set(_AGENT_REGISTRY.keys())
624
+ clone_index = _next_clone_index(base_name, existing_names, agents_dir)
625
+ clone_name = f"{base_name}-clone-{clone_index}"
626
+ clone_path = agents_dir / f"{clone_name}.json"
627
+
628
+ try:
629
+ if isinstance(agent_ref, str):
630
+ with open(agent_ref, encoding="utf-8") as f:
631
+ source_config = json.load(f)
632
+
633
+ source_display_name = source_config.get("display_name")
634
+ if not source_display_name:
635
+ source_display_name = _default_display_name(base_name)
636
+
637
+ clone_config = dict(source_config)
638
+ clone_config["name"] = clone_name
639
+ clone_config["display_name"] = _build_clone_display_name(
640
+ source_display_name, clone_index
641
+ )
642
+
643
+ tools = source_config.get("tools", [])
644
+ clone_config["tools"] = (
645
+ _filter_available_tools(tools) if isinstance(tools, list) else []
646
+ )
647
+
648
+ if not clone_config.get("model"):
649
+ clone_config.pop("model", None)
650
+ else:
651
+ agent_instance = agent_ref()
652
+ clone_config = {
653
+ "name": clone_name,
654
+ "display_name": _build_clone_display_name(
655
+ agent_instance.display_name, clone_index
656
+ ),
657
+ "description": agent_instance.description,
658
+ "system_prompt": agent_instance.get_full_system_prompt(),
659
+ "tools": _filter_available_tools(agent_instance.get_available_tools()),
660
+ }
661
+
662
+ user_prompt = agent_instance.get_user_prompt()
663
+ if user_prompt is not None:
664
+ clone_config["user_prompt"] = user_prompt
665
+
666
+ tools_config = agent_instance.get_tools_config()
667
+ if tools_config is not None:
668
+ clone_config["tools_config"] = tools_config
669
+
670
+ pinned_model = get_agent_pinned_model(agent_instance.name)
671
+ if pinned_model:
672
+ clone_config["model"] = pinned_model
673
+ except Exception as exc:
674
+ emit_warning(f"Failed to build clone for '{agent_name}': {exc}")
675
+ return None
676
+
677
+ if clone_path.exists():
678
+ emit_warning(f"Clone target '{clone_name}' already exists.")
679
+ return None
680
+
681
+ try:
682
+ with open(clone_path, "w", encoding="utf-8") as f:
683
+ json.dump(clone_config, f, indent=2, ensure_ascii=False)
684
+ emit_success(f"Cloned '{agent_name}' to '{clone_name}'.")
685
+ return clone_name
686
+ except Exception as exc:
687
+ emit_warning(f"Failed to write clone file '{clone_path}': {exc}")
688
+ return None
689
+
690
+
691
+ def delete_clone_agent(agent_name: str) -> bool:
692
+ """Delete a cloned JSON agent definition.
693
+
694
+ Args:
695
+ agent_name: Clone agent name to delete.
696
+
697
+ Returns:
698
+ True if the clone was deleted, False otherwise.
699
+ """
700
+ message_group_id = str(uuid.uuid4())
701
+ _discover_agents(message_group_id=message_group_id)
702
+
703
+ if not is_clone_agent_name(agent_name):
704
+ emit_warning(f"Agent '{agent_name}' is not a clone.")
705
+ return False
706
+
707
+ if get_current_agent_name() == agent_name:
708
+ emit_warning("Cannot delete the active agent. Switch agents first.")
709
+ return False
710
+
711
+ agent_ref = _AGENT_REGISTRY.get(agent_name)
712
+ if agent_ref is None:
713
+ emit_warning(f"Clone '{agent_name}' not found.")
714
+ return False
715
+
716
+ if not isinstance(agent_ref, str):
717
+ emit_warning(f"Clone '{agent_name}' is not a JSON agent.")
718
+ return False
719
+
720
+ clone_path = Path(agent_ref)
721
+ if not clone_path.exists():
722
+ emit_warning(f"Clone file for '{agent_name}' does not exist.")
723
+ return False
724
+
725
+ from ..config import get_user_agents_directory
726
+
727
+ agents_dir = Path(get_user_agents_directory()).resolve()
728
+ if clone_path.resolve().parent != agents_dir:
729
+ emit_warning(f"Refusing to delete non-user clone '{agent_name}'.")
730
+ return False
731
+
732
+ try:
733
+ clone_path.unlink()
734
+ emit_success(f"Deleted clone '{agent_name}'.")
735
+ _AGENT_REGISTRY.pop(agent_name, None)
736
+ _AGENT_HISTORIES.pop(agent_name, None)
737
+ return True
738
+ except Exception as exc:
739
+ emit_warning(f"Failed to delete clone '{agent_name}': {exc}")
740
+ return False