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,690 @@
1
+ """Interactive terminal UI for browsing and installing remote agent skills.
2
+
3
+ Launched from `/skills install` (wiring may live elsewhere). Provides a
4
+ split-panel prompt_toolkit UI:
5
+ - Left: categories, then skills within a category
6
+ - Right: live details preview for the current selection
7
+
8
+ Installation happens after the TUI exits, with a confirmation prompt via
9
+ `safe_input()`, and uses `download_and_install_skill()` to fetch and extract
10
+ remote ZIPs.
11
+
12
+ This module is intentionally defensive: if the remote catalog isn't available,
13
+ it shows an empty menu and returns False.
14
+ """
15
+
16
+ import logging
17
+ import sys
18
+ import time
19
+ from pathlib import Path
20
+
21
+ from prompt_toolkit.application import Application
22
+ from prompt_toolkit.key_binding import KeyBindings
23
+ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
24
+ from prompt_toolkit.layout.controls import FormattedTextControl
25
+ from prompt_toolkit.widgets import Frame
26
+
27
+ from code_muse.command_line.pagination import (
28
+ ensure_visible_page,
29
+ get_page_bounds,
30
+ get_total_pages,
31
+ )
32
+ from code_muse.command_line.utils import safe_input
33
+ from code_muse.messaging import emit_error, emit_info, emit_success, emit_warning
34
+ from code_muse.plugins.agent_skills.downloader import download_and_install_skill
35
+ from code_muse.plugins.agent_skills.installer import InstallResult
36
+ from code_muse.plugins.agent_skills.skill_catalog import SkillCatalogEntry, catalog
37
+ from code_muse.tools.command_runner import set_awaiting_user_input
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ PAGE_SIZE = 12
42
+
43
+
44
+ def is_skill_installed(skill_id: str) -> bool:
45
+ """Return True if the skill is already installed locally."""
46
+
47
+ return (Path.home() / ".muse" / "skills" / skill_id / "SKILL.md").is_file()
48
+
49
+
50
+ def _format_bytes(num_bytes: int) -> str:
51
+ """Format bytes into a human-readable string."""
52
+
53
+ try:
54
+ size = float(max(0, int(num_bytes)))
55
+ except Exception:
56
+ return "0 B"
57
+
58
+ for unit in ("B", "KB", "MB", "GB"):
59
+ if size < 1024.0 or unit == "GB":
60
+ if unit == "B":
61
+ return f"{int(size)} {unit}"
62
+ return f"{size:.1f} {unit}"
63
+ size /= 1024.0
64
+ return f"{size:.1f} GB"
65
+
66
+
67
+ def _wrap_text(text: str, width: int) -> list[str]:
68
+ """Simple word-wrap for display in the details panel."""
69
+
70
+ if not text:
71
+ return []
72
+
73
+ words = text.split()
74
+ lines: list[str] = []
75
+ current = ""
76
+
77
+ for word in words:
78
+ if not current:
79
+ current = word
80
+ continue
81
+
82
+ if len(current) + 1 + len(word) > width:
83
+ lines.append(current)
84
+ current = word
85
+ else:
86
+ current = f"{current} {word}"
87
+
88
+ if current:
89
+ lines.append(current)
90
+
91
+ return lines
92
+
93
+
94
+ def _category_key(category: str) -> str:
95
+ """Normalize a category string for icon lookup."""
96
+
97
+ return "".join(ch for ch in (category or "").casefold() if ch.isalnum())
98
+
99
+
100
+ class SkillsInstallMenu:
101
+ """Interactive TUI for browsing and installing remote skills."""
102
+
103
+ def __init__(self):
104
+ """Initialize the skills install menu with catalog data."""
105
+
106
+ self.catalog = catalog
107
+ self.categories: list[str] = []
108
+ self.current_category: str | None = None
109
+ self.current_skills: list[SkillCatalogEntry] = []
110
+
111
+ # State
112
+ self.view_mode = "categories" # categories | skills
113
+ self.selected_category_idx = 0
114
+ self.selected_skill_idx = 0
115
+ self.current_page = 0
116
+ self.result: str | None = None
117
+ self.pending_entry: SkillCatalogEntry | None = None
118
+
119
+ # UI controls
120
+ self.menu_control: FormattedTextControl | None = None
121
+ self.preview_control: FormattedTextControl | None = None
122
+
123
+ self._initialize_catalog()
124
+
125
+ def _initialize_catalog(self) -> None:
126
+ """Load categories from the remote-backed catalog."""
127
+
128
+ try:
129
+ self.categories = self.catalog.list_categories() if self.catalog else []
130
+ except Exception as e:
131
+ emit_error(f"Skill catalog not available: {e}")
132
+ self.categories = []
133
+
134
+ def _get_category_icon(self, category: str) -> str:
135
+ """Return an emoji icon for a skill category name."""
136
+
137
+ icons = {
138
+ "data": "📊",
139
+ "finance": "💰",
140
+ "legal": "⚖️",
141
+ "office": "📄",
142
+ "productmanagement": "📦",
143
+ "sales": "💼",
144
+ "biology": "🧬",
145
+ }
146
+ return icons.get(_category_key(category), "📁")
147
+
148
+ def _get_current_category(self) -> str | None:
149
+ """Get the currently highlighted category name."""
150
+
151
+ if 0 <= self.selected_category_idx < len(self.categories):
152
+ return self.categories[self.selected_category_idx]
153
+ return None
154
+
155
+ def _get_current_skill(self) -> SkillCatalogEntry | None:
156
+ """Get the currently highlighted skill entry."""
157
+
158
+ if self.view_mode == "skills" and self.current_skills:
159
+ if 0 <= self.selected_skill_idx < len(self.current_skills):
160
+ return self.current_skills[self.selected_skill_idx]
161
+ return None
162
+
163
+ def _render_navigation_hints(self, lines: list) -> None:
164
+ """Render keyboard shortcut hints at the bottom."""
165
+
166
+ lines.append(("", "\n"))
167
+ lines.append(("fg:ansibrightblack", " ↑/↓ "))
168
+ lines.append(("", "Navigate "))
169
+ lines.append(("fg:ansibrightblack", "←/→ "))
170
+ lines.append(("", "Page\n"))
171
+
172
+ if self.view_mode == "categories":
173
+ lines.append(("fg:ansigreen", " Enter "))
174
+ lines.append(("", "Browse Skills\n"))
175
+ else:
176
+ lines.append(("fg:ansigreen", " Enter "))
177
+ lines.append(("", "Install Skill\n"))
178
+ lines.append(("fg:ansibrightblack", " Esc/Back "))
179
+ lines.append(("", "Back\n"))
180
+
181
+ lines.append(("fg:ansired", " Ctrl+C "))
182
+ lines.append(("", "Cancel"))
183
+
184
+ def _render_category_list(self) -> list:
185
+ """Render the left panel with category navigation."""
186
+
187
+ lines = []
188
+
189
+ lines.append(("bold cyan", " 📂 CATEGORIES"))
190
+ lines.append(("", "\n\n"))
191
+
192
+ if not self.categories:
193
+ lines.append(("fg:ansiyellow", " No remote categories available."))
194
+ lines.append(("", "\n"))
195
+ lines.append(
196
+ (
197
+ "fg:ansibrightblack",
198
+ " (Remote catalog unavailable or empty)\n",
199
+ )
200
+ )
201
+ self._render_navigation_hints(lines)
202
+ return lines
203
+
204
+ total_pages = get_total_pages(len(self.categories), PAGE_SIZE)
205
+ start_idx, end_idx = get_page_bounds(
206
+ self.current_page, len(self.categories), PAGE_SIZE
207
+ )
208
+
209
+ for i in range(start_idx, end_idx):
210
+ category = self.categories[i]
211
+ is_selected = i == self.selected_category_idx
212
+ icon = self._get_category_icon(category)
213
+ count = 0
214
+ try:
215
+ count = (
216
+ len(self.catalog.get_by_category(category)) if self.catalog else 0
217
+ )
218
+ except Exception:
219
+ count = 0
220
+
221
+ prefix = " > " if is_selected else " "
222
+ label = f"{prefix}{icon} {category} ({count})"
223
+
224
+ if is_selected:
225
+ lines.append(("fg:ansibrightcyan bold", label))
226
+ else:
227
+ lines.append(("fg:ansibrightblack", label))
228
+ lines.append(("", "\n"))
229
+
230
+ lines.append(("", "\n"))
231
+ if total_pages > 1:
232
+ lines.append(
233
+ ("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
234
+ )
235
+ lines.append(("", "\n"))
236
+
237
+ self._render_navigation_hints(lines)
238
+ return lines
239
+
240
+ def _render_skill_list(self) -> list:
241
+ """Render the middle panel with skills in the selected category."""
242
+
243
+ lines = []
244
+
245
+ if not self.current_category:
246
+ lines.append(("fg:ansiyellow", " No category selected."))
247
+ lines.append(("", "\n\n"))
248
+ self._render_navigation_hints(lines)
249
+ return lines
250
+
251
+ icon = self._get_category_icon(self.current_category)
252
+ lines.append(("bold cyan", f" {icon} {self.current_category.upper()}"))
253
+ lines.append(("", "\n\n"))
254
+
255
+ if not self.current_skills:
256
+ lines.append(("fg:ansiyellow", " No skills in this category."))
257
+ lines.append(("", "\n\n"))
258
+ self._render_navigation_hints(lines)
259
+ return lines
260
+
261
+ total_pages = get_total_pages(len(self.current_skills), PAGE_SIZE)
262
+ start_idx, end_idx = get_page_bounds(
263
+ self.current_page, len(self.current_skills), PAGE_SIZE
264
+ )
265
+
266
+ for i in range(start_idx, end_idx):
267
+ entry = self.current_skills[i]
268
+ is_selected = i == self.selected_skill_idx
269
+
270
+ installed = is_skill_installed(entry.id)
271
+ status_icon = "✓" if installed else "○"
272
+ status_style = "fg:ansigreen" if installed else "fg:ansibrightblack"
273
+
274
+ prefix = " > " if is_selected else " "
275
+ label = f"{prefix}{status_icon} {entry.display_name}"
276
+
277
+ if is_selected:
278
+ lines.append(("fg:ansibrightcyan bold", label))
279
+ else:
280
+ lines.append((status_style, label))
281
+
282
+ lines.append(("", "\n"))
283
+
284
+ lines.append(("", "\n"))
285
+ if total_pages > 1:
286
+ lines.append(
287
+ ("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
288
+ )
289
+ lines.append(("", "\n"))
290
+
291
+ self._render_navigation_hints(lines)
292
+ return lines
293
+
294
+ def _render_details(self) -> list:
295
+ """Render the right panel with details for the selected skill."""
296
+
297
+ lines = []
298
+
299
+ lines.append(("bold cyan", " 📋 DETAILS"))
300
+ lines.append(("", "\n\n"))
301
+
302
+ if self.view_mode == "categories":
303
+ category = self._get_current_category()
304
+ if not category:
305
+ lines.append(("fg:ansiyellow", " No category selected."))
306
+ return lines
307
+
308
+ icon = self._get_category_icon(category)
309
+ lines.append(("bold", f" {icon} {category}"))
310
+ lines.append(("", "\n\n"))
311
+
312
+ skills = []
313
+ try:
314
+ skills = self.catalog.get_by_category(category) if self.catalog else []
315
+ except Exception:
316
+ skills = []
317
+
318
+ lines.append(("fg:ansibrightblack", f" {len(skills)} skills available"))
319
+ lines.append(("", "\n\n"))
320
+
321
+ # Show a preview of the first few skills
322
+ if skills:
323
+ lines.append(("bold", " Preview:"))
324
+ lines.append(("", "\n"))
325
+ for entry in skills[:6]:
326
+ lines.append(("fg:ansibrightblack", f" • {entry.display_name}"))
327
+ lines.append(("", "\n"))
328
+
329
+ return lines
330
+
331
+ entry = self._get_current_skill()
332
+ if not entry:
333
+ lines.append(("fg:ansiyellow", " No skill selected."))
334
+ return lines
335
+
336
+ installed = is_skill_installed(entry.id)
337
+ installed_text = "Installed" if installed else "Not installed"
338
+ installed_style = "fg:ansigreen" if installed else "fg:ansiyellow"
339
+
340
+ lines.append(("bold", f" {entry.display_name}"))
341
+ lines.append(("", "\n"))
342
+ lines.append((installed_style, f" {installed_text}"))
343
+ lines.append(("", "\n\n"))
344
+
345
+ lines.append(("bold", " ID:"))
346
+ lines.append(("", "\n"))
347
+ lines.append(("fg:ansibrightblack", f" {entry.id}"))
348
+ lines.append(("", "\n\n"))
349
+
350
+ lines.append(("bold", " Description:"))
351
+ lines.append(("", "\n"))
352
+ desc = entry.description or "No description available"
353
+ for line in _wrap_text(desc, 56):
354
+ lines.append(("fg:ansibrightblack", f" {line}"))
355
+ lines.append(("", "\n"))
356
+ lines.append(("", "\n"))
357
+
358
+ lines.append(("bold", " Category:"))
359
+ lines.append(("", "\n"))
360
+ lines.append(("fg:ansibrightblack", f" {entry.category}"))
361
+ lines.append(("", "\n\n"))
362
+
363
+ lines.append(("bold", " Tags:"))
364
+ lines.append(("", "\n"))
365
+ tags = entry.tags or []
366
+ lines.append(("fg:ansicyan", f" {', '.join(tags) if tags else '(none)'}"))
367
+ lines.append(("", "\n\n"))
368
+
369
+ lines.append(("bold", " Contents:"))
370
+ lines.append(("", "\n"))
371
+ lines.append(
372
+ (
373
+ "fg:ansibrightblack",
374
+ f" scripts: {'yes' if entry.has_scripts else 'no'}",
375
+ )
376
+ )
377
+ lines.append(("", "\n"))
378
+ lines.append(
379
+ (
380
+ "fg:ansibrightblack",
381
+ f" references: {'yes' if entry.has_references else 'no'}",
382
+ )
383
+ )
384
+ lines.append(("", "\n"))
385
+ lines.append(("fg:ansibrightblack", f" files: {entry.file_count}"))
386
+ lines.append(("", "\n\n"))
387
+
388
+ lines.append(("bold", " Download:"))
389
+ lines.append(("", "\n"))
390
+ lines.append(
391
+ (
392
+ "fg:ansibrightblack",
393
+ f" size: {_format_bytes(entry.zip_size_bytes)}",
394
+ )
395
+ )
396
+ lines.append(("", "\n"))
397
+ lines.append(("fg:ansibrightblack", f" url: {entry.download_url}"))
398
+ lines.append(("", "\n"))
399
+
400
+ return lines
401
+
402
+ def update_display(self) -> None:
403
+ """Refresh all three panels of the TUI display."""
404
+
405
+ if self.view_mode == "categories":
406
+ self.menu_control.text = self._render_category_list()
407
+ else:
408
+ self.menu_control.text = self._render_skill_list()
409
+
410
+ self.preview_control.text = self._render_details()
411
+
412
+ def _enter_category(self) -> None:
413
+ """Enter the currently highlighted category to browse skills."""
414
+
415
+ category = self._get_current_category()
416
+ if not category or not self.catalog:
417
+ return
418
+
419
+ self.current_category = category
420
+ try:
421
+ self.current_skills = self.catalog.get_by_category(category)
422
+ except Exception:
423
+ self.current_skills = []
424
+
425
+ self.view_mode = "skills"
426
+ self.selected_skill_idx = 0
427
+ self.current_page = 0
428
+ self.update_display()
429
+
430
+ def _go_back_to_categories(self) -> None:
431
+ """Navigate back from skill list to category list."""
432
+
433
+ self.view_mode = "categories"
434
+ self.current_category = None
435
+ self.current_skills = []
436
+ self.selected_skill_idx = 0
437
+ self.current_page = 0
438
+ self.update_display()
439
+
440
+ def _select_current_skill(self) -> None:
441
+ """Download and install the currently highlighted skill."""
442
+
443
+ entry = self._get_current_skill()
444
+ if entry:
445
+ self.pending_entry = entry
446
+ self.result = "pending_install"
447
+
448
+ def run(self) -> bool:
449
+ """Run the skills install menu.
450
+
451
+ Returns:
452
+ True if a skill was installed, False otherwise.
453
+ """
454
+
455
+ # Build UI
456
+ self.menu_control = FormattedTextControl(text="")
457
+ self.preview_control = FormattedTextControl(text="")
458
+
459
+ menu_window = Window(
460
+ content=self.menu_control, wrap_lines=True, width=Dimension(weight=35)
461
+ )
462
+ preview_window = Window(
463
+ content=self.preview_control, wrap_lines=True, width=Dimension(weight=65)
464
+ )
465
+
466
+ menu_frame = Frame(menu_window, width=Dimension(weight=35), title="Browse")
467
+ preview_frame = Frame(
468
+ preview_window, width=Dimension(weight=65), title="Details"
469
+ )
470
+
471
+ root_container = VSplit([menu_frame, preview_frame])
472
+
473
+ kb = KeyBindings()
474
+
475
+ @kb.add("up")
476
+ def _(event):
477
+ """Move cursor up."""
478
+
479
+ if self.view_mode == "categories":
480
+ if self.selected_category_idx > 0:
481
+ self.selected_category_idx -= 1
482
+ self.current_page = ensure_visible_page(
483
+ self.selected_category_idx,
484
+ self.current_page,
485
+ len(self.categories),
486
+ PAGE_SIZE,
487
+ )
488
+ else:
489
+ if self.selected_skill_idx > 0:
490
+ self.selected_skill_idx -= 1
491
+ self.current_page = ensure_visible_page(
492
+ self.selected_skill_idx,
493
+ self.current_page,
494
+ len(self.current_skills),
495
+ PAGE_SIZE,
496
+ )
497
+ self.update_display()
498
+
499
+ @kb.add("down")
500
+ def _(event):
501
+ """Move cursor down."""
502
+
503
+ if self.view_mode == "categories":
504
+ if self.selected_category_idx < len(self.categories) - 1:
505
+ self.selected_category_idx += 1
506
+ self.current_page = ensure_visible_page(
507
+ self.selected_category_idx,
508
+ self.current_page,
509
+ len(self.categories),
510
+ PAGE_SIZE,
511
+ )
512
+ else:
513
+ if self.selected_skill_idx < len(self.current_skills) - 1:
514
+ self.selected_skill_idx += 1
515
+ self.current_page = ensure_visible_page(
516
+ self.selected_skill_idx,
517
+ self.current_page,
518
+ len(self.current_skills),
519
+ PAGE_SIZE,
520
+ )
521
+ self.update_display()
522
+
523
+ @kb.add("left")
524
+ def _(event):
525
+ """Navigate to previous page."""
526
+
527
+ if self.current_page > 0:
528
+ self.current_page -= 1
529
+ if self.view_mode == "categories":
530
+ self.selected_category_idx = self.current_page * PAGE_SIZE
531
+ else:
532
+ self.selected_skill_idx = self.current_page * PAGE_SIZE
533
+ self.update_display()
534
+
535
+ @kb.add("right")
536
+ def _(event):
537
+ """Navigate to next page."""
538
+
539
+ if self.view_mode == "categories":
540
+ total_items = len(self.categories)
541
+ else:
542
+ total_items = len(self.current_skills)
543
+
544
+ total_pages = get_total_pages(total_items, PAGE_SIZE)
545
+ if self.current_page < total_pages - 1:
546
+ self.current_page += 1
547
+ if self.view_mode == "categories":
548
+ self.selected_category_idx = self.current_page * PAGE_SIZE
549
+ else:
550
+ self.selected_skill_idx = self.current_page * PAGE_SIZE
551
+ self.update_display()
552
+
553
+ @kb.add("enter")
554
+ def _(event):
555
+ """Select/enter the current item."""
556
+
557
+ if self.view_mode == "categories":
558
+ self._enter_category()
559
+ else:
560
+ self._select_current_skill()
561
+ event.app.exit()
562
+
563
+ @kb.add("escape")
564
+ def _(event):
565
+ """Go back."""
566
+
567
+ if self.view_mode == "skills":
568
+ self._go_back_to_categories()
569
+
570
+ @kb.add("backspace")
571
+ def _(event):
572
+ """Go back."""
573
+
574
+ if self.view_mode == "skills":
575
+ self._go_back_to_categories()
576
+
577
+ @kb.add("c-c")
578
+ def _(event):
579
+ """Quit the menu."""
580
+
581
+ event.app.exit()
582
+
583
+ layout = Layout(root_container)
584
+ app = Application(
585
+ layout=layout,
586
+ key_bindings=kb,
587
+ full_screen=False,
588
+ mouse_support=False,
589
+ )
590
+
591
+ set_awaiting_user_input(True)
592
+
593
+ # Enter alternate screen buffer
594
+ sys.stdout.write("\033[?1049h")
595
+ sys.stdout.write("\033[2J\033[H")
596
+ sys.stdout.flush()
597
+ time.sleep(0.05)
598
+
599
+ try:
600
+ self.update_display()
601
+ sys.stdout.write("\033[2J\033[H")
602
+ sys.stdout.flush()
603
+
604
+ app.run(in_thread=True)
605
+
606
+ finally:
607
+ sys.stdout.write("\033[?1049l")
608
+ sys.stdout.flush()
609
+
610
+ # Flush any buffered input to prevent stale keypresses
611
+ try:
612
+ import termios
613
+
614
+ termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
615
+ except Exception:
616
+ pass # ImportError on Windows, termios.error, or not a tty
617
+
618
+ # Small delay to let terminal settle before any output
619
+ time.sleep(0.1)
620
+ set_awaiting_user_input(False)
621
+
622
+ # Handle install after TUI exits
623
+ if self.result == "pending_install" and self.pending_entry:
624
+ return _prompt_and_install(self.pending_entry)
625
+
626
+ emit_info("✓ Exited skills install browser")
627
+ return False
628
+
629
+
630
+ def _prompt_and_install(entry: SkillCatalogEntry) -> bool:
631
+ """Prompt for confirmation and install the given skill."""
632
+
633
+ installed = is_skill_installed(entry.id)
634
+ size_str = _format_bytes(entry.zip_size_bytes)
635
+
636
+ try:
637
+ if installed:
638
+ answer = safe_input(
639
+ f"Skill '{entry.display_name}' is already installed. Reinstall ({size_str})? [y/N] "
640
+ )
641
+ if answer.strip().lower() not in {"y", "yes"}:
642
+ emit_info("Installation cancelled")
643
+ return False
644
+ force = True
645
+ else:
646
+ answer = safe_input(
647
+ f"Install skill '{entry.display_name}' ({size_str})? [y/N] "
648
+ )
649
+ if answer.strip().lower() not in {"y", "yes"}:
650
+ emit_info("Installation cancelled")
651
+ return False
652
+ force = False
653
+
654
+ except KeyboardInterrupt, EOFError:
655
+ emit_warning("Installation cancelled")
656
+ return False
657
+
658
+ emit_info(f"Downloading: {entry.display_name} ({size_str})")
659
+
660
+ result: InstallResult
661
+ try:
662
+ result = download_and_install_skill(
663
+ skill_name=entry.id,
664
+ download_url=entry.download_url,
665
+ force=force,
666
+ )
667
+ except Exception as e:
668
+ logger.exception(f"Unexpected error during skill install: {e}")
669
+ emit_error(f"Installation error: {e}")
670
+ return False
671
+
672
+ if result.success:
673
+ emit_success(result.message)
674
+ if result.installed_path:
675
+ emit_info(f"Installed to: {result.installed_path}")
676
+ return True
677
+
678
+ emit_error(result.message)
679
+ return False
680
+
681
+
682
+ def run_skills_install_menu() -> bool:
683
+ """Run the bundled skills install menu.
684
+
685
+ Returns:
686
+ True if a skill was installed, False otherwise.
687
+ """
688
+
689
+ menu = SkillsInstallMenu()
690
+ return menu.run()