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,607 @@
1
+ """`AICliService.run_batch()` — top-level entry for `claude_code_agent` /
2
+ `codex_agent` plugins.
3
+
4
+ Runs N parallel `AICliSession`s under an `asyncio.Semaphore`, mirroring
5
+ the semaphore-+-gather pattern already used in
6
+ `nodes/document/file_downloader.py`. No separate pool class — the
7
+ machinery is small enough to live inline.
8
+
9
+ Per-batch lifecycle:
10
+
11
+ 1. Verify `working_directory` is a git repo (uses `git rev-parse --show-toplevel`).
12
+ 2. Allocate a bearer token, register a `BatchContext` in the MCP server.
13
+ 3. `asyncio.gather` N sessions, each wrapped in `_run_session` with
14
+ try/finally cleanup.
15
+ 4. Aggregate per-task `SessionResult`s into a `BatchResult`.
16
+ 5. Deregister the bearer token in the `finally` so 401s flip on the
17
+ next MCP request after the batch settles.
18
+
19
+ Active sessions are tracked in `_active_sessions[(workflow_id, node_id)]`
20
+ so workflow cancel can target them.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import os
27
+ import time
28
+ from datetime import datetime, timezone
29
+ from pathlib import Path
30
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
31
+
32
+ import anyio
33
+
34
+ from core.logging import get_logger
35
+
36
+ from services.cli_agent.config import get_provider_config
37
+ from services.cli_agent.factory import create_cli_provider
38
+ from services.cli_agent.mcp_server import (
39
+ BatchContext,
40
+ issue_token,
41
+ register_batch,
42
+ unregister_batch,
43
+ )
44
+ from services.cli_agent.protocol import BatchResult, SessionResult
45
+ from services.cli_agent.session import AICliSession
46
+ from services.cli_agent.types import BaseAICliTaskSpec
47
+
48
+ logger = get_logger(__name__)
49
+
50
+
51
+ DEFAULT_MAX_PARALLEL = 5
52
+
53
+ BatchKey = Tuple[str, str] # (workflow_id, node_id)
54
+
55
+
56
+ class AICliService:
57
+ """Singleton service. Use `get_ai_cli_service()` to access."""
58
+
59
+ def __init__(self) -> None:
60
+ # workflow_id+node_id -> live session list (for cancel targeting).
61
+ self._active_sessions: Dict[BatchKey, List[AICliSession]] = {}
62
+ self._lock = asyncio.Lock()
63
+
64
+ # ------------------------------------------------------------------
65
+ # Public API
66
+ # ------------------------------------------------------------------
67
+
68
+ async def run_batch(
69
+ self,
70
+ provider_name: str,
71
+ *,
72
+ tasks: Iterable[BaseAICliTaskSpec],
73
+ node_id: str,
74
+ workflow_id: str,
75
+ workspace_dir: Path,
76
+ broadcaster: Any,
77
+ repo_root: Optional[Path] = None,
78
+ connected_skill_names: Optional[List[str]] = None,
79
+ connected_tools: Optional[List[Dict[str, Any]]] = None,
80
+ connected_memory: Optional[Dict[str, Any]] = None,
81
+ allowed_credentials: Optional[List[str]] = None,
82
+ max_parallel: int = DEFAULT_MAX_PARALLEL,
83
+ mcp_port: Optional[int] = None,
84
+ ) -> BatchResult:
85
+ """Run a list of CLI tasks under one batch.
86
+
87
+ Returns:
88
+ `BatchResult` aggregating per-task `SessionResult`s.
89
+
90
+ Raises:
91
+ ValueError / NotImplementedError: provider unknown / v2-deferred.
92
+ """
93
+ provider = create_cli_provider(provider_name)
94
+ task_list: List[BaseAICliTaskSpec] = list(tasks)
95
+ tool_names = [t.get("node_type") for t in (connected_tools or [])]
96
+ memory_node = (
97
+ connected_memory.get("node_id") if connected_memory else None
98
+ )
99
+ logger.info(
100
+ "[CC-Agent run_batch] enter provider=%s node=%s wf=%s tasks=%d "
101
+ "skills=%s tools=%s creds=%s memory=%s workspace=%s",
102
+ provider_name, node_id, workflow_id, len(task_list),
103
+ connected_skill_names or [], tool_names,
104
+ allowed_credentials or [], memory_node, workspace_dir,
105
+ )
106
+
107
+ # Verify the working directory is under a git repo.
108
+ resolved_repo_root = await self._resolve_repo_root(
109
+ workspace_dir=workspace_dir, override=repo_root,
110
+ )
111
+ if resolved_repo_root is None:
112
+ logger.warning(
113
+ "[CC-Agent run_batch] aborting: workspace=%s is not inside a git "
114
+ "repo (run `git init` there or set `working_directory` to "
115
+ "an existing repo).", workspace_dir,
116
+ )
117
+ return self._abort_not_git_repo(
118
+ provider_name=provider_name,
119
+ tasks=task_list,
120
+ )
121
+ logger.info(
122
+ "[CC-Agent run_batch] resolved repo_root=%s for workspace=%s",
123
+ resolved_repo_root, workspace_dir,
124
+ )
125
+
126
+ # Per-batch bearer token + MCP context
127
+ token = issue_token()
128
+ port = mcp_port or int(os.environ.get("MACHINA_BACKEND_PORT", "3010"))
129
+ ctx = BatchContext(
130
+ workflow_id=workflow_id,
131
+ node_id=node_id,
132
+ workspace_dir=Path(workspace_dir).resolve(),
133
+ connected_skill_names=set(connected_skill_names or []),
134
+ allowed_credentials=set(allowed_credentials or []),
135
+ connected_tools=list(connected_tools or []),
136
+ broadcaster=broadcaster,
137
+ )
138
+ register_batch(token, ctx)
139
+
140
+ cfg = get_provider_config(provider_name)
141
+ defaults = dict(cfg.defaults) if cfg else {}
142
+
143
+ key: BatchKey = (workflow_id, node_id)
144
+ async with self._lock:
145
+ if key in self._active_sessions:
146
+ logger.warning(
147
+ "[CC-Agent service] replacing stale session list for %s", key,
148
+ )
149
+ # Cancel anything previously left dangling.
150
+ for sess in self._active_sessions[key]:
151
+ try:
152
+ await sess.cleanup()
153
+ except Exception:
154
+ pass
155
+ self._active_sessions[key] = []
156
+
157
+ start = time.monotonic()
158
+ await self._broadcast_phase(broadcaster, node_id, workflow_id, "batch_started", {
159
+ "provider": provider_name,
160
+ "n_tasks": len(task_list),
161
+ "max_parallel": max_parallel,
162
+ "isolation": "worktree",
163
+ })
164
+
165
+ sem = asyncio.Semaphore(max(1, int(max_parallel)))
166
+
167
+ async def run_one(task: BaseAICliTaskSpec) -> SessionResult:
168
+ async with sem:
169
+ session = AICliSession(
170
+ provider=provider, task=task,
171
+ repo_root=resolved_repo_root, workspace_dir=workspace_dir,
172
+ node_id=node_id, workflow_id=workflow_id,
173
+ broadcaster=broadcaster, defaults=defaults,
174
+ mcp_port=port, batch_token=token,
175
+ connected_tool_names=[
176
+ t.get("node_type") for t in (connected_tools or [])
177
+ if t.get("node_type")
178
+ ],
179
+ connected_skill_names=list(connected_skill_names or []),
180
+ memory_bound=bool(connected_memory),
181
+ )
182
+ async with self._lock:
183
+ self._active_sessions[key].append(session)
184
+ try:
185
+ try:
186
+ await session.start()
187
+ except FileNotFoundError as exc:
188
+ return self._fail_result(provider_name, task, session.task_id,
189
+ f"cli_not_installed: {exc}")
190
+ except RuntimeError as exc:
191
+ # `_pre_spawn` raises on git-worktree failure.
192
+ return self._fail_result(provider_name, task, session.task_id,
193
+ f"worktree_setup_failed: {exc}")
194
+ except Exception as exc:
195
+ logger.exception("[CC-Agent service] start failed")
196
+ return self._fail_result(provider_name, task, session.task_id,
197
+ f"start_failed: {exc}")
198
+ return await session.wait_for_completion(task.timeout_seconds)
199
+ finally:
200
+ try:
201
+ await session.cleanup()
202
+ except Exception as exc:
203
+ logger.debug("[CC-Agent service] cleanup: %s", exc)
204
+ async with self._lock:
205
+ try:
206
+ self._active_sessions[key].remove(session)
207
+ except (KeyError, ValueError):
208
+ pass
209
+
210
+ try:
211
+ results: List[SessionResult] = await asyncio.gather(
212
+ *(run_one(t) for t in task_list),
213
+ return_exceptions=False,
214
+ )
215
+ finally:
216
+ async with self._lock:
217
+ self._active_sessions.pop(key, None)
218
+ unregister_batch(token)
219
+
220
+ # Memory bridge: persist claude's session_id + append the
221
+ # rendered exchange to simpleMemory's markdown transcript so the
222
+ # next run can `--resume <UUID>` and the UI sees the
223
+ # conversation refresh live. Fire-and-forget — failure here
224
+ # doesn't fail the batch.
225
+ if connected_memory:
226
+ try:
227
+ await self._persist_memory(
228
+ connected_memory, results, broadcaster=broadcaster,
229
+ )
230
+ except Exception as exc: # pragma: no cover — best-effort
231
+ logger.warning(
232
+ "[CC-Agent run_batch] memory persistence failed: %s",
233
+ exc,
234
+ )
235
+
236
+ elapsed_ms = int((time.monotonic() - start) * 1000)
237
+ n_succeeded = sum(1 for r in results if r.success)
238
+ n_failed = len(results) - n_succeeded
239
+
240
+ # Cost roll-up: prefer the provider's reported cost (Claude exposes
241
+ # `total_cost_usd` natively); for providers that don't (Codex,
242
+ # Gemini v2), derive USD from `canonical_usage` via the existing
243
+ # PricingService — a single source of truth for all LLM cost in
244
+ # MachinaOs.
245
+ for r in results:
246
+ if r.cost_usd is None:
247
+ derived = self._derive_cost(r, task_list)
248
+ if derived is not None:
249
+ r.cost_usd = derived
250
+
251
+ costs = [r.cost_usd for r in results]
252
+ total_cost = (
253
+ None if any(c is None for c in costs) else round(sum(c or 0 for c in costs), 6)
254
+ )
255
+
256
+ result = BatchResult(
257
+ tasks=results,
258
+ n_tasks=len(results),
259
+ n_succeeded=n_succeeded,
260
+ n_failed=n_failed,
261
+ total_cost_usd=total_cost,
262
+ wall_clock_ms=elapsed_ms,
263
+ budget_remaining_usd=None,
264
+ provider=provider_name,
265
+ timestamp=datetime.now(timezone.utc).isoformat(),
266
+ )
267
+
268
+ await self._broadcast_phase(broadcaster, node_id, workflow_id, "batch_complete", {
269
+ "provider": provider_name,
270
+ "n_succeeded": n_succeeded,
271
+ "n_failed": n_failed,
272
+ "total_cost_usd": total_cost,
273
+ "wall_clock_ms": elapsed_ms,
274
+ })
275
+ return result
276
+
277
+ async def cancel_workflow(self, workflow_id: str) -> int:
278
+ """Cancel every active session for a workflow. Returns count cancelled."""
279
+ cancelled = 0
280
+ async with self._lock:
281
+ keys = [k for k in self._active_sessions if k[0] == workflow_id]
282
+ sessions: List[AICliSession] = []
283
+ for k in keys:
284
+ sessions.extend(self._active_sessions[k])
285
+ for sess in sessions:
286
+ try:
287
+ await sess.cleanup()
288
+ cancelled += 1
289
+ except Exception as exc:
290
+ logger.debug("[CC-Agent service] cancel: %s", exc)
291
+ return cancelled
292
+
293
+ async def cancel_node(self, node_id: str) -> int:
294
+ """Cancel every active session for a node. Returns count cancelled."""
295
+ cancelled = 0
296
+ async with self._lock:
297
+ keys = [k for k in self._active_sessions if k[1] == node_id]
298
+ sessions: List[AICliSession] = []
299
+ for k in keys:
300
+ sessions.extend(self._active_sessions[k])
301
+ for sess in sessions:
302
+ try:
303
+ await sess.cleanup()
304
+ cancelled += 1
305
+ except Exception as exc:
306
+ logger.debug("[CC-Agent service] cancel: %s", exc)
307
+ return cancelled
308
+
309
+ # ------------------------------------------------------------------
310
+ # Internals
311
+ # ------------------------------------------------------------------
312
+
313
+ @staticmethod
314
+ async def _clear_stale_session_id(
315
+ connected_memory: Dict[str, Any],
316
+ ) -> None:
317
+ """Wipe a stale ``last_session_id`` that no longer maps to any
318
+ JSONL under the current cwd's project dir.
319
+
320
+ Triggered when claude reports ``No conversation found with
321
+ session ID: <UUID>``. Preserves ``memory_content`` (the
322
+ user-visible markdown mirror) since that's informational; only
323
+ the resume UUID is broken.
324
+ """
325
+ from services.plugin.deps import get_database
326
+
327
+ db = get_database()
328
+ memory_node_id = connected_memory["node_id"]
329
+ params = await db.get_node_parameters(memory_node_id) or {}
330
+ prior = params.get("last_session_id")
331
+ if not prior:
332
+ return # already cleared
333
+ params["last_session_id"] = None
334
+ await db.save_node_parameters(memory_node_id, params)
335
+ logger.warning(
336
+ "[CC-Agent _persist_memory] cleared stale last_session_id=%s "
337
+ "from memory_node=%s; next run will spawn a fresh claude "
338
+ "session and persist its new UUID.",
339
+ prior, memory_node_id,
340
+ )
341
+
342
+ @staticmethod
343
+ async def _persist_memory(
344
+ connected_memory: Dict[str, Any],
345
+ results: List[SessionResult],
346
+ broadcaster: Any = None,
347
+ ) -> None:
348
+ """Append each successful run's user prompt + assistant response
349
+ to ``simpleMemory.memory_content`` (markdown). Mirrors aiAgent /
350
+ chatAgent / deep_agent / rlm_agent's persistence pattern exactly
351
+ — same helpers (``append_to_memory_markdown``,
352
+ ``trim_markdown_window``), same field. One DB write.
353
+ """
354
+ successful = [r for r in results if r.success]
355
+ logger.info(
356
+ "[CC-Agent _persist_memory] memory_node=%s results=%d "
357
+ "successful=%d session_ids=%s",
358
+ connected_memory.get("node_id"),
359
+ len(results),
360
+ len(successful),
361
+ [r.session_id for r in successful],
362
+ )
363
+ if not successful:
364
+ logger.warning(
365
+ "[CC-Agent _persist_memory] no successful runs; skipping "
366
+ "save (memory_node=%s). Per-result: %s",
367
+ connected_memory.get("node_id"),
368
+ [
369
+ {"success": r.success, "session_id": r.session_id,
370
+ "error": (r.error or "")[:80]}
371
+ for r in results
372
+ ],
373
+ )
374
+ # Auto-recovery: claude returns
375
+ # ``No conversation found with session ID: <UUID>`` when the
376
+ # `--resume <UUID>` we passed doesn't exist under the current
377
+ # cwd's project dir (most often: a `last_session_id` saved
378
+ # before the cwd-stability fix landed, or a session JSONL
379
+ # that was wiped). Without this clear the same stale UUID
380
+ # would re-fire on every retry and lock the user out
381
+ # forever. The next run after this point will spawn a
382
+ # fresh session and `_persist_memory` will save its UUID.
383
+ stale = any(
384
+ r.error and "No conversation found with session ID" in r.error
385
+ for r in results
386
+ )
387
+ if stale:
388
+ await AICliService._clear_stale_session_id(connected_memory)
389
+ return
390
+
391
+ from services.memory import (
392
+ append_to_memory_markdown,
393
+ trim_markdown_window,
394
+ )
395
+ from services.plugin.deps import get_database
396
+
397
+ db = get_database()
398
+ memory_node_id = connected_memory["node_id"]
399
+ params = await db.get_node_parameters(memory_node_id) or {}
400
+
401
+ # 1. Persist claude's returned session_id from the most recent
402
+ # successful run. Drives `--resume <UUID>` on the next spawn so
403
+ # claude finds and continues its own JSONL transcript on disk.
404
+ last_run = next((r for r in reversed(successful) if r.session_id), None)
405
+ if last_run is not None:
406
+ params["last_session_id"] = last_run.session_id
407
+
408
+ # 2. Update the user-visible markdown mirror.
409
+ content = params.get("memory_content") or (
410
+ "# Conversation History\n\n*No messages yet.*\n"
411
+ )
412
+ for r in successful:
413
+ content = append_to_memory_markdown(content, "human", r.prompt)
414
+ content = append_to_memory_markdown(
415
+ content, "ai", r.response or "",
416
+ )
417
+
418
+ window = int(connected_memory.get("window_size") or 100)
419
+ content, removed_texts = trim_markdown_window(content, window)
420
+ params["memory_content"] = content
421
+
422
+ await db.save_node_parameters(memory_node_id, params)
423
+ logger.info(
424
+ "[CC-Agent _persist_memory] saved memory_node=%s "
425
+ "last_session_id=%s appended_turns=%d archived_blocks=%d "
426
+ "content_length=%d",
427
+ memory_node_id, params.get("last_session_id"),
428
+ len(successful), len(removed_texts), len(content),
429
+ )
430
+
431
+ # Broadcast `node_parameters_updated` so the simpleMemory's
432
+ # parameter panel + memory editor refetch live without a page
433
+ # reload. Mirrors the pattern in
434
+ # `routers/websocket.py:handle_save_node_parameters`. Without
435
+ # this the DB has the latest conversation but the UI keeps
436
+ # showing the stale snapshot it loaded at workflow open.
437
+ if broadcaster is not None:
438
+ try:
439
+ await broadcaster.broadcast({
440
+ "type": "node_parameters_updated",
441
+ "node_id": memory_node_id,
442
+ "parameters": params,
443
+ "version": 1,
444
+ "timestamp": time.time(),
445
+ })
446
+ except Exception as exc:
447
+ logger.warning(
448
+ "[CC-Agent _persist_memory] broadcast failed: %s", exc,
449
+ )
450
+
451
+ if connected_memory.get("long_term_enabled") and removed_texts:
452
+ from services.memory.vector_store import get_memory_vector_store
453
+
454
+ store = get_memory_vector_store(
455
+ connected_memory.get("session_id") or "default",
456
+ )
457
+ if store is not None:
458
+ await asyncio.to_thread(store.add_texts, removed_texts)
459
+
460
+ @staticmethod
461
+ async def _resolve_repo_root(
462
+ *,
463
+ workspace_dir: Path,
464
+ override: Optional[Path],
465
+ ) -> Optional[Path]:
466
+ """Find the git repo root via `git rev-parse --show-toplevel`.
467
+
468
+ Contract:
469
+ - When `override` is given, only consider that subtree.
470
+ - When not given, try `workspace_dir` first, then `cwd`.
471
+ """
472
+ starts: List[Path]
473
+ if override is not None:
474
+ starts = [Path(override).resolve()]
475
+ else:
476
+ starts = [Path(workspace_dir).resolve(), Path.cwd().resolve()]
477
+
478
+ for start in starts:
479
+ try:
480
+ result = await anyio.run_process(
481
+ ["git", "-C", str(start), "rev-parse", "--show-toplevel"],
482
+ check=False,
483
+ )
484
+ except FileNotFoundError:
485
+ # `git` not on PATH at all — fail-fast, nothing to fall back to.
486
+ return None
487
+ if result.returncode == 0:
488
+ root_text = (result.stdout or b"").decode("utf-8", errors="replace").strip()
489
+ if root_text:
490
+ return Path(root_text)
491
+ return None
492
+
493
+ @staticmethod
494
+ def _derive_cost(
495
+ result: SessionResult,
496
+ tasks: List[BaseAICliTaskSpec],
497
+ ) -> Optional[float]:
498
+ """Compute USD cost from `canonical_usage` via the central
499
+ `PricingService`. Returns None when token counts are zero (the
500
+ provider didn't surface them) — keeps the contract that
501
+ ``cost_usd is None`` means "we genuinely don't know the cost"."""
502
+ cu = result.canonical_usage
503
+ total_tokens = cu.input_tokens + cu.output_tokens + cu.cache_read + cu.cache_write
504
+ if total_tokens == 0:
505
+ return None
506
+
507
+ # Find the model the task requested (or the provider's default).
508
+ model = ""
509
+ for t in tasks:
510
+ if (t.task_id or "") == result.task_id:
511
+ model = t.model or ""
512
+ break
513
+
514
+ try:
515
+ from services.pricing import get_pricing_service
516
+ pricing = get_pricing_service()
517
+ breakdown = pricing.calculate_cost(
518
+ provider=result.provider,
519
+ model=model,
520
+ input_tokens=cu.input_tokens,
521
+ output_tokens=cu.output_tokens,
522
+ cache_read_tokens=cu.cache_read,
523
+ cache_creation_tokens=cu.cache_write,
524
+ reasoning_tokens=cu.reasoning_tokens,
525
+ )
526
+ total = breakdown.get("total_cost")
527
+ return float(total) if total else None
528
+ except Exception as exc: # pragma: no cover — pricing is non-critical
529
+ logger.debug("[CC-Agent service] pricing lookup failed: %s", exc)
530
+ return None
531
+
532
+ @staticmethod
533
+ def _fail_result(
534
+ provider_name: str,
535
+ task: BaseAICliTaskSpec,
536
+ task_id: str,
537
+ error: str,
538
+ ) -> SessionResult:
539
+ return SessionResult(
540
+ task_id=task_id,
541
+ provider=provider_name,
542
+ prompt=task.prompt,
543
+ success=False,
544
+ error=error,
545
+ )
546
+
547
+ @staticmethod
548
+ async def _broadcast_phase(
549
+ broadcaster: Any,
550
+ node_id: str,
551
+ workflow_id: str,
552
+ phase: str,
553
+ data: dict,
554
+ ) -> None:
555
+ if not broadcaster:
556
+ return
557
+ try:
558
+ await broadcaster.update_node_status(
559
+ node_id,
560
+ "executing",
561
+ {"phase": phase, **data},
562
+ workflow_id=workflow_id,
563
+ )
564
+ except Exception:
565
+ pass
566
+
567
+ def _abort_not_git_repo(
568
+ self,
569
+ *,
570
+ provider_name: str,
571
+ tasks: List[BaseAICliTaskSpec],
572
+ ) -> BatchResult:
573
+ results: List[SessionResult] = [
574
+ SessionResult(
575
+ task_id=t.task_id or "t_unstarted",
576
+ provider=provider_name,
577
+ prompt=t.prompt,
578
+ success=False,
579
+ error="working_directory_not_git_repo",
580
+ )
581
+ for t in tasks
582
+ ]
583
+ return BatchResult(
584
+ tasks=results,
585
+ n_tasks=len(results),
586
+ n_succeeded=0,
587
+ n_failed=len(results),
588
+ total_cost_usd=None,
589
+ wall_clock_ms=0,
590
+ budget_remaining_usd=None,
591
+ provider=provider_name,
592
+ timestamp=datetime.now(timezone.utc).isoformat(),
593
+ )
594
+
595
+
596
+ # ---------------------------------------------------------------------------
597
+ # Singleton accessor
598
+ # ---------------------------------------------------------------------------
599
+
600
+ _instance: Optional[AICliService] = None
601
+
602
+
603
+ def get_ai_cli_service() -> AICliService:
604
+ global _instance
605
+ if _instance is None:
606
+ _instance = AICliService()
607
+ return _instance