machinaos 0.0.76 → 0.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (393) hide show
  1. package/README.md +143 -107
  2. package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
  3. package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
  4. package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
  5. package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
  6. package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
  7. package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
  8. package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
  9. package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
  10. package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
  11. package/client/dist/assets/index-DQ0nwhec.js +257 -0
  12. package/client/dist/assets/index-DxmbVskS.css +1 -0
  13. package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
  14. package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
  15. package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
  16. package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
  17. package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
  18. package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
  19. package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
  20. package/client/dist/index.html +37 -3
  21. package/client/index.html +28 -1
  22. package/client/package.json +44 -40
  23. package/client/src/App.tsx +2 -0
  24. package/client/src/Dashboard.tsx +157 -45
  25. package/client/src/ParameterPanel.tsx +3 -5
  26. package/client/src/adapters/nodeSpecToDescription.ts +1 -0
  27. package/client/src/assets/icons/NodeIcon.tsx +32 -0
  28. package/client/src/assets/icons/index.ts +4 -0
  29. package/client/src/assets/icons/stripe.svg +1 -0
  30. package/client/src/assets/icons/themedGlyphs.ts +404 -0
  31. package/client/src/components/AIAgentNode.tsx +77 -53
  32. package/client/src/components/GenericNode.tsx +34 -52
  33. package/client/src/components/OutputPanel.tsx +64 -147
  34. package/client/src/components/ParameterRenderer.tsx +5 -3
  35. package/client/src/components/SkillEditorModal.tsx +9 -18
  36. package/client/src/components/SquareNode.tsx +97 -115
  37. package/client/src/components/StartNode.tsx +32 -42
  38. package/client/src/components/SvgFilterDefs.tsx +54 -0
  39. package/client/src/components/TeamMonitorNode.tsx +12 -14
  40. package/client/src/components/ToolkitNode.tsx +35 -60
  41. package/client/src/components/TriggerNode.tsx +43 -77
  42. package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
  43. package/client/src/components/credentials/CredentialsModal.tsx +98 -30
  44. package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
  45. package/client/src/components/credentials/catalogueAdapter.ts +17 -1
  46. package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
  47. package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
  48. package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
  49. package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
  50. package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
  51. package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
  52. package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
  53. package/client/src/components/credentials/types.ts +12 -2
  54. package/client/src/components/credentials/useCredentialPanel.ts +43 -19
  55. package/client/src/components/icons/AIProviderIcons.tsx +16 -0
  56. package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
  57. package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
  58. package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
  59. package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
  60. package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
  61. package/client/src/components/parameterPanel/InputSection.tsx +9 -7
  62. package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
  63. package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
  64. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
  65. package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
  66. package/client/src/components/ui/AIResultModal.tsx +1 -1
  67. package/client/src/components/ui/CollapsibleSection.tsx +9 -5
  68. package/client/src/components/ui/CommandPalette.tsx +147 -0
  69. package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
  70. package/client/src/components/ui/ComponentItem.tsx +13 -7
  71. package/client/src/components/ui/ComponentPalette.tsx +24 -13
  72. package/client/src/components/ui/ConsolePanel.tsx +19 -11
  73. package/client/src/components/ui/DropCap.tsx +28 -0
  74. package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
  75. package/client/src/components/ui/InputNodesPanel.tsx +1 -1
  76. package/client/src/components/ui/Modal.tsx +38 -6
  77. package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
  78. package/client/src/components/ui/SettingsPanel.tsx +42 -13
  79. package/client/src/components/ui/StatusBar.tsx +108 -0
  80. package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
  81. package/client/src/components/ui/TopToolbar.tsx +42 -25
  82. package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
  83. package/client/src/components/ui/action-button.tsx +40 -15
  84. package/client/src/components/ui/button.tsx +24 -1
  85. package/client/src/components/ui/dropdown-menu.tsx +24 -2
  86. package/client/src/components/ui/input.tsx +19 -2
  87. package/client/src/components/ui/select.tsx +15 -0
  88. package/client/src/components/ui/textarea.tsx +15 -2
  89. package/client/src/contexts/AuthContext.tsx +148 -109
  90. package/client/src/contexts/ThemeContext.tsx +93 -17
  91. package/client/src/contexts/WebSocketContext.tsx +373 -206
  92. package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
  93. package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
  94. package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
  95. package/client/src/hooks/useAppTheme.ts +209 -7
  96. package/client/src/hooks/useAutoSkillEdges.ts +7 -2
  97. package/client/src/hooks/useCatalogueQuery.ts +67 -1
  98. package/client/src/hooks/useDragVariable.ts +1 -1
  99. package/client/src/hooks/useNodeAllowlist.ts +115 -8
  100. package/client/src/hooks/useOnboarding.ts +20 -8
  101. package/client/src/hooks/useParameterPanel.ts +2 -1
  102. package/client/src/hooks/useReactFlowNodes.ts +2 -1
  103. package/client/src/hooks/useSound.ts +185 -0
  104. package/client/src/hooks/useWorkflowManagement.ts +6 -8
  105. package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
  106. package/client/src/index.css +65 -3
  107. package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
  108. package/client/src/lib/aiModelProviders.ts +8 -0
  109. package/client/src/lib/connectionConfig.ts +107 -0
  110. package/client/src/lib/queryPersist.ts +13 -5
  111. package/client/src/lib/sound.ts +393 -0
  112. package/client/src/main.tsx +20 -0
  113. package/client/src/store/useAppStore.ts +26 -0
  114. package/client/src/styles/canvasAnimations.ts +37 -36
  115. package/client/src/styles/theme.ts +36 -20
  116. package/client/src/test/setup.ts +1 -0
  117. package/client/src/themes/atomic.css +253 -0
  118. package/client/src/themes/base.css +373 -0
  119. package/client/src/themes/cyber.css +890 -0
  120. package/client/src/themes/dark.css +70 -0
  121. package/client/src/themes/edo.css +246 -0
  122. package/client/src/themes/greek.css +293 -0
  123. package/client/src/themes/light.css +78 -0
  124. package/client/src/themes/plague.css +253 -0
  125. package/client/src/themes/renaissance.css +727 -0
  126. package/client/src/themes/rot.css +249 -0
  127. package/client/src/themes/steampunk.css +272 -0
  128. package/client/src/themes/surveillance.css +289 -0
  129. package/client/src/themes/wasteland.css +250 -0
  130. package/client/src/types/INodeProperties.ts +5 -0
  131. package/client/src/types/NodeTypes.ts +11 -1
  132. package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
  133. package/client/src/types/cloudEvents.ts +78 -0
  134. package/client/src/vite-env.d.ts +7 -0
  135. package/client/tsconfig.json +1 -1
  136. package/client/vite.config.js +62 -2
  137. package/install.ps1 +1 -1
  138. package/install.sh +1 -1
  139. package/machina/commands/build.py +51 -7
  140. package/machina/pyproject.toml +4 -0
  141. package/machina/supervisor.py +12 -2
  142. package/machina/tree.py +71 -21
  143. package/package.json +4 -4
  144. package/scripts/install.js +16 -1
  145. package/server/config/ai_cli_providers.json +54 -0
  146. package/server/config/credential_providers.json +109 -2
  147. package/server/config/llm_defaults.json +24 -0
  148. package/server/config/model_registry.json +338 -499
  149. package/server/config/node_allowlist.json +16 -1
  150. package/server/config/pricing.json +8 -0
  151. package/server/constants.py +38 -15
  152. package/server/core/container.py +2 -2
  153. package/server/core/credentials_database.py +35 -2
  154. package/server/core/logging.py +4 -3
  155. package/server/main.py +99 -13
  156. package/server/models/node_metadata.py +1 -0
  157. package/server/nodejs/package.json +8 -6
  158. package/server/nodejs/src/index.ts +22 -5
  159. package/server/nodes/README.md +31 -4
  160. package/server/nodes/agent/_inline.py +2 -0
  161. package/server/nodes/agent/_specialized.py +6 -3
  162. package/server/nodes/agent/ai_agent.py +13 -3
  163. package/server/nodes/agent/chat_agent.py +6 -3
  164. package/server/nodes/agent/claude_code_agent.py +287 -75
  165. package/server/nodes/agent/codex_agent.py +239 -0
  166. package/server/nodes/agent/deep_agent.py +3 -3
  167. package/server/nodes/agent/rlm_agent.py +3 -3
  168. package/server/nodes/android/__init__.py +31 -1
  169. package/server/nodes/android/_base.py +9 -5
  170. package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
  171. package/server/nodes/android/_handlers.py +154 -0
  172. package/server/nodes/android/_option_loaders.py +44 -0
  173. package/server/nodes/android/_refresh.py +127 -0
  174. package/server/{services/android → nodes/android/_relay}/client.py +4 -4
  175. package/server/{routers/android.py → nodes/android/_router.py} +27 -8
  176. package/server/nodes/browser/browser.py +2 -2
  177. package/server/nodes/code/_base.py +6 -2
  178. package/server/nodes/code/_claude_code.py +134 -0
  179. package/server/nodes/document/embedding_generator.py +3 -3
  180. package/server/nodes/document/http_scraper.py +3 -3
  181. package/server/nodes/document/vector_store.py +5 -5
  182. package/server/nodes/email/__init__.py +11 -1
  183. package/server/nodes/email/_filters.py +21 -0
  184. package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
  185. package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
  186. package/server/nodes/email/email_read.py +1 -1
  187. package/server/nodes/email/email_receive.py +54 -5
  188. package/server/nodes/email/email_send.py +1 -1
  189. package/server/nodes/filesystem/shell.py +24 -1
  190. package/server/nodes/google/__init__.py +55 -1
  191. package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
  192. package/server/nodes/google/_base.py +2 -2
  193. package/server/nodes/google/_credentials.py +5 -5
  194. package/server/nodes/google/_filters.py +25 -0
  195. package/server/nodes/google/_handlers.py +57 -0
  196. package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
  197. package/server/nodes/google/_option_loaders.py +107 -0
  198. package/server/nodes/google/_refresh.py +66 -0
  199. package/server/nodes/google/_router.py +131 -0
  200. package/server/nodes/google/gmail_receive.py +41 -4
  201. package/server/nodes/groups.py +1 -0
  202. package/server/nodes/location/_credentials.py +45 -1
  203. package/server/{services/maps.py → nodes/location/_service.py} +18 -3
  204. package/server/nodes/location/gmaps_create.py +4 -4
  205. package/server/nodes/location/gmaps_locations.py +4 -4
  206. package/server/nodes/location/gmaps_nearby_places.py +4 -4
  207. package/server/nodes/model/_base.py +8 -3
  208. package/server/nodes/model/_credentials.py +96 -8
  209. package/server/nodes/model/_local_validator.py +345 -0
  210. package/server/nodes/model/lmstudio_chat_model.py +23 -0
  211. package/server/nodes/model/ollama_chat_model.py +25 -0
  212. package/server/nodes/proxy/_usage.py +2 -2
  213. package/server/nodes/proxy/proxy_config.py +14 -14
  214. package/server/nodes/proxy/proxy_request.py +4 -4
  215. package/server/nodes/scraper/_credentials.py +29 -1
  216. package/server/nodes/scraper/apify_actor.py +9 -9
  217. package/server/nodes/scraper/crawlee_scraper.py +5 -5
  218. package/server/nodes/search/brave_search.py +4 -0
  219. package/server/nodes/search/perplexity_search.py +9 -0
  220. package/server/nodes/search/serper_search.py +3 -0
  221. package/server/nodes/skill/simple_memory.py +12 -0
  222. package/server/nodes/social/_base.py +2 -2
  223. package/server/nodes/stripe/__init__.py +46 -0
  224. package/server/nodes/stripe/_credentials.py +33 -0
  225. package/server/nodes/stripe/_handlers.py +270 -0
  226. package/server/nodes/stripe/_install.py +127 -0
  227. package/server/nodes/stripe/_source.py +174 -0
  228. package/server/nodes/stripe/stripe_action.py +81 -0
  229. package/server/nodes/stripe/stripe_receive.py +92 -0
  230. package/server/nodes/telegram/_credentials.py +52 -1
  231. package/server/nodes/telegram/_handlers.py +19 -18
  232. package/server/nodes/telegram/_service.py +134 -32
  233. package/server/nodes/telegram/telegram_send.py +5 -6
  234. package/server/nodes/text/file_handler.py +2 -2
  235. package/server/nodes/text/text_generator.py +2 -2
  236. package/server/nodes/tool/agent_builder.py +630 -0
  237. package/server/nodes/tool/task_manager.py +144 -2
  238. package/server/nodes/twitter/__init__.py +38 -1
  239. package/server/nodes/twitter/_base.py +7 -7
  240. package/server/nodes/twitter/_credentials.py +1 -1
  241. package/server/nodes/twitter/_filters.py +37 -0
  242. package/server/nodes/twitter/_handlers.py +77 -0
  243. package/server/nodes/twitter/_oauth.py +124 -0
  244. package/server/nodes/twitter/_refresh.py +78 -0
  245. package/server/nodes/twitter/_router.py +29 -0
  246. package/server/nodes/twitter/twitter_receive.py +4 -0
  247. package/server/nodes/visuals.json +64 -19
  248. package/server/nodes/whatsapp/__init__.py +45 -5
  249. package/server/nodes/whatsapp/_base.py +3 -3
  250. package/server/nodes/whatsapp/_filters.py +137 -0
  251. package/server/nodes/whatsapp/_handlers.py +167 -0
  252. package/server/nodes/whatsapp/_option_loaders.py +68 -0
  253. package/server/nodes/whatsapp/_refresh.py +62 -0
  254. package/server/nodes/whatsapp/_runtime.py +1 -1
  255. package/server/pyproject.toml +29 -7
  256. package/server/routers/schemas.py +2 -2
  257. package/server/routers/webhook.py +26 -9
  258. package/server/routers/websocket.py +149 -810
  259. package/server/services/ai.py +89 -8
  260. package/server/services/auth.py +220 -43
  261. package/server/services/claude_oauth.py +126 -100
  262. package/server/services/cli_agent/__init__.py +78 -0
  263. package/server/services/cli_agent/_handlers.py +237 -0
  264. package/server/services/cli_agent/config.py +112 -0
  265. package/server/services/cli_agent/factory.py +48 -0
  266. package/server/services/cli_agent/lockfile.py +141 -0
  267. package/server/services/cli_agent/mcp_server.py +482 -0
  268. package/server/services/cli_agent/protocol.py +173 -0
  269. package/server/services/cli_agent/providers/__init__.py +9 -0
  270. package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
  271. package/server/services/cli_agent/providers/google_gemini.py +80 -0
  272. package/server/services/cli_agent/providers/openai_codex.py +310 -0
  273. package/server/services/cli_agent/service.py +607 -0
  274. package/server/services/cli_agent/session.py +618 -0
  275. package/server/services/cli_agent/types.py +227 -0
  276. package/server/services/cli_agent/workflow_tools.py +233 -0
  277. package/server/services/credential_registry.py +26 -1
  278. package/server/services/deployment/manager.py +26 -145
  279. package/server/services/deployment/poll_registry.py +59 -0
  280. package/server/services/event_waiter.py +76 -246
  281. package/server/services/events/__init__.py +54 -0
  282. package/server/services/events/cli.py +78 -0
  283. package/server/services/events/daemon.py +163 -0
  284. package/server/services/events/envelope.py +281 -0
  285. package/server/services/events/lifecycle.py +99 -0
  286. package/server/services/events/oauth_lifecycle.py +534 -0
  287. package/server/services/events/polling.py +60 -0
  288. package/server/services/events/push.py +36 -0
  289. package/server/services/events/source.py +63 -0
  290. package/server/services/events/triggers.py +118 -0
  291. package/server/services/events/verifiers/__init__.py +25 -0
  292. package/server/services/events/verifiers/base.py +28 -0
  293. package/server/services/events/verifiers/github.py +25 -0
  294. package/server/services/events/verifiers/hmac_basic.py +32 -0
  295. package/server/services/events/verifiers/standard_webhooks.py +47 -0
  296. package/server/services/events/verifiers/stripe.py +42 -0
  297. package/server/services/events/webhook.py +105 -0
  298. package/server/services/handlers/tools.py +28 -186
  299. package/server/services/llm/config.py +7 -0
  300. package/server/services/llm/factory.py +8 -2
  301. package/server/services/memory/__init__.py +52 -0
  302. package/server/services/memory/jsonl.py +80 -0
  303. package/server/services/memory/markdown.py +65 -0
  304. package/server/services/memory/state.py +112 -0
  305. package/server/services/memory/vector_store.py +40 -0
  306. package/server/services/model_registry.py +76 -0
  307. package/server/services/node_allowlist.py +71 -15
  308. package/server/services/node_executor.py +2 -2
  309. package/server/services/node_output_schemas.py +21 -10
  310. package/server/services/node_spec.py +1 -1
  311. package/server/services/oauth_utils.py +1 -1
  312. package/server/services/plugin/__init__.py +2 -0
  313. package/server/services/plugin/base.py +44 -2
  314. package/server/services/plugin/credential.py +288 -1
  315. package/server/services/plugin/deps.py +105 -0
  316. package/server/services/plugin/edge_walker.py +12 -4
  317. package/server/services/plugin/oauth.py +381 -0
  318. package/server/services/plugin/polling.py +247 -0
  319. package/server/services/plugin/registry.py +145 -0
  320. package/server/services/plugin/singleton.py +65 -0
  321. package/server/services/plugin/ws.py +81 -0
  322. package/server/services/process_service.py +31 -2
  323. package/server/services/status_broadcaster.py +155 -238
  324. package/server/services/temporal/workflow.py +7 -7
  325. package/server/services/workflow.py +21 -3
  326. package/server/services/ws_handler_registry.py +111 -28
  327. package/server/skills/GUIDE.md +16 -1
  328. package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
  329. package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
  330. package/server/tests/credentials/test_auth_service.py +16 -9
  331. package/server/tests/credentials/test_credential_broadcasts.py +219 -0
  332. package/server/tests/credentials/test_google_oauth.py +6 -6
  333. package/server/tests/credentials/test_oauth_utils.py +1 -1
  334. package/server/tests/credentials/test_twitter_oauth.py +2 -2
  335. package/server/tests/credentials/test_websocket_handlers.py +44 -20
  336. package/server/tests/llm/test_factory.py +1 -0
  337. package/server/tests/llm/test_wiring.py +5 -1
  338. package/server/tests/nodes/_compat.py +24 -24
  339. package/server/tests/nodes/test_agent_builder.py +439 -0
  340. package/server/tests/nodes/test_ai_tools.py +18 -14
  341. package/server/tests/nodes/test_code_fs_process.py +17 -8
  342. package/server/tests/nodes/test_email.py +10 -9
  343. package/server/tests/nodes/test_google_workspace.py +2 -2
  344. package/server/tests/nodes/test_specialized_agents.py +100 -53
  345. package/server/tests/nodes/test_stripe_plugin.py +293 -0
  346. package/server/tests/nodes/test_telegram_social.py +4 -4
  347. package/server/tests/nodes/test_twitter.py +1 -1
  348. package/server/tests/nodes/test_web_automation.py +2 -2
  349. package/server/tests/nodes/test_whatsapp.py +9 -9
  350. package/server/tests/services/cli_agent/__init__.py +0 -0
  351. package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
  352. package/server/tests/services/cli_agent/test_providers.py +358 -0
  353. package/server/tests/services/cli_agent/test_service.py +298 -0
  354. package/server/tests/services/memory/__init__.py +0 -0
  355. package/server/tests/services/memory/test_jsonl.py +188 -0
  356. package/server/tests/services/test_events.py +333 -0
  357. package/server/tests/test_node_spec.py +56 -16
  358. package/server/tests/test_plugin_helpers.py +116 -0
  359. package/server/tests/test_plugin_self_containment.py +486 -0
  360. package/server/tests/test_status_broadcasts.py +425 -0
  361. package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
  362. package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
  363. package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
  364. package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
  365. package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
  366. package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
  367. package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
  368. package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
  369. package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
  370. package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
  371. package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
  372. package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
  373. package/client/dist/assets/index-D9tZfgvi.js +0 -363
  374. package/client/dist/assets/index-al7snTkG.css +0 -1
  375. package/client/src/components/credentials/providers.tsx +0 -177
  376. package/server/routers/google.py +0 -277
  377. package/server/routers/maps.py +0 -142
  378. package/server/routers/twitter.py +0 -365
  379. package/server/services/claude_code_service.py +0 -106
  380. package/server/services/memory.py +0 -159
  381. package/server/services/node_option_loaders/__init__.py +0 -77
  382. package/server/services/node_option_loaders/android_loaders.py +0 -55
  383. package/server/services/node_option_loaders/google_loaders.py +0 -97
  384. package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
  385. package/server/services/twitter_oauth.py +0 -411
  386. package/server/services/websocket_client.py +0 -29
  387. /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
  388. /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
  389. /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
  390. /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
  391. /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
  392. /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
  393. /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
@@ -0,0 +1,345 @@
1
+ """Local-LLM credential validator (Ollama, LM Studio).
2
+
3
+ Lives next to the chat-model plugins so all per-provider behaviour for
4
+ the local servers stays in `nodes/model/`. Registered into
5
+ `routers.websocket._SPECIAL_PROVIDER_VALIDATORS` from the same place
6
+ the cloud-provider mapping is declared — same shim shape as apify and
7
+ google_maps, the function body just lives here instead.
8
+
9
+ The frontend reuses the standard ``validate_api_key`` WebSocket message
10
+ for these providers; the ``api_key`` field carries the user's Base URL,
11
+ not a secret. We:
12
+
13
+ 1. Save the URL under ``{provider}_proxy`` — the existing Ollama-style
14
+ auth-delegation key that the runtime path in ``services/ai.py``
15
+ already reads.
16
+ 2. Probe the user's server via the *official* SDK (``ollama`` for
17
+ Ollama, ``lmstudio`` for LM Studio) — list installed models and
18
+ their actually-loaded ``context_length``. SDK-driven introspection
19
+ beats hand-rolled httpx against ``/api/show`` / ``/api/v0/models``
20
+ because the SDK ships the typed result struct (``ShowResponse``,
21
+ ``LlmInstanceInfo``) and stays compatible with version drift
22
+ upstream.
23
+ 3. Store the placeholder api_key + discovered model list + per-model
24
+ ``context_length`` under the provider id. ``model_registry`` reads
25
+ the per-model context at runtime so a chat call never assumes a
26
+ bogus 32K when the user has a 4K-loaded model.
27
+ 4. Return ``valid=True`` only when at least one model was found, so a
28
+ misconfigured URL surfaces as a clear "no models" message instead
29
+ of a silent success.
30
+
31
+ Connection failures (server down, wrong port, auth refused, timeout)
32
+ are caught off the SDK's own exceptions and mapped to specific
33
+ user-facing toasts — operators see "is the server running?" for
34
+ connect-refused, "wrong path" for 404, etc. The catalogue ``stored``
35
+ flag flips to False on every failure path via the broadcaster, so a
36
+ failed re-probe clears the previously-green palette dot.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import time
42
+ from typing import Any, Dict, List, Optional, Tuple
43
+
44
+ import httpx
45
+ import lmstudio
46
+ import ollama
47
+ from core.logging import get_logger
48
+ from services.plugin.deps import get_auth_service
49
+ from services.status_broadcaster import get_status_broadcaster
50
+
51
+ logger = get_logger(__name__)
52
+
53
+
54
+ def _classify_http_error(provider: str, base_url: str, exc: BaseException) -> Tuple[str, str]:
55
+ """Map an httpx / generic exception to (log_summary, user_message).
56
+
57
+ Both SDKs (ollama-python, lmstudio) raise httpx errors underneath.
58
+ Mapping these directly gives clean operator logs ("connect-refused"
59
+ vs "404 vs "timeout") + actionable user toasts without depending
60
+ on the openai SDK exception hierarchy.
61
+ """
62
+ display = "LM Studio" if provider == "lmstudio" else provider.capitalize()
63
+
64
+ if isinstance(exc, httpx.TimeoutException):
65
+ return ("timeout", f"Request to {base_url} timed out — server may be overloaded or unreachable")
66
+
67
+ if isinstance(exc, httpx.ConnectError):
68
+ return ("connect-refused",
69
+ f"Could not reach {display} at {base_url}. Is the server running?")
70
+
71
+ if isinstance(exc, httpx.HTTPStatusError):
72
+ status = exc.response.status_code
73
+ if status in (401, 403):
74
+ return (f"HTTP {status}", f"{display} rejected the request — server requires auth.")
75
+ if status == 404:
76
+ url = base_url.rstrip("/")
77
+ if url.endswith("/v1"):
78
+ hint = f"the {display} server is reachable but expected endpoints aren't exposed — check the server version."
79
+ else:
80
+ hint = f"the URL likely needs to end with `/v1` (e.g. {url}/v1)."
81
+ return (f"HTTP {status}", f"{display} returned 404 — {hint}")
82
+ if status == 429:
83
+ return (f"HTTP {status}", f"{display} rate-limited the request — try again shortly.")
84
+ return (f"HTTP {status}", f"{display} returned HTTP {status} — check the server logs.")
85
+
86
+ if isinstance(exc, httpx.RequestError):
87
+ return ("network-error", f"Network error reaching {display} at {base_url}: {exc.__class__.__name__}")
88
+
89
+ # Unknown exception type: surface the class name + message so the
90
+ # operator can tell what they're looking at without a stacktrace.
91
+ return (type(exc).__name__, f"Could not reach {display}: {exc}")
92
+
93
+
94
+ async def _fail(
95
+ provider: str,
96
+ message: str,
97
+ *,
98
+ has_key: bool = False,
99
+ ) -> Dict[str, Any]:
100
+ """Common rejection path: broadcast invalid status + return envelope.
101
+
102
+ `has_key=True` is used after the URL has been persisted so the
103
+ palette doesn't go straight to the "unconfigured" gray dot — the
104
+ user's URL is on file, it just can't reach a working server right
105
+ now. `has_key=False` is used when we abort before persisting (only
106
+ the "Base URL required" early-exit today).
107
+ """
108
+ await get_status_broadcaster().update_api_key_status(
109
+ provider=provider, valid=False, message=message,
110
+ has_key=has_key, models=[],
111
+ )
112
+ return {
113
+ "provider": provider, "success": True, "valid": False,
114
+ "message": message, "models": [], "timestamp": time.time(),
115
+ }
116
+
117
+
118
+ def _strip_v1_path(base_url: str) -> str:
119
+ """Return ``base_url`` with a trailing ``/v1`` segment stripped.
120
+
121
+ The user's stored URL is the OpenAI-compatible base
122
+ (``http://host:port/v1``). Both Ollama's REST API and LM Studio's
123
+ SDK want the host:port without the OpenAI-compat suffix.
124
+ """
125
+ u = base_url.rstrip("/")
126
+ if u.endswith("/v1"):
127
+ return u[: -len("/v1")]
128
+ return u
129
+
130
+
131
+ async def _fetch_ollama_models(base_url: str) -> List[Dict[str, Any]]:
132
+ """List currently-loaded Ollama models with their actual params.
133
+
134
+ Uses ``ollama.AsyncClient.ps()`` — the official "list running models"
135
+ endpoint. Returns a typed ``ProcessResponse`` whose ``models[]``
136
+ entries already carry every field we need as proper Pydantic
137
+ attributes (no dict-key hunting, no Modelfile parameters parsing):
138
+
139
+ - ``model`` — canonical name passed to ``/v1/chat/completions``
140
+ - ``context_length``— live server-side n_ctx (this is the value
141
+ that produces the 400 overflow when a
142
+ prompt exceeds it)
143
+ - ``details`` — typed ``ModelDetails`` (family, parameter_size,
144
+ quantization_level)
145
+
146
+ If the user has models *pulled* but none currently loaded, ``ps()``
147
+ returns an empty list — same semantics as LM Studio's
148
+ ``list_loaded()``. The validator surfaces this as the "load a model
149
+ and click Fetch again" message, which is already accurate.
150
+ """
151
+ host = _strip_v1_path(base_url)
152
+ client = ollama.AsyncClient(host=host, timeout=10.0)
153
+ try:
154
+ running = await client.ps()
155
+ finally:
156
+ try:
157
+ await client._client.aclose()
158
+ except Exception:
159
+ pass
160
+
161
+ out: List[Dict[str, Any]] = []
162
+ for m in running.models or []:
163
+ mid = m.model or m.name
164
+ if not mid:
165
+ continue
166
+ entry: Dict[str, Any] = {"id": mid}
167
+ if m.context_length:
168
+ entry["context_length"] = int(m.context_length)
169
+ if m.details:
170
+ if m.details.family:
171
+ entry["architecture"] = m.details.family
172
+ if m.details.parameter_size:
173
+ entry["param_size"] = m.details.parameter_size
174
+ if m.details.quantization_level:
175
+ entry["quantization"] = m.details.quantization_level
176
+ if m.details.format:
177
+ entry["format"] = m.details.format
178
+ out.append(entry)
179
+ return out
180
+
181
+
182
+ async def _fetch_lmstudio_models(base_url: str) -> List[Dict[str, Any]]:
183
+ """List currently-loaded LM Studio models with their actual params.
184
+
185
+ Uses ``lmstudio.AsyncClient.llm.list_loaded()`` — each handle's
186
+ ``get_info()`` returns a typed ``LlmInstanceInfo``. We read only
187
+ SDK-typed fields (``context_length``, ``max_context_length``,
188
+ ``vision``, ``trained_for_tool_use``, ``architecture``,
189
+ ``params_string``); no string parsing.
190
+
191
+ LM Studio's SDK takes ``api_host`` as ``host:port`` (no scheme, no
192
+ path), so the user's stored ``http://host:port/v1`` is stripped
193
+ before construction.
194
+ """
195
+ host = _strip_v1_path(base_url)
196
+ api_host = host.split("://", 1)[-1]
197
+
198
+ client = lmstudio.AsyncClient(api_host=api_host)
199
+ out: List[Dict[str, Any]] = []
200
+ async with client:
201
+ loaded = await client.llm.list_loaded()
202
+ for handle in loaded:
203
+ try:
204
+ info = await handle.get_info()
205
+ except Exception as e:
206
+ logger.info("[lmstudio] get_info skipped for %s: %s",
207
+ getattr(handle, "identifier", "<unknown>"), type(e).__name__)
208
+ continue
209
+ mid = info.identifier or info.model_key
210
+ if not mid:
211
+ continue
212
+ entry: Dict[str, Any] = {"id": mid}
213
+ if info.context_length:
214
+ entry["context_length"] = int(info.context_length)
215
+ if info.max_context_length:
216
+ entry["max_context_length"] = int(info.max_context_length)
217
+ if info.vision is not None:
218
+ entry["vision"] = bool(info.vision)
219
+ if info.trained_for_tool_use is not None:
220
+ entry["supports_tools"] = bool(info.trained_for_tool_use)
221
+ if info.architecture:
222
+ entry["architecture"] = info.architecture
223
+ if info.params_string:
224
+ entry["param_size"] = info.params_string
225
+ if info.format:
226
+ entry["format"] = info.format
227
+ out.append(entry)
228
+ return out
229
+
230
+
231
+ async def _fetch_local_models(provider: str, base_url: str) -> List[Dict[str, Any]]:
232
+ """Dispatch to the per-provider SDK probe."""
233
+ if provider == "ollama":
234
+ return await _fetch_ollama_models(base_url)
235
+ if provider == "lmstudio":
236
+ return await _fetch_lmstudio_models(base_url)
237
+ raise ValueError(f"Unsupported local provider: {provider}")
238
+
239
+
240
+ async def validate_local_llm(data: Dict[str, Any]) -> Dict[str, Any]:
241
+ """Validator for ollama / lmstudio. Returns the standard response envelope.
242
+
243
+ Called from :meth:`nodes.model._credentials._LocalLLM.validate` (the
244
+ ``Credential.validate`` hook ``handle_validate_api_key`` dispatches
245
+ to). All side effects (URL persistence, status broadcasts, model
246
+ registry registration) go through the ``StatusBroadcaster`` /
247
+ ``AuthService`` singletons — no per-request WebSocket reference is
248
+ needed.
249
+ """
250
+ provider = data["provider"].lower()
251
+ base_url = data.get("api_key", "").strip()
252
+ session_id = data.get("session_id", "default")
253
+
254
+ if not base_url:
255
+ return {"success": False, "valid": False, "error": "Base URL required"}
256
+
257
+ auth_service = get_auth_service()
258
+
259
+ # Persist the URL first so the runtime path (services/ai.py) can
260
+ # read it via the existing {provider}_proxy lookup even before the
261
+ # probe succeeds. The URL stays persisted on probe failure so the
262
+ # user can re-click "Fetch" without re-entering it; the failure
263
+ # branch broadcasts has_key=True so the palette dot reflects "URL
264
+ # on file but currently unreachable" rather than "unconfigured".
265
+ await auth_service.store_api_key(
266
+ provider=f"{provider}_proxy",
267
+ api_key=base_url,
268
+ models=[],
269
+ session_id=session_id,
270
+ )
271
+
272
+ try:
273
+ entries = await _fetch_local_models(provider, base_url)
274
+ except (httpx.HTTPError, lmstudio.LMStudioError) as e:
275
+ log_summary, user_msg = _classify_http_error(provider, base_url, e)
276
+ logger.warning("[%s] model probe failed (%s) at %s", provider, log_summary, base_url)
277
+ return await _fail(provider, user_msg, has_key=True)
278
+ except Exception as e:
279
+ log_summary, user_msg = _classify_http_error(provider, base_url, e)
280
+ logger.warning("[%s] model probe unexpected error (%s) at %s: %s",
281
+ provider, log_summary, base_url, e)
282
+ return await _fail(provider, user_msg, has_key=True)
283
+
284
+ if not entries:
285
+ # Server reachable, responded with empty model list. Different
286
+ # failure mode from the connect-error branches above — keep the
287
+ # original "load a model" hint here since it's now accurate.
288
+ display = "LM Studio" if provider == "lmstudio" else provider.capitalize()
289
+ message = f"Connected to {display} at {base_url}, but no models are loaded. Load a model in {display} and click Fetch again."
290
+ logger.info("[%s] reachable at %s but returned 0 models", provider, base_url)
291
+ return await _fail(provider, message, has_key=True)
292
+
293
+ # Pivot the SDK-probed entries into the storage shape: a parallel
294
+ # ``models`` list (for the legacy readers) plus a ``model_params``
295
+ # dict carrying every typed field the SDK exposed
296
+ # (``context_length``, ``vision``, ``supports_tools``,
297
+ # ``architecture``, ``param_size``, ``quantization``,
298
+ # ``max_context_length``). ``model_registry.register_local_model``
299
+ # consumes these directly to populate ``ModelInfo``.
300
+ models = [e["id"] for e in entries]
301
+ model_params: Dict[str, Dict[str, Any]] = {
302
+ e["id"]: {k: v for k, v in e.items() if k != "id"}
303
+ for e in entries
304
+ if any(k != "id" for k in e)
305
+ }
306
+
307
+ # Store placeholder api_key + the real model list + per-model params.
308
+ # The placeholder ("ollama") is the documented value the OpenAI-style
309
+ # auth-delegation path expects when no real key is needed; it never
310
+ # leaves the process because the runtime SDK rewrites it when
311
+ # proxy_url is set.
312
+ await auth_service.store_api_key(
313
+ provider=provider,
314
+ api_key="ollama",
315
+ models=models,
316
+ session_id=session_id,
317
+ model_params=model_params,
318
+ )
319
+
320
+ # Register each model in the in-memory model registry so the sync
321
+ # ``get_context_length`` / ``get_max_output_tokens`` lookups pick up
322
+ # the real loaded n_ctx without re-querying the DB on every chat
323
+ # call. Also keeps the runtime path branchless — local and cloud
324
+ # models share the same ``provider/model_id`` registry key.
325
+ from services.model_registry import get_model_registry
326
+ registry = get_model_registry()
327
+ for mid, params in model_params.items():
328
+ registry.register_local_model(provider, mid, params)
329
+
330
+ await get_status_broadcaster().update_api_key_status(
331
+ provider=provider, valid=True,
332
+ message=f"{len(models)} model(s) discovered at {base_url}",
333
+ has_key=True, models=models,
334
+ )
335
+ ctx_summary = ", ".join(
336
+ f"{mid}={p['context_length']}"
337
+ for mid, p in model_params.items()
338
+ ) or "no per-model context info"
339
+ logger.info("[%s] discovered %d model(s) at %s (%s)",
340
+ provider, len(models), base_url, ctx_summary)
341
+ return {
342
+ "provider": provider, "success": True, "valid": True,
343
+ "models": models, "message": f"Connected to {provider} at {base_url}",
344
+ "timestamp": time.time(),
345
+ }
@@ -0,0 +1,23 @@
1
+ """LM Studio chat-model plugin.
2
+
3
+ Auto-registers via BaseNode.__init_subclass__. LM Studio exposes a pure
4
+ OpenAI-compatible endpoint at ``http://localhost:1234/v1`` by default,
5
+ so the existing OpenAI-compatible fallback in
6
+ ``services/llm/factory.py`` routes it through ``OpenAIProvider`` with
7
+ ``base_url`` from ``llm_defaults.json`` — same path as deepseek/kimi/
8
+ mistral. The user's custom server URL is stored as the
9
+ ``lmstudio_proxy`` credential.
10
+ """
11
+
12
+ from ._base import ChatModelBase
13
+
14
+ from ._credentials import LMStudioCredential
15
+
16
+
17
+ class LMStudioChatModelNode(ChatModelBase):
18
+ type = "lmstudioChatModel"
19
+ display_name = "LM Studio"
20
+ subtitle = "Chat Model"
21
+ group = ("model",)
22
+ description = "Run local LLMs via LM Studio's OpenAI-compatible server"
23
+ credentials = (LMStudioCredential,)
@@ -0,0 +1,25 @@
1
+ """Ollama chat-model plugin.
2
+
3
+ Auto-registers via BaseNode.__init_subclass__. Same shape as
4
+ ``mistral_chat_model.py`` — a single ``ChatModelBase`` subclass plus a
5
+ credential class. Routing happens through the existing OpenAI-compatible
6
+ fallback in ``services/llm/factory.py``: Ollama serves an OpenAI-shaped
7
+ ``/v1`` endpoint, so the factory hands it to ``OpenAIProvider`` with
8
+ ``base_url`` from ``llm_defaults.json``. The user's custom server URL
9
+ (if not localhost) is stored as the ``ollama_proxy`` credential and
10
+ flows through the same ``proxy_url`` parameter cloud providers already
11
+ use for Ollama-style auth delegation.
12
+ """
13
+
14
+ from ._base import ChatModelBase
15
+
16
+ from ._credentials import OllamaCredential
17
+
18
+
19
+ class OllamaChatModelNode(ChatModelBase):
20
+ type = "ollamaChatModel"
21
+ display_name = "Ollama"
22
+ subtitle = "Chat Model"
23
+ group = ("model",)
24
+ description = "Run local LLMs via Ollama (llama, mistral, qwen, deepseek-r1, ...)"
25
+ credentials = (OllamaCredential,)
@@ -23,7 +23,7 @@ async def track_proxy_usage(
23
23
  workflow_id: Optional[str] = None,
24
24
  session_id: str = "default",
25
25
  ) -> Dict[str, float]:
26
- from core.container import container
26
+ from services.plugin.deps import get_database
27
27
  from services.pricing import get_pricing_service
28
28
 
29
29
  pricing = get_pricing_service()
@@ -34,7 +34,7 @@ async def track_proxy_usage(
34
34
  gb = bytes_transferred / (1024 ** 3)
35
35
  total_cost = round(gb * cost_per_gb, 8)
36
36
 
37
- db = container.database()
37
+ db = get_database()
38
38
  await db.save_api_usage_metric({
39
39
  "session_id": session_id,
40
40
  "node_id": node_id,
@@ -15,7 +15,7 @@ import httpx
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
 
@@ -162,7 +162,7 @@ async def _list_routing_rules(proxy_svc) -> Dict[str, Any]:
162
162
 
163
163
 
164
164
  async def _add_provider(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
165
- from core.container import container
165
+ from services.plugin.deps import get_database
166
166
 
167
167
  name = p.get("name", "")
168
168
  if not name:
@@ -176,7 +176,7 @@ async def _add_provider(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
176
176
  except json.JSONDecodeError:
177
177
  return {"success": False, "error": f"Invalid url_template JSON: {url_template_raw}"}
178
178
 
179
- db = container.database()
179
+ db = get_database()
180
180
  await db.save_proxy_provider({
181
181
  "name": name,
182
182
  "enabled": p.get("enabled", True),
@@ -192,12 +192,12 @@ async def _add_provider(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
192
192
 
193
193
 
194
194
  async def _update_provider(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
195
- from core.container import container
195
+ from services.plugin.deps import get_database
196
196
 
197
197
  name = p.get("name", "")
198
198
  if not name:
199
199
  return {"success": False, "error": "Provider name is required"}
200
- db = container.database()
200
+ db = get_database()
201
201
  existing = await db.get_proxy_provider(name)
202
202
  if not existing:
203
203
  return {"success": False, "error": f"Provider '{name}' not found"}
@@ -225,12 +225,12 @@ async def _update_provider(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
225
225
 
226
226
 
227
227
  async def _remove_provider(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
228
- from core.container import container
228
+ from services.plugin.deps import get_database
229
229
 
230
230
  name = p.get("name", "")
231
231
  if not name:
232
232
  return {"success": False, "error": "Provider name is required"}
233
- db = container.database()
233
+ db = get_database()
234
234
  await db.delete_proxy_provider(name)
235
235
  if proxy_svc:
236
236
  await proxy_svc.reload_providers()
@@ -238,7 +238,7 @@ async def _remove_provider(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
238
238
 
239
239
 
240
240
  async def _set_credentials(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
241
- from core.container import container
241
+ from services.plugin.deps import get_auth_service
242
242
 
243
243
  name = p.get("name", "")
244
244
  if not name:
@@ -247,7 +247,7 @@ async def _set_credentials(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
247
247
  if not username or not password:
248
248
  return {"success": False, "error": "Username and password are required"}
249
249
 
250
- auth_svc = container.auth_service()
250
+ auth_svc = get_auth_service()
251
251
  await auth_svc.store_api_key(f"proxy_{name}_username", username, [])
252
252
  await auth_svc.store_api_key(f"proxy_{name}_password", password, [])
253
253
  if proxy_svc:
@@ -291,7 +291,7 @@ async def _test_provider(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
291
291
 
292
292
 
293
293
  async def _add_routing_rule(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
294
- from core.container import container
294
+ from services.plugin.deps import get_database
295
295
 
296
296
  domain_pattern = p.get("domain_pattern", "")
297
297
  if not domain_pattern:
@@ -305,7 +305,7 @@ async def _add_routing_rule(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
305
305
  except json.JSONDecodeError:
306
306
  preferred = []
307
307
 
308
- db = container.database()
308
+ db = get_database()
309
309
  await db.save_proxy_routing_rule({
310
310
  "domain_pattern": domain_pattern,
311
311
  "preferred_providers": json.dumps(preferred),
@@ -318,12 +318,12 @@ async def _add_routing_rule(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
318
318
 
319
319
 
320
320
  async def _remove_routing_rule(p: Dict[str, Any], proxy_svc) -> Dict[str, Any]:
321
- from core.container import container
321
+ from services.plugin.deps import get_database
322
322
 
323
323
  rule_id = p.get("rule_id")
324
324
  if not rule_id:
325
325
  return {"success": False, "error": "rule_id is required"}
326
- db = container.database()
326
+ db = get_database()
327
327
  await db.delete_proxy_routing_rule(int(rule_id))
328
328
  if proxy_svc:
329
329
  await proxy_svc.reload_providers()
@@ -384,5 +384,5 @@ class ProxyConfigNode(ActionNode):
384
384
  async def dispatch(self, ctx: NodeContext, params: ProxyConfigParams) -> Any:
385
385
  result = await execute_proxy_config(params.model_dump())
386
386
  if not result.get("success"):
387
- raise RuntimeError(result.get("error") or "Proxy config failed")
387
+ raise NodeUserError(result.get("error") or "Proxy config failed")
388
388
  return result
@@ -6,7 +6,7 @@ from typing import Any, Dict, Literal, Optional
6
6
 
7
7
  from pydantic import BaseModel, ConfigDict, Field
8
8
 
9
- from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
9
+ from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
10
10
 
11
11
 
12
12
  class ProxyRequestParams(BaseModel):
@@ -78,7 +78,7 @@ class ProxyRequestNode(ActionNode):
78
78
  log = get_logger(__name__)
79
79
  svc = get_proxy_service()
80
80
  if not svc or not svc.is_enabled():
81
- raise RuntimeError(
81
+ raise NodeUserError(
82
82
  "Proxy service not initialized. Use proxy_config tool to add a "
83
83
  "provider first.",
84
84
  )
@@ -86,7 +86,7 @@ class ProxyRequestNode(ActionNode):
86
86
  raw = params.model_dump()
87
87
  proxy_url = await svc.get_proxy_url(params.url, raw)
88
88
  if not proxy_url:
89
- raise RuntimeError("No proxy provider available")
89
+ raise NodeUserError("No proxy provider available")
90
90
 
91
91
  max_retries = params.max_retries
92
92
  failover = raw.get("proxy_failover", True)
@@ -132,7 +132,7 @@ class ProxyRequestNode(ActionNode):
132
132
  response_data = response.text
133
133
 
134
134
  if response.status_code >= 400:
135
- raise RuntimeError(f"HTTP {response.status_code}: {response_data!r}")
135
+ raise NodeUserError(f"HTTP {response.status_code}: {response_data!r}")
136
136
  return {
137
137
  "status": response.status_code,
138
138
  "data": response_data,
@@ -6,7 +6,7 @@ subclasses here in future.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from services.plugin.credential import ApiKeyCredential
9
+ from services.plugin.credential import ApiKeyCredential, ProbeResult
10
10
 
11
11
 
12
12
  class ApifyCredential(ApiKeyCredential):
@@ -17,3 +17,31 @@ class ApifyCredential(ApiKeyCredential):
17
17
  key_name = "Authorization"
18
18
  key_location = "bearer"
19
19
  docs_url = "https://docs.apify.com/api/v2"
20
+
21
+ @classmethod
22
+ async def _probe(cls, api_key: str) -> ProbeResult:
23
+ """Probe Apify ``/users/me`` to verify the token + capture
24
+ username / email / plan for display in the credentials panel.
25
+
26
+ ``validate_apify_token`` lives on the plugin module
27
+ (``apify_actor.py``) and uses the official ``apify_client`` SDK.
28
+ It returns a dict; we translate to :class:`ProbeResult` so the
29
+ base ``Credential.validate`` handles storage / broadcast.
30
+ """
31
+ from .apify_actor import validate_apify_token
32
+
33
+ result = await validate_apify_token(api_key)
34
+ if not result.get("valid"):
35
+ return ProbeResult(
36
+ valid=False,
37
+ message=result.get("error", "Invalid API token"),
38
+ )
39
+ return ProbeResult(
40
+ valid=True,
41
+ message=f"Apify token validated — user: {result.get('username', 'unknown')}",
42
+ extra={
43
+ "username": result.get("username"),
44
+ "email": result.get("email"),
45
+ "plan": result.get("plan"),
46
+ },
47
+ )
@@ -13,7 +13,7 @@ from typing import Any, Dict, List, Literal, Optional
13
13
  from pydantic import BaseModel, ConfigDict, Field
14
14
 
15
15
  from core.logging import get_logger
16
- from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
16
+ from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
17
17
 
18
18
  from ._credentials import ApifyCredential
19
19
 
@@ -23,8 +23,8 @@ logger = get_logger(__name__)
23
23
  async def _get_apify_client():
24
24
  """Return an authenticated Apify client, or None if no token saved."""
25
25
  from apify_client import ApifyClientAsync # lazy — optional dep
26
- from core.container import container
27
- auth_service = container.auth_service()
26
+ from services.plugin.deps import get_auth_service
27
+ auth_service = get_auth_service()
28
28
  api_token = await auth_service.get_api_key("apify", "default")
29
29
  if not api_token:
30
30
  return None
@@ -256,7 +256,7 @@ class ApifyActorNode(ActionNode):
256
256
  async def run(self, ctx: NodeContext, params: ApifyActorParams) -> ApifyActorOutput:
257
257
  client = await _get_apify_client()
258
258
  if not client:
259
- raise RuntimeError(
259
+ raise NodeUserError(
260
260
  "Apify API token not configured. Please add your token in Credentials.",
261
261
  )
262
262
 
@@ -264,7 +264,7 @@ class ApifyActorNode(ActionNode):
264
264
  if actor_id == "custom":
265
265
  actor_id = params.custom_actor_id
266
266
  if not actor_id:
267
- raise RuntimeError("Actor ID is required")
267
+ raise NodeUserError("Actor ID is required")
268
268
 
269
269
  actor_input = _build_actor_input(params.model_dump())
270
270
  timeout_secs = params.timeout
@@ -282,18 +282,18 @@ class ApifyActorNode(ActionNode):
282
282
  )
283
283
 
284
284
  if run_info is None:
285
- raise RuntimeError("Actor run failed - no result returned")
285
+ raise NodeUserError("Actor run failed - no result returned")
286
286
 
287
287
  status = run_info.get("status", "UNKNOWN")
288
288
  run_id = run_info.get("id", "")
289
289
  dataset_id = run_info.get("defaultDatasetId", "")
290
290
 
291
291
  if status == "FAILED":
292
- raise RuntimeError(run_info.get("errorMessage", "Actor run failed"))
292
+ raise NodeUserError(run_info.get("errorMessage", "Actor run failed"))
293
293
  if status == "TIMED-OUT":
294
- raise RuntimeError("Actor timed out. Try increasing the timeout.")
294
+ raise NodeUserError("Actor timed out. Try increasing the timeout.")
295
295
  if status == "ABORTED":
296
- raise RuntimeError("Actor run was aborted")
296
+ raise NodeUserError("Actor run was aborted")
297
297
 
298
298
  items: List[Dict[str, Any]] = []
299
299
  if dataset_id: