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,221 @@
1
+ /**
2
+ * Tests for the TanStack-Query-backed AuthContext.
3
+ *
4
+ * Locks in the contracts that drive perceived launch time + the
5
+ * disconnect-reconnect bug-fix:
6
+ *
7
+ * 1. Anonymous-mode happy path: backend reports `auth_enabled: false`
8
+ * → user is auto-set to the anonymous owner without further work.
9
+ * 2. Retry-then-recover: 503 fails N times then 200 succeeds → user
10
+ * is set, no LoginPage flash.
11
+ * 3. 401 fast-fail: backend returns 401 → query reports error
12
+ * immediately, NO retry budget burned (would otherwise wait 10s).
13
+ * 4. Logout invalidates the cache: after logout the cached data shows
14
+ * `authenticated: false` so the WebSocketContext logout effect
15
+ * fires deterministically.
16
+ *
17
+ * Backoff is verified at the unit level (the AUTH_RETRY constant is
18
+ * used by `lib/connectionConfig.ts`); the E2E backoff curve is covered
19
+ * by the manual flake-test plan in docs-internal/release_build_pipeline.md.
20
+ */
21
+
22
+ import React from 'react';
23
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
24
+ import { render, waitFor, act } from '@testing-library/react';
25
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
26
+ import { AuthProvider, useAuth, AUTH_STATUS_QUERY_KEY } from '../AuthContext';
27
+
28
+ // Mock the API config so the fetch URL is predictable in test logs.
29
+ vi.mock('../../config/api', () => ({
30
+ API_CONFIG: { PYTHON_BASE_URL: 'http://test' },
31
+ }));
32
+
33
+ // Each test gets a fresh QueryClient with retries disabled by default;
34
+ // individual tests opt back into retries to exercise the retry path.
35
+ function makeQueryClient(opts?: { retry?: number }): QueryClient {
36
+ return new QueryClient({
37
+ defaultOptions: {
38
+ queries: { retry: opts?.retry ?? 0, retryDelay: 0 },
39
+ mutations: { retry: 0 },
40
+ },
41
+ });
42
+ }
43
+
44
+ function Wrapper({
45
+ children,
46
+ client,
47
+ }: {
48
+ children: React.ReactNode;
49
+ client: QueryClient;
50
+ }) {
51
+ return (
52
+ <QueryClientProvider client={client}>
53
+ <AuthProvider>{children}</AuthProvider>
54
+ </QueryClientProvider>
55
+ );
56
+ }
57
+
58
+ // Captures the `useAuth()` value across renders so tests can assert on
59
+ // state transitions without remembering to await effects manually.
60
+ function makeProbe() {
61
+ const states: ReturnType<typeof useAuth>[] = [];
62
+ function Probe() {
63
+ states.push(useAuth());
64
+ return null;
65
+ }
66
+ return { states, Probe };
67
+ }
68
+
69
+ beforeEach(() => {
70
+ vi.restoreAllMocks();
71
+ });
72
+
73
+ describe('AuthContext (TanStack Query)', () => {
74
+ it('sets the anonymous user when backend reports auth_enabled: false', async () => {
75
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(
76
+ new Response(
77
+ JSON.stringify({
78
+ auth_enabled: false,
79
+ auth_mode: 'single',
80
+ authenticated: false,
81
+ user: null,
82
+ can_register: false,
83
+ }),
84
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
85
+ ),
86
+ );
87
+
88
+ const client = makeQueryClient();
89
+ const { states, Probe } = makeProbe();
90
+ render(
91
+ <Wrapper client={client}>
92
+ <Probe />
93
+ </Wrapper>,
94
+ );
95
+
96
+ await waitFor(() => {
97
+ const last = states[states.length - 1];
98
+ expect(last.isLoading).toBe(false);
99
+ expect(last.isAuthenticated).toBe(true);
100
+ expect(last.user?.email).toBe('anonymous');
101
+ });
102
+ });
103
+
104
+ it('retries on 503 and surfaces the user on the 200', async () => {
105
+ let attempt = 0;
106
+ vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
107
+ attempt += 1;
108
+ if (attempt < 3) {
109
+ return new Response('upstream not ready', { status: 503 });
110
+ }
111
+ return new Response(
112
+ JSON.stringify({
113
+ auth_enabled: true,
114
+ auth_mode: 'single',
115
+ authenticated: true,
116
+ user: { id: 1, email: 'a@b', display_name: 'A', is_owner: true },
117
+ can_register: false,
118
+ }),
119
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
120
+ );
121
+ });
122
+
123
+ // Allow up to 3 retries here so the third attempt resolves the 200.
124
+ const client = makeQueryClient({ retry: 3 });
125
+ const { states, Probe } = makeProbe();
126
+ render(
127
+ <Wrapper client={client}>
128
+ <Probe />
129
+ </Wrapper>,
130
+ );
131
+
132
+ await waitFor(() => {
133
+ const last = states[states.length - 1];
134
+ expect(last.isAuthenticated).toBe(true);
135
+ expect(last.user?.email).toBe('a@b');
136
+ });
137
+ expect(attempt).toBeGreaterThanOrEqual(3);
138
+ });
139
+
140
+ it('does not retry a 401 — surfaces "not authenticated" immediately', async () => {
141
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
142
+ new Response(JSON.stringify({ detail: 'unauthorized' }), { status: 401 }),
143
+ );
144
+
145
+ // Even with a generous retry budget the AuthContext's `retry`
146
+ // predicate refuses 401/403 — `fetchSpy` should be called exactly
147
+ // ONCE.
148
+ const client = makeQueryClient({ retry: 5 });
149
+ const { states, Probe } = makeProbe();
150
+ render(
151
+ <Wrapper client={client}>
152
+ <Probe />
153
+ </Wrapper>,
154
+ );
155
+
156
+ await waitFor(() => {
157
+ const last = states[states.length - 1];
158
+ expect(last.isLoading).toBe(false);
159
+ expect(last.isAuthenticated).toBe(false);
160
+ expect(last.error).not.toBeNull();
161
+ });
162
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
163
+ });
164
+
165
+ it('logout invalidates the auth-status cache and flips authenticated → false', async () => {
166
+ // Initial auth-enabled, logged-in user.
167
+ vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
168
+ const url = typeof input === 'string' ? input : (input as Request).url;
169
+ if (url.endsWith('/logout')) {
170
+ return new Response('', { status: 200 });
171
+ }
172
+ // /status — return a logged-in user the first call, an
173
+ // unauthenticated response on every subsequent call (after logout
174
+ // invalidates the cache and refetches).
175
+ return new Response(
176
+ JSON.stringify({
177
+ auth_enabled: true,
178
+ auth_mode: 'single',
179
+ authenticated: false,
180
+ user: null,
181
+ can_register: false,
182
+ }),
183
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
184
+ );
185
+ });
186
+
187
+ const client = makeQueryClient();
188
+ // Seed the cache with an authenticated user so we can observe the
189
+ // transition triggered by `logout()`. This mirrors the real flow:
190
+ // on first mount the query resolves authenticated, then later
191
+ // logout flips it.
192
+ client.setQueryData([...AUTH_STATUS_QUERY_KEY], {
193
+ auth_enabled: true,
194
+ auth_mode: 'single',
195
+ authenticated: true,
196
+ user: { id: 1, email: 'a@b', display_name: 'A', is_owner: true },
197
+ can_register: false,
198
+ });
199
+
200
+ const { states, Probe } = makeProbe();
201
+ render(
202
+ <Wrapper client={client}>
203
+ <Probe />
204
+ </Wrapper>,
205
+ );
206
+
207
+ await waitFor(() => {
208
+ expect(states[states.length - 1].isAuthenticated).toBe(true);
209
+ });
210
+
211
+ await act(async () => {
212
+ await states[states.length - 1].logout();
213
+ });
214
+
215
+ await waitFor(() => {
216
+ const last = states[states.length - 1];
217
+ expect(last.isAuthenticated).toBe(false);
218
+ expect(last.user).toBeNull();
219
+ });
220
+ });
221
+ });
@@ -18,8 +18,14 @@ const storeState: { currentWorkflow: any } = {
18
18
  currentWorkflow: null,
19
19
  };
20
20
 
21
+ // `useAppStore` is consumed via the documented Zustand slice-selector
22
+ // pattern: `useAppStore((s) => s.currentWorkflow)`. The mock has to
23
+ // thread the selector through so it returns the right slice — calling
24
+ // `useAppStore()` without a selector inside the hook would have given us
25
+ // the whole store object and `currentWorkflow.nodes` would be undefined.
21
26
  vi.mock('../../store/useAppStore', () => ({
22
- useAppStore: () => storeState,
27
+ useAppStore: <T,>(selector?: (state: typeof storeState) => T): T | typeof storeState =>
28
+ selector ? selector(storeState) : storeState,
23
29
  }));
24
30
 
25
31
  // nodeDefinitions/ was deleted in commit fc10fd3 — the hook now
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Tests for useWorkflowOpsListener.
3
+ *
4
+ * Locks the runtime broadcast contract:
5
+ * - subscribes to `workflow_ops_apply` via the WS context
6
+ * addEventListener API
7
+ * - applies events scoped to the current workflow via applyOperations
8
+ * - other-workflow events trigger a sonner toast (no canvas mutation)
9
+ * - unsubscribes on unmount
10
+ */
11
+
12
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
13
+ import { renderHook, act } from '@testing-library/react';
14
+ import type { Node, Edge } from 'reactflow';
15
+
16
+ import { useWorkflowOpsListener } from '../useWorkflowOpsListener';
17
+
18
+ // --- mocks (must come before importing modules that read them) ----------
19
+
20
+ const wsMock = {
21
+ addEventListener: vi.fn<(type: string, handler: (data: any) => void) => () => void>(),
22
+ saveNodeParameters: vi.fn().mockResolvedValue(true),
23
+ };
24
+
25
+ const storeMock = {
26
+ currentWorkflowId: 'wf-current',
27
+ loadWorkflow: vi.fn(),
28
+ };
29
+
30
+ vi.mock('../../contexts/WebSocketContext', () => ({
31
+ useWebSocket: () => wsMock,
32
+ }));
33
+
34
+ vi.mock('../../store/useAppStore', () => ({
35
+ useAppStore: (selector: any) => selector({
36
+ currentWorkflow: { id: storeMock.currentWorkflowId },
37
+ loadWorkflow: storeMock.loadWorkflow,
38
+ }),
39
+ }));
40
+
41
+ const applyOpsMock = vi.fn().mockResolvedValue({ applied: 0, errors: [], refMap: {} });
42
+ vi.mock('../../lib/workflowOps', () => ({
43
+ applyOperations: (...args: any[]) => applyOpsMock(...args),
44
+ }));
45
+
46
+ const toastMessageMock = vi.fn();
47
+ vi.mock('sonner', () => ({
48
+ toast: { message: (...args: any[]) => toastMessageMock(...args) },
49
+ }));
50
+
51
+ // --- test scaffolding ---------------------------------------------------
52
+
53
+ function _ctx(overrides: Partial<{ nodes: Node[]; edges: Edge[] }> = {}) {
54
+ return {
55
+ nodes: overrides.nodes ?? [],
56
+ edges: overrides.edges ?? [],
57
+ setNodes: vi.fn(),
58
+ setEdges: vi.fn(),
59
+ };
60
+ }
61
+
62
+ let registeredHandler: ((data: any) => void) | null = null;
63
+ let unsubscribeMock = vi.fn();
64
+
65
+ beforeEach(() => {
66
+ vi.clearAllMocks();
67
+ registeredHandler = null;
68
+ unsubscribeMock = vi.fn();
69
+ wsMock.addEventListener.mockImplementation((type, handler) => {
70
+ if (type === 'workflow_ops_apply') registeredHandler = handler;
71
+ return unsubscribeMock;
72
+ });
73
+ storeMock.currentWorkflowId = 'wf-current';
74
+ });
75
+
76
+ // ---------------------------------------------------------------------------
77
+
78
+ describe('useWorkflowOpsListener', () => {
79
+ it('subscribes to workflow_ops_apply on mount', () => {
80
+ renderHook(() => useWorkflowOpsListener(_ctx()));
81
+ expect(wsMock.addEventListener).toHaveBeenCalledWith(
82
+ 'workflow_ops_apply',
83
+ expect.any(Function),
84
+ );
85
+ });
86
+
87
+ it('unsubscribes on unmount', () => {
88
+ const { unmount } = renderHook(() => useWorkflowOpsListener(_ctx()));
89
+ expect(unsubscribeMock).not.toHaveBeenCalled();
90
+ unmount();
91
+ expect(unsubscribeMock).toHaveBeenCalledTimes(1);
92
+ });
93
+
94
+ it('applies operations scoped to the current workflow', async () => {
95
+ renderHook(() => useWorkflowOpsListener(_ctx()));
96
+ expect(registeredHandler).not.toBeNull();
97
+
98
+ const ops = [{ type: 'add_node', client_ref: 'n', node_type: 'x', parameters: {} }];
99
+ await act(async () => {
100
+ registeredHandler!({
101
+ workflow_id: 'wf-current',
102
+ caller_node_id: 'agent-1',
103
+ operations: ops,
104
+ });
105
+ });
106
+
107
+ expect(applyOpsMock).toHaveBeenCalledTimes(1);
108
+ expect(applyOpsMock.mock.calls[0][0]).toEqual(ops);
109
+ expect(toastMessageMock).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it('toasts (not applies) when event targets a different workflow', () => {
113
+ renderHook(() => useWorkflowOpsListener(_ctx()));
114
+ expect(registeredHandler).not.toBeNull();
115
+
116
+ act(() => {
117
+ registeredHandler!({
118
+ workflow_id: 'wf-other',
119
+ caller_node_id: 'agent-1',
120
+ operations: [{ type: 'add_node', client_ref: 'n', node_type: 'x', parameters: {} }],
121
+ });
122
+ });
123
+
124
+ expect(applyOpsMock).not.toHaveBeenCalled();
125
+ expect(toastMessageMock).toHaveBeenCalledTimes(1);
126
+ const [title, opts] = toastMessageMock.mock.calls[0];
127
+ expect(title).toMatch(/workflow created/i);
128
+ expect(opts.action.label).toBe('Switch');
129
+ // Switch handler triggers loadWorkflow with the foreign id.
130
+ opts.action.onClick();
131
+ expect(storeMock.loadWorkflow).toHaveBeenCalledWith('wf-other');
132
+ });
133
+
134
+ it('ignores empty-ops events that target the current workflow', async () => {
135
+ renderHook(() => useWorkflowOpsListener(_ctx()));
136
+ await act(async () => {
137
+ registeredHandler!({ workflow_id: 'wf-current', operations: [] });
138
+ });
139
+ expect(applyOpsMock).not.toHaveBeenCalled();
140
+ expect(toastMessageMock).not.toHaveBeenCalled();
141
+ });
142
+ });
@@ -1,17 +1,219 @@
1
+ /**
2
+ * useAppTheme — canvas + map theme accessor.
3
+ *
4
+ * Returns a flat colour pack with the same shape the canvas + map +
5
+ * grandfathered-modal call sites expect (`theme.colors.X`,
6
+ * `theme.isDarkMode`). Under light/dark this is identity-pass; under
7
+ * Renaissance / Greek / Edo / Steampunk / Atomic / Cyber / Wasteland
8
+ * / Rot / Plague / Surveillance the base pack (light or dark, picked
9
+ * by `DARK_FAMILY`) gets a small overlay of theme-specific accents
10
+ * (primary, edges, action colours) so canvas selection rings, edge
11
+ * strokes, and action button colours match the theme's identity.
12
+ *
13
+ * This is the production realisation of MIGRATION_PLAYBOOK Wave 1 —
14
+ * the upstream playbook proposes a full per-theme NodePack record;
15
+ * the overlay form here is the same idea expressed without forking
16
+ * the existing 50-key Colors shape, so the existing 14+ call sites
17
+ * keep compiling and the visual delta lives in one place.
18
+ *
19
+ * Adding a new theme override: drop an entry in `THEME_OVERRIDES`
20
+ * with whichever subset of `Colors` keys you want to override. Empty
21
+ * overrides are a no-op (theme falls back to pure light/dark).
22
+ */
23
+
1
24
  import { useMemo } from 'react';
2
- import { useTheme } from '../contexts/ThemeContext';
25
+ import { useTheme, type ThemeName } from '../contexts/ThemeContext';
3
26
  import { theme as baseTheme, lightColors, darkColors } from '../styles/theme';
4
27
 
28
+ type Colors = typeof lightColors;
29
+ /** Loose form for override entries — `lightColors` is `as const`, which
30
+ * narrows each value to a literal hex string. Themes need to substitute
31
+ * arbitrary hex / rgba strings, so the override map widens to `string`. */
32
+ type ColorOverride = Partial<Record<keyof Colors, string>>;
33
+
34
+ /** Themes whose canvas + chrome read as dark backgrounds. Mirrors the
35
+ * DARK_FAMILY in ThemeContext so the two stay in lockstep. */
36
+ const DARK_BASE_THEMES: ReadonlySet<ThemeName> = new Set([
37
+ 'dark', 'cyber', 'wasteland', 'rot', 'surveillance', 'steampunk',
38
+ ]);
39
+
40
+ /** Theme-specific overlays. Each entry is a partial Colors object —
41
+ * whatever keys appear here override the chosen base pack. Missing
42
+ * keys fall through to lightColors / darkColors. Hex values come
43
+ * from the matching client/src/themes/<theme>.css token block. */
44
+ const THEME_OVERRIDES: Partial<Record<ThemeName, ColorOverride>> = {
45
+ renaissance: {
46
+ primary: '#b8893c', // gold accent
47
+ focus: '#d4a030',
48
+ focusRing: 'rgba(212, 160, 48, 0.35)',
49
+ actionRun: '#4a6818', // olive (success)
50
+ actionDeploy: '#b8893c', // gold
51
+ actionStop: '#8a1410', // crimson
52
+ actionSave: '#d4a030', // gold leaf
53
+ edgeDefault: '#5a3a14', // ink brown
54
+ edgeSelected: '#d4a030', // gold
55
+ edgeCompleted: '#4a6818',
56
+ edgeError: '#8a1410',
57
+ edgeMemoryActive: '#b8893c',
58
+ edgeToolActive: '#c08020',
59
+ },
60
+
61
+ greek: {
62
+ primary: '#284b82', // lapis
63
+ focus: '#284b82',
64
+ focusRing: 'rgba(40, 75, 130, 0.3)',
65
+ actionRun: '#6a7a32', // olive
66
+ actionDeploy: '#284b82', // lapis
67
+ actionStop: '#7a1a18', // oxblood
68
+ actionSave: '#c8a040', // gold
69
+ edgeDefault: '#4a3818', // dark stone
70
+ edgeSelected: '#284b82',
71
+ edgeCompleted: '#6a7a32',
72
+ edgeError: '#7a1a18',
73
+ edgeMemoryActive: '#284b82',
74
+ edgeToolActive: '#c8a040',
75
+ },
76
+
77
+ edo: {
78
+ primary: '#b41e1e', // vermillion
79
+ focus: '#b41e1e',
80
+ focusRing: 'rgba(180, 30, 30, 0.3)',
81
+ actionRun: '#4a6a3a', // bamboo
82
+ actionDeploy: '#b41e1e',
83
+ actionStop: '#b41e1e',
84
+ actionSave: '#1a1410', // sumi
85
+ edgeDefault: '#1a1410',
86
+ edgeSelected: '#b41e1e',
87
+ edgeCompleted: '#4a6a3a',
88
+ edgeError: '#b41e1e',
89
+ edgeMemoryActive: '#b41e1e',
90
+ edgeToolActive: '#c89832',
91
+ },
92
+
93
+ steampunk: {
94
+ primary: '#d8a848', // brass
95
+ focus: '#d8a848',
96
+ focusRing: 'rgba(216, 168, 72, 0.4)',
97
+ actionRun: '#6a8a3a',
98
+ actionDeploy: '#d8a848',
99
+ actionStop: '#8a3a1a', // rust
100
+ actionSave: '#b8602a', // copper
101
+ edgeDefault: '#b88838',
102
+ edgeSelected: '#d8a848',
103
+ edgeCompleted: '#6a8a3a',
104
+ edgeError: '#8a3a1a',
105
+ edgeMemoryActive: '#b8602a',
106
+ edgeToolActive: '#d8a848',
107
+ },
108
+
109
+ atomic: {
110
+ primary: '#e85a26', // atomic orange
111
+ focus: '#e85a26',
112
+ focusRing: 'rgba(232, 90, 38, 0.4)',
113
+ actionRun: '#5a8a5a',
114
+ actionDeploy: '#e85a26',
115
+ actionStop: '#e85a26',
116
+ actionSave: '#3a9aa0', // turquoise
117
+ edgeDefault: '#2a3a4a', // slate
118
+ edgeSelected: '#e85a26',
119
+ edgeCompleted: '#5a8a5a',
120
+ edgeError: '#e85a26',
121
+ edgeMemoryActive: '#3a9aa0',
122
+ edgeToolActive: '#d8a838', // mustard
123
+ },
124
+
125
+ cyber: {
126
+ primary: '#f51eb6', // neon magenta
127
+ focus: '#1dd9e5', // neon cyan
128
+ focusRing: 'rgba(245, 30, 182, 0.5)',
129
+ actionRun: '#26d97a', // neon green
130
+ actionDeploy: '#f51eb6',
131
+ actionStop: '#ff2050',
132
+ actionSave: '#1dd9e5',
133
+ edgeDefault: '#f51eb6',
134
+ edgeSelected: '#1dd9e5',
135
+ edgeCompleted: '#26d97a',
136
+ edgeError: '#ff2050',
137
+ edgeMemoryActive: '#f51eb6',
138
+ edgeToolActive: '#ffd028', // neon yellow
139
+ },
140
+
141
+ wasteland: {
142
+ primary: '#e88a28', // ochre
143
+ focus: '#e88a28',
144
+ focusRing: 'rgba(232, 138, 40, 0.45)',
145
+ actionRun: '#8a9028',
146
+ actionDeploy: '#e88a28',
147
+ actionStop: '#b8281a', // rust red
148
+ actionSave: '#c8d038', // radioactive
149
+ edgeDefault: '#8a3a18',
150
+ edgeSelected: '#e88a28',
151
+ edgeCompleted: '#8a9028',
152
+ edgeError: '#b8281a',
153
+ edgeMemoryActive: '#e88a28',
154
+ edgeToolActive: '#c8d038',
155
+ },
156
+
157
+ rot: {
158
+ primary: '#78c878', // moss bloom
159
+ focus: '#78c878',
160
+ focusRing: 'rgba(120, 200, 120, 0.4)',
161
+ actionRun: '#78c878',
162
+ actionDeploy: '#e8a838', // candleflame
163
+ actionStop: '#a83838',
164
+ actionSave: '#e8a838',
165
+ edgeDefault: '#4a6a4a',
166
+ edgeSelected: '#78c878',
167
+ edgeCompleted: '#78c878',
168
+ edgeError: '#a83838',
169
+ edgeMemoryActive: '#e8a838',
170
+ edgeToolActive: '#5898b8',
171
+ },
172
+
173
+ plague: {
174
+ primary: '#783c28', // dried blood
175
+ focus: '#783c28',
176
+ focusRing: 'rgba(120, 60, 40, 0.4)',
177
+ actionRun: '#5a7028',
178
+ actionDeploy: '#783c28',
179
+ actionStop: '#783c28',
180
+ actionSave: '#98a838', // bile
181
+ edgeDefault: '#2a1c14',
182
+ edgeSelected: '#783c28',
183
+ edgeCompleted: '#5a7028',
184
+ edgeError: '#783c28',
185
+ edgeMemoryActive: '#98a838',
186
+ edgeToolActive: '#4a5868',
187
+ },
188
+
189
+ surveillance: {
190
+ primary: '#e82626', // REC red
191
+ focus: '#e82626',
192
+ focusRing: 'rgba(232, 38, 38, 0.45)',
193
+ actionRun: '#6acc6a', // phosphor green
194
+ actionDeploy: '#e82626',
195
+ actionStop: '#e82626',
196
+ actionSave: '#6acc6a',
197
+ edgeDefault: '#5a5e62',
198
+ edgeSelected: '#e82626',
199
+ edgeCompleted: '#6acc6a',
200
+ edgeError: '#e82626',
201
+ edgeMemoryActive: '#e82626',
202
+ edgeToolActive: '#5a8cc8',
203
+ },
204
+ };
205
+
5
206
  export const useAppTheme = () => {
6
- const { isDarkMode } = useTheme();
207
+ const { theme } = useTheme();
208
+ const isDarkMode = DARK_BASE_THEMES.has(theme);
7
209
 
8
- const dynamicTheme = useMemo(() => {
210
+ return useMemo(() => {
211
+ const base = isDarkMode ? darkColors : lightColors;
212
+ const overrides = THEME_OVERRIDES[theme] ?? {};
9
213
  return {
10
214
  ...baseTheme,
11
- colors: isDarkMode ? darkColors : lightColors,
215
+ colors: { ...base, ...overrides } as Colors,
12
216
  isDarkMode,
13
217
  };
14
- }, [isDarkMode]);
15
-
16
- return dynamicTheme;
218
+ }, [theme, isDarkMode]);
17
219
  };
@@ -27,10 +27,15 @@ import type { Node, Edge, Connection } from 'reactflow';
27
27
  import { useWebSocket } from '../contexts/WebSocketContext';
28
28
  import { useUserSettingsQuery } from './useUserSettingsQuery';
29
29
  import { applyOperations, type WorkflowOperation } from '../lib/workflowOps';
30
+ import { getCachedNodeSpec } from '../lib/nodeSpec';
30
31
 
31
- const MASTER_SKILL_TYPE = 'masterSkill';
32
32
  const SKILL_HANDLE = 'input-skill';
33
33
 
34
+ const isMasterSkillNode = (nodeType: string | undefined): boolean => {
35
+ if (!nodeType) return false;
36
+ return (getCachedNodeSpec(nodeType)?.uiHints as any)?.isMasterSkillEditor === true;
37
+ };
38
+
34
39
  interface UseAutoSkillEdgesProps {
35
40
  baseOnConnect: (params: Edge | Connection) => void;
36
41
  baseOnEdgesDelete: (deleted: Edge[]) => void;
@@ -73,7 +78,7 @@ export function useAutoSkillEdges({
73
78
  e => e.target === targetId && e.targetHandle === SKILL_HANDLE,
74
79
  );
75
80
  const masterSkillNode = skillEdge && nodes.find(
76
- n => n.id === skillEdge.source && n.type === MASTER_SKILL_TYPE,
81
+ n => n.id === skillEdge.source && isMasterSkillNode(n.type),
77
82
  );
78
83
  const masterSkillId = masterSkillNode?.id ?? null;
79
84
  // node.data only stores the label (see CLAUDE.md "Node Data