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,432 @@
1
+ """Durable, conflict-aware file checkpoints for agent write operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import sqlite3
9
+ import stat
10
+ import time
11
+ import uuid
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Iterable, Optional
15
+
16
+ from .run_store import RunStore
17
+
18
+
19
+ def _sha256(content: str) -> str:
20
+ return hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
21
+
22
+
23
+ def _json_dumps(value: Any) -> str:
24
+ return json.dumps(value, ensure_ascii=False, default=str, separators=(",", ":"))
25
+
26
+
27
+ def _json_loads(value: str | None) -> Dict[str, Any]:
28
+ if not value:
29
+ return {}
30
+ try:
31
+ loaded = json.loads(value)
32
+ return loaded if isinstance(loaded, dict) else {"value": loaded}
33
+ except (TypeError, json.JSONDecodeError):
34
+ return {}
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class CheckpointFile:
39
+ checkpoint_id: str
40
+ path: str
41
+ existed_before: bool
42
+ before_content: str
43
+ after_content: str
44
+ before_hash: str
45
+ after_hash: str
46
+ before_mode: Optional[int]
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class CheckpointRecord:
51
+ checkpoint_id: str
52
+ sequence: int
53
+ run_id: Optional[str]
54
+ session_id: str
55
+ source: str
56
+ label: str
57
+ status: str
58
+ created_at: float
59
+ restored_at: Optional[float]
60
+ metadata: Dict[str, Any] = field(default_factory=dict)
61
+ files: tuple[CheckpointFile, ...] = ()
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class RestoreResult:
66
+ checkpoint_ids: tuple[str, ...]
67
+ run_id: Optional[str]
68
+ restored_paths: tuple[str, ...]
69
+
70
+
71
+ class CheckpointNotFoundError(KeyError):
72
+ """Raised when a checkpoint or checkpoint group cannot be found."""
73
+
74
+
75
+ class CheckpointConflictError(RuntimeError):
76
+ """Raised when a file changed after the checkpoint was recorded."""
77
+
78
+
79
+ class CheckpointStore:
80
+ """Store file pre-images and restore them only when hashes still match."""
81
+
82
+ def __init__(self, database_path: Path | str | None = None) -> None:
83
+ self.run_store = RunStore(database_path)
84
+ self.database_path = self.run_store.database_path
85
+ self._initialize()
86
+
87
+ def _connect(self) -> sqlite3.Connection:
88
+ connection = sqlite3.connect(str(self.database_path), timeout=10.0)
89
+ connection.row_factory = sqlite3.Row
90
+ connection.execute("PRAGMA foreign_keys = ON")
91
+ connection.execute("PRAGMA busy_timeout = 10000")
92
+ return connection
93
+
94
+ def _initialize(self) -> None:
95
+ with self._connect() as connection:
96
+ connection.executescript(
97
+ """
98
+ CREATE TABLE IF NOT EXISTS checkpoints (
99
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
100
+ checkpoint_id TEXT NOT NULL UNIQUE,
101
+ run_id TEXT,
102
+ session_id TEXT NOT NULL DEFAULT '',
103
+ source TEXT NOT NULL,
104
+ label TEXT NOT NULL DEFAULT '',
105
+ status TEXT NOT NULL DEFAULT 'active',
106
+ created_at REAL NOT NULL,
107
+ restored_at REAL,
108
+ metadata_json TEXT NOT NULL DEFAULT '{}',
109
+ FOREIGN KEY(run_id) REFERENCES runs(run_id)
110
+ );
111
+
112
+ CREATE TABLE IF NOT EXISTS checkpoint_files (
113
+ checkpoint_id TEXT NOT NULL,
114
+ path TEXT NOT NULL,
115
+ existed_before INTEGER NOT NULL,
116
+ before_content TEXT NOT NULL,
117
+ after_content TEXT NOT NULL,
118
+ before_hash TEXT NOT NULL,
119
+ after_hash TEXT NOT NULL,
120
+ before_mode INTEGER,
121
+ PRIMARY KEY(checkpoint_id, path),
122
+ FOREIGN KEY(checkpoint_id)
123
+ REFERENCES checkpoints(checkpoint_id) ON DELETE CASCADE
124
+ );
125
+
126
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_run_sequence
127
+ ON checkpoints(run_id, sequence DESC);
128
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_session_sequence
129
+ ON checkpoints(session_id, sequence DESC);
130
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_status_sequence
131
+ ON checkpoints(status, sequence DESC);
132
+ """
133
+ )
134
+
135
+ @staticmethod
136
+ def _row_to_file(row: sqlite3.Row) -> CheckpointFile:
137
+ return CheckpointFile(
138
+ checkpoint_id=row["checkpoint_id"],
139
+ path=row["path"],
140
+ existed_before=bool(row["existed_before"]),
141
+ before_content=row["before_content"],
142
+ after_content=row["after_content"],
143
+ before_hash=row["before_hash"],
144
+ after_hash=row["after_hash"],
145
+ before_mode=int(row["before_mode"]) if row["before_mode"] is not None else None,
146
+ )
147
+
148
+ def _row_to_checkpoint(
149
+ self,
150
+ connection: sqlite3.Connection,
151
+ row: sqlite3.Row,
152
+ ) -> CheckpointRecord:
153
+ file_rows = connection.execute(
154
+ "SELECT * FROM checkpoint_files WHERE checkpoint_id = ? ORDER BY path",
155
+ (row["checkpoint_id"],),
156
+ ).fetchall()
157
+ return CheckpointRecord(
158
+ checkpoint_id=row["checkpoint_id"],
159
+ sequence=int(row["sequence"]),
160
+ run_id=row["run_id"],
161
+ session_id=row["session_id"],
162
+ source=row["source"],
163
+ label=row["label"],
164
+ status=row["status"],
165
+ created_at=float(row["created_at"]),
166
+ restored_at=float(row["restored_at"]) if row["restored_at"] is not None else None,
167
+ metadata=_json_loads(row["metadata_json"]),
168
+ files=tuple(self._row_to_file(file_row) for file_row in file_rows),
169
+ )
170
+
171
+ def record_change(
172
+ self,
173
+ *,
174
+ path: Path | str,
175
+ before_content: str,
176
+ after_content: str,
177
+ existed_before: bool,
178
+ source: str,
179
+ run_id: str | None = None,
180
+ session_id: str = "",
181
+ before_mode: int | None = None,
182
+ label: str = "",
183
+ metadata: Dict[str, Any] | None = None,
184
+ ) -> CheckpointRecord:
185
+ target = Path(path).expanduser().resolve()
186
+ if not target.is_file():
187
+ raise CheckpointConflictError(f"Written file is missing: {target}")
188
+ current = target.read_text(encoding="utf-8", errors="replace")
189
+ after_hash = _sha256(after_content)
190
+ if _sha256(current) != after_hash:
191
+ raise CheckpointConflictError(
192
+ f"File changed before checkpoint could be recorded: {target}"
193
+ )
194
+ if run_id and self.run_store.get_run(run_id) is None:
195
+ raise CheckpointNotFoundError(f"Unknown run id: {run_id}")
196
+
197
+ checkpoint_id = uuid.uuid4().hex[:16]
198
+ created_at = time.time()
199
+ mode = before_mode
200
+ if existed_before and mode is None:
201
+ mode = stat.S_IMODE(target.stat().st_mode)
202
+ with self._connect() as connection:
203
+ connection.execute("BEGIN IMMEDIATE")
204
+ connection.execute(
205
+ """INSERT INTO checkpoints
206
+ (checkpoint_id, run_id, session_id, source, label, status,
207
+ created_at, restored_at, metadata_json)
208
+ VALUES (?, ?, ?, ?, ?, 'active', ?, NULL, ?)""",
209
+ (
210
+ checkpoint_id,
211
+ run_id,
212
+ session_id,
213
+ source,
214
+ label,
215
+ created_at,
216
+ _json_dumps(metadata or {}),
217
+ ),
218
+ )
219
+ connection.execute(
220
+ """INSERT INTO checkpoint_files
221
+ (checkpoint_id, path, existed_before, before_content,
222
+ after_content, before_hash, after_hash, before_mode)
223
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
224
+ (
225
+ checkpoint_id,
226
+ str(target),
227
+ int(existed_before),
228
+ before_content,
229
+ after_content,
230
+ _sha256(before_content),
231
+ after_hash,
232
+ mode,
233
+ ),
234
+ )
235
+ row = connection.execute(
236
+ "SELECT * FROM checkpoints WHERE checkpoint_id = ?",
237
+ (checkpoint_id,),
238
+ ).fetchone()
239
+
240
+ if run_id:
241
+ self.run_store.append_event(run_id, "checkpoint_created", {
242
+ "checkpoint_id": checkpoint_id,
243
+ "path": str(target),
244
+ "source": source,
245
+ })
246
+ assert row is not None
247
+ with self._connect() as connection:
248
+ return self._row_to_checkpoint(connection, row)
249
+
250
+ def get(self, checkpoint_id: str) -> Optional[CheckpointRecord]:
251
+ key = (checkpoint_id or "").strip()
252
+ with self._connect() as connection:
253
+ row = connection.execute(
254
+ "SELECT * FROM checkpoints WHERE checkpoint_id = ?",
255
+ (key,),
256
+ ).fetchone()
257
+ if row is None and key:
258
+ rows = connection.execute(
259
+ "SELECT * FROM checkpoints WHERE checkpoint_id LIKE ? ORDER BY sequence DESC LIMIT 2",
260
+ (f"{key}%",),
261
+ ).fetchall()
262
+ row = rows[0] if len(rows) == 1 else None
263
+ return self._row_to_checkpoint(connection, row) if row is not None else None
264
+
265
+ def list(
266
+ self,
267
+ *,
268
+ run_id: str | None = None,
269
+ session_id: str | None = None,
270
+ status: str | None = "active",
271
+ limit: int = 50,
272
+ ) -> list[CheckpointRecord]:
273
+ clauses = []
274
+ values: list[Any] = []
275
+ if run_id:
276
+ clauses.append("run_id = ?")
277
+ values.append(run_id)
278
+ if session_id:
279
+ clauses.append("session_id = ?")
280
+ values.append(session_id)
281
+ if status:
282
+ clauses.append("status = ?")
283
+ values.append(status)
284
+ where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
285
+ values.append(max(1, int(limit)))
286
+ with self._connect() as connection:
287
+ rows = connection.execute(
288
+ f"SELECT * FROM checkpoints{where} ORDER BY sequence DESC LIMIT ?",
289
+ values,
290
+ ).fetchall()
291
+ return [self._row_to_checkpoint(connection, row) for row in rows]
292
+
293
+ def latest_run_id(self, *, session_id: str | None = None) -> Optional[str]:
294
+ clauses = ["status = 'active'", "run_id IS NOT NULL"]
295
+ values: list[Any] = []
296
+ if session_id:
297
+ clauses.append("session_id = ?")
298
+ values.append(session_id)
299
+ with self._connect() as connection:
300
+ row = connection.execute(
301
+ f"SELECT run_id FROM checkpoints WHERE {' AND '.join(clauses)} "
302
+ "ORDER BY sequence DESC LIMIT 1",
303
+ values,
304
+ ).fetchone()
305
+ return row["run_id"] if row is not None else None
306
+
307
+ @staticmethod
308
+ def _snapshot(path: Path) -> tuple[bool, str, Optional[int]]:
309
+ if not path.exists():
310
+ return False, "", None
311
+ if not path.is_file():
312
+ raise CheckpointConflictError(f"Checkpoint path is not a file: {path}")
313
+ return (
314
+ True,
315
+ path.read_text(encoding="utf-8", errors="replace"),
316
+ stat.S_IMODE(path.stat().st_mode),
317
+ )
318
+
319
+ @staticmethod
320
+ def _write_atomic(path: Path, content: str, mode: int | None) -> None:
321
+ path.parent.mkdir(parents=True, exist_ok=True)
322
+ temporary = path.with_name(f".{path.name}.aria-rewind-{uuid.uuid4().hex[:8]}")
323
+ try:
324
+ temporary.write_text(content, encoding="utf-8")
325
+ if mode is not None:
326
+ temporary.chmod(mode)
327
+ os.replace(temporary, path)
328
+ finally:
329
+ if temporary.exists():
330
+ temporary.unlink()
331
+
332
+ def _restore(self, checkpoints: Iterable[CheckpointRecord]) -> RestoreResult:
333
+ records = list(checkpoints)
334
+ if not records:
335
+ raise CheckpointNotFoundError("No active checkpoints to restore")
336
+ files = [file for record in records for file in record.files]
337
+ if not files:
338
+ raise CheckpointNotFoundError("Checkpoint contains no files")
339
+
340
+ simulated: Dict[str, tuple[bool, str]] = {}
341
+ backups: Dict[str, tuple[bool, str, Optional[int]]] = {}
342
+ for file in files:
343
+ path = Path(file.path)
344
+ if file.path not in simulated:
345
+ exists, content, mode = self._snapshot(path)
346
+ backups[file.path] = (exists, content, mode)
347
+ simulated[file.path] = (exists, _sha256(content))
348
+ exists, current_hash = simulated[file.path]
349
+ if not exists or current_hash != file.after_hash:
350
+ raise CheckpointConflictError(
351
+ f"Refusing to overwrite a file changed after the checkpoint: {path}"
352
+ )
353
+ simulated[file.path] = (file.existed_before, file.before_hash)
354
+
355
+ restored_paths: list[str] = []
356
+ try:
357
+ for file in files:
358
+ path = Path(file.path)
359
+ exists, current, _ = self._snapshot(path)
360
+ if not exists or _sha256(current) != file.after_hash:
361
+ raise CheckpointConflictError(
362
+ f"File changed while rewind was running: {path}"
363
+ )
364
+ if file.existed_before:
365
+ self._write_atomic(path, file.before_content, file.before_mode)
366
+ else:
367
+ path.unlink()
368
+ if file.path not in restored_paths:
369
+ restored_paths.append(file.path)
370
+ except Exception:
371
+ for path_text, (existed, content, mode) in backups.items():
372
+ path = Path(path_text)
373
+ if existed:
374
+ self._write_atomic(path, content, mode)
375
+ elif path.exists() and path.is_file():
376
+ path.unlink()
377
+ raise
378
+
379
+ restored_at = time.time()
380
+ checkpoint_ids = tuple(record.checkpoint_id for record in records)
381
+ with self._connect() as connection:
382
+ connection.execute("BEGIN IMMEDIATE")
383
+ connection.executemany(
384
+ "UPDATE checkpoints SET status = 'restored', restored_at = ? WHERE checkpoint_id = ?",
385
+ [(restored_at, checkpoint_id) for checkpoint_id in checkpoint_ids],
386
+ )
387
+
388
+ run_id = records[0].run_id if all(
389
+ record.run_id == records[0].run_id for record in records
390
+ ) else None
391
+ if run_id:
392
+ self.run_store.append_event(run_id, "checkpoint_restored", {
393
+ "checkpoint_ids": list(checkpoint_ids),
394
+ "paths": restored_paths,
395
+ })
396
+ return RestoreResult(checkpoint_ids, run_id, tuple(restored_paths))
397
+
398
+ def restore_checkpoint(self, checkpoint_id: str) -> RestoreResult:
399
+ record = self.get(checkpoint_id)
400
+ if record is None or record.status != "active":
401
+ raise CheckpointNotFoundError(checkpoint_id)
402
+ return self._restore([record])
403
+
404
+ def restore_run(self, run_id: str) -> RestoreResult:
405
+ records = self.list(run_id=run_id, status="active", limit=10_000)
406
+ if not records:
407
+ raise CheckpointNotFoundError(run_id)
408
+ return self._restore(records)
409
+
410
+ def restore_latest_run(self, *, session_id: str | None = None) -> RestoreResult:
411
+ run_id = self.latest_run_id(session_id=session_id)
412
+ if not run_id:
413
+ raise CheckpointNotFoundError("No active run checkpoint found")
414
+ return self.restore_run(run_id)
415
+
416
+ def restore_latest(self, *, session_id: str | None = None) -> RestoreResult:
417
+ clauses = ["status = 'active'"]
418
+ values: list[Any] = []
419
+ if session_id:
420
+ clauses.append("session_id = ?")
421
+ values.append(session_id)
422
+ with self._connect() as connection:
423
+ row = connection.execute(
424
+ f"SELECT checkpoint_id, run_id FROM checkpoints "
425
+ f"WHERE {' AND '.join(clauses)} ORDER BY sequence DESC LIMIT 1",
426
+ values,
427
+ ).fetchone()
428
+ if row is None:
429
+ raise CheckpointNotFoundError("No active checkpoint found")
430
+ if row["run_id"]:
431
+ return self.restore_run(row["run_id"])
432
+ return self.restore_checkpoint(row["checkpoint_id"])
@@ -0,0 +1,174 @@
1
+ """崩溃记录:本地优先,不向任何第三方发送。
2
+
3
+ 问题:CLI 未捕获异常时只会打一个裸 traceback 就退出,什么都不留。用户来报
4
+ bug 时能提供的往往只有"它崩了"——版本、provider、走到哪一步、当时的配置
5
+ 全部丢失,只能靠来回追问重建现场。
6
+
7
+ 为什么不直接接 Sentry:aria-code 是 local-first 的终端工具,用户的堆栈里可能
8
+ 带着文件路径、项目名、甚至提示词片段。默认把这些发到第三方服务器,跟这个
9
+ 产品的定位相悖。这里的做法是**先在本地留全**,用户愿意时再自己把文件贴出来;
10
+ 需要 Sentry 的部署可以自行在外层包一层,本模块不阻止。
11
+
12
+ 写入 ~/.aria-code/crashes/(随 aria_home() 走,老用户仍落在 ~/.arthera/)。
13
+
14
+ 脱敏是**尽力而为**,不是保证:环境变量里的密钥、明显的 key 形态会被打码,
15
+ 但堆栈的局部变量里可能藏着任何东西。所以文件默认只在本地,且提示用户贴出前
16
+ 自己看一眼。把这条说清楚,比假装"已完全脱敏"要诚实。
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import platform
24
+ import re
25
+ import sys
26
+ import traceback
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+ from typing import Any, Dict, List, Optional
30
+
31
+ __all__ = ["write_crash_report", "install_excepthook", "recent_crashes", "redact"]
32
+
33
+ _MAX_KEPT = 20
34
+
35
+ # 环境变量名里出现这些片段,值一律打码
36
+ _SECRET_HINTS = ("KEY", "TOKEN", "SECRET", "PASSWORD", "CREDENTIAL", "AUTH")
37
+
38
+ # 值本身长得像密钥的,即使变量名无辜也打码
39
+ _SECRET_VALUE_RE = re.compile(
40
+ r"\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}"
41
+ r"|AIza[A-Za-z0-9_-]{30,}|[A-Za-z0-9_-]{32,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,})\b"
42
+ )
43
+
44
+
45
+ def redact(text: str) -> str:
46
+ """给明显的密钥形态打码。尽力而为——见模块文档。"""
47
+ return _SECRET_VALUE_RE.sub("<redacted>", text or "")
48
+
49
+
50
+ def _crash_dir() -> Path:
51
+ try:
52
+ from packages.aria_core.paths import aria_home
53
+ root = aria_home()
54
+ except Exception:
55
+ root = Path.home() / ".aria-code"
56
+ return root / "crashes"
57
+
58
+
59
+ def _safe_env() -> Dict[str, str]:
60
+ """只收跟 aria 相关的环境变量,且对疑似密钥打码。
61
+
62
+ 不收全量环境:那里面有用户的 PATH、代理地址、其它项目的凭证,跟这次崩溃
63
+ 多半无关,收进来只是扩大暴露面。
64
+ """
65
+ out: Dict[str, str] = {}
66
+ for key, value in os.environ.items():
67
+ if not any(p in key.upper() for p in ("ARIA", "ARTHERA", "OLLAMA", "LMSTUDIO")):
68
+ continue
69
+ if any(h in key.upper() for h in _SECRET_HINTS):
70
+ out[key] = f"<set:{len(value)}chars>" if value else "<empty>"
71
+ else:
72
+ out[key] = redact(value)[:200]
73
+ return out
74
+
75
+
76
+ def write_crash_report(
77
+ exc: BaseException,
78
+ *,
79
+ context: Optional[Dict[str, Any]] = None,
80
+ ) -> Optional[Path]:
81
+ """把一次崩溃写成本地 JSON。返回文件路径;写入失败返回 None。
82
+
83
+ 写入失败绝不抛异常——崩溃记录器自己再崩一次,只会把原始错误从屏幕上
84
+ 冲掉,让排查更难。
85
+ """
86
+ try:
87
+ crash_dir = _crash_dir()
88
+ crash_dir.mkdir(parents=True, exist_ok=True)
89
+
90
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
91
+ path = crash_dir / f"crash-{stamp}.json"
92
+
93
+ try:
94
+ from aria_code._version import __version__ as _ver
95
+ except Exception:
96
+ _ver = "unknown"
97
+
98
+ payload = {
99
+ "timestamp": datetime.now(timezone.utc).isoformat(),
100
+ "version": _ver,
101
+ "python": sys.version.split()[0],
102
+ "platform": platform.platform(),
103
+ "error_type": type(exc).__name__,
104
+ "error": redact(str(exc))[:1000],
105
+ "traceback": redact("".join(traceback.format_exception(exc)))[-8000:],
106
+ "argv": [redact(a) for a in sys.argv[:12]],
107
+ "env": _safe_env(),
108
+ "context": {k: redact(str(v))[:500] for k, v in (context or {}).items()},
109
+ }
110
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
111
+ _prune(crash_dir)
112
+ return path
113
+ except Exception:
114
+ return None
115
+
116
+
117
+ def _prune(crash_dir: Path, keep: int = _MAX_KEPT) -> None:
118
+ """只保留最近 N 份。崩溃记录是排查素材,不是需要永久保存的资产;
119
+ 无上限增长的日志目录本身就会变成一个问题。"""
120
+ try:
121
+ files = sorted(crash_dir.glob("crash-*.json"), key=lambda p: p.stat().st_mtime)
122
+ for stale in files[:-keep]:
123
+ stale.unlink(missing_ok=True)
124
+ except Exception:
125
+ pass
126
+
127
+
128
+ def recent_crashes(limit: int = 5) -> List[Dict[str, Any]]:
129
+ """最近几次崩溃的摘要,供 /doctor 之类的命令展示。"""
130
+ out: List[Dict[str, Any]] = []
131
+ try:
132
+ files = sorted(_crash_dir().glob("crash-*.json"),
133
+ key=lambda p: p.stat().st_mtime, reverse=True)
134
+ for path in files[:limit]:
135
+ try:
136
+ data = json.loads(path.read_text(encoding="utf-8"))
137
+ out.append({
138
+ "file": str(path),
139
+ "timestamp": data.get("timestamp", ""),
140
+ "error_type": data.get("error_type", ""),
141
+ "error": (data.get("error", "") or "")[:160],
142
+ "version": data.get("version", ""),
143
+ })
144
+ except Exception:
145
+ continue
146
+ except Exception:
147
+ pass
148
+ return out
149
+
150
+
151
+ def install_excepthook(*, context_provider=None) -> None:
152
+ """接管未捕获异常:先落盘,再按原样打印。
153
+
154
+ **保留原有输出**——用户和现有的排查习惯都依赖屏幕上那个 traceback,
155
+ 悄悄换掉只会让人以为程序静默失败了。这里只是在它之前多留一份记录。
156
+
157
+ KeyboardInterrupt 不记:那是用户主动中断,不是故障。
158
+ """
159
+ previous = sys.excepthook
160
+
161
+ def _hook(exc_type, exc_value, exc_tb):
162
+ if not issubclass(exc_type, KeyboardInterrupt):
163
+ ctx = {}
164
+ if context_provider is not None:
165
+ try:
166
+ ctx = context_provider() or {}
167
+ except Exception:
168
+ ctx = {}
169
+ path = write_crash_report(exc_value, context=ctx)
170
+ if path is not None:
171
+ print(f"\n[crash report] {path}", file=sys.stderr)
172
+ previous(exc_type, exc_value, exc_tb)
173
+
174
+ sys.excepthook = _hook