aria-code 0.167.0__py3-none-win_amd64.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 (601) hide show
  1. aria_bots/__init__.py +7 -0
  2. aria_bots/channels/README.md +97 -0
  3. aria_bots/channels/__init__.py +1 -0
  4. aria_bots/channels/approvals.py +263 -0
  5. aria_bots/channels/conversation.py +280 -0
  6. aria_bots/channels/digest.py +186 -0
  7. aria_bots/channels/registry.py +95 -0
  8. aria_bots/engine.py +144 -0
  9. aria_bots/feishu_bot.py +1385 -0
  10. aria_bots/logistics/__init__.py +1 -0
  11. aria_bots/logistics/carriers.py +253 -0
  12. aria_bots/logistics/freight.py +154 -0
  13. aria_bots/logistics/inventory.py +242 -0
  14. aria_bots/logistics/mcp_server.py +156 -0
  15. aria_bots/logistics/records.py +119 -0
  16. aria_bots/logistics/tenancy.py +152 -0
  17. aria_bots/market.py +91 -0
  18. aria_bots/relay_client.py +326 -0
  19. aria_bots/telegram_bot.py +299 -0
  20. aria_code/__init__.py +111 -0
  21. aria_code/_bin/aria-native.exe +0 -0
  22. aria_code/_bin/aria-runtime.exe +0 -0
  23. aria_code/_version.py +20 -0
  24. aria_code/account_memory.py +296 -0
  25. aria_code/account_session.py +166 -0
  26. aria_code/adk_apps/__init__.py +1 -0
  27. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  28. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  29. aria_code/adk_apps/aria_research/__init__.py +1 -0
  30. aria_code/adk_apps/aria_research/agent.py +31 -0
  31. aria_code/agents/__init__.py +49 -0
  32. aria_code/agents/base.py +329 -0
  33. aria_code/agents/code_review.py +200 -0
  34. aria_code/agents/deep/__init__.py +37 -0
  35. aria_code/agents/deep/calibration_loop.py +144 -0
  36. aria_code/agents/deep/critic.py +125 -0
  37. aria_code/agents/deep/deepen.py +193 -0
  38. aria_code/agents/deep/models.py +149 -0
  39. aria_code/agents/deep/pipeline.py +164 -0
  40. aria_code/agents/deep/quant_fusion.py +192 -0
  41. aria_code/agents/deep/themes.py +95 -0
  42. aria_code/agents/deep/tiers.py +106 -0
  43. aria_code/agents/engineering/__init__.py +14 -0
  44. aria_code/agents/engineering/coder.py +222 -0
  45. aria_code/agents/engineering/tester.py +145 -0
  46. aria_code/agents/financial/__init__.py +21 -0
  47. aria_code/agents/financial/cashflow_burn.py +119 -0
  48. aria_code/agents/financial/catalyst.py +279 -0
  49. aria_code/agents/financial/corporate_finance.py +175 -0
  50. aria_code/agents/financial/debate.py +224 -0
  51. aria_code/agents/financial/earnings.py +315 -0
  52. aria_code/agents/financial/fundamental.py +178 -0
  53. aria_code/agents/financial/macro.py +111 -0
  54. aria_code/agents/financial/news.py +231 -0
  55. aria_code/agents/financial/risk.py +146 -0
  56. aria_code/agents/financial/sector.py +279 -0
  57. aria_code/agents/financial/strategist.py +121 -0
  58. aria_code/agents/financial/stripe_revenue.py +127 -0
  59. aria_code/agents/financial/synthesis.py +274 -0
  60. aria_code/agents/financial/technical.py +277 -0
  61. aria_code/agents/orchestrator.py +44 -0
  62. aria_code/agents/portfolio_agent.py +392 -0
  63. aria_code/agents/realty/__init__.py +62 -0
  64. aria_code/agents/realty/asset_diagnosis.py +152 -0
  65. aria_code/agents/realty/business_match.py +166 -0
  66. aria_code/agents/realty/cashflow_verify.py +209 -0
  67. aria_code/agents/realty/contract_rules.py +210 -0
  68. aria_code/agents/realty/energy_anomaly.py +189 -0
  69. aria_code/agents/realty/exit_settlement.py +208 -0
  70. aria_code/agents/realty/fulfillment_risk.py +206 -0
  71. aria_code/agents/realty/ops_optimize.py +160 -0
  72. aria_code/agents/realty/revenue_share.py +215 -0
  73. aria_code/agents/registry.py +168 -0
  74. aria_code/agents/signal_scheme.py +111 -0
  75. aria_code/agents/sports/__init__.py +0 -0
  76. aria_code/agents/sports/football_agent.py +197 -0
  77. aria_code/agents/supervisor.py +113 -0
  78. aria_code/agents/team.py +500 -0
  79. aria_code/agents/video/__init__.py +16 -0
  80. aria_code/agents/video/director.py +376 -0
  81. aria_code/agents/video/qa_agent.py +143 -0
  82. aria_code/agents/video/script_doctor.py +112 -0
  83. aria_code/agents/warehouse/__init__.py +25 -0
  84. aria_code/agents/warehouse/contracts.py +26 -0
  85. aria_code/agents/warehouse/dashboard.py +155 -0
  86. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  87. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  88. aria_code/agents/warehouse/inventory_health.py +24 -0
  89. aria_code/agents/warehouse/local_run.py +71 -0
  90. aria_code/agents/warehouse/local_snapshot.py +132 -0
  91. aria_code/agents/warehouse/logistics_cost.py +84 -0
  92. aria_code/agents/warehouse/logistics_sync.py +21 -0
  93. aria_code/agents/warehouse/navigation.schema.json +110 -0
  94. aria_code/agents/warehouse/task_agent.py +172 -0
  95. aria_code/agents/warehouse/workflow.py +31 -0
  96. aria_code/aliyun_data_client.py +4 -0
  97. aria_code/apps/README.md +12 -0
  98. aria_code/apps/__init__.py +2 -0
  99. aria_code/apps/channels/README.md +9 -0
  100. aria_code/apps/channels/__init__.py +17 -0
  101. aria_code/apps/channels/approvals.py +11 -0
  102. aria_code/apps/channels/conversation.py +11 -0
  103. aria_code/apps/channels/digest.py +11 -0
  104. aria_code/apps/channels/intake.py +92 -0
  105. aria_code/apps/channels/registry.py +11 -0
  106. aria_code/apps/channels/tradingview.py +94 -0
  107. aria_code/apps/cli/README.md +13 -0
  108. aria_code/apps/cli/__init__.py +2 -0
  109. aria_code/apps/cli/_optional.py +120 -0
  110. aria_code/apps/cli/app_server.py +656 -0
  111. aria_code/apps/cli/auto_update.py +125 -0
  112. aria_code/apps/cli/bootstrap.py +206 -0
  113. aria_code/apps/cli/broker_render.py +206 -0
  114. aria_code/apps/cli/chat_turn.py +1327 -0
  115. aria_code/apps/cli/cloud_models.py +231 -0
  116. aria_code/apps/cli/codegen_paths.py +29 -0
  117. aria_code/apps/cli/commands/__init__.py +34 -0
  118. aria_code/apps/cli/commands/_business_render.py +78 -0
  119. aria_code/apps/cli/commands/_ui.py +71 -0
  120. aria_code/apps/cli/commands/analysis_cmds.py +378 -0
  121. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  122. aria_code/apps/cli/commands/auth_cmds.py +215 -0
  123. aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
  124. aria_code/apps/cli/commands/broker_cmds.py +1236 -0
  125. aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
  126. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  127. aria_code/apps/cli/commands/catalog.py +189 -0
  128. aria_code/apps/cli/commands/core_cmds.py +2748 -0
  129. aria_code/apps/cli/commands/data_cmds.py +521 -0
  130. aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
  131. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  132. aria_code/apps/cli/commands/engineering_cmds.py +399 -0
  133. aria_code/apps/cli/commands/file_cmds.py +312 -0
  134. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  135. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  136. aria_code/apps/cli/commands/market.py +544 -0
  137. aria_code/apps/cli/commands/market_cmds.py +1419 -0
  138. aria_code/apps/cli/commands/market_context.py +432 -0
  139. aria_code/apps/cli/commands/market_render.py +7 -0
  140. aria_code/apps/cli/commands/model_cmds.py +2374 -0
  141. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  142. aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
  143. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  144. aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
  145. aria_code/apps/cli/commands/report.py +476 -0
  146. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  147. aria_code/apps/cli/commands/session_cmds.py +236 -0
  148. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  149. aria_code/apps/cli/commands/team.py +723 -0
  150. aria_code/apps/cli/commands/team_render.py +8 -0
  151. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  152. aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
  153. aria_code/apps/cli/commands/workflow_cmds.py +629 -0
  154. aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
  155. aria_code/apps/cli/config_paths.py +73 -0
  156. aria_code/apps/cli/config_store.py +158 -0
  157. aria_code/apps/cli/constants.py +88 -0
  158. aria_code/apps/cli/context.py +34 -0
  159. aria_code/apps/cli/conversation_policy.py +53 -0
  160. aria_code/apps/cli/deterministic.py +200 -0
  161. aria_code/apps/cli/direct.py +48 -0
  162. aria_code/apps/cli/earnings_view.py +172 -0
  163. aria_code/apps/cli/endpoint_migration.py +64 -0
  164. aria_code/apps/cli/exec_events.py +255 -0
  165. aria_code/apps/cli/file_references.py +155 -0
  166. aria_code/apps/cli/finance_service_catalog.py +111 -0
  167. aria_code/apps/cli/football_reports.py +231 -0
  168. aria_code/apps/cli/github_app_auth.py +135 -0
  169. aria_code/apps/cli/google_login.py +239 -0
  170. aria_code/apps/cli/handlers/__init__.py +11 -0
  171. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  172. aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
  173. aria_code/apps/cli/handlers/market_handlers.py +2570 -0
  174. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  175. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  176. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  177. aria_code/apps/cli/headless.py +352 -0
  178. aria_code/apps/cli/helpers.py +326 -0
  179. aria_code/apps/cli/hooks.py +180 -0
  180. aria_code/apps/cli/i18n.py +310 -0
  181. aria_code/apps/cli/intent.py +136 -0
  182. aria_code/apps/cli/intent_router.py +305 -0
  183. aria_code/apps/cli/intent_shadow.py +72 -0
  184. aria_code/apps/cli/intent_signals.py +347 -0
  185. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  186. aria_code/apps/cli/local_tool_schemas.py +550 -0
  187. aria_code/apps/cli/main.py +39 -0
  188. aria_code/apps/cli/market_hours.py +100 -0
  189. aria_code/apps/cli/market_metadata.py +135 -0
  190. aria_code/apps/cli/market_universe.py +487 -0
  191. aria_code/apps/cli/message_processing.py +310 -0
  192. aria_code/apps/cli/mixin_binding.py +32 -0
  193. aria_code/apps/cli/model_catalog.py +652 -0
  194. aria_code/apps/cli/model_probe.py +169 -0
  195. aria_code/apps/cli/model_retirement.py +65 -0
  196. aria_code/apps/cli/native_branding.py +12 -0
  197. aria_code/apps/cli/native_bridge.py +118 -0
  198. aria_code/apps/cli/native_entry.py +58 -0
  199. aria_code/apps/cli/pdf_report.py +977 -0
  200. aria_code/apps/cli/plan_mode.py +139 -0
  201. aria_code/apps/cli/plotly_html.py +15 -0
  202. aria_code/apps/cli/prediction_feedback.py +202 -0
  203. aria_code/apps/cli/preflight.py +502 -0
  204. aria_code/apps/cli/project_aria.py +60 -0
  205. aria_code/apps/cli/prompt_assembly.py +78 -0
  206. aria_code/apps/cli/prompts/__init__.py +0 -0
  207. aria_code/apps/cli/prompts/coding.py +71 -0
  208. aria_code/apps/cli/prompts/select.py +193 -0
  209. aria_code/apps/cli/prompts/system_prompts.py +562 -0
  210. aria_code/apps/cli/prompts/ui.py +436 -0
  211. aria_code/apps/cli/provider_endpoints.py +237 -0
  212. aria_code/apps/cli/provider_keys.py +73 -0
  213. aria_code/apps/cli/providers/__init__.py +1 -0
  214. aria_code/apps/cli/providers/base.py +775 -0
  215. aria_code/apps/cli/providers/chat_routing.py +139 -0
  216. aria_code/apps/cli/providers/collaboration.py +98 -0
  217. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  218. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  219. aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
  220. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  221. aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
  222. aria_code/apps/cli/providers/runtime_bridge.py +586 -0
  223. aria_code/apps/cli/providers/vertexai_stream.py +592 -0
  224. aria_code/apps/cli/release_download.py +159 -0
  225. aria_code/apps/cli/review_runner.py +111 -0
  226. aria_code/apps/cli/runtime_consumer.py +769 -0
  227. aria_code/apps/cli/sandbox.py +32 -0
  228. aria_code/apps/cli/session_export.py +120 -0
  229. aria_code/apps/cli/session_jsonl.py +208 -0
  230. aria_code/apps/cli/session_refresh.py +123 -0
  231. aria_code/apps/cli/session_store.py +125 -0
  232. aria_code/apps/cli/skills_catalog.py +273 -0
  233. aria_code/apps/cli/task_isolation.py +211 -0
  234. aria_code/apps/cli/terminal_chrome.py +412 -0
  235. aria_code/apps/cli/todo_tracker.py +190 -0
  236. aria_code/apps/cli/tool_executor.py +932 -0
  237. aria_code/apps/cli/tool_registry.py +34 -0
  238. aria_code/apps/cli/tool_scope.py +113 -0
  239. aria_code/apps/cli/tools/__init__.py +40 -0
  240. aria_code/apps/cli/tools/context.py +46 -0
  241. aria_code/apps/cli/tools/file_tools.py +181 -0
  242. aria_code/apps/cli/tools/market_tools.py +626 -0
  243. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  244. aria_code/apps/cli/tools/system_tools.py +656 -0
  245. aria_code/apps/cli/tools/write_tools.py +961 -0
  246. aria_code/apps/cli/tradingview_bridge.py +529 -0
  247. aria_code/apps/cli/transactions.py +269 -0
  248. aria_code/apps/cli/turn_planning.py +137 -0
  249. aria_code/apps/cli/update_check.py +257 -0
  250. aria_code/apps/cli/updater.py +373 -0
  251. aria_code/apps/cli/utils/__init__.py +0 -0
  252. aria_code/apps/cli/utils/market_detect.py +1663 -0
  253. aria_code/apps/cli/web_login.py +136 -0
  254. aria_code/apps/cli/workflow_runner.py +127 -0
  255. aria_code/apps/cli/workspace_route.py +121 -0
  256. aria_code/apps/daemon/README.md +14 -0
  257. aria_code/apps/vscode/README.md +115 -0
  258. aria_code/apps/vscode/package.json +70 -0
  259. aria_code/aria_cli.py +5244 -0
  260. aria_code/aria_daemon.py +1372 -0
  261. aria_code/aria_feishu_bot.py +16 -0
  262. aria_code/aria_mcp_server.py +61 -0
  263. aria_code/aria_relay_client.py +4 -0
  264. aria_code/aria_telegram_bot.py +11 -0
  265. aria_code/ariarc.py +385 -0
  266. aria_code/artifacts.py +581 -0
  267. aria_code/backtest_engine.py +4 -0
  268. aria_code/backtest_report.py +4 -0
  269. aria_code/brokers/__init__.py +78 -0
  270. aria_code/brokers/automation.py +193 -0
  271. aria_code/brokers/base.py +231 -0
  272. aria_code/brokers/capabilities.py +264 -0
  273. aria_code/brokers/cn/__init__.py +10 -0
  274. aria_code/brokers/cn/easytrader_broker.py +193 -0
  275. aria_code/brokers/cn/futu_broker.py +194 -0
  276. aria_code/brokers/cn/longbridge_broker.py +190 -0
  277. aria_code/brokers/cn/tiger_broker.py +196 -0
  278. aria_code/brokers/cn/xtquant_broker.py +225 -0
  279. aria_code/brokers/config.py +414 -0
  280. aria_code/brokers/intl/__init__.py +5 -0
  281. aria_code/brokers/intl/alpaca_broker.py +183 -0
  282. aria_code/brokers/intl/ibkr_broker.py +215 -0
  283. aria_code/brokers/intl/trading212_broker.py +183 -0
  284. aria_code/brokers/intl/webull_broker.py +156 -0
  285. aria_code/brokers/paper_broker.py +266 -0
  286. aria_code/brokers/planning.py +296 -0
  287. aria_code/brokers/registry.py +213 -0
  288. aria_code/brokers/trading.py +267 -0
  289. aria_code/canva_client.py +4 -0
  290. aria_code/change_store.py +127 -0
  291. aria_code/clients/__init__.py +0 -0
  292. aria_code/clients/aliyun_data_client.py +660 -0
  293. aria_code/clients/aria_relay_client.py +16 -0
  294. aria_code/clients/canva_client.py +284 -0
  295. aria_code/clients/figma_client.py +100 -0
  296. aria_code/clients/football_data_client.py +1671 -0
  297. aria_code/clients/kling_video_client.py +182 -0
  298. aria_code/clients/market_data_client.py +1985 -0
  299. aria_code/clients/openai_image_client.py +242 -0
  300. aria_code/clients/runway_video_client.py +157 -0
  301. aria_code/clients/warehouse_erp_client.py +105 -0
  302. aria_code/cloud_memory.py +190 -0
  303. aria_code/command_safety.py +19 -0
  304. aria_code/computer_use_server.py +276 -0
  305. aria_code/computer_use_tools.py +4 -0
  306. aria_code/dashboard_generator.py +579 -0
  307. aria_code/data_analysis_tools.py +4 -0
  308. aria_code/data_cleaner.py +483 -0
  309. aria_code/data_service.py +513 -0
  310. aria_code/datasources/__init__.py +23 -0
  311. aria_code/datasources/base.py +166 -0
  312. aria_code/datasources/router.py +253 -0
  313. aria_code/datasources/sec_statements.py +217 -0
  314. aria_code/datasources/sources/__init__.py +15 -0
  315. aria_code/datasources/sources/akshare_source.py +269 -0
  316. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  317. aria_code/datasources/sources/edgar_source.py +219 -0
  318. aria_code/datasources/sources/finnhub_source.py +198 -0
  319. aria_code/datasources/sources/fred_source.py +223 -0
  320. aria_code/datasources/sources/tushare_source.py +143 -0
  321. aria_code/datasources/sources/web_scraper_source.py +278 -0
  322. aria_code/datasources/sources/world_bank_source.py +206 -0
  323. aria_code/datasources/sources/yfinance_source.py +152 -0
  324. aria_code/doctor.py +860 -0
  325. aria_code/domain/__init__.py +0 -0
  326. aria_code/domain/_finance_core/__init__.py +5 -0
  327. aria_code/domain/_finance_core/backtest_engine.py +325 -0
  328. aria_code/domain/_finance_core/performance_stats.py +101 -0
  329. aria_code/domain/backtest_engine.py +75 -0
  330. aria_code/domain/backtest_report.py +488 -0
  331. aria_code/domain/finance_formulas.py +389 -0
  332. aria_code/domain/performance_stats.py +6 -0
  333. aria_code/domain/portfolio_ledger.py +307 -0
  334. aria_code/domain/strategy_vault.py +421 -0
  335. aria_code/evals/__init__.py +37 -0
  336. aria_code/evals/behavior.py +113 -0
  337. aria_code/evals/gap.py +93 -0
  338. aria_code/evals/harness.py +949 -0
  339. aria_code/evals/review_probe.py +132 -0
  340. aria_code/evals/runner.py +425 -0
  341. aria_code/evals/scorecard.py +322 -0
  342. aria_code/evals/summary.py +125 -0
  343. aria_code/external_agent_runner.py +115 -0
  344. aria_code/figma_client.py +4 -0
  345. aria_code/file_analysis_tools.py +4 -0
  346. aria_code/finance_formulas.py +4 -0
  347. aria_code/football_data_client.py +4 -0
  348. aria_code/image_gen_tools.py +4 -0
  349. aria_code/intent_classifier.py +459 -0
  350. aria_code/kling_video_client.py +4 -0
  351. aria_code/licensing.py +151 -0
  352. aria_code/local_finance_tools.py +4 -0
  353. aria_code/local_image_provider.py +4 -0
  354. aria_code/local_llm_provider.py +4 -0
  355. aria_code/macro_tools.py +4 -0
  356. aria_code/markdown_pdf.py +289 -0
  357. aria_code/market_data_client.py +4 -0
  358. aria_code/mcp_client.py +744 -0
  359. aria_code/memory_manager.py +273 -0
  360. aria_code/model_capability.py +512 -0
  361. aria_code/notification_tools.py +4 -0
  362. aria_code/openai_image_client.py +4 -0
  363. aria_code/packages/__init__.py +26 -0
  364. aria_code/packages/adk_bridge/__init__.py +11 -0
  365. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  366. aria_code/packages/adk_bridge/market_tools.py +125 -0
  367. aria_code/packages/aria_agents/__init__.py +5 -0
  368. aria_code/packages/aria_agents/manifest.py +78 -0
  369. aria_code/packages/aria_core/__init__.py +34 -0
  370. aria_code/packages/aria_core/architecture.py +269 -0
  371. aria_code/packages/aria_core/export.py +147 -0
  372. aria_code/packages/aria_core/manifest.py +65 -0
  373. aria_code/packages/aria_core/paths.py +57 -0
  374. aria_code/packages/aria_core/secure_file.py +68 -0
  375. aria_code/packages/aria_infra/__init__.py +15 -0
  376. aria_code/packages/aria_infra/arthera.py +58 -0
  377. aria_code/packages/aria_infra/doctor.py +246 -0
  378. aria_code/packages/aria_infra/product.py +40 -0
  379. aria_code/packages/aria_mcp/__init__.py +25 -0
  380. aria_code/packages/aria_mcp/bridge.py +232 -0
  381. aria_code/packages/aria_mcp/circuit.py +82 -0
  382. aria_code/packages/aria_mcp/config.py +162 -0
  383. aria_code/packages/aria_mcp/installer.py +95 -0
  384. aria_code/packages/aria_mcp/server.py +1705 -0
  385. aria_code/packages/aria_mcp/tools.py +81 -0
  386. aria_code/packages/aria_sdk/__init__.py +19 -0
  387. aria_code/packages/aria_sdk/client.py +393 -0
  388. aria_code/packages/aria_sdk/providers.py +70 -0
  389. aria_code/packages/aria_sdk/streaming.py +75 -0
  390. aria_code/packages/aria_sdk/types.py +86 -0
  391. aria_code/packages/aria_services/__init__.py +92 -0
  392. aria_code/packages/aria_services/cache.py +60 -0
  393. aria_code/packages/aria_services/context.py +356 -0
  394. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  395. aria_code/packages/aria_services/data.py +11 -0
  396. aria_code/packages/aria_services/provider_health.py +304 -0
  397. aria_code/packages/aria_services/references.py +247 -0
  398. aria_code/packages/aria_services/registry.py +249 -0
  399. aria_code/packages/aria_services/research_protocol.py +137 -0
  400. aria_code/packages/aria_services/research_quality.py +186 -0
  401. aria_code/packages/aria_services/settings.py +160 -0
  402. aria_code/packages/aria_services/usage.py +151 -0
  403. aria_code/packages/aria_skills/__init__.py +46 -0
  404. aria_code/packages/aria_skills/catalog.py +149 -0
  405. aria_code/packages/aria_skills/loader.py +455 -0
  406. aria_code/packages/aria_skills/registry.py +84 -0
  407. aria_code/packages/aria_tools/__init__.py +5 -0
  408. aria_code/packages/aria_tools/financial/__init__.py +5 -0
  409. aria_code/packages/aria_tools/financial/_paths.py +92 -0
  410. aria_code/packages/aria_tools/registry.py +129 -0
  411. aria_code/packages/quant_engine/__init__.py +33 -0
  412. aria_code/packages/quant_engine/_namespace.py +24 -0
  413. aria_code/packages/quant_engine/backtest/__init__.py +10 -0
  414. aria_code/packages/quant_engine/backtest/core.py +53 -0
  415. aria_code/packages/quant_engine/backtest/engine.py +102 -0
  416. aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
  417. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  418. aria_code/packages/quant_engine/sports/__init__.py +76 -0
  419. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  420. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  421. aria_code/packages/quant_engine/sports/elo.py +300 -0
  422. aria_code/packages/quant_engine/sports/form.py +188 -0
  423. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  424. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  425. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  426. aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
  427. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  428. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  429. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  430. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  431. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  432. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  433. aria_code/packs/__init__.py +85 -0
  434. aria_code/packs/base.py +168 -0
  435. aria_code/packs/finance/__init__.py +250 -0
  436. aria_code/packs/healthcare/__init__.py +40 -0
  437. aria_code/packs/logistics/__init__.py +171 -0
  438. aria_code/packs/payments/__init__.py +139 -0
  439. aria_code/packs/realty/__init__.py +190 -0
  440. aria_code/packs/registry.py +202 -0
  441. aria_code/packs/rules.py +90 -0
  442. aria_code/plan_utils.py +194 -0
  443. aria_code/plugin_loader.py +328 -0
  444. aria_code/portfolio_ledger.py +4 -0
  445. aria_code/preview_server.py +449 -0
  446. aria_code/privacy/__init__.py +5 -0
  447. aria_code/privacy/feedback.py +123 -0
  448. aria_code/project_review.py +278 -0
  449. aria_code/project_review_api.py +235 -0
  450. aria_code/project_review_server.py +18 -0
  451. aria_code/project_tools.py +4 -0
  452. aria_code/providers/__init__.py +30 -0
  453. aria_code/providers/google_video_provider.py +256 -0
  454. aria_code/providers/llm/__init__.py +19 -0
  455. aria_code/providers/llm/anthropic.py +184 -0
  456. aria_code/providers/llm/arthera_gateway.py +90 -0
  457. aria_code/providers/llm/autoconfig.py +213 -0
  458. aria_code/providers/llm/base.py +139 -0
  459. aria_code/providers/llm/custom.py +158 -0
  460. aria_code/providers/llm/ollama.py +137 -0
  461. aria_code/providers/llm/openai_compat.py +350 -0
  462. aria_code/providers/llm/registry.py +457 -0
  463. aria_code/providers/local_image_provider.py +202 -0
  464. aria_code/providers/local_llm_provider.py +549 -0
  465. aria_code/providers/ollama_models.py +79 -0
  466. aria_code/providers/openai_params.py +32 -0
  467. aria_code/providers/tool_messages.py +100 -0
  468. aria_code/realty_data_tools.py +4 -0
  469. aria_code/report_exporters.py +150 -0
  470. aria_code/report_generator.py +1764 -0
  471. aria_code/review_service.py +497 -0
  472. aria_code/runtime/__init__.py +176 -0
  473. aria_code/runtime/acceptance.py +632 -0
  474. aria_code/runtime/agent_loop.py +1851 -0
  475. aria_code/runtime/agent_profiles.py +75 -0
  476. aria_code/runtime/approval.py +71 -0
  477. aria_code/runtime/baseline.py +219 -0
  478. aria_code/runtime/budget.py +232 -0
  479. aria_code/runtime/checkpoints.py +481 -0
  480. aria_code/runtime/consult.py +65 -0
  481. aria_code/runtime/contract.py +282 -0
  482. aria_code/runtime/delivery.py +432 -0
  483. aria_code/runtime/events.py +113 -0
  484. aria_code/runtime/gateway.py +167 -0
  485. aria_code/runtime/lsp.py +484 -0
  486. aria_code/runtime/native_index.py +153 -0
  487. aria_code/runtime/orchestrator.py +55 -0
  488. aria_code/runtime/processes.py +343 -0
  489. aria_code/runtime/project_graph.py +614 -0
  490. aria_code/runtime/repo_map.py +712 -0
  491. aria_code/runtime/research_bundle.py +105 -0
  492. aria_code/runtime/review.py +269 -0
  493. aria_code/runtime/run_state.py +127 -0
  494. aria_code/runtime/run_store.py +522 -0
  495. aria_code/runtime/scoped_runs.py +113 -0
  496. aria_code/runtime/self_healing.py +422 -0
  497. aria_code/runtime/semantic_diff.py +171 -0
  498. aria_code/runtime/subagent.py +721 -0
  499. aria_code/runtime/symbol_edit.py +117 -0
  500. aria_code/runtime/task_brief.py +46 -0
  501. aria_code/runtime/task_context.py +145 -0
  502. aria_code/runtime/task_ledger.py +108 -0
  503. aria_code/runtime/task_orchestrator.py +90 -0
  504. aria_code/runtime/task_outcome.py +33 -0
  505. aria_code/runtime/task_verification.py +266 -0
  506. aria_code/runtime/task_worktree.py +448 -0
  507. aria_code/runtime/tool_executor.py +329 -0
  508. aria_code/runtime/tool_policy.py +107 -0
  509. aria_code/runtime/transactions.py +216 -0
  510. aria_code/runtime/windows_job.py +115 -0
  511. aria_code/runtime/workflows.py +222 -0
  512. aria_code/runtime/worktrees.py +215 -0
  513. aria_code/runway_video_client.py +4 -0
  514. aria_code/safety/__init__.py +27 -0
  515. aria_code/safety/client_isolation.py +178 -0
  516. aria_code/safety/permissions.py +339 -0
  517. aria_code/safety/risk.py +699 -0
  518. aria_code/safety/sandbox.py +148 -0
  519. aria_code/safety/service.py +102 -0
  520. aria_code/service_endpoints.py +14 -0
  521. aria_code/setup_wizard.py +684 -0
  522. aria_code/spreadsheet_tools.py +4 -0
  523. aria_code/strategy_vault.py +4 -0
  524. aria_code/tools/__init__.py +0 -0
  525. aria_code/tools/artifact_tools.py +252 -0
  526. aria_code/tools/broker_tools.py +92 -0
  527. aria_code/tools/build_quant_engine.py +97 -0
  528. aria_code/tools/code_audit_tools.py +192 -0
  529. aria_code/tools/computer_use_tools.py +509 -0
  530. aria_code/tools/data_analysis_tools.py +831 -0
  531. aria_code/tools/enterprise_finance_tools.py +202 -0
  532. aria_code/tools/extended_tools.py +240 -0
  533. aria_code/tools/file_analysis_tools.py +880 -0
  534. aria_code/tools/image_gen_tools.py +114 -0
  535. aria_code/tools/local_finance_tools.py +3488 -0
  536. aria_code/tools/logistics_carriers.py +11 -0
  537. aria_code/tools/logistics_inventory.py +11 -0
  538. aria_code/tools/logistics_io.py +11 -0
  539. aria_code/tools/logistics_tenancy.py +11 -0
  540. aria_code/tools/logistics_tools.py +11 -0
  541. aria_code/tools/macro_tools.py +412 -0
  542. aria_code/tools/notification_tools.py +249 -0
  543. aria_code/tools/project_tools.py +525 -0
  544. aria_code/tools/realty_data_tools.py +662 -0
  545. aria_code/tools/spreadsheet_tools.py +399 -0
  546. aria_code/tools/stripe_tools.py +115 -0
  547. aria_code/tools/video_tools.py +500 -0
  548. aria_code/training/__init__.py +1 -0
  549. aria_code/training/compare.py +117 -0
  550. aria_code/training/prelude_commands.py +111 -0
  551. aria_code/training/prelude_eval.py +390 -0
  552. aria_code/training/prelude_real.py +183 -0
  553. aria_code/training/prelude_train.py +248 -0
  554. aria_code/training/sft_dataset.py +230 -0
  555. aria_code/training/trajectory_dataset.py +263 -0
  556. aria_code/ui/__init__.py +105 -0
  557. aria_code/ui/banner.py +404 -0
  558. aria_code/ui/completer.py +571 -0
  559. aria_code/ui/console.py +285 -0
  560. aria_code/ui/input_box.py +505 -0
  561. aria_code/ui/mentions.py +206 -0
  562. aria_code/ui/picker.py +248 -0
  563. aria_code/ui/render/__init__.py +11 -0
  564. aria_code/ui/render/actions.py +297 -0
  565. aria_code/ui/render/finance.py +1732 -0
  566. aria_code/ui/render/market.py +253 -0
  567. aria_code/ui/render/output.py +954 -0
  568. aria_code/ui/render/responsive.py +75 -0
  569. aria_code/ui/render/team.py +367 -0
  570. aria_code/ui/robot.py +239 -0
  571. aria_code/ui/startup_dashboard.py +110 -0
  572. aria_code/video_analysis.py +147 -0
  573. aria_code/video_editor.py +518 -0
  574. aria_code/video_workflow/__init__.py +8 -0
  575. aria_code/video_workflow/__main__.py +61 -0
  576. aria_code/video_workflow/engines/__init__.py +18 -0
  577. aria_code/video_workflow/engines/base.py +69 -0
  578. aria_code/video_workflow/engines/custom_model.py +204 -0
  579. aria_code/video_workflow/engines/google_veo.py +94 -0
  580. aria_code/video_workflow/engines/local_engine.py +71 -0
  581. aria_code/video_workflow/engines/registry.py +98 -0
  582. aria_code/video_workflow/google_models.py +278 -0
  583. aria_code/video_workflow/music/__init__.py +5 -0
  584. aria_code/video_workflow/music/foley_service.py +132 -0
  585. aria_code/video_workflow/music/service.py +212 -0
  586. aria_code/video_workflow/pipeline.py +620 -0
  587. aria_code/video_workflow/plan.py +202 -0
  588. aria_code/video_workflow/session.py +297 -0
  589. aria_code/workspace/__init__.py +6 -0
  590. aria_code/workspace/files.py +219 -0
  591. aria_code/workspace/verify.py +167 -0
  592. aria_code/workspace_changes.py +199 -0
  593. aria_code/workspace_service.py +196 -0
  594. aria_code/workspace_tools.py +150 -0
  595. aria_code-0.167.0.dist-info/METADATA +330 -0
  596. aria_code-0.167.0.dist-info/RECORD +601 -0
  597. aria_code-0.167.0.dist-info/WHEEL +5 -0
  598. aria_code-0.167.0.dist-info/entry_points.txt +5 -0
  599. aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
  600. aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
  601. aria_code-0.167.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,961 @@
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 _client_isolation_error(p: pathlib.Path, content: str) -> str | None:
122
+ """Why a client's document must not be written as it stands (aria_code.safety.client_isolation).
123
+
124
+ A failure inside the check never blocks a write: it is a guard on top of
125
+ the write path, not a new way for writing to break.
126
+ """
127
+ try:
128
+ from aria_code.safety.client_isolation import check_write
129
+ return check_write(p, content, [pathlib.Path.cwd()])
130
+ except Exception:
131
+ return None
132
+
133
+
134
+ def _is_safe(p: pathlib.Path, params: dict | None = None) -> bool:
135
+ if params and params.get("_workspace"):
136
+ from aria_code.workspace.files import WorkspaceSecurity
137
+ return WorkspaceSecurity.from_tool_params(params).is_safe_path(p, write=True)
138
+ return _ac()._is_safe_path(p)
139
+
140
+
141
+ def _config_dir() -> str:
142
+ return str(_ac().CONFIG_DIR)
143
+
144
+
145
+ def _sessions_dir() -> str:
146
+ return str(_ac().SESSIONS_DIR)
147
+
148
+
149
+ def _ChangeConflictError():
150
+ return _ac().ChangeConflictError
151
+
152
+
153
+ def _record_checkpoint(change, params: dict, *, existed_before: bool, before_mode: "int | None") -> tuple:
154
+ """Persist an applied change without making checkpoint I/O break the write."""
155
+ if params.get("_disable_checkpoints"):
156
+ return None, None
157
+ run_id = params.get("_run_id")
158
+ session_id = str(params.get("_session_id") or "")
159
+ if not run_id and not session_id:
160
+ return None, None
161
+ try:
162
+ from aria_code.runtime.checkpoints import CheckpointStore
163
+ checkpoint = CheckpointStore().record_change(
164
+ path=change.path,
165
+ before_content=change.before_content,
166
+ after_content=change.after_content,
167
+ existed_before=existed_before,
168
+ before_mode=before_mode,
169
+ source=change.source,
170
+ run_id=run_id,
171
+ session_id=session_id,
172
+ label=pathlib.Path(change.path).name,
173
+ metadata={"change_id": change.change_id},
174
+ )
175
+ return checkpoint.checkpoint_id, None
176
+ except Exception as exc:
177
+ return None, str(exc)
178
+
179
+
180
+ # ── Helpers ───────────────────────────────────────────────────────────────────
181
+
182
+ def _strip_markdown_fences(content: str) -> str:
183
+ """Strip markdown code fences that LLMs sometimes wrap around file content."""
184
+ stripped = content.strip()
185
+ if stripped.startswith("```"):
186
+ first_nl = stripped.find("\n")
187
+ if first_nl >= 0:
188
+ stripped = stripped[first_nl + 1:]
189
+ else:
190
+ return content
191
+ if stripped.rstrip().endswith("```"):
192
+ stripped = stripped.rstrip()[:-3].rstrip()
193
+ if stripped != content.strip():
194
+ return stripped + "\n"
195
+ return content
196
+
197
+
198
+ def _auto_fix_python(content: str, path: str) -> str:
199
+ """Auto-inject missing imports and validate syntax for Python files."""
200
+ if not path.endswith(".py"):
201
+ return content
202
+
203
+ lines = content.split("\n")
204
+ imports_present: set[str] = set()
205
+ first_non_comment = 0
206
+ for i, line in enumerate(lines):
207
+ s = line.strip()
208
+ if s and not s.startswith("#") and not s.startswith('"""') and not s.startswith("'''"):
209
+ first_non_comment = i
210
+ break
211
+
212
+ for line in lines:
213
+ s = line.strip()
214
+ if s.startswith("import "):
215
+ parts = s.split()
216
+ if len(parts) >= 2:
217
+ imports_present.add(parts[1].split(".")[0].split(",")[0])
218
+ elif s.startswith("from "):
219
+ parts = s.split()
220
+ if len(parts) >= 2:
221
+ imports_present.add(parts[1].split(".")[0])
222
+
223
+ code = content
224
+ needed: list[str] = []
225
+
226
+ if ("os.path" in code or "os.expanduser" in code or "os.getcwd" in code
227
+ or "os.makedirs" in code) and "os" not in imports_present:
228
+ needed.append("import os")
229
+ if ("sys." in code or "sys.exit" in code) and "sys" not in imports_present:
230
+ needed.append("import sys")
231
+ if "np." in code and "numpy" not in imports_present and "np" not in imports_present:
232
+ needed.append("import numpy as np")
233
+ if "pd." in code and "pandas" not in imports_present and "pd" not in imports_present:
234
+ needed.append("import pandas as pd")
235
+ if "yf." in code and "yfinance" not in imports_present and "yf" not in imports_present:
236
+ needed.append("import yfinance as yf")
237
+
238
+ has_plt = "plt." in code
239
+ has_matplotlib_use = "matplotlib.use" in code
240
+ if has_plt and "matplotlib" not in imports_present:
241
+ needed.append("import matplotlib; matplotlib.use('Agg')")
242
+ needed.append("import matplotlib.pyplot as plt")
243
+ elif has_plt and not has_matplotlib_use:
244
+ for i, line in enumerate(lines):
245
+ if "import matplotlib.pyplot" in line and "matplotlib.use" not in "\n".join(lines[:i]):
246
+ lines.insert(i, "import matplotlib; matplotlib.use('Agg')")
247
+ content = "\n".join(lines)
248
+ break
249
+
250
+ if "mpf." in code and "mplfinance" not in imports_present and "mpf" not in imports_present:
251
+ needed.append("import mplfinance as mpf")
252
+ if "re." in code and "re" not in imports_present:
253
+ needed.append("import re")
254
+ if "json." in code and "json" not in imports_present:
255
+ needed.append("import json")
256
+ if "datetime" in code and "datetime" not in imports_present:
257
+ needed.append("from datetime import datetime, timedelta")
258
+ if (_re.search(r'\bta\.(?:sma|ema|rsi|macd|bbands|stoch|atr|adx|obv|vwap)\b', code)
259
+ or "pandas_ta" in code) and "ta" not in imports_present and "pandas_ta" not in imports_present:
260
+ needed.append("import pandas_ta as ta")
261
+ if (_re.search(r'\bgo\.(?:Figure|Candlestick|Scatter|Bar|Heatmap|Layout|Table)', code)
262
+ or "px." in code or "plotly" in code) and "plotly" not in imports_present:
263
+ if "go.Figure" in code or "go.Candlestick" in code:
264
+ needed.append("import plotly.graph_objects as go")
265
+ if "px." in code:
266
+ needed.append("import plotly.express as px")
267
+ if "make_subplots" in code:
268
+ needed.append("from plotly.subplots import make_subplots")
269
+ if "scipy" in code and "scipy" not in imports_present:
270
+ needed.append("import scipy")
271
+
272
+ has_warnings_in_needed = any("warnings" in n for n in needed)
273
+ if ("yf." in code or "pd." in code) and "warnings" not in imports_present and not has_warnings_in_needed:
274
+ needed.insert(0, "import warnings; warnings.filterwarnings('ignore')")
275
+ elif "warnings" in code and "warnings" not in imports_present and not has_warnings_in_needed:
276
+ needed.append("import warnings")
277
+
278
+ if needed:
279
+ for imp in reversed(needed):
280
+ lines.insert(first_non_comment, imp)
281
+ content = "\n".join(lines)
282
+
283
+ try:
284
+ import ast
285
+ ast.parse(content)
286
+ except SyntaxError as e:
287
+ console, has_rich = _ui()
288
+ if has_rich and console:
289
+ console.print(f" [dim]Warning: syntax issue at line {e.lineno}: {e.msg}[/dim]")
290
+
291
+ return content
292
+
293
+
294
+ def _write_policy_confirm(p: pathlib.Path, content: str, existed: bool) -> tuple:
295
+ """Prompt user to confirm a write. Returns (approved: bool, final_path: Path)."""
296
+ import difflib
297
+ console, has_rich = _ui()
298
+ lines_new = content.count("\n") + 1
299
+ desktop = pathlib.Path.home() / "Desktop"
300
+ is_desktop = str(p).startswith(str(desktop))
301
+
302
+ if has_rich and console:
303
+ console.print()
304
+ if existed:
305
+ old_content = p.read_text(encoding="utf-8", errors="replace")
306
+ diff = list(difflib.unified_diff(
307
+ old_content.splitlines(keepends=True),
308
+ content.splitlines(keepends=True),
309
+ fromfile=f"current/{p.name}",
310
+ tofile=f"new/{p.name}",
311
+ n=2,
312
+ ))
313
+ added = sum(1 for l in diff if l.startswith("+") and not l.startswith("+++"))
314
+ removed = sum(1 for l in diff if l.startswith("-") and not l.startswith("---"))
315
+ console.print(f" [yellow]⚠ Overwrite[/yellow] [bold]{p}[/bold]")
316
+ console.print(f" [dim] +{added} lines -{removed} lines ({lines_new} total)[/dim]")
317
+ for line in diff[:8]:
318
+ if line.startswith("+") and not line.startswith("+++"):
319
+ console.print(f" [green]{line.rstrip()}[/green]")
320
+ elif line.startswith("-") and not line.startswith("---"):
321
+ console.print(f" [red]{line.rstrip()}[/red]")
322
+ else:
323
+ loc = "[dim cyan](Desktop)[/dim cyan]" if is_desktop else "[yellow](outside Desktop)[/yellow]"
324
+ console.print(f" [cyan]New file[/cyan] {loc} [bold]{p}[/bold] ({lines_new} lines)")
325
+ console.print()
326
+ choice = console.input(" [bold]Write this file?[/bold] [dim]\\[y/n/r=redirect path][/dim] ").strip().lower()
327
+ else:
328
+ print()
329
+ print(f" {'Overwrite' if existed else 'New file'}: {p} ({lines_new} lines)")
330
+ choice = input(" Write this file? [y/n/r=redirect path] ").strip().lower()
331
+
332
+ if choice == "r":
333
+ if has_rich and console:
334
+ new_path_str = console.input(" [dim]Enter new path: [/dim]").strip()
335
+ else:
336
+ new_path_str = input(" Enter new path: ").strip()
337
+ if new_path_str:
338
+ new_p = pathlib.Path(new_path_str).expanduser().resolve()
339
+ if _is_safe(new_p):
340
+ return True, new_p
341
+ if has_rich and console:
342
+ console.print(f" [red]Path not allowed: {new_p}[/red]")
343
+ else:
344
+ print(f" Path not allowed: {new_p}")
345
+ return False, p
346
+
347
+ return choice in ("y", "yes", ""), p
348
+
349
+
350
+ # ── Rich diff helper ─────────────────────────────────────────────────────────
351
+
352
+ def _print_inline_diff(old_str: str, new_str: str, console, max_lines: int = 12) -> None:
353
+ """Print a compact color-coded inline diff after an edit_file call."""
354
+ old_lines = old_str.splitlines()
355
+ new_lines = new_str.splitlines()
356
+
357
+ total_changed = max(len(old_lines), len(new_lines))
358
+ if total_changed > max_lines:
359
+ console.print(f" [dim] (diff too large to display inline — {total_changed} lines)[/dim]")
360
+ return
361
+
362
+ for line in old_lines:
363
+ display = line[:120]
364
+ console.print(f" [red dim]- {display}[/red dim]")
365
+ for line in new_lines:
366
+ display = line[:120]
367
+ console.print(f" [green dim]+ {display}[/green dim]")
368
+
369
+
370
+ # ── Public tool functions ─────────────────────────────────────────────────────
371
+
372
+ def _attach_verification_hint(result_data: dict, path, console=None, has_rich: bool = False) -> None:
373
+ """Note the checks this change calls for, on the tool result.
374
+
375
+ One function instead of three pasted copies. The copies differed only in
376
+ what the result dict and the console were called in each enclosing scope,
377
+ and papered over that with ``_wdata if "_wdata" in locals() else _data``
378
+ plus ``locals().get("console", locals().get("_console2"))``. Both names are
379
+ compiled unconditionally, so whichever was absent from a given function was
380
+ an undefined reference — three F821s that only avoided raising because a
381
+ bare ``except Exception: pass`` sat around them, which would equally have
382
+ hidden a real failure. Passing them in makes the dependency visible and
383
+ checkable.
384
+
385
+ Advisory only: the acceptance gate is what actually runs these commands
386
+ after the turn. This tells the model what is coming, so it can factor the
387
+ check into what it does next rather than being surprised by it.
388
+ """
389
+ try:
390
+ import os
391
+
392
+ from aria_code.workspace.verify import VerificationPlanner
393
+
394
+ plan = VerificationPlanner(root=os.getcwd()).infer([str(path)])
395
+ except Exception:
396
+ # Planning is a convenience; a successful write must not be downgraded
397
+ # because the planner is unavailable.
398
+ return
399
+ if not plan.commands:
400
+ return
401
+
402
+ joined = " && ".join(plan.commands)
403
+ result_data["suggested_verification"] = (
404
+ f"Verification recommended ({plan.reason}): run `{joined}`"
405
+ )
406
+ try:
407
+ shown = joined.replace(str(path), str(pathlib.Path(path).relative_to(pathlib.Path(os.getcwd()).resolve())))
408
+ except ValueError:
409
+ shown = joined
410
+ if has_rich and console is not None:
411
+ try:
412
+ console.print(f" [cyan]✓ {'推荐验证命令' if _ui_zh() else 'Suggested check'}: {shown}[/cyan]")
413
+ except Exception:
414
+ pass
415
+
416
+
417
+ # Text that stands in for content: an ellipsis, `pass`, TODO/TBD, "your code here", "<code>".
418
+ _PLACEHOLDER_CONTENT = _re.compile(
419
+ r"^(?:[.…]{1,}|pass|todo|tbd|fixme|placeholder|lorem ipsum|your (?:code|content|text) here|"
420
+ r"(?:insert|add|put|write) (?:your )?(?:code|content|text) here|\[?(?:code|content|text)\]?|"
421
+ r"#\s*(?:todo|tbd|fixme|your code|code|implement|placeholder)\b.*|//\s*(?:todo|tbd|fixme|your code|code|implement|placeholder)\b.*)$",
422
+ _re.IGNORECASE,
423
+ )
424
+
425
+
426
+ def tool_write_file(params: dict) -> dict:
427
+ """Write content to a file (create or overwrite)."""
428
+ path = params.get("path", "")
429
+ content = params.get("content", "")
430
+ skip_confirm = params.get("_skip_confirm", False)
431
+ stage_only = bool(params.get("stage_only", False))
432
+
433
+ if not path:
434
+ return {"success": False, "error": "Missing 'path' parameter"}
435
+ if not content:
436
+ return {"success": False, "error": "Missing 'content' parameter"}
437
+
438
+ content = _strip_markdown_fences(content)
439
+ stripped_check = content.strip()
440
+
441
+ if not stripped_check:
442
+ return {"success": False, "error": "Content is empty (only whitespace). Write the file's real content."}
443
+
444
+ # A placeholder is rejected for what it says, not for how short it is. The old rule
445
+ # refused every file under 20 characters, which also refused real ones: a route of
446
+ # `RRDDRRDRRUU`, a VERSION file, a two-line CSV. A model told "write the complete
447
+ # script" for a file that is complete tends to pad it, and the padding breaks it.
448
+ if len(stripped_check) < 60 and _PLACEHOLDER_CONTENT.match(stripped_check):
449
+ return {"success": False,
450
+ "error": f"Content is a placeholder ('{stripped_check[:40]}'). "
451
+ "Write the real, complete content of the file."}
452
+
453
+ if (stripped_check.startswith("<") and stripped_check.endswith(">")
454
+ and "\n" not in stripped_check and len(stripped_check) < 200
455
+ and not stripped_check.lower().startswith("<!doctype")
456
+ and not stripped_check.lower().startswith("<html")):
457
+ return {"success": False,
458
+ "error": f"Content appears to be a placeholder tag: '{stripped_check[:120]}'. "
459
+ "Write the complete code with imports, data fetching, computation, and output."}
460
+
461
+ # Reject trivial stub Python scripts — only print() with no real logic
462
+ if path.endswith(".py"):
463
+ import re as _re
464
+ _boilerplate = {
465
+ "#!/usr/bin/env python", "#!/usr/bin/python", "# -*- coding:",
466
+ 'if __name__ == "__main__":', "if __name__ == '__main__':",
467
+ "def main():", "main()", "",
468
+ }
469
+ _work_lines = [
470
+ ln.strip() for ln in stripped_check.splitlines()
471
+ if ln.strip() and ln.strip() not in _boilerplate
472
+ and not ln.strip().startswith("#")
473
+ ]
474
+ _all_print = _work_lines and all(
475
+ _re.match(r'^print\s*\(', ln) for ln in _work_lines
476
+ )
477
+ if _all_print and len(_work_lines) <= 3:
478
+ return {
479
+ "success": False,
480
+ "error": (
481
+ f"拒绝写入: '{pathlib.Path(path).name or 'file'}' 只包含 print() 语句,是无意义的占位脚本。"
482
+ " 请直接用文字输出结果,或者写包含真实逻辑的代码(网络请求、数据处理、计算等)。"
483
+ ),
484
+ }
485
+
486
+ content = _auto_fix_python(content, path)
487
+
488
+ try:
489
+ raw_path = pathlib.Path(path).expanduser()
490
+ if not raw_path.is_absolute():
491
+ raw_path = _relative_write_base() / raw_path
492
+ p = raw_path.resolve()
493
+ if not _is_safe(p, params):
494
+ return {"success": False, "error": f"Access denied: path '{p}' is outside allowed directories"}
495
+ leak = _client_isolation_error(p, content)
496
+ if leak:
497
+ return {"success": False, "error": leak, "data": {"client_isolation": True}}
498
+
499
+ existed = p.exists()
500
+ desktop = pathlib.Path.home() / "Desktop"
501
+ import tempfile as _tf
502
+ from aria_code.artifacts import user_output_root
503
+ user_root = user_output_root().resolve()
504
+ _auto_trusted_prefixes = (
505
+ str(desktop),
506
+ str(user_root),
507
+ str(pathlib.Path(_tf.gettempdir()).resolve()),
508
+ "/tmp", "/private/tmp", "/private/var/folders",
509
+ _config_dir(), _sessions_dir(),
510
+ )
511
+ is_auto_trusted = any(str(p).startswith(pfx) for pfx in _auto_trusted_prefixes)
512
+ policy = _write_policy()[0]
513
+
514
+ needs_confirm = (
515
+ not skip_confirm
516
+ and not is_auto_trusted
517
+ and (
518
+ policy == "always_confirm"
519
+ or policy in ("desktop_only", "confirm_outside")
520
+ or existed
521
+ )
522
+ )
523
+
524
+ if needs_confirm:
525
+ approved, p = _write_policy_confirm(p, content, existed)
526
+ if not approved:
527
+ return {"success": False, "error": "Write cancelled by user.",
528
+ "data": {"cancelled": True}}
529
+
530
+ existed = p.exists()
531
+ before_mode = _stat.S_IMODE(p.stat().st_mode) if p.is_file() else None
532
+
533
+ console, has_rich = _ui()
534
+ store = _change_store()
535
+ change = store.stage(p, content, source="write_file")
536
+ lines = content.count("\n") + 1
537
+ action = "Updated" if existed else "Created"
538
+
539
+ if stage_only:
540
+ label = "Staged update" if existed else "Staged create"
541
+ if has_rich and console:
542
+ console.print(f" [dim]{label} {p} ({lines} lines, change {change.change_id})[/dim]")
543
+ else:
544
+ print(f" {label} {p} ({lines} lines, change {change.change_id})")
545
+ return {"success": True, "data": {
546
+ "path": str(p), "action": "staged", "lines": lines,
547
+ "change_id": change.change_id,
548
+ "before_hash": change.before_hash, "after_hash": change.after_hash,
549
+ "diff": change.diff, "staged": True, "applied": False,
550
+ }}
551
+
552
+ try:
553
+ applied = store.apply(change.change_id)
554
+ except _ChangeConflictError() as exc:
555
+ return {"success": False, "error": str(exc), "data": {"change_id": change.change_id}}
556
+ checkpoint_id, checkpoint_error = _record_checkpoint(
557
+ applied,
558
+ params,
559
+ existed_before=existed,
560
+ before_mode=before_mode,
561
+ )
562
+
563
+ desktop = pathlib.Path.home() / "Desktop"
564
+ is_on_desktop = str(p).startswith(str(desktop))
565
+ # In the project the user is working in, a written file is where they
566
+ # expect it: name it relative to the project, and do not open Finder.
567
+ # Revealing every .py write popped a Finder window per file in a coding
568
+ # session; that, and the "saved to" hint, are for files that land
569
+ # somewhere else (the generated folder).
570
+ try:
571
+ in_project = p.is_relative_to(pathlib.Path.cwd().resolve())
572
+ except (OSError, ValueError):
573
+ in_project = False
574
+ shown = str(p.relative_to(pathlib.Path.cwd().resolve())) if in_project else str(p)
575
+
576
+ import platform as _platform
577
+ import subprocess as _sub
578
+ _sys_name = _platform.system()
579
+ if _sys_name == "Darwin":
580
+ _reveal_hint = f'open -R "{p}"'
581
+ elif _sys_name == "Windows":
582
+ _reveal_hint = f'explorer /select,"{p}"'
583
+ else:
584
+ _reveal_hint = f'xdg-open "{p.parent}"'
585
+
586
+ if has_rich and console:
587
+ console.print(f" [dim]{action} [bold]{shown}[/bold] ({lines} lines)[/dim]")
588
+ if not in_project and not is_on_desktop and p.suffix == ".py":
589
+ zh = _ui_zh()
590
+ console.print(
591
+ f" [dim]{'提示: 文件保存在' if zh else 'Saved to'} [yellow]{p}[/yellow]\n"
592
+ f" {'打开所在目录' if zh else 'Show in folder'}: [cyan]{_reveal_hint}[/cyan][/dim]"
593
+ )
594
+ else:
595
+ print(f" {action} {shown} ({lines} lines)")
596
+
597
+ # Auto-reveal .py/.ipynb strategy files in file manager (non-blocking)
598
+ if p.suffix in (".py", ".ipynb") and not in_project:
599
+ try:
600
+ if _sys_name == "Darwin":
601
+ _sub.Popen(["open", "-R", str(p)],
602
+ stdout=_sub.DEVNULL, stderr=_sub.DEVNULL)
603
+ elif _sys_name == "Windows":
604
+ _sub.Popen(["explorer", f"/select,{str(p)}"],
605
+ stdout=_sub.DEVNULL, stderr=_sub.DEVNULL)
606
+ except Exception:
607
+ pass
608
+
609
+ try:
610
+ size_bytes = p.stat().st_size
611
+ except Exception:
612
+ size_bytes = len(content.encode("utf-8"))
613
+
614
+ _syntax_warn = _verify_python_syntax(p, content)
615
+ _console2, _has_rich2 = _ui()
616
+ if _syntax_warn and _has_rich2 and _console2:
617
+ _console2.print(" [yellow]⚠ 语法检查未通过[/yellow]")
618
+
619
+ _wdata = {
620
+ "path": str(p),
621
+ "absolute_path": str(p),
622
+ "action": action.lower(),
623
+ "lines": lines,
624
+ "size_bytes": size_bytes,
625
+ "change_id": applied.change_id,
626
+ "before_hash": applied.before_hash,
627
+ "after_hash": applied.after_hash,
628
+ "diff": applied.diff,
629
+ "staged": True,
630
+ "applied": True,
631
+ "user_message": (f"文件已保存到: {p} 打开所在目录: {_reveal_hint}" if _ui_zh()
632
+ else f"Saved to {p} · show in folder: {_reveal_hint}"),
633
+ }
634
+ try:
635
+ from aria_code.artifacts import register_existing_artifact
636
+
637
+ artifact = register_existing_artifact(
638
+ p,
639
+ topic=str(params.get("artifact_topic") or ""),
640
+ kind=str(params.get("artifact_kind") or ""),
641
+ metadata={"source": "write_file"},
642
+ )
643
+ if artifact is not None:
644
+ _wdata["artifact_registered"] = True
645
+ _wdata["artifact_metadata_path"] = str(artifact.metadata_path)
646
+ except Exception:
647
+ # Artifact indexing is convenience metadata; a successful file
648
+ # write must not be downgraded if indexing is unavailable.
649
+ pass
650
+ if checkpoint_id:
651
+ _wdata["checkpoint_id"] = checkpoint_id
652
+ if checkpoint_error:
653
+ _wdata["checkpoint_error"] = checkpoint_error
654
+ if _syntax_warn:
655
+ _wdata["syntax_check"] = "failed"
656
+ return {"success": True, "data": _wdata, "warning": _syntax_warn}
657
+
658
+
659
+ _attach_verification_hint(_wdata, p, _console2, _has_rich2)
660
+
661
+ _lsp_warn, _lsp_diags = _lsp_autocheck(p)
662
+ if _lsp_warn:
663
+ _print_lsp_diags(_lsp_diags, _console2, _has_rich2)
664
+ _wdata["diagnostics"] = _lsp_diags
665
+ return {"success": True, "data": _wdata, "warning": _lsp_warn}
666
+ return {"success": True, "data": _wdata}
667
+ except Exception as e:
668
+ return {"success": False, "error": str(e)}
669
+
670
+
671
+ def _ui_zh() -> bool:
672
+ """Whether the user's configured UI language is Chinese (tools get no config)."""
673
+ try:
674
+ from aria_code.apps.cli.bootstrap import default_config, runtime_paths
675
+ from aria_code.apps.cli.config_store import load_cli_config
676
+
677
+ return str(load_cli_config(runtime_paths(), default_config()).get("ui_lang", "en")).lower().startswith("zh")
678
+ except Exception:
679
+ return False
680
+
681
+
682
+ def _relative_write_base() -> pathlib.Path:
683
+ """Where a relative write_file path lands: the working directory.
684
+
685
+ It used to be ~/Documents/Aria Code/generated always, while read_file,
686
+ edit_file and run_command all work in the current directory — so "write
687
+ fx.py and run its tests" wrote the file in one place and ran the tests in
688
+ another, and edit_file could not find what write_file had just made.
689
+ Only a session started in the home directory or at the filesystem root
690
+ keeps the generated folder, so chatting from ~ does not scatter files.
691
+ """
692
+ from aria_code.artifacts import user_generated_dir
693
+
694
+ cwd = pathlib.Path.cwd().resolve()
695
+ if cwd in (pathlib.Path.home().resolve(), pathlib.Path(cwd.anchor)):
696
+ return user_generated_dir()
697
+ return cwd
698
+
699
+
700
+ def _find_all(text: str, needle: str) -> list[int]:
701
+ found, at = [], text.find(needle)
702
+ while at >= 0:
703
+ found.append(at)
704
+ at = text.find(needle, at + len(needle))
705
+ return found
706
+
707
+
708
+ def tool_edit_file(params: dict) -> dict:
709
+ """Edit a file by replacing old_string with new_string.
710
+
711
+ old_string must match exactly one place unless replace_all is set. This
712
+ replaced the first match whatever the count, so an old_string that also
713
+ appeared earlier in the file (a repeated `return None`, a second
714
+ `except Exception:`) edited the wrong place and reported success.
715
+ multi_edit already refused that.
716
+ """
717
+ # `symbol` names a definition instead of quoting it (runtime/symbol_edit).
718
+ # Usually already resolved at approval, so the preview showed the change.
719
+ from aria_code.runtime.symbol_edit import resolve_symbol_edit
720
+
721
+ symbol_error = resolve_symbol_edit(params)
722
+ if symbol_error:
723
+ return {"success": False, "error": f"symbol edit: {symbol_error}"}
724
+ path = params.get("path", "")
725
+ old_str = params.get("old_string", params.get("old_str", ""))
726
+ new_str = params.get("new_string", params.get("new_str", ""))
727
+ replace_all = bool(params.get("replace_all", False))
728
+ stage_only = bool(params.get("stage_only", False))
729
+
730
+ if not path:
731
+ return {"success": False, "error": "Missing 'path' parameter"}
732
+ if not old_str:
733
+ return {"success": False, "error": "Missing 'old_string' parameter"}
734
+
735
+ try:
736
+ p = pathlib.Path(path).expanduser().resolve()
737
+ if not p.exists():
738
+ return {"success": False, "error": f"File not found: {p}"}
739
+ if not _is_safe(p, params):
740
+ return {"success": False, "error": f"Access denied: path '{p}' is outside allowed directories"}
741
+
742
+ content = p.read_text(encoding="utf-8", errors="replace")
743
+ before_mode = _stat.S_IMODE(p.stat().st_mode)
744
+ if content.count(old_str) == 0:
745
+ preview = "\n".join(content.splitlines()[:10])
746
+ return {"success": False,
747
+ "error": f"old_string not found in file. "
748
+ f"The file starts with:\n{preview}\n\n"
749
+ f"HINT: Use read_file to see the actual content, then retry edit_file "
750
+ f"with the correct old_string. Or use write_file to overwrite the entire file."}
751
+ occurrences = content.count(old_str)
752
+ if occurrences > 1 and not replace_all:
753
+ lines = [content.count("\n", 0, i) + 1 for i in _find_all(content, old_str)][:10]
754
+ return {"success": False,
755
+ "error": f"old_string matches {occurrences} places (lines {', '.join(map(str, lines))}). "
756
+ f"No change made. Include more surrounding lines so it matches one place, "
757
+ f"or pass replace_all=true to change every one."}
758
+
759
+ replacements = occurrences if replace_all else 1
760
+ new_content = content.replace(old_str, new_str, -1 if replace_all else 1)
761
+ leak = _client_isolation_error(p, new_content)
762
+ if leak:
763
+ return {"success": False, "error": leak, "data": {"client_isolation": True}}
764
+ store = _change_store()
765
+ change = store.stage(p, new_content, source="edit_file")
766
+ added = len(new_str.splitlines())
767
+ removed = len(old_str.splitlines())
768
+ console, has_rich = _ui()
769
+
770
+ if stage_only:
771
+ if has_rich and console:
772
+ console.print(f" [dim]Staged edit {p} (change {change.change_id})[/dim]")
773
+ else:
774
+ print(f" Staged edit {p} (change {change.change_id})")
775
+ return {"success": True, "data": {
776
+ "path": str(p), "replacements": replacements,
777
+ "lines": new_content.count("\n") + 1,
778
+ "change_id": change.change_id,
779
+ "before_hash": change.before_hash, "after_hash": change.after_hash,
780
+ "diff": change.diff, "staged": True, "applied": False,
781
+ }}
782
+
783
+ try:
784
+ applied = store.apply(change.change_id)
785
+ except _ChangeConflictError() as exc:
786
+ return {"success": False, "error": str(exc), "data": {"change_id": change.change_id}}
787
+ checkpoint_id, checkpoint_error = _record_checkpoint(
788
+ applied,
789
+ params,
790
+ existed_before=True,
791
+ before_mode=before_mode,
792
+ )
793
+
794
+ if has_rich and console:
795
+ parts = []
796
+ if added > 0:
797
+ parts.append(f"[green]+{added}[/green]")
798
+ if removed > 0:
799
+ parts.append(f"[red]-{removed}[/red]")
800
+ short_path = str(p.name) if len(str(p)) > 60 else str(p)
801
+ console.print(f" [dim]✎ [bold]{short_path}[/bold] ({', '.join(parts)} lines)[/dim]")
802
+ _print_inline_diff(old_str, new_str, console)
803
+ else:
804
+ print(f" Applied (+{added}, -{removed} lines)")
805
+
806
+ _syntax_warn = _verify_python_syntax(p, new_content)
807
+ if _syntax_warn and has_rich and console:
808
+ console.print(" [yellow]⚠ 语法检查未通过[/yellow]")
809
+
810
+ _data = {
811
+ "path": str(p), "replacements": replacements,
812
+ "lines": new_content.count("\n") + 1,
813
+ "change_id": applied.change_id,
814
+ "before_hash": applied.before_hash, "after_hash": applied.after_hash,
815
+ "diff": applied.diff, "staged": True, "applied": True,
816
+ }
817
+ if checkpoint_id:
818
+ _data["checkpoint_id"] = checkpoint_id
819
+ if checkpoint_error:
820
+ _data["checkpoint_error"] = checkpoint_error
821
+ if _syntax_warn:
822
+ _data["syntax_check"] = "failed"
823
+ return {"success": True, "data": _data, "warning": _syntax_warn}
824
+
825
+ # Opt-in LSP diagnostics (catches what the syntax check can't)
826
+
827
+ _attach_verification_hint(_data, p, console, has_rich)
828
+
829
+ _lsp_warn, _lsp_diags = _lsp_autocheck(p)
830
+ if _lsp_warn:
831
+ _print_lsp_diags(_lsp_diags, console, has_rich)
832
+ _data["diagnostics"] = _lsp_diags
833
+ return {"success": True, "data": _data, "warning": _lsp_warn}
834
+ return {"success": True, "data": _data}
835
+ except Exception as e:
836
+ return {"success": False, "error": str(e)}
837
+
838
+
839
+ def tool_multi_edit(params: dict) -> dict:
840
+ """Apply multiple find/replace edits to a single file atomically.
841
+
842
+ All edits must succeed or none are applied (the file is only written once,
843
+ after every edit has been validated against the in-memory content). Edits
844
+ are applied in order, so a later edit can match text introduced by an
845
+ earlier one.
846
+ """
847
+ path = params.get("path", "")
848
+ edits = params.get("edits", [])
849
+ stage_only = bool(params.get("stage_only", False))
850
+
851
+ if not path:
852
+ return {"success": False, "error": "Missing 'path' parameter"}
853
+ if not isinstance(edits, list) or not edits:
854
+ return {"success": False, "error": "Missing 'edits' — expected a non-empty array of "
855
+ "{old_string, new_string, replace_all?}"}
856
+
857
+ try:
858
+ p = pathlib.Path(path).expanduser().resolve()
859
+ if not p.exists():
860
+ return {"success": False, "error": f"File not found: {p}"}
861
+ if not _is_safe(p, params):
862
+ return {"success": False, "error": f"Access denied: path '{p}' is outside allowed directories"}
863
+
864
+ content = p.read_text(encoding="utf-8", errors="replace")
865
+ before_mode = _stat.S_IMODE(p.stat().st_mode)
866
+ working = content
867
+ applied_count = 0
868
+ total_added = 0
869
+ total_removed = 0
870
+
871
+ # ── Phase 1: validate + apply every edit in memory (atomic) ───────────
872
+ for i, ed in enumerate(edits):
873
+ if not isinstance(ed, dict):
874
+ return {"success": False, "error": f"edit #{i + 1} is not an object"}
875
+ old_s = ed.get("old_string", ed.get("old_str", ""))
876
+ new_s = ed.get("new_string", ed.get("new_str", ""))
877
+ replace_all = bool(ed.get("replace_all", False))
878
+ if not old_s:
879
+ return {"success": False, "error": f"edit #{i + 1} missing 'old_string'"}
880
+ occurrences = working.count(old_s)
881
+ if occurrences == 0:
882
+ return {"success": False, "error":
883
+ f"edit #{i + 1}: old_string not found. No edits applied (atomic). "
884
+ f"HINT: read_file first to copy exact text. old_string was:\n"
885
+ f"{old_s[:200]}"}
886
+ if occurrences > 1 and not replace_all:
887
+ return {"success": False, "error":
888
+ f"edit #{i + 1}: old_string matches {occurrences} places — pass "
889
+ f"replace_all=true to replace all, or make old_string unique."}
890
+ working = working.replace(old_s, new_s, -1 if replace_all else 1)
891
+ applied_count += 1
892
+ total_added += len(new_s.splitlines())
893
+ total_removed += len(old_s.splitlines())
894
+
895
+ if working == content:
896
+ return {"success": False, "error": "No changes — edits produced identical content."}
897
+
898
+ # ── Phase 2: stage + apply once ───────────────────────────────────────
899
+ store = _change_store()
900
+ leak = _client_isolation_error(p, working)
901
+ if leak:
902
+ return {"success": False, "error": leak, "data": {"client_isolation": True}}
903
+ change = store.stage(p, working, source="multi_edit")
904
+ console, has_rich = _ui()
905
+
906
+ if stage_only:
907
+ return {"success": True, "data": {
908
+ "path": str(p), "edits_applied": applied_count,
909
+ "lines": working.count("\n") + 1,
910
+ "change_id": change.change_id,
911
+ "before_hash": change.before_hash, "after_hash": change.after_hash,
912
+ "diff": change.diff, "staged": True, "applied": False,
913
+ }}
914
+
915
+ try:
916
+ applied = store.apply(change.change_id)
917
+ except _ChangeConflictError() as exc:
918
+ return {"success": False, "error": str(exc), "data": {"change_id": change.change_id}}
919
+ checkpoint_id, checkpoint_error = _record_checkpoint(
920
+ applied,
921
+ params,
922
+ existed_before=True,
923
+ before_mode=before_mode,
924
+ )
925
+
926
+ if has_rich and console:
927
+ console.print(f" [dim]Applied {applied_count} edits "
928
+ f"([green]+{total_added}[/green]/[red]-{total_removed}[/red] lines)[/dim]")
929
+ else:
930
+ print(f" Applied {applied_count} edits (+{total_added}/-{total_removed} lines)")
931
+
932
+ _syntax_warn = _verify_python_syntax(p, working)
933
+ if _syntax_warn and has_rich and console:
934
+ console.print(" [yellow]⚠ 语法检查未通过[/yellow]")
935
+
936
+ _data = {
937
+ "path": str(p), "edits_applied": applied_count,
938
+ "lines": working.count("\n") + 1,
939
+ "change_id": applied.change_id,
940
+ "before_hash": applied.before_hash, "after_hash": applied.after_hash,
941
+ "diff": applied.diff, "staged": True, "applied": True,
942
+ }
943
+ if checkpoint_id:
944
+ _data["checkpoint_id"] = checkpoint_id
945
+ if checkpoint_error:
946
+ _data["checkpoint_error"] = checkpoint_error
947
+ if _syntax_warn:
948
+ _data["syntax_check"] = "failed"
949
+ return {"success": True, "data": _data, "warning": _syntax_warn}
950
+
951
+
952
+ _attach_verification_hint(_data, p, console, has_rich)
953
+
954
+ _lsp_warn, _lsp_diags = _lsp_autocheck(p)
955
+ if _lsp_warn:
956
+ _print_lsp_diags(_lsp_diags, console, has_rich)
957
+ _data["diagnostics"] = _lsp_diags
958
+ return {"success": True, "data": _data, "warning": _lsp_warn}
959
+ return {"success": True, "data": _data}
960
+ except Exception as e:
961
+ return {"success": False, "error": str(e)}