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,66 @@
1
+ """Build available_skills XML for system prompt injection."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .metadata import SkillMetadata
7
+
8
+
9
+ def build_available_skills_xml(skills: list[SkillMetadata]) -> str:
10
+ """Build Claude-optimized XML listing available skills.
11
+
12
+ Args:
13
+ skills: List of SkillMetadata objects to include in the XML.
14
+
15
+ Returns:
16
+ XML string listing available skills in the format:
17
+ <available_skills>
18
+ <skill>
19
+ <name>skill-name</name>
20
+ <description>What the skill does...</description>
21
+ </skill>
22
+ ...
23
+ </available_skills>
24
+
25
+ To use a skill, call activate_skill(skill_name) to load full instructions.
26
+ """
27
+ if not skills:
28
+ return "<available_skills></available_skills>"
29
+
30
+ xml_parts = ["<available_skills>"]
31
+
32
+ for skill in skills:
33
+ xml_parts.append(" <skill>")
34
+ xml_parts.append(f" <name>{skill.name}</name>")
35
+ if skill.description:
36
+ # Escape any XML special characters in the description
37
+ escaped_desc = (
38
+ skill.description.replace("&", "&amp;")
39
+ .replace("<", "&lt;")
40
+ .replace(">", "&gt;")
41
+ .replace('"', "&quot;")
42
+ .replace("'", "&#39;")
43
+ )
44
+ xml_parts.append(f" <description>{escaped_desc}</description>")
45
+ if skill.source:
46
+ xml_parts.append(f" <source>{skill.source}</source>")
47
+ if skill.trust:
48
+ xml_parts.append(f" <trust>{skill.trust}</trust>")
49
+ if skill.skill_md_hash:
50
+ xml_parts.append(f" <hash>{skill.skill_md_hash[:12]}</hash>")
51
+ xml_parts.append(" </skill>")
52
+
53
+ xml_parts.append("</available_skills>")
54
+
55
+ return "\n".join(xml_parts)
56
+
57
+
58
+ def build_skills_guidance() -> str:
59
+ """Return guidance text for how to use skills."""
60
+ return """
61
+ # Agent Skills
62
+
63
+ When `<available_skills>` appears in context, match user tasks to skill descriptions.
64
+ Call `activate_skill(skill_name)` to load full instructions before starting the task.
65
+ Use `list_or_search_skills(query)` to search for relevant skills.
66
+ """
@@ -0,0 +1,298 @@
1
+ """Agent Skills plugin - registers callbacks for skill integration.
2
+
3
+ This plugin:
4
+ 1. Injects available skills into system prompts
5
+ 2. Registers skill-related tools
6
+ 3. Provides /skills slash command (and alias /skill)
7
+ """
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from code_muse.callbacks import register_callback
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Session-level deactivated skills (not persisted to config)
18
+ _deactivated_skills: set[str] = set()
19
+
20
+
21
+ def _get_skills_prompt_section() -> str | None:
22
+ """Build the skills section to inject into system prompts.
23
+
24
+ Returns None if skills are disabled or no skills found.
25
+ """
26
+ from .config import get_disabled_skills, get_skill_directories, get_skills_enabled
27
+ from .discovery import discover_skills
28
+ from .metadata import SkillMetadata, parse_skill_metadata
29
+ from .prompt_builder import build_available_skills_xml, build_skills_guidance
30
+
31
+ # 1. Check if enabled
32
+ if not get_skills_enabled():
33
+ logger.debug("Skills integration is disabled, skipping prompt injection")
34
+ return None
35
+
36
+ # 2. Discover skills
37
+ skill_dirs = [Path(d) for d in get_skill_directories()]
38
+ discovered = discover_skills(skill_dirs)
39
+
40
+ if not discovered:
41
+ logger.debug("No skills discovered, skipping prompt injection")
42
+ return None
43
+
44
+ # 3. Parse metadata for each and filter out disabled/deactivated skills
45
+ disabled_skills = get_disabled_skills()
46
+ skills_metadata: list[SkillMetadata] = []
47
+
48
+ for skill_info in discovered:
49
+ # Skip disabled skills
50
+ if skill_info.name in disabled_skills:
51
+ logger.debug(f"Skipping disabled skill: {skill_info.name}")
52
+ continue
53
+
54
+ # Skip session-level deactivated skills
55
+ if skill_info.name in _deactivated_skills:
56
+ logger.debug(f"Skipping deactivated skill: {skill_info.name}")
57
+ continue
58
+
59
+ # Only include skills with valid SKILL.md
60
+ if not skill_info.has_skill_md:
61
+ logger.debug(f"Skipping skill without SKILL.md: {skill_info.name}")
62
+ continue
63
+
64
+ # Parse metadata
65
+ metadata = parse_skill_metadata(skill_info.path)
66
+ if metadata:
67
+ skills_metadata.append(metadata)
68
+ else:
69
+ logger.debug(f"Skipping skill with invalid metadata: {skill_info.name}")
70
+
71
+ # 4. Build XML + guidance
72
+ if not skills_metadata:
73
+ logger.debug("No valid skills with metadata found, skipping prompt injection")
74
+ return None
75
+
76
+ xml_section = build_available_skills_xml(skills_metadata)
77
+ guidance = build_skills_guidance()
78
+
79
+ # 5. Return combined string
80
+ combined = f"{xml_section}\n\n{guidance}"
81
+ logger.debug(f"Injecting skills section with {len(skills_metadata)} skills")
82
+ return combined
83
+
84
+
85
+ def _inject_skills_into_prompt(
86
+ model_name: str, default_system_prompt: str, user_prompt: str
87
+ ) -> dict[str, Any | None]:
88
+ """Callback to inject skills into system prompt.
89
+
90
+ This is registered with the 'get_model_system_prompt' callback phase.
91
+ """
92
+ skills_section = _get_skills_prompt_section()
93
+
94
+ if not skills_section:
95
+ return None # No skills, don't modify prompt
96
+
97
+ # Append skills section to system prompt
98
+ enhanced_prompt = f"{default_system_prompt}\n\n{skills_section}"
99
+
100
+ return {
101
+ "instructions": enhanced_prompt,
102
+ "user_prompt": user_prompt,
103
+ "handled": False, # Let other handlers also process
104
+ }
105
+
106
+
107
+ def _register_skills_tools() -> list[dict[str, Any]]:
108
+ """Callback to register skills tools.
109
+
110
+ This is registered with the 'register_tools' callback phase.
111
+ Returns tool definitions for the tool registry.
112
+ """
113
+ from code_muse.tools.skills_tools import (
114
+ register_activate_skill,
115
+ register_check_skill_background,
116
+ register_deactivate_skill,
117
+ register_list_or_search_skills,
118
+ register_run_skill_background,
119
+ )
120
+
121
+ return [
122
+ {"name": "activate_skill", "register_func": register_activate_skill},
123
+ {
124
+ "name": "list_or_search_skills",
125
+ "register_func": register_list_or_search_skills,
126
+ },
127
+ {"name": "deactivate_skill", "register_func": register_deactivate_skill},
128
+ {
129
+ "name": "run_skill_background",
130
+ "register_func": register_run_skill_background,
131
+ },
132
+ {
133
+ "name": "check_skill_background",
134
+ "register_func": register_check_skill_background,
135
+ },
136
+ ]
137
+
138
+
139
+ # ---------------------------------------------------------------------------
140
+ # Slash command: /skills (and alias /skill)
141
+ # ---------------------------------------------------------------------------
142
+
143
+ _COMMAND_NAME = "skills"
144
+ _ALIASES = ("skill",)
145
+
146
+
147
+ def _skills_command_help() -> list[tuple[str, str]]:
148
+ """Advertise /skills in the /help menu."""
149
+ return [
150
+ ("skills", "Manage agent skills – browse, enable, disable, install"),
151
+ ("skill", "Alias for /skills"),
152
+ ]
153
+
154
+
155
+ def _handle_skills_command(command: str, name: str) -> Any | None:
156
+ """Handle /skills and /skill slash commands.
157
+
158
+ Sub-commands:
159
+ /skills – Launch interactive TUI menu
160
+ /skills list – Quick text list of all skills
161
+ /skills install – Browse & install from remote catalog
162
+ /skills enable – Enable skills integration globally
163
+ /skills disable – Disable skills integration globally
164
+ /skills toggle – Toggle skills integration globally
165
+ /skills refresh – Force skill re-discovery and refresh local cache
166
+ /skills help – Show skills command help
167
+ """
168
+ if name not in (_COMMAND_NAME, *_ALIASES):
169
+ return None
170
+
171
+ from code_muse.messaging import emit_error, emit_info, emit_success, emit_warning
172
+ from code_muse.plugins.agent_skills.config import (
173
+ get_disabled_skills,
174
+ get_skills_enabled,
175
+ set_skills_enabled,
176
+ )
177
+ from code_muse.plugins.agent_skills.discovery import (
178
+ discover_skills,
179
+ refresh_skill_cache,
180
+ )
181
+ from code_muse.plugins.agent_skills.metadata import parse_skill_metadata
182
+ from code_muse.plugins.agent_skills.skills_menu import show_skills_menu
183
+
184
+ tokens = command.split()
185
+
186
+ if len(tokens) > 1:
187
+ subcommand = tokens[1].lower()
188
+
189
+ if subcommand == "list":
190
+ disabled_skills = get_disabled_skills()
191
+ skills = discover_skills()
192
+ enabled = get_skills_enabled()
193
+
194
+ if not skills:
195
+ emit_info("No skills found.")
196
+ emit_info("Create skills in:")
197
+ emit_info(" - ~/.muse/skills/")
198
+ emit_info(" - ./skills/")
199
+ return True
200
+
201
+ emit_info(
202
+ f"\U0001f6e0\ufe0f Skills (integration: {'enabled' if enabled else 'disabled'})"
203
+ )
204
+ emit_info(f"Found {len(skills)} skill(s):\n")
205
+
206
+ for skill in skills:
207
+ metadata = parse_skill_metadata(skill.path)
208
+ if metadata:
209
+ status = (
210
+ "\U0001f534 disabled"
211
+ if metadata.name in disabled_skills
212
+ else "\U0001f7e2 enabled"
213
+ )
214
+ version_str = f" v{metadata.version}" if metadata.version else ""
215
+ author_str = f" by {metadata.author}" if metadata.author else ""
216
+ emit_info(f" {status} {metadata.name}{version_str}{author_str}")
217
+ emit_info(f" {metadata.description}")
218
+ if metadata.tags:
219
+ emit_info(f" tags: {', '.join(metadata.tags)}")
220
+ else:
221
+ status = (
222
+ "\U0001f534 disabled"
223
+ if skill.name in disabled_skills
224
+ else "\U0001f7e2 enabled"
225
+ )
226
+ emit_info(f" {status} {skill.name}")
227
+ emit_info(" (no SKILL.md metadata found)")
228
+ emit_info("")
229
+ return True
230
+
231
+ elif subcommand == "install":
232
+ from code_muse.plugins.agent_skills.skills_install_menu import (
233
+ run_skills_install_menu,
234
+ )
235
+
236
+ run_skills_install_menu()
237
+ return True
238
+
239
+ elif subcommand == "enable":
240
+ set_skills_enabled(True)
241
+ emit_success("\u2705 Skills integration enabled globally")
242
+ return True
243
+
244
+ elif subcommand == "disable":
245
+ set_skills_enabled(False)
246
+ emit_warning("\U0001f534 Skills integration disabled globally")
247
+ return True
248
+
249
+ elif subcommand == "toggle":
250
+ new_state = not get_skills_enabled()
251
+ set_skills_enabled(new_state)
252
+ if new_state:
253
+ emit_success("✅ Skills integration enabled globally")
254
+ else:
255
+ emit_warning("🔴 Skills integration disabled globally")
256
+ return True
257
+
258
+ elif subcommand == "refresh":
259
+ refreshed = refresh_skill_cache()
260
+ valid_skills = [skill for skill in refreshed if skill.has_skill_md]
261
+ emit_success(
262
+ f"🔄 Refreshed skills cache: {len(refreshed)} discovered "
263
+ f"({len(valid_skills)} with SKILL.md)"
264
+ )
265
+ return True
266
+
267
+ elif subcommand == "help":
268
+ emit_info("Available /skills subcommands:")
269
+ emit_info(" /skills list - List all installed skills")
270
+ emit_info(" /skills install - Browse & install from catalog")
271
+ emit_info(" /skills enable - Enable skills integration globally")
272
+ emit_info(" /skills disable - Disable skills integration globally")
273
+ emit_info(" /skills toggle - Toggle skills integration globally")
274
+ emit_info(" /skills refresh - Refresh skill cache")
275
+ emit_info(" /skills - Open interactive skills menu")
276
+ return True
277
+
278
+ else:
279
+ emit_error(f"Unknown subcommand: {subcommand}")
280
+ emit_info(
281
+ "Usage: /skills [list|install|enable|disable|toggle|refresh|help]"
282
+ )
283
+ return True
284
+
285
+ # No subcommand – launch TUI menu
286
+ show_skills_menu()
287
+ return True
288
+
289
+
290
+ # ---------------------------------------------------------------------------
291
+ # Register all callbacks
292
+ # ---------------------------------------------------------------------------
293
+ register_callback("get_model_system_prompt", _inject_skills_into_prompt)
294
+ register_callback("register_tools", _register_skills_tools)
295
+ register_callback("custom_command_help", _skills_command_help)
296
+ register_callback("custom_command", _handle_skills_command)
297
+
298
+ logger.info("Agent Skills plugin loaded")
@@ -0,0 +1,320 @@
1
+ """Remote skills catalog client.
2
+
3
+ Fetches the remote skills catalog JSON and exposes a cached, parsed view.
4
+
5
+ Design goals:
6
+ - Never crash the app (defensive parsing + broad error handling).
7
+ - Local caching with TTL for fast startup and offline use.
8
+ - Synchronous networking only (httpx.Client).
9
+
10
+ Schema source:
11
+ https://www.llmspec.dev/skills/skills.json
12
+ """
13
+
14
+ import json
15
+ import logging
16
+ import time
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Any
20
+ from urllib.parse import urljoin
21
+
22
+ import httpx
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ SKILLS_JSON_URL = "https://www.llmspec.dev/skills/skills.json"
27
+
28
+ _CACHE_DIR = Path.home() / ".muse" / "cache"
29
+ _CACHE_PATH = _CACHE_DIR / "skills_catalog.json"
30
+ _CACHE_TTL_SECONDS = 30 * 60
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class RemoteSkillEntry:
35
+ """Flattened remote skill entry."""
36
+
37
+ name: str
38
+ description: str
39
+ group: str
40
+ download_url: str
41
+ zip_size_bytes: int
42
+ file_count: int
43
+ has_scripts: bool
44
+ has_references: bool
45
+ has_license: bool
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class RemoteCatalogData:
50
+ """Parsed remote catalog.
51
+
52
+ Attributes:
53
+ version: Catalog version string.
54
+ base_url: Base URL used to build absolute download_url values.
55
+ total_skills: Total number of skills in the remote catalog.
56
+ groups: Raw group objects from the JSON (kept as dicts for flexibility).
57
+ entries: Flattened list of all skills across all groups.
58
+ """
59
+
60
+ version: str
61
+ base_url: str
62
+ total_skills: int
63
+ groups: list[dict[str, Any]]
64
+ entries: list[RemoteSkillEntry]
65
+
66
+
67
+ def _safe_int(value: Any, default: int = 0) -> int:
68
+ """Convert value to int, returning default on failure."""
69
+
70
+ try:
71
+ if value is None:
72
+ return default
73
+ return int(value)
74
+ except Exception:
75
+ return default
76
+
77
+
78
+ def _safe_bool(value: Any, default: bool = False) -> bool:
79
+ """Convert value to bool, returning default on failure."""
80
+
81
+ if value is None:
82
+ return default
83
+ return bool(value)
84
+
85
+
86
+ def _cache_is_fresh(cache_path: Path, ttl_seconds: int) -> bool:
87
+ """Check whether the on-disk catalog cache is within TTL."""
88
+
89
+ try:
90
+ if not cache_path.exists():
91
+ return False
92
+ age_seconds = time.time() - cache_path.stat().st_mtime
93
+ return age_seconds <= ttl_seconds
94
+ except Exception as e:
95
+ logger.debug(f"Failed to check cache age for {cache_path}: {e}")
96
+ return False
97
+
98
+
99
+ def _read_cache(cache_path: Path) -> dict[str, Any | None]:
100
+ """Read and deserialize the cached catalog JSON from disk."""
101
+
102
+ try:
103
+ if not cache_path.exists():
104
+ return None
105
+ raw = cache_path.read_text(encoding="utf-8")
106
+ data = json.loads(raw)
107
+ if not isinstance(data, dict):
108
+ logger.warning(f"Cache JSON is not an object: {cache_path}")
109
+ return None
110
+ return data
111
+ except Exception as e:
112
+ logger.warning(f"Failed to read cache {cache_path}: {e}")
113
+ return None
114
+
115
+
116
+ def _write_cache(cache_path: Path, data: dict[str, Any]) -> bool:
117
+ """Serialize and write catalog JSON to the disk cache."""
118
+
119
+ try:
120
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
121
+ # Stable formatting so diffs are readable when debugging.
122
+ cache_path.write_text(
123
+ json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8"
124
+ )
125
+ return True
126
+ except Exception as e:
127
+ logger.warning(f"Failed to write cache {cache_path}: {e}")
128
+ return False
129
+
130
+
131
+ def _fetch_remote_json(url: str) -> dict[str, Any | None]:
132
+ """Fetch the skills catalog JSON from the remote URL."""
133
+
134
+ headers = {
135
+ "Accept": "application/json",
136
+ "User-Agent": "muse/remote-catalog",
137
+ }
138
+
139
+ try:
140
+ with httpx.Client(timeout=15, headers=headers) as client:
141
+ response = client.get(url)
142
+ response.raise_for_status()
143
+ data = response.json()
144
+
145
+ if not isinstance(data, dict):
146
+ logger.error(f"Remote catalog JSON was not an object. Got: {type(data)}")
147
+ return None
148
+
149
+ return data
150
+
151
+ except httpx.HTTPStatusError as e:
152
+ logger.warning(
153
+ "Remote catalog request returned bad status: "
154
+ f"{e.response.status_code} {e.response.reason_phrase}"
155
+ )
156
+ return None
157
+ except (httpx.ConnectError, httpx.TimeoutException, httpx.NetworkError) as e:
158
+ logger.warning(f"Remote catalog network failure: {e}")
159
+ return None
160
+ except json.JSONDecodeError as e:
161
+ logger.warning(f"Remote catalog returned invalid JSON: {e}")
162
+ return None
163
+ except Exception as e:
164
+ logger.exception(f"Unexpected error fetching remote catalog: {e}")
165
+ return None
166
+
167
+
168
+ def _parse_catalog(raw: dict[str, Any]) -> RemoteCatalogData | None:
169
+ """Parse raw JSON dicts into a list of RemoteSkillEntry objects."""
170
+
171
+ try:
172
+ version = str(raw.get("version") or "")
173
+ base_url = str(raw.get("base_url") or "")
174
+ total_skills = _safe_int(raw.get("total_skills"), default=0)
175
+
176
+ raw_groups = raw.get("groups")
177
+ if not isinstance(raw_groups, list):
178
+ logger.warning("Remote catalog 'groups' missing or not a list")
179
+ raw_groups = []
180
+
181
+ groups: list[dict[str, Any]] = []
182
+ entries: list[RemoteSkillEntry] = []
183
+
184
+ # Ensure urljoin behaves (needs trailing slash on base).
185
+ base_for_join = base_url.rstrip("/") + "/" if base_url else ""
186
+
187
+ for group_obj in raw_groups:
188
+ if not isinstance(group_obj, dict):
189
+ continue
190
+ groups.append(group_obj)
191
+
192
+ group_slug = str(group_obj.get("slug") or group_obj.get("name") or "")
193
+ skills = group_obj.get("skills")
194
+ if not isinstance(skills, list):
195
+ continue
196
+
197
+ for skill in skills:
198
+ if not isinstance(skill, dict):
199
+ continue
200
+
201
+ name = str(skill.get("name") or "").strip()
202
+ if not name:
203
+ # If name is missing, it can't be indexed/activated anyway.
204
+ continue
205
+
206
+ description = str(skill.get("description") or "")
207
+ group = str(skill.get("group") or group_slug or "")
208
+
209
+ download_path = str(skill.get("download_url") or "")
210
+ download_url = (
211
+ urljoin(base_for_join, download_path)
212
+ if base_for_join
213
+ else download_path
214
+ )
215
+
216
+ contents = skill.get("contents")
217
+ if not isinstance(contents, dict):
218
+ contents = {}
219
+
220
+ entries.append(
221
+ RemoteSkillEntry(
222
+ name=name,
223
+ description=description,
224
+ group=group,
225
+ download_url=download_url,
226
+ zip_size_bytes=_safe_int(
227
+ skill.get("zip_size_bytes"), default=0
228
+ ),
229
+ file_count=_safe_int(skill.get("file_count"), default=0),
230
+ has_scripts=_safe_bool(
231
+ contents.get("has_scripts"), default=False
232
+ ),
233
+ has_references=_safe_bool(
234
+ contents.get("has_references"), default=False
235
+ ),
236
+ has_license=_safe_bool(
237
+ contents.get("has_license"), default=False
238
+ ),
239
+ )
240
+ )
241
+
242
+ if not version:
243
+ logger.debug("Remote catalog 'version' is missing/empty")
244
+ if not base_url:
245
+ logger.debug("Remote catalog 'base_url' is missing/empty")
246
+
247
+ return RemoteCatalogData(
248
+ version=version,
249
+ base_url=base_url,
250
+ total_skills=total_skills,
251
+ groups=groups,
252
+ entries=entries,
253
+ )
254
+
255
+ except Exception as e:
256
+ logger.exception(f"Failed to parse remote catalog JSON: {e}")
257
+ return None
258
+
259
+
260
+ def fetch_remote_catalog(force_refresh: bool = False) -> RemoteCatalogData | None:
261
+ """Fetch the remote skills catalog with caching and offline fallback.
262
+
263
+ Cache behavior:
264
+ - Cache file: ~/.muse/cache/skills_catalog.json
265
+ - TTL: 30 minutes (based on file mtime)
266
+ - Offline fallback: if network fetch fails, use cache if present (even if expired)
267
+
268
+ Args:
269
+ force_refresh: If True, always attempt a network fetch.
270
+
271
+ Returns:
272
+ Parsed RemoteCatalogData on success, otherwise None.
273
+ """
274
+
275
+ cache_fresh = _cache_is_fresh(_CACHE_PATH, _CACHE_TTL_SECONDS)
276
+
277
+ # Use fresh cache unless forced.
278
+ if not force_refresh and cache_fresh:
279
+ logger.info(f"Using fresh remote catalog cache: {_CACHE_PATH}")
280
+ cached = _read_cache(_CACHE_PATH)
281
+ if cached is None:
282
+ logger.warning("Fresh cache exists but could not be read; refetching")
283
+ else:
284
+ parsed = _parse_catalog(cached)
285
+ if parsed is not None:
286
+ return parsed
287
+ logger.warning("Fresh cache exists but could not be parsed; refetching")
288
+
289
+ if force_refresh:
290
+ logger.info("Force refresh enabled; fetching remote skills catalog")
291
+ elif _CACHE_PATH.exists():
292
+ logger.info(
293
+ "Cache is missing or stale; fetching remote skills catalog "
294
+ f"(cache_path={_CACHE_PATH}, fresh={cache_fresh})"
295
+ )
296
+ else:
297
+ logger.info("No cache present; fetching remote skills catalog")
298
+
299
+ remote_raw = _fetch_remote_json(SKILLS_JSON_URL)
300
+ if remote_raw is not None:
301
+ logger.info("Fetched remote skills catalog successfully")
302
+ _write_cache(_CACHE_PATH, remote_raw)
303
+ parsed = _parse_catalog(remote_raw)
304
+ if parsed is not None:
305
+ return parsed
306
+ logger.warning("Remote catalog fetched but failed to parse")
307
+
308
+ # Offline fallback: use cache even if expired.
309
+ if _CACHE_PATH.exists():
310
+ logger.warning(
311
+ "Remote fetch failed; falling back to cached skills catalog "
312
+ f"(even if expired): {_CACHE_PATH}"
313
+ )
314
+ cached = _read_cache(_CACHE_PATH)
315
+ if cached is None:
316
+ return None
317
+ return _parse_catalog(cached)
318
+
319
+ logger.error("Remote fetch failed and no cache is available")
320
+ return None