@clawpump/claw-agent 0.1.5 → 0.1.7

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 (1212) hide show
  1. package/agent/.dockerignore +67 -0
  2. package/agent/.envrc +1 -1
  3. package/agent/.gitattributes +8 -0
  4. package/agent/AGENTS.md +216 -4
  5. package/agent/CONTRIBUTING.md +46 -8
  6. package/agent/Dockerfile +78 -35
  7. package/agent/MANIFEST.in +2 -0
  8. package/agent/README.md +12 -5
  9. package/agent/README.ur-pk.md +261 -0
  10. package/agent/README.zh-CN.md +11 -8
  11. package/agent/SECURITY.md +5 -4
  12. package/agent/acp_adapter/provenance.py +127 -0
  13. package/agent/acp_adapter/server.py +112 -5
  14. package/agent/acp_adapter/session.py +1 -6
  15. package/agent/acp_registry/agent.json +2 -2
  16. package/agent/agent/account_usage.py +313 -1
  17. package/agent/agent/agent_init.py +140 -37
  18. package/agent/agent/agent_runtime_helpers.py +342 -83
  19. package/agent/agent/anthropic_adapter.py +320 -33
  20. package/agent/agent/auxiliary_client.py +525 -105
  21. package/agent/agent/background_review.py +157 -19
  22. package/agent/agent/bedrock_adapter.py +71 -6
  23. package/agent/agent/billing_view.py +295 -0
  24. package/agent/agent/chat_completion_helpers.py +229 -4
  25. package/agent/agent/codex_responses_adapter.py +86 -10
  26. package/agent/agent/codex_runtime.py +153 -1
  27. package/agent/agent/coding_context.py +738 -0
  28. package/agent/agent/context_compressor.py +392 -44
  29. package/agent/agent/context_references.py +34 -1
  30. package/agent/agent/conversation_compression.py +159 -22
  31. package/agent/agent/conversation_loop.py +643 -908
  32. package/agent/agent/copilot_acp_client.py +4 -11
  33. package/agent/agent/credential_pool.py +5 -3
  34. package/agent/agent/credits_tracker.py +794 -0
  35. package/agent/agent/curator.py +91 -18
  36. package/agent/agent/curator_backup.py +26 -10
  37. package/agent/agent/display.py +42 -1
  38. package/agent/agent/error_classifier.py +52 -3
  39. package/agent/agent/errors.py +3 -0
  40. package/agent/agent/file_safety.py +0 -17
  41. package/agent/agent/gemini_native_adapter.py +31 -1
  42. package/agent/agent/i18n.py +48 -4
  43. package/agent/agent/image_gen_provider.py +74 -5
  44. package/agent/agent/image_routing.py +29 -0
  45. package/agent/agent/insights.py +8 -17
  46. package/agent/agent/lsp/install.py +3 -0
  47. package/agent/agent/memory_manager.py +326 -31
  48. package/agent/agent/message_content.py +50 -0
  49. package/agent/agent/model_metadata.py +214 -3
  50. package/agent/agent/moonshot_schema.py +8 -1
  51. package/agent/agent/onboarding.py +60 -0
  52. package/agent/agent/prompt_builder.py +327 -37
  53. package/agent/agent/redact.py +1 -0
  54. package/agent/agent/runtime_cwd.py +34 -5
  55. package/agent/agent/secret_scope.py +205 -0
  56. package/agent/agent/secret_sources/bitwarden.py +34 -2
  57. package/agent/agent/skill_commands.py +90 -1
  58. package/agent/agent/skill_preprocessing.py +1 -0
  59. package/agent/agent/skill_utils.py +209 -36
  60. package/agent/agent/ssl_guard.py +94 -0
  61. package/agent/agent/system_prompt.py +133 -5
  62. package/agent/agent/tool_executor.py +496 -70
  63. package/agent/agent/transports/anthropic.py +83 -21
  64. package/agent/agent/transports/chat_completions.py +94 -5
  65. package/agent/agent/transports/codex.py +67 -2
  66. package/agent/agent/transports/codex_app_server.py +1 -0
  67. package/agent/agent/transports/codex_app_server_session.py +30 -0
  68. package/agent/agent/transports/types.py +12 -0
  69. package/agent/agent/turn_context.py +408 -0
  70. package/agent/agent/turn_finalizer.py +428 -0
  71. package/agent/agent/turn_retry_state.py +68 -0
  72. package/agent/agent/usage_pricing.py +3 -0
  73. package/agent/apps/bootstrap-installer/package.json +6 -5
  74. package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
  75. package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
  76. package/agent/apps/bootstrap-installer/src/store.ts +3 -2
  77. package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
  78. package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
  79. package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
  80. package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
  81. package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
  82. package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
  83. package/agent/apps/desktop/DESIGN.md +167 -0
  84. package/agent/apps/desktop/README.md +20 -16
  85. package/agent/apps/desktop/assets/icon.icns +0 -0
  86. package/agent/apps/desktop/assets/icon.ico +0 -0
  87. package/agent/apps/desktop/assets/icon.png +0 -0
  88. package/agent/apps/desktop/electron/backend-env.cjs +112 -0
  89. package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
  90. package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
  91. package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
  92. package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
  93. package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
  94. package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
  95. package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
  96. package/agent/apps/desktop/electron/connection-config.cjs +288 -0
  97. package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
  98. package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
  99. package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
  100. package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
  101. package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
  102. package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
  103. package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
  104. package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
  105. package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
  106. package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
  107. package/agent/apps/desktop/electron/git-root.cjs +54 -0
  108. package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
  109. package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
  110. package/agent/apps/desktop/electron/hardening.cjs +123 -28
  111. package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
  112. package/agent/apps/desktop/electron/main.cjs +3121 -331
  113. package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
  114. package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
  115. package/agent/apps/desktop/electron/preload.cjs +52 -2
  116. package/agent/apps/desktop/electron/session-windows.cjs +124 -0
  117. package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
  118. package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
  119. package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
  120. package/agent/apps/desktop/electron/update-remote.cjs +56 -0
  121. package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
  122. package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
  123. package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
  124. package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
  125. package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
  126. package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
  127. package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
  128. package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
  129. package/agent/apps/desktop/eslint.config.mjs +0 -3
  130. package/agent/apps/desktop/index.html +27 -2
  131. package/agent/apps/desktop/package.json +31 -11
  132. package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
  133. package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
  134. package/agent/apps/desktop/public/nous-girl.jpg +0 -0
  135. package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
  136. package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
  137. package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
  138. package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
  139. package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
  140. package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
  141. package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
  142. package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
  143. package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
  144. package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
  145. package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
  146. package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
  147. package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
  148. package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
  149. package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
  150. package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
  151. package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
  152. package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
  153. package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
  154. package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
  155. package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
  156. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
  157. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
  158. package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
  159. package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
  160. package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
  161. package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
  162. package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
  163. package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
  164. package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
  165. package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
  166. package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
  167. package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
  168. package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
  169. package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
  170. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
  171. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
  172. package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
  173. package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
  174. package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
  175. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
  176. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
  177. package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
  178. package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
  179. package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
  180. package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
  181. package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
  182. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
  183. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
  184. package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
  185. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
  186. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
  187. package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
  188. package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
  189. package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
  190. package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
  191. package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
  192. package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
  193. package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
  194. package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
  195. package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
  196. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
  197. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
  198. package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
  199. package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
  200. package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
  201. package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
  202. package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
  203. package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
  204. package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
  205. package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
  206. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
  207. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
  208. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
  209. package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
  210. package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
  211. package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
  212. package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
  213. package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
  214. package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
  215. package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
  216. package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
  217. package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
  218. package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
  219. package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
  220. package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
  221. package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
  222. package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
  223. package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
  224. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
  225. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
  226. package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
  227. package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
  228. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
  229. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
  230. package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
  231. package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
  232. package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
  233. package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
  234. package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
  235. package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
  236. package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
  237. package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
  238. package/agent/apps/desktop/src/app/routes.ts +9 -0
  239. package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
  240. package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
  241. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
  242. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
  243. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
  244. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
  245. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
  246. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
  247. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
  248. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
  249. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
  250. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
  251. package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
  252. package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
  253. package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
  254. package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
  255. package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
  256. package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
  257. package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
  258. package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
  259. package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
  260. package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
  261. package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
  262. package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
  263. package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
  264. package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
  265. package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
  266. package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
  267. package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
  268. package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
  269. package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
  270. package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
  271. package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
  272. package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
  273. package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
  274. package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
  275. package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
  276. package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
  277. package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
  278. package/agent/apps/desktop/src/app/settings/types.ts +9 -6
  279. package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
  280. package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
  281. package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
  282. package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
  283. package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
  284. package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
  285. package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
  286. package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
  287. package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
  288. package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
  289. package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
  290. package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
  291. package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
  292. package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
  293. package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
  294. package/agent/apps/desktop/src/app/types.ts +85 -0
  295. package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
  296. package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
  297. package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
  298. package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
  299. package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
  300. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
  301. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
  302. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
  303. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
  304. package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
  305. package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
  306. package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
  307. package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
  308. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
  309. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
  310. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
  311. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
  312. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
  313. package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
  314. package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
  315. package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
  316. package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
  317. package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
  318. package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
  319. package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
  320. package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
  321. package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
  322. package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
  323. package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
  324. package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
  325. package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
  326. package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
  327. package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
  328. package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
  329. package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
  330. package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
  331. package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
  332. package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
  333. package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
  334. package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
  335. package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
  336. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
  337. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
  338. package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
  339. package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
  340. package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
  341. package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
  342. package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
  343. package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
  344. package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
  345. package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
  346. package/agent/apps/desktop/src/components/notifications.tsx +48 -27
  347. package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
  348. package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
  349. package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
  350. package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
  351. package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
  352. package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
  353. package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
  354. package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
  355. package/agent/apps/desktop/src/components/ui/control.ts +25 -0
  356. package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
  357. package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
  358. package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
  359. package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
  360. package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
  361. package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
  362. package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
  363. package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
  364. package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
  365. package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
  366. package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
  367. package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
  368. package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
  369. package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
  370. package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
  371. package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
  372. package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
  373. package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
  374. package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
  375. package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
  376. package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
  377. package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
  378. package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
  379. package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
  380. package/agent/apps/desktop/src/global.d.ts +181 -4
  381. package/agent/apps/desktop/src/hermes.test.ts +60 -0
  382. package/agent/apps/desktop/src/hermes.ts +190 -13
  383. package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
  384. package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
  385. package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
  386. package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
  387. package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
  388. package/agent/apps/desktop/src/i18n/context.tsx +183 -0
  389. package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
  390. package/agent/apps/desktop/src/i18n/en.ts +1921 -0
  391. package/agent/apps/desktop/src/i18n/index.ts +20 -0
  392. package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
  393. package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
  394. package/agent/apps/desktop/src/i18n/languages.ts +86 -0
  395. package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
  396. package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
  397. package/agent/apps/desktop/src/i18n/types.ts +1559 -0
  398. package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
  399. package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
  400. package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
  401. package/agent/apps/desktop/src/lib/ansi.ts +186 -0
  402. package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
  403. package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
  404. package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
  405. package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
  406. package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
  407. package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
  408. package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
  409. package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
  410. package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
  411. package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
  412. package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
  413. package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
  414. package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
  415. package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
  416. package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
  417. package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
  418. package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
  419. package/agent/apps/desktop/src/lib/haptics.ts +17 -0
  420. package/agent/apps/desktop/src/lib/icons.ts +10 -2
  421. package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
  422. package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
  423. package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
  424. package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
  425. package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
  426. package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
  427. package/agent/apps/desktop/src/lib/media.ts +40 -1
  428. package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
  429. package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
  430. package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
  431. package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
  432. package/agent/apps/desktop/src/lib/query-client.ts +13 -0
  433. package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
  434. package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
  435. package/agent/apps/desktop/src/lib/session-export.ts +6 -3
  436. package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
  437. package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
  438. package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
  439. package/agent/apps/desktop/src/lib/session-search.ts +21 -0
  440. package/agent/apps/desktop/src/lib/session-source.ts +126 -0
  441. package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
  442. package/agent/apps/desktop/src/lib/storage.ts +35 -1
  443. package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
  444. package/agent/apps/desktop/src/lib/todos.ts +37 -0
  445. package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
  446. package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
  447. package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
  448. package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
  449. package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
  450. package/agent/apps/desktop/src/main.tsx +19 -19
  451. package/agent/apps/desktop/src/store/boot.ts +4 -3
  452. package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
  453. package/agent/apps/desktop/src/store/clarify.ts +50 -13
  454. package/agent/apps/desktop/src/store/command-palette.ts +20 -0
  455. package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
  456. package/agent/apps/desktop/src/store/compaction.ts +38 -0
  457. package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
  458. package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
  459. package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
  460. package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
  461. package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
  462. package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
  463. package/agent/apps/desktop/src/store/composer-status.ts +277 -0
  464. package/agent/apps/desktop/src/store/composer.test.ts +106 -0
  465. package/agent/apps/desktop/src/store/composer.ts +116 -0
  466. package/agent/apps/desktop/src/store/cron.ts +19 -0
  467. package/agent/apps/desktop/src/store/gateway.ts +280 -6
  468. package/agent/apps/desktop/src/store/keybinds.ts +143 -0
  469. package/agent/apps/desktop/src/store/layout.ts +107 -9
  470. package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
  471. package/agent/apps/desktop/src/store/model-presets.ts +86 -0
  472. package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
  473. package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
  474. package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
  475. package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
  476. package/agent/apps/desktop/src/store/notifications.ts +10 -7
  477. package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
  478. package/agent/apps/desktop/src/store/onboarding.ts +268 -38
  479. package/agent/apps/desktop/src/store/preview.ts +10 -1
  480. package/agent/apps/desktop/src/store/profile.test.ts +89 -0
  481. package/agent/apps/desktop/src/store/profile.ts +395 -0
  482. package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
  483. package/agent/apps/desktop/src/store/prompts.ts +117 -0
  484. package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
  485. package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
  486. package/agent/apps/desktop/src/store/session-sync.ts +25 -0
  487. package/agent/apps/desktop/src/store/session.test.ts +268 -2
  488. package/agent/apps/desktop/src/store/session.ts +392 -18
  489. package/agent/apps/desktop/src/store/subagents.ts +3 -0
  490. package/agent/apps/desktop/src/store/system-actions.ts +48 -0
  491. package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
  492. package/agent/apps/desktop/src/store/todos.test.ts +47 -0
  493. package/agent/apps/desktop/src/store/todos.ts +64 -0
  494. package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
  495. package/agent/apps/desktop/src/store/translucency.ts +38 -0
  496. package/agent/apps/desktop/src/store/updates.test.ts +187 -2
  497. package/agent/apps/desktop/src/store/updates.ts +268 -18
  498. package/agent/apps/desktop/src/store/windows.test.ts +143 -0
  499. package/agent/apps/desktop/src/store/windows.ts +115 -0
  500. package/agent/apps/desktop/src/styles.css +510 -119
  501. package/agent/apps/desktop/src/themes/color.ts +142 -0
  502. package/agent/apps/desktop/src/themes/context.tsx +128 -75
  503. package/agent/apps/desktop/src/themes/install.test.ts +119 -0
  504. package/agent/apps/desktop/src/themes/install.ts +95 -0
  505. package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
  506. package/agent/apps/desktop/src/themes/presets.ts +13 -4
  507. package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
  508. package/agent/apps/desktop/src/themes/types.ts +35 -0
  509. package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
  510. package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
  511. package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
  512. package/agent/apps/desktop/src/themes/vscode.ts +343 -0
  513. package/agent/apps/desktop/src/types/hermes.ts +138 -1
  514. package/agent/apps/desktop/tsconfig.json +2 -2
  515. package/agent/apps/desktop/vite.config.ts +18 -0
  516. package/agent/apps/shared/package.json +1 -1
  517. package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
  518. package/agent/apps/shared/tsconfig.json +2 -2
  519. package/agent/cli-config.yaml.example +78 -1
  520. package/agent/cli.py +2177 -3162
  521. package/agent/cron/blueprint_catalog.py +713 -0
  522. package/agent/cron/jobs.py +226 -110
  523. package/agent/cron/scheduler.py +468 -193
  524. package/agent/cron/scheduler_provider.py +177 -0
  525. package/agent/cron/scripts/__init__.py +1 -0
  526. package/agent/cron/scripts/classify_items.py +226 -0
  527. package/agent/cron/suggestion_catalog.py +154 -0
  528. package/agent/cron/suggestions.py +257 -0
  529. package/agent/docs/chronos-managed-cron-contract.md +196 -0
  530. package/agent/docs/design/profile-builder.md +146 -0
  531. package/agent/docs/middleware/README.md +260 -0
  532. package/agent/docs/observability/README.md +316 -0
  533. package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
  534. package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
  535. package/agent/docs/relay-connector-contract.md +285 -0
  536. package/agent/gateway/authz_mixin.py +536 -0
  537. package/agent/gateway/channel_directory.py +65 -3
  538. package/agent/gateway/config.py +222 -12
  539. package/agent/gateway/display_config.py +10 -0
  540. package/agent/gateway/hooks.py +17 -0
  541. package/agent/gateway/kanban_watchers.py +1146 -0
  542. package/agent/gateway/message_timestamps.py +166 -0
  543. package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
  544. package/agent/gateway/platforms/api_server.py +216 -38
  545. package/agent/gateway/platforms/base.py +210 -58
  546. package/agent/gateway/platforms/email.py +122 -12
  547. package/agent/gateway/platforms/feishu.py +80 -11
  548. package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
  549. package/agent/gateway/platforms/matrix.py +1498 -297
  550. package/agent/gateway/platforms/qqbot/adapter.py +6 -0
  551. package/agent/gateway/platforms/signal.py +8 -0
  552. package/agent/gateway/platforms/slack.py +308 -12
  553. package/agent/gateway/platforms/telegram.py +831 -24
  554. package/agent/gateway/platforms/webhook.py +109 -21
  555. package/agent/gateway/platforms/weixin.py +113 -2
  556. package/agent/gateway/platforms/whatsapp.py +94 -288
  557. package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
  558. package/agent/gateway/platforms/whatsapp_common.py +367 -0
  559. package/agent/gateway/platforms/yuanbao.py +608 -191
  560. package/agent/gateway/platforms/yuanbao_proto.py +232 -23
  561. package/agent/gateway/relay/__init__.py +375 -0
  562. package/agent/gateway/relay/adapter.py +222 -0
  563. package/agent/gateway/relay/auth.py +168 -0
  564. package/agent/gateway/relay/descriptor.py +118 -0
  565. package/agent/gateway/relay/transport.py +101 -0
  566. package/agent/gateway/relay/ws_transport.py +327 -0
  567. package/agent/gateway/response_filters.py +53 -0
  568. package/agent/gateway/rich_sent_store.py +80 -0
  569. package/agent/gateway/run.py +2940 -5001
  570. package/agent/gateway/session.py +109 -8
  571. package/agent/gateway/session_context.py +22 -4
  572. package/agent/gateway/slash_commands.py +3854 -0
  573. package/agent/gateway/status.py +141 -21
  574. package/agent/gateway/stream_consumer.py +288 -31
  575. package/agent/hermes-already-has-routines.md +1 -1
  576. package/agent/hermes_cli/__init__.py +62 -17
  577. package/agent/hermes_cli/_parser.py +30 -0
  578. package/agent/hermes_cli/_subprocess_compat.py +61 -0
  579. package/agent/hermes_cli/active_sessions.py +320 -0
  580. package/agent/hermes_cli/auth.py +707 -59
  581. package/agent/hermes_cli/auth_commands.py +39 -22
  582. package/agent/hermes_cli/backup.py +109 -7
  583. package/agent/hermes_cli/banner.py +88 -0
  584. package/agent/hermes_cli/blueprint_cmd.py +318 -0
  585. package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
  586. package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
  587. package/agent/hermes_cli/commands.py +215 -91
  588. package/agent/hermes_cli/config.py +967 -130
  589. package/agent/hermes_cli/container_boot.py +76 -11
  590. package/agent/hermes_cli/cron.py +5 -11
  591. package/agent/hermes_cli/curator.py +21 -0
  592. package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
  593. package/agent/hermes_cli/dashboard_auth/base.py +62 -0
  594. package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
  595. package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
  596. package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
  597. package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
  598. package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
  599. package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
  600. package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
  601. package/agent/hermes_cli/dashboard_register.py +427 -0
  602. package/agent/hermes_cli/debug.py +155 -50
  603. package/agent/hermes_cli/doctor.py +255 -14
  604. package/agent/hermes_cli/dump.py +60 -6
  605. package/agent/hermes_cli/env_loader.py +33 -0
  606. package/agent/hermes_cli/gateway.py +755 -103
  607. package/agent/hermes_cli/gateway_enroll.py +250 -0
  608. package/agent/hermes_cli/gateway_windows.py +254 -11
  609. package/agent/hermes_cli/gui_uninstall.py +285 -0
  610. package/agent/hermes_cli/inventory.py +105 -4
  611. package/agent/hermes_cli/kanban.py +58 -71
  612. package/agent/hermes_cli/kanban_db.py +391 -14
  613. package/agent/hermes_cli/kanban_decompose.py +2 -2
  614. package/agent/hermes_cli/kanban_specify.py +3 -1
  615. package/agent/hermes_cli/logs.py +2 -0
  616. package/agent/hermes_cli/main.py +2889 -5287
  617. package/agent/hermes_cli/managed_scope.py +214 -0
  618. package/agent/hermes_cli/managed_uv.py +254 -0
  619. package/agent/hermes_cli/mcp_catalog.py +6 -3
  620. package/agent/hermes_cli/mcp_config.py +145 -21
  621. package/agent/hermes_cli/mcp_security.py +96 -0
  622. package/agent/hermes_cli/mcp_startup.py +32 -3
  623. package/agent/hermes_cli/memory_providers.py +149 -0
  624. package/agent/hermes_cli/memory_setup.py +97 -42
  625. package/agent/hermes_cli/middleware.py +313 -0
  626. package/agent/hermes_cli/model_catalog.py +31 -0
  627. package/agent/hermes_cli/model_cost_guard.py +134 -0
  628. package/agent/hermes_cli/model_normalize.py +2 -1
  629. package/agent/hermes_cli/model_setup_flows.py +2759 -0
  630. package/agent/hermes_cli/model_switch.py +242 -27
  631. package/agent/hermes_cli/models.py +284 -44
  632. package/agent/hermes_cli/nous_account.py +33 -6
  633. package/agent/hermes_cli/nous_billing.py +406 -0
  634. package/agent/hermes_cli/nous_subscription.py +202 -5
  635. package/agent/hermes_cli/platforms.py +1 -0
  636. package/agent/hermes_cli/plugins.py +218 -18
  637. package/agent/hermes_cli/plugins_cmd.py +249 -105
  638. package/agent/hermes_cli/portal_cli.py +56 -16
  639. package/agent/hermes_cli/profile_distribution.py +6 -1
  640. package/agent/hermes_cli/profiles.py +283 -32
  641. package/agent/hermes_cli/provider_catalog.py +170 -0
  642. package/agent/hermes_cli/providers.py +4 -1
  643. package/agent/hermes_cli/pty_bridge.py +53 -4
  644. package/agent/hermes_cli/runtime_provider.py +216 -34
  645. package/agent/hermes_cli/secret_prompt.py +4 -4
  646. package/agent/hermes_cli/secrets_cli.py +24 -0
  647. package/agent/hermes_cli/send_cmd.py +28 -2
  648. package/agent/hermes_cli/service_manager.py +166 -19
  649. package/agent/hermes_cli/session_listing.py +97 -0
  650. package/agent/hermes_cli/setup.py +158 -94
  651. package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
  652. package/agent/hermes_cli/skills_config.py +8 -2
  653. package/agent/hermes_cli/skills_hub.py +149 -7
  654. package/agent/hermes_cli/status.py +2 -2
  655. package/agent/hermes_cli/subcommands/__init__.py +18 -0
  656. package/agent/hermes_cli/subcommands/_shared.py +29 -0
  657. package/agent/hermes_cli/subcommands/acp.py +52 -0
  658. package/agent/hermes_cli/subcommands/auth.py +109 -0
  659. package/agent/hermes_cli/subcommands/backup.py +38 -0
  660. package/agent/hermes_cli/subcommands/claw.py +92 -0
  661. package/agent/hermes_cli/subcommands/config.py +49 -0
  662. package/agent/hermes_cli/subcommands/cron.py +163 -0
  663. package/agent/hermes_cli/subcommands/dashboard.py +143 -0
  664. package/agent/hermes_cli/subcommands/debug.py +77 -0
  665. package/agent/hermes_cli/subcommands/doctor.py +35 -0
  666. package/agent/hermes_cli/subcommands/dump.py +28 -0
  667. package/agent/hermes_cli/subcommands/gateway.py +332 -0
  668. package/agent/hermes_cli/subcommands/gui.py +63 -0
  669. package/agent/hermes_cli/subcommands/hooks.py +77 -0
  670. package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
  671. package/agent/hermes_cli/subcommands/insights.py +25 -0
  672. package/agent/hermes_cli/subcommands/login.py +78 -0
  673. package/agent/hermes_cli/subcommands/logout.py +28 -0
  674. package/agent/hermes_cli/subcommands/logs.py +78 -0
  675. package/agent/hermes_cli/subcommands/mcp.py +108 -0
  676. package/agent/hermes_cli/subcommands/memory.py +53 -0
  677. package/agent/hermes_cli/subcommands/model.py +72 -0
  678. package/agent/hermes_cli/subcommands/pairing.py +36 -0
  679. package/agent/hermes_cli/subcommands/plugins.py +94 -0
  680. package/agent/hermes_cli/subcommands/postinstall.py +23 -0
  681. package/agent/hermes_cli/subcommands/profile.py +203 -0
  682. package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
  683. package/agent/hermes_cli/subcommands/security.py +62 -0
  684. package/agent/hermes_cli/subcommands/setup.py +58 -0
  685. package/agent/hermes_cli/subcommands/skills.py +298 -0
  686. package/agent/hermes_cli/subcommands/slack.py +60 -0
  687. package/agent/hermes_cli/subcommands/status.py +28 -0
  688. package/agent/hermes_cli/subcommands/tools.py +95 -0
  689. package/agent/hermes_cli/subcommands/uninstall.py +41 -0
  690. package/agent/hermes_cli/subcommands/update.py +70 -0
  691. package/agent/hermes_cli/subcommands/version.py +18 -0
  692. package/agent/hermes_cli/subcommands/webhook.py +76 -0
  693. package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
  694. package/agent/hermes_cli/suggestions_cmd.py +153 -0
  695. package/agent/hermes_cli/telegram_managed_bot.py +358 -0
  696. package/agent/hermes_cli/tips.py +3 -4
  697. package/agent/hermes_cli/tools_config.py +155 -28
  698. package/agent/hermes_cli/uninstall.py +231 -35
  699. package/agent/hermes_cli/web_server.py +6190 -973
  700. package/agent/hermes_cli/win_pty_bridge.py +179 -0
  701. package/agent/hermes_cli/write_approval_commands.py +209 -0
  702. package/agent/hermes_constants.py +164 -33
  703. package/agent/hermes_logging.py +74 -2
  704. package/agent/hermes_state.py +919 -106
  705. package/agent/hermes_time.py +20 -0
  706. package/agent/locales/af.yaml +23 -0
  707. package/agent/locales/de.yaml +23 -0
  708. package/agent/locales/en.yaml +20 -0
  709. package/agent/locales/es.yaml +23 -0
  710. package/agent/locales/fr.yaml +23 -0
  711. package/agent/locales/ga.yaml +23 -0
  712. package/agent/locales/hu.yaml +23 -0
  713. package/agent/locales/it.yaml +23 -0
  714. package/agent/locales/ja.yaml +23 -0
  715. package/agent/locales/ko.yaml +23 -0
  716. package/agent/locales/pt.yaml +23 -0
  717. package/agent/locales/ru.yaml +23 -0
  718. package/agent/locales/tr.yaml +23 -0
  719. package/agent/locales/uk.yaml +23 -0
  720. package/agent/locales/zh-hant.yaml +23 -0
  721. package/agent/locales/zh.yaml +23 -0
  722. package/agent/model_tools.py +204 -40
  723. package/agent/optional-mcps/clawpump/manifest.yaml +4 -2
  724. package/agent/optional-mcps/clawpump-stdio/manifest.yaml +2 -0
  725. package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
  726. package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
  727. package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
  728. package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
  729. package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
  730. package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
  731. package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
  732. package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
  733. package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
  734. package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
  735. package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
  736. package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
  737. package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
  738. package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
  739. package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
  740. package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
  741. package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
  742. package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
  743. package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
  744. package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
  745. package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
  746. package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
  747. package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
  748. package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
  749. package/agent/optional-skills/security/1password/SKILL.md +1 -1
  750. package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
  751. package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
  752. package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
  753. package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
  754. package/agent/package-lock.json +4082 -7907
  755. package/agent/package.json +18 -3
  756. package/agent/plugins/browser/firecrawl/provider.py +4 -1
  757. package/agent/plugins/cron/__init__.py +344 -0
  758. package/agent/plugins/cron/chronos/__init__.py +241 -0
  759. package/agent/plugins/cron/chronos/_nas_client.py +123 -0
  760. package/agent/plugins/cron/chronos/plugin.yaml +9 -0
  761. package/agent/plugins/cron/chronos/verify.py +103 -0
  762. package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
  763. package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
  764. package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
  765. package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
  766. package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
  767. package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
  768. package/agent/plugins/google_meet/audio_bridge.py +4 -0
  769. package/agent/plugins/google_meet/meet_bot.py +7 -1
  770. package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
  771. package/agent/plugins/image_gen/fal/__init__.py +35 -6
  772. package/agent/plugins/image_gen/krea/__init__.py +56 -13
  773. package/agent/plugins/image_gen/openai/__init__.py +122 -24
  774. package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
  775. package/agent/plugins/image_gen/xai/__init__.py +92 -12
  776. package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
  777. package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
  778. package/agent/plugins/memory/__init__.py +48 -5
  779. package/agent/plugins/memory/byterover/__init__.py +1 -0
  780. package/agent/plugins/memory/hindsight/README.md +1 -1
  781. package/agent/plugins/memory/hindsight/__init__.py +138 -24
  782. package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
  783. package/agent/plugins/memory/honcho/README.md +13 -10
  784. package/agent/plugins/memory/honcho/cli.py +247 -122
  785. package/agent/plugins/memory/honcho/client.py +112 -102
  786. package/agent/plugins/memory/openviking/README.md +12 -1
  787. package/agent/plugins/memory/openviking/__init__.py +2281 -107
  788. package/agent/plugins/memory/openviking/plugin.yaml +1 -2
  789. package/agent/plugins/memory/supermemory/README.md +22 -10
  790. package/agent/plugins/memory/supermemory/__init__.py +142 -37
  791. package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
  792. package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
  793. package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
  794. package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
  795. package/agent/plugins/model-providers/custom/__init__.py +8 -2
  796. package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
  797. package/agent/plugins/model-providers/minimax/__init__.py +60 -8
  798. package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
  799. package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
  800. package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
  801. package/agent/plugins/model-providers/zai/__init__.py +1 -0
  802. package/agent/plugins/observability/langfuse/__init__.py +147 -14
  803. package/agent/plugins/observability/nemo_relay/README.md +559 -0
  804. package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
  805. package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
  806. package/agent/plugins/platforms/discord/adapter.py +932 -61
  807. package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
  808. package/agent/plugins/platforms/google_chat/adapter.py +9 -3
  809. package/agent/plugins/platforms/google_chat/oauth.py +1 -1
  810. package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
  811. package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
  812. package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
  813. package/agent/plugins/platforms/irc/adapter.py +4 -1
  814. package/agent/plugins/platforms/line/adapter.py +16 -1
  815. package/agent/plugins/platforms/mattermost/adapter.py +100 -24
  816. package/agent/plugins/platforms/photon/README.md +179 -0
  817. package/agent/plugins/platforms/photon/__init__.py +4 -0
  818. package/agent/plugins/platforms/photon/adapter.py +1586 -0
  819. package/agent/plugins/platforms/photon/auth.py +1046 -0
  820. package/agent/plugins/platforms/photon/cli.py +439 -0
  821. package/agent/plugins/platforms/photon/plugin.yaml +88 -0
  822. package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
  823. package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
  824. package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
  825. package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
  826. package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
  827. package/agent/plugins/platforms/raft/__init__.py +3 -0
  828. package/agent/plugins/platforms/raft/adapter.py +774 -0
  829. package/agent/plugins/platforms/raft/plugin.yaml +19 -0
  830. package/agent/plugins/platforms/simplex/adapter.py +777 -220
  831. package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
  832. package/agent/plugins/platforms/teams/adapter.py +175 -5
  833. package/agent/plugins/plugin_utils.py +135 -0
  834. package/agent/plugins/video_gen/fal/__init__.py +10 -3
  835. package/agent/plugins/web/searxng/provider.py +15 -2
  836. package/agent/plugins/web/xai/provider.py +2 -2
  837. package/agent/providers/base.py +22 -3
  838. package/agent/pyproject.toml +115 -21
  839. package/agent/run_agent.py +733 -39
  840. package/agent/scripts/build_skills_index.py +51 -19
  841. package/agent/scripts/check_subprocess_stdin.py +177 -0
  842. package/agent/scripts/contributor_audit.py +2 -0
  843. package/agent/scripts/docker_config_migrate.py +67 -0
  844. package/agent/scripts/install.cmd +3 -3
  845. package/agent/scripts/install.ps1 +580 -154
  846. package/agent/scripts/install.sh +402 -185
  847. package/agent/scripts/lib/node-bootstrap.sh +39 -4
  848. package/agent/scripts/release.py +183 -0
  849. package/agent/scripts/run_tests.sh +1 -0
  850. package/agent/scripts/run_tests_parallel.py +18 -23
  851. package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
  852. package/agent/setup.py +59 -0
  853. package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
  854. package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
  855. package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
  856. package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
  857. package/agent/skills/clawpump/SKILL.md +4 -1
  858. package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
  859. package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
  860. package/agent/skills/github/github-auth/SKILL.md +2 -2
  861. package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
  862. package/agent/skills/github/github-code-review/SKILL.md +2 -2
  863. package/agent/skills/github/github-issues/SKILL.md +2 -2
  864. package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
  865. package/agent/skills/github/github-repo-management/SKILL.md +2 -2
  866. package/agent/skills/media/gif-search/SKILL.md +1 -1
  867. package/agent/skills/media/youtube-content/SKILL.md +10 -7
  868. package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
  869. package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
  870. package/agent/skills/productivity/airtable/SKILL.md +2 -2
  871. package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
  872. package/agent/skills/productivity/notion/SKILL.md +2 -2
  873. package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
  874. package/agent/skills/research/llm-wiki/SKILL.md +1 -1
  875. package/agent/skills/social-media/xurl/SKILL.md +9 -0
  876. package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
  877. package/agent/skills/software-development/plan/SKILL.md +285 -5
  878. package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
  879. package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
  880. package/agent/skills/software-development/spike/SKILL.md +2 -2
  881. package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
  882. package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
  883. package/agent/tools/approval.py +302 -4
  884. package/agent/tools/async_delegation.py +386 -0
  885. package/agent/tools/blueprints.py +325 -0
  886. package/agent/tools/browser_cdp_tool.py +3 -3
  887. package/agent/tools/browser_tool.py +34 -6
  888. package/agent/tools/checkpoint_manager.py +31 -1
  889. package/agent/tools/clarify_tool.py +55 -5
  890. package/agent/tools/code_execution_tool.py +31 -14
  891. package/agent/tools/computer_use/cua_backend.py +81 -3
  892. package/agent/tools/computer_use/tool.py +79 -5
  893. package/agent/tools/computer_use/vision_routing.py +55 -3
  894. package/agent/tools/credential_files.py +31 -12
  895. package/agent/tools/cronjob_tools.py +30 -20
  896. package/agent/tools/delegate_tool.py +356 -31
  897. package/agent/tools/env_probe.py +1 -0
  898. package/agent/tools/environments/docker.py +163 -8
  899. package/agent/tools/environments/file_sync.py +2 -1
  900. package/agent/tools/environments/local.py +74 -23
  901. package/agent/tools/environments/singularity.py +4 -1
  902. package/agent/tools/environments/ssh.py +78 -11
  903. package/agent/tools/file_operations.py +277 -41
  904. package/agent/tools/file_tools.py +166 -28
  905. package/agent/tools/image_generation_tool.py +515 -29
  906. package/agent/tools/kanban_tools.py +99 -0
  907. package/agent/tools/lazy_deps.py +33 -2
  908. package/agent/tools/mcp_oauth.py +5 -5
  909. package/agent/tools/mcp_oauth_manager.py +7 -5
  910. package/agent/tools/mcp_tool.py +840 -33
  911. package/agent/tools/memory_tool.py +335 -38
  912. package/agent/tools/osv_check.py +15 -1
  913. package/agent/tools/process_registry.py +155 -11
  914. package/agent/tools/read_extract.py +248 -0
  915. package/agent/tools/read_terminal_tool.py +93 -0
  916. package/agent/tools/schema_sanitizer.py +38 -0
  917. package/agent/tools/send_message_tool.py +163 -49
  918. package/agent/tools/session_search_tool.py +189 -7
  919. package/agent/tools/skill_manager_tool.py +202 -3
  920. package/agent/tools/skill_usage.py +52 -4
  921. package/agent/tools/skills_hub.py +184 -44
  922. package/agent/tools/skills_sync.py +232 -5
  923. package/agent/tools/skills_tool.py +125 -11
  924. package/agent/tools/terminal_tool.py +148 -26
  925. package/agent/tools/tirith_security.py +2 -0
  926. package/agent/tools/todo_tool.py +32 -1
  927. package/agent/tools/transcription_tools.py +13 -5
  928. package/agent/tools/tts_tool.py +332 -38
  929. package/agent/tools/url_safety.py +52 -1
  930. package/agent/tools/vision_tools.py +124 -39
  931. package/agent/tools/voice_mode.py +4 -3
  932. package/agent/tools/web_tools.py +45 -15
  933. package/agent/tools/write_approval.py +493 -0
  934. package/agent/toolsets.py +34 -10
  935. package/agent/trajectory_compressor.py +81 -10
  936. package/agent/tui_gateway/entry.py +43 -6
  937. package/agent/tui_gateway/server.py +3335 -330
  938. package/agent/tui_gateway/slash_worker.py +61 -0
  939. package/agent/tui_gateway/ws.py +67 -9
  940. package/agent/ui-tui/eslint.config.mjs +0 -4
  941. package/agent/ui-tui/package.json +6 -6
  942. package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
  943. package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
  944. package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
  945. package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
  946. package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
  947. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
  948. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
  949. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
  950. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
  951. package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
  952. package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
  953. package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
  954. package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
  955. package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
  956. package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
  957. package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
  958. package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
  959. package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
  960. package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
  961. package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
  962. package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
  963. package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
  964. package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
  965. package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
  966. package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
  967. package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
  968. package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
  969. package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
  970. package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
  971. package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
  972. package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
  973. package/agent/ui-tui/src/app/interfaces.ts +64 -1
  974. package/agent/ui-tui/src/app/overlayStore.ts +18 -2
  975. package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
  976. package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
  977. package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
  978. package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
  979. package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
  980. package/agent/ui-tui/src/app/slash/registry.ts +4 -0
  981. package/agent/ui-tui/src/app/turnController.ts +145 -2
  982. package/agent/ui-tui/src/app/uiStore.ts +2 -0
  983. package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
  984. package/agent/ui-tui/src/app/useMainApp.ts +54 -8
  985. package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
  986. package/agent/ui-tui/src/app/useSubmission.ts +23 -31
  987. package/agent/ui-tui/src/components/appChrome.tsx +112 -5
  988. package/agent/ui-tui/src/components/appLayout.tsx +9 -0
  989. package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
  990. package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
  991. package/agent/ui-tui/src/components/branding.tsx +15 -3
  992. package/agent/ui-tui/src/components/messageLine.tsx +25 -3
  993. package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
  994. package/agent/ui-tui/src/components/prompts.tsx +31 -17
  995. package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
  996. package/agent/ui-tui/src/components/textInput.tsx +16 -0
  997. package/agent/ui-tui/src/config/env.ts +12 -0
  998. package/agent/ui-tui/src/config/limits.ts +13 -0
  999. package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
  1000. package/agent/ui-tui/src/domain/paths.ts +24 -0
  1001. package/agent/ui-tui/src/domain/slash.ts +40 -0
  1002. package/agent/ui-tui/src/entry.tsx +35 -4
  1003. package/agent/ui-tui/src/gatewayClient.ts +22 -10
  1004. package/agent/ui-tui/src/gatewayTypes.ts +130 -1
  1005. package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
  1006. package/agent/ui-tui/src/lib/memory.test.ts +162 -0
  1007. package/agent/ui-tui/src/lib/memory.ts +60 -1
  1008. package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
  1009. package/agent/ui-tui/src/lib/osc52.ts +1 -1
  1010. package/agent/ui-tui/src/lib/text.test.ts +32 -1
  1011. package/agent/ui-tui/src/lib/text.ts +29 -2
  1012. package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
  1013. package/agent/ui-tui/src/types.ts +5 -0
  1014. package/agent/ui-tui/tsconfig.build.json +0 -1
  1015. package/agent/ui-tui/tsconfig.json +2 -1
  1016. package/agent/utils.py +66 -2
  1017. package/agent/uv.lock +300 -684
  1018. package/agent/web/index.html +2 -2
  1019. package/agent/web/package.json +11 -6
  1020. package/agent/web/public/claw-bg.webp +0 -0
  1021. package/agent/web/public/claw-logo.webp +0 -0
  1022. package/agent/web/src/App.tsx +138 -48
  1023. package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
  1024. package/agent/web/src/components/Backdrop.tsx +15 -0
  1025. package/agent/web/src/components/ChatSessionList.tsx +260 -0
  1026. package/agent/web/src/components/ChatSidebar.tsx +262 -78
  1027. package/agent/web/src/components/ConfirmDialog.tsx +122 -0
  1028. package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
  1029. package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
  1030. package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
  1031. package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
  1032. package/agent/web/src/components/ReasoningPicker.tsx +167 -0
  1033. package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
  1034. package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
  1035. package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
  1036. package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
  1037. package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
  1038. package/agent/web/src/contexts/SystemActions.tsx +6 -8
  1039. package/agent/web/src/contexts/profile-context.ts +19 -0
  1040. package/agent/web/src/contexts/useProfileScope.ts +6 -0
  1041. package/agent/web/src/i18n/af.ts +5 -4
  1042. package/agent/web/src/i18n/de.ts +5 -4
  1043. package/agent/web/src/i18n/en.ts +58 -4
  1044. package/agent/web/src/i18n/es.ts +5 -3
  1045. package/agent/web/src/i18n/fr.ts +5 -3
  1046. package/agent/web/src/i18n/ga.ts +5 -4
  1047. package/agent/web/src/i18n/hu.ts +5 -4
  1048. package/agent/web/src/i18n/it.ts +5 -4
  1049. package/agent/web/src/i18n/ja.ts +5 -4
  1050. package/agent/web/src/i18n/ko.ts +5 -4
  1051. package/agent/web/src/i18n/pt.ts +5 -3
  1052. package/agent/web/src/i18n/ru.ts +5 -4
  1053. package/agent/web/src/i18n/tr.ts +5 -4
  1054. package/agent/web/src/i18n/types.ts +59 -1
  1055. package/agent/web/src/i18n/uk.ts +5 -3
  1056. package/agent/web/src/i18n/zh-hant.ts +5 -4
  1057. package/agent/web/src/i18n/zh.ts +5 -4
  1058. package/agent/web/src/index.css +2 -2
  1059. package/agent/web/src/lib/api.ts +819 -52
  1060. package/agent/web/src/lib/dashboard-flags.ts +16 -7
  1061. package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
  1062. package/agent/web/src/lib/reasoning-effort.ts +36 -0
  1063. package/agent/web/src/lib/session-refresh.test.ts +21 -0
  1064. package/agent/web/src/lib/session-refresh.ts +26 -0
  1065. package/agent/web/src/pages/ChannelsPage.tsx +529 -68
  1066. package/agent/web/src/pages/ChatPage.tsx +249 -56
  1067. package/agent/web/src/pages/ConfigPage.tsx +11 -1
  1068. package/agent/web/src/pages/CronPage.tsx +219 -31
  1069. package/agent/web/src/pages/EnvPage.tsx +25 -6
  1070. package/agent/web/src/pages/FilesPage.tsx +525 -0
  1071. package/agent/web/src/pages/McpPage.tsx +80 -3
  1072. package/agent/web/src/pages/ModelsPage.tsx +97 -12
  1073. package/agent/web/src/pages/PluginsPage.tsx +1 -1
  1074. package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
  1075. package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
  1076. package/agent/web/src/pages/SessionsPage.tsx +144 -13
  1077. package/agent/web/src/pages/SkillsPage.tsx +851 -70
  1078. package/agent/web/src/pages/SystemPage.tsx +340 -4
  1079. package/agent/web/src/pages/WalletPage.tsx +401 -0
  1080. package/agent/web/src/pages/WebhooksPage.tsx +145 -15
  1081. package/agent/web/src/pages/X402Page.tsx +207 -0
  1082. package/agent/web/src/plugins/registry.ts +28 -11
  1083. package/agent/web/src/plugins/sdk.d.ts +160 -0
  1084. package/agent/web/src/themes/context.tsx +112 -5
  1085. package/agent/web/src/themes/fonts.ts +167 -0
  1086. package/agent/web/src/themes/index.ts +7 -0
  1087. package/agent/web/tsconfig.app.json +0 -1
  1088. package/agent/web/vite.config.ts +1 -8
  1089. package/agent/web/vitest.config.ts +16 -0
  1090. package/package.json +1 -1
  1091. package/agent/apps/desktop/package-lock.json +0 -18363
  1092. package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
  1093. package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
  1094. package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
  1095. package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
  1096. package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
  1097. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
  1098. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
  1099. package/agent/skills/diagramming/DESCRIPTION.md +0 -3
  1100. package/agent/skills/domain/DESCRIPTION.md +0 -24
  1101. package/agent/skills/gifs/DESCRIPTION.md +0 -3
  1102. package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
  1103. package/agent/skills/mcp/DESCRIPTION.md +0 -3
  1104. package/agent/skills/media/spotify/SKILL.md +0 -135
  1105. package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
  1106. package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
  1107. package/agent/skills/productivity/linear/SKILL.md +0 -380
  1108. package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
  1109. package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
  1110. package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
  1111. package/agent/ui-tui/package-lock.json +0 -7449
  1112. package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
  1113. package/agent/web/package-lock.json +0 -8887
  1114. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
  1115. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
  1116. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
  1117. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
  1118. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
  1119. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
  1120. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
  1121. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
  1122. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
  1123. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
  1124. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
  1125. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
  1126. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
  1127. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
  1128. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
  1129. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
  1130. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
  1131. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
  1132. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
  1133. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
  1134. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
  1135. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
  1136. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
  1137. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
  1138. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
  1139. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
  1140. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
  1141. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
  1142. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
  1143. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
  1144. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
  1145. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
  1146. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
  1147. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
  1148. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
  1149. /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
  1150. /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
  1151. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
  1152. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
  1153. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
  1154. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
  1155. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
  1156. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
  1157. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
  1158. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
  1159. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
  1160. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
  1161. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
  1162. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
  1163. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
  1164. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
  1165. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
  1166. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
  1167. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
  1168. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
  1169. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
  1170. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
  1171. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
  1172. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
  1173. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
  1174. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
  1175. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
  1176. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
  1177. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
  1178. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
  1179. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
  1180. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
  1181. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
  1182. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
  1183. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
  1184. /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
  1185. /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
  1186. /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
  1187. /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
  1188. /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
  1189. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
  1190. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
  1191. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
  1192. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
  1193. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
  1194. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
  1195. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
  1196. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
  1197. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
  1198. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
  1199. /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
  1200. /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
  1201. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
  1202. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
  1203. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
  1204. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
  1205. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
  1206. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
  1207. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
  1208. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
  1209. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
  1210. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
  1211. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
  1212. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
@@ -14,22 +14,75 @@ import hashlib
14
14
  import json
15
15
  import logging
16
16
  import os
17
+ import re
17
18
  import struct
18
19
  import subprocess
19
20
  import tempfile
20
21
  import threading
21
22
  import time
22
23
  from collections import defaultdict
24
+ from contextlib import suppress
23
25
  from typing import Callable, Dict, List, Optional, Any, Tuple
24
26
 
25
27
  logger = logging.getLogger(__name__)
26
28
 
29
+
30
+ class _Snowflake:
31
+ """Minimal object exposing ``.id`` — satisfies discord.py's Snowflake
32
+ protocol for ``channel.history(before=...)`` without constructing a
33
+ ``discord.Object`` (which test doubles that stub the discord module
34
+ cannot build). Used to anchor reply-context scans inclusively.
35
+ """
36
+
37
+ __slots__ = ("id",)
38
+
39
+ def __init__(self, id: int) -> None: # noqa: A002 - matches discord API
40
+ self.id = id
41
+
27
42
  VALID_THREAD_AUTO_ARCHIVE_MINUTES = {60, 1440, 4320, 10080}
28
43
  _DISCORD_COMMAND_SYNC_POLICIES = {"safe", "bulk", "off"}
29
44
  _DISCORD_COMMAND_SYNC_STATE_SUBDIR = "gateway"
30
45
  _DISCORD_COMMAND_SYNC_STATE_FILENAME = "discord_command_sync_state.json"
46
+ _DISCORD_NONCONVERSATIONAL_STATE_FILENAME = "discord_nonconversational_messages.json"
31
47
  _DISCORD_COMMAND_SYNC_MUTATION_INTERVAL_SECONDS = 4.5
32
48
  _DISCORD_COMMAND_SYNC_MAX_RATE_LIMIT_SLEEP_SECONDS = 30.0
49
+ # Discord enforces a hard cap of 100 global application (slash) commands per
50
+ # app. Registering more makes the ENTIRE sync fail with error 30032
51
+ # ("Maximum number of application commands reached"), which silently breaks
52
+ # every slash command — not just the overflow ones. We keep the desired set
53
+ # at or below this limit at registration time.
54
+ _DISCORD_MAX_APP_COMMANDS = 100
55
+ _DISCORD_NONCONVERSATIONAL_METADATA_KEYS = frozenset({
56
+ "non_conversational",
57
+ "non_conversational_history",
58
+ })
59
+ # Upgrade-bridge fallback only. The primary mechanism is the persisted
60
+ # non-conversational message-ID set populated from explicitly marked sends
61
+ # (metadata["non_conversational"]). These regexes exist solely to recognize
62
+ # status bumps emitted by an older gateway version that pre-dates the marking,
63
+ # so they don't partition history after an upgrade. New emitters should set the
64
+ # metadata flag, not rely on a regex here.
65
+ _DISCORD_NONCONVERSATIONAL_HISTORY_MESSAGE_PATTERNS = (
66
+ re.compile(r"^\s*💾\s*Self-improvement review:\s+\S[\s\S]*$", re.IGNORECASE),
67
+ # Legacy/background-review test doubles used this shorter form before the
68
+ # self-improvement prefix became the stable emitter contract.
69
+ re.compile(
70
+ r"^\s*💾\s+Skill\s+['\"].+?['\"]\s+(?:created|updated|improved|patched)\.?\s*$",
71
+ re.IGNORECASE,
72
+ ),
73
+ re.compile(r"^\s*⏳\s+Working\s+—\s+\d+\s+min(?:\s|$)", re.IGNORECASE),
74
+ re.compile(
75
+ r"^\s*\[Background process\s+\S+\s+"
76
+ r"(?:finished with exit code|is still running~)[\s\S]*\]\s*$",
77
+ re.IGNORECASE,
78
+ ),
79
+ re.compile(
80
+ r"^\s*(?:✅|❌)\s+Hermes update\s+"
81
+ r"(?:finished|failed|timed out)[\s\S]*$",
82
+ re.IGNORECASE,
83
+ ),
84
+ re.compile(r"^\s*♻️?\s+Gateway\s+(?:restarted successfully|online\b)[\s\S]*$", re.IGNORECASE),
85
+ )
33
86
 
34
87
  try:
35
88
  import discord
@@ -48,7 +101,6 @@ from pathlib import Path as _Path
48
101
  sys.path.insert(0, str(_Path(__file__).resolve().parents[2]))
49
102
 
50
103
  from gateway.config import Platform, PlatformConfig
51
- import re
52
104
 
53
105
  from gateway.platforms.helpers import MessageDeduplicator, ThreadParticipationTracker
54
106
  from utils import atomic_json_write
@@ -68,6 +120,43 @@ from gateway.platforms.base import (
68
120
  from tools.url_safety import is_safe_url
69
121
 
70
122
 
123
+ async def _wait_for_ready_or_bot_exit(
124
+ ready_event: asyncio.Event,
125
+ bot_task: asyncio.Task,
126
+ timeout: float,
127
+ ) -> None:
128
+ """Wait until Discord is ready, or surface early bot startup failure.
129
+
130
+ ``discord.py`` startup errors (including SOCKS/proxy failures from
131
+ aiohttp-socks/python-socks) happen inside ``Bot.start()``. If ``connect()``
132
+ only waits on ``ready_event``, a dead background task still burns the full
133
+ ready timeout before the gateway supervisor can reconnect. Racing the ready
134
+ event against the bot task keeps failures fast and preserves the original
135
+ exception for logging/classification.
136
+ """
137
+ ready_task = asyncio.create_task(ready_event.wait())
138
+ try:
139
+ done, _pending = await asyncio.wait(
140
+ {ready_task, bot_task},
141
+ timeout=timeout,
142
+ return_when=asyncio.FIRST_COMPLETED,
143
+ )
144
+ if not done:
145
+ raise asyncio.TimeoutError
146
+ if bot_task in done:
147
+ exc = bot_task.exception()
148
+ if exc is not None:
149
+ raise exc
150
+ if not ready_task.done():
151
+ raise RuntimeError("Discord bot task exited before ready")
152
+ await ready_task
153
+ finally:
154
+ if not ready_task.done():
155
+ ready_task.cancel()
156
+ with suppress(asyncio.CancelledError):
157
+ await ready_task
158
+
159
+
71
160
  def _find_discord_windows_bundled_opus(discord_module: Any = None) -> Optional[str]:
72
161
  """Return discord.py's bundled Windows opus DLL path when present."""
73
162
  if sys.platform != "win32":
@@ -88,6 +177,73 @@ def _find_discord_windows_bundled_opus(discord_module: Any = None) -> Optional[s
88
177
  return None
89
178
 
90
179
 
180
+ class _DiscordNonConversationalMessageTracker:
181
+ """Persistent bounded set of Discord message IDs that are status noise."""
182
+
183
+ _MAX_TRACKED = 2000
184
+
185
+ def __init__(self, max_tracked: int = _MAX_TRACKED):
186
+ self._max_tracked = max_tracked
187
+ self._ids: dict[str, None] = dict.fromkeys(self._load())
188
+
189
+ def _state_path(self) -> _Path:
190
+ from hermes_constants import get_hermes_home
191
+
192
+ return (
193
+ get_hermes_home()
194
+ / _DISCORD_COMMAND_SYNC_STATE_SUBDIR
195
+ / _DISCORD_NONCONVERSATIONAL_STATE_FILENAME
196
+ )
197
+
198
+ def _load(self) -> list[str]:
199
+ path = self._state_path()
200
+ if not path.exists():
201
+ return []
202
+ try:
203
+ data = json.loads(path.read_text(encoding="utf-8"))
204
+ if isinstance(data, list):
205
+ return [str(message_id) for message_id in data if str(message_id).strip()]
206
+ except Exception:
207
+ logger.debug("[%s] Failed to load non-conversational Discord IDs", "Discord")
208
+ return []
209
+
210
+ def _save(self) -> None:
211
+ ids = list(self._ids)
212
+ if len(ids) > self._max_tracked:
213
+ ids = ids[-self._max_tracked:]
214
+ self._ids = dict.fromkeys(ids)
215
+ try:
216
+ atomic_json_write(self._state_path(), ids, indent=None)
217
+ except Exception:
218
+ logger.debug("[%s] Failed to save non-conversational Discord IDs", "Discord", exc_info=True)
219
+
220
+ def mark_many(self, message_ids: List[str]) -> None:
221
+ changed = False
222
+ for message_id in message_ids:
223
+ key = str(message_id or "").strip()
224
+ if key and key not in self._ids:
225
+ self._ids[key] = None
226
+ changed = True
227
+ if changed:
228
+ self._save()
229
+
230
+ def __contains__(self, message_id: str) -> bool:
231
+ return str(message_id or "") in self._ids
232
+
233
+
234
+ def _metadata_marks_nonconversational(metadata: Optional[Dict[str, Any]]) -> bool:
235
+ """Return True when an outbound send was explicitly marked as status-only."""
236
+ if not isinstance(metadata, dict):
237
+ return False
238
+ return any(bool(metadata.get(key)) for key in _DISCORD_NONCONVERSATIONAL_METADATA_KEYS)
239
+
240
+
241
+ def _looks_like_nonconversational_history_message(content: str) -> bool:
242
+ """Fallback recognizer for legacy status bumps missing persisted IDs."""
243
+ text = content or ""
244
+ return any(pattern.match(text) for pattern in _DISCORD_NONCONVERSATIONAL_HISTORY_MESSAGE_PATTERNS)
245
+
246
+
91
247
  def _clean_discord_id(entry: str) -> str:
92
248
  """Strip common prefixes from a Discord user ID or username entry.
93
249
 
@@ -520,6 +676,7 @@ class VoiceReceiver:
520
676
  ],
521
677
  check=True,
522
678
  timeout=10,
679
+ stdin=subprocess.DEVNULL,
523
680
  )
524
681
  finally:
525
682
  try:
@@ -573,6 +730,7 @@ class DiscordAdapter(BasePlatformAdapter):
573
730
  # Discord message limits
574
731
  MAX_MESSAGE_LENGTH = 2000
575
732
  _SPLIT_THRESHOLD = 1900 # near the 2000-char split point
733
+ supports_code_blocks = True # Discord markdown renders fenced code blocks natively
576
734
 
577
735
  # Auto-disconnect from voice channel after this many seconds of inactivity
578
736
  VOICE_TIMEOUT = 300
@@ -600,6 +758,17 @@ class DiscordAdapter(BasePlatformAdapter):
600
758
  self._voice_listen_tasks: Dict[int, asyncio.Task] = {} # guild_id -> listen loop
601
759
  self._voice_input_callback: Optional[Callable] = None # set by run.py
602
760
  self._on_voice_disconnect: Optional[Callable] = None # set by run.py
761
+ # Resolves the current voice-reply mode ("off"|"voice_only"|"all") for a
762
+ # linked text-channel id; set by run.py. Lets the inactivity timer leave
763
+ # the bot in the channel when the user deliberately picked text-only
764
+ # (/voice off) instead of leaving (/voice leave).
765
+ self._voice_mode_getter: Optional[Callable] = None # set by run.py
766
+ # Phase 3: continuous voice mixer (ambient idle bed + ducked speech).
767
+ # Installed once per guild on join; lets acks / TTS / the "thinking"
768
+ # loop overlap in one outgoing stream instead of stop-and-swap.
769
+ self._voice_mixers: Dict[int, Any] = {} # guild_id -> VoiceMixer
770
+ self._ambient_pcm_cache: Optional[bytes] = None # decoded ambient bed
771
+ self._voice_fx_cfg: Dict[str, Any] = self._load_voice_fx_config()
603
772
  # Track threads where the bot has participated so follow-up messages
604
773
  # in those threads don't require @mention. Persisted to disk so the
605
774
  # set survives gateway restarts.
@@ -609,6 +778,10 @@ class DiscordAdapter(BasePlatformAdapter):
609
778
  self._typing_tasks: Dict[str, asyncio.Task] = {}
610
779
  self._bot_task: Optional[asyncio.Task] = None
611
780
  self._post_connect_task: Optional[asyncio.Task] = None
781
+ # True while disconnect() is intentionally closing discord.py. The
782
+ # bot task's done callback uses this to distinguish an operator/service
783
+ # shutdown from a runtime websocket crash.
784
+ self._disconnecting = False
612
785
  # Dedup cache: prevents duplicate bot responses when Discord
613
786
  # RESUME replays events after reconnects.
614
787
  self._dedup = MessageDeduplicator()
@@ -620,6 +793,68 @@ class DiscordAdapter(BasePlatformAdapter):
620
793
  # history backfill to skip the full scan on hot paths. Falls back to
621
794
  # scanning channel.history() on cache miss (cold start / restart).
622
795
  self._last_self_message_id: Dict[str, str] = {}
796
+ # Persistent set of bot-authored lifecycle/status message IDs that
797
+ # should not act as conversational history boundaries after restart.
798
+ self._nonconversational_messages = _DiscordNonConversationalMessageTracker()
799
+
800
+ def _handle_bot_task_done(self, task: asyncio.Task) -> None:
801
+ """Surface post-startup discord.py task exits to the gateway supervisor.
802
+
803
+ discord.py reconnects normal gateway interruptions internally. When its
804
+ top-level ``Bot.start()`` task actually exits after the adapter has been
805
+ marked running, the Discord websocket is dead while the Hermes gateway
806
+ process can remain alive. Treat that split-brain state as a retryable
807
+ fatal adapter error so ``GatewayRunner._handle_adapter_fatal_error`` can
808
+ remove this adapter and queue Discord for the existing reconnect watcher.
809
+ """
810
+ if getattr(self, "_disconnecting", False):
811
+ # Intentional service/operator shutdown. Drain the task result so
812
+ # asyncio doesn't emit "exception was never retrieved" warnings.
813
+ with suppress(asyncio.CancelledError, Exception):
814
+ task.exception()
815
+ return
816
+
817
+ # Ignore stale callbacks from an older client if a reconnect already
818
+ # installed a newer Bot.start() task on this adapter instance.
819
+ if self._bot_task is not None and task is not self._bot_task:
820
+ with suppress(asyncio.CancelledError, Exception):
821
+ task.exception()
822
+ return
823
+
824
+ if not self._running:
825
+ # Startup failures are handled by _wait_for_ready_or_bot_exit() in
826
+ # connect(); this callback is only for post-startup split-brain.
827
+ with suppress(asyncio.CancelledError, Exception):
828
+ task.exception()
829
+ return
830
+
831
+ try:
832
+ exc = task.exception()
833
+ except asyncio.CancelledError:
834
+ return
835
+ except Exception as err: # pragma: no cover - defensive
836
+ exc = err
837
+
838
+ if exc is None:
839
+ message = "Discord gateway task exited without an exception"
840
+ else:
841
+ message = f"Discord gateway task exited: {exc}"
842
+
843
+ logger.error("[%s] %s", self.name, message, exc_info=exc if exc else False)
844
+ self._set_fatal_error("discord_gateway_task_exited", message, retryable=True)
845
+
846
+ async def _notify() -> None:
847
+ try:
848
+ await self._notify_fatal_error()
849
+ except Exception as notify_exc: # pragma: no cover - defensive logging
850
+ logger.warning(
851
+ "[%s] Failed to notify gateway supervisor about Discord task exit: %s",
852
+ self.name,
853
+ notify_exc,
854
+ exc_info=True,
855
+ )
856
+
857
+ asyncio.create_task(_notify())
623
858
 
624
859
  async def connect(self) -> bool:
625
860
  """Connect to Discord and start receiving events."""
@@ -781,6 +1016,7 @@ class DiscordAdapter(BasePlatformAdapter):
781
1016
  # Must run BEFORE the user allowlist check so that bots
782
1017
  # permitted by DISCORD_ALLOW_BOTS are not rejected for
783
1018
  # not being in DISCORD_ALLOWED_USERS (fixes #4466).
1019
+ _role_authorized = False
784
1020
  if getattr(message.author, "bot", False):
785
1021
  allow_bots = os.getenv("DISCORD_ALLOW_BOTS", "none").lower().strip()
786
1022
  if allow_bots == "none":
@@ -804,6 +1040,7 @@ class DiscordAdapter(BasePlatformAdapter):
804
1040
  is_dm=_is_dm,
805
1041
  ):
806
1042
  return
1043
+ _role_authorized = bool(getattr(self, "_allowed_role_ids", set()))
807
1044
 
808
1045
  # Multi-agent filtering: if the message mentions specific bots
809
1046
  # but NOT this bot, the sender is talking to another agent —
@@ -845,7 +1082,7 @@ class DiscordAdapter(BasePlatformAdapter):
845
1082
  if "*" not in _free_channels and not (_channel_ids & _free_channels):
846
1083
  return
847
1084
 
848
- await self._handle_message(message)
1085
+ await self._handle_message(message, role_authorized=_role_authorized)
849
1086
 
850
1087
  @self._client.event
851
1088
  async def on_voice_state_update(member, before, after):
@@ -885,25 +1122,55 @@ class DiscordAdapter(BasePlatformAdapter):
885
1122
  self._register_slash_commands()
886
1123
 
887
1124
  # Start the bot in background
1125
+ self._disconnecting = False
888
1126
  self._bot_task = asyncio.create_task(self._client.start(self.config.token))
1127
+ self._bot_task.add_done_callback(self._handle_bot_task_done)
889
1128
 
890
- # Wait for ready
891
- await asyncio.wait_for(self._ready_event.wait(), timeout=30)
1129
+ # Wait for ready, but fail fast if discord.py's background startup
1130
+ # task dies first (for example on SOCKS/proxy connect errors).
1131
+ await _wait_for_ready_or_bot_exit(self._ready_event, self._bot_task, timeout=30)
892
1132
 
893
1133
  self._running = True
894
1134
  return True
895
1135
 
896
1136
  except asyncio.TimeoutError:
897
1137
  logger.error("[%s] Timeout waiting for connection to Discord", self.name, exc_info=True)
1138
+ # Cancel the background bot task so it cannot fire on_message after
1139
+ # this adapter is discarded. Without this, the task keeps running and
1140
+ # a later successful reconnect leaves two active Discord clients that
1141
+ # each process every message, producing duplicate threads/responses.
1142
+ await self._cancel_bot_task()
898
1143
  self._release_platform_lock()
899
1144
  return False
900
1145
  except Exception as e: # pragma: no cover - defensive logging
901
1146
  logger.error("[%s] Failed to connect to Discord: %s", self.name, e, exc_info=True)
1147
+ # Same zombie-client hazard as the timeout branch: the background
1148
+ # client.start() task may already be running when a later setup
1149
+ # step raises. Cancel it so the discarded adapter cannot connect.
1150
+ await self._cancel_bot_task()
902
1151
  self._release_platform_lock()
903
1152
  return False
904
1153
 
1154
+ async def _cancel_bot_task(self) -> None:
1155
+ """Cancel and await the background client.start() task, if running."""
1156
+ if self._bot_task and not self._bot_task.done():
1157
+ self._bot_task.cancel()
1158
+ try:
1159
+ await self._bot_task
1160
+ except (asyncio.CancelledError, Exception):
1161
+ pass
1162
+ self._bot_task = None
1163
+
905
1164
  async def disconnect(self) -> None:
906
1165
  """Disconnect from Discord."""
1166
+ self._disconnecting = True
1167
+ # Cancel the bot task before closing the client. If connect() timed out
1168
+ # and returned False, the background client.start() task may still be
1169
+ # running; calling client.close() alone is not enough to stop it because
1170
+ # discord.py's reconnect loop can ignore the closed flag while a
1171
+ # WebSocket handshake is in flight. Explicitly cancelling the task here
1172
+ # ensures the zombie client cannot receive or dispatch any further events.
1173
+ await self._cancel_bot_task()
907
1174
  # Clean up all active voice connections before closing the client
908
1175
  for guild_id in list(self._voice_clients.keys()):
909
1176
  try:
@@ -1425,6 +1692,7 @@ class DiscordAdapter(BasePlatformAdapter):
1425
1692
  thread_id = None
1426
1693
  if metadata and metadata.get("thread_id"):
1427
1694
  thread_id = metadata["thread_id"]
1695
+ nonconversational = _metadata_marks_nonconversational(metadata)
1428
1696
 
1429
1697
  if thread_id:
1430
1698
  # Fetch the thread directly — threads are addressed by their own ID.
@@ -1502,7 +1770,10 @@ class DiscordAdapter(BasePlatformAdapter):
1502
1770
  # backfill — avoids a full channel.history() scan on hot paths.
1503
1771
  if message_ids:
1504
1772
  _target_id = thread_id or chat_id
1505
- self._last_self_message_id[_target_id] = message_ids[-1]
1773
+ if nonconversational:
1774
+ self._nonconversational_messages.mark_many(message_ids)
1775
+ elif not _looks_like_nonconversational_history_message(content):
1776
+ self._last_self_message_id[_target_id] = message_ids[-1]
1506
1777
 
1507
1778
  return SendResult(
1508
1779
  success=True,
@@ -1925,6 +2196,160 @@ class DiscordAdapter(BasePlatformAdapter):
1925
2196
  # Voice channel methods (join / leave / play)
1926
2197
  # ------------------------------------------------------------------
1927
2198
 
2199
+ def _load_voice_fx_config(self) -> Dict[str, Any]:
2200
+ """Read voice mixer / ambient / ack settings from config.yaml.
2201
+
2202
+ All settings live under ``discord.voice_fx`` in config.yaml (NOT the
2203
+ .env file — these are behavioral, not secrets). The feature is OFF by
2204
+ default; users opt in with ``discord.voice_fx.enabled: true``.
2205
+
2206
+ Returns a dict with safe defaults so callers never KeyError.
2207
+ """
2208
+ defaults: Dict[str, Any] = {
2209
+ "enabled": False, # master switch for the mixer subsystem
2210
+ "ambient_enabled": True, # idle "thinking" bed while tools run
2211
+ "ambient_path": "", # optional custom loop file; "" = synthesised
2212
+ "ambient_gain": 0.18, # idle bed loudness (0..1)
2213
+ "duck_gain": 0.06, # ambient loudness while speech plays
2214
+ "speech_gain": 1.0, # TTS / ack loudness
2215
+ "ack_enabled": True, # speak a short phrase before tool calls
2216
+ "ack_phrases": [
2217
+ "Let me look into that.",
2218
+ "One moment.",
2219
+ "Checking on that now.",
2220
+ "Give me a sec.",
2221
+ "On it.",
2222
+ ],
2223
+ }
2224
+ try:
2225
+ from hermes_cli.config import read_raw_config
2226
+ cfg = read_raw_config() or {}
2227
+ fx = ((cfg.get("discord") or {}).get("voice_fx") or {})
2228
+ if isinstance(fx, dict):
2229
+ for k, v in fx.items():
2230
+ if k in defaults and v is not None:
2231
+ defaults[k] = v
2232
+ except Exception as e:
2233
+ logger.debug("Could not load discord.voice_fx config: %s", e)
2234
+ return defaults
2235
+
2236
+ def _get_ambient_pcm(self) -> Optional[bytes]:
2237
+ """Return decoded 48k/stereo/s16le PCM for the ambient idle bed.
2238
+
2239
+ Uses a custom file when ``ambient_path`` is set and decodable, else a
2240
+ synthesised pad. Cached after first build.
2241
+ """
2242
+ if self._ambient_pcm_cache is not None:
2243
+ return self._ambient_pcm_cache
2244
+ if not self._voice_fx_cfg.get("ambient_enabled"):
2245
+ return None
2246
+ try:
2247
+ from voice_mixer import decode_to_pcm, synth_ambient_pcm
2248
+ except ImportError:
2249
+ from .voice_mixer import decode_to_pcm, synth_ambient_pcm
2250
+
2251
+ pcm: Optional[bytes] = None
2252
+ path = (self._voice_fx_cfg.get("ambient_path") or "").strip()
2253
+ if path and os.path.isfile(path):
2254
+ pcm = decode_to_pcm(path)
2255
+ if not pcm:
2256
+ logger.warning("Ambient file %s failed to decode; using synth bed", path)
2257
+ if not pcm:
2258
+ pcm = synth_ambient_pcm()
2259
+ self._ambient_pcm_cache = pcm
2260
+ return pcm
2261
+
2262
+ async def _install_voice_mixer(self, guild_id: int, vc) -> None:
2263
+ """Create a VoiceMixer, start the ambient bed, and play it on the VC.
2264
+
2265
+ The mixer runs continuously for the life of the connection: one
2266
+ ``vc.play(mixer)`` call, never stopped until leave.
2267
+ """
2268
+ try:
2269
+ from voice_mixer import VoiceMixer
2270
+ except ImportError:
2271
+ from .voice_mixer import VoiceMixer
2272
+
2273
+ mixer = VoiceMixer(
2274
+ ambient_gain=float(self._voice_fx_cfg.get("ambient_gain", 0.18)),
2275
+ duck_gain=float(self._voice_fx_cfg.get("duck_gain", 0.06)),
2276
+ speech_gain=float(self._voice_fx_cfg.get("speech_gain", 1.0)),
2277
+ )
2278
+ ambient = await asyncio.to_thread(self._get_ambient_pcm)
2279
+ if ambient:
2280
+ mixer.set_ambient(ambient)
2281
+
2282
+ def _after(error):
2283
+ if error:
2284
+ logger.error("Voice mixer stream error (guild=%d): %s", guild_id, error)
2285
+
2286
+ if vc.is_playing():
2287
+ vc.stop()
2288
+ vc.play(mixer, after=_after)
2289
+ self._voice_mixers[guild_id] = mixer
2290
+ logger.info("Voice mixer installed (guild=%d, ambient=%s)", guild_id, bool(ambient))
2291
+
2292
+ async def play_ack_in_voice(self, guild_id: int, phrase: Optional[str] = None) -> bool:
2293
+ """Speak a short acknowledgement over the ambient bed.
2294
+
2295
+ Called from the gateway's tool-progress hook on the first tool call of
2296
+ a turn, so the user hears "let me look into that" before the bot goes
2297
+ quiet to work. No-op unless the mixer is installed and acks enabled.
2298
+ """
2299
+ if not self._voice_fx_cfg.get("ack_enabled"):
2300
+ return False
2301
+ mixer = self._voice_mixers.get(guild_id)
2302
+ if mixer is None:
2303
+ return False
2304
+ if phrase is None:
2305
+ import random
2306
+ phrases = self._voice_fx_cfg.get("ack_phrases") or ["One moment."]
2307
+ phrase = random.choice(phrases)
2308
+
2309
+ # Synthesise the ack via the configured TTS provider, then layer it.
2310
+ import uuid as _uuid
2311
+ audio_path = os.path.join(
2312
+ tempfile.gettempdir(), "hermes_voice",
2313
+ f"ack_{_uuid.uuid4().hex[:12]}.mp3",
2314
+ )
2315
+ os.makedirs(os.path.dirname(audio_path), exist_ok=True)
2316
+ try:
2317
+ from tools.tts_tool import text_to_speech_tool
2318
+ result_json = await asyncio.to_thread(
2319
+ text_to_speech_tool, text=phrase, output_path=audio_path
2320
+ )
2321
+ result = json.loads(result_json)
2322
+ actual = result.get("file_path", audio_path)
2323
+ if not result.get("success") or not os.path.isfile(actual):
2324
+ return False
2325
+ try:
2326
+ from voice_mixer import decode_to_pcm
2327
+ except ImportError:
2328
+ from .voice_mixer import decode_to_pcm
2329
+ pcm = await asyncio.to_thread(decode_to_pcm, actual)
2330
+ if not pcm:
2331
+ return False
2332
+ mixer.play_speech(
2333
+ pcm, gain=float(self._voice_fx_cfg.get("speech_gain", 1.0))
2334
+ )
2335
+ self._reset_voice_timeout(guild_id)
2336
+ return True
2337
+ except Exception as e:
2338
+ logger.debug("play_ack_in_voice failed: %s", e)
2339
+ return False
2340
+ finally:
2341
+ for p in {audio_path, locals().get("actual")}:
2342
+ if p and os.path.isfile(p):
2343
+ try:
2344
+ os.unlink(p)
2345
+ except OSError:
2346
+ pass
2347
+
2348
+ def voice_mixer_active(self, guild_id: int) -> bool:
2349
+ """True when a continuous mixer is installed for this guild."""
2350
+ mixers = getattr(self, "_voice_mixers", None)
2351
+ return bool(mixers) and mixers.get(guild_id) is not None
2352
+
1928
2353
  async def join_voice_channel(self, channel) -> bool:
1929
2354
  """Join a Discord voice channel. Returns True on success."""
1930
2355
  if not self._client or not DISCORD_AVAILABLE:
@@ -1957,6 +2382,15 @@ class DiscordAdapter(BasePlatformAdapter):
1957
2382
  except Exception as e:
1958
2383
  logger.warning("Voice receiver failed to start: %s", e)
1959
2384
 
2385
+ # Phase 3: install the continuous mixer (ambient bed + ducked
2386
+ # speech). Best-effort — if it fails we fall back to the legacy
2387
+ # one-shot FFmpegPCMAudio playback path in play_in_voice_channel.
2388
+ if getattr(self, "_voice_fx_cfg", {}).get("enabled"):
2389
+ try:
2390
+ await self._install_voice_mixer(guild_id, vc)
2391
+ except Exception as e:
2392
+ logger.warning("Voice mixer failed to start: %s", e)
2393
+
1960
2394
  return True
1961
2395
 
1962
2396
  async def leave_voice_channel(self, guild_id: int) -> None:
@@ -1970,8 +2404,17 @@ class DiscordAdapter(BasePlatformAdapter):
1970
2404
  if listen_task:
1971
2405
  listen_task.cancel()
1972
2406
 
2407
+ # Tear down the mixer (stops the continuous outgoing stream).
2408
+ if getattr(self, "_voice_mixers", None) is not None:
2409
+ self._voice_mixers.pop(guild_id, None)
2410
+
1973
2411
  vc = self._voice_clients.pop(guild_id, None)
1974
2412
  if vc and vc.is_connected():
2413
+ try:
2414
+ if vc.is_playing():
2415
+ vc.stop()
2416
+ except Exception:
2417
+ pass
1975
2418
  await vc.disconnect()
1976
2419
  task = self._voice_timeout_tasks.pop(guild_id, None)
1977
2420
  if task:
@@ -1983,11 +2426,43 @@ class DiscordAdapter(BasePlatformAdapter):
1983
2426
  PLAYBACK_TIMEOUT = 120
1984
2427
 
1985
2428
  async def play_in_voice_channel(self, guild_id: int, audio_path: str) -> bool:
1986
- """Play an audio file in the connected voice channel."""
2429
+ """Play an audio file in the connected voice channel.
2430
+
2431
+ When the continuous mixer is installed for this guild, the clip is
2432
+ decoded to PCM and layered over the ambient bed (ducking it) so the
2433
+ reply can overlap the idle "thinking" loop seamlessly. Otherwise we
2434
+ fall back to the legacy one-shot FFmpegPCMAudio path.
2435
+ """
1987
2436
  vc = self._voice_clients.get(guild_id)
1988
2437
  if not vc or not vc.is_connected():
1989
2438
  return False
1990
2439
 
2440
+ # ── Mixer path (overlap + ducking) ──────────────────────────────
2441
+ mixer = getattr(self, "_voice_mixers", {}).get(guild_id) if getattr(self, "_voice_mixers", None) else None
2442
+ if mixer is not None:
2443
+ try:
2444
+ from voice_mixer import decode_to_pcm
2445
+ except ImportError:
2446
+ from .voice_mixer import decode_to_pcm
2447
+ pcm = await asyncio.to_thread(decode_to_pcm, audio_path)
2448
+ if pcm:
2449
+ speech_gain = float(self._voice_fx_cfg.get("speech_gain", 1.0))
2450
+ mixer.play_speech(pcm, gain=speech_gain)
2451
+ # Block until the speech child drains so callers serialise
2452
+ # replies (mirrors legacy semantics) but the ambient keeps
2453
+ # playing underneath the whole time.
2454
+ wait_start = time.monotonic()
2455
+ while mixer.speech_active:
2456
+ if time.monotonic() - wait_start > self.PLAYBACK_TIMEOUT:
2457
+ logger.warning("Mixer speech playback timed out after %ds", self.PLAYBACK_TIMEOUT)
2458
+ mixer.stop_speech()
2459
+ break
2460
+ await asyncio.sleep(0.05)
2461
+ self._reset_voice_timeout(guild_id)
2462
+ return True
2463
+ logger.warning("Mixer decode failed for %s; falling back to legacy playback", audio_path)
2464
+
2465
+ # ── Legacy one-shot path (no mixer) ─────────────────────────────
1991
2466
  # Pause voice receiver while playing (echo prevention)
1992
2467
  receiver = self._voice_receivers.get(guild_id)
1993
2468
  if receiver:
@@ -2053,6 +2528,20 @@ class DiscordAdapter(BasePlatformAdapter):
2053
2528
  except asyncio.CancelledError:
2054
2529
  return
2055
2530
  text_ch_id = self._voice_text_channels.get(guild_id)
2531
+ # ``/voice off`` mutes spoken replies but deliberately keeps the bot in
2532
+ # the channel (leaving is ``/voice leave``). The inactivity timer only
2533
+ # counts the bot's OWN audio as activity, so under voice-off mode it
2534
+ # fires every VOICE_TIMEOUT seconds, yanks the bot out, and spams the
2535
+ # text channel with "Left voice channel (inactivity timeout)." Honor the
2536
+ # user's choice: skip the auto-disconnect while voice replies are off.
2537
+ # (The timer re-arms when the bot next speaks or hears a user.)
2538
+ _mode_getter = getattr(self, "_voice_mode_getter", None)
2539
+ if text_ch_id is not None and _mode_getter is not None:
2540
+ try:
2541
+ if _mode_getter(str(text_ch_id)) == "off":
2542
+ return
2543
+ except Exception:
2544
+ pass
2056
2545
  await self.leave_voice_channel(guild_id)
2057
2546
  # Notify the runner so it can clean up voice_mode state
2058
2547
  if self._on_voice_disconnect and text_ch_id:
@@ -2183,6 +2672,11 @@ class DiscordAdapter(BasePlatformAdapter):
2183
2672
  is_dm=False,
2184
2673
  ):
2185
2674
  continue
2675
+ # A user speaking to the bot is activity too — not just the
2676
+ # bot's own playback. Reset the inactivity timer so an active
2677
+ # listener isn't disconnected mid-conversation (this also
2678
+ # covers voice-on text-only sessions that never play audio).
2679
+ self._reset_voice_timeout(guild_id)
2186
2680
  await self._process_voice_input(guild_id, user_id, pcm_data)
2187
2681
  except asyncio.CancelledError:
2188
2682
  pass
@@ -3149,6 +3643,11 @@ class DiscordAdapter(BasePlatformAdapter):
3149
3643
  )
3150
3644
 
3151
3645
  already_registered: set[str] = set()
3646
+ # Native commands above are registered first and are the highest
3647
+ # priority, so they always survive the 100-command cap. Reserve one
3648
+ # slot for the consolidated ``/skill`` group registered further below.
3649
+ slot_cap = _DISCORD_MAX_APP_COMMANDS - 1
3650
+ dropped_over_cap = 0
3152
3651
  try:
3153
3652
  from hermes_cli.commands import COMMAND_REGISTRY, _is_gateway_available, _resolve_config_gates
3154
3653
 
@@ -3166,6 +3665,9 @@ class DiscordAdapter(BasePlatformAdapter):
3166
3665
  discord_name = cmd_def.name.lower()[:32]
3167
3666
  if discord_name in already_registered:
3168
3667
  continue
3668
+ if len(already_registered) >= slot_cap:
3669
+ dropped_over_cap += 1
3670
+ continue
3169
3671
  auto_cmd = _build_auto_slash_command(
3170
3672
  cmd_def.name,
3171
3673
  cmd_def.description,
@@ -3198,6 +3700,9 @@ class DiscordAdapter(BasePlatformAdapter):
3198
3700
  discord_name = plugin_name.lower()[:32]
3199
3701
  if discord_name in already_registered:
3200
3702
  continue
3703
+ if len(already_registered) >= slot_cap:
3704
+ dropped_over_cap += 1
3705
+ continue
3201
3706
  auto_cmd = _build_auto_slash_command(
3202
3707
  plugin_name,
3203
3708
  plugin_desc,
@@ -3220,6 +3725,20 @@ class DiscordAdapter(BasePlatformAdapter):
3220
3725
  # supporting up to 25 categories × 25 skills = 625 skills.
3221
3726
  self._register_skill_group(tree)
3222
3727
 
3728
+ if dropped_over_cap:
3729
+ # Staying under the cap keeps the whole sync succeeding; without
3730
+ # this guard a single over-limit command makes Discord reject the
3731
+ # entire batch (error 30032), breaking every slash command.
3732
+ logger.warning(
3733
+ "[%s] Reached Discord's limit of %d slash commands; skipped %d "
3734
+ "lower-priority command(s) to keep the command sync working. "
3735
+ "Disable slash commands you don't need or trim installed plugins "
3736
+ "to surface them all.",
3737
+ self.name,
3738
+ _DISCORD_MAX_APP_COMMANDS,
3739
+ dropped_over_cap,
3740
+ )
3741
+
3223
3742
  # Optional defense-in-depth: hide every slash command from non-admin
3224
3743
  # guild members in Discord's slash picker. Server-side authorization
3225
3744
  # (``_check_slash_authorization``) is the actual gate; this is purely
@@ -3749,6 +4268,7 @@ class DiscordAdapter(BasePlatformAdapter):
3749
4268
  self,
3750
4269
  channel: Any,
3751
4270
  before: "DiscordMessage",
4271
+ reply_target: Optional[Any] = None,
3752
4272
  ) -> str:
3753
4273
  """Fetch recent channel messages for conversational context.
3754
4274
 
@@ -3756,6 +4276,13 @@ class DiscordAdapter(BasePlatformAdapter):
3756
4276
  a message sent by this bot (the natural partition point between
3757
4277
  bot turns) or reaches ``history_backfill_limit``.
3758
4278
 
4279
+ When ``reply_target`` is provided (the user replied to a specific
4280
+ message), a second backward scan is run ending at that target so the
4281
+ agent sees the conversation surrounding what the user pointed at —
4282
+ even when the reply target sits *before* the most recent bot turn and
4283
+ would otherwise be cut off by the self-message partition. The two
4284
+ windows are merged chronologically and de-duplicated by message ID.
4285
+
3759
4286
  Returns a formatted block like::
3760
4287
 
3761
4288
  [Recent channel messages]
@@ -3789,7 +4316,47 @@ class DiscordAdapter(BasePlatformAdapter):
3789
4316
  pass # Malformed cache entry — fall back to cold-start scan
3790
4317
 
3791
4318
  try:
3792
- collected = []
4319
+ def _keep(msg) -> Optional[str]:
4320
+ """Return a formatted ``[name] content`` line, or None to skip.
4321
+
4322
+ Encapsulates the system-message / non-conversational / other-bot
4323
+ filtering so both the primary and reply-anchored scans apply
4324
+ identical rules. Does NOT enforce the self-message partition —
4325
+ callers decide where to stop.
4326
+ """
4327
+ if msg.type not in {discord.MessageType.default, discord.MessageType.reply}:
4328
+ return None
4329
+ content = getattr(msg, "clean_content", msg.content) or ""
4330
+ if (
4331
+ str(getattr(msg, "id", "")) in self._nonconversational_messages
4332
+ or _looks_like_nonconversational_history_message(content)
4333
+ ):
4334
+ return None
4335
+ # Respect DISCORD_ALLOW_BOTS for other bots. For history
4336
+ # context, "mentions" is treated as "all" — we are deciding
4337
+ # what context to show, not whether to respond.
4338
+ if (
4339
+ getattr(msg.author, "bot", False)
4340
+ and msg.author != self._client.user
4341
+ and not include_other_bots
4342
+ ):
4343
+ return None
4344
+ if not content and msg.attachments:
4345
+ content = "(attachment)"
4346
+ if not content:
4347
+ return None
4348
+ name = (
4349
+ getattr(msg.author, "display_name", None)
4350
+ or getattr(msg.author, "name", None)
4351
+ or "unknown"
4352
+ )
4353
+ if getattr(msg.author, "bot", False):
4354
+ name = f"{name} [bot]"
4355
+ return f"[{name}] {content}"
4356
+
4357
+ # ── Primary window: recent channel activity since the last bot turn ──
4358
+ collected: List[Tuple[str, str]] = [] # (message_id, line)
4359
+ seen_ids: set = set()
3793
4360
  # IMPORTANT: pass oldest_first=False explicitly. discord.py 2.x
3794
4361
  # silently flips the default to True when `after=` is supplied,
3795
4362
  # which would select the *earliest* N messages after our last
@@ -3803,39 +4370,89 @@ class DiscordAdapter(BasePlatformAdapter):
3803
4370
  after=_after_obj,
3804
4371
  oldest_first=False,
3805
4372
  ):
3806
- # Stop at our own message — this is the partition point.
3807
- # Everything before this is already in the session transcript.
3808
- # (Redundant when _after_obj is set, but needed for cold start.)
4373
+ # Non-conversational lifecycle/status bumps (self-improvement
4374
+ # reviews, background-process notices, restart banners) must be
4375
+ # skipped BEFORE the partition check otherwise a delayed
4376
+ # status bump authored by us would be mistaken for the real
4377
+ # last bot turn and hide messages that came after it.
4378
+ _content = getattr(msg, "clean_content", msg.content) or ""
4379
+ if (
4380
+ str(getattr(msg, "id", "")) in self._nonconversational_messages
4381
+ or _looks_like_nonconversational_history_message(_content)
4382
+ ):
4383
+ continue
4384
+ # Stop at our own (conversational) message — this is the
4385
+ # partition point. Everything before this is already in the
4386
+ # session transcript. (Redundant when _after_obj is set, but
4387
+ # needed for cold start.)
3809
4388
  if msg.author == self._client.user:
3810
4389
  break
3811
-
3812
- # Skip system messages (pins, joins, thread renames, etc.)
3813
- if msg.type not in {discord.MessageType.default, discord.MessageType.reply}:
3814
- continue
3815
-
3816
- # Respect DISCORD_ALLOW_BOTS for other bots.
3817
- # For history context, "mentions" is treated as "all" — we are
3818
- # deciding what context to show, not whether to respond.
3819
- if getattr(msg.author, "bot", False) and not include_other_bots:
3820
- continue
3821
-
3822
- content = getattr(msg, "clean_content", msg.content) or ""
3823
- if not content and msg.attachments:
3824
- content = "(attachment)"
3825
- if not content:
4390
+ line = _keep(msg)
4391
+ if line is None:
3826
4392
  continue
4393
+ mid = str(getattr(msg, "id", ""))
4394
+ collected.append((mid, line))
4395
+ if mid:
4396
+ seen_ids.add(mid)
4397
+
4398
+ # ── Reply window: context around the message the user pointed at ──
4399
+ # When the user replied to a specific message that sits BEFORE the
4400
+ # primary window's partition point, the surrounding exchange isn't
4401
+ # captured above. Fetch a small window ending just after the reply
4402
+ # target so the agent sees what it was referencing. This window is
4403
+ # NOT partitioned on the self-message boundary — the whole point is
4404
+ # to surface older context the transcript lacks.
4405
+ reply_collected: List[Tuple[str, str]] = []
4406
+ reply_target_id = str(getattr(reply_target, "id", "")) if reply_target else ""
4407
+ if reply_target is not None and reply_target_id and reply_target_id not in seen_ids:
4408
+ # Reuse the same cap as the primary scan but keep the reply
4409
+ # window modest — it's anchored context, not a full backfill.
4410
+ reply_limit = max(1, min(limit, 10))
4411
+ # `before` is exclusive in discord.py, so to *include* the
4412
+ # target we anchor at target_id + 1. Use a minimal snowflake
4413
+ # shim (any object exposing ``.id`` satisfies discord.py's
4414
+ # Snowflake protocol) rather than discord.Object, so this path
4415
+ # works under test doubles that stub the discord module too.
4416
+ try:
4417
+ _before_obj = _Snowflake(int(reply_target_id) + 1)
4418
+ except (ValueError, TypeError):
4419
+ _before_obj = before
4420
+ async for msg in channel.history(
4421
+ limit=reply_limit,
4422
+ before=_before_obj,
4423
+ oldest_first=False,
4424
+ ):
4425
+ line = _keep(msg)
4426
+ if line is None:
4427
+ continue
4428
+ mid = str(getattr(msg, "id", ""))
4429
+ if mid and mid in seen_ids:
4430
+ continue
4431
+ reply_collected.append((mid, line))
4432
+ if mid:
4433
+ seen_ids.add(mid)
3827
4434
 
3828
- name = msg.author.display_name
3829
- if getattr(msg.author, "bot", False):
3830
- name = f"{name} [bot]"
3831
- collected.append(f"[{name}] {content}")
3832
-
3833
- if not collected:
4435
+ if not collected and not reply_collected:
3834
4436
  return ""
3835
4437
 
3836
- # channel.history returns newest-first (oldest_first=False); reverse for chronological order
4438
+ # channel.history returns newest-first; reverse each window for
4439
+ # chronological order, then present reply context first (it is
4440
+ # older) followed by the recent activity.
3837
4441
  collected.reverse()
3838
- return "[Recent channel messages]\n" + "\n".join(collected)
4442
+ reply_collected.reverse()
4443
+
4444
+ blocks: List[str] = []
4445
+ if reply_collected:
4446
+ blocks.append(
4447
+ "[Context around the replied-to message]\n"
4448
+ + "\n".join(line for _id, line in reply_collected)
4449
+ )
4450
+ if collected:
4451
+ blocks.append(
4452
+ "[Recent channel messages]\n"
4453
+ + "\n".join(line for _id, line in collected)
4454
+ )
4455
+ return "\n\n".join(blocks)
3839
4456
 
3840
4457
  except discord.Forbidden:
3841
4458
  logger.debug("[%s] Missing permissions to fetch channel history", self.name)
@@ -4101,6 +4718,7 @@ class DiscordAdapter(BasePlatformAdapter):
4101
4718
  )
4102
4719
 
4103
4720
  msg = await channel.send(embed=embed, view=view)
4721
+ view._message = msg # store for on_timeout expiration editing
4104
4722
  return SendResult(success=True, message_id=str(msg.id))
4105
4723
 
4106
4724
  except Exception as e:
@@ -4140,6 +4758,7 @@ class DiscordAdapter(BasePlatformAdapter):
4140
4758
  )
4141
4759
 
4142
4760
  msg = await channel.send(embed=embed, view=view)
4761
+ view._message = msg # store for on_timeout expiration editing
4143
4762
  return SendResult(success=True, message_id=str(msg.id))
4144
4763
  except Exception as e:
4145
4764
  return SendResult(success=False, error=str(e))
@@ -4164,6 +4783,13 @@ class DiscordAdapter(BasePlatformAdapter):
4164
4783
  Open-ended mode (``choices`` empty/None): renders the question as
4165
4784
  plain embed text — no buttons. The gateway's text-intercept captures
4166
4785
  the next message in this session and resolves the clarify.
4786
+
4787
+ Choice normalisation: ``choices`` may contain bare strings OR dicts
4788
+ (LLMs sometimes emit ``[{"description": "..."}]`` instead of bare
4789
+ strings, which would otherwise render as raw Python repr on the
4790
+ button label). Dict choices are unwrapped against the canonical
4791
+ LLM tool-call keys ``label``, ``description``, ``text``, ``title``
4792
+ in that order. Dicts with none of those keys are dropped.
4167
4793
  """
4168
4794
  if not self._client or not DISCORD_AVAILABLE:
4169
4795
  return SendResult(success=False, error="Not connected")
@@ -4189,8 +4815,37 @@ class DiscordAdapter(BasePlatformAdapter):
4189
4815
  color=discord.Color.orange(),
4190
4816
  )
4191
4817
 
4818
+ # Normalise choices: LLMs sometimes emit `[{"description": "..."}]`
4819
+ # instead of bare strings, which would render as raw Python repr on
4820
+ # the button label. Unwrap the common shapes, then stringify.
4821
+ def _flatten_choice(c):
4822
+ if c is None:
4823
+ return ""
4824
+ if isinstance(c, str):
4825
+ return c.strip()
4826
+ if isinstance(c, dict):
4827
+ # Prefer the canonical LLM tool-call user-facing keys
4828
+ # in the order the LLM is most likely to emit them.
4829
+ # 'name' and 'value' are deliberately NOT here: they're
4830
+ # Discord-component-shaped fields that could appear in
4831
+ # dicts that aren't meant to be choices (e.g., a
4832
+ # developer-error wiring that passes a Button-shaped
4833
+ # object). Picking them would leak raw enum values
4834
+ # or 4-char model identifiers onto user-facing buttons.
4835
+ # If a dict has none of the canonical keys, drop it
4836
+ # rather than picking some random field — a garbage
4837
+ # button label is worse than no button at all.
4838
+ for key in ("label", "description", "text", "title"):
4839
+ v = c.get(key)
4840
+ if isinstance(v, str) and v.strip():
4841
+ return v.strip()
4842
+ return ""
4843
+ if isinstance(c, (list, tuple)):
4844
+ return " ".join(_flatten_choice(x) for x in c).strip()
4845
+ return str(c).strip()
4846
+
4192
4847
  clean_choices = [
4193
- str(c).strip() for c in (choices or []) if c is not None and str(c).strip()
4848
+ s for s in (_flatten_choice(c) for c in (choices or [])) if s
4194
4849
  ]
4195
4850
  # Discord allows up to 5 buttons per row, 5 rows per view = 25.
4196
4851
  # We reserve one slot for the "Other" button, so cap at 24 choices.
@@ -4217,6 +4872,8 @@ class DiscordAdapter(BasePlatformAdapter):
4217
4872
  view = None
4218
4873
 
4219
4874
  msg = await channel.send(embed=embed, view=view) if view else await channel.send(embed=embed)
4875
+ if view:
4876
+ view._message = msg # store for on_timeout expiration editing
4220
4877
  return SendResult(success=True, message_id=str(msg.id))
4221
4878
  except Exception as e:
4222
4879
  logger.warning("[%s] send_clarify failed: %s", self.name, e)
@@ -4252,6 +4909,9 @@ class DiscordAdapter(BasePlatformAdapter):
4252
4909
  allowed_role_ids=self._allowed_role_ids,
4253
4910
  )
4254
4911
  msg = await channel.send(embed=embed, view=view)
4912
+ view._message = msg # store for on_timeout expiration editing
4913
+ if _metadata_marks_nonconversational(metadata):
4914
+ self._nonconversational_messages.mark_many([str(msg.id)])
4255
4915
  return SendResult(success=True, message_id=str(msg.id))
4256
4916
  except Exception as e:
4257
4917
  return SendResult(success=False, error=str(e))
@@ -4311,6 +4971,7 @@ class DiscordAdapter(BasePlatformAdapter):
4311
4971
  )
4312
4972
 
4313
4973
  msg = await channel.send(embed=embed, view=view)
4974
+ view._message = msg # store for on_timeout expiration editing
4314
4975
  return SendResult(success=True, message_id=str(msg.id))
4315
4976
 
4316
4977
  except Exception as e:
@@ -4484,7 +5145,7 @@ class DiscordAdapter(BasePlatformAdapter):
4484
5145
  raise Exception(f"HTTP {resp.status}")
4485
5146
  return await resp.read()
4486
5147
 
4487
- async def _handle_message(self, message: DiscordMessage) -> None:
5148
+ async def _handle_message(self, message: DiscordMessage, role_authorized: bool = False) -> None:
4488
5149
  """Handle incoming Discord messages."""
4489
5150
  # In server channels (not DMs), require the bot to be @mentioned
4490
5151
  # UNLESS the channel is in the free-response list or the message is
@@ -4598,7 +5259,13 @@ class DiscordAdapter(BasePlatformAdapter):
4598
5259
  auto_threaded_channel = thread
4599
5260
  self._threads.mark(thread_id)
4600
5261
 
4601
- all_attachments = list(message.attachments) + snapshot_attachments
5262
+ referenced_attachments = []
5263
+ reference = getattr(message, "reference", None)
5264
+ resolved_reference = getattr(reference, "resolved", None) if reference else None
5265
+ if resolved_reference is not None:
5266
+ referenced_attachments = list(getattr(resolved_reference, "attachments", []) or [])
5267
+
5268
+ all_attachments = list(message.attachments) + snapshot_attachments + referenced_attachments
4602
5269
 
4603
5270
  # Determine message type
4604
5271
  msg_type = MessageType.TEXT
@@ -4668,6 +5335,7 @@ class DiscordAdapter(BasePlatformAdapter):
4668
5335
  guild_id=str(guild.id) if guild else None,
4669
5336
  parent_chat_id=parent_channel_id,
4670
5337
  message_id=str(message.id),
5338
+ role_authorized=role_authorized,
4671
5339
  )
4672
5340
 
4673
5341
  # Build media URLs -- download image attachments to local cache so the
@@ -4818,14 +5486,40 @@ class DiscordAdapter(BasePlatformAdapter):
4818
5486
  # - any thread (in_bot_thread bypasses the mention check, but
4819
5487
  # processing-window gaps and post-restart context still need
4820
5488
  # recovery)
5489
+ # - any reply (the user pointed at a specific message; hydrate
5490
+ # the context around it even in a free-response channel where
5491
+ # no mention gap exists — otherwise replies get only the short
5492
+ # "[Replying to: ...]" snippet with no surrounding context)
4821
5493
  # DMs skip entirely because every DM message triggers the bot,
4822
5494
  # so the session transcript already has everything.
4823
5495
  # Auto-threaded messages also skip — we just created the thread,
4824
5496
  # there's nothing prior to backfill.
4825
5497
  _has_mention_gap = require_mention and not is_free_channel and not in_bot_thread
4826
- if (_has_mention_gap or is_thread) and auto_threaded_channel is None:
5498
+ _is_reply = message.reference is not None
5499
+
5500
+ # Resolve the replied-to message into an object exposing ``.id``.
5501
+ # discord.py may give us a full Message (resolved), a
5502
+ # DeletedReferencedMessage, or nothing. Duck-type on ``.id``
5503
+ # rather than isinstance(discord.Message) — under test doubles the
5504
+ # discord module (and thus discord.Message) can be a mock, which is
5505
+ # not a valid isinstance() second argument. Any object with an int
5506
+ # id works as a scan anchor; otherwise fall back to a bare snowflake
5507
+ # built from the reference's message_id.
5508
+ _reply_target = None
5509
+ if _is_reply:
5510
+ _resolved = getattr(message.reference, "resolved", None)
5511
+ _resolved_id = getattr(_resolved, "id", None) if _resolved is not None else None
5512
+ if _resolved_id is not None:
5513
+ _reply_target = _resolved
5514
+ else:
5515
+ _ref_mid = getattr(message.reference, "message_id", None)
5516
+ if _ref_mid is not None:
5517
+ with suppress(ValueError, TypeError):
5518
+ _reply_target = _Snowflake(int(_ref_mid))
5519
+
5520
+ if (_has_mention_gap or is_thread or _is_reply) and auto_threaded_channel is None:
4827
5521
  _backfill_text = await self._fetch_channel_context(
4828
- message.channel, before=message,
5522
+ message.channel, before=message, reply_target=_reply_target,
4829
5523
  )
4830
5524
  if _backfill_text:
4831
5525
  _channel_context = _backfill_text
@@ -4972,34 +5666,35 @@ def _component_check_auth(
4972
5666
  ) -> bool:
4973
5667
  """Shared user-or-role OR semantics for component view button clicks.
4974
5668
 
4975
- Mirrors ``DiscordAdapter._is_allowed_user`` / the slash and on_message
4976
- gates so every Discord interaction surface honors the same trust
4977
- boundary. Component views (ExecApprovalView, SlashConfirmView,
4978
- UpdatePromptView, ModelPickerView) used to receive only
4979
- ``allowed_user_ids``: in role-only deployments
4980
- (DISCORD_ALLOWED_ROLES set, DISCORD_ALLOWED_USERS empty) the user
4981
- set was empty and the legacy "no allowlist = allow everyone" branch
4982
- let any guild member click the buttons -- approving exec commands,
4983
- cancelling slash confirmations, switching the model.
5669
+ Mirrors the gateway's external-surface authorization model: component
5670
+ button clicks must be explicitly authorized by a Discord user/role
5671
+ allowlist, a global user allowlist, or an explicit allow-all flag.
4984
5672
 
4985
5673
  Behavior:
4986
5674
 
4987
- - both allowlists empty -> allow (preserves existing no-allowlist
4988
- deployments, no regression)
4989
- - user is in user allowlist -> allow
5675
+ - DISCORD_ALLOW_ALL_USERS or GATEWAY_ALLOW_ALL_USERS -> allow
5676
+ - user is in DISCORD_ALLOWED_USERS or GATEWAY_ALLOWED_USERS -> allow
4990
5677
  - role allowlist set + user has a role in it -> allow
4991
5678
  - role allowlist set + interaction.user has no resolvable
4992
5679
  ``roles`` attribute (e.g. DM context with a role policy active)
4993
5680
  -> reject (fail closed)
4994
5681
  - otherwise -> reject
4995
5682
  """
4996
- user_set = allowed_user_ids or set()
4997
- role_set = allowed_role_ids or set()
4998
- has_users = bool(user_set)
4999
- has_roles = bool(role_set)
5000
- if not has_users and not has_roles:
5683
+ if os.getenv("DISCORD_ALLOW_ALL_USERS", "").strip().lower() in {"true", "1", "yes"}:
5684
+ return True
5685
+ if os.getenv("GATEWAY_ALLOW_ALL_USERS", "").strip().lower() in {"true", "1", "yes"}:
5001
5686
  return True
5002
5687
 
5688
+ user_set = {str(uid).strip() for uid in (allowed_user_ids or set()) if str(uid).strip()}
5689
+ global_allowed = {
5690
+ uid.strip()
5691
+ for uid in os.getenv("GATEWAY_ALLOWED_USERS", "").split(",")
5692
+ if uid.strip()
5693
+ }
5694
+ user_set.update(global_allowed)
5695
+ role_set = set(allowed_role_ids or set())
5696
+ has_users = bool(user_set)
5697
+ has_roles = bool(role_set)
5003
5698
  user = getattr(interaction, "user", None)
5004
5699
  if user is None:
5005
5700
  return False
@@ -5009,7 +5704,7 @@ def _component_check_auth(
5009
5704
  uid = str(user.id)
5010
5705
  except AttributeError:
5011
5706
  uid = ""
5012
- if uid and uid in user_set:
5707
+ if "*" in user_set or (uid and uid in user_set):
5013
5708
  return True
5014
5709
 
5015
5710
  if has_roles:
@@ -5141,6 +5836,17 @@ def _define_discord_view_classes() -> None:
5141
5836
  self.resolved = True
5142
5837
  for child in self.children:
5143
5838
  child.disabled = True
5839
+ # Visually update the Discord message so buttons appear disabled.
5840
+ msg = getattr(self, '_message', None)
5841
+ if msg:
5842
+ try:
5843
+ embed = msg.embeds[0] if msg.embeds else None
5844
+ if embed:
5845
+ embed.color = discord.Color.greyple()
5846
+ embed.set_footer(text="⏱ Prompt expired — no action taken")
5847
+ await msg.edit(embed=embed, view=self)
5848
+ except Exception:
5849
+ pass # message deleted or too old to edit
5144
5850
 
5145
5851
  class SlashConfirmView(discord.ui.View):
5146
5852
  """Three-button view for generic slash-command confirmations.
@@ -5245,6 +5951,17 @@ def _define_discord_view_classes() -> None:
5245
5951
  self.resolved = True
5246
5952
  for child in self.children:
5247
5953
  child.disabled = True
5954
+ # Visually update the Discord message so buttons appear disabled.
5955
+ msg = getattr(self, '_message', None)
5956
+ if msg:
5957
+ try:
5958
+ embed = msg.embeds[0] if msg.embeds else None
5959
+ if embed:
5960
+ embed.color = discord.Color.greyple()
5961
+ embed.set_footer(text="⏱ Prompt expired — no action taken")
5962
+ await msg.edit(embed=embed, view=self)
5963
+ except Exception:
5964
+ pass
5248
5965
 
5249
5966
  class UpdatePromptView(discord.ui.View):
5250
5967
  """Interactive Yes/No buttons for ``hermes update`` prompts.
@@ -5330,6 +6047,17 @@ def _define_discord_view_classes() -> None:
5330
6047
  self.resolved = True
5331
6048
  for child in self.children:
5332
6049
  child.disabled = True
6050
+ # Visually update the Discord message so buttons appear disabled.
6051
+ msg = getattr(self, '_message', None)
6052
+ if msg:
6053
+ try:
6054
+ embed = msg.embeds[0] if msg.embeds else None
6055
+ if embed:
6056
+ embed.color = discord.Color.greyple()
6057
+ embed.set_footer(text="⏱ Prompt expired — no action taken")
6058
+ await msg.edit(embed=embed, view=self)
6059
+ except Exception:
6060
+ pass
5333
6061
 
5334
6062
  class ModelPickerView(discord.ui.View):
5335
6063
  """Interactive select-menu view for model switching.
@@ -5359,6 +6087,7 @@ def _define_discord_view_classes() -> None:
5359
6087
  self.allowed_role_ids = allowed_role_ids or set()
5360
6088
  self.resolved = False
5361
6089
  self._selected_provider: str = ""
6090
+ self._pending_expensive_model: str = ""
5362
6091
 
5363
6092
  self._build_provider_select()
5364
6093
 
@@ -5441,6 +6170,41 @@ def _define_discord_view_classes() -> None:
5441
6170
  cancel_btn.callback = self._on_cancel
5442
6171
  self.add_item(cancel_btn)
5443
6172
 
6173
+ def _build_expensive_confirm(self, model_id: str):
6174
+ """Build confirmation buttons for unusually expensive models."""
6175
+ self.clear_items()
6176
+ self._pending_expensive_model = model_id
6177
+
6178
+ confirm_btn = discord.ui.Button(
6179
+ label="Switch anyway",
6180
+ style=discord.ButtonStyle.red,
6181
+ custom_id="model_expensive_confirm",
6182
+ )
6183
+ confirm_btn.callback = self._on_expensive_confirm
6184
+ self.add_item(confirm_btn)
6185
+
6186
+ cancel_btn = discord.ui.Button(
6187
+ label="Cancel",
6188
+ style=discord.ButtonStyle.grey,
6189
+ custom_id="model_expensive_cancel",
6190
+ )
6191
+ cancel_btn.callback = self._on_cancel
6192
+ self.add_item(cancel_btn)
6193
+
6194
+ async def _expensive_warning_for(self, model_id: str):
6195
+ try:
6196
+ from hermes_cli.model_cost_guard import expensive_model_warning
6197
+
6198
+ # Pricing lookup can hit models.dev / a /models endpoint on a
6199
+ # cache miss — keep it off the event loop.
6200
+ return await asyncio.to_thread(
6201
+ expensive_model_warning,
6202
+ model_id,
6203
+ provider=self._selected_provider,
6204
+ )
6205
+ except Exception:
6206
+ return None
6207
+
5444
6208
  async def _on_provider_selected(self, interaction: discord.Interaction):
5445
6209
  if not self._check_auth(interaction):
5446
6210
  await interaction.response.send_message(
@@ -5470,7 +6234,11 @@ def _define_discord_view_classes() -> None:
5470
6234
  view=self,
5471
6235
  )
5472
6236
 
5473
- async def _on_model_selected(self, interaction: discord.Interaction):
6237
+ async def _switch_selected_model(
6238
+ self,
6239
+ interaction: discord.Interaction,
6240
+ model_id: str,
6241
+ ):
5474
6242
  if self.resolved:
5475
6243
  await interaction.response.send_message(
5476
6244
  "Already resolved~", ephemeral=True
@@ -5483,7 +6251,6 @@ def _define_discord_view_classes() -> None:
5483
6251
  return
5484
6252
 
5485
6253
  self.resolved = True
5486
- model_id = interaction.data["values"][0]
5487
6254
  self.clear_items()
5488
6255
  await interaction.response.edit_message(
5489
6256
  embed=discord.Embed(
@@ -5512,6 +6279,50 @@ def _define_discord_view_classes() -> None:
5512
6279
  view=None,
5513
6280
  )
5514
6281
 
6282
+ async def _on_model_selected(self, interaction: discord.Interaction):
6283
+ if self.resolved:
6284
+ await interaction.response.send_message(
6285
+ "Already resolved~", ephemeral=True
6286
+ )
6287
+ return
6288
+ if not self._check_auth(interaction):
6289
+ await interaction.response.send_message(
6290
+ "You're not authorized~", ephemeral=True
6291
+ )
6292
+ return
6293
+
6294
+ model_id = interaction.data["values"][0]
6295
+ warning = await self._expensive_warning_for(model_id)
6296
+ if warning is not None:
6297
+ self._build_expensive_confirm(model_id)
6298
+ await interaction.response.edit_message(
6299
+ embed=discord.Embed(
6300
+ title="⚠ Expensive Model Warning",
6301
+ description=warning.message,
6302
+ color=discord.Color.red(),
6303
+ ),
6304
+ view=self,
6305
+ )
6306
+ return
6307
+
6308
+ await self._switch_selected_model(interaction, model_id)
6309
+
6310
+ async def _on_expensive_confirm(self, interaction: discord.Interaction):
6311
+ if not self._check_auth(interaction):
6312
+ await interaction.response.send_message(
6313
+ "You're not authorized~", ephemeral=True
6314
+ )
6315
+ return
6316
+ if not self._pending_expensive_model:
6317
+ await interaction.response.send_message(
6318
+ "Model selection expired.", ephemeral=True
6319
+ )
6320
+ return
6321
+ await self._switch_selected_model(
6322
+ interaction,
6323
+ self._pending_expensive_model,
6324
+ )
6325
+
5515
6326
  async def _on_back(self, interaction: discord.Interaction):
5516
6327
  if not self._check_auth(interaction):
5517
6328
  await interaction.response.send_message(
@@ -5555,6 +6366,18 @@ def _define_discord_view_classes() -> None:
5555
6366
  async def on_timeout(self):
5556
6367
  self.resolved = True
5557
6368
  self.clear_items()
6369
+ # Visually update the Discord message so it appears expired.
6370
+ msg = getattr(self, '_message', None)
6371
+ if msg:
6372
+ try:
6373
+ embed = discord.Embed(
6374
+ title="⚙ Model Configuration",
6375
+ description="⏱ Selection expired — no model change.",
6376
+ color=discord.Color.greyple(),
6377
+ )
6378
+ await msg.edit(embed=embed, view=self)
6379
+ except Exception:
6380
+ pass
5558
6381
 
5559
6382
 
5560
6383
  class ClarifyChoiceView(discord.ui.View):
@@ -5587,10 +6410,47 @@ def _define_discord_view_classes() -> None:
5587
6410
  self.resolved = False
5588
6411
 
5589
6412
  for index, choice in enumerate(self.choices):
5590
- # Discord button labels are capped at 80 chars.
5591
- label_body = choice if len(choice) <= 75 else choice[:72] + "..."
6413
+ # Discord button labels are capped at 80 chars. On mobile the
6414
+ # visible width is much narrower (often <40 chars before it
6415
+ # wraps to 2 lines and the second line gets cut off), so we
6416
+ # cap aggressively and cut at a word boundary when possible
6417
+ # to keep the trailing text readable.
6418
+ #
6419
+ # Cut strategy (most-preferred to least-preferred):
6420
+ # 1. Last space in the trailing half of the budget
6421
+ # (cleanest word boundary)
6422
+ # 2. Last soft boundary in the trailing half of the
6423
+ # budget (hyphen, comma, period, paren)
6424
+ # 3. Hard cut at the budget limit (last resort)
6425
+ prefix = f"{index + 1}. "
6426
+ budget = 80 - len(prefix)
6427
+ if len(choice) <= budget:
6428
+ label_body = choice
6429
+ else:
6430
+ truncated = choice[: budget - 1].rstrip()
6431
+ cut_at = -1
6432
+ # 1. Last space in the trailing half of the budget.
6433
+ space = truncated.rfind(" ")
6434
+ if space >= budget // 2:
6435
+ cut_at = space
6436
+ # 2. Soft boundary — only if no word boundary found.
6437
+ # Find the latest soft boundary in the trailing half
6438
+ # of the budget; that maximizes preserved text length.
6439
+ # Cut AT the soft boundary (inclusive) so the label
6440
+ # ends on the soft char (e.g. "-" or ",") rather than
6441
+ # on the alpha char that followed it.
6442
+ if cut_at < 0:
6443
+ latest_soft = max(
6444
+ (truncated.rfind(s) for s in ("-", ",", ".", ")")),
6445
+ default=-1,
6446
+ )
6447
+ if latest_soft >= budget // 2:
6448
+ cut_at = latest_soft + 1
6449
+ if cut_at > 0:
6450
+ truncated = truncated[:cut_at]
6451
+ label_body = truncated.rstrip() + "…"
5592
6452
  button = discord.ui.Button(
5593
- label=f"{index + 1}. {label_body}",
6453
+ label=f"{prefix}{label_body}",
5594
6454
  style=discord.ButtonStyle.primary,
5595
6455
  custom_id=f"clarify:{clarify_id}:{index}",
5596
6456
  )
@@ -5740,6 +6600,17 @@ def _define_discord_view_classes() -> None:
5740
6600
  self.resolved = True
5741
6601
  for child in self.children:
5742
6602
  child.disabled = True
6603
+ # Visually update the Discord message so buttons appear disabled.
6604
+ msg = getattr(self, '_message', None)
6605
+ if msg:
6606
+ try:
6607
+ embed = msg.embeds[0] if msg.embeds else None
6608
+ if embed:
6609
+ embed.color = discord.Color.greyple()
6610
+ embed.set_footer(text="⏱ Prompt expired — no action taken")
6611
+ await msg.edit(embed=embed, view=self)
6612
+ except Exception:
6613
+ pass
5743
6614
  if DISCORD_AVAILABLE:
5744
6615
  _define_discord_view_classes()
5745
6616