aria-code 0.52.0__py3-none-any.whl

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 (478) hide show
  1. aria_code/__init__.py +42 -0
  2. aria_code/_version.py +20 -0
  3. aria_code/adk_apps/__init__.py +1 -0
  4. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  5. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  6. aria_code/adk_apps/aria_research/__init__.py +1 -0
  7. aria_code/adk_apps/aria_research/agent.py +31 -0
  8. aria_code/agents/__init__.py +34 -0
  9. aria_code/agents/base.py +318 -0
  10. aria_code/agents/code_review.py +200 -0
  11. aria_code/agents/deep/__init__.py +37 -0
  12. aria_code/agents/deep/calibration_loop.py +144 -0
  13. aria_code/agents/deep/critic.py +125 -0
  14. aria_code/agents/deep/deepen.py +193 -0
  15. aria_code/agents/deep/models.py +149 -0
  16. aria_code/agents/deep/pipeline.py +164 -0
  17. aria_code/agents/deep/quant_fusion.py +192 -0
  18. aria_code/agents/deep/themes.py +95 -0
  19. aria_code/agents/deep/tiers.py +106 -0
  20. aria_code/agents/engineering/__init__.py +14 -0
  21. aria_code/agents/engineering/coder.py +222 -0
  22. aria_code/agents/engineering/tester.py +145 -0
  23. aria_code/agents/financial/__init__.py +21 -0
  24. aria_code/agents/financial/cashflow_burn.py +119 -0
  25. aria_code/agents/financial/catalyst.py +279 -0
  26. aria_code/agents/financial/corporate_finance.py +175 -0
  27. aria_code/agents/financial/debate.py +224 -0
  28. aria_code/agents/financial/earnings.py +303 -0
  29. aria_code/agents/financial/fundamental.py +191 -0
  30. aria_code/agents/financial/macro.py +99 -0
  31. aria_code/agents/financial/news.py +207 -0
  32. aria_code/agents/financial/risk.py +165 -0
  33. aria_code/agents/financial/sector.py +279 -0
  34. aria_code/agents/financial/strategist.py +121 -0
  35. aria_code/agents/financial/stripe_revenue.py +127 -0
  36. aria_code/agents/financial/synthesis.py +274 -0
  37. aria_code/agents/financial/technical.py +258 -0
  38. aria_code/agents/orchestrator.py +44 -0
  39. aria_code/agents/portfolio_agent.py +363 -0
  40. aria_code/agents/realty/__init__.py +62 -0
  41. aria_code/agents/realty/asset_diagnosis.py +152 -0
  42. aria_code/agents/realty/business_match.py +166 -0
  43. aria_code/agents/realty/cashflow_verify.py +209 -0
  44. aria_code/agents/realty/contract_rules.py +210 -0
  45. aria_code/agents/realty/energy_anomaly.py +189 -0
  46. aria_code/agents/realty/exit_settlement.py +208 -0
  47. aria_code/agents/realty/fulfillment_risk.py +206 -0
  48. aria_code/agents/realty/ops_optimize.py +160 -0
  49. aria_code/agents/realty/revenue_share.py +215 -0
  50. aria_code/agents/registry.py +158 -0
  51. aria_code/agents/signal_scheme.py +111 -0
  52. aria_code/agents/sports/__init__.py +0 -0
  53. aria_code/agents/sports/football_agent.py +197 -0
  54. aria_code/agents/supervisor.py +113 -0
  55. aria_code/agents/team.py +492 -0
  56. aria_code/agents/warehouse/__init__.py +25 -0
  57. aria_code/agents/warehouse/contracts.py +26 -0
  58. aria_code/agents/warehouse/dashboard.py +155 -0
  59. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  60. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  61. aria_code/agents/warehouse/inventory_health.py +24 -0
  62. aria_code/agents/warehouse/local_run.py +71 -0
  63. aria_code/agents/warehouse/local_snapshot.py +132 -0
  64. aria_code/agents/warehouse/logistics_cost.py +84 -0
  65. aria_code/agents/warehouse/logistics_sync.py +21 -0
  66. aria_code/agents/warehouse/workflow.py +31 -0
  67. aria_code/aliyun_data_client.py +4 -0
  68. aria_code/apps/README.md +12 -0
  69. aria_code/apps/__init__.py +2 -0
  70. aria_code/apps/channels/README.md +15 -0
  71. aria_code/apps/channels/__init__.py +17 -0
  72. aria_code/apps/channels/intake.py +92 -0
  73. aria_code/apps/channels/registry.py +95 -0
  74. aria_code/apps/channels/tradingview.py +94 -0
  75. aria_code/apps/cli/README.md +13 -0
  76. aria_code/apps/cli/__init__.py +2 -0
  77. aria_code/apps/cli/_optional.py +120 -0
  78. aria_code/apps/cli/bootstrap.py +145 -0
  79. aria_code/apps/cli/broker_render.py +206 -0
  80. aria_code/apps/cli/codegen_paths.py +29 -0
  81. aria_code/apps/cli/commands/__init__.py +16 -0
  82. aria_code/apps/cli/commands/_business_render.py +78 -0
  83. aria_code/apps/cli/commands/_ui.py +71 -0
  84. aria_code/apps/cli/commands/analysis_cmds.py +369 -0
  85. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  86. aria_code/apps/cli/commands/auth_cmds.py +239 -0
  87. aria_code/apps/cli/commands/backtest_cmds.py +2543 -0
  88. aria_code/apps/cli/commands/broker_cmds.py +1234 -0
  89. aria_code/apps/cli/commands/business_workflow_cmds.py +319 -0
  90. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  91. aria_code/apps/cli/commands/catalog.py +87 -0
  92. aria_code/apps/cli/commands/core_cmds.py +2520 -0
  93. aria_code/apps/cli/commands/data_cmds.py +457 -0
  94. aria_code/apps/cli/commands/diagnostic_cmds.py +230 -0
  95. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  96. aria_code/apps/cli/commands/file_cmds.py +312 -0
  97. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  98. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  99. aria_code/apps/cli/commands/market.py +434 -0
  100. aria_code/apps/cli/commands/market_cmds.py +1381 -0
  101. aria_code/apps/cli/commands/market_context.py +425 -0
  102. aria_code/apps/cli/commands/market_render.py +7 -0
  103. aria_code/apps/cli/commands/model_cmds.py +2153 -0
  104. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  105. aria_code/apps/cli/commands/orchestrator_cmds.py +179 -0
  106. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  107. aria_code/apps/cli/commands/portfolio_cmds.py +1278 -0
  108. aria_code/apps/cli/commands/report.py +437 -0
  109. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  110. aria_code/apps/cli/commands/session_cmds.py +236 -0
  111. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  112. aria_code/apps/cli/commands/team.py +669 -0
  113. aria_code/apps/cli/commands/team_render.py +8 -0
  114. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  115. aria_code/apps/cli/commands/warehouse_cmds.py +79 -0
  116. aria_code/apps/cli/commands/workflow_cmds.py +480 -0
  117. aria_code/apps/cli/commands/workspace_cmds.py +1494 -0
  118. aria_code/apps/cli/config_paths.py +73 -0
  119. aria_code/apps/cli/config_store.py +78 -0
  120. aria_code/apps/cli/constants.py +88 -0
  121. aria_code/apps/cli/context.py +34 -0
  122. aria_code/apps/cli/deterministic.py +191 -0
  123. aria_code/apps/cli/direct.py +48 -0
  124. aria_code/apps/cli/finance_service_catalog.py +111 -0
  125. aria_code/apps/cli/football_reports.py +231 -0
  126. aria_code/apps/cli/github_app_auth.py +135 -0
  127. aria_code/apps/cli/google_login.py +196 -0
  128. aria_code/apps/cli/handlers/__init__.py +11 -0
  129. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  130. aria_code/apps/cli/handlers/chart_handlers.py +1309 -0
  131. aria_code/apps/cli/handlers/market_handlers.py +2513 -0
  132. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  133. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  134. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  135. aria_code/apps/cli/helpers.py +302 -0
  136. aria_code/apps/cli/hooks.py +180 -0
  137. aria_code/apps/cli/i18n.py +297 -0
  138. aria_code/apps/cli/intent.py +136 -0
  139. aria_code/apps/cli/intent_router.py +276 -0
  140. aria_code/apps/cli/intent_signals.py +344 -0
  141. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  142. aria_code/apps/cli/local_tool_schemas.py +536 -0
  143. aria_code/apps/cli/main.py +29 -0
  144. aria_code/apps/cli/market_metadata.py +135 -0
  145. aria_code/apps/cli/market_universe.py +487 -0
  146. aria_code/apps/cli/message_processing.py +310 -0
  147. aria_code/apps/cli/model_catalog.py +620 -0
  148. aria_code/apps/cli/pdf_report.py +977 -0
  149. aria_code/apps/cli/plan_mode.py +139 -0
  150. aria_code/apps/cli/plotly_html.py +15 -0
  151. aria_code/apps/cli/prediction_feedback.py +202 -0
  152. aria_code/apps/cli/preflight.py +502 -0
  153. aria_code/apps/cli/project_aria.py +60 -0
  154. aria_code/apps/cli/prompt_assembly.py +59 -0
  155. aria_code/apps/cli/prompts/__init__.py +0 -0
  156. aria_code/apps/cli/prompts/coding.py +53 -0
  157. aria_code/apps/cli/prompts/select.py +142 -0
  158. aria_code/apps/cli/prompts/system_prompts.py +552 -0
  159. aria_code/apps/cli/prompts/ui.py +436 -0
  160. aria_code/apps/cli/provider_endpoints.py +237 -0
  161. aria_code/apps/cli/provider_keys.py +73 -0
  162. aria_code/apps/cli/providers/__init__.py +1 -0
  163. aria_code/apps/cli/providers/base.py +521 -0
  164. aria_code/apps/cli/providers/chat_routing.py +128 -0
  165. aria_code/apps/cli/providers/collaboration.py +98 -0
  166. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  167. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  168. aria_code/apps/cli/providers/llm/ollama_stream.py +1594 -0
  169. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  170. aria_code/apps/cli/providers/llm/sse_stream.py +257 -0
  171. aria_code/apps/cli/providers/runtime_bridge.py +353 -0
  172. aria_code/apps/cli/providers/vertexai_stream.py +316 -0
  173. aria_code/apps/cli/runtime_consumer.py +657 -0
  174. aria_code/apps/cli/sandbox.py +32 -0
  175. aria_code/apps/cli/session_export.py +120 -0
  176. aria_code/apps/cli/session_jsonl.py +208 -0
  177. aria_code/apps/cli/session_store.py +112 -0
  178. aria_code/apps/cli/skills_catalog.py +273 -0
  179. aria_code/apps/cli/todo_tracker.py +190 -0
  180. aria_code/apps/cli/tool_executor.py +823 -0
  181. aria_code/apps/cli/tool_registry.py +34 -0
  182. aria_code/apps/cli/tool_scope.py +106 -0
  183. aria_code/apps/cli/tools/__init__.py +40 -0
  184. aria_code/apps/cli/tools/context.py +46 -0
  185. aria_code/apps/cli/tools/file_tools.py +180 -0
  186. aria_code/apps/cli/tools/market_tools.py +620 -0
  187. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  188. aria_code/apps/cli/tools/system_tools.py +726 -0
  189. aria_code/apps/cli/tools/write_tools.py +845 -0
  190. aria_code/apps/cli/tradingview_bridge.py +529 -0
  191. aria_code/apps/cli/turn_planning.py +137 -0
  192. aria_code/apps/cli/update_check.py +153 -0
  193. aria_code/apps/cli/utils/__init__.py +0 -0
  194. aria_code/apps/cli/utils/market_detect.py +1597 -0
  195. aria_code/apps/daemon/README.md +14 -0
  196. aria_code/apps/vscode/README.md +115 -0
  197. aria_code/apps/vscode/package.json +70 -0
  198. aria_code/aria_cli.py +6743 -0
  199. aria_code/aria_daemon.py +1398 -0
  200. aria_code/aria_feishu_bot.py +1423 -0
  201. aria_code/aria_mcp_server.py +7 -0
  202. aria_code/aria_relay_client.py +4 -0
  203. aria_code/aria_relay_server.py +490 -0
  204. aria_code/aria_telegram_bot.py +202 -0
  205. aria_code/ariarc.py +385 -0
  206. aria_code/artifacts.py +581 -0
  207. aria_code/backtest_engine.py +4 -0
  208. aria_code/backtest_report.py +4 -0
  209. aria_code/brokers/__init__.py +78 -0
  210. aria_code/brokers/automation.py +193 -0
  211. aria_code/brokers/base.py +231 -0
  212. aria_code/brokers/capabilities.py +264 -0
  213. aria_code/brokers/cn/__init__.py +10 -0
  214. aria_code/brokers/cn/easytrader_broker.py +193 -0
  215. aria_code/brokers/cn/futu_broker.py +194 -0
  216. aria_code/brokers/cn/longbridge_broker.py +190 -0
  217. aria_code/brokers/cn/tiger_broker.py +196 -0
  218. aria_code/brokers/cn/xtquant_broker.py +225 -0
  219. aria_code/brokers/config.py +414 -0
  220. aria_code/brokers/intl/__init__.py +5 -0
  221. aria_code/brokers/intl/alpaca_broker.py +183 -0
  222. aria_code/brokers/intl/ibkr_broker.py +215 -0
  223. aria_code/brokers/intl/trading212_broker.py +183 -0
  224. aria_code/brokers/intl/webull_broker.py +156 -0
  225. aria_code/brokers/paper_broker.py +266 -0
  226. aria_code/brokers/planning.py +296 -0
  227. aria_code/brokers/registry.py +213 -0
  228. aria_code/brokers/trading.py +267 -0
  229. aria_code/canva_client.py +4 -0
  230. aria_code/change_store.py +127 -0
  231. aria_code/clients/__init__.py +0 -0
  232. aria_code/clients/aliyun_data_client.py +660 -0
  233. aria_code/clients/aria_relay_client.py +182 -0
  234. aria_code/clients/canva_client.py +284 -0
  235. aria_code/clients/figma_client.py +100 -0
  236. aria_code/clients/football_data_client.py +1671 -0
  237. aria_code/clients/kling_video_client.py +182 -0
  238. aria_code/clients/market_data_client.py +1976 -0
  239. aria_code/clients/openai_image_client.py +242 -0
  240. aria_code/clients/runway_video_client.py +157 -0
  241. aria_code/clients/warehouse_erp_client.py +105 -0
  242. aria_code/cloud_memory.py +181 -0
  243. aria_code/command_safety.py +19 -0
  244. aria_code/computer_use_tools.py +4 -0
  245. aria_code/dashboard_generator.py +579 -0
  246. aria_code/data_analysis_tools.py +4 -0
  247. aria_code/data_cleaner.py +483 -0
  248. aria_code/data_service.py +513 -0
  249. aria_code/datasources/__init__.py +23 -0
  250. aria_code/datasources/base.py +166 -0
  251. aria_code/datasources/router.py +253 -0
  252. aria_code/datasources/sources/__init__.py +15 -0
  253. aria_code/datasources/sources/akshare_source.py +269 -0
  254. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  255. aria_code/datasources/sources/edgar_source.py +219 -0
  256. aria_code/datasources/sources/finnhub_source.py +198 -0
  257. aria_code/datasources/sources/fred_source.py +223 -0
  258. aria_code/datasources/sources/tushare_source.py +143 -0
  259. aria_code/datasources/sources/web_scraper_source.py +278 -0
  260. aria_code/datasources/sources/world_bank_source.py +206 -0
  261. aria_code/datasources/sources/yfinance_source.py +152 -0
  262. aria_code/demo_player.py +207 -0
  263. aria_code/doctor.py +813 -0
  264. aria_code/domain/__init__.py +0 -0
  265. aria_code/domain/backtest_engine.py +354 -0
  266. aria_code/domain/backtest_report.py +472 -0
  267. aria_code/domain/finance_formulas.py +389 -0
  268. aria_code/domain/portfolio_ledger.py +307 -0
  269. aria_code/domain/strategy_vault.py +421 -0
  270. aria_code/evals/__init__.py +37 -0
  271. aria_code/evals/harness.py +598 -0
  272. aria_code/evals/runner.py +238 -0
  273. aria_code/external_agent_runner.py +115 -0
  274. aria_code/figma_client.py +4 -0
  275. aria_code/file_analysis_tools.py +4 -0
  276. aria_code/finance_formulas.py +4 -0
  277. aria_code/football_data_client.py +4 -0
  278. aria_code/image_gen_tools.py +4 -0
  279. aria_code/intent_classifier.py +386 -0
  280. aria_code/kling_video_client.py +4 -0
  281. aria_code/licensing.py +151 -0
  282. aria_code/local_finance_tools.py +4 -0
  283. aria_code/local_image_provider.py +4 -0
  284. aria_code/local_llm_provider.py +4 -0
  285. aria_code/macro_tools.py +4 -0
  286. aria_code/markdown_pdf.py +289 -0
  287. aria_code/market_data_client.py +4 -0
  288. aria_code/mcp_client.py +639 -0
  289. aria_code/memory_manager.py +246 -0
  290. aria_code/model_capability.py +486 -0
  291. aria_code/notification_tools.py +4 -0
  292. aria_code/openai_image_client.py +4 -0
  293. aria_code/packages/__init__.py +23 -0
  294. aria_code/packages/adk_bridge/__init__.py +11 -0
  295. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  296. aria_code/packages/adk_bridge/market_tools.py +125 -0
  297. aria_code/packages/aria_agents/__init__.py +5 -0
  298. aria_code/packages/aria_agents/manifest.py +78 -0
  299. aria_code/packages/aria_core/__init__.py +34 -0
  300. aria_code/packages/aria_core/architecture.py +269 -0
  301. aria_code/packages/aria_core/export.py +147 -0
  302. aria_code/packages/aria_core/manifest.py +65 -0
  303. aria_code/packages/aria_core/paths.py +57 -0
  304. aria_code/packages/aria_core/secure_file.py +68 -0
  305. aria_code/packages/aria_infra/__init__.py +15 -0
  306. aria_code/packages/aria_infra/arthera.py +52 -0
  307. aria_code/packages/aria_infra/doctor.py +246 -0
  308. aria_code/packages/aria_infra/product.py +40 -0
  309. aria_code/packages/aria_mcp/__init__.py +25 -0
  310. aria_code/packages/aria_mcp/bridge.py +201 -0
  311. aria_code/packages/aria_mcp/circuit.py +82 -0
  312. aria_code/packages/aria_mcp/config.py +111 -0
  313. aria_code/packages/aria_mcp/installer.py +95 -0
  314. aria_code/packages/aria_mcp/server.py +1550 -0
  315. aria_code/packages/aria_mcp/tools.py +81 -0
  316. aria_code/packages/aria_sdk/__init__.py +19 -0
  317. aria_code/packages/aria_sdk/client.py +396 -0
  318. aria_code/packages/aria_sdk/providers.py +70 -0
  319. aria_code/packages/aria_sdk/streaming.py +79 -0
  320. aria_code/packages/aria_sdk/types.py +86 -0
  321. aria_code/packages/aria_services/__init__.py +92 -0
  322. aria_code/packages/aria_services/cache.py +60 -0
  323. aria_code/packages/aria_services/context.py +356 -0
  324. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  325. aria_code/packages/aria_services/data.py +11 -0
  326. aria_code/packages/aria_services/provider_health.py +268 -0
  327. aria_code/packages/aria_services/references.py +247 -0
  328. aria_code/packages/aria_services/registry.py +249 -0
  329. aria_code/packages/aria_services/research_protocol.py +57 -0
  330. aria_code/packages/aria_services/research_quality.py +186 -0
  331. aria_code/packages/aria_services/settings.py +157 -0
  332. aria_code/packages/aria_services/usage.py +151 -0
  333. aria_code/packages/aria_skills/__init__.py +46 -0
  334. aria_code/packages/aria_skills/catalog.py +149 -0
  335. aria_code/packages/aria_skills/loader.py +455 -0
  336. aria_code/packages/aria_skills/registry.py +84 -0
  337. aria_code/packages/aria_tools/__init__.py +5 -0
  338. aria_code/packages/aria_tools/financial/__init__.py +8 -0
  339. aria_code/packages/aria_tools/financial/_paths.py +130 -0
  340. aria_code/packages/aria_tools/financial/compliance.py +31 -0
  341. aria_code/packages/aria_tools/financial/factors.py +33 -0
  342. aria_code/packages/aria_tools/financial/risk_tools.py +33 -0
  343. aria_code/packages/aria_tools/financial/strategy.py +31 -0
  344. aria_code/packages/aria_tools/registry.py +128 -0
  345. aria_code/packages/quant_engine/__init__.py +34 -0
  346. aria_code/packages/quant_engine/_namespace.py +62 -0
  347. aria_code/packages/quant_engine/backtest/__init__.py +14 -0
  348. aria_code/packages/quant_engine/backtest/core.py +53 -0
  349. aria_code/packages/quant_engine/backtest/engine.py +96 -0
  350. aria_code/packages/quant_engine/portfolio/__init__.py +13 -0
  351. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  352. aria_code/packages/quant_engine/sports/__init__.py +80 -0
  353. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  354. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  355. aria_code/packages/quant_engine/sports/elo.py +300 -0
  356. aria_code/packages/quant_engine/sports/form.py +188 -0
  357. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  358. aria_code/packages/quant_engine/sports/ml_model.py +377 -0
  359. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  360. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  361. aria_code/packages/quant_engine/stochastic/__init__.py +35 -0
  362. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  363. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  364. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  365. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  366. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  367. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  368. aria_code/packs/__init__.py +85 -0
  369. aria_code/packs/base.py +168 -0
  370. aria_code/packs/finance/__init__.py +250 -0
  371. aria_code/packs/logistics/__init__.py +169 -0
  372. aria_code/packs/payments/__init__.py +139 -0
  373. aria_code/packs/realty/__init__.py +190 -0
  374. aria_code/packs/registry.py +202 -0
  375. aria_code/packs/rules.py +90 -0
  376. aria_code/plan_utils.py +194 -0
  377. aria_code/plugin_loader.py +328 -0
  378. aria_code/portfolio_ledger.py +4 -0
  379. aria_code/preview_server.py +449 -0
  380. aria_code/privacy/__init__.py +5 -0
  381. aria_code/privacy/feedback.py +123 -0
  382. aria_code/project_review.py +278 -0
  383. aria_code/project_review_api.py +235 -0
  384. aria_code/project_review_server.py +18 -0
  385. aria_code/project_tools.py +4 -0
  386. aria_code/providers/__init__.py +30 -0
  387. aria_code/providers/llm/__init__.py +19 -0
  388. aria_code/providers/llm/anthropic.py +184 -0
  389. aria_code/providers/llm/autoconfig.py +213 -0
  390. aria_code/providers/llm/base.py +139 -0
  391. aria_code/providers/llm/custom.py +158 -0
  392. aria_code/providers/llm/ollama.py +129 -0
  393. aria_code/providers/llm/openai_compat.py +313 -0
  394. aria_code/providers/llm/registry.py +444 -0
  395. aria_code/providers/local_image_provider.py +202 -0
  396. aria_code/providers/local_llm_provider.py +589 -0
  397. aria_code/realty_data_tools.py +4 -0
  398. aria_code/report_exporters.py +150 -0
  399. aria_code/report_generator.py +1764 -0
  400. aria_code/runtime/__init__.py +169 -0
  401. aria_code/runtime/acceptance.py +481 -0
  402. aria_code/runtime/agent_loop.py +1520 -0
  403. aria_code/runtime/approval.py +57 -0
  404. aria_code/runtime/budget.py +232 -0
  405. aria_code/runtime/checkpoints.py +432 -0
  406. aria_code/runtime/crash_report.py +174 -0
  407. aria_code/runtime/events.py +113 -0
  408. aria_code/runtime/gateway.py +150 -0
  409. aria_code/runtime/lsp.py +484 -0
  410. aria_code/runtime/orchestrator.py +55 -0
  411. aria_code/runtime/repo_map.py +700 -0
  412. aria_code/runtime/run_state.py +127 -0
  413. aria_code/runtime/run_store.py +450 -0
  414. aria_code/runtime/self_healing.py +422 -0
  415. aria_code/runtime/subagent.py +547 -0
  416. aria_code/runtime/task_ledger.py +86 -0
  417. aria_code/runtime/task_orchestrator.py +90 -0
  418. aria_code/runtime/tool_executor.py +217 -0
  419. aria_code/runtime/tool_policy.py +107 -0
  420. aria_code/runtime/worktrees.py +215 -0
  421. aria_code/runway_video_client.py +4 -0
  422. aria_code/safety/__init__.py +23 -0
  423. aria_code/safety/permissions.py +306 -0
  424. aria_code/safety/service.py +102 -0
  425. aria_code/setup_wizard.py +673 -0
  426. aria_code/spreadsheet_tools.py +4 -0
  427. aria_code/strategy_vault.py +4 -0
  428. aria_code/tools/__init__.py +0 -0
  429. aria_code/tools/artifact_tools.py +252 -0
  430. aria_code/tools/broker_tools.py +92 -0
  431. aria_code/tools/build_quant_engine.py +97 -0
  432. aria_code/tools/code_audit_tools.py +192 -0
  433. aria_code/tools/computer_use_tools.py +504 -0
  434. aria_code/tools/data_analysis_tools.py +809 -0
  435. aria_code/tools/enterprise_finance_tools.py +156 -0
  436. aria_code/tools/extended_tools.py +240 -0
  437. aria_code/tools/file_analysis_tools.py +885 -0
  438. aria_code/tools/image_gen_tools.py +114 -0
  439. aria_code/tools/local_finance_tools.py +3417 -0
  440. aria_code/tools/logistics_tools.py +135 -0
  441. aria_code/tools/macro_tools.py +369 -0
  442. aria_code/tools/notification_tools.py +249 -0
  443. aria_code/tools/project_tools.py +525 -0
  444. aria_code/tools/realty_data_tools.py +659 -0
  445. aria_code/tools/spreadsheet_tools.py +388 -0
  446. aria_code/tools/stripe_tools.py +115 -0
  447. aria_code/ui/__init__.py +105 -0
  448. aria_code/ui/banner.py +508 -0
  449. aria_code/ui/completer.py +501 -0
  450. aria_code/ui/console.py +281 -0
  451. aria_code/ui/image_render.py +243 -0
  452. aria_code/ui/input_box.py +488 -0
  453. aria_code/ui/picker.py +201 -0
  454. aria_code/ui/render/__init__.py +11 -0
  455. aria_code/ui/render/finance.py +1705 -0
  456. aria_code/ui/render/market.py +225 -0
  457. aria_code/ui/render/output.py +775 -0
  458. aria_code/ui/render/responsive.py +75 -0
  459. aria_code/ui/render/team.py +354 -0
  460. aria_code/ui/robot.py +235 -0
  461. aria_code/ui/startup_dashboard.py +109 -0
  462. aria_code/utils/vscode_uri.py +37 -0
  463. aria_code/video_analysis.py +147 -0
  464. aria_code/video_editor.py +271 -0
  465. aria_code/workspace/__init__.py +6 -0
  466. aria_code/workspace/files.py +193 -0
  467. aria_code/workspace/verify.py +113 -0
  468. aria_code/workspace_changes.py +199 -0
  469. aria_code/workspace_context.py +233 -0
  470. aria_code/workspace_service.py +196 -0
  471. aria_code/workspace_tools.py +150 -0
  472. aria_code-0.52.0.dist-info/METADATA +237 -0
  473. aria_code-0.52.0.dist-info/RECORD +478 -0
  474. aria_code-0.52.0.dist-info/WHEEL +5 -0
  475. aria_code-0.52.0.dist-info/entry_points.txt +3 -0
  476. aria_code-0.52.0.dist-info/licenses/LICENSE +201 -0
  477. aria_code-0.52.0.dist-info/licenses/NOTICE +49 -0
  478. aria_code-0.52.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1550 @@
1
+ """aria-code MCP server — exposes read-only aria-code capabilities over stdio JSON-RPC.
2
+
3
+ Lets any MCP-compatible client (Claude Code, Codex, Cursor, ...) call into
4
+ aria-code's market data, multi-agent research team, and generated-artifact
5
+ list. Pattern mirrors Arthera's packages/quant_engine/mcp_server.py (hand-rolled
6
+ stdio JSON-RPC, no `mcp` SDK dependency).
7
+
8
+ Usage (from repo root)::
9
+
10
+ python3 aria_mcp_server.py
11
+
12
+ Register with Claude Code::
13
+
14
+ claude mcp add aria-code -- python3 /path/to/aria-code/aria_mcp_server.py
15
+
16
+ Scope: ``skill:``-backed exposures (report/backtest skills) are still never
17
+ registered as *executable* tools — a skill is a prompt-time instruction
18
+ document, not a clean JSON-in/JSON-out callable. They are instead readable
19
+ via ``aria.skill.list``/``aria.skill.get``, which hand the SKILL.md text to
20
+ the calling model so it can follow the workflow itself. Beyond
21
+ that, everything is read-only EXCEPT the small, explicit ``_WRITE_SAFE``
22
+ allowlist below (order previews, chart/PDF generation, cloud video
23
+ generation behind its own confirm gate) — each entry there is individually
24
+ justified as unable to move money without a further, explicit gate of its
25
+ own.
26
+
27
+ Order *execution* is the one capability with real financial stakes reachable
28
+ from this server at all, and it has two independent gates, both of which
29
+ must pass — this is intentional defense in depth, not redundancy to trim:
30
+ 1. The broker itself must have chat-confirm explicitly opted in via
31
+ ``/trade allow-chat-confirm <broker_id>`` — typed at the aria-code
32
+ terminal keyboard, by a human, with the broker id retyped as
33
+ confirmation. This can NEVER be turned on from MCP/chat — see
34
+ ``brokers/config.py``'s ``set_chat_confirm_enabled`` and its callers.
35
+ 2. Even with that broker opted in, each individual ``aria.broker.confirm_order``
36
+ call still requires an explicit ``confirmed: true`` argument (same
37
+ pattern as ``aria.video.generate_submit``'s cost gate).
38
+ Without opt-in #1, ``aria.broker.confirm_order`` refuses unconditionally —
39
+ default behavior for every broker is unchanged from before: only
40
+ ``/trade confirm`` at the terminal can execute a trade.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import json
46
+ import logging
47
+ import os
48
+ import sys
49
+ import traceback
50
+ from pathlib import Path
51
+ from typing import Any, Awaitable, Callable, Dict, List
52
+
53
+ _ROOT = str(Path(__file__).resolve().parents[2])
54
+ if _ROOT not in sys.path:
55
+ sys.path.insert(0, _ROOT)
56
+
57
+ logging.basicConfig(level=logging.WARNING, stream=sys.stderr)
58
+ logger = logging.getLogger("aria_mcp_server")
59
+
60
+ MCP_PROTOCOL_VERSION = "2024-11-05"
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Input schemas for the exposed tools (default_exposures() carries names +
64
+ # descriptions but not JSON Schema — kept here, next to the handlers that
65
+ # actually consume the arguments).
66
+ # ---------------------------------------------------------------------------
67
+
68
+ _INPUT_SCHEMAS: Dict[str, Dict[str, Any]] = {
69
+ "aria.market.quote": {
70
+ "type": "object",
71
+ "properties": {
72
+ "symbol": {"type": "string", "description": "Ticker/symbol, e.g. AAPL, 600519, BTC-USD"},
73
+ },
74
+ "required": ["symbol"],
75
+ },
76
+ "aria.agent.team": {
77
+ "type": "object",
78
+ "properties": {
79
+ "symbol": {"type": "string", "description": "Ticker/symbol to research"},
80
+ "agents": {
81
+ "type": "array",
82
+ "items": {"type": "string"},
83
+ "description": "Subset of agent names to run (default: all)",
84
+ },
85
+ },
86
+ "required": ["symbol"],
87
+ },
88
+ "aria.artifacts.list": {
89
+ "type": "object",
90
+ "properties": {
91
+ "limit": {"type": "integer", "description": "Max artifacts to return (default 20)"},
92
+ },
93
+ "required": [],
94
+ },
95
+ "aria.skill.list": {
96
+ "type": "object",
97
+ "properties": {
98
+ "query": {"type": "string", "description": "Task description to rank skills against, e.g. \"design a landing page for a healthcare startup\". Omit to list all installed skills."},
99
+ },
100
+ "required": [],
101
+ },
102
+ "aria.skill.get": {
103
+ "type": "object",
104
+ "properties": {
105
+ "name": {"type": "string", "description": "Skill name from aria.skill.list — either the bare name (\"ui-design-system\") or the qualified form (\"app-engineering-skills:ui-design-system\")"},
106
+ "reference": {"type": "string", "description": "Optional. Fetch one of the skill's bundled reference docs instead of its main instructions — pass a filename from the `references` list returned by a plain aria.skill.get call, e.g. \"density_and_semantics.md\"."},
107
+ },
108
+ "required": ["name"],
109
+ },
110
+ "aria.broker.positions": {
111
+ "type": "object",
112
+ "properties": {
113
+ "broker_id": {"type": "string", "description": "Configured broker id (~/.aria-code/brokers.json). Omit to use the default broker."},
114
+ },
115
+ "required": [],
116
+ },
117
+ "aria.broker.list_previews": {
118
+ "type": "object",
119
+ "properties": {
120
+ "limit": {"type": "integer", "description": "Max previews to return (default 10)"},
121
+ },
122
+ "required": [],
123
+ },
124
+ "aria.broker.preview_order": {
125
+ "type": "object",
126
+ "properties": {
127
+ "broker_id": {"type": "string", "description": "Configured broker id. Omit to use the default broker."},
128
+ "symbol": {"type": "string", "description": "Ticker/symbol to trade"},
129
+ "side": {"type": "string", "enum": ["buy", "sell"]},
130
+ "quantity": {"type": "number", "description": "Share/unit quantity"},
131
+ "order_type": {"type": "string", "enum": ["limit", "market"], "description": "Default: limit"},
132
+ "price": {"type": "number", "description": "Limit price (required for limit orders)"},
133
+ },
134
+ "required": ["symbol", "side"],
135
+ },
136
+ "aria.broker.confirm_order": {
137
+ "type": "object",
138
+ "properties": {
139
+ "broker_id": {"type": "string", "description": "Configured broker id. Omit to use the default broker."},
140
+ "preview_id": {"type": "string", "description": "preview_id returned by aria.broker.preview_order"},
141
+ "confirmed": {"type": "boolean", "description": "Must be true. Also requires the broker to have chat-confirm opted in via /trade allow-chat-confirm <broker_id> at the aria-code terminal — see this tool's description."},
142
+ },
143
+ "required": ["preview_id", "confirmed"],
144
+ },
145
+ "aria.report.chart": {
146
+ "type": "object",
147
+ "properties": {
148
+ "symbol": {"type": "string", "description": "Ticker/symbol to chart"},
149
+ },
150
+ "required": ["symbol"],
151
+ },
152
+ "aria.report.indicator_chart": {
153
+ "type": "object",
154
+ "properties": {
155
+ "symbol": {"type": "string", "description": "Ticker/symbol to chart"},
156
+ },
157
+ "required": ["symbol"],
158
+ },
159
+ "aria.report.comparison_chart": {
160
+ "type": "object",
161
+ "properties": {
162
+ "symbols": {"type": "array", "items": {"type": "string"}, "description": "2+ tickers to compare, normalized to base=100"},
163
+ "title": {"type": "string", "description": "Optional chart title"},
164
+ },
165
+ "required": ["symbols"],
166
+ },
167
+ "aria.report.allocation_chart": {
168
+ "type": "object",
169
+ "properties": {
170
+ "broker_id": {"type": "string", "description": "Configured broker id. Omit to use the default broker."},
171
+ "title": {"type": "string", "description": "Optional chart title"},
172
+ },
173
+ "required": [],
174
+ },
175
+ "aria.report.pdf": {
176
+ "type": "object",
177
+ "properties": {
178
+ "markdown": {"type": "string", "description": "Markdown text to render"},
179
+ "title": {"type": "string", "description": "Report title, used for the filename"},
180
+ },
181
+ "required": ["markdown"],
182
+ },
183
+ "aria.report.estimate_image_cost": {
184
+ "type": "object",
185
+ "properties": {
186
+ "size": {"type": "string", "enum": ["1024x1024", "1536x1024", "1024x1536", "auto"], "description": "Default: 1024x1536"},
187
+ "quality": {"type": "string", "enum": ["low", "medium", "high", "auto"], "description": "Default: high"},
188
+ },
189
+ "required": [],
190
+ },
191
+ "aria.report.generate_image": {
192
+ "type": "object",
193
+ "properties": {
194
+ "prompt": {"type": "string", "description": "Text prompt (e.g. minimal-editorial-poster's compiled prompt)"},
195
+ "size": {"type": "string", "enum": ["1024x1024", "1536x1024", "1024x1536", "auto"], "description": "Default: 1024x1536 (portrait poster)"},
196
+ "quality": {"type": "string", "enum": ["low", "medium", "high", "auto"], "description": "Default: high"},
197
+ "confirmed": {"type": "boolean", "description": "Must be true — this spends real money the instant it succeeds"},
198
+ },
199
+ "required": ["prompt", "confirmed"],
200
+ },
201
+ "aria.report.edit_image": {
202
+ "type": "object",
203
+ "properties": {
204
+ "image_path": {"type": "string", "description": "Local path to the existing photo to transform"},
205
+ "prompt": {"type": "string", "description": "How to transform it, e.g. \"convert to duotone with warm ochre accent, simplify the busy rock background into flat negative space, add subtle scan-noise texture\""},
206
+ "mask_path": {"type": "string", "description": "Optional local path to a mask PNG for inpainting — transparent (alpha=0) areas mark where the edit applies; everything opaque is left untouched. Omit to edit the whole image."},
207
+ "size": {"type": "string", "enum": ["1024x1024", "1536x1024", "1024x1536", "auto"], "description": "Default: 1024x1536"},
208
+ "quality": {"type": "string", "enum": ["low", "medium", "high", "auto"], "description": "Default: high"},
209
+ "confirmed": {"type": "boolean", "description": "Must be true — this spends real money the instant it succeeds"},
210
+ },
211
+ "required": ["image_path", "prompt", "confirmed"],
212
+ },
213
+ "aria.report.generate_image_local": {
214
+ "type": "object",
215
+ "properties": {
216
+ "prompt": {"type": "string", "description": "Text prompt (e.g. minimal-editorial-poster's compiled prompt)"},
217
+ "width": {"type": "integer", "description": "Default: 1024"},
218
+ "height": {"type": "integer", "description": "Default: 1024"},
219
+ "steps": {"type": "integer", "description": "Inference steps; default 4 (tuned for SDXL-Turbo)"},
220
+ },
221
+ "required": ["prompt"],
222
+ },
223
+ "aria.report.edit_image_local": {
224
+ "type": "object",
225
+ "properties": {
226
+ "image_path": {"type": "string", "description": "Local path to the existing photo to transform"},
227
+ "prompt": {"type": "string", "description": "How to transform it"},
228
+ "strength": {"type": "number", "description": "0-1, how much to diverge from the original (default 0.6)"},
229
+ },
230
+ "required": ["image_path", "prompt"],
231
+ },
232
+ "aria.report.canva_design": {
233
+ "type": "object",
234
+ "properties": {
235
+ "template_id": {"type": "string", "description": "Canva brand template id"},
236
+ "data": {"type": "object", "description": "Field name -> typed DatasetValue, e.g. {\"headline\": {\"type\": \"text\", \"text\": \"Q3 Report\"}, \"chart_img\": {\"type\": \"image\", \"asset_id\": \"<from aria.report.canva_upload_asset>\"}}. Value types: text, image, video, chart, sheet — see Canva's Autofill API docs."},
237
+ },
238
+ "required": ["template_id", "data"],
239
+ },
240
+ "aria.report.canva_upload_asset": {
241
+ "type": "object",
242
+ "properties": {
243
+ "file_path": {"type": "string", "description": "Local path to the image file to upload (e.g. the path returned by aria.report.chart)"},
244
+ },
245
+ "required": ["file_path"],
246
+ },
247
+ "aria.report.docx": {
248
+ "type": "object",
249
+ "properties": {
250
+ "markdown": {"type": "string", "description": "Markdown text to render"},
251
+ "title": {"type": "string", "description": "Document title, used for the filename and cover heading"},
252
+ },
253
+ "required": ["markdown"],
254
+ },
255
+ "aria.report.pptx": {
256
+ "type": "object",
257
+ "properties": {
258
+ "markdown": {"type": "string", "description": "Markdown text to render (each heading becomes a slide)"},
259
+ "title": {"type": "string", "description": "Deck title, used for the filename and title slide"},
260
+ },
261
+ "required": ["markdown"],
262
+ },
263
+ "aria.figma.read_file": {
264
+ "type": "object",
265
+ "properties": {
266
+ "file_key": {"type": "string", "description": "Figma file key (from the file's URL: figma.com/file/<file_key>/...)"},
267
+ "depth": {"type": "integer", "description": "How many tree levels to include per page (default 2)"},
268
+ },
269
+ "required": ["file_key"],
270
+ },
271
+ "aria.figma.comments": {
272
+ "type": "object",
273
+ "properties": {
274
+ "file_key": {"type": "string", "description": "Figma file key"},
275
+ },
276
+ "required": ["file_key"],
277
+ },
278
+ "aria.report.generate": {
279
+ "type": "object",
280
+ "properties": {
281
+ "symbol": {"type": "string", "description": "Ticker/symbol to research"},
282
+ },
283
+ "required": ["symbol"],
284
+ },
285
+ "aria.backtest.run": {
286
+ "type": "object",
287
+ "properties": {
288
+ "symbol": {"type": "string", "description": "Ticker/symbol to backtest"},
289
+ "strategy": {"type": "string", "enum": ["buy_hold", "sma_cross"], "description": "Default: sma_cross"},
290
+ "days": {"type": "integer", "description": "History window in days (default 365)"},
291
+ },
292
+ "required": ["symbol"],
293
+ },
294
+ "aria.video.probe": {
295
+ "type": "object",
296
+ "properties": {
297
+ "input_path": {"type": "string", "description": "Local path to the video"},
298
+ },
299
+ "required": ["input_path"],
300
+ },
301
+ "aria.video.trim": {
302
+ "type": "object",
303
+ "properties": {
304
+ "input_path": {"type": "string"},
305
+ "start": {"type": "number", "description": "Start time in seconds"},
306
+ "end": {"type": "number", "description": "End time in seconds"},
307
+ },
308
+ "required": ["input_path", "start", "end"],
309
+ },
310
+ "aria.video.concat": {
311
+ "type": "object",
312
+ "properties": {
313
+ "input_paths": {"type": "array", "items": {"type": "string"}, "description": "Video paths, in order"},
314
+ },
315
+ "required": ["input_paths"],
316
+ },
317
+ "aria.video.overlay_text": {
318
+ "type": "object",
319
+ "properties": {
320
+ "input_path": {"type": "string"},
321
+ "text": {"type": "string"},
322
+ "position": {"type": "string", "enum": ["top", "bottom", "center"], "description": "Default: bottom"},
323
+ "font_size": {"type": "integer", "description": "Default: 36"},
324
+ "font_color": {"type": "string", "description": "Default: white"},
325
+ },
326
+ "required": ["input_path", "text"],
327
+ },
328
+ "aria.video.overlay_audio": {
329
+ "type": "object",
330
+ "properties": {
331
+ "input_path": {"type": "string"},
332
+ "audio_path": {"type": "string"},
333
+ "replace": {"type": "boolean", "description": "true = drop original audio; false (default) = mix under it"},
334
+ },
335
+ "required": ["input_path", "audio_path"],
336
+ },
337
+ "aria.video.convert": {
338
+ "type": "object",
339
+ "properties": {
340
+ "input_path": {"type": "string"},
341
+ "output_format": {"type": "string", "description": "e.g. mp4, mov, webm"},
342
+ "aspect": {"type": "string", "description": "e.g. \"9:16\" — center-crop to this aspect ratio"},
343
+ },
344
+ "required": ["input_path"],
345
+ },
346
+ "aria.video.change_speed": {
347
+ "type": "object",
348
+ "properties": {
349
+ "input_path": {"type": "string"},
350
+ "factor": {"type": "number", "description": ">1 speeds up, <1 slows down"},
351
+ },
352
+ "required": ["input_path", "factor"],
353
+ },
354
+ "aria.video.transcribe": {
355
+ "type": "object",
356
+ "properties": {
357
+ "input_path": {"type": "string"},
358
+ "model_size": {"type": "string", "description": "faster-whisper model size, default 'base'"},
359
+ "language": {"type": "string", "description": "ISO language code; omit to auto-detect"},
360
+ },
361
+ "required": ["input_path"],
362
+ },
363
+ "aria.video.detect_scenes": {
364
+ "type": "object",
365
+ "properties": {
366
+ "input_path": {"type": "string"},
367
+ "threshold": {"type": "number", "description": "Higher = less sensitive (default 30.0)"},
368
+ },
369
+ "required": ["input_path"],
370
+ },
371
+ "aria.video.generate_estimate": {
372
+ "type": "object",
373
+ "properties": {
374
+ "provider": {"type": "string", "enum": ["kling", "runway"]},
375
+ "duration": {"type": "integer", "description": "Video length in seconds"},
376
+ },
377
+ "required": ["provider", "duration"],
378
+ },
379
+ "aria.video.generate_submit": {
380
+ "type": "object",
381
+ "properties": {
382
+ "provider": {"type": "string", "enum": ["kling", "runway"]},
383
+ "prompt": {"type": "string"},
384
+ "duration": {"type": "integer", "description": "Default: 5"},
385
+ "aspect_ratio": {"type": "string", "description": "e.g. \"16:9\" (kling) or \"1280:720\" (runway) — format is provider-specific"},
386
+ "confirmed": {"type": "boolean", "description": "Must be true — this spends real money the instant it succeeds"},
387
+ },
388
+ "required": ["provider", "prompt", "confirmed"],
389
+ },
390
+ "aria.video.generate_status": {
391
+ "type": "object",
392
+ "properties": {
393
+ "provider": {"type": "string", "enum": ["kling", "runway"]},
394
+ "task_id": {"type": "string"},
395
+ },
396
+ "required": ["provider", "task_id"],
397
+ },
398
+ }
399
+
400
+ # Exposures that are NOT read_only (they write a local file, a preview
401
+ # record, a Canva design draft, or a paid cloud generation job) but are
402
+ # individually verified safe to reach from MCP without a human in this
403
+ # module blocking it first. Almost all of them can't move money or place a
404
+ # live trade — the exceptions are aria.report.generate_image / edit_image
405
+ # (real per-call OpenAI cost, billed the instant it succeeds — safe here
406
+ # only because their handlers hard-refuse without confirmed=true, see
407
+ # _call_generate_image / _call_edit_image), aria.video.generate_submit (same
408
+ # pattern for cloud video generation, see _call_video_generate_submit), and
409
+ # aria.broker.confirm_order (real trade execution — safe here only because
410
+ # its handler hard-refuses unless the broker was separately, explicitly
411
+ # opted into chat-confirm at the aria-code terminal keyboard via
412
+ # /trade allow-chat-confirm, AND confirmed=true is passed; see
413
+ # _call_broker_confirm_order and the module docstring). Every other
414
+ # non-read_only exposure stays blocked by default — see _build_tools().
415
+ _WRITE_SAFE = {
416
+ "aria.broker.preview_order", "aria.broker.confirm_order",
417
+ "aria.report.chart", "aria.report.indicator_chart", "aria.report.comparison_chart",
418
+ "aria.report.allocation_chart", "aria.report.pdf",
419
+ "aria.report.canva_design", "aria.report.canva_upload_asset",
420
+ "aria.report.docx", "aria.report.pptx",
421
+ "aria.report.generate", "aria.backtest.run",
422
+ "aria.report.generate_image", "aria.report.edit_image",
423
+ "aria.report.generate_image_local", "aria.report.edit_image_local",
424
+ "aria.video.trim", "aria.video.concat", "aria.video.overlay_text",
425
+ "aria.video.overlay_audio", "aria.video.convert", "aria.video.change_speed",
426
+ "aria.video.generate_submit",
427
+ }
428
+
429
+
430
+ # ---------------------------------------------------------------------------
431
+ # Handlers — each is `async def handler(arguments: dict) -> dict`
432
+ # ---------------------------------------------------------------------------
433
+
434
+ async def _call_market_quote(args: Dict[str, Any]) -> Dict[str, Any]:
435
+ from runtime.tool_executor import ToolExecutor
436
+ from apps.cli.tools.market_tools import tool_get_market_data
437
+
438
+ executor = ToolExecutor({"get_market_data": (tool_get_market_data, "quote + technicals")})
439
+ return await executor.execute("get_market_data", {"symbol": args.get("symbol", "")})
440
+
441
+
442
+ async def _call_agent_team(args: Dict[str, Any]) -> Dict[str, Any]:
443
+ from dataclasses import asdict
444
+
445
+ from agents.team import run_team
446
+ from datasources.router import get_router
447
+
448
+ symbol = str(args.get("symbol", "")).strip()
449
+ if not symbol:
450
+ return {"success": False, "error": "symbol is required"}
451
+ try:
452
+ data_router = get_router()
453
+ except Exception as exc:
454
+ logger.debug("get_router() unavailable, running without live data bundle: %s", exc)
455
+ data_router = None
456
+ # llm_provider intentionally left None: agents fall back to their
457
+ # deterministic template mode (see agents/base.py) rather than this
458
+ # server needing to bootstrap the CLI's full provider/config stack.
459
+ # Output is real (real data, real rule-based signals) but not
460
+ # LLM-narrated — documented in the tool description surfaced to clients.
461
+ result = await run_team(symbol, agents=args.get("agents"), llm_provider=None, data_router=data_router)
462
+ return {"success": True, **asdict(result)}
463
+
464
+
465
+ async def _call_artifacts_list(args: Dict[str, Any]) -> Dict[str, Any]:
466
+ from artifacts import recent_artifacts_all
467
+
468
+ limit = int(args.get("limit", 20) or 20)
469
+ return {"success": True, "artifacts": recent_artifacts_all(limit=limit)}
470
+
471
+
472
+ def _skill_summary(skill: Any) -> Dict[str, Any]:
473
+ """Listing shape — deliberately excludes `instructions` so a list call
474
+ stays small; aria.skill.get fetches the full text for one skill."""
475
+ return {
476
+ "name": skill.name,
477
+ "qualified_name": skill.qualified_name,
478
+ "description": skill.description,
479
+ "plugin": skill.plugin_name,
480
+ # Surfaced so the caller can tell a signed catalog skill from an
481
+ # unverified local drop-in — these are instruction documents the
482
+ # model will follow, so provenance is worth showing, not hiding.
483
+ "integrity": skill.integrity,
484
+ }
485
+
486
+
487
+ async def _call_skill_list(args: Dict[str, Any]) -> Dict[str, Any]:
488
+ from packages.aria_skills.loader import discover_external_skills, select_external_skills
489
+
490
+ query = str(args.get("query", "")).strip()
491
+ try:
492
+ skills = discover_external_skills()
493
+ except Exception as exc:
494
+ return {"success": False, "error": str(exc)}
495
+
496
+ if query:
497
+ ranked = select_external_skills(query, skills)
498
+ # select_* returns only what clears its relevance bar, which can be
499
+ # empty for an off-topic query — fall back to the full list rather
500
+ # than answering "no skills exist", which would be misleading.
501
+ chosen = ranked or skills
502
+ matched = bool(ranked)
503
+ else:
504
+ chosen = skills
505
+ matched = False
506
+
507
+ return {
508
+ "success": True,
509
+ "query": query,
510
+ "matched_by_relevance": matched,
511
+ "skills": [_skill_summary(s) for s in chosen],
512
+ }
513
+
514
+
515
+ def _skill_reference_names(skill: Any) -> List[str]:
516
+ """Reference docs bundled beside a SKILL.md. Several skills' instructions
517
+ say things like "read references/density_and_semantics.md first" — over
518
+ MCP the caller has no filesystem access to this machine, so those files
519
+ have to be fetchable through this tool or that instruction is a dead end."""
520
+ try:
521
+ ref_dir = Path(skill.path).parent / "references"
522
+ if not ref_dir.is_dir():
523
+ return []
524
+ return sorted(p.name for p in ref_dir.glob("*.md"))
525
+ except Exception:
526
+ return []
527
+
528
+
529
+ async def _call_skill_get(args: Dict[str, Any]) -> Dict[str, Any]:
530
+ from packages.aria_skills.loader import discover_external_skills
531
+
532
+ name = str(args.get("name", "")).strip()
533
+ if not name:
534
+ return {"success": False, "error": "name is required"}
535
+
536
+ try:
537
+ skills = discover_external_skills()
538
+ except Exception as exc:
539
+ return {"success": False, "error": str(exc)}
540
+
541
+ match = next(
542
+ (s for s in skills if name in (s.name, s.qualified_name)),
543
+ None,
544
+ )
545
+ if match is None:
546
+ return {
547
+ "success": False,
548
+ "error": f"No skill named {name!r}. Call aria.skill.list to see installed skills.",
549
+ "available": [s.qualified_name for s in skills],
550
+ }
551
+
552
+ references = _skill_reference_names(match)
553
+ requested = str(args.get("reference", "")).strip()
554
+ if requested:
555
+ # `reference` is caller-controlled and lands in a filesystem path, so
556
+ # resolve it and require the result to stay inside this skill's own
557
+ # references/ dir — a bare name check would still let "../../.ssh/id_rsa"
558
+ # or a symlink out of the sandbox through.
559
+ ref_dir = (Path(match.path).parent / "references").resolve()
560
+ target = (ref_dir / requested).resolve()
561
+ if not str(target).startswith(str(ref_dir) + os.sep) or not target.is_file():
562
+ return {
563
+ "success": False,
564
+ "error": f"No reference {requested!r} in skill {match.qualified_name!r}.",
565
+ "references": references,
566
+ }
567
+ try:
568
+ content = target.read_text(encoding="utf-8")
569
+ except Exception as exc:
570
+ return {"success": False, "error": str(exc)}
571
+ return {
572
+ "success": True,
573
+ **_skill_summary(match),
574
+ "reference": requested,
575
+ "content": content,
576
+ }
577
+
578
+ return {
579
+ "success": True,
580
+ **_skill_summary(match),
581
+ "instructions": match.instructions,
582
+ "references": references,
583
+ }
584
+
585
+
586
+ def _get_broker(broker_id: str = ""):
587
+ from brokers.registry import get_registry
588
+
589
+ reg = get_registry()
590
+ if broker_id:
591
+ return reg.connect(broker_id)
592
+ broker = reg.connect_default()
593
+ if broker is None:
594
+ raise ValueError("No broker_id given and no default broker configured (~/.aria-code/brokers.json)")
595
+ return broker
596
+
597
+
598
+ async def _call_broker_positions(args: Dict[str, Any]) -> Dict[str, Any]:
599
+ import asyncio
600
+ from dataclasses import asdict
601
+
602
+ loop = asyncio.get_event_loop()
603
+
604
+ def _run():
605
+ broker = _get_broker(str(args.get("broker_id", "")))
606
+ return asdict(broker.account_info()), [asdict(p) for p in broker.positions()]
607
+
608
+ try:
609
+ account, positions = await loop.run_in_executor(None, _run)
610
+ except Exception as exc:
611
+ return {"success": False, "error": str(exc)}
612
+ return {"success": True, "account": account, "positions": positions}
613
+
614
+
615
+ async def _call_broker_list_previews(args: Dict[str, Any]) -> Dict[str, Any]:
616
+ from brokers.trading import list_order_previews
617
+
618
+ limit = int(args.get("limit", 10) or 10)
619
+ return {"success": True, "previews": list_order_previews(limit=limit)}
620
+
621
+
622
+ async def _call_broker_preview_order(args: Dict[str, Any]) -> Dict[str, Any]:
623
+ # Building a preview here can never by itself result in a live order —
624
+ # execute_order_preview is only imported by _call_broker_confirm_order,
625
+ # which has its own two-gate check. See module docstring.
626
+ import asyncio
627
+
628
+ from brokers.trading import OrderIntent, build_order_preview
629
+
630
+ symbol = str(args.get("symbol", "")).strip()
631
+ side = str(args.get("side", "")).strip().lower()
632
+ if not symbol or side not in ("buy", "sell"):
633
+ return {"success": False, "error": "symbol and side (buy/sell) are required"}
634
+
635
+ intent = OrderIntent(
636
+ symbol=symbol,
637
+ side=side,
638
+ quantity=args.get("quantity"),
639
+ order_type=str(args.get("order_type") or "limit"),
640
+ price=args.get("price"),
641
+ source="mcp",
642
+ )
643
+ loop = asyncio.get_event_loop()
644
+
645
+ def _run():
646
+ broker = _get_broker(str(args.get("broker_id", "")))
647
+ return build_order_preview(broker, intent)
648
+
649
+ try:
650
+ preview = await loop.run_in_executor(None, _run)
651
+ except Exception as exc:
652
+ return {"success": False, "error": str(exc)}
653
+ return {"success": True, **preview}
654
+
655
+
656
+ async def _call_broker_confirm_order(args: Dict[str, Any]) -> Dict[str, Any]:
657
+ # The one place in this module that imports execute_order_preview. Two
658
+ # gates, both required, checked here (not just trusted from the caller):
659
+ # 1. is_chat_confirm_enabled(broker_id) — set only by a human running
660
+ # /trade allow-chat-confirm <broker_id> at the aria-code terminal
661
+ # keyboard and retyping the exact broker id. Off by default for
662
+ # every broker. This gate cannot be flipped on from MCP/chat.
663
+ # 2. confirmed is True on this specific call.
664
+ # Both must pass before execute_order_preview(confirmed=True) is ever
665
+ # reached. See the module docstring for the full rationale.
666
+ import asyncio
667
+
668
+ from brokers.config import is_chat_confirm_enabled
669
+ from brokers.trading import execute_order_preview
670
+
671
+ preview_id = str(args.get("preview_id", "")).strip()
672
+ if not preview_id:
673
+ return {"success": False, "error": "preview_id is required"}
674
+ if args.get("confirmed") is not True:
675
+ return {
676
+ "success": False,
677
+ "error": "confirmed must be true to execute a real order — this places a live trade.",
678
+ }
679
+
680
+ broker_id = str(args.get("broker_id", "")).strip()
681
+ loop = asyncio.get_event_loop()
682
+
683
+ def _run():
684
+ broker = _get_broker(broker_id)
685
+ resolved_broker_id = broker_id or getattr(broker, "broker_id", "")
686
+ if not is_chat_confirm_enabled(resolved_broker_id):
687
+ return {
688
+ "success": False,
689
+ "error": (
690
+ f"Chat-confirmed execution is not enabled for broker '{resolved_broker_id}'. "
691
+ f"Run /trade allow-chat-confirm {resolved_broker_id} in the aria-code terminal "
692
+ "yourself first — this cannot be turned on from chat/MCP."
693
+ ),
694
+ }
695
+ return execute_order_preview(broker, preview_id, confirmed=True, source="chat_mcp")
696
+
697
+ try:
698
+ return await loop.run_in_executor(None, _run)
699
+ except Exception as exc:
700
+ return {"success": False, "error": str(exc)}
701
+
702
+
703
+ async def _call_report_chart(args: Dict[str, Any]) -> Dict[str, Any]:
704
+ import asyncio
705
+ import base64
706
+
707
+ from artifacts import create_user_artifact
708
+ from report_generator import _fetch_report_data_sync, generate_price_chart
709
+
710
+ symbol = str(args.get("symbol", "")).strip().upper()
711
+ if not symbol:
712
+ return {"success": False, "error": "symbol is required"}
713
+
714
+ loop = asyncio.get_event_loop()
715
+
716
+ def _run():
717
+ df, _clean_result, fundamentals = _fetch_report_data_sync(symbol)
718
+ if df is None or df.empty:
719
+ return None
720
+ return generate_price_chart(df, symbol, fundamentals)
721
+
722
+ try:
723
+ b64_png = await loop.run_in_executor(None, _run)
724
+ except Exception as exc:
725
+ return {"success": False, "error": str(exc)}
726
+ if not b64_png:
727
+ return {"success": False, "error": f"No price data available for {symbol}"}
728
+
729
+ artifact = create_user_artifact("chart", symbol, f"{symbol}_chart", ".png")
730
+ artifact.path.write_bytes(base64.b64decode(b64_png))
731
+ return {"success": True, "path": str(artifact.path)}
732
+
733
+
734
+ async def _call_indicator_chart(args: Dict[str, Any]) -> Dict[str, Any]:
735
+ import asyncio
736
+ import base64
737
+
738
+ from artifacts import create_user_artifact
739
+ from report_generator import _fetch_report_data_sync, generate_indicator_chart
740
+
741
+ symbol = str(args.get("symbol", "")).strip().upper()
742
+ if not symbol:
743
+ return {"success": False, "error": "symbol is required"}
744
+
745
+ loop = asyncio.get_event_loop()
746
+
747
+ def _run():
748
+ df, _clean_result, _fundamentals = _fetch_report_data_sync(symbol)
749
+ if df is None or df.empty:
750
+ return None
751
+ return generate_indicator_chart(df, symbol)
752
+
753
+ try:
754
+ b64_png = await loop.run_in_executor(None, _run)
755
+ except Exception as exc:
756
+ return {"success": False, "error": str(exc)}
757
+ if not b64_png:
758
+ return {"success": False, "error": f"No price data available for {symbol}"}
759
+
760
+ artifact = create_user_artifact("chart", symbol, f"{symbol}_indicators", ".png")
761
+ artifact.path.write_bytes(base64.b64decode(b64_png))
762
+ return {"success": True, "path": str(artifact.path)}
763
+
764
+
765
+ async def _call_comparison_chart(args: Dict[str, Any]) -> Dict[str, Any]:
766
+ import asyncio
767
+ import base64
768
+
769
+ from artifacts import create_user_artifact
770
+ from report_generator import _fetch_report_data_sync, generate_comparison_chart
771
+
772
+ symbols = args.get("symbols") or []
773
+ if not isinstance(symbols, list) or len(symbols) < 2:
774
+ return {"success": False, "error": "symbols must be a list of at least 2 tickers"}
775
+ symbols = [str(s).strip().upper() for s in symbols if str(s).strip()]
776
+ title = str(args.get("title") or "").strip()
777
+
778
+ loop = asyncio.get_event_loop()
779
+
780
+ def _run():
781
+ price_data = {}
782
+ for symbol in symbols:
783
+ try:
784
+ df, _clean_result, _fundamentals = _fetch_report_data_sync(symbol)
785
+ if df is not None and not df.empty:
786
+ price_data[symbol] = df
787
+ except Exception as exc:
788
+ logger.debug("[mcp] comparison_chart: skipping %s (%s)", symbol, exc)
789
+ return generate_comparison_chart(price_data, title)
790
+
791
+ try:
792
+ b64_png = await loop.run_in_executor(None, _run)
793
+ except Exception as exc:
794
+ return {"success": False, "error": str(exc)}
795
+ if not b64_png:
796
+ return {"success": False, "error": f"No usable price data for any of {symbols}"}
797
+
798
+ artifact = create_user_artifact("chart", "_".join(symbols[:4]), "comparison", ".png")
799
+ artifact.path.write_bytes(base64.b64decode(b64_png))
800
+ return {"success": True, "path": str(artifact.path)}
801
+
802
+
803
+ async def _call_allocation_chart(args: Dict[str, Any]) -> Dict[str, Any]:
804
+ import asyncio
805
+ import base64
806
+
807
+ from artifacts import create_user_artifact
808
+ from report_generator import generate_allocation_chart
809
+
810
+ title = str(args.get("title") or "").strip()
811
+ loop = asyncio.get_event_loop()
812
+
813
+ def _asdict_position(p):
814
+ from dataclasses import asdict, is_dataclass
815
+ return asdict(p) if is_dataclass(p) else dict(p)
816
+
817
+ def _run():
818
+ broker = _get_broker(str(args.get("broker_id", "")))
819
+ positions = [_asdict_position(p) for p in broker.positions()]
820
+ return generate_allocation_chart(positions, title)
821
+
822
+ try:
823
+ b64_png = await loop.run_in_executor(None, _run)
824
+ except Exception as exc:
825
+ return {"success": False, "error": str(exc)}
826
+ if not b64_png:
827
+ return {"success": False, "error": "No positions with usable market value found"}
828
+
829
+ artifact = create_user_artifact("chart", "portfolio", "allocation", ".png")
830
+ artifact.path.write_bytes(base64.b64decode(b64_png))
831
+ return {"success": True, "path": str(artifact.path)}
832
+
833
+
834
+ async def _call_report_pdf(args: Dict[str, Any]) -> Dict[str, Any]:
835
+ import asyncio
836
+
837
+ from artifacts import create_user_artifact
838
+ from markdown_pdf import markdown_to_pdf
839
+
840
+ md = str(args.get("markdown", ""))
841
+ if not md.strip():
842
+ return {"success": False, "error": "markdown is required"}
843
+ title = str(args.get("title") or "report").strip() or "report"
844
+
845
+ artifact = create_user_artifact("report", title, title, ".pdf")
846
+ loop = asyncio.get_event_loop()
847
+ try:
848
+ result_path = await loop.run_in_executor(None, markdown_to_pdf, md, artifact.path)
849
+ except Exception as exc:
850
+ return {"success": False, "error": str(exc)}
851
+ if not result_path:
852
+ return {"success": False, "error": "PDF rendering failed (no HTML→PDF engine available)"}
853
+ return {"success": True, "path": str(result_path)}
854
+
855
+
856
+ async def _call_report_generate(args: Dict[str, Any]) -> Dict[str, Any]:
857
+ from report_generator import generate_report
858
+
859
+ symbol = str(args.get("symbol", "")).strip().upper()
860
+ if not symbol:
861
+ return {"success": False, "error": "symbol is required"}
862
+ try:
863
+ path = await generate_report(symbol)
864
+ except Exception as exc:
865
+ return {"success": False, "error": str(exc)}
866
+ if not path:
867
+ return {"success": False, "error": f"Report generation failed for {symbol} (no data?)"}
868
+ return {"success": True, "path": str(path)}
869
+
870
+
871
+ async def _call_backtest_run(args: Dict[str, Any]) -> Dict[str, Any]:
872
+ import asyncio
873
+ from dataclasses import asdict
874
+
875
+ from backtest_engine import BacktestEngine, get_strategy, load_bars
876
+
877
+ symbol = str(args.get("symbol", "")).strip().upper()
878
+ if not symbol:
879
+ return {"success": False, "error": "symbol is required"}
880
+ strategy_name = str(args.get("strategy") or "sma_cross")
881
+ days = int(args.get("days", 365) or 365)
882
+
883
+ loop = asyncio.get_event_loop()
884
+
885
+ def _run():
886
+ bars = load_bars(symbol, days=days)
887
+ if not bars:
888
+ raise ValueError(f"No price history available for {symbol}")
889
+ strategy = get_strategy(strategy_name)
890
+ engine = BacktestEngine()
891
+ return engine.run({symbol: bars}, strategy)
892
+
893
+ try:
894
+ result = await loop.run_in_executor(None, _run)
895
+ except Exception as exc:
896
+ return {"success": False, "error": str(exc)}
897
+ return {"success": True, **asdict(result)}
898
+
899
+
900
+ async def _call_figma_read_file(args: Dict[str, Any]) -> Dict[str, Any]:
901
+ import asyncio
902
+
903
+ from figma_client import get_file_summary
904
+
905
+ file_key = str(args.get("file_key", "")).strip()
906
+ if not file_key:
907
+ return {"success": False, "error": "file_key is required"}
908
+ depth = int(args.get("depth", 2) or 2)
909
+ loop = asyncio.get_event_loop()
910
+ try:
911
+ return await loop.run_in_executor(None, lambda: get_file_summary(file_key, depth=depth))
912
+ except Exception as exc:
913
+ return {"success": False, "error": str(exc)}
914
+
915
+
916
+ async def _call_figma_comments(args: Dict[str, Any]) -> Dict[str, Any]:
917
+ import asyncio
918
+
919
+ from figma_client import list_comments
920
+
921
+ file_key = str(args.get("file_key", "")).strip()
922
+ if not file_key:
923
+ return {"success": False, "error": "file_key is required"}
924
+ loop = asyncio.get_event_loop()
925
+ try:
926
+ return await loop.run_in_executor(None, list_comments, file_key)
927
+ except Exception as exc:
928
+ return {"success": False, "error": str(exc)}
929
+
930
+
931
+ async def _call_video_probe(args: Dict[str, Any]) -> Dict[str, Any]:
932
+ import asyncio
933
+
934
+ from video_editor import probe_video
935
+
936
+ input_path = str(args.get("input_path", "")).strip()
937
+ if not input_path:
938
+ return {"success": False, "error": "input_path is required"}
939
+ loop = asyncio.get_event_loop()
940
+ try:
941
+ return await loop.run_in_executor(None, probe_video, input_path)
942
+ except Exception as exc:
943
+ return {"success": False, "error": str(exc)}
944
+
945
+
946
+ async def _call_video_trim(args: Dict[str, Any]) -> Dict[str, Any]:
947
+ import asyncio
948
+ from functools import partial
949
+
950
+ from video_editor import trim_video
951
+
952
+ input_path = str(args.get("input_path", "")).strip()
953
+ if not input_path or "start" not in args or "end" not in args:
954
+ return {"success": False, "error": "input_path, start, and end are required"}
955
+ loop = asyncio.get_event_loop()
956
+ fn = partial(trim_video, input_path, float(args["start"]), float(args["end"]))
957
+ try:
958
+ return await loop.run_in_executor(None, fn)
959
+ except Exception as exc:
960
+ return {"success": False, "error": str(exc)}
961
+
962
+
963
+ async def _call_video_concat(args: Dict[str, Any]) -> Dict[str, Any]:
964
+ import asyncio
965
+
966
+ from video_editor import concat_videos
967
+
968
+ input_paths = args.get("input_paths") or []
969
+ if not isinstance(input_paths, list) or len(input_paths) < 2:
970
+ return {"success": False, "error": "input_paths must be a list of at least 2 paths"}
971
+ loop = asyncio.get_event_loop()
972
+ try:
973
+ return await loop.run_in_executor(None, concat_videos, [str(p) for p in input_paths])
974
+ except Exception as exc:
975
+ return {"success": False, "error": str(exc)}
976
+
977
+
978
+ async def _call_video_overlay_text(args: Dict[str, Any]) -> Dict[str, Any]:
979
+ import asyncio
980
+ from functools import partial
981
+
982
+ from video_editor import overlay_text
983
+
984
+ input_path = str(args.get("input_path", "")).strip()
985
+ text = str(args.get("text", ""))
986
+ if not input_path or not text:
987
+ return {"success": False, "error": "input_path and text are required"}
988
+ loop = asyncio.get_event_loop()
989
+ fn = partial(
990
+ overlay_text, input_path, text,
991
+ position=args.get("position") or "bottom",
992
+ font_size=int(args.get("font_size", 36) or 36),
993
+ font_color=args.get("font_color") or "white",
994
+ )
995
+ try:
996
+ return await loop.run_in_executor(None, fn)
997
+ except Exception as exc:
998
+ return {"success": False, "error": str(exc)}
999
+
1000
+
1001
+ async def _call_video_overlay_audio(args: Dict[str, Any]) -> Dict[str, Any]:
1002
+ import asyncio
1003
+ from functools import partial
1004
+
1005
+ from video_editor import overlay_audio
1006
+
1007
+ input_path = str(args.get("input_path", "")).strip()
1008
+ audio_path = str(args.get("audio_path", "")).strip()
1009
+ if not input_path or not audio_path:
1010
+ return {"success": False, "error": "input_path and audio_path are required"}
1011
+ loop = asyncio.get_event_loop()
1012
+ fn = partial(overlay_audio, input_path, audio_path, replace=bool(args.get("replace", False)))
1013
+ try:
1014
+ return await loop.run_in_executor(None, fn)
1015
+ except Exception as exc:
1016
+ return {"success": False, "error": str(exc)}
1017
+
1018
+
1019
+ async def _call_video_convert(args: Dict[str, Any]) -> Dict[str, Any]:
1020
+ import asyncio
1021
+ from functools import partial
1022
+
1023
+ from video_editor import convert_video
1024
+
1025
+ input_path = str(args.get("input_path", "")).strip()
1026
+ if not input_path:
1027
+ return {"success": False, "error": "input_path is required"}
1028
+ loop = asyncio.get_event_loop()
1029
+ fn = partial(convert_video, input_path, output_format=args.get("output_format"), aspect=args.get("aspect"))
1030
+ try:
1031
+ return await loop.run_in_executor(None, fn)
1032
+ except Exception as exc:
1033
+ return {"success": False, "error": str(exc)}
1034
+
1035
+
1036
+ async def _call_video_change_speed(args: Dict[str, Any]) -> Dict[str, Any]:
1037
+ import asyncio
1038
+ from functools import partial
1039
+
1040
+ from video_editor import change_speed
1041
+
1042
+ input_path = str(args.get("input_path", "")).strip()
1043
+ if not input_path or "factor" not in args:
1044
+ return {"success": False, "error": "input_path and factor are required"}
1045
+ loop = asyncio.get_event_loop()
1046
+ fn = partial(change_speed, input_path, float(args["factor"]))
1047
+ try:
1048
+ return await loop.run_in_executor(None, fn)
1049
+ except Exception as exc:
1050
+ return {"success": False, "error": str(exc)}
1051
+
1052
+
1053
+ async def _call_video_transcribe(args: Dict[str, Any]) -> Dict[str, Any]:
1054
+ import asyncio
1055
+ from functools import partial
1056
+
1057
+ from video_analysis import transcribe_video
1058
+
1059
+ input_path = str(args.get("input_path", "")).strip()
1060
+ if not input_path:
1061
+ return {"success": False, "error": "input_path is required"}
1062
+ loop = asyncio.get_event_loop()
1063
+ fn = partial(
1064
+ transcribe_video, input_path,
1065
+ model_size=args.get("model_size") or "base",
1066
+ language=args.get("language"),
1067
+ )
1068
+ try:
1069
+ return await loop.run_in_executor(None, fn)
1070
+ except Exception as exc:
1071
+ return {"success": False, "error": str(exc)}
1072
+
1073
+
1074
+ async def _call_video_detect_scenes(args: Dict[str, Any]) -> Dict[str, Any]:
1075
+ import asyncio
1076
+ from functools import partial
1077
+
1078
+ from video_analysis import detect_scenes
1079
+
1080
+ input_path = str(args.get("input_path", "")).strip()
1081
+ if not input_path:
1082
+ return {"success": False, "error": "input_path is required"}
1083
+ loop = asyncio.get_event_loop()
1084
+ fn = partial(detect_scenes, input_path, threshold=float(args.get("threshold", 30.0) or 30.0))
1085
+ try:
1086
+ return await loop.run_in_executor(None, fn)
1087
+ except Exception as exc:
1088
+ return {"success": False, "error": str(exc)}
1089
+
1090
+
1091
+ _VIDEO_GEN_PROVIDERS = {"kling": "kling_video_client", "runway": "runway_video_client"}
1092
+
1093
+
1094
+ async def _call_video_generate_estimate(args: Dict[str, Any]) -> Dict[str, Any]:
1095
+ provider = str(args.get("provider", "")).strip().lower()
1096
+ module_name = _VIDEO_GEN_PROVIDERS.get(provider)
1097
+ if not module_name:
1098
+ return {"success": False, "error": f"provider must be one of {sorted(_VIDEO_GEN_PROVIDERS)}"}
1099
+ duration = int(args.get("duration", 0) or 0)
1100
+ if duration <= 0:
1101
+ return {"success": False, "error": "duration must be a positive number of seconds"}
1102
+
1103
+ import importlib
1104
+ client = importlib.import_module(module_name)
1105
+ return {"success": True, **client.estimate_cost(duration)}
1106
+
1107
+
1108
+ async def _call_video_generate_submit(args: Dict[str, Any]) -> Dict[str, Any]:
1109
+ # Real money is spent the instant client.submit_video() succeeds — this
1110
+ # check is the actual safety boundary for aria.video.generate_submit
1111
+ # being in _WRITE_SAFE, not just the allowlist membership itself.
1112
+ if args.get("confirmed") is not True:
1113
+ return {
1114
+ "success": False,
1115
+ "error": (
1116
+ "confirmed must be true to submit a real (paid) video generation job. "
1117
+ "Call aria.video.generate_estimate first to see the cost, then resubmit "
1118
+ "with confirmed=true."
1119
+ ),
1120
+ }
1121
+
1122
+ provider = str(args.get("provider", "")).strip().lower()
1123
+ module_name = _VIDEO_GEN_PROVIDERS.get(provider)
1124
+ if not module_name:
1125
+ return {"success": False, "error": f"provider must be one of {sorted(_VIDEO_GEN_PROVIDERS)}"}
1126
+ prompt = str(args.get("prompt", "")).strip()
1127
+ if not prompt:
1128
+ return {"success": False, "error": "prompt is required"}
1129
+
1130
+ import asyncio
1131
+ import importlib
1132
+ from functools import partial
1133
+
1134
+ client = importlib.import_module(module_name)
1135
+ kwargs: Dict[str, Any] = {"duration": int(args.get("duration", 5) or 5)}
1136
+ aspect_ratio = args.get("aspect_ratio")
1137
+ if aspect_ratio:
1138
+ kwargs["aspect_ratio" if provider == "kling" else "ratio"] = aspect_ratio
1139
+
1140
+ loop = asyncio.get_event_loop()
1141
+ fn = partial(client.submit_video, prompt, **kwargs)
1142
+ try:
1143
+ return await loop.run_in_executor(None, fn)
1144
+ except Exception as exc:
1145
+ return {"success": False, "error": str(exc)}
1146
+
1147
+
1148
+ async def _call_video_generate_status(args: Dict[str, Any]) -> Dict[str, Any]:
1149
+ provider = str(args.get("provider", "")).strip().lower()
1150
+ module_name = _VIDEO_GEN_PROVIDERS.get(provider)
1151
+ if not module_name:
1152
+ return {"success": False, "error": f"provider must be one of {sorted(_VIDEO_GEN_PROVIDERS)}"}
1153
+ task_id = str(args.get("task_id", "")).strip()
1154
+ if not task_id:
1155
+ return {"success": False, "error": "task_id is required"}
1156
+
1157
+ import asyncio
1158
+ import importlib
1159
+
1160
+ client = importlib.import_module(module_name)
1161
+ loop = asyncio.get_event_loop()
1162
+ try:
1163
+ return await loop.run_in_executor(None, client.poll_video, task_id)
1164
+ except Exception as exc:
1165
+ return {"success": False, "error": str(exc)}
1166
+
1167
+
1168
+ async def _call_report_docx(args: Dict[str, Any]) -> Dict[str, Any]:
1169
+ import asyncio
1170
+
1171
+ from artifacts import create_user_artifact
1172
+ from report_exporters import markdown_to_docx
1173
+
1174
+ md = str(args.get("markdown", ""))
1175
+ if not md.strip():
1176
+ return {"success": False, "error": "markdown is required"}
1177
+ title = str(args.get("title") or "report").strip() or "report"
1178
+
1179
+ artifact = create_user_artifact("report", title, title, ".docx")
1180
+ loop = asyncio.get_event_loop()
1181
+ try:
1182
+ result_path = await loop.run_in_executor(None, markdown_to_docx, md, artifact.path, title)
1183
+ except Exception as exc:
1184
+ return {"success": False, "error": str(exc)}
1185
+ if not result_path:
1186
+ return {"success": False, "error": "DOCX rendering failed (python-docx not installed?)"}
1187
+ return {"success": True, "path": str(result_path)}
1188
+
1189
+
1190
+ async def _call_report_pptx(args: Dict[str, Any]) -> Dict[str, Any]:
1191
+ import asyncio
1192
+
1193
+ from artifacts import create_user_artifact
1194
+ from report_exporters import markdown_to_pptx
1195
+
1196
+ md = str(args.get("markdown", ""))
1197
+ if not md.strip():
1198
+ return {"success": False, "error": "markdown is required"}
1199
+ title = str(args.get("title") or "report").strip() or "report"
1200
+
1201
+ artifact = create_user_artifact("report", title, title, ".pptx")
1202
+ loop = asyncio.get_event_loop()
1203
+ try:
1204
+ result_path = await loop.run_in_executor(None, markdown_to_pptx, md, artifact.path, title)
1205
+ except Exception as exc:
1206
+ return {"success": False, "error": str(exc)}
1207
+ if not result_path:
1208
+ return {"success": False, "error": "PPTX rendering failed (python-pptx not installed?)"}
1209
+ return {"success": True, "path": str(result_path)}
1210
+
1211
+
1212
+ async def _call_estimate_image_cost(args: Dict[str, Any]) -> Dict[str, Any]:
1213
+ from openai_image_client import estimate_cost
1214
+
1215
+ return {"success": True, **estimate_cost(size=args.get("size") or "1024x1536", quality=args.get("quality") or "high")}
1216
+
1217
+
1218
+ async def _call_generate_image(args: Dict[str, Any]) -> Dict[str, Any]:
1219
+ # Real money is spent the instant generate_image() calls OpenAI — this
1220
+ # check is the actual safety boundary, not just _WRITE_SAFE membership
1221
+ # (same pattern as _call_video_generate_submit).
1222
+ if args.get("confirmed") is not True:
1223
+ return {
1224
+ "success": False,
1225
+ "error": (
1226
+ "confirmed must be true to generate a real (paid) image. "
1227
+ "Call aria.report.estimate_image_cost first to see the cost, then resubmit with confirmed=true."
1228
+ ),
1229
+ }
1230
+
1231
+ import asyncio
1232
+ from functools import partial
1233
+
1234
+ from openai_image_client import generate_image
1235
+
1236
+ prompt = str(args.get("prompt", "")).strip()
1237
+ if not prompt:
1238
+ return {"success": False, "error": "prompt is required"}
1239
+
1240
+ loop = asyncio.get_event_loop()
1241
+ try:
1242
+ fn = partial(
1243
+ generate_image, prompt,
1244
+ size=args.get("size") or "1024x1536", quality=args.get("quality") or "high",
1245
+ confirmed=True,
1246
+ )
1247
+ return await loop.run_in_executor(None, fn)
1248
+ except Exception as exc:
1249
+ return {"success": False, "error": str(exc)}
1250
+
1251
+
1252
+ async def _call_edit_image(args: Dict[str, Any]) -> Dict[str, Any]:
1253
+ # Same real-money gate as _call_generate_image above.
1254
+ if args.get("confirmed") is not True:
1255
+ return {
1256
+ "success": False,
1257
+ "error": (
1258
+ "confirmed must be true to generate a real (paid) image edit. "
1259
+ "Call aria.report.estimate_image_cost first to see the cost, then resubmit with confirmed=true."
1260
+ ),
1261
+ }
1262
+
1263
+ import asyncio
1264
+ from functools import partial
1265
+
1266
+ from openai_image_client import edit_image
1267
+
1268
+ image_path = str(args.get("image_path", "")).strip()
1269
+ prompt = str(args.get("prompt", "")).strip()
1270
+ if not image_path or not prompt:
1271
+ return {"success": False, "error": "image_path and prompt are required"}
1272
+
1273
+ mask_path = args.get("mask_path")
1274
+ mask_path = str(mask_path).strip() if mask_path else None
1275
+
1276
+ loop = asyncio.get_event_loop()
1277
+ try:
1278
+ fn = partial(
1279
+ edit_image, image_path, prompt,
1280
+ size=args.get("size") or "1024x1536", quality=args.get("quality") or "high",
1281
+ mask_path=mask_path, confirmed=True,
1282
+ )
1283
+ return await loop.run_in_executor(None, fn)
1284
+ except Exception as exc:
1285
+ return {"success": False, "error": str(exc)}
1286
+
1287
+
1288
+ async def _call_generate_image_local(args: Dict[str, Any]) -> Dict[str, Any]:
1289
+ import asyncio
1290
+ from functools import partial
1291
+
1292
+ from local_image_provider import generate_image_local
1293
+
1294
+ prompt = str(args.get("prompt", "")).strip()
1295
+ if not prompt:
1296
+ return {"success": False, "error": "prompt is required"}
1297
+
1298
+ loop = asyncio.get_event_loop()
1299
+ fn = partial(
1300
+ generate_image_local,
1301
+ prompt,
1302
+ width=int(args.get("width", 1024) or 1024),
1303
+ height=int(args.get("height", 1024) or 1024),
1304
+ steps=int(args.get("steps", 4) or 4),
1305
+ )
1306
+ try:
1307
+ return await loop.run_in_executor(None, fn)
1308
+ except Exception as exc:
1309
+ return {"success": False, "error": str(exc)}
1310
+
1311
+
1312
+ async def _call_edit_image_local(args: Dict[str, Any]) -> Dict[str, Any]:
1313
+ import asyncio
1314
+ from functools import partial
1315
+
1316
+ from local_image_provider import edit_image_local
1317
+
1318
+ image_path = str(args.get("image_path", "")).strip()
1319
+ prompt = str(args.get("prompt", "")).strip()
1320
+ if not image_path or not prompt:
1321
+ return {"success": False, "error": "image_path and prompt are required"}
1322
+
1323
+ loop = asyncio.get_event_loop()
1324
+ fn = partial(edit_image_local, image_path, prompt, strength=float(args.get("strength", 0.6) or 0.6))
1325
+ try:
1326
+ return await loop.run_in_executor(None, fn)
1327
+ except Exception as exc:
1328
+ return {"success": False, "error": str(exc)}
1329
+
1330
+
1331
+ async def _call_canva_design(args: Dict[str, Any]) -> Dict[str, Any]:
1332
+ import asyncio
1333
+
1334
+ from canva_client import autofill_design
1335
+
1336
+ template_id = str(args.get("template_id", "")).strip()
1337
+ data = args.get("data") or {}
1338
+ if not template_id or not isinstance(data, dict):
1339
+ return {"success": False, "error": "template_id and data (object) are required"}
1340
+
1341
+ loop = asyncio.get_event_loop()
1342
+ try:
1343
+ result = await loop.run_in_executor(None, autofill_design, template_id, data)
1344
+ except Exception as exc:
1345
+ return {"success": False, "error": str(exc)}
1346
+ return result
1347
+
1348
+
1349
+ async def _call_canva_upload_asset(args: Dict[str, Any]) -> Dict[str, Any]:
1350
+ import asyncio
1351
+
1352
+ from canva_client import upload_asset
1353
+
1354
+ file_path = str(args.get("file_path", "")).strip()
1355
+ if not file_path:
1356
+ return {"success": False, "error": "file_path is required"}
1357
+
1358
+ loop = asyncio.get_event_loop()
1359
+ try:
1360
+ result = await loop.run_in_executor(None, upload_asset, file_path)
1361
+ except Exception as exc:
1362
+ return {"success": False, "error": str(exc)}
1363
+ return result
1364
+
1365
+
1366
+ _HANDLERS: Dict[str, Callable[[Dict[str, Any]], Awaitable[Dict[str, Any]]]] = {
1367
+ "aria.market.quote": _call_market_quote,
1368
+ "aria.agent.team": _call_agent_team,
1369
+ "aria.artifacts.list": _call_artifacts_list,
1370
+ "aria.skill.list": _call_skill_list,
1371
+ "aria.skill.get": _call_skill_get,
1372
+ "aria.broker.positions": _call_broker_positions,
1373
+ "aria.broker.list_previews": _call_broker_list_previews,
1374
+ "aria.broker.preview_order": _call_broker_preview_order,
1375
+ "aria.broker.confirm_order": _call_broker_confirm_order,
1376
+ "aria.report.chart": _call_report_chart,
1377
+ "aria.report.indicator_chart": _call_indicator_chart,
1378
+ "aria.report.comparison_chart": _call_comparison_chart,
1379
+ "aria.report.allocation_chart": _call_allocation_chart,
1380
+ "aria.report.pdf": _call_report_pdf,
1381
+ "aria.report.estimate_image_cost": _call_estimate_image_cost,
1382
+ "aria.report.generate_image": _call_generate_image,
1383
+ "aria.report.edit_image": _call_edit_image,
1384
+ "aria.report.generate_image_local": _call_generate_image_local,
1385
+ "aria.report.edit_image_local": _call_edit_image_local,
1386
+ "aria.report.canva_design": _call_canva_design,
1387
+ "aria.report.canva_upload_asset": _call_canva_upload_asset,
1388
+ "aria.report.docx": _call_report_docx,
1389
+ "aria.report.pptx": _call_report_pptx,
1390
+ "aria.figma.read_file": _call_figma_read_file,
1391
+ "aria.figma.comments": _call_figma_comments,
1392
+ "aria.video.probe": _call_video_probe,
1393
+ "aria.video.trim": _call_video_trim,
1394
+ "aria.video.concat": _call_video_concat,
1395
+ "aria.video.overlay_text": _call_video_overlay_text,
1396
+ "aria.video.overlay_audio": _call_video_overlay_audio,
1397
+ "aria.video.convert": _call_video_convert,
1398
+ "aria.video.change_speed": _call_video_change_speed,
1399
+ "aria.video.transcribe": _call_video_transcribe,
1400
+ "aria.video.detect_scenes": _call_video_detect_scenes,
1401
+ "aria.video.generate_estimate": _call_video_generate_estimate,
1402
+ "aria.video.generate_submit": _call_video_generate_submit,
1403
+ "aria.video.generate_status": _call_video_generate_status,
1404
+ "aria.report.generate": _call_report_generate,
1405
+ "aria.backtest.run": _call_backtest_run,
1406
+ }
1407
+
1408
+
1409
+ def _build_tools() -> List[Dict[str, Any]]:
1410
+ """Real MCP tool descriptors — read-only exposures, plus the explicit
1411
+ _WRITE_SAFE allowlist.
1412
+
1413
+ Hard filter, not just trust in the exposure list: any exposure whose
1414
+ target is skill-backed (not a clean callable in v1), or that isn't
1415
+ read_only AND isn't in _WRITE_SAFE, is dropped here — so a future
1416
+ careless edit to default_exposures() can't silently make a live-trading
1417
+ or otherwise unreviewed write tool reachable. Adding something to
1418
+ _WRITE_SAFE is a deliberate, individually-justified code change, not a
1419
+ side effect of editing the exposure list.
1420
+ """
1421
+ from packages.aria_mcp.bridge import default_exposures
1422
+
1423
+ tools = []
1424
+ for exposure in default_exposures():
1425
+ if not exposure.read_only and exposure.name not in _WRITE_SAFE:
1426
+ continue
1427
+ if exposure.target.startswith("skill:"):
1428
+ continue
1429
+ if exposure.name not in _HANDLERS:
1430
+ continue
1431
+ tools.append({
1432
+ "name": exposure.name,
1433
+ "description": exposure.description,
1434
+ "inputSchema": _INPUT_SCHEMAS.get(exposure.name, {"type": "object", "properties": {}}),
1435
+ })
1436
+ return tools
1437
+
1438
+
1439
+ TOOLS: List[Dict[str, Any]] = _build_tools()
1440
+ _TOOL_NAMES = {t["name"] for t in TOOLS}
1441
+
1442
+
1443
+ # ---------------------------------------------------------------------------
1444
+ # MCP stdio server (JSON-RPC 2.0) — same wire format as
1445
+ # Arthera's packages/quant_engine/mcp_server.py
1446
+ # ---------------------------------------------------------------------------
1447
+
1448
+ def _reply(req_id: Any, result: Any) -> None:
1449
+ msg = json.dumps({"jsonrpc": "2.0", "id": req_id, "result": result}, ensure_ascii=False)
1450
+ sys.stdout.write(msg + "\n")
1451
+ sys.stdout.flush()
1452
+
1453
+
1454
+ def _error(req_id: Any, code: int, message: str) -> None:
1455
+ msg = json.dumps(
1456
+ {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}},
1457
+ ensure_ascii=False,
1458
+ )
1459
+ sys.stdout.write(msg + "\n")
1460
+ sys.stdout.flush()
1461
+
1462
+
1463
+ async def _handle(req: Dict[str, Any]) -> None:
1464
+ method = req.get("method", "")
1465
+ req_id = req.get("id")
1466
+ params = req.get("params", {}) or {}
1467
+
1468
+ if method == "initialize":
1469
+ _reply(req_id, {
1470
+ "protocolVersion": MCP_PROTOCOL_VERSION,
1471
+ "capabilities": {"tools": {"listChanged": False}},
1472
+ "serverInfo": {"name": "aria-code", "version": "1.0.0"},
1473
+ })
1474
+
1475
+ elif method == "notifications/initialized":
1476
+ pass # no response needed for notifications
1477
+
1478
+ elif method == "tools/list":
1479
+ _reply(req_id, {"tools": TOOLS})
1480
+
1481
+ elif method == "tools/call":
1482
+ name = params.get("name", "")
1483
+ arguments = params.get("arguments", {}) or {}
1484
+ if name not in _TOOL_NAMES:
1485
+ _error(req_id, -32601, f"Unknown or non-exposed tool: {name}")
1486
+ return
1487
+ try:
1488
+ result = await _HANDLERS[name](arguments)
1489
+ text = json.dumps(result, ensure_ascii=False, default=str)
1490
+ _reply(req_id, {"content": [{"type": "text", "text": text}]})
1491
+ except TypeError as exc:
1492
+ _error(req_id, -32602, f"Invalid arguments for {name}: {exc}")
1493
+ except Exception as exc:
1494
+ _error(req_id, -32603, f"Tool error: {exc}\n{traceback.format_exc()[-300:]}")
1495
+
1496
+ elif method == "ping":
1497
+ _reply(req_id, {})
1498
+
1499
+ else:
1500
+ if req_id is not None:
1501
+ _error(req_id, -32601, f"Method not found: {method}")
1502
+
1503
+
1504
+ def main() -> None:
1505
+ import asyncio
1506
+
1507
+ # --version/-V is not part of the MCP wire protocol — it exists purely
1508
+ # so a packaged binary (PyInstaller, `aria-code-mcp --version`) can be
1509
+ # smoke-tested the same way aria_cli.py's binary is, without having to
1510
+ # speak JSON-RPC over stdin just to prove the executable launches.
1511
+ # Reads the version via importlib.metadata rather than `from aria_cli
1512
+ # import __version__` deliberately — aria_cli.py's top-level imports
1513
+ # pull in the whole interactive-CLI stack (prompt_toolkit, bootstrap
1514
+ # hooks, ...), which is unnecessary weight for a binary whose only job
1515
+ # here is speaking JSON-RPC over stdio.
1516
+ if any(arg in ("--version", "-V") for arg in sys.argv[1:]):
1517
+ try:
1518
+ from importlib.metadata import version as _pkg_version
1519
+ v = _pkg_version("aria-code")
1520
+ except Exception:
1521
+ v = "unknown"
1522
+ print(f"aria-code-mcp {v}")
1523
+ return
1524
+
1525
+ logger.info("aria-code MCP server starting (stdio transport, %d tools)", len(TOOLS))
1526
+
1527
+ async def _run() -> None:
1528
+ loop = asyncio.get_event_loop()
1529
+ while True:
1530
+ line = await loop.run_in_executor(None, sys.stdin.readline)
1531
+ if not line:
1532
+ break
1533
+ line = line.strip()
1534
+ if not line:
1535
+ continue
1536
+ try:
1537
+ req = json.loads(line)
1538
+ except json.JSONDecodeError as exc:
1539
+ logger.warning("JSON parse error: %s", exc)
1540
+ continue
1541
+ try:
1542
+ await _handle(req)
1543
+ except Exception as exc:
1544
+ logger.error("Unhandled error: %s\n%s", exc, traceback.format_exc())
1545
+
1546
+ asyncio.run(_run())
1547
+
1548
+
1549
+ if __name__ == "__main__":
1550
+ main()