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
@@ -13,7 +13,7 @@ from datetime import datetime
13
13
  from typing import Dict, Any, List, Optional, Callable, TYPE_CHECKING
14
14
 
15
15
  from core.logging import get_logger
16
- from constants import WORKFLOW_TRIGGER_TYPES, POLLING_TRIGGER_TYPES
16
+ from constants import POLLING_TRIGGER_TYPES, TOOLKIT_NODE_TYPES, WORKFLOW_TRIGGER_TYPES
17
17
  from services import event_waiter
18
18
  from .state import DeploymentState, TriggerInfo
19
19
  from .triggers import TriggerManager
@@ -418,17 +418,25 @@ class DeploymentManager:
418
418
  if not trigger_manager:
419
419
  raise RuntimeError(f"No trigger manager for workflow {workflow_id}")
420
420
 
421
- # Polling triggers need active API polling instead of event_waiter
421
+ # Polling triggers need active API polling instead of event_waiter.
422
+ # Plugins register a factory via
423
+ # ``services.plugin.PollingTriggerNode.__init_subclass__`` →
424
+ # ``services.deployment.poll_registry.register_poll_coroutine_factory``.
422
425
  if node_type in POLLING_TRIGGER_TYPES:
423
- poll_coroutine = self._create_poll_coroutine(node_type, node_id, params)
424
- if poll_coroutine:
426
+ from services.deployment.poll_registry import (
427
+ get_poll_coroutine_factory,
428
+ )
429
+
430
+ factory = get_poll_coroutine_factory(node_type)
431
+ if factory is not None:
432
+ poll_coroutine = factory(node_id, params)
425
433
  await trigger_manager.setup_polling_trigger(
426
434
  node_id, node_type, params, poll_coroutine, on_event,
427
435
  self._broadcaster, workflow_id=workflow_id
428
436
  )
429
437
  return TriggerInfo(node_id, node_type)
430
- # Fall through to event_waiter if no polling implementation available
431
- logger.warning("No polling implementation for trigger", node_type=node_type)
438
+ # Fall through to event_waiter if no polling factory registered
439
+ logger.warning("No polling factory registered for trigger", node_type=node_type)
432
440
 
433
441
  await trigger_manager.setup_event_trigger(
434
442
  node_id, node_type, params, on_event, self._broadcaster,
@@ -641,10 +649,12 @@ class DeploymentManager:
641
649
  continue
642
650
  downstream_ids.add(source)
643
651
 
644
- # Include sub-nodes connected to toolkit nodes (n8n Sub-Node pattern)
645
- # Android service nodes connect to androidTool's input-main, not config handles
646
- # These need to be included so the toolkit can discover its connected services
647
- toolkit_node_ids = {n['id'] for n in nodes if n.get('type') == 'androidTool' and n['id'] in downstream_ids}
652
+ # Include sub-nodes connected to toolkit nodes (n8n Sub-Node pattern).
653
+ # Service nodes connect to a toolkit's input-main (not a config
654
+ # handle) and need to be included so the toolkit can discover
655
+ # them. ``TOOLKIT_NODE_TYPES`` is the canonical set; today only
656
+ # ``androidTool`` is in it.
657
+ toolkit_node_ids = {n['id'] for n in nodes if n.get('type') in TOOLKIT_NODE_TYPES and n['id'] in downstream_ids}
648
658
  for edge in edges:
649
659
  target = edge.get('target')
650
660
  source = edge.get('source')
@@ -677,141 +687,12 @@ class DeploymentManager:
677
687
  # POLLING TRIGGER FACTORIES
678
688
  # =========================================================================
679
689
 
680
- def _create_poll_coroutine(self, node_type: str, node_id: str,
681
- params: Dict[str, Any]) -> Optional[Callable]:
682
- """Create a polling coroutine for polling-based triggers.
683
-
684
- Returns an async function(queue, is_running_fn) or None if not supported.
685
- """
686
- if node_type == 'gmailReceive':
687
- return self._create_gmail_poll_coroutine(node_id, params)
688
- elif node_type == 'emailReceive':
689
- return self._create_email_poll_coroutine(node_id, params)
690
- elif node_type == 'twitterReceive':
691
- logger.warning("Twitter polling trigger not yet implemented for deployment")
692
- return None
693
- return None
694
-
695
- def _create_gmail_poll_coroutine(self, node_id: str,
696
- params: Dict[str, Any]) -> Callable:
697
- """Create Gmail API polling coroutine for deployment mode.
698
-
699
- Polls Gmail API at configured intervals for new emails matching the filter.
700
- Establishes a baseline of existing emails to avoid triggering on old messages.
701
- """
702
- async def poll(queue: asyncio.Queue, is_running_fn: Callable):
703
- from nodes.google._base import build_google_service
704
- from nodes.google._gmail import (
705
- poll_gmail_ids as _poll_gmail_ids,
706
- fetch_email_details as _fetch_email_details,
707
- mark_email_as_read as _mark_email_as_read,
708
- )
709
-
710
- # Authenticate with Gmail API
711
- service = await build_google_service("gmail", "v1", params, {})
712
-
713
- poll_interval = max(10, min(3600, params.get('poll_interval', 60)))
714
- filter_query = params.get('filter_query', 'is:unread')
715
- label_filter = params.get('label_filter', 'INBOX')
716
- mark_as_read = params.get('mark_as_read', False)
717
-
718
- # Build Gmail query
719
- query = filter_query
720
- if label_filter and label_filter != 'all':
721
- query = f"label:{label_filter} {query}"
722
-
723
- logger.info("Gmail poller starting",
724
- node_id=node_id, query=query,
725
- poll_interval=poll_interval)
726
-
727
- # Establish baseline (avoid triggering on existing emails)
728
- seen_ids: set = set()
729
- try:
730
- baseline = await _poll_gmail_ids(service, query)
731
- seen_ids.update(baseline)
732
- logger.info("Gmail poller baseline established",
733
- node_id=node_id, count=len(seen_ids))
734
- except Exception as e:
735
- logger.warning("Gmail poller baseline failed",
736
- node_id=node_id, error=str(e))
737
-
738
- # Poll loop
739
- poll_count = 0
740
- while is_running_fn():
741
- await asyncio.sleep(poll_interval)
742
- if not is_running_fn():
743
- break
744
-
745
- poll_count += 1
746
- try:
747
- current_ids = await _poll_gmail_ids(service, query)
748
- new_ids = current_ids - seen_ids
749
- logger.debug("Gmail poll cycle",
750
- node_id=node_id, cycle=poll_count,
751
- current=len(current_ids),
752
- seen=len(seen_ids),
753
- new=len(new_ids))
754
-
755
- for msg_id in new_ids:
756
- seen_ids.add(msg_id)
757
- email_data = await _fetch_email_details(service, msg_id)
758
-
759
- if mark_as_read:
760
- try:
761
- await _mark_email_as_read(service, msg_id)
762
- except Exception:
763
- pass
764
-
765
- await queue.put(email_data)
766
- logger.info("Gmail poller: new email queued",
767
- node_id=node_id,
768
- subject=email_data.get('subject', ''))
769
-
770
- except asyncio.CancelledError:
771
- break
772
- except Exception as e:
773
- logger.error("Gmail poll error",
774
- node_id=node_id, error=str(e))
775
-
776
- return poll
777
-
778
- def _create_email_poll_coroutine(self, node_id: str,
779
- params: Dict[str, Any]) -> Callable:
780
- """Create Himalaya email polling coroutine for deployment mode."""
781
- async def poll(queue: asyncio.Queue, is_running_fn: Callable):
782
- from services.email_service import get_email_service
783
-
784
- svc = get_email_service()
785
- creds = await svc.resolve_credentials(params)
786
- cfg = svc.resolve_poll_params(params)
787
-
788
- seen = await svc.poll_ids(creds, cfg["folder"])
789
- logger.info("Email poller starting",
790
- node_id=node_id, folder=cfg["folder"], seen=len(seen))
791
-
792
- while is_running_fn():
793
- await asyncio.sleep(cfg["interval"])
794
- if not is_running_fn():
795
- break
796
- try:
797
- for msg_id in await svc.poll_ids(creds, cfg["folder"]) - seen:
798
- seen.add(msg_id)
799
- email_data = await svc.fetch_detail(creds, msg_id, cfg["folder"])
800
- if cfg["mark_as_read"]:
801
- try:
802
- d = svc.defaults
803
- await svc.himalaya.flag_message(
804
- creds, msg_id, d.get("flag"), d.get("flag_action"), cfg["folder"])
805
- except Exception:
806
- pass
807
- await queue.put(email_data)
808
- except asyncio.CancelledError:
809
- break
810
- except Exception as e:
811
- logger.error("Email poll error",
812
- node_id=node_id, error=str(e))
813
-
814
- return poll
690
+ # _create_poll_coroutine + _create_gmail_poll_coroutine +
691
+ # _create_email_poll_coroutine REMOVED in Wave 11.I, milestone L.
692
+ # Polling-coroutine factories now self-register from each plugin's
693
+ # PollingTriggerNode subclass (services.plugin.PollingTriggerNode)
694
+ # via services.deployment.poll_registry.register_poll_coroutine_factory.
695
+ # The dispatch path lives ~140 lines up in _setup_event_trigger.
815
696
 
816
697
  async def _load_settings(self):
817
698
  """Load deployment settings from database."""
@@ -0,0 +1,59 @@
1
+ """Polling-coroutine factory registry (Wave 11.I, milestone L).
2
+
3
+ Plugin packages register a factory that produces an async polling
4
+ coroutine for a given trigger node type. ``DeploymentManager`` looks
5
+ up the factory at deploy time and hands the produced coroutine to
6
+ ``TriggerManager.setup_polling_trigger`` -- the deployment manager no
7
+ longer hardcodes per-plugin polling switches.
8
+
9
+ Mirror of :func:`services.event_waiter.register_filter_builder`:
10
+ same idempotency contract, same audience (plugin ``__init__.py``
11
+ modules), built on the shared :class:`IdempotentRegistry`.
12
+
13
+ Factory signature
14
+ -----------------
15
+
16
+ factory(node_id: str, params: Dict[str, Any]) -> async (
17
+ queue: asyncio.Queue, is_running_fn: Callable[[], bool]
18
+ ) -> None
19
+
20
+ The factory closes over node-specific config (auth, query, poll
21
+ interval); the returned coroutine drains until ``is_running_fn()``
22
+ returns False or the task is cancelled. New events go on ``queue``.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import asyncio
28
+ from typing import Any, Awaitable, Callable, Dict, Optional
29
+
30
+ from services.plugin.registry import IdempotentRegistry
31
+
32
+
33
+ # (queue, is_running) -> awaitable. The poll coroutine itself.
34
+ PollCoroutine = Callable[[asyncio.Queue, Callable[[], bool]], Awaitable[None]]
35
+
36
+ # (node_id, params) -> the bound poll coroutine.
37
+ PollCoroutineFactory = Callable[[str, Dict[str, Any]], PollCoroutine]
38
+
39
+
40
+ _REGISTRY: IdempotentRegistry[str, PollCoroutineFactory] = IdempotentRegistry(
41
+ "poll_coroutine_factory"
42
+ )
43
+
44
+
45
+ def register_poll_coroutine_factory(
46
+ node_type: str, factory: PollCoroutineFactory
47
+ ) -> None:
48
+ """Publish a polling-coroutine factory for a trigger node type.
49
+
50
+ Idempotent on re-import (same callable for the same key is a
51
+ no-op). A different callable for an existing key raises
52
+ ``ValueError`` to surface plugin namespace collisions early.
53
+ """
54
+ _REGISTRY.register(node_type, factory)
55
+
56
+
57
+ def get_poll_coroutine_factory(node_type: str) -> Optional[PollCoroutineFactory]:
58
+ """Return the factory for ``node_type``, or ``None`` if unregistered."""
59
+ return _REGISTRY.get(node_type)
@@ -85,16 +85,12 @@ class TriggerConfig:
85
85
  # Registry of supported trigger types (event-based triggers only)
86
86
  # Note: cronScheduler is NOT an event-based trigger - it uses APScheduler directly
87
87
  TRIGGER_REGISTRY: Dict[str, TriggerConfig] = {
88
+ # Framework-level triggers — not owned by any plugin domain.
88
89
  'start': TriggerConfig(
89
90
  node_type='start',
90
91
  event_type='deploy_triggered',
91
92
  display_name='Deploy Start'
92
93
  ),
93
- 'whatsappReceive': TriggerConfig(
94
- node_type='whatsappReceive',
95
- event_type='whatsapp_message_received',
96
- display_name='WhatsApp Message'
97
- ),
98
94
  'webhookTrigger': TriggerConfig(
99
95
  node_type='webhookTrigger',
100
96
  event_type='webhook_received',
@@ -110,24 +106,14 @@ TRIGGER_REGISTRY: Dict[str, TriggerConfig] = {
110
106
  event_type='task_completed',
111
107
  display_name='Task Completed'
112
108
  ),
113
- 'twitterReceive': TriggerConfig(
114
- node_type='twitterReceive',
115
- event_type='twitter_event_received',
116
- display_name='Twitter Event'
117
- ),
118
- 'gmailReceive': TriggerConfig(
119
- node_type='gmailReceive',
120
- event_type='gmail_email_received',
121
- display_name='Gmail Email'
122
- ),
123
- # 'telegramReceive' moved to nodes/telegram/ — backfilled here from
124
- # the plugin's ``event_type`` class attribute via
125
- # ``_auto_populate_from_plugins`` on first access.
126
- 'emailReceive': TriggerConfig(
127
- node_type='emailReceive',
128
- event_type='email_received',
129
- display_name='Email'
130
- ),
109
+ # Plugin-owned trigger entries (whatsappReceive, twitterReceive,
110
+ # telegramReceive, emailReceive, googleGmailReceive) live in their
111
+ # plugin folders' ``_filters.py`` and are backfilled here from each
112
+ # plugin's ``event_type`` ClassVar via
113
+ # ``_auto_populate_from_plugins`` (Wave 11.I, milestone K). Gmail's
114
+ # explicit alias entry was retired in milestone P after the
115
+ # downstream callers (POLLING_TRIGGER_TYPES, deployment manager,
116
+ # frontend trigger list) were renamed to the canonical class type.
131
117
  # Future triggers - just add to registry:
132
118
  # 'mqttTrigger': TriggerConfig('mqttTrigger', 'mqtt_message', 'MQTT Message'),
133
119
  }
@@ -213,126 +199,6 @@ def get_trigger_config(node_type: str) -> Optional[TriggerConfig]:
213
199
  # FILTER BUILDERS - One per trigger type
214
200
  # =============================================================================
215
201
 
216
- def build_whatsapp_filter(params: Dict) -> Callable[[Dict], bool]:
217
- """Build filter function for WhatsApp messages.
218
-
219
- Based on Go RPC handleIncomingMessage() event fields (service.go):
220
- - message_id: string - unique message ID
221
- - sender: string - Sender JID (may be LID for groups)
222
- - sender_phone: string - RESOLVED phone number (LID already resolved by Go RPC!)
223
- - chat_id: string - Chat JID (same as sender for DMs, group JID for groups)
224
- - timestamp: time - message timestamp
225
- - is_from_me: boolean - true if sent by connected account
226
- - is_group: boolean - true if message is in a group chat
227
- - message_type: string - text, image, video, audio, document, sticker, location, contact, contacts
228
- - text: string - text content (for text messages)
229
- - is_forwarded: boolean - true if message is forwarded
230
- - forwarding_score: int - forwarding count
231
- - group_info: object - present for group messages:
232
- - group_jid: string
233
- - sender_jid: string
234
- - sender_phone: string - RESOLVED phone number
235
- - sender_name: string - push name if available
236
-
237
- Note: The Go RPC already resolves LIDs to phone numbers before sending the event.
238
- The sender_phone field contains the resolved phone number - no manual LID resolution needed!
239
- """
240
- # Snake_case throughout — matches plugin Params (no camelCase aliases).
241
- msg_type = params.get('message_type_filter', 'all')
242
- sender_filter = params.get('filter', 'all')
243
- contact_phone = params.get('phone_number', '')
244
- group_id = params.get('group_id', '')
245
- sender_number = params.get('sender_number', '')
246
- keywords = [k.strip().lower() for k in params.get('keywords', '').split(',') if k.strip()]
247
- ignore_own = params.get('ignore_own_messages', True)
248
- forwarded_filter = params.get('forwarded_filter', 'all')
249
-
250
- logger.debug(f"[WhatsAppFilter] Built: type={msg_type}, filter={sender_filter}, group_id='{group_id}', forwarded={forwarded_filter}")
251
-
252
- def matches(m: Dict) -> bool:
253
- msg_chat_id = m.get('chat_id', '')
254
- is_group = m.get('is_group', False)
255
- group_info = m.get('group_info', {})
256
-
257
- # Use sender_phone directly - Go RPC already resolves LIDs to phone numbers!
258
- # For group messages, prefer group_info.sender_phone, fall back to root sender_phone
259
- if is_group:
260
- sender_phone = group_info.get('sender_phone', '') or m.get('sender_phone', '')
261
- else:
262
- sender_phone = m.get('sender_phone', '')
263
-
264
- # Fallback: extract phone from sender JID if sender_phone not available
265
- if not sender_phone:
266
- sender = m.get('sender', '')
267
- sender_phone = sender.split('@')[0] if '@' in sender else sender
268
-
269
- # Message type filter (schema field: message_type)
270
- if msg_type != 'all' and m.get('message_type') != msg_type:
271
- return False
272
-
273
- # Sender filter - for contact filter, use actual phone number
274
- if sender_filter == 'self':
275
- # Only accept messages in self-chat (notes to self)
276
- # Must be from me AND in a chat with myself (not replies to others)
277
- if not m.get('is_from_me'):
278
- return False
279
- # Check chat_id matches sender (self-chat)
280
- chat_id = m.get('chat_id', '')
281
- sender = m.get('sender', '')
282
- if chat_id != sender:
283
- return False
284
-
285
- if sender_filter == 'any_contact':
286
- # Only accept non-group messages (individual/contact messages)
287
- if is_group:
288
- return False
289
-
290
- if sender_filter == 'contact':
291
- if contact_phone not in sender_phone:
292
- return False
293
-
294
- if sender_filter == 'group':
295
- # For group filter, check if message is from that group
296
- if not is_group:
297
- return False
298
- if msg_chat_id != group_id:
299
- return False
300
- # Optional: filter by specific sender within group using resolved phone number
301
- if sender_number:
302
- if sender_number not in sender_phone:
303
- return False
304
-
305
- if sender_filter == 'channel':
306
- # Only accept messages from newsletter channels (chat_id ends with @newsletter)
307
- if not msg_chat_id.endswith('@newsletter'):
308
- return False
309
- # If specific channel JID provided, match exactly
310
- channel_jid = params.get('channel_jid', '')
311
- if channel_jid and msg_chat_id != channel_jid:
312
- return False
313
-
314
- if sender_filter == 'keywords':
315
- text = (m.get('text') or '').lower()
316
- if not any(kw in text for kw in keywords):
317
- return False
318
-
319
- # Ignore own messages (schema field: is_from_me) - but not when filtering for 'self'
320
- if ignore_own and sender_filter != 'self' and m.get('is_from_me'):
321
- return False
322
-
323
- # Forwarded message filter (schema field: is_forwarded)
324
- is_forwarded = m.get('is_forwarded', False)
325
- if forwarded_filter == 'only_forwarded' and not is_forwarded:
326
- return False
327
- if forwarded_filter == 'ignore_forwarded' and is_forwarded:
328
- return False
329
-
330
- logger.debug(f"[WhatsAppFilter] Matched message from {sender_phone}")
331
- return True
332
-
333
- return matches
334
-
335
-
336
202
  def build_webhook_filter(params: Dict) -> Callable[[Dict], bool]:
337
203
  """Build filter function for webhook requests.
338
204
 
@@ -425,96 +291,29 @@ def build_task_completed_filter(params: Dict) -> Callable[[Dict], bool]:
425
291
  return matches
426
292
 
427
293
 
428
- def build_twitter_filter(params: Dict) -> Callable[[Dict], bool]:
429
- """Build filter function for Twitter events.
430
-
431
- Filters by:
432
- - trigger_type: 'mentions', 'search', 'user_timeline'
433
- - search_query: Search query for 'search' trigger type
434
- - user_id: User ID for 'user_timeline' trigger type
435
-
436
- Args:
437
- params: Node parameters
438
-
439
- Returns:
440
- Filter function that checks if event matches criteria
441
- """
442
- trigger_type = params.get('trigger_type', 'mentions')
443
- search_query = params.get('search_query', '')
444
- user_id = params.get('user_id', '')
445
-
446
- def matches(data: Dict) -> bool:
447
- event_type = data.get('trigger_type', '')
448
- if trigger_type != 'all' and event_type != trigger_type:
449
- return False
450
- if trigger_type == 'search' and search_query:
451
- # Check if search query matches
452
- event_query = data.get('query', '')
453
- if search_query.lower() not in event_query.lower():
454
- return False
455
- if trigger_type == 'user_timeline' and user_id:
456
- if data.get('user_id') != user_id:
457
- return False
458
- return True
459
-
460
- return matches
461
-
462
-
463
- def build_gmail_filter(params: Dict) -> Callable[[Dict], bool]:
464
- """Build filter function for Gmail email events.
465
-
466
- Filters by label to ensure the event matches the trigger's label filter.
467
- The filter_query is applied at the Gmail API level during polling,
468
- so this filter only checks labels for events dispatched via event_waiter.
469
-
470
- Args:
471
- params: Node parameters with 'label_filter' field
472
-
473
- Returns:
474
- Filter function that checks if event labels match
475
- """
476
- label_filter = params.get('label_filter', 'INBOX')
477
-
478
- def matches(data: Dict) -> bool:
479
- if label_filter and label_filter != 'all':
480
- labels = data.get('labels', [])
481
- if label_filter not in labels:
482
- return False
483
- return True
484
-
485
- return matches
486
-
487
-
488
- def build_email_filter(params: Dict) -> Callable[[Dict], bool]:
489
- """Build filter for email events (Himalaya IMAP polling)."""
490
- folder_filter = params.get('folder', 'INBOX')
491
- filter_query = params.get('filter_query', '')
492
-
493
- def matches(data: Dict) -> bool:
494
- if folder_filter and folder_filter != 'all':
495
- if data.get('folder', '') != folder_filter:
496
- return False
497
- return True
498
-
499
- return matches
500
-
501
-
502
294
  # Registry of filter builders per trigger type. Plugin packages
503
295
  # (nodes/<group>/) call :func:`register_filter_builder` from their
504
296
  # package ``__init__.py`` to publish per-trigger filters without this
505
- # module needing to import them. The hardcoded entries below are core
506
- # triggers that don't yet live in their own plugin folder; once they
507
- # do, this dict shrinks to ``{}`` and everything is registry-driven.
297
+ # module needing to import them. The hardcoded entries below are
298
+ # **framework-level** triggers (webhook + chat + delegated-task) --
299
+ # they don't belong to any one plugin domain and intentionally stay
300
+ # here. The plugin entries (whatsappReceive, twitterReceive,
301
+ # googleGmailReceive, emailReceive) live in their plugin folders'
302
+ # ``_filters.py`` and self-register at import time.
508
303
  FILTER_BUILDERS: Dict[str, Callable[[Dict], Callable[[Dict], bool]]] = {
509
- 'whatsappReceive': build_whatsapp_filter,
510
304
  'webhookTrigger': build_webhook_filter,
511
305
  'chatTrigger': build_chat_filter,
512
306
  'taskTrigger': build_task_completed_filter,
513
- 'twitterReceive': build_twitter_filter,
514
- 'gmailReceive': build_gmail_filter,
515
- 'emailReceive': build_email_filter,
516
307
  }
517
308
 
309
+ from services.plugin.registry import IdempotentRegistry as _IdempotentRegistry # noqa: E402
310
+
311
+ # Backed by the module-level FILTER_BUILDERS dict so existing readers
312
+ # (e.g. build_filter, _ensure_populated, tests) keep working.
313
+ _FILTER_REGISTRY: _IdempotentRegistry[str, Callable[[Dict], Callable[[Dict], bool]]] = (
314
+ _IdempotentRegistry("filter_builder", items=FILTER_BUILDERS)
315
+ )
316
+
518
317
 
519
318
  def register_filter_builder(
520
319
  node_type: str,
@@ -526,13 +325,7 @@ def register_filter_builder(
526
325
  Used by plugin packages to keep all per-node-type knowledge inside
527
326
  the plugin folder instead of hardcoding it here.
528
327
  """
529
- existing = FILTER_BUILDERS.get(node_type)
530
- if existing is not None and existing is not builder:
531
- raise ValueError(
532
- f"Filter builder for '{node_type}' is already registered by "
533
- f"{existing.__module__}.{existing.__qualname__}"
534
- )
535
- FILTER_BUILDERS[node_type] = builder
328
+ _FILTER_REGISTRY.register(node_type, builder)
536
329
 
537
330
 
538
331
  def build_filter(node_type: str, params: Dict) -> Callable[[Dict], bool]:
@@ -568,6 +361,9 @@ import inspect as _inspect
568
361
 
569
362
  _TriggerPrecheck = Callable[[Dict[str, Any]], Any]
570
363
  _TRIGGER_PRECHECKS: Dict[str, _TriggerPrecheck] = {}
364
+ _TRIGGER_PRECHECK_REGISTRY: _IdempotentRegistry[str, _TriggerPrecheck] = (
365
+ _IdempotentRegistry("trigger_precheck", items=_TRIGGER_PRECHECKS)
366
+ )
571
367
 
572
368
 
573
369
  def register_trigger_precheck(node_type: str, fn: _TriggerPrecheck) -> None:
@@ -576,13 +372,7 @@ def register_trigger_precheck(node_type: str, fn: _TriggerPrecheck) -> None:
576
372
  Idempotent on re-import. The callback may be sync or async; ``run_trigger_precheck``
577
373
  awaits the coroutine when needed.
578
374
  """
579
- existing = _TRIGGER_PRECHECKS.get(node_type)
580
- if existing is not None and existing is not fn:
581
- raise ValueError(
582
- f"Trigger precheck for '{node_type}' is already registered by "
583
- f"{existing.__module__}.{existing.__qualname__}"
584
- )
585
- _TRIGGER_PRECHECKS[node_type] = fn
375
+ _TRIGGER_PRECHECK_REGISTRY.register(node_type, fn)
586
376
 
587
377
 
588
378
  async def run_trigger_precheck(node_type: str, parameters: Dict) -> Any:
@@ -826,43 +616,83 @@ def _cleanup_waiter(waiter_id: str) -> None:
826
616
  # EVENT DISPATCH
827
617
  # =============================================================================
828
618
 
829
- async def dispatch_async(event_type: str, data: Dict) -> int:
619
+ def _unpack_event(
620
+ event: "Any",
621
+ data: Optional[Dict] = None,
622
+ ) -> tuple[str, Dict]:
623
+ """Normalise either ``(WorkflowEvent,)`` or ``(event_type, data)`` to
624
+ the underlying ``(event_type, data)`` pair the dispatcher uses.
625
+
626
+ Wave 11.I, milestone Q: ``dispatch`` and ``dispatch_async`` accept a
627
+ ``WorkflowEvent`` directly so the ``WorkflowEvent(**event)`` rewrap
628
+ in ``events/triggers.py`` becomes a no-op. The legacy
629
+ ``(event_type, data)`` shape stays supported via
630
+ :meth:`WorkflowEvent.from_legacy` upstream of the dispatcher (the
631
+ public API just forwards either form).
632
+ """
633
+ # Lazy import: services.events imports event_waiter for trigger
634
+ # adaptation, so a top-level import would be circular.
635
+ from services.events.envelope import WorkflowEvent
636
+
637
+ if isinstance(event, WorkflowEvent):
638
+ return event.type, event.data if isinstance(event.data, dict) else {"data": event.data}
639
+ if isinstance(event, str):
640
+ return event, data or {}
641
+ raise TypeError(
642
+ f"dispatch expects a WorkflowEvent or (event_type: str, data: Dict); "
643
+ f"got {type(event).__name__}"
644
+ )
645
+
646
+
647
+ async def dispatch_async(
648
+ event: "Any",
649
+ data: Optional[Dict] = None,
650
+ ) -> int:
830
651
  """Dispatch event asynchronously (for Redis mode).
831
652
 
832
653
  Args:
833
- event_type: Type of event (e.g., 'whatsapp_message_received')
834
- data: Event data
654
+ event: Either a :class:`WorkflowEvent` (preferred, Wave 11.I) or
655
+ an event-type string. The legacy ``(event_type, data)`` form
656
+ stays supported.
657
+ data: Event payload (when ``event`` is a string).
835
658
 
836
659
  Returns:
837
660
  1 if event was added to stream, 0 otherwise
838
661
  """
662
+ event_type, payload = _unpack_event(event, data)
839
663
  logger.debug(f"[EventWaiter] dispatch_async: event_type='{event_type}'")
840
664
 
841
665
  if is_redis_mode():
842
666
  cache = get_cache_service()
843
667
  stream_name = _get_stream_name(event_type)
844
- msg_id = await cache.stream_add(stream_name, data)
668
+ msg_id = await cache.stream_add(stream_name, payload)
845
669
  if msg_id:
846
670
  logger.debug(f"[EventWaiter] Added event to stream {stream_name}: {msg_id}")
847
671
  return 1
848
672
  return 0
849
673
  else:
850
674
  # Fall back to sync dispatch for memory mode
851
- return dispatch(event_type, data)
675
+ return dispatch(event_type, payload)
852
676
 
853
677
 
854
- def dispatch(event_type: str, data: Dict) -> int:
678
+ def dispatch(
679
+ event: "Any",
680
+ data: Optional[Dict] = None,
681
+ ) -> int:
855
682
  """Dispatch event to matching waiters (synchronous, memory mode).
856
683
 
857
684
  Thread-safe: Can be called from APScheduler threads or async context.
858
685
 
859
686
  Args:
860
- event_type: Type of event (e.g., 'whatsapp_message_received')
861
- data: Event data
687
+ event: Either a :class:`WorkflowEvent` (preferred, Wave 11.I) or
688
+ an event-type string. The legacy ``(event_type, data)`` form
689
+ stays supported.
690
+ data: Event payload (when ``event`` is a string).
862
691
 
863
692
  Returns:
864
693
  Number of waiters resolved
865
694
  """
695
+ event_type, data = _unpack_event(event, data)
866
696
  if is_redis_mode():
867
697
  # In Redis mode, use async dispatch
868
698
  # Handle both async context and thread context (e.g., APScheduler callbacks)