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,389 @@
1
+ """Remote skill downloader/installer.
2
+
3
+ Downloads a remote skill ZIP and installs it into the local skills directory.
4
+
5
+ Security notes:
6
+ - Defends against zip-slip path traversal.
7
+ - Defends (somewhat) against zip bombs by capping total uncompressed size.
8
+
9
+ This module never raises to callers; failures are returned as InstallResult.
10
+ """
11
+
12
+ import logging
13
+ import shutil
14
+ import tempfile
15
+ import zipfile
16
+ from pathlib import Path
17
+
18
+ import httpx
19
+
20
+ from code_muse.plugins.agent_skills.discovery import refresh_skill_cache
21
+ from code_muse.plugins.agent_skills.installer import InstallResult
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ _DEFAULT_SKILLS_DIR = Path.home() / ".muse" / "skills"
26
+ _MAX_UNCOMPRESSED_BYTES = 50 * 1024 * 1024 # 50MB
27
+
28
+
29
+ def _zip_entry_parts(name: str) -> list[str]:
30
+ """Return safe-ish path parts for a zip entry.
31
+
32
+ Zip files use POSIX-style separators, but malicious zips sometimes include
33
+ backslashes. We normalize to '/' then split.
34
+ """
35
+
36
+ normalized = name.replace("\\", "/")
37
+ return [part for part in normalized.split("/") if part not in {"", "."}]
38
+
39
+
40
+ def _safe_rmtree(path: Path) -> bool:
41
+ """Remove a directory tree, logging errors instead of raising."""
42
+
43
+ try:
44
+ if not path.exists():
45
+ return True
46
+ shutil.rmtree(path)
47
+ return True
48
+ except Exception as e:
49
+ logger.warning(f"Failed to remove directory {path}: {e}")
50
+ return False
51
+
52
+
53
+ def _download_to_file(url: str, dest: Path) -> bool:
54
+ """Download a URL to a local file path with streaming."""
55
+
56
+ headers = {
57
+ "Accept": "application/zip, application/octet-stream, */*",
58
+ "User-Agent": "muse/skill-downloader",
59
+ }
60
+
61
+ try:
62
+ dest.parent.mkdir(parents=True, exist_ok=True)
63
+
64
+ with httpx.Client(timeout=30, headers=headers, follow_redirects=True) as client:
65
+ with client.stream("GET", url) as response:
66
+ response.raise_for_status()
67
+
68
+ with dest.open("wb") as f:
69
+ for chunk in response.iter_bytes():
70
+ if chunk:
71
+ f.write(chunk)
72
+
73
+ logger.info(f"Downloaded skill zip to {dest}")
74
+ return True
75
+
76
+ except httpx.HTTPStatusError as e:
77
+ logger.warning(
78
+ "Skill download failed with HTTP status: "
79
+ f"{e.response.status_code} {e.response.reason_phrase}"
80
+ )
81
+ return False
82
+ except (httpx.ConnectError, httpx.TimeoutException, httpx.NetworkError) as e:
83
+ logger.warning(f"Skill download network failure: {e}")
84
+ return False
85
+ except Exception as e:
86
+ logger.exception(f"Unexpected error downloading {url}: {e}")
87
+ return False
88
+
89
+
90
+ def _is_within_directory(base_dir: Path, candidate: Path) -> bool:
91
+ """Check that a path is safely contained within a directory."""
92
+
93
+ try:
94
+ base_resolved = base_dir.resolve()
95
+ candidate_resolved = candidate.resolve()
96
+ candidate_resolved.relative_to(base_resolved)
97
+ return True
98
+ except Exception:
99
+ return False
100
+
101
+
102
+ def _validate_zip_safety(zf: zipfile.ZipFile) -> str | None:
103
+ """Return an error message if unsafe, otherwise None."""
104
+
105
+ total_uncompressed = 0
106
+
107
+ for info in zf.infolist():
108
+ # Directory entries are fine.
109
+ if info.is_dir():
110
+ continue
111
+
112
+ total_uncompressed += int(info.file_size or 0)
113
+ if total_uncompressed > _MAX_UNCOMPRESSED_BYTES:
114
+ return (
115
+ "ZIP appears too large when uncompressed "
116
+ f"(>{_MAX_UNCOMPRESSED_BYTES} bytes)"
117
+ )
118
+
119
+ # Basic zip-slip protection: reject absolute paths and parent traversals.
120
+ name = info.filename
121
+ normalized = name.replace("\\", "/")
122
+ if normalized.startswith("/"):
123
+ return f"Unsafe zip entry path (absolute): {name}"
124
+
125
+ parts = _zip_entry_parts(name)
126
+ if ".." in parts:
127
+ return f"Unsafe zip entry path (traversal): {name}"
128
+
129
+ return None
130
+
131
+
132
+ def _safe_extract_zip(zf: zipfile.ZipFile, extract_dir: Path) -> bool:
133
+ """Safely extract zip contents into extract_dir."""
134
+
135
+ try:
136
+ extract_dir.mkdir(parents=True, exist_ok=True)
137
+
138
+ for info in zf.infolist():
139
+ parts = _zip_entry_parts(info.filename)
140
+
141
+ # Skip weird metadata folders.
142
+ if parts and parts[0] == "__MACOSX":
143
+ continue
144
+
145
+ dest_path = extract_dir.joinpath(*parts)
146
+
147
+ if not _is_within_directory(extract_dir, dest_path):
148
+ logger.warning(
149
+ f"Blocked zip entry outside extraction dir: {info.filename}"
150
+ )
151
+ return False
152
+
153
+ if info.is_dir():
154
+ dest_path.mkdir(parents=True, exist_ok=True)
155
+ continue
156
+
157
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
158
+
159
+ with zf.open(info, "r") as src, dest_path.open("wb") as dst:
160
+ shutil.copyfileobj(src, dst)
161
+
162
+ return True
163
+
164
+ except Exception as e:
165
+ logger.exception(f"Failed to extract zip safely: {e}")
166
+ return False
167
+
168
+
169
+ def _determine_extracted_root(extract_dir: Path) -> Path | None:
170
+ """Determine where the skill files live inside an extracted zip.
171
+
172
+ Supports:
173
+ - Files at the zip root
174
+ - Files inside a single top-level folder
175
+
176
+ Returns:
177
+ Path to the directory containing SKILL.md, or None.
178
+ """
179
+
180
+ try:
181
+ if (extract_dir / "SKILL.md").is_file():
182
+ return extract_dir
183
+
184
+ children = [p for p in extract_dir.iterdir() if p.name != "__MACOSX"]
185
+ dirs = [p for p in children if p.is_dir()]
186
+ files = [p for p in children if p.is_file()]
187
+
188
+ # If it's root-level but SKILL.md missing, no good.
189
+ if files:
190
+ return None
191
+
192
+ if len(dirs) == 1:
193
+ candidate = dirs[0]
194
+ if (candidate / "SKILL.md").is_file():
195
+ return candidate
196
+
197
+ return None
198
+
199
+ except Exception as e:
200
+ logger.warning(f"Failed to inspect extracted zip directory {extract_dir}: {e}")
201
+ return None
202
+
203
+
204
+ def _stage_normalized_install(
205
+ extracted_root: Path, skill_name: str, staging_base: Path
206
+ ) -> Path | None:
207
+ """Copy extracted content into staging_base/<skill_name>."""
208
+
209
+ try:
210
+ staged_skill_dir = staging_base / skill_name
211
+ if staged_skill_dir.exists():
212
+ _safe_rmtree(staged_skill_dir)
213
+
214
+ shutil.copytree(extracted_root, staged_skill_dir)
215
+
216
+ if not (staged_skill_dir / "SKILL.md").is_file():
217
+ logger.warning(
218
+ f"Staged skill is missing SKILL.md: {(staged_skill_dir / 'SKILL.md')}"
219
+ )
220
+ return None
221
+
222
+ return staged_skill_dir
223
+
224
+ except Exception as e:
225
+ logger.exception(f"Failed to stage normalized install for {skill_name}: {e}")
226
+ return None
227
+
228
+
229
+ def download_and_install_skill(
230
+ skill_name: str,
231
+ download_url: str,
232
+ target_dir: Path | None = None,
233
+ force: bool = False,
234
+ ) -> InstallResult:
235
+ """Download and install a remote skill zip.
236
+
237
+ Args:
238
+ skill_name: Skill name (directory name under target_dir).
239
+ download_url: Absolute URL to the skill .zip.
240
+ target_dir: Base skills directory. Defaults to ~/.muse/skills.
241
+ force: If True, delete any existing install first.
242
+
243
+ Returns:
244
+ InstallResult indicating success/failure.
245
+ """
246
+
247
+ skill_name = skill_name.strip()
248
+ if not skill_name:
249
+ return InstallResult(success=False, message="skill_name is required")
250
+
251
+ # Prevent path traversal via skill_name.
252
+ if Path(skill_name).name != skill_name or skill_name in {".", ".."}:
253
+ return InstallResult(
254
+ success=False, message="skill_name must be a simple directory name"
255
+ )
256
+
257
+ base_dir = target_dir or _DEFAULT_SKILLS_DIR
258
+ skill_dir = base_dir / skill_name
259
+
260
+ try:
261
+ if skill_dir.exists():
262
+ if not force:
263
+ return InstallResult(
264
+ success=False,
265
+ message=f"Skill already installed at {skill_dir} (use force=True to reinstall)",
266
+ installed_path=skill_dir,
267
+ )
268
+
269
+ logger.info(
270
+ f"Force reinstall enabled; removing existing skill at {skill_dir}"
271
+ )
272
+ if not _safe_rmtree(skill_dir):
273
+ return InstallResult(
274
+ success=False,
275
+ message=f"Failed to remove existing skill directory: {skill_dir}",
276
+ installed_path=skill_dir,
277
+ )
278
+
279
+ base_dir.mkdir(parents=True, exist_ok=True)
280
+
281
+ with tempfile.TemporaryDirectory(prefix="muse_skill_") as tmp:
282
+ tmp_dir = Path(tmp)
283
+ tmp_zip = tmp_dir / f"{skill_name}.zip"
284
+ extract_dir = tmp_dir / "extracted"
285
+ staging_dir = tmp_dir / "staging"
286
+ staging_dir.mkdir(parents=True, exist_ok=True)
287
+
288
+ if not _download_to_file(download_url, tmp_zip):
289
+ return InstallResult(
290
+ success=False,
291
+ message=f"Failed to download skill zip from {download_url}",
292
+ )
293
+
294
+ try:
295
+ with zipfile.ZipFile(tmp_zip, "r") as zf:
296
+ unsafe_reason = _validate_zip_safety(zf)
297
+ if unsafe_reason:
298
+ logger.warning(
299
+ f"Rejected unsafe zip for {skill_name}: {unsafe_reason}"
300
+ )
301
+ return InstallResult(
302
+ success=False,
303
+ message=f"Rejected unsafe zip: {unsafe_reason}",
304
+ )
305
+
306
+ if not _safe_extract_zip(zf, extract_dir):
307
+ return InstallResult(
308
+ success=False,
309
+ message="Failed to extract skill zip safely",
310
+ )
311
+ except zipfile.BadZipFile:
312
+ logger.warning(f"Downloaded file is not a valid zip: {tmp_zip}")
313
+ return InstallResult(
314
+ success=False, message="Downloaded file is not a valid zip"
315
+ )
316
+ except Exception as e:
317
+ logger.exception(f"Failed to open/extract zip for {skill_name}: {e}")
318
+ return InstallResult(success=False, message="Failed to extract zip")
319
+
320
+ extracted_root = _determine_extracted_root(extract_dir)
321
+ if extracted_root is None:
322
+ logger.warning(
323
+ "Extracted zip layout not recognized or missing SKILL.md. "
324
+ f"extract_dir={extract_dir}"
325
+ )
326
+ return InstallResult(
327
+ success=False,
328
+ message="Extracted zip missing SKILL.md or has unexpected layout",
329
+ )
330
+
331
+ staged_skill_dir = _stage_normalized_install(
332
+ extracted_root=extracted_root,
333
+ skill_name=skill_name,
334
+ staging_base=staging_dir,
335
+ )
336
+ if staged_skill_dir is None:
337
+ return InstallResult(
338
+ success=False,
339
+ message="Failed to stage extracted skill (missing SKILL.md)",
340
+ )
341
+
342
+ # Move staged install into final destination.
343
+ try:
344
+ if skill_dir.exists():
345
+ # Shouldn't happen (handled earlier), but be safe.
346
+ if force:
347
+ _safe_rmtree(skill_dir)
348
+ else:
349
+ return InstallResult(
350
+ success=False,
351
+ message=f"Skill directory already exists: {skill_dir}",
352
+ installed_path=skill_dir,
353
+ )
354
+
355
+ shutil.move(str(staged_skill_dir), str(skill_dir))
356
+ except Exception as e:
357
+ logger.exception(f"Failed to install skill into {skill_dir}: {e}")
358
+ # Cleanup partial install.
359
+ _safe_rmtree(skill_dir)
360
+ return InstallResult(
361
+ success=False, message="Failed to move skill into place"
362
+ )
363
+
364
+ # Post-install verification.
365
+ if not (skill_dir / "SKILL.md").is_file():
366
+ logger.warning(f"Installed skill missing SKILL.md: {skill_dir}")
367
+ _safe_rmtree(skill_dir)
368
+ return InstallResult(
369
+ success=False,
370
+ message="Installed skill is missing SKILL.md",
371
+ installed_path=skill_dir,
372
+ )
373
+
374
+ try:
375
+ refresh_skill_cache()
376
+ except Exception as e:
377
+ # Cache refresh failure should not poison a successful install.
378
+ logger.warning(f"Skill installed but failed to refresh skill cache: {e}")
379
+
380
+ logger.info(f"Installed skill '{skill_name}' into {skill_dir}")
381
+ return InstallResult(
382
+ success=True,
383
+ message=f"Installed skill '{skill_name}'",
384
+ installed_path=skill_dir,
385
+ )
386
+
387
+ except Exception as e:
388
+ logger.exception(f"Unexpected error installing skill {skill_name}: {e}")
389
+ return InstallResult(success=False, message="Unexpected error installing skill")
@@ -0,0 +1,19 @@
1
+ """Agent skills installation helpers.
2
+
3
+ This module currently provides the shared InstallResult type used by skill
4
+ installers (e.g. local installers, remote zip downloaders).
5
+
6
+ It is intentionally small so other modules can depend on a stable result shape.
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class InstallResult:
15
+ """Result of a skill install attempt."""
16
+
17
+ success: bool
18
+ message: str
19
+ installed_path: Path | None = None
@@ -0,0 +1,293 @@
1
+ """Skill metadata parsing - extracts info from SKILL.md frontmatter.
2
+
3
+ Includes size validation before reading and content capping for
4
+ model-context injection.
5
+ """
6
+
7
+ import hashlib
8
+ import logging
9
+ import re
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+
13
+ from code_muse.plugins.agent_skills.discovery import (
14
+ MAX_SKILL_MD_BYTES,
15
+ cap_skill_content,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Regex pattern to match YAML frontmatter between --- delimiters
21
+ FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
22
+
23
+ # Regex patterns for parsing simple key-value pairs from YAML-like frontmatter
24
+ KEY_VALUE_PATTERN = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)$", re.MULTILINE)
25
+ LIST_PATTERN = re.compile(r"^\s+-\s+(.+)$", re.MULTILINE)
26
+
27
+
28
+ @dataclass
29
+ class SkillMetadata:
30
+ """Parsed skill metadata from SKILL.md frontmatter."""
31
+
32
+ name: str
33
+ description: str
34
+ path: Path
35
+ version: str | None = None
36
+ author: str | None = None
37
+ tags: list[str] = field(default_factory=list)
38
+ source: str | None = None
39
+ trust: str | None = None
40
+ skill_md_hash: str | None = None
41
+ skill_md_size: int | None = None
42
+
43
+
44
+ def _unquote(value: str) -> str:
45
+ """Remove quotes from a YAML string value if present."""
46
+ value = value.strip()
47
+ if (value.startswith('"') and value.endswith('"')) or (
48
+ value.startswith("'") and value.endswith("'")
49
+ ):
50
+ return value[1:-1]
51
+ return value
52
+
53
+
54
+ def parse_yaml_frontmatter(content: str) -> dict:
55
+ """Extract YAML frontmatter from SKILL.md content.
56
+
57
+ Frontmatter is between --- delimiters at the start of file.
58
+ Uses simple regex parsing to avoid heavy yaml dependency.
59
+
60
+ Args:
61
+ content: The full content of the SKILL.md file.
62
+
63
+ Returns:
64
+ Dictionary containing parsed frontmatter key-value pairs.
65
+ Returns empty dict if no frontmatter found or parsing fails.
66
+ """
67
+ match = FRONTMATTER_PATTERN.match(content)
68
+ if not match:
69
+ logger.debug("No frontmatter found in content")
70
+ return {}
71
+
72
+ frontmatter = match.group(1)
73
+ result: dict = {}
74
+ current_key: str | None = None
75
+ current_list: list[str] = []
76
+
77
+ for line in frontmatter.split("\n"):
78
+ stripped = line.strip()
79
+
80
+ # Skip empty lines and comments
81
+ if not stripped or stripped.startswith("#"):
82
+ continue
83
+
84
+ # Check if this is a list item
85
+ list_match = LIST_PATTERN.match(line)
86
+ if list_match and current_key:
87
+ current_list.append(_unquote(list_match.group(1)))
88
+ continue
89
+
90
+ # Check if this is a key-value pair
91
+ kv_match = KEY_VALUE_PATTERN.match(line)
92
+ if kv_match:
93
+ # Save any accumulated list items from previous key
94
+ if current_key and current_list:
95
+ result[current_key] = current_list
96
+ current_list = []
97
+
98
+ key, value = kv_match.groups()
99
+ key = key.strip()
100
+ value = value.strip()
101
+
102
+ # If value is empty, this might be a list start
103
+ if not value:
104
+ current_key = key
105
+ result[key] = [] # Initialize as empty list
106
+ else:
107
+ result[key] = _unquote(value)
108
+ current_key = None
109
+
110
+ # Handle case where list items were at the end
111
+ if current_key and current_list:
112
+ result[current_key] = current_list
113
+
114
+ return result
115
+
116
+
117
+ def parse_skill_metadata(skill_path: Path) -> SkillMetadata | None:
118
+ """Parse metadata from a skill's SKILL.md file.
119
+
120
+ Args:
121
+ skill_path: Path to the skill directory (not the SKILL.md file)
122
+
123
+ Returns:
124
+ SkillMetadata if successful, None if parsing fails.
125
+ """
126
+ if not skill_path.exists():
127
+ logger.warning(f"Skill path does not exist: {skill_path}")
128
+ return None
129
+
130
+ skill_md_path = skill_path / "SKILL.md"
131
+ if not skill_md_path.exists():
132
+ logger.debug(f"SKILL.md not found in skill directory: {skill_path}")
133
+ return None
134
+
135
+ # Validate size before reading
136
+ try:
137
+ file_size = skill_md_path.stat().st_size
138
+ if file_size > MAX_SKILL_MD_BYTES:
139
+ logger.warning(
140
+ "SKILL.md at %s is too large (%d bytes, max %d), skipping",
141
+ skill_md_path,
142
+ file_size,
143
+ MAX_SKILL_MD_BYTES,
144
+ )
145
+ return None
146
+ except OSError as e:
147
+ logger.error("Failed to stat SKILL.md at %s: %s", skill_md_path, e)
148
+ return None
149
+
150
+ try:
151
+ content = skill_md_path.read_text(encoding="utf-8")
152
+ except Exception as e:
153
+ logger.error("Failed to read SKILL.md at %s: %s", skill_md_path, e)
154
+ return None
155
+
156
+ # Compute hash for trust/source tracking
157
+ content_hash = hashlib.sha256(content.encode()).hexdigest()
158
+
159
+ frontmatter = parse_yaml_frontmatter(content)
160
+ if not frontmatter:
161
+ logger.debug(f"No valid frontmatter found in {skill_md_path}")
162
+ return None
163
+
164
+ # Required fields
165
+ name = frontmatter.get("name")
166
+ if not name:
167
+ logger.debug(
168
+ f"'name' is required in frontmatter but not found in {skill_md_path}"
169
+ )
170
+ return None
171
+
172
+ description = frontmatter.get("description")
173
+ if not description:
174
+ logger.debug(
175
+ f"'description' is required in frontmatter but not found in {skill_md_path}"
176
+ )
177
+ return None
178
+
179
+ # Handle tags - could be a list or a comma-separated string
180
+ tags: list[str] = []
181
+ raw_tags = frontmatter.get("tags", [])
182
+ if isinstance(raw_tags, list):
183
+ tags = raw_tags
184
+ elif isinstance(raw_tags, str):
185
+ tags = [tag.strip() for tag in raw_tags.split(",") if tag.strip()]
186
+
187
+ # Classify source/trust
188
+ source = _classify_skill_source(skill_path)
189
+ trust = source # user/project/unknown
190
+
191
+ return SkillMetadata(
192
+ name=name,
193
+ description=description,
194
+ path=skill_path,
195
+ version=frontmatter.get("version"),
196
+ author=frontmatter.get("author"),
197
+ tags=tags,
198
+ source=source,
199
+ trust=trust,
200
+ skill_md_hash=content_hash,
201
+ skill_md_size=file_size,
202
+ )
203
+
204
+
205
+ def _classify_skill_source(skill_path: Path) -> str:
206
+ """Classify a skill directory as 'user', 'project', or 'unknown'."""
207
+ home_skills = Path.home() / ".muse" / "skills"
208
+ try:
209
+ skill_path.resolve().relative_to(home_skills.resolve())
210
+ return "user"
211
+ except ValueError:
212
+ pass
213
+ try:
214
+ skill_path.resolve().relative_to(Path.cwd().resolve())
215
+ return "project"
216
+ except ValueError:
217
+ pass
218
+ return "unknown"
219
+
220
+
221
+ def load_full_skill_content(skill_path: Path) -> str | None:
222
+ """Load the complete SKILL.md content for activation.
223
+
224
+ Validates file size before reading and caps content to
225
+ prevent model-context blowup.
226
+
227
+ Args:
228
+ skill_path: Path to the skill directory
229
+
230
+ Returns:
231
+ Full file content as string (capped), or None if not found.
232
+ """
233
+ if not skill_path.exists():
234
+ logger.warning("Skill path does not exist: %s", skill_path)
235
+ return None
236
+
237
+ skill_md_path = skill_path / "SKILL.md"
238
+ if not skill_md_path.exists():
239
+ logger.warning("SKILL.md not found in skill directory: %s", skill_path)
240
+ return None
241
+
242
+ # Validate size before reading
243
+ try:
244
+ file_size = skill_md_path.stat().st_size
245
+ if file_size > MAX_SKILL_MD_BYTES:
246
+ logger.warning(
247
+ "SKILL.md at %s is too large (%d bytes, max %d)",
248
+ skill_md_path,
249
+ file_size,
250
+ MAX_SKILL_MD_BYTES,
251
+ )
252
+ return None
253
+ except OSError:
254
+ return None
255
+
256
+ try:
257
+ content = skill_md_path.read_text(encoding="utf-8")
258
+ # Cap content for model-context injection
259
+ return cap_skill_content(content)
260
+ except Exception as e:
261
+ logger.error("Failed to read SKILL.md at %s: %s", skill_md_path, e)
262
+ return None
263
+
264
+
265
+ def get_skill_resources(skill_path: Path) -> list[Path]:
266
+ """List all resource files bundled with a skill.
267
+
268
+ Returns paths to all non-SKILL.md files in the skill directory.
269
+
270
+ Args:
271
+ skill_path: Path to the skill directory
272
+
273
+ Returns:
274
+ List of paths to resource files (excluding SKILL.md).
275
+ """
276
+ if not skill_path.exists():
277
+ logger.warning(f"Skill path does not exist: {skill_path}")
278
+ return []
279
+
280
+ if not skill_path.is_dir():
281
+ logger.warning(f"Skill path is not a directory: {skill_path}")
282
+ return []
283
+
284
+ resources: list[Path] = []
285
+ try:
286
+ for item in skill_path.iterdir():
287
+ if item.is_file() and item.name != "SKILL.md":
288
+ resources.append(item)
289
+ except Exception as e:
290
+ logger.error(f"Failed to list resources in {skill_path}: {e}")
291
+ return []
292
+
293
+ return sorted(resources) # Sort for consistent ordering