@clawpump/claw-agent 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1212) hide show
  1. package/agent/.dockerignore +67 -0
  2. package/agent/.envrc +1 -1
  3. package/agent/.gitattributes +8 -0
  4. package/agent/AGENTS.md +216 -4
  5. package/agent/CONTRIBUTING.md +46 -8
  6. package/agent/Dockerfile +78 -35
  7. package/agent/MANIFEST.in +2 -0
  8. package/agent/README.md +12 -5
  9. package/agent/README.ur-pk.md +261 -0
  10. package/agent/README.zh-CN.md +11 -8
  11. package/agent/SECURITY.md +5 -4
  12. package/agent/acp_adapter/provenance.py +127 -0
  13. package/agent/acp_adapter/server.py +112 -5
  14. package/agent/acp_adapter/session.py +1 -6
  15. package/agent/acp_registry/agent.json +2 -2
  16. package/agent/agent/account_usage.py +313 -1
  17. package/agent/agent/agent_init.py +140 -37
  18. package/agent/agent/agent_runtime_helpers.py +342 -83
  19. package/agent/agent/anthropic_adapter.py +320 -33
  20. package/agent/agent/auxiliary_client.py +525 -105
  21. package/agent/agent/background_review.py +157 -19
  22. package/agent/agent/bedrock_adapter.py +71 -6
  23. package/agent/agent/billing_view.py +295 -0
  24. package/agent/agent/chat_completion_helpers.py +229 -4
  25. package/agent/agent/codex_responses_adapter.py +86 -10
  26. package/agent/agent/codex_runtime.py +153 -1
  27. package/agent/agent/coding_context.py +738 -0
  28. package/agent/agent/context_compressor.py +392 -44
  29. package/agent/agent/context_references.py +34 -1
  30. package/agent/agent/conversation_compression.py +159 -22
  31. package/agent/agent/conversation_loop.py +643 -908
  32. package/agent/agent/copilot_acp_client.py +4 -11
  33. package/agent/agent/credential_pool.py +5 -3
  34. package/agent/agent/credits_tracker.py +794 -0
  35. package/agent/agent/curator.py +91 -18
  36. package/agent/agent/curator_backup.py +26 -10
  37. package/agent/agent/display.py +42 -1
  38. package/agent/agent/error_classifier.py +52 -3
  39. package/agent/agent/errors.py +3 -0
  40. package/agent/agent/file_safety.py +0 -17
  41. package/agent/agent/gemini_native_adapter.py +31 -1
  42. package/agent/agent/i18n.py +48 -4
  43. package/agent/agent/image_gen_provider.py +74 -5
  44. package/agent/agent/image_routing.py +29 -0
  45. package/agent/agent/insights.py +8 -17
  46. package/agent/agent/lsp/install.py +3 -0
  47. package/agent/agent/memory_manager.py +326 -31
  48. package/agent/agent/message_content.py +50 -0
  49. package/agent/agent/model_metadata.py +214 -3
  50. package/agent/agent/moonshot_schema.py +8 -1
  51. package/agent/agent/onboarding.py +60 -0
  52. package/agent/agent/prompt_builder.py +327 -37
  53. package/agent/agent/redact.py +1 -0
  54. package/agent/agent/runtime_cwd.py +34 -5
  55. package/agent/agent/secret_scope.py +205 -0
  56. package/agent/agent/secret_sources/bitwarden.py +34 -2
  57. package/agent/agent/skill_commands.py +90 -1
  58. package/agent/agent/skill_preprocessing.py +1 -0
  59. package/agent/agent/skill_utils.py +209 -36
  60. package/agent/agent/ssl_guard.py +94 -0
  61. package/agent/agent/system_prompt.py +133 -5
  62. package/agent/agent/tool_executor.py +496 -70
  63. package/agent/agent/transports/anthropic.py +83 -21
  64. package/agent/agent/transports/chat_completions.py +94 -5
  65. package/agent/agent/transports/codex.py +67 -2
  66. package/agent/agent/transports/codex_app_server.py +1 -0
  67. package/agent/agent/transports/codex_app_server_session.py +30 -0
  68. package/agent/agent/transports/types.py +12 -0
  69. package/agent/agent/turn_context.py +408 -0
  70. package/agent/agent/turn_finalizer.py +428 -0
  71. package/agent/agent/turn_retry_state.py +68 -0
  72. package/agent/agent/usage_pricing.py +3 -0
  73. package/agent/apps/bootstrap-installer/package.json +6 -5
  74. package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
  75. package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
  76. package/agent/apps/bootstrap-installer/src/store.ts +3 -2
  77. package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
  78. package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
  79. package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
  80. package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
  81. package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
  82. package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
  83. package/agent/apps/desktop/DESIGN.md +167 -0
  84. package/agent/apps/desktop/README.md +20 -16
  85. package/agent/apps/desktop/assets/icon.icns +0 -0
  86. package/agent/apps/desktop/assets/icon.ico +0 -0
  87. package/agent/apps/desktop/assets/icon.png +0 -0
  88. package/agent/apps/desktop/electron/backend-env.cjs +112 -0
  89. package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
  90. package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
  91. package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
  92. package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
  93. package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
  94. package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
  95. package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
  96. package/agent/apps/desktop/electron/connection-config.cjs +288 -0
  97. package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
  98. package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
  99. package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
  100. package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
  101. package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
  102. package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
  103. package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
  104. package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
  105. package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
  106. package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
  107. package/agent/apps/desktop/electron/git-root.cjs +54 -0
  108. package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
  109. package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
  110. package/agent/apps/desktop/electron/hardening.cjs +123 -28
  111. package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
  112. package/agent/apps/desktop/electron/main.cjs +3121 -331
  113. package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
  114. package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
  115. package/agent/apps/desktop/electron/preload.cjs +52 -2
  116. package/agent/apps/desktop/electron/session-windows.cjs +124 -0
  117. package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
  118. package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
  119. package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
  120. package/agent/apps/desktop/electron/update-remote.cjs +56 -0
  121. package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
  122. package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
  123. package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
  124. package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
  125. package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
  126. package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
  127. package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
  128. package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
  129. package/agent/apps/desktop/eslint.config.mjs +0 -3
  130. package/agent/apps/desktop/index.html +27 -2
  131. package/agent/apps/desktop/package.json +31 -11
  132. package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
  133. package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
  134. package/agent/apps/desktop/public/nous-girl.jpg +0 -0
  135. package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
  136. package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
  137. package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
  138. package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
  139. package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
  140. package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
  141. package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
  142. package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
  143. package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
  144. package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
  145. package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
  146. package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
  147. package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
  148. package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
  149. package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
  150. package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
  151. package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
  152. package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
  153. package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
  154. package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
  155. package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
  156. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
  157. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
  158. package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
  159. package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
  160. package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
  161. package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
  162. package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
  163. package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
  164. package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
  165. package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
  166. package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
  167. package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
  168. package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
  169. package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
  170. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
  171. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
  172. package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
  173. package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
  174. package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
  175. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
  176. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
  177. package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
  178. package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
  179. package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
  180. package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
  181. package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
  182. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
  183. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
  184. package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
  185. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
  186. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
  187. package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
  188. package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
  189. package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
  190. package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
  191. package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
  192. package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
  193. package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
  194. package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
  195. package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
  196. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
  197. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
  198. package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
  199. package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
  200. package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
  201. package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
  202. package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
  203. package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
  204. package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
  205. package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
  206. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
  207. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
  208. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
  209. package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
  210. package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
  211. package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
  212. package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
  213. package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
  214. package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
  215. package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
  216. package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
  217. package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
  218. package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
  219. package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
  220. package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
  221. package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
  222. package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
  223. package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
  224. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
  225. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
  226. package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
  227. package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
  228. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
  229. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
  230. package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
  231. package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
  232. package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
  233. package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
  234. package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
  235. package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
  236. package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
  237. package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
  238. package/agent/apps/desktop/src/app/routes.ts +9 -0
  239. package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
  240. package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
  241. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
  242. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
  243. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
  244. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
  245. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
  246. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
  247. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
  248. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
  249. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
  250. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
  251. package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
  252. package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
  253. package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
  254. package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
  255. package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
  256. package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
  257. package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
  258. package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
  259. package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
  260. package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
  261. package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
  262. package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
  263. package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
  264. package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
  265. package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
  266. package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
  267. package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
  268. package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
  269. package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
  270. package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
  271. package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
  272. package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
  273. package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
  274. package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
  275. package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
  276. package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
  277. package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
  278. package/agent/apps/desktop/src/app/settings/types.ts +9 -6
  279. package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
  280. package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
  281. package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
  282. package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
  283. package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
  284. package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
  285. package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
  286. package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
  287. package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
  288. package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
  289. package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
  290. package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
  291. package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
  292. package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
  293. package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
  294. package/agent/apps/desktop/src/app/types.ts +85 -0
  295. package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
  296. package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
  297. package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
  298. package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
  299. package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
  300. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
  301. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
  302. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
  303. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
  304. package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
  305. package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
  306. package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
  307. package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
  308. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
  309. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
  310. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
  311. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
  312. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
  313. package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
  314. package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
  315. package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
  316. package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
  317. package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
  318. package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
  319. package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
  320. package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
  321. package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
  322. package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
  323. package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
  324. package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
  325. package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
  326. package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
  327. package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
  328. package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
  329. package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
  330. package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
  331. package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
  332. package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
  333. package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
  334. package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
  335. package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
  336. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
  337. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
  338. package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
  339. package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
  340. package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
  341. package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
  342. package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
  343. package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
  344. package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
  345. package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
  346. package/agent/apps/desktop/src/components/notifications.tsx +48 -27
  347. package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
  348. package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
  349. package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
  350. package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
  351. package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
  352. package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
  353. package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
  354. package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
  355. package/agent/apps/desktop/src/components/ui/control.ts +25 -0
  356. package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
  357. package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
  358. package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
  359. package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
  360. package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
  361. package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
  362. package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
  363. package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
  364. package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
  365. package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
  366. package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
  367. package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
  368. package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
  369. package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
  370. package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
  371. package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
  372. package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
  373. package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
  374. package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
  375. package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
  376. package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
  377. package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
  378. package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
  379. package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
  380. package/agent/apps/desktop/src/global.d.ts +181 -4
  381. package/agent/apps/desktop/src/hermes.test.ts +60 -0
  382. package/agent/apps/desktop/src/hermes.ts +190 -13
  383. package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
  384. package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
  385. package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
  386. package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
  387. package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
  388. package/agent/apps/desktop/src/i18n/context.tsx +183 -0
  389. package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
  390. package/agent/apps/desktop/src/i18n/en.ts +1921 -0
  391. package/agent/apps/desktop/src/i18n/index.ts +20 -0
  392. package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
  393. package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
  394. package/agent/apps/desktop/src/i18n/languages.ts +86 -0
  395. package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
  396. package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
  397. package/agent/apps/desktop/src/i18n/types.ts +1559 -0
  398. package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
  399. package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
  400. package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
  401. package/agent/apps/desktop/src/lib/ansi.ts +186 -0
  402. package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
  403. package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
  404. package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
  405. package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
  406. package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
  407. package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
  408. package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
  409. package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
  410. package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
  411. package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
  412. package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
  413. package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
  414. package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
  415. package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
  416. package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
  417. package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
  418. package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
  419. package/agent/apps/desktop/src/lib/haptics.ts +17 -0
  420. package/agent/apps/desktop/src/lib/icons.ts +10 -2
  421. package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
  422. package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
  423. package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
  424. package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
  425. package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
  426. package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
  427. package/agent/apps/desktop/src/lib/media.ts +40 -1
  428. package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
  429. package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
  430. package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
  431. package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
  432. package/agent/apps/desktop/src/lib/query-client.ts +13 -0
  433. package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
  434. package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
  435. package/agent/apps/desktop/src/lib/session-export.ts +6 -3
  436. package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
  437. package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
  438. package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
  439. package/agent/apps/desktop/src/lib/session-search.ts +21 -0
  440. package/agent/apps/desktop/src/lib/session-source.ts +126 -0
  441. package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
  442. package/agent/apps/desktop/src/lib/storage.ts +35 -1
  443. package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
  444. package/agent/apps/desktop/src/lib/todos.ts +37 -0
  445. package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
  446. package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
  447. package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
  448. package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
  449. package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
  450. package/agent/apps/desktop/src/main.tsx +19 -19
  451. package/agent/apps/desktop/src/store/boot.ts +4 -3
  452. package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
  453. package/agent/apps/desktop/src/store/clarify.ts +50 -13
  454. package/agent/apps/desktop/src/store/command-palette.ts +20 -0
  455. package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
  456. package/agent/apps/desktop/src/store/compaction.ts +38 -0
  457. package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
  458. package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
  459. package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
  460. package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
  461. package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
  462. package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
  463. package/agent/apps/desktop/src/store/composer-status.ts +277 -0
  464. package/agent/apps/desktop/src/store/composer.test.ts +106 -0
  465. package/agent/apps/desktop/src/store/composer.ts +116 -0
  466. package/agent/apps/desktop/src/store/cron.ts +19 -0
  467. package/agent/apps/desktop/src/store/gateway.ts +280 -6
  468. package/agent/apps/desktop/src/store/keybinds.ts +143 -0
  469. package/agent/apps/desktop/src/store/layout.ts +107 -9
  470. package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
  471. package/agent/apps/desktop/src/store/model-presets.ts +86 -0
  472. package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
  473. package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
  474. package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
  475. package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
  476. package/agent/apps/desktop/src/store/notifications.ts +10 -7
  477. package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
  478. package/agent/apps/desktop/src/store/onboarding.ts +268 -38
  479. package/agent/apps/desktop/src/store/preview.ts +10 -1
  480. package/agent/apps/desktop/src/store/profile.test.ts +89 -0
  481. package/agent/apps/desktop/src/store/profile.ts +395 -0
  482. package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
  483. package/agent/apps/desktop/src/store/prompts.ts +117 -0
  484. package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
  485. package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
  486. package/agent/apps/desktop/src/store/session-sync.ts +25 -0
  487. package/agent/apps/desktop/src/store/session.test.ts +268 -2
  488. package/agent/apps/desktop/src/store/session.ts +392 -18
  489. package/agent/apps/desktop/src/store/subagents.ts +3 -0
  490. package/agent/apps/desktop/src/store/system-actions.ts +48 -0
  491. package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
  492. package/agent/apps/desktop/src/store/todos.test.ts +47 -0
  493. package/agent/apps/desktop/src/store/todos.ts +64 -0
  494. package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
  495. package/agent/apps/desktop/src/store/translucency.ts +38 -0
  496. package/agent/apps/desktop/src/store/updates.test.ts +187 -2
  497. package/agent/apps/desktop/src/store/updates.ts +268 -18
  498. package/agent/apps/desktop/src/store/windows.test.ts +143 -0
  499. package/agent/apps/desktop/src/store/windows.ts +115 -0
  500. package/agent/apps/desktop/src/styles.css +510 -119
  501. package/agent/apps/desktop/src/themes/color.ts +142 -0
  502. package/agent/apps/desktop/src/themes/context.tsx +128 -75
  503. package/agent/apps/desktop/src/themes/install.test.ts +119 -0
  504. package/agent/apps/desktop/src/themes/install.ts +95 -0
  505. package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
  506. package/agent/apps/desktop/src/themes/presets.ts +13 -4
  507. package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
  508. package/agent/apps/desktop/src/themes/types.ts +35 -0
  509. package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
  510. package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
  511. package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
  512. package/agent/apps/desktop/src/themes/vscode.ts +343 -0
  513. package/agent/apps/desktop/src/types/hermes.ts +138 -1
  514. package/agent/apps/desktop/tsconfig.json +2 -2
  515. package/agent/apps/desktop/vite.config.ts +18 -0
  516. package/agent/apps/shared/package.json +1 -1
  517. package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
  518. package/agent/apps/shared/tsconfig.json +2 -2
  519. package/agent/cli-config.yaml.example +78 -1
  520. package/agent/cli.py +2177 -3162
  521. package/agent/cron/blueprint_catalog.py +713 -0
  522. package/agent/cron/jobs.py +226 -110
  523. package/agent/cron/scheduler.py +468 -193
  524. package/agent/cron/scheduler_provider.py +177 -0
  525. package/agent/cron/scripts/__init__.py +1 -0
  526. package/agent/cron/scripts/classify_items.py +226 -0
  527. package/agent/cron/suggestion_catalog.py +154 -0
  528. package/agent/cron/suggestions.py +257 -0
  529. package/agent/docs/chronos-managed-cron-contract.md +196 -0
  530. package/agent/docs/design/profile-builder.md +146 -0
  531. package/agent/docs/middleware/README.md +260 -0
  532. package/agent/docs/observability/README.md +316 -0
  533. package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
  534. package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
  535. package/agent/docs/relay-connector-contract.md +285 -0
  536. package/agent/gateway/authz_mixin.py +536 -0
  537. package/agent/gateway/channel_directory.py +65 -3
  538. package/agent/gateway/config.py +222 -12
  539. package/agent/gateway/display_config.py +10 -0
  540. package/agent/gateway/hooks.py +17 -0
  541. package/agent/gateway/kanban_watchers.py +1146 -0
  542. package/agent/gateway/message_timestamps.py +166 -0
  543. package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
  544. package/agent/gateway/platforms/api_server.py +216 -38
  545. package/agent/gateway/platforms/base.py +210 -58
  546. package/agent/gateway/platforms/email.py +122 -12
  547. package/agent/gateway/platforms/feishu.py +80 -11
  548. package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
  549. package/agent/gateway/platforms/matrix.py +1498 -297
  550. package/agent/gateway/platforms/qqbot/adapter.py +6 -0
  551. package/agent/gateway/platforms/signal.py +8 -0
  552. package/agent/gateway/platforms/slack.py +308 -12
  553. package/agent/gateway/platforms/telegram.py +831 -24
  554. package/agent/gateway/platforms/webhook.py +109 -21
  555. package/agent/gateway/platforms/weixin.py +113 -2
  556. package/agent/gateway/platforms/whatsapp.py +94 -288
  557. package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
  558. package/agent/gateway/platforms/whatsapp_common.py +367 -0
  559. package/agent/gateway/platforms/yuanbao.py +608 -191
  560. package/agent/gateway/platforms/yuanbao_proto.py +232 -23
  561. package/agent/gateway/relay/__init__.py +375 -0
  562. package/agent/gateway/relay/adapter.py +222 -0
  563. package/agent/gateway/relay/auth.py +168 -0
  564. package/agent/gateway/relay/descriptor.py +118 -0
  565. package/agent/gateway/relay/transport.py +101 -0
  566. package/agent/gateway/relay/ws_transport.py +327 -0
  567. package/agent/gateway/response_filters.py +53 -0
  568. package/agent/gateway/rich_sent_store.py +80 -0
  569. package/agent/gateway/run.py +2940 -5001
  570. package/agent/gateway/session.py +109 -8
  571. package/agent/gateway/session_context.py +22 -4
  572. package/agent/gateway/slash_commands.py +3854 -0
  573. package/agent/gateway/status.py +141 -21
  574. package/agent/gateway/stream_consumer.py +288 -31
  575. package/agent/hermes-already-has-routines.md +1 -1
  576. package/agent/hermes_cli/__init__.py +62 -17
  577. package/agent/hermes_cli/_parser.py +30 -0
  578. package/agent/hermes_cli/_subprocess_compat.py +61 -0
  579. package/agent/hermes_cli/active_sessions.py +320 -0
  580. package/agent/hermes_cli/auth.py +707 -59
  581. package/agent/hermes_cli/auth_commands.py +39 -22
  582. package/agent/hermes_cli/backup.py +109 -7
  583. package/agent/hermes_cli/banner.py +88 -0
  584. package/agent/hermes_cli/blueprint_cmd.py +318 -0
  585. package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
  586. package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
  587. package/agent/hermes_cli/commands.py +215 -91
  588. package/agent/hermes_cli/config.py +967 -130
  589. package/agent/hermes_cli/container_boot.py +76 -11
  590. package/agent/hermes_cli/cron.py +5 -11
  591. package/agent/hermes_cli/curator.py +21 -0
  592. package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
  593. package/agent/hermes_cli/dashboard_auth/base.py +62 -0
  594. package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
  595. package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
  596. package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
  597. package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
  598. package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
  599. package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
  600. package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
  601. package/agent/hermes_cli/dashboard_register.py +427 -0
  602. package/agent/hermes_cli/debug.py +155 -50
  603. package/agent/hermes_cli/doctor.py +255 -14
  604. package/agent/hermes_cli/dump.py +60 -6
  605. package/agent/hermes_cli/env_loader.py +33 -0
  606. package/agent/hermes_cli/gateway.py +755 -103
  607. package/agent/hermes_cli/gateway_enroll.py +250 -0
  608. package/agent/hermes_cli/gateway_windows.py +254 -11
  609. package/agent/hermes_cli/gui_uninstall.py +285 -0
  610. package/agent/hermes_cli/inventory.py +105 -4
  611. package/agent/hermes_cli/kanban.py +58 -71
  612. package/agent/hermes_cli/kanban_db.py +391 -14
  613. package/agent/hermes_cli/kanban_decompose.py +2 -2
  614. package/agent/hermes_cli/kanban_specify.py +3 -1
  615. package/agent/hermes_cli/logs.py +2 -0
  616. package/agent/hermes_cli/main.py +2889 -5287
  617. package/agent/hermes_cli/managed_scope.py +214 -0
  618. package/agent/hermes_cli/managed_uv.py +254 -0
  619. package/agent/hermes_cli/mcp_catalog.py +6 -3
  620. package/agent/hermes_cli/mcp_config.py +145 -21
  621. package/agent/hermes_cli/mcp_security.py +96 -0
  622. package/agent/hermes_cli/mcp_startup.py +32 -3
  623. package/agent/hermes_cli/memory_providers.py +149 -0
  624. package/agent/hermes_cli/memory_setup.py +97 -42
  625. package/agent/hermes_cli/middleware.py +313 -0
  626. package/agent/hermes_cli/model_catalog.py +31 -0
  627. package/agent/hermes_cli/model_cost_guard.py +134 -0
  628. package/agent/hermes_cli/model_normalize.py +2 -1
  629. package/agent/hermes_cli/model_setup_flows.py +2759 -0
  630. package/agent/hermes_cli/model_switch.py +242 -27
  631. package/agent/hermes_cli/models.py +284 -44
  632. package/agent/hermes_cli/nous_account.py +33 -6
  633. package/agent/hermes_cli/nous_billing.py +406 -0
  634. package/agent/hermes_cli/nous_subscription.py +202 -5
  635. package/agent/hermes_cli/platforms.py +1 -0
  636. package/agent/hermes_cli/plugins.py +218 -18
  637. package/agent/hermes_cli/plugins_cmd.py +249 -105
  638. package/agent/hermes_cli/portal_cli.py +56 -16
  639. package/agent/hermes_cli/profile_distribution.py +6 -1
  640. package/agent/hermes_cli/profiles.py +283 -32
  641. package/agent/hermes_cli/provider_catalog.py +170 -0
  642. package/agent/hermes_cli/providers.py +4 -1
  643. package/agent/hermes_cli/pty_bridge.py +53 -4
  644. package/agent/hermes_cli/runtime_provider.py +216 -34
  645. package/agent/hermes_cli/secret_prompt.py +4 -4
  646. package/agent/hermes_cli/secrets_cli.py +24 -0
  647. package/agent/hermes_cli/send_cmd.py +28 -2
  648. package/agent/hermes_cli/service_manager.py +166 -19
  649. package/agent/hermes_cli/session_listing.py +97 -0
  650. package/agent/hermes_cli/setup.py +158 -94
  651. package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
  652. package/agent/hermes_cli/skills_config.py +8 -2
  653. package/agent/hermes_cli/skills_hub.py +149 -7
  654. package/agent/hermes_cli/status.py +2 -2
  655. package/agent/hermes_cli/subcommands/__init__.py +18 -0
  656. package/agent/hermes_cli/subcommands/_shared.py +29 -0
  657. package/agent/hermes_cli/subcommands/acp.py +52 -0
  658. package/agent/hermes_cli/subcommands/auth.py +109 -0
  659. package/agent/hermes_cli/subcommands/backup.py +38 -0
  660. package/agent/hermes_cli/subcommands/claw.py +92 -0
  661. package/agent/hermes_cli/subcommands/config.py +49 -0
  662. package/agent/hermes_cli/subcommands/cron.py +163 -0
  663. package/agent/hermes_cli/subcommands/dashboard.py +143 -0
  664. package/agent/hermes_cli/subcommands/debug.py +77 -0
  665. package/agent/hermes_cli/subcommands/doctor.py +35 -0
  666. package/agent/hermes_cli/subcommands/dump.py +28 -0
  667. package/agent/hermes_cli/subcommands/gateway.py +332 -0
  668. package/agent/hermes_cli/subcommands/gui.py +63 -0
  669. package/agent/hermes_cli/subcommands/hooks.py +77 -0
  670. package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
  671. package/agent/hermes_cli/subcommands/insights.py +25 -0
  672. package/agent/hermes_cli/subcommands/login.py +78 -0
  673. package/agent/hermes_cli/subcommands/logout.py +28 -0
  674. package/agent/hermes_cli/subcommands/logs.py +78 -0
  675. package/agent/hermes_cli/subcommands/mcp.py +108 -0
  676. package/agent/hermes_cli/subcommands/memory.py +53 -0
  677. package/agent/hermes_cli/subcommands/model.py +72 -0
  678. package/agent/hermes_cli/subcommands/pairing.py +36 -0
  679. package/agent/hermes_cli/subcommands/plugins.py +94 -0
  680. package/agent/hermes_cli/subcommands/postinstall.py +23 -0
  681. package/agent/hermes_cli/subcommands/profile.py +203 -0
  682. package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
  683. package/agent/hermes_cli/subcommands/security.py +62 -0
  684. package/agent/hermes_cli/subcommands/setup.py +58 -0
  685. package/agent/hermes_cli/subcommands/skills.py +298 -0
  686. package/agent/hermes_cli/subcommands/slack.py +60 -0
  687. package/agent/hermes_cli/subcommands/status.py +28 -0
  688. package/agent/hermes_cli/subcommands/tools.py +95 -0
  689. package/agent/hermes_cli/subcommands/uninstall.py +41 -0
  690. package/agent/hermes_cli/subcommands/update.py +70 -0
  691. package/agent/hermes_cli/subcommands/version.py +18 -0
  692. package/agent/hermes_cli/subcommands/webhook.py +76 -0
  693. package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
  694. package/agent/hermes_cli/suggestions_cmd.py +153 -0
  695. package/agent/hermes_cli/telegram_managed_bot.py +358 -0
  696. package/agent/hermes_cli/tips.py +3 -4
  697. package/agent/hermes_cli/tools_config.py +155 -28
  698. package/agent/hermes_cli/uninstall.py +231 -35
  699. package/agent/hermes_cli/web_server.py +6190 -973
  700. package/agent/hermes_cli/win_pty_bridge.py +179 -0
  701. package/agent/hermes_cli/write_approval_commands.py +209 -0
  702. package/agent/hermes_constants.py +164 -33
  703. package/agent/hermes_logging.py +74 -2
  704. package/agent/hermes_state.py +919 -106
  705. package/agent/hermes_time.py +20 -0
  706. package/agent/locales/af.yaml +23 -0
  707. package/agent/locales/de.yaml +23 -0
  708. package/agent/locales/en.yaml +20 -0
  709. package/agent/locales/es.yaml +23 -0
  710. package/agent/locales/fr.yaml +23 -0
  711. package/agent/locales/ga.yaml +23 -0
  712. package/agent/locales/hu.yaml +23 -0
  713. package/agent/locales/it.yaml +23 -0
  714. package/agent/locales/ja.yaml +23 -0
  715. package/agent/locales/ko.yaml +23 -0
  716. package/agent/locales/pt.yaml +23 -0
  717. package/agent/locales/ru.yaml +23 -0
  718. package/agent/locales/tr.yaml +23 -0
  719. package/agent/locales/uk.yaml +23 -0
  720. package/agent/locales/zh-hant.yaml +23 -0
  721. package/agent/locales/zh.yaml +23 -0
  722. package/agent/model_tools.py +204 -40
  723. package/agent/optional-mcps/clawpump/manifest.yaml +4 -2
  724. package/agent/optional-mcps/clawpump-stdio/manifest.yaml +2 -0
  725. package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
  726. package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
  727. package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
  728. package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
  729. package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
  730. package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
  731. package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
  732. package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
  733. package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
  734. package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
  735. package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
  736. package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
  737. package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
  738. package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
  739. package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
  740. package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
  741. package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
  742. package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
  743. package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
  744. package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
  745. package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
  746. package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
  747. package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
  748. package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
  749. package/agent/optional-skills/security/1password/SKILL.md +1 -1
  750. package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
  751. package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
  752. package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
  753. package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
  754. package/agent/package-lock.json +4082 -7907
  755. package/agent/package.json +18 -3
  756. package/agent/plugins/browser/firecrawl/provider.py +4 -1
  757. package/agent/plugins/cron/__init__.py +344 -0
  758. package/agent/plugins/cron/chronos/__init__.py +241 -0
  759. package/agent/plugins/cron/chronos/_nas_client.py +123 -0
  760. package/agent/plugins/cron/chronos/plugin.yaml +9 -0
  761. package/agent/plugins/cron/chronos/verify.py +103 -0
  762. package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
  763. package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
  764. package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
  765. package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
  766. package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
  767. package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
  768. package/agent/plugins/google_meet/audio_bridge.py +4 -0
  769. package/agent/plugins/google_meet/meet_bot.py +7 -1
  770. package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
  771. package/agent/plugins/image_gen/fal/__init__.py +35 -6
  772. package/agent/plugins/image_gen/krea/__init__.py +56 -13
  773. package/agent/plugins/image_gen/openai/__init__.py +122 -24
  774. package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
  775. package/agent/plugins/image_gen/xai/__init__.py +92 -12
  776. package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
  777. package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
  778. package/agent/plugins/memory/__init__.py +48 -5
  779. package/agent/plugins/memory/byterover/__init__.py +1 -0
  780. package/agent/plugins/memory/hindsight/README.md +1 -1
  781. package/agent/plugins/memory/hindsight/__init__.py +138 -24
  782. package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
  783. package/agent/plugins/memory/honcho/README.md +13 -10
  784. package/agent/plugins/memory/honcho/cli.py +247 -122
  785. package/agent/plugins/memory/honcho/client.py +112 -102
  786. package/agent/plugins/memory/openviking/README.md +12 -1
  787. package/agent/plugins/memory/openviking/__init__.py +2281 -107
  788. package/agent/plugins/memory/openviking/plugin.yaml +1 -2
  789. package/agent/plugins/memory/supermemory/README.md +22 -10
  790. package/agent/plugins/memory/supermemory/__init__.py +142 -37
  791. package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
  792. package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
  793. package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
  794. package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
  795. package/agent/plugins/model-providers/custom/__init__.py +8 -2
  796. package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
  797. package/agent/plugins/model-providers/minimax/__init__.py +60 -8
  798. package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
  799. package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
  800. package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
  801. package/agent/plugins/model-providers/zai/__init__.py +1 -0
  802. package/agent/plugins/observability/langfuse/__init__.py +147 -14
  803. package/agent/plugins/observability/nemo_relay/README.md +559 -0
  804. package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
  805. package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
  806. package/agent/plugins/platforms/discord/adapter.py +932 -61
  807. package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
  808. package/agent/plugins/platforms/google_chat/adapter.py +9 -3
  809. package/agent/plugins/platforms/google_chat/oauth.py +1 -1
  810. package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
  811. package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
  812. package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
  813. package/agent/plugins/platforms/irc/adapter.py +4 -1
  814. package/agent/plugins/platforms/line/adapter.py +16 -1
  815. package/agent/plugins/platforms/mattermost/adapter.py +100 -24
  816. package/agent/plugins/platforms/photon/README.md +179 -0
  817. package/agent/plugins/platforms/photon/__init__.py +4 -0
  818. package/agent/plugins/platforms/photon/adapter.py +1586 -0
  819. package/agent/plugins/platforms/photon/auth.py +1046 -0
  820. package/agent/plugins/platforms/photon/cli.py +439 -0
  821. package/agent/plugins/platforms/photon/plugin.yaml +88 -0
  822. package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
  823. package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
  824. package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
  825. package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
  826. package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
  827. package/agent/plugins/platforms/raft/__init__.py +3 -0
  828. package/agent/plugins/platforms/raft/adapter.py +774 -0
  829. package/agent/plugins/platforms/raft/plugin.yaml +19 -0
  830. package/agent/plugins/platforms/simplex/adapter.py +777 -220
  831. package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
  832. package/agent/plugins/platforms/teams/adapter.py +175 -5
  833. package/agent/plugins/plugin_utils.py +135 -0
  834. package/agent/plugins/video_gen/fal/__init__.py +10 -3
  835. package/agent/plugins/web/searxng/provider.py +15 -2
  836. package/agent/plugins/web/xai/provider.py +2 -2
  837. package/agent/providers/base.py +22 -3
  838. package/agent/pyproject.toml +115 -21
  839. package/agent/run_agent.py +733 -39
  840. package/agent/scripts/build_skills_index.py +51 -19
  841. package/agent/scripts/check_subprocess_stdin.py +177 -0
  842. package/agent/scripts/contributor_audit.py +2 -0
  843. package/agent/scripts/docker_config_migrate.py +67 -0
  844. package/agent/scripts/install.cmd +3 -3
  845. package/agent/scripts/install.ps1 +580 -154
  846. package/agent/scripts/install.sh +402 -185
  847. package/agent/scripts/lib/node-bootstrap.sh +39 -4
  848. package/agent/scripts/release.py +183 -0
  849. package/agent/scripts/run_tests.sh +1 -0
  850. package/agent/scripts/run_tests_parallel.py +18 -23
  851. package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
  852. package/agent/setup.py +59 -0
  853. package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
  854. package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
  855. package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
  856. package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
  857. package/agent/skills/clawpump/SKILL.md +4 -1
  858. package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
  859. package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
  860. package/agent/skills/github/github-auth/SKILL.md +2 -2
  861. package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
  862. package/agent/skills/github/github-code-review/SKILL.md +2 -2
  863. package/agent/skills/github/github-issues/SKILL.md +2 -2
  864. package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
  865. package/agent/skills/github/github-repo-management/SKILL.md +2 -2
  866. package/agent/skills/media/gif-search/SKILL.md +1 -1
  867. package/agent/skills/media/youtube-content/SKILL.md +10 -7
  868. package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
  869. package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
  870. package/agent/skills/productivity/airtable/SKILL.md +2 -2
  871. package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
  872. package/agent/skills/productivity/notion/SKILL.md +2 -2
  873. package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
  874. package/agent/skills/research/llm-wiki/SKILL.md +1 -1
  875. package/agent/skills/social-media/xurl/SKILL.md +9 -0
  876. package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
  877. package/agent/skills/software-development/plan/SKILL.md +285 -5
  878. package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
  879. package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
  880. package/agent/skills/software-development/spike/SKILL.md +2 -2
  881. package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
  882. package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
  883. package/agent/tools/approval.py +302 -4
  884. package/agent/tools/async_delegation.py +386 -0
  885. package/agent/tools/blueprints.py +325 -0
  886. package/agent/tools/browser_cdp_tool.py +3 -3
  887. package/agent/tools/browser_tool.py +34 -6
  888. package/agent/tools/checkpoint_manager.py +31 -1
  889. package/agent/tools/clarify_tool.py +55 -5
  890. package/agent/tools/code_execution_tool.py +31 -14
  891. package/agent/tools/computer_use/cua_backend.py +81 -3
  892. package/agent/tools/computer_use/tool.py +79 -5
  893. package/agent/tools/computer_use/vision_routing.py +55 -3
  894. package/agent/tools/credential_files.py +31 -12
  895. package/agent/tools/cronjob_tools.py +30 -20
  896. package/agent/tools/delegate_tool.py +356 -31
  897. package/agent/tools/env_probe.py +1 -0
  898. package/agent/tools/environments/docker.py +163 -8
  899. package/agent/tools/environments/file_sync.py +2 -1
  900. package/agent/tools/environments/local.py +74 -23
  901. package/agent/tools/environments/singularity.py +4 -1
  902. package/agent/tools/environments/ssh.py +78 -11
  903. package/agent/tools/file_operations.py +277 -41
  904. package/agent/tools/file_tools.py +166 -28
  905. package/agent/tools/image_generation_tool.py +515 -29
  906. package/agent/tools/kanban_tools.py +99 -0
  907. package/agent/tools/lazy_deps.py +33 -2
  908. package/agent/tools/mcp_oauth.py +5 -5
  909. package/agent/tools/mcp_oauth_manager.py +7 -5
  910. package/agent/tools/mcp_tool.py +840 -33
  911. package/agent/tools/memory_tool.py +335 -38
  912. package/agent/tools/osv_check.py +15 -1
  913. package/agent/tools/process_registry.py +155 -11
  914. package/agent/tools/read_extract.py +248 -0
  915. package/agent/tools/read_terminal_tool.py +93 -0
  916. package/agent/tools/schema_sanitizer.py +38 -0
  917. package/agent/tools/send_message_tool.py +163 -49
  918. package/agent/tools/session_search_tool.py +189 -7
  919. package/agent/tools/skill_manager_tool.py +202 -3
  920. package/agent/tools/skill_usage.py +52 -4
  921. package/agent/tools/skills_hub.py +184 -44
  922. package/agent/tools/skills_sync.py +232 -5
  923. package/agent/tools/skills_tool.py +125 -11
  924. package/agent/tools/terminal_tool.py +148 -26
  925. package/agent/tools/tirith_security.py +2 -0
  926. package/agent/tools/todo_tool.py +32 -1
  927. package/agent/tools/transcription_tools.py +13 -5
  928. package/agent/tools/tts_tool.py +332 -38
  929. package/agent/tools/url_safety.py +52 -1
  930. package/agent/tools/vision_tools.py +124 -39
  931. package/agent/tools/voice_mode.py +4 -3
  932. package/agent/tools/web_tools.py +45 -15
  933. package/agent/tools/write_approval.py +493 -0
  934. package/agent/toolsets.py +34 -10
  935. package/agent/trajectory_compressor.py +81 -10
  936. package/agent/tui_gateway/entry.py +43 -6
  937. package/agent/tui_gateway/server.py +3335 -330
  938. package/agent/tui_gateway/slash_worker.py +61 -0
  939. package/agent/tui_gateway/ws.py +67 -9
  940. package/agent/ui-tui/eslint.config.mjs +0 -4
  941. package/agent/ui-tui/package.json +6 -6
  942. package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
  943. package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
  944. package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
  945. package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
  946. package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
  947. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
  948. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
  949. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
  950. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
  951. package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
  952. package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
  953. package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
  954. package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
  955. package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
  956. package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
  957. package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
  958. package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
  959. package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
  960. package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
  961. package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
  962. package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
  963. package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
  964. package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
  965. package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
  966. package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
  967. package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
  968. package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
  969. package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
  970. package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
  971. package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
  972. package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
  973. package/agent/ui-tui/src/app/interfaces.ts +64 -1
  974. package/agent/ui-tui/src/app/overlayStore.ts +18 -2
  975. package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
  976. package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
  977. package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
  978. package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
  979. package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
  980. package/agent/ui-tui/src/app/slash/registry.ts +4 -0
  981. package/agent/ui-tui/src/app/turnController.ts +145 -2
  982. package/agent/ui-tui/src/app/uiStore.ts +2 -0
  983. package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
  984. package/agent/ui-tui/src/app/useMainApp.ts +54 -8
  985. package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
  986. package/agent/ui-tui/src/app/useSubmission.ts +23 -31
  987. package/agent/ui-tui/src/components/appChrome.tsx +112 -5
  988. package/agent/ui-tui/src/components/appLayout.tsx +9 -0
  989. package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
  990. package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
  991. package/agent/ui-tui/src/components/branding.tsx +15 -3
  992. package/agent/ui-tui/src/components/messageLine.tsx +25 -3
  993. package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
  994. package/agent/ui-tui/src/components/prompts.tsx +31 -17
  995. package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
  996. package/agent/ui-tui/src/components/textInput.tsx +16 -0
  997. package/agent/ui-tui/src/config/env.ts +12 -0
  998. package/agent/ui-tui/src/config/limits.ts +13 -0
  999. package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
  1000. package/agent/ui-tui/src/domain/paths.ts +24 -0
  1001. package/agent/ui-tui/src/domain/slash.ts +40 -0
  1002. package/agent/ui-tui/src/entry.tsx +35 -4
  1003. package/agent/ui-tui/src/gatewayClient.ts +22 -10
  1004. package/agent/ui-tui/src/gatewayTypes.ts +130 -1
  1005. package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
  1006. package/agent/ui-tui/src/lib/memory.test.ts +162 -0
  1007. package/agent/ui-tui/src/lib/memory.ts +60 -1
  1008. package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
  1009. package/agent/ui-tui/src/lib/osc52.ts +1 -1
  1010. package/agent/ui-tui/src/lib/text.test.ts +32 -1
  1011. package/agent/ui-tui/src/lib/text.ts +29 -2
  1012. package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
  1013. package/agent/ui-tui/src/types.ts +5 -0
  1014. package/agent/ui-tui/tsconfig.build.json +0 -1
  1015. package/agent/ui-tui/tsconfig.json +2 -1
  1016. package/agent/utils.py +66 -2
  1017. package/agent/uv.lock +300 -684
  1018. package/agent/web/index.html +2 -2
  1019. package/agent/web/package.json +11 -6
  1020. package/agent/web/public/claw-bg.webp +0 -0
  1021. package/agent/web/public/claw-logo.webp +0 -0
  1022. package/agent/web/src/App.tsx +138 -48
  1023. package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
  1024. package/agent/web/src/components/Backdrop.tsx +15 -0
  1025. package/agent/web/src/components/ChatSessionList.tsx +260 -0
  1026. package/agent/web/src/components/ChatSidebar.tsx +262 -78
  1027. package/agent/web/src/components/ConfirmDialog.tsx +122 -0
  1028. package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
  1029. package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
  1030. package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
  1031. package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
  1032. package/agent/web/src/components/ReasoningPicker.tsx +167 -0
  1033. package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
  1034. package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
  1035. package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
  1036. package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
  1037. package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
  1038. package/agent/web/src/contexts/SystemActions.tsx +6 -8
  1039. package/agent/web/src/contexts/profile-context.ts +19 -0
  1040. package/agent/web/src/contexts/useProfileScope.ts +6 -0
  1041. package/agent/web/src/i18n/af.ts +5 -4
  1042. package/agent/web/src/i18n/de.ts +5 -4
  1043. package/agent/web/src/i18n/en.ts +58 -4
  1044. package/agent/web/src/i18n/es.ts +5 -3
  1045. package/agent/web/src/i18n/fr.ts +5 -3
  1046. package/agent/web/src/i18n/ga.ts +5 -4
  1047. package/agent/web/src/i18n/hu.ts +5 -4
  1048. package/agent/web/src/i18n/it.ts +5 -4
  1049. package/agent/web/src/i18n/ja.ts +5 -4
  1050. package/agent/web/src/i18n/ko.ts +5 -4
  1051. package/agent/web/src/i18n/pt.ts +5 -3
  1052. package/agent/web/src/i18n/ru.ts +5 -4
  1053. package/agent/web/src/i18n/tr.ts +5 -4
  1054. package/agent/web/src/i18n/types.ts +59 -1
  1055. package/agent/web/src/i18n/uk.ts +5 -3
  1056. package/agent/web/src/i18n/zh-hant.ts +5 -4
  1057. package/agent/web/src/i18n/zh.ts +5 -4
  1058. package/agent/web/src/index.css +2 -2
  1059. package/agent/web/src/lib/api.ts +819 -52
  1060. package/agent/web/src/lib/dashboard-flags.ts +16 -7
  1061. package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
  1062. package/agent/web/src/lib/reasoning-effort.ts +36 -0
  1063. package/agent/web/src/lib/session-refresh.test.ts +21 -0
  1064. package/agent/web/src/lib/session-refresh.ts +26 -0
  1065. package/agent/web/src/pages/ChannelsPage.tsx +529 -68
  1066. package/agent/web/src/pages/ChatPage.tsx +249 -56
  1067. package/agent/web/src/pages/ConfigPage.tsx +11 -1
  1068. package/agent/web/src/pages/CronPage.tsx +219 -31
  1069. package/agent/web/src/pages/EnvPage.tsx +25 -6
  1070. package/agent/web/src/pages/FilesPage.tsx +525 -0
  1071. package/agent/web/src/pages/McpPage.tsx +80 -3
  1072. package/agent/web/src/pages/ModelsPage.tsx +97 -12
  1073. package/agent/web/src/pages/PluginsPage.tsx +1 -1
  1074. package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
  1075. package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
  1076. package/agent/web/src/pages/SessionsPage.tsx +144 -13
  1077. package/agent/web/src/pages/SkillsPage.tsx +851 -70
  1078. package/agent/web/src/pages/SystemPage.tsx +340 -4
  1079. package/agent/web/src/pages/WalletPage.tsx +401 -0
  1080. package/agent/web/src/pages/WebhooksPage.tsx +145 -15
  1081. package/agent/web/src/pages/X402Page.tsx +207 -0
  1082. package/agent/web/src/plugins/registry.ts +28 -11
  1083. package/agent/web/src/plugins/sdk.d.ts +160 -0
  1084. package/agent/web/src/themes/context.tsx +112 -5
  1085. package/agent/web/src/themes/fonts.ts +167 -0
  1086. package/agent/web/src/themes/index.ts +7 -0
  1087. package/agent/web/tsconfig.app.json +0 -1
  1088. package/agent/web/vite.config.ts +1 -8
  1089. package/agent/web/vitest.config.ts +16 -0
  1090. package/package.json +1 -1
  1091. package/agent/apps/desktop/package-lock.json +0 -18363
  1092. package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
  1093. package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
  1094. package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
  1095. package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
  1096. package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
  1097. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
  1098. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
  1099. package/agent/skills/diagramming/DESCRIPTION.md +0 -3
  1100. package/agent/skills/domain/DESCRIPTION.md +0 -24
  1101. package/agent/skills/gifs/DESCRIPTION.md +0 -3
  1102. package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
  1103. package/agent/skills/mcp/DESCRIPTION.md +0 -3
  1104. package/agent/skills/media/spotify/SKILL.md +0 -135
  1105. package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
  1106. package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
  1107. package/agent/skills/productivity/linear/SKILL.md +0 -380
  1108. package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
  1109. package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
  1110. package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
  1111. package/agent/ui-tui/package-lock.json +0 -7449
  1112. package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
  1113. package/agent/web/package-lock.json +0 -8887
  1114. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
  1115. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
  1116. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
  1117. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
  1118. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
  1119. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
  1120. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
  1121. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
  1122. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
  1123. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
  1124. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
  1125. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
  1126. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
  1127. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
  1128. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
  1129. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
  1130. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
  1131. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
  1132. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
  1133. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
  1134. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
  1135. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
  1136. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
  1137. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
  1138. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
  1139. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
  1140. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
  1141. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
  1142. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
  1143. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
  1144. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
  1145. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
  1146. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
  1147. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
  1148. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
  1149. /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
  1150. /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
  1151. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
  1152. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
  1153. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
  1154. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
  1155. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
  1156. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
  1157. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
  1158. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
  1159. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
  1160. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
  1161. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
  1162. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
  1163. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
  1164. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
  1165. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
  1166. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
  1167. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
  1168. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
  1169. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
  1170. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
  1171. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
  1172. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
  1173. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
  1174. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
  1175. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
  1176. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
  1177. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
  1178. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
  1179. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
  1180. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
  1181. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
  1182. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
  1183. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
  1184. /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
  1185. /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
  1186. /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
  1187. /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
  1188. /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
  1189. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
  1190. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
  1191. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
  1192. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
  1193. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
  1194. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
  1195. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
  1196. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
  1197. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
  1198. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
  1199. /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
  1200. /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
  1201. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
  1202. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
  1203. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
  1204. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
  1205. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
  1206. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
  1207. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
  1208. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
  1209. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
  1210. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
  1211. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
  1212. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
@@ -0,0 +1,2293 @@
1
+ """Slash-command handlers for the interactive CLI (god-file decomposition Phase 4).
2
+
3
+ This module hosts the ``_handle_*_command`` slash-command handlers lifted out of
4
+ ``cli.py``'s ``HermesCLI`` class. ``HermesCLI`` inherits ``CLICommandsMixin`` so
5
+ every ``self.<handler>`` call resolves unchanged via the MRO — behavior-neutral.
6
+
7
+ Import discipline (mirrors gateway/slash_commands.py, PR #41886):
8
+ * Neutral, non-cyclic deps are imported at module top-level below.
9
+ * cli.py-internal symbols (the ``_cprint``/``_ACCENT``/``save_config_value``…
10
+ module-level helpers and constants) are imported LAZILY inside each handler
11
+ via ``from cli import ...`` — that resolves at call time when ``cli`` is fully
12
+ loaded, so the mixin module never imports ``cli`` at top level (no cycle).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import sys
20
+ import threading
21
+ import time
22
+ import uuid
23
+ from datetime import datetime
24
+ from urllib.parse import urlparse
25
+
26
+ from rich import box as rich_box
27
+ from rich.markup import escape as _escape
28
+ from rich.panel import Panel
29
+
30
+ from hermes_constants import display_hermes_home, is_termux as _is_termux_environment
31
+ from hermes_cli.browser_connect import (
32
+ DEFAULT_BROWSER_CDP_URL,
33
+ is_browser_debug_ready,
34
+ manual_chrome_debug_command,
35
+ )
36
+
37
+
38
+ class CLICommandsMixin:
39
+ """Mixin holding the interactive-CLI slash-command handlers.
40
+
41
+ All methods use only ``self`` state plus the imports above and per-method
42
+ lazy ``from cli import ...`` lines, so they compose cleanly onto
43
+ ``HermesCLI`` via the MRO.
44
+ """
45
+
46
+ def _handle_rollback_command(self, command: str):
47
+ """Handle /rollback — list, diff, or restore filesystem checkpoints.
48
+
49
+ Syntax:
50
+ /rollback — list checkpoints
51
+ /rollback <N> — restore checkpoint N (also undoes last chat turn)
52
+ /rollback diff <N> — preview changes since checkpoint N
53
+ /rollback <N> <file> — restore a single file from checkpoint N
54
+ """
55
+ from tools.checkpoint_manager import format_checkpoint_list
56
+
57
+ if not hasattr(self, 'agent') or not self.agent:
58
+ print(" No active agent session.")
59
+ return
60
+
61
+ mgr = self.agent._checkpoint_mgr
62
+ if not mgr.enabled:
63
+ print(" Checkpoints are not enabled.")
64
+ print(" Enable with: hermes --checkpoints")
65
+ print(" Or in config.yaml: checkpoints: { enabled: true }")
66
+ return
67
+
68
+ cwd = os.getenv("TERMINAL_CWD", os.getcwd())
69
+ parts = command.split()
70
+ args = parts[1:] if len(parts) > 1 else []
71
+
72
+ if not args:
73
+ # List checkpoints
74
+ checkpoints = mgr.list_checkpoints(cwd)
75
+ print(format_checkpoint_list(checkpoints, cwd))
76
+ return
77
+
78
+ # Handle /rollback diff <N>
79
+ if args[0].lower() == "diff":
80
+ if len(args) < 2:
81
+ print(" Usage: /rollback diff <N>")
82
+ return
83
+ checkpoints = mgr.list_checkpoints(cwd)
84
+ if not checkpoints:
85
+ print(f" No checkpoints found for {cwd}")
86
+ return
87
+ target_hash = self._resolve_checkpoint_ref(args[1], checkpoints)
88
+ if not target_hash:
89
+ return
90
+ result = mgr.diff(cwd, target_hash)
91
+ if result["success"]:
92
+ stat = result.get("stat", "")
93
+ diff = result.get("diff", "")
94
+ if not stat and not diff:
95
+ print(" No changes since this checkpoint.")
96
+ else:
97
+ if stat:
98
+ print(f"\n{stat}")
99
+ if diff:
100
+ # Limit diff output to avoid terminal flood
101
+ diff_lines = diff.splitlines()
102
+ if len(diff_lines) > 80:
103
+ print("\n".join(diff_lines[:80]))
104
+ print(f"\n ... ({len(diff_lines) - 80} more lines, showing first 80)")
105
+ else:
106
+ print(f"\n{diff}")
107
+ else:
108
+ print(f" ❌ {result['error']}")
109
+ return
110
+
111
+ # Resolve checkpoint reference (number or hash)
112
+ checkpoints = mgr.list_checkpoints(cwd)
113
+ if not checkpoints:
114
+ print(f" No checkpoints found for {cwd}")
115
+ return
116
+
117
+ target_hash = self._resolve_checkpoint_ref(args[0], checkpoints)
118
+ if not target_hash:
119
+ return
120
+
121
+ # Check for file-level restore: /rollback <N> <file>
122
+ file_path = args[1] if len(args) > 1 else None
123
+
124
+ result = mgr.restore(cwd, target_hash, file_path=file_path)
125
+ if result["success"]:
126
+ if file_path:
127
+ print(f" ✅ Restored {file_path} from checkpoint {result['restored_to']}: {result['reason']}")
128
+ else:
129
+ print(f" ✅ Restored to checkpoint {result['restored_to']}: {result['reason']}")
130
+ print(" A pre-rollback snapshot was saved automatically.")
131
+
132
+ # Also undo the last conversation turn so the agent's context
133
+ # matches the restored filesystem state
134
+ if self.conversation_history:
135
+ self.undo_last(prefill=False)
136
+ print(" Chat turn undone to match restored file state.")
137
+ else:
138
+ print(f" ❌ {result['error']}")
139
+
140
+ def _handle_snapshot_command(self, command: str):
141
+ """Handle /snapshot — lightweight state snapshots for Hermes config/state.
142
+
143
+ Syntax:
144
+ /snapshot — list recent snapshots
145
+ /snapshot create [label] — create a snapshot
146
+ /snapshot restore <id> — restore state from snapshot
147
+ /snapshot prune [N] — prune to N snapshots (default 20)
148
+ """
149
+ from hermes_cli.backup import (
150
+ create_quick_snapshot, list_quick_snapshots,
151
+ restore_quick_snapshot, prune_quick_snapshots,
152
+ )
153
+ from hermes_constants import display_hermes_home
154
+
155
+ parts = command.split()
156
+ subcmd = parts[1].lower() if len(parts) > 1 else "list"
157
+
158
+ if subcmd in {"list", "ls"}:
159
+ snaps = list_quick_snapshots()
160
+ if not snaps:
161
+ print(" No state snapshots yet.")
162
+ print(" Create one: /snapshot create [label]")
163
+ return
164
+ print(f" State snapshots ({display_hermes_home()}/state-snapshots/):\n")
165
+ print(f" {'#':>3} {'ID':<35} {'Files':>5} {'Size':>10} {'Label'}")
166
+ print(f" {'─'*3} {'─'*35} {'─'*5} {'─'*10} {'─'*20}")
167
+ for i, s in enumerate(snaps, 1):
168
+ size = s.get("total_size", 0)
169
+ if size < 1024:
170
+ size_str = f"{size} B"
171
+ elif size < 1024 * 1024:
172
+ size_str = f"{size / 1024:.0f} KB"
173
+ else:
174
+ size_str = f"{size / 1024 / 1024:.1f} MB"
175
+ label = s.get("label") or ""
176
+ print(f" {i:3} {s['id']:<35} {s.get('file_count', 0):>5} {size_str:>10} {label}")
177
+
178
+ elif subcmd == "create":
179
+ label = " ".join(parts[2:]) if len(parts) > 2 else None
180
+ snap_id = create_quick_snapshot(label=label)
181
+ if snap_id:
182
+ print(f" Snapshot created: {snap_id}")
183
+ else:
184
+ print(" No state files found to snapshot.")
185
+
186
+ elif subcmd in {"restore", "rewind"}:
187
+ if len(parts) < 3:
188
+ print(" Usage: /snapshot restore <snapshot-id>")
189
+ # Show hint with most recent snapshot
190
+ snaps = list_quick_snapshots(limit=1)
191
+ if snaps:
192
+ print(f" Most recent: {snaps[0]['id']}")
193
+ return
194
+ snap_id = parts[2]
195
+ # Allow restore by number (1-indexed)
196
+ try:
197
+ idx = int(snap_id)
198
+ snaps = list_quick_snapshots()
199
+ if 1 <= idx <= len(snaps):
200
+ snap_id = snaps[idx - 1]["id"]
201
+ else:
202
+ print(f" Invalid snapshot number. Use 1-{len(snaps)}.")
203
+ return
204
+ except ValueError:
205
+ pass
206
+ if restore_quick_snapshot(snap_id):
207
+ print(f" Restored state from: {snap_id}")
208
+ print(" Restart recommended for state.db changes to take effect.")
209
+ else:
210
+ print(f" Snapshot not found: {snap_id}")
211
+
212
+ elif subcmd == "prune":
213
+ keep = 20
214
+ if len(parts) > 2:
215
+ try:
216
+ keep = int(parts[2])
217
+ except ValueError:
218
+ print(" Usage: /snapshot prune [keep-count]")
219
+ return
220
+ deleted = prune_quick_snapshots(keep=keep)
221
+ print(f" Pruned {deleted} old snapshot(s) (keeping {keep}).")
222
+
223
+ else:
224
+ print(f" Unknown subcommand: {subcmd}")
225
+ print(" Usage: /snapshot [list|create [label]|restore <id>|prune [N]]")
226
+
227
+ def _handle_stop_command(self):
228
+ """Handle /stop — kill all running background processes and
229
+ background (async) delegations.
230
+
231
+ Inspired by OpenAI Codex's separation of interrupt (stop current turn)
232
+ from /stop (clean up background processes). See openai/codex#14602.
233
+ """
234
+ from tools.process_registry import process_registry
235
+
236
+ processes = process_registry.list_sessions()
237
+ running = [p for p in processes if p.get("status") == "running"]
238
+
239
+ # Background subagents dispatched via delegate_task(background=true)
240
+ # live in their own registry, not the process registry.
241
+ try:
242
+ from tools.async_delegation import active_count, interrupt_all
243
+ n_async = active_count()
244
+ except Exception:
245
+ n_async = 0
246
+ interrupt_all = None
247
+
248
+ if not running and not n_async:
249
+ print(" No running background processes.")
250
+ return
251
+
252
+ if running:
253
+ print(f" Stopping {len(running)} background process(es)...")
254
+ killed = process_registry.kill_all()
255
+ print(f" ✅ Stopped {killed} process(es).")
256
+ if n_async and interrupt_all is not None:
257
+ stopped = interrupt_all(reason="/stop")
258
+ print(f" ✅ Interrupted {stopped} background delegation(s).")
259
+
260
+ def _handle_agents_command(self):
261
+ """Handle /agents — show background processes and agent status."""
262
+ from cli import _cprint
263
+ from tools.process_registry import format_uptime_short, process_registry
264
+
265
+ processes = process_registry.list_sessions()
266
+ running = [p for p in processes if p.get("status") == "running"]
267
+ finished = [p for p in processes if p.get("status") != "running"]
268
+
269
+ _cprint(f" Running processes: {len(running)}")
270
+ for p in running:
271
+ cmd = p.get("command", "")[:80]
272
+ up = format_uptime_short(p.get("uptime_seconds", 0))
273
+ _cprint(f" {p.get('session_id', '?')} · {up} · {cmd}")
274
+
275
+ if finished:
276
+ _cprint(f" Recently finished: {len(finished)}")
277
+
278
+ # Background (async) delegations — delegate_task(background=true)
279
+ try:
280
+ from tools.async_delegation import list_async_delegations
281
+ delegations = list_async_delegations()
282
+ except Exception:
283
+ delegations = []
284
+ running_d = [d for d in delegations if d.get("status") == "running"]
285
+ if delegations:
286
+ _cprint(f" Background delegations: {len(running_d)} running")
287
+ for d in delegations:
288
+ goal = (d.get("goal") or "")[:60]
289
+ _cprint(
290
+ f" {d.get('delegation_id', '?')} · "
291
+ f"{d.get('status', '?')} · {goal}"
292
+ )
293
+
294
+ agent_running = getattr(self, "_agent_running", False)
295
+ _cprint(f" Agent: {'running' if agent_running else 'idle'}")
296
+
297
+ def _handle_paste_command(self):
298
+ """Handle /paste — explicitly check clipboard for an image.
299
+
300
+ This is the reliable fallback for terminals where BracketedPaste
301
+ doesn't fire for image-only clipboard content (e.g., VSCode terminal,
302
+ Windows Terminal with WSL2).
303
+ """
304
+ from cli import _DIM, _RST, _cprint, _termux_example_image_path
305
+ if _is_termux_environment():
306
+ _cprint(
307
+ f" {_DIM}Clipboard image paste is not available on Termux — "
308
+ f"use /image <path> or paste a local image path like "
309
+ f"{_termux_example_image_path()}{_RST}"
310
+ )
311
+ return
312
+
313
+ from hermes_cli.clipboard import has_clipboard_image
314
+ if has_clipboard_image():
315
+ if self._try_attach_clipboard_image():
316
+ n = len(self._attached_images)
317
+ _cprint(f" 📎 Image #{n} attached from clipboard")
318
+ else:
319
+ _cprint(f" {_DIM}(>_<) Clipboard has an image but extraction failed{_RST}")
320
+ else:
321
+ _cprint(f" {_DIM}(._.) No image found in clipboard{_RST}")
322
+
323
+ def _handle_copy_command(self, cmd_original: str) -> None:
324
+ """Handle /copy [number] — copy assistant output to clipboard."""
325
+ from cli import _assistant_copy_text, _cprint
326
+ parts = cmd_original.split(maxsplit=1)
327
+ arg = parts[1].strip() if len(parts) > 1 else ""
328
+
329
+ assistant = [m for m in self.conversation_history if m.get("role") == "assistant"]
330
+ if not assistant:
331
+ _cprint(" Nothing to copy yet.")
332
+ return
333
+
334
+ if arg:
335
+ try:
336
+ idx = int(arg) - 1
337
+ except ValueError:
338
+ _cprint(" Usage: /copy [number]")
339
+ return
340
+ if idx < 0 or idx >= len(assistant):
341
+ _cprint(f" Invalid response number. Use 1-{len(assistant)}.")
342
+ return
343
+ else:
344
+ idx = len(assistant) - 1
345
+ while idx >= 0 and not _assistant_copy_text(assistant[idx].get("content")):
346
+ idx -= 1
347
+ if idx < 0:
348
+ _cprint(" Nothing to copy in assistant responses yet.")
349
+ return
350
+
351
+ text = _assistant_copy_text(assistant[idx].get("content"))
352
+ if not text:
353
+ _cprint(" Nothing to copy in that assistant response.")
354
+ return
355
+
356
+ try:
357
+ self._write_osc52_clipboard(text)
358
+ _cprint(f" Copied assistant response #{idx + 1} to clipboard")
359
+ except Exception as e:
360
+ _cprint(f" Clipboard copy failed: {e}")
361
+
362
+ def _handle_image_command(self, cmd_original: str):
363
+ """Handle /image <path> — attach a local image file for the next prompt."""
364
+ from cli import _DIM, _IMAGE_EXTENSIONS, _RST, _cprint, _resolve_attachment_path, _split_path_input, _termux_example_image_path
365
+ raw_args = (cmd_original.split(None, 1)[1].strip() if " " in cmd_original else "")
366
+ if not raw_args:
367
+ hint = _termux_example_image_path() if _is_termux_environment() else "/path/to/image.png"
368
+ _cprint(f" {_DIM}Usage: /image <path> e.g. /image {hint}{_RST}")
369
+ return
370
+
371
+ path_token, _remainder = _split_path_input(raw_args)
372
+ image_path = _resolve_attachment_path(path_token)
373
+ if image_path is None:
374
+ _cprint(f" {_DIM}(>_<) File not found: {path_token}{_RST}")
375
+ return
376
+ if image_path.suffix.lower() not in _IMAGE_EXTENSIONS:
377
+ _cprint(f" {_DIM}(._.) Not a supported image file: {image_path.name}{_RST}")
378
+ return
379
+
380
+ self._attached_images.append(image_path)
381
+ _cprint(f" 📎 Attached image: {image_path.name}")
382
+ if _remainder:
383
+ _cprint(f" {_DIM}Now type your prompt (or use --image in single-query mode): {_remainder}{_RST}")
384
+ elif _is_termux_environment():
385
+ _cprint(f" {_DIM}Tip: type your next message, or run hermes chat -q --image {_termux_example_image_path(image_path.name)} \"What do you see?\"{_RST}")
386
+
387
+ def _handle_tools_command(self, cmd: str):
388
+ """Handle /tools [list|disable|enable] slash commands.
389
+
390
+ /tools (no args) shows the tool list.
391
+ /tools list shows enabled/disabled status per toolset.
392
+ /tools disable/enable saves the change to config and resets
393
+ the session so the new tool set takes effect cleanly (no
394
+ prompt-cache breakage mid-conversation).
395
+ """
396
+ from cli import _ACCENT, _DIM, _RST, _cprint
397
+ import shlex
398
+ from argparse import Namespace
399
+ from contextlib import redirect_stdout
400
+ from io import StringIO
401
+ from hermes_cli.tools_config import tools_disable_enable_command
402
+
403
+ def _run_capture(ns: Namespace) -> None:
404
+ """Run tools_disable_enable_command, routing its ANSI-colored
405
+ print() output through _cprint when inside the interactive TUI
406
+ so escapes aren't mangled by patch_stdout's StdoutProxy into
407
+ garbled '?[32m...?[0m' text.
408
+
409
+ Outside the TUI (standalone mode, tests), call straight through
410
+ so real stdout / pytest capture works as expected.
411
+ """
412
+ # Standalone/tests, run as usual
413
+ if getattr(self, "_app", None) is None:
414
+ tools_disable_enable_command(ns)
415
+ return
416
+
417
+ # Buffer reports isatty()=True so color() in hermes_cli/colors.py
418
+ # still emits ANSI escapes. StringIO.isatty() is False, which
419
+ # would otherwise strip all colors before we re-render them.
420
+ class _TTYBuf(StringIO):
421
+ def isatty(self) -> bool:
422
+ return True
423
+
424
+ buf = _TTYBuf()
425
+ with redirect_stdout(buf):
426
+ tools_disable_enable_command(ns)
427
+ for line in buf.getvalue().splitlines():
428
+ _cprint(line)
429
+
430
+ try:
431
+ parts = shlex.split(cmd)
432
+ except ValueError:
433
+ parts = cmd.split()
434
+
435
+ subcommand = parts[1] if len(parts) > 1 else ""
436
+ if subcommand not in {"list", "disable", "enable"}:
437
+ self.show_tools()
438
+ return
439
+
440
+ if subcommand == "list":
441
+ _run_capture(Namespace(tools_action="list", platform="cli"))
442
+ return
443
+
444
+ names = parts[2:]
445
+ if not names:
446
+ print(f"(._.) Usage: /tools {subcommand} <name> [name ...]")
447
+ print(f" Built-in toolset: /tools {subcommand} web")
448
+ print(f" MCP tool: /tools {subcommand} github:create_issue")
449
+ return
450
+
451
+ # Apply the change directly — the user typing the command is implicit
452
+ # consent. Do NOT use input() here; it hangs inside prompt_toolkit's
453
+ # TUI event loop (known pitfall).
454
+ verb = "Disabling" if subcommand == "disable" else "Enabling"
455
+ label = ", ".join(names)
456
+ _cprint(f"{_ACCENT}{verb} {label}...{_RST}")
457
+
458
+ _run_capture(Namespace(tools_action=subcommand, names=names, platform="cli"))
459
+
460
+ # Reset session so the new tool config is picked up from a clean state
461
+ from hermes_cli.tools_config import _get_platform_tools
462
+ from hermes_cli.config import load_config
463
+ self.enabled_toolsets = _get_platform_tools(load_config(), "cli")
464
+ self.new_session()
465
+ _cprint(f"{_DIM}Session reset. New tool configuration is active.{_RST}")
466
+
467
+ def _handle_profile_command(self):
468
+ """Display active profile name and home directory."""
469
+ from hermes_constants import display_hermes_home
470
+ from hermes_cli.profiles import get_active_profile_name
471
+
472
+ display = display_hermes_home()
473
+ profile_name = get_active_profile_name()
474
+
475
+ print()
476
+ print(f" Profile: {profile_name}")
477
+ print(f" Home: {display}")
478
+ print()
479
+
480
+ def _handle_handoff_command(self, cmd_original: str) -> bool:
481
+ """Handle ``/handoff <platform>`` — transfer this CLI session to a gateway platform.
482
+
483
+ Flow:
484
+ 1. Validate platform name + the gateway has a home channel for it.
485
+ 2. Reject if the agent is currently running (the in-flight turn
486
+ would race with the gateway's switch_session).
487
+ 3. Write ``handoff_state='pending'`` on this session row.
488
+ 4. Block-poll ``state.db`` for terminal state (timeout 60s).
489
+ 5. On ``completed`` → print resume hint and signal CLI exit by
490
+ returning False (the caller honors that like ``/quit``).
491
+ 6. On ``failed`` / timeout → print error and return True so the
492
+ user keeps their CLI session.
493
+
494
+ Returns:
495
+ False to signal CLI exit, True to keep going.
496
+ """
497
+ from cli import _cprint
498
+ from hermes_state import format_session_db_unavailable
499
+
500
+ parts = cmd_original.split(maxsplit=1)
501
+ if len(parts) < 2 or not parts[1].strip():
502
+ _cprint(" Usage: /handoff <platform>")
503
+ _cprint(" Hands the current session off to that platform's home channel.")
504
+ _cprint(" The CLI session ends here; resume it later with /resume.")
505
+ return True
506
+
507
+ platform_name = parts[1].strip().lower()
508
+
509
+ # Validate platform name + home channel via the live gateway config.
510
+ try:
511
+ from gateway.config import load_gateway_config, Platform
512
+ except Exception as exc: # pragma: no cover — gateway pkg always shipped
513
+ _cprint(f" Could not load gateway config: {exc}")
514
+ return True
515
+
516
+ try:
517
+ platform = Platform(platform_name)
518
+ except (ValueError, KeyError):
519
+ _cprint(f" Unknown platform '{platform_name}'.")
520
+ return True
521
+
522
+ try:
523
+ gw_config = load_gateway_config()
524
+ except Exception as exc:
525
+ _cprint(f" Could not load gateway config: {exc}")
526
+ return True
527
+
528
+ pcfg = gw_config.platforms.get(platform)
529
+ if not pcfg or not pcfg.enabled:
530
+ _cprint(f" Platform '{platform_name}' is not configured/enabled in the gateway.")
531
+ return True
532
+
533
+ home = gw_config.get_home_channel(platform)
534
+ if not home or not home.chat_id:
535
+ _cprint(f" No home channel configured for {platform_name}.")
536
+ _cprint(f" Set one with /sethome on the destination chat first.")
537
+ return True
538
+
539
+ # Refuse mid-turn: an in-flight agent run would race with the
540
+ # gateway's switch_session and the synthetic turn dispatch.
541
+ if getattr(self, "_agent_running", False):
542
+ _cprint(" Agent is busy. Wait for the current turn to finish, then retry /handoff.")
543
+ return True
544
+
545
+ # Make sure we have a SessionDB handle.
546
+ if not self._session_db:
547
+ try:
548
+ from hermes_state import SessionDB
549
+ self._session_db = SessionDB()
550
+ except Exception:
551
+ pass
552
+ if not self._session_db:
553
+ _cprint(f" {format_session_db_unavailable()}")
554
+ return True
555
+
556
+ # Make sure the session row exists in state.db. Most CLI sessions
557
+ # are written via _flush_messages_to_session_db on the first turn
558
+ # already, but if the user tries to hand off an empty session we
559
+ # still want a row to mark.
560
+ try:
561
+ row = self._session_db.get_session(self.session_id)
562
+ if not row:
563
+ # Nothing has flushed yet. Create a stub so the gateway has
564
+ # something to switch_session onto. Inserting via title-set
565
+ # is the simplest path because set_session_title's INSERT OR
566
+ # IGNORE creates the row.
567
+ placeholder_title = f"handoff-{self.session_id[:8]}"
568
+ self._session_db.set_session_title(self.session_id, placeholder_title)
569
+ except Exception as exc:
570
+ _cprint(f" Could not ensure session row in state.db: {exc}")
571
+ return True
572
+
573
+ # Display title for messaging.
574
+ session_title = ""
575
+ try:
576
+ row = self._session_db.get_session(self.session_id)
577
+ if row:
578
+ session_title = row.get("title") or ""
579
+ except Exception:
580
+ pass
581
+ if not session_title:
582
+ session_title = self.session_id[:8]
583
+
584
+ # Mark pending — gateway watcher will pick this up.
585
+ ok = self._session_db.request_handoff(self.session_id, platform_name)
586
+ if not ok:
587
+ _cprint(" Session is already in flight for handoff. Wait for it to settle, then retry.")
588
+ return True
589
+
590
+ _cprint(f" Queued handoff of '{session_title}' → {platform_name} (home: {home.name}).")
591
+ _cprint(f" Waiting for the gateway to pick it up...")
592
+
593
+ # Poll-block on terminal state. Tick every 0.5s; bail at ~60s.
594
+ import time as _time
595
+ deadline = _time.time() + 60.0
596
+ last_state = "pending"
597
+ while _time.time() < deadline:
598
+ try:
599
+ state_row = self._session_db.get_handoff_state(self.session_id)
600
+ except Exception:
601
+ state_row = None
602
+ current = (state_row or {}).get("state") or "pending"
603
+ if current != last_state:
604
+ if current == "running":
605
+ _cprint(" Gateway picked it up; transferring...")
606
+ last_state = current
607
+ if current == "completed":
608
+ _cprint("")
609
+ _cprint(f" ↻ Handoff complete. The session is now active on {platform_name}.")
610
+ _cprint(f" Resume it on this CLI later with: /resume {session_title}")
611
+ _cprint("")
612
+ # End the CLI cleanly — same exit semantics as /quit.
613
+ self._should_exit = True
614
+ return False
615
+ if current == "failed":
616
+ err = (state_row or {}).get("error") or "unknown error"
617
+ _cprint(f" Handoff failed: {err}")
618
+ _cprint(" Your CLI session is intact. Try /handoff again, or /resume on the platform manually.")
619
+ return True
620
+ _time.sleep(0.5)
621
+
622
+ # Timed out. Clear the pending flag so the user can retry.
623
+ try:
624
+ self._session_db.fail_handoff(self.session_id, "timed out waiting for gateway")
625
+ except Exception:
626
+ pass
627
+ _cprint(" Timed out waiting for the gateway. Is `hermes gateway` running?")
628
+ _cprint(" Your CLI session is intact.")
629
+ return True
630
+
631
+ def _handle_resume_command(self, cmd_original: str) -> None:
632
+ """Handle /resume <session_id_or_title> — switch to a previous session mid-conversation."""
633
+ from cli import _cprint, _sync_process_session_id
634
+ parts = cmd_original.split(None, 1)
635
+ target = parts[1].strip() if len(parts) > 1 else ""
636
+
637
+ # Strip common outer brackets/quotes users may type literally from the
638
+ # usage hint (e.g. ``/resume <abc123>`` or ``/resume [abc123]``). The
639
+ # `/resume` help text shows angle brackets as a placeholder and a few
640
+ # users copy them through verbatim. Stripping them keeps the lookup
641
+ # working without changing the help string.
642
+ if len(target) >= 2 and (
643
+ (target[0] == "<" and target[-1] == ">")
644
+ or (target[0] == "[" and target[-1] == "]")
645
+ or (target[0] == '"' and target[-1] == '"')
646
+ or (target[0] == "'" and target[-1] == "'")
647
+ ):
648
+ target = target[1:-1].strip()
649
+
650
+ if not target:
651
+ _cprint(" Usage: /resume <number|session_id_or_title>")
652
+ if self._show_recent_sessions(reason="resume"):
653
+ # Arm a one-shot pending-resume selection so the user can type
654
+ # just the number (`3`) on the next line instead of having to
655
+ # retype `/resume 3`. The list here must match the one shown by
656
+ # _show_recent_sessions and used for index resolution below —
657
+ # all three go through _list_recent_sessions(limit=10). See
658
+ # #34584.
659
+ self._pending_resume_sessions = self._list_recent_sessions(limit=10)
660
+ return
661
+ _cprint(" Tip: Use /history or `hermes sessions list` to find sessions.")
662
+ return
663
+
664
+ # Any explicit /resume <target> supersedes a previously-armed bare
665
+ # numbered prompt.
666
+ self._pending_resume_sessions = None
667
+
668
+ if not self._session_db:
669
+ from hermes_state import format_session_db_unavailable
670
+ _cprint(f" {format_session_db_unavailable()}")
671
+ return
672
+
673
+ # Resolve numbered selection, title, or ID
674
+ if target.isdigit():
675
+ sessions = self._list_recent_sessions(limit=10)
676
+ index = int(target)
677
+ if index < 1 or index > len(sessions):
678
+ _cprint(f" Resume index {index} is out of range.")
679
+ _cprint(" Use /resume with no arguments to see available sessions.")
680
+ return
681
+ selected = sessions[index - 1]
682
+ target_id = selected["id"]
683
+ else:
684
+ from hermes_cli.main import _resolve_session_by_name_or_id
685
+ resolved = _resolve_session_by_name_or_id(target)
686
+ target_id = resolved or target
687
+
688
+ session_meta = self._session_db.get_session(target_id)
689
+ if not session_meta:
690
+ _cprint(f" Session not found: {target}")
691
+ _cprint(" Use /history or `hermes sessions list` to see available sessions.")
692
+ return
693
+
694
+ # If the target is the empty head of a compression chain, redirect to
695
+ # the descendant that actually holds the transcript. See #15000.
696
+ try:
697
+ resolved_id = self._session_db.resolve_resume_session_id(target_id)
698
+ except Exception:
699
+ resolved_id = target_id
700
+ if resolved_id and resolved_id != target_id:
701
+ _cprint(
702
+ f" Session {target_id} was compressed into {resolved_id}; "
703
+ f"resuming the descendant with your transcript."
704
+ )
705
+ target_id = resolved_id
706
+ resolved_meta = self._session_db.get_session(target_id)
707
+ if resolved_meta:
708
+ session_meta = resolved_meta
709
+
710
+ if target_id == self.session_id:
711
+ _cprint(" Already on that session.")
712
+ return
713
+
714
+ old_session_id = self.session_id
715
+ # End current session
716
+ try:
717
+ self._session_db.end_session(self.session_id, "resumed_other")
718
+ except Exception:
719
+ pass
720
+
721
+ # Switch to the target session
722
+ self.session_id = target_id
723
+ self._resumed = True
724
+ self._pending_title = None
725
+ _sync_process_session_id(target_id)
726
+
727
+ # Load conversation history (strip transcript-only metadata entries)
728
+ restored = self._session_db.get_messages_as_conversation(target_id)
729
+ restored = [m for m in (restored or []) if m.get("role") != "session_meta"]
730
+ self.conversation_history = restored
731
+
732
+ # Re-open the target session so it's not marked as ended
733
+ try:
734
+ self._session_db.reopen_session(target_id)
735
+ except Exception:
736
+ pass
737
+
738
+ # Sync the agent if already initialised
739
+ if self.agent:
740
+ self.agent.session_id = target_id
741
+ self.agent.reset_session_state()
742
+ if hasattr(self.agent, "_last_flushed_db_idx"):
743
+ self.agent._last_flushed_db_idx = len(self.conversation_history)
744
+ if hasattr(self.agent, "_todo_store"):
745
+ try:
746
+ from tools.todo_tool import TodoStore
747
+ self.agent._todo_store = TodoStore()
748
+ except Exception:
749
+ pass
750
+ if hasattr(self.agent, "_invalidate_system_prompt"):
751
+ self.agent._invalidate_system_prompt()
752
+
753
+ # Notify memory providers that session_id rotated to a resumed
754
+ # session. reset=False — the provider's accumulated state is
755
+ # still valid; it just needs to target the new session_id for
756
+ # subsequent writes. See #6672.
757
+ try:
758
+ _mm = getattr(self.agent, "_memory_manager", None)
759
+ if _mm is not None:
760
+ _mm.on_session_switch(
761
+ target_id,
762
+ parent_session_id=old_session_id or "",
763
+ reset=False,
764
+ reason="resume",
765
+ )
766
+ except Exception:
767
+ pass
768
+
769
+ title_part = f" \"{session_meta['title']}\"" if session_meta.get("title") else ""
770
+ msg_count = len([m for m in self.conversation_history if m.get("role") == "user"])
771
+ if self.conversation_history:
772
+ _cprint(
773
+ f" ↻ Resumed session {target_id}{title_part}"
774
+ f" ({msg_count} user message{'s' if msg_count != 1 else ''},"
775
+ f" {len(self.conversation_history)} total)"
776
+ )
777
+ self._display_resumed_history()
778
+ else:
779
+ _cprint(f" ↻ Resumed session {target_id}{title_part} — no messages, starting fresh.")
780
+
781
+ def _handle_sessions_command(self, cmd_original: str) -> None:
782
+ """Handle /sessions [list|<id_or_title>] — browse or resume previous sessions.
783
+
784
+ Without arguments, prints the same recent-sessions table that /resume
785
+ shows when called without a target, and tells the user how to resume.
786
+ With an explicit subcommand or target, delegates to the resume flow so
787
+ ``/sessions <id>`` and ``/resume <id>`` behave identically.
788
+
789
+ The TUI ships an interactive picker overlay for this command; the
790
+ classic CLI prints an inline list because there is no equivalent
791
+ overlay primitive here. Without this handler the canonical name
792
+ ``sessions`` falls through ``process_command``'s elif chain and
793
+ prints ``Unknown command: sessions`` even though the command is
794
+ registered in the central COMMAND_REGISTRY.
795
+ """
796
+ from cli import _cprint
797
+ parts = cmd_original.split(None, 1)
798
+ arg = parts[1].strip() if len(parts) > 1 else ""
799
+ sub = arg.lower()
800
+
801
+ # Bare /sessions or /sessions list — show recent sessions inline.
802
+ if not arg or sub in {"list", "ls", "browse"}:
803
+ if not self._session_db:
804
+ from hermes_state import format_session_db_unavailable
805
+ _cprint(f" {format_session_db_unavailable()}")
806
+ return
807
+ if not self._show_recent_sessions(reason="sessions"):
808
+ _cprint(" (._.) No previous sessions yet.")
809
+ return
810
+
811
+ # /sessions <id_or_title> behaves the same as /resume <id_or_title>.
812
+ self._handle_resume_command(f"/resume {arg}")
813
+
814
+ def _handle_branch_command(self, cmd_original: str) -> None:
815
+ """Handle /branch [name] — fork the current session into a new independent copy.
816
+
817
+ Copies the full conversation history to a new session so the user can
818
+ explore a different approach without losing the original session state.
819
+ Inspired by Claude Code's /branch command.
820
+ """
821
+ from cli import _cprint, _sync_process_session_id
822
+ if not self.conversation_history:
823
+ _cprint(" No conversation to branch — send a message first.")
824
+ return
825
+
826
+ if not self._session_db:
827
+ from hermes_state import format_session_db_unavailable
828
+ _cprint(f" {format_session_db_unavailable()}")
829
+ return
830
+
831
+ parts = cmd_original.split(None, 1)
832
+ branch_name = parts[1].strip() if len(parts) > 1 else ""
833
+
834
+ # Generate the new session ID
835
+ now = datetime.now()
836
+ timestamp_str = now.strftime("%Y%m%d_%H%M%S")
837
+ short_uuid = uuid.uuid4().hex[:6]
838
+ new_session_id = f"{timestamp_str}_{short_uuid}"
839
+
840
+ # Determine branch title
841
+ if branch_name:
842
+ branch_title = branch_name
843
+ else:
844
+ # Auto-generate from the current session title
845
+ current_title = None
846
+ if self._session_db:
847
+ current_title = self._session_db.get_session_title(self.session_id)
848
+ base = current_title or "branch"
849
+ branch_title = self._session_db.get_next_title_in_lineage(base)
850
+
851
+ # Save the current session's state before branching
852
+ parent_session_id = self.session_id
853
+
854
+ # End the old session
855
+ try:
856
+ self._session_db.end_session(self.session_id, "branched")
857
+ except Exception:
858
+ pass
859
+
860
+ # Create the new session with parent link.
861
+ # Persist a stable ``_branched_from`` marker in model_config so
862
+ # list_sessions_rich() can keep the branch visible in /resume and
863
+ # /sessions even after the parent is reopened and re-ended with a
864
+ # different end_reason (e.g. tui_shutdown overwriting 'branched').
865
+ try:
866
+ self._session_db.create_session(
867
+ session_id=new_session_id,
868
+ source=os.environ.get("HERMES_SESSION_SOURCE", "cli"),
869
+ model=self.model,
870
+ model_config={
871
+ "max_iterations": self.max_turns,
872
+ "reasoning_config": self.reasoning_config,
873
+ "_branched_from": parent_session_id,
874
+ },
875
+ parent_session_id=parent_session_id,
876
+ )
877
+ except Exception as e:
878
+ _cprint(f" Failed to create branch session: {e}")
879
+ return
880
+
881
+ # Copy conversation history to the new session
882
+ for msg in self.conversation_history:
883
+ try:
884
+ self._session_db.append_message(
885
+ session_id=new_session_id,
886
+ role=msg.get("role", "user"),
887
+ content=msg.get("content"),
888
+ tool_name=msg.get("tool_name") or msg.get("name"),
889
+ tool_calls=msg.get("tool_calls"),
890
+ tool_call_id=msg.get("tool_call_id"),
891
+ reasoning=msg.get("reasoning"),
892
+ )
893
+ except Exception:
894
+ pass # Best-effort copy
895
+
896
+ # Set title on the branch
897
+ try:
898
+ self._session_db.set_session_title(new_session_id, branch_title)
899
+ except Exception:
900
+ pass
901
+
902
+ # Switch to the new session
903
+ self._transfer_session_yolo(self.session_id, new_session_id)
904
+ self.session_id = new_session_id
905
+ self.session_start = now
906
+ self._pending_title = None
907
+ self._resumed = True # Prevents auto-title generation
908
+ _sync_process_session_id(new_session_id)
909
+
910
+ # Sync the agent
911
+ if self.agent:
912
+ self.agent.session_id = new_session_id
913
+ self.agent.session_start = now
914
+ self.agent.reset_session_state()
915
+ if hasattr(self.agent, "_last_flushed_db_idx"):
916
+ self.agent._last_flushed_db_idx = len(self.conversation_history)
917
+ if hasattr(self.agent, "_todo_store"):
918
+ try:
919
+ from tools.todo_tool import TodoStore
920
+ self.agent._todo_store = TodoStore()
921
+ except Exception:
922
+ pass
923
+ if hasattr(self.agent, "_invalidate_system_prompt"):
924
+ self.agent._invalidate_system_prompt()
925
+
926
+ # Notify memory providers that session_id forked to a new branch.
927
+ # reset=False — the branched session carries the transcript
928
+ # forward, so provider state tracks the lineage. parent_session_id
929
+ # links the branch back to the original. See #6672.
930
+ try:
931
+ _mm = getattr(self.agent, "_memory_manager", None)
932
+ if _mm is not None:
933
+ _mm.on_session_switch(
934
+ new_session_id,
935
+ parent_session_id=parent_session_id or "",
936
+ reset=False,
937
+ reason="branch",
938
+ )
939
+ except Exception:
940
+ pass
941
+
942
+ msg_count = len([m for m in self.conversation_history if m.get("role") == "user"])
943
+ _cprint(
944
+ f" ⑂ Branched session \"{branch_title}\""
945
+ f" ({msg_count} user message{'s' if msg_count != 1 else ''})"
946
+ )
947
+ _cprint(f" Original session: {parent_session_id}")
948
+ _cprint(f" Branch session: {new_session_id}")
949
+
950
+ def _handle_gquota_command(self, cmd_original: str) -> None:
951
+ """Show Google Gemini Code Assist quota usage for the current OAuth account."""
952
+ try:
953
+ from agent.google_oauth import get_valid_access_token, GoogleOAuthError, load_credentials
954
+ from agent.google_code_assist import retrieve_user_quota, CodeAssistError
955
+ except ImportError as exc:
956
+ self._console_print(f" [red]Gemini modules unavailable: {exc}[/]")
957
+ return
958
+
959
+ try:
960
+ access_token = get_valid_access_token()
961
+ except GoogleOAuthError as exc:
962
+ self._console_print(f" [yellow]{exc}[/]")
963
+ self._console_print(" Run [bold]/model[/] and pick 'Google Gemini (OAuth)' to sign in.")
964
+ return
965
+
966
+ creds = load_credentials()
967
+ project_id = (creds.project_id if creds else "") or ""
968
+
969
+ try:
970
+ buckets = retrieve_user_quota(access_token, project_id=project_id)
971
+ except CodeAssistError as exc:
972
+ self._console_print(f" [red]Quota lookup failed:[/] {exc}")
973
+ return
974
+
975
+ if not buckets:
976
+ self._console_print(" [dim]No quota buckets reported (account may be on legacy/unmetered tier).[/]")
977
+ return
978
+
979
+ # Sort for stable display, group by model
980
+ buckets.sort(key=lambda b: (b.model_id, b.token_type))
981
+ self._console_print()
982
+ self._console_print(f" [bold]Gemini Code Assist quota[/] (project: {project_id or '(auto / free-tier)'})")
983
+ self._console_print()
984
+ for b in buckets:
985
+ pct = max(0.0, min(1.0, b.remaining_fraction))
986
+ width = 20
987
+ filled = int(round(pct * width))
988
+ bar = "▓" * filled + "░" * (width - filled)
989
+ pct_str = f"{int(pct * 100):3d}%"
990
+ header = b.model_id
991
+ if b.token_type:
992
+ header += f" [{b.token_type}]"
993
+ self._console_print(f" {header:40s} {bar} {pct_str}")
994
+ self._console_print()
995
+
996
+ def _handle_personality_command(self, cmd: str):
997
+ """Handle the /personality command to set predefined personalities."""
998
+ from cli import save_config_value
999
+ parts = cmd.split(maxsplit=1)
1000
+
1001
+ if len(parts) > 1:
1002
+ # Set personality
1003
+ personality_name = parts[1].strip().lower()
1004
+
1005
+ if personality_name in {"none", "default", "neutral"}:
1006
+ self.system_prompt = ""
1007
+ self.agent = None # Force re-init
1008
+ if save_config_value("agent.system_prompt", ""):
1009
+ print("(^_^)b Personality cleared (saved to config)")
1010
+ else:
1011
+ print("(^_^) Personality cleared (session only)")
1012
+ print(" No personality overlay — using base agent behavior.")
1013
+ elif personality_name in self.personalities:
1014
+ self.system_prompt = self._resolve_personality_prompt(self.personalities[personality_name])
1015
+ self.agent = None # Force re-init
1016
+ if save_config_value("agent.system_prompt", self.system_prompt):
1017
+ print(f"(^_^)b Personality set to '{personality_name}' (saved to config)")
1018
+ else:
1019
+ print(f"(^_^) Personality set to '{personality_name}' (session only)")
1020
+ print(f" \"{self.system_prompt[:60]}{'...' if len(self.system_prompt) > 60 else ''}\"")
1021
+ else:
1022
+ print(f"(._.) Unknown personality: {personality_name}")
1023
+ print(f" Available: none, {', '.join(self.personalities.keys())}")
1024
+ else:
1025
+ # Show available personalities
1026
+ print()
1027
+ print("+" + "-" * 50 + "+")
1028
+ print("|" + " " * 12 + "(^o^)/ Personalities" + " " * 15 + "|")
1029
+ print("+" + "-" * 50 + "+")
1030
+ print()
1031
+ print(f" {'none':<12} - (no personality overlay)")
1032
+ for name, prompt in self.personalities.items():
1033
+ if isinstance(prompt, dict):
1034
+ preview = prompt.get("description") or prompt.get("system_prompt", "")[:50]
1035
+ else:
1036
+ preview = str(prompt)[:50]
1037
+ print(f" {name:<12} - {preview}")
1038
+ print()
1039
+ print(" Usage: /personality <name>")
1040
+ print()
1041
+
1042
+ def _handle_cron_command(self, cmd: str):
1043
+ """Handle the /cron command to manage scheduled tasks."""
1044
+ from cli import get_job
1045
+ import shlex
1046
+ from tools.cronjob_tools import cronjob as cronjob_tool
1047
+
1048
+ def _cron_api(**kwargs):
1049
+ return json.loads(cronjob_tool(**kwargs))
1050
+
1051
+ def _normalize_skills(values):
1052
+ normalized = []
1053
+ for value in values:
1054
+ text = str(value or "").strip()
1055
+ if text and text not in normalized:
1056
+ normalized.append(text)
1057
+ return normalized
1058
+
1059
+ def _parse_flags(tokens):
1060
+ opts = {
1061
+ "name": None,
1062
+ "deliver": None,
1063
+ "repeat": None,
1064
+ "skills": [],
1065
+ "add_skills": [],
1066
+ "remove_skills": [],
1067
+ "clear_skills": False,
1068
+ "all": False,
1069
+ "prompt": None,
1070
+ "schedule": None,
1071
+ "positionals": [],
1072
+ }
1073
+ i = 0
1074
+ while i < len(tokens):
1075
+ token = tokens[i]
1076
+ if token == "--name" and i + 1 < len(tokens):
1077
+ opts["name"] = tokens[i + 1]
1078
+ i += 2
1079
+ elif token == "--deliver" and i + 1 < len(tokens):
1080
+ opts["deliver"] = tokens[i + 1]
1081
+ i += 2
1082
+ elif token == "--repeat" and i + 1 < len(tokens):
1083
+ try:
1084
+ opts["repeat"] = int(tokens[i + 1])
1085
+ except ValueError:
1086
+ print("(._.) --repeat must be an integer")
1087
+ return None
1088
+ i += 2
1089
+ elif token == "--skill" and i + 1 < len(tokens):
1090
+ opts["skills"].append(tokens[i + 1])
1091
+ i += 2
1092
+ elif token == "--add-skill" and i + 1 < len(tokens):
1093
+ opts["add_skills"].append(tokens[i + 1])
1094
+ i += 2
1095
+ elif token == "--remove-skill" and i + 1 < len(tokens):
1096
+ opts["remove_skills"].append(tokens[i + 1])
1097
+ i += 2
1098
+ elif token == "--clear-skills":
1099
+ opts["clear_skills"] = True
1100
+ i += 1
1101
+ elif token == "--all":
1102
+ opts["all"] = True
1103
+ i += 1
1104
+ elif token == "--prompt" and i + 1 < len(tokens):
1105
+ opts["prompt"] = tokens[i + 1]
1106
+ i += 2
1107
+ elif token == "--schedule" and i + 1 < len(tokens):
1108
+ opts["schedule"] = tokens[i + 1]
1109
+ i += 2
1110
+ else:
1111
+ opts["positionals"].append(token)
1112
+ i += 1
1113
+ return opts
1114
+
1115
+ tokens = shlex.split(cmd)
1116
+
1117
+ if len(tokens) == 1:
1118
+ print()
1119
+ print("+" + "-" * 68 + "+")
1120
+ print("|" + " " * 22 + "(^_^) Scheduled Tasks" + " " * 23 + "|")
1121
+ print("+" + "-" * 68 + "+")
1122
+ print()
1123
+ print(" Commands:")
1124
+ print(" /cron list")
1125
+ print(' /cron add "every 2h" "Check server status" [--skill blogwatcher]')
1126
+ print(' /cron edit <job_id> --schedule "every 4h" --prompt "New task"')
1127
+ print(" /cron edit <job_id> --skill blogwatcher --skill maps")
1128
+ print(" /cron edit <job_id> --remove-skill blogwatcher")
1129
+ print(" /cron edit <job_id> --clear-skills")
1130
+ print(" /cron pause <job_id>")
1131
+ print(" /cron resume <job_id>")
1132
+ print(" /cron run <job_id>")
1133
+ print(" /cron remove <job_id>")
1134
+ print()
1135
+ result = _cron_api(action="list")
1136
+ jobs = result.get("jobs", []) if result.get("success") else []
1137
+ if jobs:
1138
+ print(" Current Jobs:")
1139
+ print(" " + "-" * 63)
1140
+ for job in jobs:
1141
+ repeat_str = job.get("repeat", "?")
1142
+ print(f" {job['job_id'][:12]:<12} | {job['schedule']:<15} | {repeat_str:<8}")
1143
+ if job.get("skills"):
1144
+ print(f" Skills: {', '.join(job['skills'])}")
1145
+ print(f" {job.get('prompt_preview', '')}")
1146
+ if job.get("next_run_at"):
1147
+ print(f" Next: {job['next_run_at']}")
1148
+ print()
1149
+ else:
1150
+ print(" No scheduled jobs. Use '/cron add' to create one.")
1151
+ print()
1152
+ return
1153
+
1154
+ subcommand = tokens[1].lower()
1155
+ opts = _parse_flags(tokens[2:])
1156
+ if opts is None:
1157
+ return
1158
+
1159
+ if subcommand == "list":
1160
+ result = _cron_api(action="list", include_disabled=opts["all"])
1161
+ jobs = result.get("jobs", []) if result.get("success") else []
1162
+ if not jobs:
1163
+ print("(._.) No scheduled jobs.")
1164
+ return
1165
+
1166
+ print()
1167
+ print("Scheduled Jobs:")
1168
+ print("-" * 80)
1169
+ for job in jobs:
1170
+ print(f" ID: {job['job_id']}")
1171
+ print(f" Name: {job['name']}")
1172
+ print(f" State: {job.get('state', '?')}")
1173
+ print(f" Schedule: {job['schedule']} ({job.get('repeat', '?')})")
1174
+ print(f" Next run: {job.get('next_run_at', 'N/A')}")
1175
+ if job.get("skills"):
1176
+ print(f" Skills: {', '.join(job['skills'])}")
1177
+ print(f" Prompt: {job.get('prompt_preview', '')}")
1178
+ if job.get("last_run_at"):
1179
+ print(f" Last run: {job['last_run_at']} ({job.get('last_status', '?')})")
1180
+ print()
1181
+ return
1182
+
1183
+ if subcommand in {"add", "create"}:
1184
+ positionals = opts["positionals"]
1185
+ if not positionals:
1186
+ print("(._.) Usage: /cron add <schedule> <prompt>")
1187
+ return
1188
+ schedule = opts["schedule"] or positionals[0]
1189
+ prompt = opts["prompt"] or " ".join(positionals[1:])
1190
+ skills = _normalize_skills(opts["skills"])
1191
+ if not prompt and not skills:
1192
+ print("(._.) Please provide a prompt or at least one skill")
1193
+ return
1194
+ result = _cron_api(
1195
+ action="create",
1196
+ schedule=schedule,
1197
+ prompt=prompt or None,
1198
+ name=opts["name"],
1199
+ deliver=opts["deliver"],
1200
+ repeat=opts["repeat"],
1201
+ skills=skills or None,
1202
+ )
1203
+ if result.get("success"):
1204
+ print(f"(^_^)b Created job: {result['job_id']}")
1205
+ print(f" Schedule: {result['schedule']}")
1206
+ if result.get("skills"):
1207
+ print(f" Skills: {', '.join(result['skills'])}")
1208
+ print(f" Next run: {result['next_run_at']}")
1209
+ else:
1210
+ print(f"(x_x) Failed to create job: {result.get('error')}")
1211
+ return
1212
+
1213
+ if subcommand == "edit":
1214
+ positionals = opts["positionals"]
1215
+ if not positionals:
1216
+ print("(._.) Usage: /cron edit <job_id> [--schedule ...] [--prompt ...] [--skill ...]")
1217
+ return
1218
+ job_id = positionals[0]
1219
+ existing = get_job(job_id)
1220
+ if not existing:
1221
+ print(f"(._.) Job not found: {job_id}")
1222
+ return
1223
+
1224
+ final_skills = None
1225
+ replacement_skills = _normalize_skills(opts["skills"])
1226
+ add_skills = _normalize_skills(opts["add_skills"])
1227
+ remove_skills = set(_normalize_skills(opts["remove_skills"]))
1228
+ existing_skills = list(existing.get("skills") or ([] if not existing.get("skill") else [existing.get("skill")]))
1229
+ if opts["clear_skills"]:
1230
+ final_skills = []
1231
+ elif replacement_skills:
1232
+ final_skills = replacement_skills
1233
+ elif add_skills or remove_skills:
1234
+ final_skills = [skill for skill in existing_skills if skill not in remove_skills]
1235
+ for skill in add_skills:
1236
+ if skill not in final_skills:
1237
+ final_skills.append(skill)
1238
+
1239
+ result = _cron_api(
1240
+ action="update",
1241
+ job_id=job_id,
1242
+ schedule=opts["schedule"],
1243
+ prompt=opts["prompt"],
1244
+ name=opts["name"],
1245
+ deliver=opts["deliver"],
1246
+ repeat=opts["repeat"],
1247
+ skills=final_skills,
1248
+ )
1249
+ if result.get("success"):
1250
+ job = result["job"]
1251
+ print(f"(^_^)b Updated job: {job['job_id']}")
1252
+ print(f" Schedule: {job['schedule']}")
1253
+ if job.get("skills"):
1254
+ print(f" Skills: {', '.join(job['skills'])}")
1255
+ else:
1256
+ print(" Skills: none")
1257
+ else:
1258
+ print(f"(x_x) Failed to update job: {result.get('error')}")
1259
+ return
1260
+
1261
+ if subcommand in {"pause", "resume", "run", "remove", "rm", "delete"}:
1262
+ positionals = opts["positionals"]
1263
+ if not positionals:
1264
+ print(f"(._.) Usage: /cron {subcommand} <job_id>")
1265
+ return
1266
+ job_id = positionals[0]
1267
+ action = "remove" if subcommand in {"remove", "rm", "delete"} else subcommand
1268
+ result = _cron_api(action=action, job_id=job_id, reason="paused from /cron" if action == "pause" else None)
1269
+ if not result.get("success"):
1270
+ print(f"(x_x) Failed to {action} job: {result.get('error')}")
1271
+ return
1272
+ if action == "pause":
1273
+ print(f"(^_^)b Paused job: {result['job']['name']} ({job_id})")
1274
+ elif action == "resume":
1275
+ print(f"(^_^)b Resumed job: {result['job']['name']} ({job_id})")
1276
+ print(f" Next run: {result['job'].get('next_run_at')}")
1277
+ elif action == "run":
1278
+ print(f"(^_^)b Triggered job: {result['job']['name']} ({job_id})")
1279
+ print(" It will run on the next scheduler tick.")
1280
+ else:
1281
+ removed = result.get("removed_job", {})
1282
+ print(f"(^_^)b Removed job: {removed.get('name', job_id)} ({job_id})")
1283
+ return
1284
+
1285
+ print(f"(._.) Unknown cron command: {subcommand}")
1286
+ print(" Available: list, add, edit, pause, resume, run, remove")
1287
+
1288
+ def _handle_suggestions_command(self, cmd: str):
1289
+ """Handle /suggestions — review/accept/dismiss suggested automations.
1290
+
1291
+ Delegates to the shared handler so CLI and gateway never drift. CLI
1292
+ origin is the local platform so an accepted job's "origin" delivery
1293
+ resolves to a configured home channel.
1294
+ """
1295
+ import shlex
1296
+
1297
+ try:
1298
+ tokens = shlex.split(cmd)[1:] if cmd else []
1299
+ except ValueError:
1300
+ tokens = (cmd or "").split()[1:]
1301
+ args = " ".join(tokens)
1302
+ try:
1303
+ from hermes_cli.suggestions_cmd import handle_suggestions_command
1304
+ output = handle_suggestions_command(args)
1305
+ except Exception as e:
1306
+ output = f"Suggestions command failed: {e}"
1307
+ self._console_print(output)
1308
+
1309
+ def _handle_blueprint_command(self, cmd: str):
1310
+ """Handle /blueprint — set up an automation from a blueprint template.
1311
+
1312
+ Delegates to the shared handler. A bare ``/blueprint`` lists the
1313
+ catalog; ``/blueprint <name>`` name-matches a blueprint and seeds the
1314
+ agent to ask the user for each value conversationally (the result's
1315
+ ``agent_seed``); ``/blueprint <name> slot=val …`` creates the job
1316
+ directly. When a seed is returned it is stashed as a one-shot pending
1317
+ message the interactive loop runs as the next agent turn.
1318
+ """
1319
+ import shlex
1320
+
1321
+ try:
1322
+ tokens = shlex.split(cmd)[1:] if cmd else []
1323
+ except ValueError:
1324
+ tokens = (cmd or "").split()[1:]
1325
+ args = " ".join(shlex.quote(t) for t in tokens)
1326
+ try:
1327
+ from hermes_cli.blueprint_cmd import handle_blueprint_command
1328
+ result = handle_blueprint_command(args)
1329
+ except Exception as e:
1330
+ self._console_print(f"Cron blueprint command failed: {e}")
1331
+ return
1332
+ self._console_print(result.text)
1333
+ seed = getattr(result, "agent_seed", None)
1334
+ if seed:
1335
+ # One-shot: the interactive loop picks this up right after the
1336
+ # slash command returns and runs it as a normal agent turn.
1337
+ self._pending_agent_seed = seed
1338
+
1339
+ def _handle_curator_command(self, cmd: str):
1340
+ """Handle /curator slash command.
1341
+
1342
+ Delegates to hermes_cli.curator so the CLI and the `hermes curator`
1343
+ subcommand share the same handler set.
1344
+ """
1345
+ import shlex
1346
+
1347
+ tokens = shlex.split(cmd)[1:] if cmd else []
1348
+ if not tokens:
1349
+ tokens = ["status"]
1350
+
1351
+ try:
1352
+ from hermes_cli.curator import cli_main
1353
+ cli_main(tokens)
1354
+ except SystemExit:
1355
+ # argparse calls sys.exit() on --help or errors; swallow so we
1356
+ # don't kill the interactive session.
1357
+ pass
1358
+ except Exception as exc:
1359
+ print(f"(._.) curator: {exc}")
1360
+
1361
+ def _handle_kanban_command(self, cmd: str):
1362
+ """Handle the /kanban command — delegate to the shared kanban CLI.
1363
+
1364
+ The string form passed here is the user's full ``/kanban ...``
1365
+ including the leading slash; we strip it and hand the remainder
1366
+ to ``kanban.run_slash`` which returns a single formatted string.
1367
+ """
1368
+ from hermes_cli.kanban import run_slash
1369
+
1370
+ rest = cmd.strip()
1371
+ if rest.startswith("/"):
1372
+ rest = rest.lstrip("/")
1373
+ if rest.startswith("kanban"):
1374
+ rest = rest[len("kanban"):].lstrip()
1375
+ try:
1376
+ output = run_slash(rest)
1377
+ except Exception as exc: # pragma: no cover - defensive
1378
+ output = f"(._.) kanban error: {exc}"
1379
+ if output:
1380
+ print(output)
1381
+
1382
+ def _handle_skills_command(self, cmd: str):
1383
+ """Handle /skills slash command — delegates to hermes_cli.skills_hub."""
1384
+ from cli import ChatConsole
1385
+ # Intercept write-approval review subcommands first (pending/approve/
1386
+ # reject/diff/mode); everything else goes to the skills hub.
1387
+ parts = cmd.strip().split()
1388
+ args = parts[1:] if len(parts) > 1 else []
1389
+ if args and args[0].lower() in {"pending", "approve", "apply", "reject",
1390
+ "deny", "drop", "diff", "approval", "mode"}:
1391
+ from hermes_cli.write_approval_commands import handle_pending_subcommand
1392
+ from tools import write_approval as wa
1393
+ out = handle_pending_subcommand(
1394
+ wa.SKILLS, args,
1395
+ set_mode_fn=lambda enabled: self._save_write_approval("skills", enabled),
1396
+ )
1397
+ if out is not None:
1398
+ print(out)
1399
+ return
1400
+ from hermes_cli.skills_hub import handle_skills_slash
1401
+ handle_skills_slash(cmd, ChatConsole())
1402
+
1403
+ def _handle_memory_command(self, cmd: str):
1404
+ """Handle /memory slash command — pending review + approval-gate toggle."""
1405
+ from hermes_cli.write_approval_commands import handle_pending_subcommand
1406
+ from tools import write_approval as wa
1407
+ parts = cmd.strip().split()
1408
+ args = parts[1:] if len(parts) > 1 else []
1409
+ store = getattr(self.agent, "_memory_store", None) if getattr(self, "agent", None) else None
1410
+ out = handle_pending_subcommand(
1411
+ wa.MEMORY, args,
1412
+ memory_store=store,
1413
+ set_mode_fn=lambda enabled: self._save_write_approval("memory", enabled),
1414
+ )
1415
+ if out is None:
1416
+ out = ("Unknown /memory subcommand. "
1417
+ "Use: pending, approve <id>, reject <id>, approval <on|off>.")
1418
+ print(out)
1419
+
1420
+ def _save_write_approval(self, subsystem: str, enabled: bool):
1421
+ """Persist <subsystem>.write_approval to config (for /memory|/skills approval)."""
1422
+ from cli import save_config_value
1423
+ save_config_value(f"{subsystem}.write_approval", bool(enabled))
1424
+
1425
+ def _handle_background_command(self, cmd: str):
1426
+ """Handle /background <prompt> — run a prompt in a separate background session.
1427
+
1428
+ Spawns a new AIAgent in a background thread with its own session.
1429
+ When it completes, prints the result to the CLI without modifying
1430
+ the active session's conversation history.
1431
+ """
1432
+ from cli import AIAgent, ChatConsole, _accent_hex, _cprint, _maybe_remap_for_light_mode, _render_final_assistant_content, set_approval_callback, set_secret_capture_callback, set_sudo_password_callback
1433
+ parts = cmd.strip().split(maxsplit=1)
1434
+ if len(parts) < 2 or not parts[1].strip():
1435
+ _cprint(" Usage: /background <prompt>")
1436
+ _cprint(" Example: /background Summarize the top HN stories today")
1437
+ _cprint(" The task runs in a separate session and results display here when done.")
1438
+ return
1439
+
1440
+ prompt = parts[1].strip()
1441
+ self._background_task_counter += 1
1442
+ task_num = self._background_task_counter
1443
+ task_id = f"bg_{datetime.now().strftime('%H%M%S')}_{uuid.uuid4().hex[:6]}"
1444
+
1445
+ # Make sure we have valid credentials
1446
+ if not self._ensure_runtime_credentials():
1447
+ _cprint(" (>_<) Cannot start background task: no valid credentials.")
1448
+ return
1449
+
1450
+ _cprint(f" 🔄 Background task #{task_num} started: \"{prompt[:60]}{'...' if len(prompt) > 60 else ''}\"")
1451
+ _cprint(f" Task ID: {task_id}")
1452
+ _cprint(" You can continue chatting — results will appear when done.\n")
1453
+
1454
+ turn_route = self._resolve_turn_agent_config(prompt)
1455
+
1456
+ def run_background():
1457
+ set_sudo_password_callback(self._sudo_password_callback)
1458
+ set_approval_callback(self._approval_callback)
1459
+ try:
1460
+ set_secret_capture_callback(self._secret_capture_callback)
1461
+ except Exception:
1462
+ pass
1463
+ try:
1464
+ bg_agent = AIAgent(
1465
+ model=turn_route["model"],
1466
+ api_key=turn_route["runtime"].get("api_key"),
1467
+ base_url=turn_route["runtime"].get("base_url"),
1468
+ provider=turn_route["runtime"].get("provider"),
1469
+ api_mode=turn_route["runtime"].get("api_mode"),
1470
+ acp_command=turn_route["runtime"].get("command"),
1471
+ acp_args=turn_route["runtime"].get("args"),
1472
+ max_tokens=turn_route["runtime"].get("max_tokens"),
1473
+ max_iterations=self.max_turns,
1474
+ enabled_toolsets=self.enabled_toolsets,
1475
+ quiet_mode=True,
1476
+ verbose_logging=False,
1477
+ session_id=task_id,
1478
+ platform="cli",
1479
+ session_db=self._session_db,
1480
+ reasoning_config=self.reasoning_config,
1481
+ service_tier=self.service_tier,
1482
+ request_overrides=turn_route.get("request_overrides"),
1483
+ providers_allowed=self._providers_only,
1484
+ providers_ignored=self._providers_ignore,
1485
+ providers_order=self._providers_order,
1486
+ provider_sort=self._provider_sort,
1487
+ provider_require_parameters=self._provider_require_params,
1488
+ provider_data_collection=self._provider_data_collection,
1489
+ openrouter_min_coding_score=self._openrouter_min_coding_score,
1490
+ fallback_model=self._fallback_model,
1491
+ )
1492
+ # Silence raw spinner; route thinking through TUI widget when no foreground agent is active.
1493
+ bg_agent._print_fn = lambda *_a, **_kw: None
1494
+
1495
+ def _bg_thinking(text: str) -> None:
1496
+ # Concurrent bg tasks may race on _spinner_text; acceptable for best-effort UI.
1497
+ if not self._agent_running:
1498
+ self._spinner_text = text
1499
+ if self._app:
1500
+ self._app.invalidate()
1501
+
1502
+ bg_agent.thinking_callback = _bg_thinking
1503
+
1504
+ result = bg_agent.run_conversation(
1505
+ user_message=prompt,
1506
+ task_id=task_id,
1507
+ )
1508
+
1509
+ response = result.get("final_response", "") if result else ""
1510
+ if not response and result and result.get("error"):
1511
+ response = f"Error: {result['error']}"
1512
+
1513
+ # Display result in the CLI (thread-safe via patch_stdout).
1514
+ # Force a TUI refresh first so spinner/status bar don't overlap
1515
+ # with the output (fixes #2718).
1516
+ if self._app:
1517
+ self._app.invalidate()
1518
+ time.sleep(0.05) # brief pause for refresh
1519
+ print()
1520
+ ChatConsole().print(f"[{_accent_hex()}]{'─' * 40}[/]")
1521
+ _cprint(f" ✅ Background task #{task_num} complete")
1522
+ _cprint(f" Prompt: \"{prompt[:60]}{'...' if len(prompt) > 60 else ''}\"")
1523
+ ChatConsole().print(f"[{_accent_hex()}]{'─' * 40}[/]")
1524
+ if response:
1525
+ try:
1526
+ from hermes_cli.skin_engine import get_active_skin
1527
+ _skin = get_active_skin()
1528
+ label = _skin.get_branding("response_label", "⚕ Hermes")
1529
+ _resp_color = _maybe_remap_for_light_mode(_skin.get_color("response_border", "#CD7F32"))
1530
+ _resp_text = _maybe_remap_for_light_mode(_skin.get_color("banner_text", "#FFF8DC"))
1531
+ except Exception:
1532
+ label = "⚕ Hermes"
1533
+ _resp_color = "#CD7F32"
1534
+ _resp_text = "#FFF8DC"
1535
+
1536
+ _chat_console = ChatConsole()
1537
+ _chat_console.print(Panel(
1538
+ _render_final_assistant_content(response, mode=self.final_response_markdown),
1539
+ title=f"[{_resp_color} bold]{label} (background #{task_num})[/]",
1540
+ title_align="left",
1541
+ border_style=_resp_color,
1542
+ style=_resp_text,
1543
+ box=rich_box.HORIZONTALS,
1544
+ padding=(1, 4),
1545
+ width=self._scrollback_box_width(),
1546
+ ))
1547
+ else:
1548
+ _cprint(" (No response generated)")
1549
+
1550
+ # Play bell if enabled
1551
+ if self.bell_on_complete:
1552
+ sys.stdout.write("\a")
1553
+ sys.stdout.flush()
1554
+
1555
+ except Exception as e:
1556
+ # Same TUI refresh pattern as success path (#2718)
1557
+ if self._app:
1558
+ self._app.invalidate()
1559
+ time.sleep(0.05)
1560
+ print()
1561
+ _cprint(f" ❌ Background task #{task_num} failed: {e}")
1562
+ finally:
1563
+ try:
1564
+ set_sudo_password_callback(None)
1565
+ set_approval_callback(None)
1566
+ set_secret_capture_callback(None)
1567
+ except Exception:
1568
+ pass
1569
+ self._background_tasks.pop(task_id, None)
1570
+ # Clear spinner only if no foreground agent owns it
1571
+ if not self._agent_running:
1572
+ self._spinner_text = ""
1573
+ if self._app:
1574
+ self._invalidate(min_interval=0)
1575
+
1576
+ thread = threading.Thread(target=run_background, daemon=True, name=f"bg-task-{task_id}")
1577
+ self._background_tasks[task_id] = thread
1578
+ thread.start()
1579
+
1580
+ def _handle_bundles_command(self, cmd: str) -> None:
1581
+ """In-session ``/bundles`` — show installed skill bundles.
1582
+
1583
+ Mirrors ``hermes bundles list`` but renders inside the running
1584
+ CLI so users can discover what's available without dropping out
1585
+ of their session. Bundles are loaded via ``/<bundle-name>``.
1586
+ """
1587
+ from cli import ChatConsole, _BOLD, _DIM, _RST, _accent_hex, _cprint
1588
+ try:
1589
+ from agent.skill_bundles import list_bundles, _bundles_dir
1590
+ except Exception as exc:
1591
+ _cprint(f"\033[1;31mBundle subsystem unavailable: {exc}{_RST}")
1592
+ return
1593
+
1594
+ bundles = list_bundles()
1595
+ if not bundles:
1596
+ _cprint(" No skill bundles installed.")
1597
+ _cprint(
1598
+ f" {_DIM}Create one with: hermes bundles create "
1599
+ f"<name> --skill <s1> --skill <s2>{_RST}"
1600
+ )
1601
+ _cprint(f" {_DIM}Directory: {_bundles_dir()}{_RST}")
1602
+ return
1603
+
1604
+ _cprint(f"\n ▣ {_BOLD}Skill Bundles{_RST} ({len(bundles)} installed):")
1605
+ for info in bundles:
1606
+ skill_count = len(info.get("skills", []))
1607
+ desc = info.get("description") or f"Load {skill_count} skills"
1608
+ ChatConsole().print(
1609
+ f" [bold {_accent_hex()}]/{info['slug']:<20}[/] "
1610
+ f"[dim]-[/] {_escape(desc)} [dim]({skill_count} skills)[/]"
1611
+ )
1612
+ for s in info.get("skills", []):
1613
+ ChatConsole().print(f" [dim]· {_escape(s)}[/]")
1614
+ _cprint(
1615
+ f"\n {_DIM}Invoke a bundle with /<slug>. "
1616
+ f"Manage with `hermes bundles`.{_RST}"
1617
+ )
1618
+
1619
+ def _handle_browser_command(self, cmd: str):
1620
+ """Handle /browser connect|disconnect|status — manage live Chromium-family CDP connection."""
1621
+ import platform as _plat
1622
+
1623
+ parts = cmd.strip().split(None, 1)
1624
+ sub = parts[1].lower().strip() if len(parts) > 1 else "status"
1625
+
1626
+ _DEFAULT_CDP = DEFAULT_BROWSER_CDP_URL
1627
+ current = os.environ.get("BROWSER_CDP_URL", "").strip()
1628
+
1629
+ if sub.startswith("connect"):
1630
+ # Optionally accept a custom CDP URL: /browser connect ws://host:port
1631
+ connect_parts = cmd.strip().split(None, 2) # ["/browser", "connect", "ws://..."]
1632
+ cdp_url = connect_parts[2].strip() if len(connect_parts) > 2 else _DEFAULT_CDP
1633
+ parsed_cdp = urlparse(cdp_url if "://" in cdp_url else f"http://{cdp_url}")
1634
+ if parsed_cdp.scheme not in {"http", "https", "ws", "wss"}:
1635
+ print()
1636
+ print(
1637
+ f" ⚠ Unsupported browser url scheme: {parsed_cdp.scheme or '(missing)'} "
1638
+ "(expected one of: http, https, ws, wss)"
1639
+ )
1640
+ print()
1641
+ return
1642
+ try:
1643
+ _port = parsed_cdp.port or (443 if parsed_cdp.scheme in {"https", "wss"} else 80)
1644
+ except ValueError:
1645
+ print()
1646
+ print(f" ⚠ Invalid port in browser url: {cdp_url}")
1647
+ print()
1648
+ return
1649
+ if not parsed_cdp.hostname:
1650
+ print()
1651
+ print(f" ⚠ Missing host in browser url: {cdp_url}")
1652
+ print()
1653
+ return
1654
+ _host = parsed_cdp.hostname
1655
+ if parsed_cdp.path.startswith("/devtools/browser/"):
1656
+ cdp_url = parsed_cdp.geturl()
1657
+ else:
1658
+ cdp_url = parsed_cdp._replace(
1659
+ path="",
1660
+ params="",
1661
+ query="",
1662
+ fragment="",
1663
+ ).geturl()
1664
+
1665
+ # Clear any existing browser sessions so the next tool call uses the new backend
1666
+ try:
1667
+ from tools.browser_tool import cleanup_all_browsers
1668
+ cleanup_all_browsers()
1669
+ except Exception:
1670
+ pass
1671
+
1672
+ print()
1673
+
1674
+ # Check if a Chromium-family browser is already serving CDP on the debug port
1675
+ _already_open = is_browser_debug_ready(cdp_url, timeout=1.0)
1676
+
1677
+ if _already_open:
1678
+ print(f" ✓ Chromium-family browser is already listening on port {_port}")
1679
+ elif cdp_url == _DEFAULT_CDP:
1680
+ # Try to auto-launch a Chromium-family browser with remote debugging
1681
+ print(" Chromium-family browser isn't running with remote debugging — attempting to launch...")
1682
+ _launched = self._try_launch_chrome_debug(_port, _plat.system())
1683
+ if _launched:
1684
+ # Wait for the DevTools discovery endpoint to come up
1685
+ for _wait in range(10):
1686
+ if is_browser_debug_ready(cdp_url, timeout=1.0):
1687
+ _already_open = True
1688
+ break
1689
+ time.sleep(0.5)
1690
+ if _already_open:
1691
+ print(f" ✓ Chromium-family browser launched and listening on port {_port}")
1692
+ else:
1693
+ print(f" ⚠ Browser launched but port {_port} isn't responding yet")
1694
+ print(" Try again in a few seconds — the debug instance may still be starting")
1695
+ else:
1696
+ print(" ⚠ Could not auto-launch a Chromium-family browser")
1697
+ sys_name = _plat.system()
1698
+ chrome_cmd = manual_chrome_debug_command(_port, sys_name)
1699
+ if chrome_cmd:
1700
+ print(f" Launch a Chromium-family browser manually:")
1701
+ print(f" {chrome_cmd}")
1702
+ else:
1703
+ print(" No supported Chromium-family browser executable found in this environment")
1704
+ else:
1705
+ print(f" ⚠ Port {_port} is not reachable at {cdp_url}")
1706
+
1707
+ if not _already_open:
1708
+ print()
1709
+ print("Browser not connected — start a Chromium-family browser with remote debugging and retry /browser connect")
1710
+ print()
1711
+ return
1712
+
1713
+ os.environ["BROWSER_CDP_URL"] = cdp_url
1714
+ # Eagerly start the CDP supervisor so pending_dialogs + frame_tree
1715
+ # show up in the next browser_snapshot. No-op if already started.
1716
+ try:
1717
+ from tools.browser_tool import _ensure_cdp_supervisor # type: ignore[import-not-found]
1718
+ _ensure_cdp_supervisor("default")
1719
+ except Exception:
1720
+ pass
1721
+ print()
1722
+ print("🌐 Browser connected to live Chromium-family browser via CDP")
1723
+ print(f" Endpoint: {cdp_url}")
1724
+ print()
1725
+
1726
+ # Inject context message so the model knows this slash command
1727
+ # intentionally makes the dev/debug CDP browser available for use.
1728
+ if hasattr(self, '_pending_input'):
1729
+ self._pending_input.put(
1730
+ "[System note: The user invoked /browser connect and connected your browser tools to "
1731
+ "a Chromium-family dev/debug browser via Chrome DevTools Protocol. "
1732
+ "Your browser_navigate, browser_snapshot, browser_click, and other browser tools now "
1733
+ "control that CDP browser. The command itself is a signal that using browser tools for "
1734
+ "their current browser-related request is expected; do not wait for separate permission "
1735
+ "just because CDP is connected. This is typically a Hermes-managed isolated debug "
1736
+ "profile, not the user's main everyday browser. It is still user-visible and may contain "
1737
+ "pages, logged-in sessions, or cookies in that debug profile, so avoid destructive actions, "
1738
+ "closing tabs, or navigating away unless the user's task calls for it.]"
1739
+ )
1740
+
1741
+ elif sub == "disconnect":
1742
+ if current:
1743
+ os.environ.pop("BROWSER_CDP_URL", None)
1744
+ try:
1745
+ from tools.browser_tool import cleanup_all_browsers, _stop_cdp_supervisor
1746
+ _stop_cdp_supervisor("default")
1747
+ cleanup_all_browsers()
1748
+ except Exception:
1749
+ pass
1750
+ print()
1751
+ print("🌐 Browser disconnected from live Chromium-family browser")
1752
+ print(" Browser tools reverted to default mode (local headless or cloud provider)")
1753
+ print()
1754
+
1755
+ if hasattr(self, '_pending_input'):
1756
+ self._pending_input.put(
1757
+ "[System note: The user has disconnected the browser tools from their live Chromium-family browser. "
1758
+ "Browser tools are back to default mode (headless local browser or cloud provider).]"
1759
+ )
1760
+ else:
1761
+ print()
1762
+ print("Browser is not connected to a live Chromium-family browser (already using default mode)")
1763
+ print()
1764
+
1765
+ elif sub == "status":
1766
+ print()
1767
+ if current:
1768
+ print("🌐 Browser: connected to live Chromium-family browser via CDP")
1769
+ print(f" Endpoint: {current}")
1770
+
1771
+ _port = 9222
1772
+ try:
1773
+ _port = int(current.rsplit(":", 1)[-1].split("/")[0])
1774
+ except (ValueError, IndexError):
1775
+ pass
1776
+ try:
1777
+ import socket
1778
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1779
+ s.settimeout(1)
1780
+ s.connect(("127.0.0.1", _port))
1781
+ s.close()
1782
+ print(" Status: ✓ reachable")
1783
+ except (OSError, Exception):
1784
+ print(" Status: ⚠ not reachable (browser may not be running)")
1785
+ else:
1786
+ try:
1787
+ from tools.browser_tool import _get_cloud_provider
1788
+ provider = _get_cloud_provider()
1789
+ except Exception:
1790
+ provider = None
1791
+
1792
+ if provider is not None:
1793
+ print(f"🌐 Browser: {provider.provider_name()} (cloud)")
1794
+ else:
1795
+ # Show engine info for local mode
1796
+ try:
1797
+ from tools.browser_tool import _get_browser_engine
1798
+ engine = _get_browser_engine()
1799
+ except Exception:
1800
+ engine = "auto"
1801
+ if engine == "lightpanda":
1802
+ print("🌐 Browser: local Lightpanda (agent-browser --engine lightpanda)")
1803
+ print(" ⚡ Lightpanda: faster navigation, no screenshot support")
1804
+ print(" Automatic Chromium fallback for screenshots and failed commands")
1805
+ elif engine == "chrome":
1806
+ print("🌐 Browser: local headless Chromium (agent-browser --engine chrome)")
1807
+ else:
1808
+ print("🌐 Browser: local headless Chromium (agent-browser)")
1809
+ print()
1810
+ print(" /browser connect — connect to your live Chromium-family browser")
1811
+ print(" /browser disconnect — revert to default")
1812
+ print()
1813
+
1814
+ else:
1815
+ print()
1816
+ print("Usage: /browser connect|disconnect|status")
1817
+ print()
1818
+ print(" connect Connect browser tools to your live Chromium-family browser session")
1819
+ print(" disconnect Revert to default browser backend")
1820
+ print(" status Show current browser mode")
1821
+ print()
1822
+
1823
+ def _handle_goal_command(self, cmd: str) -> None:
1824
+ """Dispatch /goal subcommands: set / status / pause / resume / clear."""
1825
+ from cli import _DIM, _RST, _cprint
1826
+ parts = (cmd or "").strip().split(None, 1)
1827
+ arg = parts[1].strip() if len(parts) > 1 else ""
1828
+
1829
+ mgr = self._get_goal_manager()
1830
+ if mgr is None:
1831
+ _cprint(f" {_DIM}Goals unavailable (no active session).{_RST}")
1832
+ return
1833
+
1834
+ lower = arg.lower()
1835
+
1836
+ # Bare /goal or /goal status → show current state
1837
+ if not arg or lower == "status":
1838
+ _cprint(f" {mgr.status_line()}")
1839
+ return
1840
+
1841
+ if lower == "pause":
1842
+ state = mgr.pause(reason="user-paused")
1843
+ if state is None:
1844
+ _cprint(f" {_DIM}No goal set.{_RST}")
1845
+ else:
1846
+ _cprint(f" ⏸ Goal paused: {state.goal}")
1847
+ return
1848
+
1849
+ if lower == "resume":
1850
+ state = mgr.resume()
1851
+ if state is None:
1852
+ _cprint(f" {_DIM}No goal to resume.{_RST}")
1853
+ else:
1854
+ _cprint(f" ▶ Goal resumed: {state.goal}")
1855
+ _cprint(
1856
+ f" {_DIM}Send any message (or press Enter on an empty prompt "
1857
+ f"is a no-op; type 'continue' to kick it off).{_RST}"
1858
+ )
1859
+ return
1860
+
1861
+ if lower in {"clear", "stop", "done"}:
1862
+ had = mgr.has_goal()
1863
+ mgr.clear()
1864
+ if had:
1865
+ _cprint(" ✓ Goal cleared.")
1866
+ else:
1867
+ _cprint(f" {_DIM}No active goal.{_RST}")
1868
+ return
1869
+
1870
+ # Otherwise treat the arg as the goal text.
1871
+ try:
1872
+ state = mgr.set(arg)
1873
+ except ValueError as exc:
1874
+ _cprint(f" Invalid goal: {exc}")
1875
+ return
1876
+
1877
+ _cprint(f" ⊙ Goal set ({state.max_turns}-turn budget): {state.goal}")
1878
+ _cprint(
1879
+ f" {_DIM}After each turn, a judge model will check if the goal is done. "
1880
+ f"Hermes keeps working until it is, you pause/clear it, or the budget is "
1881
+ f"exhausted. Use /goal status, /goal pause, /goal resume, /goal clear.{_RST}"
1882
+ )
1883
+ # Kick the loop off immediately so the user doesn't have to send a
1884
+ # separate message after setting the goal.
1885
+ try:
1886
+ self._pending_input.put(state.goal)
1887
+ except Exception:
1888
+ pass
1889
+
1890
+ def _handle_subgoal_command(self, cmd: str) -> None:
1891
+ """Dispatch /subgoal subcommands.
1892
+
1893
+ Forms:
1894
+ /subgoal show current subgoals
1895
+ /subgoal <text> append a criterion
1896
+ /subgoal remove <n> drop subgoal n (1-based)
1897
+ /subgoal clear wipe all subgoals
1898
+
1899
+ Subgoals are extra criteria the user adds mid-loop. They get
1900
+ appended to both the judge prompt (verdict must consider them)
1901
+ and the continuation prompt (agent sees them) on the next turn
1902
+ boundary. No special kick — the running turn finishes, the next
1903
+ judge call includes them.
1904
+ """
1905
+ from cli import _DIM, _RST, _cprint
1906
+ parts = (cmd or "").strip().split(None, 2)
1907
+ arg = " ".join(parts[1:]).strip() if len(parts) > 1 else ""
1908
+
1909
+ mgr = self._get_goal_manager()
1910
+ if mgr is None:
1911
+ _cprint(f" {_DIM}Goals unavailable (no active session).{_RST}")
1912
+ return
1913
+
1914
+ if not mgr.has_goal():
1915
+ _cprint(f" {_DIM}No active goal. Set one with /goal <text>.{_RST}")
1916
+ return
1917
+
1918
+ # No args → list current subgoals.
1919
+ if not arg:
1920
+ _cprint(f" {mgr.status_line()}")
1921
+ _cprint(f" {mgr.render_subgoals()}")
1922
+ return
1923
+
1924
+ tokens = arg.split(None, 1)
1925
+ verb = tokens[0].lower()
1926
+ rest = tokens[1].strip() if len(tokens) > 1 else ""
1927
+
1928
+ if verb == "remove":
1929
+ if not rest:
1930
+ _cprint(" Usage: /subgoal remove <n>")
1931
+ return
1932
+ try:
1933
+ idx = int(rest.split()[0])
1934
+ except ValueError:
1935
+ _cprint(" /subgoal remove: <n> must be an integer (1-based index).")
1936
+ return
1937
+ try:
1938
+ removed = mgr.remove_subgoal(idx)
1939
+ except (IndexError, RuntimeError) as exc:
1940
+ _cprint(f" /subgoal remove: {exc}")
1941
+ return
1942
+ _cprint(f" ✓ Removed subgoal {idx}: {removed}")
1943
+ return
1944
+
1945
+ if verb == "clear":
1946
+ try:
1947
+ prev = mgr.clear_subgoals()
1948
+ except RuntimeError as exc:
1949
+ _cprint(f" /subgoal clear: {exc}")
1950
+ return
1951
+ if prev:
1952
+ _cprint(f" ✓ Cleared {prev} subgoal{'s' if prev != 1 else ''}.")
1953
+ else:
1954
+ _cprint(f" {_DIM}No subgoals to clear.{_RST}")
1955
+ return
1956
+
1957
+ # Otherwise — append the whole arg as a new subgoal.
1958
+ try:
1959
+ text = mgr.add_subgoal(arg)
1960
+ except (ValueError, RuntimeError) as exc:
1961
+ _cprint(f" /subgoal: {exc}")
1962
+ return
1963
+ idx = len(mgr.state.subgoals) if mgr.state else 0
1964
+ _cprint(f" ✓ Added subgoal {idx}: {text}")
1965
+
1966
+ def _handle_skin_command(self, cmd: str):
1967
+ """Handle /skin [name] — show or change the display skin."""
1968
+ from cli import _ACCENT, save_config_value
1969
+ try:
1970
+ from hermes_cli.skin_engine import list_skins, set_active_skin, get_active_skin_name
1971
+ except ImportError:
1972
+ print("Skin engine not available.")
1973
+ return
1974
+
1975
+ parts = cmd.strip().split(maxsplit=1)
1976
+ if len(parts) < 2 or not parts[1].strip():
1977
+ # Show current skin and list available
1978
+ current = get_active_skin_name()
1979
+ skins = list_skins()
1980
+ print(f"\n Current skin: {current}")
1981
+ print(" Available skins:")
1982
+ for s in skins:
1983
+ marker = " ●" if s["name"] == current else " "
1984
+ source = f" ({s['source']})" if s["source"] == "user" else ""
1985
+ print(f" {marker} {s['name']}{source} — {s['description']}")
1986
+ print("\n Usage: /skin <name>")
1987
+ print(f" Custom skins: drop a YAML file in {display_hermes_home()}/skins/\n")
1988
+ return
1989
+
1990
+ new_skin = parts[1].strip().lower()
1991
+ available = {s["name"] for s in list_skins()}
1992
+ if new_skin not in available:
1993
+ print(f" Unknown skin: {new_skin}")
1994
+ print(f" Available: {', '.join(sorted(available))}")
1995
+ return
1996
+
1997
+ set_active_skin(new_skin)
1998
+ _ACCENT.reset() # Re-resolve ANSI color for the new skin
1999
+ # _DIM is now a fixed dim+italic ANSI escape (terminal-default fg)
2000
+ # so it doesn't need re-resolving on skin switch.
2001
+ if save_config_value("display.skin", new_skin):
2002
+ print(f" Skin set to: {new_skin} (saved)")
2003
+ else:
2004
+ print(f" Skin set to: {new_skin}")
2005
+ print(" Note: banner colors will update on next session start.")
2006
+ if self._apply_tui_skin_style():
2007
+ print(" Prompt + TUI colors updated.")
2008
+
2009
+ def _handle_footer_command(self, cmd_original: str) -> None:
2010
+ """Toggle or inspect ``display.runtime_footer.enabled`` from the CLI.
2011
+
2012
+ Usage:
2013
+ /footer → toggle
2014
+ /footer on|off → explicit
2015
+ /footer status → show current state
2016
+ """
2017
+ from cli import _cprint, save_config_value
2018
+ from hermes_cli.config import load_config
2019
+ from hermes_cli.colors import Colors as _Colors
2020
+
2021
+ # Parse arg
2022
+ arg = ""
2023
+ try:
2024
+ parts = (cmd_original or "").strip().split(None, 1)
2025
+ if len(parts) > 1:
2026
+ arg = parts[1].strip().lower()
2027
+ except Exception:
2028
+ arg = ""
2029
+
2030
+ cfg = load_config() or {}
2031
+ footer_cfg = ((cfg.get("display") or {}).get("runtime_footer") or {})
2032
+ current = bool(footer_cfg.get("enabled", False))
2033
+ fields = footer_cfg.get("fields") or ["model", "context_pct", "cwd"]
2034
+
2035
+ if arg in {"status", "?"}:
2036
+ state = "ON" if current else "OFF"
2037
+ _cprint(
2038
+ f" {_Colors.BOLD}Runtime footer:{_Colors.RESET} {state}\n"
2039
+ f" Fields: {', '.join(fields)}"
2040
+ )
2041
+ return
2042
+
2043
+ if arg in {"on", "enable", "true", "1"}:
2044
+ new_state = True
2045
+ elif arg in {"off", "disable", "false", "0"}:
2046
+ new_state = False
2047
+ elif arg == "":
2048
+ new_state = not current
2049
+ else:
2050
+ _cprint(" Usage: /footer [on|off|status]")
2051
+ return
2052
+
2053
+ if save_config_value("display.runtime_footer.enabled", new_state):
2054
+ state = (
2055
+ f"{_Colors.GREEN}ON{_Colors.RESET}" if new_state
2056
+ else f"{_Colors.DIM}OFF{_Colors.RESET}"
2057
+ )
2058
+ _cprint(f" Runtime footer: {state}")
2059
+ else:
2060
+ _cprint(" Failed to save runtime_footer setting to config.yaml")
2061
+
2062
+ def _handle_reasoning_command(self, cmd: str):
2063
+ """Handle /reasoning — manage effort level and display toggle.
2064
+
2065
+ Usage:
2066
+ /reasoning Show current effort level and display state
2067
+ /reasoning <level> Set reasoning effort (none, minimal, low, medium, high, xhigh)
2068
+ /reasoning show|on Show model thinking/reasoning in output
2069
+ /reasoning hide|off Hide model thinking/reasoning from output
2070
+ """
2071
+ from cli import _ACCENT, _DIM, _RST, _cprint, _parse_reasoning_config, save_config_value
2072
+ parts = cmd.strip().split(maxsplit=1)
2073
+
2074
+ if len(parts) < 2:
2075
+ # Show current state
2076
+ rc = self.reasoning_config
2077
+ if rc is None:
2078
+ level = "medium (default)"
2079
+ elif rc.get("enabled") is False:
2080
+ level = "none (disabled)"
2081
+ else:
2082
+ level = rc.get("effort", "medium")
2083
+ display_state = "on ✓" if self.show_reasoning else "off"
2084
+ _cprint(f" {_ACCENT}Reasoning effort: {level}{_RST}")
2085
+ _cprint(f" {_ACCENT}Reasoning display: {display_state}{_RST}")
2086
+ _cprint(f" {_DIM}Usage: /reasoning <none|minimal|low|medium|high|xhigh|show|hide>{_RST}")
2087
+ return
2088
+
2089
+ arg = parts[1].strip().lower()
2090
+
2091
+ # Display toggle
2092
+ if arg in {"show", "on"}:
2093
+ self.show_reasoning = True
2094
+ if self.agent:
2095
+ self.agent.reasoning_callback = self._current_reasoning_callback()
2096
+ save_config_value("display.show_reasoning", True)
2097
+ _cprint(f" {_ACCENT}✓ Reasoning display: ON (saved){_RST}")
2098
+ _cprint(f" {_DIM} Model thinking will be shown during and after each response.{_RST}")
2099
+ return
2100
+ if arg in {"hide", "off"}:
2101
+ self.show_reasoning = False
2102
+ if self.agent:
2103
+ self.agent.reasoning_callback = self._current_reasoning_callback()
2104
+ save_config_value("display.show_reasoning", False)
2105
+ _cprint(f" {_ACCENT}✓ Reasoning display: OFF (saved){_RST}")
2106
+ return
2107
+
2108
+ # Effort level change
2109
+ parsed = _parse_reasoning_config(arg)
2110
+ if parsed is None:
2111
+ _cprint(f" {_DIM}(._.) Unknown argument: {arg}{_RST}")
2112
+ _cprint(f" {_DIM}Valid levels: none, minimal, low, medium, high, xhigh{_RST}")
2113
+ _cprint(f" {_DIM}Display: show, hide{_RST}")
2114
+ return
2115
+
2116
+ self.reasoning_config = parsed
2117
+ self.agent = None # Force agent re-init with new reasoning config
2118
+
2119
+ if save_config_value("agent.reasoning_effort", arg):
2120
+ _cprint(f" {_ACCENT}✓ Reasoning effort set to '{arg}' (saved to config){_RST}")
2121
+ else:
2122
+ _cprint(f" {_ACCENT}✓ Reasoning effort set to '{arg}' (session only){_RST}")
2123
+
2124
+ def _handle_busy_command(self, cmd: str):
2125
+ """Handle /busy — control what Enter does while Hermes is working.
2126
+
2127
+ Usage:
2128
+ /busy Show current busy input mode
2129
+ /busy status Show current busy input mode
2130
+ /busy queue Queue input for the next turn instead of interrupting
2131
+ /busy steer Inject Enter mid-run via /steer (after next tool call)
2132
+ /busy interrupt Interrupt the current run on Enter (default)
2133
+ """
2134
+ from cli import _ACCENT, _DIM, _RST, _cprint, save_config_value
2135
+ parts = cmd.strip().split(maxsplit=1)
2136
+ if len(parts) < 2 or parts[1].strip().lower() == "status":
2137
+ _cprint(f" {_ACCENT}Busy input mode: {self.busy_input_mode}{_RST}")
2138
+ if self.busy_input_mode == "queue":
2139
+ _behavior = "queues for next turn"
2140
+ elif self.busy_input_mode == "steer":
2141
+ _behavior = "steers into current run (after next tool call)"
2142
+ else:
2143
+ _behavior = "interrupts current run"
2144
+ _cprint(f" {_DIM}Enter while busy: {_behavior}{_RST}")
2145
+ _cprint(f" {_DIM}Usage: /busy [queue|steer|interrupt|status]{_RST}")
2146
+ return
2147
+
2148
+ arg = parts[1].strip().lower()
2149
+ if arg not in {"queue", "interrupt", "steer"}:
2150
+ _cprint(f" {_DIM}(._.) Unknown argument: {arg}{_RST}")
2151
+ _cprint(f" {_DIM}Usage: /busy [queue|steer|interrupt|status]{_RST}")
2152
+ return
2153
+
2154
+ self.busy_input_mode = arg
2155
+ if save_config_value("display.busy_input_mode", arg):
2156
+ if arg == "queue":
2157
+ behavior = "Enter will queue follow-up input while Hermes is busy."
2158
+ elif arg == "steer":
2159
+ behavior = "Enter will steer your message into the current run (after the next tool call)."
2160
+ else:
2161
+ behavior = "Enter will interrupt the current run while Hermes is busy."
2162
+ _cprint(f" {_ACCENT}✓ Busy input mode set to '{arg}' (saved to config){_RST}")
2163
+ _cprint(f" {_DIM}{behavior}{_RST}")
2164
+ else:
2165
+ _cprint(f" {_ACCENT}✓ Busy input mode set to '{arg}' (session only){_RST}")
2166
+
2167
+ def _handle_fast_command(self, cmd: str):
2168
+ """Handle /fast — toggle fast mode (OpenAI Priority Processing / Anthropic Fast Mode)."""
2169
+ from cli import _ACCENT, _DIM, _RST, _cprint, save_config_value
2170
+ if not self._fast_command_available():
2171
+ _cprint(" (._.) /fast is only available for models that support fast mode (OpenAI Priority Processing or Anthropic Fast Mode).")
2172
+ return
2173
+
2174
+ # Determine the branding for the current model
2175
+ try:
2176
+ from hermes_cli.models import _is_anthropic_fast_model
2177
+ agent = getattr(self, "agent", None)
2178
+ model = getattr(agent, "model", None) or getattr(self, "model", None)
2179
+ feature_name = "Anthropic Fast Mode" if _is_anthropic_fast_model(model) else "Priority Processing"
2180
+ except Exception:
2181
+ feature_name = "Fast mode"
2182
+
2183
+ parts = cmd.strip().split(maxsplit=1)
2184
+ if len(parts) < 2 or parts[1].strip().lower() == "status":
2185
+ status = "fast" if self.service_tier == "priority" else "normal"
2186
+ _cprint(f" {_ACCENT}{feature_name}: {status}{_RST}")
2187
+ _cprint(f" {_DIM}Usage: /fast [normal|fast|status]{_RST}")
2188
+ return
2189
+
2190
+ arg = parts[1].strip().lower()
2191
+
2192
+ if arg in {"fast", "on"}:
2193
+ self.service_tier = "priority"
2194
+ saved_value = "fast"
2195
+ label = "FAST"
2196
+ elif arg in {"normal", "off"}:
2197
+ self.service_tier = None
2198
+ saved_value = "normal"
2199
+ label = "NORMAL"
2200
+ else:
2201
+ _cprint(f" {_DIM}(._.) Unknown argument: {arg}{_RST}")
2202
+ _cprint(f" {_DIM}Usage: /fast [normal|fast|status]{_RST}")
2203
+ return
2204
+
2205
+ self.agent = None # Force agent re-init with new service-tier config
2206
+ if save_config_value("agent.service_tier", saved_value):
2207
+ _cprint(f" {_ACCENT}✓ {feature_name} set to {label} (saved to config){_RST}")
2208
+ else:
2209
+ _cprint(f" {_ACCENT}✓ {feature_name} set to {label} (session only){_RST}")
2210
+
2211
+ def _handle_debug_command(self):
2212
+ """Handle /debug — upload debug report + logs and print paste URLs."""
2213
+ from hermes_cli.debug import run_debug_share
2214
+ from types import SimpleNamespace
2215
+
2216
+ args = SimpleNamespace(lines=200, expire=7, local=False)
2217
+ run_debug_share(args)
2218
+
2219
+ def _handle_update_command(self) -> bool:
2220
+ """Handle /update — update Hermes Agent to the latest version.
2221
+
2222
+ In the classic CLI this exits the session and relaunches as
2223
+ ``hermes update`` so the user sees update output directly and gets
2224
+ the new version on next launch.
2225
+
2226
+ Returns ``True`` when the update was confirmed (caller should trigger
2227
+ app exit so the relaunch is deferred to the main thread after
2228
+ prompt_toolkit cleans up terminal modes). Returns ``False`` / falsy
2229
+ when cancelled.
2230
+ """
2231
+ from hermes_cli.config import is_managed, format_managed_message
2232
+
2233
+ if is_managed():
2234
+ print(f" ✗ {format_managed_message('update Hermes Agent')}")
2235
+ return False
2236
+
2237
+ # Use the prompt_toolkit-native modal so the confirmation panel
2238
+ # renders properly above the composer and avoids raw input() races
2239
+ # with the prompt_toolkit event loop (same pattern as
2240
+ # _confirm_destructive_slash).
2241
+ choices = [
2242
+ ("once", "Update Now", "exit the current session and update Hermes Agent"),
2243
+ ("cancel", "Cancel", "keep the current session"),
2244
+ ]
2245
+ raw = self._prompt_text_input_modal(
2246
+ title="⚕ Update Hermes Agent",
2247
+ detail="This will exit the current session and run `hermes update`.",
2248
+ choices=choices,
2249
+ )
2250
+ if raw is None:
2251
+ print(" 🟡 /update cancelled.")
2252
+ return False
2253
+ choice = self._normalize_slash_confirm_choice(raw, choices)
2254
+ if choice != "once":
2255
+ print(" 🟡 /update cancelled.")
2256
+ return False
2257
+
2258
+ print()
2259
+ print(" ⚕ Launching update...")
2260
+ print()
2261
+
2262
+ # Store the relaunch args so run() can exec them from the main thread
2263
+ # after prompt_toolkit exits and restores terminal modes. Calling
2264
+ # relaunch() directly here (from the process_loop daemon thread) would
2265
+ # skip terminal cleanup on POSIX (execvp replaces the process mid-TUI)
2266
+ # and only exit the worker thread on Windows (subprocess.run +
2267
+ # sys.exit inside a non-main thread does not exit the process).
2268
+ self._pending_relaunch = ["update"]
2269
+ return True
2270
+
2271
+ def _handle_voice_command(self, command: str):
2272
+ """Handle /voice [on|off|tts|status] command."""
2273
+ from cli import _cprint
2274
+ parts = command.strip().split(maxsplit=1)
2275
+ subcommand = parts[1].lower().strip() if len(parts) > 1 else ""
2276
+
2277
+ if subcommand == "on":
2278
+ self._enable_voice_mode()
2279
+ elif subcommand == "off":
2280
+ self._disable_voice_mode()
2281
+ elif subcommand == "tts":
2282
+ self._toggle_voice_tts()
2283
+ elif subcommand == "status":
2284
+ self._show_voice_status()
2285
+ elif subcommand == "":
2286
+ # Toggle
2287
+ if self._voice_mode:
2288
+ self._disable_voice_mode()
2289
+ else:
2290
+ self._enable_voice_mode()
2291
+ else:
2292
+ _cprint(f"Unknown voice subcommand: {subcommand}")
2293
+ _cprint("Usage: /voice [on|off|tts|status]")