calvyn-code 0.14.0

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 (1718) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/README.zh-CN.md +180 -0
  4. package/acp_adapter/__init__.py +1 -0
  5. package/acp_adapter/__main__.py +5 -0
  6. package/acp_adapter/auth.py +68 -0
  7. package/acp_adapter/bootstrap/__init__.py +0 -0
  8. package/acp_adapter/bootstrap/bootstrap_browser_tools.ps1 +288 -0
  9. package/acp_adapter/bootstrap/bootstrap_browser_tools.sh +399 -0
  10. package/acp_adapter/entry.py +292 -0
  11. package/acp_adapter/events.py +265 -0
  12. package/acp_adapter/permissions.py +148 -0
  13. package/acp_adapter/server.py +1713 -0
  14. package/acp_adapter/session.py +629 -0
  15. package/acp_adapter/tools.py +1180 -0
  16. package/agent/__init__.py +6 -0
  17. package/agent/__pycache__/__init__.cpython-312.pyc +0 -0
  18. package/agent/__pycache__/account_usage.cpython-312.pyc +0 -0
  19. package/agent/__pycache__/anthropic_adapter.cpython-312.pyc +0 -0
  20. package/agent/__pycache__/async_utils.cpython-312.pyc +0 -0
  21. package/agent/__pycache__/auxiliary_client.cpython-312.pyc +0 -0
  22. package/agent/__pycache__/codex_responses_adapter.cpython-312.pyc +0 -0
  23. package/agent/__pycache__/context_compressor.cpython-312.pyc +0 -0
  24. package/agent/__pycache__/context_engine.cpython-312.pyc +0 -0
  25. package/agent/__pycache__/context_references.cpython-312.pyc +0 -0
  26. package/agent/__pycache__/credential_pool.cpython-312.pyc +0 -0
  27. package/agent/__pycache__/curator.cpython-312.pyc +0 -0
  28. package/agent/__pycache__/display.cpython-312.pyc +0 -0
  29. package/agent/__pycache__/error_classifier.cpython-312.pyc +0 -0
  30. package/agent/__pycache__/file_safety.cpython-312.pyc +0 -0
  31. package/agent/__pycache__/google_code_assist.cpython-312.pyc +0 -0
  32. package/agent/__pycache__/google_oauth.cpython-312.pyc +0 -0
  33. package/agent/__pycache__/i18n.cpython-312.pyc +0 -0
  34. package/agent/__pycache__/image_gen_provider.cpython-312.pyc +0 -0
  35. package/agent/__pycache__/image_gen_registry.cpython-312.pyc +0 -0
  36. package/agent/__pycache__/insights.cpython-312.pyc +0 -0
  37. package/agent/__pycache__/lmstudio_reasoning.cpython-312.pyc +0 -0
  38. package/agent/__pycache__/manual_compression_feedback.cpython-312.pyc +0 -0
  39. package/agent/__pycache__/markdown_tables.cpython-312.pyc +0 -0
  40. package/agent/__pycache__/memory_manager.cpython-312.pyc +0 -0
  41. package/agent/__pycache__/memory_provider.cpython-312.pyc +0 -0
  42. package/agent/__pycache__/model_metadata.cpython-312.pyc +0 -0
  43. package/agent/__pycache__/models_dev.cpython-312.pyc +0 -0
  44. package/agent/__pycache__/moonshot_schema.cpython-312.pyc +0 -0
  45. package/agent/__pycache__/onboarding.cpython-312.pyc +0 -0
  46. package/agent/__pycache__/portal_tags.cpython-312.pyc +0 -0
  47. package/agent/__pycache__/prompt_builder.cpython-312.pyc +0 -0
  48. package/agent/__pycache__/prompt_caching.cpython-312.pyc +0 -0
  49. package/agent/__pycache__/redact.cpython-312.pyc +0 -0
  50. package/agent/__pycache__/retry_utils.cpython-312.pyc +0 -0
  51. package/agent/__pycache__/shell_hooks.cpython-312.pyc +0 -0
  52. package/agent/__pycache__/skill_commands.cpython-312.pyc +0 -0
  53. package/agent/__pycache__/skill_preprocessing.cpython-312.pyc +0 -0
  54. package/agent/__pycache__/skill_utils.cpython-312.pyc +0 -0
  55. package/agent/__pycache__/subdirectory_hints.cpython-312.pyc +0 -0
  56. package/agent/__pycache__/think_scrubber.cpython-312.pyc +0 -0
  57. package/agent/__pycache__/title_generator.cpython-312.pyc +0 -0
  58. package/agent/__pycache__/tool_guardrails.cpython-312.pyc +0 -0
  59. package/agent/__pycache__/tool_result_classification.cpython-312.pyc +0 -0
  60. package/agent/__pycache__/trajectory.cpython-312.pyc +0 -0
  61. package/agent/__pycache__/usage_pricing.cpython-312.pyc +0 -0
  62. package/agent/__pycache__/video_gen_provider.cpython-312.pyc +0 -0
  63. package/agent/__pycache__/video_gen_registry.cpython-312.pyc +0 -0
  64. package/agent/__pycache__/web_search_provider.cpython-312.pyc +0 -0
  65. package/agent/__pycache__/web_search_registry.cpython-312.pyc +0 -0
  66. package/agent/account_usage.py +326 -0
  67. package/agent/anthropic_adapter.py +2087 -0
  68. package/agent/async_utils.py +68 -0
  69. package/agent/auxiliary_client.py +4893 -0
  70. package/agent/bedrock_adapter.py +1276 -0
  71. package/agent/codex_responses_adapter.py +1084 -0
  72. package/agent/context_compressor.py +1583 -0
  73. package/agent/context_engine.py +211 -0
  74. package/agent/context_references.py +519 -0
  75. package/agent/copilot_acp_client.py +684 -0
  76. package/agent/credential_pool.py +1780 -0
  77. package/agent/credential_sources.py +449 -0
  78. package/agent/curator.py +1782 -0
  79. package/agent/curator_backup.py +694 -0
  80. package/agent/display.py +987 -0
  81. package/agent/error_classifier.py +1058 -0
  82. package/agent/file_safety.py +112 -0
  83. package/agent/gemini_cloudcode_adapter.py +909 -0
  84. package/agent/gemini_native_adapter.py +971 -0
  85. package/agent/gemini_schema.py +99 -0
  86. package/agent/google_code_assist.py +452 -0
  87. package/agent/google_oauth.py +1062 -0
  88. package/agent/i18n.py +258 -0
  89. package/agent/image_gen_provider.py +243 -0
  90. package/agent/image_gen_registry.py +145 -0
  91. package/agent/image_routing.py +301 -0
  92. package/agent/insights.py +931 -0
  93. package/agent/lmstudio_reasoning.py +48 -0
  94. package/agent/lsp/__init__.py +106 -0
  95. package/agent/lsp/__pycache__/__init__.cpython-312.pyc +0 -0
  96. package/agent/lsp/__pycache__/cli.cpython-312.pyc +0 -0
  97. package/agent/lsp/__pycache__/client.cpython-312.pyc +0 -0
  98. package/agent/lsp/__pycache__/eventlog.cpython-312.pyc +0 -0
  99. package/agent/lsp/__pycache__/manager.cpython-312.pyc +0 -0
  100. package/agent/lsp/__pycache__/protocol.cpython-312.pyc +0 -0
  101. package/agent/lsp/__pycache__/servers.cpython-312.pyc +0 -0
  102. package/agent/lsp/__pycache__/workspace.cpython-312.pyc +0 -0
  103. package/agent/lsp/cli.py +308 -0
  104. package/agent/lsp/client.py +930 -0
  105. package/agent/lsp/eventlog.py +213 -0
  106. package/agent/lsp/install.py +376 -0
  107. package/agent/lsp/manager.py +644 -0
  108. package/agent/lsp/protocol.py +196 -0
  109. package/agent/lsp/range_shift.py +149 -0
  110. package/agent/lsp/reporter.py +78 -0
  111. package/agent/lsp/servers.py +1040 -0
  112. package/agent/lsp/workspace.py +223 -0
  113. package/agent/manual_compression_feedback.py +49 -0
  114. package/agent/markdown_tables.py +309 -0
  115. package/agent/memory_manager.py +556 -0
  116. package/agent/memory_provider.py +279 -0
  117. package/agent/model_metadata.py +1827 -0
  118. package/agent/models_dev.py +724 -0
  119. package/agent/moonshot_schema.py +231 -0
  120. package/agent/nous_rate_guard.py +326 -0
  121. package/agent/onboarding.py +193 -0
  122. package/agent/plugin_llm.py +1046 -0
  123. package/agent/portal_tags.py +64 -0
  124. package/agent/prompt_builder.py +1457 -0
  125. package/agent/prompt_caching.py +79 -0
  126. package/agent/rate_limit_tracker.py +246 -0
  127. package/agent/redact.py +403 -0
  128. package/agent/retry_utils.py +57 -0
  129. package/agent/shell_hooks.py +837 -0
  130. package/agent/skill_commands.py +502 -0
  131. package/agent/skill_preprocessing.py +131 -0
  132. package/agent/skill_utils.py +512 -0
  133. package/agent/subdirectory_hints.py +224 -0
  134. package/agent/think_scrubber.py +386 -0
  135. package/agent/title_generator.py +171 -0
  136. package/agent/tool_guardrails.py +458 -0
  137. package/agent/tool_result_classification.py +26 -0
  138. package/agent/trajectory.py +56 -0
  139. package/agent/transports/__init__.py +68 -0
  140. package/agent/transports/__pycache__/__init__.cpython-312.pyc +0 -0
  141. package/agent/transports/__pycache__/anthropic.cpython-312.pyc +0 -0
  142. package/agent/transports/__pycache__/base.cpython-312.pyc +0 -0
  143. package/agent/transports/__pycache__/bedrock.cpython-312.pyc +0 -0
  144. package/agent/transports/__pycache__/chat_completions.cpython-312.pyc +0 -0
  145. package/agent/transports/__pycache__/codex.cpython-312.pyc +0 -0
  146. package/agent/transports/__pycache__/types.cpython-312.pyc +0 -0
  147. package/agent/transports/anthropic.py +179 -0
  148. package/agent/transports/base.py +89 -0
  149. package/agent/transports/bedrock.py +154 -0
  150. package/agent/transports/chat_completions.py +614 -0
  151. package/agent/transports/codex.py +283 -0
  152. package/agent/transports/codex_app_server.py +368 -0
  153. package/agent/transports/codex_app_server_session.py +810 -0
  154. package/agent/transports/codex_event_projector.py +312 -0
  155. package/agent/transports/hermes_tools_mcp_server.py +233 -0
  156. package/agent/transports/types.py +162 -0
  157. package/agent/usage_pricing.py +877 -0
  158. package/agent/video_gen_provider.py +300 -0
  159. package/agent/video_gen_registry.py +117 -0
  160. package/agent/web_search_provider.py +221 -0
  161. package/agent/web_search_registry.py +262 -0
  162. package/assets/banner.png +0 -0
  163. package/batch_runner.py +1303 -0
  164. package/bin/calvyn.js +67 -0
  165. package/calvyn_bootstrap.py +130 -0
  166. package/calvyn_constants.py +346 -0
  167. package/calvyn_logging.py +390 -0
  168. package/calvyn_state.py +2967 -0
  169. package/calvyn_time.py +105 -0
  170. package/cli.py +14160 -0
  171. package/cron/__init__.py +42 -0
  172. package/cron/__pycache__/__init__.cpython-312.pyc +0 -0
  173. package/cron/__pycache__/jobs.cpython-312.pyc +0 -0
  174. package/cron/__pycache__/scheduler.cpython-312.pyc +0 -0
  175. package/cron/jobs.py +1160 -0
  176. package/cron/scheduler.py +1832 -0
  177. package/gateway/__init__.py +35 -0
  178. package/gateway/__pycache__/__init__.cpython-312.pyc +0 -0
  179. package/gateway/__pycache__/channel_directory.cpython-312.pyc +0 -0
  180. package/gateway/__pycache__/config.cpython-312.pyc +0 -0
  181. package/gateway/__pycache__/delivery.cpython-312.pyc +0 -0
  182. package/gateway/__pycache__/display_config.cpython-312.pyc +0 -0
  183. package/gateway/__pycache__/hooks.cpython-312.pyc +0 -0
  184. package/gateway/__pycache__/pairing.cpython-312.pyc +0 -0
  185. package/gateway/__pycache__/platform_registry.cpython-312.pyc +0 -0
  186. package/gateway/__pycache__/restart.cpython-312.pyc +0 -0
  187. package/gateway/__pycache__/run.cpython-312.pyc +0 -0
  188. package/gateway/__pycache__/runtime_footer.cpython-312.pyc +0 -0
  189. package/gateway/__pycache__/session.cpython-312.pyc +0 -0
  190. package/gateway/__pycache__/session_context.cpython-312.pyc +0 -0
  191. package/gateway/__pycache__/shutdown_forensics.cpython-312.pyc +0 -0
  192. package/gateway/__pycache__/slash_access.cpython-312.pyc +0 -0
  193. package/gateway/__pycache__/status.cpython-312.pyc +0 -0
  194. package/gateway/__pycache__/stream_consumer.cpython-312.pyc +0 -0
  195. package/gateway/__pycache__/whatsapp_identity.cpython-312.pyc +0 -0
  196. package/gateway/assets/telegram-botfather-threads-settings.jpg +0 -0
  197. package/gateway/builtin_hooks/__init__.py +1 -0
  198. package/gateway/channel_directory.py +357 -0
  199. package/gateway/config.py +1873 -0
  200. package/gateway/delivery.py +258 -0
  201. package/gateway/display_config.py +206 -0
  202. package/gateway/hooks.py +210 -0
  203. package/gateway/mirror.py +179 -0
  204. package/gateway/pairing.py +322 -0
  205. package/gateway/platform_registry.py +260 -0
  206. package/gateway/platforms/ADDING_A_PLATFORM.md +374 -0
  207. package/gateway/platforms/__init__.py +45 -0
  208. package/gateway/platforms/__pycache__/__init__.cpython-312.pyc +0 -0
  209. package/gateway/platforms/__pycache__/base.cpython-312.pyc +0 -0
  210. package/gateway/platforms/__pycache__/helpers.cpython-312.pyc +0 -0
  211. package/gateway/platforms/__pycache__/telegram.cpython-312.pyc +0 -0
  212. package/gateway/platforms/__pycache__/telegram_network.cpython-312.pyc +0 -0
  213. package/gateway/platforms/__pycache__/yuanbao.cpython-312.pyc +0 -0
  214. package/gateway/platforms/__pycache__/yuanbao_media.cpython-312.pyc +0 -0
  215. package/gateway/platforms/__pycache__/yuanbao_proto.cpython-312.pyc +0 -0
  216. package/gateway/platforms/_http_client_limits.py +84 -0
  217. package/gateway/platforms/api_server.py +3488 -0
  218. package/gateway/platforms/base.py +3747 -0
  219. package/gateway/platforms/bluebubbles.py +937 -0
  220. package/gateway/platforms/dingtalk.py +1473 -0
  221. package/gateway/platforms/discord.py +5584 -0
  222. package/gateway/platforms/email.py +773 -0
  223. package/gateway/platforms/feishu.py +5059 -0
  224. package/gateway/platforms/feishu_comment.py +1382 -0
  225. package/gateway/platforms/feishu_comment_rules.py +430 -0
  226. package/gateway/platforms/helpers.py +279 -0
  227. package/gateway/platforms/homeassistant.py +449 -0
  228. package/gateway/platforms/matrix.py +2777 -0
  229. package/gateway/platforms/mattermost.py +852 -0
  230. package/gateway/platforms/msgraph_webhook.py +397 -0
  231. package/gateway/platforms/qqbot/__init__.py +91 -0
  232. package/gateway/platforms/qqbot/adapter.py +3072 -0
  233. package/gateway/platforms/qqbot/chunked_upload.py +602 -0
  234. package/gateway/platforms/qqbot/constants.py +74 -0
  235. package/gateway/platforms/qqbot/crypto.py +45 -0
  236. package/gateway/platforms/qqbot/keyboards.py +473 -0
  237. package/gateway/platforms/qqbot/onboard.py +220 -0
  238. package/gateway/platforms/qqbot/utils.py +71 -0
  239. package/gateway/platforms/signal.py +1518 -0
  240. package/gateway/platforms/signal_rate_limit.py +369 -0
  241. package/gateway/platforms/slack.py +3028 -0
  242. package/gateway/platforms/sms.py +377 -0
  243. package/gateway/platforms/telegram.py +4836 -0
  244. package/gateway/platforms/telegram_network.py +249 -0
  245. package/gateway/platforms/webhook.py +806 -0
  246. package/gateway/platforms/wecom.py +1610 -0
  247. package/gateway/platforms/wecom_callback.py +403 -0
  248. package/gateway/platforms/wecom_crypto.py +142 -0
  249. package/gateway/platforms/weixin.py +2170 -0
  250. package/gateway/platforms/whatsapp.py +1283 -0
  251. package/gateway/platforms/yuanbao.py +4873 -0
  252. package/gateway/platforms/yuanbao_media.py +645 -0
  253. package/gateway/platforms/yuanbao_proto.py +1209 -0
  254. package/gateway/platforms/yuanbao_sticker.py +558 -0
  255. package/gateway/restart.py +20 -0
  256. package/gateway/run.py +17074 -0
  257. package/gateway/runtime_footer.py +150 -0
  258. package/gateway/session.py +1399 -0
  259. package/gateway/session_context.py +156 -0
  260. package/gateway/shutdown_forensics.py +462 -0
  261. package/gateway/slash_access.py +229 -0
  262. package/gateway/status.py +972 -0
  263. package/gateway/sticker_cache.py +111 -0
  264. package/gateway/stream_consumer.py +1286 -0
  265. package/gateway/whatsapp_identity.py +156 -0
  266. package/hermes_cli/__init__.py +47 -0
  267. package/hermes_cli/__pycache__/__init__.cpython-312.pyc +0 -0
  268. package/hermes_cli/__pycache__/_parser.cpython-312.pyc +0 -0
  269. package/hermes_cli/__pycache__/auth.cpython-312.pyc +0 -0
  270. package/hermes_cli/__pycache__/banner.cpython-312.pyc +0 -0
  271. package/hermes_cli/__pycache__/browser_connect.cpython-312.pyc +0 -0
  272. package/hermes_cli/__pycache__/callbacks.cpython-312.pyc +0 -0
  273. package/hermes_cli/__pycache__/checkpoints.cpython-312.pyc +0 -0
  274. package/hermes_cli/__pycache__/cli_output.cpython-312.pyc +0 -0
  275. package/hermes_cli/__pycache__/codex_models.cpython-312.pyc +0 -0
  276. package/hermes_cli/__pycache__/codex_runtime_switch.cpython-312.pyc +0 -0
  277. package/hermes_cli/__pycache__/colors.cpython-312.pyc +0 -0
  278. package/hermes_cli/__pycache__/commands.cpython-312.pyc +0 -0
  279. package/hermes_cli/__pycache__/config.cpython-312.pyc +0 -0
  280. package/hermes_cli/__pycache__/copilot_auth.cpython-312.pyc +0 -0
  281. package/hermes_cli/__pycache__/curator.cpython-312.pyc +0 -0
  282. package/hermes_cli/__pycache__/curses_ui.cpython-312.pyc +0 -0
  283. package/hermes_cli/__pycache__/debug.cpython-312.pyc +0 -0
  284. package/hermes_cli/__pycache__/default_soul.cpython-312.pyc +0 -0
  285. package/hermes_cli/__pycache__/env_loader.cpython-312.pyc +0 -0
  286. package/hermes_cli/__pycache__/fallback_cmd.cpython-312.pyc +0 -0
  287. package/hermes_cli/__pycache__/gateway.cpython-312.pyc +0 -0
  288. package/hermes_cli/__pycache__/gateway_windows.cpython-312.pyc +0 -0
  289. package/hermes_cli/__pycache__/goals.cpython-312.pyc +0 -0
  290. package/hermes_cli/__pycache__/inventory.cpython-312.pyc +0 -0
  291. package/hermes_cli/__pycache__/kanban.cpython-312.pyc +0 -0
  292. package/hermes_cli/__pycache__/kanban_db.cpython-312.pyc +0 -0
  293. package/hermes_cli/__pycache__/main.cpython-312.pyc +0 -0
  294. package/hermes_cli/__pycache__/model_catalog.cpython-312.pyc +0 -0
  295. package/hermes_cli/__pycache__/model_normalize.cpython-312.pyc +0 -0
  296. package/hermes_cli/__pycache__/model_switch.cpython-312.pyc +0 -0
  297. package/hermes_cli/__pycache__/models.cpython-312.pyc +0 -0
  298. package/hermes_cli/__pycache__/nous_subscription.cpython-312.pyc +0 -0
  299. package/hermes_cli/__pycache__/pairing.cpython-312.pyc +0 -0
  300. package/hermes_cli/__pycache__/platforms.cpython-312.pyc +0 -0
  301. package/hermes_cli/__pycache__/plugins.cpython-312.pyc +0 -0
  302. package/hermes_cli/__pycache__/profiles.cpython-312.pyc +0 -0
  303. package/hermes_cli/__pycache__/providers.cpython-312.pyc +0 -0
  304. package/hermes_cli/__pycache__/pt_input_extras.cpython-312.pyc +0 -0
  305. package/hermes_cli/__pycache__/runtime_provider.cpython-312.pyc +0 -0
  306. package/hermes_cli/__pycache__/security_advisories.cpython-312.pyc +0 -0
  307. package/hermes_cli/__pycache__/setup.cpython-312.pyc +0 -0
  308. package/hermes_cli/__pycache__/skills_hub.cpython-312.pyc +0 -0
  309. package/hermes_cli/__pycache__/skin_engine.cpython-312.pyc +0 -0
  310. package/hermes_cli/__pycache__/stdio.cpython-312.pyc +0 -0
  311. package/hermes_cli/__pycache__/timeouts.cpython-312.pyc +0 -0
  312. package/hermes_cli/__pycache__/tips.cpython-312.pyc +0 -0
  313. package/hermes_cli/__pycache__/tools_config.cpython-312.pyc +0 -0
  314. package/hermes_cli/__pycache__/voice.cpython-312.pyc +0 -0
  315. package/hermes_cli/_parser.py +365 -0
  316. package/hermes_cli/_subprocess_compat.py +175 -0
  317. package/hermes_cli/auth.py +6299 -0
  318. package/hermes_cli/auth_commands.py +749 -0
  319. package/hermes_cli/azure_detect.py +300 -0
  320. package/hermes_cli/backup.py +938 -0
  321. package/hermes_cli/banner.py +703 -0
  322. package/hermes_cli/browser_connect.py +139 -0
  323. package/hermes_cli/callbacks.py +243 -0
  324. package/hermes_cli/checkpoints.py +244 -0
  325. package/hermes_cli/claw.py +810 -0
  326. package/hermes_cli/cli_output.py +78 -0
  327. package/hermes_cli/clipboard.py +495 -0
  328. package/hermes_cli/codex_models.py +198 -0
  329. package/hermes_cli/codex_runtime_plugin_migration.py +757 -0
  330. package/hermes_cli/codex_runtime_switch.py +266 -0
  331. package/hermes_cli/colors.py +38 -0
  332. package/hermes_cli/commands.py +1728 -0
  333. package/hermes_cli/completion.py +315 -0
  334. package/hermes_cli/config.py +5382 -0
  335. package/hermes_cli/copilot_auth.py +392 -0
  336. package/hermes_cli/cron.py +313 -0
  337. package/hermes_cli/curator.py +598 -0
  338. package/hermes_cli/curses_ui.py +472 -0
  339. package/hermes_cli/debug.py +747 -0
  340. package/hermes_cli/default_soul.py +11 -0
  341. package/hermes_cli/dep_ensure.py +107 -0
  342. package/hermes_cli/dingtalk_auth.py +293 -0
  343. package/hermes_cli/doctor.py +1863 -0
  344. package/hermes_cli/dump.py +326 -0
  345. package/hermes_cli/env_loader.py +175 -0
  346. package/hermes_cli/fallback_cmd.py +361 -0
  347. package/hermes_cli/gateway.py +5422 -0
  348. package/hermes_cli/gateway_windows.py +692 -0
  349. package/hermes_cli/goals.py +757 -0
  350. package/hermes_cli/hooks.py +385 -0
  351. package/hermes_cli/inventory.py +240 -0
  352. package/hermes_cli/kanban.py +2252 -0
  353. package/hermes_cli/kanban_db.py +4840 -0
  354. package/hermes_cli/kanban_diagnostics.py +776 -0
  355. package/hermes_cli/kanban_specify.py +266 -0
  356. package/hermes_cli/logs.py +391 -0
  357. package/hermes_cli/main.py +12396 -0
  358. package/hermes_cli/mcp_config.py +781 -0
  359. package/hermes_cli/memory_setup.py +465 -0
  360. package/hermes_cli/model_catalog.py +330 -0
  361. package/hermes_cli/model_normalize.py +473 -0
  362. package/hermes_cli/model_switch.py +1777 -0
  363. package/hermes_cli/models.py +3789 -0
  364. package/hermes_cli/nous_subscription.py +799 -0
  365. package/hermes_cli/oneshot.py +351 -0
  366. package/hermes_cli/pairing.py +115 -0
  367. package/hermes_cli/platforms.py +83 -0
  368. package/hermes_cli/plugins.py +1562 -0
  369. package/hermes_cli/plugins_cmd.py +1587 -0
  370. package/hermes_cli/profile_distribution.py +703 -0
  371. package/hermes_cli/profiles.py +1319 -0
  372. package/hermes_cli/providers.py +720 -0
  373. package/hermes_cli/proxy/__init__.py +20 -0
  374. package/hermes_cli/proxy/adapters/__init__.py +35 -0
  375. package/hermes_cli/proxy/adapters/base.py +94 -0
  376. package/hermes_cli/proxy/adapters/nous_portal.py +137 -0
  377. package/hermes_cli/proxy/cli.py +141 -0
  378. package/hermes_cli/proxy/server.py +265 -0
  379. package/hermes_cli/pt_input_extras.py +83 -0
  380. package/hermes_cli/pty_bridge.py +237 -0
  381. package/hermes_cli/relaunch.py +205 -0
  382. package/hermes_cli/runtime_provider.py +1428 -0
  383. package/hermes_cli/security_advisories.py +452 -0
  384. package/hermes_cli/setup.py +3559 -0
  385. package/hermes_cli/skills_config.py +177 -0
  386. package/hermes_cli/skills_hub.py +1595 -0
  387. package/hermes_cli/skin_engine.py +929 -0
  388. package/hermes_cli/slack_cli.py +160 -0
  389. package/hermes_cli/status.py +550 -0
  390. package/hermes_cli/stdio.py +252 -0
  391. package/hermes_cli/timeouts.py +82 -0
  392. package/hermes_cli/tips.py +487 -0
  393. package/hermes_cli/tools_config.py +3151 -0
  394. package/hermes_cli/uninstall.py +681 -0
  395. package/hermes_cli/vercel_auth.py +70 -0
  396. package/hermes_cli/voice.py +846 -0
  397. package/hermes_cli/web_server.py +4438 -0
  398. package/hermes_cli/webhook.py +275 -0
  399. package/locales/af.yaml +350 -0
  400. package/locales/de.yaml +350 -0
  401. package/locales/en.yaml +365 -0
  402. package/locales/es.yaml +350 -0
  403. package/locales/fr.yaml +350 -0
  404. package/locales/ga.yaml +354 -0
  405. package/locales/hu.yaml +350 -0
  406. package/locales/it.yaml +350 -0
  407. package/locales/ja.yaml +350 -0
  408. package/locales/ko.yaml +350 -0
  409. package/locales/pt.yaml +350 -0
  410. package/locales/ru.yaml +350 -0
  411. package/locales/tr.yaml +350 -0
  412. package/locales/uk.yaml +350 -0
  413. package/locales/zh-hant.yaml +350 -0
  414. package/locales/zh.yaml +350 -0
  415. package/mcp_serve.py +898 -0
  416. package/model_tools.py +899 -0
  417. package/optional-skills/DESCRIPTION.md +24 -0
  418. package/optional-skills/autonomous-ai-agents/DESCRIPTION.md +2 -0
  419. package/optional-skills/autonomous-ai-agents/blackbox/SKILL.md +144 -0
  420. package/optional-skills/autonomous-ai-agents/honcho/SKILL.md +431 -0
  421. package/optional-skills/blockchain/evm/SKILL.md +211 -0
  422. package/optional-skills/blockchain/evm/scripts/evm_client.py +1508 -0
  423. package/optional-skills/blockchain/hyperliquid/SKILL.md +211 -0
  424. package/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1660 -0
  425. package/optional-skills/blockchain/solana/SKILL.md +208 -0
  426. package/optional-skills/blockchain/solana/scripts/solana_client.py +698 -0
  427. package/optional-skills/communication/DESCRIPTION.md +1 -0
  428. package/optional-skills/communication/one-three-one-rule/SKILL.md +104 -0
  429. package/optional-skills/creative/blender-mcp/SKILL.md +117 -0
  430. package/optional-skills/creative/concept-diagrams/SKILL.md +362 -0
  431. package/optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
  432. package/optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
  433. package/optional-skills/creative/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
  434. package/optional-skills/creative/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
  435. package/optional-skills/creative/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
  436. package/optional-skills/creative/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
  437. package/optional-skills/creative/concept-diagrams/examples/electricity-grid-flow.md +182 -0
  438. package/optional-skills/creative/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
  439. package/optional-skills/creative/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
  440. package/optional-skills/creative/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
  441. package/optional-skills/creative/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
  442. package/optional-skills/creative/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
  443. package/optional-skills/creative/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
  444. package/optional-skills/creative/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
  445. package/optional-skills/creative/concept-diagrams/examples/wind-turbine-structure.md +338 -0
  446. package/optional-skills/creative/concept-diagrams/references/dashboard-patterns.md +43 -0
  447. package/optional-skills/creative/concept-diagrams/references/infrastructure-patterns.md +144 -0
  448. package/optional-skills/creative/concept-diagrams/references/physical-shape-cookbook.md +42 -0
  449. package/optional-skills/creative/concept-diagrams/templates/template.html +174 -0
  450. package/optional-skills/creative/hyperframes/SKILL.md +191 -0
  451. package/optional-skills/creative/hyperframes/references/cli.md +185 -0
  452. package/optional-skills/creative/hyperframes/references/composition.md +129 -0
  453. package/optional-skills/creative/hyperframes/references/features.md +289 -0
  454. package/optional-skills/creative/hyperframes/references/gsap.md +136 -0
  455. package/optional-skills/creative/hyperframes/references/troubleshooting.md +137 -0
  456. package/optional-skills/creative/hyperframes/references/website-to-video.md +145 -0
  457. package/optional-skills/creative/hyperframes/scripts/setup.sh +135 -0
  458. package/optional-skills/creative/kanban-video-orchestrator/SKILL.md +207 -0
  459. package/optional-skills/creative/kanban-video-orchestrator/assets/brief.md.tmpl +79 -0
  460. package/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +185 -0
  461. package/optional-skills/creative/kanban-video-orchestrator/assets/soul.md.tmpl +38 -0
  462. package/optional-skills/creative/kanban-video-orchestrator/references/examples.md +227 -0
  463. package/optional-skills/creative/kanban-video-orchestrator/references/intake.md +166 -0
  464. package/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +276 -0
  465. package/optional-skills/creative/kanban-video-orchestrator/references/monitoring.md +180 -0
  466. package/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md +298 -0
  467. package/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +317 -0
  468. package/optional-skills/creative/kanban-video-orchestrator/scripts/bootstrap_pipeline.py +501 -0
  469. package/optional-skills/creative/kanban-video-orchestrator/scripts/monitor.py +195 -0
  470. package/optional-skills/creative/meme-generation/EXAMPLES.md +46 -0
  471. package/optional-skills/creative/meme-generation/SKILL.md +130 -0
  472. package/optional-skills/creative/meme-generation/scripts/generate_meme.py +471 -0
  473. package/optional-skills/creative/meme-generation/scripts/templates.json +97 -0
  474. package/optional-skills/devops/cli/SKILL.md +156 -0
  475. package/optional-skills/devops/cli/references/app-discovery.md +112 -0
  476. package/optional-skills/devops/cli/references/authentication.md +59 -0
  477. package/optional-skills/devops/cli/references/cli-reference.md +104 -0
  478. package/optional-skills/devops/cli/references/running-apps.md +171 -0
  479. package/optional-skills/devops/docker-management/SKILL.md +281 -0
  480. package/optional-skills/devops/pinggy-tunnel/SKILL.md +309 -0
  481. package/optional-skills/devops/watchers/SKILL.md +112 -0
  482. package/optional-skills/devops/watchers/scripts/_watermark.py +148 -0
  483. package/optional-skills/devops/watchers/scripts/watch_github.py +168 -0
  484. package/optional-skills/devops/watchers/scripts/watch_http_json.py +131 -0
  485. package/optional-skills/devops/watchers/scripts/watch_rss.py +121 -0
  486. package/optional-skills/dogfood/DESCRIPTION.md +3 -0
  487. package/optional-skills/dogfood/adversarial-ux-test/SKILL.md +191 -0
  488. package/optional-skills/email/agentmail/SKILL.md +126 -0
  489. package/optional-skills/finance/3-statement-model/SKILL.md +433 -0
  490. package/optional-skills/finance/3-statement-model/references/formatting.md +118 -0
  491. package/optional-skills/finance/3-statement-model/references/formulas.md +292 -0
  492. package/optional-skills/finance/3-statement-model/references/sec-filings.md +125 -0
  493. package/optional-skills/finance/comps-analysis/SKILL.md +662 -0
  494. package/optional-skills/finance/dcf-model/SKILL.md +1270 -0
  495. package/optional-skills/finance/dcf-model/TROUBLESHOOTING.md +40 -0
  496. package/optional-skills/finance/dcf-model/requirements.txt +7 -0
  497. package/optional-skills/finance/dcf-model/scripts/validate_dcf.py +292 -0
  498. package/optional-skills/finance/excel-author/SKILL.md +244 -0
  499. package/optional-skills/finance/excel-author/scripts/recalc.py +88 -0
  500. package/optional-skills/finance/lbo-model/SKILL.md +291 -0
  501. package/optional-skills/finance/merger-model/SKILL.md +144 -0
  502. package/optional-skills/finance/pptx-author/SKILL.md +173 -0
  503. package/optional-skills/finance/stocks/SKILL.md +95 -0
  504. package/optional-skills/finance/stocks/scripts/stocks_client.py +755 -0
  505. package/optional-skills/health/DESCRIPTION.md +1 -0
  506. package/optional-skills/health/fitness-nutrition/SKILL.md +256 -0
  507. package/optional-skills/health/fitness-nutrition/references/FORMULAS.md +100 -0
  508. package/optional-skills/health/fitness-nutrition/scripts/body_calc.py +210 -0
  509. package/optional-skills/health/fitness-nutrition/scripts/nutrition_search.py +86 -0
  510. package/optional-skills/health/neuroskill-bci/SKILL.md +459 -0
  511. package/optional-skills/health/neuroskill-bci/references/api.md +286 -0
  512. package/optional-skills/health/neuroskill-bci/references/metrics.md +220 -0
  513. package/optional-skills/health/neuroskill-bci/references/protocols.md +452 -0
  514. package/optional-skills/mcp/DESCRIPTION.md +3 -0
  515. package/optional-skills/mcp/fastmcp/SKILL.md +300 -0
  516. package/optional-skills/mcp/fastmcp/references/fastmcp-cli.md +110 -0
  517. package/optional-skills/mcp/fastmcp/scripts/scaffold_fastmcp.py +56 -0
  518. package/optional-skills/mcp/fastmcp/templates/api_wrapper.py +54 -0
  519. package/optional-skills/mcp/fastmcp/templates/database_server.py +77 -0
  520. package/optional-skills/mcp/fastmcp/templates/file_processor.py +55 -0
  521. package/optional-skills/mcp/mcporter/SKILL.md +123 -0
  522. package/optional-skills/migration/DESCRIPTION.md +2 -0
  523. package/optional-skills/migration/openclaw-migration/SKILL.md +298 -0
  524. package/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py +3136 -0
  525. package/optional-skills/mlops/accelerate/SKILL.md +336 -0
  526. package/optional-skills/mlops/accelerate/references/custom-plugins.md +453 -0
  527. package/optional-skills/mlops/accelerate/references/megatron-integration.md +489 -0
  528. package/optional-skills/mlops/accelerate/references/performance.md +525 -0
  529. package/optional-skills/mlops/chroma/SKILL.md +410 -0
  530. package/optional-skills/mlops/chroma/references/integration.md +38 -0
  531. package/optional-skills/mlops/clip/SKILL.md +257 -0
  532. package/optional-skills/mlops/clip/references/applications.md +207 -0
  533. package/optional-skills/mlops/faiss/SKILL.md +225 -0
  534. package/optional-skills/mlops/faiss/references/index_types.md +280 -0
  535. package/optional-skills/mlops/flash-attention/SKILL.md +367 -0
  536. package/optional-skills/mlops/flash-attention/references/benchmarks.md +215 -0
  537. package/optional-skills/mlops/flash-attention/references/transformers-integration.md +293 -0
  538. package/optional-skills/mlops/guidance/SKILL.md +576 -0
  539. package/optional-skills/mlops/guidance/references/backends.md +554 -0
  540. package/optional-skills/mlops/guidance/references/constraints.md +674 -0
  541. package/optional-skills/mlops/guidance/references/examples.md +767 -0
  542. package/optional-skills/mlops/huggingface-tokenizers/SKILL.md +520 -0
  543. package/optional-skills/mlops/huggingface-tokenizers/references/algorithms.md +653 -0
  544. package/optional-skills/mlops/huggingface-tokenizers/references/integration.md +637 -0
  545. package/optional-skills/mlops/huggingface-tokenizers/references/pipeline.md +723 -0
  546. package/optional-skills/mlops/huggingface-tokenizers/references/training.md +565 -0
  547. package/optional-skills/mlops/inference/outlines/SKILL.md +656 -0
  548. package/optional-skills/mlops/inference/outlines/references/backends.md +615 -0
  549. package/optional-skills/mlops/inference/outlines/references/examples.md +773 -0
  550. package/optional-skills/mlops/inference/outlines/references/json_generation.md +652 -0
  551. package/optional-skills/mlops/instructor/SKILL.md +744 -0
  552. package/optional-skills/mlops/instructor/references/examples.md +107 -0
  553. package/optional-skills/mlops/instructor/references/providers.md +70 -0
  554. package/optional-skills/mlops/instructor/references/validation.md +606 -0
  555. package/optional-skills/mlops/lambda-labs/SKILL.md +549 -0
  556. package/optional-skills/mlops/lambda-labs/references/advanced-usage.md +611 -0
  557. package/optional-skills/mlops/lambda-labs/references/troubleshooting.md +530 -0
  558. package/optional-skills/mlops/llava/SKILL.md +308 -0
  559. package/optional-skills/mlops/llava/references/training.md +197 -0
  560. package/optional-skills/mlops/modal/SKILL.md +345 -0
  561. package/optional-skills/mlops/modal/references/advanced-usage.md +503 -0
  562. package/optional-skills/mlops/modal/references/troubleshooting.md +494 -0
  563. package/optional-skills/mlops/nemo-curator/SKILL.md +387 -0
  564. package/optional-skills/mlops/nemo-curator/references/deduplication.md +87 -0
  565. package/optional-skills/mlops/nemo-curator/references/filtering.md +102 -0
  566. package/optional-skills/mlops/peft/SKILL.md +435 -0
  567. package/optional-skills/mlops/peft/references/advanced-usage.md +514 -0
  568. package/optional-skills/mlops/peft/references/troubleshooting.md +480 -0
  569. package/optional-skills/mlops/pinecone/SKILL.md +362 -0
  570. package/optional-skills/mlops/pinecone/references/deployment.md +181 -0
  571. package/optional-skills/mlops/pytorch-fsdp/SKILL.md +130 -0
  572. package/optional-skills/mlops/pytorch-fsdp/references/index.md +7 -0
  573. package/optional-skills/mlops/pytorch-fsdp/references/other.md +4261 -0
  574. package/optional-skills/mlops/pytorch-lightning/SKILL.md +350 -0
  575. package/optional-skills/mlops/pytorch-lightning/references/callbacks.md +436 -0
  576. package/optional-skills/mlops/pytorch-lightning/references/distributed.md +490 -0
  577. package/optional-skills/mlops/pytorch-lightning/references/hyperparameter-tuning.md +556 -0
  578. package/optional-skills/mlops/qdrant/SKILL.md +497 -0
  579. package/optional-skills/mlops/qdrant/references/advanced-usage.md +648 -0
  580. package/optional-skills/mlops/qdrant/references/troubleshooting.md +631 -0
  581. package/optional-skills/mlops/saelens/SKILL.md +390 -0
  582. package/optional-skills/mlops/saelens/references/README.md +69 -0
  583. package/optional-skills/mlops/saelens/references/api.md +333 -0
  584. package/optional-skills/mlops/saelens/references/tutorials.md +318 -0
  585. package/optional-skills/mlops/simpo/SKILL.md +223 -0
  586. package/optional-skills/mlops/simpo/references/datasets.md +478 -0
  587. package/optional-skills/mlops/simpo/references/hyperparameters.md +452 -0
  588. package/optional-skills/mlops/simpo/references/loss-functions.md +350 -0
  589. package/optional-skills/mlops/slime/SKILL.md +468 -0
  590. package/optional-skills/mlops/slime/references/api-reference.md +392 -0
  591. package/optional-skills/mlops/slime/references/troubleshooting.md +386 -0
  592. package/optional-skills/mlops/stable-diffusion/SKILL.md +523 -0
  593. package/optional-skills/mlops/stable-diffusion/references/advanced-usage.md +716 -0
  594. package/optional-skills/mlops/stable-diffusion/references/troubleshooting.md +555 -0
  595. package/optional-skills/mlops/tensorrt-llm/SKILL.md +191 -0
  596. package/optional-skills/mlops/tensorrt-llm/references/multi-gpu.md +298 -0
  597. package/optional-skills/mlops/tensorrt-llm/references/optimization.md +242 -0
  598. package/optional-skills/mlops/tensorrt-llm/references/serving.md +470 -0
  599. package/optional-skills/mlops/torchtitan/SKILL.md +362 -0
  600. package/optional-skills/mlops/torchtitan/references/checkpoint.md +181 -0
  601. package/optional-skills/mlops/torchtitan/references/custom-models.md +258 -0
  602. package/optional-skills/mlops/torchtitan/references/float8.md +133 -0
  603. package/optional-skills/mlops/torchtitan/references/fsdp.md +126 -0
  604. package/optional-skills/mlops/training/axolotl/SKILL.md +166 -0
  605. package/optional-skills/mlops/training/axolotl/references/api.md +5548 -0
  606. package/optional-skills/mlops/training/axolotl/references/dataset-formats.md +1029 -0
  607. package/optional-skills/mlops/training/axolotl/references/index.md +15 -0
  608. package/optional-skills/mlops/training/axolotl/references/other.md +3563 -0
  609. package/optional-skills/mlops/training/trl-fine-tuning/SKILL.md +463 -0
  610. package/optional-skills/mlops/training/trl-fine-tuning/references/dpo-variants.md +227 -0
  611. package/optional-skills/mlops/training/trl-fine-tuning/references/grpo-training.md +504 -0
  612. package/optional-skills/mlops/training/trl-fine-tuning/references/online-rl.md +82 -0
  613. package/optional-skills/mlops/training/trl-fine-tuning/references/reward-modeling.md +122 -0
  614. package/optional-skills/mlops/training/trl-fine-tuning/references/sft-training.md +168 -0
  615. package/optional-skills/mlops/training/trl-fine-tuning/templates/basic_grpo_training.py +228 -0
  616. package/optional-skills/mlops/training/unsloth/SKILL.md +84 -0
  617. package/optional-skills/mlops/training/unsloth/references/index.md +7 -0
  618. package/optional-skills/mlops/training/unsloth/references/llms-full.md +16799 -0
  619. package/optional-skills/mlops/training/unsloth/references/llms-txt.md +12044 -0
  620. package/optional-skills/mlops/training/unsloth/references/llms.md +82 -0
  621. package/optional-skills/mlops/whisper/SKILL.md +321 -0
  622. package/optional-skills/mlops/whisper/references/languages.md +189 -0
  623. package/optional-skills/productivity/canvas/SKILL.md +98 -0
  624. package/optional-skills/productivity/canvas/scripts/canvas_api.py +157 -0
  625. package/optional-skills/productivity/here-now/SKILL.md +217 -0
  626. package/optional-skills/productivity/here-now/scripts/drive.sh +406 -0
  627. package/optional-skills/productivity/here-now/scripts/publish.sh +445 -0
  628. package/optional-skills/productivity/memento-flashcards/SKILL.md +324 -0
  629. package/optional-skills/productivity/memento-flashcards/scripts/memento_cards.py +353 -0
  630. package/optional-skills/productivity/memento-flashcards/scripts/youtube_quiz.py +88 -0
  631. package/optional-skills/productivity/shop-app/SKILL.md +340 -0
  632. package/optional-skills/productivity/shopify/SKILL.md +373 -0
  633. package/optional-skills/productivity/siyuan/SKILL.md +298 -0
  634. package/optional-skills/productivity/telephony/SKILL.md +418 -0
  635. package/optional-skills/productivity/telephony/scripts/telephony.py +1343 -0
  636. package/optional-skills/research/bioinformatics/SKILL.md +235 -0
  637. package/optional-skills/research/darwinian-evolver/SKILL.md +199 -0
  638. package/optional-skills/research/darwinian-evolver/scripts/parrot_openrouter.py +218 -0
  639. package/optional-skills/research/darwinian-evolver/scripts/show_snapshot.py +69 -0
  640. package/optional-skills/research/darwinian-evolver/templates/custom_problem_template.py +240 -0
  641. package/optional-skills/research/domain-intel/SKILL.md +97 -0
  642. package/optional-skills/research/domain-intel/scripts/domain_intel.py +397 -0
  643. package/optional-skills/research/drug-discovery/SKILL.md +227 -0
  644. package/optional-skills/research/drug-discovery/references/ADMET_REFERENCE.md +66 -0
  645. package/optional-skills/research/drug-discovery/scripts/chembl_target.py +53 -0
  646. package/optional-skills/research/drug-discovery/scripts/ro5_screen.py +44 -0
  647. package/optional-skills/research/duckduckgo-search/SKILL.md +238 -0
  648. package/optional-skills/research/duckduckgo-search/scripts/duckduckgo.sh +28 -0
  649. package/optional-skills/research/gitnexus-explorer/SKILL.md +214 -0
  650. package/optional-skills/research/gitnexus-explorer/scripts/proxy.mjs +92 -0
  651. package/optional-skills/research/osint-investigation/SKILL.md +277 -0
  652. package/optional-skills/research/osint-investigation/references/sources/courtlistener.md +98 -0
  653. package/optional-skills/research/osint-investigation/references/sources/gdelt.md +104 -0
  654. package/optional-skills/research/osint-investigation/references/sources/icij-offshore.md +104 -0
  655. package/optional-skills/research/osint-investigation/references/sources/nyc-acris.md +90 -0
  656. package/optional-skills/research/osint-investigation/references/sources/ofac-sdn.md +92 -0
  657. package/optional-skills/research/osint-investigation/references/sources/opencorporates.md +103 -0
  658. package/optional-skills/research/osint-investigation/references/sources/sec-edgar.md +83 -0
  659. package/optional-skills/research/osint-investigation/references/sources/senate-ld.md +89 -0
  660. package/optional-skills/research/osint-investigation/references/sources/usaspending.md +97 -0
  661. package/optional-skills/research/osint-investigation/references/sources/wayback.md +93 -0
  662. package/optional-skills/research/osint-investigation/references/sources/wikipedia.md +107 -0
  663. package/optional-skills/research/osint-investigation/scripts/_http.py +82 -0
  664. package/optional-skills/research/osint-investigation/scripts/_normalize.py +67 -0
  665. package/optional-skills/research/osint-investigation/scripts/build_findings.py +221 -0
  666. package/optional-skills/research/osint-investigation/scripts/entity_resolution.py +228 -0
  667. package/optional-skills/research/osint-investigation/scripts/fetch_courtlistener.py +149 -0
  668. package/optional-skills/research/osint-investigation/scripts/fetch_gdelt.py +162 -0
  669. package/optional-skills/research/osint-investigation/scripts/fetch_icij_offshore.py +234 -0
  670. package/optional-skills/research/osint-investigation/scripts/fetch_nyc_acris.py +203 -0
  671. package/optional-skills/research/osint-investigation/scripts/fetch_ofac_sdn.py +175 -0
  672. package/optional-skills/research/osint-investigation/scripts/fetch_opencorporates.py +192 -0
  673. package/optional-skills/research/osint-investigation/scripts/fetch_sec_edgar.py +184 -0
  674. package/optional-skills/research/osint-investigation/scripts/fetch_senate_ld.py +146 -0
  675. package/optional-skills/research/osint-investigation/scripts/fetch_usaspending.py +170 -0
  676. package/optional-skills/research/osint-investigation/scripts/fetch_wayback.py +142 -0
  677. package/optional-skills/research/osint-investigation/scripts/fetch_wikipedia.py +267 -0
  678. package/optional-skills/research/osint-investigation/scripts/timing_analysis.py +253 -0
  679. package/optional-skills/research/osint-investigation/templates/source-template.md +59 -0
  680. package/optional-skills/research/parallel-cli/SKILL.md +391 -0
  681. package/optional-skills/research/qmd/SKILL.md +441 -0
  682. package/optional-skills/research/scrapling/SKILL.md +336 -0
  683. package/optional-skills/research/searxng-search/SKILL.md +212 -0
  684. package/optional-skills/research/searxng-search/scripts/searxng.sh +22 -0
  685. package/optional-skills/security/1password/SKILL.md +163 -0
  686. package/optional-skills/security/1password/references/cli-examples.md +31 -0
  687. package/optional-skills/security/1password/references/get-started.md +21 -0
  688. package/optional-skills/security/DESCRIPTION.md +3 -0
  689. package/optional-skills/security/oss-forensics/SKILL.md +423 -0
  690. package/optional-skills/security/oss-forensics/references/evidence-types.md +89 -0
  691. package/optional-skills/security/oss-forensics/references/github-archive-guide.md +184 -0
  692. package/optional-skills/security/oss-forensics/references/investigation-templates.md +131 -0
  693. package/optional-skills/security/oss-forensics/references/recovery-techniques.md +164 -0
  694. package/optional-skills/security/oss-forensics/scripts/evidence-store.py +313 -0
  695. package/optional-skills/security/oss-forensics/templates/forensic-report.md +151 -0
  696. package/optional-skills/security/oss-forensics/templates/malicious-package-report.md +43 -0
  697. package/optional-skills/security/sherlock/SKILL.md +193 -0
  698. package/optional-skills/software-development/rest-graphql-debug/SKILL.md +514 -0
  699. package/optional-skills/web-development/DESCRIPTION.md +5 -0
  700. package/optional-skills/web-development/page-agent/SKILL.md +190 -0
  701. package/package.json +78 -0
  702. package/plugins/__init__.py +1 -0
  703. package/plugins/__pycache__/__init__.cpython-312.pyc +0 -0
  704. package/plugins/context_engine/__init__.py +219 -0
  705. package/plugins/disk-cleanup/README.md +51 -0
  706. package/plugins/disk-cleanup/__init__.py +316 -0
  707. package/plugins/disk-cleanup/disk_cleanup.py +497 -0
  708. package/plugins/disk-cleanup/plugin.yaml +7 -0
  709. package/plugins/example-dashboard/dashboard/manifest.json +14 -0
  710. package/plugins/example-dashboard/dashboard/plugin_api.py +17 -0
  711. package/plugins/google_meet/README.md +131 -0
  712. package/plugins/google_meet/SKILL.md +148 -0
  713. package/plugins/google_meet/__init__.py +103 -0
  714. package/plugins/google_meet/audio_bridge.py +244 -0
  715. package/plugins/google_meet/cli.py +479 -0
  716. package/plugins/google_meet/meet_bot.py +852 -0
  717. package/plugins/google_meet/node/__init__.py +54 -0
  718. package/plugins/google_meet/node/cli.py +125 -0
  719. package/plugins/google_meet/node/client.py +107 -0
  720. package/plugins/google_meet/node/protocol.py +124 -0
  721. package/plugins/google_meet/node/registry.py +113 -0
  722. package/plugins/google_meet/node/server.py +201 -0
  723. package/plugins/google_meet/plugin.yaml +16 -0
  724. package/plugins/google_meet/process_manager.py +324 -0
  725. package/plugins/google_meet/realtime/__init__.py +10 -0
  726. package/plugins/google_meet/realtime/openai_client.py +332 -0
  727. package/plugins/google_meet/tools.py +348 -0
  728. package/plugins/hermes-achievements/LICENSE +21 -0
  729. package/plugins/hermes-achievements/README.md +150 -0
  730. package/plugins/hermes-achievements/dashboard/dist/index.js +732 -0
  731. package/plugins/hermes-achievements/dashboard/dist/style.css +146 -0
  732. package/plugins/hermes-achievements/dashboard/manifest.json +11 -0
  733. package/plugins/hermes-achievements/dashboard/plugin_api.py +1062 -0
  734. package/plugins/hermes-achievements/docs/achievements-performance-implementation-plan.md +157 -0
  735. package/plugins/hermes-achievements/docs/achievements-performance-implementation-spec.md +219 -0
  736. package/plugins/hermes-achievements/docs/achievements-performance-spec.md +174 -0
  737. package/plugins/hermes-achievements/docs/assets/achievements-dashboard-hd.png +0 -0
  738. package/plugins/hermes-achievements/docs/assets/achievements-tier-showcase-hd.png +0 -0
  739. package/plugins/hermes-achievements/tests/test_achievement_engine.py +156 -0
  740. package/plugins/image_gen/openai/__init__.py +303 -0
  741. package/plugins/image_gen/openai/__pycache__/__init__.cpython-312.pyc +0 -0
  742. package/plugins/image_gen/openai/plugin.yaml +7 -0
  743. package/plugins/image_gen/openai-codex/__init__.py +378 -0
  744. package/plugins/image_gen/openai-codex/__pycache__/__init__.cpython-312.pyc +0 -0
  745. package/plugins/image_gen/openai-codex/plugin.yaml +5 -0
  746. package/plugins/image_gen/xai/__init__.py +316 -0
  747. package/plugins/image_gen/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  748. package/plugins/image_gen/xai/plugin.yaml +7 -0
  749. package/plugins/kanban/dashboard/dist/index.js +3143 -0
  750. package/plugins/kanban/dashboard/dist/style.css +1500 -0
  751. package/plugins/kanban/dashboard/manifest.json +14 -0
  752. package/plugins/kanban/dashboard/plugin_api.py +1612 -0
  753. package/plugins/kanban/systemd/hermes-kanban-dispatcher.service +32 -0
  754. package/plugins/memory/__init__.py +408 -0
  755. package/plugins/memory/byterover/README.md +41 -0
  756. package/plugins/memory/byterover/__init__.py +384 -0
  757. package/plugins/memory/byterover/plugin.yaml +9 -0
  758. package/plugins/memory/hindsight/README.md +138 -0
  759. package/plugins/memory/hindsight/__init__.py +1758 -0
  760. package/plugins/memory/hindsight/plugin.yaml +8 -0
  761. package/plugins/memory/holographic/README.md +36 -0
  762. package/plugins/memory/holographic/__init__.py +409 -0
  763. package/plugins/memory/holographic/holographic.py +203 -0
  764. package/plugins/memory/holographic/plugin.yaml +5 -0
  765. package/plugins/memory/holographic/retrieval.py +593 -0
  766. package/plugins/memory/holographic/store.py +579 -0
  767. package/plugins/memory/honcho/README.md +328 -0
  768. package/plugins/memory/honcho/__init__.py +1329 -0
  769. package/plugins/memory/honcho/cli.py +1452 -0
  770. package/plugins/memory/honcho/client.py +784 -0
  771. package/plugins/memory/honcho/plugin.yaml +7 -0
  772. package/plugins/memory/honcho/session.py +1255 -0
  773. package/plugins/memory/mem0/README.md +38 -0
  774. package/plugins/memory/mem0/__init__.py +374 -0
  775. package/plugins/memory/mem0/plugin.yaml +5 -0
  776. package/plugins/memory/openviking/README.md +40 -0
  777. package/plugins/memory/openviking/__init__.py +945 -0
  778. package/plugins/memory/openviking/plugin.yaml +9 -0
  779. package/plugins/memory/retaindb/README.md +40 -0
  780. package/plugins/memory/retaindb/__init__.py +767 -0
  781. package/plugins/memory/retaindb/plugin.yaml +7 -0
  782. package/plugins/memory/supermemory/README.md +99 -0
  783. package/plugins/memory/supermemory/__init__.py +792 -0
  784. package/plugins/memory/supermemory/plugin.yaml +5 -0
  785. package/plugins/model-providers/README.md +70 -0
  786. package/plugins/model-providers/ai-gateway/__init__.py +43 -0
  787. package/plugins/model-providers/ai-gateway/__pycache__/__init__.cpython-312.pyc +0 -0
  788. package/plugins/model-providers/ai-gateway/plugin.yaml +5 -0
  789. package/plugins/model-providers/alibaba/__init__.py +13 -0
  790. package/plugins/model-providers/alibaba/__pycache__/__init__.cpython-312.pyc +0 -0
  791. package/plugins/model-providers/alibaba/plugin.yaml +5 -0
  792. package/plugins/model-providers/alibaba-coding-plan/__init__.py +21 -0
  793. package/plugins/model-providers/alibaba-coding-plan/__pycache__/__init__.cpython-312.pyc +0 -0
  794. package/plugins/model-providers/alibaba-coding-plan/plugin.yaml +5 -0
  795. package/plugins/model-providers/anthropic/__init__.py +52 -0
  796. package/plugins/model-providers/anthropic/__pycache__/__init__.cpython-312.pyc +0 -0
  797. package/plugins/model-providers/anthropic/plugin.yaml +5 -0
  798. package/plugins/model-providers/arcee/__init__.py +13 -0
  799. package/plugins/model-providers/arcee/__pycache__/__init__.cpython-312.pyc +0 -0
  800. package/plugins/model-providers/arcee/plugin.yaml +5 -0
  801. package/plugins/model-providers/azure-foundry/__init__.py +21 -0
  802. package/plugins/model-providers/azure-foundry/__pycache__/__init__.cpython-312.pyc +0 -0
  803. package/plugins/model-providers/azure-foundry/plugin.yaml +5 -0
  804. package/plugins/model-providers/bedrock/__init__.py +29 -0
  805. package/plugins/model-providers/bedrock/__pycache__/__init__.cpython-312.pyc +0 -0
  806. package/plugins/model-providers/bedrock/plugin.yaml +5 -0
  807. package/plugins/model-providers/copilot/__init__.py +58 -0
  808. package/plugins/model-providers/copilot/__pycache__/__init__.cpython-312.pyc +0 -0
  809. package/plugins/model-providers/copilot/plugin.yaml +5 -0
  810. package/plugins/model-providers/copilot-acp/__init__.py +34 -0
  811. package/plugins/model-providers/copilot-acp/__pycache__/__init__.cpython-312.pyc +0 -0
  812. package/plugins/model-providers/copilot-acp/plugin.yaml +5 -0
  813. package/plugins/model-providers/custom/__init__.py +68 -0
  814. package/plugins/model-providers/custom/__pycache__/__init__.cpython-312.pyc +0 -0
  815. package/plugins/model-providers/custom/plugin.yaml +5 -0
  816. package/plugins/model-providers/deepseek/__init__.py +99 -0
  817. package/plugins/model-providers/deepseek/__pycache__/__init__.cpython-312.pyc +0 -0
  818. package/plugins/model-providers/deepseek/plugin.yaml +5 -0
  819. package/plugins/model-providers/gemini/__init__.py +72 -0
  820. package/plugins/model-providers/gemini/__pycache__/__init__.cpython-312.pyc +0 -0
  821. package/plugins/model-providers/gemini/plugin.yaml +5 -0
  822. package/plugins/model-providers/gmi/__init__.py +31 -0
  823. package/plugins/model-providers/gmi/__pycache__/__init__.cpython-312.pyc +0 -0
  824. package/plugins/model-providers/gmi/plugin.yaml +5 -0
  825. package/plugins/model-providers/huggingface/__init__.py +20 -0
  826. package/plugins/model-providers/huggingface/__pycache__/__init__.cpython-312.pyc +0 -0
  827. package/plugins/model-providers/huggingface/plugin.yaml +5 -0
  828. package/plugins/model-providers/kilocode/__init__.py +14 -0
  829. package/plugins/model-providers/kilocode/__pycache__/__init__.cpython-312.pyc +0 -0
  830. package/plugins/model-providers/kilocode/plugin.yaml +5 -0
  831. package/plugins/model-providers/kimi-coding/__init__.py +71 -0
  832. package/plugins/model-providers/kimi-coding/__pycache__/__init__.cpython-312.pyc +0 -0
  833. package/plugins/model-providers/kimi-coding/plugin.yaml +5 -0
  834. package/plugins/model-providers/minimax/__init__.py +45 -0
  835. package/plugins/model-providers/minimax/__pycache__/__init__.cpython-312.pyc +0 -0
  836. package/plugins/model-providers/minimax/plugin.yaml +5 -0
  837. package/plugins/model-providers/nous/__init__.py +54 -0
  838. package/plugins/model-providers/nous/__pycache__/__init__.cpython-312.pyc +0 -0
  839. package/plugins/model-providers/nous/plugin.yaml +5 -0
  840. package/plugins/model-providers/novita/__init__.py +27 -0
  841. package/plugins/model-providers/novita/__pycache__/__init__.cpython-312.pyc +0 -0
  842. package/plugins/model-providers/novita/plugin.yaml +5 -0
  843. package/plugins/model-providers/nvidia/__init__.py +21 -0
  844. package/plugins/model-providers/nvidia/__pycache__/__init__.cpython-312.pyc +0 -0
  845. package/plugins/model-providers/nvidia/plugin.yaml +5 -0
  846. package/plugins/model-providers/ollama-cloud/__init__.py +14 -0
  847. package/plugins/model-providers/ollama-cloud/__pycache__/__init__.cpython-312.pyc +0 -0
  848. package/plugins/model-providers/ollama-cloud/plugin.yaml +5 -0
  849. package/plugins/model-providers/openai-codex/__init__.py +15 -0
  850. package/plugins/model-providers/openai-codex/__pycache__/__init__.cpython-312.pyc +0 -0
  851. package/plugins/model-providers/openai-codex/plugin.yaml +5 -0
  852. package/plugins/model-providers/opencode-zen/__init__.py +30 -0
  853. package/plugins/model-providers/opencode-zen/__pycache__/__init__.cpython-312.pyc +0 -0
  854. package/plugins/model-providers/opencode-zen/plugin.yaml +5 -0
  855. package/plugins/model-providers/openrouter/__init__.py +115 -0
  856. package/plugins/model-providers/openrouter/__pycache__/__init__.cpython-312.pyc +0 -0
  857. package/plugins/model-providers/openrouter/plugin.yaml +5 -0
  858. package/plugins/model-providers/qwen-oauth/__init__.py +82 -0
  859. package/plugins/model-providers/qwen-oauth/__pycache__/__init__.cpython-312.pyc +0 -0
  860. package/plugins/model-providers/qwen-oauth/plugin.yaml +5 -0
  861. package/plugins/model-providers/stepfun/__init__.py +14 -0
  862. package/plugins/model-providers/stepfun/__pycache__/__init__.cpython-312.pyc +0 -0
  863. package/plugins/model-providers/stepfun/plugin.yaml +5 -0
  864. package/plugins/model-providers/xai/__init__.py +15 -0
  865. package/plugins/model-providers/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  866. package/plugins/model-providers/xai/plugin.yaml +5 -0
  867. package/plugins/model-providers/xiaomi/__init__.py +14 -0
  868. package/plugins/model-providers/xiaomi/__pycache__/__init__.cpython-312.pyc +0 -0
  869. package/plugins/model-providers/xiaomi/plugin.yaml +5 -0
  870. package/plugins/model-providers/zai/__init__.py +21 -0
  871. package/plugins/model-providers/zai/__pycache__/__init__.cpython-312.pyc +0 -0
  872. package/plugins/model-providers/zai/plugin.yaml +5 -0
  873. package/plugins/observability/langfuse/README.md +53 -0
  874. package/plugins/observability/langfuse/__init__.py +1004 -0
  875. package/plugins/observability/langfuse/plugin.yaml +14 -0
  876. package/plugins/platforms/google_chat/__init__.py +3 -0
  877. package/plugins/platforms/google_chat/__pycache__/__init__.cpython-312.pyc +0 -0
  878. package/plugins/platforms/google_chat/__pycache__/adapter.cpython-312.pyc +0 -0
  879. package/plugins/platforms/google_chat/adapter.py +3343 -0
  880. package/plugins/platforms/google_chat/oauth.py +639 -0
  881. package/plugins/platforms/google_chat/plugin.yaml +39 -0
  882. package/plugins/platforms/irc/__init__.py +3 -0
  883. package/plugins/platforms/irc/__pycache__/__init__.cpython-312.pyc +0 -0
  884. package/plugins/platforms/irc/__pycache__/adapter.cpython-312.pyc +0 -0
  885. package/plugins/platforms/irc/adapter.py +969 -0
  886. package/plugins/platforms/irc/plugin.yaml +54 -0
  887. package/plugins/platforms/line/__init__.py +3 -0
  888. package/plugins/platforms/line/__pycache__/__init__.cpython-312.pyc +0 -0
  889. package/plugins/platforms/line/__pycache__/adapter.cpython-312.pyc +0 -0
  890. package/plugins/platforms/line/adapter.py +1639 -0
  891. package/plugins/platforms/line/plugin.yaml +65 -0
  892. package/plugins/platforms/simplex/__init__.py +3 -0
  893. package/plugins/platforms/simplex/__pycache__/__init__.cpython-312.pyc +0 -0
  894. package/plugins/platforms/simplex/__pycache__/adapter.cpython-312.pyc +0 -0
  895. package/plugins/platforms/simplex/adapter.py +746 -0
  896. package/plugins/platforms/simplex/plugin.yaml +37 -0
  897. package/plugins/platforms/teams/__init__.py +3 -0
  898. package/plugins/platforms/teams/__pycache__/__init__.cpython-312.pyc +0 -0
  899. package/plugins/platforms/teams/__pycache__/adapter.cpython-312.pyc +0 -0
  900. package/plugins/platforms/teams/adapter.py +1188 -0
  901. package/plugins/platforms/teams/plugin.yaml +48 -0
  902. package/plugins/spotify/__init__.py +66 -0
  903. package/plugins/spotify/__pycache__/__init__.cpython-312.pyc +0 -0
  904. package/plugins/spotify/__pycache__/client.cpython-312.pyc +0 -0
  905. package/plugins/spotify/__pycache__/tools.cpython-312.pyc +0 -0
  906. package/plugins/spotify/client.py +435 -0
  907. package/plugins/spotify/plugin.yaml +13 -0
  908. package/plugins/spotify/tools.py +454 -0
  909. package/plugins/teams_pipeline/__init__.py +23 -0
  910. package/plugins/teams_pipeline/cli.py +463 -0
  911. package/plugins/teams_pipeline/meetings.py +333 -0
  912. package/plugins/teams_pipeline/models.py +350 -0
  913. package/plugins/teams_pipeline/pipeline.py +692 -0
  914. package/plugins/teams_pipeline/plugin.yaml +9 -0
  915. package/plugins/teams_pipeline/runtime.py +135 -0
  916. package/plugins/teams_pipeline/store.py +194 -0
  917. package/plugins/teams_pipeline/subscriptions.py +249 -0
  918. package/plugins/video_gen/fal/__init__.py +523 -0
  919. package/plugins/video_gen/fal/__pycache__/__init__.cpython-312.pyc +0 -0
  920. package/plugins/video_gen/fal/plugin.yaml +7 -0
  921. package/plugins/video_gen/xai/__init__.py +441 -0
  922. package/plugins/video_gen/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  923. package/plugins/video_gen/xai/plugin.yaml +7 -0
  924. package/plugins/web/__init__.py +7 -0
  925. package/plugins/web/__pycache__/__init__.cpython-312.pyc +0 -0
  926. package/plugins/web/brave_free/__init__.py +14 -0
  927. package/plugins/web/brave_free/__pycache__/__init__.cpython-312.pyc +0 -0
  928. package/plugins/web/brave_free/__pycache__/provider.cpython-312.pyc +0 -0
  929. package/plugins/web/brave_free/plugin.yaml +7 -0
  930. package/plugins/web/brave_free/provider.py +137 -0
  931. package/plugins/web/ddgs/__init__.py +15 -0
  932. package/plugins/web/ddgs/__pycache__/__init__.cpython-312.pyc +0 -0
  933. package/plugins/web/ddgs/__pycache__/provider.cpython-312.pyc +0 -0
  934. package/plugins/web/ddgs/plugin.yaml +7 -0
  935. package/plugins/web/ddgs/provider.py +104 -0
  936. package/plugins/web/exa/__init__.py +15 -0
  937. package/plugins/web/exa/__pycache__/__init__.cpython-312.pyc +0 -0
  938. package/plugins/web/exa/__pycache__/provider.cpython-312.pyc +0 -0
  939. package/plugins/web/exa/plugin.yaml +7 -0
  940. package/plugins/web/exa/provider.py +212 -0
  941. package/plugins/web/firecrawl/__init__.py +28 -0
  942. package/plugins/web/firecrawl/__pycache__/__init__.cpython-312.pyc +0 -0
  943. package/plugins/web/firecrawl/__pycache__/provider.cpython-312.pyc +0 -0
  944. package/plugins/web/firecrawl/plugin.yaml +7 -0
  945. package/plugins/web/firecrawl/provider.py +773 -0
  946. package/plugins/web/parallel/__init__.py +16 -0
  947. package/plugins/web/parallel/__pycache__/__init__.cpython-312.pyc +0 -0
  948. package/plugins/web/parallel/__pycache__/provider.cpython-312.pyc +0 -0
  949. package/plugins/web/parallel/plugin.yaml +7 -0
  950. package/plugins/web/parallel/provider.py +291 -0
  951. package/plugins/web/searxng/__init__.py +15 -0
  952. package/plugins/web/searxng/__pycache__/__init__.cpython-312.pyc +0 -0
  953. package/plugins/web/searxng/__pycache__/provider.cpython-312.pyc +0 -0
  954. package/plugins/web/searxng/plugin.yaml +7 -0
  955. package/plugins/web/searxng/provider.py +140 -0
  956. package/plugins/web/tavily/__init__.py +15 -0
  957. package/plugins/web/tavily/__pycache__/__init__.cpython-312.pyc +0 -0
  958. package/plugins/web/tavily/__pycache__/provider.cpython-312.pyc +0 -0
  959. package/plugins/web/tavily/plugin.yaml +7 -0
  960. package/plugins/web/tavily/provider.py +285 -0
  961. package/providers/README.md +78 -0
  962. package/providers/__init__.py +192 -0
  963. package/providers/__pycache__/__init__.cpython-312.pyc +0 -0
  964. package/providers/__pycache__/base.cpython-312.pyc +0 -0
  965. package/providers/base.py +184 -0
  966. package/pyproject.toml +255 -0
  967. package/run_agent.py +16409 -0
  968. package/scripts/benchmark_browser_eval.py +138 -0
  969. package/scripts/build_model_catalog.py +95 -0
  970. package/scripts/build_skills_index.py +325 -0
  971. package/scripts/check-windows-footguns.py +624 -0
  972. package/scripts/contributor_audit.py +473 -0
  973. package/scripts/discord-voice-doctor.py +396 -0
  974. package/scripts/hermes-gateway +416 -0
  975. package/scripts/install.cmd +28 -0
  976. package/scripts/install.ps1 +1611 -0
  977. package/scripts/install.sh +2007 -0
  978. package/scripts/install_psutil_android.py +117 -0
  979. package/scripts/keystroke_diagnostic.py +81 -0
  980. package/scripts/kill_modal.sh +34 -0
  981. package/scripts/lib/node-bootstrap.sh +238 -0
  982. package/scripts/lint_diff.py +207 -0
  983. package/scripts/postinstall.js +150 -0
  984. package/scripts/profile-tui.py +626 -0
  985. package/scripts/release.py +1680 -0
  986. package/scripts/run_tests.sh +129 -0
  987. package/scripts/sample_and_compress.py +409 -0
  988. package/scripts/setup_open_webui.sh +349 -0
  989. package/scripts/whatsapp-bridge/allowlist.js +88 -0
  990. package/scripts/whatsapp-bridge/allowlist.test.mjs +80 -0
  991. package/scripts/whatsapp-bridge/bridge.js +729 -0
  992. package/scripts/whatsapp-bridge/package-lock.json +2141 -0
  993. package/scripts/whatsapp-bridge/package.json +19 -0
  994. package/skills/apple/DESCRIPTION.md +2 -0
  995. package/skills/apple/apple-notes/SKILL.md +90 -0
  996. package/skills/apple/apple-reminders/SKILL.md +98 -0
  997. package/skills/apple/findmy/SKILL.md +131 -0
  998. package/skills/apple/imessage/SKILL.md +102 -0
  999. package/skills/apple/macos-computer-use/SKILL.md +201 -0
  1000. package/skills/autonomous-ai-agents/DESCRIPTION.md +3 -0
  1001. package/skills/autonomous-ai-agents/claude-code/SKILL.md +745 -0
  1002. package/skills/autonomous-ai-agents/codex/SKILL.md +130 -0
  1003. package/skills/autonomous-ai-agents/hermes-agent/SKILL.md +1014 -0
  1004. package/skills/autonomous-ai-agents/opencode/SKILL.md +219 -0
  1005. package/skills/creative/DESCRIPTION.md +3 -0
  1006. package/skills/creative/architecture-diagram/SKILL.md +148 -0
  1007. package/skills/creative/architecture-diagram/templates/template.html +319 -0
  1008. package/skills/creative/ascii-art/SKILL.md +322 -0
  1009. package/skills/creative/ascii-video/README.md +290 -0
  1010. package/skills/creative/ascii-video/SKILL.md +241 -0
  1011. package/skills/creative/ascii-video/references/architecture.md +802 -0
  1012. package/skills/creative/ascii-video/references/composition.md +892 -0
  1013. package/skills/creative/ascii-video/references/effects.md +1865 -0
  1014. package/skills/creative/ascii-video/references/inputs.md +685 -0
  1015. package/skills/creative/ascii-video/references/optimization.md +688 -0
  1016. package/skills/creative/ascii-video/references/scenes.md +1011 -0
  1017. package/skills/creative/ascii-video/references/shaders.md +1385 -0
  1018. package/skills/creative/ascii-video/references/troubleshooting.md +367 -0
  1019. package/skills/creative/baoyu-comic/PORT_NOTES.md +77 -0
  1020. package/skills/creative/baoyu-comic/SKILL.md +247 -0
  1021. package/skills/creative/baoyu-comic/references/analysis-framework.md +176 -0
  1022. package/skills/creative/baoyu-comic/references/art-styles/chalk.md +101 -0
  1023. package/skills/creative/baoyu-comic/references/art-styles/ink-brush.md +97 -0
  1024. package/skills/creative/baoyu-comic/references/art-styles/ligne-claire.md +75 -0
  1025. package/skills/creative/baoyu-comic/references/art-styles/manga.md +93 -0
  1026. package/skills/creative/baoyu-comic/references/art-styles/minimalist.md +84 -0
  1027. package/skills/creative/baoyu-comic/references/art-styles/realistic.md +89 -0
  1028. package/skills/creative/baoyu-comic/references/auto-selection.md +71 -0
  1029. package/skills/creative/baoyu-comic/references/base-prompt.md +98 -0
  1030. package/skills/creative/baoyu-comic/references/character-template.md +180 -0
  1031. package/skills/creative/baoyu-comic/references/layouts/cinematic.md +23 -0
  1032. package/skills/creative/baoyu-comic/references/layouts/dense.md +23 -0
  1033. package/skills/creative/baoyu-comic/references/layouts/four-panel.md +40 -0
  1034. package/skills/creative/baoyu-comic/references/layouts/mixed.md +23 -0
  1035. package/skills/creative/baoyu-comic/references/layouts/splash.md +23 -0
  1036. package/skills/creative/baoyu-comic/references/layouts/standard.md +23 -0
  1037. package/skills/creative/baoyu-comic/references/layouts/webtoon.md +30 -0
  1038. package/skills/creative/baoyu-comic/references/ohmsha-guide.md +85 -0
  1039. package/skills/creative/baoyu-comic/references/partial-workflows.md +106 -0
  1040. package/skills/creative/baoyu-comic/references/presets/concept-story.md +121 -0
  1041. package/skills/creative/baoyu-comic/references/presets/four-panel.md +107 -0
  1042. package/skills/creative/baoyu-comic/references/presets/ohmsha.md +114 -0
  1043. package/skills/creative/baoyu-comic/references/presets/shoujo.md +116 -0
  1044. package/skills/creative/baoyu-comic/references/presets/wuxia.md +110 -0
  1045. package/skills/creative/baoyu-comic/references/storyboard-template.md +143 -0
  1046. package/skills/creative/baoyu-comic/references/tones/action.md +110 -0
  1047. package/skills/creative/baoyu-comic/references/tones/dramatic.md +95 -0
  1048. package/skills/creative/baoyu-comic/references/tones/energetic.md +105 -0
  1049. package/skills/creative/baoyu-comic/references/tones/neutral.md +63 -0
  1050. package/skills/creative/baoyu-comic/references/tones/romantic.md +100 -0
  1051. package/skills/creative/baoyu-comic/references/tones/vintage.md +104 -0
  1052. package/skills/creative/baoyu-comic/references/tones/warm.md +94 -0
  1053. package/skills/creative/baoyu-comic/references/workflow.md +401 -0
  1054. package/skills/creative/baoyu-infographic/PORT_NOTES.md +43 -0
  1055. package/skills/creative/baoyu-infographic/SKILL.md +237 -0
  1056. package/skills/creative/baoyu-infographic/references/analysis-framework.md +182 -0
  1057. package/skills/creative/baoyu-infographic/references/base-prompt.md +43 -0
  1058. package/skills/creative/baoyu-infographic/references/layouts/bento-grid.md +41 -0
  1059. package/skills/creative/baoyu-infographic/references/layouts/binary-comparison.md +48 -0
  1060. package/skills/creative/baoyu-infographic/references/layouts/bridge.md +41 -0
  1061. package/skills/creative/baoyu-infographic/references/layouts/circular-flow.md +41 -0
  1062. package/skills/creative/baoyu-infographic/references/layouts/comic-strip.md +41 -0
  1063. package/skills/creative/baoyu-infographic/references/layouts/comparison-matrix.md +41 -0
  1064. package/skills/creative/baoyu-infographic/references/layouts/dashboard.md +41 -0
  1065. package/skills/creative/baoyu-infographic/references/layouts/dense-modules.md +72 -0
  1066. package/skills/creative/baoyu-infographic/references/layouts/funnel.md +41 -0
  1067. package/skills/creative/baoyu-infographic/references/layouts/hierarchical-layers.md +48 -0
  1068. package/skills/creative/baoyu-infographic/references/layouts/hub-spoke.md +41 -0
  1069. package/skills/creative/baoyu-infographic/references/layouts/iceberg.md +41 -0
  1070. package/skills/creative/baoyu-infographic/references/layouts/isometric-map.md +41 -0
  1071. package/skills/creative/baoyu-infographic/references/layouts/jigsaw.md +41 -0
  1072. package/skills/creative/baoyu-infographic/references/layouts/linear-progression.md +48 -0
  1073. package/skills/creative/baoyu-infographic/references/layouts/periodic-table.md +41 -0
  1074. package/skills/creative/baoyu-infographic/references/layouts/story-mountain.md +41 -0
  1075. package/skills/creative/baoyu-infographic/references/layouts/structural-breakdown.md +48 -0
  1076. package/skills/creative/baoyu-infographic/references/layouts/tree-branching.md +41 -0
  1077. package/skills/creative/baoyu-infographic/references/layouts/venn-diagram.md +41 -0
  1078. package/skills/creative/baoyu-infographic/references/layouts/winding-roadmap.md +41 -0
  1079. package/skills/creative/baoyu-infographic/references/structured-content-template.md +244 -0
  1080. package/skills/creative/baoyu-infographic/references/styles/aged-academia.md +36 -0
  1081. package/skills/creative/baoyu-infographic/references/styles/bold-graphic.md +36 -0
  1082. package/skills/creative/baoyu-infographic/references/styles/chalkboard.md +61 -0
  1083. package/skills/creative/baoyu-infographic/references/styles/claymation.md +29 -0
  1084. package/skills/creative/baoyu-infographic/references/styles/corporate-memphis.md +29 -0
  1085. package/skills/creative/baoyu-infographic/references/styles/craft-handmade.md +44 -0
  1086. package/skills/creative/baoyu-infographic/references/styles/cyberpunk-neon.md +29 -0
  1087. package/skills/creative/baoyu-infographic/references/styles/hand-drawn-edu.md +63 -0
  1088. package/skills/creative/baoyu-infographic/references/styles/ikea-manual.md +29 -0
  1089. package/skills/creative/baoyu-infographic/references/styles/kawaii.md +29 -0
  1090. package/skills/creative/baoyu-infographic/references/styles/knolling.md +29 -0
  1091. package/skills/creative/baoyu-infographic/references/styles/lego-brick.md +29 -0
  1092. package/skills/creative/baoyu-infographic/references/styles/morandi-journal.md +60 -0
  1093. package/skills/creative/baoyu-infographic/references/styles/origami.md +29 -0
  1094. package/skills/creative/baoyu-infographic/references/styles/pixel-art.md +29 -0
  1095. package/skills/creative/baoyu-infographic/references/styles/pop-laboratory.md +48 -0
  1096. package/skills/creative/baoyu-infographic/references/styles/retro-pop-grid.md +47 -0
  1097. package/skills/creative/baoyu-infographic/references/styles/storybook-watercolor.md +29 -0
  1098. package/skills/creative/baoyu-infographic/references/styles/subway-map.md +29 -0
  1099. package/skills/creative/baoyu-infographic/references/styles/technical-schematic.md +36 -0
  1100. package/skills/creative/baoyu-infographic/references/styles/ui-wireframe.md +29 -0
  1101. package/skills/creative/claude-design/SKILL.md +591 -0
  1102. package/skills/creative/comfyui/SKILL.md +612 -0
  1103. package/skills/creative/comfyui/references/official-cli.md +255 -0
  1104. package/skills/creative/comfyui/references/rest-api.md +312 -0
  1105. package/skills/creative/comfyui/references/template-integrity.md +243 -0
  1106. package/skills/creative/comfyui/references/workflow-format.md +226 -0
  1107. package/skills/creative/comfyui/scripts/_common.py +835 -0
  1108. package/skills/creative/comfyui/scripts/auto_fix_deps.py +225 -0
  1109. package/skills/creative/comfyui/scripts/check_deps.py +437 -0
  1110. package/skills/creative/comfyui/scripts/comfyui_setup.sh +286 -0
  1111. package/skills/creative/comfyui/scripts/extract_schema.py +315 -0
  1112. package/skills/creative/comfyui/scripts/fetch_logs.py +158 -0
  1113. package/skills/creative/comfyui/scripts/hardware_check.py +497 -0
  1114. package/skills/creative/comfyui/scripts/health_check.py +223 -0
  1115. package/skills/creative/comfyui/scripts/run_batch.py +243 -0
  1116. package/skills/creative/comfyui/scripts/run_workflow.py +796 -0
  1117. package/skills/creative/comfyui/scripts/ws_monitor.py +267 -0
  1118. package/skills/creative/comfyui/tests/README.md +50 -0
  1119. package/skills/creative/comfyui/tests/conftest.py +64 -0
  1120. package/skills/creative/comfyui/tests/pytest.ini +5 -0
  1121. package/skills/creative/comfyui/tests/test_check_deps.py +68 -0
  1122. package/skills/creative/comfyui/tests/test_cloud_integration.py +95 -0
  1123. package/skills/creative/comfyui/tests/test_common.py +447 -0
  1124. package/skills/creative/comfyui/tests/test_extract_schema.py +185 -0
  1125. package/skills/creative/comfyui/tests/test_run_workflow.py +213 -0
  1126. package/skills/creative/comfyui/workflows/README.md +86 -0
  1127. package/skills/creative/comfyui/workflows/animatediff_video.json +64 -0
  1128. package/skills/creative/comfyui/workflows/flux_dev_txt2img.json +78 -0
  1129. package/skills/creative/comfyui/workflows/sd15_txt2img.json +49 -0
  1130. package/skills/creative/comfyui/workflows/sdxl_img2img.json +54 -0
  1131. package/skills/creative/comfyui/workflows/sdxl_inpaint.json +59 -0
  1132. package/skills/creative/comfyui/workflows/sdxl_txt2img.json +49 -0
  1133. package/skills/creative/comfyui/workflows/upscale_4x.json +27 -0
  1134. package/skills/creative/comfyui/workflows/wan_video_t2v.json +69 -0
  1135. package/skills/creative/creative-ideation/SKILL.md +152 -0
  1136. package/skills/creative/creative-ideation/references/full-prompt-library.md +110 -0
  1137. package/skills/creative/design-md/SKILL.md +199 -0
  1138. package/skills/creative/design-md/templates/starter.md +99 -0
  1139. package/skills/creative/excalidraw/SKILL.md +199 -0
  1140. package/skills/creative/excalidraw/references/colors.md +44 -0
  1141. package/skills/creative/excalidraw/references/dark-mode.md +68 -0
  1142. package/skills/creative/excalidraw/references/examples.md +141 -0
  1143. package/skills/creative/excalidraw/scripts/upload.py +133 -0
  1144. package/skills/creative/humanizer/LICENSE +21 -0
  1145. package/skills/creative/humanizer/SKILL.md +578 -0
  1146. package/skills/creative/manim-video/README.md +23 -0
  1147. package/skills/creative/manim-video/SKILL.md +269 -0
  1148. package/skills/creative/manim-video/references/animation-design-thinking.md +161 -0
  1149. package/skills/creative/manim-video/references/animations.md +282 -0
  1150. package/skills/creative/manim-video/references/camera-and-3d.md +135 -0
  1151. package/skills/creative/manim-video/references/decorations.md +202 -0
  1152. package/skills/creative/manim-video/references/equations.md +216 -0
  1153. package/skills/creative/manim-video/references/graphs-and-data.md +163 -0
  1154. package/skills/creative/manim-video/references/mobjects.md +333 -0
  1155. package/skills/creative/manim-video/references/paper-explainer.md +255 -0
  1156. package/skills/creative/manim-video/references/production-quality.md +190 -0
  1157. package/skills/creative/manim-video/references/rendering.md +185 -0
  1158. package/skills/creative/manim-video/references/scene-planning.md +118 -0
  1159. package/skills/creative/manim-video/references/troubleshooting.md +135 -0
  1160. package/skills/creative/manim-video/references/updaters-and-trackers.md +260 -0
  1161. package/skills/creative/manim-video/references/visual-design.md +124 -0
  1162. package/skills/creative/manim-video/scripts/setup.sh +14 -0
  1163. package/skills/creative/p5js/README.md +64 -0
  1164. package/skills/creative/p5js/SKILL.md +556 -0
  1165. package/skills/creative/p5js/references/animation.md +439 -0
  1166. package/skills/creative/p5js/references/color-systems.md +352 -0
  1167. package/skills/creative/p5js/references/core-api.md +410 -0
  1168. package/skills/creative/p5js/references/export-pipeline.md +566 -0
  1169. package/skills/creative/p5js/references/interaction.md +398 -0
  1170. package/skills/creative/p5js/references/shapes-and-geometry.md +300 -0
  1171. package/skills/creative/p5js/references/troubleshooting.md +532 -0
  1172. package/skills/creative/p5js/references/typography.md +302 -0
  1173. package/skills/creative/p5js/references/visual-effects.md +895 -0
  1174. package/skills/creative/p5js/references/webgl-and-3d.md +423 -0
  1175. package/skills/creative/p5js/scripts/export-frames.js +179 -0
  1176. package/skills/creative/p5js/scripts/render.sh +108 -0
  1177. package/skills/creative/p5js/scripts/serve.sh +28 -0
  1178. package/skills/creative/p5js/scripts/setup.sh +87 -0
  1179. package/skills/creative/p5js/templates/viewer.html +395 -0
  1180. package/skills/creative/pixel-art/ATTRIBUTION.md +54 -0
  1181. package/skills/creative/pixel-art/SKILL.md +218 -0
  1182. package/skills/creative/pixel-art/references/palettes.md +49 -0
  1183. package/skills/creative/pixel-art/scripts/__init__.py +0 -0
  1184. package/skills/creative/pixel-art/scripts/palettes.py +167 -0
  1185. package/skills/creative/pixel-art/scripts/pixel_art.py +162 -0
  1186. package/skills/creative/pixel-art/scripts/pixel_art_video.py +345 -0
  1187. package/skills/creative/popular-web-designs/SKILL.md +214 -0
  1188. package/skills/creative/popular-web-designs/templates/airbnb.md +259 -0
  1189. package/skills/creative/popular-web-designs/templates/airtable.md +102 -0
  1190. package/skills/creative/popular-web-designs/templates/apple.md +326 -0
  1191. package/skills/creative/popular-web-designs/templates/bmw.md +193 -0
  1192. package/skills/creative/popular-web-designs/templates/cal.md +272 -0
  1193. package/skills/creative/popular-web-designs/templates/claude.md +325 -0
  1194. package/skills/creative/popular-web-designs/templates/clay.md +317 -0
  1195. package/skills/creative/popular-web-designs/templates/clickhouse.md +294 -0
  1196. package/skills/creative/popular-web-designs/templates/cohere.md +279 -0
  1197. package/skills/creative/popular-web-designs/templates/coinbase.md +142 -0
  1198. package/skills/creative/popular-web-designs/templates/composio.md +320 -0
  1199. package/skills/creative/popular-web-designs/templates/cursor.md +322 -0
  1200. package/skills/creative/popular-web-designs/templates/elevenlabs.md +278 -0
  1201. package/skills/creative/popular-web-designs/templates/expo.md +294 -0
  1202. package/skills/creative/popular-web-designs/templates/figma.md +233 -0
  1203. package/skills/creative/popular-web-designs/templates/framer.md +259 -0
  1204. package/skills/creative/popular-web-designs/templates/hashicorp.md +291 -0
  1205. package/skills/creative/popular-web-designs/templates/ibm.md +345 -0
  1206. package/skills/creative/popular-web-designs/templates/intercom.md +159 -0
  1207. package/skills/creative/popular-web-designs/templates/kraken.md +138 -0
  1208. package/skills/creative/popular-web-designs/templates/linear.app.md +380 -0
  1209. package/skills/creative/popular-web-designs/templates/lovable.md +311 -0
  1210. package/skills/creative/popular-web-designs/templates/minimax.md +270 -0
  1211. package/skills/creative/popular-web-designs/templates/mintlify.md +339 -0
  1212. package/skills/creative/popular-web-designs/templates/miro.md +121 -0
  1213. package/skills/creative/popular-web-designs/templates/mistral.ai.md +274 -0
  1214. package/skills/creative/popular-web-designs/templates/mongodb.md +279 -0
  1215. package/skills/creative/popular-web-designs/templates/notion.md +322 -0
  1216. package/skills/creative/popular-web-designs/templates/nvidia.md +306 -0
  1217. package/skills/creative/popular-web-designs/templates/ollama.md +280 -0
  1218. package/skills/creative/popular-web-designs/templates/opencode.ai.md +294 -0
  1219. package/skills/creative/popular-web-designs/templates/pinterest.md +243 -0
  1220. package/skills/creative/popular-web-designs/templates/posthog.md +269 -0
  1221. package/skills/creative/popular-web-designs/templates/raycast.md +281 -0
  1222. package/skills/creative/popular-web-designs/templates/replicate.md +274 -0
  1223. package/skills/creative/popular-web-designs/templates/resend.md +316 -0
  1224. package/skills/creative/popular-web-designs/templates/revolut.md +198 -0
  1225. package/skills/creative/popular-web-designs/templates/runwayml.md +257 -0
  1226. package/skills/creative/popular-web-designs/templates/sanity.md +370 -0
  1227. package/skills/creative/popular-web-designs/templates/sentry.md +275 -0
  1228. package/skills/creative/popular-web-designs/templates/spacex.md +207 -0
  1229. package/skills/creative/popular-web-designs/templates/spotify.md +259 -0
  1230. package/skills/creative/popular-web-designs/templates/stripe.md +335 -0
  1231. package/skills/creative/popular-web-designs/templates/supabase.md +268 -0
  1232. package/skills/creative/popular-web-designs/templates/superhuman.md +265 -0
  1233. package/skills/creative/popular-web-designs/templates/together.ai.md +276 -0
  1234. package/skills/creative/popular-web-designs/templates/uber.md +308 -0
  1235. package/skills/creative/popular-web-designs/templates/vercel.md +323 -0
  1236. package/skills/creative/popular-web-designs/templates/voltagent.md +336 -0
  1237. package/skills/creative/popular-web-designs/templates/warp.md +266 -0
  1238. package/skills/creative/popular-web-designs/templates/webflow.md +105 -0
  1239. package/skills/creative/popular-web-designs/templates/wise.md +186 -0
  1240. package/skills/creative/popular-web-designs/templates/x.ai.md +270 -0
  1241. package/skills/creative/popular-web-designs/templates/zapier.md +341 -0
  1242. package/skills/creative/pretext/SKILL.md +220 -0
  1243. package/skills/creative/pretext/references/patterns.md +258 -0
  1244. package/skills/creative/pretext/templates/donut-orbit.html +1468 -0
  1245. package/skills/creative/pretext/templates/hello-orb-flow.html +95 -0
  1246. package/skills/creative/sketch/SKILL.md +218 -0
  1247. package/skills/creative/songwriting-and-ai-music/SKILL.md +287 -0
  1248. package/skills/creative/touchdesigner-mcp/SKILL.md +356 -0
  1249. package/skills/creative/touchdesigner-mcp/references/3d-scene.md +275 -0
  1250. package/skills/creative/touchdesigner-mcp/references/animation.md +221 -0
  1251. package/skills/creative/touchdesigner-mcp/references/audio-reactive.md +175 -0
  1252. package/skills/creative/touchdesigner-mcp/references/dat-scripting.md +352 -0
  1253. package/skills/creative/touchdesigner-mcp/references/external-data.md +322 -0
  1254. package/skills/creative/touchdesigner-mcp/references/geometry-comp.md +121 -0
  1255. package/skills/creative/touchdesigner-mcp/references/glsl.md +151 -0
  1256. package/skills/creative/touchdesigner-mcp/references/layout-compositor.md +131 -0
  1257. package/skills/creative/touchdesigner-mcp/references/mcp-tools.md +382 -0
  1258. package/skills/creative/touchdesigner-mcp/references/midi-osc.md +211 -0
  1259. package/skills/creative/touchdesigner-mcp/references/network-patterns.md +966 -0
  1260. package/skills/creative/touchdesigner-mcp/references/operator-tips.md +106 -0
  1261. package/skills/creative/touchdesigner-mcp/references/operators.md +239 -0
  1262. package/skills/creative/touchdesigner-mcp/references/panel-ui.md +281 -0
  1263. package/skills/creative/touchdesigner-mcp/references/particles.md +245 -0
  1264. package/skills/creative/touchdesigner-mcp/references/pitfalls.md +704 -0
  1265. package/skills/creative/touchdesigner-mcp/references/postfx.md +183 -0
  1266. package/skills/creative/touchdesigner-mcp/references/projection-mapping.md +211 -0
  1267. package/skills/creative/touchdesigner-mcp/references/python-api.md +463 -0
  1268. package/skills/creative/touchdesigner-mcp/references/replicator.md +198 -0
  1269. package/skills/creative/touchdesigner-mcp/references/troubleshooting.md +244 -0
  1270. package/skills/creative/touchdesigner-mcp/scripts/setup.sh +115 -0
  1271. package/skills/data-science/DESCRIPTION.md +3 -0
  1272. package/skills/data-science/jupyter-live-kernel/SKILL.md +167 -0
  1273. package/skills/devops/kanban-orchestrator/SKILL.md +189 -0
  1274. package/skills/devops/kanban-worker/SKILL.md +184 -0
  1275. package/skills/devops/webhook-subscriptions/SKILL.md +204 -0
  1276. package/skills/diagramming/DESCRIPTION.md +3 -0
  1277. package/skills/dogfood/SKILL.md +162 -0
  1278. package/skills/dogfood/references/issue-taxonomy.md +109 -0
  1279. package/skills/dogfood/templates/dogfood-report-template.md +86 -0
  1280. package/skills/domain/DESCRIPTION.md +24 -0
  1281. package/skills/email/DESCRIPTION.md +3 -0
  1282. package/skills/email/himalaya/SKILL.md +299 -0
  1283. package/skills/email/himalaya/references/configuration.md +227 -0
  1284. package/skills/email/himalaya/references/message-composition.md +199 -0
  1285. package/skills/gaming/DESCRIPTION.md +3 -0
  1286. package/skills/gaming/minecraft-modpack-server/SKILL.md +187 -0
  1287. package/skills/gaming/pokemon-player/SKILL.md +216 -0
  1288. package/skills/gifs/DESCRIPTION.md +3 -0
  1289. package/skills/github/DESCRIPTION.md +3 -0
  1290. package/skills/github/codebase-inspection/SKILL.md +116 -0
  1291. package/skills/github/github-auth/SKILL.md +247 -0
  1292. package/skills/github/github-auth/scripts/gh-env.sh +66 -0
  1293. package/skills/github/github-code-review/SKILL.md +481 -0
  1294. package/skills/github/github-code-review/references/review-output-template.md +74 -0
  1295. package/skills/github/github-issues/SKILL.md +370 -0
  1296. package/skills/github/github-issues/templates/bug-report.md +35 -0
  1297. package/skills/github/github-issues/templates/feature-request.md +31 -0
  1298. package/skills/github/github-pr-workflow/SKILL.md +367 -0
  1299. package/skills/github/github-pr-workflow/references/ci-troubleshooting.md +183 -0
  1300. package/skills/github/github-pr-workflow/references/conventional-commits.md +71 -0
  1301. package/skills/github/github-pr-workflow/templates/pr-body-bugfix.md +35 -0
  1302. package/skills/github/github-pr-workflow/templates/pr-body-feature.md +33 -0
  1303. package/skills/github/github-repo-management/SKILL.md +516 -0
  1304. package/skills/github/github-repo-management/references/github-api-cheatsheet.md +161 -0
  1305. package/skills/index-cache/anthropics_skills_skills_.json +1 -0
  1306. package/skills/index-cache/claude_marketplace_anthropics_skills.json +1 -0
  1307. package/skills/index-cache/lobehub_index.json +1 -0
  1308. package/skills/index-cache/openai_skills_skills_.json +1 -0
  1309. package/skills/inference-sh/DESCRIPTION.md +19 -0
  1310. package/skills/mcp/DESCRIPTION.md +3 -0
  1311. package/skills/mcp/native-mcp/SKILL.md +357 -0
  1312. package/skills/media/DESCRIPTION.md +3 -0
  1313. package/skills/media/gif-search/SKILL.md +91 -0
  1314. package/skills/media/heartmula/SKILL.md +171 -0
  1315. package/skills/media/songsee/SKILL.md +83 -0
  1316. package/skills/media/spotify/SKILL.md +135 -0
  1317. package/skills/media/youtube-content/SKILL.md +73 -0
  1318. package/skills/media/youtube-content/references/output-formats.md +56 -0
  1319. package/skills/media/youtube-content/scripts/fetch_transcript.py +124 -0
  1320. package/skills/mlops/DESCRIPTION.md +3 -0
  1321. package/skills/mlops/evaluation/DESCRIPTION.md +3 -0
  1322. package/skills/mlops/evaluation/lm-evaluation-harness/SKILL.md +498 -0
  1323. package/skills/mlops/evaluation/lm-evaluation-harness/references/api-evaluation.md +490 -0
  1324. package/skills/mlops/evaluation/lm-evaluation-harness/references/benchmark-guide.md +488 -0
  1325. package/skills/mlops/evaluation/lm-evaluation-harness/references/custom-tasks.md +602 -0
  1326. package/skills/mlops/evaluation/lm-evaluation-harness/references/distributed-eval.md +519 -0
  1327. package/skills/mlops/evaluation/weights-and-biases/SKILL.md +594 -0
  1328. package/skills/mlops/evaluation/weights-and-biases/references/artifacts.md +584 -0
  1329. package/skills/mlops/evaluation/weights-and-biases/references/integrations.md +700 -0
  1330. package/skills/mlops/evaluation/weights-and-biases/references/sweeps.md +847 -0
  1331. package/skills/mlops/huggingface-hub/SKILL.md +81 -0
  1332. package/skills/mlops/inference/DESCRIPTION.md +3 -0
  1333. package/skills/mlops/inference/llama-cpp/SKILL.md +249 -0
  1334. package/skills/mlops/inference/llama-cpp/references/advanced-usage.md +504 -0
  1335. package/skills/mlops/inference/llama-cpp/references/hub-discovery.md +168 -0
  1336. package/skills/mlops/inference/llama-cpp/references/optimization.md +89 -0
  1337. package/skills/mlops/inference/llama-cpp/references/quantization.md +243 -0
  1338. package/skills/mlops/inference/llama-cpp/references/server.md +150 -0
  1339. package/skills/mlops/inference/llama-cpp/references/troubleshooting.md +442 -0
  1340. package/skills/mlops/inference/obliteratus/SKILL.md +342 -0
  1341. package/skills/mlops/inference/obliteratus/references/analysis-modules.md +166 -0
  1342. package/skills/mlops/inference/obliteratus/references/methods-guide.md +141 -0
  1343. package/skills/mlops/inference/obliteratus/templates/abliteration-config.yaml +33 -0
  1344. package/skills/mlops/inference/obliteratus/templates/analysis-study.yaml +40 -0
  1345. package/skills/mlops/inference/obliteratus/templates/batch-abliteration.yaml +41 -0
  1346. package/skills/mlops/inference/vllm/SKILL.md +372 -0
  1347. package/skills/mlops/inference/vllm/references/optimization.md +226 -0
  1348. package/skills/mlops/inference/vllm/references/quantization.md +284 -0
  1349. package/skills/mlops/inference/vllm/references/server-deployment.md +255 -0
  1350. package/skills/mlops/inference/vllm/references/troubleshooting.md +447 -0
  1351. package/skills/mlops/models/DESCRIPTION.md +3 -0
  1352. package/skills/mlops/models/audiocraft/SKILL.md +568 -0
  1353. package/skills/mlops/models/audiocraft/references/advanced-usage.md +666 -0
  1354. package/skills/mlops/models/audiocraft/references/troubleshooting.md +504 -0
  1355. package/skills/mlops/models/segment-anything/SKILL.md +506 -0
  1356. package/skills/mlops/models/segment-anything/references/advanced-usage.md +589 -0
  1357. package/skills/mlops/models/segment-anything/references/troubleshooting.md +484 -0
  1358. package/skills/mlops/research/DESCRIPTION.md +3 -0
  1359. package/skills/mlops/research/dspy/SKILL.md +594 -0
  1360. package/skills/mlops/research/dspy/references/examples.md +663 -0
  1361. package/skills/mlops/research/dspy/references/modules.md +475 -0
  1362. package/skills/mlops/research/dspy/references/optimizers.md +566 -0
  1363. package/skills/mlops/training/DESCRIPTION.md +3 -0
  1364. package/skills/mlops/vector-databases/DESCRIPTION.md +3 -0
  1365. package/skills/note-taking/DESCRIPTION.md +3 -0
  1366. package/skills/note-taking/obsidian/SKILL.md +61 -0
  1367. package/skills/productivity/DESCRIPTION.md +3 -0
  1368. package/skills/productivity/airtable/SKILL.md +229 -0
  1369. package/skills/productivity/google-workspace/SKILL.md +335 -0
  1370. package/skills/productivity/google-workspace/references/gmail-search-syntax.md +63 -0
  1371. package/skills/productivity/google-workspace/scripts/_hermes_home.py +43 -0
  1372. package/skills/productivity/google-workspace/scripts/google_api.py +1221 -0
  1373. package/skills/productivity/google-workspace/scripts/gws_bridge.py +108 -0
  1374. package/skills/productivity/google-workspace/scripts/setup.py +454 -0
  1375. package/skills/productivity/linear/SKILL.md +380 -0
  1376. package/skills/productivity/linear/scripts/linear_api.py +445 -0
  1377. package/skills/productivity/maps/SKILL.md +195 -0
  1378. package/skills/productivity/maps/scripts/maps_client.py +1298 -0
  1379. package/skills/productivity/nano-pdf/SKILL.md +52 -0
  1380. package/skills/productivity/notion/SKILL.md +448 -0
  1381. package/skills/productivity/notion/references/block-types.md +112 -0
  1382. package/skills/productivity/ocr-and-documents/DESCRIPTION.md +3 -0
  1383. package/skills/productivity/ocr-and-documents/SKILL.md +172 -0
  1384. package/skills/productivity/ocr-and-documents/scripts/extract_marker.py +87 -0
  1385. package/skills/productivity/ocr-and-documents/scripts/extract_pymupdf.py +98 -0
  1386. package/skills/productivity/powerpoint/LICENSE.txt +30 -0
  1387. package/skills/productivity/powerpoint/SKILL.md +237 -0
  1388. package/skills/productivity/powerpoint/editing.md +205 -0
  1389. package/skills/productivity/powerpoint/pptxgenjs.md +420 -0
  1390. package/skills/productivity/powerpoint/scripts/__init__.py +0 -0
  1391. package/skills/productivity/powerpoint/scripts/add_slide.py +195 -0
  1392. package/skills/productivity/powerpoint/scripts/clean.py +286 -0
  1393. package/skills/productivity/powerpoint/scripts/office/helpers/__init__.py +0 -0
  1394. package/skills/productivity/powerpoint/scripts/office/helpers/merge_runs.py +199 -0
  1395. package/skills/productivity/powerpoint/scripts/office/helpers/simplify_redlines.py +197 -0
  1396. package/skills/productivity/powerpoint/scripts/office/pack.py +159 -0
  1397. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  1398. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  1399. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  1400. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  1401. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  1402. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  1403. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  1404. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  1405. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  1406. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  1407. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  1408. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  1409. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  1410. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  1411. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  1412. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  1413. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  1414. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  1415. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  1416. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  1417. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  1418. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  1419. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  1420. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  1421. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  1422. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  1423. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  1424. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-contentTypes.xsd +42 -0
  1425. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-coreProperties.xsd +50 -0
  1426. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-digSig.xsd +49 -0
  1427. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-relationships.xsd +33 -0
  1428. package/skills/productivity/powerpoint/scripts/office/schemas/mce/mc.xsd +75 -0
  1429. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  1430. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  1431. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  1432. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  1433. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  1434. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  1435. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  1436. package/skills/productivity/teams-meeting-pipeline/SKILL.md +116 -0
  1437. package/skills/red-teaming/godmode/SKILL.md +404 -0
  1438. package/skills/red-teaming/godmode/references/jailbreak-templates.md +128 -0
  1439. package/skills/red-teaming/godmode/references/refusal-detection.md +142 -0
  1440. package/skills/red-teaming/godmode/scripts/auto_jailbreak.py +769 -0
  1441. package/skills/red-teaming/godmode/scripts/godmode_race.py +530 -0
  1442. package/skills/red-teaming/godmode/scripts/load_godmode.py +45 -0
  1443. package/skills/red-teaming/godmode/scripts/parseltongue.py +550 -0
  1444. package/skills/red-teaming/godmode/templates/prefill-subtle.json +10 -0
  1445. package/skills/red-teaming/godmode/templates/prefill.json +18 -0
  1446. package/skills/research/DESCRIPTION.md +3 -0
  1447. package/skills/research/arxiv/SKILL.md +282 -0
  1448. package/skills/research/arxiv/scripts/search_arxiv.py +114 -0
  1449. package/skills/research/blogwatcher/SKILL.md +137 -0
  1450. package/skills/research/llm-wiki/SKILL.md +507 -0
  1451. package/skills/research/polymarket/SKILL.md +77 -0
  1452. package/skills/research/polymarket/references/api-endpoints.md +220 -0
  1453. package/skills/research/polymarket/scripts/polymarket.py +284 -0
  1454. package/skills/research/research-paper-writing/SKILL.md +2377 -0
  1455. package/skills/research/research-paper-writing/references/autoreason-methodology.md +394 -0
  1456. package/skills/research/research-paper-writing/references/checklists.md +434 -0
  1457. package/skills/research/research-paper-writing/references/citation-workflow.md +564 -0
  1458. package/skills/research/research-paper-writing/references/experiment-patterns.md +728 -0
  1459. package/skills/research/research-paper-writing/references/human-evaluation.md +476 -0
  1460. package/skills/research/research-paper-writing/references/paper-types.md +481 -0
  1461. package/skills/research/research-paper-writing/references/reviewer-guidelines.md +433 -0
  1462. package/skills/research/research-paper-writing/references/sources.md +191 -0
  1463. package/skills/research/research-paper-writing/references/writing-guide.md +474 -0
  1464. package/skills/research/research-paper-writing/templates/README.md +251 -0
  1465. package/skills/research/research-paper-writing/templates/aaai2026/README.md +534 -0
  1466. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
  1467. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026-unified-template.tex +952 -0
  1468. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.bib +111 -0
  1469. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.bst +1493 -0
  1470. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.sty +315 -0
  1471. package/skills/research/research-paper-writing/templates/acl/README.md +50 -0
  1472. package/skills/research/research-paper-writing/templates/acl/acl.sty +312 -0
  1473. package/skills/research/research-paper-writing/templates/acl/acl_latex.tex +377 -0
  1474. package/skills/research/research-paper-writing/templates/acl/acl_lualatex.tex +101 -0
  1475. package/skills/research/research-paper-writing/templates/acl/acl_natbib.bst +1940 -0
  1476. package/skills/research/research-paper-writing/templates/acl/anthology.bib.txt +26 -0
  1477. package/skills/research/research-paper-writing/templates/acl/custom.bib +70 -0
  1478. package/skills/research/research-paper-writing/templates/acl/formatting.md +326 -0
  1479. package/skills/research/research-paper-writing/templates/colm2025/README.md +3 -0
  1480. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.bib +11 -0
  1481. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.bst +1440 -0
  1482. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.pdf +0 -0
  1483. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.sty +218 -0
  1484. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.tex +305 -0
  1485. package/skills/research/research-paper-writing/templates/colm2025/fancyhdr.sty +485 -0
  1486. package/skills/research/research-paper-writing/templates/colm2025/math_commands.tex +508 -0
  1487. package/skills/research/research-paper-writing/templates/colm2025/natbib.sty +1246 -0
  1488. package/skills/research/research-paper-writing/templates/iclr2026/fancyhdr.sty +485 -0
  1489. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.bib +24 -0
  1490. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.bst +1440 -0
  1491. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.pdf +0 -0
  1492. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.sty +246 -0
  1493. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.tex +414 -0
  1494. package/skills/research/research-paper-writing/templates/iclr2026/math_commands.tex +508 -0
  1495. package/skills/research/research-paper-writing/templates/iclr2026/natbib.sty +1246 -0
  1496. package/skills/research/research-paper-writing/templates/icml2026/algorithm.sty +79 -0
  1497. package/skills/research/research-paper-writing/templates/icml2026/algorithmic.sty +201 -0
  1498. package/skills/research/research-paper-writing/templates/icml2026/example_paper.bib +75 -0
  1499. package/skills/research/research-paper-writing/templates/icml2026/example_paper.pdf +0 -0
  1500. package/skills/research/research-paper-writing/templates/icml2026/example_paper.tex +662 -0
  1501. package/skills/research/research-paper-writing/templates/icml2026/fancyhdr.sty +864 -0
  1502. package/skills/research/research-paper-writing/templates/icml2026/icml2026.bst +1443 -0
  1503. package/skills/research/research-paper-writing/templates/icml2026/icml2026.sty +767 -0
  1504. package/skills/research/research-paper-writing/templates/icml2026/icml_numpapers.pdf +0 -0
  1505. package/skills/research/research-paper-writing/templates/neurips2025/Makefile +36 -0
  1506. package/skills/research/research-paper-writing/templates/neurips2025/extra_pkgs.tex +53 -0
  1507. package/skills/research/research-paper-writing/templates/neurips2025/main.tex +38 -0
  1508. package/skills/research/research-paper-writing/templates/neurips2025/neurips.sty +382 -0
  1509. package/skills/smart-home/DESCRIPTION.md +3 -0
  1510. package/skills/smart-home/openhue/SKILL.md +109 -0
  1511. package/skills/social-media/DESCRIPTION.md +3 -0
  1512. package/skills/social-media/xurl/SKILL.md +414 -0
  1513. package/skills/software-development/debugging-hermes-tui-commands/SKILL.md +152 -0
  1514. package/skills/software-development/hermes-agent-skill-authoring/SKILL.md +165 -0
  1515. package/skills/software-development/node-inspect-debugger/SKILL.md +319 -0
  1516. package/skills/software-development/plan/SKILL.md +58 -0
  1517. package/skills/software-development/python-debugpy/SKILL.md +375 -0
  1518. package/skills/software-development/requesting-code-review/SKILL.md +280 -0
  1519. package/skills/software-development/spike/SKILL.md +197 -0
  1520. package/skills/software-development/subagent-driven-development/SKILL.md +352 -0
  1521. package/skills/software-development/subagent-driven-development/references/context-budget-discipline.md +53 -0
  1522. package/skills/software-development/subagent-driven-development/references/gates-taxonomy.md +93 -0
  1523. package/skills/software-development/systematic-debugging/SKILL.md +367 -0
  1524. package/skills/software-development/test-driven-development/SKILL.md +343 -0
  1525. package/skills/software-development/writing-plans/SKILL.md +297 -0
  1526. package/skills/yuanbao/SKILL.md +108 -0
  1527. package/tools/__init__.py +25 -0
  1528. package/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  1529. package/tools/__pycache__/approval.cpython-312.pyc +0 -0
  1530. package/tools/__pycache__/binary_extensions.cpython-312.pyc +0 -0
  1531. package/tools/__pycache__/browser_camofox.cpython-312.pyc +0 -0
  1532. package/tools/__pycache__/browser_camofox_state.cpython-312.pyc +0 -0
  1533. package/tools/__pycache__/browser_cdp_tool.cpython-312.pyc +0 -0
  1534. package/tools/__pycache__/browser_dialog_tool.cpython-312.pyc +0 -0
  1535. package/tools/__pycache__/browser_supervisor.cpython-312.pyc +0 -0
  1536. package/tools/__pycache__/browser_tool.cpython-312.pyc +0 -0
  1537. package/tools/__pycache__/budget_config.cpython-312.pyc +0 -0
  1538. package/tools/__pycache__/checkpoint_manager.cpython-312.pyc +0 -0
  1539. package/tools/__pycache__/clarify_gateway.cpython-312.pyc +0 -0
  1540. package/tools/__pycache__/clarify_tool.cpython-312.pyc +0 -0
  1541. package/tools/__pycache__/code_execution_tool.cpython-312.pyc +0 -0
  1542. package/tools/__pycache__/computer_use_tool.cpython-312.pyc +0 -0
  1543. package/tools/__pycache__/cronjob_tools.cpython-312.pyc +0 -0
  1544. package/tools/__pycache__/debug_helpers.cpython-312.pyc +0 -0
  1545. package/tools/__pycache__/delegate_tool.cpython-312.pyc +0 -0
  1546. package/tools/__pycache__/discord_tool.cpython-312.pyc +0 -0
  1547. package/tools/__pycache__/feishu_doc_tool.cpython-312.pyc +0 -0
  1548. package/tools/__pycache__/feishu_drive_tool.cpython-312.pyc +0 -0
  1549. package/tools/__pycache__/file_operations.cpython-312.pyc +0 -0
  1550. package/tools/__pycache__/file_state.cpython-312.pyc +0 -0
  1551. package/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
  1552. package/tools/__pycache__/homeassistant_tool.cpython-312.pyc +0 -0
  1553. package/tools/__pycache__/image_generation_tool.cpython-312.pyc +0 -0
  1554. package/tools/__pycache__/interrupt.cpython-312.pyc +0 -0
  1555. package/tools/__pycache__/kanban_tools.cpython-312.pyc +0 -0
  1556. package/tools/__pycache__/lazy_deps.cpython-312.pyc +0 -0
  1557. package/tools/__pycache__/managed_tool_gateway.cpython-312.pyc +0 -0
  1558. package/tools/__pycache__/mcp_tool.cpython-312.pyc +0 -0
  1559. package/tools/__pycache__/memory_tool.cpython-312.pyc +0 -0
  1560. package/tools/__pycache__/mixture_of_agents_tool.cpython-312.pyc +0 -0
  1561. package/tools/__pycache__/openrouter_client.cpython-312.pyc +0 -0
  1562. package/tools/__pycache__/process_registry.cpython-312.pyc +0 -0
  1563. package/tools/__pycache__/registry.cpython-312.pyc +0 -0
  1564. package/tools/__pycache__/schema_sanitizer.cpython-312.pyc +0 -0
  1565. package/tools/__pycache__/send_message_tool.cpython-312.pyc +0 -0
  1566. package/tools/__pycache__/session_search_tool.cpython-312.pyc +0 -0
  1567. package/tools/__pycache__/skill_manager_tool.cpython-312.pyc +0 -0
  1568. package/tools/__pycache__/skill_provenance.cpython-312.pyc +0 -0
  1569. package/tools/__pycache__/skill_usage.cpython-312.pyc +0 -0
  1570. package/tools/__pycache__/skills_guard.cpython-312.pyc +0 -0
  1571. package/tools/__pycache__/skills_sync.cpython-312.pyc +0 -0
  1572. package/tools/__pycache__/skills_tool.cpython-312.pyc +0 -0
  1573. package/tools/__pycache__/slash_confirm.cpython-312.pyc +0 -0
  1574. package/tools/__pycache__/terminal_tool.cpython-312.pyc +0 -0
  1575. package/tools/__pycache__/tirith_security.cpython-312.pyc +0 -0
  1576. package/tools/__pycache__/todo_tool.cpython-312.pyc +0 -0
  1577. package/tools/__pycache__/tool_backend_helpers.cpython-312.pyc +0 -0
  1578. package/tools/__pycache__/tool_result_storage.cpython-312.pyc +0 -0
  1579. package/tools/__pycache__/tts_tool.cpython-312.pyc +0 -0
  1580. package/tools/__pycache__/url_safety.cpython-312.pyc +0 -0
  1581. package/tools/__pycache__/video_generation_tool.cpython-312.pyc +0 -0
  1582. package/tools/__pycache__/vision_tools.cpython-312.pyc +0 -0
  1583. package/tools/__pycache__/voice_mode.cpython-312.pyc +0 -0
  1584. package/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
  1585. package/tools/__pycache__/website_policy.cpython-312.pyc +0 -0
  1586. package/tools/__pycache__/x_search_tool.cpython-312.pyc +0 -0
  1587. package/tools/__pycache__/xai_http.cpython-312.pyc +0 -0
  1588. package/tools/__pycache__/yuanbao_tools.cpython-312.pyc +0 -0
  1589. package/tools/ansi_strip.py +44 -0
  1590. package/tools/approval.py +1392 -0
  1591. package/tools/binary_extensions.py +42 -0
  1592. package/tools/browser_camofox.py +700 -0
  1593. package/tools/browser_camofox_state.py +48 -0
  1594. package/tools/browser_cdp_tool.py +569 -0
  1595. package/tools/browser_dialog_tool.py +148 -0
  1596. package/tools/browser_providers/__init__.py +10 -0
  1597. package/tools/browser_providers/__pycache__/__init__.cpython-312.pyc +0 -0
  1598. package/tools/browser_providers/__pycache__/base.cpython-312.pyc +0 -0
  1599. package/tools/browser_providers/__pycache__/browser_use.cpython-312.pyc +0 -0
  1600. package/tools/browser_providers/__pycache__/browserbase.cpython-312.pyc +0 -0
  1601. package/tools/browser_providers/__pycache__/firecrawl.cpython-312.pyc +0 -0
  1602. package/tools/browser_providers/base.py +59 -0
  1603. package/tools/browser_providers/browser_use.py +225 -0
  1604. package/tools/browser_providers/browserbase.py +222 -0
  1605. package/tools/browser_providers/firecrawl.py +112 -0
  1606. package/tools/browser_supervisor.py +1457 -0
  1607. package/tools/browser_tool.py +3676 -0
  1608. package/tools/budget_config.py +51 -0
  1609. package/tools/checkpoint_manager.py +1639 -0
  1610. package/tools/clarify_gateway.py +278 -0
  1611. package/tools/clarify_tool.py +141 -0
  1612. package/tools/code_execution_tool.py +1782 -0
  1613. package/tools/computer_use/__init__.py +43 -0
  1614. package/tools/computer_use/__pycache__/__init__.cpython-312.pyc +0 -0
  1615. package/tools/computer_use/__pycache__/backend.cpython-312.pyc +0 -0
  1616. package/tools/computer_use/__pycache__/schema.cpython-312.pyc +0 -0
  1617. package/tools/computer_use/__pycache__/tool.cpython-312.pyc +0 -0
  1618. package/tools/computer_use/backend.py +150 -0
  1619. package/tools/computer_use/cua_backend.py +682 -0
  1620. package/tools/computer_use/schema.py +191 -0
  1621. package/tools/computer_use/tool.py +521 -0
  1622. package/tools/computer_use_tool.py +39 -0
  1623. package/tools/credential_files.py +437 -0
  1624. package/tools/cronjob_tools.py +719 -0
  1625. package/tools/debug_helpers.py +106 -0
  1626. package/tools/delegate_tool.py +2797 -0
  1627. package/tools/discord_tool.py +959 -0
  1628. package/tools/env_passthrough.py +145 -0
  1629. package/tools/environments/__init__.py +14 -0
  1630. package/tools/environments/__pycache__/__init__.cpython-312.pyc +0 -0
  1631. package/tools/environments/__pycache__/base.cpython-312.pyc +0 -0
  1632. package/tools/environments/__pycache__/docker.cpython-312.pyc +0 -0
  1633. package/tools/environments/__pycache__/file_sync.cpython-312.pyc +0 -0
  1634. package/tools/environments/__pycache__/local.cpython-312.pyc +0 -0
  1635. package/tools/environments/__pycache__/managed_modal.cpython-312.pyc +0 -0
  1636. package/tools/environments/__pycache__/modal.cpython-312.pyc +0 -0
  1637. package/tools/environments/__pycache__/modal_utils.cpython-312.pyc +0 -0
  1638. package/tools/environments/__pycache__/singularity.cpython-312.pyc +0 -0
  1639. package/tools/environments/__pycache__/ssh.cpython-312.pyc +0 -0
  1640. package/tools/environments/base.py +844 -0
  1641. package/tools/environments/daytona.py +270 -0
  1642. package/tools/environments/docker.py +656 -0
  1643. package/tools/environments/file_sync.py +400 -0
  1644. package/tools/environments/local.py +658 -0
  1645. package/tools/environments/managed_modal.py +282 -0
  1646. package/tools/environments/modal.py +479 -0
  1647. package/tools/environments/modal_utils.py +199 -0
  1648. package/tools/environments/singularity.py +263 -0
  1649. package/tools/environments/ssh.py +295 -0
  1650. package/tools/environments/vercel_sandbox.py +655 -0
  1651. package/tools/feishu_doc_tool.py +138 -0
  1652. package/tools/feishu_drive_tool.py +431 -0
  1653. package/tools/file_operations.py +1825 -0
  1654. package/tools/file_state.py +332 -0
  1655. package/tools/file_tools.py +1172 -0
  1656. package/tools/fuzzy_match.py +703 -0
  1657. package/tools/homeassistant_tool.py +513 -0
  1658. package/tools/image_generation_tool.py +1098 -0
  1659. package/tools/interrupt.py +98 -0
  1660. package/tools/kanban_tools.py +1139 -0
  1661. package/tools/lazy_deps.py +608 -0
  1662. package/tools/managed_tool_gateway.py +168 -0
  1663. package/tools/mcp_oauth.py +633 -0
  1664. package/tools/mcp_oauth_manager.py +607 -0
  1665. package/tools/mcp_tool.py +3483 -0
  1666. package/tools/memory_tool.py +584 -0
  1667. package/tools/microsoft_graph_auth.py +245 -0
  1668. package/tools/microsoft_graph_client.py +408 -0
  1669. package/tools/mixture_of_agents_tool.py +542 -0
  1670. package/tools/neutts_samples/jo.txt +1 -0
  1671. package/tools/neutts_samples/jo.wav +0 -0
  1672. package/tools/neutts_synth.py +104 -0
  1673. package/tools/openrouter_client.py +33 -0
  1674. package/tools/osv_check.py +155 -0
  1675. package/tools/patch_parser.py +592 -0
  1676. package/tools/path_security.py +43 -0
  1677. package/tools/process_registry.py +1534 -0
  1678. package/tools/registry.py +589 -0
  1679. package/tools/schema_sanitizer.py +370 -0
  1680. package/tools/send_message_tool.py +1900 -0
  1681. package/tools/session_search_tool.py +613 -0
  1682. package/tools/skill_manager_tool.py +932 -0
  1683. package/tools/skill_provenance.py +78 -0
  1684. package/tools/skill_usage.py +610 -0
  1685. package/tools/skills_guard.py +932 -0
  1686. package/tools/skills_hub.py +3263 -0
  1687. package/tools/skills_sync.py +432 -0
  1688. package/tools/skills_tool.py +1569 -0
  1689. package/tools/slash_confirm.py +167 -0
  1690. package/tools/terminal_tool.py +2376 -0
  1691. package/tools/tirith_security.py +775 -0
  1692. package/tools/todo_tool.py +277 -0
  1693. package/tools/tool_backend_helpers.py +144 -0
  1694. package/tools/tool_output_limits.py +92 -0
  1695. package/tools/tool_result_storage.py +232 -0
  1696. package/tools/transcription_tools.py +936 -0
  1697. package/tools/tts_tool.py +2285 -0
  1698. package/tools/url_safety.py +330 -0
  1699. package/tools/video_generation_tool.py +561 -0
  1700. package/tools/vision_tools.py +1422 -0
  1701. package/tools/voice_mode.py +1019 -0
  1702. package/tools/web_tools.py +1551 -0
  1703. package/tools/website_policy.py +283 -0
  1704. package/tools/x_search_tool.py +424 -0
  1705. package/tools/xai_http.py +83 -0
  1706. package/tools/yuanbao_tools.py +736 -0
  1707. package/toolset_distributions.py +364 -0
  1708. package/toolsets.py +866 -0
  1709. package/trajectory_compressor.py +1509 -0
  1710. package/tui_gateway/__init__.py +0 -0
  1711. package/tui_gateway/entry.py +251 -0
  1712. package/tui_gateway/event_publisher.py +126 -0
  1713. package/tui_gateway/render.py +49 -0
  1714. package/tui_gateway/server.py +6623 -0
  1715. package/tui_gateway/slash_worker.py +76 -0
  1716. package/tui_gateway/transport.py +219 -0
  1717. package/tui_gateway/ws.py +178 -0
  1718. package/utils.py +361 -0
@@ -0,0 +1,3263 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Skills Hub — Source adapters and hub state management for the Hermes Skills Hub.
4
+
5
+ This is a library module (not an agent tool). It provides:
6
+ - GitHubAuth: Shared GitHub API authentication (PAT, gh CLI, GitHub App)
7
+ - SkillSource ABC: Interface for all skill registry adapters
8
+ - OptionalSkillSource: Official optional skills shipped with the repo (not activated by default)
9
+ - GitHubSource: Fetch skills from any GitHub repo via the Contents API
10
+ - HubLockFile: Track provenance of installed hub skills
11
+ - Hub state directory management (quarantine, audit log, taps, index cache)
12
+
13
+ Used by hermes_cli/skills_hub.py for CLI commands and the /skills slash command.
14
+ """
15
+
16
+ import hashlib
17
+ import json
18
+ import logging
19
+ import os
20
+ import re
21
+ import shutil
22
+ import subprocess
23
+ import time
24
+ from abc import ABC, abstractmethod
25
+ from dataclasses import dataclass, field
26
+ from datetime import datetime, timezone
27
+ from pathlib import Path, PurePosixPath
28
+ from calvyn_constants import get_hermes_home
29
+ from typing import Any, Dict, List, Optional, Tuple, Union
30
+ from urllib.parse import urljoin, urlparse, urlunparse
31
+
32
+ import httpx
33
+ import yaml
34
+
35
+ from tools.skills_guard import (
36
+ ScanResult, content_hash, TRUSTED_REPOS,
37
+ )
38
+ from tools.url_safety import is_safe_url
39
+ from tools.website_policy import check_website_access
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Paths
46
+ # ---------------------------------------------------------------------------
47
+
48
+ HERMES_HOME = get_hermes_home()
49
+ SKILLS_DIR = HERMES_HOME / "skills"
50
+ HUB_DIR = SKILLS_DIR / ".hub"
51
+ LOCK_FILE = HUB_DIR / "lock.json"
52
+ QUARANTINE_DIR = HUB_DIR / "quarantine"
53
+ AUDIT_LOG = HUB_DIR / "audit.log"
54
+ TAPS_FILE = HUB_DIR / "taps.json"
55
+ INDEX_CACHE_DIR = HUB_DIR / "index-cache"
56
+
57
+ # Cache duration for remote index fetches
58
+ INDEX_CACHE_TTL = 3600 # 1 hour
59
+
60
+ _REDIRECT_STATUS_CODES = {301, 302, 303, 307, 308}
61
+ _MAX_SKILL_FETCH_REDIRECTS = 5
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Data models
66
+ # ---------------------------------------------------------------------------
67
+
68
+ @dataclass
69
+ class SkillMeta:
70
+ """Minimal metadata returned by search results."""
71
+ name: str
72
+ description: str
73
+ source: str # "official", "github", "clawhub", "claude-marketplace", "lobehub"
74
+ identifier: str # source-specific ID (e.g. "openai/skills/skill-creator")
75
+ trust_level: str # "builtin" | "trusted" | "community"
76
+ repo: Optional[str] = None
77
+ path: Optional[str] = None
78
+ tags: List[str] = field(default_factory=list)
79
+ extra: Dict[str, Any] = field(default_factory=dict)
80
+
81
+
82
+ @dataclass
83
+ class SkillBundle:
84
+ """A downloaded skill ready for quarantine/scanning/installation."""
85
+ name: str
86
+ files: Dict[str, Union[str, bytes]] # relative_path -> file content
87
+ source: str
88
+ identifier: str
89
+ trust_level: str
90
+ metadata: Dict[str, Any] = field(default_factory=dict)
91
+
92
+
93
+ def _normalize_bundle_path(path_value: str, *, field_name: str, allow_nested: bool) -> str:
94
+ """Normalize and validate bundle-controlled paths before touching disk."""
95
+ if not isinstance(path_value, str):
96
+ raise ValueError(f"Unsafe {field_name}: expected a string")
97
+
98
+ raw = path_value.strip()
99
+ if not raw:
100
+ raise ValueError(f"Unsafe {field_name}: empty path")
101
+
102
+ normalized = raw.replace("\\", "/")
103
+ path = PurePosixPath(normalized)
104
+ parts = [part for part in path.parts if part not in {"", "."}]
105
+
106
+ if normalized.startswith("/") or path.is_absolute():
107
+ raise ValueError(f"Unsafe {field_name}: {path_value}")
108
+ if not parts or any(part == ".." for part in parts):
109
+ raise ValueError(f"Unsafe {field_name}: {path_value}")
110
+ if re.fullmatch(r"[A-Za-z]:", parts[0]):
111
+ raise ValueError(f"Unsafe {field_name}: {path_value}")
112
+ if not allow_nested and len(parts) != 1:
113
+ raise ValueError(f"Unsafe {field_name}: {path_value}")
114
+
115
+ return "/".join(parts)
116
+
117
+
118
+ def _validate_skill_name(name: str) -> str:
119
+ return _normalize_bundle_path(name, field_name="skill name", allow_nested=False)
120
+
121
+
122
+ def _validate_category_name(category: str) -> str:
123
+ return _normalize_bundle_path(category, field_name="category", allow_nested=False)
124
+
125
+
126
+ def _guarded_http_get(url: str, *, timeout: int = 20) -> Optional[httpx.Response]:
127
+ """Fetch a URL with SSRF and redirect-target validation."""
128
+ current_url = url
129
+
130
+ for _ in range(_MAX_SKILL_FETCH_REDIRECTS + 1):
131
+ if not is_safe_url(current_url):
132
+ logger.warning("Blocked unsafe Skills Hub URL: %s", current_url)
133
+ return None
134
+
135
+ blocked = check_website_access(current_url)
136
+ if blocked:
137
+ logger.info(
138
+ "Blocked Skills Hub fetch for %s by rule %s",
139
+ blocked["host"],
140
+ blocked["rule"],
141
+ )
142
+ return None
143
+
144
+ try:
145
+ resp = httpx.get(current_url, timeout=timeout, follow_redirects=False)
146
+ except httpx.HTTPError as exc:
147
+ logger.debug("Skills Hub fetch failed for %s: %s", current_url, exc)
148
+ return None
149
+
150
+ if resp.status_code in _REDIRECT_STATUS_CODES:
151
+ location = getattr(resp, "headers", {}).get("location")
152
+ if not location:
153
+ return None
154
+ current_url = urljoin(current_url, location)
155
+ continue
156
+
157
+ return resp
158
+
159
+ logger.warning("Skills Hub fetch exceeded redirect limit for %s", url)
160
+ return None
161
+
162
+
163
+ def _validate_bundle_rel_path(rel_path: str) -> str:
164
+ return _normalize_bundle_path(rel_path, field_name="bundle file path", allow_nested=True)
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # GitHub Authentication
169
+ # ---------------------------------------------------------------------------
170
+
171
+ class GitHubAuth:
172
+ """
173
+ GitHub API authentication. Tries methods in priority order:
174
+ 1. GITHUB_TOKEN / GH_TOKEN env var (PAT — the default)
175
+ 2. `gh auth token` subprocess (if gh CLI is installed)
176
+ 3. GitHub App JWT + installation token (if app credentials configured)
177
+ 4. Unauthenticated (60 req/hr, public repos only)
178
+ """
179
+
180
+ def __init__(self):
181
+ self._cached_token: Optional[str] = None
182
+ self._cached_method: Optional[str] = None
183
+ self._app_token_expiry: float = 0
184
+
185
+ def get_headers(self) -> Dict[str, str]:
186
+ """Return authorization headers for GitHub API requests."""
187
+ token = self._resolve_token()
188
+ headers = {"Accept": "application/vnd.github.v3+json"}
189
+ if token:
190
+ headers["Authorization"] = f"token {token}"
191
+ return headers
192
+
193
+ def is_authenticated(self) -> bool:
194
+ return self._resolve_token() is not None
195
+
196
+ def auth_method(self) -> str:
197
+ """Return which auth method is active: 'pat', 'gh-cli', 'github-app', or 'anonymous'."""
198
+ self._resolve_token()
199
+ return self._cached_method or "anonymous"
200
+
201
+ def _resolve_token(self) -> Optional[str]:
202
+ # Return cached token if still valid
203
+ if self._cached_token:
204
+ if self._cached_method != "github-app" or time.time() < self._app_token_expiry:
205
+ return self._cached_token
206
+
207
+ # 1. Environment variable
208
+ token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
209
+ if token:
210
+ self._cached_token = token
211
+ self._cached_method = "pat"
212
+ return token
213
+
214
+ # 2. gh CLI
215
+ token = self._try_gh_cli()
216
+ if token:
217
+ self._cached_token = token
218
+ self._cached_method = "gh-cli"
219
+ return token
220
+
221
+ # 3. GitHub App
222
+ token = self._try_github_app()
223
+ if token:
224
+ self._cached_token = token
225
+ self._cached_method = "github-app"
226
+ self._app_token_expiry = time.time() + 3500 # ~58 min (tokens last 1 hour)
227
+ return token
228
+
229
+ self._cached_method = "anonymous"
230
+ return None
231
+
232
+ def _try_gh_cli(self) -> Optional[str]:
233
+ """Try to get a token from the gh CLI."""
234
+ try:
235
+ result = subprocess.run(
236
+ ["gh", "auth", "token"],
237
+ capture_output=True, text=True, timeout=5,
238
+ )
239
+ if result.returncode == 0 and result.stdout.strip():
240
+ return result.stdout.strip()
241
+ except (FileNotFoundError, subprocess.TimeoutExpired) as e:
242
+ logger.debug("gh CLI token lookup failed: %s", e)
243
+ return None
244
+
245
+ def _try_github_app(self) -> Optional[str]:
246
+ """Try GitHub App JWT authentication if credentials are configured."""
247
+ app_id = os.environ.get("GITHUB_APP_ID")
248
+ key_path = os.environ.get("GITHUB_APP_PRIVATE_KEY_PATH")
249
+ installation_id = os.environ.get("GITHUB_APP_INSTALLATION_ID")
250
+
251
+ if not all([app_id, key_path, installation_id]):
252
+ return None
253
+
254
+ try:
255
+ import jwt # PyJWT
256
+ except ImportError:
257
+ logger.debug("PyJWT not installed, skipping GitHub App auth")
258
+ return None
259
+
260
+ try:
261
+ key_file = Path(key_path)
262
+ if not key_file.exists():
263
+ return None
264
+ private_key = key_file.read_text(encoding="utf-8")
265
+
266
+ now = int(time.time())
267
+ payload = {
268
+ "iat": now - 60,
269
+ "exp": now + (10 * 60),
270
+ "iss": app_id,
271
+ }
272
+ encoded_jwt = jwt.encode(payload, private_key, algorithm="RS256")
273
+
274
+ resp = httpx.post(
275
+ f"https://api.github.com/app/installations/{installation_id}/access_tokens",
276
+ headers={
277
+ "Authorization": f"Bearer {encoded_jwt}",
278
+ "Accept": "application/vnd.github.v3+json",
279
+ },
280
+ timeout=10,
281
+ )
282
+ if resp.status_code == 201:
283
+ return resp.json().get("token")
284
+ except Exception as e:
285
+ logger.debug(f"GitHub App auth failed: {e}")
286
+
287
+ return None
288
+
289
+
290
+ # ---------------------------------------------------------------------------
291
+ # Source adapter interface
292
+ # ---------------------------------------------------------------------------
293
+
294
+ class SkillSource(ABC):
295
+ """Abstract base for all skill registry adapters."""
296
+
297
+ @abstractmethod
298
+ def search(self, query: str, limit: int = 10) -> List[SkillMeta]:
299
+ """Search for skills matching a query string."""
300
+ ...
301
+
302
+ @abstractmethod
303
+ def fetch(self, identifier: str) -> Optional[SkillBundle]:
304
+ """Download a skill bundle by identifier."""
305
+ ...
306
+
307
+ @abstractmethod
308
+ def inspect(self, identifier: str) -> Optional[SkillMeta]:
309
+ """Fetch metadata for a skill without downloading all files."""
310
+ ...
311
+
312
+ @abstractmethod
313
+ def source_id(self) -> str:
314
+ """Unique identifier for this source (e.g. 'github', 'clawhub')."""
315
+ ...
316
+
317
+ def trust_level_for(self, identifier: str) -> str:
318
+ """Determine trust level for a skill from this source."""
319
+ return "community"
320
+
321
+
322
+ # ---------------------------------------------------------------------------
323
+ # GitHub source adapter
324
+ # ---------------------------------------------------------------------------
325
+
326
+ class GitHubSource(SkillSource):
327
+ """Fetch skills from GitHub repos via the Contents API."""
328
+
329
+ DEFAULT_TAPS = [
330
+ {"repo": "openai/skills", "path": "skills/"},
331
+ {"repo": "anthropics/skills", "path": "skills/"},
332
+ {"repo": "huggingface/skills", "path": "skills/"},
333
+ {"repo": "VoltAgent/awesome-agent-skills", "path": "skills/"},
334
+ {"repo": "garrytan/gstack", "path": ""},
335
+ {"repo": "MiniMax-AI/cli", "path": "skill/"},
336
+ ]
337
+
338
+ def __init__(self, auth: GitHubAuth, extra_taps: Optional[List[Dict]] = None):
339
+ self.auth = auth
340
+ self.taps = list(self.DEFAULT_TAPS)
341
+ if extra_taps:
342
+ self.taps.extend(extra_taps)
343
+ # Per-instance cache: repo -> (default_branch, tree_entries)
344
+ # Survives within a single search/install flow, avoiding redundant API calls.
345
+ self._tree_cache: Dict[str, Tuple[str, List[dict]]] = {}
346
+ # Set when GitHub returns 403 with rate limit exhausted
347
+ self._rate_limited: bool = False
348
+
349
+ def source_id(self) -> str:
350
+ return "github"
351
+
352
+ @property
353
+ def is_rate_limited(self) -> bool:
354
+ """Whether GitHub API rate limit was hit during operations."""
355
+ return self._rate_limited
356
+
357
+ def trust_level_for(self, identifier: str) -> str:
358
+ # identifier format: "owner/repo/path/to/skill"
359
+ parts = identifier.split("/", 2)
360
+ if len(parts) >= 2:
361
+ repo = f"{parts[0]}/{parts[1]}"
362
+ if repo in TRUSTED_REPOS:
363
+ return "trusted"
364
+ return "community"
365
+
366
+ def search(self, query: str, limit: int = 10) -> List[SkillMeta]:
367
+ """Search all taps for skills matching the query."""
368
+ results: List[SkillMeta] = []
369
+ query_lower = query.lower()
370
+
371
+ for tap in self.taps:
372
+ try:
373
+ skills = self._list_skills_in_repo(tap["repo"], tap.get("path", ""))
374
+ for skill in skills:
375
+ searchable = f"{skill.name} {skill.description} {' '.join(skill.tags)}".lower()
376
+ if query_lower in searchable:
377
+ results.append(skill)
378
+ except Exception as e:
379
+ logger.debug(f"Failed to search {tap['repo']}: {e}")
380
+ continue
381
+
382
+ # Deduplicate by name, preferring higher trust levels
383
+ _trust_rank = {"builtin": 2, "trusted": 1, "community": 0}
384
+ seen = {}
385
+ for r in results:
386
+ if r.name not in seen:
387
+ seen[r.name] = r
388
+ elif _trust_rank.get(r.trust_level, 0) > _trust_rank.get(seen[r.name].trust_level, 0):
389
+ seen[r.name] = r
390
+ results = list(seen.values())
391
+
392
+ return results[:limit]
393
+
394
+ def fetch(self, identifier: str) -> Optional[SkillBundle]:
395
+ """
396
+ Download a skill from GitHub.
397
+ identifier format: "owner/repo/path/to/skill-dir"
398
+ """
399
+ parts = identifier.split("/", 2)
400
+ if len(parts) < 3:
401
+ return None
402
+
403
+ repo = f"{parts[0]}/{parts[1]}"
404
+ skill_path = parts[2]
405
+
406
+ files = self._download_directory(repo, skill_path)
407
+ if not files or "SKILL.md" not in files:
408
+ return None
409
+
410
+ skill_name = skill_path.rstrip("/").split("/")[-1]
411
+ trust = self.trust_level_for(identifier)
412
+
413
+ return SkillBundle(
414
+ name=skill_name,
415
+ files=files,
416
+ source="github",
417
+ identifier=identifier,
418
+ trust_level=trust,
419
+ )
420
+
421
+ def inspect(self, identifier: str) -> Optional[SkillMeta]:
422
+ """Fetch just the SKILL.md metadata for preview."""
423
+ parts = identifier.split("/", 2)
424
+ if len(parts) < 3:
425
+ return None
426
+
427
+ repo = f"{parts[0]}/{parts[1]}"
428
+ skill_path = parts[2].rstrip("/")
429
+ skill_md_path = f"{skill_path}/SKILL.md"
430
+
431
+ content = self._fetch_file_content(repo, skill_md_path)
432
+ if not content:
433
+ return None
434
+
435
+ fm = self._parse_frontmatter_quick(content)
436
+ skill_name = fm.get("name", skill_path.split("/")[-1])
437
+ description = fm.get("description", "")
438
+
439
+ tags = []
440
+ metadata = fm.get("metadata", {})
441
+ if isinstance(metadata, dict):
442
+ hermes_meta = metadata.get("hermes", {})
443
+ if isinstance(hermes_meta, dict):
444
+ tags = hermes_meta.get("tags", [])
445
+ if not tags:
446
+ raw_tags = fm.get("tags", [])
447
+ tags = raw_tags if isinstance(raw_tags, list) else []
448
+
449
+ return SkillMeta(
450
+ name=skill_name,
451
+ description=str(description),
452
+ source="github",
453
+ identifier=identifier,
454
+ trust_level=self.trust_level_for(identifier),
455
+ repo=repo,
456
+ path=skill_path,
457
+ tags=[str(t) for t in tags],
458
+ )
459
+
460
+ # -- Internal helpers --
461
+
462
+ def _list_skills_in_repo(self, repo: str, path: str) -> List[SkillMeta]:
463
+ """List skill directories in a GitHub repo path, using cached index."""
464
+ cache_key = f"{repo}_{path}".replace("/", "_").replace(" ", "_")
465
+ cached = self._read_cache(cache_key)
466
+ if cached is not None:
467
+ return [SkillMeta(**s) for s in cached]
468
+
469
+ url = f"https://api.github.com/repos/{repo}/contents/{path.rstrip('/')}"
470
+ try:
471
+ resp = httpx.get(url, headers=self.auth.get_headers(), timeout=15, follow_redirects=True)
472
+ if resp.status_code != 200:
473
+ return []
474
+ except httpx.HTTPError:
475
+ return []
476
+
477
+ entries = resp.json()
478
+ if not isinstance(entries, list):
479
+ return []
480
+
481
+ skills: List[SkillMeta] = []
482
+ for entry in entries:
483
+ if entry.get("type") != "dir":
484
+ continue
485
+
486
+ dir_name = entry["name"]
487
+ if dir_name.startswith((".", "_")):
488
+ continue
489
+
490
+ prefix = path.rstrip("/")
491
+ skill_identifier = f"{repo}/{prefix}/{dir_name}" if prefix else f"{repo}/{dir_name}"
492
+ meta = self.inspect(skill_identifier)
493
+ if meta:
494
+ skills.append(meta)
495
+
496
+ # Cache the results
497
+ self._write_cache(cache_key, [self._meta_to_dict(s) for s in skills])
498
+ return skills
499
+
500
+ # -- Repo tree cache (avoids redundant API calls) --
501
+
502
+ def _get_repo_tree(self, repo: str) -> Optional[Tuple[str, List[dict]]]:
503
+ """Get cached or fresh repo tree.
504
+
505
+ Returns ``(default_branch, tree_entries)`` or ``None``.
506
+ A single install can call ``_download_directory_via_tree`` and
507
+ ``_find_skill_in_repo_tree`` multiple times for the same repo — this
508
+ cache eliminates the redundant ``GET /repos/{repo}`` +
509
+ ``GET /repos/{repo}/git/trees/{branch}`` round-trips (previously up to
510
+ 6 duplicated pairs per install, consuming ~12 of the 60/hr
511
+ unauthenticated rate limit for nothing).
512
+ """
513
+ if repo in self._tree_cache:
514
+ return self._tree_cache[repo]
515
+
516
+ headers = self.auth.get_headers()
517
+
518
+ # Resolve default branch
519
+ try:
520
+ resp = httpx.get(
521
+ f"https://api.github.com/repos/{repo}",
522
+ headers=headers, timeout=15, follow_redirects=True,
523
+ )
524
+ if resp.status_code != 200:
525
+ self._check_rate_limit_response(resp)
526
+ return None
527
+ default_branch = resp.json().get("default_branch", "main")
528
+ except (httpx.HTTPError, ValueError):
529
+ return None
530
+
531
+ # Fetch recursive tree
532
+ try:
533
+ resp = httpx.get(
534
+ f"https://api.github.com/repos/{repo}/git/trees/{default_branch}",
535
+ params={"recursive": "1"},
536
+ headers=headers, timeout=30, follow_redirects=True,
537
+ )
538
+ if resp.status_code != 200:
539
+ self._check_rate_limit_response(resp)
540
+ return None
541
+ tree_data = resp.json()
542
+ if tree_data.get("truncated"):
543
+ logger.debug("Git tree truncated for %s, cannot cache", repo)
544
+ return None
545
+ except (httpx.HTTPError, ValueError):
546
+ return None
547
+
548
+ entries = tree_data.get("tree", [])
549
+ self._tree_cache[repo] = (default_branch, entries)
550
+ return (default_branch, entries)
551
+
552
+ def _check_rate_limit_response(self, resp: "httpx.Response") -> None:
553
+ """Flag the instance as rate-limited when GitHub returns 403 + exhausted quota."""
554
+ if resp.status_code == 403:
555
+ remaining = resp.headers.get("X-RateLimit-Remaining", "")
556
+ if remaining == "0":
557
+ self._rate_limited = True
558
+ logger.warning(
559
+ "GitHub API rate limit exhausted (unauthenticated: 60 req/hr). "
560
+ "Set GITHUB_TOKEN or install the gh CLI to raise the limit to 5,000/hr."
561
+ )
562
+
563
+ def _download_directory(self, repo: str, path: str) -> Dict[str, str]:
564
+ """Recursively download all text files from a GitHub directory.
565
+
566
+ Uses the Git Trees API first (single call for the entire tree) to
567
+ avoid per-directory rate limiting that causes silent subdirectory
568
+ loss. Falls back to the recursive Contents API when the tree
569
+ endpoint is unavailable or the response is truncated.
570
+ """
571
+ files = self._download_directory_via_tree(repo, path)
572
+ if files is not None:
573
+ return files
574
+ logger.debug("Tree API unavailable for %s/%s, falling back to Contents API", repo, path)
575
+ return self._download_directory_recursive(repo, path)
576
+
577
+ def _download_directory_via_tree(self, repo: str, path: str) -> Optional[Dict[str, str]]:
578
+ """Download an entire directory using the Git Trees API (single request).
579
+
580
+ Returns:
581
+ dict of files if the path exists and has content,
582
+ empty dict ``{}`` if the tree is cached but the path doesn't exist
583
+ (prevents unnecessary Contents API fallback),
584
+ ``None`` if the tree couldn't be fetched (triggers Contents API fallback).
585
+ """
586
+ path = path.rstrip("/")
587
+
588
+ cached = self._get_repo_tree(repo)
589
+ if cached is None:
590
+ return None
591
+ _default_branch, tree_entries = cached
592
+
593
+ # Check if ANY entry lives under the target path
594
+ prefix = f"{path}/"
595
+ has_entries = any(
596
+ item.get("path", "").startswith(prefix) for item in tree_entries
597
+ )
598
+ if not has_entries:
599
+ # Path definitively doesn't exist in the repo — return empty
600
+ # instead of None to skip the Contents API fallback.
601
+ return {}
602
+
603
+ # Filter to blobs under our target path and fetch content
604
+ files: Dict[str, str] = {}
605
+ for item in tree_entries:
606
+ if item.get("type") != "blob":
607
+ continue
608
+ item_path = item.get("path", "")
609
+ if not item_path.startswith(prefix):
610
+ continue
611
+ rel_path = item_path[len(prefix):]
612
+ content = self._fetch_file_content(repo, item_path)
613
+ if content is not None:
614
+ files[rel_path] = content
615
+ else:
616
+ logger.debug("Skipped file (fetch failed): %s/%s", repo, item_path)
617
+
618
+ return files if files else None
619
+
620
+ def _download_directory_recursive(self, repo: str, path: str) -> Dict[str, str]:
621
+ """Recursively download via Contents API (fallback)."""
622
+ url = f"https://api.github.com/repos/{repo}/contents/{path.rstrip('/')}"
623
+ try:
624
+ resp = httpx.get(url, headers=self.auth.get_headers(), timeout=15, follow_redirects=True)
625
+ if resp.status_code != 200:
626
+ logger.debug("Contents API returned %d for %s/%s", resp.status_code, repo, path)
627
+ return {}
628
+ except httpx.HTTPError:
629
+ return {}
630
+
631
+ entries = resp.json()
632
+ if not isinstance(entries, list):
633
+ return {}
634
+
635
+ files: Dict[str, str] = {}
636
+ for entry in entries:
637
+ name = entry.get("name", "")
638
+ entry_type = entry.get("type", "")
639
+
640
+ if entry_type == "file":
641
+ content = self._fetch_file_content(repo, entry.get("path", ""))
642
+ if content is not None:
643
+ rel_path = name
644
+ files[rel_path] = content
645
+ elif entry_type == "dir":
646
+ sub_files = self._download_directory_recursive(repo, entry.get("path", ""))
647
+ if not sub_files:
648
+ logger.debug("Empty or failed subdirectory: %s/%s", repo, entry.get("path", ""))
649
+ for sub_name, sub_content in sub_files.items():
650
+ files[f"{name}/{sub_name}"] = sub_content
651
+
652
+ return files
653
+
654
+ def _find_skill_in_repo_tree(self, repo: str, skill_name: str) -> Optional[str]:
655
+ """Use the GitHub Trees API to find a skill directory anywhere in the repo.
656
+
657
+ Returns the full identifier (``repo/path/to/skill``) or ``None``.
658
+ This is a single API call regardless of repo depth, so it efficiently
659
+ handles deeply nested directory structures like
660
+ ``cli-tool/components/skills/development/<skill>/SKILL.md``.
661
+ """
662
+ cached = self._get_repo_tree(repo)
663
+ if cached is None:
664
+ return None
665
+ _default_branch, tree_entries = cached
666
+
667
+ # Look for SKILL.md files inside directories named <skill_name>
668
+ skill_md_suffix = f"/{skill_name}/SKILL.md"
669
+ for entry in tree_entries:
670
+ if entry.get("type") != "blob":
671
+ continue
672
+ path = entry.get("path", "")
673
+ if path.endswith(skill_md_suffix) or path == f"{skill_name}/SKILL.md":
674
+ # Strip /SKILL.md to get the skill directory path
675
+ skill_dir = path[: -len("/SKILL.md")]
676
+ return f"{repo}/{skill_dir}"
677
+
678
+ return None
679
+
680
+ def _fetch_file_content(self, repo: str, path: str) -> Optional[str]:
681
+ """Fetch a single file's content from GitHub."""
682
+ url = f"https://api.github.com/repos/{repo}/contents/{path}"
683
+ try:
684
+ resp = httpx.get(
685
+ url,
686
+ headers={**self.auth.get_headers(), "Accept": "application/vnd.github.v3.raw"},
687
+ timeout=15, follow_redirects=True,
688
+ )
689
+ if resp.status_code == 200:
690
+ return resp.text
691
+ self._check_rate_limit_response(resp)
692
+ except httpx.HTTPError as e:
693
+ logger.debug("GitHub contents API fetch failed: %s", e)
694
+ return None
695
+
696
+ def _read_cache(self, key: str) -> Optional[list]:
697
+ """Read cached index if not expired."""
698
+ cache_file = INDEX_CACHE_DIR / f"{key}.json"
699
+ if not cache_file.exists():
700
+ return None
701
+ try:
702
+ stat = cache_file.stat()
703
+ if time.time() - stat.st_mtime > INDEX_CACHE_TTL:
704
+ return None
705
+ return json.loads(cache_file.read_text())
706
+ except (OSError, json.JSONDecodeError):
707
+ return None
708
+
709
+ def _write_cache(self, key: str, data: list) -> None:
710
+ """Write index data to cache."""
711
+ INDEX_CACHE_DIR.mkdir(parents=True, exist_ok=True)
712
+ cache_file = INDEX_CACHE_DIR / f"{key}.json"
713
+ try:
714
+ cache_file.write_text(json.dumps(data, ensure_ascii=False))
715
+ except OSError as e:
716
+ logger.debug("Could not write cache: %s", e)
717
+
718
+ @staticmethod
719
+ def _meta_to_dict(meta: SkillMeta) -> dict:
720
+ return {
721
+ "name": meta.name,
722
+ "description": meta.description,
723
+ "source": meta.source,
724
+ "identifier": meta.identifier,
725
+ "trust_level": meta.trust_level,
726
+ "repo": meta.repo,
727
+ "path": meta.path,
728
+ "tags": meta.tags,
729
+ }
730
+
731
+ @staticmethod
732
+ def _parse_frontmatter_quick(content: str) -> dict:
733
+ """Parse YAML frontmatter from SKILL.md content."""
734
+ if not content.startswith("---"):
735
+ return {}
736
+ match = re.search(r'\n---\s*\n', content[3:])
737
+ if not match:
738
+ return {}
739
+ yaml_text = content[3:match.start() + 3]
740
+ try:
741
+ parsed = yaml.safe_load(yaml_text)
742
+ return parsed if isinstance(parsed, dict) else {}
743
+ except yaml.YAMLError:
744
+ return {}
745
+
746
+
747
+ # ---------------------------------------------------------------------------
748
+ # Well-known Agent Skills endpoint source adapter
749
+ # ---------------------------------------------------------------------------
750
+
751
+ class WellKnownSkillSource(SkillSource):
752
+ """Read skills from a domain exposing /.well-known/skills/index.json."""
753
+
754
+ BASE_PATH = "/.well-known/skills"
755
+
756
+ def source_id(self) -> str:
757
+ return "well-known"
758
+
759
+ def trust_level_for(self, identifier: str) -> str:
760
+ return "community"
761
+
762
+ def search(self, query: str, limit: int = 10) -> List[SkillMeta]:
763
+ index_url = self._query_to_index_url(query)
764
+ if not index_url:
765
+ return []
766
+
767
+ parsed = self._parse_index(index_url)
768
+ if not parsed:
769
+ return []
770
+
771
+ results: List[SkillMeta] = []
772
+ for entry in parsed["skills"][:limit]:
773
+ name = entry.get("name")
774
+ if not isinstance(name, str) or not name:
775
+ continue
776
+ description = entry.get("description", "")
777
+ files = entry.get("files", ["SKILL.md"])
778
+ results.append(SkillMeta(
779
+ name=name,
780
+ description=str(description),
781
+ source="well-known",
782
+ identifier=self._wrap_identifier(parsed["base_url"], name),
783
+ trust_level="community",
784
+ path=name,
785
+ extra={
786
+ "index_url": parsed["index_url"],
787
+ "base_url": parsed["base_url"],
788
+ "files": files if isinstance(files, list) else ["SKILL.md"],
789
+ },
790
+ ))
791
+ return results
792
+
793
+ def inspect(self, identifier: str) -> Optional[SkillMeta]:
794
+ parsed = self._parse_identifier(identifier)
795
+ if not parsed:
796
+ return None
797
+
798
+ entry = self._index_entry(parsed["index_url"], parsed["skill_name"])
799
+ if not entry:
800
+ return None
801
+
802
+ skill_md = self._fetch_text(f"{parsed['skill_url']}/SKILL.md")
803
+ if skill_md is None:
804
+ return None
805
+
806
+ fm = GitHubSource._parse_frontmatter_quick(skill_md)
807
+ description = str(fm.get("description") or entry.get("description") or "")
808
+ name = str(fm.get("name") or parsed["skill_name"])
809
+ return SkillMeta(
810
+ name=name,
811
+ description=description,
812
+ source="well-known",
813
+ identifier=self._wrap_identifier(parsed["base_url"], parsed["skill_name"]),
814
+ trust_level="community",
815
+ path=parsed["skill_name"],
816
+ extra={
817
+ "index_url": parsed["index_url"],
818
+ "base_url": parsed["base_url"],
819
+ "files": entry.get("files", ["SKILL.md"]),
820
+ "endpoint": parsed["skill_url"],
821
+ },
822
+ )
823
+
824
+ def fetch(self, identifier: str) -> Optional[SkillBundle]:
825
+ parsed = self._parse_identifier(identifier)
826
+ if not parsed:
827
+ return None
828
+
829
+ try:
830
+ skill_name = _validate_skill_name(parsed["skill_name"])
831
+ except ValueError:
832
+ logger.warning("Well-known skill identifier contained unsafe skill name: %s", identifier)
833
+ return None
834
+
835
+ entry = self._index_entry(parsed["index_url"], parsed["skill_name"])
836
+ if not entry:
837
+ return None
838
+
839
+ files = entry.get("files", ["SKILL.md"])
840
+ if not isinstance(files, list) or not files:
841
+ files = ["SKILL.md"]
842
+
843
+ downloaded: Dict[str, str] = {}
844
+ for rel_path in files:
845
+ if not isinstance(rel_path, str) or not rel_path:
846
+ continue
847
+ try:
848
+ safe_rel_path = _validate_bundle_rel_path(rel_path)
849
+ except ValueError:
850
+ logger.warning(
851
+ "Well-known skill %s advertised unsafe file path: %r",
852
+ identifier,
853
+ rel_path,
854
+ )
855
+ return None
856
+ text = self._fetch_text(f"{parsed['skill_url']}/{safe_rel_path}")
857
+ if text is None:
858
+ return None
859
+ downloaded[safe_rel_path] = text
860
+
861
+ if "SKILL.md" not in downloaded:
862
+ return None
863
+
864
+ return SkillBundle(
865
+ name=skill_name,
866
+ files=downloaded,
867
+ source="well-known",
868
+ identifier=self._wrap_identifier(parsed["base_url"], skill_name),
869
+ trust_level="community",
870
+ metadata={
871
+ "index_url": parsed["index_url"],
872
+ "base_url": parsed["base_url"],
873
+ "endpoint": parsed["skill_url"],
874
+ "files": files,
875
+ },
876
+ )
877
+
878
+ def _query_to_index_url(self, query: str) -> Optional[str]:
879
+ query = query.strip()
880
+ if not query.startswith(("http://", "https://")):
881
+ return None
882
+ if query.endswith("/index.json"):
883
+ return query
884
+ if f"{self.BASE_PATH}/" in query:
885
+ base_url = query.split(f"{self.BASE_PATH}/", 1)[0] + self.BASE_PATH
886
+ return f"{base_url}/index.json"
887
+ return query.rstrip("/") + f"{self.BASE_PATH}/index.json"
888
+
889
+ def _parse_identifier(self, identifier: str) -> Optional[dict]:
890
+ raw = identifier[len("well-known:"):] if identifier.startswith("well-known:") else identifier
891
+ if not raw.startswith(("http://", "https://")):
892
+ return None
893
+
894
+ parsed_url = urlparse(raw)
895
+ clean_url = urlunparse(parsed_url._replace(fragment=""))
896
+ fragment = parsed_url.fragment
897
+
898
+ if clean_url.endswith("/index.json"):
899
+ if not fragment:
900
+ return None
901
+ base_url = clean_url[:-len("/index.json")]
902
+ skill_name = fragment
903
+ skill_url = f"{base_url}/{skill_name}"
904
+ return {
905
+ "index_url": clean_url,
906
+ "base_url": base_url,
907
+ "skill_name": skill_name,
908
+ "skill_url": skill_url,
909
+ }
910
+
911
+ if clean_url.endswith("/SKILL.md"):
912
+ skill_url = clean_url[:-len("/SKILL.md")]
913
+ else:
914
+ skill_url = clean_url.rstrip("/")
915
+
916
+ if f"{self.BASE_PATH}/" not in skill_url:
917
+ return None
918
+
919
+ base_url, skill_name = skill_url.rsplit("/", 1)
920
+ return {
921
+ "index_url": f"{base_url}/index.json",
922
+ "base_url": base_url,
923
+ "skill_name": skill_name,
924
+ "skill_url": skill_url,
925
+ }
926
+
927
+ def _parse_index(self, index_url: str) -> Optional[dict]:
928
+ cache_key = f"well_known_index_{hashlib.md5(index_url.encode()).hexdigest()}"
929
+ cached = _read_index_cache(cache_key)
930
+ if isinstance(cached, dict) and isinstance(cached.get("skills"), list):
931
+ return cached
932
+
933
+ resp = _guarded_http_get(index_url, timeout=20)
934
+ if resp is None or resp.status_code != 200:
935
+ return None
936
+ try:
937
+ data = resp.json()
938
+ except json.JSONDecodeError:
939
+ return None
940
+
941
+ skills = data.get("skills", []) if isinstance(data, dict) else []
942
+ if not isinstance(skills, list):
943
+ return None
944
+
945
+ parsed = {
946
+ "index_url": index_url,
947
+ "base_url": index_url[:-len("/index.json")],
948
+ "skills": skills,
949
+ }
950
+ _write_index_cache(cache_key, parsed)
951
+ return parsed
952
+
953
+ def _index_entry(self, index_url: str, skill_name: str) -> Optional[dict]:
954
+ parsed = self._parse_index(index_url)
955
+ if not parsed:
956
+ return None
957
+ for entry in parsed["skills"]:
958
+ if isinstance(entry, dict) and entry.get("name") == skill_name:
959
+ return entry
960
+ return None
961
+
962
+ @staticmethod
963
+ def _fetch_text(url: str) -> Optional[str]:
964
+ resp = _guarded_http_get(url, timeout=20)
965
+ if resp is not None and resp.status_code == 200:
966
+ return resp.text
967
+ return None
968
+
969
+ @staticmethod
970
+ def _wrap_identifier(base_url: str, skill_name: str) -> str:
971
+ return f"well-known:{base_url.rstrip('/')}/{skill_name}"
972
+
973
+
974
+ # ---------------------------------------------------------------------------
975
+ # Direct URL source adapter
976
+ # ---------------------------------------------------------------------------
977
+
978
+ class UrlSource(SkillSource):
979
+ """Fetch a single-file SKILL.md skill directly from an HTTP(S) URL.
980
+
981
+ The identifier IS the URL (e.g. ``https://example.com/path/SKILL.md``).
982
+ Only single-file skills are supported — multi-file skills with
983
+ ``references/`` or ``scripts/`` subfolders need a manifest we can't
984
+ discover from a bare URL.
985
+
986
+ The skill name is read from the ``name:`` field in the SKILL.md YAML
987
+ frontmatter (with a URL-slug fallback). Trust level is always
988
+ ``community`` and the same security scan runs as for every other source.
989
+ """
990
+
991
+ def source_id(self) -> str:
992
+ return "url"
993
+
994
+ def trust_level_for(self, identifier: str) -> str:
995
+ return "community"
996
+
997
+ # Search is meaningless for a direct URL — skip (return empty).
998
+ def search(self, query: str, limit: int = 10) -> List[SkillMeta]:
999
+ return []
1000
+
1001
+ def _matches(self, identifier: str) -> bool:
1002
+ """Return True iff this source should handle ``identifier``.
1003
+
1004
+ We claim bare HTTP(S) URLs that end in ``.md`` (typically
1005
+ ``.../SKILL.md``). Wrapped identifiers (``github:``,
1006
+ ``well-known:``, etc.) and ``/.well-known/skills/`` URLs are
1007
+ left for their respective adapters.
1008
+ """
1009
+ if not isinstance(identifier, str):
1010
+ return False
1011
+ ident = identifier.strip()
1012
+ if not ident.lower().startswith(("http://", "https://")):
1013
+ return False
1014
+ # Don't steal well-known URLs.
1015
+ if "/.well-known/skills/" in ident or ident.rstrip("/").endswith("/index.json"):
1016
+ return False
1017
+ # Only claim URLs that look like a markdown file.
1018
+ try:
1019
+ path = urlparse(ident).path
1020
+ except ValueError:
1021
+ return False
1022
+ return path.lower().endswith(".md")
1023
+
1024
+ def inspect(self, identifier: str) -> Optional[SkillMeta]:
1025
+ if not self._matches(identifier):
1026
+ return None
1027
+ url = identifier.strip()
1028
+ text = self._fetch_text(url)
1029
+ if text is None:
1030
+ return None
1031
+ fm = GitHubSource._parse_frontmatter_quick(text)
1032
+ name = self._resolve_skill_name(fm, url)
1033
+ description = str(fm.get("description") or "")
1034
+ tags: List[str] = []
1035
+ metadata = fm.get("metadata", {})
1036
+ if isinstance(metadata, dict):
1037
+ hermes_meta = metadata.get("hermes", {})
1038
+ if isinstance(hermes_meta, dict):
1039
+ raw_tags = hermes_meta.get("tags", [])
1040
+ if isinstance(raw_tags, list):
1041
+ tags = [str(t) for t in raw_tags]
1042
+ return SkillMeta(
1043
+ name=name or "",
1044
+ description=description,
1045
+ source="url",
1046
+ identifier=url,
1047
+ trust_level="community",
1048
+ path=name or "",
1049
+ tags=tags,
1050
+ extra={"url": url, "awaiting_name": name is None},
1051
+ )
1052
+
1053
+ def fetch(self, identifier: str) -> Optional[SkillBundle]:
1054
+ if not self._matches(identifier):
1055
+ return None
1056
+ url = identifier.strip()
1057
+ text = self._fetch_text(url)
1058
+ if text is None:
1059
+ return None
1060
+
1061
+ fm = GitHubSource._parse_frontmatter_quick(text)
1062
+ name = self._resolve_skill_name(fm, url)
1063
+
1064
+ # When auto-resolution fails, return a bundle with an empty name and
1065
+ # ``awaiting_name=True`` in metadata. The install flow (``do_install``)
1066
+ # either prompts the user on a TTY or refuses with an actionable error
1067
+ # on non-interactive surfaces. Keep the expensive HTTP fetch's result
1068
+ # so the caller doesn't have to re-download after picking a name.
1069
+ skill_name = ""
1070
+ if name is not None:
1071
+ try:
1072
+ skill_name = _validate_skill_name(name)
1073
+ except ValueError:
1074
+ logger.warning("URL skill %s produced unsafe skill name: %r", url, name)
1075
+ return None
1076
+
1077
+ return SkillBundle(
1078
+ name=skill_name,
1079
+ files={"SKILL.md": text},
1080
+ source="url",
1081
+ identifier=url,
1082
+ trust_level="community",
1083
+ metadata={"url": url, "awaiting_name": not skill_name},
1084
+ )
1085
+
1086
+ @staticmethod
1087
+ def _fetch_text(url: str) -> Optional[str]:
1088
+ resp = _guarded_http_get(url, timeout=20)
1089
+ if resp is not None and resp.status_code == 200:
1090
+ return resp.text
1091
+ return None
1092
+
1093
+ # Skill names must look like identifiers: lowercase letters/digits with
1094
+ # optional hyphens/underscores. Blocks dangerous (``../evil``) AND useless
1095
+ # (``SKILL``, ``README``, empty) candidates before they hit the disk.
1096
+ _VALID_NAME_RE = re.compile(r"^[a-z][a-z0-9_-]*$")
1097
+
1098
+ @classmethod
1099
+ def _is_valid_skill_name(cls, name: Optional[str]) -> bool:
1100
+ if not isinstance(name, str):
1101
+ return False
1102
+ candidate = name.strip().lower()
1103
+ if not candidate or candidate in {"skill", "readme", "index", "unnamed-skill"}:
1104
+ return False
1105
+ return bool(cls._VALID_NAME_RE.match(candidate))
1106
+
1107
+ @classmethod
1108
+ def _resolve_skill_name(cls, fm: dict, url: str) -> Optional[str]:
1109
+ """Pick a skill name from frontmatter or URL.
1110
+
1111
+ Returns ``None`` when neither source produces a valid identifier;
1112
+ callers (CLI ``do_install``) then prompt the user or refuse. Preferring
1113
+ a clean failure over a useless auto-name like ``SKILL`` or ``unnamed-skill``.
1114
+ """
1115
+ # 1. Frontmatter ``name:`` is authoritative when present and valid.
1116
+ fm_name = fm.get("name") if isinstance(fm, dict) else None
1117
+ if isinstance(fm_name, str) and cls._is_valid_skill_name(fm_name):
1118
+ return fm_name.strip()
1119
+
1120
+ # 2. URL-slug heuristic: ``.../<name>/SKILL.md`` → ``<name>``;
1121
+ # ``.../<name>.md`` → ``<name>``. Validate each candidate.
1122
+ try:
1123
+ path = urlparse(url).path
1124
+ except ValueError:
1125
+ return None
1126
+ parts = [p for p in path.split("/") if p]
1127
+ if parts and parts[-1].lower() == "skill.md" and len(parts) >= 2:
1128
+ candidate = parts[-2]
1129
+ if cls._is_valid_skill_name(candidate):
1130
+ return candidate
1131
+ if parts:
1132
+ candidate = re.sub(r"\.md$", "", parts[-1], flags=re.IGNORECASE)
1133
+ if cls._is_valid_skill_name(candidate):
1134
+ return candidate
1135
+
1136
+ # Nothing usable — let the caller handle it.
1137
+ return None
1138
+
1139
+
1140
+ # ---------------------------------------------------------------------------
1141
+ # skills.sh source adapter
1142
+ # ---------------------------------------------------------------------------
1143
+
1144
+ class SkillsShSource(SkillSource):
1145
+ """Discover skills via skills.sh and fetch content from the underlying GitHub repo."""
1146
+
1147
+ BASE_URL = "https://skills.sh"
1148
+ SEARCH_URL = f"{BASE_URL}/api/search"
1149
+ _SKILL_LINK_RE = re.compile(r'href=["\']/(?P<id>(?!agents/|_next/|api/)[^"\'/]+/[^"\'/]+/[^"\'/]+)["\']')
1150
+ _INSTALL_CMD_RE = re.compile(
1151
+ r'npx\s+skills\s+add\s+(?P<repo>https?://github\.com/[^\s<]+|[^\s<]+)'
1152
+ r'(?:\s+--skill\s+(?P<skill>[^\s<]+))?',
1153
+ re.IGNORECASE,
1154
+ )
1155
+ _PAGE_H1_RE = re.compile(r'<h1[^>]*>(?P<title>.*?)</h1>', re.IGNORECASE | re.DOTALL)
1156
+ _PROSE_H1_RE = re.compile(
1157
+ r'<div[^>]*class=["\'][^"\']*prose[^"\']*["\'][^>]*>.*?<h1[^>]*>(?P<title>.*?)</h1>',
1158
+ re.IGNORECASE | re.DOTALL,
1159
+ )
1160
+ _PROSE_P_RE = re.compile(
1161
+ r'<div[^>]*class=["\'][^"\']*prose[^"\']*["\'][^>]*>.*?<p[^>]*>(?P<body>.*?)</p>',
1162
+ re.IGNORECASE | re.DOTALL,
1163
+ )
1164
+ _WEEKLY_INSTALLS_RE = re.compile(r'Weekly Installs.*?children\\":\\"(?P<count>[0-9.,Kk]+)\\"', re.DOTALL)
1165
+
1166
+ def __init__(self, auth: GitHubAuth):
1167
+ self.auth = auth
1168
+ self.github = GitHubSource(auth=auth)
1169
+
1170
+ def source_id(self) -> str:
1171
+ return "skills-sh"
1172
+
1173
+ def trust_level_for(self, identifier: str) -> str:
1174
+ return self.github.trust_level_for(self._normalize_identifier(identifier))
1175
+
1176
+ def search(self, query: str, limit: int = 10) -> List[SkillMeta]:
1177
+ if not query.strip():
1178
+ return self._featured_skills(limit)
1179
+
1180
+ cache_key = f"skills_sh_search_{hashlib.md5(f'{query}|{limit}'.encode()).hexdigest()}"
1181
+ cached = _read_index_cache(cache_key)
1182
+ if cached is not None:
1183
+ return [SkillMeta(**item) for item in cached][:limit]
1184
+
1185
+ try:
1186
+ resp = httpx.get(
1187
+ self.SEARCH_URL,
1188
+ params={"q": query, "limit": limit},
1189
+ timeout=20,
1190
+ )
1191
+ if resp.status_code != 200:
1192
+ return []
1193
+ data = resp.json()
1194
+ except (httpx.HTTPError, json.JSONDecodeError):
1195
+ return []
1196
+
1197
+ items = data.get("skills", []) if isinstance(data, dict) else []
1198
+ if not isinstance(items, list):
1199
+ return []
1200
+
1201
+ results: List[SkillMeta] = []
1202
+ for item in items[:limit]:
1203
+ meta = self._meta_from_search_item(item)
1204
+ if meta:
1205
+ results.append(meta)
1206
+
1207
+ _write_index_cache(cache_key, [_skill_meta_to_dict(item) for item in results])
1208
+ return results
1209
+
1210
+ def fetch(self, identifier: str) -> Optional[SkillBundle]:
1211
+ canonical = self._normalize_identifier(identifier)
1212
+ detail = self._fetch_detail_page(canonical)
1213
+ for candidate in self._candidate_identifiers(canonical):
1214
+ bundle = self.github.fetch(candidate)
1215
+ if bundle:
1216
+ bundle.source = "skills.sh"
1217
+ bundle.identifier = self._wrap_identifier(canonical)
1218
+ bundle.metadata.update(self._detail_to_metadata(canonical, detail))
1219
+ return bundle
1220
+
1221
+ resolved = self._discover_identifier(canonical, detail=detail)
1222
+ if resolved:
1223
+ bundle = self.github.fetch(resolved)
1224
+ if bundle:
1225
+ bundle.source = "skills.sh"
1226
+ bundle.identifier = self._wrap_identifier(canonical)
1227
+ bundle.metadata.update(self._detail_to_metadata(canonical, detail))
1228
+ return bundle
1229
+ return None
1230
+
1231
+ def inspect(self, identifier: str) -> Optional[SkillMeta]:
1232
+ canonical = self._normalize_identifier(identifier)
1233
+ detail = self._fetch_detail_page(canonical)
1234
+ meta = self._resolve_github_meta(canonical, detail=detail)
1235
+ if meta:
1236
+ return self._finalize_inspect_meta(meta, canonical, detail)
1237
+ return None
1238
+
1239
+ def _featured_skills(self, limit: int) -> List[SkillMeta]:
1240
+ cache_key = "skills_sh_featured"
1241
+ cached = _read_index_cache(cache_key)
1242
+ if cached is not None:
1243
+ return [SkillMeta(**item) for item in cached][:limit]
1244
+
1245
+ try:
1246
+ resp = httpx.get(self.BASE_URL, timeout=20)
1247
+ if resp.status_code != 200:
1248
+ return []
1249
+ except httpx.HTTPError:
1250
+ return []
1251
+
1252
+ seen: set[str] = set()
1253
+ results: List[SkillMeta] = []
1254
+ for match in self._SKILL_LINK_RE.finditer(resp.text):
1255
+ canonical = match.group("id")
1256
+ if canonical in seen:
1257
+ continue
1258
+ seen.add(canonical)
1259
+ parts = canonical.split("/", 2)
1260
+ if len(parts) < 3:
1261
+ continue
1262
+ repo = f"{parts[0]}/{parts[1]}"
1263
+ skill_path = parts[2]
1264
+ results.append(SkillMeta(
1265
+ name=skill_path.split("/")[-1],
1266
+ description=f"Featured on skills.sh from {repo}",
1267
+ source="skills.sh",
1268
+ identifier=self._wrap_identifier(canonical),
1269
+ trust_level=self.github.trust_level_for(canonical),
1270
+ repo=repo,
1271
+ path=skill_path,
1272
+ ))
1273
+ if len(results) >= limit:
1274
+ break
1275
+
1276
+ _write_index_cache(cache_key, [_skill_meta_to_dict(item) for item in results])
1277
+ return results
1278
+
1279
+ def _meta_from_search_item(self, item: dict) -> Optional[SkillMeta]:
1280
+ if not isinstance(item, dict):
1281
+ return None
1282
+
1283
+ canonical = item.get("id")
1284
+ repo = item.get("source")
1285
+ skill_path = item.get("skillId")
1286
+ if not isinstance(canonical, str) or canonical.count("/") < 2:
1287
+ if not (isinstance(repo, str) and isinstance(skill_path, str)):
1288
+ return None
1289
+ canonical = f"{repo}/{skill_path}"
1290
+
1291
+ parts = canonical.split("/", 2)
1292
+ if len(parts) < 3:
1293
+ return None
1294
+
1295
+ repo = f"{parts[0]}/{parts[1]}"
1296
+ skill_path = parts[2]
1297
+ installs = item.get("installs")
1298
+ installs_label = f" · {int(installs):,} installs" if isinstance(installs, int) else ""
1299
+
1300
+ return SkillMeta(
1301
+ name=str(item.get("name") or skill_path.split("/")[-1]),
1302
+ description=f"Indexed by skills.sh from {repo}{installs_label}",
1303
+ source="skills.sh",
1304
+ identifier=self._wrap_identifier(canonical),
1305
+ trust_level=self.github.trust_level_for(canonical),
1306
+ repo=repo,
1307
+ path=skill_path,
1308
+ extra={
1309
+ "installs": installs,
1310
+ "detail_url": f"{self.BASE_URL}/{canonical}",
1311
+ "repo_url": f"https://github.com/{repo}",
1312
+ },
1313
+ )
1314
+
1315
+ def _fetch_detail_page(self, identifier: str) -> Optional[dict]:
1316
+ cache_key = f"skills_sh_detail_{hashlib.md5(identifier.encode()).hexdigest()}"
1317
+ cached = _read_index_cache(cache_key)
1318
+ if isinstance(cached, dict):
1319
+ return cached
1320
+
1321
+ try:
1322
+ resp = httpx.get(f"{self.BASE_URL}/{identifier}", timeout=20)
1323
+ if resp.status_code != 200:
1324
+ return None
1325
+ except httpx.HTTPError:
1326
+ return None
1327
+
1328
+ detail = self._parse_detail_page(identifier, resp.text)
1329
+ if detail:
1330
+ _write_index_cache(cache_key, detail)
1331
+ return detail
1332
+
1333
+ def _parse_detail_page(self, identifier: str, html: str) -> Optional[dict]:
1334
+ parts = identifier.split("/", 2)
1335
+ if len(parts) < 3:
1336
+ return None
1337
+
1338
+ default_repo = f"{parts[0]}/{parts[1]}"
1339
+ skill_token = parts[2]
1340
+ repo = default_repo
1341
+ install_skill = skill_token
1342
+
1343
+ install_command = None
1344
+ install_match = self._INSTALL_CMD_RE.search(html)
1345
+ if install_match:
1346
+ install_command = install_match.group(0).strip()
1347
+ repo_value = (install_match.group("repo") or "").strip()
1348
+ install_skill = (install_match.group("skill") or install_skill).strip()
1349
+ repo = self._extract_repo_slug(repo_value) or repo
1350
+
1351
+ page_title = self._extract_first_match(self._PAGE_H1_RE, html)
1352
+ body_title = self._extract_first_match(self._PROSE_H1_RE, html)
1353
+ body_summary = self._extract_first_match(self._PROSE_P_RE, html)
1354
+ weekly_installs = self._extract_weekly_installs(html)
1355
+ security_audits = self._extract_security_audits(html, identifier)
1356
+
1357
+ return {
1358
+ "repo": repo,
1359
+ "install_skill": install_skill,
1360
+ "page_title": page_title,
1361
+ "body_title": body_title,
1362
+ "body_summary": body_summary,
1363
+ "weekly_installs": weekly_installs,
1364
+ "install_command": install_command,
1365
+ "repo_url": f"https://github.com/{repo}",
1366
+ "detail_url": f"{self.BASE_URL}/{identifier}",
1367
+ "security_audits": security_audits,
1368
+ }
1369
+
1370
+ def _discover_identifier(self, identifier: str, detail: Optional[dict] = None) -> Optional[str]:
1371
+ parts = identifier.split("/", 2)
1372
+ if len(parts) < 3:
1373
+ return None
1374
+
1375
+ default_repo = f"{parts[0]}/{parts[1]}"
1376
+ repo = detail.get("repo", default_repo) if isinstance(detail, dict) else default_repo
1377
+ skill_token=parts[2].split("/")[-1]
1378
+ tokens=[skill_token]
1379
+ if isinstance(detail, dict):
1380
+ tokens.extend([
1381
+ detail.get("install_skill", ""),
1382
+ detail.get("page_title", ""),
1383
+ detail.get("body_title", ""),
1384
+ ])
1385
+
1386
+ # Standard skill paths
1387
+ base_paths = ["skills/", ".agents/skills/", ".claude/skills/"]
1388
+
1389
+ for base_path in base_paths:
1390
+ try:
1391
+ skills = self.github._list_skills_in_repo(repo, base_path)
1392
+ except Exception:
1393
+ continue
1394
+ for meta in skills:
1395
+ if self._matches_skill_tokens(meta, tokens):
1396
+ return meta.identifier
1397
+
1398
+ # Prefer a single recursive tree lookup before brute-forcing every
1399
+ # top-level directory. This avoids large request bursts on categorized
1400
+ # repos like borghei/claude-skills.
1401
+ tree_result = self.github._find_skill_in_repo_tree(repo, skill_token)
1402
+ if tree_result:
1403
+ return tree_result
1404
+
1405
+ # Fallback: scan repo root for directories that might contain skills
1406
+ try:
1407
+ root_url = f"https://api.github.com/repos/{repo}/contents/"
1408
+ resp = httpx.get(root_url, headers=self.github.auth.get_headers(),
1409
+ timeout=15, follow_redirects=True)
1410
+ if resp.status_code == 200:
1411
+ entries = resp.json()
1412
+ if isinstance(entries, list):
1413
+ for entry in entries:
1414
+ if entry.get("type") != "dir":
1415
+ continue
1416
+ dir_name = entry["name"]
1417
+ if dir_name.startswith((".", "_")):
1418
+ continue
1419
+ if dir_name in {"skills", ".agents", ".claude"}:
1420
+ continue # already tried
1421
+ # Try direct: repo/dir/skill_token
1422
+ direct_id = f"{repo}/{dir_name}/{skill_token}"
1423
+ meta = self.github.inspect(direct_id)
1424
+ if meta:
1425
+ return meta.identifier
1426
+ # Try listing skills in this directory
1427
+ try:
1428
+ skills = self.github._list_skills_in_repo(repo, dir_name + "/")
1429
+ except Exception:
1430
+ continue
1431
+ for meta in skills:
1432
+ if self._matches_skill_tokens(meta, tokens):
1433
+ return meta.identifier
1434
+ except Exception:
1435
+ pass
1436
+
1437
+ return None
1438
+
1439
+ def _resolve_github_meta(self, identifier: str, detail: Optional[dict] = None) -> Optional[SkillMeta]:
1440
+ for candidate in self._candidate_identifiers(identifier):
1441
+ meta = self.github.inspect(candidate)
1442
+ if meta:
1443
+ return meta
1444
+
1445
+ resolved = self._discover_identifier(identifier, detail=detail)
1446
+ if resolved:
1447
+ return self.github.inspect(resolved)
1448
+ return None
1449
+
1450
+ def _finalize_inspect_meta(self, meta: SkillMeta, canonical: str, detail: Optional[dict]) -> SkillMeta:
1451
+ meta.source = "skills.sh"
1452
+ meta.identifier = self._wrap_identifier(canonical)
1453
+ meta.trust_level = self.trust_level_for(canonical)
1454
+ merged_extra = dict(meta.extra)
1455
+ merged_extra.update(self._detail_to_metadata(canonical, detail))
1456
+ meta.extra = merged_extra
1457
+
1458
+ if isinstance(detail, dict):
1459
+ body_summary = detail.get("body_summary")
1460
+ weekly_installs = detail.get("weekly_installs")
1461
+ if body_summary:
1462
+ meta.description = body_summary
1463
+ elif meta.description and weekly_installs:
1464
+ meta.description = f"{meta.description} · {weekly_installs} weekly installs on skills.sh"
1465
+ return meta
1466
+
1467
+ @classmethod
1468
+ def _matches_skill_tokens(cls, meta: SkillMeta, skill_tokens: List[str]) -> bool:
1469
+ candidates = set()
1470
+ candidates.update(cls._token_variants(meta.name))
1471
+ candidates.update(cls._token_variants(meta.path))
1472
+ candidates.update(cls._token_variants(meta.identifier.split("/", 2)[-1] if meta.identifier else None))
1473
+
1474
+ for token in skill_tokens:
1475
+ variants = cls._token_variants(token)
1476
+ if variants & candidates:
1477
+ return True
1478
+ return False
1479
+
1480
+ @staticmethod
1481
+ def _token_variants(value: Optional[str]) -> set[str]:
1482
+ if not value:
1483
+ return set()
1484
+
1485
+ plain = SkillsShSource._strip_html(str(value)).strip().strip("/").lower()
1486
+ if not plain:
1487
+ return set()
1488
+
1489
+ base = plain.split("/")[-1]
1490
+ sanitized = re.sub(r'[^a-z0-9/_-]+', '-', plain).strip('-')
1491
+ sanitized_base = sanitized.split("/")[-1] if sanitized else ""
1492
+ slash_tail = plain.split("/")[-1]
1493
+ slash_tail_clean = slash_tail.lstrip('@')
1494
+ slash_tail_clean = slash_tail_clean.split('/')[-1]
1495
+
1496
+ variants = {
1497
+ plain,
1498
+ plain.replace("_", "-"),
1499
+ plain.replace("/", "-"),
1500
+ base,
1501
+ base.replace("_", "-"),
1502
+ base.replace("/", "-"),
1503
+ sanitized,
1504
+ sanitized.replace("/", "-") if sanitized else "",
1505
+ sanitized_base,
1506
+ slash_tail_clean,
1507
+ slash_tail_clean.replace("_", "-"),
1508
+ }
1509
+ return {v for v in variants if v}
1510
+
1511
+ @staticmethod
1512
+ def _extract_repo_slug(repo_value: str) -> Optional[str]:
1513
+ repo_value = repo_value.strip()
1514
+ if repo_value.startswith("https://github.com/"):
1515
+ repo_value = repo_value[len("https://github.com/"):]
1516
+ repo_value = repo_value.strip("/")
1517
+ parts = repo_value.split("/")
1518
+ if len(parts) >= 2:
1519
+ return f"{parts[0]}/{parts[1]}"
1520
+ return None
1521
+
1522
+ @staticmethod
1523
+ def _extract_first_match(pattern: re.Pattern, text: str) -> Optional[str]:
1524
+ match = pattern.search(text)
1525
+ if not match:
1526
+ return None
1527
+ value = next((group for group in match.groups() if group), None)
1528
+ if value is None:
1529
+ return None
1530
+ return SkillsShSource._strip_html(value).strip() or None
1531
+
1532
+ def _detail_to_metadata(self, canonical: str, detail: Optional[dict]) -> Dict[str, Any]:
1533
+ parts = canonical.split("/", 2)
1534
+ repo = f"{parts[0]}/{parts[1]}" if len(parts) >= 2 else ""
1535
+ metadata = {
1536
+ "detail_url": f"{self.BASE_URL}/{canonical}",
1537
+ }
1538
+ if repo:
1539
+ metadata["repo_url"] = f"https://github.com/{repo}"
1540
+ if isinstance(detail, dict):
1541
+ for key in ("weekly_installs", "install_command", "repo_url", "detail_url", "security_audits"):
1542
+ value = detail.get(key)
1543
+ if value:
1544
+ metadata[key] = value
1545
+ return metadata
1546
+
1547
+ @staticmethod
1548
+ def _extract_weekly_installs(html: str) -> Optional[str]:
1549
+ match = SkillsShSource._WEEKLY_INSTALLS_RE.search(html)
1550
+ if not match:
1551
+ return None
1552
+ return match.group("count")
1553
+
1554
+ @staticmethod
1555
+ def _extract_security_audits(html: str, identifier: str) -> Dict[str, str]:
1556
+ audits: Dict[str, str] = {}
1557
+ for audit in ("agent-trust-hub", "socket", "snyk"):
1558
+ idx = html.find(f"/security/{audit}")
1559
+ if idx == -1:
1560
+ continue
1561
+ window = html[idx:idx + 500]
1562
+ match = re.search(r'(Pass|Warn|Fail)', window, re.IGNORECASE)
1563
+ if match:
1564
+ audits[audit] = match.group(1).title()
1565
+ return audits
1566
+
1567
+ @staticmethod
1568
+ def _strip_html(value: str) -> str:
1569
+ return re.sub(r'<[^>]+>', '', value)
1570
+
1571
+ @staticmethod
1572
+ def _normalize_identifier(identifier: str) -> str:
1573
+ prefix_aliases = (
1574
+ "skills-sh/",
1575
+ "skills.sh/",
1576
+ "skils-sh/",
1577
+ "skils.sh/",
1578
+ )
1579
+ for prefix in prefix_aliases:
1580
+ if identifier.startswith(prefix):
1581
+ return identifier[len(prefix):]
1582
+ return identifier
1583
+
1584
+ @staticmethod
1585
+ def _candidate_identifiers(identifier: str) -> List[str]:
1586
+ parts = identifier.split("/", 2)
1587
+ if len(parts) < 3:
1588
+ return [identifier]
1589
+
1590
+ repo = f"{parts[0]}/{parts[1]}"
1591
+ skill_path = parts[2].lstrip("/")
1592
+ candidates = [
1593
+ f"{repo}/{skill_path}",
1594
+ f"{repo}/skills/{skill_path}",
1595
+ f"{repo}/.agents/skills/{skill_path}",
1596
+ f"{repo}/.claude/skills/{skill_path}",
1597
+ ]
1598
+
1599
+ seen = set()
1600
+ deduped: List[str] = []
1601
+ for candidate in candidates:
1602
+ if candidate not in seen:
1603
+ seen.add(candidate)
1604
+ deduped.append(candidate)
1605
+ return deduped
1606
+
1607
+ @staticmethod
1608
+ def _wrap_identifier(identifier: str) -> str:
1609
+ return f"skills-sh/{identifier}"
1610
+
1611
+
1612
+ # ---------------------------------------------------------------------------
1613
+ # ClawHub source adapter
1614
+ # ---------------------------------------------------------------------------
1615
+
1616
+ class ClawHubSource(SkillSource):
1617
+ """
1618
+ Fetch skills from ClawHub (clawhub.ai) via their HTTP API.
1619
+ All skills are treated as community trust — ClawHavoc incident showed
1620
+ their vetting is insufficient (341 malicious skills found Feb 2026).
1621
+ """
1622
+
1623
+ BASE_URL = "https://clawhub.ai/api/v1"
1624
+
1625
+ def source_id(self) -> str:
1626
+ return "clawhub"
1627
+
1628
+ def trust_level_for(self, identifier: str) -> str:
1629
+ return "community"
1630
+
1631
+ @staticmethod
1632
+ def _normalize_tags(tags: Any) -> List[str]:
1633
+ if isinstance(tags, list):
1634
+ return [str(t) for t in tags]
1635
+ if isinstance(tags, dict):
1636
+ return [str(k) for k in tags if str(k) != "latest"]
1637
+ return []
1638
+
1639
+ @staticmethod
1640
+ def _coerce_skill_payload(data: Any) -> Optional[Dict[str, Any]]:
1641
+ if not isinstance(data, dict):
1642
+ return None
1643
+ nested = data.get("skill")
1644
+ if isinstance(nested, dict):
1645
+ merged = dict(nested)
1646
+ latest_version = data.get("latestVersion")
1647
+ if latest_version is not None and "latestVersion" not in merged:
1648
+ merged["latestVersion"] = latest_version
1649
+ return merged
1650
+ return data
1651
+
1652
+ @staticmethod
1653
+ def _query_terms(query: str) -> List[str]:
1654
+ return [term for term in re.split(r"[^a-z0-9]+", query.lower()) if term]
1655
+
1656
+ @classmethod
1657
+ def _search_score(cls, query: str, meta: SkillMeta) -> int:
1658
+ query_norm = query.strip().lower()
1659
+ if not query_norm:
1660
+ return 1
1661
+
1662
+ identifier = (meta.identifier or "").lower()
1663
+ name = (meta.name or "").lower()
1664
+ description = (meta.description or "").lower()
1665
+ normalized_identifier = " ".join(cls._query_terms(identifier))
1666
+ normalized_name = " ".join(cls._query_terms(name))
1667
+ query_terms = cls._query_terms(query_norm)
1668
+ identifier_terms = cls._query_terms(identifier)
1669
+ name_terms = cls._query_terms(name)
1670
+ score = 0
1671
+
1672
+ if query_norm == identifier:
1673
+ score += 140
1674
+ if query_norm == name:
1675
+ score += 130
1676
+ if normalized_identifier == query_norm:
1677
+ score += 125
1678
+ if normalized_name == query_norm:
1679
+ score += 120
1680
+ if normalized_identifier.startswith(query_norm):
1681
+ score += 95
1682
+ if normalized_name.startswith(query_norm):
1683
+ score += 90
1684
+ if query_terms and identifier_terms[: len(query_terms)] == query_terms:
1685
+ score += 70
1686
+ if query_terms and name_terms[: len(query_terms)] == query_terms:
1687
+ score += 65
1688
+ if query_norm in identifier:
1689
+ score += 40
1690
+ if query_norm in name:
1691
+ score += 35
1692
+ if query_norm in description:
1693
+ score += 10
1694
+
1695
+ for term in query_terms:
1696
+ if term in identifier_terms:
1697
+ score += 15
1698
+ if term in name_terms:
1699
+ score += 12
1700
+ if term in description:
1701
+ score += 3
1702
+
1703
+ return score
1704
+
1705
+ @staticmethod
1706
+ def _dedupe_results(results: List[SkillMeta]) -> List[SkillMeta]:
1707
+ seen: set[str] = set()
1708
+ deduped: List[SkillMeta] = []
1709
+ for result in results:
1710
+ key = (result.identifier or result.name).lower()
1711
+ if key in seen:
1712
+ continue
1713
+ seen.add(key)
1714
+ deduped.append(result)
1715
+ return deduped
1716
+
1717
+ def _exact_slug_meta(self, query: str) -> Optional[SkillMeta]:
1718
+ slug = query.strip().split("/")[-1]
1719
+ query_terms = self._query_terms(query)
1720
+ candidates: List[str] = []
1721
+
1722
+ if slug and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", slug):
1723
+ candidates.append(slug)
1724
+
1725
+ if query_terms:
1726
+ base_slug = "-".join(query_terms)
1727
+ if len(query_terms) >= 2:
1728
+ candidates.extend([
1729
+ f"{base_slug}-agent",
1730
+ f"{base_slug}-skill",
1731
+ f"{base_slug}-tool",
1732
+ f"{base_slug}-assistant",
1733
+ f"{base_slug}-playbook",
1734
+ base_slug,
1735
+ ])
1736
+ else:
1737
+ candidates.append(base_slug)
1738
+
1739
+ seen: set[str] = set()
1740
+ for candidate in candidates:
1741
+ if candidate in seen:
1742
+ continue
1743
+ seen.add(candidate)
1744
+ meta = self.inspect(candidate)
1745
+ if meta:
1746
+ return meta
1747
+
1748
+ return None
1749
+
1750
+ def _finalize_search_results(self, query: str, results: List[SkillMeta], limit: int) -> List[SkillMeta]:
1751
+ query_norm = query.strip()
1752
+ if not query_norm:
1753
+ return self._dedupe_results(results)[:limit]
1754
+
1755
+ filtered = [meta for meta in results if self._search_score(query_norm, meta) > 0]
1756
+ filtered.sort(
1757
+ key=lambda meta: (
1758
+ -self._search_score(query_norm, meta),
1759
+ meta.name.lower(),
1760
+ meta.identifier.lower(),
1761
+ )
1762
+ )
1763
+ filtered = self._dedupe_results(filtered)
1764
+
1765
+ exact = self._exact_slug_meta(query_norm)
1766
+ if exact:
1767
+ filtered = [meta for meta in filtered if self._search_score(query_norm, meta) >= 20]
1768
+ filtered = self._dedupe_results([exact] + filtered)
1769
+
1770
+ if filtered:
1771
+ return filtered[:limit]
1772
+
1773
+ if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._/-]*", query_norm):
1774
+ return []
1775
+
1776
+ return self._dedupe_results(results)[:limit]
1777
+
1778
+ def search(self, query: str, limit: int = 10) -> List[SkillMeta]:
1779
+ query = query.strip()
1780
+
1781
+ if query:
1782
+ query_terms = self._query_terms(query)
1783
+ if len(query_terms) >= 2:
1784
+ direct = self._exact_slug_meta(query)
1785
+ if direct:
1786
+ return [direct]
1787
+
1788
+ results = self._search_catalog(query, limit=limit)
1789
+ if results:
1790
+ return results
1791
+
1792
+ # Empty query or catalog fallback failure: use the lightweight listing API.
1793
+ cache_key = f"clawhub_search_listing_v1_{hashlib.md5(query.encode()).hexdigest()}_{limit}"
1794
+ cached = _read_index_cache(cache_key)
1795
+ if cached is not None:
1796
+ return self._finalize_search_results(
1797
+ query,
1798
+ [SkillMeta(**s) for s in cached],
1799
+ limit,
1800
+ )
1801
+
1802
+ try:
1803
+ resp = httpx.get(
1804
+ f"{self.BASE_URL}/skills",
1805
+ params={"search": query, "limit": limit},
1806
+ timeout=15,
1807
+ )
1808
+ if resp.status_code != 200:
1809
+ return []
1810
+ data = resp.json()
1811
+ except (httpx.HTTPError, json.JSONDecodeError):
1812
+ return []
1813
+
1814
+ skills_data = data.get("items", data) if isinstance(data, dict) else data
1815
+ if not isinstance(skills_data, list):
1816
+ return []
1817
+
1818
+ results = []
1819
+ for item in skills_data[:limit]:
1820
+ slug = item.get("slug")
1821
+ if not slug:
1822
+ continue
1823
+ display_name = item.get("displayName") or item.get("name") or slug
1824
+ summary = item.get("summary") or item.get("description") or ""
1825
+ tags = self._normalize_tags(item.get("tags", []))
1826
+ results.append(SkillMeta(
1827
+ name=display_name,
1828
+ description=summary,
1829
+ source="clawhub",
1830
+ identifier=slug,
1831
+ trust_level="community",
1832
+ tags=tags,
1833
+ ))
1834
+
1835
+ final_results = self._finalize_search_results(query, results, limit)
1836
+ _write_index_cache(cache_key, [_skill_meta_to_dict(s) for s in final_results])
1837
+ return final_results
1838
+
1839
+ def fetch(self, identifier: str) -> Optional[SkillBundle]:
1840
+ slug = identifier.split("/")[-1]
1841
+
1842
+ skill_data = self._get_json(f"{self.BASE_URL}/skills/{slug}")
1843
+ if not isinstance(skill_data, dict):
1844
+ return None
1845
+
1846
+ latest_version = self._resolve_latest_version(slug, skill_data)
1847
+ if not latest_version:
1848
+ logger.warning("ClawHub fetch failed for %s: could not resolve latest version", slug)
1849
+ return None
1850
+
1851
+ # Primary method: download the skill as a ZIP bundle from /download
1852
+ files = self._download_zip(slug, latest_version)
1853
+
1854
+ # Fallback: try the version metadata endpoint for inline/raw content
1855
+ if "SKILL.md" not in files:
1856
+ version_data = self._get_json(f"{self.BASE_URL}/skills/{slug}/versions/{latest_version}")
1857
+ if isinstance(version_data, dict):
1858
+ # Files may be nested under version_data["version"]["files"]
1859
+ files = self._extract_files(version_data) or files
1860
+ if "SKILL.md" not in files:
1861
+ nested = version_data.get("version", {})
1862
+ if isinstance(nested, dict):
1863
+ files = self._extract_files(nested) or files
1864
+
1865
+ if "SKILL.md" not in files:
1866
+ logger.warning(
1867
+ "ClawHub fetch for %s resolved version %s but could not retrieve file content",
1868
+ slug,
1869
+ latest_version,
1870
+ )
1871
+ return None
1872
+
1873
+ return SkillBundle(
1874
+ name=slug,
1875
+ files=files,
1876
+ source="clawhub",
1877
+ identifier=slug,
1878
+ trust_level="community",
1879
+ )
1880
+
1881
+ def inspect(self, identifier: str) -> Optional[SkillMeta]:
1882
+ slug = identifier.split("/")[-1]
1883
+ data = self._coerce_skill_payload(self._get_json(f"{self.BASE_URL}/skills/{slug}"))
1884
+ if not isinstance(data, dict):
1885
+ return None
1886
+
1887
+ tags = self._normalize_tags(data.get("tags", []))
1888
+
1889
+ return SkillMeta(
1890
+ name=data.get("displayName") or data.get("name") or data.get("slug") or slug,
1891
+ description=data.get("summary") or data.get("description") or "",
1892
+ source="clawhub",
1893
+ identifier=data.get("slug") or slug,
1894
+ trust_level="community",
1895
+ tags=tags,
1896
+ )
1897
+
1898
+ def _search_catalog(self, query: str, limit: int = 10) -> List[SkillMeta]:
1899
+ cache_key = f"clawhub_search_catalog_v1_{hashlib.md5(f'{query}|{limit}'.encode()).hexdigest()}"
1900
+ cached = _read_index_cache(cache_key)
1901
+ if cached is not None:
1902
+ return [SkillMeta(**s) for s in cached][:limit]
1903
+
1904
+ catalog = self._load_catalog_index()
1905
+ if not catalog:
1906
+ return []
1907
+
1908
+ results = self._finalize_search_results(query, catalog, limit)
1909
+ _write_index_cache(cache_key, [_skill_meta_to_dict(s) for s in results])
1910
+ return results
1911
+
1912
+ def _load_catalog_index(self) -> List[SkillMeta]:
1913
+ cache_key = "clawhub_catalog_v1"
1914
+ cached = _read_index_cache(cache_key)
1915
+ if cached is not None:
1916
+ return [SkillMeta(**s) for s in cached]
1917
+
1918
+ cursor: Optional[str] = None
1919
+ results: List[SkillMeta] = []
1920
+ seen: set[str] = set()
1921
+ max_pages = 50
1922
+
1923
+ for _ in range(max_pages):
1924
+ params: Dict[str, Any] = {"limit": 200}
1925
+ if cursor:
1926
+ params["cursor"] = cursor
1927
+
1928
+ try:
1929
+ resp = httpx.get(f"{self.BASE_URL}/skills", params=params, timeout=30)
1930
+ if resp.status_code != 200:
1931
+ break
1932
+ data = resp.json()
1933
+ except (httpx.HTTPError, json.JSONDecodeError):
1934
+ break
1935
+
1936
+ items = data.get("items", []) if isinstance(data, dict) else []
1937
+ if not isinstance(items, list) or not items:
1938
+ break
1939
+
1940
+ for item in items:
1941
+ slug = item.get("slug")
1942
+ if not isinstance(slug, str) or not slug or slug in seen:
1943
+ continue
1944
+ seen.add(slug)
1945
+ display_name = item.get("displayName") or item.get("name") or slug
1946
+ summary = item.get("summary") or item.get("description") or ""
1947
+ tags = self._normalize_tags(item.get("tags", []))
1948
+ results.append(SkillMeta(
1949
+ name=display_name,
1950
+ description=summary,
1951
+ source="clawhub",
1952
+ identifier=slug,
1953
+ trust_level="community",
1954
+ tags=tags,
1955
+ ))
1956
+
1957
+ cursor = data.get("nextCursor") if isinstance(data, dict) else None
1958
+ if not isinstance(cursor, str) or not cursor:
1959
+ break
1960
+
1961
+ _write_index_cache(cache_key, [_skill_meta_to_dict(s) for s in results])
1962
+ return results
1963
+
1964
+ def _get_json(self, url: str, timeout: int = 20) -> Optional[Any]:
1965
+ try:
1966
+ resp = httpx.get(url, timeout=timeout)
1967
+ if resp.status_code != 200:
1968
+ return None
1969
+ return resp.json()
1970
+ except (httpx.HTTPError, json.JSONDecodeError):
1971
+ return None
1972
+
1973
+ def _resolve_latest_version(self, slug: str, skill_data: Dict[str, Any]) -> Optional[str]:
1974
+ latest = skill_data.get("latestVersion")
1975
+ if isinstance(latest, dict):
1976
+ version = latest.get("version")
1977
+ if isinstance(version, str) and version:
1978
+ return version
1979
+
1980
+ tags = skill_data.get("tags")
1981
+ if isinstance(tags, dict):
1982
+ latest_tag = tags.get("latest")
1983
+ if isinstance(latest_tag, str) and latest_tag:
1984
+ return latest_tag
1985
+
1986
+ versions_data = self._get_json(f"{self.BASE_URL}/skills/{slug}/versions")
1987
+ if isinstance(versions_data, list) and versions_data:
1988
+ first = versions_data[0]
1989
+ if isinstance(first, dict):
1990
+ version = first.get("version")
1991
+ if isinstance(version, str) and version:
1992
+ return version
1993
+ return None
1994
+
1995
+ def _extract_files(self, version_data: Dict[str, Any]) -> Dict[str, str]:
1996
+ files: Dict[str, str] = {}
1997
+ file_list = version_data.get("files")
1998
+
1999
+ if isinstance(file_list, dict):
2000
+ return {k: v for k, v in file_list.items() if isinstance(v, str)}
2001
+
2002
+ if not isinstance(file_list, list):
2003
+ return files
2004
+
2005
+ for file_meta in file_list:
2006
+ if not isinstance(file_meta, dict):
2007
+ continue
2008
+
2009
+ fname = file_meta.get("path") or file_meta.get("name")
2010
+ if not fname or not isinstance(fname, str):
2011
+ continue
2012
+
2013
+ inline_content = file_meta.get("content")
2014
+ if isinstance(inline_content, str):
2015
+ files[fname] = inline_content
2016
+ continue
2017
+
2018
+ raw_url = file_meta.get("rawUrl") or file_meta.get("downloadUrl") or file_meta.get("url")
2019
+ if isinstance(raw_url, str) and raw_url.startswith("http"):
2020
+ content = self._fetch_text(raw_url)
2021
+ if content is not None:
2022
+ files[fname] = content
2023
+
2024
+ return files
2025
+
2026
+ def _download_zip(self, slug: str, version: str) -> Dict[str, str]:
2027
+ """Download skill as a ZIP bundle from the /download endpoint and extract text files."""
2028
+ import io
2029
+ import zipfile
2030
+
2031
+ files: Dict[str, str] = {}
2032
+ max_retries = 3
2033
+ for attempt in range(max_retries):
2034
+ try:
2035
+ resp = httpx.get(
2036
+ f"{self.BASE_URL}/download",
2037
+ params={"slug": slug, "version": version},
2038
+ timeout=30,
2039
+ follow_redirects=True,
2040
+ )
2041
+ if resp.status_code == 429:
2042
+ try:
2043
+ retry_after = int(resp.headers.get("retry-after", "5"))
2044
+ except (ValueError, TypeError):
2045
+ retry_after = 5
2046
+ retry_after = min(retry_after, 15) # Cap wait time
2047
+ logger.debug(
2048
+ "ClawHub download rate-limited for %s, retrying in %ds (attempt %d/%d)",
2049
+ slug, retry_after, attempt + 1, max_retries,
2050
+ )
2051
+ time.sleep(retry_after)
2052
+ continue
2053
+ if resp.status_code != 200:
2054
+ logger.debug("ClawHub ZIP download for %s v%s returned %s", slug, version, resp.status_code)
2055
+ return files
2056
+
2057
+ with zipfile.ZipFile(io.BytesIO(resp.content)) as zf:
2058
+ for info in zf.infolist():
2059
+ if info.is_dir():
2060
+ continue
2061
+ try:
2062
+ name = _validate_bundle_rel_path(info.filename)
2063
+ except ValueError:
2064
+ logger.debug("Skipping unsafe ZIP member path: %s", info.filename)
2065
+ continue
2066
+ # Only extract text-sized files (skip large binaries)
2067
+ if info.file_size > 500_000:
2068
+ logger.debug("Skipping large file in ZIP: %s (%d bytes)", name, info.file_size)
2069
+ continue
2070
+ try:
2071
+ raw = zf.read(info.filename)
2072
+ files[name] = raw.decode("utf-8")
2073
+ except (UnicodeDecodeError, KeyError):
2074
+ logger.debug("Skipping non-text file in ZIP: %s", name)
2075
+ continue
2076
+
2077
+ return files
2078
+
2079
+ except zipfile.BadZipFile:
2080
+ logger.warning("ClawHub returned invalid ZIP for %s v%s", slug, version)
2081
+ return files
2082
+ except httpx.HTTPError as exc:
2083
+ logger.debug("ClawHub ZIP download failed for %s v%s: %s", slug, version, exc)
2084
+ return files
2085
+
2086
+ logger.debug("ClawHub ZIP download exhausted retries for %s v%s", slug, version)
2087
+ return files
2088
+
2089
+ def _fetch_text(self, url: str) -> Optional[str]:
2090
+ resp = _guarded_http_get(url, timeout=20)
2091
+ if resp is not None and resp.status_code == 200:
2092
+ return resp.text
2093
+ return None
2094
+
2095
+
2096
+ # ---------------------------------------------------------------------------
2097
+ # Claude Code marketplace source adapter
2098
+ # ---------------------------------------------------------------------------
2099
+
2100
+ class ClaudeMarketplaceSource(SkillSource):
2101
+ """
2102
+ Discover skills from Claude Code marketplace repos.
2103
+ Marketplace repos contain .claude-plugin/marketplace.json with plugin listings.
2104
+ """
2105
+
2106
+ KNOWN_MARKETPLACES = [
2107
+ "anthropics/skills",
2108
+ "aiskillstore/marketplace",
2109
+ ]
2110
+
2111
+ def __init__(self, auth: GitHubAuth):
2112
+ self.auth = auth
2113
+
2114
+ def source_id(self) -> str:
2115
+ return "claude-marketplace"
2116
+
2117
+ def trust_level_for(self, identifier: str) -> str:
2118
+ parts = identifier.split("/", 2)
2119
+ if len(parts) >= 2:
2120
+ repo = f"{parts[0]}/{parts[1]}"
2121
+ if repo in TRUSTED_REPOS:
2122
+ return "trusted"
2123
+ return "community"
2124
+
2125
+ def search(self, query: str, limit: int = 10) -> List[SkillMeta]:
2126
+ results: List[SkillMeta] = []
2127
+ query_lower = query.lower()
2128
+
2129
+ for marketplace_repo in self.KNOWN_MARKETPLACES:
2130
+ plugins = self._fetch_marketplace_index(marketplace_repo)
2131
+ for plugin in plugins:
2132
+ searchable = f"{plugin.get('name', '')} {plugin.get('description', '')}".lower()
2133
+ if query_lower in searchable:
2134
+ source_path = plugin.get("source", "")
2135
+ if source_path.startswith("./"):
2136
+ identifier = f"{marketplace_repo}/{source_path[2:]}"
2137
+ elif "/" in source_path:
2138
+ identifier = source_path
2139
+ else:
2140
+ identifier = f"{marketplace_repo}/{source_path}"
2141
+
2142
+ results.append(SkillMeta(
2143
+ name=plugin.get("name", ""),
2144
+ description=plugin.get("description", ""),
2145
+ source="claude-marketplace",
2146
+ identifier=identifier,
2147
+ trust_level=self.trust_level_for(identifier),
2148
+ repo=marketplace_repo,
2149
+ ))
2150
+
2151
+ return results[:limit]
2152
+
2153
+ def fetch(self, identifier: str) -> Optional[SkillBundle]:
2154
+ # Delegate to GitHub Contents API since marketplace skills live in GitHub repos
2155
+ gh = GitHubSource(auth=self.auth)
2156
+ bundle = gh.fetch(identifier)
2157
+ if bundle:
2158
+ bundle.source = "claude-marketplace"
2159
+ return bundle
2160
+
2161
+ def inspect(self, identifier: str) -> Optional[SkillMeta]:
2162
+ gh = GitHubSource(auth=self.auth)
2163
+ meta = gh.inspect(identifier)
2164
+ if meta:
2165
+ meta.source = "claude-marketplace"
2166
+ meta.trust_level = self.trust_level_for(identifier)
2167
+ return meta
2168
+
2169
+ def _fetch_marketplace_index(self, repo: str) -> List[dict]:
2170
+ """Fetch and parse .claude-plugin/marketplace.json from a repo."""
2171
+ cache_key = f"claude_marketplace_{repo.replace('/', '_')}"
2172
+ cached = _read_index_cache(cache_key)
2173
+ if cached is not None:
2174
+ return cached
2175
+
2176
+ url = f"https://api.github.com/repos/{repo}/contents/.claude-plugin/marketplace.json"
2177
+ try:
2178
+ resp = httpx.get(
2179
+ url,
2180
+ headers={**self.auth.get_headers(), "Accept": "application/vnd.github.v3.raw"},
2181
+ timeout=15,
2182
+ )
2183
+ if resp.status_code != 200:
2184
+ return []
2185
+ data = json.loads(resp.text)
2186
+ except (httpx.HTTPError, json.JSONDecodeError):
2187
+ return []
2188
+
2189
+ plugins = data.get("plugins", [])
2190
+ _write_index_cache(cache_key, plugins)
2191
+ return plugins
2192
+
2193
+
2194
+ # ---------------------------------------------------------------------------
2195
+ # LobeHub source adapter
2196
+ # ---------------------------------------------------------------------------
2197
+
2198
+ class LobeHubSource(SkillSource):
2199
+ """
2200
+ Fetch skills from LobeHub's agent marketplace (14,500+ agents).
2201
+ LobeHub agents are system prompt templates — we convert them to SKILL.md on fetch.
2202
+ Data lives in GitHub: lobehub/lobe-chat-agents.
2203
+ """
2204
+
2205
+ INDEX_URL = "https://chat-agents.lobehub.com/index.json"
2206
+
2207
+ def source_id(self) -> str:
2208
+ return "lobehub"
2209
+
2210
+ def trust_level_for(self, identifier: str) -> str:
2211
+ return "community"
2212
+
2213
+ def search(self, query: str, limit: int = 10) -> List[SkillMeta]:
2214
+ index = self._fetch_index()
2215
+ if not index:
2216
+ return []
2217
+
2218
+ query_lower = query.lower()
2219
+ results: List[SkillMeta] = []
2220
+
2221
+ agents = index.get("agents", index) if isinstance(index, dict) else index
2222
+ if not isinstance(agents, list):
2223
+ return []
2224
+
2225
+ for agent in agents:
2226
+ meta = agent.get("meta", agent)
2227
+ title = meta.get("title", agent.get("identifier", ""))
2228
+ desc = meta.get("description", "")
2229
+ tags = meta.get("tags", [])
2230
+
2231
+ searchable = f"{title} {desc} {' '.join(tags) if isinstance(tags, list) else ''}".lower()
2232
+ if query_lower in searchable:
2233
+ identifier = agent.get("identifier", title.lower().replace(" ", "-"))
2234
+ results.append(SkillMeta(
2235
+ name=identifier,
2236
+ description=desc[:200],
2237
+ source="lobehub",
2238
+ identifier=f"lobehub/{identifier}",
2239
+ trust_level="community",
2240
+ tags=tags if isinstance(tags, list) else [],
2241
+ ))
2242
+
2243
+ if len(results) >= limit:
2244
+ break
2245
+
2246
+ return results
2247
+
2248
+ def fetch(self, identifier: str) -> Optional[SkillBundle]:
2249
+ # Strip "lobehub/" prefix if present
2250
+ agent_id = identifier.split("/", 1)[-1] if identifier.startswith("lobehub/") else identifier
2251
+
2252
+ agent_data = self._fetch_agent(agent_id)
2253
+ if not agent_data:
2254
+ return None
2255
+
2256
+ skill_md = self._convert_to_skill_md(agent_data)
2257
+ return SkillBundle(
2258
+ name=agent_id,
2259
+ files={"SKILL.md": skill_md},
2260
+ source="lobehub",
2261
+ identifier=f"lobehub/{agent_id}",
2262
+ trust_level="community",
2263
+ )
2264
+
2265
+ def inspect(self, identifier: str) -> Optional[SkillMeta]:
2266
+ agent_id = identifier.split("/", 1)[-1] if identifier.startswith("lobehub/") else identifier
2267
+ index = self._fetch_index()
2268
+ if not index:
2269
+ return None
2270
+
2271
+ agents = index.get("agents", index) if isinstance(index, dict) else index
2272
+ if not isinstance(agents, list):
2273
+ return None
2274
+
2275
+ for agent in agents:
2276
+ if agent.get("identifier") == agent_id:
2277
+ meta = agent.get("meta", agent)
2278
+ return SkillMeta(
2279
+ name=agent_id,
2280
+ description=meta.get("description", ""),
2281
+ source="lobehub",
2282
+ identifier=f"lobehub/{agent_id}",
2283
+ trust_level="community",
2284
+ tags=meta.get("tags", []) if isinstance(meta.get("tags"), list) else [],
2285
+ )
2286
+ return None
2287
+
2288
+ def _fetch_index(self) -> Optional[Any]:
2289
+ """Fetch the LobeHub agent index (cached for 1 hour)."""
2290
+ cache_key = "lobehub_index"
2291
+ cached = _read_index_cache(cache_key)
2292
+ if cached is not None:
2293
+ return cached
2294
+
2295
+ try:
2296
+ resp = httpx.get(self.INDEX_URL, timeout=30)
2297
+ if resp.status_code != 200:
2298
+ return None
2299
+ data = resp.json()
2300
+ except (httpx.HTTPError, json.JSONDecodeError):
2301
+ return None
2302
+
2303
+ _write_index_cache(cache_key, data)
2304
+ return data
2305
+
2306
+ def _fetch_agent(self, agent_id: str) -> Optional[dict]:
2307
+ """Fetch a single agent's JSON file."""
2308
+ url = f"https://chat-agents.lobehub.com/{agent_id}.json"
2309
+ try:
2310
+ resp = httpx.get(url, timeout=15)
2311
+ if resp.status_code == 200:
2312
+ return resp.json()
2313
+ except (httpx.HTTPError, json.JSONDecodeError) as e:
2314
+ logger.debug("LobeHub agent fetch failed: %s", e)
2315
+ return None
2316
+
2317
+ @staticmethod
2318
+ def _convert_to_skill_md(agent_data: dict) -> str:
2319
+ """Convert a LobeHub agent JSON into SKILL.md format."""
2320
+ meta = agent_data.get("meta", agent_data)
2321
+ identifier = agent_data.get("identifier", "lobehub-agent")
2322
+ title = meta.get("title", identifier)
2323
+ description = meta.get("description", "")
2324
+ tags = meta.get("tags", [])
2325
+ system_role = agent_data.get("config", {}).get("systemRole", "")
2326
+
2327
+ tag_list = tags if isinstance(tags, list) else []
2328
+ fm_lines = [
2329
+ "---",
2330
+ f"name: {identifier}",
2331
+ f"description: {description[:500]}",
2332
+ "metadata:",
2333
+ " hermes:",
2334
+ f" tags: [{', '.join(str(t) for t in tag_list)}]",
2335
+ " lobehub:",
2336
+ " source: lobehub",
2337
+ "---",
2338
+ ]
2339
+
2340
+ body_lines = [
2341
+ f"# {title}",
2342
+ "",
2343
+ description,
2344
+ "",
2345
+ "## Instructions",
2346
+ "",
2347
+ system_role if system_role else "(No system role defined)",
2348
+ ]
2349
+
2350
+ return "\n".join(fm_lines) + "\n\n" + "\n".join(body_lines) + "\n"
2351
+
2352
+
2353
+ # ---------------------------------------------------------------------------
2354
+ # Official optional skills source adapter
2355
+ # ---------------------------------------------------------------------------
2356
+
2357
+ class OptionalSkillSource(SkillSource):
2358
+ """
2359
+ Fetch skills from the optional-skills/ directory shipped with the repo.
2360
+
2361
+ These skills are official (maintained by Nous Research) but not activated
2362
+ by default — they don't appear in the system prompt and aren't copied to
2363
+ ~/.hermes/skills/ during setup. They are discoverable via the Skills Hub
2364
+ (search / install / inspect) and labelled "official" with "builtin" trust.
2365
+ """
2366
+
2367
+ def __init__(self):
2368
+ from calvyn_constants import get_optional_skills_dir
2369
+
2370
+ self._optional_dir = get_optional_skills_dir(
2371
+ Path(__file__).parent.parent / "optional-skills"
2372
+ )
2373
+
2374
+ def source_id(self) -> str:
2375
+ return "official"
2376
+
2377
+ def trust_level_for(self, identifier: str) -> str:
2378
+ return "builtin"
2379
+
2380
+ # -- search -----------------------------------------------------------
2381
+
2382
+ def search(self, query: str, limit: int = 10) -> List[SkillMeta]:
2383
+ results: List[SkillMeta] = []
2384
+ query_lower = query.lower()
2385
+
2386
+ for meta in self._scan_all():
2387
+ searchable = f"{meta.name} {meta.description} {' '.join(meta.tags)}".lower()
2388
+ if query_lower in searchable:
2389
+ results.append(meta)
2390
+ if len(results) >= limit:
2391
+ break
2392
+
2393
+ return results
2394
+
2395
+ # -- fetch ------------------------------------------------------------
2396
+
2397
+ def fetch(self, identifier: str) -> Optional[SkillBundle]:
2398
+ # identifier format: "official/category/skill" or "official/skill"
2399
+ rel = identifier.split("/", 1)[-1] if identifier.startswith("official/") else identifier
2400
+ skill_dir = self._optional_dir / rel
2401
+
2402
+ # Guard against path traversal (e.g. "official/../../etc")
2403
+ try:
2404
+ resolved = skill_dir.resolve()
2405
+ if not str(resolved).startswith(str(self._optional_dir.resolve())):
2406
+ return None
2407
+ except (OSError, ValueError):
2408
+ return None
2409
+
2410
+ if not resolved.is_dir():
2411
+ # Try searching by skill name only (last segment)
2412
+ skill_name = rel.rsplit("/", 1)[-1]
2413
+ skill_dir = self._find_skill_dir(skill_name)
2414
+ if not skill_dir:
2415
+ return None
2416
+ else:
2417
+ skill_dir = resolved
2418
+
2419
+ files: Dict[str, Union[str, bytes]] = {}
2420
+ for f in skill_dir.rglob("*"):
2421
+ if (
2422
+ f.is_file()
2423
+ and not f.name.startswith(".")
2424
+ and "__pycache__" not in f.parts
2425
+ and f.suffix != ".pyc"
2426
+ ):
2427
+ rel_path = str(f.relative_to(skill_dir))
2428
+ try:
2429
+ files[rel_path] = f.read_bytes()
2430
+ except OSError:
2431
+ continue
2432
+
2433
+ if not files:
2434
+ return None
2435
+
2436
+ # Determine category from directory structure
2437
+ name = skill_dir.name
2438
+
2439
+ return SkillBundle(
2440
+ name=name,
2441
+ files=files,
2442
+ source="official",
2443
+ identifier=f"official/{skill_dir.relative_to(self._optional_dir)}",
2444
+ trust_level="builtin",
2445
+ )
2446
+
2447
+ # -- inspect ----------------------------------------------------------
2448
+
2449
+ def inspect(self, identifier: str) -> Optional[SkillMeta]:
2450
+ rel = identifier.split("/", 1)[-1] if identifier.startswith("official/") else identifier
2451
+ skill_name = rel.rsplit("/", 1)[-1]
2452
+
2453
+ for meta in self._scan_all():
2454
+ if meta.name == skill_name:
2455
+ return meta
2456
+ return None
2457
+
2458
+ # -- internal helpers -------------------------------------------------
2459
+
2460
+ def _find_skill_dir(self, name: str) -> Optional[Path]:
2461
+ """Find a skill directory by name anywhere in optional-skills/."""
2462
+ if not self._optional_dir.is_dir():
2463
+ return None
2464
+ for skill_md in self._optional_dir.rglob("SKILL.md"):
2465
+ if skill_md.parent.name == name:
2466
+ return skill_md.parent
2467
+ return None
2468
+
2469
+ def _scan_all(self) -> List[SkillMeta]:
2470
+ """Enumerate all optional skills with metadata."""
2471
+ if not self._optional_dir.is_dir():
2472
+ return []
2473
+
2474
+ results: List[SkillMeta] = []
2475
+ for skill_md in sorted(self._optional_dir.rglob("SKILL.md")):
2476
+ parent = skill_md.parent
2477
+ rel_parts = parent.relative_to(self._optional_dir).parts
2478
+ if any(part.startswith(".") for part in rel_parts):
2479
+ continue
2480
+
2481
+ try:
2482
+ content = skill_md.read_text(encoding="utf-8")
2483
+ except (OSError, UnicodeDecodeError):
2484
+ continue
2485
+
2486
+ fm = self._parse_frontmatter(content)
2487
+ name = fm.get("name", parent.name)
2488
+ desc = fm.get("description", "")
2489
+ tags = []
2490
+ meta_block = fm.get("metadata", {})
2491
+ if isinstance(meta_block, dict):
2492
+ hermes_meta = meta_block.get("hermes", {})
2493
+ if isinstance(hermes_meta, dict):
2494
+ tags = hermes_meta.get("tags", [])
2495
+
2496
+ rel_path = str(parent.relative_to(self._optional_dir))
2497
+
2498
+ results.append(SkillMeta(
2499
+ name=name,
2500
+ description=desc[:200],
2501
+ source="official",
2502
+ identifier=f"official/{rel_path}",
2503
+ trust_level="builtin",
2504
+ path=rel_path,
2505
+ tags=tags if isinstance(tags, list) else [],
2506
+ ))
2507
+
2508
+ return results
2509
+
2510
+ @staticmethod
2511
+ def _parse_frontmatter(content: str) -> dict:
2512
+ """Parse YAML frontmatter from SKILL.md content."""
2513
+ if not content.startswith("---"):
2514
+ return {}
2515
+ match = re.search(r'\n---\s*\n', content[3:])
2516
+ if not match:
2517
+ return {}
2518
+ yaml_text = content[3:match.start() + 3]
2519
+ try:
2520
+ parsed = yaml.safe_load(yaml_text)
2521
+ return parsed if isinstance(parsed, dict) else {}
2522
+ except yaml.YAMLError:
2523
+ return {}
2524
+
2525
+
2526
+ # ---------------------------------------------------------------------------
2527
+ # Shared cache helpers (used by multiple adapters)
2528
+ # ---------------------------------------------------------------------------
2529
+
2530
+ def _read_index_cache(key: str) -> Optional[Any]:
2531
+ """Read cached data if not expired."""
2532
+ cache_file = INDEX_CACHE_DIR / f"{key}.json"
2533
+ if not cache_file.exists():
2534
+ return None
2535
+ try:
2536
+ stat = cache_file.stat()
2537
+ if time.time() - stat.st_mtime > INDEX_CACHE_TTL:
2538
+ return None
2539
+ return json.loads(cache_file.read_text())
2540
+ except (OSError, json.JSONDecodeError):
2541
+ return None
2542
+
2543
+
2544
+ def _write_index_cache(key: str, data: Any) -> None:
2545
+ """Write data to cache."""
2546
+ INDEX_CACHE_DIR.mkdir(parents=True, exist_ok=True)
2547
+ # Ensure .ignore exists so ripgrep (and tools respecting .ignore) skip
2548
+ # this directory. Cache files contain unvetted community content that
2549
+ # could include adversarial text (prompt injection via catalog entries).
2550
+ ignore_file = HUB_DIR / ".ignore"
2551
+ if not ignore_file.exists():
2552
+ try:
2553
+ ignore_file.write_text("# Exclude hub internals from search tools\n*\n")
2554
+ except OSError:
2555
+ pass
2556
+ cache_file = INDEX_CACHE_DIR / f"{key}.json"
2557
+ try:
2558
+ cache_file.write_text(json.dumps(data, ensure_ascii=False, default=str))
2559
+ except OSError as e:
2560
+ logger.debug("Could not write cache: %s", e)
2561
+
2562
+
2563
+ def _skill_meta_to_dict(meta: SkillMeta) -> dict:
2564
+ """Convert a SkillMeta to a dict for caching."""
2565
+ return {
2566
+ "name": meta.name,
2567
+ "description": meta.description,
2568
+ "source": meta.source,
2569
+ "identifier": meta.identifier,
2570
+ "trust_level": meta.trust_level,
2571
+ "repo": meta.repo,
2572
+ "path": meta.path,
2573
+ "tags": meta.tags,
2574
+ "extra": meta.extra,
2575
+ }
2576
+
2577
+
2578
+ # ---------------------------------------------------------------------------
2579
+ # Lock file management
2580
+ # ---------------------------------------------------------------------------
2581
+
2582
+ class HubLockFile:
2583
+ """Manages skills/.hub/lock.json — tracks provenance of installed hub skills."""
2584
+
2585
+ def __init__(self, path: Path = LOCK_FILE):
2586
+ self.path = path
2587
+
2588
+ def load(self) -> dict:
2589
+ if not self.path.exists():
2590
+ return {"version": 1, "installed": {}}
2591
+ try:
2592
+ return json.loads(self.path.read_text())
2593
+ except (json.JSONDecodeError, OSError):
2594
+ return {"version": 1, "installed": {}}
2595
+
2596
+ def save(self, data: dict) -> None:
2597
+ self.path.parent.mkdir(parents=True, exist_ok=True)
2598
+ self.path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
2599
+
2600
+ def record_install(
2601
+ self,
2602
+ name: str,
2603
+ source: str,
2604
+ identifier: str,
2605
+ trust_level: str,
2606
+ scan_verdict: str,
2607
+ skill_hash: str,
2608
+ install_path: str,
2609
+ files: List[str],
2610
+ metadata: Optional[Dict[str, Any]] = None,
2611
+ ) -> None:
2612
+ data = self.load()
2613
+ data["installed"][name] = {
2614
+ "source": source,
2615
+ "identifier": identifier,
2616
+ "trust_level": trust_level,
2617
+ "scan_verdict": scan_verdict,
2618
+ "content_hash": skill_hash,
2619
+ "install_path": install_path,
2620
+ "files": files,
2621
+ "metadata": metadata or {},
2622
+ "installed_at": datetime.now(timezone.utc).isoformat(),
2623
+ "updated_at": datetime.now(timezone.utc).isoformat(),
2624
+ }
2625
+ self.save(data)
2626
+
2627
+ def record_uninstall(self, name: str) -> None:
2628
+ data = self.load()
2629
+ data["installed"].pop(name, None)
2630
+ self.save(data)
2631
+
2632
+ def get_installed(self, name: str) -> Optional[dict]:
2633
+ data = self.load()
2634
+ return data["installed"].get(name)
2635
+
2636
+ def list_installed(self) -> List[dict]:
2637
+ data = self.load()
2638
+ result = []
2639
+ for name, entry in data["installed"].items():
2640
+ result.append({"name": name, **entry})
2641
+ return result
2642
+
2643
+
2644
+ # ---------------------------------------------------------------------------
2645
+ # Taps management
2646
+ # ---------------------------------------------------------------------------
2647
+
2648
+ class TapsManager:
2649
+ """Manages the taps.json file — custom GitHub repo sources."""
2650
+
2651
+ def __init__(self, path: Path = TAPS_FILE):
2652
+ self.path = path
2653
+
2654
+ def load(self) -> List[dict]:
2655
+ if not self.path.exists():
2656
+ return []
2657
+ try:
2658
+ data = json.loads(self.path.read_text())
2659
+ return data.get("taps", [])
2660
+ except (json.JSONDecodeError, OSError):
2661
+ return []
2662
+
2663
+ def save(self, taps: List[dict]) -> None:
2664
+ self.path.parent.mkdir(parents=True, exist_ok=True)
2665
+ self.path.write_text(json.dumps({"taps": taps}, indent=2) + "\n")
2666
+
2667
+ def add(self, repo: str, path: str = "skills/") -> bool:
2668
+ """Add a tap. Returns False if already exists."""
2669
+ taps = self.load()
2670
+ if any(t["repo"] == repo for t in taps):
2671
+ return False
2672
+ taps.append({"repo": repo, "path": path})
2673
+ self.save(taps)
2674
+ return True
2675
+
2676
+ def remove(self, repo: str) -> bool:
2677
+ """Remove a tap by repo name. Returns False if not found."""
2678
+ taps = self.load()
2679
+ new_taps = [t for t in taps if t["repo"] != repo]
2680
+ if len(new_taps) == len(taps):
2681
+ return False
2682
+ self.save(new_taps)
2683
+ return True
2684
+
2685
+ def list_taps(self) -> List[dict]:
2686
+ return self.load()
2687
+
2688
+
2689
+ # ---------------------------------------------------------------------------
2690
+ # Audit log
2691
+ # ---------------------------------------------------------------------------
2692
+
2693
+ def append_audit_log(action: str, skill_name: str, source: str,
2694
+ trust_level: str, verdict: str, extra: str = "") -> None:
2695
+ """Append a line to the audit log."""
2696
+ AUDIT_LOG.parent.mkdir(parents=True, exist_ok=True)
2697
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
2698
+ parts = [timestamp, action, skill_name, f"{source}:{trust_level}", verdict]
2699
+ if extra:
2700
+ parts.append(extra)
2701
+ line = " ".join(parts) + "\n"
2702
+ try:
2703
+ with open(AUDIT_LOG, "a", encoding="utf-8") as f:
2704
+ f.write(line)
2705
+ except OSError as e:
2706
+ logger.debug("Could not write audit log: %s", e)
2707
+
2708
+
2709
+ # ---------------------------------------------------------------------------
2710
+ # Hub operations (high-level)
2711
+ # ---------------------------------------------------------------------------
2712
+
2713
+ def ensure_hub_dirs() -> None:
2714
+ """Create the .hub directory structure if it doesn't exist."""
2715
+ HUB_DIR.mkdir(parents=True, exist_ok=True)
2716
+ QUARANTINE_DIR.mkdir(exist_ok=True)
2717
+ INDEX_CACHE_DIR.mkdir(exist_ok=True)
2718
+ if not LOCK_FILE.exists():
2719
+ LOCK_FILE.write_text('{"version": 1, "installed": {}}\n')
2720
+ if not AUDIT_LOG.exists():
2721
+ AUDIT_LOG.touch()
2722
+ if not TAPS_FILE.exists():
2723
+ TAPS_FILE.write_text('{"taps": []}\n')
2724
+
2725
+
2726
+ def quarantine_bundle(bundle: SkillBundle) -> Path:
2727
+ """Write a skill bundle to the quarantine directory for scanning."""
2728
+ ensure_hub_dirs()
2729
+ skill_name = _validate_skill_name(bundle.name)
2730
+ validated_files: List[Tuple[str, Union[str, bytes]]] = []
2731
+ for rel_path, file_content in bundle.files.items():
2732
+ safe_rel_path = _validate_bundle_rel_path(rel_path)
2733
+ validated_files.append((safe_rel_path, file_content))
2734
+
2735
+ dest = QUARANTINE_DIR / skill_name
2736
+ if dest.exists():
2737
+ shutil.rmtree(dest)
2738
+ dest.mkdir(parents=True)
2739
+
2740
+ for rel_path, file_content in validated_files:
2741
+ file_dest = dest.joinpath(*rel_path.split("/"))
2742
+ file_dest.parent.mkdir(parents=True, exist_ok=True)
2743
+ if isinstance(file_content, bytes):
2744
+ file_dest.write_bytes(file_content)
2745
+ else:
2746
+ file_dest.write_text(file_content, encoding="utf-8")
2747
+
2748
+ return dest
2749
+
2750
+
2751
+ def install_from_quarantine(
2752
+ quarantine_path: Path,
2753
+ skill_name: str,
2754
+ category: str,
2755
+ bundle: SkillBundle,
2756
+ scan_result: ScanResult,
2757
+ ) -> Path:
2758
+ """Move a scanned skill from quarantine into the skills directory."""
2759
+ safe_skill_name = _validate_skill_name(skill_name)
2760
+ safe_category = _validate_category_name(category) if category else ""
2761
+ quarantine_resolved = quarantine_path.resolve()
2762
+ quarantine_root = QUARANTINE_DIR.resolve()
2763
+ if not quarantine_resolved.is_relative_to(quarantine_root):
2764
+ raise ValueError(f"Unsafe quarantine path: {quarantine_path}")
2765
+
2766
+ if safe_category:
2767
+ install_dir = SKILLS_DIR / safe_category / safe_skill_name
2768
+ else:
2769
+ install_dir = SKILLS_DIR / safe_skill_name
2770
+
2771
+ if install_dir.exists():
2772
+ shutil.rmtree(install_dir)
2773
+
2774
+ # Warn (but don't block) if SKILL.md is very large
2775
+ skill_md = quarantine_path / "SKILL.md"
2776
+ if skill_md.exists():
2777
+ try:
2778
+ skill_size = skill_md.stat().st_size
2779
+ if skill_size > 100_000:
2780
+ logger.warning(
2781
+ "Skill '%s' has a large SKILL.md (%s chars). "
2782
+ "Large skills consume significant context when loaded. "
2783
+ "Consider asking the author to split it into smaller files.",
2784
+ safe_skill_name,
2785
+ f"{skill_size:,}",
2786
+ )
2787
+ except OSError:
2788
+ pass
2789
+
2790
+ install_dir.parent.mkdir(parents=True, exist_ok=True)
2791
+ shutil.move(str(quarantine_path), str(install_dir))
2792
+
2793
+ # Record in lock file
2794
+ lock = HubLockFile()
2795
+ lock.record_install(
2796
+ name=safe_skill_name,
2797
+ source=bundle.source,
2798
+ identifier=bundle.identifier,
2799
+ trust_level=bundle.trust_level,
2800
+ scan_verdict=scan_result.verdict,
2801
+ skill_hash=content_hash(install_dir),
2802
+ install_path=str(install_dir.relative_to(SKILLS_DIR)),
2803
+ files=list(bundle.files.keys()),
2804
+ metadata=bundle.metadata,
2805
+ )
2806
+
2807
+ append_audit_log(
2808
+ "INSTALL", safe_skill_name, bundle.source,
2809
+ bundle.trust_level, scan_result.verdict,
2810
+ content_hash(install_dir),
2811
+ )
2812
+
2813
+ return install_dir
2814
+
2815
+
2816
+ def uninstall_skill(skill_name: str) -> Tuple[bool, str]:
2817
+ """Remove a hub-installed skill. Refuses to remove builtins."""
2818
+ lock = HubLockFile()
2819
+ entry = lock.get_installed(skill_name)
2820
+ if not entry:
2821
+ return False, f"'{skill_name}' is not a hub-installed skill (may be a builtin)"
2822
+
2823
+ install_path = SKILLS_DIR / entry["install_path"]
2824
+ if install_path.exists():
2825
+ shutil.rmtree(install_path)
2826
+
2827
+ lock.record_uninstall(skill_name)
2828
+ append_audit_log("UNINSTALL", skill_name, entry["source"], entry["trust_level"], "n/a", "user_request")
2829
+
2830
+ return True, f"Uninstalled '{skill_name}' from {entry['install_path']}"
2831
+
2832
+
2833
+ def bundle_content_hash(bundle: SkillBundle) -> str:
2834
+ """Compute a deterministic hash for an in-memory skill bundle."""
2835
+ h = hashlib.sha256()
2836
+ for rel_path in sorted(bundle.files):
2837
+ content = bundle.files[rel_path]
2838
+ if isinstance(content, bytes):
2839
+ h.update(content)
2840
+ else:
2841
+ h.update(content.encode("utf-8"))
2842
+ return f"sha256:{h.hexdigest()[:16]}"
2843
+
2844
+
2845
+ def _source_matches(source: SkillSource, source_name: str) -> bool:
2846
+ aliases = {
2847
+ "skills.sh": "skills-sh",
2848
+ }
2849
+ normalized = aliases.get(source_name, source_name)
2850
+ return source.source_id() == normalized
2851
+
2852
+
2853
+ def check_for_skill_updates(
2854
+ name: Optional[str] = None,
2855
+ *,
2856
+ lock: Optional[HubLockFile] = None,
2857
+ sources: Optional[List[SkillSource]] = None,
2858
+ auth: Optional[GitHubAuth] = None,
2859
+ ) -> List[dict]:
2860
+ """Check installed hub skills for upstream changes."""
2861
+ lock = lock or HubLockFile()
2862
+ installed = lock.list_installed()
2863
+ if name:
2864
+ installed = [entry for entry in installed if entry.get("name") == name]
2865
+
2866
+ if sources is None:
2867
+ sources = create_source_router(auth=auth)
2868
+
2869
+ results: List[dict] = []
2870
+ for entry in installed:
2871
+ identifier = entry.get("identifier", "")
2872
+ source_name = entry.get("source", "")
2873
+ candidate_sources = [src for src in sources if _source_matches(src, source_name)] or sources
2874
+
2875
+ bundle = None
2876
+ for src in candidate_sources:
2877
+ try:
2878
+ bundle = src.fetch(identifier)
2879
+ except Exception:
2880
+ bundle = None
2881
+ if bundle:
2882
+ break
2883
+
2884
+ if not bundle:
2885
+ results.append({
2886
+ "name": entry.get("name", ""),
2887
+ "identifier": identifier,
2888
+ "source": source_name,
2889
+ "status": "unavailable",
2890
+ })
2891
+ continue
2892
+
2893
+ current_hash = entry.get("content_hash", "")
2894
+ latest_hash = bundle_content_hash(bundle)
2895
+ status = "up_to_date" if current_hash == latest_hash else "update_available"
2896
+ results.append({
2897
+ "name": entry.get("name", ""),
2898
+ "identifier": identifier,
2899
+ "source": source_name,
2900
+ "status": status,
2901
+ "current_hash": current_hash,
2902
+ "latest_hash": latest_hash,
2903
+ "bundle": bundle,
2904
+ })
2905
+
2906
+ return results
2907
+
2908
+
2909
+ # ---------------------------------------------------------------------------
2910
+ # Hermes centralized index source
2911
+ # ---------------------------------------------------------------------------
2912
+
2913
+ HERMES_INDEX_URL = "https://hermes-agent.nousresearch.com/docs/api/skills-index.json"
2914
+ HERMES_INDEX_CACHE_FILE = INDEX_CACHE_DIR / "hermes-index.json"
2915
+ HERMES_INDEX_TTL = 6 * 3600 # 6 hours
2916
+
2917
+
2918
+ def _load_hermes_index() -> Optional[dict]:
2919
+ """Fetch the centralized skills index, with local cache.
2920
+
2921
+ The index is a JSON file hosted on the docs site, rebuilt daily by CI.
2922
+ We cache it locally for HERMES_INDEX_TTL seconds to avoid repeated
2923
+ downloads within a session.
2924
+ """
2925
+ # Check local cache
2926
+ if HERMES_INDEX_CACHE_FILE.exists():
2927
+ try:
2928
+ age = time.time() - HERMES_INDEX_CACHE_FILE.stat().st_mtime
2929
+ if age < HERMES_INDEX_TTL:
2930
+ return json.loads(HERMES_INDEX_CACHE_FILE.read_text())
2931
+ except (OSError, json.JSONDecodeError):
2932
+ pass
2933
+
2934
+ # Fetch from docs site
2935
+ try:
2936
+ resp = httpx.get(HERMES_INDEX_URL, timeout=15, follow_redirects=True)
2937
+ if resp.status_code != 200:
2938
+ logger.debug("Hermes index fetch returned %d", resp.status_code)
2939
+ return _load_stale_index_cache()
2940
+ data = resp.json()
2941
+ except (httpx.HTTPError, json.JSONDecodeError) as e:
2942
+ logger.debug("Hermes index fetch failed: %s", e)
2943
+ return _load_stale_index_cache()
2944
+
2945
+ # Validate structure
2946
+ if not isinstance(data, dict) or "skills" not in data:
2947
+ return _load_stale_index_cache()
2948
+
2949
+ # Cache locally
2950
+ try:
2951
+ HERMES_INDEX_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
2952
+ HERMES_INDEX_CACHE_FILE.write_text(json.dumps(data))
2953
+ except OSError:
2954
+ pass
2955
+
2956
+ return data
2957
+
2958
+
2959
+ def _load_stale_index_cache() -> Optional[dict]:
2960
+ """Fall back to stale cache when the network fetch fails."""
2961
+ if HERMES_INDEX_CACHE_FILE.exists():
2962
+ try:
2963
+ return json.loads(HERMES_INDEX_CACHE_FILE.read_text())
2964
+ except (OSError, json.JSONDecodeError):
2965
+ pass
2966
+ return None
2967
+
2968
+
2969
+ class HermesIndexSource(SkillSource):
2970
+ """Skill source backed by the centralized Hermes Skills Index.
2971
+
2972
+ The index is a JSON catalog published to the docs site and rebuilt
2973
+ daily by CI. It contains metadata + resolved GitHub paths for every
2974
+ skill, eliminating the need for users to hit the GitHub API for
2975
+ search or path discovery.
2976
+
2977
+ When the index is unavailable, all methods return empty / None so
2978
+ downstream sources take over transparently.
2979
+ """
2980
+
2981
+ def __init__(self, auth: GitHubAuth):
2982
+ self._index: Optional[dict] = None
2983
+ self._loaded = False
2984
+ self.auth = auth
2985
+ # Lazily create GitHubSource for fetch — only used when actually
2986
+ # downloading files, which requires real GitHub API calls.
2987
+ self._github: Optional[GitHubSource] = None
2988
+
2989
+ def _ensure_loaded(self) -> dict:
2990
+ if not self._loaded:
2991
+ self._index = _load_hermes_index()
2992
+ self._loaded = True
2993
+ return self._index or {}
2994
+
2995
+ def _get_github(self) -> GitHubSource:
2996
+ if self._github is None:
2997
+ self._github = GitHubSource(auth=self.auth)
2998
+ return self._github
2999
+
3000
+ def source_id(self) -> str:
3001
+ return "hermes-index"
3002
+
3003
+ @property
3004
+ def is_available(self) -> bool:
3005
+ """Whether the index is loaded and has skills."""
3006
+ index = self._ensure_loaded()
3007
+ return bool(index.get("skills"))
3008
+
3009
+ def trust_level_for(self, identifier: str) -> str:
3010
+ index = self._ensure_loaded()
3011
+ for skill in index.get("skills", []):
3012
+ if skill.get("identifier") == identifier:
3013
+ return skill.get("trust_level", "community")
3014
+ return "community"
3015
+
3016
+ def search(self, query: str, limit: int = 10) -> List[SkillMeta]:
3017
+ """Search the cached index. Zero API calls."""
3018
+ index = self._ensure_loaded()
3019
+ skills = index.get("skills", [])
3020
+ if not skills:
3021
+ return []
3022
+
3023
+ if not query.strip():
3024
+ # No query — return featured/popular
3025
+ return [self._to_meta(s) for s in skills[:limit]]
3026
+
3027
+ query_lower = query.lower()
3028
+ results: List[SkillMeta] = []
3029
+ for s in skills:
3030
+ searchable = f"{s.get('name', '')} {s.get('description', '')} {' '.join(s.get('tags', []))}".lower()
3031
+ if query_lower in searchable:
3032
+ results.append(self._to_meta(s))
3033
+ if len(results) >= limit:
3034
+ break
3035
+ return results
3036
+
3037
+ def fetch(self, identifier: str) -> Optional[SkillBundle]:
3038
+ """Fetch a skill using the resolved path from the index.
3039
+
3040
+ If the index has a ``resolved_github_id`` for this skill, we skip
3041
+ the entire candidate/discovery chain and go directly to GitHub
3042
+ with the exact path. This reduces install from ~31 API calls to
3043
+ just the file content downloads (~5-22 depending on skill size).
3044
+ """
3045
+ index = self._ensure_loaded()
3046
+ entry = self._find_entry(identifier, index)
3047
+ if not entry:
3048
+ return None
3049
+
3050
+ # Use resolved path if available
3051
+ resolved = entry.get("resolved_github_id")
3052
+ if resolved:
3053
+ bundle = self._get_github().fetch(resolved)
3054
+ if bundle:
3055
+ bundle.source = entry.get("source", "hermes-index")
3056
+ bundle.identifier = identifier
3057
+ return bundle
3058
+
3059
+ # Fall back to identifier-based fetch via repo/path
3060
+ repo = entry.get("repo", "")
3061
+ path = entry.get("path", "")
3062
+ if repo and path:
3063
+ github_id = f"{repo}/{path}"
3064
+ bundle = self._get_github().fetch(github_id)
3065
+ if bundle:
3066
+ bundle.source = entry.get("source", "hermes-index")
3067
+ bundle.identifier = identifier
3068
+ return bundle
3069
+
3070
+ return None
3071
+
3072
+ def inspect(self, identifier: str) -> Optional[SkillMeta]:
3073
+ """Return metadata from the index. Zero API calls."""
3074
+ index = self._ensure_loaded()
3075
+ entry = self._find_entry(identifier, index)
3076
+ if entry:
3077
+ return self._to_meta(entry)
3078
+ return None
3079
+
3080
+ def _find_entry(self, identifier: str, index: dict) -> Optional[dict]:
3081
+ """Look up a skill in the index by identifier or name."""
3082
+ skills = index.get("skills", [])
3083
+
3084
+ # Exact identifier match
3085
+ for s in skills:
3086
+ if s.get("identifier") == identifier:
3087
+ return s
3088
+
3089
+ # Try without source prefix (e.g. "skills-sh/" stripped)
3090
+ normalized = identifier
3091
+ for prefix in ("skills-sh/", "skills.sh/", "official/", "github/", "clawhub/"):
3092
+ if identifier.startswith(prefix):
3093
+ normalized = identifier[len(prefix):]
3094
+ break
3095
+
3096
+ # Match on normalized identifier or name
3097
+ for s in skills:
3098
+ sid = s.get("identifier", "")
3099
+ # Strip prefix from stored identifier too
3100
+ stored_normalized = sid
3101
+ for prefix in ("skills-sh/", "skills.sh/", "official/", "github/", "clawhub/"):
3102
+ if sid.startswith(prefix):
3103
+ stored_normalized = sid[len(prefix):]
3104
+ break
3105
+ if stored_normalized == normalized:
3106
+ return s
3107
+
3108
+ return None
3109
+
3110
+ @staticmethod
3111
+ def _to_meta(entry: dict) -> SkillMeta:
3112
+ return SkillMeta(
3113
+ name=entry.get("name", ""),
3114
+ description=entry.get("description", ""),
3115
+ source=entry.get("source", "hermes-index"),
3116
+ identifier=entry.get("identifier", ""),
3117
+ trust_level=entry.get("trust_level", "community"),
3118
+ repo=entry.get("repo"),
3119
+ path=entry.get("path"),
3120
+ tags=entry.get("tags", []),
3121
+ extra=entry.get("extra", {}),
3122
+ )
3123
+
3124
+
3125
+ def create_source_router(auth: Optional[GitHubAuth] = None) -> List[SkillSource]:
3126
+ """
3127
+ Create all configured source adapters.
3128
+ Returns a list of active sources for search/fetch operations.
3129
+ """
3130
+ if auth is None:
3131
+ auth = GitHubAuth()
3132
+
3133
+ taps_mgr = TapsManager()
3134
+ extra_taps = taps_mgr.list_taps()
3135
+
3136
+ sources: List[SkillSource] = [
3137
+ OptionalSkillSource(), # Official optional skills (highest priority)
3138
+ HermesIndexSource(auth=auth), # Centralized index (search + resolved install paths)
3139
+ SkillsShSource(auth=auth),
3140
+ WellKnownSkillSource(),
3141
+ UrlSource(), # Direct HTTP(S) URL to a SKILL.md file
3142
+ GitHubSource(auth=auth, extra_taps=extra_taps),
3143
+ ClawHubSource(),
3144
+ ClaudeMarketplaceSource(auth=auth),
3145
+ LobeHubSource(),
3146
+ ]
3147
+
3148
+ return sources
3149
+
3150
+
3151
+ def _search_one_source(
3152
+ src: SkillSource, query: str, limit: int
3153
+ ) -> Tuple[str, List[SkillMeta]]:
3154
+ """Search a single source. Runs in a thread for parallelism."""
3155
+ try:
3156
+ return src.source_id(), src.search(query, limit=limit)
3157
+ except Exception as e:
3158
+ logger.debug("Search failed for %s: %s", src.source_id(), e)
3159
+ return src.source_id(), []
3160
+
3161
+
3162
+ def parallel_search_sources(
3163
+ sources: List[SkillSource],
3164
+ query: str = "",
3165
+ per_source_limits: Optional[Dict[str, int]] = None,
3166
+ source_filter: str = "all",
3167
+ overall_timeout: float = 30,
3168
+ on_source_done: Optional[Any] = None,
3169
+ ) -> Tuple[List[SkillMeta], Dict[str, int], List[str]]:
3170
+ """Search all sources in parallel with per-source timeout.
3171
+
3172
+ Returns ``(all_results, source_counts, timed_out_ids)``.
3173
+
3174
+ *on_source_done* is an optional callback ``(source_id, count) -> None``
3175
+ invoked as each source completes — useful for progress indicators.
3176
+ """
3177
+ from concurrent.futures import ThreadPoolExecutor, as_completed
3178
+
3179
+ per_source_limits = per_source_limits or {}
3180
+
3181
+ active: List[SkillSource] = []
3182
+ # When the centralized index is available and the user hasn't filtered
3183
+ # to a specific source, skip external API sources (github, skills-sh,
3184
+ # clawhub, etc.) — the index already has their data. This avoids
3185
+ # ~70 GitHub API calls per search for unauthenticated users.
3186
+ _index_available = False
3187
+ _api_source_ids = frozenset({"github", "skills-sh", "clawhub",
3188
+ "claude-marketplace", "lobehub", "well-known"})
3189
+ if source_filter == "all":
3190
+ for src in sources:
3191
+ if (src.source_id() == "hermes-index"
3192
+ and getattr(src, "is_available", False)):
3193
+ _index_available = True
3194
+ break
3195
+
3196
+ for src in sources:
3197
+ sid = src.source_id()
3198
+ if source_filter != "all" and sid != source_filter and sid != "official":
3199
+ continue
3200
+ # Skip external API sources when the index covers them
3201
+ if _index_available and sid in _api_source_ids:
3202
+ continue
3203
+ active.append(src)
3204
+
3205
+ all_results: List[SkillMeta] = []
3206
+ source_counts: Dict[str, int] = {}
3207
+ timed_out_ids: List[str] = []
3208
+
3209
+ if not active:
3210
+ return all_results, source_counts, timed_out_ids
3211
+
3212
+ with ThreadPoolExecutor(max_workers=min(len(active), 8)) as pool:
3213
+ futures = {}
3214
+ for src in active:
3215
+ lim = per_source_limits.get(src.source_id(), 50)
3216
+ fut = pool.submit(_search_one_source, src, query, lim)
3217
+ futures[fut] = src.source_id()
3218
+
3219
+ try:
3220
+ for fut in as_completed(futures, timeout=overall_timeout):
3221
+ try:
3222
+ sid, results = fut.result(timeout=0)
3223
+ source_counts[sid] = len(results)
3224
+ all_results.extend(results)
3225
+ if on_source_done:
3226
+ on_source_done(sid, len(results))
3227
+ except Exception:
3228
+ pass
3229
+ except TimeoutError:
3230
+ timed_out_ids = [
3231
+ futures[f] for f in futures if not f.done()
3232
+ ]
3233
+ if timed_out_ids:
3234
+ logger.debug(
3235
+ "Skills browse timed out waiting for: %s",
3236
+ ", ".join(timed_out_ids),
3237
+ )
3238
+
3239
+ return all_results, source_counts, timed_out_ids
3240
+
3241
+
3242
+ def unified_search(query: str, sources: List[SkillSource],
3243
+ source_filter: str = "all", limit: int = 10) -> List[SkillMeta]:
3244
+ """Search all sources (in parallel) and merge results."""
3245
+ all_results, _, _ = parallel_search_sources(
3246
+ sources,
3247
+ query=query,
3248
+ source_filter=source_filter,
3249
+ overall_timeout=30,
3250
+ )
3251
+
3252
+ # Deduplicate by name, preferring higher trust levels
3253
+ _TRUST_RANK = {"builtin": 2, "trusted": 1, "community": 0}
3254
+ seen: Dict[str, SkillMeta] = {}
3255
+ for r in all_results:
3256
+ if r.name not in seen:
3257
+ seen[r.name] = r
3258
+ elif _TRUST_RANK.get(r.trust_level, 0) > _TRUST_RANK.get(seen[r.name].trust_level, 0):
3259
+ seen[r.name] = r
3260
+ deduped = list(seen.values())
3261
+
3262
+ return deduped[:limit]
3263
+