aria-code 0.52.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. aria_code/__init__.py +42 -0
  2. aria_code/_version.py +20 -0
  3. aria_code/adk_apps/__init__.py +1 -0
  4. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  5. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  6. aria_code/adk_apps/aria_research/__init__.py +1 -0
  7. aria_code/adk_apps/aria_research/agent.py +31 -0
  8. aria_code/agents/__init__.py +34 -0
  9. aria_code/agents/base.py +318 -0
  10. aria_code/agents/code_review.py +200 -0
  11. aria_code/agents/deep/__init__.py +37 -0
  12. aria_code/agents/deep/calibration_loop.py +144 -0
  13. aria_code/agents/deep/critic.py +125 -0
  14. aria_code/agents/deep/deepen.py +193 -0
  15. aria_code/agents/deep/models.py +149 -0
  16. aria_code/agents/deep/pipeline.py +164 -0
  17. aria_code/agents/deep/quant_fusion.py +192 -0
  18. aria_code/agents/deep/themes.py +95 -0
  19. aria_code/agents/deep/tiers.py +106 -0
  20. aria_code/agents/engineering/__init__.py +14 -0
  21. aria_code/agents/engineering/coder.py +222 -0
  22. aria_code/agents/engineering/tester.py +145 -0
  23. aria_code/agents/financial/__init__.py +21 -0
  24. aria_code/agents/financial/cashflow_burn.py +119 -0
  25. aria_code/agents/financial/catalyst.py +279 -0
  26. aria_code/agents/financial/corporate_finance.py +175 -0
  27. aria_code/agents/financial/debate.py +224 -0
  28. aria_code/agents/financial/earnings.py +303 -0
  29. aria_code/agents/financial/fundamental.py +191 -0
  30. aria_code/agents/financial/macro.py +99 -0
  31. aria_code/agents/financial/news.py +207 -0
  32. aria_code/agents/financial/risk.py +165 -0
  33. aria_code/agents/financial/sector.py +279 -0
  34. aria_code/agents/financial/strategist.py +121 -0
  35. aria_code/agents/financial/stripe_revenue.py +127 -0
  36. aria_code/agents/financial/synthesis.py +274 -0
  37. aria_code/agents/financial/technical.py +258 -0
  38. aria_code/agents/orchestrator.py +44 -0
  39. aria_code/agents/portfolio_agent.py +363 -0
  40. aria_code/agents/realty/__init__.py +62 -0
  41. aria_code/agents/realty/asset_diagnosis.py +152 -0
  42. aria_code/agents/realty/business_match.py +166 -0
  43. aria_code/agents/realty/cashflow_verify.py +209 -0
  44. aria_code/agents/realty/contract_rules.py +210 -0
  45. aria_code/agents/realty/energy_anomaly.py +189 -0
  46. aria_code/agents/realty/exit_settlement.py +208 -0
  47. aria_code/agents/realty/fulfillment_risk.py +206 -0
  48. aria_code/agents/realty/ops_optimize.py +160 -0
  49. aria_code/agents/realty/revenue_share.py +215 -0
  50. aria_code/agents/registry.py +158 -0
  51. aria_code/agents/signal_scheme.py +111 -0
  52. aria_code/agents/sports/__init__.py +0 -0
  53. aria_code/agents/sports/football_agent.py +197 -0
  54. aria_code/agents/supervisor.py +113 -0
  55. aria_code/agents/team.py +492 -0
  56. aria_code/agents/warehouse/__init__.py +25 -0
  57. aria_code/agents/warehouse/contracts.py +26 -0
  58. aria_code/agents/warehouse/dashboard.py +155 -0
  59. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  60. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  61. aria_code/agents/warehouse/inventory_health.py +24 -0
  62. aria_code/agents/warehouse/local_run.py +71 -0
  63. aria_code/agents/warehouse/local_snapshot.py +132 -0
  64. aria_code/agents/warehouse/logistics_cost.py +84 -0
  65. aria_code/agents/warehouse/logistics_sync.py +21 -0
  66. aria_code/agents/warehouse/workflow.py +31 -0
  67. aria_code/aliyun_data_client.py +4 -0
  68. aria_code/apps/README.md +12 -0
  69. aria_code/apps/__init__.py +2 -0
  70. aria_code/apps/channels/README.md +15 -0
  71. aria_code/apps/channels/__init__.py +17 -0
  72. aria_code/apps/channels/intake.py +92 -0
  73. aria_code/apps/channels/registry.py +95 -0
  74. aria_code/apps/channels/tradingview.py +94 -0
  75. aria_code/apps/cli/README.md +13 -0
  76. aria_code/apps/cli/__init__.py +2 -0
  77. aria_code/apps/cli/_optional.py +120 -0
  78. aria_code/apps/cli/bootstrap.py +145 -0
  79. aria_code/apps/cli/broker_render.py +206 -0
  80. aria_code/apps/cli/codegen_paths.py +29 -0
  81. aria_code/apps/cli/commands/__init__.py +16 -0
  82. aria_code/apps/cli/commands/_business_render.py +78 -0
  83. aria_code/apps/cli/commands/_ui.py +71 -0
  84. aria_code/apps/cli/commands/analysis_cmds.py +369 -0
  85. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  86. aria_code/apps/cli/commands/auth_cmds.py +239 -0
  87. aria_code/apps/cli/commands/backtest_cmds.py +2543 -0
  88. aria_code/apps/cli/commands/broker_cmds.py +1234 -0
  89. aria_code/apps/cli/commands/business_workflow_cmds.py +319 -0
  90. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  91. aria_code/apps/cli/commands/catalog.py +87 -0
  92. aria_code/apps/cli/commands/core_cmds.py +2520 -0
  93. aria_code/apps/cli/commands/data_cmds.py +457 -0
  94. aria_code/apps/cli/commands/diagnostic_cmds.py +230 -0
  95. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  96. aria_code/apps/cli/commands/file_cmds.py +312 -0
  97. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  98. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  99. aria_code/apps/cli/commands/market.py +434 -0
  100. aria_code/apps/cli/commands/market_cmds.py +1381 -0
  101. aria_code/apps/cli/commands/market_context.py +425 -0
  102. aria_code/apps/cli/commands/market_render.py +7 -0
  103. aria_code/apps/cli/commands/model_cmds.py +2153 -0
  104. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  105. aria_code/apps/cli/commands/orchestrator_cmds.py +179 -0
  106. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  107. aria_code/apps/cli/commands/portfolio_cmds.py +1278 -0
  108. aria_code/apps/cli/commands/report.py +437 -0
  109. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  110. aria_code/apps/cli/commands/session_cmds.py +236 -0
  111. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  112. aria_code/apps/cli/commands/team.py +669 -0
  113. aria_code/apps/cli/commands/team_render.py +8 -0
  114. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  115. aria_code/apps/cli/commands/warehouse_cmds.py +79 -0
  116. aria_code/apps/cli/commands/workflow_cmds.py +480 -0
  117. aria_code/apps/cli/commands/workspace_cmds.py +1494 -0
  118. aria_code/apps/cli/config_paths.py +73 -0
  119. aria_code/apps/cli/config_store.py +78 -0
  120. aria_code/apps/cli/constants.py +88 -0
  121. aria_code/apps/cli/context.py +34 -0
  122. aria_code/apps/cli/deterministic.py +191 -0
  123. aria_code/apps/cli/direct.py +48 -0
  124. aria_code/apps/cli/finance_service_catalog.py +111 -0
  125. aria_code/apps/cli/football_reports.py +231 -0
  126. aria_code/apps/cli/github_app_auth.py +135 -0
  127. aria_code/apps/cli/google_login.py +196 -0
  128. aria_code/apps/cli/handlers/__init__.py +11 -0
  129. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  130. aria_code/apps/cli/handlers/chart_handlers.py +1309 -0
  131. aria_code/apps/cli/handlers/market_handlers.py +2513 -0
  132. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  133. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  134. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  135. aria_code/apps/cli/helpers.py +302 -0
  136. aria_code/apps/cli/hooks.py +180 -0
  137. aria_code/apps/cli/i18n.py +297 -0
  138. aria_code/apps/cli/intent.py +136 -0
  139. aria_code/apps/cli/intent_router.py +276 -0
  140. aria_code/apps/cli/intent_signals.py +344 -0
  141. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  142. aria_code/apps/cli/local_tool_schemas.py +536 -0
  143. aria_code/apps/cli/main.py +29 -0
  144. aria_code/apps/cli/market_metadata.py +135 -0
  145. aria_code/apps/cli/market_universe.py +487 -0
  146. aria_code/apps/cli/message_processing.py +310 -0
  147. aria_code/apps/cli/model_catalog.py +620 -0
  148. aria_code/apps/cli/pdf_report.py +977 -0
  149. aria_code/apps/cli/plan_mode.py +139 -0
  150. aria_code/apps/cli/plotly_html.py +15 -0
  151. aria_code/apps/cli/prediction_feedback.py +202 -0
  152. aria_code/apps/cli/preflight.py +502 -0
  153. aria_code/apps/cli/project_aria.py +60 -0
  154. aria_code/apps/cli/prompt_assembly.py +59 -0
  155. aria_code/apps/cli/prompts/__init__.py +0 -0
  156. aria_code/apps/cli/prompts/coding.py +53 -0
  157. aria_code/apps/cli/prompts/select.py +142 -0
  158. aria_code/apps/cli/prompts/system_prompts.py +552 -0
  159. aria_code/apps/cli/prompts/ui.py +436 -0
  160. aria_code/apps/cli/provider_endpoints.py +237 -0
  161. aria_code/apps/cli/provider_keys.py +73 -0
  162. aria_code/apps/cli/providers/__init__.py +1 -0
  163. aria_code/apps/cli/providers/base.py +521 -0
  164. aria_code/apps/cli/providers/chat_routing.py +128 -0
  165. aria_code/apps/cli/providers/collaboration.py +98 -0
  166. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  167. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  168. aria_code/apps/cli/providers/llm/ollama_stream.py +1594 -0
  169. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  170. aria_code/apps/cli/providers/llm/sse_stream.py +257 -0
  171. aria_code/apps/cli/providers/runtime_bridge.py +353 -0
  172. aria_code/apps/cli/providers/vertexai_stream.py +316 -0
  173. aria_code/apps/cli/runtime_consumer.py +657 -0
  174. aria_code/apps/cli/sandbox.py +32 -0
  175. aria_code/apps/cli/session_export.py +120 -0
  176. aria_code/apps/cli/session_jsonl.py +208 -0
  177. aria_code/apps/cli/session_store.py +112 -0
  178. aria_code/apps/cli/skills_catalog.py +273 -0
  179. aria_code/apps/cli/todo_tracker.py +190 -0
  180. aria_code/apps/cli/tool_executor.py +823 -0
  181. aria_code/apps/cli/tool_registry.py +34 -0
  182. aria_code/apps/cli/tool_scope.py +106 -0
  183. aria_code/apps/cli/tools/__init__.py +40 -0
  184. aria_code/apps/cli/tools/context.py +46 -0
  185. aria_code/apps/cli/tools/file_tools.py +180 -0
  186. aria_code/apps/cli/tools/market_tools.py +620 -0
  187. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  188. aria_code/apps/cli/tools/system_tools.py +726 -0
  189. aria_code/apps/cli/tools/write_tools.py +845 -0
  190. aria_code/apps/cli/tradingview_bridge.py +529 -0
  191. aria_code/apps/cli/turn_planning.py +137 -0
  192. aria_code/apps/cli/update_check.py +153 -0
  193. aria_code/apps/cli/utils/__init__.py +0 -0
  194. aria_code/apps/cli/utils/market_detect.py +1597 -0
  195. aria_code/apps/daemon/README.md +14 -0
  196. aria_code/apps/vscode/README.md +115 -0
  197. aria_code/apps/vscode/package.json +70 -0
  198. aria_code/aria_cli.py +6743 -0
  199. aria_code/aria_daemon.py +1398 -0
  200. aria_code/aria_feishu_bot.py +1423 -0
  201. aria_code/aria_mcp_server.py +7 -0
  202. aria_code/aria_relay_client.py +4 -0
  203. aria_code/aria_relay_server.py +490 -0
  204. aria_code/aria_telegram_bot.py +202 -0
  205. aria_code/ariarc.py +385 -0
  206. aria_code/artifacts.py +581 -0
  207. aria_code/backtest_engine.py +4 -0
  208. aria_code/backtest_report.py +4 -0
  209. aria_code/brokers/__init__.py +78 -0
  210. aria_code/brokers/automation.py +193 -0
  211. aria_code/brokers/base.py +231 -0
  212. aria_code/brokers/capabilities.py +264 -0
  213. aria_code/brokers/cn/__init__.py +10 -0
  214. aria_code/brokers/cn/easytrader_broker.py +193 -0
  215. aria_code/brokers/cn/futu_broker.py +194 -0
  216. aria_code/brokers/cn/longbridge_broker.py +190 -0
  217. aria_code/brokers/cn/tiger_broker.py +196 -0
  218. aria_code/brokers/cn/xtquant_broker.py +225 -0
  219. aria_code/brokers/config.py +414 -0
  220. aria_code/brokers/intl/__init__.py +5 -0
  221. aria_code/brokers/intl/alpaca_broker.py +183 -0
  222. aria_code/brokers/intl/ibkr_broker.py +215 -0
  223. aria_code/brokers/intl/trading212_broker.py +183 -0
  224. aria_code/brokers/intl/webull_broker.py +156 -0
  225. aria_code/brokers/paper_broker.py +266 -0
  226. aria_code/brokers/planning.py +296 -0
  227. aria_code/brokers/registry.py +213 -0
  228. aria_code/brokers/trading.py +267 -0
  229. aria_code/canva_client.py +4 -0
  230. aria_code/change_store.py +127 -0
  231. aria_code/clients/__init__.py +0 -0
  232. aria_code/clients/aliyun_data_client.py +660 -0
  233. aria_code/clients/aria_relay_client.py +182 -0
  234. aria_code/clients/canva_client.py +284 -0
  235. aria_code/clients/figma_client.py +100 -0
  236. aria_code/clients/football_data_client.py +1671 -0
  237. aria_code/clients/kling_video_client.py +182 -0
  238. aria_code/clients/market_data_client.py +1976 -0
  239. aria_code/clients/openai_image_client.py +242 -0
  240. aria_code/clients/runway_video_client.py +157 -0
  241. aria_code/clients/warehouse_erp_client.py +105 -0
  242. aria_code/cloud_memory.py +181 -0
  243. aria_code/command_safety.py +19 -0
  244. aria_code/computer_use_tools.py +4 -0
  245. aria_code/dashboard_generator.py +579 -0
  246. aria_code/data_analysis_tools.py +4 -0
  247. aria_code/data_cleaner.py +483 -0
  248. aria_code/data_service.py +513 -0
  249. aria_code/datasources/__init__.py +23 -0
  250. aria_code/datasources/base.py +166 -0
  251. aria_code/datasources/router.py +253 -0
  252. aria_code/datasources/sources/__init__.py +15 -0
  253. aria_code/datasources/sources/akshare_source.py +269 -0
  254. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  255. aria_code/datasources/sources/edgar_source.py +219 -0
  256. aria_code/datasources/sources/finnhub_source.py +198 -0
  257. aria_code/datasources/sources/fred_source.py +223 -0
  258. aria_code/datasources/sources/tushare_source.py +143 -0
  259. aria_code/datasources/sources/web_scraper_source.py +278 -0
  260. aria_code/datasources/sources/world_bank_source.py +206 -0
  261. aria_code/datasources/sources/yfinance_source.py +152 -0
  262. aria_code/demo_player.py +207 -0
  263. aria_code/doctor.py +813 -0
  264. aria_code/domain/__init__.py +0 -0
  265. aria_code/domain/backtest_engine.py +354 -0
  266. aria_code/domain/backtest_report.py +472 -0
  267. aria_code/domain/finance_formulas.py +389 -0
  268. aria_code/domain/portfolio_ledger.py +307 -0
  269. aria_code/domain/strategy_vault.py +421 -0
  270. aria_code/evals/__init__.py +37 -0
  271. aria_code/evals/harness.py +598 -0
  272. aria_code/evals/runner.py +238 -0
  273. aria_code/external_agent_runner.py +115 -0
  274. aria_code/figma_client.py +4 -0
  275. aria_code/file_analysis_tools.py +4 -0
  276. aria_code/finance_formulas.py +4 -0
  277. aria_code/football_data_client.py +4 -0
  278. aria_code/image_gen_tools.py +4 -0
  279. aria_code/intent_classifier.py +386 -0
  280. aria_code/kling_video_client.py +4 -0
  281. aria_code/licensing.py +151 -0
  282. aria_code/local_finance_tools.py +4 -0
  283. aria_code/local_image_provider.py +4 -0
  284. aria_code/local_llm_provider.py +4 -0
  285. aria_code/macro_tools.py +4 -0
  286. aria_code/markdown_pdf.py +289 -0
  287. aria_code/market_data_client.py +4 -0
  288. aria_code/mcp_client.py +639 -0
  289. aria_code/memory_manager.py +246 -0
  290. aria_code/model_capability.py +486 -0
  291. aria_code/notification_tools.py +4 -0
  292. aria_code/openai_image_client.py +4 -0
  293. aria_code/packages/__init__.py +23 -0
  294. aria_code/packages/adk_bridge/__init__.py +11 -0
  295. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  296. aria_code/packages/adk_bridge/market_tools.py +125 -0
  297. aria_code/packages/aria_agents/__init__.py +5 -0
  298. aria_code/packages/aria_agents/manifest.py +78 -0
  299. aria_code/packages/aria_core/__init__.py +34 -0
  300. aria_code/packages/aria_core/architecture.py +269 -0
  301. aria_code/packages/aria_core/export.py +147 -0
  302. aria_code/packages/aria_core/manifest.py +65 -0
  303. aria_code/packages/aria_core/paths.py +57 -0
  304. aria_code/packages/aria_core/secure_file.py +68 -0
  305. aria_code/packages/aria_infra/__init__.py +15 -0
  306. aria_code/packages/aria_infra/arthera.py +52 -0
  307. aria_code/packages/aria_infra/doctor.py +246 -0
  308. aria_code/packages/aria_infra/product.py +40 -0
  309. aria_code/packages/aria_mcp/__init__.py +25 -0
  310. aria_code/packages/aria_mcp/bridge.py +201 -0
  311. aria_code/packages/aria_mcp/circuit.py +82 -0
  312. aria_code/packages/aria_mcp/config.py +111 -0
  313. aria_code/packages/aria_mcp/installer.py +95 -0
  314. aria_code/packages/aria_mcp/server.py +1550 -0
  315. aria_code/packages/aria_mcp/tools.py +81 -0
  316. aria_code/packages/aria_sdk/__init__.py +19 -0
  317. aria_code/packages/aria_sdk/client.py +396 -0
  318. aria_code/packages/aria_sdk/providers.py +70 -0
  319. aria_code/packages/aria_sdk/streaming.py +79 -0
  320. aria_code/packages/aria_sdk/types.py +86 -0
  321. aria_code/packages/aria_services/__init__.py +92 -0
  322. aria_code/packages/aria_services/cache.py +60 -0
  323. aria_code/packages/aria_services/context.py +356 -0
  324. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  325. aria_code/packages/aria_services/data.py +11 -0
  326. aria_code/packages/aria_services/provider_health.py +268 -0
  327. aria_code/packages/aria_services/references.py +247 -0
  328. aria_code/packages/aria_services/registry.py +249 -0
  329. aria_code/packages/aria_services/research_protocol.py +57 -0
  330. aria_code/packages/aria_services/research_quality.py +186 -0
  331. aria_code/packages/aria_services/settings.py +157 -0
  332. aria_code/packages/aria_services/usage.py +151 -0
  333. aria_code/packages/aria_skills/__init__.py +46 -0
  334. aria_code/packages/aria_skills/catalog.py +149 -0
  335. aria_code/packages/aria_skills/loader.py +455 -0
  336. aria_code/packages/aria_skills/registry.py +84 -0
  337. aria_code/packages/aria_tools/__init__.py +5 -0
  338. aria_code/packages/aria_tools/financial/__init__.py +8 -0
  339. aria_code/packages/aria_tools/financial/_paths.py +130 -0
  340. aria_code/packages/aria_tools/financial/compliance.py +31 -0
  341. aria_code/packages/aria_tools/financial/factors.py +33 -0
  342. aria_code/packages/aria_tools/financial/risk_tools.py +33 -0
  343. aria_code/packages/aria_tools/financial/strategy.py +31 -0
  344. aria_code/packages/aria_tools/registry.py +128 -0
  345. aria_code/packages/quant_engine/__init__.py +34 -0
  346. aria_code/packages/quant_engine/_namespace.py +62 -0
  347. aria_code/packages/quant_engine/backtest/__init__.py +14 -0
  348. aria_code/packages/quant_engine/backtest/core.py +53 -0
  349. aria_code/packages/quant_engine/backtest/engine.py +96 -0
  350. aria_code/packages/quant_engine/portfolio/__init__.py +13 -0
  351. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  352. aria_code/packages/quant_engine/sports/__init__.py +80 -0
  353. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  354. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  355. aria_code/packages/quant_engine/sports/elo.py +300 -0
  356. aria_code/packages/quant_engine/sports/form.py +188 -0
  357. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  358. aria_code/packages/quant_engine/sports/ml_model.py +377 -0
  359. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  360. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  361. aria_code/packages/quant_engine/stochastic/__init__.py +35 -0
  362. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  363. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  364. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  365. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  366. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  367. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  368. aria_code/packs/__init__.py +85 -0
  369. aria_code/packs/base.py +168 -0
  370. aria_code/packs/finance/__init__.py +250 -0
  371. aria_code/packs/logistics/__init__.py +169 -0
  372. aria_code/packs/payments/__init__.py +139 -0
  373. aria_code/packs/realty/__init__.py +190 -0
  374. aria_code/packs/registry.py +202 -0
  375. aria_code/packs/rules.py +90 -0
  376. aria_code/plan_utils.py +194 -0
  377. aria_code/plugin_loader.py +328 -0
  378. aria_code/portfolio_ledger.py +4 -0
  379. aria_code/preview_server.py +449 -0
  380. aria_code/privacy/__init__.py +5 -0
  381. aria_code/privacy/feedback.py +123 -0
  382. aria_code/project_review.py +278 -0
  383. aria_code/project_review_api.py +235 -0
  384. aria_code/project_review_server.py +18 -0
  385. aria_code/project_tools.py +4 -0
  386. aria_code/providers/__init__.py +30 -0
  387. aria_code/providers/llm/__init__.py +19 -0
  388. aria_code/providers/llm/anthropic.py +184 -0
  389. aria_code/providers/llm/autoconfig.py +213 -0
  390. aria_code/providers/llm/base.py +139 -0
  391. aria_code/providers/llm/custom.py +158 -0
  392. aria_code/providers/llm/ollama.py +129 -0
  393. aria_code/providers/llm/openai_compat.py +313 -0
  394. aria_code/providers/llm/registry.py +444 -0
  395. aria_code/providers/local_image_provider.py +202 -0
  396. aria_code/providers/local_llm_provider.py +589 -0
  397. aria_code/realty_data_tools.py +4 -0
  398. aria_code/report_exporters.py +150 -0
  399. aria_code/report_generator.py +1764 -0
  400. aria_code/runtime/__init__.py +169 -0
  401. aria_code/runtime/acceptance.py +481 -0
  402. aria_code/runtime/agent_loop.py +1520 -0
  403. aria_code/runtime/approval.py +57 -0
  404. aria_code/runtime/budget.py +232 -0
  405. aria_code/runtime/checkpoints.py +432 -0
  406. aria_code/runtime/crash_report.py +174 -0
  407. aria_code/runtime/events.py +113 -0
  408. aria_code/runtime/gateway.py +150 -0
  409. aria_code/runtime/lsp.py +484 -0
  410. aria_code/runtime/orchestrator.py +55 -0
  411. aria_code/runtime/repo_map.py +700 -0
  412. aria_code/runtime/run_state.py +127 -0
  413. aria_code/runtime/run_store.py +450 -0
  414. aria_code/runtime/self_healing.py +422 -0
  415. aria_code/runtime/subagent.py +547 -0
  416. aria_code/runtime/task_ledger.py +86 -0
  417. aria_code/runtime/task_orchestrator.py +90 -0
  418. aria_code/runtime/tool_executor.py +217 -0
  419. aria_code/runtime/tool_policy.py +107 -0
  420. aria_code/runtime/worktrees.py +215 -0
  421. aria_code/runway_video_client.py +4 -0
  422. aria_code/safety/__init__.py +23 -0
  423. aria_code/safety/permissions.py +306 -0
  424. aria_code/safety/service.py +102 -0
  425. aria_code/setup_wizard.py +673 -0
  426. aria_code/spreadsheet_tools.py +4 -0
  427. aria_code/strategy_vault.py +4 -0
  428. aria_code/tools/__init__.py +0 -0
  429. aria_code/tools/artifact_tools.py +252 -0
  430. aria_code/tools/broker_tools.py +92 -0
  431. aria_code/tools/build_quant_engine.py +97 -0
  432. aria_code/tools/code_audit_tools.py +192 -0
  433. aria_code/tools/computer_use_tools.py +504 -0
  434. aria_code/tools/data_analysis_tools.py +809 -0
  435. aria_code/tools/enterprise_finance_tools.py +156 -0
  436. aria_code/tools/extended_tools.py +240 -0
  437. aria_code/tools/file_analysis_tools.py +885 -0
  438. aria_code/tools/image_gen_tools.py +114 -0
  439. aria_code/tools/local_finance_tools.py +3417 -0
  440. aria_code/tools/logistics_tools.py +135 -0
  441. aria_code/tools/macro_tools.py +369 -0
  442. aria_code/tools/notification_tools.py +249 -0
  443. aria_code/tools/project_tools.py +525 -0
  444. aria_code/tools/realty_data_tools.py +659 -0
  445. aria_code/tools/spreadsheet_tools.py +388 -0
  446. aria_code/tools/stripe_tools.py +115 -0
  447. aria_code/ui/__init__.py +105 -0
  448. aria_code/ui/banner.py +508 -0
  449. aria_code/ui/completer.py +501 -0
  450. aria_code/ui/console.py +281 -0
  451. aria_code/ui/image_render.py +243 -0
  452. aria_code/ui/input_box.py +488 -0
  453. aria_code/ui/picker.py +201 -0
  454. aria_code/ui/render/__init__.py +11 -0
  455. aria_code/ui/render/finance.py +1705 -0
  456. aria_code/ui/render/market.py +225 -0
  457. aria_code/ui/render/output.py +775 -0
  458. aria_code/ui/render/responsive.py +75 -0
  459. aria_code/ui/render/team.py +354 -0
  460. aria_code/ui/robot.py +235 -0
  461. aria_code/ui/startup_dashboard.py +109 -0
  462. aria_code/utils/vscode_uri.py +37 -0
  463. aria_code/video_analysis.py +147 -0
  464. aria_code/video_editor.py +271 -0
  465. aria_code/workspace/__init__.py +6 -0
  466. aria_code/workspace/files.py +193 -0
  467. aria_code/workspace/verify.py +113 -0
  468. aria_code/workspace_changes.py +199 -0
  469. aria_code/workspace_context.py +233 -0
  470. aria_code/workspace_service.py +196 -0
  471. aria_code/workspace_tools.py +150 -0
  472. aria_code-0.52.0.dist-info/METADATA +237 -0
  473. aria_code-0.52.0.dist-info/RECORD +478 -0
  474. aria_code-0.52.0.dist-info/WHEEL +5 -0
  475. aria_code-0.52.0.dist-info/entry_points.txt +3 -0
  476. aria_code-0.52.0.dist-info/licenses/LICENSE +201 -0
  477. aria_code-0.52.0.dist-info/licenses/NOTICE +49 -0
  478. aria_code-0.52.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1494 @@
1
+ """
2
+ WorkspaceCommandsMixin — Workspace commands: packages, file, project, init, setup, memory.
3
+
4
+ Extracted from aria_cli.py. Methods' __globals__ are rebound to aria_cli's namespace
5
+ by _rebind_mixin_globals() called at module load time.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ._ui import print_error, rich_box
11
+ from aria_code.packages.aria_core.paths import aria_home
12
+
13
+
14
+ import json
15
+ import asyncio
16
+ import datetime
17
+ import time
18
+ import shlex
19
+ import sys
20
+ import os
21
+ from typing import Dict, Any, Optional
22
+
23
+ def detect_ollama_models_rich(*args, **kwargs):
24
+ from aria_code.apps.cli.helpers import detect_ollama_models_rich as fn
25
+ return fn(*args, **kwargs)
26
+ def _load_project_context(*args, **kwargs):
27
+ from aria_code.apps.cli.helpers import _load_project_context as fn
28
+ return fn(*args, **kwargs)
29
+ def _get__HAS_MCP():
30
+ from aria_code.apps.cli._optional import HAS_MCP as val
31
+ return val
32
+ def _get_LOCAL_TOOL_SCHEMAS():
33
+ # Owned by apps/cli/tool_registry.py; aria_cli fills it in place.
34
+ from ..tool_registry import LOCAL_TOOL_SCHEMAS as val
35
+ return val
36
+ import pathlib
37
+ def _get_provider_key(*args, **kwargs):
38
+ from aria_code.apps.cli.provider_keys import get_provider_key as fn
39
+ return fn(*args, **kwargs)
40
+ def _get_rich_box():
41
+ # Resolved locally now — see ._ui; no aria_cli round-trip.
42
+ return rich_box
43
+ def _arrow_select(*args, **kwargs):
44
+ # Re-exported by aria_cli; taken from its own module here.
45
+ from aria_code.ui.picker import arrow_select as fn
46
+ return fn(*args, **kwargs)
47
+ def _get___version__():
48
+ from aria_code.aria_cli import __version__ as val
49
+ return val
50
+ def _get_LOCAL_TOOLS():
51
+ # Owned by apps/cli/tool_registry.py; aria_cli fills it in place.
52
+ from ..tool_registry import LOCAL_TOOLS as val
53
+ return val
54
+ def get_model_capability(*args, **kwargs):
55
+ from aria_code.apps.cli._optional import get_model_capability as fn
56
+ return fn(*args, **kwargs)
57
+ def _get_MCP_CONFIG_PATH():
58
+ from aria_code.apps.cli._optional import MCP_CONFIG_PATH as val
59
+ return val
60
+ def _get_SESSIONS_DIR():
61
+ from aria_code.aria_cli import SESSIONS_DIR as val
62
+ return val
63
+ def _get__HAS_MODEL_CAP():
64
+ from aria_code.apps.cli._optional import HAS_MODEL_CAP as val
65
+ return val
66
+ def _strip_markdown_fences(*args, **kwargs):
67
+ from aria_code.aria_cli import _strip_markdown_fences as fn
68
+ return fn(*args, **kwargs)
69
+ def _load_data_keys(*args, **kwargs):
70
+ from aria_code.apps.cli.provider_keys import load_data_keys as fn
71
+ return fn(*args, **kwargs)
72
+
73
+ import json
74
+ import asyncio
75
+ import datetime
76
+ import time
77
+ import shlex
78
+ import sys
79
+ import os
80
+ from typing import Dict, Any, Optional
81
+
82
+
83
+ class WorkspaceCommandsMixin:
84
+ """Mixin: Workspace commands: packages, file, project, init, setup, memory."""
85
+
86
+ async def cmd_packages(self, args: str):
87
+ """Show Aria Code package facades and Arthera package bridge status."""
88
+ try:
89
+ from aria_code.packages.aria_agents import list_agent_manifests
90
+ from aria_code.packages.aria_core import (
91
+ build_package_manifest,
92
+ list_architecture_layers,
93
+ required_architecture_layer_names,
94
+ write_package_manifest,
95
+ )
96
+ from aria_code.packages.aria_infra import (
97
+ aria_code_identity,
98
+ build_package_doctor_report,
99
+ discover_arthera_packages,
100
+ )
101
+ from aria_code.packages.aria_mcp import (
102
+ arthera_quant_engine_server_config,
103
+ default_exposures,
104
+ load_mcp_config,
105
+ merge_server_config,
106
+ mcp_server_status,
107
+ mcp_tools_to_specs,
108
+ write_mcp_config,
109
+ )
110
+ from aria_code.packages.aria_services import (
111
+ list_service_specs,
112
+ list_service_usage_specs,
113
+ required_service_names,
114
+ )
115
+ from aria_code.packages.aria_services.provider_health import GLOBAL_PROVIDER_HEALTH
116
+ from aria_code.packages.aria_skills import builtin_skill_specs
117
+ from aria_code.packages.aria_tools import build_registry_from_legacy
118
+ except Exception as exc:
119
+ print_error(self.context, f"packages facade unavailable: {exc}")
120
+ return
121
+
122
+ sub = args.strip().lower()
123
+ identity = aria_code_identity(_get___version__())
124
+ arthera = discover_arthera_packages()
125
+ tool_registry = build_registry_from_legacy(_get_LOCAL_TOOLS(), _get_LOCAL_TOOL_SCHEMAS())
126
+ services = list_service_specs()
127
+ agent_count = len(list_agent_manifests())
128
+ skill_count = len(builtin_skill_specs())
129
+ mcp_exposure_count = len(default_exposures())
130
+ server_cfg = arthera_quant_engine_server_config()
131
+ mcp_config_path = _get_MCP_CONFIG_PATH() if _get__HAS_MCP() else aria_home() / "mcp_servers.json"
132
+
133
+ if sub.startswith("export-manifest") or sub.startswith("manifest"):
134
+ raw_parts = args.strip().split(maxsplit=1)
135
+ out_path = None
136
+ if len(raw_parts) > 1:
137
+ out_path = pathlib.Path(raw_parts[1]).expanduser()
138
+ else:
139
+ from aria_code.artifacts import artifact_dir
140
+ out_path = artifact_dir("manifests", "packages") / "aria_package_manifest.json"
141
+
142
+ reg = getattr(self.terminal, "_mcp_registry", None)
143
+ mcp_tools = []
144
+ if reg:
145
+ try:
146
+ mcp_tools = [
147
+ tool for tool in reg.all_tools()
148
+ if tool.get("server") == "arthera_quant_engine"
149
+ ]
150
+ except Exception:
151
+ mcp_tools = []
152
+ arthera_specs = mcp_tools_to_specs(mcp_tools, "arthera_quant_engine")
153
+ manifest = build_package_manifest(
154
+ identity=identity,
155
+ services=services,
156
+ tools=tool_registry.list(),
157
+ agents=list_agent_manifests(),
158
+ skills=builtin_skill_specs(),
159
+ mcp_exposures=default_exposures(),
160
+ arthera_packages=arthera,
161
+ arthera_mcp_tools=arthera_specs,
162
+ )
163
+ write_package_manifest(out_path, manifest)
164
+ if self.context.has_rich:
165
+ self.context.console.print()
166
+ self.context.console.print(f" [green]Package manifest exported[/green] [dim]{out_path}[/dim]")
167
+ self.context.console.print(
168
+ f" [dim]tools={len(manifest['capabilities']['tools'])} "
169
+ f"services={len(manifest['capabilities']['services'])} "
170
+ f"agents={len(manifest['capabilities']['agents'])} "
171
+ f"skills={len(manifest['capabilities']['skills'])} "
172
+ f"arthera_mcp_tools={len(manifest['capabilities']['arthera_mcp_tools'])}[/dim]\n"
173
+ )
174
+ else:
175
+ print(f"Package manifest exported: {out_path}")
176
+ return
177
+
178
+ if sub in ("doctor", "doctor arthera", "arthera doctor"):
179
+ reg = getattr(self.terminal, "_mcp_registry", None)
180
+ runtime_status = []
181
+ mcp_tools = []
182
+ if reg:
183
+ try:
184
+ runtime_status = reg.status()
185
+ mcp_tools = [
186
+ tool for tool in reg.all_tools()
187
+ if tool.get("server") == "arthera_quant_engine"
188
+ ]
189
+ except Exception:
190
+ runtime_status = []
191
+ mcp_tools = []
192
+ status = mcp_server_status(mcp_config_path, "arthera_quant_engine", runtime_status)
193
+ specs = mcp_tools_to_specs(mcp_tools, "arthera_quant_engine")
194
+ from aria_code.artifacts import artifact_dir
195
+ manifest_path = artifact_dir("manifests", "packages", create=False) / "aria_package_manifest.json"
196
+ report = build_package_doctor_report(
197
+ arthera=arthera,
198
+ mcp_status=status,
199
+ tool_count=len(specs),
200
+ manifest_can_export=True,
201
+ manifest_path=manifest_path,
202
+ services=services,
203
+ required_services=required_service_names(),
204
+ architecture_layers=list_architecture_layers(),
205
+ required_architecture_layers=required_architecture_layer_names(),
206
+ provider_health=GLOBAL_PROVIDER_HEALTH.snapshot(),
207
+ )
208
+ if self.context.has_rich:
209
+ from rich.table import Table as _Table
210
+ color = "green" if report.status == "ok" else "yellow" if report.status == "warn" else "red"
211
+ self.context.console.print()
212
+ self.context.console.print(
213
+ f" [bold]{identity.product}[/bold] "
214
+ f"[dim]· {identity.company} packages doctor ·[/dim] [{color}]{report.status}[/{color}]\n"
215
+ )
216
+ tbl = _Table(
217
+ box=_get_rich_box().ROUNDED,
218
+ border_style="dim",
219
+ show_header=True,
220
+ header_style="bold dim",
221
+ )
222
+ tbl.add_column("Check", width=22)
223
+ tbl.add_column("Status", width=8)
224
+ tbl.add_column("Detail")
225
+ tbl.add_column("Next")
226
+ for check in report.checks:
227
+ st_color = "green" if check.status == "ok" else "yellow" if check.status == "warn" else "red"
228
+ tbl.add_row(
229
+ check.name,
230
+ f"[{st_color}]{check.status}[/{st_color}]",
231
+ check.detail,
232
+ check.remediation,
233
+ )
234
+ self.context.console.print(tbl)
235
+ self.context.console.print()
236
+ else:
237
+ print(f"{identity.product} packages doctor: {report.status}")
238
+ for check in report.checks:
239
+ print(f"{check.status:5s} {check.name}: {check.detail} {check.remediation}")
240
+ return
241
+
242
+ if sub in ("status", "status arthera", "arthera status"):
243
+ runtime_status = []
244
+ reg = getattr(self.terminal, "_mcp_registry", None)
245
+ if reg:
246
+ try:
247
+ runtime_status = reg.status()
248
+ except Exception:
249
+ runtime_status = []
250
+ status = mcp_server_status(mcp_config_path, "arthera_quant_engine", runtime_status)
251
+
252
+ if self.context.has_rich:
253
+ self.context.console.print()
254
+ self.context.console.print(
255
+ f" [bold]{identity.product}[/bold] "
256
+ f"[dim]· {identity.company} package bridge status[/dim]\n"
257
+ )
258
+ rows = [
259
+ ("config", status["config_path"]),
260
+ ("configured", "yes" if status["configured"] else "no"),
261
+ ("server", status["server_path"] or "—"),
262
+ ("server file", "found" if status["server_file_exists"] else "missing"),
263
+ ("runtime", "running" if status["running"] else "not running"),
264
+ ("tools", str(status["tool_count"])),
265
+ ]
266
+ for key, value in rows:
267
+ style = "green" if value in ("yes", "found", "running") else "yellow" if value in ("no", "missing", "not running") else "dim"
268
+ self.context.console.print(f" [dim]{key:12s}[/dim] [{style}]{value}[/{style}]")
269
+ if status["tools"]:
270
+ self.context.console.print(" [dim]tool names:[/dim] " + ", ".join(status["tools"][:12]))
271
+ if not status["configured"]:
272
+ self.context.console.print(" [dim]Run /packages connect arthera to write the MCP bridge.[/dim]")
273
+ elif not status["running"]:
274
+ self.context.console.print(" [dim]Run /mcp reload or /packages connect arthera --reload.[/dim]")
275
+ self.context.console.print()
276
+ else:
277
+ print(f"{identity.product} · {identity.company} package bridge status")
278
+ for key, value in status.items():
279
+ if key != "tools":
280
+ print(f"{key}: {value}")
281
+ return
282
+
283
+ if sub in ("tools arthera", "arthera tools", "tools"):
284
+ reg = getattr(self.terminal, "_mcp_registry", None)
285
+ mcp_tools = []
286
+ if reg:
287
+ try:
288
+ mcp_tools = [
289
+ tool for tool in reg.all_tools()
290
+ if tool.get("server") == "arthera_quant_engine"
291
+ ]
292
+ except Exception:
293
+ mcp_tools = []
294
+ specs = mcp_tools_to_specs(mcp_tools, "arthera_quant_engine")
295
+
296
+ if self.context.has_rich:
297
+ from rich.table import Table as _Table
298
+ self.context.console.print()
299
+ self.context.console.print(
300
+ f" [bold]{identity.product}[/bold] "
301
+ f"[dim]· {identity.company} MCP tool manifests[/dim]\n"
302
+ )
303
+ if not specs:
304
+ self.context.console.print(" [yellow]No Arthera MCP tools discovered.[/yellow]")
305
+ self.context.console.print(" [dim]Run /packages connect arthera --reload, then retry /packages tools arthera.[/dim]\n")
306
+ return
307
+ tbl = _Table(
308
+ title="[bold]Arthera QuantEngine Tools[/bold]",
309
+ box=_get_rich_box().ROUNDED,
310
+ border_style="dim",
311
+ show_header=True,
312
+ header_style="bold dim",
313
+ )
314
+ tbl.add_column("Tool", width=34)
315
+ tbl.add_column("Permissions", width=24)
316
+ tbl.add_column("Capabilities", width=30)
317
+ tbl.add_column("Schema")
318
+ for spec in specs:
319
+ perms = ", ".join(p.value for p in spec.permissions)
320
+ caps = ", ".join(spec.capabilities)
321
+ schema_state = "yes" if spec.schema else "no"
322
+ tbl.add_row(spec.name, perms, caps, schema_state)
323
+ self.context.console.print(tbl)
324
+ self.context.console.print()
325
+ else:
326
+ if not specs:
327
+ print("No Arthera MCP tools discovered. Run /packages connect arthera --reload.")
328
+ return
329
+ for spec in specs:
330
+ print(f"{spec.name}: {', '.join(spec.capabilities)}")
331
+ return
332
+
333
+ if sub in ("services", "service", "usage", "use", "map"):
334
+ usage_specs = list_service_usage_specs()
335
+ if self.context.has_rich:
336
+ from rich.table import Table as _Table
337
+ self.context.console.print()
338
+ self.context.console.print(
339
+ f" [bold]{identity.product}[/bold] "
340
+ f"[dim]· service usage map[/dim]\n"
341
+ )
342
+ tbl = _Table(
343
+ title="[bold]Project Services[/bold]",
344
+ box=_get_rich_box().ROUNDED,
345
+ border_style="dim",
346
+ show_header=True,
347
+ header_style="bold dim",
348
+ )
349
+ tbl.add_column("Service", width=22)
350
+ tbl.add_column("Purpose", width=34)
351
+ tbl.add_column("CLI", width=32)
352
+ tbl.add_column("Arthera / MCP", width=34)
353
+ tbl.add_column("Next")
354
+ for spec in usage_specs:
355
+ mcp = ", ".join(spec.mcp_tools[:4]) if spec.mcp_tools else "—"
356
+ packages = ", ".join(spec.package_sources[:2])
357
+ tbl.add_row(
358
+ spec.name,
359
+ spec.purpose,
360
+ ", ".join(spec.cli_entrypoints),
361
+ f"{packages}\n[dim]{mcp}[/dim]",
362
+ spec.next_step,
363
+ )
364
+ self.context.console.print(tbl)
365
+ self.context.console.print("[dim]连接 Arthera MCP: /packages connect arthera --reload;券商接入: /broker guide[/dim]\n")
366
+ else:
367
+ for spec in usage_specs:
368
+ print(f"{spec.name}: {spec.purpose} -> {', '.join(spec.cli_entrypoints)}")
369
+ return
370
+
371
+ if sub.startswith("connect arthera") or sub in ("connect", "connect-quant", "connect quant"):
372
+ existing = load_mcp_config(mcp_config_path)
373
+ updated = merge_server_config(existing, server_cfg)
374
+ write_mcp_config(mcp_config_path, updated)
375
+ server_path = pathlib.Path(str(server_cfg["args"][0]))
376
+ ready = server_path.exists()
377
+ if self.context.has_rich:
378
+ self.context.console.print()
379
+ self.context.console.print(
380
+ f" [bold]{identity.product}[/bold] "
381
+ f"[dim]connected to {identity.company} package bridge[/dim]"
382
+ )
383
+ status = "[green]ready[/green]" if ready else "[yellow]configured, server file not found[/yellow]"
384
+ self.context.console.print(f" {status} [dim]{server_cfg['name']}[/dim]")
385
+ self.context.console.print(f" [dim]config:[/dim] {mcp_config_path}")
386
+ self.context.console.print(f" [dim]server:[/dim] {server_path}")
387
+ else:
388
+ print(f"Connected {identity.product} -> {server_cfg['name']}")
389
+ print(f"config: {mcp_config_path}")
390
+ print(f"server: {server_path} ({'ready' if ready else 'missing'})")
391
+
392
+ if "--reload" in sub or " reload" in sub:
393
+ await self.cmd_mcp("reload")
394
+ else:
395
+ if self.context.has_rich:
396
+ self.context.console.print(" [dim]Run /mcp reload to start the server.[/dim]\n")
397
+ else:
398
+ print("Run /mcp reload to start the server.")
399
+ return
400
+
401
+ if self.context.has_rich:
402
+ from rich.table import Table as _Table
403
+
404
+ self.context.console.print()
405
+ self.context.console.print(
406
+ f" [bold]{identity.product}[/bold] "
407
+ f"[dim]v{identity.version} · {identity.company} product[/dim]"
408
+ )
409
+ self.context.console.print(f" [dim]{identity.description}[/dim]\n")
410
+
411
+ tbl = _Table(
412
+ title="[bold]Package Facades[/bold]",
413
+ box=_get_rich_box().ROUNDED,
414
+ border_style="dim",
415
+ show_header=True,
416
+ header_style="bold dim",
417
+ )
418
+ tbl.add_column("Package")
419
+ tbl.add_column("Status")
420
+ tbl.add_column("Surface")
421
+ tbl.add_row("aria_core", "ready", "CapabilityManifest, permissions")
422
+ tbl.add_row("aria_services", "ready", f"{len(services)} service boundaries")
423
+ tbl.add_row("aria_tools", "ready", f"{len(tool_registry.list())} tools")
424
+ tbl.add_row("aria_agents", "ready", f"{agent_count} agents")
425
+ tbl.add_row("aria_skills", "ready", f"{skill_count} skills")
426
+ tbl.add_row("aria_mcp", "ready", f"{mcp_exposure_count} planned exposures")
427
+ tbl.add_row("aria_infra", "ready", "Arthera package discovery")
428
+ self.context.console.print(tbl)
429
+
430
+ self.context.console.print()
431
+ self.context.console.print("[bold]Arthera Packages[/bold]")
432
+ from aria_code.ui.render.output import display_path as _display_path
433
+ if arthera.available:
434
+ self.context.console.print(f" [green]found[/green] [dim]{_display_path(arthera.root, fallback='package root')}[/dim]")
435
+ for name, path in sorted(arthera.packages.items()):
436
+ self.context.console.print(f" [dim]·[/dim] [bold]{name:14s}[/bold] [dim]{_display_path(path, fallback='package')}[/dim]")
437
+ if arthera.mcp_servers:
438
+ self.context.console.print(" [dim]MCP server candidates:[/dim]")
439
+ for path in arthera.mcp_servers[:5]:
440
+ self.context.console.print(f" [dim]{_display_path(path, fallback='server')}[/dim]")
441
+ else:
442
+ self.context.console.print(f" [yellow]not found[/yellow] [dim]{_display_path(arthera.root, fallback='package root')}[/dim]")
443
+
444
+ self.context.console.print()
445
+ self.context.console.print("[bold]Recommended MCP bridge[/bold]")
446
+ self.context.console.print(f" [dim]name:[/dim] {server_cfg['name']}")
447
+ self.context.console.print(f" [dim]command:[/dim] {server_cfg['command']} {' '.join(server_cfg['args'])}")
448
+ self.context.console.print(f" [dim]env PYTHONPATH:[/dim] {server_cfg['env']['PYTHONPATH']}")
449
+ self.context.console.print(f" [dim]config:[/dim] {mcp_config_path}")
450
+ self.context.console.print(" [dim]Run /packages connect arthera to write this MCP bridge.[/dim]\n")
451
+ else:
452
+ print(f"{identity.product} v{identity.version} · {identity.company} product")
453
+ print(f"services={len(services)} tools={len(tool_registry.list())} agents={agent_count} skills={skill_count} mcp={mcp_exposure_count}")
454
+ print(f"Arthera packages: {'found' if arthera.available else 'not found'} {arthera.root}")
455
+ print(f"Recommended MCP: {server_cfg}")
456
+
457
+ async def cmd_file(self, args: str):
458
+ """
459
+ /file load <路径> — 加载文件到会话
460
+ /file analyze [1|2|3|4] — 分层分析(1=摘要 2=深度 3=领域 4=建议)
461
+ /file ask <问题> — 就已加载文件提问
462
+ /file list — 列出会话中的所有文件
463
+ /file switch <文件名> — 切换活跃文件
464
+ /file clear [文件名] — 清除文件
465
+ /file check — 检查可用解析器
466
+ """
467
+ import asyncio as _asyncio
468
+ loop = _asyncio.get_event_loop()
469
+ parts = args.strip().split(None, 1) if args.strip() else []
470
+ sub = parts[0].lower() if parts else "help"
471
+ rest = parts[1].strip() if len(parts) > 1 else ""
472
+
473
+ # ── 确保 file_session 已初始化 ────────────────────────────────────────
474
+ if self.terminal._file_session is None:
475
+ try:
476
+ from aria_code.file_analysis_tools import FileSession
477
+ self.terminal._file_session = FileSession()
478
+ except ImportError as e:
479
+ if self.context.has_rich:
480
+ self.context.console.print(f"[red]file_analysis_tools 未加载: {e}[/red]")
481
+ return
482
+
483
+ fs = self.terminal._file_session
484
+
485
+ # ────────────────── /file load ────────────────────────────────────────
486
+ if sub == "load":
487
+ if not rest:
488
+ if self.context.has_rich:
489
+ self.context.console.print("[dim]用法: /file load <文件路径>[/dim]")
490
+ self.context.console.print("[dim]支持: PDF DOCX XLSX CSV JSON TXT MD 图片 代码文件[/dim]")
491
+ return
492
+
493
+ if self.context.has_rich:
494
+ self.context.console.print(f"[dim]正在解析 {rest}...[/dim]")
495
+
496
+ # Include images only for vision-capable models
497
+ _curr_model = self.terminal.config.get("model", "")
498
+ include_img = False
499
+ if _get__HAS_MODEL_CAP():
500
+ try:
501
+ _mc = get_model_capability(_curr_model)
502
+ include_img = bool(_mc.vision)
503
+ except Exception:
504
+ pass
505
+
506
+ from aria_code.file_analysis_tools import parse_file, check_parsers
507
+ fc = await loop.run_in_executor(
508
+ None, lambda: parse_file(rest, include_images=include_img))
509
+
510
+ if not fc.success:
511
+ if self.context.has_rich:
512
+ self.context.console.print(f"[red]解析失败: {fc.error}[/red]")
513
+ # Show which parsers are available
514
+ parsers = check_parsers()
515
+ missing = [k for k, v in parsers.items() if not v]
516
+ if missing:
517
+ self.context.console.print(f"[yellow]⚠ 未安装解析器: {', '.join(missing)}[/yellow]")
518
+ self.context.console.print(f"[dim]安装命令: pip install {' '.join(missing)}[/dim]")
519
+ return
520
+
521
+ fs.load(rest, include_images=include_img)
522
+ self.terminal._file_ctx_injected = False # Reset so next msg injects file
523
+
524
+ if self.context.has_rich:
525
+ from rich.panel import Panel as _P
526
+ from rich import box as _box
527
+ info_lines = [
528
+ f"[green]✓[/green] [bold]{fc.filename}[/bold]",
529
+ f"[dim]类型: {fc.file_type.upper()} 大小: {fc.size_kb:.1f} KB "
530
+ f"提取: {fc.char_count:,} 字符[/dim]",
531
+ ]
532
+ for k, v in fc.metadata.items():
533
+ if k in ("pages","rows","columns","lines","language",
534
+ "sheets","title","author","symbols"):
535
+ val = v[:5] if isinstance(v, list) else v
536
+ info_lines.append(f"[dim]{k}: {val}[/dim]")
537
+ if fc.truncated:
538
+ info_lines.append("[yellow]⚠ 内容已截断(文件较大)[/yellow]")
539
+ if fc.tables:
540
+ info_lines.append(f"[dim]包含 {len(fc.tables)} 个表格[/dim]")
541
+ info_lines.append("\n[dim]发送任何消息即可开始分析,或使用 /file analyze 1-4[/dim]")
542
+ self.context.console.print(_P("\n".join(info_lines),
543
+ title="[bold]📄 文件已加载[/bold]",
544
+ border_style="green", box=_box.ROUNDED))
545
+
546
+ # ────────────────── /file analyze ─────────────────────────────────────
547
+ elif sub == "analyze":
548
+ fc = fs.get_active()
549
+ if not fc:
550
+ if self.context.has_rich: self.context.console.print("[dim]请先使用 /file load <路径> 加载文件[/dim]")
551
+ return
552
+
553
+ # Determine layer(s) to run
554
+ layer_arg = rest.strip()
555
+ if layer_arg == "all":
556
+ layers_to_run = [1, 2, 3, 4]
557
+ else:
558
+ try:
559
+ layers_to_run = [int(layer_arg)] if layer_arg else [1, 2]
560
+ except ValueError:
561
+ layers_to_run = [1, 2]
562
+
563
+ layer_names = {1: "📌 快速摘要", 2: "🔍 深度分析", 3: "💡 领域洞察", 4: "✅ 行动建议"}
564
+
565
+ from aria_code.file_analysis_tools import build_analysis_prompt
566
+
567
+ for layer in layers_to_run:
568
+ if self.context.has_rich:
569
+ self.context.console.print(f"\n[bold]{layer_names.get(layer, f'层{layer}')}[/bold]")
570
+ self.context.console.print(f"[dim]{'─'*50}[/dim]")
571
+
572
+ prompt = build_analysis_prompt(fc, layer=layer)
573
+ # Send to LLM via the normal message pipeline
574
+ await self.terminal.send_message(
575
+ prompt,
576
+ system_override=(
577
+ "你是专业文档分析助手,具备金融、法律、技术、不动产等多领域知识。"
578
+ "分析要精确、结构化,优先使用数字和具体事实。"
579
+ ),
580
+ )
581
+
582
+ if len(layers_to_run) > 1 and layer < layers_to_run[-1]:
583
+ if self.context.has_rich:
584
+ self.context.console.print(f"\n[dim]{'═'*60}[/dim]")
585
+ self.context.console.print("[dim]进入下一层分析...[/dim]\n")
586
+
587
+ # ────────────────── /file ask ──────────────────────────────────────────
588
+ elif sub == "ask":
589
+ fc = fs.get_active()
590
+ if not fc:
591
+ if self.context.has_rich: self.context.console.print("[dim]请先使用 /file load <路径> 加载文件[/dim]")
592
+ return
593
+ question = rest.strip()
594
+ if not question:
595
+ if self.context.has_rich:
596
+ from rich.prompt import Prompt as _Prompt
597
+ question = _Prompt.ask(" 请输入问题")
598
+ else:
599
+ question = input("请输入问题: ")
600
+ if not question:
601
+ return
602
+
603
+ from aria_code.file_analysis_tools import build_analysis_prompt
604
+ prompt = build_analysis_prompt(fc, layer=0, question=question)
605
+ await self.terminal.send_message(
606
+ prompt,
607
+ system_override=(
608
+ "你是专业文档分析助手。请基于用户提供的文件内容准确回答问题,"
609
+ "若文件中无法找到答案,请如实说明。"
610
+ ),
611
+ )
612
+
613
+ # ────────────────── /file list ─────────────────────────────────────────
614
+ elif sub == "list":
615
+ files = fs.list_files()
616
+ if not files:
617
+ if self.context.has_rich: self.context.console.print("[dim]会话中暂无已加载文件。使用 /file load <路径>[/dim]")
618
+ return
619
+ if self.context.has_rich:
620
+ from rich.table import Table as _T
621
+ from rich import box as _box
622
+ tb = _T(title="[bold]📂 已加载文件[/bold]", box=_box.ROUNDED)
623
+ tb.add_column("状态", width=4); tb.add_column("文件名")
624
+ tb.add_column("类型", style="dim"); tb.add_column("大小KB", justify="right", style="dim")
625
+ tb.add_column("字符数", justify="right", style="dim"); tb.add_column("截断", style="dim")
626
+ for f in files:
627
+ status = "[green]●[/green]" if f["active"] else "[dim]○[/dim]"
628
+ tb.add_row(status, f["filename"], f["type"].upper(),
629
+ str(f["size_kb"]), f"{f['chars']:,}",
630
+ "[yellow]是[/yellow]" if f["truncated"] else "否")
631
+ self.context.console.print(tb)
632
+ self.context.console.print("[dim]/file ask <问题> 向活跃文件提问[/dim]")
633
+
634
+ # ────────────────── /file switch ──────────────────────────────────────
635
+ elif sub == "switch":
636
+ if not rest:
637
+ if self.context.has_rich: self.context.console.print("[dim]用法: /file switch <文件名>[/dim]")
638
+ return
639
+ if fs.set_active(rest):
640
+ fc = fs.get_active()
641
+ self.terminal._file_ctx_injected = False
642
+ if self.context.has_rich:
643
+ self.context.console.print(f"[green]✓ 已切换到: {fc.filename}[/green]")
644
+ else:
645
+ if self.context.has_rich: self.context.console.print(f"[red]未找到文件: {rest}[/red]")
646
+
647
+ # ────────────────── /file clear ──────────────────────────────────────
648
+ elif sub == "clear":
649
+ fs.clear(rest if rest else None)
650
+ self.terminal._file_ctx_injected = False
651
+ msg = f"已清除文件: {rest}" if rest else "已清除所有已加载文件"
652
+ if self.context.has_rich: self.context.console.print(f"[dim]{msg}[/dim]")
653
+
654
+ # ────────────────── /file check ───────────────────────────────────────
655
+ elif sub == "check":
656
+ from aria_code.file_analysis_tools import check_parsers
657
+ parsers = check_parsers()
658
+ if self.context.has_rich:
659
+ from rich.table import Table as _T
660
+ from rich import box as _box
661
+ tb = _T(title="[bold]📦 文件解析器状态[/bold]", box=_box.ROUNDED)
662
+ tb.add_column("库"); tb.add_column("状态"); tb.add_column("安装命令", style="dim")
663
+ _CMDS = {
664
+ "pdfplumber": "pip install pdfplumber",
665
+ "pypdf": "pip install pypdf",
666
+ "python-docx":"pip install python-docx",
667
+ "pandas": "pip install pandas",
668
+ "openpyxl": "pip install openpyxl",
669
+ "beautifulsoup4": "pip install beautifulsoup4",
670
+ "Pillow": "pip install Pillow",
671
+ }
672
+ for lib, ok in parsers.items():
673
+ status = "[green]✓ 已安装[/green]" if ok else "[red]✗ 未安装[/red]"
674
+ tb.add_row(lib, status, "" if ok else _CMDS.get(lib,""))
675
+ self.context.console.print(tb)
676
+ formats_ok = []
677
+ if parsers.get("pdfplumber") or parsers.get("pypdf"):
678
+ formats_ok.append("PDF")
679
+ if parsers.get("python-docx"):
680
+ formats_ok.append("Word/DOCX")
681
+ if parsers.get("pandas") and parsers.get("openpyxl"):
682
+ formats_ok.append("Excel/CSV")
683
+ formats_ok.extend(["JSON", "TXT/MD", "代码文件"])
684
+ self.context.console.print(f"[dim]可解析格式: {', '.join(formats_ok)}[/dim]")
685
+
686
+ # ────────────────── /file help ─────────────────────────────────────────
687
+ else:
688
+ if self.context.has_rich:
689
+ self.context.console.print("[bold]📄 /file 文件分析命令[/bold]")
690
+ rows = [
691
+ ("/file load <路径>", "加载文件 (PDF/DOCX/XLSX/CSV/JSON/TXT/代码/图片)"),
692
+ ("/file analyze 1", "快速摘要 (300字)"),
693
+ ("/file analyze 2", "深度内容分析 (结构/要点/异常)"),
694
+ ("/file analyze 3", "领域专项分析 (财务/法律/技术/不动产)"),
695
+ ("/file analyze 4", "行动建议与风险清单"),
696
+ ("/file analyze all", "依次运行 4 层分析"),
697
+ ("/file ask <问题>", "就文件内容多轮提问"),
698
+ ("/file list", "查看已加载文件"),
699
+ ("/file switch <文件名>", "切换分析目标文件"),
700
+ ("/file clear", "清除所有已加载文件"),
701
+ ("/file check", "检查已安装的解析器"),
702
+ ]
703
+ from rich.table import Table as _T
704
+ from rich import box as _box
705
+ tb = _T(box=_box.MINIMAL)
706
+ tb.add_column("命令", style="cyan"); tb.add_column("说明", style="dim")
707
+ for cmd, desc in rows:
708
+ tb.add_row(cmd, desc)
709
+ self.context.console.print(tb)
710
+
711
+ async def cmd_project(self, args: str):
712
+ """项目分析 (Claude Code / Codex 风格): /project load|tree|grep|ask|task|status|info <参数>"""
713
+ # 本方法体内三处用到 _T / _box,但导入只写在别的方法里。普通方法的
714
+ # __globals__ 被 _rebind_mixin_globals 换成了 aria_cli 的,本文件模块级
715
+ # 的名字一律看不见——所以必须在方法内自己导入,跟本文件其它方法一致。
716
+ from rich.table import Table as _T
717
+ from rich import box as _box
718
+ try:
719
+ from aria_code.project_tools import ProjectSession, scan_project, format_grep_results
720
+ _HAS_PT = True
721
+ except ImportError:
722
+ _HAS_PT = False
723
+
724
+ if not _HAS_PT:
725
+ self.context.console.print("[red]❌ project_tools.py 未找到,请确保文件存在。[/red]")
726
+ return
727
+
728
+ parts = args.strip().split(maxsplit=1)
729
+ sub = parts[0].lower() if parts else "info"
730
+ rest = parts[1].strip() if len(parts) > 1 else ""
731
+ ps = self.terminal._project_session # type: ignore[attr-defined]
732
+
733
+ # ── load ──────────────────────────────────────────────────────────────
734
+ if sub == "load":
735
+ if not rest:
736
+ self.context.console.print("[yellow]用法: /project load <目录路径>[/yellow]")
737
+ return
738
+ from pathlib import Path as _Path
739
+ target = _Path(rest).expanduser().resolve()
740
+ if not target.exists():
741
+ self.context.console.print(f"[red]路径不存在: {target}[/red]")
742
+ return
743
+
744
+ self.context.console.print(f"[dim]正在扫描项目: {target} …[/dim]")
745
+ try:
746
+ new_ps = scan_project(str(target), max_files=2000)
747
+ except Exception as e:
748
+ self.context.console.print(f"[red]扫描失败: {e}[/red]")
749
+ return
750
+
751
+ self.terminal._project_session = new_ps # type: ignore[attr-defined]
752
+ self.terminal._project_ctx_injected = False # type: ignore[attr-defined]
753
+
754
+ # Auto-archive project into global memory
755
+ if getattr(self.terminal, "memory_mgr", None):
756
+ try:
757
+ _ps_s = new_ps.summary()
758
+ self.terminal.memory_mgr.upsert_project(_ps_s["name"], {
759
+ "root": _ps_s["root"],
760
+ "type": _ps_s["type"],
761
+ "languages": _ps_s.get("languages", []),
762
+ })
763
+ except Exception:
764
+ pass
765
+
766
+ s = new_ps.summary()
767
+ tb = _T(box=_box.ROUNDED, show_header=False, padding=(0, 1))
768
+ tb.add_column("k", style="dim", width=14)
769
+ tb.add_column("v", style="cyan")
770
+ tb.add_row("项目名", s["name"])
771
+ tb.add_row("路径", s["root"])
772
+ tb.add_row("类型", s["type"])
773
+ tb.add_row("语言", ", ".join(s["languages"][:4]))
774
+ tb.add_row("文件数", str(s["total_files"]))
775
+ tb.add_row("代码行", f"{s['total_lines']:,}")
776
+ tb.add_row("总大小", f"{s['total_size_kb']} KB")
777
+ if s["git"].get("branch"):
778
+ tb.add_row("Git 分支", s["git"]["branch"])
779
+ if s["git"].get("changed_count"):
780
+ tb.add_row("变更文件", str(s["git"]["changed_count"]))
781
+ self.context.console.print("\n[bold]项目已加载 ✓[/bold]")
782
+ self.context.console.print(tb)
783
+ self.context.console.print(f"\n[dim]关键文件: {', '.join(s['key_files'][:6])}[/dim]")
784
+ self.context.console.print("[dim]现在可以直接对话,Aria 将根据项目上下文回答。[/dim]\n")
785
+ return
786
+
787
+ # ── 未加载时提示 ──────────────────────────────────────────────────────
788
+ if ps is None:
789
+ self.context.console.print("[yellow]请先加载项目: /project load <目录路径>[/yellow]")
790
+ return
791
+
792
+ # ── tree ──────────────────────────────────────────────────────────────
793
+ if sub == "tree":
794
+ depth_arg = rest.strip()
795
+ max_lines = 120
796
+ if depth_arg.isdigit():
797
+ max_lines = int(depth_arg) * 30 # rough approximation
798
+ tree_str = ps.get_tree(max_lines=max_lines)
799
+ self.context.console.print(f"\n[bold]{ps.name}/[/bold]")
800
+ self.context.console.print(f"[dim]{tree_str}[/dim]")
801
+ self.context.console.print(f"\n[dim]共 {ps.stats.get('total_files', 0)} 个文件[/dim]\n")
802
+
803
+ # ── grep / search ─────────────────────────────────────────────────────
804
+ elif sub in ("grep", "search"):
805
+ if not rest:
806
+ self.context.console.print("[yellow]用法: /project grep <正则表达式> [glob模式][/yellow]")
807
+ return
808
+ parts2 = rest.split(maxsplit=1)
809
+ pattern = parts2[0]
810
+ glob = parts2[1] if len(parts2) > 1 else "**/*"
811
+ self.context.console.print(f"[dim]搜索 \"{pattern}\" …[/dim]")
812
+ results = ps.grep(pattern, glob=glob, max_results=60)
813
+ self.context.console.print(format_grep_results(results, pattern))
814
+
815
+ # ── status ────────────────────────────────────────────────────────────
816
+ elif sub == "status":
817
+ gi = ps.git_info
818
+ if not gi:
819
+ self.context.console.print("[dim]当前项目不是 Git 仓库[/dim]")
820
+ return
821
+ self.context.console.print(f"\n[bold]Git 状态[/bold] — {ps.name}")
822
+ self.context.console.print(f" 分支: [cyan]{gi.get('branch','?')}[/cyan] "
823
+ f"变更: [yellow]{gi.get('changed_count', 0)}[/yellow] 个文件")
824
+ if gi.get("changed_files"):
825
+ for f in gi["changed_files"][:15]:
826
+ self.context.console.print(f" [dim]{f}[/dim]")
827
+ if gi.get("recent_commits"):
828
+ self.context.console.print("\n[bold]最近提交:[/bold]")
829
+ for c in gi["recent_commits"][:5]:
830
+ self.context.console.print(f" [dim]{c}[/dim]")
831
+ self.context.console.print()
832
+
833
+ # ── info ──────────────────────────────────────────────────────────────
834
+ elif sub in ("info", "summary", ""):
835
+ s = ps.summary()
836
+ tb = _T(box=_box.ROUNDED, show_header=False, padding=(0, 1))
837
+ tb.add_column("k", style="dim", width=14)
838
+ tb.add_column("v", style="cyan")
839
+ tb.add_row("项目名", s["name"])
840
+ tb.add_row("路径", s["root"])
841
+ tb.add_row("类型", s["type"])
842
+ tb.add_row("主要语言", ", ".join(s["languages"][:4]))
843
+ tb.add_row("文件数", str(s["total_files"]))
844
+ tb.add_row("代码行", f"{s['total_lines']:,}")
845
+ tb.add_row("大小", f"{s['total_size_kb']} KB")
846
+ if s["git"].get("branch"):
847
+ tb.add_row("Git 分支", s["git"]["branch"])
848
+ self.context.console.print(tb)
849
+ self.context.console.print(f"\n[dim]关键文件: {', '.join(s['key_files'][:8])}[/dim]\n")
850
+
851
+ # ── read ──────────────────────────────────────────────────────────────
852
+ elif sub == "read":
853
+ if not rest:
854
+ self.context.console.print("[yellow]用法: /project read <文件路径>[/yellow]")
855
+ return
856
+ ok, content = ps.read_file(rest)
857
+ if not ok:
858
+ self.context.console.print(f"[red]{content}[/red]")
859
+ return
860
+ lang = rest.rsplit(".", 1)[-1] if "." in rest else "text"
861
+ self.context.console.print(f"\n[bold]{rest}[/bold]")
862
+ if self.context.has_rich:
863
+ from rich.syntax import Syntax
864
+ self.context.console.print(Syntax(content, lang, theme="monokai", line_numbers=True,
865
+ word_wrap=False))
866
+ else:
867
+ print(content)
868
+
869
+ # ── clear ─────────────────────────────────────────────────────────────
870
+ elif sub == "clear":
871
+ self.terminal._project_session = None # type: ignore[attr-defined]
872
+ self.terminal._project_ctx_injected = False # type: ignore[attr-defined]
873
+ self.context.console.print("[dim]项目上下文已清除[/dim]")
874
+
875
+ # ── ask / task → forward to AI with project context ───────────────────
876
+ elif sub in ("ask", "task"):
877
+ if not rest:
878
+ self.context.console.print(f"[yellow]用法: /project {sub} <问题或任务描述>[/yellow]")
879
+ return
880
+ # Delegate to the AI; project context is injected automatically via send_message
881
+ prefix = "请基于当前项目完成以下任务:\n" if sub == "task" else ""
882
+ await self.terminal.send_message(prefix + rest) # type: ignore[attr-defined]
883
+
884
+ # ── help ─────────────────────────────────────────────────────────────
885
+ else:
886
+ rows = [
887
+ ("/project load <path>", "加载项目目录,构建文件索引"),
888
+ ("/project tree [depth]", "显示文件树结构"),
889
+ ("/project grep <pattern>", "跨文件正则搜索"),
890
+ ("/project read <file>", "查看项目中的文件内容"),
891
+ ("/project status", "Git 状态 + 最近提交"),
892
+ ("/project info", "项目摘要(类型/语言/规模)"),
893
+ ("/project ask <question>", "向 AI 提问(使用项目上下文)"),
894
+ ("/project task <description>", "让 AI 执行任务(工具调用模式)"),
895
+ ("/project clear", "卸载当前项目上下文"),
896
+ ]
897
+ tb = _T(box=_box.MINIMAL)
898
+ tb.add_column("命令", style="cyan")
899
+ tb.add_column("说明", style="dim")
900
+ for cmd, desc in rows:
901
+ tb.add_row(cmd, desc)
902
+ self.context.console.print("\n[bold]/project — 项目分析命令[/bold]\n")
903
+ self.context.console.print(tb)
904
+ self.context.console.print()
905
+
906
+ async def cmd_init(self, args: str):
907
+ """Bootstrap an ARIA.md memory file, or scaffold a new project.
908
+
909
+ Without arguments: scans the current directory and generates ARIA.md.
910
+ With a template name: creates a fully-runnable project scaffold.
911
+
912
+ Usage:
913
+ /init — generate ARIA.md for current project
914
+ /init --force — regenerate even if ARIA.md already exists
915
+ /init list — list available scaffold templates
916
+ /init quant [dir] — quantitative strategy project
917
+ /init analysis [dir] — data analysis project
918
+ /init fastapi [dir] — FastAPI financial data service
919
+ /init dashboard [dir] — Plotly Dash interactive dashboard
920
+ """
921
+ global _PROJECT_CONTEXT
922
+ cwd = pathlib.Path.cwd()
923
+
924
+ # ── /init <template> — project scaffold ──────────────────────────────
925
+ _tmpl_key = args.strip().lower().split()[0] if args.strip() else ""
926
+ if _tmpl_key == "list":
927
+ rows = [(k, v["desc"]) for k, v in self._SCAFFOLD_TEMPLATES.items()]
928
+ if self.context.has_rich:
929
+ from rich.table import Table as _Table
930
+ t = _Table(box=None, show_header=True, header_style="bold cyan", padding=(0,2))
931
+ t.add_column("模板", style="green")
932
+ t.add_column("说明")
933
+ for k, d in rows:
934
+ t.add_row(k, d)
935
+ self.context.console.print("\n [bold]可用脚手架模板[/bold]")
936
+ self.context.console.print(t)
937
+ self.context.console.print("\n 用法: [cyan]/init <模板名> [目录名][/cyan]\n")
938
+ else:
939
+ print("可用模板:"); [print(f" {k}: {d}") for k, d in rows]
940
+ return
941
+
942
+ if _tmpl_key in self._SCAFFOLD_TEMPLATES:
943
+ tmpl = self._SCAFFOLD_TEMPLATES[_tmpl_key]
944
+ # Optional second arg: target directory name
945
+ _args_parts = args.strip().split()
946
+ _target_name = _args_parts[1] if len(_args_parts) > 1 else f"{_tmpl_key}_project"
947
+ _target_path = pathlib.Path(_target_name).expanduser()
948
+ if _target_path.is_absolute():
949
+ target_dir = _target_path
950
+ else:
951
+ from aria_code.artifacts import user_projects_dir as _user_projects_dir
952
+ target_dir = _user_projects_dir() / _target_name
953
+ if target_dir.exists():
954
+ self.context.console.print(f"[yellow]目录已存在: {target_dir}[/yellow]") if self.context.has_rich else print(f"目录已存在: {target_dir}")
955
+ else:
956
+ target_dir.mkdir(parents=True)
957
+ created = self._create_scaffold(target_dir, tmpl)
958
+ if self.context.has_rich:
959
+ from rich.panel import Panel as _SPanel
960
+ from rich import box as _sbox
961
+ lines = "\n".join(f" [dim]{pathlib.Path(p)}[/dim]" for p in created)
962
+ self.context.console.print(_SPanel(
963
+ f"[green]✅ 项目脚手架已创建[/green] [bold]{_target_name}[/bold]\n\n{lines}\n\n"
964
+ f"[dim]cd \"{target_dir}\" && pip install -r requirements.txt[/dim]",
965
+ title=f"[bold cyan]/init {_tmpl_key}[/bold cyan]",
966
+ border_style="cyan",
967
+ box=_sbox.ROUNDED,
968
+ padding=(1, 2),
969
+ ))
970
+ else:
971
+ print(f"✅ 创建: {target_dir}"); [print(f" {p}") for p in created]
972
+ # Optionally generate ARIA.md inside the new project
973
+ try:
974
+ from aria_code.apps.cli.project_aria import build_project_aria_md
975
+ _aria_tgt = target_dir / "ARIA.md"
976
+ if not _aria_tgt.exists():
977
+ _aria_tgt.write_text(
978
+ build_project_aria_md(
979
+ project_name=_target_name,
980
+ stack=tmpl["desc"],
981
+ entry="main.py",
982
+ purpose=f"由 /init {_tmpl_key} 生成的脚手架项目",
983
+ notes=[
984
+ "脚手架目录已经就绪,后续功能与服务应继续按模块拆分。",
985
+ ],
986
+ ),
987
+ encoding="utf-8",
988
+ )
989
+ except Exception:
990
+ pass
991
+ return
992
+ # ── /init [--force] — generate ARIA.md for current project ──────────
993
+
994
+ aria_md = cwd / "ARIA.md"
995
+ force = "--force" in args
996
+
997
+ if aria_md.exists() and not force:
998
+ msg = "ARIA.md already exists. Use /init --force to regenerate."
999
+ self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
1000
+ return
1001
+
1002
+ # Scan for common project signal files
1003
+ _SCAN_FILES = [
1004
+ "README.md", "README.rst", "README.txt",
1005
+ "package.json", "pyproject.toml", "setup.py", "setup.cfg",
1006
+ "requirements.txt", "Pipfile", "poetry.lock",
1007
+ "Cargo.toml", "go.mod", "pom.xml", "build.gradle",
1008
+ "Makefile", "Dockerfile", ".env.example",
1009
+ "CLAUDE.md", ".ariarc",
1010
+ ]
1011
+ snippets, found_files = [], []
1012
+ for fname in _SCAN_FILES:
1013
+ fp = cwd / fname
1014
+ if fp.exists():
1015
+ found_files.append(fname)
1016
+ try:
1017
+ snippets.append(f"### {fname}\n{fp.read_text(errors='replace')[:1200]}")
1018
+ except Exception:
1019
+ pass
1020
+
1021
+ code_exts = {".py", ".ts", ".js", ".go", ".rs", ".java", ".cpp", ".c"}
1022
+ code_files = sorted(
1023
+ f.name for f in cwd.iterdir()
1024
+ if f.is_file() and f.suffix in code_exts
1025
+ )[:10]
1026
+
1027
+ scan_summary = "\n\n".join(snippets[:5])
1028
+
1029
+ prompt = (
1030
+ f"分析以下项目信息,生成一个 ARIA.md 记忆文件。\n\n"
1031
+ f"目录: {cwd}\n"
1032
+ f"发现的配置文件: {', '.join(found_files) or '无'}\n"
1033
+ f"代码文件: {', '.join(code_files) or '无'}\n\n"
1034
+ f"文件内容:\n{scan_summary}\n\n"
1035
+ f"请生成符合以下格式的 ARIA.md(只输出文件内容本身,不加任何解释):\n\n"
1036
+ f"# Memory\n\n"
1037
+ f"- **Project**: <项目名称>\n"
1038
+ f"- **Stack**: <语言/框架>\n"
1039
+ f"- **Entry**: <主入口文件>\n"
1040
+ f"- **Conventions**: <代码规范或约定>\n"
1041
+ f"- **Notes**: <其他重要信息>\n"
1042
+ )
1043
+
1044
+ self.context.console.print("[dim]分析项目结构中...[/dim]") if self.context.has_rich else print("Analyzing project...")
1045
+ await self.terminal.send_message(prompt)
1046
+
1047
+ # Extract the last assistant response and write to ARIA.md
1048
+ if self.terminal.conversation:
1049
+ last_ai = next(
1050
+ (m["content"] for m in reversed(self.terminal.conversation)
1051
+ if m.get("role") == "assistant"),
1052
+ None,
1053
+ )
1054
+ if last_ai:
1055
+ content = _strip_markdown_fences(last_ai).strip()
1056
+ # Strip injected market-data blocks (lines starting with ## 📊 or *⚠️*)
1057
+ # that the market-data prefetch may have appended to the AI response.
1058
+ import re as _re_init
1059
+ content = _re_init.sub(
1060
+ r'\n*## 📊.*?(?=\n#|\Z)', '', content, flags=_re_init.DOTALL
1061
+ ).strip()
1062
+ content = _re_init.sub(r'\n*\*⚠️.*?\*\n*', '\n', content).strip()
1063
+ if not content.startswith("# Memory"):
1064
+ content = "# Memory\n\n" + content
1065
+ aria_md.write_text(content + "\n", encoding="utf-8")
1066
+ _PROJECT_CONTEXT = _load_project_context()
1067
+ msg = f"ARIA.md created at {aria_md}"
1068
+ self.context.console.print(f"\n[green]{msg}[/green]") if self.context.has_rich else print(f"\n{msg}")
1069
+
1070
+ async def cmd_setup(self, args: str):
1071
+ """Guided first-run setup wizard (Open Interpreter style).
1072
+
1073
+ Usage: /setup
1074
+ """
1075
+ import getpass as _gp
1076
+
1077
+ _is_interactive = sys.stdin.isatty()
1078
+
1079
+ if self.context.has_rich:
1080
+ self.context.console.print()
1081
+ self.context.console.print("[bold cyan]━━ Aria Setup Wizard ━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]")
1082
+ self.context.console.print()
1083
+
1084
+ # ── Step 1: Detect LOCAL backends only (not cloud LLM providers) ───────
1085
+ _LOCAL_BACKENDS_ONLY = {"ollama", "lmstudio", "vllm", "llamacpp", "jan"}
1086
+ try:
1087
+ from aria_code.local_llm_provider import probe_all_backends, BACKEND_DEFAULTS
1088
+ _all_backends = probe_all_backends()
1089
+ # Filter to only true local backends — cloud providers appear in Step 3
1090
+ backends = {k: v for k, v in _all_backends.items() if k in _LOCAL_BACKENDS_ONLY}
1091
+ except ImportError:
1092
+ backends = {}
1093
+
1094
+ self.context.console.print(" [bold]Step 1/4 · 本地 Backend[/bold]") if self.context.has_rich else print("Step 1: Local Backends")
1095
+ ollama_online = backends.get("ollama", False)
1096
+ for name, ok in backends.items():
1097
+ icon = "✅" if ok else "○"
1098
+ color = "green" if ok else "dim"
1099
+ url = BACKEND_DEFAULTS.get(name, {}).get("default_url", "") if "BACKEND_DEFAULTS" in dir() else ""
1100
+ if self.context.has_rich:
1101
+ self.context.console.print(f" {icon} [{color}]{name:12s}[/{color}] [dim]{url}[/dim]")
1102
+ else:
1103
+ print(f" {'✓' if ok else '✗'} {name:12s} {url}")
1104
+ self.context.console.print() if self.context.has_rich else print()
1105
+
1106
+ # ── Step 2: Pick default Ollama model (if Ollama online) ────────────
1107
+ if ollama_online and _is_interactive:
1108
+ self.context.console.print(" [bold]Step 2/4 · 选择默认本地模型[/bold]") if self.context.has_rich else print("Step 2: Default model")
1109
+ rich_models, _ = detect_ollama_models_rich(
1110
+ self.terminal.config.get("ollama_url", "http://localhost:11434")
1111
+ )
1112
+ if rich_models:
1113
+ model_names = [m["name"] for m in rich_models]
1114
+ current_id = self.terminal.config.get("model", "")
1115
+ sel_idx = next((i for i, n in enumerate(model_names) if n == current_id), 0)
1116
+ options = [(f" {n}", "") for n in model_names]
1117
+ picked = _arrow_select(options, sel_idx, "选择默认模型")
1118
+ if picked is not None:
1119
+ chosen = model_names[picked]
1120
+ self.terminal.config["model"] = chosen
1121
+ self.context.save_config(self.terminal.config)
1122
+ msg = f"✓ 默认模型设为 {chosen}"
1123
+ self.context.console.print(f" [green]{msg}[/green]") if self.context.has_rich else print(f" {msg}")
1124
+ self.context.console.print() if self.context.has_rich else print()
1125
+ else:
1126
+ self.context.console.print(" [dim]Step 2/4 · (Ollama 未运行,跳过模型选择)[/dim]") if self.context.has_rich else print(" Skipping model select (Ollama offline)")
1127
+ self.context.console.print() if self.context.has_rich else print()
1128
+
1129
+ # ── Step 3: Cloud API keys ───────────────────────────────────────────
1130
+ self.context.console.print(" [bold]Step 3/4 · Cloud API Key 配置[/bold]") if self.context.has_rich else print("Step 3: Cloud API Keys")
1131
+ _SETUP_PROVIDERS = [
1132
+ ("deepseek", "DeepSeek", "推荐:deepseek-chat,性价比最高"),
1133
+ ("openai", "OpenAI", "GPT-4o,o1等"),
1134
+ ("groq", "Groq", "免费 llama/mixtral 推理,极速"),
1135
+ ("anthropic", "Anthropic", "Claude 3.5/3.7"),
1136
+ ]
1137
+ for prov, label, desc in _SETUP_PROVIDERS:
1138
+ existing_key = _get_provider_key(prov)
1139
+ if existing_key:
1140
+ masked = existing_key[:6] + "****" + existing_key[-4:]
1141
+ self.context.console.print(f" 🔑 {label:12s} [dim]已配置 ({masked})[/dim]") if self.context.has_rich else print(f" {label}: 已配置")
1142
+ continue
1143
+ if _is_interactive:
1144
+ self.context.console.print(f" [cyan]{label}[/cyan] [dim]({desc})[/dim]") if self.context.has_rich else print(f" {label}: {desc}")
1145
+ try:
1146
+ key = _gp.getpass(f" Enter {label} API key (留空跳过): ").strip()
1147
+ except Exception:
1148
+ key = ""
1149
+ if key:
1150
+ self.cmd_apikey(f"set {prov} {key}")
1151
+ else:
1152
+ self.context.console.print(f" ○ {label:12s} [dim]未配置 → /apikey set {prov} <key>[/dim]") if self.context.has_rich else print(f" {label}: not configured")
1153
+ self.context.console.print() if self.context.has_rich else print()
1154
+
1155
+ # ── Step 3.5: Data Service API keys ──────────────────────────────────
1156
+ self.context.console.print(" [bold]Step 3.5/4 · 市场数据服务 Key(后端离线时使用)[/bold]") if self.context.has_rich else print("Step 3.5: Data Service Keys")
1157
+ _SETUP_DATA = [
1158
+ ("finnhub", "Finnhub", "股票实时行情+新闻", "https://finnhub.io/register"),
1159
+ ("newsapi", "NewsAPI", "财经新闻聚合", "https://newsapi.org/register"),
1160
+ ("brave", "Brave Search", "网页搜索", "https://api.search.brave.com/app/keys"),
1161
+ ("alphavantage", "Alpha Vantage", "股票历史数据", "https://www.alphavantage.co/support/#api-key"),
1162
+ ]
1163
+ _existing_data = _load_data_keys()
1164
+ for svc, label, desc, signup_url in _SETUP_DATA:
1165
+ existing_key = _existing_data.get(svc, "")
1166
+ if existing_key:
1167
+ masked = existing_key[:6] + "****" + existing_key[-4:]
1168
+ self.context.console.print(f" 🔑 {label:16s} [dim]已配置 ({masked})[/dim]") if self.context.has_rich else print(f" {label}: configured")
1169
+ continue
1170
+ if _is_interactive:
1171
+ self.context.console.print(f" [cyan]{label}[/cyan] [dim]({desc})[/dim]") if self.context.has_rich else print(f" {label}: {desc}")
1172
+ self.context.console.print(f" [dim]注册:{signup_url}[/dim]") if self.context.has_rich else print(f" Register: {signup_url}")
1173
+ try:
1174
+ key = _gp.getpass(f" Enter {label} API key (留空跳过): ").strip()
1175
+ except Exception:
1176
+ key = ""
1177
+ if key:
1178
+ self.cmd_apikey(f"set {svc} {key}")
1179
+ else:
1180
+ if self.context.has_rich:
1181
+ self.context.console.print(f" ○ {label:16s} [dim]未配置 → /apikey set {svc} <key>[/dim]")
1182
+ self.context.console.print(f" [dim]注册:{signup_url}[/dim]")
1183
+ else:
1184
+ print(f" {label}: not configured → /apikey set {svc} <key>")
1185
+ self.context.console.print() if self.context.has_rich else print()
1186
+
1187
+ # ── Step 3.8: MCP servers ────────────────────────────────────────────
1188
+ sub = args.strip().lower()
1189
+ if sub in ("mcp", "all"):
1190
+ self.context.console.print(" [bold]Step 3.8/4 · MCP 服务器[/bold]") if self.context.has_rich else print("Step 3.8: MCP Servers")
1191
+ _mcp_cfg_path = aria_home() / "mcp_servers.json"
1192
+ if _mcp_cfg_path.exists():
1193
+ try:
1194
+ import json as _j2
1195
+ _mcp_data = _j2.loads(_mcp_cfg_path.read_text())
1196
+ _servers = _mcp_data.get("servers", [])
1197
+ enabled_srv = [s for s in _servers if s.get("enabled", False)]
1198
+ disabled_srv = [s for s in _servers if not s.get("enabled", True)]
1199
+ for s in enabled_srv:
1200
+ self.context.console.print(f" ✅ {s['name']:16s} [dim]{s.get('description','')[:50]}[/dim]") if self.context.has_rich else print(f" ✓ {s['name']}")
1201
+ for s in disabled_srv:
1202
+ note = s.get("_setup", "")
1203
+ self.context.console.print(f" ○ {s['name']:16s} [dim]{s.get('description','')[:50]}[/dim]") if self.context.has_rich else print(f" ✗ {s['name']}")
1204
+ if note:
1205
+ self.context.console.print(f" [dim]安装: {note}[/dim]") if self.context.has_rich else print(f" Setup: {note}")
1206
+ if disabled_srv:
1207
+ self.context.console.print() if self.context.has_rich else print()
1208
+ self.context.console.print(" [dim]安装后编辑 ~/.arthera/mcp_servers.json 将对应项 enabled 改为 true[/dim]") if self.context.has_rich else print(" Edit mcp_servers.json: set enabled=true after installing")
1209
+ except Exception:
1210
+ pass
1211
+ self.context.console.print() if self.context.has_rich else print()
1212
+
1213
+ # ── Step 4: Messaging channels (Feishu / Telegram) ──────────────────
1214
+ if sub in ("feishu", "telegram", "notify", "all", ""):
1215
+ self.context.console.print(" [bold]Step 4/5 · 消息通知连接[/bold]") if self.context.has_rich else print("Step 4: Messaging")
1216
+ _env_path = pathlib.Path.home() / ".aria" / ".env"
1217
+ _env_vars: dict = {}
1218
+ if _env_path.exists():
1219
+ for _line in _env_path.read_text().splitlines():
1220
+ if "=" in _line and not _line.startswith("#"):
1221
+ k, _, v = _line.partition("=")
1222
+ _env_vars[k.strip()] = v.strip()
1223
+
1224
+ # Feishu status
1225
+ _fs_mode = _env_vars.get("ARIA_RELAY_MODE", "")
1226
+ _fs_id = _env_vars.get("ARIA_RELAY_CLIENT_ID", "")
1227
+ _fs_app = _env_vars.get("FEISHU_APP_ID", "")
1228
+ if _fs_mode == "relay" and _fs_id:
1229
+ _fs_status = f"[green]✓ 中继模式[/green] ID: {_fs_id[:12]}…"
1230
+ elif _fs_mode == "own_app" and _fs_app:
1231
+ _fs_status = f"[green]✓ 自建应用[/green] {_fs_app}"
1232
+ else:
1233
+ _fs_status = "[dim]未配置[/dim] → /setup feishu"
1234
+
1235
+ # Telegram status
1236
+ _tg_token = _env_vars.get("TELEGRAM_BOT_TOKEN", "")
1237
+ _tg_ids = _env_vars.get("TELEGRAM_ALLOWED_IDS", "")
1238
+ if _tg_token and _tg_token != "your_bot_token_here":
1239
+ _tg_status = f"[green]✓ 已配置[/green] Chat IDs: {_tg_ids or '(未设置)'}"
1240
+ else:
1241
+ _tg_status = "[dim]未配置[/dim] → /setup telegram"
1242
+
1243
+ if self.context.has_rich:
1244
+ self.context.console.print(f" 飞书 {_fs_status}")
1245
+ self.context.console.print(f" Telegram {_tg_status}")
1246
+ else:
1247
+ print(f" Feishu: {_fs_mode or 'not configured'}")
1248
+ print(f" Telegram: {'configured' if _tg_token else 'not configured'}")
1249
+
1250
+ # Sub-command: launch wizard for just this channel
1251
+ if sub in ("feishu", "telegram"):
1252
+ self.context.console.print() if self.context.has_rich else print()
1253
+ try:
1254
+ # setup_wizard 是随包发布的顶层模块(见 pyproject py-modules),
1255
+ # 直接 import 即可。原先用 __file__ 往上数四层拼路径:这个方法
1256
+ # 的 __globals__ 被 _rebind_mixin_globals 换成了 aria_cli 的,
1257
+ # __file__ 因此指向仓库根的 aria_cli.py 而非本文件,四层 parent
1258
+ # 算出 /Users/setup_wizard.py,永远不存在——异常被下面的
1259
+ # except 吞掉,这条分支一直静默退化成"请手动运行"提示。
1260
+ from aria_code import setup_wizard as _wiz
1261
+ _e = _wiz._load_env()
1262
+ if sub == "feishu":
1263
+ _wiz.setup_feishu(_e)
1264
+ else:
1265
+ _wiz.setup_telegram(_e)
1266
+ _wiz._save_env(_e)
1267
+ except Exception as _we:
1268
+ _fallback_flag = "--feishu" if sub == "feishu" else "--telegram"
1269
+ if self.context.has_rich:
1270
+ self.context.console.print(f" [yellow]请运行: python3 -m aria_code.setup_wizard {_fallback_flag}[/yellow]")
1271
+ else:
1272
+ print(f" Run: python3 -m aria_code.setup_wizard {_fallback_flag}")
1273
+ return
1274
+
1275
+ self.context.console.print() if self.context.has_rich else print()
1276
+
1277
+ # ── Step 5: Summary ─────────────────────────────────────────────────
1278
+ self.context.console.print(" [bold]Step 5/5 · 配置完成[/bold]") if self.context.has_rich else print("Step 5: Done")
1279
+ model = self.terminal.config.get("model", "?")
1280
+ provider = self.terminal.config.get("local_provider", "ollama")
1281
+ self.context.console.print(f" 模型: [cyan]{model}[/cyan] Provider: [cyan]{provider}[/cyan]") if self.context.has_rich else print(f" Model: {model} Provider: {provider}")
1282
+ self.context.console.print() if self.context.has_rich else print()
1283
+ self.context.console.print(
1284
+ " [dim]提示: /model — 切换模型 /providers — 查看所有 provider\n"
1285
+ " /setup feishu — 配置飞书 /setup telegram — 配置 Telegram[/dim]"
1286
+ ) if self.context.has_rich else print(" Tip: /model /providers /setup feishu /setup telegram")
1287
+ self.context.console.print("[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]") if self.context.has_rich else print("─" * 50)
1288
+ self.context.console.print() if self.context.has_rich else print()
1289
+
1290
+ def cmd_memory(self, args: str):
1291
+ """Manage persistent memory: project ARIA.md and global user profile.
1292
+
1293
+ Usage:
1294
+ /memory show — display current project ARIA.md
1295
+ /memory add <fact> — append fact to project ARIA.md
1296
+ /memory clear — wipe project ARIA.md memory section
1297
+ /memory search <query> — search across ARIA.md + sessions
1298
+ /memory profile — show global ~/.arthera/ARIA.md (injected every session)
1299
+ /memory profile add <text> — append to global profile
1300
+ /memory profile clear — reset global profile
1301
+ /memory global — legacy global Memory entries
1302
+ """
1303
+ global _PROJECT_CONTEXT
1304
+ aria_md = pathlib.Path.cwd() / "ARIA.md"
1305
+ parts = args.strip().split(maxsplit=1)
1306
+ sub = parts[0].lower() if parts else "show"
1307
+ rest = parts[1].strip() if len(parts) > 1 else ""
1308
+
1309
+ if sub == "show":
1310
+ if not aria_md.exists():
1311
+ msg = f"No ARIA.md in {pathlib.Path.cwd()}"
1312
+ self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
1313
+ return
1314
+ content = aria_md.read_text(encoding="utf-8")
1315
+ if self.context.has_rich:
1316
+ try:
1317
+ from rich.markdown import Markdown as _RMd
1318
+ self.context.console.print(_RMd(content))
1319
+ except Exception:
1320
+ self.context.console.print(content)
1321
+ else:
1322
+ print(content)
1323
+
1324
+ elif sub == "add":
1325
+ if not rest:
1326
+ self.context.console.print("[dim]Usage: /memory add <fact>[/dim]") if self.context.has_rich else print("Usage: /memory add <fact>")
1327
+ return
1328
+ self.cmd_note(rest)
1329
+
1330
+ elif sub == "clear":
1331
+ if aria_md.exists():
1332
+ aria_md.write_text("# Memory\n\n", encoding="utf-8")
1333
+ _PROJECT_CONTEXT = _load_project_context()
1334
+ self.context.console.print("[dim]Memory cleared.[/dim]") if self.context.has_rich else print("Memory cleared.")
1335
+ else:
1336
+ self.context.console.print("[dim]Nothing to clear.[/dim]") if self.context.has_rich else print("Nothing to clear.")
1337
+
1338
+ elif sub == "search":
1339
+ # Semantic search in ARIA.md and strategy vault using simple grep
1340
+ # (ChromaDB RAG upgrade planned for Phase 2)
1341
+ if not rest:
1342
+ self.context.console.print("[dim]Usage: /memory search <query>[/dim]") if self.context.has_rich else print("Usage: /memory search <query>")
1343
+ return
1344
+ query_low = rest.lower()
1345
+ results = []
1346
+ # 1. Search ARIA.md
1347
+ if aria_md.exists():
1348
+ for line in aria_md.read_text(encoding="utf-8").splitlines():
1349
+ if query_low in line.lower() and line.strip():
1350
+ results.append(("ARIA.md", line.strip()))
1351
+ # 2. Search session history titles
1352
+ for sess_file in sorted(_get_SESSIONS_DIR().glob("*.json"), key=lambda p: -p.stat().st_mtime)[:20]:
1353
+ try:
1354
+ sess = json.loads(sess_file.read_text(encoding="utf-8"))
1355
+ title = sess.get("metadata", {}).get("title", "")
1356
+ if query_low in title.lower():
1357
+ results.append(("Session", title[:80]))
1358
+ except Exception:
1359
+ pass
1360
+ # 3. Search strategy vault
1361
+ try:
1362
+ from aria_code.strategy_vault import get_vault as _gv
1363
+ vault = _gv()
1364
+ for s in (vault.list() or []):
1365
+ name = str(s.get("name", ""))
1366
+ msg = str(s.get("message", ""))
1367
+ if query_low in name.lower() or query_low in msg.lower():
1368
+ results.append(("Strategy", f"{name}: {msg[:60]}"))
1369
+ except Exception:
1370
+ pass
1371
+
1372
+ if results:
1373
+ if self.context.has_rich:
1374
+ self.context.console.print()
1375
+ self.context.console.print(f" [bold]记忆搜索: '{rest}'[/bold] [dim]{len(results)} 条结果[/dim]")
1376
+ self.context.console.print()
1377
+ for src, text in results[:15]:
1378
+ self.context.console.print(f" [dim]{src:<12s}[/dim] {text}")
1379
+ self.context.console.print()
1380
+ else:
1381
+ print(f" Search '{rest}': {len(results)} results")
1382
+ for src, text in results[:15]:
1383
+ print(f" [{src}] {text}")
1384
+ else:
1385
+ msg = f"未找到与 '{rest}' 相关的记忆"
1386
+ self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
1387
+
1388
+ elif sub == "profile":
1389
+ # Per-user ARIA.md at ~/.arthera/ARIA.md — injected into every session
1390
+ _profile_path = aria_home() / "ARIA.md"
1391
+ gparts = rest.strip().split(maxsplit=1)
1392
+ gsub = gparts[0].lower() if gparts else "show"
1393
+ grest = gparts[1].strip() if len(gparts) > 1 else ""
1394
+
1395
+ if gsub == "show":
1396
+ if not _profile_path.exists():
1397
+ if self.context.has_rich:
1398
+ self.context.console.print("[dim]~/.arthera/ARIA.md 还不存在。用 /memory profile add <内容> 创建。[/dim]")
1399
+ else:
1400
+ print("~/.arthera/ARIA.md not found. Use /memory profile add <text> to create.")
1401
+ return
1402
+ content = _profile_path.read_text(encoding="utf-8")
1403
+ if self.context.has_rich:
1404
+ try:
1405
+ from rich.markdown import Markdown as _RMd3
1406
+ self.context.console.print()
1407
+ self.context.console.print(" [dim]~/.arthera/ARIA.md[/dim]")
1408
+ self.context.console.print(_RMd3(content))
1409
+ except Exception:
1410
+ self.context.console.print(content)
1411
+ else:
1412
+ print(content)
1413
+
1414
+ elif gsub == "add":
1415
+ if not grest:
1416
+ self.context.console.print("[dim]Usage: /memory profile add <内容>[/dim]") if self.context.has_rich else print("Usage: /memory profile add <text>")
1417
+ return
1418
+ _profile_path.parent.mkdir(parents=True, exist_ok=True)
1419
+ now_str = datetime.now().strftime("%Y-%m-%d")
1420
+ if _profile_path.exists():
1421
+ existing = _profile_path.read_text(encoding="utf-8")
1422
+ if "## 偏好与背景" not in existing and "## Preferences" not in existing:
1423
+ existing += "\n\n## 偏好与背景\n"
1424
+ existing += f"\n- [{now_str}] {grest}"
1425
+ _profile_path.write_text(existing, encoding="utf-8")
1426
+ else:
1427
+ _profile_path.write_text(
1428
+ f"# 用户背景\n\n## 偏好与背景\n\n- [{now_str}] {grest}\n",
1429
+ encoding="utf-8",
1430
+ )
1431
+ # Refresh project context so change takes effect immediately
1432
+ _PROJECT_CONTEXT = _load_project_context()
1433
+ if self.context.has_rich:
1434
+ self.context.console.print(" [dim]✓ 已写入 ~/.arthera/ARIA.md — 下次对话自动注入[/dim]")
1435
+ else:
1436
+ print("Saved to ~/.arthera/ARIA.md")
1437
+
1438
+ elif gsub == "clear":
1439
+ if _profile_path.exists():
1440
+ _profile_path.write_text("# 用户背景\n\n", encoding="utf-8")
1441
+ _PROJECT_CONTEXT = _load_project_context()
1442
+ self.context.console.print("[dim]~/.arthera/ARIA.md 已清空。[/dim]") if self.context.has_rich else print("Profile cleared.")
1443
+ else:
1444
+ self.context.console.print("[dim]文件不存在,无需清空。[/dim]") if self.context.has_rich else print("Nothing to clear.")
1445
+
1446
+ else:
1447
+ if self.context.has_rich:
1448
+ self.context.console.print("[dim]Usage: /memory profile [show|add <内容>|clear][/dim]")
1449
+ else:
1450
+ print("Usage: /memory profile [show|add <text>|clear]")
1451
+
1452
+ elif sub == "global":
1453
+ # Global user memory (cross-project, cross-session)
1454
+ if not self.memory_mgr:
1455
+ self.context.console.print("[dim]Memory manager not available.[/dim]") if self.context.has_rich else print("Memory manager not available.")
1456
+ return
1457
+ gparts = rest.strip().split(maxsplit=1)
1458
+ gsub = gparts[0].lower() if gparts else "show"
1459
+ grest = gparts[1].strip() if len(gparts) > 1 else ""
1460
+
1461
+ if gsub == "show":
1462
+ entries = self.memory_mgr.list_all()
1463
+ if not entries:
1464
+ self.context.console.print("[dim]全局 Memory 为空。用 /memory global add <内容> 添加。[/dim]") if self.context.has_rich else print("Global memory is empty.")
1465
+ return
1466
+ if self.context.has_rich:
1467
+ from rich.markdown import Markdown as _RMd2
1468
+ for e in entries:
1469
+ self.context.console.print(f"\n[bold cyan]{e['title']}[/bold cyan] [dim]{e['file']}[/dim]")
1470
+ self.context.console.print(_RMd2(e["content"]) if e["content"] else "[dim](empty)[/dim]")
1471
+ else:
1472
+ for e in entries:
1473
+ print(f"\n## {e['title']}\n{e['content']}")
1474
+
1475
+ elif gsub == "add":
1476
+ if not grest:
1477
+ self.context.console.print("[dim]Usage: /memory global add <内容>[/dim]") if self.context.has_rich else print("Usage: /memory global add <content>")
1478
+ return
1479
+ self.memory_mgr.append("user_profile", grest, title="User Profile")
1480
+ self.context.console.print(f"[dim]已写入全局 Memory: {grest[:60]}[/dim]") if self.context.has_rich else print(f"Saved: {grest[:60]}")
1481
+
1482
+ elif gsub == "clear":
1483
+ n = self.memory_mgr.clear_all()
1484
+ self.context.console.print(f"[dim]全局 Memory 已清空(删除 {n} 个文件)。[/dim]") if self.context.has_rich else print(f"Global memory cleared ({n} files).")
1485
+
1486
+ else:
1487
+ self.context.console.print("[dim]Usage: /memory global [show|add <内容>|clear][/dim]") if self.context.has_rich else print("Usage: /memory global [show|add|clear]")
1488
+
1489
+ else:
1490
+ if self.context.has_rich:
1491
+ self.context.console.print("[dim]Usage: /memory [show|add <fact>|clear|search <query>|profile|global][/dim]")
1492
+ self.context.console.print("[dim] /memory profile add <内容> — 写入全局用户背景(每次会话自动注入)[/dim]")
1493
+ else:
1494
+ print("Usage: /memory [show|add <fact>|clear|search <query>|profile|global]")