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
@@ -207,8 +207,8 @@ class RelayWebSocketClient:
207
207
  async def _clear_stored_session(self):
208
208
  """Clear stored pairing session from database."""
209
209
  try:
210
- from core.container import container
211
- database = container.database()
210
+ from services.plugin.deps import get_database
211
+ database = get_database()
212
212
 
213
213
  await database.clear_android_relay_session()
214
214
  logger.debug("[Relay] Cleared stored pairing session")
@@ -368,8 +368,8 @@ class RelayWebSocketClient:
368
368
  async def _save_pairing_session(self):
369
369
  """Save pairing session to database for auto-reconnect."""
370
370
  try:
371
- from core.container import container
372
- database = container.database()
371
+ from services.plugin.deps import get_database
372
+ database = get_database()
373
373
 
374
374
  await database.save_android_relay_session(
375
375
  relay_url=self.base_url,
@@ -1,16 +1,32 @@
1
1
  """Android System Services routes."""
2
2
 
3
- from fastapi import APIRouter, Depends
3
+ import re
4
+
5
+ from fastapi import APIRouter, Depends, HTTPException
4
6
  from pydantic import BaseModel, Field
5
7
  from typing import Dict, Any
6
8
 
7
- from core.container import container
8
- from services.android_service import AndroidService
9
+ from ._dispatcher import AndroidService
9
10
  from core.logging import get_logger
11
+ from services.plugin.deps import get_android_service
10
12
 
11
13
  logger = get_logger(__name__)
12
14
  router = APIRouter(prefix="/api/android", tags=["android"])
13
15
 
16
+ # ADB device IDs: USB serials (alphanumeric), TCP "host:port" (digits + dots
17
+ # + colon), or "emulator-NNNN". All are safe characters but we lock the
18
+ # accepted set explicitly so untrusted input can't slip a flag or path
19
+ # separator into the argv list we pass to subprocess.run. Anything outside
20
+ # `[A-Za-z0-9._:-]` (and longer than 64 chars) is rejected with 400.
21
+ _DEVICE_ID_PATTERN = re.compile(r"^[A-Za-z0-9._:-]{1,64}$")
22
+
23
+
24
+ def _validate_device_id(device_id: str) -> str:
25
+ """Return device_id if it matches the ADB device-id shape, else 400."""
26
+ if not _DEVICE_ID_PATTERN.fullmatch(device_id):
27
+ raise HTTPException(status_code=400, detail="invalid device_id")
28
+ return device_id
29
+
14
30
 
15
31
  class AndroidServiceRequest(BaseModel):
16
32
  """Request model for Android service execution."""
@@ -24,7 +40,7 @@ class AndroidServiceRequest(BaseModel):
24
40
  @router.post("/execute")
25
41
  async def execute_android_service(
26
42
  request: AndroidServiceRequest,
27
- android_service: AndroidService = Depends(lambda: container.android_service())
43
+ android_service: AndroidService = Depends(get_android_service)
28
44
  ):
29
45
  """Execute an Android system service action.
30
46
 
@@ -57,7 +73,7 @@ async def execute_android_service(
57
73
  async def check_device_status(
58
74
  android_host: str = "localhost",
59
75
  android_port: int = 8888,
60
- android_service: AndroidService = Depends(lambda: container.android_service())
76
+ android_service: AndroidService = Depends(get_android_service)
61
77
  ):
62
78
  """Check if Android device API is reachable."""
63
79
  logger.info(
@@ -77,7 +93,7 @@ async def check_device_status(
77
93
  @router.get("/services/{service_id}/actions")
78
94
  async def get_service_actions(
79
95
  service_id: str,
80
- android_service: AndroidService = Depends(lambda: container.android_service())
96
+ android_service: AndroidService = Depends(get_android_service)
81
97
  ):
82
98
  """Get available actions for a specific Android service.
83
99
 
@@ -105,7 +121,7 @@ async def get_service_actions(
105
121
  async def get_action_parameters(
106
122
  service_id: str,
107
123
  action: str,
108
- android_service: AndroidService = Depends(lambda: container.android_service())
124
+ android_service: AndroidService = Depends(get_android_service)
109
125
  ):
110
126
  """Get default parameters for a specific service action.
111
127
 
@@ -195,9 +211,12 @@ async def setup_port_forwarding(
195
211
  device_port: int = 8888
196
212
  ):
197
213
  """Setup ADB port forwarding for Android device communication."""
214
+ device_id = _validate_device_id(device_id)
198
215
  import subprocess
199
216
  try:
200
217
  # Setup port forwarding: adb -s device_id forward tcp:local_port tcp:device_port
218
+ # device_id passes _DEVICE_ID_PATTERN above; subprocess.run is called
219
+ # with an argv list (no shell), so no further interpolation risk.
201
220
  cmd = ["adb", "-s", device_id, "forward", f"tcp:{local_port}", f"tcp:{device_port}"]
202
221
 
203
222
  result = subprocess.run(
@@ -258,7 +277,7 @@ async def get_relay_connection_status():
258
277
  and the paired Android device.
259
278
  """
260
279
  try:
261
- from services.android import get_current_relay_client
280
+ from ._relay import get_current_relay_client
262
281
 
263
282
  relay_client = get_current_relay_client()
264
283
 
@@ -3,7 +3,7 @@
3
3
  Interactive browser automation via the agent-browser CLI. The plugin
4
4
  maps the high-level operation enum to CLI argv, resolves the browser
5
5
  binary (system Chrome / Edge / Chromium / bundled), and delegates the
6
- subprocess invocation to ``services.browser_service``.
6
+ subprocess invocation to ``_service`` (the plugin-private service).
7
7
  """
8
8
 
9
9
  from __future__ import annotations
@@ -317,7 +317,7 @@ class BrowserNode(ActionNode):
317
317
 
318
318
  @Operation("dispatch")
319
319
  async def dispatch(self, ctx: NodeContext, params: BrowserParams) -> BrowserOutput:
320
- from services.browser_service import get_browser_service
320
+ from ._service import get_browser_service
321
321
 
322
322
  svc = get_browser_service()
323
323
  if not svc:
@@ -29,9 +29,13 @@ class CodeExecutorOutput(BaseModel):
29
29
 
30
30
 
31
31
  class CodeExecutorBase(ActionNode, abstract=True):
32
- """Subclass and set type / display_name / icon / handler import."""
32
+ """Subclass and set type / display_name / handler import.
33
+
34
+ Visual metadata (icon + color) lives in ``server/nodes/visuals.json``
35
+ keyed by individual plugin type. The ``_visuals.py`` resolver picks
36
+ each entry up at NodeSpec emit time; no class-level ClassVars needed.
37
+ """
33
38
 
34
- color = "#ffb86c"
35
39
  group = ("code", "tool")
36
40
  component_kind = "square"
37
41
  handles = (
@@ -0,0 +1,134 @@
1
+ """Legacy single-task Claude Code shim.
2
+
3
+ Kept for back-compat with any caller that still imports
4
+ `get_claude_code_service()`. New code should call
5
+ `services.cli_agent.AICliService.run_batch("claude", ...)` directly via
6
+ the `claude_code_agent` plugin.
7
+
8
+ This module:
9
+ - Builds a single ``ClaudeTaskSpec`` from kwargs
10
+ - Calls ``AICliService.run_batch("claude", ...)``
11
+ - Adapts the ``BatchResult`` back into the dict shape the legacy
12
+ callers expected
13
+
14
+ Eventually deletable once all imports point at ``cli_agent.service``.
15
+ The hardcoded 300s `wait_for` is gone — ``timeout_seconds`` is now per
16
+ task and configurable.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from pathlib import Path
23
+ from typing import Any, Dict, Optional
24
+
25
+ from core.config import Settings
26
+ from core.logging import get_logger
27
+
28
+ from services.cli_agent import ClaudeTaskSpec
29
+ from services.cli_agent.service import get_ai_cli_service
30
+ from services.plugin.singleton import ServiceSingleton
31
+
32
+ logger = get_logger(__name__)
33
+
34
+
35
+ class ClaudeCodeService(ServiceSingleton):
36
+ """Thin shim that adapts to the new AICliService."""
37
+
38
+ def __init__(self) -> None:
39
+ self._session_map: Dict[str, str] = {} # node_id -> session_id
40
+
41
+ async def execute(
42
+ self,
43
+ prompt: str,
44
+ node_id: str = "",
45
+ model: str = "claude-sonnet-4-6",
46
+ cwd: Optional[str] = None,
47
+ allowed_tools: str = "Read,Edit,Bash,Glob,Grep,Write",
48
+ max_turns: int = 10,
49
+ max_budget_usd: float = 5.0,
50
+ system_prompt: Optional[str] = None,
51
+ timeout_seconds: int = 600,
52
+ ) -> Dict[str, Any]:
53
+ """Run a single Claude Code task. Returns legacy dict shape."""
54
+ if not cwd:
55
+ cwd = os.path.join(Settings().workspace_base_resolved, "default")
56
+ os.makedirs(cwd, exist_ok=True)
57
+
58
+ # Resume the prior session for this node if we have one
59
+ resume_session_id = self._session_map.get(node_id) if node_id else None
60
+
61
+ task = ClaudeTaskSpec(
62
+ task_id=f"legacy_{node_id}" if node_id else "legacy",
63
+ prompt=prompt,
64
+ model=model,
65
+ max_turns=max_turns,
66
+ max_budget_usd=max_budget_usd,
67
+ allowed_tools=allowed_tools,
68
+ system_prompt=system_prompt,
69
+ timeout_seconds=timeout_seconds,
70
+ resume_session_id=resume_session_id,
71
+ )
72
+
73
+ svc = get_ai_cli_service()
74
+ workspace_dir = Path(cwd)
75
+ repo_root = self._find_git_repo(workspace_dir)
76
+
77
+ result = await svc.run_batch(
78
+ "claude",
79
+ tasks=[task],
80
+ node_id=node_id or "legacy_node",
81
+ workflow_id="legacy_workflow",
82
+ workspace_dir=workspace_dir,
83
+ broadcaster=None,
84
+ repo_root=repo_root,
85
+ )
86
+
87
+ if not result.tasks:
88
+ raise RuntimeError("AICliService returned empty batch")
89
+
90
+ sr = result.tasks[0]
91
+ if not sr.success:
92
+ raise RuntimeError(sr.error or "claude_code_service: task failed")
93
+
94
+ # Persist session_id for future resume
95
+ if sr.session_id and node_id:
96
+ self._session_map[node_id] = sr.session_id
97
+
98
+ return {
99
+ "result": sr.response,
100
+ "session_id": sr.session_id or "",
101
+ "total_cost_usd": sr.cost_usd,
102
+ "duration_ms": sr.duration_ms,
103
+ "num_turns": sr.num_turns,
104
+ "usage": {
105
+ "input_tokens": sr.canonical_usage.input_tokens,
106
+ "output_tokens": sr.canonical_usage.output_tokens,
107
+ "cache_creation_input_tokens": sr.canonical_usage.cache_write,
108
+ "cache_read_input_tokens": sr.canonical_usage.cache_read,
109
+ },
110
+ }
111
+
112
+ def get_session_id(self, node_id: str) -> Optional[str]:
113
+ return self._session_map.get(node_id)
114
+
115
+ def clear_session(self, node_id: str) -> None:
116
+ self._session_map.pop(node_id, None)
117
+
118
+ @staticmethod
119
+ def _find_git_repo(start: Path) -> Optional[Path]:
120
+ """Walk up from `start` looking for a `.git` directory."""
121
+ cur = start.resolve()
122
+ for _ in range(8):
123
+ if (cur / ".git").exists():
124
+ return cur
125
+ if cur.parent == cur:
126
+ return None
127
+ cur = cur.parent
128
+ return None
129
+
130
+
131
+ def get_claude_code_service() -> ClaudeCodeService:
132
+ """Module-level accessor preserved for legacy callers; delegates to
133
+ the :class:`ServiceSingleton` mixin's ``instance()`` classmethod."""
134
+ return ClaudeCodeService.instance()
@@ -7,7 +7,7 @@ from typing import List, Literal, Optional
7
7
 
8
8
  from pydantic import BaseModel, ConfigDict, Field
9
9
 
10
- from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
10
+ from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
11
11
 
12
12
 
13
13
  class EmbeddingGeneratorParams(BaseModel):
@@ -83,7 +83,7 @@ class EmbeddingGeneratorNode(ActionNode):
83
83
  try:
84
84
  from langchain_huggingface import HuggingFaceEmbeddings
85
85
  except ImportError:
86
- raise RuntimeError(
86
+ raise NodeUserError(
87
87
  "HuggingFace embeddings unavailable. "
88
88
  "pip install langchain-huggingface sentence-transformers",
89
89
  )
@@ -95,7 +95,7 @@ class EmbeddingGeneratorNode(ActionNode):
95
95
  from langchain_ollama import OllamaEmbeddings
96
96
  embedder = OllamaEmbeddings(model=model)
97
97
  else:
98
- raise RuntimeError(f"Unknown provider: {provider}")
98
+ raise NodeUserError(f"Unknown provider: {provider}")
99
99
 
100
100
  embeddings = await asyncio.to_thread(embedder.embed_documents, texts)
101
101
  dimensions = len(embeddings[0]) if embeddings else 0
@@ -15,7 +15,7 @@ from bs4 import BeautifulSoup
15
15
  from pydantic import BaseModel, ConfigDict, Field
16
16
 
17
17
  from core.logging import get_logger
18
- from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
18
+ from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
19
19
 
20
20
  logger = get_logger(__name__)
21
21
 
@@ -130,7 +130,7 @@ class HttpScraperNode(ActionNode):
130
130
  async def scrape(self, ctx: NodeContext, params: HttpScraperParams) -> HttpScraperOutput:
131
131
  url = params.url
132
132
  if not url:
133
- raise RuntimeError("URL is required")
133
+ raise NodeUserError("URL is required")
134
134
 
135
135
  iteration_mode = params.iteration_mode
136
136
  link_selector = params.link_selector or 'a[href$=".pdf"]'
@@ -140,7 +140,7 @@ class HttpScraperNode(ActionNode):
140
140
  urls_to_fetch = []
141
141
  if iteration_mode == 'date':
142
142
  if not params.start_date or not params.end_date:
143
- raise RuntimeError("start_date/end_date required for date mode")
143
+ raise NodeUserError("start_date/end_date required for date mode")
144
144
  placeholder = params.date_placeholder or '{date}'
145
145
  start = datetime.strptime(params.start_date, "%Y-%m-%d")
146
146
  end = datetime.strptime(params.end_date, "%Y-%m-%d")
@@ -13,7 +13,7 @@ from typing import Any, Dict, Literal, Optional
13
13
 
14
14
  from pydantic import BaseModel, ConfigDict, Field
15
15
 
16
- from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
16
+ from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
17
17
 
18
18
 
19
19
  class VectorStoreParams(BaseModel):
@@ -70,7 +70,7 @@ async def _chroma_op(operation: str, params: Dict[str, Any], collection: str) ->
70
70
  try:
71
71
  import chromadb
72
72
  except ImportError:
73
- raise RuntimeError("ChromaDB unavailable. pip install chromadb")
73
+ raise NodeUserError("ChromaDB unavailable. pip install chromadb")
74
74
 
75
75
  persist_dir = params.get('persist_dir', './data/vectors')
76
76
  client = chromadb.PersistentClient(path=persist_dir)
@@ -126,7 +126,7 @@ async def _qdrant_op(operation: str, params: Dict[str, Any], collection: str) ->
126
126
  from qdrant_client import QdrantClient
127
127
  from qdrant_client.models import Distance, PointStruct, VectorParams
128
128
  except ImportError:
129
- raise RuntimeError("Qdrant client unavailable. pip install qdrant-client")
129
+ raise NodeUserError("Qdrant client unavailable. pip install qdrant-client")
130
130
 
131
131
  url = params.get('qdrant_url', 'http://localhost:6333')
132
132
  client = QdrantClient(url=url)
@@ -188,7 +188,7 @@ async def _pinecone_op(operation: str, params: Dict[str, Any], collection: str)
188
188
 
189
189
  api_key = params.get('pinecone_api_key', '')
190
190
  if not api_key:
191
- raise RuntimeError("Pinecone API key required")
191
+ raise NodeUserError("Pinecone API key required")
192
192
 
193
193
  pc = Pinecone(api_key=api_key)
194
194
  index = pc.Index(collection)
@@ -271,7 +271,7 @@ class VectorStoreNode(ActionNode):
271
271
  elif backend == 'pinecone':
272
272
  result = await _pinecone_op(operation, p, collection)
273
273
  else:
274
- raise RuntimeError(f"Unknown backend: {backend_raw}")
274
+ raise NodeUserError(f"Unknown backend: {backend_raw}")
275
275
 
276
276
  result['backend'] = backend
277
277
  result['collection_name'] = collection
@@ -1 +1,11 @@
1
- """Plugins for the 'email' palette group. See ../__init__.py for the package layout."""
1
+ """Plugins for the 'email' palette group.
2
+
3
+ Self-registers the trigger filter builder for ``emailReceive`` so the
4
+ central ``services/event_waiter.py`` carries no plugin-specific code.
5
+ """
6
+
7
+ from services.event_waiter import register_filter_builder
8
+
9
+ from ._filters import build_filter as build_email_filter
10
+
11
+ register_filter_builder("emailReceive", build_email_filter)
@@ -0,0 +1,21 @@
1
+ """Email event-trigger filter builder (Wave 11.I, milestone K).
2
+
3
+ Moved verbatim from ``services/event_waiter.build_email_filter``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Callable, Dict
9
+
10
+
11
+ def build_filter(params: Dict) -> Callable[[Dict], bool]:
12
+ """Build filter for email events (Himalaya IMAP polling)."""
13
+ folder_filter = params.get('folder', 'INBOX')
14
+
15
+ def matches(data: Dict) -> bool:
16
+ if folder_filter and folder_filter != 'all':
17
+ if data.get('folder', '') != folder_filter:
18
+ return False
19
+ return True
20
+
21
+ return matches
@@ -14,24 +14,20 @@ from pathlib import Path
14
14
  from typing import Any, Dict, List, Optional
15
15
 
16
16
  from core.logging import get_logger
17
+ from services.plugin.singleton import ServiceSingleton
17
18
 
18
19
  logger = get_logger(__name__)
19
20
 
20
21
 
21
- class HimalayaService:
22
- """Manages Himalaya CLI configuration and execution."""
22
+ class HimalayaService(ServiceSingleton):
23
+ """Manages Himalaya CLI configuration and execution.
23
24
 
24
- _instance: Optional["HimalayaService"] = None
25
+ Inherits ``instance`` / ``reset_instance`` from
26
+ :class:`ServiceSingleton`."""
25
27
 
26
28
  def __init__(self):
27
29
  self._binary_path: Optional[str] = None
28
30
 
29
- @classmethod
30
- def get_instance(cls) -> "HimalayaService":
31
- if cls._instance is None:
32
- cls._instance = cls()
33
- return cls._instance
34
-
35
31
  async def ensure_binary(self) -> str:
36
32
  """Detect himalaya binary in PATH. Returns path or raises."""
37
33
  if self._binary_path:
@@ -265,4 +261,4 @@ class HimalayaService:
265
261
 
266
262
  def get_himalaya_service() -> HimalayaService:
267
263
  """Get singleton instance."""
268
- return HimalayaService.get_instance()
264
+ return HimalayaService.instance()
@@ -8,11 +8,12 @@ from pathlib import Path
8
8
  from typing import Any, Dict, Optional, Set
9
9
 
10
10
  from core.logging import get_logger
11
+ from services.plugin.singleton import ServiceSingleton
11
12
 
12
13
  logger = get_logger(__name__)
13
14
 
14
15
  _CONFIG: Optional[Dict] = None
15
- _CONFIG_PATH = Path(__file__).parent.parent / "config" / "email_providers.json"
16
+ _CONFIG_PATH = Path(__file__).resolve().parents[2] / "config" / "email_providers.json"
16
17
 
17
18
 
18
19
  def _load_config() -> Dict:
@@ -23,14 +24,9 @@ def _load_config() -> Dict:
23
24
  return _CONFIG
24
25
 
25
26
 
26
- class EmailService:
27
- _instance: Optional["EmailService"] = None
28
-
29
- @classmethod
30
- def get_instance(cls) -> "EmailService":
31
- if cls._instance is None:
32
- cls._instance = cls()
33
- return cls._instance
27
+ class EmailService(ServiceSingleton):
28
+ """Plugin-owned email orchestrator. Inherits ``instance`` /
29
+ ``reset_instance`` from :class:`ServiceSingleton`."""
34
30
 
35
31
  @property
36
32
  def config(self) -> Dict:
@@ -46,7 +42,7 @@ class EmailService:
46
42
 
47
43
  @property
48
44
  def himalaya(self):
49
- from services.himalaya_service import get_himalaya_service
45
+ from ._himalaya import get_himalaya_service
50
46
  return get_himalaya_service()
51
47
 
52
48
  def _provider_preset(self, name: str) -> Dict:
@@ -61,8 +57,8 @@ class EmailService:
61
57
  Stored custom keys (email_imap_host, email_smtp_port, etc.) are used when
62
58
  the provider is 'custom' or when a preset field is empty.
63
59
  """
64
- from core.container import container
65
- auth = container.auth_service()
60
+ from services.plugin.deps import get_auth_service
61
+ auth = get_auth_service()
66
62
 
67
63
  provider = params.get("provider") or await auth.get_api_key("email_provider") or self.defaults.get("provider")
68
64
  preset = self._provider_preset(provider)
@@ -184,4 +180,4 @@ class EmailService:
184
180
 
185
181
 
186
182
  def get_email_service() -> EmailService:
187
- return EmailService.get_instance()
183
+ return EmailService.instance()
@@ -120,5 +120,5 @@ class EmailReadNode(ActionNode):
120
120
  @Operation("query", cost={"service": "email", "action": "imap", "count": 1})
121
121
  async def query(self, ctx: NodeContext, params: EmailReadParams) -> Any:
122
122
  # Body inlined from handlers/email.py (Wave 11.D.1).
123
- from services.email_service import get_email_service
123
+ from ._service import get_email_service
124
124
  return await get_email_service().read(params.model_dump())
@@ -6,11 +6,13 @@ IMAP polling for new mail via Himalaya CLI. Thin delegation to
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from typing import Any, Dict, Literal, Optional
9
+ from typing import Any, Dict, Literal, Optional, Set
10
10
 
11
11
  from pydantic import BaseModel, ConfigDict, Field
12
12
 
13
- from services.plugin import NodeContext, Operation, TaskQueue, TriggerNode
13
+ from services.plugin import (
14
+ NodeContext, Operation, PollingTriggerNode, TaskQueue,
15
+ )
14
16
 
15
17
 
16
18
  class EmailReceiveParams(BaseModel):
@@ -35,23 +37,70 @@ class EmailReceiveOutput(BaseModel):
35
37
  model_config = ConfigDict(extra="allow")
36
38
 
37
39
 
38
- class EmailReceiveNode(TriggerNode):
40
+ class EmailReceiveNode(PollingTriggerNode):
39
41
  type = "emailReceive"
40
42
  display_name = "Email Receive"
41
43
  subtitle = "IMAP Polling"
42
44
  group = ("email", "trigger")
43
45
  description = "Polling trigger for new emails via IMAP"
44
46
  component_kind = "trigger"
47
+ # Wave 11.I, milestone K: ``event_type`` ClassVar lets
48
+ # ``event_waiter._auto_populate_from_plugins`` backfill
49
+ # TRIGGER_REGISTRY without a hardcoded entry in event_waiter.
50
+ event_type = "email_received"
45
51
  handles = (
46
52
  {"name": "output-main", "kind": "output", "position": "right",
47
53
  "label": "Output", "role": "main"},
48
54
  )
49
55
  task_queue = TaskQueue.TRIGGERS_POLL
50
- mode = "polling"
56
+ # Email keeps its 30s lower bound (legacy floor in
57
+ # config/email_providers.json). Gmail uses the default (10, 3600).
58
+ poll_interval_clamp = (30, 3600)
51
59
 
52
60
  Params = EmailReceiveParams
53
61
  Output = EmailReceiveOutput
54
62
 
63
+ # ---- PollingTriggerNode hooks (deployment-mode loop) -------------
64
+ #
65
+ # The Run-button path lives in ``execute()`` below and stays
66
+ # bespoke: it broadcasts ``waiting`` status, dispatches via
67
+ # event_waiter, and returns after the first new email. The
68
+ # deployment loop owned by ``PollingTriggerNode`` drains
69
+ # continuously via the deployment manager's queue and uses the
70
+ # four hooks below.
71
+
72
+ async def setup_service(self, params: Dict[str, Any]) -> Any:
73
+ from ._service import get_email_service
74
+
75
+ svc = get_email_service()
76
+ creds = await svc.resolve_credentials(params)
77
+ cfg = svc.resolve_poll_params(params)
78
+ return svc, creds, cfg["folder"], cfg.get("mark_as_read", False)
79
+
80
+ async def fetch_ids(self, service: Any, params: Dict[str, Any]) -> Set[str]:
81
+ svc, creds, folder, _mark = service
82
+ return await svc.poll_ids(creds, folder)
83
+
84
+ async def fetch_detail(
85
+ self, service: Any, msg_id: str, params: Dict[str, Any]
86
+ ) -> Dict[str, Any]:
87
+ svc, creds, folder, _mark = service
88
+ return await svc.fetch_detail(creds, msg_id, folder)
89
+
90
+ async def post_emit(
91
+ self, service: Any, msg_id: str, params: Dict[str, Any]
92
+ ) -> None:
93
+ svc, creds, folder, mark = service
94
+ if not mark:
95
+ return
96
+ try:
97
+ d = svc.defaults
98
+ await svc.himalaya.flag_message(
99
+ creds, msg_id, d.get("flag"), d.get("flag_action"), folder,
100
+ )
101
+ except Exception:
102
+ pass
103
+
55
104
  async def execute(
56
105
  self,
57
106
  node_id: str,
@@ -68,7 +117,7 @@ class EmailReceiveNode(TriggerNode):
68
117
  import asyncio
69
118
  import time
70
119
  from datetime import datetime
71
- from services.email_service import get_email_service
120
+ from ._service import get_email_service
72
121
  from services.status_broadcaster import get_status_broadcaster
73
122
  from services import event_waiter
74
123
 
@@ -54,5 +54,5 @@ class EmailSendNode(ActionNode):
54
54
  @Operation("send", cost={"service": "email", "action": "send", "count": 1})
55
55
  async def send(self, ctx: NodeContext, params: EmailSendParams) -> Any:
56
56
  # Body inlined from handlers/email.py (Wave 11.D.1).
57
- from services.email_service import get_email_service
57
+ from ._service import get_email_service
58
58
  return await get_email_service().send(params.model_dump(by_alias=False))
@@ -2,11 +2,21 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  from typing import Any, Optional
6
7
 
7
8
  from pydantic import BaseModel, ConfigDict, Field
8
9
 
9
- from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
10
+ from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
11
+
12
+
13
+ # Bash chain-operators (` && ` / ` || `) are explicitly rejected by the
14
+ # Nushell parser. Detect them up-front so the LLM gets a corrective
15
+ # message ("use `;` or `try { … }`") instead of nu's
16
+ # ``shell_andand`` / ``shell_oror`` parse error two layers down.
17
+ # Surrounding spaces ensure we don't flag valid nu syntax accidentally
18
+ # (closure params `|x|` never carry spaces around the pipe pair).
19
+ _BASH_CHAIN_RE = re.compile(r"\s(\&\&|\|\|)\s")
10
20
 
11
21
 
12
22
  class ShellParams(BaseModel):
@@ -51,6 +61,19 @@ class ShellNode(ActionNode):
51
61
  from ._backend import get_backend
52
62
 
53
63
  log = get_logger(__name__)
64
+
65
+ # Pre-flight: catch the most common bash-style chain mistake before
66
+ # Nushell's parser does, so the LLM sees an actionable hint instead
67
+ # of ``nu::parser::shell_andand``. Documented in
68
+ # ``server/skills/terminal/shell-skill/SKILL.md``.
69
+ if (m := _BASH_CHAIN_RE.search(params.command)):
70
+ op = m.group(1)
71
+ replacement = "; (sequential)" if op == "&&" else "try { … } catch { … }"
72
+ raise NodeUserError(
73
+ f"Nushell does not support `{op}`. Use `{replacement}` instead. "
74
+ "See shell-skill: https://www.nushell.sh/book/control_flow.html"
75
+ )
76
+
54
77
  backend = get_backend(params.model_dump(), ctx.raw)
55
78
  # "non-blocking" here only meant the asyncio event loop isn't
56
79
  # blocked (the call is offloaded via ``to_thread``). The