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
@@ -0,0 +1,54 @@
1
+ """Generalized event-source framework (Wave 12).
2
+
3
+ Three concrete EventSource subclasses cover all current MachinaOs
4
+ trigger integrations: ``PushEventSource`` (HTTP/RPC pushes),
5
+ ``PollingEventSource`` (interval-based pull), ``DaemonEventSource``
6
+ (long-lived subprocess driver). Webhook flow is a thin specialisation
7
+ of PushEventSource via :class:`WebhookSource` + the path registry in
8
+ ``server/routers/webhook.py``.
9
+
10
+ The unified payload type :class:`WorkflowEvent` mirrors CloudEvents
11
+ v1.0 verbatim — see ``envelope.py``. A back-compat shim in
12
+ ``services.event_waiter`` auto-wraps legacy ``Dict`` dispatches so
13
+ existing plugins keep working untouched until they migrate.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from .cli import run_cli_command
19
+ from .envelope import WorkflowEvent
20
+ from .lifecycle import make_lifecycle_handlers, make_status_refresh
21
+ from .source import EventSource
22
+ from .push import PushEventSource
23
+ from .polling import PollingEventSource
24
+ from .daemon import DaemonEventSource
25
+ from .triggers import BaseTriggerParams, WebhookTriggerNode
26
+ from .webhook import WebhookSource, WEBHOOK_SOURCES, register_webhook_source
27
+ from .verifiers import (
28
+ WebhookVerifier,
29
+ HmacVerifier,
30
+ StripeVerifier,
31
+ StandardWebhooksVerifier,
32
+ GitHubVerifier,
33
+ )
34
+
35
+ __all__ = [
36
+ "BaseTriggerParams",
37
+ "DaemonEventSource",
38
+ "EventSource",
39
+ "GitHubVerifier",
40
+ "HmacVerifier",
41
+ "PollingEventSource",
42
+ "PushEventSource",
43
+ "StandardWebhooksVerifier",
44
+ "StripeVerifier",
45
+ "WEBHOOK_SOURCES",
46
+ "WebhookSource",
47
+ "WebhookTriggerNode",
48
+ "WebhookVerifier",
49
+ "WorkflowEvent",
50
+ "make_lifecycle_handlers",
51
+ "make_status_refresh",
52
+ "register_webhook_source",
53
+ "run_cli_command",
54
+ ]
@@ -0,0 +1,78 @@
1
+ """Generic CLI invocation helper.
2
+
3
+ ``run_cli_command`` resolves the binary on PATH, injects the
4
+ plugin's API key via the convention flag (``--api-key`` by default),
5
+ runs the subprocess with a timeout, captures stdout/stderr, and parses
6
+ stdout as JSON when possible. Used by any plugin that wraps a CLI
7
+ tool (Stripe, future GitHub-CLI / Cloudflare-Wrangler / etc.).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+ import shutil
15
+ from typing import Any, Dict, List, Optional, Type
16
+
17
+
18
+ async def run_cli_command(
19
+ *,
20
+ binary: str,
21
+ argv: List[str],
22
+ credential: Optional[Type] = None,
23
+ api_key_arg: str = "--api-key",
24
+ timeout: float = 30.0,
25
+ env: Optional[Dict[str, str]] = None,
26
+ ) -> Dict[str, Any]:
27
+ """Run ``<binary> <argv> [api_key_arg <key>]`` once, return parsed JSON.
28
+
29
+ ``env``: optional process environment override. When None, the child
30
+ inherits the parent's environment (asyncio default).
31
+
32
+ Returns a uniform envelope:
33
+ {"success": bool, "result": parsed-JSON-or-None,
34
+ "stdout": str, "stderr": str, "error": str or None}
35
+ """
36
+ api_key: Optional[str] = None
37
+ if credential is not None:
38
+ try:
39
+ secrets = await credential.resolve()
40
+ except PermissionError:
41
+ return {"success": False, "error": f"{binary}: credential required"}
42
+ api_key = secrets.get("api_key")
43
+ if not api_key:
44
+ return {"success": False, "error": f"{binary}: credential required"}
45
+
46
+ resolved = shutil.which(binary)
47
+ if resolved is None:
48
+ return {"success": False, "error": f"{binary!r} not on PATH"}
49
+
50
+ full_argv = [resolved, *argv]
51
+ if api_key:
52
+ full_argv.extend([api_key_arg, api_key])
53
+
54
+ try:
55
+ proc = await asyncio.create_subprocess_exec(
56
+ *full_argv,
57
+ stdout=asyncio.subprocess.PIPE,
58
+ stderr=asyncio.subprocess.PIPE,
59
+ env=env,
60
+ )
61
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
62
+ except asyncio.TimeoutError:
63
+ return {"success": False, "error": f"{binary} timed out ({timeout}s)"}
64
+
65
+ out = stdout.decode(errors="replace").strip()
66
+ err = stderr.decode(errors="replace").strip()
67
+ if proc.returncode != 0:
68
+ return {
69
+ "success": False,
70
+ "stdout": out,
71
+ "stderr": err,
72
+ "error": err or f"exit {proc.returncode}",
73
+ }
74
+ try:
75
+ parsed = json.loads(out) if out else None
76
+ except json.JSONDecodeError:
77
+ parsed = None
78
+ return {"success": True, "result": parsed, "stdout": out, "stderr": err, "error": None}
@@ -0,0 +1,163 @@
1
+ """DaemonEventSource — long-lived subprocess (or SDK loop) that emits
2
+ events into the framework.
3
+
4
+ Used by stripe-listen, future telegram/whatsapp daemon migrations, and
5
+ any plugin that wraps a CLI tool. The class delegates lifecycle to
6
+ :class:`services.process_service.ProcessService` (already battle-tested:
7
+ PATHEXT-aware, ``kill_tree`` cleanup, log capture, terminal broadcast)
8
+ and subscribes to its standard per-line callback hook for typed event
9
+ emission. We do NOT re-tail the on-disk log files — that's the same
10
+ data ProcessService already streamed to us via ``line_handler``.
11
+
12
+ Subclasses contribute:
13
+
14
+ - :meth:`build_command(secrets) -> str` — full command string
15
+ - :meth:`parse_line(stream, line) -> Optional[WorkflowEvent]` — turn one
16
+ line into one event (or ``None`` for log-only lines)
17
+ - :attr:`binary_name` (optional) — pre-flight ``shutil.which`` check
18
+ with a clear error if the binary isn't on PATH
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ import shutil
25
+ from pathlib import Path
26
+ from typing import Any, AsyncIterator, ClassVar, Dict, Optional
27
+
28
+ from core.logging import get_logger
29
+ from services.process_service import get_process_service
30
+
31
+ from .envelope import WorkflowEvent
32
+ from .source import EventSource
33
+
34
+ logger = get_logger(__name__)
35
+
36
+
37
+ class DaemonEventSource(EventSource):
38
+ """Long-lived subprocess driver. Subclasses provide:
39
+
40
+ * :attr:`process_name` — unique name for ProcessService key
41
+ * :attr:`binary_name` — the executable to look up via PATH
42
+ * :meth:`build_command(secrets)` — full command string
43
+ * :meth:`parse_line(stream, line)` — turn output lines into events
44
+ (return ``None`` to suppress; e.g. log-only lines)
45
+ * :meth:`install_hint` (optional) — install URL surfaced in the
46
+ "binary not on PATH" error
47
+ """
48
+
49
+ process_name: ClassVar[str] = ""
50
+ binary_name: ClassVar[str] = ""
51
+ workflow_namespace: ClassVar[str] = "_daemon"
52
+ install_hint: ClassVar[str] = ""
53
+
54
+ def __init__(self) -> None:
55
+ super().__init__()
56
+ self._pid: Optional[int] = None
57
+ self._lock = asyncio.Lock()
58
+ self._queue: asyncio.Queue[WorkflowEvent] = asyncio.Queue()
59
+
60
+ @property
61
+ def pid(self) -> Optional[int]:
62
+ return self._pid
63
+
64
+ def build_command(self, secrets: Dict) -> str:
65
+ raise NotImplementedError
66
+
67
+ def parse_line(self, stream: str, line: str) -> Optional[WorkflowEvent]:
68
+ """Override to emit events from output lines. ``stream`` is
69
+ ``"stdout"`` or ``"stderr"``. Returning ``None`` swallows the line."""
70
+ return None
71
+
72
+ def workdir(self) -> Path:
73
+ from core.config import Settings
74
+ cwd = Path(Settings().workspace_base_resolved).resolve() / self.workflow_namespace
75
+ cwd.mkdir(parents=True, exist_ok=True)
76
+ return cwd
77
+
78
+ async def _resolve_secrets(self) -> Dict[str, Any]:
79
+ if self.credential is None:
80
+ return {}
81
+ try:
82
+ return await self.credential.resolve()
83
+ except PermissionError:
84
+ return {}
85
+
86
+ async def has_credential(self) -> bool:
87
+ secrets = await self._resolve_secrets()
88
+ return bool(secrets.get("api_key"))
89
+
90
+ async def status(self) -> Dict[str, Any]:
91
+ return {
92
+ "type": self.type,
93
+ "running": self._started,
94
+ "pid": self._pid,
95
+ }
96
+
97
+ async def start(self) -> Dict[str, Any]:
98
+ async with self._lock:
99
+ if self._started:
100
+ logger.info("[%s] start() noop — already running (pid=%s)", self.type, self._pid)
101
+ return {"success": True, "message": "already running", "status": await self.status()}
102
+
103
+ logger.info("[%s] start() entered: resolving secrets + checking credential gate", self.type)
104
+ secrets = await self._resolve_secrets()
105
+ # Gate via :meth:`has_credential` so subclasses that authenticate
106
+ # without an ``api_key`` field (Stripe — auth lives in the CLI's
107
+ # config file, signalled by ``is_logged_in()``) can override the
108
+ # check. The default ``has_credential`` still tests
109
+ # ``secrets["api_key"]`` so api-key plugins keep working.
110
+ if self.credential is not None and not await self.has_credential():
111
+ logger.warning(
112
+ "[%s] start() blocked: has_credential() returned False — refusing to spawn daemon",
113
+ self.type,
114
+ )
115
+ return {"success": False, "error": f"{self.type}: credential required"}
116
+
117
+ if self.binary_name and shutil.which(self.binary_name) is None:
118
+ hint = f" Install: {self.install_hint}" if self.install_hint else ""
119
+ return {"success": False, "error": f"{self.binary_name!r} not on PATH.{hint}"}
120
+
121
+ cmd = self.build_command(secrets)
122
+ cwd = self.workdir()
123
+ result = await get_process_service().start(
124
+ name=self.process_name,
125
+ command=cmd,
126
+ workflow_id=self.workflow_namespace,
127
+ working_directory=str(cwd),
128
+ line_handler=self._on_line,
129
+ )
130
+ if not result.get("success"):
131
+ return result
132
+
133
+ self._pid = (result.get("result") or {}).get("pid")
134
+ self._started = True
135
+ self._stopped = False
136
+ logger.info("[%s] daemon started pid=%s", self.type, self._pid)
137
+ return {"success": True, "message": f"started (pid {self._pid})", "status": await self.status()}
138
+
139
+ async def stop(self) -> Dict[str, Any]:
140
+ async with self._lock:
141
+ self._stopped = True
142
+ await get_process_service().stop(
143
+ name=self.process_name, workflow_id=self.workflow_namespace,
144
+ )
145
+ self._started = False
146
+ self._pid = None
147
+ return {"success": True, "message": "disconnected"}
148
+
149
+ async def restart(self) -> Dict[str, Any]:
150
+ await self.stop()
151
+ return await self.start()
152
+
153
+ async def _on_line(self, stream: str, line: str) -> None:
154
+ """ProcessService callback: one decoded stdout/stderr line.
155
+ Subclasses' :meth:`parse_line` decides whether to emit an event."""
156
+ event = self.parse_line(stream, line)
157
+ if event is not None:
158
+ await self._queue.put(event)
159
+
160
+ async def emit(self) -> AsyncIterator[WorkflowEvent]:
161
+ while not self._stopped:
162
+ event = await self._queue.get()
163
+ yield event
@@ -0,0 +1,281 @@
1
+ """WorkflowEvent — CloudEvents v1.0 envelope (in-house, no external dep).
2
+
3
+ Field set mirrors https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md
4
+ verbatim so future interop with EventBridge / Knative is a JSON-schema swap.
5
+ The MachinaOs extensions (workflow_id, trigger_node_id, correlation_id)
6
+ ride as CloudEvents extension attributes — fully compliant with the spec.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime, timezone
12
+ from typing import Any, Literal, Mapping, Optional
13
+ from uuid import uuid4
14
+
15
+ from pydantic import BaseModel, ConfigDict, Field
16
+
17
+
18
+ class WorkflowEvent(BaseModel):
19
+ """Unified event envelope used by every EventSource."""
20
+
21
+ specversion: str = "1.0"
22
+ id: str = Field(default_factory=lambda: uuid4().hex)
23
+ source: str
24
+ type: str
25
+ time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
26
+ subject: Optional[str] = None
27
+ datacontenttype: str = "application/json"
28
+ dataschema: Optional[str] = None
29
+ data: Any = None
30
+
31
+ workflow_id: Optional[str] = None
32
+ trigger_node_id: Optional[str] = None
33
+ correlation_id: Optional[str] = None
34
+
35
+ model_config = ConfigDict(extra="allow")
36
+
37
+ @classmethod
38
+ def from_legacy(cls, event_type: str, payload: dict) -> "WorkflowEvent":
39
+ """Wrap a legacy Dict payload from the pre-framework dispatch path.
40
+
41
+ Used by the back-compat shim in event_waiter.dispatch so existing
42
+ plugins (telegram, whatsapp, gmail, etc.) keep working until they
43
+ migrate to native WorkflowEvent emission.
44
+ """
45
+ return cls(
46
+ source=f"legacy://{event_type}",
47
+ type=event_type,
48
+ data=payload,
49
+ )
50
+
51
+ # ---- Typed factory classmethods (Wave 11.I, milestone Q) -----------
52
+ #
53
+ # Mirrors the official `cloudevents` Python SDK's `from_http` /
54
+ # `to_http` convenience pattern. Each factory enforces the same
55
+ # source/type/subject conventions across the codebase so callers
56
+ # don't hand-construct envelopes with drifting URI shapes.
57
+
58
+ @classmethod
59
+ def credential(
60
+ cls,
61
+ provider: str,
62
+ action: Literal[
63
+ "api_key.saved",
64
+ "api_key.deleted",
65
+ "api_key.validated",
66
+ "oauth.connected",
67
+ "oauth.disconnected",
68
+ "oauth.validated",
69
+ ],
70
+ **extra: Any,
71
+ ) -> "WorkflowEvent":
72
+ """Credential mutation event (matches the existing
73
+ ``broadcast_credential_event`` contract locked by
74
+ ``test_credential_broadcasts.py``)."""
75
+ return cls(
76
+ source="machinaos://services/credentials",
77
+ type=f"credential.{action}",
78
+ subject=provider,
79
+ data={"provider": provider, **extra} if extra else {"provider": provider},
80
+ )
81
+
82
+ @classmethod
83
+ def connection_status(
84
+ cls,
85
+ plugin: str,
86
+ *,
87
+ connected: bool,
88
+ subject: Optional[str] = None,
89
+ data: Optional[Mapping[str, Any]] = None,
90
+ ) -> "WorkflowEvent":
91
+ """Plugin connection-state event (whatsapp / telegram / android-relay
92
+ / twitter / google connect-disconnect)."""
93
+ return cls(
94
+ source=f"machinaos://nodes/{plugin}",
95
+ type=f"{plugin}.connection.{'opened' if connected else 'closed'}",
96
+ subject=subject,
97
+ data=dict(data) if data else {},
98
+ )
99
+
100
+ @classmethod
101
+ def oauth_completed(
102
+ cls,
103
+ provider: str,
104
+ *,
105
+ identifier: str,
106
+ data: Optional[Mapping[str, Any]] = None,
107
+ ) -> "WorkflowEvent":
108
+ """OAuth callback completion. ``identifier`` is the user-facing
109
+ handle (email / username) used as ``subject``."""
110
+ return cls(
111
+ source=f"machinaos://nodes/{provider}",
112
+ type=f"{provider}.oauth.completed",
113
+ subject=identifier,
114
+ data=dict(data) if data else {"identifier": identifier},
115
+ )
116
+
117
+ @classmethod
118
+ def message(
119
+ cls,
120
+ plugin: str,
121
+ direction: Literal["sent", "received"],
122
+ data: Mapping[str, Any],
123
+ ) -> "WorkflowEvent":
124
+ """Plugin-emitted message event (whatsapp / telegram / email
125
+ send + receive). ``subject`` defaults to the chat / sender id
126
+ in the payload (cast to str — Telegram uses numeric chat ids),
127
+ falling back to None."""
128
+ payload = dict(data)
129
+ raw_subject = (
130
+ payload.get("chat_id") or payload.get("from_id") or payload.get("sender")
131
+ )
132
+ return cls(
133
+ source=f"machinaos://nodes/{plugin}",
134
+ type=f"{plugin}.message.{direction}",
135
+ subject=str(raw_subject) if raw_subject is not None else None,
136
+ data=payload,
137
+ )
138
+
139
+ @classmethod
140
+ def team_event(
141
+ cls,
142
+ team_id: str,
143
+ kind: str,
144
+ data: Mapping[str, Any],
145
+ ) -> "WorkflowEvent":
146
+ """Agent-team lifecycle / task event (created / dissolved /
147
+ task.added / task.claimed / task.completed / task.failed /
148
+ message.sent)."""
149
+ return cls(
150
+ source="machinaos://services/agent_team",
151
+ type=f"team.{kind}",
152
+ subject=team_id,
153
+ data=dict(data),
154
+ )
155
+
156
+ @classmethod
157
+ def workflow_lifecycle(
158
+ cls,
159
+ stage: Literal[
160
+ "deployment.started",
161
+ "deployment.stopped",
162
+ "lock.acquired",
163
+ "lock.released",
164
+ "execution.started",
165
+ "execution.stopped",
166
+ ],
167
+ *,
168
+ workflow_id: str,
169
+ data: Optional[Mapping[str, Any]] = None,
170
+ ) -> "WorkflowEvent":
171
+ """Workflow lifecycle event. Carries ``workflow_id`` both as
172
+ ``subject`` (CloudEvents convention) and as the
173
+ ``workflow_id`` extension attribute (existing reader
174
+ contract)."""
175
+ return cls(
176
+ source="machinaos://services/workflow",
177
+ type=f"workflow.{stage}",
178
+ subject=workflow_id,
179
+ workflow_id=workflow_id,
180
+ data=dict(data) if data else {},
181
+ )
182
+
183
+ @classmethod
184
+ def agent_progress(
185
+ cls,
186
+ node_id: str,
187
+ *,
188
+ workflow_id: Optional[str],
189
+ iteration: int,
190
+ max_iterations: int,
191
+ phase: Optional[str] = None,
192
+ data: Optional[Mapping[str, Any]] = None,
193
+ ) -> "WorkflowEvent":
194
+ """Live LangGraph supervised-loop progress for one agent node.
195
+
196
+ Emitted from inside the ``astream`` loop in
197
+ ``services/ai.py:execute_agent`` and ``execute_chat_agent`` after
198
+ each super-step. ``iteration`` advances on every ``agent_node``
199
+ invocation; ``max_iterations`` mirrors the LangGraph
200
+ ``recursion_limit`` (sourced from
201
+ ``llm_defaults.json:agent.recursion_limit``).
202
+
203
+ ``subject`` carries the executing node id so the FE routes the
204
+ update straight to ``nodeStatusStore`` for that node. The
205
+ ``workflow_id`` extension attribute scopes per-workflow displays
206
+ the same way ``node_status`` broadcasts do.
207
+ """
208
+ return cls(
209
+ source="machinaos://services/agent",
210
+ type="agent.progress",
211
+ subject=node_id,
212
+ workflow_id=workflow_id,
213
+ data={
214
+ "node_id": node_id,
215
+ "iteration": iteration,
216
+ "max_iterations": max_iterations,
217
+ **({"phase": phase} if phase else {}),
218
+ **(dict(data) if data else {}),
219
+ },
220
+ )
221
+
222
+ @classmethod
223
+ def deployment_snapshot(
224
+ cls,
225
+ running_workflow_ids: list[str],
226
+ ) -> "WorkflowEvent":
227
+ """Push-on-connect snapshot of every currently-running deployment.
228
+
229
+ Emitted by ``broadcaster.broadcast_deployment_snapshot()`` to a
230
+ single WebSocket target right after ``initial_status``. Lets the
231
+ FE reconcile its local ``deploymentStatus`` / ``runningWorkflows``
232
+ cache against the backend's source of truth on every (re)connect,
233
+ instead of carrying stale "isRunning=true" forward through a
234
+ backend restart that wiped the in-memory deployment dict.
235
+
236
+ Distinct from ``workflow_lifecycle("deployment.started")`` —
237
+ that fires when a deployment STARTS (one-shot edge event).
238
+ ``deployment.snapshot`` is an idempotent state dump tied to
239
+ client connect, not to a state transition. Empty list is
240
+ meaningful: "no deployments are running, drop your stale
241
+ local state."
242
+ """
243
+ return cls(
244
+ source="machinaos://services/workflow",
245
+ type="workflow.deployment.snapshot",
246
+ data={"running_workflow_ids": list(running_workflow_ids)},
247
+ )
248
+
249
+ @classmethod
250
+ def task_completed(
251
+ cls,
252
+ task_id: str,
253
+ *,
254
+ status: Literal["completed", "error"],
255
+ agent: str,
256
+ data: Optional[Mapping[str, Any]] = None,
257
+ ) -> "WorkflowEvent":
258
+ """Delegated child-agent completion. Type discriminates on
259
+ succeeded vs failed; ``taskTrigger`` filters by both."""
260
+ return cls(
261
+ source="machinaos://services/agent",
262
+ type=f"agent.task.{'succeeded' if status == 'completed' else 'failed'}",
263
+ subject=task_id,
264
+ data=dict(data) if data else {"task_id": task_id, "agent": agent, "status": status},
265
+ )
266
+
267
+ def matches_type(self, pattern: str) -> bool:
268
+ """Glob-style match on event type. ``"all"``/empty matches any.
269
+
270
+ Examples:
271
+ "stripe.charge.succeeded" matches itself
272
+ "stripe.charge.*" matches "stripe.charge.succeeded"
273
+ "stripe.*" matches "stripe.charge.succeeded"
274
+ "all" or "" matches everything
275
+ """
276
+ if not pattern or pattern == "all":
277
+ return True
278
+ if pattern.endswith(".*"):
279
+ prefix = pattern[:-2]
280
+ return self.type.startswith(prefix + ".") or self.type == prefix
281
+ return self.type == pattern
@@ -0,0 +1,99 @@
1
+ """Generic lifecycle wiring helpers.
2
+
3
+ :func:`make_lifecycle_handlers` returns the standard 4 WebSocket
4
+ handlers (``connect`` / ``disconnect`` / ``reconnect`` / ``status``) for
5
+ any :class:`EventSource`. Plugins call it once and register the dict —
6
+ no per-handler boilerplate.
7
+
8
+ :func:`make_status_refresh` returns a ``register_service_refresh``
9
+ callback that auto-reconnects the source when a credential is stored
10
+ and mirrors its status into the broadcaster cache.
11
+
12
+ Together these collapse ~50 LOC of identical boilerplate per plugin
13
+ into two function calls.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any, Awaitable, Callable, Dict, TYPE_CHECKING
19
+
20
+ from fastapi import WebSocket
21
+
22
+ from core.logging import get_logger
23
+
24
+ from .source import EventSource
25
+
26
+ if TYPE_CHECKING:
27
+ from services.status_broadcaster import StatusBroadcaster
28
+
29
+ logger = get_logger(__name__)
30
+
31
+
32
+ def make_lifecycle_handlers(
33
+ prefix: str,
34
+ source: EventSource,
35
+ *,
36
+ extra: Dict[str, Callable[[Dict[str, Any], WebSocket], Awaitable[Dict[str, Any]]]] | None = None,
37
+ ) -> Dict[str, Callable[[Dict[str, Any], WebSocket], Awaitable[Dict[str, Any]]]]:
38
+ """Return ``{prefix}_connect / _disconnect / _reconnect / _status`` handlers.
39
+
40
+ Pass ``extra`` to merge additional plugin-specific handlers (e.g.
41
+ ``{"stripe_trigger": handle_stripe_trigger}``).
42
+ """
43
+
44
+ async def _connect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
45
+ return await source.start()
46
+
47
+ async def _disconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
48
+ return await source.stop()
49
+
50
+ async def _reconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
51
+ restart = getattr(source, "restart", None)
52
+ if restart is not None:
53
+ return await restart()
54
+ await source.stop()
55
+ return await source.start()
56
+
57
+ async def _status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
58
+ status = await source.status()
59
+ if hasattr(source, "has_credential"):
60
+ status["has_stored_key"] = await source.has_credential() # type: ignore[attr-defined]
61
+ return {"success": True, "status": status}
62
+
63
+ handlers: Dict[str, Callable[[Dict[str, Any], WebSocket], Awaitable[Dict[str, Any]]]] = {
64
+ f"{prefix}_connect": _connect,
65
+ f"{prefix}_disconnect": _disconnect,
66
+ f"{prefix}_reconnect": _reconnect,
67
+ f"{prefix}_status": _status,
68
+ }
69
+ if extra:
70
+ handlers.update(extra)
71
+ return handlers
72
+
73
+
74
+ def make_status_refresh(
75
+ source: EventSource,
76
+ *,
77
+ status_key: str,
78
+ broadcast_type: str,
79
+ ) -> Callable[["StatusBroadcaster"], Awaitable[None]]:
80
+ """Return a ``register_service_refresh`` callback.
81
+
82
+ The callback auto-starts the source if it has stored credentials,
83
+ mirrors its status into ``broadcaster._status[status_key]``, and
84
+ emits a ``broadcast_type`` broadcast.
85
+ """
86
+
87
+ async def refresh(broadcaster: "StatusBroadcaster") -> None:
88
+ try:
89
+ if not getattr(source, "_started", False) and hasattr(source, "has_credential"):
90
+ if await source.has_credential(): # type: ignore[attr-defined]
91
+ logger.info("[StatusBroadcaster] auto-reconnecting %s", status_key)
92
+ await source.start()
93
+ status = await source.status()
94
+ broadcaster._status[status_key] = status
95
+ await broadcaster.broadcast({"type": broadcast_type, "data": status})
96
+ except Exception as e:
97
+ logger.debug("[StatusBroadcaster] %s refresh failed: %s", status_key, e)
98
+
99
+ return refresh