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,239 @@
1
+ """OpenAI Codex CLI agent — multi-instance via `AICliService`.
2
+
3
+ Sandbox-first companion to `claude_code_agent`. Each task gets its own
4
+ git worktree; sandbox is enforced by Codex itself, not by us.
5
+
6
+ Codex has no session/resume/budget/turns surface — `CodexTaskSpec`
7
+ exposes only `sandbox` + `ask_for_approval` as task-level overrides.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import time
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Any, List, Optional
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field
18
+
19
+ from core.logging import get_logger
20
+ from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
21
+ from services.plugin.edge_walker import collect_agent_connections
22
+
23
+ from ._handles import STD_AGENT_HINTS, std_agent_handles
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ from services.cli_agent import CodexTaskSpec # noqa: E402
29
+
30
+
31
+ class CodexAgentParams(BaseModel):
32
+ """Multi-task batch parameters for Codex."""
33
+
34
+ tasks: List[CodexTaskSpec] = Field(
35
+ default_factory=list,
36
+ description="List of Codex tasks to run in parallel (max 5 concurrent).",
37
+ json_schema_extra={"rows": 1},
38
+ )
39
+ prompt: str = Field(
40
+ default="",
41
+ description="Legacy: single-prompt fallback used only when "
42
+ "`tasks` is empty.",
43
+ json_schema_extra={"rows": 4, "placeholder": "Or use the tasks array..."},
44
+ )
45
+ model: str = Field(
46
+ default="gpt-5.2-codex",
47
+ description="Default model for tasks that don't override it.",
48
+ )
49
+ sandbox: str = Field(
50
+ default="workspace-write",
51
+ description="Default sandbox for tasks that don't override it. "
52
+ "One of: read-only | workspace-write | danger-full-access.",
53
+ )
54
+ ask_for_approval: str = Field(
55
+ default="never",
56
+ description="Default approval mode: untrusted | on-request | never.",
57
+ )
58
+ system_prompt: Optional[str] = Field(default=None, json_schema_extra={"rows": 3})
59
+ working_directory: Optional[str] = None
60
+ max_parallel: int = Field(default=5, ge=1, le=20)
61
+ allowed_credentials: List[str] = Field(default_factory=list)
62
+
63
+ model_config = ConfigDict(extra="ignore")
64
+
65
+
66
+ class CodexAgentOutput(BaseModel):
67
+ success: bool = True
68
+ n_tasks: int = 0
69
+ n_succeeded: int = 0
70
+ n_failed: int = 0
71
+ wall_clock_ms: int = 0
72
+ tasks: List[Any] = Field(default_factory=list)
73
+ provider: str = "codex"
74
+ timestamp: Optional[str] = None
75
+ # Legacy single-task convenience
76
+ response: Optional[str] = None
77
+ model_config = ConfigDict(extra="allow")
78
+
79
+
80
+ class CodexAgentNode(ActionNode):
81
+ type = "codex_agent"
82
+ display_name = "Codex"
83
+ subtitle = "Sandboxed Coding"
84
+ group = ("agent",)
85
+ description = (
86
+ "Run N parallel OpenAI Codex CLI sessions. Sandbox enforced "
87
+ "by Codex itself; per-task git worktree isolation."
88
+ )
89
+ component_kind = "agent"
90
+ handles = std_agent_handles()
91
+ ui_hints = STD_AGENT_HINTS
92
+ annotations = {"destructive": True, "readonly": False, "open_world": True}
93
+ task_queue = TaskQueue.AI_HEAVY
94
+
95
+ Params = CodexAgentParams
96
+ Output = CodexAgentOutput
97
+
98
+ @Operation(
99
+ "execute",
100
+ cost={"service": "codex_agent", "action": "run", "count": 1},
101
+ )
102
+ async def execute_op(
103
+ self, ctx: NodeContext, params: CodexAgentParams,
104
+ ) -> Any:
105
+ from services.cli_agent.service import get_ai_cli_service
106
+ from services.cli_agent.types import session_result_to_model
107
+ from services.plugin.deps import get_database
108
+ from services.status_broadcaster import get_status_broadcaster
109
+
110
+ start_time = time.time()
111
+ broadcaster = get_status_broadcaster()
112
+ workflow_id = ctx.workflow_id
113
+ node_id = ctx.node_id
114
+
115
+ await broadcaster.update_node_status(
116
+ node_id, "executing",
117
+ {"message": "Starting Codex batch..."},
118
+ workflow_id=workflow_id,
119
+ )
120
+
121
+ tasks = list(params.tasks)
122
+ if not tasks:
123
+ prompt = params.prompt or self._infer_prompt_from_inputs(ctx, node_id)
124
+ if not prompt:
125
+ raise RuntimeError(
126
+ "codex_agent: provide either `tasks` or `prompt`"
127
+ )
128
+ tasks = [
129
+ CodexTaskSpec(
130
+ prompt=prompt,
131
+ model=params.model,
132
+ sandbox=params.sandbox, # type: ignore[arg-type]
133
+ ask_for_approval=params.ask_for_approval, # type: ignore[arg-type]
134
+ system_prompt=params.system_prompt,
135
+ ),
136
+ ]
137
+ else:
138
+ for i, t in enumerate(tasks):
139
+ changed: dict = {}
140
+ if not t.model and params.model:
141
+ changed["model"] = params.model
142
+ if not t.system_prompt and params.system_prompt:
143
+ changed["system_prompt"] = params.system_prompt
144
+ if changed:
145
+ tasks[i] = t.model_copy(update=changed)
146
+
147
+ database = get_database()
148
+ _, skill_data, _, _, _ = await collect_agent_connections(
149
+ node_id, ctx.raw, database,
150
+ )
151
+ connected_skills = [
152
+ s.get("skill_name") or s.get("label")
153
+ for s in skill_data
154
+ if s.get("skill_name") or s.get("label")
155
+ ]
156
+
157
+ workspace_dir = ctx.raw.get("workspace_dir") or params.working_directory
158
+ if workspace_dir is None:
159
+ from core.config import Settings
160
+ workspace_dir = Path(Settings().workspace_base_resolved) / (
161
+ workflow_id or "default"
162
+ )
163
+ workspace_dir = Path(workspace_dir)
164
+
165
+ repo_root = (
166
+ Path(params.working_directory) if params.working_directory else None
167
+ )
168
+
169
+ svc = get_ai_cli_service()
170
+ result = await svc.run_batch(
171
+ "codex",
172
+ tasks=tasks,
173
+ node_id=node_id,
174
+ workflow_id=workflow_id or "",
175
+ workspace_dir=workspace_dir,
176
+ broadcaster=broadcaster,
177
+ repo_root=repo_root,
178
+ connected_skill_names=connected_skills,
179
+ allowed_credentials=params.allowed_credentials,
180
+ max_parallel=params.max_parallel,
181
+ )
182
+
183
+ elapsed = time.time() - start_time
184
+ logger.debug(
185
+ "[codex_agent] node=%s tasks=%d ok=%d fail=%d elapsed=%.2fs",
186
+ node_id, result.n_tasks, result.n_succeeded, result.n_failed, elapsed,
187
+ )
188
+
189
+ await broadcaster.update_node_status(
190
+ node_id, "success" if result.n_failed == 0 else "warning",
191
+ {
192
+ "message": (
193
+ f"Batch complete: {result.n_succeeded}/{result.n_tasks} "
194
+ f"succeeded"
195
+ ),
196
+ "n_tasks": result.n_tasks,
197
+ "n_succeeded": result.n_succeeded,
198
+ "n_failed": result.n_failed,
199
+ },
200
+ workflow_id=workflow_id,
201
+ )
202
+
203
+ task_models = [session_result_to_model(t).model_dump() for t in result.tasks]
204
+
205
+ legacy_response = (
206
+ result.tasks[0].response if len(result.tasks) == 1 else None
207
+ )
208
+
209
+ return {
210
+ "success": result.n_failed == 0,
211
+ "n_tasks": result.n_tasks,
212
+ "n_succeeded": result.n_succeeded,
213
+ "n_failed": result.n_failed,
214
+ "total_cost_usd": result.total_cost_usd, # always None for Codex
215
+ "wall_clock_ms": result.wall_clock_ms,
216
+ "tasks": task_models,
217
+ "provider": result.provider,
218
+ "timestamp": result.timestamp or datetime.now().isoformat(),
219
+ "response": legacy_response,
220
+ }
221
+
222
+ @staticmethod
223
+ def _infer_prompt_from_inputs(ctx: NodeContext, node_id: str) -> str:
224
+ for edge in ctx.raw.get("edges", []):
225
+ if edge.get("target") != node_id:
226
+ continue
227
+ handle = edge.get("targetHandle")
228
+ if handle not in ("input-main", None):
229
+ continue
230
+ src = ctx.raw.get("outputs", {}).get(edge.get("source"), {})
231
+ if isinstance(src, dict):
232
+ for k in ("message", "text", "content", "prompt"):
233
+ val = src.get(k)
234
+ if val:
235
+ return str(val)
236
+ return str(src)
237
+ elif src:
238
+ return str(src)
239
+ return ""
@@ -39,11 +39,11 @@ class DeepAgentNode(ActionNode):
39
39
 
40
40
  @Operation("execute", cost={"service": "deep_agent", "action": "run", "count": 1})
41
41
  async def execute_op(self, ctx: NodeContext, params: SpecializedAgentParams) -> Any:
42
- from core.container import container
42
+ from services.plugin.deps import get_ai_service, get_database
43
43
  from services.status_broadcaster import get_status_broadcaster
44
44
 
45
- ai_service = container.ai_service()
46
- database = container.database()
45
+ ai_service = get_ai_service()
46
+ database = get_database()
47
47
  node_id = ctx.node_id
48
48
  workflow_id = ctx.workflow_id
49
49
  payload = params.model_dump()
@@ -38,11 +38,11 @@ class RLMAgentNode(ActionNode):
38
38
 
39
39
  @Operation("execute", cost={"service": "rlm_agent", "action": "run", "count": 1})
40
40
  async def execute_op(self, ctx: NodeContext, params: SpecializedAgentParams) -> Any:
41
- from core.container import container
41
+ from services.plugin.deps import get_ai_service, get_database
42
42
  from services.status_broadcaster import get_status_broadcaster
43
43
 
44
- ai_service = container.ai_service()
45
- database = container.database()
44
+ ai_service = get_ai_service()
45
+ database = get_database()
46
46
  node_id = ctx.node_id
47
47
  workflow_id = ctx.workflow_id
48
48
  payload = params.model_dump()
@@ -1 +1,31 @@
1
- """Plugins for the 'android' palette group. See ../__init__.py for the package layout."""
1
+ """Plugins for the 'android' palette group.
2
+
3
+ Self-contained plugin folder (Wave 11.H pattern) — owns its
4
+ WebSocket handlers (`_handlers.py`), HTTP router (`_router.py`),
5
+ action-dispatch service (`_dispatcher.py`), and the relay-pairing
6
+ sub-package (`_relay/`). The 16 service plugins (battery_monitor,
7
+ wifi_automation, ...) live alongside as siblings.
8
+
9
+ Wiring is body-free; both registrations are idempotent.
10
+ """
11
+
12
+ from services.status_broadcaster import register_service_refresh
13
+ from services.ws_handler_registry import (
14
+ register_option_loader,
15
+ register_router,
16
+ register_ws_handlers,
17
+ )
18
+
19
+ from . import _router
20
+ from ._handlers import WS_HANDLERS
21
+ from ._option_loaders import load_service_actions
22
+ from ._refresh import refresh_android_status
23
+
24
+ register_ws_handlers(WS_HANDLERS)
25
+ register_router(_router.router, name="android")
26
+ register_service_refresh(refresh_android_status)
27
+
28
+ # loadOptionsMethod loader (Wave 11.I, milestone M.3) -- last entry to
29
+ # leave services/node_option_loaders/, which is deleted at the same
30
+ # commit.
31
+ register_option_loader("getAndroidServiceActions", load_service_actions)
@@ -90,9 +90,13 @@ class AndroidServiceOutput(BaseModel):
90
90
 
91
91
 
92
92
  class AndroidServiceBase(ActionNode, abstract=True):
93
- """Subclass and set type/display_name/icon/description."""
93
+ """Subclass and set type/display_name/icon/description.
94
+
95
+ Visual metadata (icon + color) lives in ``server/nodes/visuals.json``
96
+ keyed by individual plugin type. The ``_visuals.py`` resolver picks
97
+ each entry up at NodeSpec emit time; no class-level ClassVars needed.
98
+ """
94
99
 
95
- color = "#50fa7b"
96
100
  group = ("android", "service")
97
101
  component_kind = "square"
98
102
  handles = (
@@ -110,9 +114,9 @@ class AndroidServiceBase(ActionNode, abstract=True):
110
114
 
111
115
  @Operation("invoke", cost={"service": "android", "action": "service_call", "count": 1})
112
116
  async def invoke(self, ctx: NodeContext, params: AndroidServiceParams) -> Any:
113
- from core.container import container
117
+ from services.plugin.deps import get_android_service
114
118
 
115
- android_service = container.android_service()
119
+ android_service = get_android_service()
116
120
  payload = params.model_dump()
117
121
 
118
122
  # Derive service_id from the registered node type (battery etc.) —
@@ -174,7 +178,7 @@ async def _execute_with_broadcast(
174
178
  host: str, port: int, log_label: str,
175
179
  ) -> Dict[str, Any]:
176
180
  """Run an Android service call with broadcast status for the UI node."""
177
- from services.android_service import AndroidService
181
+ from ._dispatcher import AndroidService
178
182
  from services.status_broadcaster import get_status_broadcaster
179
183
 
180
184
  broadcaster = get_status_broadcaster()
@@ -218,7 +218,7 @@ class AndroidService:
218
218
  Execution result with service response data
219
219
  """
220
220
  try:
221
- from services.android import get_current_relay_client
221
+ from ._relay import get_current_relay_client
222
222
 
223
223
  relay_client = get_current_relay_client()
224
224
  if not relay_client:
@@ -373,7 +373,7 @@ class AndroidService:
373
373
 
374
374
  try:
375
375
  # Check if relay connection is available (remote device)
376
- from services.android import get_current_relay_client
376
+ from ._relay import get_current_relay_client
377
377
  relay_client = get_current_relay_client()
378
378
 
379
379
  if relay_client and relay_client.is_paired():
@@ -0,0 +1,154 @@
1
+ """WebSocket handlers for the Android plugin.
2
+
3
+ Five handlers split into two concerns:
4
+
5
+ - ADB / device-action commands (`get_android_devices`,
6
+ `execute_android_action`) talk to the local `AndroidService`
7
+ dispatcher to enumerate devices and run service actions.
8
+
9
+ - Relay-pairing commands (`android_relay_connect` / `_disconnect` /
10
+ `_reconnect`) drive the WebSocket relay client lifecycle for
11
+ remote devices.
12
+
13
+ Self-registers via ``services.ws_handler_registry.register_ws_handlers``
14
+ from ``__init__.py`` (Wave 11.H plugin pattern).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import time
21
+ from typing import Any, Dict
22
+
23
+ from starlette.websockets import WebSocket
24
+
25
+ from core.logging import get_logger
26
+ from services.plugin.ws import ws_response
27
+ from services.status_broadcaster import get_status_broadcaster
28
+
29
+ logger = get_logger(__name__)
30
+
31
+
32
+ async def handle_get_android_devices(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
33
+ """Get list of connected Android devices."""
34
+ from services.plugin.deps import get_android_service
35
+
36
+ android_service = get_android_service()
37
+ devices = await android_service.list_devices()
38
+ return {"devices": devices, "timestamp": time.time()}
39
+
40
+
41
+ async def handle_execute_android_action(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
42
+ """Execute an Android service action."""
43
+ from services.plugin.deps import get_android_service
44
+
45
+ android_service = get_android_service()
46
+ broadcaster = get_status_broadcaster()
47
+ service_id, action = data["service_id"], data["action"]
48
+ node_id = data.get("node_id", f"android_{service_id}_{action}")
49
+
50
+ await broadcaster.update_node_status(node_id, "executing")
51
+ result = await android_service.execute_service(
52
+ node_id=node_id, service_id=service_id, action=action,
53
+ parameters=data.get("parameters", {}),
54
+ android_host=data.get("android_host", "localhost"),
55
+ android_port=data.get("android_port", 8888)
56
+ )
57
+
58
+ status = "success" if result.get("success") else "error"
59
+ await broadcaster.update_node_status(
60
+ node_id, status, result.get("result") or {"error": result.get("error")}
61
+ )
62
+ return result
63
+
64
+
65
+ @ws_response
66
+ async def handle_android_relay_connect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
67
+ """Connect to the Android relay server.
68
+
69
+ Establishes the WebSocket connection and broadcasts QR pairing data.
70
+ Status updates are emitted from the relay client itself.
71
+ """
72
+ from ._relay import get_relay_client
73
+
74
+ url = data.get("url", "")
75
+ api_key = data.get("api_key")
76
+
77
+ if not url:
78
+ return {"success": False, "connected": False, "error": "Relay URL is required"}
79
+ if not api_key:
80
+ return {"success": False, "connected": False, "error": "API key is required"}
81
+
82
+ logger.info(f"[WebSocket] Android relay connect: {url}")
83
+
84
+ client, error = await get_relay_client(url, api_key)
85
+ if client:
86
+ logger.info(
87
+ f"[WebSocket] Android relay connect success, qr_data present: "
88
+ f"{bool(client.qr_data)}, session_token: {client.session_token}"
89
+ )
90
+ return {
91
+ "success": True,
92
+ "connected": True,
93
+ "session_token": client.session_token,
94
+ "qr_data": client.qr_data,
95
+ "message": "Connected to relay server",
96
+ }
97
+ return {
98
+ "success": False,
99
+ "connected": False,
100
+ "error": error or "Failed to connect to relay server",
101
+ }
102
+
103
+
104
+ @ws_response
105
+ async def handle_android_relay_disconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
106
+ """Disconnect from the Android relay server."""
107
+ from ._relay import close_relay_client
108
+
109
+ logger.info("[WebSocket] Android relay disconnect requested")
110
+ await close_relay_client()
111
+ return {"success": True, "connected": False, "message": "Disconnected from relay server"}
112
+
113
+
114
+ @ws_response
115
+ async def handle_android_relay_reconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
116
+ """Reconnect to the relay with a fresh session token + QR code."""
117
+ from ._relay import close_relay_client, get_relay_client
118
+
119
+ url = data.get("url", "")
120
+ api_key = data.get("api_key")
121
+
122
+ if not url:
123
+ return {"success": False, "connected": False, "error": "Relay URL is required"}
124
+ if not api_key:
125
+ return {"success": False, "connected": False, "error": "API key is required"}
126
+
127
+ logger.info("[WebSocket] Android relay reconnect: forcing new session")
128
+
129
+ await close_relay_client()
130
+ await asyncio.sleep(0.5) # ensure clean disconnect
131
+
132
+ client, error = await get_relay_client(url, api_key)
133
+ if client:
134
+ return {
135
+ "success": True,
136
+ "connected": True,
137
+ "session_token": client.session_token,
138
+ "qr_data": client.qr_data,
139
+ "message": "Reconnected with new session token",
140
+ }
141
+ return {
142
+ "success": False,
143
+ "connected": False,
144
+ "error": error or "Failed to reconnect to relay server",
145
+ }
146
+
147
+
148
+ WS_HANDLERS = {
149
+ "get_android_devices": handle_get_android_devices,
150
+ "execute_android_action": handle_execute_android_action,
151
+ "android_relay_connect": handle_android_relay_connect,
152
+ "android_relay_disconnect": handle_android_relay_disconnect,
153
+ "android_relay_reconnect": handle_android_relay_reconnect,
154
+ }
@@ -0,0 +1,44 @@
1
+ """Android ``loadOptionsMethod`` loaders.
2
+
3
+ Wave 11.I, milestone M.3. Returns the list of actions supported by each
4
+ Android service node. The frontend passes ``node_type`` in params; the
5
+ loader maps it to the service's ``action`` enum advertised by the
6
+ running Android bridge.
7
+
8
+ ``SERVICE_ID_MAP`` is the single source of truth -- imported from
9
+ ``_base.py`` instead of being duplicated here (the pre-Wave-11.I
10
+ ``services/node_option_loaders/android_loaders.py`` carried its own
11
+ copy under a different name; that copy is gone now).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any, Dict, List
17
+
18
+ from ._base import SERVICE_ID_MAP
19
+
20
+
21
+ async def load_service_actions(params: Dict[str, Any]) -> List[Dict[str, Any]]:
22
+ """Return ``[{value, label}]`` for the given Android service node.
23
+
24
+ Each service's ``execute_action`` endpoint validates the action at
25
+ call time; the dropdown is convenience UX, not a correctness
26
+ boundary. The frontend falls back to a free-text input if the
27
+ loader returns an empty list (service offline / discovery failed).
28
+ """
29
+ node_type = params.get("node_type") or ""
30
+ service_id = SERVICE_ID_MAP.get(node_type)
31
+ if not service_id:
32
+ return []
33
+
34
+ try:
35
+ from services.plugin.deps import get_android_service
36
+
37
+ android_svc = get_android_service()
38
+ actions = await android_svc.list_actions(service_id) # type: ignore[attr-defined]
39
+ return [
40
+ {"value": a, "label": a.replace("_", " ").title()}
41
+ for a in actions or []
42
+ ]
43
+ except Exception:
44
+ return []
@@ -0,0 +1,127 @@
1
+ """Android service-status refresh callback (Wave 11.I, milestone J).
2
+
3
+ Moved from ``services/status_broadcaster._auto_reconnect_android_relay``
4
+ (plus the ``_auto_reconnect_android_relay_body`` helper). Plugin
5
+ packages register their own callback via
6
+ ``status_broadcaster.register_service_refresh``; the broadcaster no
7
+ longer hardcodes a per-service refresh.
8
+
9
+ Load-bearing: this is the path that auto-reconnects the Android relay
10
+ from a stored pairing session after server restart. Without it the
11
+ relay sits idle until the user manually clicks Connect. Runs once on
12
+ lifespan startup (post-V).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from typing import TYPE_CHECKING
19
+
20
+ from opentelemetry import trace
21
+
22
+ if TYPE_CHECKING:
23
+ from services.status_broadcaster import StatusBroadcaster
24
+
25
+ logger = logging.getLogger(__name__)
26
+ tracer = trace.get_tracer(__name__)
27
+
28
+
29
+ async def refresh_android_status(broadcaster: "StatusBroadcaster") -> None:
30
+ """Auto-reconnect to the Android relay if a stored pairing exists.
31
+
32
+ Called once per ``_refresh_all_services`` cycle (post-V: at
33
+ lifespan startup only).
34
+ """
35
+ with tracer.start_as_current_span("broadcaster.refresh_android") as span:
36
+ await _auto_reconnect_body(broadcaster, span)
37
+
38
+
39
+ async def _auto_reconnect_body(broadcaster: "StatusBroadcaster", span) -> None:
40
+ try:
41
+ # Already connected? Refresh the cached snapshot and stop.
42
+ from ._relay.manager import get_current_relay_client
43
+
44
+ existing = get_current_relay_client()
45
+ if existing and existing.is_connected():
46
+ broadcaster._status["android"] = {
47
+ "connected": True,
48
+ "paired": existing.is_paired(),
49
+ "device_id": existing.paired_device_id,
50
+ "device_name": existing.paired_device_name,
51
+ "connected_devices": list(existing.get_connected_devices()),
52
+ "connection_type": "relay",
53
+ "qr_data": existing.qr_data,
54
+ "session_token": existing.session_token,
55
+ }
56
+ logger.debug("[StatusBroadcaster] Android relay already connected")
57
+ await broadcaster.broadcast({
58
+ "type": "android_status",
59
+ "data": broadcaster._status["android"],
60
+ })
61
+ span.set_attribute("path", "already_connected")
62
+ return
63
+
64
+ # Look for a stored pairing session.
65
+ from services.plugin.deps import get_database
66
+
67
+ database = get_database()
68
+ session = await database.get_android_relay_session()
69
+ if not session:
70
+ span.set_attribute("path", "no_session")
71
+ logger.debug("[StatusBroadcaster] No stored Android relay session")
72
+ return
73
+
74
+ relay_url = session.get("relay_url")
75
+ api_key = session.get("api_key")
76
+ device_id = session.get("device_id")
77
+
78
+ if not relay_url or not api_key:
79
+ span.set_attribute("path", "session_missing_creds")
80
+ logger.debug(
81
+ "[StatusBroadcaster] Stored session missing relay URL or API key"
82
+ )
83
+ return
84
+
85
+ span.set_attribute("path", "auto_reconnect")
86
+ logger.info(
87
+ "[StatusBroadcaster] Auto-reconnecting to Android relay...",
88
+ relay_url=relay_url,
89
+ device_id=device_id,
90
+ )
91
+
92
+ from ._relay.manager import get_relay_client
93
+
94
+ client, error = await get_relay_client(relay_url, api_key)
95
+
96
+ if client and client.is_connected():
97
+ logger.info("[StatusBroadcaster] Android relay reconnected successfully")
98
+ # The relay server creates a new session on each connect, so
99
+ # pairing may be lost -- mirror whatever the new client
100
+ # reports.
101
+ broadcaster._status["android"] = {
102
+ "connected": True,
103
+ "paired": client.is_paired(),
104
+ "device_id": client.paired_device_id,
105
+ "device_name": client.paired_device_name,
106
+ "connected_devices": list(client.get_connected_devices()),
107
+ "connection_type": "relay",
108
+ "qr_data": client.qr_data,
109
+ "session_token": client.session_token,
110
+ }
111
+ await broadcaster.broadcast({
112
+ "type": "android_status",
113
+ "data": broadcaster._status["android"],
114
+ })
115
+ span.set_attribute("reconnect_ok", True)
116
+ else:
117
+ span.set_attribute("reconnect_ok", False)
118
+ logger.warning(
119
+ "[StatusBroadcaster] Failed to reconnect Android relay: %s", error
120
+ )
121
+ # Stored session is stale; drop it.
122
+ await database.clear_android_relay_session()
123
+ except Exception as exc: # noqa: BLE001 -- mirror pre-migration behaviour
124
+ span.record_exception(exc)
125
+ logger.debug(
126
+ "[StatusBroadcaster] Could not auto-reconnect Android relay: %s", exc
127
+ )