@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,1586 @@
1
+ """
2
+ Photon Spectrum (iMessage) platform adapter for Hermes Agent.
3
+
4
+ Both directions of traffic flow through a small supervised Node sidecar
5
+ (see ``sidecar/index.mjs``) that runs the ``spectrum-ts`` SDK — the SDK is
6
+ TypeScript-only and there is no public HTTP message API, so a sidecar is
7
+ unavoidable.
8
+
9
+ Inbound:
10
+ The SDK's ``app.messages`` is a long-lived **gRPC** stream. The sidecar
11
+ serializes each message to a normalized JSON event and streams it to this
12
+ adapter over a loopback ``GET /inbound`` (NDJSON). A background task here
13
+ consumes that stream, dedupes on ``messageId``, and dispatches a
14
+ ``MessageEvent`` to the gateway via ``BasePlatformAdapter.handle_message``.
15
+ No webhook, no public URL, no signing secret.
16
+
17
+ Outbound:
18
+ ``send`` / ``send_typing`` are loopback POSTs to the sidecar's control
19
+ endpoints, authenticated with a shared bearer token. Outbound media
20
+ (images, voice notes, video, documents) goes through spectrum-ts'
21
+ ``attachment()`` / ``voice()`` content builders via the sidecar's
22
+ ``/send-attachment`` endpoint.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ import base64
28
+ import json
29
+ import logging
30
+ import os
31
+ import re
32
+ import secrets
33
+ import shutil
34
+ import signal
35
+ import subprocess
36
+ import sys
37
+ import time
38
+ from datetime import datetime, timezone
39
+ from pathlib import Path
40
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
41
+
42
+ if TYPE_CHECKING:
43
+ # Type checkers see ``httpx`` as the always-imported module, so every use
44
+ # site type-checks cleanly. The runtime fallback below keeps the optional
45
+ # dependency truly optional (each use site is guarded by HTTPX_AVAILABLE).
46
+ import httpx
47
+ HTTPX_AVAILABLE = True
48
+ else:
49
+ try:
50
+ import httpx
51
+ HTTPX_AVAILABLE = True
52
+ except ImportError: # pragma: no cover - httpx is already a Hermes dep
53
+ HTTPX_AVAILABLE = False
54
+ httpx = None
55
+
56
+ from gateway.config import Platform, PlatformConfig
57
+ from gateway.platforms.base import (
58
+ BasePlatformAdapter,
59
+ MessageEvent,
60
+ MessageType,
61
+ ProcessingOutcome,
62
+ SendResult,
63
+ )
64
+ from gateway.platforms.helpers import strip_markdown
65
+
66
+ from .auth import load_project_credentials
67
+
68
+ logger = logging.getLogger(__name__)
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Constants
72
+
73
+ _DEFAULT_SIDECAR_PORT = 8789
74
+ _DEFAULT_SIDECAR_BIND = "127.0.0.1"
75
+
76
+ # Photon iMessage messages from the SDK side have no documented hard
77
+ # limit, but the underlying iMessage protocol limits practical message
78
+ # size to ~16 KB. Keep a conservative cap that matches BlueBubbles.
79
+ _MAX_MESSAGE_LENGTH = 8000
80
+
81
+ # Dedup parameters — the gRPC stream is at-least-once, and a sidecar
82
+ # reconnect can replay, so keep at least 1k ids for ~48h.
83
+ _DEDUP_MAX_SIZE = 4000
84
+ _DEDUP_WINDOW_SECONDS = 48 * 3600
85
+
86
+ _SIDECAR_DIR = Path(__file__).parent / "sidecar"
87
+
88
+ # Group-chat mention wake words. When ``require_mention`` is enabled, group
89
+ # messages are ignored unless they match one of these patterns — same
90
+ # behavior and defaults as the BlueBubbles iMessage channel so the two
91
+ # iMessage adapters gate group chats identically.
92
+ _DEFAULT_MENTION_PATTERNS = [
93
+ r"(?<![\w@])@?hermes\s+agent\b[,:\-]?",
94
+ r"(?<![\w@])@?hermes\b[,:\-]?",
95
+ ]
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # Module-level helpers — also used by check_fn / standalone send
100
+
101
+ def _coerce_port(value: Any, default: int) -> int:
102
+ try:
103
+ return int(value)
104
+ except (TypeError, ValueError):
105
+ return default
106
+
107
+
108
+ def check_requirements() -> bool:
109
+ """Return True when both Python deps and the Node sidecar are available."""
110
+ if not HTTPX_AVAILABLE:
111
+ return False
112
+ if not shutil.which(os.getenv("PHOTON_NODE_BIN") or "node"):
113
+ return False
114
+ if not (_SIDECAR_DIR / "node_modules").exists():
115
+ # spectrum-ts not installed yet — `hermes photon setup` will
116
+ # install it. check_fn still returns False so the gateway
117
+ # surfaces the missing-deps state in `hermes setup` / status.
118
+ return False
119
+ return True
120
+
121
+
122
+ def validate_config(cfg: PlatformConfig) -> bool:
123
+ extra = cfg.extra or {}
124
+ project_id = extra.get("project_id") or os.getenv("PHOTON_PROJECT_ID")
125
+ project_secret = extra.get("project_secret") or os.getenv("PHOTON_PROJECT_SECRET")
126
+ if not project_id or not project_secret:
127
+ # Fall back to auth.json
128
+ stored_id, stored_sec = load_project_credentials()
129
+ return bool(stored_id and stored_sec)
130
+ return True
131
+
132
+
133
+ def is_connected(cfg: PlatformConfig) -> bool:
134
+ return validate_config(cfg)
135
+
136
+
137
+ def _env_enablement() -> Optional[dict]:
138
+ """Seed PlatformConfig.extra from env so env-only setups appear in status.
139
+
140
+ The special ``home_channel`` key is handled by the core plugin hook and
141
+ becomes a proper ``HomeChannel`` on ``PlatformConfig``.
142
+ """
143
+ project_id, project_secret = load_project_credentials()
144
+ if not (project_id and project_secret):
145
+ return None
146
+ seed: dict = {"project_id": project_id, "project_secret": project_secret}
147
+ home = os.getenv("PHOTON_HOME_CHANNEL", "").strip()
148
+ if home:
149
+ seed["home_channel"] = {
150
+ "chat_id": home,
151
+ "name": os.getenv("PHOTON_HOME_CHANNEL_NAME", "Home"),
152
+ }
153
+ return seed
154
+
155
+
156
+ def _markdown_enabled() -> bool:
157
+ """Send agent replies as markdown (spectrum-ts ``markdown()`` builder).
158
+
159
+ iMessage renders it natively; other Spectrum platforms degrade to
160
+ readable plain text. On-device rendering can't be unit-tested, so
161
+ ``PHOTON_MARKDOWN=false`` is the kill-switch back to stripped plain
162
+ text without a release.
163
+ """
164
+ return os.getenv("PHOTON_MARKDOWN", "true").strip().lower() not in {
165
+ "false", "0", "no",
166
+ }
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # Adapter
171
+
172
+ class PhotonAdapter(BasePlatformAdapter):
173
+ """Bidirectional bridge to Photon Spectrum via the Node spectrum-ts sidecar.
174
+
175
+ Inbound: consume the sidecar's ``/inbound`` gRPC stream.
176
+ Outbound: loopback POSTs to the sidecar's control channel.
177
+ """
178
+
179
+ MAX_MESSAGE_LENGTH = _MAX_MESSAGE_LENGTH
180
+
181
+ def __init__(self, config: PlatformConfig):
182
+ super().__init__(config, Platform("photon"))
183
+ extra = config.extra or {}
184
+
185
+ # Project credentials (env wins, then config.extra, then auth.json).
186
+ # ``project_id`` here is the project's spectrumProjectId — the value
187
+ # the spectrum-ts SDK authenticates with.
188
+ stored_id, stored_sec = load_project_credentials()
189
+ self._project_id: str = (
190
+ os.getenv("PHOTON_PROJECT_ID")
191
+ or extra.get("project_id")
192
+ or stored_id
193
+ or ""
194
+ )
195
+ self._project_secret: str = (
196
+ os.getenv("PHOTON_PROJECT_SECRET")
197
+ or extra.get("project_secret")
198
+ or stored_sec
199
+ or ""
200
+ )
201
+
202
+ # Sidecar
203
+ self._sidecar_port = _coerce_port(
204
+ extra.get("sidecar_port") or os.getenv("PHOTON_SIDECAR_PORT"),
205
+ _DEFAULT_SIDECAR_PORT,
206
+ )
207
+ self._sidecar_bind = _DEFAULT_SIDECAR_BIND
208
+ self._sidecar_token = (
209
+ os.getenv("PHOTON_SIDECAR_TOKEN") or secrets.token_hex(16)
210
+ )
211
+ self._autostart_sidecar = str(
212
+ os.getenv("PHOTON_SIDECAR_AUTOSTART", "true")
213
+ ).lower() not in ("0", "false", "no")
214
+ self._node_bin = os.getenv("PHOTON_NODE_BIN") or shutil.which("node") or "node"
215
+
216
+ # With markdown on, format_message preserves fences and the sidecar's
217
+ # markdown() builder renders them (or degrades them readably).
218
+ self.supports_code_blocks = _markdown_enabled()
219
+
220
+ # Runtime state
221
+ self._sidecar_proc: Optional[subprocess.Popen] = None
222
+ self._sidecar_supervisor_task: Optional[asyncio.Task] = None
223
+ self._inbound_task: Optional[asyncio.Task] = None
224
+ self._inbound_running = False
225
+ self._http_client: Optional["httpx.AsyncClient"] = None
226
+ # Lightweight in-memory dedup. The gRPC stream is at-least-once, so we
227
+ # may see the same messageId more than once (e.g. after a reconnect).
228
+ self._seen_messages: Dict[str, float] = {}
229
+ # Ids of messages WE sent (bounded, insertion-order eviction). Inbound
230
+ # reaction events are only routed to the agent when they target one of
231
+ # these — a tapback on a human↔human message is not addressed to us.
232
+ self._sent_message_ids: Dict[str, float] = {}
233
+ # Latest inbound message id per chat (bounded). Lets the agent-facing
234
+ # react action default to "the message that triggered me" without
235
+ # requiring the model to thread message ids through tool calls.
236
+ self._last_inbound_by_chat: Dict[str, str] = {}
237
+
238
+ # Group-chat mention gating (parity with BlueBubbles). When enabled,
239
+ # group messages are ignored unless they match a wake word; DMs are
240
+ # always processed. Config key wins, then env var.
241
+ _require_mention = extra.get("require_mention")
242
+ if _require_mention is None:
243
+ _require_mention = os.getenv("PHOTON_REQUIRE_MENTION")
244
+ self.require_mention = str(_require_mention).strip().lower() in {
245
+ "true", "1", "yes", "on",
246
+ }
247
+ self._mention_patterns = self._compile_mention_patterns(
248
+ extra["mention_patterns"]
249
+ if "mention_patterns" in extra
250
+ else os.getenv("PHOTON_MENTION_PATTERNS")
251
+ )
252
+
253
+ # -- Group-mention gating (parity with BlueBubbles) -------------------
254
+
255
+ @staticmethod
256
+ def _compile_mention_patterns(raw: Any) -> "list[re.Pattern]":
257
+ """Compile group-mention wake words from config/env.
258
+
259
+ ``raw`` is a list (config or env JSON), a string (env var: JSON
260
+ list, or comma/newline-separated), or None (use Hermes defaults).
261
+ Mirrors the BlueBubbles implementation so both iMessage channels
262
+ accept the same configuration shapes.
263
+ """
264
+ if raw is None:
265
+ patterns = list(_DEFAULT_MENTION_PATTERNS)
266
+ elif isinstance(raw, str):
267
+ text = raw.strip()
268
+ try:
269
+ loaded = json.loads(text) if text else []
270
+ except Exception:
271
+ loaded = None
272
+ patterns = loaded if isinstance(loaded, list) else [
273
+ part.strip()
274
+ for line in text.splitlines()
275
+ for part in line.split(",")
276
+ ]
277
+ elif isinstance(raw, list):
278
+ patterns = raw
279
+ else:
280
+ patterns = [raw]
281
+
282
+ compiled: "list[re.Pattern]" = []
283
+ for pattern in patterns:
284
+ text = str(pattern).strip()
285
+ if not text:
286
+ continue
287
+ try:
288
+ compiled.append(re.compile(text, re.IGNORECASE))
289
+ except re.error as exc:
290
+ logger.warning("[photon] Invalid mention pattern %r: %s", text, exc)
291
+ return compiled
292
+
293
+ def _message_matches_mention_patterns(self, text: str) -> bool:
294
+ if not text or not self._mention_patterns:
295
+ return False
296
+ return any(pattern.search(text) for pattern in self._mention_patterns)
297
+
298
+ def _clean_mention_text(self, text: str) -> str:
299
+ """Strip a leading wake word before dispatch.
300
+
301
+ Custom mention patterns are regexes, so we only strip a leading
302
+ match to avoid deleting ordinary words later in the prompt.
303
+ """
304
+ if not text:
305
+ return text
306
+ for pattern in self._mention_patterns:
307
+ match = pattern.match(text.lstrip())
308
+ if match:
309
+ cleaned = text.lstrip()[match.end():].lstrip(" ,:-")
310
+ return cleaned or text
311
+ return text
312
+
313
+ # -- Connection lifecycle ---------------------------------------------
314
+
315
+ async def connect(self) -> bool:
316
+ if not HTTPX_AVAILABLE:
317
+ self._set_fatal_error(
318
+ "MISSING_DEP", "httpx not installed", retryable=False
319
+ )
320
+ return False
321
+ if not self._project_id or not self._project_secret:
322
+ self._set_fatal_error(
323
+ "MISSING_CREDENTIALS",
324
+ "PHOTON_PROJECT_ID and PHOTON_PROJECT_SECRET are required. "
325
+ "Run: hermes photon setup",
326
+ retryable=False,
327
+ )
328
+ return False
329
+
330
+ client = httpx.AsyncClient(timeout=30.0)
331
+ self._http_client = client
332
+
333
+ # The sidecar holds the gRPC stream for BOTH directions, so it is
334
+ # required now (not just for outbound).
335
+ if self._autostart_sidecar:
336
+ try:
337
+ await self._start_sidecar()
338
+ except Exception as e:
339
+ self._set_fatal_error(
340
+ "SIDECAR_FAILED",
341
+ f"failed to start Photon sidecar: {e}",
342
+ retryable=True,
343
+ )
344
+ await client.aclose()
345
+ self._http_client = None
346
+ return False
347
+ else:
348
+ logger.warning(
349
+ "[photon] sidecar autostart disabled — inbound + outbound will fail"
350
+ )
351
+
352
+ # Start consuming the inbound gRPC stream from the sidecar.
353
+ self._inbound_running = True
354
+ self._inbound_task = asyncio.get_event_loop().create_task(
355
+ self._inbound_loop()
356
+ )
357
+
358
+ self._mark_connected()
359
+ logger.info(
360
+ "[photon] connected — sidecar on %s:%d, streaming inbound over gRPC",
361
+ self._sidecar_bind, self._sidecar_port,
362
+ )
363
+ return True
364
+
365
+ async def disconnect(self) -> None:
366
+ self._inbound_running = False
367
+ if self._inbound_task is not None:
368
+ self._inbound_task.cancel()
369
+ try:
370
+ await self._inbound_task
371
+ except asyncio.CancelledError:
372
+ pass
373
+ except Exception:
374
+ pass
375
+ self._inbound_task = None
376
+ await self._stop_sidecar()
377
+ if self._http_client is not None:
378
+ try:
379
+ await self._http_client.aclose()
380
+ except Exception:
381
+ pass
382
+ self._http_client = None
383
+ self._mark_disconnected()
384
+
385
+ # -- Inbound stream consumer ------------------------------------------
386
+
387
+ async def _inbound_loop(self) -> None:
388
+ """Consume the sidecar's ``/inbound`` NDJSON stream, with reconnect.
389
+
390
+ The sidecar owns the gRPC reconnect/heartbeat to Photon; this loop
391
+ only has to re-open the loopback HTTP stream if it drops (e.g. the
392
+ sidecar restarts).
393
+ """
394
+ client = self._http_client
395
+ if client is None:
396
+ return
397
+ url = f"http://{self._sidecar_bind}:{self._sidecar_port}/inbound"
398
+ headers = {"X-Hermes-Sidecar-Token": self._sidecar_token}
399
+ backoff = 1.0
400
+ while self._inbound_running:
401
+ try:
402
+ async with client.stream(
403
+ "GET", url, headers=headers, timeout=None,
404
+ ) as resp:
405
+ if resp.status_code != 200:
406
+ raise RuntimeError(f"/inbound returned {resp.status_code}")
407
+ backoff = 1.0 # reset on a successful connect
408
+ async for line in resp.aiter_lines():
409
+ if not self._inbound_running:
410
+ break
411
+ line = line.strip()
412
+ if not line:
413
+ continue # heartbeat
414
+ await self._on_inbound_line(line)
415
+ except asyncio.CancelledError:
416
+ raise
417
+ except Exception as e:
418
+ if not self._inbound_running:
419
+ break
420
+ logger.warning(
421
+ "[photon] inbound stream dropped (%s); reconnecting in %.1fs",
422
+ e, backoff,
423
+ )
424
+ await asyncio.sleep(backoff)
425
+ backoff = min(backoff * 2, 30.0)
426
+
427
+ async def _on_inbound_line(self, line: str) -> None:
428
+ try:
429
+ event = json.loads(line)
430
+ except json.JSONDecodeError:
431
+ logger.debug("[photon] skipping non-JSON inbound line")
432
+ return
433
+ msg_id = event.get("messageId")
434
+ if msg_id and self._is_duplicate(msg_id):
435
+ return
436
+ try:
437
+ await self._dispatch_inbound(event)
438
+ except Exception:
439
+ logger.exception("[photon] inbound dispatch failed")
440
+
441
+ def _is_duplicate(self, msg_id: str) -> bool:
442
+ now = time.time()
443
+ seen = self._seen_messages
444
+ t = seen.get(msg_id)
445
+ if t is not None and now - t < _DEDUP_WINDOW_SECONDS:
446
+ return True # seen, unexpired
447
+ # New or expired: record and enforce a HARD size bound (evict oldest,
448
+ # insertion-order) so a burst of unique ids within the window can't grow
449
+ # the dict without limit — not just the expired-only prune.
450
+ if msg_id in seen:
451
+ del seen[msg_id] # refresh insertion order
452
+ seen[msg_id] = now
453
+ if len(seen) > _DEDUP_MAX_SIZE:
454
+ for old in list(seen.keys())[: len(seen) - _DEDUP_MAX_SIZE]:
455
+ del seen[old]
456
+ return False
457
+
458
+ async def _dispatch_inbound(self, event: Dict[str, Any]) -> None:
459
+ """Normalize a sidecar inbound event and dispatch it to the gateway.
460
+
461
+ Event shape (from ``sidecar/index.mjs``)::
462
+
463
+ {
464
+ "messageId": "...",
465
+ "platform": "iMessage",
466
+ "space": {"id": "...", "type": "dm"|"group", "phone": "+E164"},
467
+ "sender": {"id": "+E164"},
468
+ "content": {"type": "text", "text": "..."}
469
+ | {"type": "attachment"|"voice", "id", "name",
470
+ "mimeType", "size", "duration"?, "data"?,
471
+ "encoding"?}
472
+ | {"type": "reaction", "emoji": "❤️",
473
+ "targetMessageId": "..." | null,
474
+ "targetDirection": "inbound"|"outbound" | null},
475
+ "timestamp": "2026-05-14T19:06:32.000Z"
476
+
477
+ Attachment and voice content carry the bytes inline as base64 ``data``
478
+ (with ``encoding == "base64"``) when the sidecar could read them
479
+ within its size cap; otherwise only metadata is present and we surface
480
+ a marker.
481
+ }
482
+ """
483
+ space = event.get("space") or {}
484
+ sender = event.get("sender") or {}
485
+ content = event.get("content") or {}
486
+
487
+ space_id = space.get("id") or ""
488
+ if not space_id:
489
+ logger.warning("[photon] inbound missing space.id")
490
+ return
491
+
492
+ # iMessage spaces carry their type directly — no id string-sniffing.
493
+ chat_type = "group" if space.get("type") == "group" else "dm"
494
+ sender_id = sender.get("id") or space.get("phone") or space_id
495
+
496
+ ts_str = event.get("timestamp") or ""
497
+ try:
498
+ timestamp = (
499
+ datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
500
+ if ts_str
501
+ else datetime.now(tz=timezone.utc)
502
+ )
503
+ except ValueError:
504
+ timestamp = datetime.now(tz=timezone.utc)
505
+
506
+ # Media attachments (local cached paths) handed to the agent via the
507
+ # gateway's image-routing path, exactly like the BlueBubbles channel.
508
+ media_urls: List[str] = []
509
+ media_types: List[str] = []
510
+
511
+ def _normalize_binary_payload(
512
+ payload: Dict[str, Any]
513
+ ) -> tuple[str, MessageType, List[str], List[str]]:
514
+ is_voice = payload.get("type") == "voice"
515
+ name = payload.get("name") or ("voice" if is_voice else "(unnamed)")
516
+ mime = payload.get("mimeType") or ""
517
+ mtype = MessageType.VOICE if is_voice else _attachment_message_type(mime)
518
+ cached = _cache_inbound_attachment(
519
+ payload, name, mime, force_audio=is_voice
520
+ )
521
+ if cached:
522
+ return (
523
+ "(voice)" if is_voice else "(attachment)",
524
+ mtype,
525
+ [cached],
526
+ [mime or ("audio/mp4" if is_voice else "application/octet-stream")],
527
+ )
528
+ label = "voice" if is_voice else "attachment"
529
+ duration = payload.get("duration")
530
+ duration_text = (
531
+ f", duration: {duration}s"
532
+ if isinstance(duration, (int, float))
533
+ else ""
534
+ )
535
+ return (
536
+ f"[Photon {label} received: {name} "
537
+ f"({mime or 'unknown MIME'}{duration_text})]",
538
+ mtype,
539
+ [],
540
+ [],
541
+ )
542
+
543
+ ctype = content.get("type")
544
+ if ctype == "reaction":
545
+ # Route only tapbacks on messages WE sent — those are implicitly
546
+ # addressed to the bot (feishu precedent: synthetic text event).
547
+ # Reactions on human↔human messages are not for us. Checked before
548
+ # the mention gate: a tapback never carries a wake word.
549
+ target_id = content.get("targetMessageId")
550
+ is_ours = content.get("targetDirection") == "outbound" or (
551
+ target_id and target_id in self._sent_message_ids
552
+ )
553
+ if not is_ours:
554
+ logger.debug(
555
+ "[photon] ignoring reaction on a message we didn't send"
556
+ )
557
+ return
558
+ emoji = content.get("emoji") or ""
559
+ source = self.build_source(
560
+ chat_id=space_id,
561
+ chat_name=space_id,
562
+ chat_type=chat_type,
563
+ user_id=sender_id,
564
+ user_name=sender_id or None,
565
+ )
566
+ await self.handle_message(
567
+ MessageEvent(
568
+ text=f"reaction:added:{emoji}",
569
+ message_type=MessageType.TEXT,
570
+ source=source,
571
+ message_id=event.get("messageId"),
572
+ raw_message=event,
573
+ timestamp=timestamp,
574
+ )
575
+ )
576
+ return
577
+ # Anything past here is a real (reactable) message — remember it as
578
+ # the chat's latest inbound so `add_reaction` can target it when the
579
+ # caller doesn't pass an explicit message id. Recorded before the
580
+ # mention gate: a reaction to a non-wake-word group message is valid.
581
+ self._record_last_inbound(space_id, event.get("messageId"))
582
+ if ctype == "text":
583
+ text = content.get("text") or ""
584
+ mtype = MessageType.TEXT
585
+ elif ctype in {"attachment", "voice"}:
586
+ text, mtype, media_urls, media_types = _normalize_binary_payload(content)
587
+ elif ctype == "group":
588
+ text_parts: List[str] = []
589
+ mtype = MessageType.TEXT
590
+ for item in content.get("items") or []:
591
+ if not isinstance(item, dict):
592
+ continue
593
+ item_content = item.get("content") or {}
594
+ if not isinstance(item_content, dict):
595
+ continue
596
+ item_type = item_content.get("type")
597
+ if item_type == "text":
598
+ item_text = item_content.get("text") or ""
599
+ if item_text:
600
+ text_parts.append(item_text)
601
+ continue
602
+ if item_type in {"attachment", "voice"}:
603
+ marker, item_mtype, item_urls, item_types = _normalize_binary_payload(
604
+ item_content
605
+ )
606
+ if mtype == MessageType.TEXT:
607
+ mtype = item_mtype
608
+ media_urls.extend(item_urls)
609
+ media_types.extend(item_types)
610
+ if not item_urls:
611
+ text_parts.append(marker)
612
+ continue
613
+ if item_type:
614
+ text_parts.append(f"[Photon content type not handled: {item_type}]")
615
+ if media_urls and mtype == MessageType.TEXT:
616
+ mtype = MessageType.DOCUMENT
617
+ text = "\n".join(part for part in text_parts if part).strip()
618
+ if not text:
619
+ text = "(attachment)" if media_urls else "[Photon empty group received]"
620
+ else:
621
+ text = f"[Photon content type not handled: {ctype}]"
622
+ mtype = MessageType.TEXT
623
+
624
+ # Group-mention gating (parity with BlueBubbles). In group chats with
625
+ # require_mention enabled, drop messages that don't hit a wake word;
626
+ # strip the leading wake word from the ones that do. DMs are never
627
+ # gated.
628
+ if chat_type == "group" and self.require_mention:
629
+ if not self._message_matches_mention_patterns(text):
630
+ logger.debug(
631
+ "[photon] ignoring group message "
632
+ "(require_mention=true, no mention pattern matched)"
633
+ )
634
+ return
635
+ text = self._clean_mention_text(text)
636
+
637
+ source = self.build_source(
638
+ chat_id=space_id,
639
+ chat_name=space_id,
640
+ chat_type=chat_type,
641
+ user_id=sender_id,
642
+ user_name=sender_id or None,
643
+ )
644
+ message_event = MessageEvent(
645
+ text=text,
646
+ message_type=mtype,
647
+ source=source,
648
+ message_id=event.get("messageId"),
649
+ raw_message=event,
650
+ timestamp=timestamp,
651
+ media_urls=media_urls,
652
+ media_types=media_types,
653
+ )
654
+ await self.handle_message(message_event)
655
+
656
+ # -- Sidecar lifecycle -------------------------------------------------
657
+
658
+ @staticmethod
659
+ def _find_listener_pids(port: int) -> List[int]:
660
+ """PIDs listening on a local TCP port (empty if none/undeterminable)."""
661
+ try:
662
+ out = subprocess.run( # noqa: S603, S607
663
+ ["lsof", "-ti", f"tcp:{port}", "-sTCP:LISTEN"],
664
+ capture_output=True, text=True, timeout=5.0, check=False,
665
+ )
666
+ except (OSError, subprocess.TimeoutExpired):
667
+ return []
668
+ return [int(tok) for tok in out.stdout.split() if tok.strip().isdigit()]
669
+
670
+ @staticmethod
671
+ def _pid_is_sidecar(pid: int) -> bool:
672
+ """True if ``pid``'s command line is a Photon sidecar process."""
673
+ try:
674
+ out = subprocess.run( # noqa: S603, S607
675
+ ["ps", "-p", str(pid), "-o", "command="],
676
+ capture_output=True, text=True, timeout=5.0, check=False,
677
+ )
678
+ except (OSError, subprocess.TimeoutExpired):
679
+ return False
680
+ # Checkout-agnostic: any Hermes checkout's sidecar entry point.
681
+ return "photon/sidecar/index.mjs" in out.stdout
682
+
683
+ @staticmethod
684
+ def _pid_alive(pid: int) -> bool:
685
+ try:
686
+ os.kill(pid, 0) # windows-footgun: ok — only called from _reap_stale_sidecar which win32-guards early
687
+ return True
688
+ except OSError:
689
+ return False
690
+
691
+ async def _reap_stale_sidecar(self) -> None:
692
+ """Kill an orphaned sidecar squatting our port before spawning ours.
693
+
694
+ A hard gateway exit (crash, SIGKILL, supervisor restart) used to leave
695
+ the detached sidecar running with a token the new gateway doesn't
696
+ know, so it can't be told to ``/shutdown`` — and every replacement
697
+ spawn died on EADDRINUSE, failing each reconnect attempt. The
698
+ stdin-EOF watch prevents new orphans; this reclaims the port from
699
+ orphans that predate it (or survived it). Listeners are verified by
700
+ command line before being signalled.
701
+ """
702
+ if sys.platform == "win32": # lsof/ps; orphaning is a POSIX-only path
703
+ return
704
+ try:
705
+ async with httpx.AsyncClient(timeout=2.0) as client:
706
+ await client.post(
707
+ f"http://{self._sidecar_bind}:{self._sidecar_port}/healthz",
708
+ headers={"X-Hermes-Sidecar-Token": self._sidecar_token},
709
+ )
710
+ except httpx.RequestError:
711
+ return # nothing listening — the normal case
712
+ pids = self._find_listener_pids(self._sidecar_port)
713
+ stale = [pid for pid in pids if self._pid_is_sidecar(pid)]
714
+ foreign = [pid for pid in pids if pid not in stale]
715
+ if not stale:
716
+ raise RuntimeError(
717
+ f"port {self._sidecar_port} is in use by another process "
718
+ f"(pids: {foreign or 'unknown'}, not a Photon sidecar) — "
719
+ f"free it or set PHOTON_SIDECAR_PORT to a different port"
720
+ )
721
+ for pid in stale:
722
+ logger.warning(
723
+ "[photon] reaping orphaned sidecar (pid %d) on port %d",
724
+ pid, self._sidecar_port,
725
+ )
726
+ try:
727
+ os.kill(pid, signal.SIGTERM)
728
+ except OSError:
729
+ pass
730
+ deadline = time.time() + 3.0
731
+ while time.time() < deadline and any(self._pid_alive(p) for p in stale):
732
+ await asyncio.sleep(0.1)
733
+ for pid in stale:
734
+ if self._pid_alive(pid):
735
+ try:
736
+ os.kill(pid, signal.SIGKILL) # windows-footgun: ok — unreachable on win32 (early return above)
737
+ except OSError:
738
+ pass
739
+ # Give the OS a beat to release the listening socket.
740
+ await asyncio.sleep(0.2)
741
+ if foreign:
742
+ raise RuntimeError(
743
+ f"port {self._sidecar_port} is also held by non-sidecar "
744
+ f"processes (pids: {foreign}) — free it or set "
745
+ f"PHOTON_SIDECAR_PORT to a different port"
746
+ )
747
+
748
+ async def _start_sidecar(self) -> None:
749
+ if not (_SIDECAR_DIR / "node_modules").exists():
750
+ raise RuntimeError(
751
+ f"Photon sidecar deps not installed. Run: "
752
+ f"cd {_SIDECAR_DIR} && npm install (or `hermes photon setup`)"
753
+ )
754
+ await self._reap_stale_sidecar()
755
+
756
+ env = os.environ.copy()
757
+ env["PHOTON_PROJECT_ID"] = self._project_id
758
+ env["PHOTON_PROJECT_SECRET"] = self._project_secret
759
+ env["PHOTON_SIDECAR_PORT"] = str(self._sidecar_port)
760
+ env["PHOTON_SIDECAR_BIND"] = self._sidecar_bind
761
+ env["PHOTON_SIDECAR_TOKEN"] = self._sidecar_token
762
+ # The sidecar exits when its stdin (the pipe below) hits EOF, so a
763
+ # gateway death of ANY kind — including SIGKILL, where disconnect()
764
+ # never runs — can't leave it orphaned on the port.
765
+ env["PHOTON_SIDECAR_WATCH_STDIN"] = "1"
766
+
767
+ try:
768
+ patch = subprocess.run( # noqa: S603
769
+ [
770
+ self._node_bin,
771
+ str(_SIDECAR_DIR / "patch-spectrum-mixed-attachments.mjs"),
772
+ str(_SIDECAR_DIR),
773
+ ],
774
+ capture_output=True,
775
+ text=True,
776
+ timeout=10,
777
+ check=False,
778
+ )
779
+ if patch.returncode != 0:
780
+ raise RuntimeError((patch.stderr or patch.stdout or "").strip())
781
+ if patch.stderr.strip():
782
+ logger.debug("[photon] %s", patch.stderr.strip())
783
+ except Exception as exc:
784
+ logger.warning(
785
+ "[photon] failed to apply Spectrum mixed attachment patch: %s",
786
+ exc,
787
+ )
788
+
789
+ self._sidecar_proc = subprocess.Popen( # noqa: S603
790
+ [self._node_bin, str(_SIDECAR_DIR / "index.mjs")],
791
+ stdin=subprocess.PIPE,
792
+ stdout=subprocess.PIPE,
793
+ stderr=subprocess.STDOUT,
794
+ env=env,
795
+ start_new_session=(sys.platform != "win32"),
796
+ )
797
+
798
+ # Pump sidecar stderr/stdout into our logger so users see crashes.
799
+ loop = asyncio.get_event_loop()
800
+ self._sidecar_supervisor_task = loop.create_task(
801
+ self._supervise_sidecar(self._sidecar_proc)
802
+ )
803
+
804
+ # Wait for /healthz to come up — give it up to 15s on cold start.
805
+ deadline = time.time() + 15.0
806
+ last_err: Optional[Exception] = None
807
+ async with httpx.AsyncClient(timeout=2.0) as client:
808
+ while time.time() < deadline:
809
+ if self._sidecar_proc.poll() is not None:
810
+ raise RuntimeError(
811
+ f"Photon sidecar exited with code "
812
+ f"{self._sidecar_proc.returncode} before becoming ready"
813
+ )
814
+ try:
815
+ resp = await client.post(
816
+ f"http://{self._sidecar_bind}:{self._sidecar_port}/healthz",
817
+ headers={"X-Hermes-Sidecar-Token": self._sidecar_token},
818
+ )
819
+ if resp.status_code == 200:
820
+ return
821
+ except httpx.RequestError as e:
822
+ last_err = e
823
+ await asyncio.sleep(0.2)
824
+ raise RuntimeError(
825
+ f"Photon sidecar did not become ready within 15s: {last_err}"
826
+ )
827
+
828
+ async def _supervise_sidecar(self, proc: subprocess.Popen) -> None:
829
+ """Pump the sidecar's stdout/stderr into our logger."""
830
+ if proc.stdout is None: # subprocess was launched without stdout=PIPE
831
+ return
832
+ stdout = proc.stdout
833
+ loop = asyncio.get_event_loop()
834
+ try:
835
+ while True:
836
+ line = await loop.run_in_executor(None, stdout.readline)
837
+ if not line:
838
+ break
839
+ logger.info("[photon-sidecar] %s", line.decode("utf-8", "replace").rstrip())
840
+ except Exception as e: # pragma: no cover - defensive
841
+ logger.warning("[photon-sidecar] supervisor exited: %s", e)
842
+
843
+ async def _stop_sidecar(self) -> None:
844
+ proc = self._sidecar_proc
845
+ if proc is None:
846
+ return
847
+ try:
848
+ # Closing our end of the stdin pipe is itself a shutdown signal
849
+ # (the sidecar watches for EOF), and covers the case where the
850
+ # HTTP call below can't get through.
851
+ if proc.stdin is not None:
852
+ try:
853
+ proc.stdin.close()
854
+ except Exception:
855
+ pass
856
+ # Polite shutdown first.
857
+ if self._http_client is not None:
858
+ try:
859
+ await self._http_client.post(
860
+ f"http://{self._sidecar_bind}:{self._sidecar_port}/shutdown",
861
+ headers={"X-Hermes-Sidecar-Token": self._sidecar_token},
862
+ timeout=2.0,
863
+ )
864
+ except Exception:
865
+ pass
866
+ try:
867
+ proc.wait(timeout=3.0)
868
+ except subprocess.TimeoutExpired:
869
+ if sys.platform != "win32":
870
+ try:
871
+ os.killpg(os.getpgid(proc.pid), signal.SIGTERM) # windows-footgun: ok
872
+ except (ProcessLookupError, PermissionError):
873
+ proc.terminate()
874
+ else:
875
+ proc.terminate()
876
+ try:
877
+ proc.wait(timeout=2.0)
878
+ except subprocess.TimeoutExpired:
879
+ proc.kill()
880
+ finally:
881
+ self._sidecar_proc = None
882
+ if self._sidecar_supervisor_task is not None:
883
+ self._sidecar_supervisor_task.cancel()
884
+ self._sidecar_supervisor_task = None
885
+
886
+ # -- Outbound ----------------------------------------------------------
887
+
888
+ async def send(
889
+ self,
890
+ chat_id: str,
891
+ content: str,
892
+ reply_to: Optional[str] = None,
893
+ metadata: Optional[Dict[str, Any]] = None,
894
+ ) -> SendResult:
895
+ return await self._sidecar_send(chat_id, self.format_message(content))
896
+
897
+ # -- Outbound media (parity with the BlueBubbles iMessage channel) -----
898
+ #
899
+ # Photon ships outbound attachments via spectrum-ts' `attachment()` /
900
+ # `voice()` content builders. The sidecar's `/send-attachment` endpoint
901
+ # wraps `space.send(attachment(path, {...}))`. These overrides mirror
902
+ # BlueBubbles: URL-based helpers cache to a local path first, file-based
903
+ # helpers pass the path straight through.
904
+
905
+ async def send_image(
906
+ self,
907
+ chat_id: str,
908
+ image_url: str,
909
+ caption: Optional[str] = None,
910
+ reply_to: Optional[str] = None,
911
+ metadata: Optional[Dict[str, Any]] = None,
912
+ ) -> SendResult:
913
+ try:
914
+ from gateway.platforms.base import cache_image_from_url
915
+
916
+ local_path = await cache_image_from_url(image_url)
917
+ except Exception:
918
+ # Couldn't fetch the URL — fall back to sending it as text.
919
+ return await super().send_image(chat_id, image_url, caption, reply_to)
920
+ return await self._sidecar_send_attachment(
921
+ chat_id, local_path, caption=caption,
922
+ )
923
+
924
+ async def send_image_file(
925
+ self,
926
+ chat_id: str,
927
+ image_path: str,
928
+ caption: Optional[str] = None,
929
+ reply_to: Optional[str] = None,
930
+ metadata: Optional[Dict[str, Any]] = None,
931
+ **kwargs,
932
+ ) -> SendResult:
933
+ return await self._sidecar_send_attachment(
934
+ chat_id, image_path, caption=caption,
935
+ )
936
+
937
+ async def send_voice(
938
+ self,
939
+ chat_id: str,
940
+ audio_path: str,
941
+ caption: Optional[str] = None,
942
+ reply_to: Optional[str] = None,
943
+ metadata: Optional[Dict[str, Any]] = None,
944
+ **kwargs,
945
+ ) -> SendResult:
946
+ return await self._sidecar_send_attachment(
947
+ chat_id, audio_path, caption=caption, kind="voice",
948
+ )
949
+
950
+ async def send_video(
951
+ self,
952
+ chat_id: str,
953
+ video_path: str,
954
+ caption: Optional[str] = None,
955
+ reply_to: Optional[str] = None,
956
+ metadata: Optional[Dict[str, Any]] = None,
957
+ **kwargs,
958
+ ) -> SendResult:
959
+ return await self._sidecar_send_attachment(
960
+ chat_id, video_path, caption=caption,
961
+ )
962
+
963
+ async def send_document(
964
+ self,
965
+ chat_id: str,
966
+ file_path: str,
967
+ caption: Optional[str] = None,
968
+ file_name: Optional[str] = None,
969
+ reply_to: Optional[str] = None,
970
+ metadata: Optional[Dict[str, Any]] = None,
971
+ **kwargs,
972
+ ) -> SendResult:
973
+ return await self._sidecar_send_attachment(
974
+ chat_id, file_path, name=file_name, caption=caption,
975
+ )
976
+
977
+ async def send_animation(
978
+ self,
979
+ chat_id: str,
980
+ animation_url: str,
981
+ caption: Optional[str] = None,
982
+ reply_to: Optional[str] = None,
983
+ metadata: Optional[Dict[str, Any]] = None,
984
+ ) -> SendResult:
985
+ # iMessage renders GIFs inline as ordinary image attachments.
986
+ return await self.send_image(
987
+ chat_id, animation_url, caption, reply_to, metadata,
988
+ )
989
+
990
+ async def send_typing(self, chat_id: str, metadata=None) -> None:
991
+ try:
992
+ await self._sidecar_call(
993
+ "/typing", {"spaceId": chat_id, "state": "start"}
994
+ )
995
+ except Exception as e:
996
+ logger.debug("[photon] send_typing failed: %s", e)
997
+
998
+ async def stop_typing(self, chat_id: str) -> None:
999
+ try:
1000
+ await self._sidecar_call(
1001
+ "/typing", {"spaceId": chat_id, "state": "stop"}
1002
+ )
1003
+ except Exception as e:
1004
+ logger.debug("[photon] stop_typing failed: %s", e)
1005
+
1006
+ # -- Reactions (tapbacks) -----------------------------------------------
1007
+ #
1008
+ # Same lifecycle-hook pattern as Telegram/Discord: 👀 while processing,
1009
+ # swapped for 👍/👎 on completion. Opt-in via PHOTON_REACTIONS — iMessage
1010
+ # is a personal-texting channel, and a tapback on every text is noisy.
1011
+
1012
+ _SENT_IDS_MAX = 1000
1013
+ _LAST_INBOUND_CHATS_MAX = 200
1014
+
1015
+ def _record_sent_message(self, message_id: Optional[str]) -> None:
1016
+ if not message_id:
1017
+ return
1018
+ sent = self._sent_message_ids
1019
+ if message_id in sent:
1020
+ del sent[message_id] # refresh insertion order
1021
+ sent[message_id] = time.time()
1022
+ if len(sent) > self._SENT_IDS_MAX:
1023
+ for old in list(sent.keys())[: len(sent) - self._SENT_IDS_MAX]:
1024
+ del sent[old]
1025
+
1026
+ # A DM space is addressable two ways — the chat GUID (`any;-;+1555...`)
1027
+ # that inbound events carry, and the bare E.164 phone that home-channel
1028
+ # config typically uses. The sidecar's resolveSpace treats them as the
1029
+ # same space; normalize to the bare phone so the last-inbound tracker
1030
+ # does too (mirrors phoneTargetFromSpaceId in sidecar/index.mjs).
1031
+ _DM_CHAT_GUID_RE = re.compile(r"^any;-;(\+\d{6,})$")
1032
+
1033
+ @classmethod
1034
+ def _normalize_chat_key(cls, chat_id: str) -> str:
1035
+ match = cls._DM_CHAT_GUID_RE.match(chat_id)
1036
+ return match.group(1) if match else chat_id
1037
+
1038
+ def _record_last_inbound(
1039
+ self, chat_id: Optional[str], message_id: Optional[str]
1040
+ ) -> None:
1041
+ if not chat_id or not message_id:
1042
+ return
1043
+ key = self._normalize_chat_key(chat_id)
1044
+ last = self._last_inbound_by_chat
1045
+ if key in last:
1046
+ del last[key] # refresh insertion order
1047
+ last[key] = message_id
1048
+ if len(last) > self._LAST_INBOUND_CHATS_MAX:
1049
+ for old in list(last.keys())[
1050
+ : len(last) - self._LAST_INBOUND_CHATS_MAX
1051
+ ]:
1052
+ del last[old]
1053
+
1054
+ def _reactions_enabled(self) -> bool:
1055
+ return os.getenv("PHOTON_REACTIONS", "false").strip().lower() in {
1056
+ "true", "1", "yes", "on",
1057
+ }
1058
+
1059
+ async def _add_reaction(
1060
+ self, chat_id: str, message_id: str, emoji: str
1061
+ ) -> bool:
1062
+ """Tapback ``emoji`` onto a message. Soft-fails (False), never raises."""
1063
+ try:
1064
+ await self._sidecar_call(
1065
+ "/react",
1066
+ {"spaceId": chat_id, "messageId": message_id, "emoji": emoji},
1067
+ )
1068
+ return True
1069
+ except Exception as e:
1070
+ logger.debug("[photon] add_reaction failed: %s", e)
1071
+ return False
1072
+
1073
+ async def _remove_reaction(self, chat_id: str, message_id: str) -> bool:
1074
+ """Retract our tapback from a message. Soft-fails (False), never raises.
1075
+
1076
+ The sidecar tracks one reaction handle per target message; after a
1077
+ sidecar restart the handle is gone and removal is best-effort (the
1078
+ stale tapback self-heals when the next reaction replaces it).
1079
+ """
1080
+ try:
1081
+ await self._sidecar_call(
1082
+ "/unreact", {"spaceId": chat_id, "messageId": message_id},
1083
+ )
1084
+ return True
1085
+ except Exception as e:
1086
+ logger.debug("[photon] remove_reaction failed: %s", e)
1087
+ return False
1088
+
1089
+ # -- Agent-facing reactions (send_message action="react") ---------------
1090
+ #
1091
+ # Unlike the lifecycle hooks below, these are deliberate agent intents,
1092
+ # so they are NOT gated by PHOTON_REACTIONS (that env var exists to mute
1093
+ # the automatic per-message tapback noise, not explicit requests).
1094
+
1095
+ async def add_reaction(
1096
+ self,
1097
+ chat_id: str,
1098
+ emoji: str,
1099
+ message_id: Optional[str] = None,
1100
+ ) -> Dict[str, Any]:
1101
+ """Tapback ``emoji`` onto a message in ``chat_id``.
1102
+
1103
+ Without ``message_id``, targets the chat's most recent inbound
1104
+ message (typically the one the agent is responding to). iMessage
1105
+ maps ❤️👍👎😂‼️❓ to native tapbacks; anything else uses Apple's
1106
+ custom-emoji reaction.
1107
+ """
1108
+ target = message_id or self._last_inbound_by_chat.get(
1109
+ self._normalize_chat_key(chat_id)
1110
+ )
1111
+ if not target:
1112
+ return {
1113
+ "success": False,
1114
+ "error": "no message to react to — pass message_id (no "
1115
+ "inbound message seen in this chat since the gateway started)",
1116
+ }
1117
+ ok = await self._add_reaction(chat_id, target, emoji)
1118
+ if not ok:
1119
+ return {
1120
+ "success": False,
1121
+ "error": "reaction failed (see gateway debug log)",
1122
+ }
1123
+ return {"success": True, "message_id": target}
1124
+
1125
+ async def remove_reaction(
1126
+ self, chat_id: str, message_id: Optional[str] = None
1127
+ ) -> Dict[str, Any]:
1128
+ """Retract our tapback from a message (best-effort)."""
1129
+ target = message_id or self._last_inbound_by_chat.get(
1130
+ self._normalize_chat_key(chat_id)
1131
+ )
1132
+ if not target:
1133
+ return {
1134
+ "success": False,
1135
+ "error": "no message to unreact — pass message_id",
1136
+ }
1137
+ ok = await self._remove_reaction(chat_id, target)
1138
+ if not ok:
1139
+ return {
1140
+ "success": False,
1141
+ "error": "unreact failed (see gateway debug log)",
1142
+ }
1143
+ return {"success": True, "message_id": target}
1144
+
1145
+ async def on_processing_start(self, event: MessageEvent) -> None:
1146
+ """Tapback 👀 on the triggering message while the agent works."""
1147
+ if not self._reactions_enabled():
1148
+ return
1149
+ chat_id = getattr(event.source, "chat_id", None)
1150
+ message_id = getattr(event, "message_id", None)
1151
+ if chat_id and message_id:
1152
+ await self._add_reaction(chat_id, message_id, "\U0001f440")
1153
+
1154
+ async def on_processing_complete(
1155
+ self, event: MessageEvent, outcome: ProcessingOutcome
1156
+ ) -> None:
1157
+ """Swap the 👀 progress tapback for a 👍/👎 result.
1158
+
1159
+ Remove-then-add rather than a bare replace: deterministic whether the
1160
+ platform replaces a sender's previous tapback or stacks them, and it
1161
+ keeps the sidecar's reaction-handle slot coherent.
1162
+ """
1163
+ if not self._reactions_enabled():
1164
+ return
1165
+ chat_id = getattr(event.source, "chat_id", None)
1166
+ message_id = getattr(event, "message_id", None)
1167
+ if not chat_id or not message_id:
1168
+ return
1169
+ await self._remove_reaction(chat_id, message_id)
1170
+ if outcome == ProcessingOutcome.SUCCESS:
1171
+ await self._add_reaction(chat_id, message_id, "\U0001f44d")
1172
+ elif outcome == ProcessingOutcome.FAILURE:
1173
+ await self._add_reaction(chat_id, message_id, "\U0001f44e")
1174
+ # CANCELLED: leave the message unreacted.
1175
+
1176
+ async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
1177
+ """Return whatever we know about a Spectrum space id.
1178
+
1179
+ Photon's ``space.id`` is opaque; the inbound event also carries the
1180
+ DM/group type, but here we only have the id, so infer conservatively.
1181
+ """
1182
+ return {"name": chat_id, "type": "dm", "id": chat_id}
1183
+
1184
+ def format_message(self, content: str) -> str:
1185
+ # Markdown is passed through verbatim — the sidecar sends it with the
1186
+ # markdown() builder and iMessage renders it. The strip path remains
1187
+ # as the PHOTON_MARKDOWN=false kill-switch.
1188
+ if _markdown_enabled():
1189
+ return content
1190
+ return strip_markdown(content)
1191
+
1192
+ async def _send_with_retry(
1193
+ self,
1194
+ chat_id: str,
1195
+ content: str,
1196
+ reply_to: Optional[str] = None,
1197
+ metadata: Any = None,
1198
+ max_retries: int = 2,
1199
+ base_delay: float = 2.0,
1200
+ ) -> SendResult:
1201
+ """Retry sends without the generic Markdown banner.
1202
+
1203
+ Photon replies are markdown (rendered by iMessage) or stripped plain
1204
+ text under ``PHOTON_MARKDOWN=false`` — either way the gateway's
1205
+ generic banner never applies.
1206
+ """
1207
+ text = self.format_message(content)
1208
+ result = await self.send(
1209
+ chat_id=chat_id,
1210
+ content=text,
1211
+ reply_to=reply_to,
1212
+ metadata=metadata,
1213
+ )
1214
+ if result.success:
1215
+ return result
1216
+
1217
+ error_str = result.error or ""
1218
+ is_network = result.retryable or self._is_retryable_error(error_str)
1219
+ if not is_network and self._is_timeout_error(error_str):
1220
+ return result
1221
+
1222
+ if is_network:
1223
+ for attempt in range(1, max_retries + 1):
1224
+ delay = base_delay * (2 ** (attempt - 1))
1225
+ logger.warning(
1226
+ "[photon] Send failed (attempt %d/%d, retrying in %.1fs): %s",
1227
+ attempt, max_retries, delay, error_str,
1228
+ )
1229
+ await asyncio.sleep(delay)
1230
+ result = await self.send(
1231
+ chat_id=chat_id,
1232
+ content=text,
1233
+ reply_to=reply_to,
1234
+ metadata=metadata,
1235
+ )
1236
+ if result.success:
1237
+ return result
1238
+ error_str = result.error or ""
1239
+ if not (result.retryable or self._is_retryable_error(error_str)):
1240
+ break
1241
+ else:
1242
+ logger.error(
1243
+ "[photon] Failed to deliver response after %d retries: %s",
1244
+ max_retries, error_str,
1245
+ )
1246
+ return result
1247
+
1248
+ logger.warning(
1249
+ "[photon] Send failed: %s - retrying plain-text message",
1250
+ error_str,
1251
+ )
1252
+ fallback_result = await self.send(
1253
+ chat_id=chat_id,
1254
+ content=text[: self.MAX_MESSAGE_LENGTH],
1255
+ reply_to=reply_to,
1256
+ metadata=metadata,
1257
+ )
1258
+ if not fallback_result.success:
1259
+ logger.error("[photon] Plain-text retry also failed: %s", fallback_result.error)
1260
+ return fallback_result
1261
+
1262
+ async def _sidecar_send(self, space_id: str, text: str) -> SendResult:
1263
+ if len(text) > self.MAX_MESSAGE_LENGTH:
1264
+ logger.warning(
1265
+ "[photon] truncating outbound from %d to %d chars",
1266
+ len(text), self.MAX_MESSAGE_LENGTH,
1267
+ )
1268
+ text = text[: self.MAX_MESSAGE_LENGTH]
1269
+ body: Dict[str, Any] = {"spaceId": space_id, "text": text}
1270
+ # Omit the key when disabled so an older sidecar (pre-`format`)
1271
+ # keeps accepting the body during a half-upgraded restart.
1272
+ if _markdown_enabled():
1273
+ body["format"] = "markdown"
1274
+ try:
1275
+ data = await self._sidecar_call("/send", body)
1276
+ except Exception as e:
1277
+ return SendResult(success=False, error=str(e))
1278
+ self._record_sent_message(data.get("messageId"))
1279
+ return SendResult(success=True, message_id=data.get("messageId"))
1280
+
1281
+ async def _sidecar_send_attachment(
1282
+ self,
1283
+ space_id: str,
1284
+ path: str,
1285
+ *,
1286
+ name: Optional[str] = None,
1287
+ mime_type: Optional[str] = None,
1288
+ caption: Optional[str] = None,
1289
+ kind: str = "attachment",
1290
+ ) -> SendResult:
1291
+ """POST a local file to the sidecar's ``/send-attachment`` endpoint.
1292
+
1293
+ ``kind`` is ``"voice"`` for audio sent as a voice note (downgrades
1294
+ to a plain audio attachment on platforms without voice notes),
1295
+ otherwise ``"attachment"``. spectrum-ts infers ``name`` and
1296
+ ``mimeType`` from the file extension; we only pass overrides when
1297
+ Hermes supplied them.
1298
+ """
1299
+ # Defense-in-depth: re-validate the path before handing it to the
1300
+ # Node sidecar. The gateway already filters MEDIA paths, but
1301
+ # send_*_file / cron callers may pass arbitrary strings.
1302
+ safe_path = self.validate_media_delivery_path(str(path))
1303
+ if not safe_path:
1304
+ return SendResult(
1305
+ success=False, error=f"unsafe or missing attachment path: {path}"
1306
+ )
1307
+ if not mime_type:
1308
+ import mimetypes
1309
+
1310
+ guessed, _ = mimetypes.guess_type(safe_path)
1311
+ mime_type = guessed or None
1312
+ body: Dict[str, Any] = {
1313
+ "spaceId": space_id,
1314
+ "path": safe_path,
1315
+ "kind": "voice" if kind == "voice" else "attachment",
1316
+ }
1317
+ if name:
1318
+ body["name"] = name
1319
+ if mime_type:
1320
+ body["mimeType"] = mime_type
1321
+ if caption:
1322
+ body["caption"] = caption
1323
+ try:
1324
+ data = await self._sidecar_call("/send-attachment", body)
1325
+ except Exception as e:
1326
+ return SendResult(success=False, error=str(e))
1327
+ self._record_sent_message(data.get("messageId"))
1328
+ return SendResult(success=True, message_id=data.get("messageId"))
1329
+
1330
+ async def _sidecar_call(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
1331
+ # Guard: adapter not yet connected (no sidecar address known).
1332
+ if self._http_client is None:
1333
+ raise RuntimeError("Photon adapter not connected")
1334
+ # Use a fresh client per call so this method is safe when invoked from
1335
+ # a worker thread that owns a different event loop than the one the
1336
+ # persistent _http_client was created on (e.g. via _run_async in
1337
+ # send_message_tool). The inbound streaming loop continues to use
1338
+ # _http_client directly — it always runs on the gateway's loop.
1339
+ url = f"http://{self._sidecar_bind}:{self._sidecar_port}{path}"
1340
+ headers = {"X-Hermes-Sidecar-Token": self._sidecar_token}
1341
+ async with httpx.AsyncClient(timeout=30.0) as client:
1342
+ resp = await client.post(url, json=body, headers=headers)
1343
+ if resp.status_code != 200:
1344
+ raise RuntimeError(
1345
+ f"Photon sidecar {path} returned {resp.status_code}: {resp.text[:200]}"
1346
+ )
1347
+ data = resp.json() or {}
1348
+ if not data.get("ok"):
1349
+ raise RuntimeError(
1350
+ f"Photon sidecar {path} reported error: {data.get('error')}"
1351
+ )
1352
+ return data
1353
+
1354
+
1355
+ # ---------------------------------------------------------------------------
1356
+ # Helpers
1357
+
1358
+ def _attachment_message_type(mime: str) -> MessageType:
1359
+ mime = (mime or "").lower()
1360
+ if mime.startswith("image/"):
1361
+ return MessageType.PHOTO
1362
+ if mime.startswith("video/"):
1363
+ return MessageType.VIDEO
1364
+ if mime.startswith("audio/"):
1365
+ return MessageType.AUDIO
1366
+ if mime.startswith("application/"):
1367
+ return MessageType.DOCUMENT
1368
+ return MessageType.DOCUMENT
1369
+
1370
+
1371
+ # MIME → file-extension maps for caching inbound attachment bytes. These mirror
1372
+ # the BlueBubbles iMessage channel so both adapters name cached media the same.
1373
+ _IMAGE_EXT_BY_MIME = {
1374
+ "image/jpeg": ".jpg",
1375
+ "image/png": ".png",
1376
+ "image/gif": ".gif",
1377
+ "image/webp": ".webp",
1378
+ "image/heic": ".jpg",
1379
+ "image/heif": ".jpg",
1380
+ "image/tiff": ".jpg",
1381
+ }
1382
+ _AUDIO_EXT_BY_MIME = {
1383
+ "audio/mp3": ".mp3",
1384
+ "audio/mpeg": ".mp3",
1385
+ "audio/ogg": ".ogg",
1386
+ "audio/wav": ".wav",
1387
+ "audio/x-caf": ".mp3",
1388
+ "audio/mp4": ".m4a",
1389
+ "audio/aac": ".m4a",
1390
+ }
1391
+
1392
+
1393
+ def _cache_inbound_attachment(
1394
+ content: Dict[str, Any],
1395
+ name: str,
1396
+ mime: str,
1397
+ *,
1398
+ force_audio: bool = False,
1399
+ ) -> Optional[str]:
1400
+ """Decode a base64-inlined inbound attachment and cache it locally.
1401
+
1402
+ The sidecar inlines the attachment bytes as ``content["data"]`` (base64).
1403
+ We decode them and route to the shared media cache by MIME type, returning
1404
+ the cached absolute path so the caller can populate ``media_urls`` (which
1405
+ the gateway then hands to the model). Returns ``None`` when there are no
1406
+ bytes (over the sidecar's inline cap or a failed read) or when caching
1407
+ fails, so the caller can fall back to a text marker.
1408
+ """
1409
+ data_b64 = content.get("data")
1410
+ if not data_b64:
1411
+ return None
1412
+ try:
1413
+ raw = base64.b64decode(data_b64)
1414
+ except (ValueError, TypeError) as exc:
1415
+ logger.warning("[photon] failed to decode inbound attachment bytes: %s", exc)
1416
+ return None
1417
+
1418
+ from gateway.platforms.base import (
1419
+ cache_audio_from_bytes,
1420
+ cache_document_from_bytes,
1421
+ cache_image_from_bytes,
1422
+ )
1423
+
1424
+ mime = (mime or "").lower()
1425
+ # Prefer the real extension from the filename; fall back to the MIME map.
1426
+ suffix = Path(name).suffix if name else ""
1427
+ try:
1428
+ if mime.startswith("image/"):
1429
+ ext = suffix or _IMAGE_EXT_BY_MIME.get(mime, ".jpg")
1430
+ try:
1431
+ return cache_image_from_bytes(raw, ext)
1432
+ except ValueError:
1433
+ # Bytes don't look like a supported image (e.g. HEIC magic) —
1434
+ # still deliver them as a document rather than dropping them.
1435
+ return cache_document_from_bytes(raw, name)
1436
+ if force_audio or mime.startswith("audio/"):
1437
+ ext = suffix or _AUDIO_EXT_BY_MIME.get(
1438
+ mime, ".m4a" if force_audio else ".mp3"
1439
+ )
1440
+ return cache_audio_from_bytes(raw, ext)
1441
+ # Video, application/*, and everything else → document cache.
1442
+ return cache_document_from_bytes(raw, name)
1443
+ except Exception as exc:
1444
+ logger.warning("[photon] failed to cache inbound attachment %s: %s", name, exc)
1445
+ return None
1446
+
1447
+
1448
+ # ---------------------------------------------------------------------------
1449
+ # Standalone (out-of-process) send for cron deliveries when the gateway
1450
+ # is not co-resident. Reuses a live sidecar already listening on the
1451
+ # configured port (cron processes cannot spawn the sidecar themselves).
1452
+
1453
+ async def _standalone_send(
1454
+ pconfig: PlatformConfig,
1455
+ chat_id: str,
1456
+ message: str,
1457
+ *,
1458
+ thread_id: Optional[str] = None, # noqa: ARG001 — Spectrum has no threads yet
1459
+ media_files: Optional[list] = None,
1460
+ force_document: bool = False, # noqa: ARG001 — iMessage auto-detects file kind
1461
+ ) -> Dict[str, Any]:
1462
+ if not HTTPX_AVAILABLE:
1463
+ return {"error": "httpx not installed"}
1464
+ port = _coerce_port(
1465
+ (pconfig.extra or {}).get("sidecar_port") or os.getenv("PHOTON_SIDECAR_PORT"),
1466
+ _DEFAULT_SIDECAR_PORT,
1467
+ )
1468
+ token = os.getenv("PHOTON_SIDECAR_TOKEN")
1469
+ if not token:
1470
+ return {
1471
+ "error": (
1472
+ "Photon standalone send requires a running sidecar with "
1473
+ "PHOTON_SIDECAR_TOKEN set in the environment. Cron processes "
1474
+ "cannot spawn the sidecar themselves."
1475
+ )
1476
+ }
1477
+ base = f"http://{_DEFAULT_SIDECAR_BIND}:{port}"
1478
+ headers = {"X-Hermes-Sidecar-Token": token}
1479
+ last_message_id: Optional[str] = None
1480
+ try:
1481
+ async with httpx.AsyncClient(timeout=30.0) as client:
1482
+ # 1. Text body first (if any), so it leads the conversation.
1483
+ if message:
1484
+ send_body: Dict[str, Any] = {
1485
+ "spaceId": chat_id,
1486
+ "text": message[:_MAX_MESSAGE_LENGTH],
1487
+ }
1488
+ if _markdown_enabled():
1489
+ send_body["format"] = "markdown"
1490
+ resp = await client.post(
1491
+ f"{base}/send", json=send_body, headers=headers,
1492
+ )
1493
+ if resp.status_code != 200:
1494
+ return {"error": f"sidecar returned {resp.status_code}: {resp.text[:200]}"}
1495
+ data = resp.json() or {}
1496
+ if not data.get("ok"):
1497
+ return {"error": data.get("error") or "sidecar reported failure"}
1498
+ last_message_id = data.get("messageId")
1499
+
1500
+ # 2. Each attachment as a separate /send-attachment call.
1501
+ # media_files is List[Tuple[path, is_voice]] (see
1502
+ # BasePlatformAdapter.filter_media_delivery_paths).
1503
+ import mimetypes
1504
+
1505
+ for media_path, is_voice in media_files or []:
1506
+ safe_path = BasePlatformAdapter.validate_media_delivery_path(str(media_path))
1507
+ if not safe_path:
1508
+ logger.warning("[photon] standalone send skipping unsafe path")
1509
+ continue
1510
+ guessed, _ = mimetypes.guess_type(safe_path)
1511
+ att_body: Dict[str, Any] = {
1512
+ "spaceId": chat_id,
1513
+ "path": safe_path,
1514
+ "kind": "voice" if is_voice else "attachment",
1515
+ }
1516
+ if guessed:
1517
+ att_body["mimeType"] = guessed
1518
+ resp = await client.post(
1519
+ f"{base}/send-attachment", json=att_body, headers=headers,
1520
+ )
1521
+ if resp.status_code != 200:
1522
+ return {"error": f"sidecar returned {resp.status_code}: {resp.text[:200]}"}
1523
+ data = resp.json() or {}
1524
+ if not data.get("ok"):
1525
+ return {"error": data.get("error") or "sidecar reported failure"}
1526
+ last_message_id = data.get("messageId") or last_message_id
1527
+
1528
+ return {"success": True, "message_id": last_message_id}
1529
+ except Exception as e:
1530
+ return {"error": f"Photon standalone send failed: {e}"}
1531
+
1532
+
1533
+ # ---------------------------------------------------------------------------
1534
+ # Plugin entry point
1535
+
1536
+ def register(ctx) -> None:
1537
+ """Called by the Hermes plugin loader at startup."""
1538
+ # Local import to avoid argparse work at module load; reused for both the
1539
+ # gateway-setup hook and the `hermes photon` CLI command below.
1540
+ from . import cli as _cli
1541
+
1542
+ ctx.register_platform(
1543
+ name="photon",
1544
+ label="iMessage via Photon",
1545
+ adapter_factory=lambda cfg: PhotonAdapter(cfg),
1546
+ check_fn=check_requirements,
1547
+ validate_config=validate_config,
1548
+ is_connected=is_connected,
1549
+ required_env=["PHOTON_PROJECT_ID", "PHOTON_PROJECT_SECRET"],
1550
+ install_hint=(
1551
+ "Run: hermes photon setup (logs in via device flow, creates a "
1552
+ "Spectrum project, links your phone number, installs the "
1553
+ "spectrum-ts sidecar)."
1554
+ ),
1555
+ # Surfaces Photon in `hermes gateway setup` alongside every other
1556
+ # channel — same unified onboarding wizard, no Photon-only detour.
1557
+ setup_fn=_cli.gateway_setup,
1558
+ env_enablement_fn=_env_enablement,
1559
+ cron_deliver_env_var="PHOTON_HOME_CHANNEL",
1560
+ standalone_sender_fn=_standalone_send,
1561
+ allowed_users_env="PHOTON_ALLOWED_USERS",
1562
+ allow_all_env="PHOTON_ALLOW_ALL_USERS",
1563
+ max_message_length=_MAX_MESSAGE_LENGTH,
1564
+ emoji="📱",
1565
+ # iMessage carries E.164 phone numbers — treat session descriptions
1566
+ # as PII-sensitive so they get redacted before reaching the LLM
1567
+ # (matches the BlueBubbles iMessage channel in _PII_SAFE_PLATFORMS).
1568
+ pii_safe=True,
1569
+ allow_update_command=True,
1570
+ platform_hint=(
1571
+ "You are communicating via Photon Spectrum (iMessage). "
1572
+ "Treat replies like regular text messages — short and friendly. "
1573
+ "Markdown is rendered (bold, italics, lists, code), but keep "
1574
+ "formatting light and conversational. Recipient identifiers are "
1575
+ "E.164 phone numbers; never expose them in responses unless the "
1576
+ "user asked. Attachments arrive as metadata only."
1577
+ ),
1578
+ )
1579
+
1580
+ # Register CLI subcommands — `hermes photon ...`
1581
+ ctx.register_cli_command(
1582
+ name="photon",
1583
+ help="Set up and manage the Photon iMessage integration",
1584
+ setup_fn=_cli.register_cli,
1585
+ handler_fn=_cli.dispatch,
1586
+ )