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.
- aria_code/__init__.py +42 -0
- aria_code/_version.py +20 -0
- aria_code/adk_apps/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/agent.py +32 -0
- aria_code/adk_apps/aria_research/__init__.py +1 -0
- aria_code/adk_apps/aria_research/agent.py +31 -0
- aria_code/agents/__init__.py +34 -0
- aria_code/agents/base.py +318 -0
- aria_code/agents/code_review.py +200 -0
- aria_code/agents/deep/__init__.py +37 -0
- aria_code/agents/deep/calibration_loop.py +144 -0
- aria_code/agents/deep/critic.py +125 -0
- aria_code/agents/deep/deepen.py +193 -0
- aria_code/agents/deep/models.py +149 -0
- aria_code/agents/deep/pipeline.py +164 -0
- aria_code/agents/deep/quant_fusion.py +192 -0
- aria_code/agents/deep/themes.py +95 -0
- aria_code/agents/deep/tiers.py +106 -0
- aria_code/agents/engineering/__init__.py +14 -0
- aria_code/agents/engineering/coder.py +222 -0
- aria_code/agents/engineering/tester.py +145 -0
- aria_code/agents/financial/__init__.py +21 -0
- aria_code/agents/financial/cashflow_burn.py +119 -0
- aria_code/agents/financial/catalyst.py +279 -0
- aria_code/agents/financial/corporate_finance.py +175 -0
- aria_code/agents/financial/debate.py +224 -0
- aria_code/agents/financial/earnings.py +303 -0
- aria_code/agents/financial/fundamental.py +191 -0
- aria_code/agents/financial/macro.py +99 -0
- aria_code/agents/financial/news.py +207 -0
- aria_code/agents/financial/risk.py +165 -0
- aria_code/agents/financial/sector.py +279 -0
- aria_code/agents/financial/strategist.py +121 -0
- aria_code/agents/financial/stripe_revenue.py +127 -0
- aria_code/agents/financial/synthesis.py +274 -0
- aria_code/agents/financial/technical.py +258 -0
- aria_code/agents/orchestrator.py +44 -0
- aria_code/agents/portfolio_agent.py +363 -0
- aria_code/agents/realty/__init__.py +62 -0
- aria_code/agents/realty/asset_diagnosis.py +152 -0
- aria_code/agents/realty/business_match.py +166 -0
- aria_code/agents/realty/cashflow_verify.py +209 -0
- aria_code/agents/realty/contract_rules.py +210 -0
- aria_code/agents/realty/energy_anomaly.py +189 -0
- aria_code/agents/realty/exit_settlement.py +208 -0
- aria_code/agents/realty/fulfillment_risk.py +206 -0
- aria_code/agents/realty/ops_optimize.py +160 -0
- aria_code/agents/realty/revenue_share.py +215 -0
- aria_code/agents/registry.py +158 -0
- aria_code/agents/signal_scheme.py +111 -0
- aria_code/agents/sports/__init__.py +0 -0
- aria_code/agents/sports/football_agent.py +197 -0
- aria_code/agents/supervisor.py +113 -0
- aria_code/agents/team.py +492 -0
- aria_code/agents/warehouse/__init__.py +25 -0
- aria_code/agents/warehouse/contracts.py +26 -0
- aria_code/agents/warehouse/dashboard.py +155 -0
- aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
- aria_code/agents/warehouse/inbound_exceptions.py +25 -0
- aria_code/agents/warehouse/inventory_health.py +24 -0
- aria_code/agents/warehouse/local_run.py +71 -0
- aria_code/agents/warehouse/local_snapshot.py +132 -0
- aria_code/agents/warehouse/logistics_cost.py +84 -0
- aria_code/agents/warehouse/logistics_sync.py +21 -0
- aria_code/agents/warehouse/workflow.py +31 -0
- aria_code/aliyun_data_client.py +4 -0
- aria_code/apps/README.md +12 -0
- aria_code/apps/__init__.py +2 -0
- aria_code/apps/channels/README.md +15 -0
- aria_code/apps/channels/__init__.py +17 -0
- aria_code/apps/channels/intake.py +92 -0
- aria_code/apps/channels/registry.py +95 -0
- aria_code/apps/channels/tradingview.py +94 -0
- aria_code/apps/cli/README.md +13 -0
- aria_code/apps/cli/__init__.py +2 -0
- aria_code/apps/cli/_optional.py +120 -0
- aria_code/apps/cli/bootstrap.py +145 -0
- aria_code/apps/cli/broker_render.py +206 -0
- aria_code/apps/cli/codegen_paths.py +29 -0
- aria_code/apps/cli/commands/__init__.py +16 -0
- aria_code/apps/cli/commands/_business_render.py +78 -0
- aria_code/apps/cli/commands/_ui.py +71 -0
- aria_code/apps/cli/commands/analysis_cmds.py +369 -0
- aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
- aria_code/apps/cli/commands/auth_cmds.py +239 -0
- aria_code/apps/cli/commands/backtest_cmds.py +2543 -0
- aria_code/apps/cli/commands/broker_cmds.py +1234 -0
- aria_code/apps/cli/commands/business_workflow_cmds.py +319 -0
- aria_code/apps/cli/commands/canvas_cmds.py +77 -0
- aria_code/apps/cli/commands/catalog.py +87 -0
- aria_code/apps/cli/commands/core_cmds.py +2520 -0
- aria_code/apps/cli/commands/data_cmds.py +457 -0
- aria_code/apps/cli/commands/diagnostic_cmds.py +230 -0
- aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
- aria_code/apps/cli/commands/file_cmds.py +312 -0
- aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
- aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
- aria_code/apps/cli/commands/market.py +434 -0
- aria_code/apps/cli/commands/market_cmds.py +1381 -0
- aria_code/apps/cli/commands/market_context.py +425 -0
- aria_code/apps/cli/commands/market_render.py +7 -0
- aria_code/apps/cli/commands/model_cmds.py +2153 -0
- aria_code/apps/cli/commands/ops_cmds.py +721 -0
- aria_code/apps/cli/commands/orchestrator_cmds.py +179 -0
- aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
- aria_code/apps/cli/commands/portfolio_cmds.py +1278 -0
- aria_code/apps/cli/commands/report.py +437 -0
- aria_code/apps/cli/commands/scaffold_templates.py +617 -0
- aria_code/apps/cli/commands/session_cmds.py +236 -0
- aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
- aria_code/apps/cli/commands/team.py +669 -0
- aria_code/apps/cli/commands/team_render.py +8 -0
- aria_code/apps/cli/commands/ui_cmds.py +401 -0
- aria_code/apps/cli/commands/warehouse_cmds.py +79 -0
- aria_code/apps/cli/commands/workflow_cmds.py +480 -0
- aria_code/apps/cli/commands/workspace_cmds.py +1494 -0
- aria_code/apps/cli/config_paths.py +73 -0
- aria_code/apps/cli/config_store.py +78 -0
- aria_code/apps/cli/constants.py +88 -0
- aria_code/apps/cli/context.py +34 -0
- aria_code/apps/cli/deterministic.py +191 -0
- aria_code/apps/cli/direct.py +48 -0
- aria_code/apps/cli/finance_service_catalog.py +111 -0
- aria_code/apps/cli/football_reports.py +231 -0
- aria_code/apps/cli/github_app_auth.py +135 -0
- aria_code/apps/cli/google_login.py +196 -0
- aria_code/apps/cli/handlers/__init__.py +11 -0
- aria_code/apps/cli/handlers/broker_handlers.py +122 -0
- aria_code/apps/cli/handlers/chart_handlers.py +1309 -0
- aria_code/apps/cli/handlers/market_handlers.py +2513 -0
- aria_code/apps/cli/handlers/realty_handlers.py +114 -0
- aria_code/apps/cli/handlers/sports_handlers.py +22 -0
- aria_code/apps/cli/handlers/strategy_advice.py +82 -0
- aria_code/apps/cli/helpers.py +302 -0
- aria_code/apps/cli/hooks.py +180 -0
- aria_code/apps/cli/i18n.py +297 -0
- aria_code/apps/cli/intent.py +136 -0
- aria_code/apps/cli/intent_router.py +276 -0
- aria_code/apps/cli/intent_signals.py +344 -0
- aria_code/apps/cli/lifecycle_hooks.py +48 -0
- aria_code/apps/cli/local_tool_schemas.py +536 -0
- aria_code/apps/cli/main.py +29 -0
- aria_code/apps/cli/market_metadata.py +135 -0
- aria_code/apps/cli/market_universe.py +487 -0
- aria_code/apps/cli/message_processing.py +310 -0
- aria_code/apps/cli/model_catalog.py +620 -0
- aria_code/apps/cli/pdf_report.py +977 -0
- aria_code/apps/cli/plan_mode.py +139 -0
- aria_code/apps/cli/plotly_html.py +15 -0
- aria_code/apps/cli/prediction_feedback.py +202 -0
- aria_code/apps/cli/preflight.py +502 -0
- aria_code/apps/cli/project_aria.py +60 -0
- aria_code/apps/cli/prompt_assembly.py +59 -0
- aria_code/apps/cli/prompts/__init__.py +0 -0
- aria_code/apps/cli/prompts/coding.py +53 -0
- aria_code/apps/cli/prompts/select.py +142 -0
- aria_code/apps/cli/prompts/system_prompts.py +552 -0
- aria_code/apps/cli/prompts/ui.py +436 -0
- aria_code/apps/cli/provider_endpoints.py +237 -0
- aria_code/apps/cli/provider_keys.py +73 -0
- aria_code/apps/cli/providers/__init__.py +1 -0
- aria_code/apps/cli/providers/base.py +521 -0
- aria_code/apps/cli/providers/chat_routing.py +128 -0
- aria_code/apps/cli/providers/collaboration.py +98 -0
- aria_code/apps/cli/providers/llm/__init__.py +1 -0
- aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
- aria_code/apps/cli/providers/llm/ollama_stream.py +1594 -0
- aria_code/apps/cli/providers/llm/response_cache.py +60 -0
- aria_code/apps/cli/providers/llm/sse_stream.py +257 -0
- aria_code/apps/cli/providers/runtime_bridge.py +353 -0
- aria_code/apps/cli/providers/vertexai_stream.py +316 -0
- aria_code/apps/cli/runtime_consumer.py +657 -0
- aria_code/apps/cli/sandbox.py +32 -0
- aria_code/apps/cli/session_export.py +120 -0
- aria_code/apps/cli/session_jsonl.py +208 -0
- aria_code/apps/cli/session_store.py +112 -0
- aria_code/apps/cli/skills_catalog.py +273 -0
- aria_code/apps/cli/todo_tracker.py +190 -0
- aria_code/apps/cli/tool_executor.py +823 -0
- aria_code/apps/cli/tool_registry.py +34 -0
- aria_code/apps/cli/tool_scope.py +106 -0
- aria_code/apps/cli/tools/__init__.py +40 -0
- aria_code/apps/cli/tools/context.py +46 -0
- aria_code/apps/cli/tools/file_tools.py +180 -0
- aria_code/apps/cli/tools/market_tools.py +620 -0
- aria_code/apps/cli/tools/notebook_tools.py +111 -0
- aria_code/apps/cli/tools/system_tools.py +726 -0
- aria_code/apps/cli/tools/write_tools.py +845 -0
- aria_code/apps/cli/tradingview_bridge.py +529 -0
- aria_code/apps/cli/turn_planning.py +137 -0
- aria_code/apps/cli/update_check.py +153 -0
- aria_code/apps/cli/utils/__init__.py +0 -0
- aria_code/apps/cli/utils/market_detect.py +1597 -0
- aria_code/apps/daemon/README.md +14 -0
- aria_code/apps/vscode/README.md +115 -0
- aria_code/apps/vscode/package.json +70 -0
- aria_code/aria_cli.py +6743 -0
- aria_code/aria_daemon.py +1398 -0
- aria_code/aria_feishu_bot.py +1423 -0
- aria_code/aria_mcp_server.py +7 -0
- aria_code/aria_relay_client.py +4 -0
- aria_code/aria_relay_server.py +490 -0
- aria_code/aria_telegram_bot.py +202 -0
- aria_code/ariarc.py +385 -0
- aria_code/artifacts.py +581 -0
- aria_code/backtest_engine.py +4 -0
- aria_code/backtest_report.py +4 -0
- aria_code/brokers/__init__.py +78 -0
- aria_code/brokers/automation.py +193 -0
- aria_code/brokers/base.py +231 -0
- aria_code/brokers/capabilities.py +264 -0
- aria_code/brokers/cn/__init__.py +10 -0
- aria_code/brokers/cn/easytrader_broker.py +193 -0
- aria_code/brokers/cn/futu_broker.py +194 -0
- aria_code/brokers/cn/longbridge_broker.py +190 -0
- aria_code/brokers/cn/tiger_broker.py +196 -0
- aria_code/brokers/cn/xtquant_broker.py +225 -0
- aria_code/brokers/config.py +414 -0
- aria_code/brokers/intl/__init__.py +5 -0
- aria_code/brokers/intl/alpaca_broker.py +183 -0
- aria_code/brokers/intl/ibkr_broker.py +215 -0
- aria_code/brokers/intl/trading212_broker.py +183 -0
- aria_code/brokers/intl/webull_broker.py +156 -0
- aria_code/brokers/paper_broker.py +266 -0
- aria_code/brokers/planning.py +296 -0
- aria_code/brokers/registry.py +213 -0
- aria_code/brokers/trading.py +267 -0
- aria_code/canva_client.py +4 -0
- aria_code/change_store.py +127 -0
- aria_code/clients/__init__.py +0 -0
- aria_code/clients/aliyun_data_client.py +660 -0
- aria_code/clients/aria_relay_client.py +182 -0
- aria_code/clients/canva_client.py +284 -0
- aria_code/clients/figma_client.py +100 -0
- aria_code/clients/football_data_client.py +1671 -0
- aria_code/clients/kling_video_client.py +182 -0
- aria_code/clients/market_data_client.py +1976 -0
- aria_code/clients/openai_image_client.py +242 -0
- aria_code/clients/runway_video_client.py +157 -0
- aria_code/clients/warehouse_erp_client.py +105 -0
- aria_code/cloud_memory.py +181 -0
- aria_code/command_safety.py +19 -0
- aria_code/computer_use_tools.py +4 -0
- aria_code/dashboard_generator.py +579 -0
- aria_code/data_analysis_tools.py +4 -0
- aria_code/data_cleaner.py +483 -0
- aria_code/data_service.py +513 -0
- aria_code/datasources/__init__.py +23 -0
- aria_code/datasources/base.py +166 -0
- aria_code/datasources/router.py +253 -0
- aria_code/datasources/sources/__init__.py +15 -0
- aria_code/datasources/sources/akshare_source.py +269 -0
- aria_code/datasources/sources/alpha_vantage_source.py +203 -0
- aria_code/datasources/sources/edgar_source.py +219 -0
- aria_code/datasources/sources/finnhub_source.py +198 -0
- aria_code/datasources/sources/fred_source.py +223 -0
- aria_code/datasources/sources/tushare_source.py +143 -0
- aria_code/datasources/sources/web_scraper_source.py +278 -0
- aria_code/datasources/sources/world_bank_source.py +206 -0
- aria_code/datasources/sources/yfinance_source.py +152 -0
- aria_code/demo_player.py +207 -0
- aria_code/doctor.py +813 -0
- aria_code/domain/__init__.py +0 -0
- aria_code/domain/backtest_engine.py +354 -0
- aria_code/domain/backtest_report.py +472 -0
- aria_code/domain/finance_formulas.py +389 -0
- aria_code/domain/portfolio_ledger.py +307 -0
- aria_code/domain/strategy_vault.py +421 -0
- aria_code/evals/__init__.py +37 -0
- aria_code/evals/harness.py +598 -0
- aria_code/evals/runner.py +238 -0
- aria_code/external_agent_runner.py +115 -0
- aria_code/figma_client.py +4 -0
- aria_code/file_analysis_tools.py +4 -0
- aria_code/finance_formulas.py +4 -0
- aria_code/football_data_client.py +4 -0
- aria_code/image_gen_tools.py +4 -0
- aria_code/intent_classifier.py +386 -0
- aria_code/kling_video_client.py +4 -0
- aria_code/licensing.py +151 -0
- aria_code/local_finance_tools.py +4 -0
- aria_code/local_image_provider.py +4 -0
- aria_code/local_llm_provider.py +4 -0
- aria_code/macro_tools.py +4 -0
- aria_code/markdown_pdf.py +289 -0
- aria_code/market_data_client.py +4 -0
- aria_code/mcp_client.py +639 -0
- aria_code/memory_manager.py +246 -0
- aria_code/model_capability.py +486 -0
- aria_code/notification_tools.py +4 -0
- aria_code/openai_image_client.py +4 -0
- aria_code/packages/__init__.py +23 -0
- aria_code/packages/adk_bridge/__init__.py +11 -0
- aria_code/packages/adk_bridge/code_review_tools.py +49 -0
- aria_code/packages/adk_bridge/market_tools.py +125 -0
- aria_code/packages/aria_agents/__init__.py +5 -0
- aria_code/packages/aria_agents/manifest.py +78 -0
- aria_code/packages/aria_core/__init__.py +34 -0
- aria_code/packages/aria_core/architecture.py +269 -0
- aria_code/packages/aria_core/export.py +147 -0
- aria_code/packages/aria_core/manifest.py +65 -0
- aria_code/packages/aria_core/paths.py +57 -0
- aria_code/packages/aria_core/secure_file.py +68 -0
- aria_code/packages/aria_infra/__init__.py +15 -0
- aria_code/packages/aria_infra/arthera.py +52 -0
- aria_code/packages/aria_infra/doctor.py +246 -0
- aria_code/packages/aria_infra/product.py +40 -0
- aria_code/packages/aria_mcp/__init__.py +25 -0
- aria_code/packages/aria_mcp/bridge.py +201 -0
- aria_code/packages/aria_mcp/circuit.py +82 -0
- aria_code/packages/aria_mcp/config.py +111 -0
- aria_code/packages/aria_mcp/installer.py +95 -0
- aria_code/packages/aria_mcp/server.py +1550 -0
- aria_code/packages/aria_mcp/tools.py +81 -0
- aria_code/packages/aria_sdk/__init__.py +19 -0
- aria_code/packages/aria_sdk/client.py +396 -0
- aria_code/packages/aria_sdk/providers.py +70 -0
- aria_code/packages/aria_sdk/streaming.py +79 -0
- aria_code/packages/aria_sdk/types.py +86 -0
- aria_code/packages/aria_services/__init__.py +92 -0
- aria_code/packages/aria_services/cache.py +60 -0
- aria_code/packages/aria_services/context.py +356 -0
- aria_code/packages/aria_services/context_checkpoints.py +157 -0
- aria_code/packages/aria_services/data.py +11 -0
- aria_code/packages/aria_services/provider_health.py +268 -0
- aria_code/packages/aria_services/references.py +247 -0
- aria_code/packages/aria_services/registry.py +249 -0
- aria_code/packages/aria_services/research_protocol.py +57 -0
- aria_code/packages/aria_services/research_quality.py +186 -0
- aria_code/packages/aria_services/settings.py +157 -0
- aria_code/packages/aria_services/usage.py +151 -0
- aria_code/packages/aria_skills/__init__.py +46 -0
- aria_code/packages/aria_skills/catalog.py +149 -0
- aria_code/packages/aria_skills/loader.py +455 -0
- aria_code/packages/aria_skills/registry.py +84 -0
- aria_code/packages/aria_tools/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/__init__.py +8 -0
- aria_code/packages/aria_tools/financial/_paths.py +130 -0
- aria_code/packages/aria_tools/financial/compliance.py +31 -0
- aria_code/packages/aria_tools/financial/factors.py +33 -0
- aria_code/packages/aria_tools/financial/risk_tools.py +33 -0
- aria_code/packages/aria_tools/financial/strategy.py +31 -0
- aria_code/packages/aria_tools/registry.py +128 -0
- aria_code/packages/quant_engine/__init__.py +34 -0
- aria_code/packages/quant_engine/_namespace.py +62 -0
- aria_code/packages/quant_engine/backtest/__init__.py +14 -0
- aria_code/packages/quant_engine/backtest/core.py +53 -0
- aria_code/packages/quant_engine/backtest/engine.py +96 -0
- aria_code/packages/quant_engine/portfolio/__init__.py +13 -0
- aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
- aria_code/packages/quant_engine/sports/__init__.py +80 -0
- aria_code/packages/quant_engine/sports/calibrator.py +354 -0
- aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
- aria_code/packages/quant_engine/sports/elo.py +300 -0
- aria_code/packages/quant_engine/sports/form.py +188 -0
- aria_code/packages/quant_engine/sports/h2h.py +195 -0
- aria_code/packages/quant_engine/sports/ml_model.py +377 -0
- aria_code/packages/quant_engine/sports/predictor.py +312 -0
- aria_code/packages/quant_engine/sports/tracker.py +665 -0
- aria_code/packages/quant_engine/stochastic/__init__.py +35 -0
- aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
- aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
- aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- aria_code/packs/__init__.py +85 -0
- aria_code/packs/base.py +168 -0
- aria_code/packs/finance/__init__.py +250 -0
- aria_code/packs/logistics/__init__.py +169 -0
- aria_code/packs/payments/__init__.py +139 -0
- aria_code/packs/realty/__init__.py +190 -0
- aria_code/packs/registry.py +202 -0
- aria_code/packs/rules.py +90 -0
- aria_code/plan_utils.py +194 -0
- aria_code/plugin_loader.py +328 -0
- aria_code/portfolio_ledger.py +4 -0
- aria_code/preview_server.py +449 -0
- aria_code/privacy/__init__.py +5 -0
- aria_code/privacy/feedback.py +123 -0
- aria_code/project_review.py +278 -0
- aria_code/project_review_api.py +235 -0
- aria_code/project_review_server.py +18 -0
- aria_code/project_tools.py +4 -0
- aria_code/providers/__init__.py +30 -0
- aria_code/providers/llm/__init__.py +19 -0
- aria_code/providers/llm/anthropic.py +184 -0
- aria_code/providers/llm/autoconfig.py +213 -0
- aria_code/providers/llm/base.py +139 -0
- aria_code/providers/llm/custom.py +158 -0
- aria_code/providers/llm/ollama.py +129 -0
- aria_code/providers/llm/openai_compat.py +313 -0
- aria_code/providers/llm/registry.py +444 -0
- aria_code/providers/local_image_provider.py +202 -0
- aria_code/providers/local_llm_provider.py +589 -0
- aria_code/realty_data_tools.py +4 -0
- aria_code/report_exporters.py +150 -0
- aria_code/report_generator.py +1764 -0
- aria_code/runtime/__init__.py +169 -0
- aria_code/runtime/acceptance.py +481 -0
- aria_code/runtime/agent_loop.py +1520 -0
- aria_code/runtime/approval.py +57 -0
- aria_code/runtime/budget.py +232 -0
- aria_code/runtime/checkpoints.py +432 -0
- aria_code/runtime/crash_report.py +174 -0
- aria_code/runtime/events.py +113 -0
- aria_code/runtime/gateway.py +150 -0
- aria_code/runtime/lsp.py +484 -0
- aria_code/runtime/orchestrator.py +55 -0
- aria_code/runtime/repo_map.py +700 -0
- aria_code/runtime/run_state.py +127 -0
- aria_code/runtime/run_store.py +450 -0
- aria_code/runtime/self_healing.py +422 -0
- aria_code/runtime/subagent.py +547 -0
- aria_code/runtime/task_ledger.py +86 -0
- aria_code/runtime/task_orchestrator.py +90 -0
- aria_code/runtime/tool_executor.py +217 -0
- aria_code/runtime/tool_policy.py +107 -0
- aria_code/runtime/worktrees.py +215 -0
- aria_code/runway_video_client.py +4 -0
- aria_code/safety/__init__.py +23 -0
- aria_code/safety/permissions.py +306 -0
- aria_code/safety/service.py +102 -0
- aria_code/setup_wizard.py +673 -0
- aria_code/spreadsheet_tools.py +4 -0
- aria_code/strategy_vault.py +4 -0
- aria_code/tools/__init__.py +0 -0
- aria_code/tools/artifact_tools.py +252 -0
- aria_code/tools/broker_tools.py +92 -0
- aria_code/tools/build_quant_engine.py +97 -0
- aria_code/tools/code_audit_tools.py +192 -0
- aria_code/tools/computer_use_tools.py +504 -0
- aria_code/tools/data_analysis_tools.py +809 -0
- aria_code/tools/enterprise_finance_tools.py +156 -0
- aria_code/tools/extended_tools.py +240 -0
- aria_code/tools/file_analysis_tools.py +885 -0
- aria_code/tools/image_gen_tools.py +114 -0
- aria_code/tools/local_finance_tools.py +3417 -0
- aria_code/tools/logistics_tools.py +135 -0
- aria_code/tools/macro_tools.py +369 -0
- aria_code/tools/notification_tools.py +249 -0
- aria_code/tools/project_tools.py +525 -0
- aria_code/tools/realty_data_tools.py +659 -0
- aria_code/tools/spreadsheet_tools.py +388 -0
- aria_code/tools/stripe_tools.py +115 -0
- aria_code/ui/__init__.py +105 -0
- aria_code/ui/banner.py +508 -0
- aria_code/ui/completer.py +501 -0
- aria_code/ui/console.py +281 -0
- aria_code/ui/image_render.py +243 -0
- aria_code/ui/input_box.py +488 -0
- aria_code/ui/picker.py +201 -0
- aria_code/ui/render/__init__.py +11 -0
- aria_code/ui/render/finance.py +1705 -0
- aria_code/ui/render/market.py +225 -0
- aria_code/ui/render/output.py +775 -0
- aria_code/ui/render/responsive.py +75 -0
- aria_code/ui/render/team.py +354 -0
- aria_code/ui/robot.py +235 -0
- aria_code/ui/startup_dashboard.py +109 -0
- aria_code/utils/vscode_uri.py +37 -0
- aria_code/video_analysis.py +147 -0
- aria_code/video_editor.py +271 -0
- aria_code/workspace/__init__.py +6 -0
- aria_code/workspace/files.py +193 -0
- aria_code/workspace/verify.py +113 -0
- aria_code/workspace_changes.py +199 -0
- aria_code/workspace_context.py +233 -0
- aria_code/workspace_service.py +196 -0
- aria_code/workspace_tools.py +150 -0
- aria_code-0.52.0.dist-info/METADATA +237 -0
- aria_code-0.52.0.dist-info/RECORD +478 -0
- aria_code-0.52.0.dist-info/WHEEL +5 -0
- aria_code-0.52.0.dist-info/entry_points.txt +3 -0
- aria_code-0.52.0.dist-info/licenses/LICENSE +201 -0
- aria_code-0.52.0.dist-info/licenses/NOTICE +49 -0
- 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()
|