machinaos 0.0.76 → 0.0.78

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 (393) hide show
  1. package/README.md +143 -107
  2. package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
  3. package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
  4. package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
  5. package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
  6. package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
  7. package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
  8. package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
  9. package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
  10. package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
  11. package/client/dist/assets/index-DQ0nwhec.js +257 -0
  12. package/client/dist/assets/index-DxmbVskS.css +1 -0
  13. package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
  14. package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
  15. package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
  16. package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
  17. package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
  18. package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
  19. package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
  20. package/client/dist/index.html +37 -3
  21. package/client/index.html +28 -1
  22. package/client/package.json +44 -40
  23. package/client/src/App.tsx +2 -0
  24. package/client/src/Dashboard.tsx +157 -45
  25. package/client/src/ParameterPanel.tsx +3 -5
  26. package/client/src/adapters/nodeSpecToDescription.ts +1 -0
  27. package/client/src/assets/icons/NodeIcon.tsx +32 -0
  28. package/client/src/assets/icons/index.ts +4 -0
  29. package/client/src/assets/icons/stripe.svg +1 -0
  30. package/client/src/assets/icons/themedGlyphs.ts +404 -0
  31. package/client/src/components/AIAgentNode.tsx +77 -53
  32. package/client/src/components/GenericNode.tsx +34 -52
  33. package/client/src/components/OutputPanel.tsx +64 -147
  34. package/client/src/components/ParameterRenderer.tsx +5 -3
  35. package/client/src/components/SkillEditorModal.tsx +9 -18
  36. package/client/src/components/SquareNode.tsx +97 -115
  37. package/client/src/components/StartNode.tsx +32 -42
  38. package/client/src/components/SvgFilterDefs.tsx +54 -0
  39. package/client/src/components/TeamMonitorNode.tsx +12 -14
  40. package/client/src/components/ToolkitNode.tsx +35 -60
  41. package/client/src/components/TriggerNode.tsx +43 -77
  42. package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
  43. package/client/src/components/credentials/CredentialsModal.tsx +98 -30
  44. package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
  45. package/client/src/components/credentials/catalogueAdapter.ts +17 -1
  46. package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
  47. package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
  48. package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
  49. package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
  50. package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
  51. package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
  52. package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
  53. package/client/src/components/credentials/types.ts +12 -2
  54. package/client/src/components/credentials/useCredentialPanel.ts +43 -19
  55. package/client/src/components/icons/AIProviderIcons.tsx +16 -0
  56. package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
  57. package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
  58. package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
  59. package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
  60. package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
  61. package/client/src/components/parameterPanel/InputSection.tsx +9 -7
  62. package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
  63. package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
  64. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
  65. package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
  66. package/client/src/components/ui/AIResultModal.tsx +1 -1
  67. package/client/src/components/ui/CollapsibleSection.tsx +9 -5
  68. package/client/src/components/ui/CommandPalette.tsx +147 -0
  69. package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
  70. package/client/src/components/ui/ComponentItem.tsx +13 -7
  71. package/client/src/components/ui/ComponentPalette.tsx +24 -13
  72. package/client/src/components/ui/ConsolePanel.tsx +19 -11
  73. package/client/src/components/ui/DropCap.tsx +28 -0
  74. package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
  75. package/client/src/components/ui/InputNodesPanel.tsx +1 -1
  76. package/client/src/components/ui/Modal.tsx +38 -6
  77. package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
  78. package/client/src/components/ui/SettingsPanel.tsx +42 -13
  79. package/client/src/components/ui/StatusBar.tsx +108 -0
  80. package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
  81. package/client/src/components/ui/TopToolbar.tsx +42 -25
  82. package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
  83. package/client/src/components/ui/action-button.tsx +40 -15
  84. package/client/src/components/ui/button.tsx +24 -1
  85. package/client/src/components/ui/dropdown-menu.tsx +24 -2
  86. package/client/src/components/ui/input.tsx +19 -2
  87. package/client/src/components/ui/select.tsx +15 -0
  88. package/client/src/components/ui/textarea.tsx +15 -2
  89. package/client/src/contexts/AuthContext.tsx +148 -109
  90. package/client/src/contexts/ThemeContext.tsx +93 -17
  91. package/client/src/contexts/WebSocketContext.tsx +373 -206
  92. package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
  93. package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
  94. package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
  95. package/client/src/hooks/useAppTheme.ts +209 -7
  96. package/client/src/hooks/useAutoSkillEdges.ts +7 -2
  97. package/client/src/hooks/useCatalogueQuery.ts +67 -1
  98. package/client/src/hooks/useDragVariable.ts +1 -1
  99. package/client/src/hooks/useNodeAllowlist.ts +115 -8
  100. package/client/src/hooks/useOnboarding.ts +20 -8
  101. package/client/src/hooks/useParameterPanel.ts +2 -1
  102. package/client/src/hooks/useReactFlowNodes.ts +2 -1
  103. package/client/src/hooks/useSound.ts +185 -0
  104. package/client/src/hooks/useWorkflowManagement.ts +6 -8
  105. package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
  106. package/client/src/index.css +65 -3
  107. package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
  108. package/client/src/lib/aiModelProviders.ts +8 -0
  109. package/client/src/lib/connectionConfig.ts +107 -0
  110. package/client/src/lib/queryPersist.ts +13 -5
  111. package/client/src/lib/sound.ts +393 -0
  112. package/client/src/main.tsx +20 -0
  113. package/client/src/store/useAppStore.ts +26 -0
  114. package/client/src/styles/canvasAnimations.ts +37 -36
  115. package/client/src/styles/theme.ts +36 -20
  116. package/client/src/test/setup.ts +1 -0
  117. package/client/src/themes/atomic.css +253 -0
  118. package/client/src/themes/base.css +373 -0
  119. package/client/src/themes/cyber.css +890 -0
  120. package/client/src/themes/dark.css +70 -0
  121. package/client/src/themes/edo.css +246 -0
  122. package/client/src/themes/greek.css +293 -0
  123. package/client/src/themes/light.css +78 -0
  124. package/client/src/themes/plague.css +253 -0
  125. package/client/src/themes/renaissance.css +727 -0
  126. package/client/src/themes/rot.css +249 -0
  127. package/client/src/themes/steampunk.css +272 -0
  128. package/client/src/themes/surveillance.css +289 -0
  129. package/client/src/themes/wasteland.css +250 -0
  130. package/client/src/types/INodeProperties.ts +5 -0
  131. package/client/src/types/NodeTypes.ts +11 -1
  132. package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
  133. package/client/src/types/cloudEvents.ts +78 -0
  134. package/client/src/vite-env.d.ts +7 -0
  135. package/client/tsconfig.json +1 -1
  136. package/client/vite.config.js +62 -2
  137. package/install.ps1 +1 -1
  138. package/install.sh +1 -1
  139. package/machina/commands/build.py +51 -7
  140. package/machina/pyproject.toml +4 -0
  141. package/machina/supervisor.py +12 -2
  142. package/machina/tree.py +71 -21
  143. package/package.json +4 -4
  144. package/scripts/install.js +16 -1
  145. package/server/config/ai_cli_providers.json +54 -0
  146. package/server/config/credential_providers.json +109 -2
  147. package/server/config/llm_defaults.json +24 -0
  148. package/server/config/model_registry.json +338 -499
  149. package/server/config/node_allowlist.json +16 -1
  150. package/server/config/pricing.json +8 -0
  151. package/server/constants.py +38 -15
  152. package/server/core/container.py +2 -2
  153. package/server/core/credentials_database.py +35 -2
  154. package/server/core/logging.py +4 -3
  155. package/server/main.py +99 -13
  156. package/server/models/node_metadata.py +1 -0
  157. package/server/nodejs/package.json +8 -6
  158. package/server/nodejs/src/index.ts +22 -5
  159. package/server/nodes/README.md +31 -4
  160. package/server/nodes/agent/_inline.py +2 -0
  161. package/server/nodes/agent/_specialized.py +6 -3
  162. package/server/nodes/agent/ai_agent.py +13 -3
  163. package/server/nodes/agent/chat_agent.py +6 -3
  164. package/server/nodes/agent/claude_code_agent.py +287 -75
  165. package/server/nodes/agent/codex_agent.py +239 -0
  166. package/server/nodes/agent/deep_agent.py +3 -3
  167. package/server/nodes/agent/rlm_agent.py +3 -3
  168. package/server/nodes/android/__init__.py +31 -1
  169. package/server/nodes/android/_base.py +9 -5
  170. package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
  171. package/server/nodes/android/_handlers.py +154 -0
  172. package/server/nodes/android/_option_loaders.py +44 -0
  173. package/server/nodes/android/_refresh.py +127 -0
  174. package/server/{services/android → nodes/android/_relay}/client.py +4 -4
  175. package/server/{routers/android.py → nodes/android/_router.py} +27 -8
  176. package/server/nodes/browser/browser.py +2 -2
  177. package/server/nodes/code/_base.py +6 -2
  178. package/server/nodes/code/_claude_code.py +134 -0
  179. package/server/nodes/document/embedding_generator.py +3 -3
  180. package/server/nodes/document/http_scraper.py +3 -3
  181. package/server/nodes/document/vector_store.py +5 -5
  182. package/server/nodes/email/__init__.py +11 -1
  183. package/server/nodes/email/_filters.py +21 -0
  184. package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
  185. package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
  186. package/server/nodes/email/email_read.py +1 -1
  187. package/server/nodes/email/email_receive.py +54 -5
  188. package/server/nodes/email/email_send.py +1 -1
  189. package/server/nodes/filesystem/shell.py +24 -1
  190. package/server/nodes/google/__init__.py +55 -1
  191. package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
  192. package/server/nodes/google/_base.py +2 -2
  193. package/server/nodes/google/_credentials.py +5 -5
  194. package/server/nodes/google/_filters.py +25 -0
  195. package/server/nodes/google/_handlers.py +57 -0
  196. package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
  197. package/server/nodes/google/_option_loaders.py +107 -0
  198. package/server/nodes/google/_refresh.py +66 -0
  199. package/server/nodes/google/_router.py +131 -0
  200. package/server/nodes/google/gmail_receive.py +41 -4
  201. package/server/nodes/groups.py +1 -0
  202. package/server/nodes/location/_credentials.py +45 -1
  203. package/server/{services/maps.py → nodes/location/_service.py} +18 -3
  204. package/server/nodes/location/gmaps_create.py +4 -4
  205. package/server/nodes/location/gmaps_locations.py +4 -4
  206. package/server/nodes/location/gmaps_nearby_places.py +4 -4
  207. package/server/nodes/model/_base.py +8 -3
  208. package/server/nodes/model/_credentials.py +96 -8
  209. package/server/nodes/model/_local_validator.py +345 -0
  210. package/server/nodes/model/lmstudio_chat_model.py +23 -0
  211. package/server/nodes/model/ollama_chat_model.py +25 -0
  212. package/server/nodes/proxy/_usage.py +2 -2
  213. package/server/nodes/proxy/proxy_config.py +14 -14
  214. package/server/nodes/proxy/proxy_request.py +4 -4
  215. package/server/nodes/scraper/_credentials.py +29 -1
  216. package/server/nodes/scraper/apify_actor.py +9 -9
  217. package/server/nodes/scraper/crawlee_scraper.py +5 -5
  218. package/server/nodes/search/brave_search.py +4 -0
  219. package/server/nodes/search/perplexity_search.py +9 -0
  220. package/server/nodes/search/serper_search.py +3 -0
  221. package/server/nodes/skill/simple_memory.py +12 -0
  222. package/server/nodes/social/_base.py +2 -2
  223. package/server/nodes/stripe/__init__.py +46 -0
  224. package/server/nodes/stripe/_credentials.py +33 -0
  225. package/server/nodes/stripe/_handlers.py +270 -0
  226. package/server/nodes/stripe/_install.py +127 -0
  227. package/server/nodes/stripe/_source.py +174 -0
  228. package/server/nodes/stripe/stripe_action.py +81 -0
  229. package/server/nodes/stripe/stripe_receive.py +92 -0
  230. package/server/nodes/telegram/_credentials.py +52 -1
  231. package/server/nodes/telegram/_handlers.py +19 -18
  232. package/server/nodes/telegram/_service.py +134 -32
  233. package/server/nodes/telegram/telegram_send.py +5 -6
  234. package/server/nodes/text/file_handler.py +2 -2
  235. package/server/nodes/text/text_generator.py +2 -2
  236. package/server/nodes/tool/agent_builder.py +630 -0
  237. package/server/nodes/tool/task_manager.py +144 -2
  238. package/server/nodes/twitter/__init__.py +38 -1
  239. package/server/nodes/twitter/_base.py +7 -7
  240. package/server/nodes/twitter/_credentials.py +1 -1
  241. package/server/nodes/twitter/_filters.py +37 -0
  242. package/server/nodes/twitter/_handlers.py +77 -0
  243. package/server/nodes/twitter/_oauth.py +124 -0
  244. package/server/nodes/twitter/_refresh.py +78 -0
  245. package/server/nodes/twitter/_router.py +29 -0
  246. package/server/nodes/twitter/twitter_receive.py +4 -0
  247. package/server/nodes/visuals.json +64 -19
  248. package/server/nodes/whatsapp/__init__.py +45 -5
  249. package/server/nodes/whatsapp/_base.py +3 -3
  250. package/server/nodes/whatsapp/_filters.py +137 -0
  251. package/server/nodes/whatsapp/_handlers.py +167 -0
  252. package/server/nodes/whatsapp/_option_loaders.py +68 -0
  253. package/server/nodes/whatsapp/_refresh.py +62 -0
  254. package/server/nodes/whatsapp/_runtime.py +1 -1
  255. package/server/pyproject.toml +29 -7
  256. package/server/routers/schemas.py +2 -2
  257. package/server/routers/webhook.py +26 -9
  258. package/server/routers/websocket.py +149 -810
  259. package/server/services/ai.py +89 -8
  260. package/server/services/auth.py +220 -43
  261. package/server/services/claude_oauth.py +126 -100
  262. package/server/services/cli_agent/__init__.py +78 -0
  263. package/server/services/cli_agent/_handlers.py +237 -0
  264. package/server/services/cli_agent/config.py +112 -0
  265. package/server/services/cli_agent/factory.py +48 -0
  266. package/server/services/cli_agent/lockfile.py +141 -0
  267. package/server/services/cli_agent/mcp_server.py +482 -0
  268. package/server/services/cli_agent/protocol.py +173 -0
  269. package/server/services/cli_agent/providers/__init__.py +9 -0
  270. package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
  271. package/server/services/cli_agent/providers/google_gemini.py +80 -0
  272. package/server/services/cli_agent/providers/openai_codex.py +310 -0
  273. package/server/services/cli_agent/service.py +607 -0
  274. package/server/services/cli_agent/session.py +618 -0
  275. package/server/services/cli_agent/types.py +227 -0
  276. package/server/services/cli_agent/workflow_tools.py +233 -0
  277. package/server/services/credential_registry.py +26 -1
  278. package/server/services/deployment/manager.py +26 -145
  279. package/server/services/deployment/poll_registry.py +59 -0
  280. package/server/services/event_waiter.py +76 -246
  281. package/server/services/events/__init__.py +54 -0
  282. package/server/services/events/cli.py +78 -0
  283. package/server/services/events/daemon.py +163 -0
  284. package/server/services/events/envelope.py +281 -0
  285. package/server/services/events/lifecycle.py +99 -0
  286. package/server/services/events/oauth_lifecycle.py +534 -0
  287. package/server/services/events/polling.py +60 -0
  288. package/server/services/events/push.py +36 -0
  289. package/server/services/events/source.py +63 -0
  290. package/server/services/events/triggers.py +118 -0
  291. package/server/services/events/verifiers/__init__.py +25 -0
  292. package/server/services/events/verifiers/base.py +28 -0
  293. package/server/services/events/verifiers/github.py +25 -0
  294. package/server/services/events/verifiers/hmac_basic.py +32 -0
  295. package/server/services/events/verifiers/standard_webhooks.py +47 -0
  296. package/server/services/events/verifiers/stripe.py +42 -0
  297. package/server/services/events/webhook.py +105 -0
  298. package/server/services/handlers/tools.py +28 -186
  299. package/server/services/llm/config.py +7 -0
  300. package/server/services/llm/factory.py +8 -2
  301. package/server/services/memory/__init__.py +52 -0
  302. package/server/services/memory/jsonl.py +80 -0
  303. package/server/services/memory/markdown.py +65 -0
  304. package/server/services/memory/state.py +112 -0
  305. package/server/services/memory/vector_store.py +40 -0
  306. package/server/services/model_registry.py +76 -0
  307. package/server/services/node_allowlist.py +71 -15
  308. package/server/services/node_executor.py +2 -2
  309. package/server/services/node_output_schemas.py +21 -10
  310. package/server/services/node_spec.py +1 -1
  311. package/server/services/oauth_utils.py +1 -1
  312. package/server/services/plugin/__init__.py +2 -0
  313. package/server/services/plugin/base.py +44 -2
  314. package/server/services/plugin/credential.py +288 -1
  315. package/server/services/plugin/deps.py +105 -0
  316. package/server/services/plugin/edge_walker.py +12 -4
  317. package/server/services/plugin/oauth.py +381 -0
  318. package/server/services/plugin/polling.py +247 -0
  319. package/server/services/plugin/registry.py +145 -0
  320. package/server/services/plugin/singleton.py +65 -0
  321. package/server/services/plugin/ws.py +81 -0
  322. package/server/services/process_service.py +31 -2
  323. package/server/services/status_broadcaster.py +155 -238
  324. package/server/services/temporal/workflow.py +7 -7
  325. package/server/services/workflow.py +21 -3
  326. package/server/services/ws_handler_registry.py +111 -28
  327. package/server/skills/GUIDE.md +16 -1
  328. package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
  329. package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
  330. package/server/tests/credentials/test_auth_service.py +16 -9
  331. package/server/tests/credentials/test_credential_broadcasts.py +219 -0
  332. package/server/tests/credentials/test_google_oauth.py +6 -6
  333. package/server/tests/credentials/test_oauth_utils.py +1 -1
  334. package/server/tests/credentials/test_twitter_oauth.py +2 -2
  335. package/server/tests/credentials/test_websocket_handlers.py +44 -20
  336. package/server/tests/llm/test_factory.py +1 -0
  337. package/server/tests/llm/test_wiring.py +5 -1
  338. package/server/tests/nodes/_compat.py +24 -24
  339. package/server/tests/nodes/test_agent_builder.py +439 -0
  340. package/server/tests/nodes/test_ai_tools.py +18 -14
  341. package/server/tests/nodes/test_code_fs_process.py +17 -8
  342. package/server/tests/nodes/test_email.py +10 -9
  343. package/server/tests/nodes/test_google_workspace.py +2 -2
  344. package/server/tests/nodes/test_specialized_agents.py +100 -53
  345. package/server/tests/nodes/test_stripe_plugin.py +293 -0
  346. package/server/tests/nodes/test_telegram_social.py +4 -4
  347. package/server/tests/nodes/test_twitter.py +1 -1
  348. package/server/tests/nodes/test_web_automation.py +2 -2
  349. package/server/tests/nodes/test_whatsapp.py +9 -9
  350. package/server/tests/services/cli_agent/__init__.py +0 -0
  351. package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
  352. package/server/tests/services/cli_agent/test_providers.py +358 -0
  353. package/server/tests/services/cli_agent/test_service.py +298 -0
  354. package/server/tests/services/memory/__init__.py +0 -0
  355. package/server/tests/services/memory/test_jsonl.py +188 -0
  356. package/server/tests/services/test_events.py +333 -0
  357. package/server/tests/test_node_spec.py +56 -16
  358. package/server/tests/test_plugin_helpers.py +116 -0
  359. package/server/tests/test_plugin_self_containment.py +486 -0
  360. package/server/tests/test_status_broadcasts.py +425 -0
  361. package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
  362. package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
  363. package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
  364. package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
  365. package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
  366. package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
  367. package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
  368. package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
  369. package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
  370. package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
  371. package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
  372. package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
  373. package/client/dist/assets/index-D9tZfgvi.js +0 -363
  374. package/client/dist/assets/index-al7snTkG.css +0 -1
  375. package/client/src/components/credentials/providers.tsx +0 -177
  376. package/server/routers/google.py +0 -277
  377. package/server/routers/maps.py +0 -142
  378. package/server/routers/twitter.py +0 -365
  379. package/server/services/claude_code_service.py +0 -106
  380. package/server/services/memory.py +0 -159
  381. package/server/services/node_option_loaders/__init__.py +0 -77
  382. package/server/services/node_option_loaders/android_loaders.py +0 -55
  383. package/server/services/node_option_loaders/google_loaders.py +0 -97
  384. package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
  385. package/server/services/twitter_oauth.py +0 -411
  386. package/server/services/websocket_client.py +0 -29
  387. /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
  388. /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
  389. /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
  390. /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
  391. /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
  392. /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
  393. /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
@@ -61,7 +61,10 @@ MessageHandler = Callable[[Dict[str, Any], WebSocket], Awaitable[Dict[str, Any]]
61
61
 
62
62
  def ws_handler(*required_fields: str):
63
63
  """Simple decorator for WebSocket handlers. Validates required fields and wraps errors."""
64
+ import functools
65
+
64
66
  def decorator(func: MessageHandler) -> MessageHandler:
67
+ @functools.wraps(func)
65
68
  async def wrapper(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
66
69
  for field in required_fields:
67
70
  if not data.get(field):
@@ -297,7 +300,7 @@ async def handle_load_options(
297
300
  Body: ``{"method": "...", "params": {...}}``
298
301
  Response: ``{"options": [{"value": ..., "label": ...}]}``
299
302
  """
300
- from services.node_option_loaders import dispatch_load_options
303
+ from services.ws_handler_registry import dispatch_load_options
301
304
 
302
305
  options = await dispatch_load_options(data["method"], data.get("params", {}))
303
306
  return {"method": data["method"], "options": options}
@@ -309,7 +312,7 @@ async def handle_list_load_options_methods(
309
312
  ) -> Dict[str, Any]:
310
313
  """Return registered loadOptionsMethod names. Editor uses this to
311
314
  know which dynamic-option loaders are wired."""
312
- from services.node_option_loaders import list_load_options_methods
315
+ from services.ws_handler_registry import list_load_options_methods
313
316
 
314
317
  return {"methods": list_load_options_methods()}
315
318
 
@@ -358,9 +361,24 @@ async def handle_get_credential_catalogue(data: Dict[str, Any], websocket: WebSo
358
361
  kind = provider.get("kind", "")
359
362
  status_hook = provider.get("status_hook")
360
363
 
361
- if status_hook:
362
- # Status-hook providers (whatsapp, android, twitter, google, telegram)
363
- # use OAuth tokens or special connection state.
364
+ tokens = None
365
+ # Declarative per-provider override for the "stored" check.
366
+ # Lets Telegram (kind=oauth + status_hook but actual storage
367
+ # is api_key for the bot token) signal that "stored" should
368
+ # be ``has_valid_key("telegram_bot_token")`` rather than the
369
+ # default ``get_oauth_tokens(status_hook)`` lookup. Other
370
+ # providers don't declare ``stored_check`` and keep the
371
+ # original kind/status_hook-based logic untouched -- so
372
+ # Google's saved client_secret (password field) does NOT
373
+ # flip the connected dot before the user actually completes
374
+ # the OAuth flow.
375
+ stored_check = provider.get("stored_check")
376
+ if stored_check and stored_check.get("type") == "api_key":
377
+ provider["stored"] = await auth_service.has_valid_key(stored_check.get("key", pid))
378
+ elif status_hook:
379
+ # Status-hook providers (whatsapp, android, twitter, google,
380
+ # claude_code, codex_cli) use OAuth tokens for the runtime
381
+ # connection state.
364
382
  tokens = await auth_service.get_oauth_tokens(status_hook)
365
383
  provider["stored"] = tokens is not None
366
384
  elif kind == "apiKey":
@@ -373,6 +391,15 @@ async def handle_get_credential_catalogue(data: Dict[str, Any], websocket: WebSo
373
391
  else:
374
392
  provider["stored"] = False
375
393
 
394
+ # Surface the connected account identifier (email > display name)
395
+ # so the modal can render "Connected as foo@bar.com" without a
396
+ # per-provider status hook. Twitter / Google / Stripe / Claude
397
+ # all populate `email` / `name` via `auth_service.store_oauth_tokens`.
398
+ if tokens:
399
+ provider["account_label"] = tokens.get("email") or tokens.get("name")
400
+ else:
401
+ provider["account_label"] = None
402
+
376
403
  return catalogue
377
404
 
378
405
 
@@ -1218,35 +1245,61 @@ async def handle_get_ai_models(data: Dict[str, Any], websocket: WebSocket) -> Di
1218
1245
  async def handle_validate_api_key(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1219
1246
  """Validate and store an API key.
1220
1247
 
1221
- Single entry point for all providers. Dispatches to provider-specific
1222
- validators (Google Maps geocode probe, Apify /users/me probe) for
1223
- non-LLM providers; otherwise falls through to the default LLM
1224
- ``/v1/models`` probe via ``ai_service.fetch_models``. The frontend
1225
- calls this one handler for every provider no per-provider branching
1226
- on the TypeScript side.
1248
+ Pure dispatch looks up the plugin's ``Credential`` subclass in
1249
+ ``CREDENTIAL_REGISTRY`` and calls its ``validate`` classmethod. The
1250
+ base ``Credential.validate`` (defined in
1251
+ ``services/plugin/credential.py``) wires the shared scaffold
1252
+ (storage + status broadcast + error classification + response
1253
+ envelope) and dispatches the per-provider probe via the
1254
+ subclass-supplied ``_probe`` hook. Cloud LLM providers inherit
1255
+ ``_LLMApiKey._probe`` (``ai_service.fetch_models``); Maps + Apify +
1256
+ local-LLM credentials override ``_probe`` (or the whole ``validate``
1257
+ method, in the local-LLM case) with their own bespoke probes.
1258
+
1259
+ The router doesn't know about specific providers — adding a new
1260
+ provider with a special validator is a single new ``_probe``
1261
+ override on the plugin's ``Credential`` subclass.
1227
1262
  """
1263
+ from services.plugin.credential import CREDENTIAL_REGISTRY
1264
+
1228
1265
  provider = data["provider"].lower()
1229
1266
  normalized = dict(data, provider=provider)
1230
1267
 
1231
- if provider in _SPECIAL_PROVIDER_VALIDATORS:
1232
- return await _SPECIAL_PROVIDER_VALIDATORS[provider](normalized, websocket)
1268
+ cred_cls = CREDENTIAL_REGISTRY.get(provider)
1269
+ if cred_cls is None:
1270
+ return {
1271
+ "success": False,
1272
+ "valid": False,
1273
+ "error": f"Unknown provider '{provider}' — no Credential class registered.",
1274
+ }
1275
+ return await cred_cls.validate(normalized)
1233
1276
 
1234
- ai_service = container.ai_service()
1235
- auth_service = container.auth_service()
1236
- broadcaster = get_status_broadcaster()
1237
- api_key = data["api_key"].strip()
1238
1277
 
1239
- # Fetch models for AI providers (any provider with a models_endpoint in llm_defaults.json)
1240
- from services.ai import PROVIDER_CONFIGS
1241
- models = await ai_service.fetch_models(provider, api_key) if provider in PROVIDER_CONFIGS else []
1242
- await auth_service.store_api_key(provider=provider, api_key=api_key, models=models,
1243
- session_id=data.get("session_id", "default"))
1244
- # Broadcast with hasKey and models so frontend can update reactively
1245
- await broadcaster.update_api_key_status(
1246
- provider=provider, valid=True, message="API key validated",
1247
- has_key=True, models=models
1248
- )
1249
- return {"provider": provider, "valid": True, "models": models, "timestamp": time.time()}
1278
+ def _lookup_credential_default(storage_key: str) -> Optional[str]:
1279
+ """Look up a field's catalogue ``default`` for the given storage key.
1280
+
1281
+ Storage keys are either the provider id (``"openai"`` for cloud
1282
+ providers whose field key is ``apiKey``) or the field key itself
1283
+ (``"lmstudio_proxy"`` etc. for local-LLM providers). Both shapes
1284
+ map back to a credential_providers.json field; this helper finds
1285
+ the one and returns its ``default`` value if declared. Used by
1286
+ ``handle_get_stored_api_key`` to surface canonical defaults
1287
+ (e.g. local-LLM Base URL ``http://localhost:1234/v1``) to the
1288
+ frontend without requiring per-panel pre-fill logic.
1289
+ """
1290
+ from services.credential_registry import get_credential_registry
1291
+ registry = get_credential_registry()
1292
+ for provider in registry.get_all_providers():
1293
+ provider_id = provider.get("id") or provider.get("name", "").lower()
1294
+ for field in (provider.get("fields") or []):
1295
+ field_key = field.get("key")
1296
+ if not field_key:
1297
+ continue
1298
+ field_storage_key = provider_id if field_key == "apiKey" else field_key
1299
+ if field_storage_key == storage_key:
1300
+ default = field.get("default")
1301
+ return default if default else None
1302
+ return None
1250
1303
 
1251
1304
 
1252
1305
  @ws_handler("provider")
@@ -1257,11 +1310,21 @@ async def handle_get_stored_api_key(data: Dict[str, Any], websocket: WebSocket)
1257
1310
  ``update_api_key_status`` broadcast shape — every WS payload the
1258
1311
  frontend receives for API key state uses the same convention, so no
1259
1312
  per-field adapter is needed on the TypeScript side.
1313
+
1314
+ When nothing is stored AND the catalogue declares a ``default``
1315
+ for this field (e.g. local-LLM canonical Base URL), the default
1316
+ value is returned in ``apiKey`` with ``hasKey: false``. The
1317
+ frontend renders the value but tracks ``stored`` separately via
1318
+ ``hasKey`` so the validated/connected badge stays honest. Lets
1319
+ users click Fetch on a fresh install without retyping the URL.
1260
1320
  """
1261
1321
  auth_service = container.auth_service()
1262
1322
  provider = data["provider"].lower()
1263
1323
  api_key = await auth_service.get_api_key(provider, data.get("session_id", "default"))
1264
1324
  if not api_key:
1325
+ default = _lookup_credential_default(provider)
1326
+ if default is not None:
1327
+ return {"provider": provider, "hasKey": False, "apiKey": default}
1265
1328
  return {"provider": provider, "hasKey": False}
1266
1329
  models = await auth_service.get_stored_models(provider, data.get("session_id", "default"))
1267
1330
  return {"provider": provider, "hasKey": True, "apiKey": api_key, "models": models, "timestamp": time.time()}
@@ -1283,12 +1346,20 @@ async def handle_save_api_key(data: Dict[str, Any], websocket: WebSocket) -> Dic
1283
1346
 
1284
1347
  async def _do_save() -> Dict[str, Any]:
1285
1348
  auth_service = container.auth_service()
1349
+ broadcaster = get_status_broadcaster()
1286
1350
  await auth_service.store_api_key(
1287
1351
  provider=provider,
1288
1352
  api_key=data["api_key"].strip(),
1289
1353
  models=data.get("models", []),
1290
1354
  session_id=data.get("session_id", "default"),
1291
1355
  )
1356
+ # Symmetric broadcast: tells every connected client to refetch
1357
+ # the catalogue so the `stored` flag flips on this provider.
1358
+ # Don't claim validity here — save_api_key doesn't validate.
1359
+ await broadcaster.broadcast_credential_event(
1360
+ "credential.api_key.saved",
1361
+ provider=provider,
1362
+ )
1292
1363
  return {"provider": data["provider"]}
1293
1364
 
1294
1365
  return await store.run(data.get("request_id"), _do_save)
@@ -1307,7 +1378,20 @@ async def handle_delete_api_key(data: Dict[str, Any], websocket: WebSocket) -> D
1307
1378
 
1308
1379
  async def _do_delete() -> Dict[str, Any]:
1309
1380
  auth_service = container.auth_service()
1381
+ broadcaster = get_status_broadcaster()
1310
1382
  await auth_service.remove_api_key(provider, data.get("session_id", "default"))
1383
+ # Two broadcasts: api_key_status clears `apiKeyStatuses[provider]`
1384
+ # on every connected client (in-memory validation cache); the
1385
+ # CloudEvents-typed credential.api_key.deleted invalidates the
1386
+ # catalogue so the `stored` flag flips. Both go through the
1387
+ # 300 ms invalidateCatalogue debounce — one refetch.
1388
+ await broadcaster.update_api_key_status(
1389
+ provider, valid=False, has_key=False, message="deleted", models=[],
1390
+ )
1391
+ await broadcaster.broadcast_credential_event(
1392
+ "credential.api_key.deleted",
1393
+ provider=provider,
1394
+ )
1311
1395
  return {"provider": data["provider"]}
1312
1396
 
1313
1397
  return await store.run(data.get("request_id"), _do_delete)
@@ -1326,306 +1410,17 @@ async def handle_claude_oauth_login(data: Dict[str, Any], websocket: WebSocket)
1326
1410
 
1327
1411
  @ws_handler()
1328
1412
  async def handle_claude_oauth_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1329
- """Check Claude OAuth credentials status."""
1330
- from services.claude_oauth import get_claude_credentials
1331
- return get_claude_credentials()
1332
-
1333
-
1334
- # ============================================================================
1335
- # Twitter OAuth Handlers
1336
- # ============================================================================
1337
-
1338
- @ws_handler()
1339
- async def handle_twitter_oauth_login(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1340
- """
1341
- Initiate Twitter OAuth 2.0 with PKCE flow.
1342
-
1343
- Opens browser to Twitter authorization page. After user authorizes,
1344
- Twitter redirects to /api/twitter/callback which stores tokens.
1345
- """
1346
- from services.twitter_oauth import TwitterOAuth
1347
-
1348
- auth_service = container.auth_service()
1349
-
1350
- # Get stored client credentials (configured via Credentials Modal)
1351
- client_id = await auth_service.get_api_key("twitter_client_id")
1352
- client_secret = await auth_service.get_api_key("twitter_client_secret")
1353
-
1354
- if not client_id:
1355
- return {
1356
- "success": False,
1357
- "error": "Twitter Client ID not configured. Add your Twitter API credentials first."
1358
- }
1359
-
1360
- # Create OAuth instance and generate authorization URL
1361
- from services.oauth_utils import get_redirect_uri
1362
- redirect_uri = get_redirect_uri(websocket, "twitter")
1363
-
1364
- oauth = TwitterOAuth(
1365
- client_id=client_id,
1366
- client_secret=client_secret,
1367
- redirect_uri=redirect_uri,
1368
- )
1369
-
1370
- auth_data = oauth.generate_authorization_url()
1371
-
1372
- return {
1373
- "success": True,
1374
- "message": "Opening Twitter authorization in browser...",
1375
- "url": auth_data["url"],
1376
- "state": auth_data["state"],
1377
- }
1378
-
1379
-
1380
- @ws_handler()
1381
- async def handle_twitter_oauth_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1382
- """
1383
- Check Twitter OAuth connection status.
1384
-
1385
- Returns connection status and user info if connected.
1386
- Uses OAuth token system (matches REST endpoint in routers/twitter.py).
1387
- """
1388
- from services.twitter_oauth import TwitterOAuth
1389
-
1390
- auth_service = container.auth_service()
1391
-
1392
- # Get tokens from OAuth system (NOT api_key system)
1393
- tokens = await auth_service.get_oauth_tokens("twitter", customer_id="owner")
1394
-
1395
- if not tokens or not tokens.get("access_token"):
1396
- return {
1397
- "connected": False,
1398
- "username": None,
1399
- "user_id": None,
1400
- }
1401
-
1402
- access_token = tokens["access_token"]
1403
- refresh_token = tokens.get("refresh_token", "")
1404
-
1405
- # Get client credentials for API calls (these are correctly stored as API keys)
1406
- client_id = await auth_service.get_api_key("twitter_client_id") or ""
1407
- client_secret = await auth_service.get_api_key("twitter_client_secret")
1408
-
1409
- from services.oauth_utils import get_redirect_uri
1410
- redirect_uri = get_redirect_uri(websocket, "twitter")
1411
-
1412
- oauth = TwitterOAuth(
1413
- client_id=client_id,
1414
- client_secret=client_secret,
1415
- redirect_uri=redirect_uri,
1416
- )
1417
-
1418
- # Verify token by getting user info
1419
- user_info = await oauth.get_user_info(access_token)
1420
-
1421
- if not user_info.get("success") and refresh_token:
1422
- refresh_result = await oauth.refresh_access_token(refresh_token)
1423
- if refresh_result.get("success"):
1424
- # Store refreshed tokens in OAuth system
1425
- await auth_service.store_oauth_tokens(
1426
- provider="twitter",
1427
- access_token=refresh_result["access_token"],
1428
- refresh_token=refresh_result.get("refresh_token") or refresh_token,
1429
- email=tokens.get("email", ""),
1430
- name=tokens.get("name", ""),
1431
- scopes=tokens.get("scopes", ""),
1432
- customer_id="owner",
1433
- )
1434
- # Retry user info
1435
- user_info = await oauth.get_user_info(refresh_result["access_token"])
1436
-
1437
- if not user_info.get("success"):
1438
- return {
1439
- "connected": False,
1440
- "username": None,
1441
- "user_id": None,
1442
- "error": user_info.get("error"),
1443
- }
1444
-
1445
- return {
1446
- "connected": True,
1447
- "username": user_info.get("username"),
1448
- "user_id": user_info.get("id"),
1449
- "name": user_info.get("name"),
1450
- "profile_image_url": user_info.get("profile_image_url"),
1451
- "verified": user_info.get("verified"),
1452
- }
1453
-
1454
-
1455
- @ws_handler()
1456
- async def handle_twitter_logout(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1457
- """
1458
- Disconnect Twitter by revoking tokens and clearing stored credentials.
1459
- Uses OAuth token system (matches REST endpoint in routers/twitter.py).
1460
- """
1461
- from services.twitter_oauth import TwitterOAuth
1462
-
1463
- auth_service = container.auth_service()
1464
-
1465
- # Get tokens from OAuth system
1466
- tokens = await auth_service.get_oauth_tokens("twitter", customer_id="owner")
1467
- access_token = tokens.get("access_token") if tokens else None
1468
- refresh_token = tokens.get("refresh_token") if tokens else None
1469
-
1470
- # Get client credentials (correctly stored as API keys)
1471
- client_id = await auth_service.get_api_key("twitter_client_id") or ""
1472
- client_secret = await auth_service.get_api_key("twitter_client_secret")
1473
-
1474
- # Revoke tokens if we have them
1475
- if access_token or refresh_token:
1476
- from services.oauth_utils import get_redirect_uri
1477
- redirect_uri = get_redirect_uri(websocket, "twitter")
1478
-
1479
- oauth = TwitterOAuth(
1480
- client_id=client_id,
1481
- client_secret=client_secret,
1482
- redirect_uri=redirect_uri,
1483
- )
1484
-
1485
- if access_token:
1486
- await oauth.revoke_token(access_token, "access_token")
1487
- if refresh_token:
1488
- await oauth.revoke_token(refresh_token, "refresh_token")
1489
-
1490
- # Clear from OAuth system
1491
- await auth_service.remove_oauth_tokens("twitter", customer_id="owner")
1492
-
1493
- # Clean up any stale API key entries (from old broken code)
1494
- for key in ["twitter_access_token", "twitter_refresh_token", "twitter_user_info"]:
1495
- try:
1496
- await auth_service.remove_api_key(key)
1497
- except Exception:
1498
- pass
1499
-
1500
- return {"success": True, "message": "Twitter disconnected"}
1501
-
1502
-
1503
- # ============================================================================
1504
- # Google Workspace OAuth Handlers
1505
- # ============================================================================
1506
-
1507
- @ws_handler()
1508
- async def handle_google_oauth_login(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1509
- """
1510
- Initiate Google Workspace OAuth 2.0 flow.
1511
-
1512
- Opens browser to Google authorization page. After user authorizes,
1513
- Google redirects to /api/google/callback which stores tokens.
1514
- Grants access to all Google Workspace services (Gmail, Calendar, Drive, etc).
1515
- """
1516
- from services.google_oauth import GoogleOAuth
1517
-
1518
- auth_service = container.auth_service()
1519
-
1520
- client_id = await auth_service.get_api_key("google_client_id")
1521
- client_secret = await auth_service.get_api_key("google_client_secret")
1522
-
1523
- if not client_id or not client_secret:
1524
- return {
1525
- "success": False,
1526
- "error": "Google Workspace Client ID and Secret not configured. Add your Google API credentials first."
1527
- }
1528
-
1529
- from services.oauth_utils import get_redirect_uri
1530
- redirect_uri = get_redirect_uri(websocket, "google")
1413
+ """Check Claude OAuth credentials status via ``claude auth status``."""
1414
+ from services.claude_oauth import claude_auth_status
1415
+ has_token = await claude_auth_status()
1416
+ return {"success": True, "has_token": has_token}
1531
1417
 
1532
- oauth = GoogleOAuth(
1533
- client_id=client_id,
1534
- client_secret=client_secret,
1535
- redirect_uri=redirect_uri,
1536
- )
1537
1418
 
1538
- auth_data = oauth.generate_authorization_url()
1419
+ # NOTE: `cli_login` / `cli_auth_status` handlers are owned by
1420
+ # `services/cli_agent/_handlers.py` and self-registered into
1421
+ # `services.ws_handler_registry` on package import — no entries needed
1422
+ # here. See `services/cli_agent/__init__.py`.
1539
1423
 
1540
- return {
1541
- "success": True,
1542
- "message": "Opening Google authorization in browser...",
1543
- "url": auth_data["url"],
1544
- "state": auth_data["state"],
1545
- }
1546
-
1547
-
1548
- @ws_handler()
1549
- async def handle_google_oauth_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1550
- """
1551
- Check Google Workspace OAuth connection status.
1552
-
1553
- Returns connection status and user info if connected.
1554
- Also updates broadcaster status so all clients stay in sync.
1555
- """
1556
- from services.google_oauth import GoogleOAuth
1557
- from services.status_broadcaster import get_status_broadcaster
1558
-
1559
- auth_service = container.auth_service()
1560
- broadcaster = get_status_broadcaster()
1561
-
1562
- tokens = await auth_service.get_oauth_tokens("google", customer_id="owner")
1563
-
1564
- if not tokens or not tokens.get("access_token"):
1565
- status = {"connected": False, "email": None, "name": None}
1566
- broadcaster._status["google"] = status
1567
- return status
1568
-
1569
- email = tokens.get("email")
1570
- name = tokens.get("name")
1571
- refresh_token = tokens.get("refresh_token")
1572
-
1573
- # Try to refresh token proactively
1574
- try:
1575
- client_id = await auth_service.get_api_key("google_client_id") or ""
1576
- client_secret = await auth_service.get_api_key("google_client_secret") or ""
1577
-
1578
- if refresh_token and client_id and client_secret:
1579
- refreshed = GoogleOAuth.refresh_credentials(
1580
- refresh_token=refresh_token,
1581
- client_id=client_id,
1582
- client_secret=client_secret,
1583
- )
1584
- if refreshed.get("success") and refreshed.get("access_token"):
1585
- await auth_service.store_oauth_tokens(
1586
- provider="google",
1587
- access_token=refreshed["access_token"],
1588
- refresh_token=refresh_token,
1589
- email=email,
1590
- name=name,
1591
- customer_id="owner",
1592
- )
1593
-
1594
- status = {"connected": True, "email": email, "name": name}
1595
- broadcaster._status["google"] = status
1596
- # Broadcast so all connected clients update
1597
- await broadcaster.broadcast({
1598
- "type": "google_status",
1599
- "data": status,
1600
- })
1601
- return status
1602
- except Exception as e:
1603
- logger.warning(f"Google token validation failed: {e}")
1604
- status = {"connected": False, "email": None, "name": None, "error": str(e)}
1605
- broadcaster._status["google"] = {"connected": False, "email": None, "name": None}
1606
- return status
1607
-
1608
-
1609
- @ws_handler()
1610
- async def handle_google_logout(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1611
- """
1612
- Disconnect Google Workspace by clearing stored credentials.
1613
- """
1614
- from services.status_broadcaster import get_status_broadcaster
1615
-
1616
- auth_service = container.auth_service()
1617
- broadcaster = get_status_broadcaster()
1618
-
1619
- await auth_service.remove_oauth_tokens("google", customer_id="owner")
1620
-
1621
- # Clear broadcaster status and notify all clients
1622
- broadcaster._status["google"] = {"connected": False, "email": None, "name": None}
1623
- await broadcaster.broadcast({
1624
- "type": "google_status",
1625
- "data": {"connected": False, "email": None, "name": None},
1626
- })
1627
-
1628
- return {"success": True, "message": "Google Workspace disconnected"}
1629
1424
 
1630
1425
 
1631
1426
  @ws_handler("url")
@@ -1666,458 +1461,22 @@ async def handle_test_ai_proxy(data: Dict[str, Any], websocket: WebSocket) -> Di
1666
1461
 
1667
1462
 
1668
1463
  # ============================================================================
1669
- # Android Handlers
1670
- # ============================================================================
1671
-
1672
- @ws_handler()
1673
- async def handle_get_android_devices(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1674
- """Get list of connected Android devices."""
1675
- android_service = container.android_service()
1676
- devices = await android_service.list_devices()
1677
- return {"devices": devices, "timestamp": time.time()}
1678
-
1679
-
1680
- @ws_handler("service_id", "action")
1681
- async def handle_execute_android_action(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1682
- """Execute an Android service action."""
1683
- android_service = container.android_service()
1684
- broadcaster = get_status_broadcaster()
1685
- service_id, action = data["service_id"], data["action"]
1686
- node_id = data.get("node_id", f"android_{service_id}_{action}")
1687
-
1688
- await broadcaster.update_node_status(node_id, "executing")
1689
- result = await android_service.execute_service(
1690
- node_id=node_id, service_id=service_id, action=action,
1691
- parameters=data.get("parameters", {}),
1692
- android_host=data.get("android_host", "localhost"),
1693
- android_port=data.get("android_port", 8888)
1694
- )
1695
-
1696
- status = "success" if result.get("success") else "error"
1697
- await broadcaster.update_node_status(node_id, status, result.get("result") or {"error": result.get("error")})
1698
- return result
1699
-
1700
-
1701
- @ws_handler()
1702
- async def handle_android_relay_connect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1703
- """Connect to Android relay server.
1704
-
1705
- Establishes WebSocket connection to relay server and broadcasts QR code for pairing.
1706
- Status updates are automatically broadcast via the relay client's broadcaster integration.
1707
- """
1708
- from services.android import get_relay_client
1709
-
1710
- url = data.get("url", "")
1711
- api_key = data.get("api_key")
1712
-
1713
- if not url:
1714
- return {
1715
- "success": False,
1716
- "connected": False,
1717
- "error": "Relay URL is required"
1718
- }
1719
-
1720
- if not api_key:
1721
- return {
1722
- "success": False,
1723
- "connected": False,
1724
- "error": "API key is required"
1725
- }
1726
-
1727
- logger.info(f"[WebSocket] Android relay connect: {url}")
1728
-
1729
- try:
1730
- client, error = await get_relay_client(url, api_key)
1731
- if client:
1732
- logger.info(f"[WebSocket] Android relay connect success, qr_data present: {bool(client.qr_data)}, session_token: {client.session_token}")
1733
- return {
1734
- "success": True,
1735
- "connected": True,
1736
- "session_token": client.session_token,
1737
- "qr_data": client.qr_data,
1738
- "message": "Connected to relay server"
1739
- }
1740
- else:
1741
- return {
1742
- "success": False,
1743
- "connected": False,
1744
- "error": error or "Failed to connect to relay server"
1745
- }
1746
- except Exception as e:
1747
- logger.error(f"[WebSocket] Android relay connect error: {e}")
1748
- return {"success": False, "error": str(e)}
1749
-
1750
-
1751
- @ws_handler()
1752
- async def handle_android_relay_disconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1753
- """Disconnect from Android relay server.
1754
-
1755
- Closes the relay WebSocket connection and broadcasts disconnected status.
1756
- """
1757
- from services.android import close_relay_client
1758
-
1759
- logger.info("[WebSocket] Android relay disconnect requested")
1760
-
1761
- try:
1762
- await close_relay_client()
1763
- return {
1764
- "success": True,
1765
- "connected": False,
1766
- "message": "Disconnected from relay server"
1767
- }
1768
- except Exception as e:
1769
- logger.error(f"[WebSocket] Android relay disconnect error: {e}")
1770
- return {"success": False, "error": str(e)}
1771
-
1772
-
1773
- @ws_handler()
1774
- async def handle_android_relay_reconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1775
- """Reconnect to Android relay server with a new session token.
1776
-
1777
- Forces disconnect and reconnect to get fresh session_token and QR code.
1778
- Useful when pairing fails or Android device needs to re-pair.
1779
- """
1780
- from services.android import close_relay_client, get_relay_client
1781
-
1782
- url = data.get("url", "")
1783
- api_key = data.get("api_key")
1784
-
1785
- if not url:
1786
- return {
1787
- "success": False,
1788
- "connected": False,
1789
- "error": "Relay URL is required"
1790
- }
1791
-
1792
- if not api_key:
1793
- return {
1794
- "success": False,
1795
- "connected": False,
1796
- "error": "API key is required"
1797
- }
1798
-
1799
- logger.info("[WebSocket] Android relay reconnect: forcing new session")
1800
-
1801
- try:
1802
- # Force disconnect existing connection
1803
- await close_relay_client()
1804
-
1805
- # Small delay to ensure clean disconnect
1806
- await asyncio.sleep(0.5)
1807
-
1808
- # Reconnect with fresh session
1809
- client, error = await get_relay_client(url, api_key)
1810
- if client:
1811
- return {
1812
- "success": True,
1813
- "connected": True,
1814
- "session_token": client.session_token,
1815
- "qr_data": client.qr_data,
1816
- "message": "Reconnected with new session token"
1817
- }
1818
- else:
1819
- return {
1820
- "success": False,
1821
- "connected": False,
1822
- "error": error or "Failed to reconnect to relay server"
1823
- }
1824
- except Exception as e:
1825
- logger.error(f"[WebSocket] Android relay reconnect error: {e}")
1826
- return {"success": False, "error": str(e)}
1827
-
1828
-
1829
- # ============================================================================
1830
- # Maps Handlers
1831
- # ============================================================================
1832
-
1833
- async def handle_validate_maps_key(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1834
- """Validate Google Maps API key and save to database if valid."""
1835
- import httpx
1836
- broadcaster = get_status_broadcaster()
1837
- auth_service = container.auth_service()
1838
-
1839
- api_key = data.get("api_key", "").strip()
1840
- session_id = data.get("session_id", "default")
1841
-
1842
- if not api_key:
1843
- return {"success": False, "valid": False, "error": "api_key required"}
1844
-
1845
- try:
1846
- # Test the API key with a simple geocoding request
1847
- async with httpx.AsyncClient() as client:
1848
- response = await client.get(
1849
- "https://maps.googleapis.com/maps/api/geocode/json",
1850
- params={
1851
- "address": "1600 Amphitheatre Parkway, Mountain View, CA",
1852
- "key": api_key
1853
- },
1854
- timeout=10.0
1855
- )
1856
-
1857
- response_data = response.json()
1858
-
1859
- if response_data.get("status") == "OK":
1860
- # Save the validated key to database
1861
- await auth_service.store_api_key(
1862
- provider="google_maps",
1863
- api_key=api_key,
1864
- models=[],
1865
- session_id=session_id
1866
- )
1867
- await broadcaster.update_api_key_status(
1868
- provider="google_maps",
1869
- valid=True,
1870
- message="API key validated successfully"
1871
- )
1872
- return {"success": True, "valid": True, "message": "Google Maps API key is valid"}
1873
-
1874
- elif response_data.get("status") == "REQUEST_DENIED":
1875
- error_msg = response_data.get("error_message", "Invalid API key")
1876
- await broadcaster.update_api_key_status(
1877
- provider="google_maps",
1878
- valid=False,
1879
- message=error_msg
1880
- )
1881
- return {"success": True, "valid": False, "message": error_msg}
1882
-
1883
- else:
1884
- # Other statuses like ZERO_RESULTS still mean the key works
1885
- # Save the validated key to database
1886
- await auth_service.store_api_key(
1887
- provider="google_maps",
1888
- api_key=api_key,
1889
- models=[],
1890
- session_id=session_id
1891
- )
1892
- await broadcaster.update_api_key_status(
1893
- provider="google_maps",
1894
- valid=True,
1895
- message="API key validated"
1896
- )
1897
- return {"success": True, "valid": True, "message": f"API key is valid (status: {response_data.get('status')})"}
1898
-
1899
- except httpx.TimeoutException:
1900
- await broadcaster.update_api_key_status(
1901
- provider="google_maps",
1902
- valid=False,
1903
- message="Validation request timed out"
1904
- )
1905
- return {"success": False, "valid": False, "error": "Validation request timed out"}
1906
-
1907
- except Exception as e:
1908
- logger.error("Maps key validation failed", error=str(e))
1909
- await broadcaster.update_api_key_status(
1910
- provider="google_maps",
1911
- valid=False,
1912
- message=str(e)
1913
- )
1914
- return {"success": False, "valid": False, "error": str(e)}
1915
-
1916
-
1917
- # ============================================================================
1918
- # Apify Handlers
1464
+ # Android handlers (5 of them: get_android_devices, execute_android_action,
1465
+ # android_relay_{connect,disconnect,reconnect}) live in
1466
+ # ``nodes/android/_handlers.py`` and self-register via
1467
+ # ``register_ws_handlers``. The plugin's HTTP router lives in
1468
+ # ``nodes/android/_router.py`` and mounts via the plugin-router loop.
1919
1469
  # ============================================================================
1920
1470
 
1921
- async def handle_validate_apify_key(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1922
- """Validate Apify API token and save to database if valid."""
1923
- from nodes.scraper.apify_actor import validate_apify_token
1924
-
1925
- broadcaster = get_status_broadcaster()
1926
- auth_service = container.auth_service()
1927
-
1928
- api_key = data.get("api_key", "").strip()
1929
- session_id = data.get("session_id", "default")
1930
-
1931
- if not api_key:
1932
- return {"success": False, "valid": False, "error": "api_key required"}
1933
-
1934
- try:
1935
- result = await validate_apify_token(api_key)
1936
-
1937
- if result.get("valid"):
1938
- # Save the validated key to database
1939
- await auth_service.store_api_key(
1940
- provider="apify",
1941
- api_key=api_key,
1942
- models=[],
1943
- session_id=session_id
1944
- )
1945
- await broadcaster.update_api_key_status(
1946
- provider="apify",
1947
- valid=True,
1948
- message=f"Apify token validated - user: {result.get('username', 'unknown')}"
1949
- )
1950
- return {
1951
- "success": True,
1952
- "valid": True,
1953
- "message": "Apify API token is valid",
1954
- "username": result.get("username"),
1955
- "email": result.get("email"),
1956
- "plan": result.get("plan")
1957
- }
1958
- else:
1959
- error_msg = result.get("error", "Invalid API token")
1960
- await broadcaster.update_api_key_status(
1961
- provider="apify",
1962
- valid=False,
1963
- message=error_msg
1964
- )
1965
- return {"success": True, "valid": False, "message": error_msg}
1966
-
1967
- except Exception as e:
1968
- logger.error("Apify key validation failed", error=str(e))
1969
- await broadcaster.update_api_key_status(
1970
- provider="apify",
1971
- valid=False,
1972
- message=str(e)
1973
- )
1974
- return {"success": False, "valid": False, "error": str(e)}
1975
1471
 
1976
-
1977
- # Per-provider validation strategies. Register any non-LLM provider here
1978
- # and `handle_validate_api_key` will dispatch to it automatically, so the
1979
- # frontend only ever calls one WS message type.
1980
- _SPECIAL_PROVIDER_VALIDATORS = {
1981
- "google_maps": handle_validate_maps_key,
1982
- "apify": handle_validate_apify_key,
1983
- }
1984
-
1985
-
1986
- # ============================================================================
1987
- # WhatsApp Handlers - Wrappers for services.whatsapp_service functions
1988
- # ============================================================================
1989
-
1990
- from services.whatsapp_service import (
1991
- handle_whatsapp_status as _wa_status,
1992
- handle_whatsapp_connected_phone as _wa_connected_phone,
1993
- handle_whatsapp_qr as _wa_qr,
1994
- handle_whatsapp_send as _wa_send,
1995
- handle_whatsapp_start as _wa_start,
1996
- handle_whatsapp_restart as _wa_restart,
1997
- handle_whatsapp_groups as _wa_groups,
1998
- handle_whatsapp_group_info as _wa_group_info,
1999
- handle_whatsapp_chat_history as _wa_chat_history,
2000
- handle_whatsapp_newsletters as _wa_newsletters,
2001
- whatsapp_rpc_call as _wa_rpc_call,
2002
- )
2003
-
2004
-
2005
- async def handle_whatsapp_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2006
- return await _wa_status()
2007
-
2008
-
2009
- async def handle_whatsapp_connected_phone(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2010
- """Get the connected WhatsApp phone number."""
2011
- return await _wa_connected_phone()
2012
-
2013
-
2014
- async def handle_whatsapp_qr(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2015
- return await _wa_qr()
2016
-
2017
-
2018
- async def handle_whatsapp_send(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2019
- """Forward all send params to WhatsApp handler - supports all message types."""
2020
- return await _wa_send(data)
2021
-
2022
-
2023
- async def handle_whatsapp_start(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2024
- return await _wa_start()
2025
-
2026
-
2027
- async def handle_whatsapp_restart(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2028
- return await _wa_restart()
2029
-
2030
-
2031
- async def handle_whatsapp_groups(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2032
- return await _wa_groups()
2033
-
2034
-
2035
- async def handle_whatsapp_newsletters(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2036
- """Get list of subscribed newsletter channels."""
2037
- return await _wa_newsletters()
2038
-
2039
-
2040
- async def handle_whatsapp_group_info(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2041
- """Get group participants with resolved phone numbers."""
2042
- group_id = data.get("group_id", "")
2043
- return await _wa_group_info(group_id)
2044
-
2045
-
2046
- async def handle_whatsapp_chat_history(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2047
- """Get chat history from WhatsApp history store."""
2048
- return await _wa_chat_history(data)
2049
-
2050
-
2051
- async def handle_whatsapp_rate_limit_get(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2052
- """Get rate limit config and current stats."""
2053
- result = await _wa_rpc_call("rate_limit_get", {})
2054
- return result
2055
-
2056
-
2057
- async def handle_whatsapp_rate_limit_set(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2058
- """Update rate limit configuration."""
2059
- config = data.get("config", {})
2060
- result = await _wa_rpc_call("rate_limit_set", config)
2061
- return result
2062
-
2063
-
2064
- async def handle_whatsapp_rate_limit_stats(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2065
- """Get current rate limit statistics."""
2066
- result = await _wa_rpc_call("rate_limit_stats", {})
2067
- return result
2068
-
2069
-
2070
- async def handle_whatsapp_rate_limit_unpause(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2071
- """Resume rate limiting after automatic pause."""
2072
- result = await _wa_rpc_call("rate_limit_unpause", {})
2073
- return result
2074
-
2075
-
2076
- async def handle_whatsapp_mark_read(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2077
- """Mark messages as read. Schema: mark_read({message_ids, chat_jid, sender_jid?})"""
2078
- message_ids = data.get("message_ids", [])
2079
- chat_jid = data.get("chat_jid", "")
2080
- if not message_ids or not chat_jid:
2081
- return {"success": False, "error": "message_ids (array) and chat_jid are required"}
2082
- params: Dict[str, Any] = {"message_ids": message_ids, "chat_jid": chat_jid}
2083
- sender_jid = data.get("sender_jid")
2084
- if sender_jid:
2085
- params["sender_jid"] = sender_jid
2086
- result = await _wa_rpc_call("mark_read", params)
2087
- return result
2088
-
2089
-
2090
- async def handle_whatsapp_typing(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2091
- """Send typing indicator. Schema: typing({jid, state: 'composing'|'paused', media?})"""
2092
- jid = data.get("jid", "")
2093
- state = data.get("state", "composing")
2094
- if not jid:
2095
- return {"success": False, "error": "jid is required"}
2096
- params: Dict[str, Any] = {"jid": jid, "state": state}
2097
- media = data.get("media")
2098
- if media:
2099
- params["media"] = media
2100
- result = await _wa_rpc_call("typing", params)
2101
- return result
2102
-
2103
-
2104
- async def handle_whatsapp_presence(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2105
- """Set online/offline presence. Schema: presence({status: 'available'|'unavailable'})"""
2106
- status = data.get("status", "available")
2107
- result = await _wa_rpc_call("presence", {"status": status})
2108
- return result
2109
-
2110
-
2111
- async def handle_whatsapp_stop(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2112
- """Graceful WhatsApp shutdown."""
2113
- result = await _wa_rpc_call("stop", {})
2114
- return result
2115
-
2116
-
2117
- async def handle_whatsapp_diagnostics(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2118
- """Get WhatsApp diagnostics/debug info."""
2119
- result = await _wa_rpc_call("diagnostics", {})
2120
- return result
1472
+ # ----------------------------------------------------------------------------
1473
+ # Per-provider credential validators (Maps geocode probe, Apify /users/me,
1474
+ # Ollama / LM Studio SDK probes) live on their plugins Credential
1475
+ # subclass via the shared Credential._probe / Credential.validate scaffold
1476
+ # in services/plugin/credential.py. handle_validate_api_key (above) is a
1477
+ # pure dispatch through CREDENTIAL_REGISTRY no per-provider branches in
1478
+ # this router.
1479
+ # ----------------------------------------------------------------------------
2121
1480
 
2122
1481
 
2123
1482
  # ============================================================================
@@ -2736,17 +2095,25 @@ async def handle_clear_memory(data: Dict[str, Any], websocket: WebSocket) -> Dic
2736
2095
 
2737
2096
  Business logic lives in :func:`services.memory.clear_agent_session_state`
2738
2097
  — this handler only decodes the request and shapes the response.
2098
+
2099
+ When ``memory_node_id`` is provided (claude_code_agent JSONL bridge
2100
+ surface), the simpleMemory node's ``memory_content`` is reset and
2101
+ ``memory_jsonl`` + ``last_session_id`` are wiped server-side. Legacy
2102
+ callers that omit it still get ``default_content`` for the
2103
+ frontend's existing markdown reset path.
2739
2104
  """
2740
2105
  from services.memory import clear_agent_session_state
2741
2106
 
2742
2107
  session_id = data.get("session_id", "default")
2743
2108
  workflow_id = data.get("workflow_id")
2744
2109
  clear_long_term = data.get("clear_long_term", False)
2110
+ memory_node_id = data.get("memory_node_id")
2745
2111
 
2746
- cleared = clear_agent_session_state(
2112
+ cleared = await clear_agent_session_state(
2747
2113
  session_id=session_id,
2748
2114
  workflow_id=workflow_id,
2749
2115
  clear_long_term=clear_long_term,
2116
+ memory_node_id=memory_node_id,
2750
2117
  )
2751
2118
 
2752
2119
  return {
@@ -2754,6 +2121,7 @@ async def handle_clear_memory(data: Dict[str, Any], websocket: WebSocket) -> Dic
2754
2121
  "default_content": "# Conversation History\n\n*No messages yet.*\n",
2755
2122
  "cleared_vector_store": cleared["cleared_vector_store"],
2756
2123
  "cleared_todo_keys": cleared["cleared_todo_keys"],
2124
+ "cleared_memory_node": cleared["cleared_memory_node"],
2757
2125
  "session_id": session_id,
2758
2126
  }
2759
2127
 
@@ -3326,48 +2694,19 @@ MESSAGE_HANDLERS: Dict[str, MessageHandler] = {
3326
2694
  "claude_oauth_status": handle_claude_oauth_status,
3327
2695
 
3328
2696
  # Twitter OAuth operations
3329
- "twitter_oauth_login": handle_twitter_oauth_login,
3330
- "twitter_oauth_status": handle_twitter_oauth_status,
3331
- "twitter_logout": handle_twitter_logout,
3332
2697
 
3333
2698
  # Google Workspace OAuth operations
3334
- "google_oauth_login": handle_google_oauth_login,
3335
- "google_oauth_status": handle_google_oauth_status,
3336
- "google_logout": handle_google_logout,
3337
2699
 
3338
2700
  # Android operations
3339
- "get_android_devices": handle_get_android_devices,
3340
- "execute_android_action": handle_execute_android_action,
3341
- "android_relay_connect": handle_android_relay_connect,
3342
- "android_relay_disconnect": handle_android_relay_disconnect,
3343
- "android_relay_reconnect": handle_android_relay_reconnect,
3344
-
3345
- # Maps operations
3346
- "validate_maps_key": handle_validate_maps_key,
3347
2701
 
3348
- # Apify operations
3349
- "validate_apify_key": handle_validate_apify_key,
2702
+ # Maps + Apify validation now flow through ``handle_validate_api_key``
2703
+ # (which dispatches via ``CREDENTIAL_REGISTRY`` to
2704
+ # ``GoogleMapsCredential._probe`` / ``ApifyCredential._probe``).
2705
+ # The legacy ``validate_maps_key`` / ``validate_apify_key`` WS message
2706
+ # types are no longer needed — the frontend already uses
2707
+ # ``validate_api_key`` for all providers.
3350
2708
 
3351
2709
  # WhatsApp operations
3352
- "whatsapp_status": handle_whatsapp_status,
3353
- "whatsapp_connected_phone": handle_whatsapp_connected_phone,
3354
- "whatsapp_qr": handle_whatsapp_qr,
3355
- "whatsapp_send": handle_whatsapp_send,
3356
- "whatsapp_start": handle_whatsapp_start,
3357
- "whatsapp_restart": handle_whatsapp_restart,
3358
- "whatsapp_groups": handle_whatsapp_groups,
3359
- "whatsapp_newsletters": handle_whatsapp_newsletters,
3360
- "whatsapp_group_info": handle_whatsapp_group_info,
3361
- "whatsapp_chat_history": handle_whatsapp_chat_history,
3362
- "whatsapp_rate_limit_get": handle_whatsapp_rate_limit_get,
3363
- "whatsapp_rate_limit_set": handle_whatsapp_rate_limit_set,
3364
- "whatsapp_rate_limit_stats": handle_whatsapp_rate_limit_stats,
3365
- "whatsapp_rate_limit_unpause": handle_whatsapp_rate_limit_unpause,
3366
- "whatsapp_mark_read": handle_whatsapp_mark_read,
3367
- "whatsapp_typing": handle_whatsapp_typing,
3368
- "whatsapp_presence": handle_whatsapp_presence,
3369
- "whatsapp_stop": handle_whatsapp_stop,
3370
- "whatsapp_diagnostics": handle_whatsapp_diagnostics,
3371
2710
 
3372
2711
  # Telegram operations live in nodes/telegram/_handlers.py and
3373
2712
  # self-register via services.ws_handler_registry. Dispatch hits them