@clawpump/claw-agent 0.1.4 → 0.1.6

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 (1214) 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 +2294 -3146
  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/clawpump_cli.py +3 -3
  586. package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
  587. package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
  588. package/agent/hermes_cli/commands.py +216 -91
  589. package/agent/hermes_cli/config.py +967 -130
  590. package/agent/hermes_cli/container_boot.py +76 -11
  591. package/agent/hermes_cli/cron.py +5 -11
  592. package/agent/hermes_cli/curator.py +21 -0
  593. package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
  594. package/agent/hermes_cli/dashboard_auth/base.py +62 -0
  595. package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
  596. package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
  597. package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
  598. package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
  599. package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
  600. package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
  601. package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
  602. package/agent/hermes_cli/dashboard_register.py +427 -0
  603. package/agent/hermes_cli/debug.py +155 -50
  604. package/agent/hermes_cli/distribution.py +227 -0
  605. package/agent/hermes_cli/doctor.py +255 -14
  606. package/agent/hermes_cli/dump.py +60 -6
  607. package/agent/hermes_cli/env_loader.py +33 -0
  608. package/agent/hermes_cli/gateway.py +755 -103
  609. package/agent/hermes_cli/gateway_enroll.py +250 -0
  610. package/agent/hermes_cli/gateway_windows.py +254 -11
  611. package/agent/hermes_cli/gui_uninstall.py +285 -0
  612. package/agent/hermes_cli/inventory.py +105 -4
  613. package/agent/hermes_cli/kanban.py +58 -71
  614. package/agent/hermes_cli/kanban_db.py +391 -14
  615. package/agent/hermes_cli/kanban_decompose.py +2 -2
  616. package/agent/hermes_cli/kanban_specify.py +3 -1
  617. package/agent/hermes_cli/logs.py +2 -0
  618. package/agent/hermes_cli/main.py +2889 -5287
  619. package/agent/hermes_cli/managed_scope.py +214 -0
  620. package/agent/hermes_cli/managed_uv.py +254 -0
  621. package/agent/hermes_cli/mcp_catalog.py +6 -3
  622. package/agent/hermes_cli/mcp_config.py +145 -21
  623. package/agent/hermes_cli/mcp_security.py +96 -0
  624. package/agent/hermes_cli/mcp_startup.py +32 -3
  625. package/agent/hermes_cli/memory_providers.py +149 -0
  626. package/agent/hermes_cli/memory_setup.py +97 -42
  627. package/agent/hermes_cli/middleware.py +313 -0
  628. package/agent/hermes_cli/model_catalog.py +31 -0
  629. package/agent/hermes_cli/model_cost_guard.py +134 -0
  630. package/agent/hermes_cli/model_normalize.py +2 -1
  631. package/agent/hermes_cli/model_setup_flows.py +2759 -0
  632. package/agent/hermes_cli/model_switch.py +242 -27
  633. package/agent/hermes_cli/models.py +284 -44
  634. package/agent/hermes_cli/nous_account.py +33 -6
  635. package/agent/hermes_cli/nous_billing.py +406 -0
  636. package/agent/hermes_cli/nous_subscription.py +202 -5
  637. package/agent/hermes_cli/platforms.py +1 -0
  638. package/agent/hermes_cli/plugins.py +218 -18
  639. package/agent/hermes_cli/plugins_cmd.py +249 -105
  640. package/agent/hermes_cli/portal_cli.py +56 -16
  641. package/agent/hermes_cli/profile_distribution.py +6 -1
  642. package/agent/hermes_cli/profiles.py +283 -32
  643. package/agent/hermes_cli/provider_catalog.py +170 -0
  644. package/agent/hermes_cli/providers.py +4 -1
  645. package/agent/hermes_cli/pty_bridge.py +53 -4
  646. package/agent/hermes_cli/runtime_provider.py +216 -34
  647. package/agent/hermes_cli/secret_prompt.py +4 -4
  648. package/agent/hermes_cli/secrets_cli.py +24 -0
  649. package/agent/hermes_cli/send_cmd.py +28 -2
  650. package/agent/hermes_cli/service_manager.py +166 -19
  651. package/agent/hermes_cli/session_listing.py +97 -0
  652. package/agent/hermes_cli/setup.py +158 -94
  653. package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
  654. package/agent/hermes_cli/skills_config.py +8 -2
  655. package/agent/hermes_cli/skills_hub.py +149 -7
  656. package/agent/hermes_cli/status.py +2 -2
  657. package/agent/hermes_cli/subcommands/__init__.py +18 -0
  658. package/agent/hermes_cli/subcommands/_shared.py +29 -0
  659. package/agent/hermes_cli/subcommands/acp.py +52 -0
  660. package/agent/hermes_cli/subcommands/auth.py +109 -0
  661. package/agent/hermes_cli/subcommands/backup.py +38 -0
  662. package/agent/hermes_cli/subcommands/claw.py +92 -0
  663. package/agent/hermes_cli/subcommands/config.py +49 -0
  664. package/agent/hermes_cli/subcommands/cron.py +163 -0
  665. package/agent/hermes_cli/subcommands/dashboard.py +143 -0
  666. package/agent/hermes_cli/subcommands/debug.py +77 -0
  667. package/agent/hermes_cli/subcommands/doctor.py +35 -0
  668. package/agent/hermes_cli/subcommands/dump.py +28 -0
  669. package/agent/hermes_cli/subcommands/gateway.py +332 -0
  670. package/agent/hermes_cli/subcommands/gui.py +63 -0
  671. package/agent/hermes_cli/subcommands/hooks.py +77 -0
  672. package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
  673. package/agent/hermes_cli/subcommands/insights.py +25 -0
  674. package/agent/hermes_cli/subcommands/login.py +78 -0
  675. package/agent/hermes_cli/subcommands/logout.py +28 -0
  676. package/agent/hermes_cli/subcommands/logs.py +78 -0
  677. package/agent/hermes_cli/subcommands/mcp.py +108 -0
  678. package/agent/hermes_cli/subcommands/memory.py +53 -0
  679. package/agent/hermes_cli/subcommands/model.py +72 -0
  680. package/agent/hermes_cli/subcommands/pairing.py +36 -0
  681. package/agent/hermes_cli/subcommands/plugins.py +94 -0
  682. package/agent/hermes_cli/subcommands/postinstall.py +23 -0
  683. package/agent/hermes_cli/subcommands/profile.py +203 -0
  684. package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
  685. package/agent/hermes_cli/subcommands/security.py +62 -0
  686. package/agent/hermes_cli/subcommands/setup.py +58 -0
  687. package/agent/hermes_cli/subcommands/skills.py +298 -0
  688. package/agent/hermes_cli/subcommands/slack.py +60 -0
  689. package/agent/hermes_cli/subcommands/status.py +28 -0
  690. package/agent/hermes_cli/subcommands/tools.py +95 -0
  691. package/agent/hermes_cli/subcommands/uninstall.py +41 -0
  692. package/agent/hermes_cli/subcommands/update.py +70 -0
  693. package/agent/hermes_cli/subcommands/version.py +18 -0
  694. package/agent/hermes_cli/subcommands/webhook.py +76 -0
  695. package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
  696. package/agent/hermes_cli/suggestions_cmd.py +153 -0
  697. package/agent/hermes_cli/telegram_managed_bot.py +358 -0
  698. package/agent/hermes_cli/tips.py +3 -4
  699. package/agent/hermes_cli/tools_config.py +155 -28
  700. package/agent/hermes_cli/uninstall.py +231 -35
  701. package/agent/hermes_cli/web_server.py +6188 -975
  702. package/agent/hermes_cli/win_pty_bridge.py +179 -0
  703. package/agent/hermes_cli/write_approval_commands.py +209 -0
  704. package/agent/hermes_constants.py +164 -33
  705. package/agent/hermes_logging.py +74 -2
  706. package/agent/hermes_state.py +919 -106
  707. package/agent/hermes_time.py +20 -0
  708. package/agent/locales/af.yaml +23 -0
  709. package/agent/locales/de.yaml +23 -0
  710. package/agent/locales/en.yaml +20 -0
  711. package/agent/locales/es.yaml +23 -0
  712. package/agent/locales/fr.yaml +23 -0
  713. package/agent/locales/ga.yaml +23 -0
  714. package/agent/locales/hu.yaml +23 -0
  715. package/agent/locales/it.yaml +23 -0
  716. package/agent/locales/ja.yaml +23 -0
  717. package/agent/locales/ko.yaml +23 -0
  718. package/agent/locales/pt.yaml +23 -0
  719. package/agent/locales/ru.yaml +23 -0
  720. package/agent/locales/tr.yaml +23 -0
  721. package/agent/locales/uk.yaml +23 -0
  722. package/agent/locales/zh-hant.yaml +23 -0
  723. package/agent/locales/zh.yaml +23 -0
  724. package/agent/model_tools.py +204 -40
  725. package/agent/optional-mcps/clawpump/manifest.yaml +15 -5
  726. package/agent/optional-mcps/clawpump-stdio/manifest.yaml +14 -4
  727. package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
  728. package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
  729. package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
  730. package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
  731. package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
  732. package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
  733. package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
  734. package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
  735. package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
  736. package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
  737. package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
  738. package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
  739. package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
  740. package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
  741. package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
  742. package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
  743. package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
  744. package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
  745. package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
  746. package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
  747. package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
  748. package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
  749. package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
  750. package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
  751. package/agent/optional-skills/security/1password/SKILL.md +1 -1
  752. package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
  753. package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
  754. package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
  755. package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
  756. package/agent/package-lock.json +4082 -7907
  757. package/agent/package.json +18 -3
  758. package/agent/plugins/browser/firecrawl/provider.py +4 -1
  759. package/agent/plugins/cron/__init__.py +344 -0
  760. package/agent/plugins/cron/chronos/__init__.py +241 -0
  761. package/agent/plugins/cron/chronos/_nas_client.py +123 -0
  762. package/agent/plugins/cron/chronos/plugin.yaml +9 -0
  763. package/agent/plugins/cron/chronos/verify.py +103 -0
  764. package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
  765. package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
  766. package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
  767. package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
  768. package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
  769. package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
  770. package/agent/plugins/google_meet/audio_bridge.py +4 -0
  771. package/agent/plugins/google_meet/meet_bot.py +7 -1
  772. package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
  773. package/agent/plugins/image_gen/fal/__init__.py +35 -6
  774. package/agent/plugins/image_gen/krea/__init__.py +56 -13
  775. package/agent/plugins/image_gen/openai/__init__.py +122 -24
  776. package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
  777. package/agent/plugins/image_gen/xai/__init__.py +92 -12
  778. package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
  779. package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
  780. package/agent/plugins/memory/__init__.py +48 -5
  781. package/agent/plugins/memory/byterover/__init__.py +1 -0
  782. package/agent/plugins/memory/hindsight/README.md +1 -1
  783. package/agent/plugins/memory/hindsight/__init__.py +138 -24
  784. package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
  785. package/agent/plugins/memory/honcho/README.md +13 -10
  786. package/agent/plugins/memory/honcho/cli.py +247 -122
  787. package/agent/plugins/memory/honcho/client.py +112 -102
  788. package/agent/plugins/memory/openviking/README.md +12 -1
  789. package/agent/plugins/memory/openviking/__init__.py +2281 -107
  790. package/agent/plugins/memory/openviking/plugin.yaml +1 -2
  791. package/agent/plugins/memory/supermemory/README.md +22 -10
  792. package/agent/plugins/memory/supermemory/__init__.py +142 -37
  793. package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
  794. package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
  795. package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
  796. package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
  797. package/agent/plugins/model-providers/custom/__init__.py +8 -2
  798. package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
  799. package/agent/plugins/model-providers/minimax/__init__.py +60 -8
  800. package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
  801. package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
  802. package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
  803. package/agent/plugins/model-providers/zai/__init__.py +1 -0
  804. package/agent/plugins/observability/langfuse/__init__.py +147 -14
  805. package/agent/plugins/observability/nemo_relay/README.md +559 -0
  806. package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
  807. package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
  808. package/agent/plugins/platforms/discord/adapter.py +932 -61
  809. package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
  810. package/agent/plugins/platforms/google_chat/adapter.py +9 -3
  811. package/agent/plugins/platforms/google_chat/oauth.py +1 -1
  812. package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
  813. package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
  814. package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
  815. package/agent/plugins/platforms/irc/adapter.py +4 -1
  816. package/agent/plugins/platforms/line/adapter.py +16 -1
  817. package/agent/plugins/platforms/mattermost/adapter.py +100 -24
  818. package/agent/plugins/platforms/photon/README.md +179 -0
  819. package/agent/plugins/platforms/photon/__init__.py +4 -0
  820. package/agent/plugins/platforms/photon/adapter.py +1586 -0
  821. package/agent/plugins/platforms/photon/auth.py +1046 -0
  822. package/agent/plugins/platforms/photon/cli.py +439 -0
  823. package/agent/plugins/platforms/photon/plugin.yaml +88 -0
  824. package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
  825. package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
  826. package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
  827. package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
  828. package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
  829. package/agent/plugins/platforms/raft/__init__.py +3 -0
  830. package/agent/plugins/platforms/raft/adapter.py +774 -0
  831. package/agent/plugins/platforms/raft/plugin.yaml +19 -0
  832. package/agent/plugins/platforms/simplex/adapter.py +777 -220
  833. package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
  834. package/agent/plugins/platforms/teams/adapter.py +175 -5
  835. package/agent/plugins/plugin_utils.py +135 -0
  836. package/agent/plugins/video_gen/fal/__init__.py +10 -3
  837. package/agent/plugins/web/searxng/provider.py +15 -2
  838. package/agent/plugins/web/xai/provider.py +2 -2
  839. package/agent/providers/base.py +22 -3
  840. package/agent/pyproject.toml +115 -21
  841. package/agent/run_agent.py +733 -39
  842. package/agent/scripts/build_skills_index.py +51 -19
  843. package/agent/scripts/check_subprocess_stdin.py +177 -0
  844. package/agent/scripts/contributor_audit.py +2 -0
  845. package/agent/scripts/docker_config_migrate.py +67 -0
  846. package/agent/scripts/install.cmd +3 -3
  847. package/agent/scripts/install.ps1 +580 -154
  848. package/agent/scripts/install.sh +402 -185
  849. package/agent/scripts/lib/node-bootstrap.sh +39 -4
  850. package/agent/scripts/release.py +183 -0
  851. package/agent/scripts/run_tests.sh +1 -0
  852. package/agent/scripts/run_tests_parallel.py +18 -23
  853. package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
  854. package/agent/setup.py +59 -0
  855. package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
  856. package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
  857. package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
  858. package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
  859. package/agent/skills/clawpump/SKILL.md +53 -5
  860. package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
  861. package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
  862. package/agent/skills/github/github-auth/SKILL.md +2 -2
  863. package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
  864. package/agent/skills/github/github-code-review/SKILL.md +2 -2
  865. package/agent/skills/github/github-issues/SKILL.md +2 -2
  866. package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
  867. package/agent/skills/github/github-repo-management/SKILL.md +2 -2
  868. package/agent/skills/media/gif-search/SKILL.md +1 -1
  869. package/agent/skills/media/youtube-content/SKILL.md +10 -7
  870. package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
  871. package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
  872. package/agent/skills/productivity/airtable/SKILL.md +2 -2
  873. package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
  874. package/agent/skills/productivity/notion/SKILL.md +2 -2
  875. package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
  876. package/agent/skills/research/llm-wiki/SKILL.md +1 -1
  877. package/agent/skills/social-media/xurl/SKILL.md +9 -0
  878. package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
  879. package/agent/skills/software-development/plan/SKILL.md +285 -5
  880. package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
  881. package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
  882. package/agent/skills/software-development/spike/SKILL.md +2 -2
  883. package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
  884. package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
  885. package/agent/tools/approval.py +302 -4
  886. package/agent/tools/async_delegation.py +386 -0
  887. package/agent/tools/blueprints.py +325 -0
  888. package/agent/tools/browser_cdp_tool.py +3 -3
  889. package/agent/tools/browser_tool.py +34 -6
  890. package/agent/tools/checkpoint_manager.py +31 -1
  891. package/agent/tools/clarify_tool.py +55 -5
  892. package/agent/tools/code_execution_tool.py +31 -14
  893. package/agent/tools/computer_use/cua_backend.py +81 -3
  894. package/agent/tools/computer_use/tool.py +79 -5
  895. package/agent/tools/computer_use/vision_routing.py +55 -3
  896. package/agent/tools/credential_files.py +31 -12
  897. package/agent/tools/cronjob_tools.py +30 -20
  898. package/agent/tools/delegate_tool.py +356 -31
  899. package/agent/tools/env_probe.py +1 -0
  900. package/agent/tools/environments/docker.py +163 -8
  901. package/agent/tools/environments/file_sync.py +2 -1
  902. package/agent/tools/environments/local.py +74 -23
  903. package/agent/tools/environments/singularity.py +4 -1
  904. package/agent/tools/environments/ssh.py +78 -11
  905. package/agent/tools/file_operations.py +277 -41
  906. package/agent/tools/file_tools.py +166 -28
  907. package/agent/tools/image_generation_tool.py +515 -29
  908. package/agent/tools/kanban_tools.py +99 -0
  909. package/agent/tools/lazy_deps.py +33 -2
  910. package/agent/tools/mcp_oauth.py +5 -5
  911. package/agent/tools/mcp_oauth_manager.py +7 -5
  912. package/agent/tools/mcp_tool.py +840 -33
  913. package/agent/tools/memory_tool.py +335 -38
  914. package/agent/tools/osv_check.py +15 -1
  915. package/agent/tools/process_registry.py +155 -11
  916. package/agent/tools/read_extract.py +248 -0
  917. package/agent/tools/read_terminal_tool.py +93 -0
  918. package/agent/tools/schema_sanitizer.py +38 -0
  919. package/agent/tools/send_message_tool.py +163 -49
  920. package/agent/tools/session_search_tool.py +189 -7
  921. package/agent/tools/skill_manager_tool.py +202 -3
  922. package/agent/tools/skill_usage.py +52 -4
  923. package/agent/tools/skills_hub.py +184 -44
  924. package/agent/tools/skills_sync.py +232 -5
  925. package/agent/tools/skills_tool.py +125 -11
  926. package/agent/tools/terminal_tool.py +148 -26
  927. package/agent/tools/tirith_security.py +2 -0
  928. package/agent/tools/todo_tool.py +32 -1
  929. package/agent/tools/transcription_tools.py +13 -5
  930. package/agent/tools/tts_tool.py +332 -38
  931. package/agent/tools/url_safety.py +52 -1
  932. package/agent/tools/vision_tools.py +124 -39
  933. package/agent/tools/voice_mode.py +4 -3
  934. package/agent/tools/web_tools.py +45 -15
  935. package/agent/tools/write_approval.py +493 -0
  936. package/agent/toolsets.py +34 -10
  937. package/agent/trajectory_compressor.py +81 -10
  938. package/agent/tui_gateway/entry.py +43 -6
  939. package/agent/tui_gateway/server.py +3335 -330
  940. package/agent/tui_gateway/slash_worker.py +61 -0
  941. package/agent/tui_gateway/ws.py +67 -9
  942. package/agent/ui-tui/eslint.config.mjs +0 -4
  943. package/agent/ui-tui/package.json +6 -6
  944. package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
  945. package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
  946. package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
  947. package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
  948. package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
  949. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
  950. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
  951. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
  952. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
  953. package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
  954. package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
  955. package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
  956. package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
  957. package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
  958. package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
  959. package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
  960. package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
  961. package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
  962. package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
  963. package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
  964. package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
  965. package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
  966. package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
  967. package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
  968. package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
  969. package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
  970. package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
  971. package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
  972. package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
  973. package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
  974. package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
  975. package/agent/ui-tui/src/app/interfaces.ts +64 -1
  976. package/agent/ui-tui/src/app/overlayStore.ts +18 -2
  977. package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
  978. package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
  979. package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
  980. package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
  981. package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
  982. package/agent/ui-tui/src/app/slash/registry.ts +4 -0
  983. package/agent/ui-tui/src/app/turnController.ts +145 -2
  984. package/agent/ui-tui/src/app/uiStore.ts +2 -0
  985. package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
  986. package/agent/ui-tui/src/app/useMainApp.ts +54 -8
  987. package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
  988. package/agent/ui-tui/src/app/useSubmission.ts +23 -31
  989. package/agent/ui-tui/src/components/appChrome.tsx +112 -5
  990. package/agent/ui-tui/src/components/appLayout.tsx +9 -0
  991. package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
  992. package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
  993. package/agent/ui-tui/src/components/branding.tsx +15 -3
  994. package/agent/ui-tui/src/components/messageLine.tsx +25 -3
  995. package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
  996. package/agent/ui-tui/src/components/prompts.tsx +31 -17
  997. package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
  998. package/agent/ui-tui/src/components/textInput.tsx +16 -0
  999. package/agent/ui-tui/src/config/env.ts +12 -0
  1000. package/agent/ui-tui/src/config/limits.ts +13 -0
  1001. package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
  1002. package/agent/ui-tui/src/domain/paths.ts +24 -0
  1003. package/agent/ui-tui/src/domain/slash.ts +40 -0
  1004. package/agent/ui-tui/src/entry.tsx +35 -4
  1005. package/agent/ui-tui/src/gatewayClient.ts +22 -10
  1006. package/agent/ui-tui/src/gatewayTypes.ts +130 -1
  1007. package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
  1008. package/agent/ui-tui/src/lib/memory.test.ts +162 -0
  1009. package/agent/ui-tui/src/lib/memory.ts +60 -1
  1010. package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
  1011. package/agent/ui-tui/src/lib/osc52.ts +1 -1
  1012. package/agent/ui-tui/src/lib/text.test.ts +32 -1
  1013. package/agent/ui-tui/src/lib/text.ts +29 -2
  1014. package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
  1015. package/agent/ui-tui/src/types.ts +5 -0
  1016. package/agent/ui-tui/tsconfig.build.json +0 -1
  1017. package/agent/ui-tui/tsconfig.json +2 -1
  1018. package/agent/utils.py +66 -2
  1019. package/agent/uv.lock +308 -696
  1020. package/agent/web/index.html +2 -2
  1021. package/agent/web/package.json +11 -6
  1022. package/agent/web/public/claw-bg.webp +0 -0
  1023. package/agent/web/public/claw-logo.webp +0 -0
  1024. package/agent/web/src/App.tsx +138 -48
  1025. package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
  1026. package/agent/web/src/components/Backdrop.tsx +15 -0
  1027. package/agent/web/src/components/ChatSessionList.tsx +260 -0
  1028. package/agent/web/src/components/ChatSidebar.tsx +262 -78
  1029. package/agent/web/src/components/ConfirmDialog.tsx +122 -0
  1030. package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
  1031. package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
  1032. package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
  1033. package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
  1034. package/agent/web/src/components/ReasoningPicker.tsx +167 -0
  1035. package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
  1036. package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
  1037. package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
  1038. package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
  1039. package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
  1040. package/agent/web/src/contexts/SystemActions.tsx +6 -8
  1041. package/agent/web/src/contexts/profile-context.ts +19 -0
  1042. package/agent/web/src/contexts/useProfileScope.ts +6 -0
  1043. package/agent/web/src/i18n/af.ts +5 -4
  1044. package/agent/web/src/i18n/de.ts +5 -4
  1045. package/agent/web/src/i18n/en.ts +58 -4
  1046. package/agent/web/src/i18n/es.ts +5 -3
  1047. package/agent/web/src/i18n/fr.ts +5 -3
  1048. package/agent/web/src/i18n/ga.ts +5 -4
  1049. package/agent/web/src/i18n/hu.ts +5 -4
  1050. package/agent/web/src/i18n/it.ts +5 -4
  1051. package/agent/web/src/i18n/ja.ts +5 -4
  1052. package/agent/web/src/i18n/ko.ts +5 -4
  1053. package/agent/web/src/i18n/pt.ts +5 -3
  1054. package/agent/web/src/i18n/ru.ts +5 -4
  1055. package/agent/web/src/i18n/tr.ts +5 -4
  1056. package/agent/web/src/i18n/types.ts +59 -1
  1057. package/agent/web/src/i18n/uk.ts +5 -3
  1058. package/agent/web/src/i18n/zh-hant.ts +5 -4
  1059. package/agent/web/src/i18n/zh.ts +5 -4
  1060. package/agent/web/src/index.css +2 -2
  1061. package/agent/web/src/lib/api.ts +819 -52
  1062. package/agent/web/src/lib/dashboard-flags.ts +16 -7
  1063. package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
  1064. package/agent/web/src/lib/reasoning-effort.ts +36 -0
  1065. package/agent/web/src/lib/session-refresh.test.ts +21 -0
  1066. package/agent/web/src/lib/session-refresh.ts +26 -0
  1067. package/agent/web/src/pages/ChannelsPage.tsx +529 -68
  1068. package/agent/web/src/pages/ChatPage.tsx +249 -56
  1069. package/agent/web/src/pages/ConfigPage.tsx +11 -1
  1070. package/agent/web/src/pages/CronPage.tsx +219 -31
  1071. package/agent/web/src/pages/EnvPage.tsx +25 -6
  1072. package/agent/web/src/pages/FilesPage.tsx +525 -0
  1073. package/agent/web/src/pages/McpPage.tsx +80 -3
  1074. package/agent/web/src/pages/ModelsPage.tsx +97 -12
  1075. package/agent/web/src/pages/PluginsPage.tsx +1 -1
  1076. package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
  1077. package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
  1078. package/agent/web/src/pages/SessionsPage.tsx +144 -13
  1079. package/agent/web/src/pages/SkillsPage.tsx +851 -70
  1080. package/agent/web/src/pages/SystemPage.tsx +340 -4
  1081. package/agent/web/src/pages/WalletPage.tsx +401 -0
  1082. package/agent/web/src/pages/WebhooksPage.tsx +145 -15
  1083. package/agent/web/src/pages/X402Page.tsx +207 -0
  1084. package/agent/web/src/plugins/registry.ts +28 -11
  1085. package/agent/web/src/plugins/sdk.d.ts +160 -0
  1086. package/agent/web/src/themes/context.tsx +112 -5
  1087. package/agent/web/src/themes/fonts.ts +167 -0
  1088. package/agent/web/src/themes/index.ts +7 -0
  1089. package/agent/web/tsconfig.app.json +0 -1
  1090. package/agent/web/vite.config.ts +1 -8
  1091. package/agent/web/vitest.config.ts +16 -0
  1092. package/package.json +1 -1
  1093. package/agent/apps/desktop/package-lock.json +0 -18363
  1094. package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
  1095. package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
  1096. package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
  1097. package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
  1098. package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
  1099. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
  1100. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
  1101. package/agent/skills/diagramming/DESCRIPTION.md +0 -3
  1102. package/agent/skills/domain/DESCRIPTION.md +0 -24
  1103. package/agent/skills/gifs/DESCRIPTION.md +0 -3
  1104. package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
  1105. package/agent/skills/mcp/DESCRIPTION.md +0 -3
  1106. package/agent/skills/media/spotify/SKILL.md +0 -135
  1107. package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
  1108. package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
  1109. package/agent/skills/productivity/linear/SKILL.md +0 -380
  1110. package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
  1111. package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
  1112. package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
  1113. package/agent/ui-tui/package-lock.json +0 -7449
  1114. package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
  1115. package/agent/web/package-lock.json +0 -8887
  1116. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
  1117. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
  1118. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
  1119. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
  1120. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
  1121. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
  1122. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
  1123. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
  1124. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
  1125. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
  1126. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
  1127. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
  1128. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
  1129. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
  1130. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
  1131. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
  1132. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
  1133. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
  1134. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
  1135. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
  1136. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
  1137. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
  1138. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
  1139. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
  1140. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
  1141. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
  1142. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
  1143. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
  1144. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
  1145. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
  1146. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
  1147. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
  1148. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
  1149. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
  1150. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
  1151. /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
  1152. /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
  1153. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
  1154. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
  1155. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
  1156. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
  1157. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
  1158. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
  1159. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
  1160. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
  1161. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
  1162. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
  1163. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
  1164. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
  1165. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
  1166. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
  1167. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
  1168. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
  1169. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
  1170. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
  1171. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
  1172. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
  1173. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
  1174. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
  1175. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
  1176. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
  1177. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
  1178. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
  1179. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
  1180. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
  1181. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
  1182. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
  1183. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
  1184. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
  1185. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
  1186. /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
  1187. /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
  1188. /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
  1189. /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
  1190. /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
  1191. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
  1192. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
  1193. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
  1194. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
  1195. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
  1196. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
  1197. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
  1198. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
  1199. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
  1200. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
  1201. /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
  1202. /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
  1203. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
  1204. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
  1205. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
  1206. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
  1207. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
  1208. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
  1209. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
  1210. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
  1211. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
  1212. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
  1213. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
  1214. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
@@ -17,8 +17,12 @@ Example config::
17
17
  command: "npx"
18
18
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
19
19
  env: {}
20
- timeout: 120 # per-tool-call timeout in seconds (default: 120)
20
+ timeout: 120 # per-tool-call timeout in seconds (default: 300)
21
21
  connect_timeout: 60 # initial connection timeout (default: 60)
22
+ keepalive_interval: 10 # liveness ping cadence in seconds (default:
23
+ # 180). Set below the server's session TTL for
24
+ # servers that GC idle sessions quickly (e.g.
25
+ # Unreal Engine editor MCP, ~15s). Floored at 5s.
22
26
  github:
23
27
  command: "npx"
24
28
  args: ["-y", "@modelcontextprotocol/server-github"]
@@ -78,6 +82,7 @@ Thread safety:
78
82
  """
79
83
 
80
84
  import asyncio
85
+ import contextvars
81
86
  import concurrent.futures
82
87
  import inspect
83
88
  import json
@@ -89,8 +94,9 @@ import shutil
89
94
  import sys
90
95
  import threading
91
96
  import time
97
+ from typing import Callable
92
98
  from datetime import datetime
93
- from typing import Any, Dict, List, Optional
99
+ from typing import Any, Coroutine, Dict, List, Optional
94
100
  from urllib.parse import urlparse
95
101
 
96
102
  logger = logging.getLogger(__name__)
@@ -175,6 +181,7 @@ _MCP_AVAILABLE = False
175
181
  _MCP_HTTP_AVAILABLE = False
176
182
  _MCP_SAMPLING_TYPES = False
177
183
  _MCP_NOTIFICATION_TYPES = False
184
+ _MCP_ELICITATION_TYPES = False
178
185
  _MCP_MESSAGE_HANDLER_SUPPORTED = False
179
186
  # Conservative fallback for SDK builds that don't export LATEST_PROTOCOL_VERSION.
180
187
  # Streamable HTTP was introduced by 2025-03-26, so this remains valid for the
@@ -220,6 +227,16 @@ try:
220
227
  _MCP_SAMPLING_TYPES = True
221
228
  except ImportError:
222
229
  logger.debug("MCP sampling types not available -- sampling disabled")
230
+ # Elicitation types -- gated separately for the same reason as sampling.
231
+ # Added in mcp Python SDK 1.11.0 (Jul 2025); servers use elicitation to
232
+ # ask the client for structured input mid-tool-call (e.g. payment
233
+ # authorization). Missing types just disable the feature; everything
234
+ # else keeps working.
235
+ try:
236
+ from mcp.types import ElicitRequestParams, ElicitResult
237
+ _MCP_ELICITATION_TYPES = True
238
+ except ImportError:
239
+ logger.debug("MCP elicitation types not available -- elicitation disabled")
223
240
  # Notification types for dynamic tool discovery (tools/list_changed)
224
241
  try:
225
242
  from mcp.types import (
@@ -257,17 +274,60 @@ if _MCP_AVAILABLE and not _MCP_MESSAGE_HANDLER_SUPPORTED:
257
274
  # Constants
258
275
  # ---------------------------------------------------------------------------
259
276
 
260
- _DEFAULT_TOOL_TIMEOUT = 120 # seconds for tool calls
277
+ _DEFAULT_TOOL_TIMEOUT = 300 # seconds for tool calls
261
278
  _DEFAULT_CONNECT_TIMEOUT = 60 # seconds for initial connection per server
262
279
  _MAX_RECONNECT_RETRIES = 5
263
280
  _MAX_INITIAL_CONNECT_RETRIES = 3 # retries for the very first connection attempt
264
281
  _MAX_BACKOFF_SECONDS = 60
265
282
 
283
+ # Keepalive cadence for HTTP/SSE sessions. The MCP spec lets a server expire
284
+ # idle sessions on any TTL it chooses (Streamable HTTP "Session Management"),
285
+ # so a client that wants a session to survive idle periods MUST refresh faster
286
+ # than that TTL. The default suits long LB/NAT idle windows (commonly
287
+ # 300-600s); servers with short session TTLs (e.g. Unreal Engine's editor MCP,
288
+ # ~15s) need a smaller ``keepalive_interval`` in their config or every idle
289
+ # tool call lands on a dead session and pays the full reconnect path. The floor
290
+ # stops a misconfigured tiny interval from busy-looping the keepalive.
291
+ _DEFAULT_KEEPALIVE_INTERVAL = 180 # seconds between liveness pings
292
+ _MIN_KEEPALIVE_INTERVAL = 5 # clamp floor for configured intervals
293
+
266
294
  # Environment variables that are safe to pass to stdio subprocesses
267
295
  _SAFE_ENV_KEYS = frozenset({
268
296
  "PATH", "HOME", "USER", "LANG", "LC_ALL", "TERM", "SHELL", "TMPDIR",
269
297
  })
270
298
 
299
+ _SAFE_ENV_KEYS_CASE_INSENSITIVE = frozenset({
300
+ # Windows process/location vars. These are needed by launcher-style tools
301
+ # such as Docker Desktop's MCP plugin discovery, and do not carry secrets.
302
+ "ALLUSERSPROFILE",
303
+ "APPDATA",
304
+ "COMMONPROGRAMFILES",
305
+ "COMMONPROGRAMFILES(X86)",
306
+ "COMMONPROGRAMW6432",
307
+ "COMPUTERNAME",
308
+ "COMSPEC",
309
+ "HOMEDRIVE",
310
+ "HOMEPATH",
311
+ "LOCALAPPDATA",
312
+ "NUMBER_OF_PROCESSORS",
313
+ "OS",
314
+ "PATHEXT",
315
+ "PROCESSOR_ARCHITECTURE",
316
+ "PROGRAMDATA",
317
+ "PROGRAMFILES",
318
+ "PROGRAMFILES(X86)",
319
+ "PROGRAMW6432",
320
+ "PUBLIC",
321
+ "SYSTEMDRIVE",
322
+ "SYSTEMROOT",
323
+ "TEMP",
324
+ "TMP",
325
+ "USERDOMAIN",
326
+ "USERNAME",
327
+ "USERPROFILE",
328
+ "WINDIR",
329
+ })
330
+
271
331
  # Regex for credential patterns to strip from error messages
272
332
  _CREDENTIAL_PATTERN = re.compile(
273
333
  r"(?:"
@@ -305,7 +365,11 @@ def _build_safe_env(user_env: Optional[dict]) -> dict:
305
365
  """
306
366
  env = {}
307
367
  for key, value in os.environ.items():
308
- if key in _SAFE_ENV_KEYS or key.startswith("XDG_"):
368
+ if (
369
+ key in _SAFE_ENV_KEYS
370
+ or key.upper() in _SAFE_ENV_KEYS_CASE_INSENSITIVE
371
+ or key.startswith("XDG_")
372
+ ):
309
373
  env[key] = value
310
374
  if user_env:
311
375
  env.update(user_env)
@@ -333,6 +397,40 @@ def _exc_str(exc: BaseException) -> str:
333
397
  return text if text else repr(exc)
334
398
 
335
399
 
400
+ # JSON-RPC "method not found" — the error a server returns when it does not
401
+ # implement a requested method (e.g. a tool-capable server that never wired up
402
+ # the optional ``ping`` utility). Defined locally with a fallback so detection
403
+ # works even on SDK builds that don't export the constant.
404
+ try:
405
+ from mcp.types import METHOD_NOT_FOUND as _JSONRPC_METHOD_NOT_FOUND
406
+ except Exception: # pragma: no cover — older/newer SDK without the constant
407
+ _JSONRPC_METHOD_NOT_FOUND = -32601
408
+
409
+
410
+ def _is_method_not_found_error(exc: BaseException) -> bool:
411
+ """Return True if *exc* is a JSON-RPC ``method not found`` (-32601).
412
+
413
+ ``ping`` is an *optional* MCP utility (spec: "optional ping mechanism").
414
+ A server that doesn't implement it answers a ping with -32601 rather than
415
+ an empty result. Structurally inspect ``McpError.error.code`` first, then
416
+ fall back to a substring match so detection survives SDK version drift and
417
+ servers that surface the condition as a plain message.
418
+ """
419
+ # Structural: mcp.shared.exceptions.McpError carries ErrorData.code.
420
+ err = getattr(exc, "error", None)
421
+ code = getattr(err, "code", None)
422
+ if code == _JSONRPC_METHOD_NOT_FOUND:
423
+ return True
424
+ msg = str(exc).lower()
425
+ if not msg:
426
+ return False
427
+ return (
428
+ str(_JSONRPC_METHOD_NOT_FOUND) in msg
429
+ or "method not found" in msg
430
+ or "not found: ping" in msg
431
+ )
432
+
433
+
336
434
  # ---------------------------------------------------------------------------
337
435
  # MCP tool description content scanning
338
436
  # ---------------------------------------------------------------------------
@@ -1104,6 +1202,193 @@ class SamplingHandler:
1104
1202
  return self._build_text_result(choice, response)
1105
1203
 
1106
1204
 
1205
+ # ---------------------------------------------------------------------------
1206
+ # Elicitation handler
1207
+ # ---------------------------------------------------------------------------
1208
+
1209
+ def _format_elicitation_schema_summary(schema: dict, server_name: str) -> str:
1210
+ """Render a JSON-schema-ish requested_schema to a human-readable field list.
1211
+
1212
+ Elicitation schemas are restricted to a flat object with named top-level
1213
+ properties. We surface field names, types, and descriptions so the user
1214
+ can tell what the server is asking for before approving.
1215
+ """
1216
+ props = schema.get("properties") if isinstance(schema, dict) else None
1217
+ if not isinstance(props, dict) or not props:
1218
+ return f"Approval requested by MCP server '{server_name}'."
1219
+
1220
+ lines = [f"Fields requested by MCP server '{server_name}':"]
1221
+ for field_name, field_spec in props.items():
1222
+ field_type = ""
1223
+ field_desc = ""
1224
+ if isinstance(field_spec, dict):
1225
+ field_type = str(field_spec.get("type", "") or "")
1226
+ field_desc = str(field_spec.get("description", "") or "")
1227
+ suffix = f" ({field_type})" if field_type else ""
1228
+ if field_desc:
1229
+ lines.append(f" - {field_name}{suffix}: {field_desc}")
1230
+ else:
1231
+ lines.append(f" - {field_name}{suffix}")
1232
+ return "\n".join(lines)
1233
+
1234
+
1235
+ class ElicitationHandler:
1236
+ """Handles ``elicitation/create`` requests for a single MCP server.
1237
+
1238
+ Each ``MCPServerTask`` that has elicitation enabled creates one handler.
1239
+ The handler is callable and passed directly to ``ClientSession`` as the
1240
+ ``elicitation_callback`` (added in mcp Python SDK 1.11.0).
1241
+
1242
+ Elicitation lets a server ask the client to collect structured input from
1243
+ the user mid-tool-call (e.g. payment authorization, OAuth confirmation).
1244
+ Form-mode elicitations are routed through Hermes' existing approval
1245
+ system (``tools.approval.prompt_dangerous_approval``), which surfaces
1246
+ the prompt on whichever surface the active session uses -- CLI, TUI,
1247
+ Telegram, Slack, etc. URL-mode elicitations are declined as unsupported.
1248
+
1249
+ Failure modes are fail-closed: any timeout, exception, or unexpected
1250
+ state returns ``decline``/``cancel`` rather than silently accepting.
1251
+ The server treats this as the user not approving.
1252
+ """
1253
+
1254
+ # Outer cap for the approval await. ``prompt_dangerous_approval`` runs
1255
+ # its own input() timeout via the approval-config value; this is an
1256
+ # asyncio-side safety net so the MCP event loop never blocks
1257
+ # indefinitely if the inner timeout machinery is bypassed.
1258
+ _OUTER_TIMEOUT_GRACE_SECONDS = 5
1259
+
1260
+ def __init__(self, server_name: str, config: dict, owner: Optional["MCPServerTask"] = None):
1261
+ self.server_name = server_name
1262
+ # Per-elicitation timeout. Default 5 min mirrors the gateway approval
1263
+ # default so users on async surfaces (Telegram, Slack) have time to
1264
+ # respond before the server gives up.
1265
+ self.timeout = _safe_numeric(config.get("timeout", 300), 300, float)
1266
+ # Back-reference to the MCPServerTask so we can read the agent's
1267
+ # captured contextvars snapshot at elicitation time. Optional so
1268
+ # the handler stays unit-testable in isolation.
1269
+ self.owner = owner
1270
+ self.metrics = {
1271
+ "requests": 0,
1272
+ "accepted": 0,
1273
+ "declined": 0,
1274
+ "errors": 0,
1275
+ }
1276
+
1277
+ def session_kwargs(self) -> dict:
1278
+ """Return kwargs to pass to ClientSession for elicitation support."""
1279
+ return {"elicitation_callback": self}
1280
+
1281
+ async def __call__(self, context, params):
1282
+ """Elicitation callback invoked by the MCP SDK.
1283
+
1284
+ Conforms to ``ElicitationFnT`` protocol. Returns ``ElicitResult``
1285
+ or ``ErrorData``.
1286
+ """
1287
+ self.metrics["requests"] += 1
1288
+
1289
+ # URL-mode elicitations point the user to an external URL for
1290
+ # sensitive out-of-band flows (OAuth, payment processing). Honouring
1291
+ # them requires opening a browser to that URL and waiting for the
1292
+ # server's notifications/elicitation/complete -- out of scope for
1293
+ # the initial implementation. Decline cleanly so the server does
1294
+ # not hang.
1295
+ mode = getattr(params, "mode", "form")
1296
+ if mode == "url":
1297
+ logger.info(
1298
+ "MCP server '%s' requested URL-mode elicitation; "
1299
+ "declining (URL-mode elicitation not implemented)",
1300
+ self.server_name,
1301
+ )
1302
+ self.metrics["declined"] += 1
1303
+ return ElicitResult(action="decline")
1304
+
1305
+ message = getattr(params, "message", "") or (
1306
+ f"MCP server '{self.server_name}' is requesting your approval"
1307
+ )
1308
+ schema = getattr(params, "requested_schema", {}) or {}
1309
+ description = _format_elicitation_schema_summary(schema, self.server_name)
1310
+
1311
+ logger.info(
1312
+ "MCP server '%s' elicitation request: %s",
1313
+ self.server_name, _sanitize_error(message)[:200],
1314
+ )
1315
+
1316
+ # Lazy import: tools.approval is imported very early during process
1317
+ # bootstrap; matching the lazy pattern used by _fire_approval_hook
1318
+ # avoids any chance of import-order coupling.
1319
+ try:
1320
+ from tools.approval import request_elicitation_consent
1321
+ except Exception as exc: # pragma: no cover -- defensive
1322
+ logger.error(
1323
+ "MCP server '%s' elicitation: approval system unavailable: %s",
1324
+ self.server_name, exc,
1325
+ )
1326
+ self.metrics["errors"] += 1
1327
+ return ElicitResult(action="decline")
1328
+
1329
+ # Offload the sync consent flow to a worker thread. Running it
1330
+ # inline would freeze the MCP background event loop, blocking every
1331
+ # other RPC on this session. request_elicitation_consent() routes
1332
+ # itself to the right surface (gateway notify_cb for Telegram /
1333
+ # Slack / etc., prompt_dangerous_approval for CLI / TUI) and
1334
+ # normalizes the answer to one of accept / decline / cancel.
1335
+ #
1336
+ # The recv-loop task that fires this callback does NOT inherit
1337
+ # the agent's contextvars (HERMES_SESSION_PLATFORM etc.). When
1338
+ # the MCP tool wrapper captured the agent's context onto
1339
+ # owner._pending_call_context we replay it here via
1340
+ # contextvars.Context.run so the gateway-platform detection in
1341
+ # request_elicitation_consent picks up the right session.
1342
+ captured = getattr(self.owner, "_pending_call_context", None) if self.owner else None
1343
+
1344
+ def _invoke_consent() -> str:
1345
+ if captured is None:
1346
+ return request_elicitation_consent(
1347
+ message,
1348
+ description,
1349
+ timeout_seconds=int(self.timeout),
1350
+ surface=f"mcp-elicitation/{self.server_name}",
1351
+ )
1352
+ # Context.run can only execute a context once — copy to allow
1353
+ # multiple elicitations within a single tool call.
1354
+ return captured.copy().run(
1355
+ request_elicitation_consent,
1356
+ message,
1357
+ description,
1358
+ timeout_seconds=int(self.timeout),
1359
+ surface=f"mcp-elicitation/{self.server_name}",
1360
+ )
1361
+
1362
+ try:
1363
+ answer = await asyncio.wait_for(
1364
+ asyncio.to_thread(_invoke_consent),
1365
+ timeout=self.timeout + self._OUTER_TIMEOUT_GRACE_SECONDS,
1366
+ )
1367
+ except asyncio.TimeoutError:
1368
+ logger.warning(
1369
+ "MCP server '%s' elicitation timed out after %ds",
1370
+ self.server_name, int(self.timeout),
1371
+ )
1372
+ self.metrics["errors"] += 1
1373
+ return ElicitResult(action="cancel")
1374
+ except Exception as exc:
1375
+ logger.error(
1376
+ "MCP server '%s' elicitation failed: %s",
1377
+ self.server_name, exc, exc_info=True,
1378
+ )
1379
+ self.metrics["errors"] += 1
1380
+ return ElicitResult(action="decline")
1381
+
1382
+ if answer == "accept":
1383
+ self.metrics["accepted"] += 1
1384
+ return ElicitResult(action="accept", content={})
1385
+ if answer == "cancel":
1386
+ self.metrics["errors"] += 1
1387
+ return ElicitResult(action="cancel")
1388
+ self.metrics["declined"] += 1
1389
+ return ElicitResult(action="decline")
1390
+
1391
+
1107
1392
  # ---------------------------------------------------------------------------
1108
1393
  # Server task -- each MCP server lives in one long-lived asyncio Task
1109
1394
  # ---------------------------------------------------------------------------
@@ -1122,9 +1407,11 @@ class MCPServerTask:
1122
1407
  "name", "session", "tool_timeout",
1123
1408
  "_task", "_ready", "_shutdown_event", "_reconnect_event",
1124
1409
  "_tools", "_error", "_config",
1125
- "_sampling", "_registered_tool_names", "_auth_type", "_refresh_lock",
1410
+ "_sampling", "_elicitation",
1411
+ "_registered_tool_names", "_auth_type", "_refresh_lock",
1126
1412
  "_rpc_lock", "_pending_refresh_tasks",
1127
- "initialize_result",
1413
+ "_pending_call_context",
1414
+ "initialize_result", "_ping_unsupported",
1128
1415
  )
1129
1416
 
1130
1417
  def __init__(self, name: str):
@@ -1144,6 +1431,7 @@ class MCPServerTask:
1144
1431
  self._error: Optional[Exception] = None
1145
1432
  self._config: dict = {}
1146
1433
  self._sampling: Optional[SamplingHandler] = None
1434
+ self._elicitation: Optional[ElicitationHandler] = None
1147
1435
  self._registered_tool_names: list[str] = []
1148
1436
  self._auth_type: str = ""
1149
1437
  self._refresh_lock = asyncio.Lock()
@@ -1155,17 +1443,53 @@ class MCPServerTask:
1155
1443
  # transports for conservative per-server ordering.
1156
1444
  self._rpc_lock = asyncio.Lock()
1157
1445
  self._pending_refresh_tasks: set[asyncio.Task] = set()
1446
+ # contextvars snapshot of the agent task that's currently in
1447
+ # session.call_tool(). The MCP recv loop dispatches incoming
1448
+ # elicitation/create requests on a SEPARATE asyncio task whose
1449
+ # context doesn't inherit HERMES_SESSION_PLATFORM, so the
1450
+ # elicitation handler has no way to detect the gateway session
1451
+ # that triggered the call. Capturing the agent's context here
1452
+ # and replaying it inside the elicitation callback restores
1453
+ # gateway-platform attribution and routes the approval prompt
1454
+ # to the right surface (Telegram, Slack, etc.).
1455
+ self._pending_call_context: Optional[contextvars.Context] = None
1158
1456
  # Captures the ``InitializeResult`` returned by
1159
1457
  # ``await session.initialize()`` so downstream code can inspect the
1160
1458
  # server's real advertised capabilities (``.capabilities.resources``,
1161
1459
  # ``.capabilities.prompts``) instead of assuming every ``ClientSession``
1162
1460
  # method attribute corresponds to a supported server method. See #18051.
1163
1461
  self.initialize_result: Optional[Any] = None
1462
+ # Set True the first time a keepalive ``ping`` returns JSON-RPC
1463
+ # -32601 (method not found): the server is tool-capable but doesn't
1464
+ # implement the optional ``ping`` utility. Subsequent keepalives fall
1465
+ # back to ``list_tools`` (the pre-ping probe) so we neither spam pings
1466
+ # nor reconnect-loop. Reset on each fresh transport connection.
1467
+ self._ping_unsupported: bool = False
1164
1468
 
1165
1469
  def _is_http(self) -> bool:
1166
1470
  """Check if this server uses HTTP transport."""
1167
1471
  return "url" in self._config
1168
1472
 
1473
+ def _advertises_tools(self) -> bool:
1474
+ """Whether the server advertises the ``tools`` capability.
1475
+
1476
+ Per the MCP spec, ``InitializeResult.capabilities.tools`` is non-None
1477
+ iff the server implements the ``tools/*`` request family. Prompt-only
1478
+ or resource-only servers omit it, and calling ``tools/list`` against
1479
+ them raises ``McpError(-32601 Method not found)`` — which previously
1480
+ killed the connection during discovery and made every keepalive fail.
1481
+ (Ported from anomalyco/opencode#31271.)
1482
+
1483
+ Returns True when no capability info was captured (legacy fallback:
1484
+ preserve the old always-call-list_tools behavior rather than regress
1485
+ any server that was working before this gate).
1486
+ """
1487
+ init_result = self.initialize_result
1488
+ caps = getattr(init_result, "capabilities", None) if init_result is not None else None
1489
+ if caps is None:
1490
+ return True
1491
+ return getattr(caps, "tools", None) is not None
1492
+
1169
1493
  # ----- Dynamic tool discovery (notifications/tools/list_changed) -----
1170
1494
 
1171
1495
  async def _refresh_tools_task(self):
@@ -1237,6 +1561,12 @@ class MCPServerTask:
1237
1561
  """
1238
1562
  from tools.registry import registry
1239
1563
 
1564
+ if not self._advertises_tools():
1565
+ # A server that doesn't implement tools/* should never send
1566
+ # tools/list_changed, but guard anyway — calling tools/list
1567
+ # would raise McpError(-32601).
1568
+ return
1569
+
1240
1570
  async with self._refresh_lock:
1241
1571
  # Capture old tool names for change diff
1242
1572
  old_tool_names = set(self._registered_tool_names)
@@ -1289,6 +1619,46 @@ class MCPServerTask:
1289
1619
  self.name, len(self._registered_tool_names),
1290
1620
  )
1291
1621
 
1622
+ async def _keepalive_probe(self) -> None:
1623
+ """Exercise the session to detect a stale/expired connection.
1624
+
1625
+ Uses ``ping`` (cheap, transport-agnostic liveness) by default. ``ping``
1626
+ is an OPTIONAL MCP utility: a server that doesn't implement it answers
1627
+ JSON-RPC -32601. The first time that happens we latch
1628
+ ``_ping_unsupported`` and fall back to the pre-ping probe — capability
1629
+ permitting, ``list_tools``; otherwise ``ping`` is the only option and
1630
+ the -32601 propagates (a server advertising neither a working ping nor
1631
+ tools has no liveness primitive left). The latch resets on each fresh
1632
+ transport connection so a server that gains ping support after a
1633
+ reconnect is re-probed with the cheap path.
1634
+
1635
+ Raises on a genuine connection failure so the caller triggers a
1636
+ reconnect; returns normally when the session is alive.
1637
+ """
1638
+ if not self._ping_unsupported:
1639
+ try:
1640
+ await asyncio.wait_for(self.session.send_ping(), timeout=30.0)
1641
+ return
1642
+ except Exception as exc:
1643
+ # Only a "method not found" means ping is unsupported. Any
1644
+ # other error (timeout, closed transport, session expired) is
1645
+ # a real liveness failure — propagate so we reconnect.
1646
+ if not _is_method_not_found_error(exc):
1647
+ raise
1648
+ if not self._advertises_tools():
1649
+ # No ping, no tools → no cheaper probe to fall back to.
1650
+ raise
1651
+ self._ping_unsupported = True
1652
+ logger.info(
1653
+ "MCP server '%s': does not implement the optional 'ping' "
1654
+ "utility (-32601); using 'list_tools' for keepalive on "
1655
+ "this connection.",
1656
+ self.name,
1657
+ )
1658
+
1659
+ # Fallback probe for servers without ping support.
1660
+ await asyncio.wait_for(self.session.list_tools(), timeout=30.0)
1661
+
1292
1662
  async def _wait_for_lifecycle_event(self) -> str:
1293
1663
  """Block until either _shutdown_event or _reconnect_event fires.
1294
1664
 
@@ -1302,13 +1672,29 @@ class MCPServerTask:
1302
1672
 
1303
1673
  Shutdown takes precedence if both events are set simultaneously.
1304
1674
 
1305
- Periodically sends a lightweight keepalive (``list_tools``) to
1306
- prevent TCP connections from going stale during long idle
1307
- periods (#17003). If the keepalive fails, triggers a reconnect.
1675
+ Periodically sends a lightweight keepalive (``ping``, with a
1676
+ ``list_tools`` fallback for servers that don't implement the optional
1677
+ ping utility see :meth:`_keepalive_probe`) to prevent TCP/session
1678
+ state from going stale during idle periods (#17003). If the keepalive
1679
+ fails, triggers a reconnect.
1680
+
1681
+ The cadence is ``keepalive_interval`` from server config (default
1682
+ :data:`_DEFAULT_KEEPALIVE_INTERVAL`, floored at
1683
+ :data:`_MIN_KEEPALIVE_INTERVAL`). Servers that GC idle sessions on a
1684
+ short TTL (e.g. Unreal Engine's editor MCP, ~15s) need an interval
1685
+ below that TTL, otherwise every idle tool call lands on an
1686
+ already-expired session and pays the full reconnect path.
1308
1687
  """
1309
- # Keepalive interval in seconds. Must be shorter than typical
1310
- # LB / NAT idle-timeout (commonly 300-600s).
1311
- _KEEPALIVE_INTERVAL = 180 # 3 minutes
1688
+ # Refresh faster than the server's session TTL. ``ping`` (MCP base
1689
+ # protocol liveness) is used rather than ``list_tools`` so the probe
1690
+ # stays a few bytes regardless of how many tools the server exposes —
1691
+ # a ``list_tools`` keepalive against an 830-tool server would pull
1692
+ # ~1 MB every cycle. Tool-list changes still arrive out-of-band via
1693
+ # ``notifications/tools/list_changed`` → ``_refresh_tools``.
1694
+ keepalive_interval = max(
1695
+ _MIN_KEEPALIVE_INTERVAL,
1696
+ float(self._config.get("keepalive_interval", _DEFAULT_KEEPALIVE_INTERVAL)),
1697
+ )
1312
1698
 
1313
1699
  shutdown_task = asyncio.create_task(self._shutdown_event.wait())
1314
1700
  reconnect_task = asyncio.create_task(self._reconnect_event.wait())
@@ -1316,20 +1702,23 @@ class MCPServerTask:
1316
1702
  while True:
1317
1703
  done, _pending = await asyncio.wait(
1318
1704
  {shutdown_task, reconnect_task},
1319
- timeout=_KEEPALIVE_INTERVAL,
1705
+ timeout=keepalive_interval,
1320
1706
  return_when=asyncio.FIRST_COMPLETED,
1321
1707
  )
1322
1708
  if done:
1323
1709
  break
1324
1710
 
1325
- # Timeout — no lifecycle event fired. Send a keepalive
1326
- # to exercise the connection and detect stale sockets.
1711
+ # Timeout — no lifecycle event fired. Probe the connection
1712
+ # to detect stale/expired sessions. Prefer ``ping`` (MCP base
1713
+ # protocol liveness): it works uniformly and stays a few bytes
1714
+ # regardless of tool count, unlike ``list_tools`` (~1 MB on an
1715
+ # 830-tool server). ``ping`` is an OPTIONAL utility, so a
1716
+ # tool-capable server that doesn't implement it answers -32601;
1717
+ # in that case fall back to the pre-ping ``list_tools`` probe
1718
+ # for the rest of this connection rather than reconnect-looping.
1327
1719
  if self.session:
1328
1720
  try:
1329
- await asyncio.wait_for(
1330
- self.session.list_tools(),
1331
- timeout=30.0,
1332
- )
1721
+ await self._keepalive_probe()
1333
1722
  except Exception as exc:
1334
1723
  logger.warning(
1335
1724
  "MCP server '%s' keepalive failed, "
@@ -1390,6 +1779,8 @@ class MCPServerTask:
1390
1779
  )
1391
1780
 
1392
1781
  sampling_kwargs = self._sampling.session_kwargs() if self._sampling else {}
1782
+ if self._elicitation:
1783
+ sampling_kwargs.update(self._elicitation.session_kwargs())
1393
1784
  if _MCP_NOTIFICATION_TYPES and _MCP_MESSAGE_HANDLER_SUPPORTED:
1394
1785
  sampling_kwargs["message_handler"] = self._make_message_handler()
1395
1786
 
@@ -1591,6 +1982,8 @@ class MCPServerTask:
1591
1982
  raise
1592
1983
 
1593
1984
  sampling_kwargs = self._sampling.session_kwargs() if self._sampling else {}
1985
+ if self._elicitation:
1986
+ sampling_kwargs.update(self._elicitation.session_kwargs())
1594
1987
  if _MCP_NOTIFICATION_TYPES and _MCP_MESSAGE_HANDLER_SUPPORTED:
1595
1988
  sampling_kwargs["message_handler"] = self._make_message_handler()
1596
1989
 
@@ -1742,9 +2135,29 @@ class MCPServerTask:
1742
2135
  )
1743
2136
 
1744
2137
  async def _discover_tools(self):
1745
- """Discover tools from the connected session."""
2138
+ """Discover tools from the connected session.
2139
+
2140
+ Capability-gated: prompt-only / resource-only MCP servers don't
2141
+ implement ``tools/list``, and calling it raises ``McpError(-32601)``,
2142
+ which previously aborted the connection — those servers could never
2143
+ stay connected for their prompts/resources. Skip the call when the
2144
+ server doesn't advertise the ``tools`` capability.
2145
+ (Ported from anomalyco/opencode#31271.)
2146
+ """
2147
+ # Fresh transport connection → re-probe with the cheap ``ping`` path.
2148
+ # Clears any latch from a prior connection in case the server gained
2149
+ # ping support across the reconnect.
2150
+ self._ping_unsupported = False
1746
2151
  if self.session is None:
1747
2152
  return
2153
+ if not self._advertises_tools():
2154
+ logger.info(
2155
+ "MCP server '%s': does not advertise 'tools' capability — "
2156
+ "skipping tools/list (prompts/resources remain available)",
2157
+ self.name,
2158
+ )
2159
+ self._tools = []
2160
+ return
1748
2161
  async with self._rpc_lock:
1749
2162
  tools_result = await self.session.list_tools()
1750
2163
  self._tools = (
@@ -1770,6 +2183,16 @@ class MCPServerTask:
1770
2183
  else:
1771
2184
  self._sampling = None
1772
2185
 
2186
+ # Set up elicitation handler if enabled and SDK types are available.
2187
+ # Servers use elicitation/create to ask the client for structured
2188
+ # input mid-tool-call (e.g. payment authorization). The handler
2189
+ # routes those requests through Hermes' approval system.
2190
+ elicitation_config = config.get("elicitation", {})
2191
+ if elicitation_config.get("enabled", True) and _MCP_ELICITATION_TYPES:
2192
+ self._elicitation = ElicitationHandler(self.name, elicitation_config, owner=self)
2193
+ else:
2194
+ self._elicitation = None
2195
+
1773
2196
  # Validate: warn if both url and command are present
1774
2197
  if "url" in config and "command" in config:
1775
2198
  logger.warning(
@@ -1800,7 +2223,12 @@ class MCPServerTask:
1800
2223
  # before surfacing an opaque CancelledError. Probing here — once,
1801
2224
  # outside the SDK task group — fails fast and non-retryably with
1802
2225
  # an actionable message, mirroring the URL-validation path above.
1803
- if config.get("transport") != "sse":
2226
+ # Skip the probe when _ready is already set: that only happens
2227
+ # after a prior successful connect, so this run() invocation is a
2228
+ # reconnect (OAuth recovery / manual refresh). The endpoint was
2229
+ # already validated once; re-probing burns a redundant network
2230
+ # round-trip against a known-good server on every reconnect.
2231
+ if config.get("transport") != "sse" and not self._ready.is_set():
1804
2232
  try:
1805
2233
  _probe_headers = dict(config.get("headers") or {})
1806
2234
  await self._preflight_content_type(
@@ -1981,6 +2409,8 @@ class MCPServerTask:
1981
2409
  # ---------------------------------------------------------------------------
1982
2410
 
1983
2411
  _servers: Dict[str, MCPServerTask] = {}
2412
+ _server_connecting: set[str] = set()
2413
+ _server_connect_errors: Dict[str, str] = {}
1984
2414
 
1985
2415
  # Circuit breaker: consecutive error counts per server. After
1986
2416
  # _CIRCUIT_BREAKER_THRESHOLD consecutive failures, the handler returns
@@ -2367,8 +2797,8 @@ _mcp_tool_server_names: Dict[str, str] = {}
2367
2797
  _mcp_loop: Optional[asyncio.AbstractEventLoop] = None
2368
2798
  _mcp_thread: Optional[threading.Thread] = None
2369
2799
 
2370
- # Protects _mcp_loop, _mcp_thread, _servers, _parallel_safe_servers,
2371
- # _mcp_tool_server_names, and _stdio_pids.
2800
+ # Protects _mcp_loop, _mcp_thread, _servers, MCP connection status maps,
2801
+ # _parallel_safe_servers, _mcp_tool_server_names, and _stdio_pids.
2372
2802
  _lock = threading.Lock()
2373
2803
 
2374
2804
  # PIDs of stdio MCP server subprocesses. Tracked so we can force-kill
@@ -2455,6 +2885,37 @@ def _ensure_mcp_loop():
2455
2885
  _mcp_thread.start()
2456
2886
 
2457
2887
 
2888
+ def _wrap_with_home_override(coro: "Coroutine") -> "Coroutine":
2889
+ """Carry the caller's context-local HERMES_HOME override into ``coro``.
2890
+
2891
+ Returns ``coro`` unchanged when no override is active. Otherwise wraps
2892
+ it so the override is set inside the coroutine's own (task-local)
2893
+ context on the MCP loop and reset when it completes — concurrent calls
2894
+ carrying different scopes don't interfere.
2895
+ """
2896
+ try:
2897
+ from hermes_constants import (
2898
+ get_hermes_home_override,
2899
+ reset_hermes_home_override,
2900
+ set_hermes_home_override,
2901
+ )
2902
+
2903
+ home_override = get_hermes_home_override()
2904
+ except Exception:
2905
+ return coro
2906
+ if not home_override:
2907
+ return coro
2908
+
2909
+ async def _scoped():
2910
+ token = set_hermes_home_override(home_override)
2911
+ try:
2912
+ return await coro
2913
+ finally:
2914
+ reset_hermes_home_override(token)
2915
+
2916
+ return _scoped()
2917
+
2918
+
2458
2919
  def _run_on_mcp_loop(coro_or_factory, timeout: float = 30):
2459
2920
  """Schedule a coroutine on the MCP event loop and block until done.
2460
2921
 
@@ -2477,6 +2938,19 @@ def _run_on_mcp_loop(coro_or_factory, timeout: float = 30):
2477
2938
  raise RuntimeError("MCP event loop is not running")
2478
2939
 
2479
2940
  coro = coro_or_factory() if callable(coro_or_factory) else coro_or_factory
2941
+
2942
+ # Propagate the context-local HERMES_HOME override onto the MCP loop.
2943
+ # Tasks scheduled via run_coroutine_threadsafe are created INSIDE the
2944
+ # loop thread, so they copy the loop thread's context — not the
2945
+ # scheduling thread's. A per-request profile scope (the dashboard's
2946
+ # ?profile= endpoints, e.g. the MCP "Test server" probe) would silently
2947
+ # vanish here: OAuth token stores and any other get_hermes_home()
2948
+ # resolution inside the coroutine would read the process home instead
2949
+ # of the selected profile's. Re-establish the override inside the
2950
+ # task's own context (task-local — concurrent calls carrying different
2951
+ # scopes don't interfere). No-op when no override is active.
2952
+ coro = _wrap_with_home_override(coro)
2953
+
2480
2954
  future = safe_schedule_threadsafe(
2481
2955
  coro, loop,
2482
2956
  logger=logger,
@@ -2522,10 +2996,19 @@ def _interrupted_call_result() -> str:
2522
2996
  # ---------------------------------------------------------------------------
2523
2997
 
2524
2998
  def _interpolate_env_vars(value):
2525
- """Recursively resolve ``${VAR}`` placeholders from ``os.environ``."""
2999
+ """Recursively resolve ``${VAR}`` placeholders.
3000
+
3001
+ Resolves from the active profile's secret scope when multiplexing is on
3002
+ (so an MCP server config's ``${API_KEY}`` picks up the routed profile's
3003
+ value, not the process-global ``os.environ`` which may hold another
3004
+ profile's), falling back to ``os.environ`` otherwise. Unset vars keep the
3005
+ literal ``${VAR}`` placeholder, as before.
3006
+ """
3007
+ from agent.secret_scope import get_secret as _get_secret
3008
+
2526
3009
  if isinstance(value, str):
2527
3010
  def _replace(m):
2528
- return os.environ.get(m.group(1), m.group(0))
3011
+ return _get_secret(m.group(1), m.group(0)) or m.group(0)
2529
3012
  return _ENV_VAR_PATTERN.sub(_replace, value)
2530
3013
  if isinstance(value, dict):
2531
3014
  return {k: _interpolate_env_vars(v) for k, v in value.items()}
@@ -2534,6 +3017,33 @@ def _interpolate_env_vars(value):
2534
3017
  return value
2535
3018
 
2536
3019
 
3020
+ def _filter_suspicious_mcp_servers(servers: Dict[str, dict]) -> Dict[str, dict]:
3021
+ """Drop exfiltration-shaped MCP configs before any stdio spawn path."""
3022
+ try:
3023
+ from hermes_cli.mcp_security import validate_mcp_server_entry as _validate_mcp_server_entry
3024
+ except Exception:
3025
+ _validate_mcp_server_entry: Callable[[str, dict[str, Any]], list[str]] | None = None
3026
+
3027
+ if _validate_mcp_server_entry is None:
3028
+ return servers
3029
+
3030
+ safe_servers = {}
3031
+ for name, cfg in servers.items():
3032
+ if not isinstance(cfg, dict):
3033
+ safe_servers[name] = cfg
3034
+ continue
3035
+ issues = _validate_mcp_server_entry(name, cfg)
3036
+ if issues:
3037
+ logger.warning(
3038
+ "Skipping suspicious MCP server '%s': %s",
3039
+ name,
3040
+ "; ".join(issues),
3041
+ )
3042
+ continue
3043
+ safe_servers[name] = cfg
3044
+ return safe_servers
3045
+
3046
+
2537
3047
  def _load_mcp_config() -> Dict[str, dict]:
2538
3048
  """Read ``mcp_servers`` from the Hermes config file.
2539
3049
 
@@ -2547,6 +3057,11 @@ def _load_mcp_config() -> Dict[str, dict]:
2547
3057
  """
2548
3058
  try:
2549
3059
  from hermes_cli.config import load_config
3060
+ # Safe mode (--safe-mode / HERMES_SAFE_MODE=1): troubleshooting run
3061
+ # with all customizations disabled — no MCP servers connect.
3062
+ from utils import env_var_enabled as _env_enabled
3063
+ if _env_enabled("HERMES_SAFE_MODE"):
3064
+ return {}
2550
3065
  config = load_config()
2551
3066
  servers = config.get("mcp_servers")
2552
3067
  if not servers or not isinstance(servers, dict):
@@ -2557,7 +3072,12 @@ def _load_mcp_config() -> Dict[str, dict]:
2557
3072
  load_hermes_dotenv()
2558
3073
  except Exception:
2559
3074
  pass
2560
- return {name: _interpolate_env_vars(cfg) for name, cfg in servers.items()}
3075
+ safe_servers: Dict[str, dict] = {}
3076
+ for name, cfg in _filter_suspicious_mcp_servers(servers).items():
3077
+ interpolated = _interpolate_env_vars(cfg)
3078
+ if isinstance(interpolated, dict):
3079
+ safe_servers[name] = interpolated
3080
+ return safe_servers
2561
3081
  except Exception as exc:
2562
3082
  logger.debug("Failed to load MCP config: %s", exc)
2563
3083
  return {}
@@ -2631,7 +3151,15 @@ def _make_tool_handler(server_name: str, tool_name: str, tool_timeout: float):
2631
3151
 
2632
3152
  async def _call():
2633
3153
  async with server._rpc_lock:
2634
- result = await server.session.call_tool(tool_name, arguments=args)
3154
+ # Snapshot the agent's context so an elicitation callback
3155
+ # triggered during this call (fired on the MCP recv loop
3156
+ # task, which doesn't inherit our contextvars) can replay
3157
+ # it and detect the gateway platform / session for routing.
3158
+ server._pending_call_context = contextvars.copy_context()
3159
+ try:
3160
+ result = await server.session.call_tool(tool_name, arguments=args)
3161
+ finally:
3162
+ server._pending_call_context = None
2635
3163
  # MCP CallToolResult has .content (list of content blocks) and .isError
2636
3164
  if result.isError:
2637
3165
  error_text = ""
@@ -3468,6 +3996,8 @@ async def _discover_and_register_server(name: str, config: dict) -> List[str]:
3468
3996
  timeout=connect_timeout,
3469
3997
  )
3470
3998
  with _lock:
3999
+ _server_connecting.discard(name)
4000
+ _server_connect_errors.pop(name, None)
3471
4001
  _servers[name] = server
3472
4002
 
3473
4003
  registered_names = _register_server_tools(name, server, config)
@@ -3502,6 +4032,7 @@ def register_mcp_servers(servers: Dict[str, dict]) -> List[str]:
3502
4032
  logger.debug("MCP SDK not available -- skipping explicit MCP registration")
3503
4033
  return []
3504
4034
 
4035
+ servers = _filter_suspicious_mcp_servers(servers)
3505
4036
  if not servers:
3506
4037
  logger.debug("No explicit MCP servers provided")
3507
4038
  return []
@@ -3514,6 +4045,9 @@ def register_mcp_servers(servers: Dict[str, dict]) -> List[str]:
3514
4045
  for k, v in servers.items()
3515
4046
  if k not in _servers and _parse_boolish(v.get("enabled", True), default=True)
3516
4047
  }
4048
+ _server_connecting.update(new_servers)
4049
+ for srv_name in new_servers:
4050
+ _server_connect_errors.pop(srv_name, None)
3517
4051
  # Track which servers opt-in to parallel tool calls (idempotent).
3518
4052
  for srv_name, srv_cfg in servers.items():
3519
4053
  if _parse_boolish(srv_cfg.get("supports_parallel_tool_calls", False), default=False):
@@ -3541,12 +4075,20 @@ def register_mcp_servers(servers: Dict[str, dict]) -> List[str]:
3541
4075
  for name, result in zip(server_names, results):
3542
4076
  if isinstance(result, BaseException):
3543
4077
  command = new_servers.get(name, {}).get("command")
4078
+ message = _format_connect_error(result)
4079
+ with _lock:
4080
+ _server_connecting.discard(name)
4081
+ _server_connect_errors[name] = message
3544
4082
  logger.warning(
3545
4083
  "Failed to connect to MCP server '%s'%s: %s",
3546
4084
  name,
3547
4085
  f" (command={command})" if command else "",
3548
- _format_connect_error(result),
4086
+ message,
3549
4087
  )
4088
+ else:
4089
+ with _lock:
4090
+ _server_connecting.discard(name)
4091
+ _server_connect_errors.pop(name, None)
3550
4092
 
3551
4093
  # Per-server timeouts are handled inside _discover_and_register_server.
3552
4094
  # The outer timeout is generous: 120s total for parallel discovery.
@@ -3651,8 +4193,10 @@ def is_mcp_tool_parallel_safe(tool_name: str) -> bool:
3651
4193
  def get_mcp_status() -> List[dict]:
3652
4194
  """Return status of all configured MCP servers for banner display.
3653
4195
 
3654
- Returns a list of dicts with keys: name, transport, tools, connected.
3655
- Includes both successfully connected servers and configured-but-failed ones.
4196
+ Returns a list of dicts with keys: name, transport, tools, connected,
4197
+ disabled, and status. Includes connected servers, disabled servers,
4198
+ in-flight connection attempts, recorded failures, and servers that are
4199
+ configured but have not been started in this process yet.
3656
4200
  """
3657
4201
  result: List[dict] = []
3658
4202
 
@@ -3663,9 +4207,12 @@ def get_mcp_status() -> List[dict]:
3663
4207
 
3664
4208
  with _lock:
3665
4209
  active_servers = dict(_servers)
4210
+ connecting = set(_server_connecting)
4211
+ connect_errors = dict(_server_connect_errors)
3666
4212
 
3667
4213
  for name, cfg in configured.items():
3668
4214
  transport = cfg.get("transport", "http") if "url" in cfg else "stdio"
4215
+ enabled = _parse_boolish(cfg.get("enabled", True), default=True)
3669
4216
  server = active_servers.get(name)
3670
4217
  if server and server.session is not None:
3671
4218
  entry = {
@@ -3673,16 +4220,51 @@ def get_mcp_status() -> List[dict]:
3673
4220
  "transport": transport,
3674
4221
  "tools": len(server._registered_tool_names) if hasattr(server, "_registered_tool_names") else len(server._tools),
3675
4222
  "connected": True,
4223
+ "disabled": False,
4224
+ "status": "connected",
3676
4225
  }
3677
4226
  if server._sampling:
3678
4227
  entry["sampling"] = dict(server._sampling.metrics)
3679
4228
  result.append(entry)
4229
+ elif not enabled:
4230
+ # A server with enabled: false is intentionally not connected — it is
4231
+ # disabled, not failed. Surface that distinction so consumers (banner,
4232
+ # TUI) can render "disabled" rather than an alarming "failed".
4233
+ result.append({
4234
+ "name": name,
4235
+ "transport": transport,
4236
+ "tools": 0,
4237
+ "connected": False,
4238
+ "disabled": True,
4239
+ "status": "disabled",
4240
+ })
4241
+ elif name in connecting:
4242
+ result.append({
4243
+ "name": name,
4244
+ "transport": transport,
4245
+ "tools": 0,
4246
+ "connected": False,
4247
+ "disabled": False,
4248
+ "status": "connecting",
4249
+ })
4250
+ elif name in connect_errors:
4251
+ result.append({
4252
+ "name": name,
4253
+ "transport": transport,
4254
+ "tools": 0,
4255
+ "connected": False,
4256
+ "disabled": False,
4257
+ "status": "failed",
4258
+ "error": connect_errors[name],
4259
+ })
3680
4260
  else:
3681
4261
  result.append({
3682
4262
  "name": name,
3683
4263
  "transport": transport,
3684
4264
  "tools": 0,
3685
4265
  "connected": False,
4266
+ "disabled": False,
4267
+ "status": "configured",
3686
4268
  })
3687
4269
 
3688
4270
  return result
@@ -3749,11 +4331,220 @@ def probe_mcp_server_tools() -> Dict[str, List[tuple]]:
3749
4331
  except Exception as exc:
3750
4332
  logger.debug("MCP probe failed: %s", exc)
3751
4333
  finally:
3752
- _stop_mcp_loop()
4334
+ _stop_mcp_loop_if_idle()
3753
4335
 
3754
4336
  return result
3755
4337
 
3756
4338
 
4339
+ # Serializes in-place mutation of an agent's tool snapshot. The reload RPC,
4340
+ # the gateway reload, and the late-binding refresh thread all swap
4341
+ # ``agent.tools`` / ``agent.valid_tool_names`` after the agent was built; the
4342
+ # agent's run loop reads those during tool iteration, so a concurrent write
4343
+ # mid-read could otherwise expose a half-updated list.
4344
+ _agent_tools_lock = threading.Lock()
4345
+
4346
+
4347
+ def has_registered_mcp_tools() -> bool:
4348
+ """True if any MCP server has actually registered tools into the registry.
4349
+
4350
+ Cheap — checks the global MCP-tool→server name map under ``_lock``, no
4351
+ registry walk. Used by the per-turn refresh hook so a session with no MCP
4352
+ tools (the common case, and also a connected-but-zero-tool/prompt-only
4353
+ server) skips the ``get_tool_definitions`` rebuild entirely. Checks
4354
+ registered TOOLS, not connected servers, so a server that registers no tools
4355
+ doesn't keep the hook firing every turn.
4356
+ """
4357
+ with _lock:
4358
+ return bool(_mcp_tool_server_names)
4359
+
4360
+
4361
+ def refresh_agent_mcp_tools(
4362
+ agent,
4363
+ *,
4364
+ enabled_override=None,
4365
+ disabled_override=None,
4366
+ quiet_mode: bool = True,
4367
+ ) -> set:
4368
+ """Re-derive an already-built agent's tool snapshot from the live registry.
4369
+
4370
+ The agent snapshots ``agent.tools`` once at build time and never re-reads
4371
+ the registry (see ``run_agent`` / ``agent_init``). When MCP servers connect
4372
+ *after* that snapshot — a slow HTTP/OAuth server that misses the bounded
4373
+ startup wait, or a ``/reload-mcp`` — their tools are invisible until the
4374
+ snapshot is rebuilt. This is the single shared rebuild used by every such
4375
+ caller (the TUI ``reload.mcp`` RPC, the gateway reload, the late-binding
4376
+ refresh thread, and the per-turn between-turns refresh) so they can't drift
4377
+ apart again.
4378
+
4379
+ The rebuild respects the agent's own ``enabled_toolsets`` /
4380
+ ``disabled_toolsets`` (the same filtering it was built with) and diffs by
4381
+ tool **name** (not count — a count compare misses an equal-size add/remove
4382
+ swap).
4383
+
4384
+ Crucially it is **additive-preserving**: ``get_tool_definitions`` returns
4385
+ only the registry-derived tools, but ``agent_init`` appends two further
4386
+ families directly onto ``agent.tools`` *after* that — external
4387
+ memory-provider tools (mem0/honcho/…) and context-engine tools
4388
+ (``lcm_*``). A naive ``agent.tools = get_tool_definitions(...)`` would
4389
+ silently DELETE those. So after rebuilding the registry set we re-run the
4390
+ same post-build injectors ``agent_init`` used, reconstructing the full
4391
+ surface. The new ``(tools, valid_tool_names)`` pair is published together
4392
+ under ``_agent_tools_lock`` so a concurrent reader never sees a
4393
+ cross-attribute half-swap.
4394
+
4395
+ Returns the set of newly-added tool names (empty when nothing changed), so
4396
+ callers can decide whether to notify the user / re-emit session info. The
4397
+ caller owns the prompt-cache contract: this helper does NOT check turn state,
4398
+ because each caller has a different policy (``/reload-mcp`` rebuilds after
4399
+ explicit user consent; the late-binding and between-turns paths only rebuild
4400
+ at a turn boundary, before that turn's ``tools=`` prefix is assembled).
4401
+ """
4402
+ from model_tools import get_tool_definitions
4403
+ from tools.registry import registry
4404
+
4405
+ # Explicit reloads (/reload-mcp) pass freshly-resolved toolsets so a server
4406
+ # the user just ENABLED in config is picked up; the agent's stored selection
4407
+ # is then updated to match. The automatic paths (between-turns, late-binding)
4408
+ # pass nothing and reuse the agent's build-time selection unchanged.
4409
+ if enabled_override is not None or disabled_override is not None:
4410
+ enabled = enabled_override if enabled_override is not None else getattr(agent, "enabled_toolsets", None)
4411
+ disabled = disabled_override if disabled_override is not None else getattr(agent, "disabled_toolsets", None)
4412
+ agent.enabled_toolsets = enabled
4413
+ agent.disabled_toolsets = disabled
4414
+ else:
4415
+ enabled = getattr(agent, "enabled_toolsets", None)
4416
+ disabled = getattr(agent, "disabled_toolsets", None)
4417
+
4418
+ # Capture the registry generation this rebuild is derived from BEFORE the
4419
+ # (potentially slow) get_tool_definitions call. Used at publish time to
4420
+ # reject a stale write: if two callers race (e.g. the late-refresh daemon
4421
+ # and the between-turns prologue around turn 1), a slower caller that
4422
+ # computed an OLDER set must not clobber a newer set another caller already
4423
+ # published. ``registry._generation`` bumps on every (de)register.
4424
+ snapshot_generation = registry._generation
4425
+
4426
+ # Registry-derived tools (built-ins + MCP), filtered to the agent's toolsets.
4427
+ # Computed OUTSIDE the lock (get_tool_definitions can be slow); the diff and
4428
+ # publish below happen together in ONE critical section so two concurrent
4429
+ # callers can't torn-publish or compute overlapping ``added`` sets.
4430
+ new_defs = list(
4431
+ get_tool_definitions(
4432
+ enabled_toolsets=enabled,
4433
+ disabled_toolsets=disabled,
4434
+ quiet_mode=quiet_mode,
4435
+ )
4436
+ or []
4437
+ )
4438
+ new_names = {t["function"]["name"] for t in new_defs}
4439
+
4440
+ # Re-append the post-build injected families that get_tool_definitions does
4441
+ # NOT reproduce, so a refresh never strips them (memory-provider + context-
4442
+ # engine tools). Staged entirely on LOCALS — the live ``agent.tools`` /
4443
+ # ``valid_tool_names`` / ``_context_engine_tool_names`` are never touched
4444
+ # until the single atomic publish below, so a concurrent reader
4445
+ # (``build_api_kwargs``) can't see a partial rebuild or a cross-attribute
4446
+ # half-swap. ``staged_engine_names`` are the context-engine routing names
4447
+ # this rebuild actually appended (matching agent_init's dedup-aware add).
4448
+ staged_engine_names = _reinject_post_build_tools(agent, new_defs, new_names)
4449
+
4450
+ # Single atomic read-diff-publish so the returned ``added`` is consistent
4451
+ # with what was actually published, even under concurrent callers, and a
4452
+ # stale (older-generation) rebuild can't overwrite a newer published one.
4453
+ with _agent_tools_lock:
4454
+ # Defensive: the published generation should be an int, but tolerate an
4455
+ # agent that never set it (or set a non-int, e.g. a test mock) rather
4456
+ # than throwing TypeError on the comparison and silently failing the
4457
+ # whole refresh.
4458
+ published_gen_raw = getattr(agent, "_tool_snapshot_generation", -1)
4459
+ published_gen = published_gen_raw if isinstance(published_gen_raw, int) else -1
4460
+ if snapshot_generation < published_gen:
4461
+ # A newer snapshot already won; our set is stale — drop it.
4462
+ return set()
4463
+ current = {
4464
+ t["function"]["name"]
4465
+ for t in (getattr(agent, "tools", None) or [])
4466
+ }
4467
+ if new_names == current:
4468
+ # No change → leave the live snapshot untouched (no churn), but
4469
+ # record the generation so an in-flight older caller can't clobber.
4470
+ agent._tool_snapshot_generation = max(published_gen, snapshot_generation)
4471
+ return set()
4472
+ agent.tools = new_defs
4473
+ agent.valid_tool_names = new_names
4474
+ # Publish context-engine routing names atomically with the snapshot.
4475
+ engine_names = getattr(agent, "_context_engine_tool_names", None)
4476
+ if isinstance(engine_names, set):
4477
+ engine_names.clear()
4478
+ engine_names.update(staged_engine_names)
4479
+ agent._tool_snapshot_generation = max(published_gen, snapshot_generation)
4480
+ return new_names - current
4481
+
4482
+
4483
+ def _reinject_post_build_tools(agent, tools_list: list, name_set: set) -> set:
4484
+ """Append memory-provider and context-engine tools onto staged locals.
4485
+
4486
+ Mirrors the post-``get_tool_definitions`` injection in ``agent_init`` so a
4487
+ snapshot rebuild reconstructs the FULL tool surface, not just the
4488
+ registry-derived subset. Operates ONLY on the caller's staged ``tools_list``
4489
+ / ``name_set`` (never the live agent attributes) so the rebuild stays atomic.
4490
+ Idempotent (skips names already present) and fail-soft.
4491
+
4492
+ Returns the set of context-engine routing names actually appended by THIS
4493
+ rebuild — matching ``agent_init``'s dedup behavior (a name already provided
4494
+ by a registry/plugin tool is NOT claimed for context-engine routing). The
4495
+ caller publishes this into ``agent._context_engine_tool_names`` atomically
4496
+ with the snapshot.
4497
+ """
4498
+ def _add(schema: dict) -> bool:
4499
+ name = schema.get("name", "")
4500
+ if not name or name in name_set:
4501
+ return False
4502
+ tools_list.append({"type": "function", "function": schema})
4503
+ name_set.add(name)
4504
+ return True
4505
+
4506
+ # Memory-provider tools (mem0/honcho/byterover/supermemory/…).
4507
+ try:
4508
+ memory_manager = getattr(agent, "_memory_manager", None)
4509
+ get_mem_schemas = getattr(memory_manager, "get_all_tool_schemas", None) if memory_manager else None
4510
+ if callable(get_mem_schemas):
4511
+ # Honor the same enablement gate inject_memory_provider_tools uses.
4512
+ from agent.memory_manager import memory_provider_tools_enabled
4513
+ if "memory" in name_set or memory_provider_tools_enabled(getattr(agent, "enabled_toolsets", None)):
4514
+ for schema in get_mem_schemas():
4515
+ if isinstance(schema, dict):
4516
+ _add(schema)
4517
+ except Exception:
4518
+ logger.debug("Memory-provider tool re-injection skipped", exc_info=True)
4519
+
4520
+ # Context-engine tools (lcm_grep/lcm_describe/…) — the `context_engine`
4521
+ # toolset is intentionally empty, so these only exist via this append.
4522
+ # Honor the same enabled_toolsets gate agent_init uses (#5544): without it a
4523
+ # restricted-toolset platform (e.g. platform_toolsets: telegram: []) would
4524
+ # re-leak lcm_* tools the build deliberately excluded, and pay the local-
4525
+ # model latency penalty.
4526
+ staged_engine_names: set = set()
4527
+ try:
4528
+ enabled = getattr(agent, "enabled_toolsets", None)
4529
+ context_engine_allowed = enabled is None or "context_engine" in enabled
4530
+ compressor = getattr(agent, "context_compressor", None)
4531
+ get_schemas = getattr(compressor, "get_tool_schemas", None) if compressor else None
4532
+ if context_engine_allowed and callable(get_schemas):
4533
+ for schema in get_schemas():
4534
+ if not isinstance(schema, dict):
4535
+ continue
4536
+ name = schema.get("name", "")
4537
+ # Only claim the routing name when WE appended the schema, so a
4538
+ # name already owned by a registry/plugin tool keeps its own
4539
+ # dispatch (matches agent_init.py's `continue`-before-claim).
4540
+ if _add(schema) and name:
4541
+ staged_engine_names.add(name)
4542
+ except Exception:
4543
+ logger.debug("Context-engine tool re-injection skipped", exc_info=True)
4544
+
4545
+ return staged_engine_names
4546
+
4547
+
3757
4548
  def shutdown_mcp_servers():
3758
4549
  """Close all MCP server connections and stop the background loop.
3759
4550
 
@@ -3794,7 +4585,7 @@ def shutdown_mcp_servers():
3794
4585
  if future is not None:
3795
4586
  try:
3796
4587
  future.result(timeout=15)
3797
- except Exception as exc:
4588
+ except BaseException as exc:
3798
4589
  logger.debug("Error during MCP shutdown: %s", exc)
3799
4590
 
3800
4591
  _stop_mcp_loop()
@@ -3887,10 +4678,25 @@ def _kill_orphaned_mcp_children(include_active: bool = False) -> None:
3887
4678
  )
3888
4679
 
3889
4680
 
3890
- def _stop_mcp_loop():
4681
+ def _stop_mcp_loop_if_idle() -> bool:
4682
+ """Stop the MCP loop only when no registered server still owns it.
4683
+
4684
+ Probe paths create temporary MCPServerTask instances that are not placed in
4685
+ ``_servers``. They should clean up an otherwise-idle loop, but must not
4686
+ tear down the process-global loop when live agent tools are registered on
4687
+ it. Otherwise a dashboard/CLI probe can make later MCP tool calls fail
4688
+ with ``MCP event loop is not running``.
4689
+ """
4690
+ return _stop_mcp_loop(only_if_idle=True)
4691
+
4692
+
4693
+ def _stop_mcp_loop(*, only_if_idle: bool = False) -> bool:
3891
4694
  """Stop the background event loop and join its thread."""
3892
4695
  global _mcp_loop, _mcp_thread
3893
4696
  with _lock:
4697
+ if only_if_idle and (_servers or _server_connecting):
4698
+ logger.debug("Leaving MCP event loop running; active servers are registered or connecting")
4699
+ return False
3894
4700
  loop = _mcp_loop
3895
4701
  thread = _mcp_thread
3896
4702
  _mcp_loop = None
@@ -3907,3 +4713,4 @@ def _stop_mcp_loop():
3907
4713
  # graceful shutdown are now orphaned — include active PIDs too
3908
4714
  # since the loop is gone and no session can still be in flight.
3909
4715
  _kill_orphaned_mcp_children(include_active=True)
4716
+ return True