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
@@ -27,7 +27,8 @@ pytestmark = pytest.mark.node_contract
27
27
 
28
28
  def _reset_singletons():
29
29
  """Wipe cached singletons so each test gets a clean EmailService / HimalayaService."""
30
- from services import email_service, himalaya_service
30
+ from nodes.email import _service as email_service
31
+ from nodes.email import _himalaya as himalaya_service
31
32
 
32
33
  email_service.EmailService._instance = None
33
34
  himalaya_service.HimalayaService._instance = None
@@ -47,7 +48,7 @@ def _base_creds_params(**overrides):
47
48
  def _patch_ensure_binary():
48
49
  """Patch HimalayaService.ensure_binary to return a fake path without shutil.which."""
49
50
  return patch(
50
- "services.himalaya_service.HimalayaService.ensure_binary",
51
+ "nodes.email._himalaya.HimalayaService.ensure_binary",
51
52
  new=AsyncMock(return_value="/usr/bin/himalaya"),
52
53
  )
53
54
 
@@ -91,7 +92,7 @@ class TestEmailSend:
91
92
  # Return empty JSON so himalaya "succeeds" with no extra fields
92
93
  with _patch_ensure_binary(), patched_subprocess(stdout=b"{}", returncode=0), \
93
94
  patched_container(auth_api_keys={"email_address": "alice@example.com", "email_password": "sekret", "email_provider": "gmail"}), patched_pricing(), \
94
- patch("services.himalaya_service.HimalayaService.execute",
95
+ patch("nodes.email._himalaya.HimalayaService.execute",
95
96
  new=AsyncMock(return_value={})) as mock_exec:
96
97
  result = await harness.execute(
97
98
  "emailSend",
@@ -149,7 +150,7 @@ class TestEmailRead:
149
150
  ]
150
151
  # Himalaya returns a list for envelope list -> wrapped in {data: ...}
151
152
  with _patch_ensure_binary(), \
152
- patch("services.himalaya_service.HimalayaService.execute",
153
+ patch("nodes.email._himalaya.HimalayaService.execute",
153
154
  new=AsyncMock(return_value=envelopes)), \
154
155
  patched_container(auth_api_keys={"email_address": "alice@example.com", "email_password": "sekret", "email_provider": "gmail"}), patched_pricing():
155
156
  result = await harness.execute(
@@ -166,7 +167,7 @@ class TestEmailRead:
166
167
  async def test_read_merges_dict_output(self, harness):
167
168
  message = {"subject": "hello", "body": "world", "from": "a@x.com"}
168
169
  with _patch_ensure_binary(), \
169
- patch("services.himalaya_service.HimalayaService.execute",
170
+ patch("nodes.email._himalaya.HimalayaService.execute",
170
171
  new=AsyncMock(return_value=message)), \
171
172
  patched_container(auth_api_keys={"email_address": "alice@example.com", "email_password": "sekret", "email_provider": "gmail"}), patched_pricing():
172
173
  result = await harness.execute(
@@ -213,7 +214,7 @@ class TestEmailRead:
213
214
  class TestEmailReceive:
214
215
  async def test_new_email_detected_on_second_poll(self, harness):
215
216
  """Baseline sees {id1}; next poll returns {id1,id2} -> dispatch id2."""
216
- from services.email_service import EmailService
217
+ from nodes.email._service import EmailService
217
218
 
218
219
  email_detail = {"from": "c@x.com", "subject": "new!", "body": "hi"}
219
220
 
@@ -248,8 +249,8 @@ class TestEmailReceive:
248
249
  assert dispatch_mock.call_args.args[0] == "email_received"
249
250
 
250
251
  async def test_mark_as_read_adds_seen_flag(self, harness):
251
- from services.email_service import EmailService
252
- from services.himalaya_service import HimalayaService
252
+ from nodes.email._service import EmailService
253
+ from nodes.email._himalaya import HimalayaService
253
254
 
254
255
  poll_ids_mock = AsyncMock(side_effect=[set(), {"42"}])
255
256
  fetch_detail_mock = AsyncMock(return_value={"message_id": "42", "folder": "INBOX"})
@@ -291,7 +292,7 @@ class TestEmailReceive:
291
292
 
292
293
  async def test_subprocess_error_surfaces_as_envelope(self, harness):
293
294
  """If poll_ids raises (e.g. Himalaya subprocess fails) the handler returns an error envelope."""
294
- from services.email_service import EmailService
295
+ from nodes.email._service import EmailService
295
296
 
296
297
  poll_ids_mock = AsyncMock(side_effect=RuntimeError("himalaya error: connection refused"))
297
298
 
@@ -42,10 +42,10 @@ def _patch_creds(module_name: str, creds_return=None, side_effect=None):
42
42
  By default returns a MagicMock that stands in for a Credentials instance.
43
43
  Pass `side_effect=ValueError(...)` to simulate missing credentials.
44
44
  """
45
- # Scaling-branch: shared helper lives at services.handlers.google_auth.
45
+ # Scaling-branch: shared helper lives at nodes.google._auth_helper.
46
46
  # `module_name` is retained for API symmetry but not part of the patch path.
47
47
  _ = module_name
48
- target = "services.handlers.google_auth.get_google_credentials"
48
+ target = "nodes.google._auth_helper.get_google_credentials"
49
49
  kwargs = {}
50
50
  if side_effect is not None:
51
51
  kwargs["side_effect"] = side_effect
@@ -463,73 +463,120 @@ class TestRLMAgent:
463
463
 
464
464
 
465
465
  class TestClaudeCodeAgent:
466
- def _wire_claude_service(self, result_payload=None):
467
- service = MagicMock(name="ClaudeCodeService")
468
- service.execute = AsyncMock(
469
- return_value=result_payload
470
- or {
471
- "result": "cli response",
472
- "session_id": "sess-abc",
473
- "usage": {"input_tokens": 10, "output_tokens": 5},
474
- }
466
+ """The claude_code_agent plugin now goes through `AICliService.run_batch`
467
+ (multi-task batch). Single-prompt input gets adapted to a one-task batch
468
+ for back-compat. These tests mock the new service path."""
469
+
470
+ def _wire_cli_service(
471
+ self,
472
+ *,
473
+ response: str = "cli response",
474
+ session_id: str = "sess-abc",
475
+ cost_usd: float = 0.012,
476
+ success: bool = True,
477
+ error: str | None = None,
478
+ ):
479
+ """Mock `AICliService` with a `run_batch` that returns a one-task BatchResult."""
480
+ from services.cli_agent.protocol import (
481
+ BatchResult,
482
+ CanonicalUsage,
483
+ SessionResult,
475
484
  )
476
- return service
477
485
 
478
- async def test_happy_path_spawns_cli_and_returns_response(self, harness):
479
- service = self._wire_claude_service()
486
+ async def fake_run_batch(provider, *, tasks, **kwargs):
487
+ tasks_list = list(tasks)
488
+ results = []
489
+ for t in tasks_list:
490
+ results.append(SessionResult(
491
+ task_id=t.task_id or "t_test",
492
+ session_id=session_id,
493
+ provider=provider,
494
+ prompt=t.prompt,
495
+ response=response,
496
+ cost_usd=cost_usd,
497
+ duration_ms=1234,
498
+ num_turns=2,
499
+ canonical_usage=CanonicalUsage(input_tokens=10, output_tokens=5),
500
+ success=success,
501
+ error=error,
502
+ ))
503
+ n_succeeded = sum(1 for r in results if r.success)
504
+ return BatchResult(
505
+ tasks=results,
506
+ n_tasks=len(results),
507
+ n_succeeded=n_succeeded,
508
+ n_failed=len(results) - n_succeeded,
509
+ total_cost_usd=cost_usd if all(r.success for r in results) else None,
510
+ wall_clock_ms=1500,
511
+ provider=provider,
512
+ timestamp="2026-05-04T00:00:00Z",
513
+ )
514
+
515
+ svc = MagicMock(name="AICliService")
516
+ svc.run_batch = AsyncMock(side_effect=fake_run_batch)
517
+ svc.cancel_workflow = AsyncMock(return_value=0)
518
+ svc.cancel_node = AsyncMock(return_value=0)
519
+ return svc
520
+
521
+ async def test_happy_path_routes_through_run_batch(self, harness):
522
+ svc = self._wire_cli_service()
480
523
 
481
524
  with patched_container(auth_api_keys={}), patched_broadcaster(), patch(
482
- "services.claude_code_service.get_claude_code_service",
483
- return_value=service,
525
+ "services.cli_agent.service.get_ai_cli_service",
526
+ return_value=svc,
484
527
  ):
485
528
  result = await harness.execute(
486
529
  "claude_code_agent",
487
530
  {
488
531
  "prompt": "write a hello world script",
489
532
  "model": "claude-sonnet-4-6",
490
- "max_turns": 5,
491
- "max_budget_usd": 2.0,
492
533
  },
493
534
  )
494
535
 
495
536
  harness.assert_envelope(result, success=True)
496
537
  payload = result["result"]
497
538
  assert payload["response"] == "cli response"
498
- assert payload["provider"] == "anthropic"
499
539
  assert payload["session_id"] == "sess-abc"
500
- assert payload["model"] == "claude-sonnet-4-6"
501
-
502
- # The service was called with the documented kwargs.
503
- call = service.execute.await_args
504
- assert call.kwargs["model"] == "claude-sonnet-4-6"
505
- assert call.kwargs["max_turns"] == 5
506
- assert call.kwargs["max_budget_usd"] == 2.0
507
- assert call.kwargs["prompt"] == "write a hello world script"
508
-
509
- async def test_max_budget_usd_flag_is_passed_through(self, harness):
510
- """Guards against the historical '--max-cost' bug: the handler must
511
- forward maxBudgetUsd to the service as max_budget_usd."""
512
- service = self._wire_claude_service()
540
+ assert payload["provider"] == "claude"
541
+ assert payload["n_tasks"] == 1
542
+ assert payload["n_succeeded"] == 1
543
+
544
+ # AICliService.run_batch was called with provider="claude" + 1 task
545
+ call = svc.run_batch.await_args
546
+ assert call.args[0] == "claude"
547
+ tasks = list(call.kwargs["tasks"])
548
+ assert len(tasks) == 1
549
+ assert tasks[0].prompt == "write a hello world script"
550
+ assert tasks[0].model == "claude-sonnet-4-6"
551
+
552
+ async def test_max_budget_usd_propagates_via_task_spec(self, harness):
553
+ """Per-task max_budget_usd must reach the ClaudeTaskSpec."""
554
+ svc = self._wire_cli_service()
513
555
 
514
556
  with patched_container(auth_api_keys={}), patched_broadcaster(), patch(
515
- "services.claude_code_service.get_claude_code_service",
516
- return_value=service,
557
+ "services.cli_agent.service.get_ai_cli_service",
558
+ return_value=svc,
517
559
  ):
518
560
  await harness.execute(
519
561
  "claude_code_agent",
520
- {"prompt": "x", "max_budget_usd": 7.5},
562
+ {
563
+ "tasks": [
564
+ {"prompt": "x", "provider": "claude", "max_budget_usd": 7.5},
565
+ ],
566
+ },
521
567
  )
522
568
 
523
- call = service.execute.await_args
524
- assert call.kwargs["max_budget_usd"] == 7.5
569
+ call = svc.run_batch.await_args
570
+ tasks = list(call.kwargs["tasks"])
571
+ assert tasks[0].max_budget_usd == 7.5
525
572
 
526
573
  async def test_no_prompt_returns_failure(self, harness):
527
- """Unique to claude_code_agent: explicit no-prompt short-circuit."""
528
- service = self._wire_claude_service()
574
+ """No prompt + no tasks must short-circuit before constructing a batch."""
575
+ svc = self._wire_cli_service()
529
576
 
530
577
  with patched_container(auth_api_keys={}), patched_broadcaster(), patch(
531
- "services.claude_code_service.get_claude_code_service",
532
- return_value=service,
578
+ "services.cli_agent.service.get_ai_cli_service",
579
+ return_value=svc,
533
580
  ):
534
581
  result = await harness.execute(
535
582
  "claude_code_agent",
@@ -538,18 +585,17 @@ class TestClaudeCodeAgent:
538
585
 
539
586
  harness.assert_envelope(result, success=False)
540
587
  assert "prompt" in result["error"].lower()
541
- # CLI must not have been spawned.
542
- assert service.execute.await_count == 0
588
+ # AICliService must not have been engaged.
589
+ assert svc.run_batch.await_count == 0
543
590
 
544
- async def test_subprocess_failure_becomes_envelope(self, harness):
545
- """When ClaudeCodeService.execute raises, the handler returns
546
- success=false with the error message."""
547
- service = MagicMock(name="ClaudeCodeService")
548
- service.execute = AsyncMock(side_effect=RuntimeError("cli exit 1: boom"))
591
+ async def test_run_batch_failure_becomes_envelope(self, harness):
592
+ """When AICliService.run_batch raises, the handler surfaces the error."""
593
+ svc = MagicMock(name="AICliService")
594
+ svc.run_batch = AsyncMock(side_effect=RuntimeError("cli exit 1: boom"))
549
595
 
550
596
  with patched_container(auth_api_keys={}), patched_broadcaster(), patch(
551
- "services.claude_code_service.get_claude_code_service",
552
- return_value=service,
597
+ "services.cli_agent.service.get_ai_cli_service",
598
+ return_value=svc,
553
599
  ):
554
600
  result = await harness.execute(
555
601
  "claude_code_agent",
@@ -560,7 +606,7 @@ class TestClaudeCodeAgent:
560
606
  assert "boom" in result["error"]
561
607
 
562
608
  async def test_auto_prompt_fallback_from_input_main(self, harness):
563
- service = self._wire_claude_service()
609
+ svc = self._wire_cli_service()
564
610
 
565
611
  nodes = [
566
612
  {"id": "src-1", "type": "chatTrigger"},
@@ -576,8 +622,8 @@ class TestClaudeCodeAgent:
576
622
  )
577
623
 
578
624
  with patched_container(auth_api_keys={}), patched_broadcaster(), patch(
579
- "services.claude_code_service.get_claude_code_service",
580
- return_value=service,
625
+ "services.cli_agent.service.get_ai_cli_service",
626
+ return_value=svc,
581
627
  ):
582
628
  await harness.execute(
583
629
  "claude_code_agent",
@@ -586,5 +632,6 @@ class TestClaudeCodeAgent:
586
632
  context=ctx,
587
633
  )
588
634
 
589
- call = service.execute.await_args
590
- assert call.kwargs["prompt"] == "upstream text"
635
+ call = svc.run_batch.await_args
636
+ tasks = list(call.kwargs["tasks"])
637
+ assert tasks[0].prompt == "upstream text"
@@ -0,0 +1,293 @@
1
+ """Contract tests for the Stripe plugin (Wave 12 framework version).
2
+
3
+ The plugin is now a thin specialisation of ``services.events`` —
4
+ ``StripeListenSource`` (DaemonEventSource) supervises ``stripe listen``
5
+ and ``StripeWebhookSource`` (WebhookSource) receives forwarded events.
6
+ These tests verify the plugin wiring and the receive-node reshape;
7
+ end-to-end smoke against the real CLI requires the binary on PATH.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import hashlib
14
+ import hmac
15
+ import json
16
+ import time
17
+ from unittest.mock import AsyncMock, patch
18
+
19
+ import pytest
20
+
21
+ pytestmark = pytest.mark.node_contract
22
+
23
+
24
+ def _run(coro):
25
+ return asyncio.new_event_loop().run_until_complete(coro)
26
+
27
+
28
+ def _signed(body: bytes, secret: str) -> dict:
29
+ ts = int(time.time())
30
+ sig = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
31
+ return {"Stripe-Signature": f"t={ts},v1={sig}"}
32
+
33
+
34
+ def _fake_request(body: bytes, headers: dict | None = None):
35
+ """Return an object with the two attrs WebhookSource.handle uses."""
36
+
37
+ class R:
38
+ pass
39
+
40
+ r = R()
41
+ r.headers = headers or {}
42
+
43
+ async def _body():
44
+ return body
45
+
46
+ r.body = _body
47
+ return r
48
+
49
+
50
+ # ============================================================================
51
+ # StripeWebhookSource — shape() and end-to-end handle()
52
+ # ============================================================================
53
+
54
+
55
+ class TestStripeWebhookShape:
56
+ SECRET = "whsec_test_xyz"
57
+
58
+ def _stub_secret_resolution(self):
59
+ return patch(
60
+ "nodes.stripe._source.StripeCredential.resolve",
61
+ AsyncMock(return_value={"api_key": "sk_test", "stripe_webhook_secret": self.SECRET}),
62
+ )
63
+
64
+ def test_shape_extracts_stripe_fields(self):
65
+ from nodes.stripe._source import get_webhook_source
66
+ src = get_webhook_source()
67
+ payload = {
68
+ "id": "evt_1",
69
+ "type": "charge.succeeded",
70
+ "created": 1700000000,
71
+ "livemode": False,
72
+ "account": "acct_test",
73
+ "data": {"object": {"amount": 1000}},
74
+ }
75
+
76
+ class FakeReq:
77
+ headers = {}
78
+
79
+ async def body(self):
80
+ return b""
81
+
82
+ ev = _run(src.shape(FakeReq(), b"", payload))
83
+ assert ev.id == "evt_1"
84
+ assert ev.type == "stripe.charge.succeeded"
85
+ assert ev.source == "stripe://acct_test"
86
+ assert ev.data == {"object": {"amount": 1000}}
87
+
88
+ def test_handle_dispatches_on_valid_signature(self):
89
+ from nodes.stripe._source import get_webhook_source
90
+ src = get_webhook_source()
91
+ body = json.dumps({"id": "evt_2", "type": "charge.succeeded", "data": {}}).encode()
92
+ req = _fake_request(body, headers=_signed(body, self.SECRET))
93
+ with self._stub_secret_resolution(), \
94
+ patch("services.event_waiter.dispatch") as dispatch:
95
+ ev = _run(src.handle(req))
96
+ assert ev.type == "stripe.charge.succeeded"
97
+ dispatch.assert_called_once()
98
+ ev_type, dispatched = dispatch.call_args[0]
99
+ assert ev_type == "stripe.webhook"
100
+ assert dispatched.id == "evt_2"
101
+
102
+ def test_handle_rejects_tampered_signature(self):
103
+ from fastapi import HTTPException
104
+ from nodes.stripe._source import get_webhook_source
105
+ src = get_webhook_source()
106
+ body = json.dumps({"id": "evt_3", "type": "charge.succeeded"}).encode()
107
+ req = _fake_request(body, headers=_signed(body, "whsec_other"))
108
+ with self._stub_secret_resolution():
109
+ with pytest.raises(HTTPException) as exc:
110
+ _run(src.handle(req))
111
+ assert exc.value.status_code == 400
112
+
113
+
114
+ # ============================================================================
115
+ # StripeReceiveNode — filter + reshape
116
+ # ============================================================================
117
+
118
+
119
+ class TestStripeReceiveFilter:
120
+ def _filter(self, params: dict | None = None):
121
+ from nodes.stripe.stripe_receive import StripeReceiveNode, StripeReceiveParams
122
+ node = StripeReceiveNode()
123
+ return node.build_filter(StripeReceiveParams(**(params or {})))
124
+
125
+ def _ev(self, stripe_type: str, livemode: bool = False):
126
+ from services.events import WorkflowEvent
127
+ return WorkflowEvent(
128
+ source="stripe://acct_1",
129
+ type=f"stripe.{stripe_type}",
130
+ data={"livemode": livemode},
131
+ )
132
+
133
+ def test_all_matches_anything(self):
134
+ f = self._filter({"event_type_filter": "all"})
135
+ assert f(self._ev("charge.succeeded")) is True
136
+ assert f(self._ev("payment_intent.created")) is True
137
+
138
+ def test_exact_match(self):
139
+ f = self._filter({"event_type_filter": "charge.succeeded"})
140
+ assert f(self._ev("charge.succeeded")) is True
141
+ assert f(self._ev("charge.refunded")) is False
142
+
143
+ def test_wildcard_prefix(self):
144
+ f = self._filter({"event_type_filter": "charge.*"})
145
+ assert f(self._ev("charge.succeeded")) is True
146
+ assert f(self._ev("charge.refunded")) is True
147
+ assert f(self._ev("payment_intent.created")) is False
148
+
149
+ def test_livemode_filter(self):
150
+ live = self._filter({"livemode_filter": "live"})
151
+ test = self._filter({"livemode_filter": "test"})
152
+ assert live(self._ev("charge.succeeded", livemode=True)) is True
153
+ assert live(self._ev("charge.succeeded", livemode=False)) is False
154
+ assert test(self._ev("charge.succeeded", livemode=False)) is True
155
+ assert test(self._ev("charge.succeeded", livemode=True)) is False
156
+
157
+
158
+ class TestStripeReceiveReshape:
159
+ def test_shape_output_extracts_stripe_fields(self):
160
+ from nodes.stripe.stripe_receive import StripeReceiveNode
161
+ from services.events import WorkflowEvent
162
+ ev = WorkflowEvent(
163
+ id="evt_1",
164
+ source="stripe://acct_42",
165
+ type="stripe.charge.succeeded",
166
+ data={
167
+ "request": {"id": "req_99"},
168
+ "data": {"object": {"amount": 2500}},
169
+ "livemode": True,
170
+ "api_version": "2024-04-10",
171
+ },
172
+ )
173
+ out = StripeReceiveNode().shape_output(ev)
174
+ assert out["event_id"] == "evt_1"
175
+ assert out["event_type"] == "charge.succeeded"
176
+ assert out["request_id"] == "req_99"
177
+ assert out["account"] == "acct_42"
178
+ assert out["livemode"] is True
179
+ assert out["data"] == {"object": {"amount": 2500}}
180
+
181
+
182
+ # ============================================================================
183
+ # StripeActionNode — pass-through over the CLI
184
+ # ============================================================================
185
+
186
+
187
+ class TestStripeActionPassthrough:
188
+ @pytest.fixture
189
+ def cli_capture(self):
190
+ captured: list[list[str]] = []
191
+
192
+ async def fake_run(*, binary, argv, **kwargs):
193
+ # Stripe CLI uses ~/.config/stripe/config.toml — no credential injection.
194
+ captured.append(list(argv))
195
+ return {"success": True, "result": {"id": "x"}, "stdout": "{}"}
196
+
197
+ return captured, fake_run
198
+
199
+ def test_command_is_shlex_split(self, cli_capture):
200
+ captured, fake = cli_capture
201
+ with patch("nodes.stripe.stripe_action.run_cli_command", AsyncMock(side_effect=fake)):
202
+ from nodes.stripe.stripe_action import StripeActionNode, StripeActionParams
203
+ node = StripeActionNode()
204
+ result = _run(node.run(None, StripeActionParams(command="customers create --email a@b.com")))
205
+ assert result["success"] is True
206
+ assert captured == [["customers", "create", "--email", "a@b.com"]]
207
+
208
+ def test_quoted_args_preserved(self, cli_capture):
209
+ captured, fake = cli_capture
210
+ with patch("nodes.stripe.stripe_action.run_cli_command", AsyncMock(side_effect=fake)):
211
+ from nodes.stripe.stripe_action import StripeActionNode, StripeActionParams
212
+ node = StripeActionNode()
213
+ _run(node.run(None, StripeActionParams(command="customers create --name 'Acme Inc'")))
214
+ assert captured == [["customers", "create", "--name", "Acme Inc"]]
215
+
216
+ def test_empty_command_raises(self):
217
+ from nodes.stripe.stripe_action import StripeActionNode, StripeActionParams
218
+ node = StripeActionNode()
219
+ with pytest.raises(RuntimeError, match="command is required"):
220
+ _run(node.run(None, StripeActionParams(command=" ")))
221
+
222
+ def test_cli_failure_raises(self):
223
+ from nodes.stripe.stripe_action import StripeActionNode, StripeActionParams
224
+
225
+ async def fake_fail(*, binary, argv, **kwargs):
226
+ return {"success": False, "error": "stripe: unknown command 'frobnicate'"}
227
+
228
+ with patch("nodes.stripe.stripe_action.run_cli_command", AsyncMock(side_effect=fake_fail)):
229
+ node = StripeActionNode()
230
+ with pytest.raises(RuntimeError, match="frobnicate"):
231
+ _run(node.run(None, StripeActionParams(command="frobnicate")))
232
+
233
+
234
+ # ============================================================================
235
+ # Plugin self-registration
236
+ # ============================================================================
237
+
238
+
239
+ class TestStripePluginRegistration:
240
+ def test_ws_handlers_registered(self):
241
+ import nodes.stripe # noqa: F401
242
+ from services.ws_handler_registry import get_ws_handlers
243
+ registered = get_ws_handlers()
244
+ for name in (
245
+ "stripe_login", "stripe_logout",
246
+ "stripe_connect", "stripe_disconnect", "stripe_reconnect",
247
+ "stripe_status", "stripe_trigger",
248
+ ):
249
+ assert name in registered, f"WS handler '{name}' not registered"
250
+
251
+ def test_webhook_source_registered(self):
252
+ import nodes.stripe # noqa: F401
253
+ from services.events import WEBHOOK_SOURCES
254
+ assert "stripe" in WEBHOOK_SOURCES
255
+ assert WEBHOOK_SOURCES["stripe"].type == "stripe.webhook"
256
+
257
+ def test_node_classes_registered(self):
258
+ import nodes.stripe # noqa: F401
259
+ from services.node_registry import get_node_class
260
+ assert get_node_class("stripeReceive").__name__ == "StripeReceiveNode"
261
+ assert get_node_class("stripeAction").__name__ == "StripeActionNode"
262
+
263
+ def test_credential_registered(self):
264
+ import nodes.stripe # noqa: F401
265
+ from services.plugin.credential import CREDENTIAL_REGISTRY
266
+ # Stripe CLI manages auth at ~/.config/stripe/config.toml; the
267
+ # credential class is a thin marker keyed by "stripe" (no api_key).
268
+ assert "stripe" in CREDENTIAL_REGISTRY
269
+
270
+ def test_output_schemas_registered(self):
271
+ import nodes.stripe # noqa: F401
272
+ from services.node_output_schemas import NODE_OUTPUT_SCHEMAS
273
+ assert "stripeReceive" in NODE_OUTPUT_SCHEMAS
274
+ assert "stripeAction" in NODE_OUTPUT_SCHEMAS
275
+
276
+ def test_action_node_is_ai_tool(self):
277
+ from nodes.stripe.stripe_action import StripeActionNode
278
+ assert StripeActionNode.usable_as_tool is True
279
+
280
+ def test_receive_node_subscribes_to_stripe_webhook(self):
281
+ from nodes.stripe.stripe_receive import StripeReceiveNode
282
+ assert StripeReceiveNode.event_type == "stripe.webhook"
283
+
284
+ def test_listen_source_has_correct_namespace(self):
285
+ from nodes.stripe._source import get_listen_source
286
+ src = get_listen_source()
287
+ assert src.process_name == "stripe-listen"
288
+ assert src.workflow_namespace == "_stripe"
289
+ # Empty binary_name disables the framework's PATH check; the
290
+ # plugin resolves the binary itself via ensure_stripe_cli (which
291
+ # falls back to a workspace-local download on systems without
292
+ # a system install of the Stripe CLI).
293
+ assert src.binary_name == ""
@@ -355,7 +355,7 @@ class TestSocialSend:
355
355
  return_value={"success": True, "message_id": "wamid.xyz"}
356
356
  )
357
357
 
358
- with patch("services.whatsapp_service.handle_whatsapp_send", whatsapp_send):
358
+ with patch("nodes.whatsapp._service.handle_whatsapp_send", whatsapp_send):
359
359
  result = await harness.execute(
360
360
  "socialSend",
361
361
  {
@@ -385,7 +385,7 @@ class TestSocialSend:
385
385
  # recipientType=phone but no phone param -> ValueError inside handler
386
386
  whatsapp_send = AsyncMock()
387
387
 
388
- with patch("services.whatsapp_service.handle_whatsapp_send", whatsapp_send):
388
+ with patch("nodes.whatsapp._service.handle_whatsapp_send", whatsapp_send):
389
389
  result = await harness.execute(
390
390
  "socialSend",
391
391
  {
@@ -404,7 +404,7 @@ class TestSocialSend:
404
404
  # Any non-whatsapp channel should surface as a failed envelope.
405
405
  whatsapp_send = AsyncMock()
406
406
 
407
- with patch("services.whatsapp_service.handle_whatsapp_send", whatsapp_send):
407
+ with patch("nodes.whatsapp._service.handle_whatsapp_send", whatsapp_send):
408
408
  result = await harness.execute(
409
409
  "socialSend",
410
410
  {
@@ -425,7 +425,7 @@ class TestSocialSend:
425
425
  return_value={"success": False, "error": "rpc boom"}
426
426
  )
427
427
 
428
- with patch("services.whatsapp_service.handle_whatsapp_send", whatsapp_send):
428
+ with patch("nodes.whatsapp._service.handle_whatsapp_send", whatsapp_send):
429
429
  result = await harness.execute(
430
430
  "socialSend",
431
431
  {
@@ -242,7 +242,7 @@ class TestTwitterSend:
242
242
  with _patch_client(stub_cls), patched_container(
243
243
  auth_oauth_tokens=_ok_tokens()
244
244
  ), patched_pricing(), patch(
245
- "services.twitter_oauth.TwitterOAuth"
245
+ "nodes.twitter._oauth.TwitterOAuth"
246
246
  ) as oauth_cls:
247
247
  oauth_instance = MagicMock()
248
248
  oauth_instance.refresh_access_token = refresh_mock
@@ -50,7 +50,7 @@ class _FakeBrowserService:
50
50
 
51
51
  def _patch_browser_service(svc):
52
52
  """Patch get_browser_service to return the fake."""
53
- return patch("services.browser_service.get_browser_service", return_value=svc)
53
+ return patch("nodes.browser._service.get_browser_service", return_value=svc)
54
54
 
55
55
 
56
56
  # ============================================================================
@@ -140,7 +140,7 @@ class TestBrowser:
140
140
 
141
141
  async def test_service_not_installed(self, harness):
142
142
  with patch(
143
- "services.browser_service.get_browser_service", return_value=None
143
+ "nodes.browser._service.get_browser_service", return_value=None
144
144
  ):
145
145
  result = await harness.execute("browser", {"operation": "navigate", "url": "https://x"})
146
146