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
@@ -15,7 +15,7 @@ from urllib.parse import urljoin
15
15
  from pydantic import BaseModel, ConfigDict, Field
16
16
 
17
17
  from core.logging import get_logger
18
- from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
18
+ from services.plugin import ActionNode, NodeContext, NodeUserError, Operation, TaskQueue
19
19
 
20
20
  logger = get_logger(__name__)
21
21
 
@@ -296,7 +296,7 @@ class CrawleeScraperNode(ActionNode):
296
296
  async def scrape(self, ctx: NodeContext, params: CrawleeScraperParams) -> CrawleeScraperOutput:
297
297
  url = (params.url or "").strip()
298
298
  if not url:
299
- raise RuntimeError("URL is required")
299
+ raise NodeUserError("URL is required")
300
300
 
301
301
  crawler_type = params.crawler_type
302
302
  mode = params.mode
@@ -326,16 +326,16 @@ class CrawleeScraperNode(ActionNode):
326
326
  max_concurrency, timeout_secs,
327
327
  )
328
328
  else:
329
- raise RuntimeError(f"Unknown crawler type: {crawler_type}")
329
+ raise NodeUserError(f"Unknown crawler type: {crawler_type}")
330
330
  except ImportError as e:
331
331
  msg = str(e).lower()
332
332
  if 'playwright' in msg:
333
- raise RuntimeError(
333
+ raise NodeUserError(
334
334
  "Playwright not installed. Run: "
335
335
  "pip install 'crawlee[playwright]' && playwright install chromium",
336
336
  )
337
337
  if 'crawlee' in msg:
338
- raise RuntimeError(
338
+ raise NodeUserError(
339
339
  "Crawlee not installed. Run: pip install 'crawlee[beautifulsoup]'",
340
340
  )
341
341
  raise
@@ -36,6 +36,10 @@ class BraveSearchCredential(ApiKeyCredential):
36
36
  key_name = "X-Subscription-Token"
37
37
  key_location = "header"
38
38
  docs_url = "https://api.search.brave.com/app/keys"
39
+ # Lightweight probe — minimal query, count=1 just to confirm the
40
+ # token authenticates against the web-search endpoint.
41
+ probe_url = "https://api.search.brave.com/res/v1/web/search"
42
+ probe_params = {"q": "ping", "count": 1}
39
43
 
40
44
 
41
45
  class BraveSearchResult(BaseModel):
@@ -30,6 +30,15 @@ class PerplexityCredential(ApiKeyCredential):
30
30
  key_name = "Authorization"
31
31
  key_location = "bearer"
32
32
  docs_url = "https://docs.perplexity.ai/guides/getting-started"
33
+ # Cheapest valid request: minimal completion, max_tokens=1.
34
+ # Sonar is the always-available default model -- no entitlement gate.
35
+ probe_url = "https://api.perplexity.ai/chat/completions"
36
+ probe_method = "POST"
37
+ probe_json = {
38
+ "model": "sonar",
39
+ "messages": [{"role": "user", "content": "ping"}],
40
+ "max_tokens": 1,
41
+ }
33
42
 
34
43
 
35
44
  class PerplexityResult(BaseModel):
@@ -23,6 +23,9 @@ class SerperCredential(ApiKeyCredential):
23
23
  key_name = "X-API-KEY"
24
24
  key_location = "header"
25
25
  docs_url = "https://serper.dev/api-key"
26
+ probe_url = "https://google.serper.dev/search"
27
+ probe_method = "POST"
28
+ probe_json = {"q": "ping", "num": 1}
26
29
 
27
30
 
28
31
  class SerperSearchResult(BaseModel):
@@ -68,6 +68,18 @@ class SimpleMemoryParams(BaseModel):
68
68
  description="Number of relevant memories to retrieve from long-term storage",
69
69
  json_schema_extra={"displayOptions": {"show": {"long_term_enabled": [True]}}},
70
70
  )
71
+ last_session_id: Optional[str] = Field(
72
+ default=None,
73
+ title="Last Claude Session ID",
74
+ description=(
75
+ "Internal: the session UUID claude returned on the most "
76
+ "recent successful run. Drives `--resume <UUID>` on the "
77
+ "next spawn so claude finds and continues its own JSONL "
78
+ "transcript on disk. Hidden from the UI; clearing the "
79
+ "memory wipes this too."
80
+ ),
81
+ json_schema_extra={"hidden": True},
82
+ )
71
83
 
72
84
  model_config = ConfigDict(extra="ignore")
73
85
 
@@ -7,7 +7,7 @@ Email, Matrix, Teams).
7
7
 
8
8
  Imported by :class:`nodes.social.social_receive.SocialReceiveNode` and
9
9
  :class:`nodes.social.social_send.SocialSendNode`. Calls into
10
- ``services.whatsapp_service`` for the WhatsApp bridge stay unchanged — moving
10
+ ``nodes.whatsapp._service`` for the WhatsApp bridge stay unchanged — moving
11
11
  them out is a separate refactor.
12
12
  """
13
13
 
@@ -475,7 +475,7 @@ async def _send_via_whatsapp(
475
475
 
476
476
  Maps socialSend parameters to whatsappSend parameters.
477
477
  """
478
- from services.whatsapp_service import handle_whatsapp_send as whatsapp_send_handler
478
+ from nodes.whatsapp._service import handle_whatsapp_send as whatsapp_send_handler
479
479
 
480
480
  # Map socialSend params to whatsappSend format
481
481
  whatsapp_params = {
@@ -0,0 +1,46 @@
1
+ """Plugins for the 'payments' palette group — Stripe.
2
+
3
+ Self-contained Wave 12 plugin. All boilerplate (lifecycle WS handlers,
4
+ status refresh, signature verification, CLI subprocess) lives in
5
+ ``services.events``. This package contributes only the Stripe-specific
6
+ shapes: command builder, secret-capture regex, output reshape, the
7
+ livemode filter, and the credential class.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from services.events import make_status_refresh, register_webhook_source
13
+ from services.node_output_schemas import register_output_schema
14
+ from services.status_broadcaster import register_service_refresh
15
+ from services.ws_handler_registry import register_ws_handlers
16
+
17
+ from ._credentials import StripeCredential
18
+ from ._handlers import WS_HANDLERS
19
+ from ._source import (
20
+ StripeListenSource,
21
+ StripeWebhookSource,
22
+ get_listen_source,
23
+ get_webhook_source,
24
+ )
25
+
26
+ from .stripe_action import StripeActionNode, StripeActionOutput
27
+ from .stripe_receive import StripeReceiveNode, StripeReceiveOutput
28
+
29
+
30
+ register_ws_handlers(WS_HANDLERS)
31
+ register_webhook_source(get_webhook_source())
32
+ register_service_refresh(make_status_refresh(
33
+ get_listen_source(), status_key="stripe", broadcast_type="stripe_status",
34
+ ))
35
+ register_output_schema("stripeReceive", StripeReceiveOutput)
36
+ register_output_schema("stripeAction", StripeActionOutput)
37
+
38
+
39
+ __all__ = [
40
+ "StripeCredential",
41
+ "StripeListenSource",
42
+ "StripeWebhookSource",
43
+ "WS_HANDLERS",
44
+ "get_listen_source",
45
+ "get_webhook_source",
46
+ ]
@@ -0,0 +1,33 @@
1
+ """Stripe credential — thin marker.
2
+
3
+ The Stripe CLI manages its own auth state at
4
+ ``~/.config/stripe/config.toml`` (populated by ``stripe login`` and
5
+ cleared by ``stripe logout``). MachinaOs doesn't store an API key
6
+ itself — only the captured webhook signing secret rides along as an
7
+ extra field so :class:`StripeWebhookSource` can verify
8
+ ``Stripe-Signature`` on incoming events.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Dict
14
+
15
+ from services.plugin.credential import Credential
16
+
17
+
18
+ class StripeCredential(Credential):
19
+ id = "stripe"
20
+ display_name = "Stripe"
21
+ category = "Payments"
22
+ icon = "asset:stripe"
23
+ auth = "custom"
24
+ docs_url = "https://stripe.com/docs/cli"
25
+
26
+ @classmethod
27
+ async def resolve(cls, *, user_id: str = "owner") -> Dict[str, Any]:
28
+ """Return only the captured webhook signing secret. There is
29
+ no api_key — auth lives in the Stripe CLI's config file."""
30
+ from services.plugin.deps import get_auth_service
31
+
32
+ secret = await get_auth_service().get_api_key("stripe_webhook_secret")
33
+ return {"stripe_webhook_secret": secret} if secret else {}
@@ -0,0 +1,270 @@
1
+ """Stripe WebSocket handlers.
2
+
3
+ Login is a thin wrap around the Stripe CLI's two machine-friendly
4
+ flags:
5
+
6
+ * ``stripe login --non-interactive`` prints
7
+ ``{browser_url, verification_code, next_step}`` JSON and exits.
8
+ * ``stripe login --complete <next_step>`` polls Stripe until the
9
+ user authorises in the browser, then writes credentials to
10
+ ``~/.config/stripe/config.toml`` and exits 0.
11
+
12
+ The ``stripe_login`` handler runs step 1 synchronously, returns the
13
+ URL and verification code to the frontend (same shape as Twitter /
14
+ Google ``oauth_login`` handlers), then fires step 2 as a background
15
+ task. When step 2 finishes, we kick the broadcaster's stripe-status
16
+ refresh callback so the modal updates reactively.
17
+
18
+ Every other lifecycle command (connect/disconnect/reconnect/status)
19
+ comes from :func:`services.events.make_lifecycle_handlers`.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import json
26
+ import shlex
27
+ from typing import Any, Dict
28
+
29
+ from fastapi import WebSocket
30
+
31
+ from core.logging import get_logger
32
+ from services.events import make_lifecycle_handlers, run_cli_command
33
+
34
+ from ._install import ensure_stripe_cli, stripe_cli_path
35
+ from ._source import (
36
+ get_listen_source,
37
+ is_logged_in,
38
+ stripe_config_path,
39
+ )
40
+
41
+ logger = get_logger(__name__)
42
+
43
+
44
+ _LOGIN_TIMEOUT_SECONDS = 600
45
+
46
+
47
+ async def _resolved_binary() -> str | None:
48
+ """Resolve the stripe binary path, downloading on first use.
49
+ Returns None on install failure (caller should surface error)."""
50
+ try:
51
+ return str(await ensure_stripe_cli())
52
+ except Exception as e:
53
+ logger.warning("[Stripe] CLI install failed: %s", e)
54
+ return None
55
+
56
+
57
+ async def _status_snapshot() -> Dict[str, Any]:
58
+ """Compose the daemon-status + login-state dict the modal renders."""
59
+ src = get_listen_source()
60
+ status = await src.status()
61
+ status["logged_in"] = is_logged_in()
62
+ status["connected"] = bool(status.get("running")) and status["logged_in"]
63
+ return status
64
+
65
+
66
+ # --- Catalogue-stored marker -------------------------------------------------
67
+ #
68
+ # The catalogue handler in routers/websocket.py keys its "stored" check off
69
+ # `auth_service.get_oauth_tokens(status_hook)` for any provider with
70
+ # `status_hook` set (the Google / Twitter pattern). The Stripe CLI manages
71
+ # its own auth at ~/.config/stripe/config.toml — there are no real OAuth
72
+ # tokens for us to store. We persist a synthetic marker via the same API
73
+ # Google/Twitter use, so the catalogue's existing logic flips
74
+ # `stored: true` after login without any node-specific code in the
75
+ # catalogue handler.
76
+
77
+ _MARKER_TOKEN = "cli-managed"
78
+
79
+
80
+ async def _mark_logged_in() -> None:
81
+ from services.plugin.deps import get_auth_service
82
+ await get_auth_service().store_oauth_tokens(
83
+ provider="stripe",
84
+ access_token=_MARKER_TOKEN,
85
+ refresh_token=_MARKER_TOKEN,
86
+ )
87
+
88
+
89
+ async def _mark_logged_out() -> None:
90
+ from services.plugin.deps import get_auth_service
91
+ await get_auth_service().remove_oauth_tokens("stripe")
92
+
93
+
94
+ async def _broadcast_credential_event(event_type: str) -> None:
95
+ """Emit a CloudEvents-shaped credential mutation broadcast.
96
+
97
+ Wraps :class:`services.events.envelope.WorkflowEvent` via the canonical
98
+ helper :func:`StatusBroadcaster.broadcast_credential_event` — same
99
+ invariant that ``handle_save_api_key`` / ``handle_twitter_logout`` are
100
+ locked to in ``tests/credentials/test_credential_broadcasts.py``. The
101
+ frontend listens on ``case 'credential_catalogue_updated'`` (the
102
+ helper's outer wire-format type) and invalidates the catalogue query.
103
+ """
104
+ from services.status_broadcaster import get_status_broadcaster
105
+ await get_status_broadcaster().broadcast_credential_event(
106
+ event_type, provider="stripe",
107
+ )
108
+
109
+
110
+ async def handle_stripe_trigger(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
111
+ """Run ``stripe trigger <event>`` for synthetic test events."""
112
+ event = data.get("event")
113
+ if not event:
114
+ return {"success": False, "error": "event required (e.g. 'charge.succeeded')"}
115
+ binary = await _resolved_binary()
116
+ if not binary:
117
+ return {"success": False, "error": "Stripe CLI install failed"}
118
+ return await run_cli_command(binary=binary, argv=["trigger", event])
119
+
120
+
121
+ async def handle_stripe_login(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
122
+ """Step 1 of CLI OAuth: get the browser URL + verification code."""
123
+ logger.info("[Stripe] login flow: step 1/2 (--non-interactive) starting")
124
+ binary = await _resolved_binary()
125
+ if not binary:
126
+ logger.warning("[Stripe] login failed: CLI binary unavailable")
127
+ return {
128
+ "success": False,
129
+ "error": "Stripe CLI install failed. Manual install: https://stripe.com/docs/stripe-cli#install",
130
+ }
131
+ logger.info("[Stripe] using binary: %s", binary)
132
+ result = await run_cli_command(
133
+ binary=binary, argv=["login", "--non-interactive"], timeout=10.0,
134
+ )
135
+ if not result["success"]:
136
+ logger.warning(
137
+ "[Stripe] login step 1 CLI failure: %s | stderr=%r",
138
+ result.get("error"), (result.get("stderr") or "")[:300],
139
+ )
140
+ return result
141
+ try:
142
+ info = json.loads(result["stdout"])
143
+ except json.JSONDecodeError as e:
144
+ logger.warning("[Stripe] login step 1 unparseable stdout=%r", result["stdout"][:300])
145
+ return {"success": False, "error": f"unparseable stripe login response: {e}"}
146
+
147
+ next_step_raw = info.get("next_step")
148
+ url = info.get("browser_url") or info.get("url")
149
+ if not (url and next_step_raw):
150
+ logger.warning("[Stripe] login response missing url/next_step: keys=%s", list(info.keys()))
151
+ return {"success": False, "error": "stripe login response missing browser_url / next_step"}
152
+
153
+ # ``next_step`` is the LITERAL shell command the user would otherwise
154
+ # type, e.g.:
155
+ # stripe login --complete 'https://dashboard.stripe.com/stripecli/auth/…?secret=…'
156
+ # Feeding the whole string into ``--complete`` makes the CLI try to
157
+ # URL-parse it and bail with "first path segment in URL cannot contain
158
+ # colon". Tokenise it and pass just the auth URL (the last argument)
159
+ # to ``--complete``.
160
+ try:
161
+ complete_url = shlex.split(next_step_raw)[-1]
162
+ except (ValueError, IndexError):
163
+ complete_url = next_step_raw
164
+ if not complete_url.startswith("http"):
165
+ logger.warning(
166
+ "[Stripe] could not extract auth URL from next_step=%r — falling back to raw value",
167
+ next_step_raw,
168
+ )
169
+ complete_url = next_step_raw
170
+
171
+ logger.info(
172
+ "[Stripe] login step 1 ok: code=%s, browser_url issued — opening on frontend; "
173
+ "spawning step 2 (--complete) in background (timeout=%ss)",
174
+ info.get("verification_code"), _LOGIN_TIMEOUT_SECONDS,
175
+ )
176
+ asyncio.create_task(_complete_login(binary, complete_url))
177
+ return {
178
+ "success": True,
179
+ "url": url,
180
+ "verification_code": info.get("verification_code"),
181
+ }
182
+
183
+
184
+ async def _complete_login(binary: str, next_step: str) -> None:
185
+ """Step 2: block on ``stripe login --complete`` until the user
186
+ authorises (or the 10-min timeout fires). On success, write the
187
+ same kind of marker the Google / Twitter callbacks write so the
188
+ catalogue's stored-check flips, then auto-start the listen
189
+ daemon and trigger the generic catalogue refresh on the frontend."""
190
+ logger.info("[Stripe] login step 2/2 polling for browser confirmation")
191
+ try:
192
+ result = await run_cli_command(
193
+ binary=binary, argv=["login", "--complete", next_step],
194
+ timeout=_LOGIN_TIMEOUT_SECONDS,
195
+ )
196
+ except Exception as e:
197
+ # ``asyncio.create_task`` swallows exceptions silently — log them.
198
+ # Nothing was persisted, so no broadcast: catalogue state is unchanged.
199
+ logger.exception("[Stripe] login step 2 raised unexpectedly: %s", e)
200
+ return
201
+
202
+ if not result.get("success"):
203
+ logger.warning(
204
+ "[Stripe] login step 2 CLI failure: %s | stderr=%r",
205
+ result.get("error"), (result.get("stderr") or "")[:300],
206
+ )
207
+
208
+ if not is_logged_in():
209
+ logger.warning(
210
+ "[Stripe] step 2 finished but ``is_logged_in()`` is False — config file %s missing/empty; "
211
+ "user likely closed the browser before authorising",
212
+ stripe_config_path(),
213
+ )
214
+ return
215
+
216
+ logger.info(
217
+ "[Stripe] auth successful — credentials written to %s; persisting catalogue marker + starting listen daemon",
218
+ stripe_config_path(),
219
+ )
220
+ await _mark_logged_in()
221
+ logger.info("[Stripe] catalogue marker token persisted (auth_service.store_oauth_tokens)")
222
+ start_result = await get_listen_source().start()
223
+ if start_result.get("success"):
224
+ logger.info("[Stripe] listen daemon started (pid=%s)", start_result.get("status", {}).get("pid"))
225
+ else:
226
+ logger.warning("[Stripe] listen daemon failed to start: %s", start_result.get("error"))
227
+ await _broadcast_credential_event("credential.oauth.connected")
228
+
229
+
230
+ async def handle_stripe_logout(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
231
+ """Stop the daemon (if running) and run ``stripe logout --all`` to
232
+ clear ``~/.config/stripe/config.toml``. Mirror the Google/Twitter
233
+ logout shape: drop the catalogue marker token + broadcast the
234
+ generic catalogue invalidation so the modal flips immediately."""
235
+ logger.info("[Stripe] logout starting: stopping daemon + running 'stripe logout --all'")
236
+ await get_listen_source().stop()
237
+ cached = stripe_cli_path()
238
+ if cached is None:
239
+ cfg = stripe_config_path()
240
+ if cfg.exists():
241
+ cfg.unlink(missing_ok=True)
242
+ result: Dict[str, Any] = {"success": True, "message": "Logged out (CLI not yet installed; cleared config file)"}
243
+ logger.info("[Stripe] logout fallback: CLI not installed; deleted %s", cfg)
244
+ else:
245
+ result = await run_cli_command(binary=str(cached), argv=["logout", "--all"], timeout=10.0)
246
+ logger.info(
247
+ "[Stripe] logout CLI result: success=%s err=%s",
248
+ result.get("success"), result.get("error"),
249
+ )
250
+ await _mark_logged_out()
251
+ await _broadcast_credential_event("credential.oauth.disconnected")
252
+ logger.info("[Stripe] logout complete: marker token removed + catalogue broadcast sent")
253
+ return result
254
+
255
+
256
+ async def handle_stripe_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
257
+ """Augments the stock daemon-status with login-state."""
258
+ return {"success": True, "status": await _status_snapshot()}
259
+
260
+
261
+ WS_HANDLERS = make_lifecycle_handlers(
262
+ prefix="stripe",
263
+ source=get_listen_source(),
264
+ extra={
265
+ "stripe_login": handle_stripe_login,
266
+ "stripe_logout": handle_stripe_logout,
267
+ "stripe_trigger": handle_stripe_trigger,
268
+ },
269
+ )
270
+ WS_HANDLERS["stripe_status"] = handle_stripe_status
@@ -0,0 +1,127 @@
1
+ """Stripe CLI auto-installer.
2
+
3
+ On first use, downloads the official Stripe CLI binary from GitHub
4
+ releases (https://github.com/stripe/stripe-cli/releases) and caches
5
+ it under the workspace dir at ``_stripe/bin/stripe[.exe]``. A system
6
+ install on PATH (brew / scoop / apt / direct binary) is preferred —
7
+ the download path only fires when no system binary is found.
8
+
9
+ Pin a version here when bumping; pre-built archives are signed by
10
+ Stripe and served over GitHub's CDN.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import io
17
+ import platform
18
+ import shutil
19
+ import stat
20
+ import tarfile
21
+ import zipfile
22
+ from pathlib import Path
23
+ from typing import Optional, Tuple
24
+
25
+ import httpx
26
+
27
+ from core.logging import get_logger
28
+
29
+ logger = get_logger(__name__)
30
+
31
+
32
+ _VERSION = "1.40.9"
33
+ _RELEASE_BASE = f"https://github.com/stripe/stripe-cli/releases/download/v{_VERSION}"
34
+
35
+ # (system, machine) → (asset filename, archive type, member name to extract)
36
+ _ASSETS: dict[Tuple[str, str], Tuple[str, str, str]] = {
37
+ ("Windows", "AMD64"): (f"stripe_{_VERSION}_windows_x86_64.zip", "zip", "stripe.exe"),
38
+ ("Linux", "x86_64"): (f"stripe_{_VERSION}_linux_x86_64.tar.gz", "tar", "stripe"),
39
+ ("Linux", "aarch64"): (f"stripe_{_VERSION}_linux_arm64.tar.gz", "tar", "stripe"),
40
+ ("Linux", "arm64"): (f"stripe_{_VERSION}_linux_arm64.tar.gz", "tar", "stripe"),
41
+ ("Darwin", "x86_64"): (f"stripe_{_VERSION}_mac-os_x86_64.tar.gz", "tar", "stripe"),
42
+ ("Darwin", "arm64"): (f"stripe_{_VERSION}_mac-os_arm64.tar.gz", "tar", "stripe"),
43
+ }
44
+
45
+
46
+ _cached_path: Optional[Path] = None
47
+ _install_lock = asyncio.Lock()
48
+
49
+
50
+ def _bin_dir() -> Path:
51
+ from core.config import Settings
52
+ p = Path(Settings().workspace_base_resolved).resolve() / "_stripe" / "bin"
53
+ p.mkdir(parents=True, exist_ok=True)
54
+ return p
55
+
56
+
57
+ def stripe_cli_path() -> Optional[Path]:
58
+ """Sync getter for the resolved binary path. Returns ``None`` if
59
+ :func:`ensure_stripe_cli` hasn't run yet (or never resolved)."""
60
+ return _cached_path
61
+
62
+
63
+ async def ensure_stripe_cli() -> Path:
64
+ """Return absolute path to the stripe binary, downloading it if
65
+ necessary. Idempotent + concurrent-safe.
66
+
67
+ Resolution order:
68
+ 1. Cached path from a prior call (in-process).
69
+ 2. ``shutil.which("stripe")`` — system install on PATH.
70
+ 3. Workspace-local copy at ``_stripe/bin/stripe[.exe]``.
71
+ 4. Fresh download from GitHub releases under
72
+ :data:`_VERSION`.
73
+ """
74
+ global _cached_path
75
+ if _cached_path and _cached_path.exists():
76
+ return _cached_path
77
+
78
+ sys_path = shutil.which("stripe")
79
+ if sys_path:
80
+ _cached_path = Path(sys_path)
81
+ logger.info("[Stripe] using system CLI at %s", _cached_path)
82
+ return _cached_path
83
+
84
+ binary_name = "stripe.exe" if platform.system() == "Windows" else "stripe"
85
+ target = _bin_dir() / binary_name
86
+
87
+ async with _install_lock:
88
+ if _cached_path and _cached_path.exists():
89
+ return _cached_path
90
+ if target.exists():
91
+ _cached_path = target
92
+ return target
93
+ await _download_release(target)
94
+ _cached_path = target
95
+ return target
96
+
97
+
98
+ async def _download_release(target: Path) -> None:
99
+ key = (platform.system(), platform.machine())
100
+ asset = _ASSETS.get(key)
101
+ if asset is None:
102
+ raise RuntimeError(
103
+ f"No prebuilt Stripe CLI for {key}. Install manually from "
104
+ "https://stripe.com/docs/stripe-cli#install"
105
+ )
106
+ asset_name, kind, member = asset
107
+ url = f"{_RELEASE_BASE}/{asset_name}"
108
+ logger.info("[Stripe] downloading CLI v%s from %s", _VERSION, url)
109
+
110
+ async with httpx.AsyncClient(follow_redirects=True, timeout=120) as client:
111
+ resp = await client.get(url)
112
+ resp.raise_for_status()
113
+ archive = resp.content
114
+
115
+ if kind == "zip":
116
+ with zipfile.ZipFile(io.BytesIO(archive)) as z:
117
+ target.write_bytes(z.read(member))
118
+ else:
119
+ with tarfile.open(fileobj=io.BytesIO(archive), mode="r:gz") as t:
120
+ f = t.extractfile(member)
121
+ if f is None:
122
+ raise RuntimeError(f"Member {member!r} missing from {asset_name}")
123
+ target.write_bytes(f.read())
124
+
125
+ if platform.system() != "Windows":
126
+ target.chmod(target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
127
+ logger.info("[Stripe] CLI installed to %s", target)