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,845 @@
1
+ """Write/edit tool implementations extracted from aria_cli.py.
2
+
3
+ These functions depend on aria_cli globals (GLOBAL_CHANGE_STORE, console, etc.).
4
+ They use lazy runtime imports to avoid circular imports at load time — aria_cli
5
+ is already fully initialised by the time any tool executes.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import pathlib
10
+ import re as _re
11
+ import stat as _stat
12
+
13
+
14
+ # ── Lazy access to aria_cli singletons ───────────────────────────────────────
15
+
16
+ def _ac():
17
+ """Return the aria_cli module (already loaded, never reimported from scratch)."""
18
+ from aria_code import aria_cli
19
+ return aria_cli
20
+
21
+
22
+ def _ui():
23
+ """Return (console, HAS_RICH) from the live aria_cli namespace."""
24
+ m = _ac()
25
+ return getattr(m, "console", None), getattr(m, "HAS_RICH", False)
26
+
27
+
28
+ def _change_store():
29
+ return _ac().GLOBAL_CHANGE_STORE
30
+
31
+
32
+ def _verify_python_syntax(path: "pathlib.Path", content: str) -> "str | None":
33
+ """Compile-check a Python file after a write/edit.
34
+
35
+ Returns a short LLM-actionable error string if the file no longer parses,
36
+ or None if it is fine / not a Python file. This mirrors Claude Code's
37
+ edit→verify discipline so syntax breakage is caught immediately instead of
38
+ at the next run.
39
+ """
40
+ if path.suffix != ".py":
41
+ return None
42
+ try:
43
+ compile(content, str(path), "exec")
44
+ return None
45
+ except SyntaxError as exc:
46
+ line = exc.lineno or "?"
47
+ msg = exc.msg or "syntax error"
48
+ # Show the offending line for context
49
+ ctx = ""
50
+ try:
51
+ lines = content.splitlines()
52
+ if isinstance(exc.lineno, int) and 1 <= exc.lineno <= len(lines):
53
+ ctx = f"\n → 第 {line} 行: {lines[exc.lineno - 1].strip()[:120]}"
54
+ except Exception:
55
+ pass
56
+ return (
57
+ f"⚠ 语法检查失败 (SyntaxError: {msg} @ line {line}){ctx}\n"
58
+ f"改动已写入,但文件无法编译。请用 read_file 查看该行附近,再用 edit_file 修复语法。"
59
+ )
60
+ except Exception:
61
+ # Non-syntax compile issues (e.g. null bytes) — don't block, just skip
62
+ return None
63
+
64
+
65
+ def _write_policy():
66
+ return _ac()._ACTIVE_WRITE_POLICY
67
+
68
+
69
+ def _lsp_autocheck(path: "pathlib.Path") -> "tuple[str | None, list]":
70
+ """Optionally run language-server diagnostics after a write/edit.
71
+
72
+ Gated on config flag `lsp_autocheck` (default off) so the common edit path
73
+ pays zero cost. When on, this surfaces real diagnostics — undefined names,
74
+ type errors, unused imports — that the syntax compile-check can't catch, the
75
+ way Claude Code injects LSP feedback after edits. Returns (summary, diags);
76
+ summary is None when there's nothing to report. Never raises.
77
+ """
78
+ try:
79
+ if not _ac()._ACTIVE_LSP_AUTOCHECK[0]:
80
+ return None, []
81
+ except Exception:
82
+ return None, []
83
+ try:
84
+ from aria_code.runtime.lsp import server_for, get_diagnostics
85
+ if not server_for(path):
86
+ return None, []
87
+ diags = get_diagnostics(path, timeout=6.0)
88
+ except Exception:
89
+ return None, []
90
+ if not diags:
91
+ return None, []
92
+ errors = sum(1 for d in diags if d["severity"] == "error")
93
+ warnings = sum(1 for d in diags if d["severity"] == "warning")
94
+ if errors == 0 and warnings == 0:
95
+ return None, diags
96
+ head = diags[0]
97
+ summary = (
98
+ f"LSP 诊断: {errors} error / {warnings} warning. "
99
+ f"首条 — 第 {head['line']} 行: {head['message'][:120]}"
100
+ )
101
+ return summary, diags
102
+
103
+
104
+ def _print_lsp_diags(diags: list, console, has_rich: bool, limit: int = 6) -> None:
105
+ """Print a compact diagnostics list after an auto-checked edit."""
106
+ if not (diags and console):
107
+ return
108
+ _color = {"error": "red", "warning": "yellow", "info": "cyan", "hint": "dim"}
109
+ for d in diags[:limit]:
110
+ if has_rich:
111
+ c = _color.get(d["severity"], "white")
112
+ console.print(f" [{c}]●[/{c}] [dim]{d['line']}:{d['col']}[/dim] "
113
+ f"{d['message'][:100]}")
114
+ else:
115
+ print(f" {d['line']}:{d['col']} {d['severity']}: {d['message'][:100]}")
116
+ if len(diags) > limit:
117
+ extra = len(diags) - limit
118
+ console.print(f" [dim]… +{extra} more[/dim]") if has_rich else print(f" ... +{extra} more")
119
+
120
+
121
+ def _is_safe(p: pathlib.Path) -> bool:
122
+ return _ac()._is_safe_path(p)
123
+
124
+
125
+ def _config_dir() -> str:
126
+ return str(_ac().CONFIG_DIR)
127
+
128
+
129
+ def _sessions_dir() -> str:
130
+ return str(_ac().SESSIONS_DIR)
131
+
132
+
133
+ def _ChangeConflictError():
134
+ return _ac().ChangeConflictError
135
+
136
+
137
+ def _record_checkpoint(change, params: dict, *, existed_before: bool, before_mode: "int | None") -> tuple:
138
+ """Persist an applied change without making checkpoint I/O break the write."""
139
+ if params.get("_disable_checkpoints"):
140
+ return None, None
141
+ run_id = params.get("_run_id")
142
+ session_id = str(params.get("_session_id") or "")
143
+ if not run_id and not session_id:
144
+ return None, None
145
+ try:
146
+ from aria_code.runtime.checkpoints import CheckpointStore
147
+ checkpoint = CheckpointStore().record_change(
148
+ path=change.path,
149
+ before_content=change.before_content,
150
+ after_content=change.after_content,
151
+ existed_before=existed_before,
152
+ before_mode=before_mode,
153
+ source=change.source,
154
+ run_id=run_id,
155
+ session_id=session_id,
156
+ label=pathlib.Path(change.path).name,
157
+ metadata={"change_id": change.change_id},
158
+ )
159
+ return checkpoint.checkpoint_id, None
160
+ except Exception as exc:
161
+ return None, str(exc)
162
+
163
+
164
+ # ── Helpers ───────────────────────────────────────────────────────────────────
165
+
166
+ def _strip_markdown_fences(content: str) -> str:
167
+ """Strip markdown code fences that LLMs sometimes wrap around file content."""
168
+ stripped = content.strip()
169
+ if stripped.startswith("```"):
170
+ first_nl = stripped.find("\n")
171
+ if first_nl >= 0:
172
+ stripped = stripped[first_nl + 1:]
173
+ else:
174
+ return content
175
+ if stripped.rstrip().endswith("```"):
176
+ stripped = stripped.rstrip()[:-3].rstrip()
177
+ if stripped != content.strip():
178
+ return stripped + "\n"
179
+ return content
180
+
181
+
182
+ def _auto_fix_python(content: str, path: str) -> str:
183
+ """Auto-inject missing imports and validate syntax for Python files."""
184
+ if not path.endswith(".py"):
185
+ return content
186
+
187
+ lines = content.split("\n")
188
+ imports_present: set[str] = set()
189
+ first_non_comment = 0
190
+ for i, line in enumerate(lines):
191
+ s = line.strip()
192
+ if s and not s.startswith("#") and not s.startswith('"""') and not s.startswith("'''"):
193
+ first_non_comment = i
194
+ break
195
+
196
+ for line in lines:
197
+ s = line.strip()
198
+ if s.startswith("import "):
199
+ parts = s.split()
200
+ if len(parts) >= 2:
201
+ imports_present.add(parts[1].split(".")[0].split(",")[0])
202
+ elif s.startswith("from "):
203
+ parts = s.split()
204
+ if len(parts) >= 2:
205
+ imports_present.add(parts[1].split(".")[0])
206
+
207
+ code = content
208
+ needed: list[str] = []
209
+
210
+ if ("os.path" in code or "os.expanduser" in code or "os.getcwd" in code
211
+ or "os.makedirs" in code) and "os" not in imports_present:
212
+ needed.append("import os")
213
+ if ("sys." in code or "sys.exit" in code) and "sys" not in imports_present:
214
+ needed.append("import sys")
215
+ if "np." in code and "numpy" not in imports_present and "np" not in imports_present:
216
+ needed.append("import numpy as np")
217
+ if "pd." in code and "pandas" not in imports_present and "pd" not in imports_present:
218
+ needed.append("import pandas as pd")
219
+ if "yf." in code and "yfinance" not in imports_present and "yf" not in imports_present:
220
+ needed.append("import yfinance as yf")
221
+
222
+ has_plt = "plt." in code
223
+ has_matplotlib_use = "matplotlib.use" in code
224
+ if has_plt and "matplotlib" not in imports_present:
225
+ needed.append("import matplotlib; matplotlib.use('Agg')")
226
+ needed.append("import matplotlib.pyplot as plt")
227
+ elif has_plt and not has_matplotlib_use:
228
+ for i, line in enumerate(lines):
229
+ if "import matplotlib.pyplot" in line and "matplotlib.use" not in "\n".join(lines[:i]):
230
+ lines.insert(i, "import matplotlib; matplotlib.use('Agg')")
231
+ content = "\n".join(lines)
232
+ break
233
+
234
+ if "mpf." in code and "mplfinance" not in imports_present and "mpf" not in imports_present:
235
+ needed.append("import mplfinance as mpf")
236
+ if "re." in code and "re" not in imports_present:
237
+ needed.append("import re")
238
+ if "json." in code and "json" not in imports_present:
239
+ needed.append("import json")
240
+ if "datetime" in code and "datetime" not in imports_present:
241
+ needed.append("from datetime import datetime, timedelta")
242
+ if (_re.search(r'\bta\.(?:sma|ema|rsi|macd|bbands|stoch|atr|adx|obv|vwap)\b', code)
243
+ or "pandas_ta" in code) and "ta" not in imports_present and "pandas_ta" not in imports_present:
244
+ needed.append("import pandas_ta as ta")
245
+ if (_re.search(r'\bgo\.(?:Figure|Candlestick|Scatter|Bar|Heatmap|Layout|Table)', code)
246
+ or "px." in code or "plotly" in code) and "plotly" not in imports_present:
247
+ if "go.Figure" in code or "go.Candlestick" in code:
248
+ needed.append("import plotly.graph_objects as go")
249
+ if "px." in code:
250
+ needed.append("import plotly.express as px")
251
+ if "make_subplots" in code:
252
+ needed.append("from plotly.subplots import make_subplots")
253
+ if "scipy" in code and "scipy" not in imports_present:
254
+ needed.append("import scipy")
255
+
256
+ has_warnings_in_needed = any("warnings" in n for n in needed)
257
+ if ("yf." in code or "pd." in code) and "warnings" not in imports_present and not has_warnings_in_needed:
258
+ needed.insert(0, "import warnings; warnings.filterwarnings('ignore')")
259
+ elif "warnings" in code and "warnings" not in imports_present and not has_warnings_in_needed:
260
+ needed.append("import warnings")
261
+
262
+ if needed:
263
+ for imp in reversed(needed):
264
+ lines.insert(first_non_comment, imp)
265
+ content = "\n".join(lines)
266
+
267
+ try:
268
+ import ast
269
+ ast.parse(content)
270
+ except SyntaxError as e:
271
+ console, has_rich = _ui()
272
+ if has_rich and console:
273
+ console.print(f" [dim]Warning: syntax issue at line {e.lineno}: {e.msg}[/dim]")
274
+
275
+ return content
276
+
277
+
278
+ def _write_policy_confirm(p: pathlib.Path, content: str, existed: bool) -> tuple:
279
+ """Prompt user to confirm a write. Returns (approved: bool, final_path: Path)."""
280
+ import difflib
281
+ console, has_rich = _ui()
282
+ lines_new = content.count("\n") + 1
283
+ desktop = pathlib.Path.home() / "Desktop"
284
+ is_desktop = str(p).startswith(str(desktop))
285
+
286
+ if has_rich and console:
287
+ console.print()
288
+ if existed:
289
+ old_content = p.read_text(errors="replace")
290
+ diff = list(difflib.unified_diff(
291
+ old_content.splitlines(keepends=True),
292
+ content.splitlines(keepends=True),
293
+ fromfile=f"current/{p.name}",
294
+ tofile=f"new/{p.name}",
295
+ n=2,
296
+ ))
297
+ added = sum(1 for l in diff if l.startswith("+") and not l.startswith("+++"))
298
+ removed = sum(1 for l in diff if l.startswith("-") and not l.startswith("---"))
299
+ console.print(f" [yellow]⚠ Overwrite[/yellow] [bold]{p}[/bold]")
300
+ console.print(f" [dim] +{added} lines -{removed} lines ({lines_new} total)[/dim]")
301
+ for line in diff[:8]:
302
+ if line.startswith("+") and not line.startswith("+++"):
303
+ console.print(f" [green]{line.rstrip()}[/green]")
304
+ elif line.startswith("-") and not line.startswith("---"):
305
+ console.print(f" [red]{line.rstrip()}[/red]")
306
+ else:
307
+ loc = "[dim cyan](Desktop)[/dim cyan]" if is_desktop else "[yellow](outside Desktop)[/yellow]"
308
+ console.print(f" [cyan]New file[/cyan] {loc} [bold]{p}[/bold] ({lines_new} lines)")
309
+ console.print()
310
+ choice = console.input(" [bold]Write this file?[/bold] [dim]\\[y/n/r=redirect path][/dim] ").strip().lower()
311
+ else:
312
+ print()
313
+ print(f" {'Overwrite' if existed else 'New file'}: {p} ({lines_new} lines)")
314
+ choice = input(" Write this file? [y/n/r=redirect path] ").strip().lower()
315
+
316
+ if choice == "r":
317
+ if has_rich and console:
318
+ new_path_str = console.input(" [dim]Enter new path: [/dim]").strip()
319
+ else:
320
+ new_path_str = input(" Enter new path: ").strip()
321
+ if new_path_str:
322
+ new_p = pathlib.Path(new_path_str).expanduser().resolve()
323
+ if _is_safe(new_p):
324
+ return True, new_p
325
+ if has_rich and console:
326
+ console.print(f" [red]Path not allowed: {new_p}[/red]")
327
+ else:
328
+ print(f" Path not allowed: {new_p}")
329
+ return False, p
330
+
331
+ return choice in ("y", "yes", ""), p
332
+
333
+
334
+ # ── Rich diff helper ─────────────────────────────────────────────────────────
335
+
336
+ def _print_inline_diff(old_str: str, new_str: str, console, max_lines: int = 12) -> None:
337
+ """Print a compact color-coded inline diff after an edit_file call."""
338
+ old_lines = old_str.splitlines()
339
+ new_lines = new_str.splitlines()
340
+
341
+ total_changed = max(len(old_lines), len(new_lines))
342
+ if total_changed > max_lines:
343
+ console.print(f" [dim] (diff too large to display inline — {total_changed} lines)[/dim]")
344
+ return
345
+
346
+ for line in old_lines:
347
+ display = line[:120]
348
+ console.print(f" [red dim]- {display}[/red dim]")
349
+ for line in new_lines:
350
+ display = line[:120]
351
+ console.print(f" [green dim]+ {display}[/green dim]")
352
+
353
+
354
+ # ── Public tool functions ─────────────────────────────────────────────────────
355
+
356
+ def _attach_verification_hint(result_data: dict, path, console=None, has_rich: bool = False) -> None:
357
+ """Note the checks this change calls for, on the tool result.
358
+
359
+ One function instead of three pasted copies. The copies differed only in
360
+ what the result dict and the console were called in each enclosing scope,
361
+ and papered over that with ``_wdata if "_wdata" in locals() else _data``
362
+ plus ``locals().get("console", locals().get("_console2"))``. Both names are
363
+ compiled unconditionally, so whichever was absent from a given function was
364
+ an undefined reference — three F821s that only avoided raising because a
365
+ bare ``except Exception: pass`` sat around them, which would equally have
366
+ hidden a real failure. Passing them in makes the dependency visible and
367
+ checkable.
368
+
369
+ Advisory only: the acceptance gate is what actually runs these commands
370
+ after the turn. This tells the model what is coming, so it can factor the
371
+ check into what it does next rather than being surprised by it.
372
+ """
373
+ try:
374
+ import os
375
+
376
+ from aria_code.workspace.verify import VerificationPlanner
377
+
378
+ plan = VerificationPlanner(root=os.getcwd()).infer([str(path)])
379
+ except Exception:
380
+ # Planning is a convenience; a successful write must not be downgraded
381
+ # because the planner is unavailable.
382
+ return
383
+ if not plan.commands:
384
+ return
385
+
386
+ joined = " && ".join(plan.commands)
387
+ result_data["suggested_verification"] = (
388
+ f"Verification recommended ({plan.reason}): run `{joined}`"
389
+ )
390
+ if has_rich and console is not None:
391
+ try:
392
+ console.print(f" [cyan]✓ 推荐验证命令: {joined}[/cyan]")
393
+ except Exception:
394
+ pass
395
+
396
+
397
+ def tool_write_file(params: dict) -> dict:
398
+ """Write content to a file (create or overwrite)."""
399
+ path = params.get("path", "")
400
+ content = params.get("content", "")
401
+ skip_confirm = params.get("_skip_confirm", False)
402
+ stage_only = bool(params.get("stage_only", False))
403
+
404
+ if not path:
405
+ return {"success": False, "error": "Missing 'path' parameter"}
406
+ if not content:
407
+ return {"success": False, "error": "Missing 'content' parameter"}
408
+
409
+ content = _strip_markdown_fences(content)
410
+ stripped_check = content.strip()
411
+
412
+ if len(stripped_check) < 20:
413
+ return {"success": False,
414
+ "error": f"Content too short ({len(stripped_check)} chars). "
415
+ "You must write the COMPLETE script code, not a placeholder."}
416
+
417
+ if (stripped_check.startswith("<") and stripped_check.endswith(">")
418
+ and "\n" not in stripped_check and len(stripped_check) < 200
419
+ and not stripped_check.lower().startswith("<!doctype")
420
+ and not stripped_check.lower().startswith("<html")):
421
+ return {"success": False,
422
+ "error": f"Content appears to be a placeholder tag: '{stripped_check[:120]}'. "
423
+ "Write the complete code with imports, data fetching, computation, and output."}
424
+
425
+ # Reject trivial stub Python scripts — only print() with no real logic
426
+ if path.endswith(".py"):
427
+ import re as _re
428
+ _boilerplate = {
429
+ "#!/usr/bin/env python", "#!/usr/bin/python", "# -*- coding:",
430
+ 'if __name__ == "__main__":', "if __name__ == '__main__':",
431
+ "def main():", "main()", "",
432
+ }
433
+ _work_lines = [
434
+ ln.strip() for ln in stripped_check.splitlines()
435
+ if ln.strip() and ln.strip() not in _boilerplate
436
+ and not ln.strip().startswith("#")
437
+ ]
438
+ _all_print = _work_lines and all(
439
+ _re.match(r'^print\s*\(', ln) for ln in _work_lines
440
+ )
441
+ if _all_print and len(_work_lines) <= 3:
442
+ return {
443
+ "success": False,
444
+ "error": (
445
+ f"拒绝写入: '{pathlib.Path(path).name or 'file'}' 只包含 print() 语句,是无意义的占位脚本。"
446
+ " 请直接用文字输出结果,或者写包含真实逻辑的代码(网络请求、数据处理、计算等)。"
447
+ ),
448
+ }
449
+
450
+ content = _auto_fix_python(content, path)
451
+
452
+ try:
453
+ raw_path = pathlib.Path(path).expanduser()
454
+ if not raw_path.is_absolute():
455
+ from aria_code.artifacts import user_generated_dir
456
+ raw_path = user_generated_dir() / raw_path
457
+ p = raw_path.resolve()
458
+ if not _is_safe(p):
459
+ return {"success": False, "error": f"Access denied: path '{p}' is outside allowed directories"}
460
+
461
+ existed = p.exists()
462
+ desktop = pathlib.Path.home() / "Desktop"
463
+ import tempfile as _tf
464
+ from aria_code.artifacts import user_output_root
465
+ user_root = user_output_root().resolve()
466
+ _auto_trusted_prefixes = (
467
+ str(desktop),
468
+ str(user_root),
469
+ str(pathlib.Path(_tf.gettempdir()).resolve()),
470
+ "/tmp", "/private/tmp", "/private/var/folders",
471
+ _config_dir(), _sessions_dir(),
472
+ )
473
+ is_auto_trusted = any(str(p).startswith(pfx) for pfx in _auto_trusted_prefixes)
474
+ policy = _write_policy()[0]
475
+
476
+ needs_confirm = (
477
+ not skip_confirm
478
+ and not is_auto_trusted
479
+ and (
480
+ policy == "always_confirm"
481
+ or policy in ("desktop_only", "confirm_outside")
482
+ or existed
483
+ )
484
+ )
485
+
486
+ if needs_confirm:
487
+ approved, p = _write_policy_confirm(p, content, existed)
488
+ if not approved:
489
+ return {"success": False, "error": "Write cancelled by user.",
490
+ "data": {"cancelled": True}}
491
+
492
+ existed = p.exists()
493
+ before_mode = _stat.S_IMODE(p.stat().st_mode) if p.is_file() else None
494
+
495
+ console, has_rich = _ui()
496
+ store = _change_store()
497
+ change = store.stage(p, content, source="write_file")
498
+ lines = content.count("\n") + 1
499
+ action = "Updated" if existed else "Created"
500
+
501
+ if stage_only:
502
+ label = "Staged update" if existed else "Staged create"
503
+ if has_rich and console:
504
+ console.print(f" [dim]{label} {p} ({lines} lines, change {change.change_id})[/dim]")
505
+ else:
506
+ print(f" {label} {p} ({lines} lines, change {change.change_id})")
507
+ return {"success": True, "data": {
508
+ "path": str(p), "action": "staged", "lines": lines,
509
+ "change_id": change.change_id,
510
+ "before_hash": change.before_hash, "after_hash": change.after_hash,
511
+ "diff": change.diff, "staged": True, "applied": False,
512
+ }}
513
+
514
+ try:
515
+ applied = store.apply(change.change_id)
516
+ except _ChangeConflictError() as exc:
517
+ return {"success": False, "error": str(exc), "data": {"change_id": change.change_id}}
518
+ checkpoint_id, checkpoint_error = _record_checkpoint(
519
+ applied,
520
+ params,
521
+ existed_before=existed,
522
+ before_mode=before_mode,
523
+ )
524
+
525
+ desktop = pathlib.Path.home() / "Desktop"
526
+ is_on_desktop = str(p).startswith(str(desktop))
527
+
528
+ import platform as _platform
529
+ import subprocess as _sub
530
+ _sys_name = _platform.system()
531
+ if _sys_name == "Darwin":
532
+ _reveal_hint = f'open -R "{p}"'
533
+ elif _sys_name == "Windows":
534
+ _reveal_hint = f'explorer /select,"{p}"'
535
+ else:
536
+ _reveal_hint = f'xdg-open "{p.parent}"'
537
+
538
+ if has_rich and console:
539
+ console.print(f" [dim]{action} [bold]{p}[/bold] ({lines} lines)[/dim]")
540
+ if not is_on_desktop and p.suffix == ".py":
541
+ console.print(
542
+ f" [dim]提示: 文件保存在 [yellow]{p}[/yellow]\n"
543
+ f" 打开所在目录: [cyan]{_reveal_hint}[/cyan][/dim]"
544
+ )
545
+ else:
546
+ print(f" {action} {p} ({lines} lines)")
547
+
548
+ # Auto-reveal .py/.ipynb strategy files in file manager (non-blocking)
549
+ if p.suffix in (".py", ".ipynb"):
550
+ try:
551
+ if _sys_name == "Darwin":
552
+ _sub.Popen(["open", "-R", str(p)],
553
+ stdout=_sub.DEVNULL, stderr=_sub.DEVNULL)
554
+ elif _sys_name == "Windows":
555
+ _sub.Popen(["explorer", f"/select,{str(p)}"],
556
+ stdout=_sub.DEVNULL, stderr=_sub.DEVNULL)
557
+ except Exception:
558
+ pass
559
+
560
+ try:
561
+ size_bytes = p.stat().st_size
562
+ except Exception:
563
+ size_bytes = len(content.encode("utf-8"))
564
+
565
+ _syntax_warn = _verify_python_syntax(p, content)
566
+ _console2, _has_rich2 = _ui()
567
+ if _syntax_warn and _has_rich2 and _console2:
568
+ _console2.print(" [yellow]⚠ 语法检查未通过[/yellow]")
569
+
570
+ _wdata = {
571
+ "path": str(p),
572
+ "absolute_path": str(p),
573
+ "action": action.lower(),
574
+ "lines": lines,
575
+ "size_bytes": size_bytes,
576
+ "change_id": applied.change_id,
577
+ "before_hash": applied.before_hash,
578
+ "after_hash": applied.after_hash,
579
+ "diff": applied.diff,
580
+ "staged": True,
581
+ "applied": True,
582
+ "user_message": f"文件已保存到: {p} 打开所在目录: {_reveal_hint}",
583
+ }
584
+ try:
585
+ from aria_code.artifacts import register_existing_artifact
586
+
587
+ artifact = register_existing_artifact(
588
+ p,
589
+ topic=str(params.get("artifact_topic") or ""),
590
+ kind=str(params.get("artifact_kind") or ""),
591
+ metadata={"source": "write_file"},
592
+ )
593
+ if artifact is not None:
594
+ _wdata["artifact_registered"] = True
595
+ _wdata["artifact_metadata_path"] = str(artifact.metadata_path)
596
+ except Exception:
597
+ # Artifact indexing is convenience metadata; a successful file
598
+ # write must not be downgraded if indexing is unavailable.
599
+ pass
600
+ if checkpoint_id:
601
+ _wdata["checkpoint_id"] = checkpoint_id
602
+ if checkpoint_error:
603
+ _wdata["checkpoint_error"] = checkpoint_error
604
+ if _syntax_warn:
605
+ _wdata["syntax_check"] = "failed"
606
+ return {"success": True, "data": _wdata, "warning": _syntax_warn}
607
+
608
+
609
+ _attach_verification_hint(_wdata, p, _console2, _has_rich2)
610
+
611
+ _lsp_warn, _lsp_diags = _lsp_autocheck(p)
612
+ if _lsp_warn:
613
+ _print_lsp_diags(_lsp_diags, _console2, _has_rich2)
614
+ _wdata["diagnostics"] = _lsp_diags
615
+ return {"success": True, "data": _wdata, "warning": _lsp_warn}
616
+ return {"success": True, "data": _wdata}
617
+ except Exception as e:
618
+ return {"success": False, "error": str(e)}
619
+
620
+
621
+ def tool_edit_file(params: dict) -> dict:
622
+ """Edit a file by replacing old_string with new_string (first occurrence)."""
623
+ path = params.get("path", "")
624
+ old_str = params.get("old_string", params.get("old_str", ""))
625
+ new_str = params.get("new_string", params.get("new_str", ""))
626
+ stage_only = bool(params.get("stage_only", False))
627
+
628
+ if not path:
629
+ return {"success": False, "error": "Missing 'path' parameter"}
630
+ if not old_str:
631
+ return {"success": False, "error": "Missing 'old_string' parameter"}
632
+
633
+ try:
634
+ p = pathlib.Path(path).expanduser().resolve()
635
+ if not p.exists():
636
+ return {"success": False, "error": f"File not found: {p}"}
637
+ if not _is_safe(p):
638
+ return {"success": False, "error": f"Access denied: path '{p}' is outside allowed directories"}
639
+
640
+ content = p.read_text(errors="replace")
641
+ before_mode = _stat.S_IMODE(p.stat().st_mode)
642
+ if content.count(old_str) == 0:
643
+ preview = "\n".join(content.splitlines()[:10])
644
+ return {"success": False,
645
+ "error": f"old_string not found in file. "
646
+ f"The file starts with:\n{preview}\n\n"
647
+ f"HINT: Use read_file to see the actual content, then retry edit_file "
648
+ f"with the correct old_string. Or use write_file to overwrite the entire file."}
649
+
650
+ new_content = content.replace(old_str, new_str, 1)
651
+ store = _change_store()
652
+ change = store.stage(p, new_content, source="edit_file")
653
+ added = len(new_str.splitlines())
654
+ removed = len(old_str.splitlines())
655
+ console, has_rich = _ui()
656
+
657
+ if stage_only:
658
+ if has_rich and console:
659
+ console.print(f" [dim]Staged edit {p} (change {change.change_id})[/dim]")
660
+ else:
661
+ print(f" Staged edit {p} (change {change.change_id})")
662
+ return {"success": True, "data": {
663
+ "path": str(p), "replacements": 1,
664
+ "lines": new_content.count("\n") + 1,
665
+ "change_id": change.change_id,
666
+ "before_hash": change.before_hash, "after_hash": change.after_hash,
667
+ "diff": change.diff, "staged": True, "applied": False,
668
+ }}
669
+
670
+ try:
671
+ applied = store.apply(change.change_id)
672
+ except _ChangeConflictError() as exc:
673
+ return {"success": False, "error": str(exc), "data": {"change_id": change.change_id}}
674
+ checkpoint_id, checkpoint_error = _record_checkpoint(
675
+ applied,
676
+ params,
677
+ existed_before=True,
678
+ before_mode=before_mode,
679
+ )
680
+
681
+ if has_rich and console:
682
+ parts = []
683
+ if added > 0:
684
+ parts.append(f"[green]+{added}[/green]")
685
+ if removed > 0:
686
+ parts.append(f"[red]-{removed}[/red]")
687
+ short_path = str(p.name) if len(str(p)) > 60 else str(p)
688
+ console.print(f" [dim]✎ [bold]{short_path}[/bold] ({', '.join(parts)} lines)[/dim]")
689
+ _print_inline_diff(old_str, new_str, console)
690
+ else:
691
+ print(f" Applied (+{added}, -{removed} lines)")
692
+
693
+ _syntax_warn = _verify_python_syntax(p, new_content)
694
+ if _syntax_warn and has_rich and console:
695
+ console.print(" [yellow]⚠ 语法检查未通过[/yellow]")
696
+
697
+ _data = {
698
+ "path": str(p), "replacements": 1,
699
+ "lines": new_content.count("\n") + 1,
700
+ "change_id": applied.change_id,
701
+ "before_hash": applied.before_hash, "after_hash": applied.after_hash,
702
+ "diff": applied.diff, "staged": True, "applied": True,
703
+ }
704
+ if checkpoint_id:
705
+ _data["checkpoint_id"] = checkpoint_id
706
+ if checkpoint_error:
707
+ _data["checkpoint_error"] = checkpoint_error
708
+ if _syntax_warn:
709
+ _data["syntax_check"] = "failed"
710
+ return {"success": True, "data": _data, "warning": _syntax_warn}
711
+
712
+ # Opt-in LSP diagnostics (catches what the syntax check can't)
713
+
714
+ _attach_verification_hint(_data, p, console, has_rich)
715
+
716
+ _lsp_warn, _lsp_diags = _lsp_autocheck(p)
717
+ if _lsp_warn:
718
+ _print_lsp_diags(_lsp_diags, console, has_rich)
719
+ _data["diagnostics"] = _lsp_diags
720
+ return {"success": True, "data": _data, "warning": _lsp_warn}
721
+ return {"success": True, "data": _data}
722
+ except Exception as e:
723
+ return {"success": False, "error": str(e)}
724
+
725
+
726
+ def tool_multi_edit(params: dict) -> dict:
727
+ """Apply multiple find/replace edits to a single file atomically.
728
+
729
+ All edits must succeed or none are applied (the file is only written once,
730
+ after every edit has been validated against the in-memory content). Edits
731
+ are applied in order, so a later edit can match text introduced by an
732
+ earlier one.
733
+ """
734
+ path = params.get("path", "")
735
+ edits = params.get("edits", [])
736
+ stage_only = bool(params.get("stage_only", False))
737
+
738
+ if not path:
739
+ return {"success": False, "error": "Missing 'path' parameter"}
740
+ if not isinstance(edits, list) or not edits:
741
+ return {"success": False, "error": "Missing 'edits' — expected a non-empty array of "
742
+ "{old_string, new_string, replace_all?}"}
743
+
744
+ try:
745
+ p = pathlib.Path(path).expanduser().resolve()
746
+ if not p.exists():
747
+ return {"success": False, "error": f"File not found: {p}"}
748
+ if not _is_safe(p):
749
+ return {"success": False, "error": f"Access denied: path '{p}' is outside allowed directories"}
750
+
751
+ content = p.read_text(errors="replace")
752
+ before_mode = _stat.S_IMODE(p.stat().st_mode)
753
+ working = content
754
+ applied_count = 0
755
+ total_added = 0
756
+ total_removed = 0
757
+
758
+ # ── Phase 1: validate + apply every edit in memory (atomic) ───────────
759
+ for i, ed in enumerate(edits):
760
+ if not isinstance(ed, dict):
761
+ return {"success": False, "error": f"edit #{i + 1} is not an object"}
762
+ old_s = ed.get("old_string", ed.get("old_str", ""))
763
+ new_s = ed.get("new_string", ed.get("new_str", ""))
764
+ replace_all = bool(ed.get("replace_all", False))
765
+ if not old_s:
766
+ return {"success": False, "error": f"edit #{i + 1} missing 'old_string'"}
767
+ occurrences = working.count(old_s)
768
+ if occurrences == 0:
769
+ return {"success": False, "error":
770
+ f"edit #{i + 1}: old_string not found. No edits applied (atomic). "
771
+ f"HINT: read_file first to copy exact text. old_string was:\n"
772
+ f"{old_s[:200]}"}
773
+ if occurrences > 1 and not replace_all:
774
+ return {"success": False, "error":
775
+ f"edit #{i + 1}: old_string matches {occurrences} places — pass "
776
+ f"replace_all=true to replace all, or make old_string unique."}
777
+ working = working.replace(old_s, new_s, -1 if replace_all else 1)
778
+ applied_count += 1
779
+ total_added += len(new_s.splitlines())
780
+ total_removed += len(old_s.splitlines())
781
+
782
+ if working == content:
783
+ return {"success": False, "error": "No changes — edits produced identical content."}
784
+
785
+ # ── Phase 2: stage + apply once ───────────────────────────────────────
786
+ store = _change_store()
787
+ change = store.stage(p, working, source="multi_edit")
788
+ console, has_rich = _ui()
789
+
790
+ if stage_only:
791
+ return {"success": True, "data": {
792
+ "path": str(p), "edits_applied": applied_count,
793
+ "lines": working.count("\n") + 1,
794
+ "change_id": change.change_id,
795
+ "before_hash": change.before_hash, "after_hash": change.after_hash,
796
+ "diff": change.diff, "staged": True, "applied": False,
797
+ }}
798
+
799
+ try:
800
+ applied = store.apply(change.change_id)
801
+ except _ChangeConflictError() as exc:
802
+ return {"success": False, "error": str(exc), "data": {"change_id": change.change_id}}
803
+ checkpoint_id, checkpoint_error = _record_checkpoint(
804
+ applied,
805
+ params,
806
+ existed_before=True,
807
+ before_mode=before_mode,
808
+ )
809
+
810
+ if has_rich and console:
811
+ console.print(f" [dim]Applied {applied_count} edits "
812
+ f"([green]+{total_added}[/green]/[red]-{total_removed}[/red] lines)[/dim]")
813
+ else:
814
+ print(f" Applied {applied_count} edits (+{total_added}/-{total_removed} lines)")
815
+
816
+ _syntax_warn = _verify_python_syntax(p, working)
817
+ if _syntax_warn and has_rich and console:
818
+ console.print(" [yellow]⚠ 语法检查未通过[/yellow]")
819
+
820
+ _data = {
821
+ "path": str(p), "edits_applied": applied_count,
822
+ "lines": working.count("\n") + 1,
823
+ "change_id": applied.change_id,
824
+ "before_hash": applied.before_hash, "after_hash": applied.after_hash,
825
+ "diff": applied.diff, "staged": True, "applied": True,
826
+ }
827
+ if checkpoint_id:
828
+ _data["checkpoint_id"] = checkpoint_id
829
+ if checkpoint_error:
830
+ _data["checkpoint_error"] = checkpoint_error
831
+ if _syntax_warn:
832
+ _data["syntax_check"] = "failed"
833
+ return {"success": True, "data": _data, "warning": _syntax_warn}
834
+
835
+
836
+ _attach_verification_hint(_data, p, console, has_rich)
837
+
838
+ _lsp_warn, _lsp_diags = _lsp_autocheck(p)
839
+ if _lsp_warn:
840
+ _print_lsp_diags(_lsp_diags, console, has_rich)
841
+ _data["diagnostics"] = _lsp_diags
842
+ return {"success": True, "data": _data, "warning": _lsp_warn}
843
+ return {"success": True, "data": _data}
844
+ except Exception as e:
845
+ return {"success": False, "error": str(e)}