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,2520 @@
|
|
|
1
|
+
"""Core slash commands mixin.
|
|
2
|
+
|
|
3
|
+
本文件绝大多数名字(self.context.console / self.context.has_rich / MODELS …)是有意留作裸名的,由
|
|
4
|
+
aria_cli 的 _rebind_mixin_globals() 把方法 __globals__ 指向它的命名空间来解析。
|
|
5
|
+
|
|
6
|
+
但 pathlib 不能这么处理:它出现在 _create_scaffold() 的**参数注解**里,而注解
|
|
7
|
+
在 class 语句执行时就要求值——那时候 rebind 还没发生(rebind 作用于已定义好的
|
|
8
|
+
类)。所以这一个必须真的 import。
|
|
9
|
+
|
|
10
|
+
2026-08-19 发现:此前没有这行,导致本模块在 Python 3.10–3.13 上 import 即
|
|
11
|
+
NameError,而这正是 requires-python = "<3.14,>=3.10" 声明支持的全部范围。
|
|
12
|
+
开发机 .venv 是 3.14(PEP 649 注解惰性求值)所以一直没暴露,1352 个测试也
|
|
13
|
+
全绿。已发布的 4.3.0 同样中招:pip install 后运行 aria-code 直接崩溃。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from ._ui import print_error
|
|
17
|
+
|
|
18
|
+
import pathlib
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import asyncio
|
|
23
|
+
import datetime
|
|
24
|
+
import time
|
|
25
|
+
import shlex
|
|
26
|
+
import sys
|
|
27
|
+
import os
|
|
28
|
+
from typing import Dict, Any, Optional
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import asyncio
|
|
33
|
+
import datetime
|
|
34
|
+
import time
|
|
35
|
+
import shlex
|
|
36
|
+
import sys
|
|
37
|
+
import os
|
|
38
|
+
from typing import Dict, Any, Optional
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CoreCommandsMixin:
|
|
42
|
+
def _cmd_rewind_unavailable(self, args: str):
|
|
43
|
+
# cmd_rewind lands with the checkpoint store (runtime/checkpoints.py,
|
|
44
|
+
# still a separate, uncommitted change) — degrade instead of crashing
|
|
45
|
+
# SlashCommands construction when it isn't present yet.
|
|
46
|
+
msg = "/rewind is not available in this build yet."
|
|
47
|
+
self.context.console.print(f"[dim]{msg}[/dim]" if self.context.has_rich else msg)
|
|
48
|
+
def is_command(self, text: str) -> bool:
|
|
49
|
+
if not text.startswith("/"):
|
|
50
|
+
return False
|
|
51
|
+
# Only match registered commands and skills, not file paths like /Users/...
|
|
52
|
+
cmd = text.split(maxsplit=1)[0].lower()
|
|
53
|
+
return cmd in self.commands or cmd in self.skill_map
|
|
54
|
+
async def execute(self, text: str):
|
|
55
|
+
from aria_code.apps.cli.helpers import _fuzzy_match
|
|
56
|
+
reference_service = getattr(self.terminal, "_reference_service", None)
|
|
57
|
+
if reference_service is not None and "@" in text:
|
|
58
|
+
prepared = reference_service.prepare(text)
|
|
59
|
+
if prepared.errors:
|
|
60
|
+
self.terminal._print_reference_errors(prepared)
|
|
61
|
+
return
|
|
62
|
+
if prepared.references:
|
|
63
|
+
self.terminal._print_reference_summary(prepared)
|
|
64
|
+
text = prepared.expanded_text
|
|
65
|
+
parts = text.split(maxsplit=1)
|
|
66
|
+
cmd_name = parts[0].lower()
|
|
67
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
68
|
+
|
|
69
|
+
if cmd_name in self.commands:
|
|
70
|
+
handler, _ = self.commands[cmd_name]
|
|
71
|
+
try:
|
|
72
|
+
result = handler(args)
|
|
73
|
+
if asyncio.iscoroutine(result):
|
|
74
|
+
await result
|
|
75
|
+
except KeyboardInterrupt:
|
|
76
|
+
if self.context.has_rich:
|
|
77
|
+
self.context.console.print("\n[dim]已取消[/dim]")
|
|
78
|
+
else:
|
|
79
|
+
print("\n已取消")
|
|
80
|
+
except Exception as _cmd_err:
|
|
81
|
+
import traceback as _tb
|
|
82
|
+
_tb_str = _tb.format_exc()
|
|
83
|
+
if self.context.has_rich:
|
|
84
|
+
from rich.panel import Panel as _P
|
|
85
|
+
from rich import box as _rbox
|
|
86
|
+
self.context.console.print(_P(
|
|
87
|
+
f"[red]{type(_cmd_err).__name__}: {_cmd_err}[/red]\n"
|
|
88
|
+
f"[dim]{_tb_str.strip()[-800:]}[/dim]",
|
|
89
|
+
title=f"[red]{cmd_name} 崩溃[/red]",
|
|
90
|
+
border_style="red",
|
|
91
|
+
box=_rbox.ROUNDED,
|
|
92
|
+
))
|
|
93
|
+
else:
|
|
94
|
+
print(f"\n ✗ {cmd_name} error: {_cmd_err}\n{_tb_str}")
|
|
95
|
+
elif cmd_name in self.skill_map:
|
|
96
|
+
await self._execute_skill(self.skill_map[cmd_name], args)
|
|
97
|
+
else:
|
|
98
|
+
# Fuzzy match: suggest closest command
|
|
99
|
+
all_cmds = list(self.commands.keys()) + list(self.skill_map.keys())
|
|
100
|
+
suggestions = _fuzzy_match(cmd_name, all_cmds, max_results=3)
|
|
101
|
+
if self.context.has_rich:
|
|
102
|
+
self.context.console.print(f"[red]Unknown command: {cmd_name}[/red]")
|
|
103
|
+
if suggestions:
|
|
104
|
+
self.context.console.print(f" [dim]Did you mean: {', '.join(suggestions)}?[/dim]")
|
|
105
|
+
else:
|
|
106
|
+
print(f"Unknown command: {cmd_name}")
|
|
107
|
+
if suggestions:
|
|
108
|
+
print(f" Did you mean: {', '.join(suggestions)}?")
|
|
109
|
+
def cmd_help(self, args: str):
|
|
110
|
+
# Contextual help: /help <command>
|
|
111
|
+
from aria_code.apps.cli.skills_catalog import SKILLS
|
|
112
|
+
target = args.strip().lower()
|
|
113
|
+
if target:
|
|
114
|
+
cmd_key = target if target.startswith("/") else f"/{target}"
|
|
115
|
+
if cmd_key in self.commands:
|
|
116
|
+
_, desc = self.commands[cmd_key]
|
|
117
|
+
if self.context.has_rich:
|
|
118
|
+
self.context.console.print()
|
|
119
|
+
self.context.console.print(f" [bold #C08050]{cmd_key}[/bold #C08050] [dim]{desc}[/dim]")
|
|
120
|
+
h = self._COMMAND_HELP.get(cmd_key)
|
|
121
|
+
if h:
|
|
122
|
+
self.context.console.print(f" {h[0]}")
|
|
123
|
+
self.context.console.print()
|
|
124
|
+
self.context.console.print(" [dim]Examples:[/dim]")
|
|
125
|
+
for ex in h[1]:
|
|
126
|
+
self.context.console.print(f" [bold]{ex}[/bold]")
|
|
127
|
+
self.context.console.print()
|
|
128
|
+
else:
|
|
129
|
+
print(f"\n {cmd_key} {desc}")
|
|
130
|
+
return
|
|
131
|
+
# Check skills
|
|
132
|
+
for s in SKILLS:
|
|
133
|
+
if s["command"] == cmd_key:
|
|
134
|
+
if self.context.has_rich:
|
|
135
|
+
self.context.console.print()
|
|
136
|
+
self.context.console.print(f" [bold #C08050]{s['command']}[/bold #C08050] [dim]{s['description']}[/dim]")
|
|
137
|
+
self.context.console.print(f" [dim]Category:[/dim] {s['category']}")
|
|
138
|
+
self.context.console.print()
|
|
139
|
+
else:
|
|
140
|
+
print(f"\n {s['command']} {s['description']}")
|
|
141
|
+
return
|
|
142
|
+
self.context.console.print(f"[dim]No help for: {target}. Try /help[/dim]" if self.context.has_rich else f"No help for: {target}")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# Full help listing
|
|
146
|
+
if self.context.has_rich:
|
|
147
|
+
self.context.console.print()
|
|
148
|
+
|
|
149
|
+
# ── Natural language first ──────────────────────────────────────
|
|
150
|
+
self.context.console.print("[bold]Just type what you want[/bold] [dim]— no command needed[/dim]")
|
|
151
|
+
self.context.console.print()
|
|
152
|
+
nl_examples = [
|
|
153
|
+
("宁德时代今天怎么样?", "How's CATL today?"),
|
|
154
|
+
("帮我画AAPL的K线图", "Draw a candlestick chart for AAPL"),
|
|
155
|
+
("分析我的组合风险", "Analyze my portfolio risk"),
|
|
156
|
+
("生成今日A股晨报", "Generate a morning brief for A-shares"),
|
|
157
|
+
("NVDA和AMD哪个更值得买?", "NVDA vs AMD — which is a better buy?"),
|
|
158
|
+
("给我写一个动量回测策略", "Write a momentum backtest strategy"),
|
|
159
|
+
]
|
|
160
|
+
for zh, en in nl_examples:
|
|
161
|
+
self.context.console.print(f" [#C08050]{zh}[/#C08050] [dim]{en}[/dim]")
|
|
162
|
+
self.context.console.print()
|
|
163
|
+
|
|
164
|
+
# ── Context references ─────────────────────────────────────────
|
|
165
|
+
_ref_is_zh = str(self.terminal.config.get("ui_lang", "en")).lower().startswith("zh")
|
|
166
|
+
_ref_title = "上下文引用" if _ref_is_zh else "Context references"
|
|
167
|
+
_ref_subtitle = (
|
|
168
|
+
"@ 只附加只读上下文,不执行操作"
|
|
169
|
+
if _ref_is_zh else
|
|
170
|
+
"@ attaches read-only context; it never runs an action"
|
|
171
|
+
)
|
|
172
|
+
self.context.console.print(f"[bold]{_ref_title}[/bold] [dim]({_ref_subtitle})[/dim]")
|
|
173
|
+
self.context.console.print()
|
|
174
|
+
for example, description_en, description_zh in (
|
|
175
|
+
("@file:src/model.py", "file pointer → read_file", "文件引用 → read_file"),
|
|
176
|
+
("@folder:apps/cli", "folder pointer → list_files", "目录引用 → list_files"),
|
|
177
|
+
("@asset:AAPL", "market asset", "市场资产"),
|
|
178
|
+
("@portfolio:core", "saved portfolio", "已保存投资组合"),
|
|
179
|
+
("@strategy:momentum-v2", "saved strategy", "已保存策略"),
|
|
180
|
+
("@dataset:prices", "local dataset", "本地数据集"),
|
|
181
|
+
("@run:walk-forward-42", "research run", "研究运行"),
|
|
182
|
+
("@report:daily", "generated report", "生成报告"),
|
|
183
|
+
):
|
|
184
|
+
description = description_zh if _ref_is_zh else description_en
|
|
185
|
+
self.context.console.print(f" [#C08050]{example:27s}[/#C08050] [dim]{description}[/dim]")
|
|
186
|
+
_combine = "组合使用" if _ref_is_zh else "Combine them"
|
|
187
|
+
self.context.console.print(f" [dim]{_combine}: /risk @portfolio:core · /review @folder:apps/cli[/dim]")
|
|
188
|
+
self.context.console.print()
|
|
189
|
+
|
|
190
|
+
# ── Slash commands — grouped ────────────────────────────────────
|
|
191
|
+
self.context.console.print("[bold]Slash commands[/bold] [dim](for direct actions and mode switches)[/dim]")
|
|
192
|
+
self.context.console.print()
|
|
193
|
+
groups = [
|
|
194
|
+
("Session", ["/help","/clear","/compact","/cost","/status","/health",
|
|
195
|
+
"/regen","/undo","/rewind","/copy","/recap","/btw",
|
|
196
|
+
"/save","/load","/sessions","/export","/export-pdf"]),
|
|
197
|
+
("Config", ["/model","/thinking","/config","/privacy","/local",
|
|
198
|
+
"/setup","/apikey","/doctor","/mcp"]),
|
|
199
|
+
("Data", ["/alert","/journal","/watch","/note","/todo","/memory",
|
|
200
|
+
"/artifacts","/strategy","/accuracy"]),
|
|
201
|
+
("Broker", ["/broker","/paper","/trade","/account","/positions","/orders"]),
|
|
202
|
+
("Code", ["/project","/init","/review","/code","/plan","/run",
|
|
203
|
+
"/read","/write","/edit","/ls","/search","/verify",
|
|
204
|
+
"/scaffold","/apply","/changes"]),
|
|
205
|
+
("Quant", ["/backtest","/wf","/compare","/auto-strategy","/execution"]),
|
|
206
|
+
("UI", ["/ui","/vision","/file"]),
|
|
207
|
+
("Info", ["/skills","/services","/tools","/providers","/ariarc",
|
|
208
|
+
"/packages","/datasource"]),
|
|
209
|
+
]
|
|
210
|
+
for group_name, cmd_names in groups:
|
|
211
|
+
visible = [n for n in cmd_names if n in self.commands]
|
|
212
|
+
if not visible:
|
|
213
|
+
continue
|
|
214
|
+
self.context.console.print(f" [dim]{group_name}[/dim]")
|
|
215
|
+
for name in visible:
|
|
216
|
+
_, desc = self.commands[name]
|
|
217
|
+
self.context.console.print(f" [bold #C08050]{name:18s}[/bold #C08050][dim]{desc}[/dim]")
|
|
218
|
+
self.context.console.print()
|
|
219
|
+
|
|
220
|
+
# ── Skills ─────────────────────────────────────────────────────
|
|
221
|
+
self.context.console.print("[bold]Skills[/bold] [dim](type the command or just describe what you want)[/dim]")
|
|
222
|
+
self.context.console.print()
|
|
223
|
+
categories: dict = {}
|
|
224
|
+
for s in SKILLS:
|
|
225
|
+
categories.setdefault(s["category"], []).append(s)
|
|
226
|
+
for cat, skills in categories.items():
|
|
227
|
+
self.context.console.print(f" [dim]{cat}[/dim]")
|
|
228
|
+
for s in skills:
|
|
229
|
+
self.context.console.print(f" [bold #C08050]{s['command']:20s}[/bold #C08050][dim]{s['description']}[/dim]")
|
|
230
|
+
self.context.console.print()
|
|
231
|
+
|
|
232
|
+
# ── Keys ───────────────────────────────────────────────────────
|
|
233
|
+
self.context.console.print("[dim]ESC cancel · Ctrl+D exit · ↑↓ history · Tab autocomplete · \"\"\" multiline[/dim]")
|
|
234
|
+
self.context.console.print()
|
|
235
|
+
|
|
236
|
+
else:
|
|
237
|
+
print("\nJust type what you want — or use a slash command:\n")
|
|
238
|
+
for name, (_, desc) in self.commands.items():
|
|
239
|
+
if name in self._visible_cmds:
|
|
240
|
+
print(f" {name:18s} {desc}")
|
|
241
|
+
print("\nSkills:")
|
|
242
|
+
for s in SKILLS:
|
|
243
|
+
print(f" {s['command']:20s} {s['description']}")
|
|
244
|
+
async def cmd_artifacts(self, args: str):
|
|
245
|
+
from aria_code.ui.render.output import display_path as _display_path
|
|
246
|
+
from aria_code.apps.cli.helpers import _copy_text_to_clipboard, _open_path_or_url, _reveal_path_in_finder
|
|
247
|
+
tokens = [part for part in args.split() if part]
|
|
248
|
+
mode = "list"
|
|
249
|
+
limit = 20
|
|
250
|
+
keep = 20
|
|
251
|
+
dry_run = False
|
|
252
|
+
selector = "latest"
|
|
253
|
+
if tokens:
|
|
254
|
+
head = tokens[0].lower()
|
|
255
|
+
if head in {"prune", "cleanup", "gc", "purge"}:
|
|
256
|
+
mode = "prune"
|
|
257
|
+
for token in tokens[1:]:
|
|
258
|
+
if token == "--dry-run":
|
|
259
|
+
dry_run = True
|
|
260
|
+
continue
|
|
261
|
+
try:
|
|
262
|
+
keep = int(token)
|
|
263
|
+
except Exception:
|
|
264
|
+
continue
|
|
265
|
+
elif head in {"stats", "summary"}:
|
|
266
|
+
mode = "stats"
|
|
267
|
+
elif head in {"open", "reveal", "show", "path", "copy-path", "copy"}:
|
|
268
|
+
mode = "copy-path" if head == "copy" else head
|
|
269
|
+
selector = tokens[1] if len(tokens) > 1 else "latest"
|
|
270
|
+
else:
|
|
271
|
+
try:
|
|
272
|
+
limit = int(head)
|
|
273
|
+
except Exception:
|
|
274
|
+
limit = 20
|
|
275
|
+
from artifacts import artifact_summary_all, prune_artifacts_all, recent_artifacts_all
|
|
276
|
+
|
|
277
|
+
if mode in {"open", "reveal", "show", "path", "copy-path"}:
|
|
278
|
+
items = recent_artifacts_all(limit=100)
|
|
279
|
+
selected = None
|
|
280
|
+
if items:
|
|
281
|
+
if selector.lower() in {"latest", "last", "newest", "最近", "最新"}:
|
|
282
|
+
selected = items[0]
|
|
283
|
+
elif selector.isdigit() and 1 <= int(selector) <= len(items):
|
|
284
|
+
selected = items[int(selector) - 1]
|
|
285
|
+
else:
|
|
286
|
+
needle = selector.lower()
|
|
287
|
+
selected = next(
|
|
288
|
+
(
|
|
289
|
+
item for item in items
|
|
290
|
+
if needle in pathlib.Path(str(item.get("path") or "")).name.lower()
|
|
291
|
+
),
|
|
292
|
+
None,
|
|
293
|
+
)
|
|
294
|
+
target = str((selected or {}).get("path") or "").strip()
|
|
295
|
+
if not target:
|
|
296
|
+
msg = f"未找到产物 {selector!r}。先运行 /artifacts 查看可用编号。"
|
|
297
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
ok = True
|
|
301
|
+
error = ""
|
|
302
|
+
if mode == "open":
|
|
303
|
+
ok, error = _open_path_or_url(target)
|
|
304
|
+
action_text = "已打开"
|
|
305
|
+
elif mode in {"reveal", "show"}:
|
|
306
|
+
ok, error = _reveal_path_in_finder(target)
|
|
307
|
+
action_text = "已在访达中定位"
|
|
308
|
+
elif mode == "copy-path":
|
|
309
|
+
ok, error = _copy_text_to_clipboard(target)
|
|
310
|
+
action_text = "路径已复制"
|
|
311
|
+
else:
|
|
312
|
+
action_text = "文件路径"
|
|
313
|
+
if ok:
|
|
314
|
+
msg = f"{action_text}:{target}"
|
|
315
|
+
self.terminal._pending_market_artifact = {
|
|
316
|
+
"kind": str((selected or {}).get("kind") or "artifact"),
|
|
317
|
+
"path": target,
|
|
318
|
+
}
|
|
319
|
+
if self.context.has_rich:
|
|
320
|
+
self.context.console.print(f"[green]✓[/green] {msg}")
|
|
321
|
+
else:
|
|
322
|
+
print(msg)
|
|
323
|
+
else:
|
|
324
|
+
msg = f"操作失败:{error or target}"
|
|
325
|
+
if self.context.has_rich:
|
|
326
|
+
self.context.console.print(f"[red]✗[/red] {msg}")
|
|
327
|
+
else:
|
|
328
|
+
print(msg)
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
if mode == "stats":
|
|
332
|
+
summary = artifact_summary_all()
|
|
333
|
+
roots = [pathlib.Path(str(r)) for r in summary.get("roots") or []]
|
|
334
|
+
total = int(summary.get("total") or 0)
|
|
335
|
+
total_size = int(summary.get("total_size_bytes") or 0)
|
|
336
|
+
by_kind = summary.get("by_kind") or {}
|
|
337
|
+
if self.context.has_rich:
|
|
338
|
+
self.context.console.print("[bold]Artifact inventory[/bold]")
|
|
339
|
+
for r in roots:
|
|
340
|
+
self.context.console.print(f" [dim]root[/dim]: {_display_path(r, fallback=r.name)}")
|
|
341
|
+
self.context.console.print(f" total: [bold]{total}[/bold] size: [bold]{total_size:,} bytes[/bold]")
|
|
342
|
+
if by_kind:
|
|
343
|
+
for kind, count in by_kind.items():
|
|
344
|
+
self.context.console.print(f" [dim]{kind}[/dim]: {count}")
|
|
345
|
+
else:
|
|
346
|
+
print("Artifact inventory")
|
|
347
|
+
print(f" total: {total}")
|
|
348
|
+
print(f" size: {total_size} bytes")
|
|
349
|
+
for kind, count in by_kind.items():
|
|
350
|
+
print(f" {kind}: {count}")
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
if mode == "prune":
|
|
354
|
+
result = prune_artifacts_all(keep=keep, dry_run=dry_run)
|
|
355
|
+
removed = int(result.get("removed") or 0)
|
|
356
|
+
scanned = int(result.get("scanned") or 0)
|
|
357
|
+
action = "Would remove" if dry_run else "Removed"
|
|
358
|
+
if self.context.has_rich:
|
|
359
|
+
self.context.console.print("[bold]Artifact prune[/bold]")
|
|
360
|
+
for r in result.get("roots") or []:
|
|
361
|
+
self.context.console.print(f" [dim]root[/dim]: {_display_path(r, fallback=pathlib.Path(str(r)).name)}")
|
|
362
|
+
self.context.console.print(f" keep: [bold]{result.get('keep', keep)}[/bold] scanned: [bold]{scanned}[/bold] removed: [bold]{removed}[/bold]")
|
|
363
|
+
if removed:
|
|
364
|
+
for entry in result.get("deleted") or []:
|
|
365
|
+
name = pathlib.Path(str(entry.get("path") or entry.get("metadata_path") or "")).name
|
|
366
|
+
root_name = pathlib.Path(str(entry.get("root") or "")).name
|
|
367
|
+
self.context.console.print(f" [dim]{action}[/dim] {name} [dim]({root_name})[/dim]")
|
|
368
|
+
else:
|
|
369
|
+
print("Artifact prune")
|
|
370
|
+
print(f" keep: {result.get('keep', keep)}")
|
|
371
|
+
print(f" scanned: {scanned}")
|
|
372
|
+
print(f" removed: {removed}")
|
|
373
|
+
for entry in result.get("deleted") or []:
|
|
374
|
+
name = pathlib.Path(str(entry.get("path") or entry.get("metadata_path") or "")).name
|
|
375
|
+
root_name = pathlib.Path(str(entry.get("root") or "")).name
|
|
376
|
+
print(f" {action}: {name} ({root_name})")
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
items = recent_artifacts_all(limit=limit)
|
|
380
|
+
if not items:
|
|
381
|
+
msg = "No artifacts found"
|
|
382
|
+
self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
title = "生成的产物" if str(self.terminal.config.get("ui_lang", "en")).lower().startswith("zh") else "Generated artifacts"
|
|
386
|
+
if self.context.has_rich:
|
|
387
|
+
self.context.console.print(f"[bold]{title}[/bold]")
|
|
388
|
+
else:
|
|
389
|
+
print(title)
|
|
390
|
+
for index, item in enumerate(items, start=1):
|
|
391
|
+
_path = item.get("path") or item.get("metadata_path") or ""
|
|
392
|
+
_name = pathlib.Path(str(_path)).name if _path else ""
|
|
393
|
+
_root_name = pathlib.Path(str(item.get("root") or "")).name if item.get("root") else ""
|
|
394
|
+
_status = str(item.get("status") or "unknown")
|
|
395
|
+
_icon = "✓" if _status == "complete" else "!" if _status in {"partial", "failed"} else "·"
|
|
396
|
+
_kind = str(item.get("kind") or "artifact")
|
|
397
|
+
_topic = str(item.get("topic") or "").strip()
|
|
398
|
+
_headline = f"#{index} {_icon} {_kind}" + (f" · {_topic}" if _topic else "")
|
|
399
|
+
_detail = f" {_name}" + (f" · {_root_name}" if _root_name else "")
|
|
400
|
+
if self.context.has_rich:
|
|
401
|
+
self.context.console.print(f"[bold]{_headline}[/bold]")
|
|
402
|
+
self.context.console.print(f"[dim]{_detail}[/dim]")
|
|
403
|
+
else:
|
|
404
|
+
print(_headline)
|
|
405
|
+
print(_detail)
|
|
406
|
+
hint = "操作:/artifacts open 1 · reveal 1 · copy-path 1"
|
|
407
|
+
self.context.console.print(f"[dim]{hint}[/dim]") if self.context.has_rich else print(hint)
|
|
408
|
+
async def _run_in_executor(self, fn, *args):
|
|
409
|
+
import asyncio
|
|
410
|
+
loop = asyncio.get_event_loop()
|
|
411
|
+
result = await loop.run_in_executor(None, fn, *args)
|
|
412
|
+
return result
|
|
413
|
+
def cmd_plan(self, args: str):
|
|
414
|
+
from aria_cli import OpsCommandsMixin, _PLAN_MODE
|
|
415
|
+
sub = args.strip().lower().split()[0] if args.strip() else ""
|
|
416
|
+
|
|
417
|
+
if sub in ("mode", "enter", "on", "start"):
|
|
418
|
+
_PLAN_MODE.enter()
|
|
419
|
+
if self.context.has_rich:
|
|
420
|
+
self.context.console.print()
|
|
421
|
+
self.context.console.print(
|
|
422
|
+
" [bold cyan]◆ 计划模式已激活[/bold cyan] "
|
|
423
|
+
"[dim]每个工具调用前都会显示确认提示[/dim]"
|
|
424
|
+
)
|
|
425
|
+
self.context.console.print(" [dim]/plan exit — 退出计划模式[/dim]")
|
|
426
|
+
self.context.console.print()
|
|
427
|
+
else:
|
|
428
|
+
print(" Plan mode ON — every tool call will ask for approval.")
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
if sub in ("exit", "off", "stop", "quit"):
|
|
432
|
+
summary = _PLAN_MODE.summary()
|
|
433
|
+
_PLAN_MODE.exit()
|
|
434
|
+
if self.context.has_rich:
|
|
435
|
+
self.context.console.print(
|
|
436
|
+
f" [dim]◆ 计划模式已退出 "
|
|
437
|
+
f"(共 {summary['total']} 步 · "
|
|
438
|
+
f"[green]{summary['approved']} 执行[/green] · "
|
|
439
|
+
f"[red]{summary['rejected']} 跳过[/red])[/dim]"
|
|
440
|
+
)
|
|
441
|
+
else:
|
|
442
|
+
print(f" Plan mode OFF. ({summary['total']} steps, {summary['approved']} approved)")
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
if sub == "status":
|
|
446
|
+
if _PLAN_MODE.active:
|
|
447
|
+
summary = _PLAN_MODE.summary()
|
|
448
|
+
msg = (
|
|
449
|
+
f"计划模式: [bold cyan]激活[/bold cyan] "
|
|
450
|
+
f"(已执行 {summary['approved']} 步,跳过 {summary['rejected']} 步)"
|
|
451
|
+
)
|
|
452
|
+
else:
|
|
453
|
+
msg = "计划模式: [dim]未激活[/dim] (/plan mode 开启)"
|
|
454
|
+
self.context.console.print(f" {msg}") if self.context.has_rich else print(f" {msg.replace('[bold cyan]', '').replace('[/bold cyan]', '').replace('[dim]', '').replace('[/dim]', '')}")
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
return OpsCommandsMixin.cmd_plan(self, args)
|
|
458
|
+
def cmd_tasks(self, args: str):
|
|
459
|
+
"""Show and manage background subagent tasks."""
|
|
460
|
+
try:
|
|
461
|
+
from runtime.subagent import _TASKS, tool_task_cancel
|
|
462
|
+
except ImportError:
|
|
463
|
+
msg = "Subagent module not available."
|
|
464
|
+
self.context.console.print(f"[red]{msg}[/red]") if self.context.has_rich else print(msg)
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
parts = args.strip().split()
|
|
468
|
+
sub = parts[0].lower() if parts else "list"
|
|
469
|
+
|
|
470
|
+
if sub == "cancel" and len(parts) > 1:
|
|
471
|
+
result = tool_task_cancel({"task_id": parts[1]})
|
|
472
|
+
if result.get("success"):
|
|
473
|
+
msg = f"✓ Task {parts[1]} cancelled"
|
|
474
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
475
|
+
else:
|
|
476
|
+
self.context.console.print(f"[red]{result.get('error', 'Error')}[/red]") if self.context.has_rich else print(result.get("error"))
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
# Default: list all tasks
|
|
480
|
+
tasks = list(_TASKS.values())
|
|
481
|
+
if not tasks:
|
|
482
|
+
msg = "没有活跃的后台任务。使用 spawn_task 工具创建任务。"
|
|
483
|
+
self.context.console.print(f" [dim]{msg}[/dim]") if self.context.has_rich else print(msg)
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
if self.context.has_rich:
|
|
487
|
+
self.context.console.print()
|
|
488
|
+
self.context.console.print(" [bold]后台任务[/bold]")
|
|
489
|
+
self.context.console.print()
|
|
490
|
+
status_colors = {"pending": "yellow", "running": "cyan", "done": "green",
|
|
491
|
+
"failed": "red", "cancelled": "dim"}
|
|
492
|
+
for t in tasks:
|
|
493
|
+
col = status_colors.get(t.status, "white")
|
|
494
|
+
preview = t.prompt[:60] + ("…" if len(t.prompt) > 60 else "")
|
|
495
|
+
self.context.console.print(
|
|
496
|
+
f" [{col}]●[/{col}] [bold]{t.task_id}[/bold] "
|
|
497
|
+
f"[{col}]{t.status:10s}[/{col}] "
|
|
498
|
+
f"[dim]{t.age_str():>5s}[/dim] {preview}"
|
|
499
|
+
)
|
|
500
|
+
self.context.console.print()
|
|
501
|
+
self.context.console.print(" [dim]/tasks cancel <id> — 取消任务[/dim]")
|
|
502
|
+
self.context.console.print()
|
|
503
|
+
else:
|
|
504
|
+
print(f"\n Background Tasks ({len(tasks)}):")
|
|
505
|
+
for t in tasks:
|
|
506
|
+
preview = t.prompt[:50]
|
|
507
|
+
print(f" {t.task_id} {t.status:10s} {t.age_str():>5s} {preview}")
|
|
508
|
+
def cmd_delegate(self, args: str):
|
|
509
|
+
"""Delegate a task to the Claude Code or Codex CLI as a background subagent."""
|
|
510
|
+
try:
|
|
511
|
+
from runtime.subagent import tool_spawn_task
|
|
512
|
+
except ImportError:
|
|
513
|
+
msg = "Subagent module not available."
|
|
514
|
+
self.context.console.print(f"[red]{msg}[/red]") if self.context.has_rich else print(msg)
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
parts = args.strip().split(maxsplit=1)
|
|
518
|
+
if len(parts) < 2 or parts[0].lower() not in {"claude", "codex"}:
|
|
519
|
+
msg = 'Usage: /delegate claude|codex "<prompt>"'
|
|
520
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
521
|
+
return
|
|
522
|
+
|
|
523
|
+
backend, prompt = parts[0].lower(), parts[1].strip().strip('"')
|
|
524
|
+
result = tool_spawn_task({
|
|
525
|
+
"prompt": prompt,
|
|
526
|
+
"backend": backend,
|
|
527
|
+
"mode": "workspace-write",
|
|
528
|
+
"isolation": "auto",
|
|
529
|
+
})
|
|
530
|
+
if not result.get("success"):
|
|
531
|
+
self.context.console.print(f"[red]{result.get('error', 'Error')}[/red]") if self.context.has_rich else print(result.get("error"))
|
|
532
|
+
return
|
|
533
|
+
task_id = result["task_id"]
|
|
534
|
+
msg = f"✓ Delegated to {backend}: task {task_id}. Check with /tasks or task_status('{task_id}')."
|
|
535
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
536
|
+
def cmd_canva(self, args: str):
|
|
537
|
+
"""Manage the Canva Connect integration used for report design drafts."""
|
|
538
|
+
parts = args.strip().split(maxsplit=2)
|
|
539
|
+
sub = parts[0].lower() if parts else "status"
|
|
540
|
+
|
|
541
|
+
if sub == "connect":
|
|
542
|
+
if len(parts) < 3:
|
|
543
|
+
msg = "Usage: /canva connect <client_id> <client_secret> (register an app first at https://www.canva.com/developers/)"
|
|
544
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
545
|
+
return
|
|
546
|
+
client_id, client_secret = parts[1], parts[2]
|
|
547
|
+
from canva_client import connect as _canva_connect
|
|
548
|
+
msg = "打开浏览器完成 Canva 授权…"
|
|
549
|
+
self.context.console.print(f"[cyan]{msg}[/cyan]") if self.context.has_rich else print(msg)
|
|
550
|
+
result = _canva_connect(client_id, client_secret)
|
|
551
|
+
if result.get("success"):
|
|
552
|
+
msg = "✓ Canva 已连接"
|
|
553
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
554
|
+
else:
|
|
555
|
+
self.context.console.print(f"[red]{result.get('error')}[/red]") if self.context.has_rich else print(result.get("error"))
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
if sub == "status":
|
|
559
|
+
from canva_client import _load_canva_config
|
|
560
|
+
entry = _load_canva_config()
|
|
561
|
+
msg = "✓ Canva 已连接" if entry.get("access_token") else "未连接 Canva。运行 /canva connect <client_id> <client_secret>"
|
|
562
|
+
self.context.console.print(msg) if self.context.has_rich else print(msg)
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
msg = "Usage: /canva connect <client_id> <client_secret> | /canva status"
|
|
566
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
567
|
+
def _confirm_high_risk_command(self, command: str, risk: str, policy: str) -> bool:
|
|
568
|
+
"""Double-confirm high-risk commands even if policy allows them."""
|
|
569
|
+
msg = f"High-risk command under policy '{policy}' (risk={risk}): {command}\nRun it? [y/N]: "
|
|
570
|
+
try:
|
|
571
|
+
answer = self.context.console.input(msg) if self.context.has_rich else input(msg)
|
|
572
|
+
except (EOFError, KeyboardInterrupt):
|
|
573
|
+
return False
|
|
574
|
+
return answer.strip().lower() in {"y", "yes"}
|
|
575
|
+
def _open_file(self, path: str):
|
|
576
|
+
"""Open a local file using platform default app."""
|
|
577
|
+
path_q = shlex.quote(path)
|
|
578
|
+
if sys.platform == "darwin":
|
|
579
|
+
os.system(f"open {path_q}")
|
|
580
|
+
elif os.name == "nt":
|
|
581
|
+
os.system(f'start "" {path_q}')
|
|
582
|
+
else:
|
|
583
|
+
os.system(f"xdg-open {path_q} >/dev/null 2>&1")
|
|
584
|
+
async def _smart_compact_async(self, silent: bool = False):
|
|
585
|
+
"""AI-powered conversation compaction (inspired by Claude Code).
|
|
586
|
+
|
|
587
|
+
Sends all messages to the current model and asks for a dense summary,
|
|
588
|
+
then replaces conversation with [system summary] + last 2 message pairs.
|
|
589
|
+
Falls back to hard trim if the summary call fails.
|
|
590
|
+
"""
|
|
591
|
+
from aria_cli import OllamaProvider, stream_provider_result
|
|
592
|
+
from aria_code.apps.cli.model_catalog import get_model_cfg
|
|
593
|
+
conv = self.terminal.conversation
|
|
594
|
+
if len(conv) <= 4:
|
|
595
|
+
if not silent:
|
|
596
|
+
self.context.console.print("[dim]Context small enough — no compaction needed[/dim]" if self.context.has_rich
|
|
597
|
+
else "Context small enough")
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
if not silent and self.context.has_rich:
|
|
601
|
+
self.context.console.print("[dim]Summarising conversation...[/dim]")
|
|
602
|
+
|
|
603
|
+
from packages.aria_services.context import build_context_service
|
|
604
|
+
|
|
605
|
+
model_key = self.terminal.config.get("model", "qwen2.5:7b")
|
|
606
|
+
max_ctx = int(get_model_cfg(model_key).get("num_ctx", 16384) or 16384)
|
|
607
|
+
context_service = build_context_service(max_tokens=max_ctx)
|
|
608
|
+
summary_prompt = context_service.build_summary_prompt(conv)
|
|
609
|
+
|
|
610
|
+
summary = ""
|
|
611
|
+
try:
|
|
612
|
+
result = await stream_provider_result(
|
|
613
|
+
OllamaProvider(
|
|
614
|
+
self.terminal.config.get("ollama_url", "http://localhost:11434"),
|
|
615
|
+
self.terminal.config.get("model", "qwen2.5:7b"),
|
|
616
|
+
show_market_prefetch_status=False,
|
|
617
|
+
),
|
|
618
|
+
summary_prompt,
|
|
619
|
+
[], # no history — pure summarisation task
|
|
620
|
+
tools=[],
|
|
621
|
+
)
|
|
622
|
+
if result.get("success") and result.get("response"):
|
|
623
|
+
summary = result["response"].strip()
|
|
624
|
+
except Exception:
|
|
625
|
+
pass
|
|
626
|
+
|
|
627
|
+
if not summary:
|
|
628
|
+
# Fallback: local structural compaction before hard trimming.
|
|
629
|
+
try:
|
|
630
|
+
compacted = context_service.compact_messages(conv)
|
|
631
|
+
except Exception:
|
|
632
|
+
compacted = []
|
|
633
|
+
self.terminal.conversation = compacted if compacted and len(compacted) < len(conv) else conv[-8:]
|
|
634
|
+
if not silent:
|
|
635
|
+
self.context.console.print("[dim]Compacted (summary failed, used local fallback)[/dim]" if self.context.has_rich
|
|
636
|
+
else "Compacted (summary fallback)")
|
|
637
|
+
return
|
|
638
|
+
|
|
639
|
+
envelope = context_service.build_summary_envelope(conv, summary)
|
|
640
|
+
self.terminal.conversation = envelope.messages
|
|
641
|
+
new_count = len(self.terminal.conversation)
|
|
642
|
+
old_count = len(conv)
|
|
643
|
+
if not silent:
|
|
644
|
+
if self.context.has_rich:
|
|
645
|
+
self.context.console.print(
|
|
646
|
+
f" [dim]✓ Compacted {old_count} → {new_count} messages "
|
|
647
|
+
f"(summary preserved context)[/dim]"
|
|
648
|
+
)
|
|
649
|
+
else:
|
|
650
|
+
print(f"Compacted {old_count} → {new_count} messages")
|
|
651
|
+
def cmd_doctor(self, args: str):
|
|
652
|
+
from aria_cli import DiagnosticOpsCommandsMixin
|
|
653
|
+
_r = DiagnosticOpsCommandsMixin.cmd_doctor(self, args)
|
|
654
|
+
# Architecture coverage summary (observability layer) — /architecture for the
|
|
655
|
+
# full layered view. Best-effort; never let it break /doctor.
|
|
656
|
+
try:
|
|
657
|
+
from packages.aria_core import architecture_gaps, architecture_status_counts
|
|
658
|
+
_c = architecture_status_counts()
|
|
659
|
+
_done, _total = _c.get("done", 0), sum(_c.values())
|
|
660
|
+
_line = (f"架构契约: {_done}/{_total} 层完成 · {len(architecture_gaps())} 层待办"
|
|
661
|
+
f" (/architecture 看详情, --gaps 只看待办)")
|
|
662
|
+
self.context.console.print(f"\n [dim]{_line}[/dim]") if self.context.has_rich else print(f"\n {_line}")
|
|
663
|
+
except Exception:
|
|
664
|
+
pass
|
|
665
|
+
return _r
|
|
666
|
+
@staticmethod
|
|
667
|
+
def _create_scaffold(target_dir: pathlib.Path, template: dict) -> list:
|
|
668
|
+
"""Create dirs + write files from a scaffold template. Returns list of created paths."""
|
|
669
|
+
created = []
|
|
670
|
+
for d in template.get("dirs", []):
|
|
671
|
+
dp = target_dir / d
|
|
672
|
+
dp.mkdir(parents=True, exist_ok=True)
|
|
673
|
+
created.append(str(dp))
|
|
674
|
+
for rel, content in template.get("files", {}).items():
|
|
675
|
+
fp = target_dir / rel
|
|
676
|
+
fp.parent.mkdir(parents=True, exist_ok=True)
|
|
677
|
+
if not fp.exists():
|
|
678
|
+
fp.write_text(content, encoding="utf-8")
|
|
679
|
+
created.append(str(fp))
|
|
680
|
+
return created
|
|
681
|
+
def cmd_verify(self, args: str):
|
|
682
|
+
"""Infer and run focused verification checks."""
|
|
683
|
+
from aria_cli import VerificationPlanner, _SYNTAX_THEME, _tool_run_command
|
|
684
|
+
from ._ui import Syntax
|
|
685
|
+
parts = args.split()
|
|
686
|
+
dry_run = "--dry-run" in parts
|
|
687
|
+
paths = [p for p in parts if p != "--dry-run"]
|
|
688
|
+
plan = VerificationPlanner(pathlib.Path.cwd()).infer(paths)
|
|
689
|
+
if not plan.commands:
|
|
690
|
+
msg = "No verification command inferred."
|
|
691
|
+
self.context.console.print(f"[dim]{msg}[/dim]" if self.context.has_rich else msg)
|
|
692
|
+
return
|
|
693
|
+
if self.context.has_rich:
|
|
694
|
+
self.context.console.print(f"[dim]Verification plan: {plan.reason}[/dim]")
|
|
695
|
+
for idx, command in enumerate(plan.commands, 1):
|
|
696
|
+
self.context.console.print(f" [bold]{idx}.[/bold] {command}")
|
|
697
|
+
else:
|
|
698
|
+
print(f"Verification plan: {plan.reason}")
|
|
699
|
+
for idx, command in enumerate(plan.commands, 1):
|
|
700
|
+
print(f" {idx}. {command}")
|
|
701
|
+
if dry_run:
|
|
702
|
+
return
|
|
703
|
+
for command in plan.commands:
|
|
704
|
+
result = _tool_run_command({
|
|
705
|
+
"command": command,
|
|
706
|
+
"policy": "balanced",
|
|
707
|
+
"permission_mode": self.terminal.config.get("permission_mode", "workspace-write"),
|
|
708
|
+
"network_enabled": bool(self.terminal.config.get("network_enabled", True)),
|
|
709
|
+
"user_approved": True,
|
|
710
|
+
"timeout": 300,
|
|
711
|
+
})
|
|
712
|
+
if not result.get("success"):
|
|
713
|
+
self.context.console.print(f"[red]Verification failed: {command}[/red]" if self.context.has_rich else f"Verification failed: {command}")
|
|
714
|
+
self.context.console.print(f"[red]{result.get('error', '')}[/red]" if self.context.has_rich else result.get("error", ""))
|
|
715
|
+
return
|
|
716
|
+
data = result.get("data", {})
|
|
717
|
+
if data.get("stdout"):
|
|
718
|
+
self.context.console.print(Syntax(data["stdout"], "text", theme=_SYNTAX_THEME) if self.context.has_rich else data["stdout"])
|
|
719
|
+
if data.get("stderr"):
|
|
720
|
+
self.context.console.print(f"[yellow]{data['stderr']}[/yellow]" if self.context.has_rich else data["stderr"])
|
|
721
|
+
msg = "Verification passed."
|
|
722
|
+
self.context.console.print(f"[green]{msg}[/green]" if self.context.has_rich else msg)
|
|
723
|
+
def cmd_run(self, args: str):
|
|
724
|
+
"""Run a command: /run <command>"""
|
|
725
|
+
from aria_cli import _SYNTAX_THEME, _tool_run_command
|
|
726
|
+
from aria_code.safety import SafetyService
|
|
727
|
+
from ._ui import Syntax
|
|
728
|
+
if not args.strip():
|
|
729
|
+
self.context.console.print("[dim]Usage: /run [--dry-run] <command>[/dim]" if self.context.has_rich
|
|
730
|
+
else "Usage: /run [--dry-run] <command>")
|
|
731
|
+
return
|
|
732
|
+
text = args.strip()
|
|
733
|
+
dry_run = False
|
|
734
|
+
if text.startswith("--dry-run "):
|
|
735
|
+
dry_run = True
|
|
736
|
+
text = text[len("--dry-run "):].strip()
|
|
737
|
+
if not text:
|
|
738
|
+
self.context.console.print("[dim]Usage: /run [--dry-run] <command>[/dim]" if self.context.has_rich
|
|
739
|
+
else "Usage: /run [--dry-run] <command>")
|
|
740
|
+
return
|
|
741
|
+
|
|
742
|
+
policy = self.terminal.config.get("command_policy", "safe")
|
|
743
|
+
# Built per call, not held: self.terminal.config is mutated in place by
|
|
744
|
+
# /config set, so a cached service would answer with a stale mode.
|
|
745
|
+
decision = SafetyService(self.terminal.config).evaluate_command(text, policy)
|
|
746
|
+
if not dry_run and decision.allowed and decision.risk == "high":
|
|
747
|
+
if not self._confirm_high_risk_command(decision.normalized_command, decision.risk, policy):
|
|
748
|
+
msg = "Cancelled by user."
|
|
749
|
+
self.context.console.print(f"[dim]{msg}[/dim]" if self.context.has_rich else msg)
|
|
750
|
+
return
|
|
751
|
+
result = _tool_run_command({
|
|
752
|
+
"command": text,
|
|
753
|
+
"policy": policy,
|
|
754
|
+
"permission_mode": self.terminal.config.get("permission_mode", "workspace-write"),
|
|
755
|
+
"network_enabled": bool(self.terminal.config.get("network_enabled", True)),
|
|
756
|
+
"dry_run": dry_run,
|
|
757
|
+
})
|
|
758
|
+
if result["success"]:
|
|
759
|
+
data = result["data"]
|
|
760
|
+
if dry_run:
|
|
761
|
+
msg = (
|
|
762
|
+
f"Dry run: risk={data.get('risk', '?')} "
|
|
763
|
+
f"policy={data.get('policy', '?')} "
|
|
764
|
+
f"approval={data.get('requires_approval', False)} "
|
|
765
|
+
f"network={data.get('network', False)} "
|
|
766
|
+
f"command={data.get('command', '')}"
|
|
767
|
+
)
|
|
768
|
+
self.context.console.print(f"[dim]{msg}[/dim]" if self.context.has_rich else msg)
|
|
769
|
+
return
|
|
770
|
+
if data["stdout"]:
|
|
771
|
+
if self.context.has_rich:
|
|
772
|
+
self.context.console.print(Syntax(data["stdout"], "text", theme=_SYNTAX_THEME))
|
|
773
|
+
else:
|
|
774
|
+
print(data["stdout"])
|
|
775
|
+
if data["stderr"]:
|
|
776
|
+
if self.context.has_rich:
|
|
777
|
+
self.context.console.print(f"[red]{data['stderr']}[/red]")
|
|
778
|
+
else:
|
|
779
|
+
print(data["stderr"], file=sys.stderr)
|
|
780
|
+
else:
|
|
781
|
+
self.context.console.print(f"[red]{result['error']}[/red]" if self.context.has_rich else result["error"])
|
|
782
|
+
def cmd_apply(self, args: str):
|
|
783
|
+
"""Extract code from last AI response and save to file."""
|
|
784
|
+
from aria_code.apps.cli.helpers import _extract_code_block
|
|
785
|
+
from aria_cli import _SYNTAX_THEME, _tool_write_file
|
|
786
|
+
from ._ui import Syntax
|
|
787
|
+
filename = args.strip()
|
|
788
|
+
last_response = ""
|
|
789
|
+
for msg in reversed(self.terminal.conversation):
|
|
790
|
+
if msg["role"] == "assistant":
|
|
791
|
+
last_response = msg["content"]
|
|
792
|
+
break
|
|
793
|
+
if not last_response:
|
|
794
|
+
self.context.console.print("[dim]No AI response to extract from[/dim]" if self.context.has_rich
|
|
795
|
+
else "No response")
|
|
796
|
+
return
|
|
797
|
+
|
|
798
|
+
code = _extract_code_block(last_response)
|
|
799
|
+
if not code:
|
|
800
|
+
self.context.console.print("[dim]No code block found in last response[/dim]" if self.context.has_rich
|
|
801
|
+
else "No code block found")
|
|
802
|
+
return
|
|
803
|
+
|
|
804
|
+
if not filename:
|
|
805
|
+
# Show code preview and ask for filename
|
|
806
|
+
preview = code[:500] + ("..." if len(code) > 500 else "")
|
|
807
|
+
if self.context.has_rich:
|
|
808
|
+
self.context.console.print(f"\n[dim]Found code block ({len(code.splitlines())} lines):[/dim]")
|
|
809
|
+
self.context.console.print(Syntax(preview, "python", theme=_SYNTAX_THEME))
|
|
810
|
+
else:
|
|
811
|
+
print(f"\nFound code ({len(code.splitlines())} lines):")
|
|
812
|
+
print(preview)
|
|
813
|
+
try:
|
|
814
|
+
filename = (self.context.console.input("\n[bold]>[/bold] Save to: ") if self.context.has_rich
|
|
815
|
+
else input("\nSave to: ")).strip()
|
|
816
|
+
except (EOFError, KeyboardInterrupt):
|
|
817
|
+
return
|
|
818
|
+
if not filename:
|
|
819
|
+
return
|
|
820
|
+
|
|
821
|
+
result = _tool_write_file({"path": filename, "content": code})
|
|
822
|
+
if not result["success"]:
|
|
823
|
+
self.context.console.print(f"[red]{result['error']}[/red]" if self.context.has_rich else result["error"])
|
|
824
|
+
async def cmd_code(self, args: str):
|
|
825
|
+
"""Generate code and optionally save to file. Usage: /code <description> [--save file.py]"""
|
|
826
|
+
from aria_code.apps.cli.helpers import _extract_code_block
|
|
827
|
+
from aria_code.ui.render.output import display_path as _display_path
|
|
828
|
+
if not args.strip():
|
|
829
|
+
if self.context.has_rich:
|
|
830
|
+
self.context.console.print("[dim]Usage: /code <description> [--save file.py][/dim]")
|
|
831
|
+
self.context.console.print("[dim]Examples:[/dim]")
|
|
832
|
+
self.context.console.print("[dim] /code momentum strategy for AAPL[/dim]")
|
|
833
|
+
self.context.console.print("[dim] /code portfolio optimizer --save optimizer.py[/dim]")
|
|
834
|
+
self.context.console.print("[dim] /code backtest report generator --save report.py[/dim]")
|
|
835
|
+
else:
|
|
836
|
+
print("Usage: /code <description> [--save file.py]")
|
|
837
|
+
return
|
|
838
|
+
|
|
839
|
+
# Parse --save flag
|
|
840
|
+
save_path = None
|
|
841
|
+
description = args
|
|
842
|
+
if "--save" in args:
|
|
843
|
+
parts = args.split("--save")
|
|
844
|
+
description = parts[0].strip()
|
|
845
|
+
save_path = parts[1].strip() if len(parts) > 1 else None
|
|
846
|
+
|
|
847
|
+
from artifacts import user_generated_dir
|
|
848
|
+
from apps.cli.codegen_paths import resolve_user_code_path
|
|
849
|
+
default_save = resolve_user_code_path(
|
|
850
|
+
description,
|
|
851
|
+
None,
|
|
852
|
+
user_generated_dir=user_generated_dir(),
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
# Build code generation prompt
|
|
856
|
+
prompt = (
|
|
857
|
+
f"Generate complete, production-ready Python code for: {description}\n\n"
|
|
858
|
+
"Requirements:\n"
|
|
859
|
+
"- Include all necessary imports\n"
|
|
860
|
+
"- Add clear inline comments\n"
|
|
861
|
+
"- Include error handling\n"
|
|
862
|
+
"- Use type hints where appropriate\n"
|
|
863
|
+
"- Make it runnable as a standalone script\n\n"
|
|
864
|
+
"Return the code wrapped in ```python``` fences."
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
if self.context.has_rich:
|
|
868
|
+
self.context.console.print(f"[bold]Generating code:[/bold] [bold]{description}[/bold]")
|
|
869
|
+
else:
|
|
870
|
+
print(f"Generating: {description}")
|
|
871
|
+
|
|
872
|
+
# Use best available model for code gen
|
|
873
|
+
original_model = self.terminal.config.get("model", "qwen2.5:7b")
|
|
874
|
+
self.terminal.config["model"] = "qwen2.5:7b"
|
|
875
|
+
|
|
876
|
+
await self.terminal.send_message(prompt)
|
|
877
|
+
|
|
878
|
+
# Restore model
|
|
879
|
+
self.terminal.config["model"] = original_model
|
|
880
|
+
|
|
881
|
+
# Extract code from last AI response and save if requested
|
|
882
|
+
if save_path or description:
|
|
883
|
+
last_response = ""
|
|
884
|
+
for msg in reversed(self.terminal.conversation):
|
|
885
|
+
if msg["role"] == "assistant":
|
|
886
|
+
last_response = msg["content"]
|
|
887
|
+
break
|
|
888
|
+
code = _extract_code_block(last_response)
|
|
889
|
+
if code:
|
|
890
|
+
if save_path:
|
|
891
|
+
_save_path = resolve_user_code_path(
|
|
892
|
+
description,
|
|
893
|
+
save_path,
|
|
894
|
+
user_generated_dir=user_generated_dir(),
|
|
895
|
+
)
|
|
896
|
+
else:
|
|
897
|
+
_save_path = default_save
|
|
898
|
+
_save_path.parent.mkdir(parents=True, exist_ok=True)
|
|
899
|
+
_save_path.write_text(code, encoding="utf-8")
|
|
900
|
+
_save_label = _display_path(str(_save_path))
|
|
901
|
+
if self.context.has_rich:
|
|
902
|
+
self.context.console.print(f"\n[green]Code saved to {_save_label}[/green] "
|
|
903
|
+
f"[dim]({len(code.splitlines())} lines)[/dim]")
|
|
904
|
+
else:
|
|
905
|
+
print(f"\nSaved: {_save_label} ({len(code.splitlines())} lines)")
|
|
906
|
+
else:
|
|
907
|
+
if self.context.has_rich:
|
|
908
|
+
self.context.console.print("[dim]No code block found in response to save[/dim]")
|
|
909
|
+
else:
|
|
910
|
+
print("No code block found to save")
|
|
911
|
+
async def cmd_feedback(self, args: str):
|
|
912
|
+
"""Rate the last AI response and store feedback locally by default.
|
|
913
|
+
|
|
914
|
+
Usage: /feedback good|bad [comment]
|
|
915
|
+
/feedback note <comment>
|
|
916
|
+
"""
|
|
917
|
+
from aria_cli import CONFIG_DIR
|
|
918
|
+
from aria_code.privacy import FeedbackRecord
|
|
919
|
+
from aria_code.privacy import FeedbackStore
|
|
920
|
+
from aria_code.ui.render.output import display_path as _display_path
|
|
921
|
+
parts = args.strip().split(maxsplit=1)
|
|
922
|
+
vote = parts[0].lower() if parts else ""
|
|
923
|
+
comment = parts[1].strip() if len(parts) > 1 else ""
|
|
924
|
+
|
|
925
|
+
aliases = {
|
|
926
|
+
"good": "positive", "up": "positive", "1": "positive", "+": "positive",
|
|
927
|
+
"bad": "negative", "down": "negative", "0": "negative", "-": "negative",
|
|
928
|
+
"note": "note",
|
|
929
|
+
}
|
|
930
|
+
rating = aliases.get(vote)
|
|
931
|
+
if rating is None or (rating == "note" and not comment):
|
|
932
|
+
self.context.console.print("[dim]Usage: /feedback good|bad [comment] | /feedback note <comment>[/dim]" if self.context.has_rich
|
|
933
|
+
else "Usage: /feedback good|bad [comment] | /feedback note <comment>")
|
|
934
|
+
return
|
|
935
|
+
|
|
936
|
+
# Find last assistant message and its position
|
|
937
|
+
last_msg = None
|
|
938
|
+
msg_idx = None
|
|
939
|
+
for i in range(len(self.terminal.conversation) - 1, -1, -1):
|
|
940
|
+
if self.terminal.conversation[i]["role"] == "assistant":
|
|
941
|
+
last_msg = self.terminal.conversation[i]["content"][:500]
|
|
942
|
+
msg_idx = i
|
|
943
|
+
break
|
|
944
|
+
if not last_msg:
|
|
945
|
+
self.context.console.print("[dim]No AI response to rate[/dim]" if self.context.has_rich else "No response to rate")
|
|
946
|
+
return
|
|
947
|
+
|
|
948
|
+
from aria_code.safety import SafetyService
|
|
949
|
+
settings = SafetyService(self.terminal.config).privacy()
|
|
950
|
+
record = FeedbackRecord.create(
|
|
951
|
+
rating=rating,
|
|
952
|
+
message=last_msg,
|
|
953
|
+
comment=comment,
|
|
954
|
+
model=self.terminal.config.get("model", ""),
|
|
955
|
+
session_id=self.terminal.session_id,
|
|
956
|
+
message_index=msg_idx,
|
|
957
|
+
shared=settings.data_sharing and settings.feedback_upload,
|
|
958
|
+
)
|
|
959
|
+
store = FeedbackStore(CONFIG_DIR)
|
|
960
|
+
|
|
961
|
+
# Persist locally first. This is the default and works offline.
|
|
962
|
+
try:
|
|
963
|
+
feedback_path = store.append(record)
|
|
964
|
+
except Exception as exc:
|
|
965
|
+
msg = f"Could not save feedback locally: {exc}"
|
|
966
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
967
|
+
return
|
|
968
|
+
|
|
969
|
+
# Optional remote upload only after explicit opt-in. Posts to the
|
|
970
|
+
# backend /feedback endpoint in the shape it expects (FeedbackRequest)
|
|
971
|
+
# so the rating reaches production_data_collector → DPO training.
|
|
972
|
+
# No trail_id needed: the backend matches the latest sample for this
|
|
973
|
+
# session_id (good→thumbs_up, bad→thumbs_down; bad+comment→DPO pair).
|
|
974
|
+
api_success = False
|
|
975
|
+
upload_attempted = settings.data_sharing and settings.feedback_upload
|
|
976
|
+
if upload_attempted and rating in ("positive", "negative"):
|
|
977
|
+
try:
|
|
978
|
+
import aiohttp
|
|
979
|
+
headers = {}
|
|
980
|
+
if self.terminal.config.get("auth_token"):
|
|
981
|
+
headers["Authorization"] = f"Bearer {self.terminal.config['auth_token']}"
|
|
982
|
+
payload = {
|
|
983
|
+
"user_id": self.terminal.config.get("user_id") or "cli",
|
|
984
|
+
"session_id": self.terminal.session_id,
|
|
985
|
+
"feedback_type": "thumbs_up" if rating == "positive" else "thumbs_down",
|
|
986
|
+
"response_id": self.terminal.session_id, # latest-in-session match
|
|
987
|
+
"value": comment or None,
|
|
988
|
+
}
|
|
989
|
+
async with aiohttp.ClientSession() as session:
|
|
990
|
+
async with session.post(
|
|
991
|
+
f"{self.terminal.api_url}/feedback",
|
|
992
|
+
json=payload,
|
|
993
|
+
headers=headers,
|
|
994
|
+
timeout=aiohttp.ClientTimeout(total=8)
|
|
995
|
+
) as resp:
|
|
996
|
+
api_success = resp.status in (200, 201, 204)
|
|
997
|
+
except Exception:
|
|
998
|
+
api_success = False
|
|
999
|
+
|
|
1000
|
+
icon = "↑" if rating == "positive" else ("↓" if rating == "negative" else "note")
|
|
1001
|
+
if upload_attempted:
|
|
1002
|
+
sync_note = "" if api_success else " [dim](saved locally; upload failed)[/dim]"
|
|
1003
|
+
else:
|
|
1004
|
+
sync_note = " [dim](saved locally; sharing off)[/dim]"
|
|
1005
|
+
if self.context.has_rich:
|
|
1006
|
+
comment_note = f" — {comment}" if comment else ""
|
|
1007
|
+
self.context.console.print(f"[green]Feedback {icon}[/green]{comment_note}{sync_note}")
|
|
1008
|
+
self.context.console.print(f"[dim]Saved: {_display_path(feedback_path)}[/dim]")
|
|
1009
|
+
else:
|
|
1010
|
+
print(f"Feedback {icon}" + (f" — {comment}" if comment else "") +
|
|
1011
|
+
(" (uploaded)" if api_success else " (saved locally)"))
|
|
1012
|
+
def cmd_privacy(self, args: str):
|
|
1013
|
+
"""Manage local privacy and feedback-sharing settings."""
|
|
1014
|
+
from aria_cli import CONFIG_DIR
|
|
1015
|
+
from aria_code.privacy import FeedbackStore
|
|
1016
|
+
from aria_code.privacy import PrivacySettings
|
|
1017
|
+
from aria_code.ui.render.output import display_path as _display_path
|
|
1018
|
+
parts = args.strip().split(maxsplit=1)
|
|
1019
|
+
sub = parts[0].lower() if parts else "status"
|
|
1020
|
+
rest = parts[1].strip() if len(parts) > 1 else ""
|
|
1021
|
+
store = FeedbackStore(CONFIG_DIR)
|
|
1022
|
+
from aria_code.safety import SafetyService
|
|
1023
|
+
settings = SafetyService(self.terminal.config).privacy()
|
|
1024
|
+
|
|
1025
|
+
def _save_settings(new_settings: PrivacySettings):
|
|
1026
|
+
new_settings.apply_to_config(self.terminal.config)
|
|
1027
|
+
self.context.save_config(self.terminal.config)
|
|
1028
|
+
|
|
1029
|
+
if sub in {"status", "show"}:
|
|
1030
|
+
shared_state = "ON — feedback may be shared with Arthera" if (
|
|
1031
|
+
settings.data_sharing and settings.feedback_upload
|
|
1032
|
+
) else "OFF — local-only, nothing leaves this machine"
|
|
1033
|
+
lines = [
|
|
1034
|
+
"Privacy status",
|
|
1035
|
+
f" sharing: {shared_state}",
|
|
1036
|
+
f" data_sharing: {settings.data_sharing}",
|
|
1037
|
+
f" feedback_upload: {settings.feedback_upload}",
|
|
1038
|
+
f" feedback_records: {store.count()}",
|
|
1039
|
+
f" local_feedback: {store.feedback_file}",
|
|
1040
|
+
" default: local-only; no upload unless you run /privacy opt-in",
|
|
1041
|
+
" full policy: /privacy policy (or see PRIVACY.md)",
|
|
1042
|
+
]
|
|
1043
|
+
if self.context.has_rich:
|
|
1044
|
+
self.context.console.print()
|
|
1045
|
+
self.context.console.print("[bold]Privacy[/bold]")
|
|
1046
|
+
for line in lines[1:]:
|
|
1047
|
+
self.context.console.print(f"[dim]{line}[/dim]")
|
|
1048
|
+
else:
|
|
1049
|
+
print("\n".join(lines))
|
|
1050
|
+
return
|
|
1051
|
+
|
|
1052
|
+
if sub == "policy":
|
|
1053
|
+
url = "https://github.com/artherahq/aria-code/blob/aria-code/PRIVACY.md"
|
|
1054
|
+
local = pathlib.Path(__file__).resolve().parent / "PRIVACY.md"
|
|
1055
|
+
lines = [
|
|
1056
|
+
"Aria Code is local-first: by default nothing is collected or uploaded.",
|
|
1057
|
+
"Only `/privacy opt-in` shares feedback (rating + related message) with Arthera.",
|
|
1058
|
+
"Credentials, positions, and financial data always stay on your machine.",
|
|
1059
|
+
f" full policy: {local if local.exists() else url}",
|
|
1060
|
+
" manage: /privacy opt-in | opt-out | export [path] | delete",
|
|
1061
|
+
]
|
|
1062
|
+
if self.context.has_rich:
|
|
1063
|
+
self.context.console.print()
|
|
1064
|
+
self.context.console.print("[bold]Privacy policy[/bold]")
|
|
1065
|
+
for line in lines:
|
|
1066
|
+
self.context.console.print(f"[dim]{line}[/dim]")
|
|
1067
|
+
else:
|
|
1068
|
+
print("\n".join(lines))
|
|
1069
|
+
return
|
|
1070
|
+
|
|
1071
|
+
if sub in {"opt-in", "on", "enable"}:
|
|
1072
|
+
_save_settings(PrivacySettings(data_sharing=True, feedback_upload=True))
|
|
1073
|
+
if self.context.has_rich:
|
|
1074
|
+
self.context.console.print("[green]Data sharing enabled.[/green]")
|
|
1075
|
+
self.context.console.print("[dim] You consent to share /feedback records (rating, the related[/dim]")
|
|
1076
|
+
self.context.console.print("[dim] model message, optional comment, model id, session id, time)[/dim]")
|
|
1077
|
+
self.context.console.print("[dim] with Arthera to improve the product. Local copies are kept.[/dim]")
|
|
1078
|
+
self.context.console.print("[dim] Credentials & financial data are never shared. Details: /privacy policy[/dim]")
|
|
1079
|
+
self.context.console.print("[dim] Withdraw any time: /privacy opt-out[/dim]")
|
|
1080
|
+
else:
|
|
1081
|
+
print("Data sharing enabled. You consent to share /feedback records "
|
|
1082
|
+
"(rating + related message) with Arthera. Credentials & financial "
|
|
1083
|
+
"data are never shared. Withdraw: /privacy opt-out. Details: /privacy policy")
|
|
1084
|
+
return
|
|
1085
|
+
|
|
1086
|
+
if sub in {"opt-out", "off", "disable"}:
|
|
1087
|
+
_save_settings(PrivacySettings(data_sharing=False, feedback_upload=False))
|
|
1088
|
+
msg = "Data sharing disabled. Feedback stays local only."
|
|
1089
|
+
self.context.console.print(f"[green]{msg}[/green]" if self.context.has_rich else msg)
|
|
1090
|
+
return
|
|
1091
|
+
|
|
1092
|
+
if sub == "export":
|
|
1093
|
+
dest = rest or None
|
|
1094
|
+
try:
|
|
1095
|
+
path = store.export_jsonl(dest)
|
|
1096
|
+
except Exception as exc:
|
|
1097
|
+
msg = f"Export failed: {exc}"
|
|
1098
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
1099
|
+
return
|
|
1100
|
+
msg = f"Exported feedback to {_display_path(path)}"
|
|
1101
|
+
self.context.console.print(f"[green]{msg}[/green]" if self.context.has_rich else msg)
|
|
1102
|
+
return
|
|
1103
|
+
|
|
1104
|
+
if sub in {"delete", "clear"}:
|
|
1105
|
+
count = store.delete_all()
|
|
1106
|
+
msg = f"Deleted {count} local feedback record(s)."
|
|
1107
|
+
self.context.console.print(f"[green]{msg}[/green]" if self.context.has_rich else msg)
|
|
1108
|
+
return
|
|
1109
|
+
|
|
1110
|
+
msg = "Usage: /privacy [status|policy|opt-in|opt-out|export [path]|delete]"
|
|
1111
|
+
self.context.console.print(f"[dim]{msg}[/dim]" if self.context.has_rich else msg)
|
|
1112
|
+
async def _run_tool_cmd(self, tool_name: str, params: dict, label: str = ""):
|
|
1113
|
+
"""Generic helper: run tool with spinner and formatted output.
|
|
1114
|
+
|
|
1115
|
+
Routing priority:
|
|
1116
|
+
1. LOCAL_TOOLS (via executor — never blocks event loop)
|
|
1117
|
+
2. Remote Aria backend (AWS) — if local not available
|
|
1118
|
+
3. Graceful error if both fail
|
|
1119
|
+
"""
|
|
1120
|
+
from aria_cli import Text
|
|
1121
|
+
from aria_code.apps.cli.tool_executor import execute_aria_tool
|
|
1122
|
+
from aria_code.apps.cli.tool_registry import LOCAL_TOOLS
|
|
1123
|
+
display = label or tool_name
|
|
1124
|
+
|
|
1125
|
+
# ── 1. Try LOCAL_TOOLS first (run in executor to avoid blocking) ──
|
|
1126
|
+
if tool_name in LOCAL_TOOLS:
|
|
1127
|
+
handler, _ = LOCAL_TOOLS[tool_name]
|
|
1128
|
+
if self.context.has_rich:
|
|
1129
|
+
with self.context.console.status(f"[dim]{display}...[/dim]", spinner="dots"):
|
|
1130
|
+
result = await asyncio.get_event_loop().run_in_executor(
|
|
1131
|
+
None, handler, params
|
|
1132
|
+
)
|
|
1133
|
+
else:
|
|
1134
|
+
print(f"Running {display}...")
|
|
1135
|
+
result = await asyncio.get_event_loop().run_in_executor(
|
|
1136
|
+
None, handler, params
|
|
1137
|
+
)
|
|
1138
|
+
else:
|
|
1139
|
+
# ── 2. Fall through to remote Aria backend ────────────────────
|
|
1140
|
+
local_mode = self.terminal.config.get("local_mode", False)
|
|
1141
|
+
if local_mode:
|
|
1142
|
+
result = {
|
|
1143
|
+
"success": False,
|
|
1144
|
+
"error": f"Tool '{tool_name}' has no local implementation. "
|
|
1145
|
+
"Run '/local off' to use the Aria backend, or "
|
|
1146
|
+
"add a handler in aria_tools.py.",
|
|
1147
|
+
}
|
|
1148
|
+
else:
|
|
1149
|
+
if self.context.has_rich:
|
|
1150
|
+
with self.context.console.status(f"[dim]Running {display}...[/dim]", spinner="dots"):
|
|
1151
|
+
result = await execute_aria_tool(self.terminal.api_url, tool_name, params)
|
|
1152
|
+
else:
|
|
1153
|
+
print(f"Running {display}...")
|
|
1154
|
+
result = await execute_aria_tool(self.terminal.api_url, tool_name, params)
|
|
1155
|
+
|
|
1156
|
+
if result.get("success"):
|
|
1157
|
+
data = result.get("data", {})
|
|
1158
|
+
if isinstance(data, dict) and self.context.has_rich:
|
|
1159
|
+
out = Text()
|
|
1160
|
+
for k, v in data.items():
|
|
1161
|
+
if k in ("chart_prices", "raw", "metadata"):
|
|
1162
|
+
continue
|
|
1163
|
+
label_str = k.replace("_", " ").title()
|
|
1164
|
+
val_str = f"{v:.4f}" if isinstance(v, float) else str(v)
|
|
1165
|
+
if isinstance(v, (int, float)):
|
|
1166
|
+
color = "green" if v >= 0 else "red" if v < 0 else ""
|
|
1167
|
+
out.append(f" {label_str:<20s}", style="dim")
|
|
1168
|
+
out.append(f"{val_str}\n", style=color if color else "")
|
|
1169
|
+
else:
|
|
1170
|
+
out.append(f" {label_str:<20s}", style="dim")
|
|
1171
|
+
out.append(f"{val_str}\n")
|
|
1172
|
+
self.context.console.print(out)
|
|
1173
|
+
else:
|
|
1174
|
+
self.context.console.print(f" [dim]{json.dumps(data, ensure_ascii=False)[:500]}[/dim]" if self.context.has_rich
|
|
1175
|
+
else json.dumps(data, ensure_ascii=False)[:500])
|
|
1176
|
+
else:
|
|
1177
|
+
print_error(self.context, f"Failed: {result.get('error', 'No data')}")
|
|
1178
|
+
async def _run_parallel(self, tool_name: str,
|
|
1179
|
+
param_list: list,
|
|
1180
|
+
label_fn=None):
|
|
1181
|
+
"""Run a tool in parallel for multiple param dicts, display each result."""
|
|
1182
|
+
from aria_cli import _print_finance_result
|
|
1183
|
+
from aria_code.apps.cli.tool_executor import execute_aria_tool
|
|
1184
|
+
from aria_code.apps.cli.tool_registry import LOCAL_TOOLS
|
|
1185
|
+
tasks = [
|
|
1186
|
+
asyncio.create_task(
|
|
1187
|
+
asyncio.get_event_loop().run_in_executor(
|
|
1188
|
+
None, LOCAL_TOOLS[tool_name][0], p
|
|
1189
|
+
) if tool_name in LOCAL_TOOLS
|
|
1190
|
+
else execute_aria_tool(self.terminal.api_url, tool_name, p)
|
|
1191
|
+
)
|
|
1192
|
+
for p in param_list
|
|
1193
|
+
]
|
|
1194
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
1195
|
+
for p, r in zip(param_list, results):
|
|
1196
|
+
lbl = label_fn(p) if label_fn else tool_name
|
|
1197
|
+
if isinstance(r, Exception):
|
|
1198
|
+
print_error(self.context, f"{lbl}: {r}")
|
|
1199
|
+
else:
|
|
1200
|
+
_print_finance_result(tool_name, r)
|
|
1201
|
+
async def _fetch_and_display_finance(self, tool_name: str, params: dict, label: str,
|
|
1202
|
+
mdc_fallback_symbol: str = "") -> bool:
|
|
1203
|
+
"""Try tool → local finance tool → market_data_client fallback. Returns True if data shown."""
|
|
1204
|
+
from aria_cli import _print_finance_result
|
|
1205
|
+
from aria_code.apps.cli.tool_executor import execute_aria_tool
|
|
1206
|
+
from aria_code.apps.cli.tool_registry import LOCAL_TOOLS
|
|
1207
|
+
from aria_code.apps.cli._optional import HAS_MDC as _HAS_MDC, get_mdc as _get_mdc
|
|
1208
|
+
result = None
|
|
1209
|
+
# 1. LOCAL_TOOLS (ccxt / local finance)
|
|
1210
|
+
if tool_name in LOCAL_TOOLS:
|
|
1211
|
+
fn = LOCAL_TOOLS[tool_name][0]
|
|
1212
|
+
result = await asyncio.get_event_loop().run_in_executor(None, fn, params)
|
|
1213
|
+
# 2. Remote backend
|
|
1214
|
+
if not (result and result.get("success")):
|
|
1215
|
+
result = await execute_aria_tool(self.terminal.api_url, tool_name, params)
|
|
1216
|
+
# 3. MarketDataClient yfinance fallback
|
|
1217
|
+
if not (result and result.get("success")) and _HAS_MDC and mdc_fallback_symbol:
|
|
1218
|
+
try:
|
|
1219
|
+
mdc = _get_mdc()
|
|
1220
|
+
result = mdc.quote(mdc_fallback_symbol)
|
|
1221
|
+
if result.get("success"):
|
|
1222
|
+
result["provider"] = "yfinance"
|
|
1223
|
+
except Exception:
|
|
1224
|
+
pass
|
|
1225
|
+
|
|
1226
|
+
if result and result.get("success"):
|
|
1227
|
+
_print_finance_result(tool_name, result)
|
|
1228
|
+
# Also show basic price line if _print_finance_result didn't handle this tool
|
|
1229
|
+
if tool_name not in ("get_market_data", "get_crypto_data", "get_forex_data"):
|
|
1230
|
+
px = result.get("price", result.get("rate", 0))
|
|
1231
|
+
chg = result.get("change_pct", 0)
|
|
1232
|
+
sign = "+" if chg >= 0 else ""
|
|
1233
|
+
color = "green" if chg >= 0 else "red"
|
|
1234
|
+
prov = result.get("provider", "")
|
|
1235
|
+
if self.context.has_rich and px:
|
|
1236
|
+
self.context.console.print(f" [bold]{label:<12}[/bold] {px} [{color}]{sign}{chg:.2f}%[/{color}] [dim]{prov}[/dim]")
|
|
1237
|
+
return True
|
|
1238
|
+
else:
|
|
1239
|
+
err = (result or {}).get("error") or "数据暂不可用"
|
|
1240
|
+
if self.context.has_rich:
|
|
1241
|
+
self.context.console.print(f" [yellow]⚠ {label}: {err}[/yellow]")
|
|
1242
|
+
else:
|
|
1243
|
+
print(f" ⚠ {label}: {err}")
|
|
1244
|
+
return False
|
|
1245
|
+
async def cmd_risk(self, args: str):
|
|
1246
|
+
"""Risk metrics: /risk AAPL or /risk portfolio"""
|
|
1247
|
+
from aria_code.apps.cli.tool_executor import execute_aria_tool
|
|
1248
|
+
from aria_code.apps.cli.tool_registry import LOCAL_TOOLS
|
|
1249
|
+
target = args.strip().upper() or "AAPL"
|
|
1250
|
+
if target == "PORTFOLIO":
|
|
1251
|
+
await self._run_tool_cmd("assess_portfolio_risk", {
|
|
1252
|
+
"holdings": self.terminal.config.get("watchlist", ["AAPL", "MSFT"]),
|
|
1253
|
+
}, "portfolio risk")
|
|
1254
|
+
return
|
|
1255
|
+
|
|
1256
|
+
# Try remote tool; fall back to local get_risk_metrics if backend unavailable
|
|
1257
|
+
result = await execute_aria_tool(self.terminal.api_url, "get_risk_metrics", {"symbol": target})
|
|
1258
|
+
if result.get("success"):
|
|
1259
|
+
data = result.get("data", {})
|
|
1260
|
+
if self.context.has_rich:
|
|
1261
|
+
self.context.console.print()
|
|
1262
|
+
for k, v in (data.items() if isinstance(data, dict) else {}.items()):
|
|
1263
|
+
val_str = f"{v:.4f}" if isinstance(v, float) else str(v)
|
|
1264
|
+
color = "green" if isinstance(v, float) and v >= 0 else ("red" if isinstance(v, float) and v < 0 else "")
|
|
1265
|
+
self.context.console.print(f" [dim]{k.replace('_',' ').title():<24s}[/dim] [{color}]{val_str}[/{color}]" if color
|
|
1266
|
+
else f" [dim]{k.replace('_',' ').title():<24s}[/dim] {val_str}")
|
|
1267
|
+
self.context.console.print()
|
|
1268
|
+
elif "get_risk_metrics" in LOCAL_TOOLS:
|
|
1269
|
+
# Local fallback
|
|
1270
|
+
local_fn = LOCAL_TOOLS["get_risk_metrics"][0]
|
|
1271
|
+
local_result = await asyncio.get_event_loop().run_in_executor(None, local_fn, {"symbol": target})
|
|
1272
|
+
if local_result.get("success"):
|
|
1273
|
+
data = local_result.get("data", {})
|
|
1274
|
+
if self.context.has_rich:
|
|
1275
|
+
self.context.console.print()
|
|
1276
|
+
self.context.console.print(f" [bold]{target} Risk Metrics[/bold] [dim](local calculation)[/dim]")
|
|
1277
|
+
self.context.console.print()
|
|
1278
|
+
for k, v in (data.items() if isinstance(data, dict) else {}.items()):
|
|
1279
|
+
val_str = f"{v:.4f}" if isinstance(v, float) else str(v)
|
|
1280
|
+
self.context.console.print(f" [dim]{k.replace('_',' ').title():<24s}[/dim] {val_str}")
|
|
1281
|
+
self.context.console.print()
|
|
1282
|
+
else:
|
|
1283
|
+
print(f" {target} Risk Metrics (local):")
|
|
1284
|
+
for k, v in (data.items() if isinstance(data, dict) else {}.items()):
|
|
1285
|
+
print(f" {k}: {v}")
|
|
1286
|
+
else:
|
|
1287
|
+
self.context.console.print(f"[dim]Risk metrics unavailable for {target}: {local_result.get('error','')}[/dim]") if self.context.has_rich else print(f"Risk unavailable: {local_result.get('error','')}")
|
|
1288
|
+
else:
|
|
1289
|
+
msg = f"⚠ 风险指标服务暂不可用 ({result.get('error','')[:60]})"
|
|
1290
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
1291
|
+
async def cmd_market(self, args: str):
|
|
1292
|
+
"""Market overview: /market [indices|sectors]"""
|
|
1293
|
+
from aria_code.apps.cli.tool_executor import execute_aria_tool
|
|
1294
|
+
from aria_code.apps.cli._optional import HAS_MDC as _HAS_MDC, get_mdc as _get_mdc
|
|
1295
|
+
sub = args.strip().lower()
|
|
1296
|
+
if sub == "sectors":
|
|
1297
|
+
await self._run_tool_cmd("get_sector_performance", {}, "sector performance")
|
|
1298
|
+
else:
|
|
1299
|
+
# Try remote tool first; fall back to local MarketDataClient if backend unavailable
|
|
1300
|
+
result = await execute_aria_tool(self.terminal.api_url, "get_market_indices", {})
|
|
1301
|
+
if result and result.get("success"):
|
|
1302
|
+
await self._run_tool_cmd("get_market_indices", {}, "market indices")
|
|
1303
|
+
elif _HAS_MDC:
|
|
1304
|
+
# Local fallback via MarketDataClient.indices()
|
|
1305
|
+
try:
|
|
1306
|
+
mdc = _get_mdc()
|
|
1307
|
+
idx_result = mdc.indices()
|
|
1308
|
+
if idx_result.get("success") and idx_result.get("indices"):
|
|
1309
|
+
if self.context.has_rich:
|
|
1310
|
+
self.context.console.print()
|
|
1311
|
+
self.context.console.print(" [bold]Global Indices[/bold] [dim](local data)[/dim]")
|
|
1312
|
+
self.context.console.print()
|
|
1313
|
+
for name, d in idx_result["indices"].items():
|
|
1314
|
+
price = d.get("price", "N/A")
|
|
1315
|
+
chg = d.get("change_pct", 0)
|
|
1316
|
+
sign = "+" if chg >= 0 else ""
|
|
1317
|
+
color = "green" if chg >= 0 else "red"
|
|
1318
|
+
if self.context.has_rich:
|
|
1319
|
+
self.context.console.print(f" [dim]{name:<20s}[/dim] {price:>10} [{color}]{sign}{chg:.2f}%[/{color}]")
|
|
1320
|
+
else:
|
|
1321
|
+
print(f" {name:<20s} {price:>10} {sign}{chg:.2f}%")
|
|
1322
|
+
else:
|
|
1323
|
+
self.context.console.print("[dim]市场数据暂不可用。请检查网络连接。[/dim]") if self.context.has_rich else print("Market data unavailable.")
|
|
1324
|
+
except Exception as _e:
|
|
1325
|
+
self.context.console.print(f"[dim]本地数据获取失败: {_e}[/dim]") if self.context.has_rich else print(f"Local data error: {_e}")
|
|
1326
|
+
else:
|
|
1327
|
+
self.context.console.print("[dim]后端不可用,本地数据模块未加载。使用 /indices 命令查看实时行情。[/dim]") if self.context.has_rich else print("Backend unavailable. Try /indices.")
|
|
1328
|
+
async def cmd_optimize(self, args: str):
|
|
1329
|
+
"""Optimize portfolio: /optimize [symbols...]"""
|
|
1330
|
+
symbols = args.upper().split() if args else self.terminal.config.get("watchlist", ["AAPL", "MSFT", "GOOGL"])
|
|
1331
|
+
await self._run_tool_cmd("optimize_positions", {
|
|
1332
|
+
"symbols": symbols, "objective": "max_sharpe",
|
|
1333
|
+
}, f"optimizing {len(symbols)} positions")
|
|
1334
|
+
async def cmd_stress(self, args: str):
|
|
1335
|
+
"""Stress test: /stress <strategy> [symbol]"""
|
|
1336
|
+
parts = args.split() if args else ["momentum", "SPY"]
|
|
1337
|
+
strategy = parts[0] if parts else "momentum"
|
|
1338
|
+
symbol = parts[1].upper() if len(parts) > 1 else "SPY"
|
|
1339
|
+
await self._run_tool_cmd("stress_test_strategy", {
|
|
1340
|
+
"strategy": strategy, "symbol": symbol,
|
|
1341
|
+
}, f"stress test {strategy}/{symbol}")
|
|
1342
|
+
async def cmd_factors(self, args: str):
|
|
1343
|
+
"""Factor analysis: /factors AAPL"""
|
|
1344
|
+
symbol = args.strip().upper() or "AAPL"
|
|
1345
|
+
await self._run_tool_cmd("calculate_factors", {"symbol": symbol}, f"factors {symbol}")
|
|
1346
|
+
async def cmd_factor_lab(self, args: str):
|
|
1347
|
+
"""/factor-lab <SYMBOL> [days=252] — 量化因子工作台(动量/波动率/Sharpe/Amihud)"""
|
|
1348
|
+
parts = args.strip().split()
|
|
1349
|
+
symbol = parts[0].upper() if parts else "AAPL"
|
|
1350
|
+
market = "CN" if any(symbol.startswith(p) for p in ("SH", "SZ", "6", "0", "3")) else "US"
|
|
1351
|
+
|
|
1352
|
+
await self._run_tool_cmd(
|
|
1353
|
+
"equity_factor_scores",
|
|
1354
|
+
{"symbol": symbol, "period": "1y", "market": market},
|
|
1355
|
+
f"factor-lab {symbol}",
|
|
1356
|
+
)
|
|
1357
|
+
async def cmd_execution(self, args: str):
|
|
1358
|
+
"""/execution <SYMBOL> <buy|sell> <qty> [algo=compare] [price=0] — 执行算法对比"""
|
|
1359
|
+
parts = args.strip().split()
|
|
1360
|
+
if len(parts) < 3:
|
|
1361
|
+
if self.context.has_rich:
|
|
1362
|
+
self.context.console.print("[dim]Usage: /execution AAPL buy 100000 [algo=compare] [price=180][/dim]")
|
|
1363
|
+
return
|
|
1364
|
+
|
|
1365
|
+
symbol = parts[0].upper()
|
|
1366
|
+
side = parts[1].lower()
|
|
1367
|
+
try:
|
|
1368
|
+
total_qty = float(parts[2].replace(",", ""))
|
|
1369
|
+
except ValueError:
|
|
1370
|
+
if self.context.has_rich: self.context.console.print("[red]qty 必须是数字[/red]")
|
|
1371
|
+
return
|
|
1372
|
+
|
|
1373
|
+
algo = "compare"
|
|
1374
|
+
price = 0.0
|
|
1375
|
+
for p in parts[3:]:
|
|
1376
|
+
if p.startswith("algo="):
|
|
1377
|
+
algo = p.split("=", 1)[1]
|
|
1378
|
+
elif p.startswith("price="):
|
|
1379
|
+
try:
|
|
1380
|
+
price = float(p.split("=", 1)[1])
|
|
1381
|
+
except ValueError:
|
|
1382
|
+
pass
|
|
1383
|
+
|
|
1384
|
+
if price <= 0:
|
|
1385
|
+
# 尝试从市场数据获取现价
|
|
1386
|
+
try:
|
|
1387
|
+
import yfinance as yf
|
|
1388
|
+
t = yf.Ticker(symbol)
|
|
1389
|
+
info = t.fast_info
|
|
1390
|
+
price = float(getattr(info, "last_price", 0) or 0)
|
|
1391
|
+
except Exception:
|
|
1392
|
+
price = 100.0
|
|
1393
|
+
|
|
1394
|
+
await self._run_tool_cmd(
|
|
1395
|
+
"execution_schedule",
|
|
1396
|
+
{
|
|
1397
|
+
"symbol": symbol,
|
|
1398
|
+
"side": side,
|
|
1399
|
+
"total_qty": total_qty,
|
|
1400
|
+
"benchmark_price": price,
|
|
1401
|
+
"algo": algo,
|
|
1402
|
+
},
|
|
1403
|
+
f"执行计划 {symbol} {side} {total_qty:,.0f}股",
|
|
1404
|
+
)
|
|
1405
|
+
async def cmd_stat_arb(self, args: str):
|
|
1406
|
+
"""/stat-arb <SYMBOL_A> <SYMBOL_B> [period=2y] — 配对协整检验 + 当前 z-score"""
|
|
1407
|
+
from aria_cli import _generate_stat_arb_chart
|
|
1408
|
+
parts = args.strip().split()
|
|
1409
|
+
if len(parts) < 2:
|
|
1410
|
+
if self.context.has_rich:
|
|
1411
|
+
self.context.console.print("[dim]Usage: /stat-arb GLD SLV [period=2y][/dim]")
|
|
1412
|
+
return
|
|
1413
|
+
|
|
1414
|
+
sym_a = parts[0].upper()
|
|
1415
|
+
sym_b = parts[1].upper()
|
|
1416
|
+
period = "2y"
|
|
1417
|
+
for p in parts[2:]:
|
|
1418
|
+
if p.startswith("period="):
|
|
1419
|
+
period = p.split("=", 1)[1]
|
|
1420
|
+
|
|
1421
|
+
await self._run_tool_cmd(
|
|
1422
|
+
"pair_stats",
|
|
1423
|
+
{"symbol_a": sym_a, "symbol_b": sym_b, "period": period},
|
|
1424
|
+
f"配对检验 {sym_a}/{sym_b}",
|
|
1425
|
+
)
|
|
1426
|
+
# Generate interactive z-score chart
|
|
1427
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
1428
|
+
None, lambda: _generate_stat_arb_chart(sym_a, sym_b, period)
|
|
1429
|
+
)
|
|
1430
|
+
async def cmd_compliance(self, args: str):
|
|
1431
|
+
"""Compliance check: /compliance <strategy>"""
|
|
1432
|
+
strategy = args.strip() or "momentum"
|
|
1433
|
+
await self._run_tool_cmd("check_strategy_compliance", {
|
|
1434
|
+
"strategy": strategy,
|
|
1435
|
+
}, f"compliance {strategy}")
|
|
1436
|
+
async def cmd_search_web(self, args: str):
|
|
1437
|
+
"""Web search: /web <query>"""
|
|
1438
|
+
query = args.strip()
|
|
1439
|
+
if not query:
|
|
1440
|
+
self.context.console.print("[dim]Usage: /web <search query>[/dim]" if self.context.has_rich else "Usage: /web <query>")
|
|
1441
|
+
return
|
|
1442
|
+
await self._run_tool_cmd("web_search", {"query": query}, f"searching: {query[:30]}")
|
|
1443
|
+
def cmd_local(self, args: str):
|
|
1444
|
+
"""Toggle local-only mode (skip AWS, always use Ollama)."""
|
|
1445
|
+
cfg = self.terminal.config
|
|
1446
|
+
arg = args.strip().lower()
|
|
1447
|
+
if arg in ("on", "1", "true", "yes"):
|
|
1448
|
+
cfg["local_mode"] = True
|
|
1449
|
+
elif arg in ("off", "0", "false", "no"):
|
|
1450
|
+
cfg["local_mode"] = False
|
|
1451
|
+
else:
|
|
1452
|
+
cfg["local_mode"] = not cfg.get("local_mode", False)
|
|
1453
|
+
self.context.save_config(cfg)
|
|
1454
|
+
state = "ON" if cfg["local_mode"] else "OFF"
|
|
1455
|
+
model = cfg.get("model", "qwen2.5:7b")
|
|
1456
|
+
if self.context.has_rich:
|
|
1457
|
+
color = "green" if cfg["local_mode"] else "yellow"
|
|
1458
|
+
self.context.console.print(f" [{color}]Local mode {state}[/{color}] model=[bold]{model}[/bold] ollama={cfg.get('ollama_url','http://localhost:11434')}")
|
|
1459
|
+
else:
|
|
1460
|
+
print(f" Local mode {state} model={model}")
|
|
1461
|
+
async def cmd_mcp(self, args: str):
|
|
1462
|
+
"""Manage MCP servers: /mcp status | /mcp tools | /mcp reload [server]"""
|
|
1463
|
+
from aria_cli import CONFIG_DIR
|
|
1464
|
+
from aria_code.apps.cli._optional import MCP_CONFIG_PATH
|
|
1465
|
+
from aria_code.apps.cli._optional import HAS_MCP as _HAS_MCP
|
|
1466
|
+
from aria_code.apps.cli.tool_registry import LOCAL_TOOLS, LOCAL_TOOL_SCHEMAS
|
|
1467
|
+
if not _HAS_MCP:
|
|
1468
|
+
self.context.console.print(" [dim]mcp_client.py not available[/dim]" if self.context.has_rich else "MCP not available")
|
|
1469
|
+
return
|
|
1470
|
+
sub = args.strip().lower()
|
|
1471
|
+
reg = self.terminal._mcp_registry
|
|
1472
|
+
|
|
1473
|
+
_parts = sub.split()
|
|
1474
|
+
if _parts and _parts[0] in ("reload", "restart") and len(_parts) > 1:
|
|
1475
|
+
# Per-server reload: restart one subprocess + reset its circuit,
|
|
1476
|
+
# without tearing down every other server.
|
|
1477
|
+
_name = args.strip().split()[1]
|
|
1478
|
+
if not reg:
|
|
1479
|
+
self.context.console.print(" [dim]No MCP servers running[/dim]" if self.context.has_rich else "No MCP servers")
|
|
1480
|
+
return
|
|
1481
|
+
ok = await reg.reload_server(_name)
|
|
1482
|
+
msg = (f"MCP server {_name!r} reloaded" if ok
|
|
1483
|
+
else f"MCP server {_name!r} not found or failed to restart")
|
|
1484
|
+
if self.context.has_rich:
|
|
1485
|
+
self.context.console.print(f" [{'green' if ok else 'red'}]{msg}[/{'green' if ok else 'red'}]")
|
|
1486
|
+
else:
|
|
1487
|
+
print(f" {msg}")
|
|
1488
|
+
return
|
|
1489
|
+
|
|
1490
|
+
if sub in ("reload", "restart"):
|
|
1491
|
+
if reg:
|
|
1492
|
+
await reg.stop_all()
|
|
1493
|
+
self.terminal._mcp_started = False
|
|
1494
|
+
self.terminal._mcp_registry = None
|
|
1495
|
+
if self.context.has_rich:
|
|
1496
|
+
self.context.console.print(" [dim]Restarting MCP servers…[/dim]")
|
|
1497
|
+
from mcp_client import MCPToolRegistry
|
|
1498
|
+
self.terminal._mcp_registry = MCPToolRegistry()
|
|
1499
|
+
results = await self.terminal._mcp_registry.start_all()
|
|
1500
|
+
n = self.terminal._mcp_registry.register_into(LOCAL_TOOLS, LOCAL_TOOL_SCHEMAS, overwrite=True)
|
|
1501
|
+
if self.context.has_rich:
|
|
1502
|
+
self.context.console.print(f" [green]MCP reloaded: {n} tools from {len(results)} servers[/green]")
|
|
1503
|
+
return
|
|
1504
|
+
|
|
1505
|
+
if sub == "tools":
|
|
1506
|
+
if not reg:
|
|
1507
|
+
self.context.console.print(" [dim]No MCP servers running[/dim]" if self.context.has_rich else "No MCP servers")
|
|
1508
|
+
return
|
|
1509
|
+
tools = reg.all_tools()
|
|
1510
|
+
if self.context.has_rich:
|
|
1511
|
+
self.context.console.print(f"\n [bold]MCP Tools[/bold] ({len(tools)} total)\n")
|
|
1512
|
+
for t in tools:
|
|
1513
|
+
self.context.console.print(f" [bold]{t['qualified_name']:40s}[/bold][dim]{t.get('description','')[:60]}[/dim]")
|
|
1514
|
+
self.context.console.print()
|
|
1515
|
+
else:
|
|
1516
|
+
for t in tools:
|
|
1517
|
+
print(f" {t['qualified_name']:40s} {t.get('description','')[:60]}")
|
|
1518
|
+
return
|
|
1519
|
+
|
|
1520
|
+
# Default: status
|
|
1521
|
+
if self.context.has_rich:
|
|
1522
|
+
self.context.console.print()
|
|
1523
|
+
self.context.console.print(" [bold]MCP Servers[/bold]")
|
|
1524
|
+
if not _HAS_MCP:
|
|
1525
|
+
self.context.console.print(" [dim]Not available (mcp_client.py missing)[/dim]")
|
|
1526
|
+
elif not reg:
|
|
1527
|
+
config_path = str(MCP_CONFIG_PATH)
|
|
1528
|
+
self.context.console.print(f" [dim]No servers started. Configure: {config_path}[/dim]")
|
|
1529
|
+
self.context.console.print(" [dim]Example: add quant_engine MCP server pointing to your mcp_server.py[/dim]")
|
|
1530
|
+
else:
|
|
1531
|
+
for s in reg.status():
|
|
1532
|
+
color = "green" if s["running"] else "red"
|
|
1533
|
+
icon = "●" if s["running"] else "○"
|
|
1534
|
+
self.context.console.print(
|
|
1535
|
+
f" [{color}]{icon}[/{color}] [bold]{s['name']:20s}[/bold]"
|
|
1536
|
+
f" [dim]{s['tool_count']} tools {s['description'][:50]}[/dim]"
|
|
1537
|
+
)
|
|
1538
|
+
self.context.console.print()
|
|
1539
|
+
else:
|
|
1540
|
+
if not reg:
|
|
1541
|
+
print(f" No MCP servers. Configure {CONFIG_DIR}/mcp_servers.json")
|
|
1542
|
+
else:
|
|
1543
|
+
for s in reg.status():
|
|
1544
|
+
print(f" {'●' if s['running'] else '○'} {s['name']:20s} {s['tool_count']} tools")
|
|
1545
|
+
def cmd_license(self, args: str):
|
|
1546
|
+
"""Show feature license / entitlement status."""
|
|
1547
|
+
try:
|
|
1548
|
+
from licensing import current_license, license_status
|
|
1549
|
+
current_license(refresh=True) # re-read in case a key was just installed
|
|
1550
|
+
st = license_status()
|
|
1551
|
+
except Exception as e:
|
|
1552
|
+
self.context.console.print(f" [yellow]license 模块不可用: {e}[/yellow]" if self.context.has_rich else f"license unavailable: {e}")
|
|
1553
|
+
return
|
|
1554
|
+
tier = st.get("tier", "free")
|
|
1555
|
+
valid = st.get("valid", True)
|
|
1556
|
+
if self.context.has_rich:
|
|
1557
|
+
color = "green" if (tier != "free" and valid) else "dim"
|
|
1558
|
+
self.context.console.print(f" 授权等级: [{color}]{tier}[/{color}] 有效: {valid}"
|
|
1559
|
+
+ (f" 到期: {st['exp']}" if st.get("exp") else ""))
|
|
1560
|
+
if st.get("reason"):
|
|
1561
|
+
self.context.console.print(f" [yellow]{st['reason']}[/yellow]")
|
|
1562
|
+
feats = st.get("features") or []
|
|
1563
|
+
self.context.console.print(f" 已解锁: {', '.join(feats) if feats else '仅免费功能'}")
|
|
1564
|
+
self.context.console.print(f" 签名校验模式: {'开启' if st.get('signed_mode') else '关闭(开发/自托管)'}")
|
|
1565
|
+
self.context.console.print(" [dim]免费版含全部核心功能;专业功能配置 ARIA_LICENSE_KEY 或 ~/.arthera/license.json 解锁。[/dim]")
|
|
1566
|
+
else:
|
|
1567
|
+
print(f"license tier={tier} valid={valid} features={st.get('features')}")
|
|
1568
|
+
def cmd_ariarc(self, args: str):
|
|
1569
|
+
"""Show or reload .ariarc project configuration."""
|
|
1570
|
+
from aria_cli import _HAS_ARIARC, get_ariarc, reload_ariarc
|
|
1571
|
+
if not _HAS_ARIARC:
|
|
1572
|
+
self.context.console.print(" [dim]ariarc.py not available[/dim]" if self.context.has_rich else "ariarc not available")
|
|
1573
|
+
return
|
|
1574
|
+
if "reload" in args.lower():
|
|
1575
|
+
arc = reload_ariarc()
|
|
1576
|
+
self.terminal.ariarc = arc
|
|
1577
|
+
if self.context.has_rich:
|
|
1578
|
+
if arc.found:
|
|
1579
|
+
self.context.console.print(f" [green]ariarc reloaded: {arc.source_path}[/green]")
|
|
1580
|
+
else:
|
|
1581
|
+
self.context.console.print(" [yellow]No .ariarc found in current directory tree[/yellow]")
|
|
1582
|
+
return
|
|
1583
|
+
|
|
1584
|
+
arc = self.terminal.ariarc or get_ariarc()
|
|
1585
|
+
if self.context.has_rich:
|
|
1586
|
+
self.context.console.print()
|
|
1587
|
+
if not arc.found:
|
|
1588
|
+
self.context.console.print(" [dim]No .ariarc found (create .ariarc in your project root)[/dim]")
|
|
1589
|
+
self.context.console.print()
|
|
1590
|
+
_example = """{
|
|
1591
|
+
"project": "My Quant Strategy",
|
|
1592
|
+
"description": "A-share momentum + mean-reversion strategy",
|
|
1593
|
+
"market": "cn",
|
|
1594
|
+
"default_symbols": ["sh600519", "sh601318", "sz000858"],
|
|
1595
|
+
"system_prompt": "Focus on A-share market mechanics and T+1 constraints.",
|
|
1596
|
+
"context_files": ["README.md"],
|
|
1597
|
+
"auto_context": ["strategy/main.py"],
|
|
1598
|
+
"commands": {
|
|
1599
|
+
"/morning-cn": "生成A股早盘简报,重点关注 {default_symbols}"
|
|
1600
|
+
}
|
|
1601
|
+
}"""
|
|
1602
|
+
self.context.console.print(f" [dim]Example .ariarc:[/dim]\n{_example}")
|
|
1603
|
+
else:
|
|
1604
|
+
d = arc.to_dict()
|
|
1605
|
+
self.context.console.print(f" [bold]Project:[/bold] {arc.project or '(unnamed)'}")
|
|
1606
|
+
self.context.console.print(f" [bold]Source:[/bold] [dim]{d['source_path']}[/dim]")
|
|
1607
|
+
self.context.console.print(f" [bold]Market:[/bold] {arc.market}")
|
|
1608
|
+
if arc.default_symbols:
|
|
1609
|
+
self.context.console.print(f" [bold]Symbols:[/bold] {', '.join(arc.default_symbols)}")
|
|
1610
|
+
if arc.commands:
|
|
1611
|
+
self.context.console.print(f" [bold]Commands:[/bold] {', '.join(arc.commands.keys())}")
|
|
1612
|
+
if arc.tools_blacklist:
|
|
1613
|
+
self.context.console.print(f" [bold]Blocked tools:[/bold] {', '.join(arc.tools_blacklist)}")
|
|
1614
|
+
if arc.auto_context:
|
|
1615
|
+
self.context.console.print(f" [bold]Auto context:[/bold] {', '.join(arc.auto_context)}")
|
|
1616
|
+
self.context.console.print()
|
|
1617
|
+
else:
|
|
1618
|
+
if arc.found:
|
|
1619
|
+
import json as _j
|
|
1620
|
+
print(_j.dumps(arc.to_dict(), indent=2, ensure_ascii=False))
|
|
1621
|
+
async def cmd_signal(self, args: str):
|
|
1622
|
+
"""
|
|
1623
|
+
AI trading signal (BUY/SELL/HOLD) from Alibaba Cloud.
|
|
1624
|
+
Usage: /signal sh600519 /signal AAPL US
|
|
1625
|
+
"""
|
|
1626
|
+
from aria_code.apps.cli.helpers import _is_ashare_symbol
|
|
1627
|
+
parts = args.strip().split()
|
|
1628
|
+
symbol = parts[0].upper() if parts else "sh600519"
|
|
1629
|
+
market = parts[1].upper() if len(parts) > 1 else ("CN" if _is_ashare_symbol(symbol) else "US")
|
|
1630
|
+
await self._run_tool_cmd("get_ai_signal", {"symbol": symbol, "market": market},
|
|
1631
|
+
f"AI signal {symbol}")
|
|
1632
|
+
async def cmd_predict(self, args: str):
|
|
1633
|
+
"""
|
|
1634
|
+
ML return predictions for a list of symbols.
|
|
1635
|
+
Usage: /predict sh600519 sh601318 sz000858
|
|
1636
|
+
"""
|
|
1637
|
+
parts = args.strip().split() if args.strip() else ["sh600519"]
|
|
1638
|
+
symbols = [s for s in parts if not s.isdigit() or len(s) == 6]
|
|
1639
|
+
days = 5
|
|
1640
|
+
for p in parts:
|
|
1641
|
+
if p.startswith("d="):
|
|
1642
|
+
try:
|
|
1643
|
+
days = int(p[2:])
|
|
1644
|
+
except ValueError:
|
|
1645
|
+
pass
|
|
1646
|
+
await self._run_tool_cmd("get_predictions",
|
|
1647
|
+
{"symbols": symbols, "prediction_days": days},
|
|
1648
|
+
f"ML predict {len(symbols)} stocks")
|
|
1649
|
+
async def cmd_cloudbt(self, args: str):
|
|
1650
|
+
"""
|
|
1651
|
+
Full ML-powered backtest on Alibaba Cloud.
|
|
1652
|
+
Usage: /cloudbt sh600519 sh601318 [model=lightgbm] [months=12] [freq=weekly] [top=3]
|
|
1653
|
+
"""
|
|
1654
|
+
parts = args.strip().split() if args.strip() else []
|
|
1655
|
+
symbols = []
|
|
1656
|
+
kwargs: Dict[str, Any] = {}
|
|
1657
|
+
for p in parts:
|
|
1658
|
+
if "=" in p:
|
|
1659
|
+
k, v = p.split("=", 1)
|
|
1660
|
+
kwargs[k] = v
|
|
1661
|
+
else:
|
|
1662
|
+
symbols.append(p)
|
|
1663
|
+
if not symbols:
|
|
1664
|
+
symbols = ["sh600519"]
|
|
1665
|
+
params = {
|
|
1666
|
+
"symbols": symbols,
|
|
1667
|
+
"model_type": kwargs.get("model", "lightgbm"),
|
|
1668
|
+
"months": int(kwargs.get("months", 12)),
|
|
1669
|
+
"rebalance_freq": kwargs.get("freq", "weekly"),
|
|
1670
|
+
"top_k": int(kwargs.get("top", 3)),
|
|
1671
|
+
}
|
|
1672
|
+
await self._run_tool_cmd("cloud_backtest", params,
|
|
1673
|
+
f"cloud backtest {len(symbols)} stocks")
|
|
1674
|
+
async def cmd_insights(self, args: str):
|
|
1675
|
+
"""
|
|
1676
|
+
AI market insights for a basket of stocks.
|
|
1677
|
+
Usage: /insights sh600519 sh601318 sz000858
|
|
1678
|
+
"""
|
|
1679
|
+
parts = args.strip().split() if args.strip() else ["sh600519"]
|
|
1680
|
+
symbols = parts
|
|
1681
|
+
await self._run_tool_cmd("get_market_insights",
|
|
1682
|
+
{"symbols": symbols},
|
|
1683
|
+
f"market insights {len(symbols)} stocks")
|
|
1684
|
+
def cmd_recommend(self, args: str):
|
|
1685
|
+
"""Recommend best local models for financial analysis."""
|
|
1686
|
+
from aria_cli import RECOMMENDED_FINANCE_MODELS
|
|
1687
|
+
from aria_code.apps.cli.helpers import detect_ollama_models
|
|
1688
|
+
if self.context.has_rich:
|
|
1689
|
+
self.context.console.print()
|
|
1690
|
+
self.context.console.print(" [bold]Recommended Local Models for Finance[/bold]")
|
|
1691
|
+
self.context.console.print()
|
|
1692
|
+
try:
|
|
1693
|
+
available = detect_ollama_models(
|
|
1694
|
+
self.terminal.config.get("ollama_url", "http://localhost:11434")
|
|
1695
|
+
)
|
|
1696
|
+
for rec in RECOMMENDED_FINANCE_MODELS:
|
|
1697
|
+
model_id = rec["model"]
|
|
1698
|
+
installed = any(a.startswith(model_id.split(":")[0]) for a in available)
|
|
1699
|
+
icon = "[green]●[/green]" if installed else "[dim]○[/dim]"
|
|
1700
|
+
vram = rec.get("vram_gb", "?")
|
|
1701
|
+
self.context.console.print(
|
|
1702
|
+
f" {icon} [bold]{model_id:30s}[/bold] "
|
|
1703
|
+
f"[dim]VRAM≈{vram}GB {rec['reason'][:60]}[/dim]"
|
|
1704
|
+
)
|
|
1705
|
+
if not installed:
|
|
1706
|
+
self.context.console.print(f" [dim]Install: {rec['install']}[/dim]")
|
|
1707
|
+
self.context.console.print()
|
|
1708
|
+
except Exception:
|
|
1709
|
+
self.context.console.print(" [dim]Could not check installed models[/dim]")
|
|
1710
|
+
else:
|
|
1711
|
+
for rec in RECOMMENDED_FINANCE_MODELS:
|
|
1712
|
+
print(f" {rec['model']:30s} {rec['reason']}")
|
|
1713
|
+
print(f" Install: {rec['install']}")
|
|
1714
|
+
async def cmd_optimize_port(self, args: str):
|
|
1715
|
+
"""Portfolio weight optimisation."""
|
|
1716
|
+
from aria_code.apps.cli.tool_registry import LOCAL_TOOLS
|
|
1717
|
+
symbols = [s.strip().upper() for s in args.split() if s.strip()]
|
|
1718
|
+
if not symbols:
|
|
1719
|
+
self.context.console.print(" [dim]Usage: /optimize-port AAPL MSFT GOOGL [method=max_sharpe][/dim]" if self.context.has_rich
|
|
1720
|
+
else "Usage: /optimize-port AAPL MSFT [method=max_sharpe]")
|
|
1721
|
+
return
|
|
1722
|
+
# Check if last token is method=X
|
|
1723
|
+
method = "max_sharpe"
|
|
1724
|
+
if symbols and "=" in symbols[-1]:
|
|
1725
|
+
k, v = symbols.pop().split("=", 1)
|
|
1726
|
+
if k == "method":
|
|
1727
|
+
method = v
|
|
1728
|
+
params = {"symbols": symbols, "method": method}
|
|
1729
|
+
tool_name = "optimize_positions"
|
|
1730
|
+
if tool_name in LOCAL_TOOLS:
|
|
1731
|
+
await self._run_local_tool(tool_name, params, f"优化持仓 {method}")
|
|
1732
|
+
else:
|
|
1733
|
+
await self.terminal.send_message(
|
|
1734
|
+
f"帮我用 {method} 方法优化以下资产组合的仓位权重:{', '.join(symbols)}"
|
|
1735
|
+
)
|
|
1736
|
+
async def _run_local_tool(self, tool_name: str, params: dict, label: str = ""):
|
|
1737
|
+
"""Run a LOCAL_TOOLS entry, display result with Rich formatting."""
|
|
1738
|
+
from aria_cli import _print_tool_result
|
|
1739
|
+
from aria_code.apps.cli.tool_registry import LOCAL_TOOLS
|
|
1740
|
+
from aria_code.apps.cli.helpers import _clean_tool_error_message
|
|
1741
|
+
if tool_name not in LOCAL_TOOLS:
|
|
1742
|
+
if self.context.has_rich:
|
|
1743
|
+
self.context.console.print(f" [dim]Tool {tool_name!r} not available[/dim]")
|
|
1744
|
+
return
|
|
1745
|
+
handler, _ = LOCAL_TOOLS[tool_name]
|
|
1746
|
+
label_text = label or tool_name
|
|
1747
|
+
if self.context.has_rich:
|
|
1748
|
+
with self.context.console.status(f"[dim]{label_text}…[/dim]", spinner="dots"):
|
|
1749
|
+
result = handler(params)
|
|
1750
|
+
else:
|
|
1751
|
+
print(f" {label_text}…")
|
|
1752
|
+
result = handler(params)
|
|
1753
|
+
|
|
1754
|
+
if not result.get("success", True):
|
|
1755
|
+
err = _clean_tool_error_message(result.get("error", "unknown error"))
|
|
1756
|
+
if self.context.has_rich:
|
|
1757
|
+
self.context.console.print(f" [red]✗[/red] {err}")
|
|
1758
|
+
else:
|
|
1759
|
+
print(f" ✗ {err}")
|
|
1760
|
+
return
|
|
1761
|
+
|
|
1762
|
+
# Pretty-print result
|
|
1763
|
+
_print_tool_result(tool_name, result, elapsed=0)
|
|
1764
|
+
async def cmd_ui(self, args: str):
|
|
1765
|
+
"""
|
|
1766
|
+
Generate a Bloomberg Terminal-style HTML file for any requested view.
|
|
1767
|
+
Usage: /ui <description>
|
|
1768
|
+
/ui 今日A股板块热力图
|
|
1769
|
+
/ui 持仓组合风险报告
|
|
1770
|
+
/ui 宏观利率与大宗商品看板
|
|
1771
|
+
"""
|
|
1772
|
+
desc = args.strip()
|
|
1773
|
+
if not desc:
|
|
1774
|
+
if self.context.has_rich:
|
|
1775
|
+
self.context.console.print("[dim]Usage: /ui <描述>[/dim]")
|
|
1776
|
+
self.context.console.print("[dim] /ui 今日A股热力图[/dim]")
|
|
1777
|
+
self.context.console.print("[dim] /ui 持仓组合报告[/dim]")
|
|
1778
|
+
self.context.console.print("[dim] /ui 市场晨报看板[/dim]")
|
|
1779
|
+
else:
|
|
1780
|
+
print("Usage: /ui <description>")
|
|
1781
|
+
return
|
|
1782
|
+
|
|
1783
|
+
try:
|
|
1784
|
+
from apps.cli.prompts.ui import UI_SYSTEM_PROMPT
|
|
1785
|
+
except ImportError:
|
|
1786
|
+
UI_SYSTEM_PROMPT = ""
|
|
1787
|
+
try:
|
|
1788
|
+
from artifacts import user_generated_dir
|
|
1789
|
+
generated_dir = user_generated_dir()
|
|
1790
|
+
except Exception:
|
|
1791
|
+
generated_dir = pathlib.Path.home() / "Documents" / "Aria Code" / "generated"
|
|
1792
|
+
|
|
1793
|
+
prompt = (
|
|
1794
|
+
f"{UI_SYSTEM_PROMPT}\n\n"
|
|
1795
|
+
"---\n\n"
|
|
1796
|
+
f"USER REQUEST: {desc}\n\n"
|
|
1797
|
+
"Generate a Bloomberg Terminal-style HTML dashboard for this request.\n"
|
|
1798
|
+
"Follow the workflow:\n"
|
|
1799
|
+
"1. Identify what data you need (symbols, metrics, time range).\n"
|
|
1800
|
+
f"2. Write a Python generator script under {generated_dir} using write_file:\n"
|
|
1801
|
+
" - Fetch all data with yfinance / akshare\n"
|
|
1802
|
+
" - Embed data as JS/HTML constants\n"
|
|
1803
|
+
" - Apply the full Bloomberg CSS design system (dark/light via prefers-color-scheme)\n"
|
|
1804
|
+
f" - Save timestamped HTML under {generated_dir}\n"
|
|
1805
|
+
"3. Run the script with run_command.\n"
|
|
1806
|
+
"4. Open the output HTML in the browser.\n\n"
|
|
1807
|
+
"Design requirements (non-negotiable):\n"
|
|
1808
|
+
"- No emojis anywhere — use [+] [-] [!] [ACTIVE] text indicators\n"
|
|
1809
|
+
"- border-radius: 0 everywhere\n"
|
|
1810
|
+
"- IBM Plex Mono for all prices and numbers\n"
|
|
1811
|
+
"- prefers-color-scheme: dark default, light override\n"
|
|
1812
|
+
"- ALL CAPS section headers, 10px, letter-spacing 0.12em\n"
|
|
1813
|
+
"- Accent color: dark=#F5A623, light=#B8520A\n"
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
if self.context.has_rich:
|
|
1817
|
+
self.context.console.print(f"[bold]UI Generation:[/bold] {desc}")
|
|
1818
|
+
else:
|
|
1819
|
+
print(f"Generating UI: {desc}")
|
|
1820
|
+
|
|
1821
|
+
await self.terminal.send_message(prompt)
|
|
1822
|
+
async def cmd_dashboard(self, args: str):
|
|
1823
|
+
"""
|
|
1824
|
+
生成个人化 Dashboard HTML,自动在浏览器打开。
|
|
1825
|
+
Usage: /dashboard [brief|market|portfolio|full]
|
|
1826
|
+
数据来源: 本地持仓DB + 价格预警DB + yfinance 实时行情 + 最近生成文件
|
|
1827
|
+
"""
|
|
1828
|
+
try:
|
|
1829
|
+
from dashboard_generator import generate_and_open
|
|
1830
|
+
except ImportError:
|
|
1831
|
+
if self.context.has_rich:
|
|
1832
|
+
self.context.console.print("[red]dashboard_generator.py 未找到,请检查安装[/red]")
|
|
1833
|
+
else:
|
|
1834
|
+
print("dashboard_generator.py 未找到")
|
|
1835
|
+
return
|
|
1836
|
+
|
|
1837
|
+
parts = args.strip().split()
|
|
1838
|
+
mode = parts[0].lower() if parts and parts[0].lower() in {"brief", "market", "portfolio", "full"} else "brief"
|
|
1839
|
+
watchlist = self.terminal.config.get("watchlist", [])
|
|
1840
|
+
if self.context.has_rich:
|
|
1841
|
+
self.context.console.print(f"[dim]正在抓取数据并生成 Dashboard({mode})…[/dim]")
|
|
1842
|
+
else:
|
|
1843
|
+
print(f"正在生成 Dashboard({mode})…")
|
|
1844
|
+
|
|
1845
|
+
try:
|
|
1846
|
+
out = generate_and_open(watchlist=watchlist, config=self.terminal.config, mode=mode)
|
|
1847
|
+
if self.context.has_rich:
|
|
1848
|
+
self.context.console.print(
|
|
1849
|
+
f" [green]✓[/green] Dashboard 已生成并在浏览器打开\n"
|
|
1850
|
+
f" [dim]路径: [bold]{out}[/bold][/dim]"
|
|
1851
|
+
)
|
|
1852
|
+
else:
|
|
1853
|
+
print(f"Dashboard saved: {out}")
|
|
1854
|
+
except Exception as exc:
|
|
1855
|
+
if self.context.has_rich:
|
|
1856
|
+
self.context.console.print(f"[red]生成失败: {exc}[/red]")
|
|
1857
|
+
else:
|
|
1858
|
+
print(f"生成失败: {exc}")
|
|
1859
|
+
async def cmd_tv(self, args: str):
|
|
1860
|
+
"""Print a TradingView chart URL or export a Pine Script strategy."""
|
|
1861
|
+
from aria_cli import _build_tradingview_indicator_readout, _resolve_market_arg_symbol, _src_market_snapshot_analysis, _strip_latex, _write_text_companion, make_markdown
|
|
1862
|
+
from aria_code.apps.cli.helpers import _chart_display_label, _copy_text_to_clipboard, _open_path_or_url, _reveal_path_in_finder
|
|
1863
|
+
parts = [p.strip() for p in args.strip().split() if p.strip()]
|
|
1864
|
+
open_browser = any(p in {"--open", "-o", "open"} for p in parts)
|
|
1865
|
+
export_pine = any(p in {"--pine", "pine", "--strategy", "strategy", "策略"} for p in parts)
|
|
1866
|
+
copy_pine = any(p in {"--copy", "copy", "复制"} for p in parts)
|
|
1867
|
+
reveal_file = any(p in {"--reveal", "reveal", "finder", "访达", "目录"} for p in parts)
|
|
1868
|
+
txt_copy = any(p in {"--txt", "txt", "文本"} for p in parts)
|
|
1869
|
+
bullish_analysis = any(p in {"--bullish", "bullish", "看涨", "偏多"} for p in parts)
|
|
1870
|
+
bearish_analysis = any(p in {"--bearish", "bearish", "看跌", "偏空"} for p in parts)
|
|
1871
|
+
general_analysis = any(p in {"--analyze", "--analysis", "analyze", "analysis", "分析"} for p in parts)
|
|
1872
|
+
tv_analysis_mode = "bullish" if bullish_analysis else ("bearish" if bearish_analysis else ("analyze" if general_analysis else ""))
|
|
1873
|
+
interval = None
|
|
1874
|
+
symbol_parts = []
|
|
1875
|
+
skip_next = False
|
|
1876
|
+
for idx, part in enumerate(parts):
|
|
1877
|
+
if skip_next:
|
|
1878
|
+
skip_next = False
|
|
1879
|
+
continue
|
|
1880
|
+
low = part.lower()
|
|
1881
|
+
if low in {
|
|
1882
|
+
"--open", "-o", "open", "--pine", "pine", "--strategy", "strategy", "策略",
|
|
1883
|
+
"--copy", "copy", "复制", "--reveal", "reveal", "finder", "访达", "目录",
|
|
1884
|
+
"--txt", "txt", "文本", "--bullish", "bullish", "看涨", "偏多",
|
|
1885
|
+
"--bearish", "bearish", "看跌", "偏空", "--analyze", "--analysis",
|
|
1886
|
+
"analyze", "analysis", "分析",
|
|
1887
|
+
}:
|
|
1888
|
+
continue
|
|
1889
|
+
if low.startswith("--interval="):
|
|
1890
|
+
interval = part.split("=", 1)[1]
|
|
1891
|
+
continue
|
|
1892
|
+
if low == "--interval" and idx + 1 < len(parts):
|
|
1893
|
+
interval = parts[idx + 1]
|
|
1894
|
+
skip_next = True
|
|
1895
|
+
continue
|
|
1896
|
+
symbol_parts.append(part.strip(",,"))
|
|
1897
|
+
|
|
1898
|
+
raw_symbol = symbol_parts[0] if symbol_parts else "AAPL"
|
|
1899
|
+
symbol = _resolve_market_arg_symbol(raw_symbol)
|
|
1900
|
+
try:
|
|
1901
|
+
from apps.cli.tradingview_bridge import (
|
|
1902
|
+
export_pine_strategy,
|
|
1903
|
+
tradingview_symbol,
|
|
1904
|
+
tradingview_url,
|
|
1905
|
+
)
|
|
1906
|
+
tv_symbol = tradingview_symbol(symbol)
|
|
1907
|
+
url = tradingview_url(symbol, interval=interval)
|
|
1908
|
+
except Exception as exc:
|
|
1909
|
+
print_error(self.context, f"TradingView URL 生成失败: {exc}")
|
|
1910
|
+
return
|
|
1911
|
+
|
|
1912
|
+
if self.context.has_rich:
|
|
1913
|
+
self.context.console.print(f"\n [bold]TradingView[/bold] {_chart_display_label(raw_symbol, symbol)}")
|
|
1914
|
+
self.context.console.print(f" [dim]symbol:[/dim] {symbol} [dim]tv:[/dim] {tv_symbol}")
|
|
1915
|
+
self.context.console.print(f" [link={url}]{url}[/link]")
|
|
1916
|
+
else:
|
|
1917
|
+
print(f"TradingView {symbol} -> {tv_symbol}")
|
|
1918
|
+
print(url)
|
|
1919
|
+
self.terminal._pending_market_artifact = {
|
|
1920
|
+
"kind": "tradingview_chart",
|
|
1921
|
+
"symbol": symbol,
|
|
1922
|
+
"display": _chart_display_label(raw_symbol, symbol),
|
|
1923
|
+
"tv_symbol": tv_symbol,
|
|
1924
|
+
"url": url,
|
|
1925
|
+
"command": f"/tv {symbol}",
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
if export_pine:
|
|
1929
|
+
try:
|
|
1930
|
+
pine_path = export_pine_strategy(symbol)
|
|
1931
|
+
self.terminal._pending_market_artifact = {
|
|
1932
|
+
"kind": "tradingview_pine_strategy",
|
|
1933
|
+
"symbol": symbol,
|
|
1934
|
+
"display": _chart_display_label(raw_symbol, symbol),
|
|
1935
|
+
"tv_symbol": tv_symbol,
|
|
1936
|
+
"url": url,
|
|
1937
|
+
"pine_path": str(pine_path),
|
|
1938
|
+
"path": str(pine_path),
|
|
1939
|
+
"command": f"/tv {symbol} --pine",
|
|
1940
|
+
}
|
|
1941
|
+
if self.context.has_rich:
|
|
1942
|
+
self.context.console.print(f" [green]✓[/green] Pine strategy saved: [link={pine_path}]{pine_path}[/link]")
|
|
1943
|
+
self.context.console.print(" [dim]Use: TradingView → Pine Editor → paste script → Save → Add to chart[/dim]")
|
|
1944
|
+
else:
|
|
1945
|
+
print(f"Pine strategy saved: {pine_path}")
|
|
1946
|
+
print("Use: TradingView -> Pine Editor -> paste script -> Save -> Add to chart")
|
|
1947
|
+
if txt_copy:
|
|
1948
|
+
companion = _write_text_companion(str(pine_path))
|
|
1949
|
+
if companion:
|
|
1950
|
+
if self.context.has_rich:
|
|
1951
|
+
self.context.console.print(f" [green]✓[/green] Text copy saved: [link={companion}]{companion}[/link]")
|
|
1952
|
+
else:
|
|
1953
|
+
print(f"Text copy saved: {companion}")
|
|
1954
|
+
if copy_pine:
|
|
1955
|
+
try:
|
|
1956
|
+
copied, copy_err = _copy_text_to_clipboard(pine_path.read_text(encoding="utf-8"))
|
|
1957
|
+
except Exception as exc:
|
|
1958
|
+
copied, copy_err = False, str(exc)
|
|
1959
|
+
if self.context.has_rich:
|
|
1960
|
+
self.context.console.print(" [green]✓[/green] Pine script copied to clipboard" if copied else f" [yellow]copy failed:[/yellow] {copy_err}")
|
|
1961
|
+
else:
|
|
1962
|
+
print("Pine script copied to clipboard" if copied else f"copy failed: {copy_err}")
|
|
1963
|
+
if reveal_file:
|
|
1964
|
+
revealed, reveal_err = _reveal_path_in_finder(str(pine_path))
|
|
1965
|
+
if self.context.has_rich:
|
|
1966
|
+
self.context.console.print(" [green]✓[/green] Revealed in Finder" if revealed else f" [yellow]reveal failed:[/yellow] {reveal_err}")
|
|
1967
|
+
else:
|
|
1968
|
+
print("Revealed in Finder" if revealed else f"reveal failed: {reveal_err}")
|
|
1969
|
+
except Exception as exc:
|
|
1970
|
+
print_error(self.context, f"Pine Script 导出失败: {exc}")
|
|
1971
|
+
return
|
|
1972
|
+
|
|
1973
|
+
if open_browser and url:
|
|
1974
|
+
opened, open_err = _open_path_or_url(url)
|
|
1975
|
+
if self.context.has_rich:
|
|
1976
|
+
self.context.console.print(" [green]✓[/green] opened in browser" if opened else f" [yellow]open failed:[/yellow] {open_err}")
|
|
1977
|
+
else:
|
|
1978
|
+
print("opened in browser" if opened else f"open failed: {open_err}")
|
|
1979
|
+
|
|
1980
|
+
if tv_analysis_mode:
|
|
1981
|
+
snapshot = _src_market_snapshot_analysis(f"分析 {symbol} 股票")
|
|
1982
|
+
if snapshot.get("success"):
|
|
1983
|
+
readout = _build_tradingview_indicator_readout(
|
|
1984
|
+
snapshot,
|
|
1985
|
+
tv_symbol=tv_symbol,
|
|
1986
|
+
mode=tv_analysis_mode,
|
|
1987
|
+
)
|
|
1988
|
+
if self.context.has_rich:
|
|
1989
|
+
self.context.console.print(make_markdown(_strip_latex(readout)))
|
|
1990
|
+
else:
|
|
1991
|
+
print(readout)
|
|
1992
|
+
self.terminal._last_response = readout
|
|
1993
|
+
else:
|
|
1994
|
+
msg = (
|
|
1995
|
+
f"TradingView 已打开,但当前无法生成 {symbol} 的指标分析:"
|
|
1996
|
+
f"{snapshot.get('error') or '行情数据不可用'}"
|
|
1997
|
+
)
|
|
1998
|
+
if self.context.has_rich:
|
|
1999
|
+
self.context.console.print(f" [yellow]{msg}[/yellow]")
|
|
2000
|
+
else:
|
|
2001
|
+
print(msg)
|
|
2002
|
+
async def cmd_chart(self, args: str):
|
|
2003
|
+
"""
|
|
2004
|
+
生成股票分析图表(HTML,含K线/均线/RSI/MACD)。
|
|
2005
|
+
Usage: /chart AAPL [period]
|
|
2006
|
+
/chart 600519 3m (A股,3个月)
|
|
2007
|
+
/chart BTC-USD 2y
|
|
2008
|
+
支持 period: 1m 3m 6m 1y 2y 3y 5y ytd max
|
|
2009
|
+
"""
|
|
2010
|
+
from aria_cli import _generate_chart_sync, _resolve_market_arg_symbol, sanitize_chart_symbol_args
|
|
2011
|
+
from aria_code.ui.render.output import display_path as _display_path
|
|
2012
|
+
from aria_code.apps.cli.helpers import _chart_display_label
|
|
2013
|
+
_VALID_PERIODS = {"1m","3m","6m","1y","2y","3y","5y","ytd","max",
|
|
2014
|
+
"1mo","3mo","6mo"}
|
|
2015
|
+
parts = args.strip().split()
|
|
2016
|
+
period = "1y"
|
|
2017
|
+
symbol_parts = []
|
|
2018
|
+
for p in parts:
|
|
2019
|
+
if p.lower() in _VALID_PERIODS:
|
|
2020
|
+
period = p.lower()
|
|
2021
|
+
else:
|
|
2022
|
+
symbol_parts.append(p.strip(",,"))
|
|
2023
|
+
raw_symbols = sanitize_chart_symbol_args(symbol_parts) or ["AAPL"]
|
|
2024
|
+
symbols = []
|
|
2025
|
+
seen_symbols = set()
|
|
2026
|
+
for raw_symbol in raw_symbols:
|
|
2027
|
+
resolved = _resolve_market_arg_symbol(raw_symbol)
|
|
2028
|
+
if resolved and resolved not in seen_symbols:
|
|
2029
|
+
seen_symbols.add(resolved)
|
|
2030
|
+
symbols.append((raw_symbol, resolved))
|
|
2031
|
+
if not symbols:
|
|
2032
|
+
symbols = [("AAPL", "AAPL")]
|
|
2033
|
+
|
|
2034
|
+
if len(symbols) > 1:
|
|
2035
|
+
await self._cmd_chart_multi(symbols, period)
|
|
2036
|
+
return
|
|
2037
|
+
|
|
2038
|
+
raw_symbol, symbol = symbols[0]
|
|
2039
|
+
|
|
2040
|
+
msg = f"chart {_chart_display_label(raw_symbol, symbol)} · {period}"
|
|
2041
|
+
chart_start = time.perf_counter()
|
|
2042
|
+
if self.context.has_rich:
|
|
2043
|
+
self.context.console.print(f"\n [bold]⏺[/bold] {msg}")
|
|
2044
|
+
with self.context.console.status("[dim]fetching OHLCV and calculating indicators…[/dim]", spinner="dots"):
|
|
2045
|
+
result = await asyncio.get_event_loop().run_in_executor(
|
|
2046
|
+
None, lambda: _generate_chart_sync(symbol, period=period)
|
|
2047
|
+
)
|
|
2048
|
+
else:
|
|
2049
|
+
print(f" > {msg}")
|
|
2050
|
+
result = await asyncio.get_event_loop().run_in_executor(
|
|
2051
|
+
None, lambda: _generate_chart_sync(symbol, period=period)
|
|
2052
|
+
)
|
|
2053
|
+
elapsed_ms = int((time.perf_counter() - chart_start) * 1000)
|
|
2054
|
+
|
|
2055
|
+
if result.get("success"):
|
|
2056
|
+
path = result.get("chart_path", "")
|
|
2057
|
+
path_label = _display_path(path, fallback="chart")
|
|
2058
|
+
issues = result.get("review_issues") or []
|
|
2059
|
+
sup3 = result.get("support") or []
|
|
2060
|
+
res3 = result.get("resistance") or []
|
|
2061
|
+
rsi_val = result.get("rsi")
|
|
2062
|
+
provider = result.get("provider") or "market data"
|
|
2063
|
+
display_label = _chart_display_label(raw_symbol, symbol, result)
|
|
2064
|
+
self.terminal._pending_market_artifact = {
|
|
2065
|
+
"kind": "stock_chart",
|
|
2066
|
+
"symbol": symbol,
|
|
2067
|
+
"display": display_label,
|
|
2068
|
+
"period": period,
|
|
2069
|
+
"html_path": path,
|
|
2070
|
+
"png_path": result.get("png_path") or "",
|
|
2071
|
+
"command": f"/chart {symbol} {period}",
|
|
2072
|
+
"provider": provider,
|
|
2073
|
+
"metrics": {
|
|
2074
|
+
"trend": result.get("trend"),
|
|
2075
|
+
"rsi": rsi_val,
|
|
2076
|
+
"support": sup3,
|
|
2077
|
+
"resistance": res3,
|
|
2078
|
+
},
|
|
2079
|
+
}
|
|
2080
|
+
if self.context.has_rich:
|
|
2081
|
+
self.context.console.print(f" [green]✓[/green] chart generated [dim]({elapsed_ms}ms)[/dim]")
|
|
2082
|
+
self.context.console.print(f" saved: [link={path}]{path_label}[/link]")
|
|
2083
|
+
self.context.console.print(
|
|
2084
|
+
f" [dim]{display_label} "
|
|
2085
|
+
f"趋势: {result.get('trend','—')} "
|
|
2086
|
+
f"RSI: {f'{rsi_val:.1f}' if rsi_val else '—'} "
|
|
2087
|
+
f"支撑: {'/'.join(str(v) for v in sup3) or '—'} "
|
|
2088
|
+
f"阻力: {'/'.join(str(v) for v in res3) or '—'} "
|
|
2089
|
+
f"数据: {provider}[/dim]"
|
|
2090
|
+
)
|
|
2091
|
+
if issues:
|
|
2092
|
+
self.context.console.print(f" [yellow]⚠ 自审发现 {len(issues)} 个问题:[/yellow]")
|
|
2093
|
+
for iss in issues:
|
|
2094
|
+
self.context.console.print(f" [yellow]· {iss}[/yellow]")
|
|
2095
|
+
else:
|
|
2096
|
+
self.context.console.print(" [green dim]✓ 自审通过(数据质量正常)[/green dim]")
|
|
2097
|
+
else:
|
|
2098
|
+
print(f" OK chart generated ({elapsed_ms}ms): {path_label}")
|
|
2099
|
+
print(f" {display_label} 趋势: {result.get('trend','—')} RSI: {f'{rsi_val:.1f}' if rsi_val else '—'} 数据: {provider}")
|
|
2100
|
+
if issues:
|
|
2101
|
+
print(f" ⚠ 自审发现 {len(issues)} 个问题:")
|
|
2102
|
+
for iss in issues:
|
|
2103
|
+
print(f" · {iss}")
|
|
2104
|
+
import subprocess as _sp
|
|
2105
|
+
try:
|
|
2106
|
+
_sp.Popen(["open", path])
|
|
2107
|
+
except Exception:
|
|
2108
|
+
pass
|
|
2109
|
+
else:
|
|
2110
|
+
err = result.get("error") or result.get("response", "未知错误")
|
|
2111
|
+
print_error(self.context, f"图表生成失败: {err[:120]}")
|
|
2112
|
+
async def _cmd_chart_multi(self, symbols: list[tuple[str, str]], period: str):
|
|
2113
|
+
"""Generate a normalized comparison chart plus individual K-line charts."""
|
|
2114
|
+
from aria_code.ui.render.output import display_path as _display_path
|
|
2115
|
+
from aria_code.apps.cli.helpers import _chart_display_label
|
|
2116
|
+
labels = [_chart_display_label(raw, resolved) for raw, resolved in symbols]
|
|
2117
|
+
if self.context.has_rich:
|
|
2118
|
+
self.context.console.print(f"\n [bold]⏺[/bold] compare {' · '.join(labels)} · {period}")
|
|
2119
|
+
else:
|
|
2120
|
+
print(f" > compare {' '.join(labels)} · {period}")
|
|
2121
|
+
|
|
2122
|
+
compare_start = time.perf_counter()
|
|
2123
|
+
try:
|
|
2124
|
+
from apps.cli.handlers.chart_handlers import handle_multi_stock_comparison_direct as _multi_chart
|
|
2125
|
+
result = await asyncio.get_event_loop().run_in_executor(
|
|
2126
|
+
None, lambda: _multi_chart([resolved for _, resolved in symbols], period=period)
|
|
2127
|
+
)
|
|
2128
|
+
except Exception as exc:
|
|
2129
|
+
result = {"success": False, "error": str(exc)}
|
|
2130
|
+
elapsed_ms = int((time.perf_counter() - compare_start) * 1000)
|
|
2131
|
+
|
|
2132
|
+
if result.get("success"):
|
|
2133
|
+
path = result.get("chart_path", "")
|
|
2134
|
+
path_label = _display_path(path, fallback="comparison chart")
|
|
2135
|
+
self.terminal._pending_market_artifact = {
|
|
2136
|
+
"kind": "stock_comparison_chart",
|
|
2137
|
+
"symbol": " ".join(resolved for _, resolved in symbols),
|
|
2138
|
+
"display": " vs ".join(labels),
|
|
2139
|
+
"period": period,
|
|
2140
|
+
"html_path": path,
|
|
2141
|
+
"command": f"/chart {' '.join(resolved for _, resolved in symbols)} {period}",
|
|
2142
|
+
"metrics": result.get("metrics") or [],
|
|
2143
|
+
"children": [],
|
|
2144
|
+
}
|
|
2145
|
+
if self.context.has_rich:
|
|
2146
|
+
self.context.console.print(f" [green]✓[/green] comparison chart generated [dim]({elapsed_ms}ms)[/dim]")
|
|
2147
|
+
self.context.console.print(f" saved: [link={path}]{path_label}[/link]")
|
|
2148
|
+
rows = result.get("metrics") or []
|
|
2149
|
+
for row in rows[:6]:
|
|
2150
|
+
self.context.console.print(
|
|
2151
|
+
f" [dim]{row.get('symbol')}: "
|
|
2152
|
+
f"收益 {row.get('total_return_pct', 0):+.2f}% "
|
|
2153
|
+
f"波动 {row.get('volatility_pct', 0):.2f}% "
|
|
2154
|
+
f"最大回撤 {row.get('max_drawdown_pct', 0):.2f}%[/dim]"
|
|
2155
|
+
)
|
|
2156
|
+
else:
|
|
2157
|
+
print(f" OK comparison chart generated ({elapsed_ms}ms): {path_label}")
|
|
2158
|
+
else:
|
|
2159
|
+
print_error(self.context, f"对比图生成失败: {(result.get('error') or 'unknown')[:120]}")
|
|
2160
|
+
|
|
2161
|
+
child_artifacts = []
|
|
2162
|
+
for raw_symbol, resolved in symbols:
|
|
2163
|
+
await self.cmd_chart(f"{resolved} {period}")
|
|
2164
|
+
child = dict(getattr(self.terminal, "_pending_market_artifact", {}) or {})
|
|
2165
|
+
if child.get("html_path"):
|
|
2166
|
+
child_artifacts.append(child)
|
|
2167
|
+
if result.get("success"):
|
|
2168
|
+
self.terminal._pending_market_artifact = {
|
|
2169
|
+
"kind": "stock_comparison_chart",
|
|
2170
|
+
"symbol": " ".join(resolved for _, resolved in symbols),
|
|
2171
|
+
"display": " vs ".join(labels),
|
|
2172
|
+
"period": period,
|
|
2173
|
+
"html_path": result.get("chart_path", ""),
|
|
2174
|
+
"command": f"/chart {' '.join(resolved for _, resolved in symbols)} {period}",
|
|
2175
|
+
"metrics": result.get("metrics") or [],
|
|
2176
|
+
"children": child_artifacts,
|
|
2177
|
+
}
|
|
2178
|
+
async def cmd_shortterm(self, args: str):
|
|
2179
|
+
"""
|
|
2180
|
+
运行 A股短线分析(日线级别,3-15交易日)并输出报告。
|
|
2181
|
+
Usage: /shortterm
|
|
2182
|
+
/shortterm 000333 601138 300750
|
|
2183
|
+
"""
|
|
2184
|
+
|
|
2185
|
+
import subprocess
|
|
2186
|
+
import sys as _sys
|
|
2187
|
+
_base = pathlib.Path(__file__).parent.parent.parent / "research" / "shortterm"
|
|
2188
|
+
script = _base / "run_shortterm.py"
|
|
2189
|
+
if not script.exists():
|
|
2190
|
+
print_error(self.context, f"短线分析脚本未找到: {script}")
|
|
2191
|
+
return
|
|
2192
|
+
codes = args.strip().split()
|
|
2193
|
+
cmd = [_sys.executable, str(script)]
|
|
2194
|
+
if codes:
|
|
2195
|
+
cmd += ["--code"] + codes
|
|
2196
|
+
if self.context.has_rich:
|
|
2197
|
+
self.context.console.print("\n 📊 运行短线分析...\n")
|
|
2198
|
+
else:
|
|
2199
|
+
print("\n 📊 运行短线分析...\n")
|
|
2200
|
+
result = subprocess.run(cmd, text=True, capture_output=False)
|
|
2201
|
+
if result.returncode != 0:
|
|
2202
|
+
print_error(self.context, "短线分析执行失败,请检查 research/shortterm/")
|
|
2203
|
+
async def cmd_longterm(self, args: str):
|
|
2204
|
+
"""
|
|
2205
|
+
运行 A股长线分析(月线级别,3-18个月目标)并输出报告。
|
|
2206
|
+
Usage: /longterm
|
|
2207
|
+
/longterm --quick (只分析 core 级标的)
|
|
2208
|
+
/longterm 600519 000858
|
|
2209
|
+
"""
|
|
2210
|
+
|
|
2211
|
+
import subprocess
|
|
2212
|
+
import sys as _sys
|
|
2213
|
+
_base = pathlib.Path(__file__).parent.parent.parent / "research" / "longterm"
|
|
2214
|
+
script = _base / "run_longterm.py"
|
|
2215
|
+
if not script.exists():
|
|
2216
|
+
print_error(self.context, f"长线分析脚本未找到: {script}")
|
|
2217
|
+
return
|
|
2218
|
+
parts = args.strip().split()
|
|
2219
|
+
cmd = [_sys.executable, str(script)]
|
|
2220
|
+
if "--quick" in parts:
|
|
2221
|
+
cmd.append("--quick")
|
|
2222
|
+
parts.remove("--quick")
|
|
2223
|
+
if parts:
|
|
2224
|
+
cmd += ["--code"] + parts
|
|
2225
|
+
if self.context.has_rich:
|
|
2226
|
+
self.context.console.print("\n 📈 运行长线分析...\n")
|
|
2227
|
+
else:
|
|
2228
|
+
print("\n 📈 运行长线分析...\n")
|
|
2229
|
+
result = subprocess.run(cmd, text=True, capture_output=False)
|
|
2230
|
+
if result.returncode != 0:
|
|
2231
|
+
print_error(self.context, "长线分析执行失败,请检查 research/longterm/")
|
|
2232
|
+
async def cmd_indices(self, args: str):
|
|
2233
|
+
"""全球主要指数实时行情."""
|
|
2234
|
+
from aria_code.apps.cli._optional import HAS_MDC as _HAS_MDC, get_mdc as _get_mdc
|
|
2235
|
+
from aria_code.apps.cli.helpers import _clean_tool_error_message
|
|
2236
|
+
if not _HAS_MDC:
|
|
2237
|
+
self.context.console.print(" [dim]market_data_client 未加载[/dim]" if self.context.has_rich else "market_data_client not loaded")
|
|
2238
|
+
return
|
|
2239
|
+
mdc = _get_mdc()
|
|
2240
|
+
if self.context.has_rich:
|
|
2241
|
+
with self.context.console.status("[dim]获取全球指数...[/dim]", spinner="dots"):
|
|
2242
|
+
r = mdc.indices()
|
|
2243
|
+
else:
|
|
2244
|
+
print(" 获取全球指数...")
|
|
2245
|
+
r = mdc.indices()
|
|
2246
|
+
|
|
2247
|
+
if not r.get("success"):
|
|
2248
|
+
err = _clean_tool_error_message(r.get("error", "failed"))
|
|
2249
|
+
self.context.console.print(f" [red]{err}[/red]" if self.context.has_rich else err)
|
|
2250
|
+
return
|
|
2251
|
+
|
|
2252
|
+
if self.context.has_rich:
|
|
2253
|
+
self.context.console.print()
|
|
2254
|
+
self.context.console.print(" [bold]全球指数行情[/bold] "
|
|
2255
|
+
f"[dim]{datetime.now().strftime('%H:%M:%S')}[/dim]")
|
|
2256
|
+
self.context.console.print()
|
|
2257
|
+
for name, d in r["indices"].items():
|
|
2258
|
+
chg = d.get("change_pct", 0)
|
|
2259
|
+
color = "green" if chg >= 0 else "red"
|
|
2260
|
+
sign = "+" if chg >= 0 else ""
|
|
2261
|
+
self.context.console.print(
|
|
2262
|
+
f" [bold]{name:<14s}[/bold]"
|
|
2263
|
+
f" {str(d.get('price','')):<12}"
|
|
2264
|
+
f" [{color}]{sign}{chg:.2f}%[/{color}]"
|
|
2265
|
+
)
|
|
2266
|
+
self.context.console.print()
|
|
2267
|
+
else:
|
|
2268
|
+
for name, d in r["indices"].items():
|
|
2269
|
+
chg = d.get("change_pct", 0)
|
|
2270
|
+
sign = "+" if chg >= 0 else ""
|
|
2271
|
+
print(f" {name:<16} {str(d.get('price','')):<12} {sign}{chg:.2f}%")
|
|
2272
|
+
async def cmd_hot(self, args: str):
|
|
2273
|
+
"""热门/活跃股票榜单. Usage: /hot [cn|us] [top=20]"""
|
|
2274
|
+
from aria_code.apps.cli._optional import HAS_MDC as _HAS_MDC, get_mdc as _get_mdc
|
|
2275
|
+
if not _HAS_MDC:
|
|
2276
|
+
self.context.console.print(" [dim]market_data_client 未加载[/dim]" if self.context.has_rich else "market_data_client not loaded")
|
|
2277
|
+
return
|
|
2278
|
+
parts = args.strip().lower().split()
|
|
2279
|
+
market = "us" if "us" in parts else "cn"
|
|
2280
|
+
top_n = 20
|
|
2281
|
+
for p in parts:
|
|
2282
|
+
if p.startswith("top="):
|
|
2283
|
+
try: top_n = int(p.split("=")[1])
|
|
2284
|
+
except ValueError: pass
|
|
2285
|
+
|
|
2286
|
+
mdc = _get_mdc()
|
|
2287
|
+
if self.context.has_rich:
|
|
2288
|
+
with self.context.console.status(f"[dim]获取{market.upper()}热门股...[/dim]", spinner="dots"):
|
|
2289
|
+
r = mdc.hot_stocks(market=market, top_n=top_n)
|
|
2290
|
+
else:
|
|
2291
|
+
r = mdc.hot_stocks(market=market, top_n=top_n)
|
|
2292
|
+
|
|
2293
|
+
if not r.get("success"):
|
|
2294
|
+
self.context.console.print(f" [red]{r.get('error','failed')}[/red]" if self.context.has_rich else r.get('error'))
|
|
2295
|
+
return
|
|
2296
|
+
|
|
2297
|
+
stocks = r.get("stocks", [])
|
|
2298
|
+
if self.context.has_rich:
|
|
2299
|
+
self.context.console.print()
|
|
2300
|
+
self.context.console.print(f" [bold]{market.upper()} 热门股 Top {len(stocks)}[/bold] "
|
|
2301
|
+
f"[dim]provider: {r.get('provider','')}[/dim]")
|
|
2302
|
+
self.context.console.print()
|
|
2303
|
+
for i, s in enumerate(stocks, 1):
|
|
2304
|
+
sym = s.get("code") or s.get("symbol","")
|
|
2305
|
+
name = s.get("name", sym)
|
|
2306
|
+
p = s.get("price", "-")
|
|
2307
|
+
chg = s.get("change_pct", 0)
|
|
2308
|
+
color = "green" if chg >= 0 else "red"
|
|
2309
|
+
sign = "+" if chg >= 0 else ""
|
|
2310
|
+
self.context.console.print(
|
|
2311
|
+
f" [dim]{i:2d}.[/dim] [bold]{name:<8s}[/bold] "
|
|
2312
|
+
f"[dim]{sym:<8s}[/dim] {str(p):<8} "
|
|
2313
|
+
f"[{color}]{sign}{chg:.2f}%[/{color}]"
|
|
2314
|
+
)
|
|
2315
|
+
self.context.console.print()
|
|
2316
|
+
else:
|
|
2317
|
+
for s in stocks:
|
|
2318
|
+
sym = s.get("code") or s.get("symbol","")
|
|
2319
|
+
print(f" {s.get('name',sym):<10} {sym:<8} {s.get('price','-'):<8} {s.get('change_pct',0):+.2f}%")
|
|
2320
|
+
async def cmd_ta(self, args: str):
|
|
2321
|
+
"""技术指标分析. Usage: /ta NVDA [days=120]"""
|
|
2322
|
+
from aria_cli import _generate_chart_sync, parse_technical_args, print_ta_result
|
|
2323
|
+
from aria_code.ui.render.output import display_path as _display_path
|
|
2324
|
+
from aria_code.apps.cli.helpers import _chart_period_from_ta_days, _display_value
|
|
2325
|
+
parsed = parse_technical_args(args)
|
|
2326
|
+
symbol = parsed.symbol
|
|
2327
|
+
days = parsed.days
|
|
2328
|
+
|
|
2329
|
+
service_result = None
|
|
2330
|
+
if self.context.has_rich:
|
|
2331
|
+
with self.context.console.status(f"[dim]计算 {symbol} 技术指标...[/dim]", spinner="dots"):
|
|
2332
|
+
from packages.aria_services.data import DataService
|
|
2333
|
+
service_result = DataService().technical_indicators(symbol, days=days)
|
|
2334
|
+
else:
|
|
2335
|
+
from packages.aria_services.data import DataService
|
|
2336
|
+
service_result = DataService().technical_indicators(symbol, days=days)
|
|
2337
|
+
if not service_result or not service_result.success:
|
|
2338
|
+
_ta_warns = (service_result.warnings or []) if service_result else []
|
|
2339
|
+
_ta_errs = (service_result.errors or []) if service_result else []
|
|
2340
|
+
_ta_data = (service_result.data or {}) if service_result else {}
|
|
2341
|
+
_missing = ", ".join(service_result.missing_fields) if service_result else ""
|
|
2342
|
+
_all_msgs = " ".join(_ta_warns + _ta_errs).lower()
|
|
2343
|
+
# Show current price when we have partial data (e.g. new IPO with 1 bar)
|
|
2344
|
+
_price_line = ""
|
|
2345
|
+
if _ta_data.get("price"):
|
|
2346
|
+
_price_line = f" 当前价格 [bold]{_display_value(_ta_data['price'])}[/bold]"
|
|
2347
|
+
if _ta_data.get("history_bars"):
|
|
2348
|
+
_price_line += f" [dim]({_ta_data['history_bars']} 个交易日数据)[/dim]"
|
|
2349
|
+
_price_line += "\n"
|
|
2350
|
+
if "数据不足" in _all_msgs or "新上市" in _all_msgs:
|
|
2351
|
+
_reason = f"[yellow]历史数据不足[/yellow] — {symbol} 上市时间较短(< 14 个交易日),TA 指标无法计算\n [dim]可待更多交易日积累后重试,或运行 `/analyze {symbol}` 查看基本面[/dim]"
|
|
2352
|
+
elif "rate" in _all_msgs or "429" in _all_msgs or "too many" in _all_msgs:
|
|
2353
|
+
_reason = "[yellow]数据源频率限制[/yellow] — 稍后重试,或用 `/apikey set finnhub <KEY>` 切换数据源"
|
|
2354
|
+
else:
|
|
2355
|
+
_err = "; ".join(_ta_errs or _ta_warns) or "数据源暂时不可用"
|
|
2356
|
+
_reason = f"[red]{_err[:120]}[/red]"
|
|
2357
|
+
if _missing:
|
|
2358
|
+
_reason += f" [dim]missing: {_missing}[/dim]"
|
|
2359
|
+
if self.context.has_rich:
|
|
2360
|
+
if _price_line:
|
|
2361
|
+
self.context.console.print(f"\n{_price_line}")
|
|
2362
|
+
self.context.console.print(f" {_reason}\n")
|
|
2363
|
+
else:
|
|
2364
|
+
import re as _re
|
|
2365
|
+
print(f"\n {_re.sub(r'[[/].*?]', '', _price_line + _reason)}\n")
|
|
2366
|
+
return
|
|
2367
|
+
|
|
2368
|
+
print_ta_result(
|
|
2369
|
+
console=self.context.console,
|
|
2370
|
+
has_rich=self.context.has_rich,
|
|
2371
|
+
symbol=symbol,
|
|
2372
|
+
days=days,
|
|
2373
|
+
service_result=service_result,
|
|
2374
|
+
formatter=_display_value,
|
|
2375
|
+
)
|
|
2376
|
+
|
|
2377
|
+
period = _chart_period_from_ta_days(days)
|
|
2378
|
+
self.terminal._pending_market_artifact = {
|
|
2379
|
+
"kind": "ta_chart",
|
|
2380
|
+
"symbol": symbol,
|
|
2381
|
+
"period": period,
|
|
2382
|
+
"command": f"/chart {symbol} {period}",
|
|
2383
|
+
}
|
|
2384
|
+
chart_result = None
|
|
2385
|
+
if self.context.has_rich:
|
|
2386
|
+
with self.context.console.status(f"[dim]生成 {symbol} 技术图表 HTML/PNG...[/dim]", spinner="dots"):
|
|
2387
|
+
chart_result = await asyncio.get_event_loop().run_in_executor(
|
|
2388
|
+
None, lambda: _generate_chart_sync(symbol, period=period)
|
|
2389
|
+
)
|
|
2390
|
+
else:
|
|
2391
|
+
print(f" 生成 {symbol} 技术图表 HTML/PNG...")
|
|
2392
|
+
chart_result = await asyncio.get_event_loop().run_in_executor(
|
|
2393
|
+
None, lambda: _generate_chart_sync(symbol, period=period)
|
|
2394
|
+
)
|
|
2395
|
+
|
|
2396
|
+
if chart_result and chart_result.get("success"):
|
|
2397
|
+
html_path = chart_result.get("chart_path") or ""
|
|
2398
|
+
png_path = chart_result.get("png_path") or ""
|
|
2399
|
+
png_error = chart_result.get("png_error") or ""
|
|
2400
|
+
self.terminal._pending_market_artifact = {
|
|
2401
|
+
"kind": "ta_chart",
|
|
2402
|
+
"symbol": symbol,
|
|
2403
|
+
"period": period,
|
|
2404
|
+
"html_path": html_path,
|
|
2405
|
+
"png_path": png_path,
|
|
2406
|
+
"command": f"/chart {symbol} {period}",
|
|
2407
|
+
}
|
|
2408
|
+
if self.context.has_rich:
|
|
2409
|
+
self.context.console.print()
|
|
2410
|
+
self.context.console.print(" [green]✓[/green] 技术图表已生成")
|
|
2411
|
+
if html_path:
|
|
2412
|
+
self.context.console.print(f" [dim]HTML:[/dim] [link={html_path}]{_display_path(html_path)}[/link]")
|
|
2413
|
+
if png_path:
|
|
2414
|
+
self.context.console.print(f" [dim]PNG :[/dim] [link={png_path}]{_display_path(png_path)}[/link]")
|
|
2415
|
+
elif png_error:
|
|
2416
|
+
self.context.console.print(f" [yellow]PNG 跳过:[/yellow] {png_error[:90]}")
|
|
2417
|
+
else:
|
|
2418
|
+
print("\n 技术图表已生成")
|
|
2419
|
+
if html_path:
|
|
2420
|
+
print(f" HTML: {_display_path(html_path)}")
|
|
2421
|
+
if png_path:
|
|
2422
|
+
print(f" PNG : {_display_path(png_path)}")
|
|
2423
|
+
elif png_error:
|
|
2424
|
+
print(f" PNG 跳过: {png_error[:90]}")
|
|
2425
|
+
elif chart_result:
|
|
2426
|
+
err = chart_result.get("error") or "图表生成失败"
|
|
2427
|
+
if self.context.has_rich:
|
|
2428
|
+
self.context.console.print(f" [yellow]图表生成跳过:[/yellow] {err[:120]}")
|
|
2429
|
+
else:
|
|
2430
|
+
print(f" 图表生成跳过: {err[:120]}")
|
|
2431
|
+
def _extract_last_code(self) -> str:
|
|
2432
|
+
"""从对话历史中提取最后一段 Python 代码块."""
|
|
2433
|
+
import re
|
|
2434
|
+
for msg in reversed(self.terminal.conversation):
|
|
2435
|
+
content = msg.get("content", "")
|
|
2436
|
+
# Match ```python ... ``` blocks
|
|
2437
|
+
matches = re.findall(r"```(?:python)?\n(.*?)```", content, re.DOTALL)
|
|
2438
|
+
if matches:
|
|
2439
|
+
# Return the longest code block
|
|
2440
|
+
return max(matches, key=len)
|
|
2441
|
+
return ""
|
|
2442
|
+
async def cmd_orcl(self, args: str):
|
|
2443
|
+
"""Oracle Corporation (ORCL) analysis."""
|
|
2444
|
+
deep = "deep" in args.lower()
|
|
2445
|
+
if deep:
|
|
2446
|
+
prompt = (
|
|
2447
|
+
"Perform a comprehensive multi-factor analysis of Oracle Corporation (ORCL):\n"
|
|
2448
|
+
"1. Technical: trend, RSI, MACD, key support/resistance levels\n"
|
|
2449
|
+
"2. Fundamental: revenue growth, cloud transition progress, margins, PE vs peers (MSFT, SAP, NOW)\n"
|
|
2450
|
+
"3. Competitive: OCI vs AWS/Azure/GCP market share, Autonomous DB moat\n"
|
|
2451
|
+
"4. AI angle: Oracle's AI infrastructure deals (NVIDIA partnership, xAI, OpenAI cloud)\n"
|
|
2452
|
+
"5. Risks: debt load from cloud capex, Cerner integration, FX exposure\n"
|
|
2453
|
+
"6. Verdict: Bull/Bear/Neutral with price target and conviction level"
|
|
2454
|
+
)
|
|
2455
|
+
else:
|
|
2456
|
+
prompt = (
|
|
2457
|
+
"Give me a quick snapshot of Oracle (ORCL):\n"
|
|
2458
|
+
"1. Current price, YTD performance vs S&P500\n"
|
|
2459
|
+
"2. Key metrics: PE, forward PE, revenue growth, cloud ARR\n"
|
|
2460
|
+
"3. Recent news and catalysts\n"
|
|
2461
|
+
"4. Technical signal: Buy/Hold/Sell\n"
|
|
2462
|
+
"5. One-line thesis"
|
|
2463
|
+
)
|
|
2464
|
+
await self.terminal._handle_ai_message(prompt)
|
|
2465
|
+
async def cmd_edgar(self, args: str):
|
|
2466
|
+
"""
|
|
2467
|
+
SEC EDGAR 美国上市公司财报与披露查询(完全免费)。
|
|
2468
|
+
Usage: /edgar AAPL — 最近财报列表
|
|
2469
|
+
/edgar MSFT filings — 10-K/10-Q 提交记录
|
|
2470
|
+
/edgar TSLA facts — 财务事实(收入/利润历史)
|
|
2471
|
+
/edgar NVDA insider — 内幕交易披露 (Form 4)
|
|
2472
|
+
"""
|
|
2473
|
+
from aria_cli import _fetch_edgar_data
|
|
2474
|
+
parts = args.strip().split()
|
|
2475
|
+
if not parts:
|
|
2476
|
+
self.context.console.print(" [dim]Usage: /edgar SYMBOL [filings|facts|insider][/dim]" if self.context.has_rich
|
|
2477
|
+
else "Usage: /edgar SYMBOL [filings|facts|insider]")
|
|
2478
|
+
return
|
|
2479
|
+
|
|
2480
|
+
symbol = parts[0].upper()
|
|
2481
|
+
sub = parts[1].lower() if len(parts) > 1 else "filings"
|
|
2482
|
+
|
|
2483
|
+
if self.context.has_rich:
|
|
2484
|
+
with self.context.console.status(f"[dim]查询 EDGAR {symbol}...[/dim]", spinner="dots"):
|
|
2485
|
+
result = await asyncio.get_event_loop().run_in_executor(
|
|
2486
|
+
None, lambda: _fetch_edgar_data(symbol, sub)
|
|
2487
|
+
)
|
|
2488
|
+
else:
|
|
2489
|
+
result = await asyncio.get_event_loop().run_in_executor(
|
|
2490
|
+
None, lambda: _fetch_edgar_data(symbol, sub)
|
|
2491
|
+
)
|
|
2492
|
+
|
|
2493
|
+
if not result:
|
|
2494
|
+
print_error(self.context, f"未找到 {symbol} 的 EDGAR 数据")
|
|
2495
|
+
return
|
|
2496
|
+
|
|
2497
|
+
if self.context.has_rich:
|
|
2498
|
+
from rich.table import Table
|
|
2499
|
+
from rich import box as rich_box
|
|
2500
|
+
if sub == "filings":
|
|
2501
|
+
table = Table(title=f"[bold]{symbol}[/bold] SEC 财报提交",
|
|
2502
|
+
box=rich_box.SIMPLE, header_style="bold dim")
|
|
2503
|
+
table.add_column("类型", width=6)
|
|
2504
|
+
table.add_column("日期", width=12)
|
|
2505
|
+
table.add_column("链接", style="dim")
|
|
2506
|
+
for f in result[:10]:
|
|
2507
|
+
table.add_row(f.get("form",""), f.get("date",""), f.get("url","")[:60])
|
|
2508
|
+
self.context.console.print(table)
|
|
2509
|
+
elif sub == "facts":
|
|
2510
|
+
self.context.console.print(f" [bold]{symbol}[/bold] 财务摘要:")
|
|
2511
|
+
for metric, entries in result.get("metrics", {}).items():
|
|
2512
|
+
if entries:
|
|
2513
|
+
latest = entries[0]
|
|
2514
|
+
self.context.console.print(f" [dim]{metric}[/dim] {latest.get('val',0):,.0f} ({latest.get('end','')})")
|
|
2515
|
+
elif sub == "insider":
|
|
2516
|
+
self.context.console.print(f" [bold]{symbol}[/bold] 近期内幕交易 ({len(result)} 条):")
|
|
2517
|
+
for f in result[:10]:
|
|
2518
|
+
self.context.console.print(f" [dim]{f.get('date','')}[/dim] Form 4")
|
|
2519
|
+
else:
|
|
2520
|
+
print(f" {symbol} EDGAR 数据: {len(result) if isinstance(result, list) else 'OK'}")
|