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
@@ -17,25 +17,32 @@
17
17
  import * as React from 'react';
18
18
  import { cva, type VariantProps } from 'class-variance-authority';
19
19
  import { cn } from '@/lib/utils';
20
+ import { Sounds } from '@/lib/sound';
20
21
 
21
22
  export const actionButtonVariants = cva(
22
23
  // Base: 32px tall pill with icon-text gap, semibold, focus ring, smooth hover.
23
- 'inline-flex h-8 items-center gap-1.5 rounded-md border px-3.5 text-[13px] font-semibold transition-all outline-none select-none disabled:cursor-not-allowed focus-visible:ring-2 focus-visible:ring-ring/40',
24
+ // `action-btn` + `btn` co-classes are the design-handoff structural
25
+ // hooks for per-theme decorations (gold-foil on Renaissance, neon
26
+ // outline on Cyber, hard 4px shadow on Atomic) and the global hover
27
+ // sound delegate. Disabled state uses shadcn-idiomatic
28
+ // `disabled:opacity-50` so we don't do per-token opacity arithmetic
29
+ // at the call site.
30
+ 'action-btn btn inline-flex h-8 items-center gap-1.5 rounded-md border px-3.5 text-[13px] font-semibold transition-all outline-none select-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-ring/40',
24
31
  {
25
32
  variants: {
26
33
  intent: {
27
34
  run:
28
- 'border-action-run-border bg-action-run-soft text-action-run hover:bg-action-run/25 disabled:border-primary/40 disabled:bg-primary/10 disabled:text-primary',
35
+ 'border-action-run-border bg-action-run-soft text-action-run hover:bg-action-run-hover',
29
36
  stop:
30
- 'border-action-stop-border bg-action-stop-soft text-action-stop hover:bg-action-stop/25 disabled:border-primary/40 disabled:bg-primary/10 disabled:text-primary',
37
+ 'border-action-stop-border bg-action-stop-soft text-action-stop hover:bg-action-stop-hover',
31
38
  save:
32
- 'border-action-save-border bg-action-save-soft text-action-save hover:bg-action-save/25 disabled:border-primary/40 disabled:bg-primary/10 disabled:text-primary',
39
+ 'border-action-save-border bg-action-save-soft text-action-save hover:bg-action-save-hover',
33
40
  config:
34
- 'border-action-config-border bg-action-config-soft text-action-config hover:bg-action-config/25 disabled:border-primary/40 disabled:bg-primary/10 disabled:text-primary',
41
+ 'border-action-config-border bg-action-config-soft text-action-config hover:bg-action-config-hover',
35
42
  secret:
36
- 'border-action-secret-border bg-action-secret-soft text-action-secret hover:bg-action-secret/25 disabled:border-primary/40 disabled:bg-primary/10 disabled:text-primary',
43
+ 'border-action-secret-border bg-action-secret-soft text-action-secret hover:bg-action-secret-hover',
37
44
  tools:
38
- 'border-action-tools-border bg-action-tools-soft text-action-tools hover:bg-action-tools/25 disabled:border-primary/40 disabled:bg-primary/10 disabled:text-primary',
45
+ 'border-action-tools-border bg-action-tools-soft text-action-tools hover:bg-action-tools-hover',
39
46
  },
40
47
  },
41
48
  defaultVariants: { intent: 'save' },
@@ -49,13 +56,31 @@ export interface ActionButtonProps
49
56
  VariantProps<typeof actionButtonVariants> {}
50
57
 
51
58
  export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
52
- ({ className, intent, ...props }, ref) => (
53
- <button
54
- ref={ref}
55
- type={props.type ?? 'button'}
56
- className={cn(actionButtonVariants({ intent }), className)}
57
- {...props}
58
- />
59
- ),
59
+ ({ className, intent, onClick, ...props }, ref) => {
60
+ // Fire the per-theme `click` sound BEFORE the user-supplied
61
+ // handler so the audio cue doesn't depend on the action
62
+ // succeeding (e.g., even a disabled-late workflow run still
63
+ // gives feedback). Sounds.play() is a no-op when the engine is
64
+ // disabled or the active pack is `none`, so this costs nothing
65
+ // in the default state.
66
+ const handleClick = onClick
67
+ ? (event: React.MouseEvent<HTMLButtonElement>) => {
68
+ Sounds.play('click');
69
+ onClick(event);
70
+ }
71
+ : (_event: React.MouseEvent<HTMLButtonElement>) => {
72
+ Sounds.play('click');
73
+ };
74
+
75
+ return (
76
+ <button
77
+ ref={ref}
78
+ type={props.type ?? 'button'}
79
+ className={cn(actionButtonVariants({ intent }), className)}
80
+ onClick={handleClick}
81
+ {...props}
82
+ />
83
+ );
84
+ },
60
85
  );
61
86
  ActionButton.displayName = 'ActionButton';
@@ -3,9 +3,16 @@ import { cva, type VariantProps } from "class-variance-authority"
3
3
  import { Slot } from "radix-ui"
4
4
 
5
5
  import { cn } from "@/lib/utils"
6
+ import { Sounds } from "@/lib/sound"
6
7
 
8
+ // `btn` co-class is the design-handoff structural hook — every shadcn
9
+ // Button gets it so per-theme CSS rules (`.btn`, `.btn-primary`, etc.)
10
+ // activate (gold-foil on Renaissance, neon outline on Cyber, riveted
11
+ // brass on Steampunk, boomerang shadow on Atomic, etc.). It also
12
+ // powers the global hover-sound delegate which selects on `.btn, .row,
13
+ // .action-btn, .menu-pop-item, .wf-card, .comp, .cmdk-item`.
7
14
  const buttonVariants = cva(
8
- "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
15
+ "btn group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
16
  {
10
17
  variants: {
11
18
  variant: {
@@ -46,6 +53,7 @@ function Button({
46
53
  variant = "default",
47
54
  size = "default",
48
55
  asChild = false,
56
+ onClick,
49
57
  ...props
50
58
  }: React.ComponentProps<"button"> &
51
59
  VariantProps<typeof buttonVariants> & {
@@ -53,12 +61,27 @@ function Button({
53
61
  }) {
54
62
  const Comp = asChild ? Slot.Root : "button"
55
63
 
64
+ // Fire the per-theme `click` sound BEFORE the user-supplied handler
65
+ // so audio feedback is instant — mirrors the ActionButton pattern.
66
+ // No-op when the engine is disabled or the active pack is `none`.
67
+ // Disabled buttons never reach here (the `disabled` prop filters
68
+ // pointer events at the DOM level).
69
+ const handleClick = onClick
70
+ ? (event: React.MouseEvent<HTMLButtonElement>) => {
71
+ Sounds.play('click');
72
+ onClick(event);
73
+ }
74
+ : (_event: React.MouseEvent<HTMLButtonElement>) => {
75
+ Sounds.play('click');
76
+ };
77
+
56
78
  return (
57
79
  <Comp
58
80
  data-slot="button"
59
81
  data-variant={variant}
60
82
  data-size={size}
61
83
  className={cn(buttonVariants({ variant, size, className }))}
84
+ onClick={handleClick}
62
85
  {...props}
63
86
  />
64
87
  )
@@ -4,6 +4,7 @@ import * as React from "react"
4
4
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
5
5
 
6
6
  import { cn } from "@/lib/utils"
7
+ import { Sounds } from "@/lib/sound"
7
8
  import { CheckIcon, ChevronRightIcon } from "lucide-react"
8
9
 
9
10
  function DropdownMenu({
@@ -43,7 +44,11 @@ function DropdownMenuContent({
43
44
  data-slot="dropdown-menu-content"
44
45
  sideOffset={sideOffset}
45
46
  align={align}
46
- className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className )}
47
+ // `menu-pop` co-class is the design-handoff structural hook
48
+ // for per-theme dropdown decorations (parchment + gilded
49
+ // border on Renaissance, neon outline + > prefix on Cyber,
50
+ // etc.).
51
+ className={cn("menu-pop z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className )}
47
52
  {...props}
48
53
  />
49
54
  </DropdownMenuPrimitive.Portal>
@@ -62,18 +67,35 @@ function DropdownMenuItem({
62
67
  className,
63
68
  inset,
64
69
  variant = "default",
70
+ onSelect,
65
71
  ...props
66
72
  }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
67
73
  inset?: boolean
68
74
  variant?: "default" | "destructive"
69
75
  }) {
76
+ // Fire the per-theme `click` sound when the item is selected (Radix
77
+ // calls onSelect for both pointer + keyboard activation, which
78
+ // matches the upstream's `.menu-pop-item` click trigger).
79
+ const handleSelect = onSelect
80
+ ? (event: Event) => {
81
+ Sounds.play('click');
82
+ onSelect(event);
83
+ }
84
+ : (_event: Event) => {
85
+ Sounds.play('click');
86
+ };
87
+
70
88
  return (
71
89
  <DropdownMenuPrimitive.Item
72
90
  data-slot="dropdown-menu-item"
73
91
  data-inset={inset}
74
92
  data-variant={variant}
93
+ onSelect={handleSelect}
94
+ // `menu-pop-item` co-class powers per-theme item decorations
95
+ // (Renaissance ink-spread hover + ✦ marker; Cyber `> ` prefix +
96
+ // neon glow; etc.) and the W18 hover-sound delegate.
75
97
  className={cn(
76
- "group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
98
+ "menu-pop-item group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
77
99
  className
78
100
  )}
79
101
  {...props}
@@ -1,14 +1,31 @@
1
1
  import * as React from "react"
2
2
 
3
3
  import { cn } from "@/lib/utils"
4
+ import { Sounds } from "@/lib/sound"
5
+
6
+ function Input({ className, type, onChange, ...props }: React.ComponentProps<"input">) {
7
+ // Fire the per-theme `type` sound on every keystroke. The engine
8
+ // throttles `type` internally (W19 30 ms last-fire window) so rapid
9
+ // typing into a long input doesn't queue dozens of OscillatorNodes.
10
+ // No-op when sound is disabled.
11
+ const handleChange = onChange
12
+ ? (event: React.ChangeEvent<HTMLInputElement>) => {
13
+ Sounds.play('type');
14
+ onChange(event);
15
+ }
16
+ : undefined;
4
17
 
5
- function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
18
  return (
7
19
  <input
8
20
  type={type}
9
21
  data-slot="input"
22
+ onChange={handleChange}
23
+ // `input` co-class is the design-handoff structural hook —
24
+ // per-theme CSS attaches ruled-line backgrounds (Renaissance),
25
+ // terminal prompt prefix (Cyber), washi underline (Edo), etc.
26
+ // Also targeted by the W18 `type` sound delegate.
10
27
  className={cn(
11
- "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
28
+ "input h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
12
29
  className
13
30
  )}
14
31
  {...props}
@@ -4,6 +4,7 @@ import * as React from "react"
4
4
  import { Select as SelectPrimitive } from "radix-ui"
5
5
 
6
6
  import { cn } from "@/lib/utils"
7
+ import { Sounds } from "@/lib/sound"
7
8
  import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
8
9
 
9
10
  function Select({
@@ -106,8 +107,21 @@ function SelectLabel({
106
107
  function SelectItem({
107
108
  className,
108
109
  children,
110
+ onClick,
109
111
  ...props
110
112
  }: React.ComponentProps<typeof SelectPrimitive.Item>) {
113
+ // Radix Select doesn't surface an `onSelect` per-item; clicks +
114
+ // keyboard activation both surface as DOM clicks on the Item, so we
115
+ // wrap onClick. Falls through to the original handler if any.
116
+ const handleClick = onClick
117
+ ? (event: React.MouseEvent<HTMLDivElement>) => {
118
+ Sounds.play('click');
119
+ onClick(event);
120
+ }
121
+ : (_event: React.MouseEvent<HTMLDivElement>) => {
122
+ Sounds.play('click');
123
+ };
124
+
111
125
  return (
112
126
  <SelectPrimitive.Item
113
127
  data-slot="select-item"
@@ -115,6 +129,7 @@ function SelectItem({
115
129
  "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
116
130
  className
117
131
  )}
132
+ onClick={handleClick}
118
133
  {...props}
119
134
  >
120
135
  <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
@@ -1,13 +1,26 @@
1
1
  import * as React from "react"
2
2
 
3
3
  import { cn } from "@/lib/utils"
4
+ import { Sounds } from "@/lib/sound"
5
+
6
+ function Textarea({ className, onChange, ...props }: React.ComponentProps<"textarea">) {
7
+ // Fire the per-theme `type` sound on every keystroke (throttled by
8
+ // the W19 last-fire window inside the engine).
9
+ const handleChange = onChange
10
+ ? (event: React.ChangeEvent<HTMLTextAreaElement>) => {
11
+ Sounds.play('type');
12
+ onChange(event);
13
+ }
14
+ : undefined;
4
15
 
5
- function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
16
  return (
7
17
  <textarea
8
18
  data-slot="textarea"
19
+ onChange={handleChange}
20
+ // `input` co-class shares per-theme decorations + W18 type delegate
21
+ // with the Input primitive.
9
22
  className={cn(
10
- "flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
23
+ "input flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
11
24
  className
12
25
  )}
13
26
  {...props}
@@ -1,15 +1,27 @@
1
1
  /**
2
2
  * Authentication Context for user session management.
3
3
  *
4
- * Provides:
5
- * - Login/logout/register functions
6
- * - Current user state
7
- * - Authentication status
8
- * - Auth mode (single-owner vs multi-user)
4
+ * The auth-status check runs through TanStack Query (`useQuery`) so
5
+ * exponential backoff with full jitter, AbortController-based unmount
6
+ * cleanup, Strict-Mode safety, and 401/403 fast-fail are all delegated
7
+ * to the library — see https://tanstack.com/query/v5/docs/framework/react/guides/query-retries.
8
+ *
9
+ * The context's public surface (user, isAuthenticated, isLoading,
10
+ * authMode, canRegister, error, login, register, logout, checkAuth) is
11
+ * unchanged so consumer code does not move.
12
+ *
13
+ * Login / register / logout mutate the auth state by invalidating the
14
+ * `['auth', 'status']` query rather than calling a private setter — the
15
+ * single source of truth stays the query cache, which TanStack Query
16
+ * dedupes by reference equality, eliminating the spurious
17
+ * `isAuthenticated` flips that closed the WS prematurely under React
18
+ * Strict Mode.
9
19
  */
10
20
 
11
- import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
21
+ import React, { createContext, useContext, useCallback, useMemo } from 'react';
22
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
12
23
  import { API_CONFIG } from '../config/api';
24
+ import { AUTH_RETRY } from '../lib/connectionConfig';
13
25
 
14
26
  export interface User {
15
27
  id: number;
@@ -43,159 +55,185 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
43
55
 
44
56
  const getApiBase = () => `${API_CONFIG.PYTHON_BASE_URL}/api/auth`;
45
57
 
46
- export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
47
- const [user, setUser] = useState<User | null>(null);
48
- const [isLoading, setIsLoading] = useState(true);
49
- const [authMode, setAuthMode] = useState<'single' | 'multi'>('single');
50
- const [canRegister, setCanRegister] = useState(false);
51
- const [error, setError] = useState<string | null>(null);
58
+ const ANONYMOUS_USER: User = {
59
+ id: 0,
60
+ email: 'anonymous',
61
+ display_name: 'Anonymous',
62
+ is_owner: true,
63
+ };
52
64
 
53
- const checkAuth = useCallback(async (retryCount = 0): Promise<void> => {
54
- const maxRetries = 5;
55
- const baseDelay = 1000; // 1 second
65
+ // `['auth', 'status']` is the canonical key for the bootstrap query.
66
+ // Login / register / logout invalidate it via `queryClient.invalidateQueries`.
67
+ export const AUTH_STATUS_QUERY_KEY = ['auth', 'status'] as const;
56
68
 
57
- try {
58
- const response = await fetch(`${getApiBase()}/status`, {
59
- credentials: 'include'
60
- });
61
- const data: AuthStatus = await response.json();
62
-
63
- // Runtime auth check: if backend says auth is disabled, set anonymous user
64
- if (data.auth_enabled === false) {
65
- setUser({ id: 0, email: 'anonymous', display_name: 'Anonymous', is_owner: true });
66
- setIsLoading(false);
67
- setError(null);
68
- return;
69
- }
69
+ /**
70
+ * Full-jitter exponential backoff. Constants live in
71
+ * `lib/connectionConfig.ts` (`AUTH_RETRY`) so a future tuning pass is a
72
+ * single-file edit. See that module for the rationale and the reference
73
+ * link to the AWS Architecture Blog.
74
+ */
75
+ const authRetryDelay = (attemptIndex: number): number =>
76
+ Math.random() * Math.min(AUTH_RETRY.CAP_MS, AUTH_RETRY.BASE_MS * 2 ** attemptIndex);
70
77
 
71
- setAuthMode(data.auth_mode);
72
- setCanRegister(data.can_register);
78
+ /**
79
+ * Retry on network failures + 5xx; never retry on auth errors (401/403)
80
+ * because those are valid responses meaning "auth disabled / not logged
81
+ * in", not "backend unavailable". Cap at `AUTH_RETRY.MAX_ATTEMPTS`.
82
+ */
83
+ const authShouldRetry = (failureCount: number, error: unknown): boolean => {
84
+ if (failureCount >= AUTH_RETRY.MAX_ATTEMPTS) return false;
85
+ const msg = error instanceof Error ? error.message : String(error);
86
+ if (msg.includes('HTTP 401') || msg.includes('HTTP 403')) return false;
87
+ return true;
88
+ };
73
89
 
74
- if (data.authenticated && data.user) {
75
- setUser(data.user);
76
- } else {
77
- setUser(null);
78
- }
79
- setError(null);
80
- setIsLoading(false);
81
- } catch (err) {
82
- console.error(`Failed to check auth status (attempt ${retryCount + 1}/${maxRetries + 1}):`, err);
83
-
84
- // Retry with exponential backoff if server not ready
85
- if (retryCount < maxRetries) {
86
- const delay = baseDelay * Math.pow(2, retryCount);
87
- console.log(`Retrying in ${delay}ms...`);
88
- setTimeout(() => checkAuth(retryCount + 1), delay);
89
- } else {
90
- setUser(null);
91
- setError('Failed to connect to server');
92
- setIsLoading(false);
93
- }
94
- }
95
- }, []);
90
+ const fetchAuthStatus = async ({ signal }: { signal: AbortSignal }): Promise<AuthStatus> => {
91
+ const response = await fetch(`${getApiBase()}/status`, {
92
+ credentials: 'include',
93
+ signal,
94
+ });
95
+ if (!response.ok) {
96
+ // Wrap status in the error message so `authShouldRetry` can detect
97
+ // 401/403 without parsing the original Response.
98
+ throw new Error(`auth.status: HTTP ${response.status}`);
99
+ }
100
+ return response.json() as Promise<AuthStatus>;
101
+ };
102
+
103
+ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
104
+ const queryClient = useQueryClient();
96
105
 
97
- // Check auth status on mount
98
- useEffect(() => {
99
- checkAuth();
100
- }, [checkAuth]);
106
+ // Bootstrap auth-status query. The `signal` plumbed through `queryFn`
107
+ // is automatically aborted when the component unmounts (Strict Mode
108
+ // double-mount lifecycle handled by TanStack Query, see
109
+ // https://tanstack.com/query/v5/docs/react/guides/cancellation).
110
+ const authQuery = useQuery({
111
+ queryKey: AUTH_STATUS_QUERY_KEY,
112
+ queryFn: fetchAuthStatus,
113
+ retry: authShouldRetry,
114
+ retryDelay: authRetryDelay,
115
+ // Boot-once: never refetch on focus / mount / network reconnect.
116
+ // Logout / login explicitly invalidate.
117
+ staleTime: Infinity,
118
+ refetchOnMount: false,
119
+ refetchOnWindowFocus: false,
120
+ refetchOnReconnect: false,
121
+ });
101
122
 
102
- const login = useCallback(async (email: string, password: string): Promise<boolean> => {
103
- setError(null);
104
- setIsLoading(true);
123
+ const data = authQuery.data;
124
+ const user: User | null = useMemo(() => {
125
+ if (!data) return null;
126
+ if (data.auth_enabled === false) return ANONYMOUS_USER;
127
+ return data.authenticated ? data.user : null;
128
+ }, [data]);
129
+
130
+ const authMode: 'single' | 'multi' = data?.auth_mode ?? 'single';
131
+ const canRegister = data?.can_register ?? false;
132
+ const isAuthenticated = user !== null;
133
+ const isLoading = authQuery.isPending;
134
+ const error = authQuery.isError ? 'Failed to connect to server' : null;
135
+
136
+ const invalidateAuth = useCallback(
137
+ () => queryClient.invalidateQueries({ queryKey: AUTH_STATUS_QUERY_KEY }),
138
+ [queryClient],
139
+ );
105
140
 
141
+ const login = useCallback(async (email: string, password: string): Promise<boolean> => {
106
142
  try {
107
143
  const response = await fetch(`${getApiBase()}/login`, {
108
144
  method: 'POST',
109
145
  headers: { 'Content-Type': 'application/json' },
110
146
  credentials: 'include',
111
- body: JSON.stringify({ email, password })
147
+ body: JSON.stringify({ email, password }),
112
148
  });
149
+ const body = await response.json();
113
150
 
114
- const data = await response.json();
115
-
116
- if (!response.ok) {
117
- setError(data.detail || 'Login failed');
118
- setIsLoading(false);
151
+ if (!response.ok || !body.success || !body.user) {
152
+ // Surface the server's error via the query cache so `error` flips
153
+ // through the same path as a normal `isError` would.
154
+ queryClient.setQueryData<AuthStatus | null>(AUTH_STATUS_QUERY_KEY, null);
119
155
  return false;
120
156
  }
121
157
 
122
- if (data.success && data.user) {
123
- setUser(data.user);
124
- setIsLoading(false);
125
- return true;
126
- }
127
-
128
- setError('Login failed');
129
- setIsLoading(false);
130
- return false;
158
+ // Optimistically write the new user into the cache so the UI
159
+ // updates this render; then invalidate so the next refetch
160
+ // picks up server-derived fields (auth_mode, can_register).
161
+ queryClient.setQueryData<AuthStatus>(AUTH_STATUS_QUERY_KEY, {
162
+ auth_enabled: true,
163
+ auth_mode: authMode,
164
+ authenticated: true,
165
+ user: body.user,
166
+ can_register: false,
167
+ });
168
+ await invalidateAuth();
169
+ return true;
131
170
  } catch (err) {
132
171
  console.error('Login error:', err);
133
- setError('Failed to connect to server');
134
- setIsLoading(false);
135
172
  return false;
136
173
  }
137
- }, []);
174
+ }, [queryClient, invalidateAuth, authMode]);
138
175
 
139
176
  const register = useCallback(async (
140
177
  email: string,
141
178
  password: string,
142
- displayName: string
179
+ displayName: string,
143
180
  ): Promise<boolean> => {
144
- setError(null);
145
- setIsLoading(true);
146
-
147
181
  try {
148
182
  const response = await fetch(`${getApiBase()}/register`, {
149
183
  method: 'POST',
150
184
  headers: { 'Content-Type': 'application/json' },
151
185
  credentials: 'include',
152
- body: JSON.stringify({ email, password, display_name: displayName })
186
+ body: JSON.stringify({ email, password, display_name: displayName }),
153
187
  });
188
+ const body = await response.json();
154
189
 
155
- const data = await response.json();
156
-
157
- if (!response.ok) {
158
- setError(data.detail || 'Registration failed');
159
- setIsLoading(false);
190
+ if (!response.ok || !body.success || !body.user) {
160
191
  return false;
161
192
  }
162
193
 
163
- if (data.success && data.user) {
164
- setUser(data.user);
165
- setCanRegister(false); // After successful registration in single-owner mode
166
- setIsLoading(false);
167
- return true;
168
- }
169
-
170
- setError('Registration failed');
171
- setIsLoading(false);
172
- return false;
194
+ queryClient.setQueryData<AuthStatus>(AUTH_STATUS_QUERY_KEY, {
195
+ auth_enabled: true,
196
+ auth_mode: authMode,
197
+ authenticated: true,
198
+ user: body.user,
199
+ can_register: false,
200
+ });
201
+ await invalidateAuth();
202
+ return true;
173
203
  } catch (err) {
174
204
  console.error('Register error:', err);
175
- setError('Failed to connect to server');
176
- setIsLoading(false);
177
205
  return false;
178
206
  }
179
- }, []);
207
+ }, [queryClient, invalidateAuth, authMode]);
180
208
 
181
209
  const logout = useCallback(async () => {
182
210
  try {
183
211
  await fetch(`${getApiBase()}/logout`, {
184
212
  method: 'POST',
185
- credentials: 'include'
213
+ credentials: 'include',
186
214
  });
187
215
  } catch (err) {
188
216
  console.error('Logout error:', err);
189
217
  } finally {
190
- setUser(null);
191
- // Re-check auth status to update canRegister
192
- await checkAuth();
218
+ // Force `authenticated: false` immediately so consumers (esp. the
219
+ // WebSocket logout effect) react this render; then refetch so
220
+ // `can_register` and `auth_mode` come back fresh.
221
+ queryClient.setQueryData<AuthStatus>(AUTH_STATUS_QUERY_KEY, (prev) => ({
222
+ ...(prev ?? { auth_enabled: true, auth_mode: 'single' as const, can_register: false }),
223
+ authenticated: false,
224
+ user: null,
225
+ }));
226
+ await invalidateAuth();
193
227
  }
194
- }, [checkAuth]);
228
+ }, [queryClient, invalidateAuth]);
229
+
230
+ const checkAuth = useCallback(async () => {
231
+ await authQuery.refetch();
232
+ }, [authQuery]);
195
233
 
196
- const value: AuthContextType = {
234
+ const value: AuthContextType = useMemo(() => ({
197
235
  user,
198
- isAuthenticated: user !== null,
236
+ isAuthenticated,
199
237
  isLoading,
200
238
  authMode,
201
239
  canRegister,
@@ -203,8 +241,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
203
241
  login,
204
242
  register,
205
243
  logout,
206
- checkAuth
207
- };
244
+ checkAuth,
245
+ }), [user, isAuthenticated, isLoading, authMode, canRegister, error,
246
+ login, register, logout, checkAuth]);
208
247
 
209
248
  return (
210
249
  <AuthContext.Provider value={value}>