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,66 @@
1
+ """Google Workspace service-status refresh callback (Wave 11.I, milestone J).
2
+
3
+ Moved from ``services/status_broadcaster._refresh_google_status``.
4
+ Plugin packages register their own callback via
5
+ ``status_broadcaster.register_service_refresh``; the broadcaster no
6
+ longer hardcodes a per-service refresh.
7
+
8
+ Reads OAuth tokens via ``auth_service.get_oauth_tokens("google")`` and
9
+ mirrors the result into the broadcaster cache.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from typing import TYPE_CHECKING
16
+
17
+ from opentelemetry import trace
18
+
19
+ if TYPE_CHECKING:
20
+ from services.status_broadcaster import StatusBroadcaster
21
+
22
+ logger = logging.getLogger(__name__)
23
+ tracer = trace.get_tracer(__name__)
24
+
25
+
26
+ async def refresh_google_status(broadcaster: "StatusBroadcaster") -> None:
27
+ """Refresh Google cache + broadcast. One pass per
28
+ ``_refresh_all_services`` cycle.
29
+ """
30
+ with tracer.start_as_current_span("broadcaster.refresh_google") as span:
31
+ try:
32
+ from services.plugin.deps import get_auth_service
33
+
34
+ auth_service = get_auth_service()
35
+ tokens = await auth_service.get_oauth_tokens(
36
+ "google", customer_id="owner"
37
+ )
38
+ if not tokens or not tokens.get("access_token"):
39
+ broadcaster._status["google"] = {
40
+ "connected": False,
41
+ "email": None,
42
+ "name": None,
43
+ }
44
+ else:
45
+ broadcaster._status["google"] = {
46
+ "connected": True,
47
+ "email": tokens.get("email"),
48
+ "name": tokens.get("name"),
49
+ }
50
+ logger.debug(
51
+ "[StatusBroadcaster] Google status: connected as %s",
52
+ tokens.get("email"),
53
+ )
54
+
55
+ await broadcaster.broadcast({
56
+ "type": "google_status",
57
+ "data": broadcaster._status["google"],
58
+ })
59
+ span.set_attribute(
60
+ "connected", bool(broadcaster._status["google"]["connected"])
61
+ )
62
+ except Exception as exc: # noqa: BLE001
63
+ span.record_exception(exc)
64
+ logger.debug(
65
+ "[StatusBroadcaster] Could not refresh Google status: %s", exc
66
+ )
@@ -0,0 +1,131 @@
1
+ """Google Workspace OAuth callback router — factory-built (Wave 11.I, S.2).
2
+
3
+ Two endpoints:
4
+
5
+ * ``GET /api/google/callback`` -- built by
6
+ :func:`services.events.oauth_lifecycle.make_oauth_callback_router`.
7
+ ``extra_state_handler`` routes customer-mode logins (where
8
+ ``state_data["mode"] == "customer"`` and a ``customer_id`` is set)
9
+ to the ``google_connections`` table by overriding ``customer_id``
10
+ on ``store_oauth_tokens``. ``redirect_after`` from the same state
11
+ data triggers a 302 to the customer-portal URL.
12
+ * ``POST /api/google/customer-auth-url`` -- generates an OAuth URL for
13
+ a specific customer (Google-only multi-tenant feature; Twitter
14
+ ships owner-mode only).
15
+
16
+ The pre-S file also exposed ``GET /status`` and ``POST /logout`` REST
17
+ routes; both duplicated the WS handlers in ``_handlers.py`` and have
18
+ been retired in line with the Twitter migration.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Any, Dict, Optional
24
+
25
+ from fastapi import APIRouter, Request
26
+
27
+ from services.events.oauth_lifecycle import make_oauth_callback_router
28
+
29
+ from ._handlers import _google_oauth_factory
30
+
31
+
32
+ def _user_info_to_email(info: Dict[str, Any]) -> str:
33
+ return info.get("email", "Unknown") or "Unknown"
34
+
35
+
36
+ def _user_info_to_name(info: Dict[str, Any]) -> str:
37
+ return info.get("name", "") or ""
38
+
39
+
40
+ async def _customer_mode_handler(
41
+ payload: Dict[str, Any],
42
+ ) -> Optional[Dict[str, Any]]:
43
+ """If the auth flow was launched in customer mode, route the
44
+ token storage to the customer's slot + return a 302 target.
45
+
46
+ Owner mode -> returns ``None`` (lifecycle factory keeps its
47
+ defaults).
48
+ """
49
+ state_data = payload.get("state_data") or {}
50
+ if state_data.get("mode") != "customer":
51
+ return None
52
+
53
+ customer_id = state_data.get("customer_id")
54
+ if not customer_id:
55
+ return None
56
+
57
+ user_info = payload.get("user_info") or {}
58
+ email = user_info.get("email", "Unknown")
59
+ redirect_after = state_data.get("redirect_after")
60
+
61
+ overrides: Dict[str, Any] = {"customer_id": customer_id}
62
+ if redirect_after:
63
+ overrides["redirect_after"] = (
64
+ f"{redirect_after}?google_connected=true"
65
+ f"&customer={customer_id}&email={email}"
66
+ )
67
+ return overrides
68
+
69
+
70
+ # The factory mounts ``GET /api/google/callback``. Google-specific
71
+ # extras (the customer-auth-url route + the customer-mode hook) layer
72
+ # on top of the same router instance.
73
+ router: APIRouter = make_oauth_callback_router(
74
+ provider="google",
75
+ oauth_factory=_google_oauth_factory,
76
+ user_info_to_email=_user_info_to_email,
77
+ user_info_to_name=_user_info_to_name,
78
+ extra_state_handler=_customer_mode_handler,
79
+ color_hex="#34a853",
80
+ )
81
+
82
+
83
+ @router.post("/customer-auth-url")
84
+ async def generate_customer_auth_url(
85
+ request: Request, customer_id: str, redirect_after: Optional[str] = None,
86
+ ):
87
+ """Generate OAuth URL for a customer to connect their Google account."""
88
+ from services.oauth_utils import get_redirect_uri
89
+
90
+ redirect_uri = get_redirect_uri(request, "google")
91
+ oauth = await _google_oauth_factory(redirect_uri=redirect_uri)
92
+ if not oauth.client_id or not oauth.client_secret:
93
+ return {
94
+ "success": False,
95
+ "error": "Google not configured. Add Client ID and Secret.",
96
+ }
97
+ result = oauth.generate_authorization_url(
98
+ state_data={
99
+ "customer_id": customer_id,
100
+ "redirect_after": redirect_after,
101
+ "mode": "customer",
102
+ },
103
+ )
104
+ return {"success": True, "url": result["url"], "state": result["state"]}
105
+
106
+
107
+ @router.get("/customer/{customer_id}/status")
108
+ async def get_customer_google_status(customer_id: str):
109
+ """Get Google connection status for a customer."""
110
+ from services.plugin.deps import get_auth_service
111
+
112
+ auth_service = get_auth_service()
113
+ tokens = await auth_service.get_oauth_tokens("google", customer_id=customer_id)
114
+ if not tokens:
115
+ return {"connected": False, "customer_id": customer_id}
116
+ return {
117
+ "connected": True,
118
+ "customer_id": customer_id,
119
+ "email": tokens.get("email"),
120
+ "name": tokens.get("name"),
121
+ }
122
+
123
+
124
+ @router.post("/customer/{customer_id}/disconnect")
125
+ async def disconnect_customer_google(customer_id: str):
126
+ """Disconnect a customer's Google account."""
127
+ from services.plugin.deps import get_auth_service
128
+
129
+ auth_service = get_auth_service()
130
+ await auth_service.remove_oauth_tokens("google", customer_id=customer_id)
131
+ return {"success": True, "customer_id": customer_id}
@@ -10,7 +10,9 @@ from typing import Any, Dict, Literal, Optional, Set
10
10
  from pydantic import BaseModel, ConfigDict, Field
11
11
 
12
12
  from core.logging import get_logger
13
- from services.plugin import NodeContext, Operation, TaskQueue, TriggerNode
13
+ from services.plugin import (
14
+ NodeContext, Operation, PollingTriggerNode, TaskQueue,
15
+ )
14
16
 
15
17
  from ._credentials import GoogleCredential
16
18
 
@@ -52,8 +54,16 @@ class GmailReceiveOutput(BaseModel):
52
54
  model_config = ConfigDict(extra="allow")
53
55
 
54
56
 
55
- class GmailReceiveNode(TriggerNode):
57
+ class GmailReceiveNode(PollingTriggerNode):
56
58
  type = "googleGmailReceive"
59
+ # Wave 11.I, milestone P: ``event_type`` ClassVar lets
60
+ # ``event_waiter._auto_populate_from_plugins`` backfill
61
+ # TRIGGER_REGISTRY without a hardcoded entry in event_waiter. The
62
+ # legacy ``type_alias = "gmailReceive"`` shim was retired -- the
63
+ # downstream callers (POLLING_TRIGGER_TYPES, deployment manager,
64
+ # frontend trigger list, _filters registration) all rename to the
65
+ # canonical class type in the same commit.
66
+ event_type = "gmail_email_received"
57
67
  display_name = "Gmail Receive"
58
68
  subtitle = "Inbound Email"
59
69
  group = ("google", "trigger")
@@ -65,12 +75,39 @@ class GmailReceiveNode(TriggerNode):
65
75
  )
66
76
  credentials = (GoogleCredential,)
67
77
  task_queue = TaskQueue.TRIGGERS_POLL
68
- mode = "polling"
69
78
  default_poll_interval = 60
70
79
 
71
80
  Params = GmailReceiveParams
72
81
  Output = GmailReceiveOutput
73
82
 
83
+ # ---- PollingTriggerNode hooks (deployment-mode loop) -------------
84
+
85
+ @staticmethod
86
+ def _build_query(parameters: Dict[str, Any]) -> str:
87
+ query = parameters.get("filter_query", "is:unread")
88
+ label = parameters.get("label_filter", "INBOX")
89
+ return f"label:{label} {query}" if label and label != "all" else query
90
+
91
+ async def setup_service(self, params: Dict[str, Any]) -> Any:
92
+ return await build_google_service("gmail", "v1", params, {})
93
+
94
+ async def fetch_ids(self, service: Any, params: Dict[str, Any]) -> Set[str]:
95
+ return await poll_gmail_ids(service, self._build_query(params))
96
+
97
+ async def fetch_detail(
98
+ self, service: Any, msg_id: str, params: Dict[str, Any]
99
+ ) -> Dict[str, Any]:
100
+ return await fetch_email_details(service, msg_id)
101
+
102
+ async def post_emit(
103
+ self, service: Any, msg_id: str, params: Dict[str, Any]
104
+ ) -> None:
105
+ if params.get("mark_as_read"):
106
+ try:
107
+ await mark_email_as_read(service, msg_id)
108
+ except Exception as exc: # noqa: BLE001
109
+ logger.warning(f"[GmailReceive] Failed to mark as read: {exc}")
110
+
74
111
  async def execute(
75
112
  self,
76
113
  node_id: str,
@@ -168,5 +205,5 @@ class GmailReceiveNode(TriggerNode):
168
205
  @Operation("wait")
169
206
  async def wait(self, ctx: NodeContext, params: GmailReceiveParams) -> GmailReceiveOutput:
170
207
  raise NotImplementedError(
171
- "gmailReceive uses execute() override (Wave 11.D.5 inlined)."
208
+ "googleGmailReceive uses execute() override (Wave 11.D.5 inlined)."
172
209
  )
@@ -49,6 +49,7 @@ register_group(key="scheduler", metadata={"label": "Schedulers", "icon"
49
49
  register_group(key="proxy", metadata={"label": "Proxy", "icon": "🛡", "color": "#bd93f9", "visibility": "dev"})
50
50
  register_group(key="whatsapp", metadata={"label": "WhatsApp", "icon": "💬", "color": "#25D366", "visibility": "dev"})
51
51
  register_group(key="email", metadata={"label": "Email", "icon": "✉️", "color": "#8be9fd", "visibility": "dev"})
52
+ register_group(key="payments", metadata={"label": "Payments", "icon": "asset:stripe", "color": "#635BFF", "visibility": "dev"})
52
53
  register_group(key="browser", metadata={"label": "Browser", "icon": "🌐", "color": "#ff79c6", "visibility": "dev"})
53
54
  register_group(key="scraper", metadata={"label": "Scrapers", "icon": "🕸", "color": "#ff79c6", "visibility": "dev"})
54
55
  register_group(key="filesystem", metadata={"label": "Filesystem", "icon": "📁", "color": "#8be9fd", "visibility": "dev"})
@@ -8,7 +8,17 @@ gmaps_locations, gmaps_nearby_places). Separate from
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from services.plugin.credential import ApiKeyCredential
11
+ import httpx
12
+
13
+ from services.plugin.credential import ApiKeyCredential, ProbeResult
14
+
15
+
16
+ # Google's documented sentinel address for connectivity tests on the
17
+ # Geocoding API. Returns ``status=OK`` for any working key with the
18
+ # Geocoding API enabled; ``REQUEST_DENIED`` for a bad key or disabled
19
+ # API. We never store the response — it's just a probe.
20
+ _GEOCODE_SENTINEL = "1600 Amphitheatre Parkway, Mountain View, CA"
21
+ _GEOCODE_URL = "https://maps.googleapis.com/maps/api/geocode/json"
12
22
 
13
23
 
14
24
  class GoogleMapsCredential(ApiKeyCredential):
@@ -19,3 +29,37 @@ class GoogleMapsCredential(ApiKeyCredential):
19
29
  key_name = "key"
20
30
  key_location = "query"
21
31
  docs_url = "https://developers.google.com/maps/documentation"
32
+
33
+ @classmethod
34
+ async def _probe(cls, api_key: str) -> ProbeResult:
35
+ """Geocode a sentinel address to verify the key is live.
36
+
37
+ Replaces ``handle_validate_maps_key`` in
38
+ ``routers/websocket.py``; the storage + broadcast wiring is
39
+ handled by the base :meth:`Credential.validate` so this method
40
+ only needs to translate the geocode response into a
41
+ :class:`ProbeResult`. ``REQUEST_DENIED`` → invalid; ``OK`` /
42
+ ``ZERO_RESULTS`` / ``OVER_QUERY_LIMIT`` → valid (the key
43
+ authenticated, even if the query didn't return rows).
44
+ """
45
+ async with httpx.AsyncClient(timeout=10.0) as client:
46
+ response = await client.get(
47
+ _GEOCODE_URL,
48
+ params={"address": _GEOCODE_SENTINEL, "key": api_key},
49
+ )
50
+ payload = response.json()
51
+ status = payload.get("status")
52
+
53
+ if status == "REQUEST_DENIED":
54
+ return ProbeResult(
55
+ valid=False,
56
+ message=payload.get("error_message", "Invalid API key"),
57
+ )
58
+ return ProbeResult(
59
+ valid=True,
60
+ message=(
61
+ "Google Maps API key is valid"
62
+ if status == "OK"
63
+ else f"API key is valid (status: {status})"
64
+ ),
65
+ )
@@ -1,4 +1,19 @@
1
- """Google Maps service for location operations."""
1
+ """Google Maps service for location operations.
2
+
3
+ Wave 11.I, milestone N: relocated from ``services/maps.py`` into the
4
+ location plugin folder. Three nodes consume it through the DI container
5
+ (``container.maps_service()``): ``gmaps_create``, ``gmaps_locations``,
6
+ ``gmaps_nearby_places``. The container provider in ``core/container.py``
7
+ imports ``MapsService`` from this module.
8
+
9
+ API-key validation lives in :mod:`._credentials`'s
10
+ :class:`GoogleMapsCredential._probe` (Wave 11.I scaffold) -- the legacy
11
+ REST endpoint at ``/python/maps/validate-key`` was retired with this
12
+ move. Workflow nodes still call ``MapsService.create_map`` /
13
+ ``geocode_location`` / ``find_nearby_places`` directly via the DI
14
+ container; the dead ``/python/<name>/execute`` REST endpoints (also
15
+ retired) duplicated the same paths.
16
+ """
2
17
 
3
18
  import time
4
19
  import googlemaps
@@ -32,13 +47,13 @@ async def _track_maps_usage(
32
47
  Returns:
33
48
  Cost breakdown dict with operation, unit_cost, resource_count, total_cost
34
49
  """
35
- from core.container import container
50
+ from services.plugin.deps import get_database
36
51
 
37
52
  pricing = get_pricing_service()
38
53
  cost_data = pricing.calculate_api_cost('google_maps', action, resource_count)
39
54
 
40
55
  # Save to database
41
- db = container.database()
56
+ db = get_database()
42
57
  await db.save_api_usage_metric({
43
58
  'session_id': session_id,
44
59
  'node_id': node_id,
@@ -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
  from ._credentials import GoogleMapsCredential
12
12
 
@@ -61,12 +61,12 @@ class GmapsCreateNode(ActionNode):
61
61
 
62
62
  @Operation("create")
63
63
  async def create(self, ctx: NodeContext, params: GmapsCreateParams) -> Any:
64
- from core.container import container
64
+ from services.plugin.deps import get_maps_service
65
65
 
66
- maps_service = container.maps_service()
66
+ maps_service = get_maps_service()
67
67
  response = await maps_service.create_map(
68
68
  ctx.node_id, params.model_dump(), ctx.raw,
69
69
  )
70
70
  if response.get("success"):
71
71
  return response.get("result") or response
72
- raise RuntimeError(response.get("error") or "Map create failed")
72
+ raise NodeUserError(response.get("error") or "Map create failed")
@@ -6,7 +6,7 @@ from typing import Any, 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
  from ._credentials import GoogleMapsCredential
12
12
 
@@ -72,12 +72,12 @@ class GmapsLocationsNode(ActionNode):
72
72
 
73
73
  @Operation("geocode", cost={"service": "google_maps", "action": "geocode", "count": 1})
74
74
  async def geocode(self, ctx: NodeContext, params: GmapsLocationsParams) -> Any:
75
- from core.container import container
75
+ from services.plugin.deps import get_maps_service
76
76
 
77
- maps_service = container.maps_service()
77
+ maps_service = get_maps_service()
78
78
  response = await maps_service.geocode_location(
79
79
  ctx.node_id, params.model_dump(), ctx.raw,
80
80
  )
81
81
  if response.get("success"):
82
82
  return response.get("result") or response
83
- raise RuntimeError(response.get("error") or "Geocoding failed")
83
+ raise NodeUserError(response.get("error") or "Geocoding failed")
@@ -6,7 +6,7 @@ from typing import Any, 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
  from ._credentials import GoogleMapsCredential
12
12
 
@@ -51,12 +51,12 @@ class GmapsNearbyPlacesNode(ActionNode):
51
51
 
52
52
  @Operation("nearby", cost={"service": "google_maps", "action": "places_nearby", "count": 1})
53
53
  async def nearby(self, ctx: NodeContext, params: GmapsNearbyPlacesParams) -> Any:
54
- from core.container import container
54
+ from services.plugin.deps import get_maps_service
55
55
 
56
- maps_service = container.maps_service()
56
+ maps_service = get_maps_service()
57
57
  response = await maps_service.find_nearby_places(
58
58
  ctx.node_id, params.model_dump(), ctx.raw,
59
59
  )
60
60
  if response.get("success"):
61
61
  return response.get("result") or response
62
- raise RuntimeError(response.get("error") or "Nearby places failed")
62
+ raise NodeUserError(response.get("error") or "Nearby places failed")
@@ -97,9 +97,14 @@ class ChatModelBase(ActionNode, abstract=True):
97
97
 
98
98
  @Operation("chat", cost={"service": "chat_model", "action": "chat", "count": 1})
99
99
  async def chat(self, ctx: NodeContext, params: ChatModelParams) -> Any:
100
- from core.container import container
101
-
102
- ai_service = container.ai_service()
100
+ from services.plugin.deps import get_ai_service
101
+
102
+ ai_service = get_ai_service()
103
+ # ``execute_chat`` raises ``NodeUserError`` directly for typed
104
+ # openai SDK failures (context overflow, bad key, server down) —
105
+ # BaseNode.execute() then logs at WARN with no traceback.
106
+ # ``RuntimeError`` here only fires for "success=False" responses
107
+ # that aren't typed SDK errors (real bugs worth a stacktrace).
103
108
  response = await ai_service.execute_chat(
104
109
  ctx.node_id, self.type, params.model_dump(),
105
110
  )
@@ -1,26 +1,64 @@
1
1
  """LLM provider credentials (Wave 11.E.1 — per-domain).
2
2
 
3
- One :class:`ApiKeyCredential` per provider. Used by the 9 chat-model
3
+ One :class:`ApiKeyCredential` per provider. Used by the chat-model
4
4
  plugins in this folder (openai, anthropic, gemini, openrouter, groq,
5
- cerebras, deepseek, kimi, mistral) plus the xAI credential referenced
6
- by agent plugins. At execution time the plugin's LangChain / native
7
- SDK client pulls the key directly from :mod:`services.auth`; this
8
- class is the Credentials-modal + discovery manifest, not the runtime
9
- client.
5
+ cerebras, deepseek, kimi, mistral, ollama, lmstudio) plus the xAI
6
+ credential referenced by agent plugins. At execution time the plugin's
7
+ LangChain / native SDK client pulls the key directly from
8
+ :mod:`services.auth`; this class is the Credentials-modal + discovery
9
+ manifest, not the runtime client.
10
+
11
+ Local servers (Ollama, LM Studio) follow the same shape as the cloud
12
+ credentials but their api_key is optional — many users run them on
13
+ localhost with no auth. The existing ``{provider}_proxy`` mechanism
14
+ in :func:`services.ai.AIService.create_model` already handles the
15
+ "override base_url + use placeholder api_key" path; the credential
16
+ class only needs to return a placeholder when nothing is stored so
17
+ the central "API key is required" check in ``execute_chat`` passes.
10
18
  """
11
19
 
12
20
  from __future__ import annotations
13
21
 
14
- from services.plugin.credential import ApiKeyCredential
22
+ from typing import Any, Dict
23
+
24
+ from services.plugin.credential import ApiKeyCredential, ProbeResult
15
25
 
16
26
 
17
27
  class _LLMApiKey(ApiKeyCredential):
18
- """Shared defaults. Subclasses only set id / display_name / icon."""
28
+ """Shared defaults. Subclasses only set id / display_name / icon.
29
+
30
+ The :meth:`_probe` override calls ``ai_service.fetch_models`` —
31
+ every cloud LLM provider in this file inherits it, so adding a new
32
+ OpenAI-compatible provider is purely declarative (id + base_url in
33
+ JSON; no validator code). The local-server credential override
34
+ (:class:`_LocalLLM`) supersedes ``validate`` entirely because its
35
+ side-effect ordering differs (URL stored under ``{id}_proxy``
36
+ before the probe + per-model context registration after).
37
+ """
19
38
 
20
39
  category = "AI"
21
40
  key_name = "Authorization"
22
41
  key_location = "bearer"
23
42
 
43
+ @classmethod
44
+ async def _probe(cls, api_key: str) -> ProbeResult:
45
+ """Default LLM probe: fetch the provider's model list.
46
+
47
+ Hits ``GET /v1/models`` (or the provider equivalent) via
48
+ :meth:`AIService.fetch_models`. Returns a populated
49
+ :class:`ProbeResult` on success; raises ``httpx``/``openai``
50
+ exceptions for the base ``Credential.validate`` to classify.
51
+ """
52
+ from services.plugin.deps import get_ai_service
53
+
54
+ ai_service = get_ai_service()
55
+ models = await ai_service.fetch_models(cls.id, api_key)
56
+ return ProbeResult(
57
+ valid=True,
58
+ message="API key validated",
59
+ models=models,
60
+ )
61
+
24
62
 
25
63
  class OpenAICredential(_LLMApiKey):
26
64
  id = "openai"
@@ -95,3 +133,53 @@ class XaiCredential(_LLMApiKey):
95
133
  display_name = "xAI (Grok)"
96
134
  icon = "asset:xai"
97
135
  docs_url = "https://console.x.ai"
136
+
137
+
138
+ class _LocalLLM(_LLMApiKey):
139
+ """Base for local-server credentials (Ollama, LM Studio).
140
+
141
+ Same shape as :class:`_LLMApiKey`, but ``resolve()`` returns the
142
+ documented Ollama placeholder when no key is stored instead of
143
+ raising. The user's custom server address rides on the existing
144
+ ``{id}_proxy`` credential — :func:`services.ai.AIService.create_model`
145
+ already reads it and OpenAIProvider already overrides ``base_url``
146
+ + forces ``api_key="ollama"``. Nothing else to wire.
147
+ """
148
+
149
+ @classmethod
150
+ async def resolve(cls, *, user_id: str = "owner") -> Dict[str, Any]:
151
+ from services.plugin.deps import get_auth_service
152
+
153
+ api_key = await get_auth_service().get_api_key(cls.id)
154
+ return {"api_key": api_key or "ollama"}
155
+
156
+ @classmethod
157
+ async def validate(cls, data: Dict[str, Any]) -> Dict[str, Any]:
158
+ """Probe the user's local server via the official SDK.
159
+
160
+ Overrides the base ``Credential.validate`` because local-LLM
161
+ side-effect ordering genuinely differs from the cloud case:
162
+ the user's URL is persisted under ``{cls.id}_proxy`` BEFORE
163
+ the probe runs, the placeholder ``api_key="ollama"`` is
164
+ stored under ``cls.id`` only on success, and per-model context
165
+ is registered in the model registry. Delegates to the
166
+ SDK-typed probe in ``_local_validator.py`` which already owns
167
+ that full flow.
168
+ """
169
+ from ._local_validator import validate_local_llm
170
+
171
+ return await validate_local_llm(dict(data, provider=cls.id))
172
+
173
+
174
+ class OllamaCredential(_LocalLLM):
175
+ id = "ollama"
176
+ display_name = "Ollama"
177
+ icon = "lobehub:ollama"
178
+ docs_url = "https://ollama.com/download"
179
+
180
+
181
+ class LMStudioCredential(_LocalLLM):
182
+ id = "lmstudio"
183
+ display_name = "LM Studio"
184
+ icon = "lobehub:lmstudio"
185
+ docs_url = "https://lmstudio.ai/docs/local-server"