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,2170 @@
1
+ """
2
+ Weixin platform adapter.
3
+
4
+ Connects Hermes Agent to WeChat personal accounts via Tencent's iLink Bot API.
5
+
6
+ Design notes:
7
+ - Long-poll ``getupdates`` drives inbound delivery.
8
+ - Every outbound reply must echo the latest ``context_token`` for the peer.
9
+ - Media files move through an AES-128-ECB encrypted CDN protocol.
10
+ - QR login is exposed as a helper for the gateway setup wizard.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import base64
17
+ import hashlib
18
+ import json
19
+ import logging
20
+ import mimetypes
21
+ import os
22
+ import re
23
+ import secrets
24
+ import struct
25
+ import tempfile
26
+ import textwrap
27
+ import time
28
+ import uuid
29
+ from datetime import datetime
30
+ from pathlib import Path
31
+ from typing import Any, Dict, List, Optional, Tuple
32
+ from urllib.parse import quote, urlparse
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ WEIXIN_COPY_LINE_WIDTH = 120
37
+
38
+ try:
39
+ import aiohttp
40
+
41
+ AIOHTTP_AVAILABLE = True
42
+ except ImportError: # pragma: no cover - dependency gate
43
+ aiohttp = None # type: ignore[assignment]
44
+ AIOHTTP_AVAILABLE = False
45
+
46
+ try:
47
+ from cryptography.hazmat.backends import default_backend
48
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
49
+
50
+ CRYPTO_AVAILABLE = True
51
+ except ImportError: # pragma: no cover - dependency gate
52
+ default_backend = None # type: ignore[assignment]
53
+ Cipher = None # type: ignore[assignment]
54
+ algorithms = None # type: ignore[assignment]
55
+ modes = None # type: ignore[assignment]
56
+ CRYPTO_AVAILABLE = False
57
+
58
+ from gateway.config import Platform, PlatformConfig
59
+ from gateway.platforms.helpers import MessageDeduplicator
60
+ from gateway.platforms.base import (
61
+ BasePlatformAdapter,
62
+ MessageEvent,
63
+ MessageType,
64
+ SendResult,
65
+ cache_audio_from_bytes,
66
+ cache_document_from_bytes,
67
+ cache_image_from_bytes,
68
+ )
69
+ from calvyn_constants import get_hermes_home
70
+ from utils import atomic_json_write
71
+
72
+ ILINK_BASE_URL = "https://ilinkai.weixin.qq.com"
73
+ WEIXIN_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c"
74
+ ILINK_APP_ID = "bot"
75
+ CHANNEL_VERSION = "2.2.0"
76
+ ILINK_APP_CLIENT_VERSION = (2 << 16) | (2 << 8) | 0
77
+
78
+ EP_GET_UPDATES = "ilink/bot/getupdates"
79
+ EP_SEND_MESSAGE = "ilink/bot/sendmessage"
80
+ EP_SEND_TYPING = "ilink/bot/sendtyping"
81
+ EP_GET_CONFIG = "ilink/bot/getconfig"
82
+ EP_GET_UPLOAD_URL = "ilink/bot/getuploadurl"
83
+ EP_GET_BOT_QR = "ilink/bot/get_bot_qrcode"
84
+ EP_GET_QR_STATUS = "ilink/bot/get_qrcode_status"
85
+
86
+ LONG_POLL_TIMEOUT_MS = 35_000
87
+ API_TIMEOUT_MS = 15_000
88
+ CONFIG_TIMEOUT_MS = 10_000
89
+ QR_TIMEOUT_MS = 35_000
90
+
91
+ MAX_CONSECUTIVE_FAILURES = 3
92
+ RETRY_DELAY_SECONDS = 2
93
+ BACKOFF_DELAY_SECONDS = 30
94
+ SESSION_EXPIRED_ERRCODE = -14
95
+ RATE_LIMIT_ERRCODE = -2 # iLink frequency limit — backoff and retry
96
+ MESSAGE_DEDUP_TTL_SECONDS = 300
97
+
98
+
99
+ def _is_stale_session_ret(
100
+ ret: "Optional[int]", errcode: "Optional[int]", errmsg: "Optional[str]",
101
+ ) -> bool:
102
+ """True when iLink returns ret=-2 / errcode=-2 with 'unknown error',
103
+ which is a stale-session signal (same as errcode=-14) rather than
104
+ a genuine rate limit."""
105
+ if ret != RATE_LIMIT_ERRCODE and errcode != RATE_LIMIT_ERRCODE:
106
+ return False
107
+ return (errmsg or "").lower() == "unknown error"
108
+
109
+
110
+ MEDIA_IMAGE = 1
111
+ MEDIA_VIDEO = 2
112
+ MEDIA_FILE = 3
113
+ MEDIA_VOICE = 4
114
+
115
+ _LIVE_ADAPTERS: Dict[str, Any] = {}
116
+
117
+
118
+ def _make_ssl_connector() -> Optional["aiohttp.TCPConnector"]:
119
+ """Return a TCPConnector with a certifi CA bundle, or None if certifi is unavailable.
120
+
121
+ Tencent's iLink server (``ilinkai.weixin.qq.com``) is not verifiable against
122
+ some system CA stores (notably Homebrew's OpenSSL on macOS Apple Silicon).
123
+ When ``certifi`` is installed, use its Mozilla CA bundle to guarantee
124
+ verification. Otherwise fall back to aiohttp's default (which honors
125
+ ``SSL_CERT_FILE`` env var via ``trust_env=True``).
126
+ """
127
+ try:
128
+ import ssl
129
+ import certifi
130
+ except ImportError:
131
+ return None
132
+ if not AIOHTTP_AVAILABLE:
133
+ return None
134
+ ssl_ctx = ssl.create_default_context(cafile=certifi.where())
135
+ return aiohttp.TCPConnector(ssl=ssl_ctx)
136
+
137
+ ITEM_TEXT = 1
138
+ ITEM_IMAGE = 2
139
+ ITEM_VOICE = 3
140
+ ITEM_FILE = 4
141
+ ITEM_VIDEO = 5
142
+
143
+ MSG_TYPE_USER = 1
144
+ MSG_TYPE_BOT = 2
145
+ MSG_STATE_FINISH = 2
146
+
147
+ TYPING_START = 1
148
+ TYPING_STOP = 2
149
+
150
+ _HEADER_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
151
+ _TABLE_RULE_RE = re.compile(r"^\s*\|?(?:\s*:?-{3,}:?\s*\|)+\s*:?-{3,}:?\s*\|?\s*$")
152
+ _FENCE_RE = re.compile(r"^```([^\n`]*)\s*$")
153
+ _MARKDOWN_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
154
+
155
+
156
+ def check_weixin_requirements() -> bool:
157
+ """Return True when runtime dependencies for Weixin are available."""
158
+ return AIOHTTP_AVAILABLE and CRYPTO_AVAILABLE
159
+
160
+
161
+ def _safe_id(value: Optional[str], keep: int = 8) -> str:
162
+ raw = str(value or "").strip()
163
+ if not raw:
164
+ return "?"
165
+ if len(raw) <= keep:
166
+ return raw
167
+ return raw[:keep]
168
+
169
+
170
+ def _json_dumps(payload: Dict[str, Any]) -> str:
171
+ return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
172
+
173
+
174
+ def _pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
175
+ pad_len = block_size - (len(data) % block_size)
176
+ return data + bytes([pad_len] * pad_len)
177
+
178
+
179
+ def _aes128_ecb_encrypt(plaintext: bytes, key: bytes) -> bytes:
180
+ cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
181
+ encryptor = cipher.encryptor()
182
+ return encryptor.update(_pkcs7_pad(plaintext)) + encryptor.finalize()
183
+
184
+
185
+ def _aes128_ecb_decrypt(ciphertext: bytes, key: bytes) -> bytes:
186
+ cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
187
+ decryptor = cipher.decryptor()
188
+ padded = decryptor.update(ciphertext) + decryptor.finalize()
189
+ if not padded:
190
+ return padded
191
+ pad_len = padded[-1]
192
+ if 1 <= pad_len <= 16 and padded.endswith(bytes([pad_len]) * pad_len):
193
+ return padded[:-pad_len]
194
+ return padded
195
+
196
+
197
+ def _aes_padded_size(size: int) -> int:
198
+ return ((size + 1 + 15) // 16) * 16
199
+
200
+
201
+ def _random_wechat_uin() -> str:
202
+ value = struct.unpack(">I", secrets.token_bytes(4))[0]
203
+ return base64.b64encode(str(value).encode("utf-8")).decode("ascii")
204
+
205
+
206
+ def _base_info() -> Dict[str, Any]:
207
+ return {"channel_version": CHANNEL_VERSION}
208
+
209
+
210
+ def _headers(token: Optional[str], body: str) -> Dict[str, str]:
211
+ headers = {
212
+ "Content-Type": "application/json",
213
+ "AuthorizationType": "ilink_bot_token",
214
+ "Content-Length": str(len(body.encode("utf-8"))),
215
+ "X-WECHAT-UIN": _random_wechat_uin(),
216
+ "iLink-App-Id": ILINK_APP_ID,
217
+ "iLink-App-ClientVersion": str(ILINK_APP_CLIENT_VERSION),
218
+ }
219
+ if token:
220
+ headers["Authorization"] = f"Bearer {token}"
221
+ return headers
222
+
223
+
224
+ def _account_dir(hermes_home: str) -> Path:
225
+ path = Path(hermes_home) / "weixin" / "accounts"
226
+ path.mkdir(parents=True, exist_ok=True)
227
+ return path
228
+
229
+
230
+ def _account_file(hermes_home: str, account_id: str) -> Path:
231
+ return _account_dir(hermes_home) / f"{account_id}.json"
232
+
233
+
234
+ def save_weixin_account(
235
+ hermes_home: str,
236
+ *,
237
+ account_id: str,
238
+ token: str,
239
+ base_url: str,
240
+ user_id: str = "",
241
+ ) -> None:
242
+ """Persist account credentials for later reuse."""
243
+ payload = {
244
+ "token": token,
245
+ "base_url": base_url,
246
+ "user_id": user_id,
247
+ "saved_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
248
+ }
249
+ path = _account_file(hermes_home, account_id)
250
+ atomic_json_write(path, payload)
251
+ try:
252
+ path.chmod(0o600)
253
+ except OSError:
254
+ pass
255
+
256
+
257
+ def load_weixin_account(hermes_home: str, account_id: str) -> Optional[Dict[str, Any]]:
258
+ """Load persisted account credentials."""
259
+ path = _account_file(hermes_home, account_id)
260
+ if not path.exists():
261
+ return None
262
+ try:
263
+ return json.loads(path.read_text(encoding="utf-8"))
264
+ except Exception:
265
+ return None
266
+
267
+
268
+ class ContextTokenStore:
269
+ """Disk-backed ``context_token`` cache keyed by account + peer."""
270
+
271
+ def __init__(self, hermes_home: str):
272
+ self._root = _account_dir(hermes_home)
273
+ self._cache: Dict[str, str] = {}
274
+
275
+ def _path(self, account_id: str) -> Path:
276
+ return self._root / f"{account_id}.context-tokens.json"
277
+
278
+ def _key(self, account_id: str, user_id: str) -> str:
279
+ return f"{account_id}:{user_id}"
280
+
281
+ def restore(self, account_id: str) -> None:
282
+ path = self._path(account_id)
283
+ if not path.exists():
284
+ return
285
+ try:
286
+ data = json.loads(path.read_text(encoding="utf-8"))
287
+ except Exception as exc:
288
+ logger.warning("weixin: failed to restore context tokens for %s: %s", _safe_id(account_id), exc)
289
+ return
290
+ restored = 0
291
+ for user_id, token in data.items():
292
+ if isinstance(token, str) and token:
293
+ self._cache[self._key(account_id, user_id)] = token
294
+ restored += 1
295
+ if restored:
296
+ logger.info("weixin: restored %d context token(s) for %s", restored, _safe_id(account_id))
297
+
298
+ def get(self, account_id: str, user_id: str) -> Optional[str]:
299
+ return self._cache.get(self._key(account_id, user_id))
300
+
301
+ def set(self, account_id: str, user_id: str, token: str) -> None:
302
+ self._cache[self._key(account_id, user_id)] = token
303
+ self._persist(account_id)
304
+
305
+ def _persist(self, account_id: str) -> None:
306
+ prefix = f"{account_id}:"
307
+ payload = {
308
+ key[len(prefix) :]: value
309
+ for key, value in self._cache.items()
310
+ if key.startswith(prefix)
311
+ }
312
+ try:
313
+ atomic_json_write(self._path(account_id), payload)
314
+ except Exception as exc:
315
+ logger.warning("weixin: failed to persist context tokens for %s: %s", _safe_id(account_id), exc)
316
+
317
+
318
+ class TypingTicketCache:
319
+ """Short-lived typing ticket cache from ``getconfig``."""
320
+
321
+ def __init__(self, ttl_seconds: float = 600.0):
322
+ self._ttl_seconds = ttl_seconds
323
+ self._cache: Dict[str, Tuple[str, float]] = {}
324
+
325
+ def get(self, user_id: str) -> Optional[str]:
326
+ entry = self._cache.get(user_id)
327
+ if not entry:
328
+ return None
329
+ if time.time() - entry[1] >= self._ttl_seconds:
330
+ self._cache.pop(user_id, None)
331
+ return None
332
+ return entry[0]
333
+
334
+ def set(self, user_id: str, ticket: str) -> None:
335
+ self._cache[user_id] = (ticket, time.time())
336
+
337
+
338
+ def _cdn_download_url(cdn_base_url: str, encrypted_query_param: str) -> str:
339
+ return f"{cdn_base_url.rstrip('/')}/download?encrypted_query_param={quote(encrypted_query_param, safe='')}"
340
+
341
+
342
+ def _cdn_upload_url(cdn_base_url: str, upload_param: str, filekey: str) -> str:
343
+ return (
344
+ f"{cdn_base_url.rstrip('/')}/upload"
345
+ f"?encrypted_query_param={quote(upload_param, safe='')}"
346
+ f"&filekey={quote(filekey, safe='')}"
347
+ )
348
+
349
+
350
+ def _parse_aes_key(aes_key_b64: str) -> bytes:
351
+ decoded = base64.b64decode(aes_key_b64)
352
+ if len(decoded) == 16:
353
+ return decoded
354
+ if len(decoded) == 32:
355
+ text = decoded.decode("ascii", errors="ignore")
356
+ if text and all(ch in "0123456789abcdefABCDEF" for ch in text):
357
+ return bytes.fromhex(text)
358
+ raise ValueError(f"unexpected aes_key format ({len(decoded)} decoded bytes)")
359
+
360
+
361
+ def _guess_chat_type(message: Dict[str, Any], account_id: str) -> Tuple[str, str]:
362
+ room_id = str(message.get("room_id") or message.get("chat_room_id") or "").strip()
363
+ to_user_id = str(message.get("to_user_id") or "").strip()
364
+ is_group = bool(room_id) or (to_user_id and account_id and to_user_id != account_id and message.get("msg_type") == 1)
365
+ if is_group:
366
+ return "group", room_id or to_user_id or str(message.get("from_user_id") or "")
367
+ return "dm", str(message.get("from_user_id") or "")
368
+
369
+
370
+ async def _api_post(
371
+ session: "aiohttp.ClientSession",
372
+ *,
373
+ base_url: str,
374
+ endpoint: str,
375
+ payload: Dict[str, Any],
376
+ token: Optional[str],
377
+ timeout_ms: int,
378
+ ) -> Dict[str, Any]:
379
+ body = _json_dumps({**payload, "base_info": _base_info()})
380
+ url = f"{base_url.rstrip('/')}/{endpoint}"
381
+ timeout = aiohttp.ClientTimeout(total=timeout_ms / 1000)
382
+ async with session.post(url, data=body, headers=_headers(token, body), timeout=timeout) as response:
383
+ raw = await response.text()
384
+ if not response.ok:
385
+ raise RuntimeError(f"iLink POST {endpoint} HTTP {response.status}: {raw[:200]}")
386
+ return json.loads(raw)
387
+
388
+
389
+ async def _api_get(
390
+ session: "aiohttp.ClientSession",
391
+ *,
392
+ base_url: str,
393
+ endpoint: str,
394
+ timeout_ms: int,
395
+ ) -> Dict[str, Any]:
396
+ url = f"{base_url.rstrip('/')}/{endpoint}"
397
+ headers = {
398
+ "iLink-App-Id": ILINK_APP_ID,
399
+ "iLink-App-ClientVersion": str(ILINK_APP_CLIENT_VERSION),
400
+ }
401
+ timeout = aiohttp.ClientTimeout(total=timeout_ms / 1000)
402
+ async with session.get(url, headers=headers, timeout=timeout) as response:
403
+ raw = await response.text()
404
+ if not response.ok:
405
+ raise RuntimeError(f"iLink GET {endpoint} HTTP {response.status}: {raw[:200]}")
406
+ return json.loads(raw)
407
+
408
+
409
+ async def _get_updates(
410
+ session: "aiohttp.ClientSession",
411
+ *,
412
+ base_url: str,
413
+ token: str,
414
+ sync_buf: str,
415
+ timeout_ms: int,
416
+ ) -> Dict[str, Any]:
417
+ try:
418
+ return await _api_post(
419
+ session,
420
+ base_url=base_url,
421
+ endpoint=EP_GET_UPDATES,
422
+ payload={"get_updates_buf": sync_buf},
423
+ token=token,
424
+ timeout_ms=timeout_ms,
425
+ )
426
+ except asyncio.TimeoutError:
427
+ return {"ret": 0, "msgs": [], "get_updates_buf": sync_buf}
428
+
429
+
430
+ async def _send_message(
431
+ session: "aiohttp.ClientSession",
432
+ *,
433
+ base_url: str,
434
+ token: str,
435
+ to: str,
436
+ text: str,
437
+ context_token: Optional[str],
438
+ client_id: str,
439
+ ) -> Dict[str, Any]:
440
+ """Send a text message via iLink sendmessage API.
441
+
442
+ Returns the raw API response dict (may contain error codes like
443
+ ``errcode: -14`` for session expiry that the caller can inspect).
444
+ """
445
+ if not text or not text.strip():
446
+ raise ValueError("_send_message: text must not be empty")
447
+ message: Dict[str, Any] = {
448
+ "from_user_id": "",
449
+ "to_user_id": to,
450
+ "client_id": client_id,
451
+ "message_type": MSG_TYPE_BOT,
452
+ "message_state": MSG_STATE_FINISH,
453
+ "item_list": [{"type": ITEM_TEXT, "text_item": {"text": text}}],
454
+ }
455
+ if context_token:
456
+ message["context_token"] = context_token
457
+ return await _api_post(
458
+ session,
459
+ base_url=base_url,
460
+ endpoint=EP_SEND_MESSAGE,
461
+ payload={"msg": message},
462
+ token=token,
463
+ timeout_ms=API_TIMEOUT_MS,
464
+ )
465
+
466
+
467
+ async def _send_typing(
468
+ session: "aiohttp.ClientSession",
469
+ *,
470
+ base_url: str,
471
+ token: str,
472
+ to_user_id: str,
473
+ typing_ticket: str,
474
+ status: int,
475
+ ) -> None:
476
+ await _api_post(
477
+ session,
478
+ base_url=base_url,
479
+ endpoint=EP_SEND_TYPING,
480
+ payload={
481
+ "ilink_user_id": to_user_id,
482
+ "typing_ticket": typing_ticket,
483
+ "status": status,
484
+ },
485
+ token=token,
486
+ timeout_ms=CONFIG_TIMEOUT_MS,
487
+ )
488
+
489
+
490
+ async def _get_config(
491
+ session: "aiohttp.ClientSession",
492
+ *,
493
+ base_url: str,
494
+ token: str,
495
+ user_id: str,
496
+ context_token: Optional[str],
497
+ ) -> Dict[str, Any]:
498
+ payload: Dict[str, Any] = {"ilink_user_id": user_id}
499
+ if context_token:
500
+ payload["context_token"] = context_token
501
+ return await _api_post(
502
+ session,
503
+ base_url=base_url,
504
+ endpoint=EP_GET_CONFIG,
505
+ payload=payload,
506
+ token=token,
507
+ timeout_ms=CONFIG_TIMEOUT_MS,
508
+ )
509
+
510
+
511
+ async def _get_upload_url(
512
+ session: "aiohttp.ClientSession",
513
+ *,
514
+ base_url: str,
515
+ token: str,
516
+ to_user_id: str,
517
+ media_type: int,
518
+ filekey: str,
519
+ rawsize: int,
520
+ rawfilemd5: str,
521
+ filesize: int,
522
+ aeskey_hex: str,
523
+ ) -> Dict[str, Any]:
524
+ return await _api_post(
525
+ session,
526
+ base_url=base_url,
527
+ endpoint=EP_GET_UPLOAD_URL,
528
+ payload={
529
+ "filekey": filekey,
530
+ "media_type": media_type,
531
+ "to_user_id": to_user_id,
532
+ "rawsize": rawsize,
533
+ "rawfilemd5": rawfilemd5,
534
+ "filesize": filesize,
535
+ "no_need_thumb": True,
536
+ "aeskey": aeskey_hex,
537
+ },
538
+ token=token,
539
+ timeout_ms=API_TIMEOUT_MS,
540
+ )
541
+
542
+
543
+ async def _upload_ciphertext(
544
+ session: "aiohttp.ClientSession",
545
+ *,
546
+ ciphertext: bytes,
547
+ upload_url: str,
548
+ ) -> str:
549
+ """Upload encrypted media to the CDN.
550
+
551
+ Accepts either a constructed CDN URL (from upload_param) or a direct
552
+ upload_full_url — both use POST with the raw ciphertext as the body.
553
+ """
554
+ # Use asyncio.wait_for() instead of aiohttp ClientTimeout to avoid
555
+ # "Timeout context manager should be used inside a task" errors when
556
+ # invoked via asyncio.run_coroutine_threadsafe() from cron jobs.
557
+ async def _do_upload() -> str:
558
+ async with session.post(upload_url, data=ciphertext, headers={"Content-Type": "application/octet-stream"}) as response:
559
+ if response.status == 200:
560
+ encrypted_param = response.headers.get("x-encrypted-param")
561
+ if encrypted_param:
562
+ await response.read()
563
+ return encrypted_param
564
+ raw = await response.text()
565
+ raise RuntimeError(f"CDN upload missing x-encrypted-param header: {raw[:200]}")
566
+ raw = await response.text()
567
+ raise RuntimeError(f"CDN upload HTTP {response.status}: {raw[:200]}")
568
+ return await asyncio.wait_for(_do_upload(), timeout=120)
569
+
570
+
571
+ async def _download_bytes(
572
+ session: "aiohttp.ClientSession",
573
+ *,
574
+ url: str,
575
+ timeout_seconds: float = 60.0,
576
+ ) -> bytes:
577
+ # Use asyncio.wait_for() instead of aiohttp ClientTimeout to avoid
578
+ # "Timeout context manager should be used inside a task" errors.
579
+ async def _do_download() -> bytes:
580
+ async with session.get(url) as response:
581
+ response.raise_for_status()
582
+ return await response.read()
583
+ return await asyncio.wait_for(_do_download(), timeout=timeout_seconds)
584
+
585
+
586
+ _WEIXIN_CDN_ALLOWLIST: frozenset[str] = frozenset(
587
+ {
588
+ "novac2c.cdn.weixin.qq.com",
589
+ "ilinkai.weixin.qq.com",
590
+ "wx.qlogo.cn",
591
+ "thirdwx.qlogo.cn",
592
+ "res.wx.qq.com",
593
+ "mmbiz.qpic.cn",
594
+ "mmbiz.qlogo.cn",
595
+ }
596
+ )
597
+
598
+
599
+ def _assert_weixin_cdn_url(url: str) -> None:
600
+ """Raise ValueError if *url* does not point at a known WeChat CDN host."""
601
+ try:
602
+ parsed = urlparse(url)
603
+ scheme = parsed.scheme.lower()
604
+ host = parsed.hostname or ""
605
+ except Exception as exc: # noqa: BLE001
606
+ raise ValueError(f"Unparseable media URL: {url!r}") from exc
607
+
608
+ if scheme not in {"http", "https"}:
609
+ raise ValueError(
610
+ f"Media URL has disallowed scheme {scheme!r}; only http/https are permitted."
611
+ )
612
+ if host not in _WEIXIN_CDN_ALLOWLIST:
613
+ raise ValueError(
614
+ f"Media URL host {host!r} is not in the WeChat CDN allowlist. "
615
+ "Refusing to fetch to prevent SSRF."
616
+ )
617
+
618
+
619
+ def _media_reference(item: Dict[str, Any], key: str) -> Dict[str, Any]:
620
+ return (item.get(key) or {}).get("media") or {}
621
+
622
+
623
+ async def _download_and_decrypt_media(
624
+ session: "aiohttp.ClientSession",
625
+ *,
626
+ cdn_base_url: str,
627
+ encrypted_query_param: Optional[str],
628
+ aes_key_b64: Optional[str],
629
+ full_url: Optional[str],
630
+ timeout_seconds: float,
631
+ ) -> bytes:
632
+ if encrypted_query_param:
633
+ raw = await _download_bytes(
634
+ session,
635
+ url=_cdn_download_url(cdn_base_url, encrypted_query_param),
636
+ timeout_seconds=timeout_seconds,
637
+ )
638
+ elif full_url:
639
+ _assert_weixin_cdn_url(full_url)
640
+ raw = await _download_bytes(session, url=full_url, timeout_seconds=timeout_seconds)
641
+ else:
642
+ raise RuntimeError("media item had neither encrypt_query_param nor full_url")
643
+ if aes_key_b64:
644
+ raw = _aes128_ecb_decrypt(raw, _parse_aes_key(aes_key_b64))
645
+ return raw
646
+
647
+
648
+ def _mime_from_filename(filename: str) -> str:
649
+ return mimetypes.guess_type(filename)[0] or "application/octet-stream"
650
+
651
+
652
+ def _split_table_row(line: str) -> List[str]:
653
+ row = line.strip()
654
+ if row.startswith("|"):
655
+ row = row[1:]
656
+ if row.endswith("|"):
657
+ row = row[:-1]
658
+ return [cell.strip() for cell in row.split("|")]
659
+
660
+
661
+ def _rewrite_headers_for_weixin(line: str) -> str:
662
+ match = _HEADER_RE.match(line)
663
+ if not match:
664
+ return line.rstrip()
665
+ level = len(match.group(1))
666
+ title = match.group(2).strip()
667
+ if level == 1:
668
+ return f"【{title}】"
669
+ return f"**{title}**"
670
+
671
+
672
+ def _rewrite_table_block_for_weixin(lines: List[str]) -> str:
673
+ if len(lines) < 2:
674
+ return "\n".join(lines)
675
+ headers = _split_table_row(lines[0])
676
+ body_rows = [_split_table_row(line) for line in lines[2:] if line.strip()]
677
+ if not headers or not body_rows:
678
+ return "\n".join(lines)
679
+
680
+ formatted_rows: List[str] = []
681
+ for row in body_rows:
682
+ pairs = []
683
+ for idx, header in enumerate(headers):
684
+ if idx >= len(row):
685
+ break
686
+ label = header or f"Column {idx + 1}"
687
+ value = row[idx].strip()
688
+ if value:
689
+ pairs.append((label, value))
690
+ if not pairs:
691
+ continue
692
+ if len(pairs) == 1:
693
+ label, value = pairs[0]
694
+ formatted_rows.append(f"- {label}: {value}")
695
+ continue
696
+ if len(pairs) == 2:
697
+ label, value = pairs[0]
698
+ other_label, other_value = pairs[1]
699
+ formatted_rows.append(f"- {label}: {value}")
700
+ formatted_rows.append(f" {other_label}: {other_value}")
701
+ continue
702
+ summary = " | ".join(f"{label}: {value}" for label, value in pairs)
703
+ formatted_rows.append(f"- {summary}")
704
+ return "\n".join(formatted_rows) if formatted_rows else "\n".join(lines)
705
+
706
+
707
+ def _normalize_markdown_blocks(content: str) -> str:
708
+ lines = content.splitlines()
709
+ result: List[str] = []
710
+ in_code_block = False
711
+ blank_run = 0
712
+
713
+ for raw_line in lines:
714
+ line = raw_line.rstrip()
715
+ if _FENCE_RE.match(line.strip()):
716
+ in_code_block = not in_code_block
717
+ result.append(line)
718
+ blank_run = 0
719
+ continue
720
+
721
+ if in_code_block:
722
+ result.append(line)
723
+ continue
724
+
725
+ if not line.strip():
726
+ blank_run += 1
727
+ if blank_run <= 1:
728
+ result.append("")
729
+ continue
730
+
731
+ blank_run = 0
732
+ result.append(line)
733
+
734
+ return "\n".join(result).strip()
735
+
736
+
737
+ def _wrap_copy_friendly_lines_for_weixin(content: str) -> str:
738
+ """Wrap long display lines that are hard to copy in WeChat clients."""
739
+ if not content:
740
+ return content
741
+
742
+ wrapped: List[str] = []
743
+ in_code_block = False
744
+
745
+ for raw_line in content.splitlines():
746
+ line = raw_line.rstrip()
747
+ stripped = line.strip()
748
+
749
+ if _FENCE_RE.match(stripped):
750
+ in_code_block = not in_code_block
751
+ wrapped.append(line)
752
+ continue
753
+
754
+ if (
755
+ in_code_block
756
+ or len(line) <= WEIXIN_COPY_LINE_WIDTH
757
+ or not stripped
758
+ or stripped.startswith("|")
759
+ or _TABLE_RULE_RE.match(stripped)
760
+ ):
761
+ wrapped.append(line)
762
+ continue
763
+
764
+ wrapped_lines = textwrap.wrap(
765
+ line,
766
+ width=WEIXIN_COPY_LINE_WIDTH,
767
+ break_long_words=False,
768
+ break_on_hyphens=False,
769
+ replace_whitespace=False,
770
+ drop_whitespace=True,
771
+ )
772
+ wrapped.extend(wrapped_lines or [line])
773
+
774
+ return "\n".join(wrapped).strip()
775
+
776
+
777
+ def _split_markdown_blocks(content: str) -> List[str]:
778
+ if not content:
779
+ return []
780
+
781
+ blocks: List[str] = []
782
+ lines = content.splitlines()
783
+ current: List[str] = []
784
+ in_code_block = False
785
+
786
+ for raw_line in lines:
787
+ line = raw_line.rstrip()
788
+ if _FENCE_RE.match(line.strip()):
789
+ if not in_code_block and current:
790
+ blocks.append("\n".join(current).strip())
791
+ current = []
792
+ current.append(line)
793
+ in_code_block = not in_code_block
794
+ if not in_code_block:
795
+ blocks.append("\n".join(current).strip())
796
+ current = []
797
+ continue
798
+
799
+ if in_code_block:
800
+ current.append(line)
801
+ continue
802
+
803
+ if not line.strip():
804
+ if current:
805
+ blocks.append("\n".join(current).strip())
806
+ current = []
807
+ continue
808
+ current.append(line)
809
+
810
+ if current:
811
+ blocks.append("\n".join(current).strip())
812
+ return [block for block in blocks if block]
813
+
814
+
815
+ def _split_delivery_units_for_weixin(content: str) -> List[str]:
816
+ """Split formatted content into chat-friendly delivery units.
817
+
818
+ Weixin can render Markdown, but chat readability is better when top-level
819
+ line breaks become separate messages. Keep fenced code blocks intact and
820
+ attach indented continuation lines to the previous top-level line so nested
821
+ list items do not get torn apart.
822
+ """
823
+ units: List[str] = []
824
+
825
+ for block in _split_markdown_blocks(content):
826
+ if _FENCE_RE.match(block.splitlines()[0].strip()):
827
+ units.append(block)
828
+ continue
829
+
830
+ current: List[str] = []
831
+ for raw_line in block.splitlines():
832
+ line = raw_line.rstrip()
833
+ if not line.strip():
834
+ if current:
835
+ units.append("\n".join(current).strip())
836
+ current = []
837
+ continue
838
+
839
+ is_continuation = bool(current) and raw_line.startswith((" ", "\t"))
840
+ if is_continuation:
841
+ current.append(line)
842
+ continue
843
+
844
+ if current:
845
+ units.append("\n".join(current).strip())
846
+ current = [line]
847
+
848
+ if current:
849
+ units.append("\n".join(current).strip())
850
+
851
+ return [unit for unit in units if unit]
852
+
853
+
854
+ def _looks_like_chatty_line_for_weixin(line: str) -> bool:
855
+ """Return True when a line looks like a standalone chat utterance."""
856
+ stripped = line.strip()
857
+ if not stripped:
858
+ return False
859
+ if len(stripped) > 48:
860
+ return False
861
+ if line.startswith((" ", "\t")):
862
+ return False
863
+ if stripped.startswith((">", "-", "*", "【", "#", "|")):
864
+ return False
865
+ if _TABLE_RULE_RE.match(stripped):
866
+ return False
867
+ if re.match(r"^\*\*[^*]+\*\*$", stripped):
868
+ return False
869
+ if re.match(r"^\d+\.\s", stripped):
870
+ return False
871
+ return True
872
+
873
+
874
+ def _looks_like_heading_line_for_weixin(line: str) -> bool:
875
+ """Return True when a short line behaves like a heading."""
876
+ stripped = line.strip()
877
+ if not stripped:
878
+ return False
879
+ if _HEADER_RE.match(stripped):
880
+ return True
881
+ return len(stripped) <= 24 and stripped.endswith((":", ":"))
882
+
883
+
884
+ def _should_split_short_chat_block_for_weixin(block: str) -> bool:
885
+ """Split only chat-like multiline blocks into separate bubbles."""
886
+ lines = [line for line in block.splitlines() if line.strip()]
887
+ if not 2 <= len(lines) <= 6:
888
+ return False
889
+ if _looks_like_heading_line_for_weixin(lines[0]):
890
+ return False
891
+ return all(_looks_like_chatty_line_for_weixin(line) for line in lines)
892
+
893
+
894
+ def _pack_markdown_blocks_for_weixin(content: str, max_length: int) -> List[str]:
895
+ if len(content) <= max_length:
896
+ return [content]
897
+
898
+ packed: List[str] = []
899
+ current = ""
900
+ for block in _split_markdown_blocks(content):
901
+ candidate = block if not current else f"{current}\n\n{block}"
902
+ if len(candidate) <= max_length:
903
+ current = candidate
904
+ continue
905
+ if current:
906
+ packed.append(current)
907
+ current = ""
908
+ if len(block) <= max_length:
909
+ current = block
910
+ continue
911
+ packed.extend(BasePlatformAdapter.truncate_message(block, max_length))
912
+ if current:
913
+ packed.append(current)
914
+ return packed
915
+
916
+
917
+ def _split_text_for_weixin_delivery(
918
+ content: str, max_length: int, split_per_line: bool = False,
919
+ ) -> List[str]:
920
+ """Split content into sequential Weixin messages.
921
+
922
+ *compact* (default): Keep everything in a single message whenever it fits
923
+ within the platform limit, even when the author used explicit line breaks.
924
+ Only fall back to block-aware packing when the payload exceeds
925
+ ``max_length``.
926
+
927
+ *per_line* (``split_per_line=True``): Legacy behavior — top-level line
928
+ breaks become separate chat messages; oversized units still use
929
+ block-aware packing.
930
+
931
+ The active mode is controlled via ``config.yaml`` ->
932
+ ``platforms.weixin.extra.split_multiline_messages`` (``true`` / ``false``)
933
+ or the env var ``WEIXIN_SPLIT_MULTILINE_MESSAGES``.
934
+ """
935
+ if not content:
936
+ return []
937
+ if split_per_line:
938
+ # Legacy: one message per top-level delivery unit.
939
+ if len(content) <= max_length and "\n" not in content:
940
+ return [content]
941
+ chunks: List[str] = []
942
+ for unit in _split_delivery_units_for_weixin(content):
943
+ if len(unit) <= max_length:
944
+ chunks.append(unit)
945
+ continue
946
+ chunks.extend(_pack_markdown_blocks_for_weixin(unit, max_length))
947
+ return [c for c in chunks if c] or [content]
948
+
949
+ # Compact (default): single message when under the limit — unless the
950
+ # content looks like a short chatty exchange, in which case split into
951
+ # separate bubbles for a more natural chat feel.
952
+ if len(content) <= max_length:
953
+ return (
954
+ [u for u in _split_delivery_units_for_weixin(content) if u]
955
+ if _should_split_short_chat_block_for_weixin(content)
956
+ else [content]
957
+ )
958
+ return _pack_markdown_blocks_for_weixin(content, max_length) or [content]
959
+
960
+
961
+ def _coerce_bool(value: Any, default: bool = True) -> bool:
962
+ """Coerce a config value to bool, tolerating strings like ``"true"``."""
963
+ if value is None:
964
+ return default
965
+ if isinstance(value, bool):
966
+ return value
967
+ if isinstance(value, (int, float)):
968
+ return bool(value)
969
+ text = str(value).strip().lower()
970
+ if not text:
971
+ return default
972
+ if text in {"1", "true", "yes", "on"}:
973
+ return True
974
+ if text in {"0", "false", "no", "off"}:
975
+ return False
976
+ return default
977
+
978
+
979
+ def _extract_text(item_list: List[Dict[str, Any]]) -> str:
980
+ for item in item_list:
981
+ if item.get("type") == ITEM_TEXT:
982
+ text = str((item.get("text_item") or {}).get("text") or "")
983
+ ref = item.get("ref_msg") or {}
984
+ ref_item = ref.get("message_item") or {}
985
+ ref_type = ref_item.get("type")
986
+ if ref_type in {ITEM_IMAGE, ITEM_VIDEO, ITEM_FILE, ITEM_VOICE}:
987
+ title = ref.get("title") or ""
988
+ prefix = f"[引用媒体: {title}]\n" if title else "[引用媒体]\n"
989
+ return f"{prefix}{text}".strip()
990
+ if ref_item:
991
+ parts: List[str] = []
992
+ if ref.get("title"):
993
+ parts.append(str(ref["title"]))
994
+ ref_text = _extract_text([ref_item])
995
+ if ref_text:
996
+ parts.append(ref_text)
997
+ if parts:
998
+ return f"[引用: {' | '.join(parts)}]\n{text}".strip()
999
+ return text
1000
+ for item in item_list:
1001
+ if item.get("type") == ITEM_VOICE:
1002
+ voice_text = str((item.get("voice_item") or {}).get("text") or "")
1003
+ if voice_text:
1004
+ return voice_text
1005
+ return ""
1006
+
1007
+
1008
+ def _message_type_from_media(media_types: List[str], text: str) -> MessageType:
1009
+ if any(m.startswith("image/") for m in media_types):
1010
+ return MessageType.PHOTO
1011
+ if any(m.startswith("video/") for m in media_types):
1012
+ return MessageType.VIDEO
1013
+ if any(m.startswith("audio/") for m in media_types):
1014
+ return MessageType.VOICE
1015
+ if media_types:
1016
+ return MessageType.DOCUMENT
1017
+ if text.startswith("/"):
1018
+ return MessageType.COMMAND
1019
+ return MessageType.TEXT
1020
+
1021
+
1022
+ def _sync_buf_path(hermes_home: str, account_id: str) -> Path:
1023
+ return _account_dir(hermes_home) / f"{account_id}.sync.json"
1024
+
1025
+
1026
+ def _load_sync_buf(hermes_home: str, account_id: str) -> str:
1027
+ path = _sync_buf_path(hermes_home, account_id)
1028
+ if not path.exists():
1029
+ return ""
1030
+ try:
1031
+ return json.loads(path.read_text(encoding="utf-8")).get("get_updates_buf", "")
1032
+ except Exception:
1033
+ return ""
1034
+
1035
+
1036
+ def _save_sync_buf(hermes_home: str, account_id: str, sync_buf: str) -> None:
1037
+ path = _sync_buf_path(hermes_home, account_id)
1038
+ atomic_json_write(path, {"get_updates_buf": sync_buf})
1039
+
1040
+
1041
+ async def qr_login(
1042
+ hermes_home: str,
1043
+ *,
1044
+ bot_type: str = "3",
1045
+ timeout_seconds: int = 480,
1046
+ ) -> Optional[Dict[str, str]]:
1047
+ """
1048
+ Run the interactive iLink QR login flow.
1049
+
1050
+ Returns a credential dict on success, or ``None`` if login fails or times out.
1051
+ """
1052
+ if not AIOHTTP_AVAILABLE:
1053
+ raise RuntimeError("aiohttp is required for Weixin QR login")
1054
+
1055
+ async with aiohttp.ClientSession(trust_env=True, connector=_make_ssl_connector()) as session:
1056
+ try:
1057
+ qr_resp = await _api_get(
1058
+ session,
1059
+ base_url=ILINK_BASE_URL,
1060
+ endpoint=f"{EP_GET_BOT_QR}?bot_type={bot_type}",
1061
+ timeout_ms=QR_TIMEOUT_MS,
1062
+ )
1063
+ except Exception as exc:
1064
+ logger.error("weixin: failed to fetch QR code: %s", exc)
1065
+ return None
1066
+
1067
+ qrcode_value = str(qr_resp.get("qrcode") or "")
1068
+ qrcode_url = str(qr_resp.get("qrcode_img_content") or "")
1069
+ if not qrcode_value:
1070
+ logger.error("weixin: QR response missing qrcode")
1071
+ return None
1072
+
1073
+ # qrcode_url is the full scannable liteapp URL; qrcode_value is just the hex token
1074
+ # WeChat needs to scan the full URL, not the raw hex string
1075
+ qr_scan_data = qrcode_url if qrcode_url else qrcode_value
1076
+
1077
+ print("\n请使用微信扫描以下二维码:")
1078
+ if qrcode_url:
1079
+ print(qrcode_url)
1080
+ try:
1081
+ import qrcode
1082
+
1083
+ qr = qrcode.QRCode()
1084
+ qr.add_data(qr_scan_data)
1085
+ qr.make(fit=True)
1086
+ qr.print_ascii(invert=True)
1087
+ except Exception as _qr_exc:
1088
+ print(f"(终端二维码渲染失败: {_qr_exc},请直接打开上面的二维码链接)")
1089
+
1090
+ deadline = time.monotonic() + timeout_seconds
1091
+ current_base_url = ILINK_BASE_URL
1092
+ refresh_count = 0
1093
+
1094
+ while time.monotonic() < deadline:
1095
+ try:
1096
+ status_resp = await _api_get(
1097
+ session,
1098
+ base_url=current_base_url,
1099
+ endpoint=f"{EP_GET_QR_STATUS}?qrcode={qrcode_value}",
1100
+ timeout_ms=QR_TIMEOUT_MS,
1101
+ )
1102
+ except asyncio.TimeoutError:
1103
+ await asyncio.sleep(1)
1104
+ continue
1105
+ except Exception as exc:
1106
+ logger.warning("weixin: QR poll error: %s", exc)
1107
+ await asyncio.sleep(1)
1108
+ continue
1109
+
1110
+ status = str(status_resp.get("status") or "wait")
1111
+ if status == "wait":
1112
+ print(".", end="", flush=True)
1113
+ elif status == "scaned":
1114
+ print("\n已扫码,请在微信里确认...")
1115
+ elif status == "scaned_but_redirect":
1116
+ redirect_host = str(status_resp.get("redirect_host") or "")
1117
+ if redirect_host:
1118
+ current_base_url = f"https://{redirect_host}"
1119
+ elif status == "expired":
1120
+ refresh_count += 1
1121
+ if refresh_count > 3:
1122
+ print("\n二维码多次过期,请重新执行登录。")
1123
+ return None
1124
+ print(f"\n二维码已过期,正在刷新... ({refresh_count}/3)")
1125
+ try:
1126
+ qr_resp = await _api_get(
1127
+ session,
1128
+ base_url=ILINK_BASE_URL,
1129
+ endpoint=f"{EP_GET_BOT_QR}?bot_type={bot_type}",
1130
+ timeout_ms=QR_TIMEOUT_MS,
1131
+ )
1132
+ qrcode_value = str(qr_resp.get("qrcode") or "")
1133
+ qrcode_url = str(qr_resp.get("qrcode_img_content") or "")
1134
+ qr_scan_data = qrcode_url if qrcode_url else qrcode_value
1135
+ if qrcode_url:
1136
+ print(qrcode_url)
1137
+ try:
1138
+ import qrcode as _qrcode
1139
+ qr = _qrcode.QRCode()
1140
+ qr.add_data(qr_scan_data)
1141
+ qr.make(fit=True)
1142
+ qr.print_ascii(invert=True)
1143
+ except Exception:
1144
+ pass
1145
+ except Exception as exc:
1146
+ logger.error("weixin: QR refresh failed: %s", exc)
1147
+ return None
1148
+ elif status == "confirmed":
1149
+ account_id = str(status_resp.get("ilink_bot_id") or "")
1150
+ token = str(status_resp.get("bot_token") or "")
1151
+ base_url = str(status_resp.get("baseurl") or ILINK_BASE_URL)
1152
+ user_id = str(status_resp.get("ilink_user_id") or "")
1153
+ if not account_id or not token:
1154
+ logger.error("weixin: QR confirmed but credential payload was incomplete")
1155
+ return None
1156
+ save_weixin_account(
1157
+ hermes_home,
1158
+ account_id=account_id,
1159
+ token=token,
1160
+ base_url=base_url,
1161
+ user_id=user_id,
1162
+ )
1163
+ print(f"\n微信连接成功,account_id={account_id}")
1164
+ return {
1165
+ "account_id": account_id,
1166
+ "token": token,
1167
+ "base_url": base_url,
1168
+ "user_id": user_id,
1169
+ }
1170
+ await asyncio.sleep(1)
1171
+
1172
+ print("\n微信登录超时。")
1173
+ return None
1174
+
1175
+
1176
+ class WeixinAdapter(BasePlatformAdapter):
1177
+ """Native Hermes adapter for Weixin personal accounts."""
1178
+
1179
+ MAX_MESSAGE_LENGTH = 2000
1180
+
1181
+ # WeChat does not support editing sent messages — streaming must use the
1182
+ # fallback "send-final-only" path so the cursor (▉) is never left visible.
1183
+ SUPPORTS_MESSAGE_EDITING = False
1184
+
1185
+ def __init__(self, config: PlatformConfig):
1186
+ super().__init__(config, Platform.WEIXIN)
1187
+ extra = config.extra or {}
1188
+ hermes_home = str(get_hermes_home())
1189
+ self._hermes_home = hermes_home
1190
+ self._token_store = ContextTokenStore(hermes_home)
1191
+ self._typing_cache = TypingTicketCache()
1192
+ self._poll_session: Optional[aiohttp.ClientSession] = None
1193
+ self._send_session: Optional[aiohttp.ClientSession] = None
1194
+ self._poll_task: Optional[asyncio.Task] = None
1195
+ self._dedup = MessageDeduplicator(ttl_seconds=MESSAGE_DEDUP_TTL_SECONDS)
1196
+
1197
+ self._account_id = str(extra.get("account_id") or os.getenv("WEIXIN_ACCOUNT_ID", "")).strip()
1198
+ self._token = str(config.token or extra.get("token") or os.getenv("WEIXIN_TOKEN", "")).strip()
1199
+ self._base_url = str(extra.get("base_url") or os.getenv("WEIXIN_BASE_URL", ILINK_BASE_URL)).strip().rstrip("/")
1200
+ self._cdn_base_url = str(
1201
+ extra.get("cdn_base_url") or os.getenv("WEIXIN_CDN_BASE_URL", WEIXIN_CDN_BASE_URL)
1202
+ ).strip().rstrip("/")
1203
+ self._send_chunk_delay_seconds = float(
1204
+ extra.get("send_chunk_delay_seconds") or os.getenv("WEIXIN_SEND_CHUNK_DELAY_SECONDS", "1.5")
1205
+ )
1206
+ self._send_chunk_retries = int(
1207
+ extra.get("send_chunk_retries") or os.getenv("WEIXIN_SEND_CHUNK_RETRIES", "4")
1208
+ )
1209
+ self._send_chunk_retry_delay_seconds = float(
1210
+ extra.get("send_chunk_retry_delay_seconds")
1211
+ or os.getenv("WEIXIN_SEND_CHUNK_RETRY_DELAY_SECONDS", "1.0")
1212
+ )
1213
+ self._dm_policy = str(extra.get("dm_policy") or os.getenv("WEIXIN_DM_POLICY", "open")).strip().lower()
1214
+ self._group_policy = str(extra.get("group_policy") or os.getenv("WEIXIN_GROUP_POLICY", "disabled")).strip().lower()
1215
+ allow_from = extra.get("allow_from")
1216
+ if allow_from is None:
1217
+ allow_from = os.getenv("WEIXIN_ALLOWED_USERS", "")
1218
+ group_allow_from = extra.get("group_allow_from")
1219
+ if group_allow_from is None:
1220
+ group_allow_from = os.getenv("WEIXIN_GROUP_ALLOWED_USERS", "")
1221
+ self._allow_from = self._coerce_list(allow_from)
1222
+ self._group_allow_from = self._coerce_list(group_allow_from)
1223
+ self._split_multiline_messages = _coerce_bool(
1224
+ extra.get("split_multiline_messages")
1225
+ or os.getenv("WEIXIN_SPLIT_MULTILINE_MESSAGES"),
1226
+ default=False,
1227
+ )
1228
+
1229
+ if self._account_id and not self._token:
1230
+ persisted = load_weixin_account(hermes_home, self._account_id)
1231
+ if persisted:
1232
+ self._token = str(persisted.get("token") or "").strip()
1233
+ self._base_url = str(persisted.get("base_url") or self._base_url).strip().rstrip("/")
1234
+
1235
+ @staticmethod
1236
+ def _coerce_list(value: Any) -> List[str]:
1237
+ if value is None:
1238
+ return []
1239
+ if isinstance(value, str):
1240
+ return [item.strip() for item in value.split(",") if item.strip()]
1241
+ if isinstance(value, (list, tuple, set)):
1242
+ return [str(item).strip() for item in value if str(item).strip()]
1243
+ return [str(value).strip()] if str(value).strip() else []
1244
+
1245
+ async def connect(self) -> bool:
1246
+ if not check_weixin_requirements():
1247
+ message = "Weixin startup failed: aiohttp and cryptography are required"
1248
+ self._set_fatal_error("weixin_missing_dependency", message, retryable=False)
1249
+ logger.warning("[%s] %s", self.name, message)
1250
+ return False
1251
+ if not self._token:
1252
+ message = "Weixin startup failed: WEIXIN_TOKEN is required"
1253
+ self._set_fatal_error("weixin_missing_token", message, retryable=False)
1254
+ logger.warning("[%s] %s", self.name, message)
1255
+ return False
1256
+ if not self._account_id:
1257
+ message = "Weixin startup failed: WEIXIN_ACCOUNT_ID is required"
1258
+ self._set_fatal_error("weixin_missing_account", message, retryable=False)
1259
+ logger.warning("[%s] %s", self.name, message)
1260
+ return False
1261
+
1262
+ try:
1263
+ if not self._acquire_platform_lock('weixin-bot-token', self._token, 'Weixin bot token'):
1264
+ return False
1265
+ except Exception as exc:
1266
+ logger.debug("[%s] Token lock unavailable (non-fatal): %s", self.name, exc)
1267
+
1268
+ self._poll_session = aiohttp.ClientSession(trust_env=True, connector=_make_ssl_connector())
1269
+ # Disable aiohttp's built-in ClientTimeout (total=None) to prevent
1270
+ # "Timeout context manager should be used inside a task" errors when
1271
+ # send() is invoked via asyncio.run_coroutine_threadsafe() from cron.
1272
+ # Timeout is managed externally via asyncio.wait_for() in _api_post/_api_get.
1273
+ _no_aiohttp_timeout = aiohttp.ClientTimeout(total=None, connect=None, sock_connect=None, sock_read=None)
1274
+ self._send_session = aiohttp.ClientSession(trust_env=True, connector=_make_ssl_connector(), timeout=_no_aiohttp_timeout)
1275
+ self._token_store.restore(self._account_id)
1276
+ self._poll_task = asyncio.create_task(self._poll_loop(), name="weixin-poll")
1277
+ self._mark_connected()
1278
+ _LIVE_ADAPTERS[self._token] = self
1279
+ logger.info("[%s] Connected account=%s base=%s", self.name, _safe_id(self._account_id), self._base_url)
1280
+ if self._group_policy != "disabled":
1281
+ logger.warning(
1282
+ "[%s] WEIXIN_GROUP_POLICY=%s is set, but QR-login connects an iLink bot "
1283
+ "identity (e.g. ...@im.bot) which typically cannot be invited into ordinary "
1284
+ "WeChat groups. iLink usually does not deliver ordinary-group events for "
1285
+ "these accounts, so group messages may never reach Hermes regardless of this "
1286
+ "policy. If group delivery doesn't work, the limitation is on the iLink side, "
1287
+ "not in Hermes.",
1288
+ self.name,
1289
+ self._group_policy,
1290
+ )
1291
+ return True
1292
+
1293
+ async def disconnect(self) -> None:
1294
+ _LIVE_ADAPTERS.pop(self._token, None)
1295
+ self._running = False
1296
+ if self._poll_task and not self._poll_task.done():
1297
+ self._poll_task.cancel()
1298
+ try:
1299
+ await self._poll_task
1300
+ except asyncio.CancelledError:
1301
+ pass
1302
+ self._poll_task = None
1303
+ if self._poll_session and not self._poll_session.closed:
1304
+ await self._poll_session.close()
1305
+ self._poll_session = None
1306
+ if self._send_session and not self._send_session.closed:
1307
+ await self._send_session.close()
1308
+ self._send_session = None
1309
+ self._release_platform_lock()
1310
+ self._mark_disconnected()
1311
+ logger.info("[%s] Disconnected", self.name)
1312
+
1313
+ async def _poll_loop(self) -> None:
1314
+ assert self._poll_session is not None
1315
+ sync_buf = _load_sync_buf(self._hermes_home, self._account_id)
1316
+ timeout_ms = LONG_POLL_TIMEOUT_MS
1317
+ consecutive_failures = 0
1318
+
1319
+ while self._running:
1320
+ try:
1321
+ response = await _get_updates(
1322
+ self._poll_session,
1323
+ base_url=self._base_url,
1324
+ token=self._token,
1325
+ sync_buf=sync_buf,
1326
+ timeout_ms=timeout_ms,
1327
+ )
1328
+ suggested_timeout = response.get("longpolling_timeout_ms")
1329
+ if isinstance(suggested_timeout, int) and suggested_timeout > 0:
1330
+ timeout_ms = suggested_timeout
1331
+
1332
+ ret = response.get("ret", 0)
1333
+ errcode = response.get("errcode", 0)
1334
+ if ret not in {0, None} or errcode not in {0, None}:
1335
+ if (ret == SESSION_EXPIRED_ERRCODE or errcode == SESSION_EXPIRED_ERRCODE
1336
+ or _is_stale_session_ret(ret, errcode, response.get("errmsg"))):
1337
+ logger.error("[%s] Session expired; pausing for 10 minutes", self.name)
1338
+ await asyncio.sleep(600)
1339
+ consecutive_failures = 0
1340
+ continue
1341
+ consecutive_failures += 1
1342
+ logger.warning(
1343
+ "[%s] getUpdates failed ret=%s errcode=%s errmsg=%s (%d/%d)",
1344
+ self.name,
1345
+ ret,
1346
+ errcode,
1347
+ response.get("errmsg", ""),
1348
+ consecutive_failures,
1349
+ MAX_CONSECUTIVE_FAILURES,
1350
+ )
1351
+ await asyncio.sleep(BACKOFF_DELAY_SECONDS if consecutive_failures >= MAX_CONSECUTIVE_FAILURES else RETRY_DELAY_SECONDS)
1352
+ if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
1353
+ consecutive_failures = 0
1354
+ continue
1355
+
1356
+ consecutive_failures = 0
1357
+ new_sync_buf = str(response.get("get_updates_buf") or "")
1358
+ if new_sync_buf:
1359
+ sync_buf = new_sync_buf
1360
+ _save_sync_buf(self._hermes_home, self._account_id, sync_buf)
1361
+
1362
+ for message in response.get("msgs") or []:
1363
+ asyncio.create_task(self._process_message_safe(message))
1364
+ except asyncio.CancelledError:
1365
+ break
1366
+ except Exception as exc:
1367
+ consecutive_failures += 1
1368
+ logger.error("[%s] poll error (%d/%d): %s", self.name, consecutive_failures, MAX_CONSECUTIVE_FAILURES, exc)
1369
+ await asyncio.sleep(BACKOFF_DELAY_SECONDS if consecutive_failures >= MAX_CONSECUTIVE_FAILURES else RETRY_DELAY_SECONDS)
1370
+ if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
1371
+ consecutive_failures = 0
1372
+
1373
+ async def _process_message_safe(self, message: Dict[str, Any]) -> None:
1374
+ try:
1375
+ await self._process_message(message)
1376
+ except Exception as exc:
1377
+ logger.error("[%s] unhandled inbound error from=%s: %s", self.name, _safe_id(message.get("from_user_id")), exc, exc_info=True)
1378
+
1379
+ async def _process_message(self, message: Dict[str, Any]) -> None:
1380
+ assert self._poll_session is not None
1381
+ sender_id = str(message.get("from_user_id") or "").strip()
1382
+ if not sender_id:
1383
+ return
1384
+ if sender_id == self._account_id:
1385
+ return
1386
+
1387
+ message_id = str(message.get("message_id") or "").strip()
1388
+ if message_id and self._dedup.is_duplicate(message_id):
1389
+ return
1390
+
1391
+ # Secondary content-fingerprint dedup for text messages
1392
+ item_list = message.get("item_list") or []
1393
+ text = _extract_text(item_list)
1394
+ if text:
1395
+ content_key = f"content:{sender_id}:{hashlib.md5(text.encode()).hexdigest()}"
1396
+ if self._dedup.is_duplicate(content_key):
1397
+ logger.debug("[%s] Content-dedup: skipping duplicate message from %s", self.name, sender_id)
1398
+ return
1399
+
1400
+ chat_type, effective_chat_id = _guess_chat_type(message, self._account_id)
1401
+ if chat_type == "group":
1402
+ if self._group_policy == "disabled":
1403
+ return
1404
+ if self._group_policy == "allowlist" and effective_chat_id not in self._group_allow_from:
1405
+ return
1406
+ elif not self._is_dm_allowed(sender_id):
1407
+ return
1408
+
1409
+ context_token = str(message.get("context_token") or "").strip()
1410
+ if context_token:
1411
+ self._token_store.set(self._account_id, sender_id, context_token)
1412
+ asyncio.create_task(self._maybe_fetch_typing_ticket(sender_id, context_token or None))
1413
+
1414
+ media_paths: List[str] = []
1415
+ media_types: List[str] = []
1416
+
1417
+ for item in item_list:
1418
+ await self._collect_media(item, media_paths, media_types)
1419
+ ref_message = item.get("ref_msg") or {}
1420
+ ref_item = ref_message.get("message_item")
1421
+ if isinstance(ref_item, dict):
1422
+ await self._collect_media(ref_item, media_paths, media_types)
1423
+
1424
+ if not text and not media_paths:
1425
+ return
1426
+
1427
+ source = self.build_source(
1428
+ chat_id=effective_chat_id,
1429
+ chat_type=chat_type,
1430
+ user_id=sender_id,
1431
+ user_name=sender_id,
1432
+ )
1433
+ event = MessageEvent(
1434
+ text=text,
1435
+ message_type=_message_type_from_media(media_types, text),
1436
+ source=source,
1437
+ raw_message=message,
1438
+ message_id=message_id or None,
1439
+ media_urls=media_paths,
1440
+ media_types=media_types,
1441
+ timestamp=datetime.now(),
1442
+ )
1443
+ logger.info("[%s] inbound from=%s type=%s media=%d", self.name, _safe_id(sender_id), source.chat_type, len(media_paths))
1444
+ await self.handle_message(event)
1445
+
1446
+ def _is_dm_allowed(self, sender_id: str) -> bool:
1447
+ if self._dm_policy == "disabled":
1448
+ return False
1449
+ if self._dm_policy == "allowlist":
1450
+ return sender_id in self._allow_from
1451
+ return True
1452
+
1453
+ async def _collect_media(self, item: Dict[str, Any], media_paths: List[str], media_types: List[str]) -> None:
1454
+ item_type = item.get("type")
1455
+ if item_type == ITEM_IMAGE:
1456
+ path = await self._download_image(item)
1457
+ if path:
1458
+ media_paths.append(path)
1459
+ media_types.append("image/jpeg")
1460
+ elif item_type == ITEM_VIDEO:
1461
+ path = await self._download_video(item)
1462
+ if path:
1463
+ media_paths.append(path)
1464
+ media_types.append("video/mp4")
1465
+ elif item_type == ITEM_FILE:
1466
+ path, mime = await self._download_file(item)
1467
+ if path:
1468
+ media_paths.append(path)
1469
+ media_types.append(mime)
1470
+ elif item_type == ITEM_VOICE:
1471
+ voice_path = await self._download_voice(item)
1472
+ if voice_path:
1473
+ media_paths.append(voice_path)
1474
+ media_types.append("audio/silk")
1475
+
1476
+ async def _download_image(self, item: Dict[str, Any]) -> Optional[str]:
1477
+ media = _media_reference(item, "image_item")
1478
+ try:
1479
+ data = await _download_and_decrypt_media(
1480
+ self._poll_session,
1481
+ cdn_base_url=self._cdn_base_url,
1482
+ encrypted_query_param=media.get("encrypt_query_param"),
1483
+ aes_key_b64=(item.get("image_item") or {}).get("aeskey")
1484
+ and base64.b64encode(bytes.fromhex(str((item.get("image_item") or {}).get("aeskey")))).decode("ascii")
1485
+ or media.get("aes_key"),
1486
+ full_url=media.get("full_url"),
1487
+ timeout_seconds=30.0,
1488
+ )
1489
+ return cache_image_from_bytes(data, ".jpg")
1490
+ except Exception as exc:
1491
+ logger.warning("[%s] image download failed: %s", self.name, exc)
1492
+ return None
1493
+
1494
+ async def _download_video(self, item: Dict[str, Any]) -> Optional[str]:
1495
+ media = _media_reference(item, "video_item")
1496
+ try:
1497
+ data = await _download_and_decrypt_media(
1498
+ self._poll_session,
1499
+ cdn_base_url=self._cdn_base_url,
1500
+ encrypted_query_param=media.get("encrypt_query_param"),
1501
+ aes_key_b64=media.get("aes_key"),
1502
+ full_url=media.get("full_url"),
1503
+ timeout_seconds=120.0,
1504
+ )
1505
+ return cache_document_from_bytes(data, "video.mp4")
1506
+ except Exception as exc:
1507
+ logger.warning("[%s] video download failed: %s", self.name, exc)
1508
+ return None
1509
+
1510
+ async def _download_file(self, item: Dict[str, Any]) -> Tuple[Optional[str], str]:
1511
+ file_item = item.get("file_item") or {}
1512
+ media = file_item.get("media") or {}
1513
+ filename = str(file_item.get("file_name") or "document.bin")
1514
+ mime = _mime_from_filename(filename)
1515
+ try:
1516
+ data = await _download_and_decrypt_media(
1517
+ self._poll_session,
1518
+ cdn_base_url=self._cdn_base_url,
1519
+ encrypted_query_param=media.get("encrypt_query_param"),
1520
+ aes_key_b64=media.get("aes_key"),
1521
+ full_url=media.get("full_url"),
1522
+ timeout_seconds=60.0,
1523
+ )
1524
+ return cache_document_from_bytes(data, filename), mime
1525
+ except Exception as exc:
1526
+ logger.warning("[%s] file download failed: %s", self.name, exc)
1527
+ return None, mime
1528
+
1529
+ async def _download_voice(self, item: Dict[str, Any]) -> Optional[str]:
1530
+ voice_item = item.get("voice_item") or {}
1531
+ media = voice_item.get("media") or {}
1532
+ if voice_item.get("text"):
1533
+ return None
1534
+ try:
1535
+ data = await _download_and_decrypt_media(
1536
+ self._poll_session,
1537
+ cdn_base_url=self._cdn_base_url,
1538
+ encrypted_query_param=media.get("encrypt_query_param"),
1539
+ aes_key_b64=media.get("aes_key"),
1540
+ full_url=media.get("full_url"),
1541
+ timeout_seconds=60.0,
1542
+ )
1543
+ return cache_audio_from_bytes(data, ".silk")
1544
+ except Exception as exc:
1545
+ logger.warning("[%s] voice download failed: %s", self.name, exc)
1546
+ return None
1547
+
1548
+ async def _maybe_fetch_typing_ticket(self, user_id: str, context_token: Optional[str]) -> None:
1549
+ if not self._poll_session or not self._token:
1550
+ return
1551
+ if self._typing_cache.get(user_id):
1552
+ return
1553
+ try:
1554
+ response = await _get_config(
1555
+ self._poll_session,
1556
+ base_url=self._base_url,
1557
+ token=self._token,
1558
+ user_id=user_id,
1559
+ context_token=context_token,
1560
+ )
1561
+ typing_ticket = str(response.get("typing_ticket") or "")
1562
+ if typing_ticket:
1563
+ self._typing_cache.set(user_id, typing_ticket)
1564
+ except Exception as exc:
1565
+ logger.debug("[%s] getConfig failed for %s: %s", self.name, _safe_id(user_id), exc)
1566
+
1567
+ def _split_text(self, content: str) -> List[str]:
1568
+ return _split_text_for_weixin_delivery(
1569
+ content, self.MAX_MESSAGE_LENGTH, self._split_multiline_messages,
1570
+ )
1571
+
1572
+ async def _send_text_chunk(
1573
+ self,
1574
+ *,
1575
+ chat_id: str,
1576
+ chunk: str,
1577
+ context_token: Optional[str],
1578
+ client_id: str,
1579
+ ) -> None:
1580
+ """Send a single text chunk with per-chunk retry and backoff.
1581
+
1582
+ On session-expired errors (errcode -14), automatically retries
1583
+ *without* ``context_token`` — iLink accepts tokenless sends as a
1584
+ degraded fallback, which keeps cron-initiated push messages working
1585
+ even when no user message has refreshed the session recently.
1586
+ """
1587
+ last_error: Optional[Exception] = None
1588
+ retried_without_token = False
1589
+ for attempt in range(self._send_chunk_retries + 1):
1590
+ try:
1591
+ resp = await _send_message(
1592
+ self._send_session,
1593
+ base_url=self._base_url,
1594
+ token=self._token,
1595
+ to=chat_id,
1596
+ text=chunk,
1597
+ context_token=context_token,
1598
+ client_id=client_id,
1599
+ )
1600
+ # Check iLink response for session-expired error
1601
+ if resp and isinstance(resp, dict):
1602
+ ret = resp.get("ret")
1603
+ errcode = resp.get("errcode")
1604
+ if (ret is not None and ret not in {0,}) or (errcode is not None and errcode not in {0,}):
1605
+ is_session_expired = (
1606
+ ret == SESSION_EXPIRED_ERRCODE
1607
+ or errcode == SESSION_EXPIRED_ERRCODE
1608
+ or _is_stale_session_ret(ret, errcode, resp.get("errmsg"))
1609
+ )
1610
+ # Session expired — strip token and retry once
1611
+ if is_session_expired and not retried_without_token and context_token:
1612
+ retried_without_token = True
1613
+ context_token = None
1614
+ self._token_store._cache.pop(
1615
+ self._token_store._key(self._account_id, chat_id), None
1616
+ )
1617
+ logger.warning(
1618
+ "[%s] session expired for %s; retrying without context_token",
1619
+ self.name, _safe_id(chat_id),
1620
+ )
1621
+ continue
1622
+ # Rate limit (-2) — backoff and retry
1623
+ is_rate_limited = (
1624
+ ret == RATE_LIMIT_ERRCODE
1625
+ or errcode == RATE_LIMIT_ERRCODE
1626
+ )
1627
+ if is_rate_limited:
1628
+ errmsg = resp.get("errmsg") or resp.get("msg") or "rate limited"
1629
+ # Record the error so we raise a descriptive
1630
+ # RuntimeError (instead of AssertionError) if the
1631
+ # loop exhausts with the server still rate-limiting.
1632
+ last_error = RuntimeError(
1633
+ f"iLink sendmessage rate limited: ret={ret} errcode={errcode} errmsg={errmsg}"
1634
+ )
1635
+ if attempt >= self._send_chunk_retries:
1636
+ break
1637
+ wait = self._send_chunk_retry_delay_seconds * 3 # 3x backoff for rate limit
1638
+ logger.warning(
1639
+ "[%s] rate limited for %s; backing off %.1fs before retry",
1640
+ self.name, _safe_id(chat_id), wait,
1641
+ )
1642
+ await asyncio.sleep(wait)
1643
+ continue
1644
+ errmsg = resp.get("errmsg") or resp.get("msg") or "unknown error"
1645
+ raise RuntimeError(
1646
+ f"iLink sendmessage error: ret={ret} errcode={errcode} errmsg={errmsg}"
1647
+ )
1648
+ return
1649
+ except Exception as exc:
1650
+ last_error = exc
1651
+ if attempt >= self._send_chunk_retries:
1652
+ break
1653
+ wait = self._send_chunk_retry_delay_seconds * (attempt + 1)
1654
+ logger.warning(
1655
+ "[%s] send chunk failed to=%s attempt=%d/%d, retrying in %.2fs: %s",
1656
+ self.name,
1657
+ _safe_id(chat_id),
1658
+ attempt + 1,
1659
+ self._send_chunk_retries + 1,
1660
+ wait,
1661
+ exc,
1662
+ )
1663
+ if wait > 0:
1664
+ await asyncio.sleep(wait)
1665
+ assert last_error is not None
1666
+ raise last_error
1667
+
1668
+ async def send(
1669
+ self,
1670
+ chat_id: str,
1671
+ content: str,
1672
+ reply_to: Optional[str] = None,
1673
+ metadata: Optional[Dict[str, Any]] = None,
1674
+ ) -> SendResult:
1675
+ if not self._send_session or not self._token:
1676
+ return SendResult(success=False, error="Not connected")
1677
+ context_token = self._token_store.get(self._account_id, chat_id)
1678
+ last_message_id: Optional[str] = None
1679
+
1680
+ # Extract MEDIA: tags and bare local file paths before text delivery.
1681
+ media_files, cleaned_content = self.extract_media(content)
1682
+ _, image_cleaned = self.extract_images(cleaned_content)
1683
+ local_files, final_content = self.extract_local_files(image_cleaned)
1684
+
1685
+ _AUDIO_EXTS = {".ogg", ".opus", ".mp3", ".wav", ".m4a", ".flac"}
1686
+ _VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".3gp"}
1687
+ _IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
1688
+
1689
+ async def _deliver_media(path: str, is_voice: bool = False) -> None:
1690
+ ext = Path(path).suffix.lower()
1691
+ if is_voice or ext in _AUDIO_EXTS:
1692
+ await self.send_voice(chat_id=chat_id, audio_path=path, metadata=metadata)
1693
+ elif ext in _VIDEO_EXTS:
1694
+ await self.send_video(chat_id=chat_id, video_path=path, metadata=metadata)
1695
+ elif ext in _IMAGE_EXTS:
1696
+ await self.send_image_file(chat_id=chat_id, image_path=path, metadata=metadata)
1697
+ else:
1698
+ await self.send_document(chat_id=chat_id, file_path=path, metadata=metadata)
1699
+
1700
+ try:
1701
+ # Deliver extracted MEDIA: attachments first.
1702
+ for media_path, is_voice in media_files:
1703
+ try:
1704
+ await _deliver_media(media_path, is_voice)
1705
+ except Exception as exc:
1706
+ logger.warning("[%s] media delivery failed for %s: %s", self.name, media_path, exc)
1707
+
1708
+ # Deliver bare local file paths.
1709
+ for file_path in local_files:
1710
+ try:
1711
+ await _deliver_media(file_path, is_voice=False)
1712
+ except Exception as exc:
1713
+ logger.warning("[%s] local file delivery failed for %s: %s", self.name, file_path, exc)
1714
+
1715
+ # Deliver text content.
1716
+ chunks = [c for c in self._split_text(self.format_message(final_content)) if c and c.strip()]
1717
+ for idx, chunk in enumerate(chunks):
1718
+ client_id = f"hermes-weixin-{uuid.uuid4().hex}"
1719
+ await self._send_text_chunk(
1720
+ chat_id=chat_id,
1721
+ chunk=chunk,
1722
+ context_token=context_token,
1723
+ client_id=client_id,
1724
+ )
1725
+ last_message_id = client_id
1726
+ if idx < len(chunks) - 1 and self._send_chunk_delay_seconds > 0:
1727
+ await asyncio.sleep(self._send_chunk_delay_seconds)
1728
+ return SendResult(success=True, message_id=last_message_id)
1729
+ except Exception as exc:
1730
+ logger.error("[%s] send failed to=%s: %s", self.name, _safe_id(chat_id), exc)
1731
+ return SendResult(success=False, error=str(exc))
1732
+
1733
+ async def send_typing(self, chat_id: str, metadata: Optional[Dict[str, Any]] = None) -> None:
1734
+ if not self._send_session or not self._token:
1735
+ return
1736
+ typing_ticket = self._typing_cache.get(chat_id)
1737
+ if not typing_ticket:
1738
+ return
1739
+ try:
1740
+ await _send_typing(
1741
+ self._send_session,
1742
+ base_url=self._base_url,
1743
+ token=self._token,
1744
+ to_user_id=chat_id,
1745
+ typing_ticket=typing_ticket,
1746
+ status=TYPING_START,
1747
+ )
1748
+ except Exception as exc:
1749
+ logger.debug("[%s] typing start failed for %s: %s", self.name, _safe_id(chat_id), exc)
1750
+
1751
+ async def stop_typing(self, chat_id: str) -> None:
1752
+ if not self._send_session or not self._token:
1753
+ return
1754
+ typing_ticket = self._typing_cache.get(chat_id)
1755
+ if not typing_ticket:
1756
+ return
1757
+ try:
1758
+ await _send_typing(
1759
+ self._send_session,
1760
+ base_url=self._base_url,
1761
+ token=self._token,
1762
+ to_user_id=chat_id,
1763
+ typing_ticket=typing_ticket,
1764
+ status=TYPING_STOP,
1765
+ )
1766
+ except Exception as exc:
1767
+ logger.debug("[%s] typing stop failed for %s: %s", self.name, _safe_id(chat_id), exc)
1768
+
1769
+ async def send_image(
1770
+ self,
1771
+ chat_id: str,
1772
+ image_url: str,
1773
+ caption: str,
1774
+ reply_to: Optional[str] = None,
1775
+ metadata: Optional[Dict[str, Any]] = None,
1776
+ ) -> SendResult:
1777
+ if image_url.startswith(("http://", "https://")):
1778
+ file_path = await self._download_remote_media(image_url)
1779
+ cleanup = True
1780
+ else:
1781
+ file_path = image_url.replace("file://", "")
1782
+ if not os.path.isabs(file_path):
1783
+ file_path = os.path.abspath(file_path)
1784
+ cleanup = False
1785
+ try:
1786
+ return await self.send_document(chat_id, file_path, caption=caption, metadata=metadata)
1787
+ finally:
1788
+ if cleanup and file_path and os.path.exists(file_path):
1789
+ try:
1790
+ os.unlink(file_path)
1791
+ except OSError:
1792
+ pass
1793
+
1794
+ async def send_image_file(
1795
+ self,
1796
+ chat_id: str,
1797
+ image_path: str,
1798
+ caption: Optional[str] = None,
1799
+ reply_to: Optional[str] = None,
1800
+ metadata: Optional[Dict[str, Any]] = None,
1801
+ **kwargs,
1802
+ ) -> SendResult:
1803
+ del reply_to, kwargs
1804
+ return await self.send_document(
1805
+ chat_id=chat_id,
1806
+ file_path=image_path,
1807
+ caption=caption,
1808
+ metadata=metadata,
1809
+ )
1810
+
1811
+ async def send_document(
1812
+ self,
1813
+ chat_id: str,
1814
+ file_path: str,
1815
+ caption: Optional[str] = None,
1816
+ file_name: Optional[str] = None,
1817
+ reply_to: Optional[str] = None,
1818
+ metadata: Optional[Dict[str, Any]] = None,
1819
+ **kwargs,
1820
+ ) -> SendResult:
1821
+ del file_name, reply_to, metadata, kwargs
1822
+ if not self._send_session or not self._token:
1823
+ return SendResult(success=False, error="Not connected")
1824
+ try:
1825
+ message_id = await self._send_file(chat_id, file_path, caption or "")
1826
+ return SendResult(success=True, message_id=message_id)
1827
+ except Exception as exc:
1828
+ logger.error("[%s] send_document failed to=%s: %s", self.name, _safe_id(chat_id), exc)
1829
+ return SendResult(success=False, error=str(exc))
1830
+
1831
+ async def send_video(
1832
+ self,
1833
+ chat_id: str,
1834
+ video_path: str,
1835
+ caption: Optional[str] = None,
1836
+ reply_to: Optional[str] = None,
1837
+ metadata: Optional[Dict[str, Any]] = None,
1838
+ ) -> SendResult:
1839
+ if not self._send_session or not self._token:
1840
+ return SendResult(success=False, error="Not connected")
1841
+ try:
1842
+ message_id = await self._send_file(chat_id, video_path, caption or "")
1843
+ return SendResult(success=True, message_id=message_id)
1844
+ except Exception as exc:
1845
+ logger.error("[%s] send_video failed to=%s: %s", self.name, _safe_id(chat_id), exc)
1846
+ return SendResult(success=False, error=str(exc))
1847
+
1848
+ async def send_voice(
1849
+ self,
1850
+ chat_id: str,
1851
+ audio_path: str,
1852
+ caption: Optional[str] = None,
1853
+ reply_to: Optional[str] = None,
1854
+ metadata: Optional[Dict[str, Any]] = None,
1855
+ ) -> SendResult:
1856
+ if not self._send_session or not self._token:
1857
+ return SendResult(success=False, error="Not connected")
1858
+
1859
+ # Native outbound Weixin voice bubbles are not proven-working in the
1860
+ # upstream reference implementation. Prefer a reliable file attachment
1861
+ # fallback so users at least receive playable audio, even for .silk.
1862
+ fallback_caption = caption or "[voice message as attachment]"
1863
+ try:
1864
+ message_id = await self._send_file(
1865
+ chat_id,
1866
+ audio_path,
1867
+ fallback_caption,
1868
+ force_file_attachment=True,
1869
+ )
1870
+ return SendResult(success=True, message_id=message_id)
1871
+ except Exception as exc:
1872
+ logger.error("[%s] send_voice failed to=%s: %s", self.name, _safe_id(chat_id), exc)
1873
+ return SendResult(success=False, error=str(exc))
1874
+
1875
+ async def _download_remote_media(self, url: str) -> str:
1876
+ from tools.url_safety import is_safe_url
1877
+
1878
+ if not is_safe_url(url):
1879
+ raise ValueError(f"Blocked unsafe URL (SSRF protection): {url}")
1880
+
1881
+ assert self._send_session is not None
1882
+ # Use asyncio.wait_for() instead of aiohttp ClientTimeout to avoid
1883
+ # "Timeout context manager should be used inside a task" errors.
1884
+ async def _do_fetch():
1885
+ async with self._send_session.get(url) as response:
1886
+ response.raise_for_status()
1887
+ return await response.read()
1888
+ data = await asyncio.wait_for(_do_fetch(), timeout=30)
1889
+ suffix = Path(url.split("?", 1)[0]).suffix or ".bin"
1890
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as handle:
1891
+ handle.write(data)
1892
+ return handle.name
1893
+
1894
+ async def _send_file(
1895
+ self,
1896
+ chat_id: str,
1897
+ path: str,
1898
+ caption: str,
1899
+ force_file_attachment: bool = False,
1900
+ ) -> str:
1901
+ assert self._send_session is not None and self._token is not None
1902
+ plaintext = Path(path).read_bytes()
1903
+ media_type, item_builder = self._outbound_media_builder(path, force_file_attachment=force_file_attachment)
1904
+ filekey = secrets.token_hex(16)
1905
+ aes_key = secrets.token_bytes(16)
1906
+ rawsize = len(plaintext)
1907
+ rawfilemd5 = hashlib.md5(plaintext).hexdigest()
1908
+ upload_response = await _get_upload_url(
1909
+ self._send_session,
1910
+ base_url=self._base_url,
1911
+ token=self._token,
1912
+ to_user_id=chat_id,
1913
+ media_type=media_type,
1914
+ filekey=filekey,
1915
+ rawsize=rawsize,
1916
+ rawfilemd5=rawfilemd5,
1917
+ filesize=_aes_padded_size(rawsize),
1918
+ aeskey_hex=aes_key.hex(),
1919
+ )
1920
+ upload_param = str(upload_response.get("upload_param") or "")
1921
+ upload_full_url = str(upload_response.get("upload_full_url") or "")
1922
+ ciphertext = _aes128_ecb_encrypt(plaintext, aes_key)
1923
+
1924
+ # Prefer upload_full_url (direct CDN), fall back to constructed CDN URL
1925
+ # from upload_param. Both paths use POST — the old PUT for
1926
+ # upload_full_url caused 404s on the WeChat CDN.
1927
+ if upload_full_url:
1928
+ upload_url = upload_full_url
1929
+ elif upload_param:
1930
+ upload_url = _cdn_upload_url(self._cdn_base_url, upload_param, filekey)
1931
+ else:
1932
+ raise RuntimeError(f"getUploadUrl returned neither upload_param nor upload_full_url: {upload_response}")
1933
+
1934
+ encrypted_query_param = await _upload_ciphertext(
1935
+ self._send_session,
1936
+ ciphertext=ciphertext,
1937
+ upload_url=upload_url,
1938
+ )
1939
+ context_token = self._token_store.get(self._account_id, chat_id)
1940
+ # The iLink API expects aes_key as base64(hex_string), not base64(raw_bytes).
1941
+ # Sending base64(raw_bytes) causes images to show as grey boxes on the
1942
+ # receiver side because the decryption key doesn't match.
1943
+ aes_key_for_api = base64.b64encode(aes_key.hex().encode("ascii")).decode("ascii")
1944
+ item_kwargs = {
1945
+ "encrypt_query_param": encrypted_query_param,
1946
+ "aes_key_for_api": aes_key_for_api,
1947
+ "ciphertext_size": len(ciphertext),
1948
+ "plaintext_size": rawsize,
1949
+ "filename": Path(path).name,
1950
+ "rawfilemd5": rawfilemd5,
1951
+ }
1952
+ if media_type == MEDIA_VOICE and path.endswith(".silk"):
1953
+ item_kwargs["encode_type"] = 6
1954
+ item_kwargs["sample_rate"] = 24000
1955
+ item_kwargs["bits_per_sample"] = 16
1956
+ media_item = item_builder(**item_kwargs)
1957
+
1958
+ last_message_id = None
1959
+ if caption:
1960
+ last_message_id = f"hermes-weixin-{uuid.uuid4().hex}"
1961
+ await _send_message(
1962
+ self._send_session,
1963
+ base_url=self._base_url,
1964
+ token=self._token,
1965
+ to=chat_id,
1966
+ text=self.format_message(caption),
1967
+ context_token=context_token,
1968
+ client_id=last_message_id,
1969
+ )
1970
+
1971
+ last_message_id = f"hermes-weixin-{uuid.uuid4().hex}"
1972
+ await _api_post(
1973
+ self._send_session,
1974
+ base_url=self._base_url,
1975
+ endpoint=EP_SEND_MESSAGE,
1976
+ payload={
1977
+ "msg": {
1978
+ "from_user_id": "",
1979
+ "to_user_id": chat_id,
1980
+ "client_id": last_message_id,
1981
+ "message_type": MSG_TYPE_BOT,
1982
+ "message_state": MSG_STATE_FINISH,
1983
+ "item_list": [media_item],
1984
+ **({"context_token": context_token} if context_token else {}),
1985
+ }
1986
+ },
1987
+ token=self._token,
1988
+ timeout_ms=API_TIMEOUT_MS,
1989
+ )
1990
+ return last_message_id
1991
+
1992
+ def _outbound_media_builder(self, path: str, force_file_attachment: bool = False):
1993
+ mime = mimetypes.guess_type(path)[0] or "application/octet-stream"
1994
+ if mime.startswith("image/"):
1995
+ return MEDIA_IMAGE, lambda **kw: {
1996
+ "type": ITEM_IMAGE,
1997
+ "image_item": {
1998
+ "media": {
1999
+ "encrypt_query_param": kw["encrypt_query_param"],
2000
+ "aes_key": kw["aes_key_for_api"],
2001
+ "encrypt_type": 1,
2002
+ },
2003
+ "mid_size": kw["ciphertext_size"],
2004
+ },
2005
+ }
2006
+ if mime.startswith("video/"):
2007
+ return MEDIA_VIDEO, lambda **kw: {
2008
+ "type": ITEM_VIDEO,
2009
+ "video_item": {
2010
+ "media": {
2011
+ "encrypt_query_param": kw["encrypt_query_param"],
2012
+ "aes_key": kw["aes_key_for_api"],
2013
+ "encrypt_type": 1,
2014
+ },
2015
+ "video_size": kw["ciphertext_size"],
2016
+ "play_length": kw.get("play_length", 0),
2017
+ "video_md5": kw.get("rawfilemd5", ""),
2018
+ },
2019
+ }
2020
+ if path.endswith(".silk") and not force_file_attachment:
2021
+ return MEDIA_VOICE, lambda **kw: {
2022
+ "type": ITEM_VOICE,
2023
+ "voice_item": {
2024
+ "media": {
2025
+ "encrypt_query_param": kw["encrypt_query_param"],
2026
+ "aes_key": kw["aes_key_for_api"],
2027
+ "encrypt_type": 1,
2028
+ },
2029
+ "encode_type": kw.get("encode_type"),
2030
+ "bits_per_sample": kw.get("bits_per_sample"),
2031
+ "sample_rate": kw.get("sample_rate"),
2032
+ "playtime": kw.get("playtime", 0),
2033
+ },
2034
+ }
2035
+ if mime.startswith("audio/"):
2036
+ return MEDIA_FILE, lambda **kw: {
2037
+ "type": ITEM_FILE,
2038
+ "file_item": {
2039
+ "media": {
2040
+ "encrypt_query_param": kw["encrypt_query_param"],
2041
+ "aes_key": kw["aes_key_for_api"],
2042
+ "encrypt_type": 1,
2043
+ },
2044
+ "file_name": kw["filename"],
2045
+ "len": str(kw["plaintext_size"]),
2046
+ },
2047
+ }
2048
+ return MEDIA_FILE, lambda **kw: {
2049
+ "type": ITEM_FILE,
2050
+ "file_item": {
2051
+ "media": {
2052
+ "encrypt_query_param": kw["encrypt_query_param"],
2053
+ "aes_key": kw["aes_key_for_api"],
2054
+ "encrypt_type": 1,
2055
+ },
2056
+ "file_name": kw["filename"],
2057
+ "len": str(kw["plaintext_size"]),
2058
+ },
2059
+ }
2060
+
2061
+ async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
2062
+ chat_type = "group" if chat_id.endswith("@chatroom") else "dm"
2063
+ return {"name": chat_id, "type": chat_type, "chat_id": chat_id}
2064
+
2065
+ def format_message(self, content: Optional[str]) -> str:
2066
+ if content is None:
2067
+ return ""
2068
+ return _wrap_copy_friendly_lines_for_weixin(_normalize_markdown_blocks(content))
2069
+
2070
+
2071
+ async def send_weixin_direct(
2072
+ *,
2073
+ extra: Dict[str, Any],
2074
+ token: Optional[str],
2075
+ chat_id: str,
2076
+ message: str,
2077
+ media_files: Optional[List[Tuple[str, bool]]] = None,
2078
+ ) -> Dict[str, Any]:
2079
+ """
2080
+ One-shot send helper for ``send_message`` and cron delivery.
2081
+
2082
+ This bypasses the long-poll adapter lifecycle and uses the raw API directly.
2083
+ """
2084
+ account_id = str(extra.get("account_id") or os.getenv("WEIXIN_ACCOUNT_ID", "")).strip()
2085
+ base_url = str(extra.get("base_url") or os.getenv("WEIXIN_BASE_URL", ILINK_BASE_URL)).strip().rstrip("/")
2086
+ cdn_base_url = str(extra.get("cdn_base_url") or os.getenv("WEIXIN_CDN_BASE_URL", WEIXIN_CDN_BASE_URL)).strip().rstrip("/")
2087
+ resolved_token = str(token or extra.get("token") or os.getenv("WEIXIN_TOKEN", "")).strip()
2088
+ if not resolved_token:
2089
+ return {"error": "Weixin token missing. Configure WEIXIN_TOKEN or platforms.weixin.token."}
2090
+ if not account_id:
2091
+ return {"error": "Weixin account ID missing. Configure WEIXIN_ACCOUNT_ID or platforms.weixin.extra.account_id."}
2092
+
2093
+ token_store = ContextTokenStore(str(get_hermes_home()))
2094
+ token_store.restore(account_id)
2095
+ context_token = token_store.get(account_id, chat_id)
2096
+
2097
+ live_adapter = _LIVE_ADAPTERS.get(resolved_token)
2098
+ send_session = getattr(live_adapter, '_send_session', None)
2099
+ if (live_adapter is not None and send_session is not None
2100
+ and not send_session.closed
2101
+ and send_session._loop is asyncio.get_running_loop()):
2102
+ last_result: Optional[SendResult] = None
2103
+ cleaned = live_adapter.format_message(message)
2104
+ if cleaned:
2105
+ last_result = await live_adapter.send(chat_id, cleaned)
2106
+ if not last_result.success:
2107
+ return {"error": f"Weixin send failed: {last_result.error}"}
2108
+
2109
+ for media_path, _is_voice in media_files or []:
2110
+ ext = Path(media_path).suffix.lower()
2111
+ if ext in {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}:
2112
+ last_result = await live_adapter.send_image_file(chat_id, media_path)
2113
+ else:
2114
+ last_result = await live_adapter.send_document(chat_id, media_path)
2115
+ if not last_result.success:
2116
+ return {"error": f"Weixin media send failed: {last_result.error}"}
2117
+
2118
+ return {
2119
+ "success": True,
2120
+ "platform": "weixin",
2121
+ "chat_id": chat_id,
2122
+ "message_id": last_result.message_id if last_result else None,
2123
+ "context_token_used": bool(context_token),
2124
+ }
2125
+
2126
+ async with aiohttp.ClientSession(trust_env=True, connector=_make_ssl_connector()) as session:
2127
+ adapter = WeixinAdapter(
2128
+ PlatformConfig(
2129
+ enabled=True,
2130
+ token=resolved_token,
2131
+ extra={
2132
+ **dict(extra or {}),
2133
+ "account_id": account_id,
2134
+ "base_url": base_url,
2135
+ "cdn_base_url": cdn_base_url,
2136
+ },
2137
+ )
2138
+ )
2139
+ adapter._send_session = session
2140
+ adapter._session = session
2141
+ adapter._token = resolved_token
2142
+ adapter._account_id = account_id
2143
+ adapter._base_url = base_url
2144
+ adapter._cdn_base_url = cdn_base_url
2145
+ adapter._token_store = token_store
2146
+
2147
+ last_result: Optional[SendResult] = None
2148
+ cleaned = adapter.format_message(message)
2149
+ if cleaned:
2150
+ last_result = await adapter.send(chat_id, cleaned)
2151
+ if not last_result.success:
2152
+ return {"error": f"Weixin send failed: {last_result.error}"}
2153
+
2154
+ for media_path, _is_voice in media_files or []:
2155
+ ext = Path(media_path).suffix.lower()
2156
+ if ext in {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}:
2157
+ last_result = await adapter.send_image_file(chat_id, media_path)
2158
+ else:
2159
+ last_result = await adapter.send_document(chat_id, media_path)
2160
+ if not last_result.success:
2161
+ return {"error": f"Weixin media send failed: {last_result.error}"}
2162
+
2163
+ return {
2164
+ "success": True,
2165
+ "platform": "weixin",
2166
+ "chat_id": chat_id,
2167
+ "message_id": last_result.message_id if last_result else None,
2168
+ "context_token_used": bool(context_token),
2169
+ }
2170
+