aria-code 4.1.3__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 (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
@@ -0,0 +1,669 @@
1
+ """System-level tools: run_command, web_fetch, github.
2
+
3
+ All functions are pure (no module-level globals). Console output is
4
+ injected via keyword args so aria_cli.py thin wrappers supply the
5
+ Rich console and global state defaults.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import pathlib
11
+ import re
12
+ import shlex
13
+ import subprocess
14
+ import sys
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+
18
+ _ROOT = Path(__file__).parent.parent.parent.parent # aria-code/
19
+ if str(_ROOT) not in sys.path:
20
+ sys.path.insert(0, str(_ROOT))
21
+
22
+ from safety import evaluate_command_policy # noqa: E402
23
+
24
+
25
+ def _persist_command_output(command: str, stdout: str, stderr: str, returncode: int) -> dict:
26
+ """Persist full command output when it is too large for inline tool context."""
27
+ stdout_lines = stdout.splitlines()
28
+ stderr_lines = stderr.splitlines()
29
+ if (
30
+ len(stdout) <= 2500
31
+ and len(stderr) <= 1200
32
+ and len(stdout_lines) <= 18
33
+ and len(stderr_lines) <= 8
34
+ ):
35
+ return {}
36
+ try:
37
+ from artifacts import create_artifact, write_artifact_metadata
38
+
39
+ record = create_artifact(
40
+ "command-output",
41
+ "shell",
42
+ "command_output",
43
+ ".txt",
44
+ timestamp=datetime.now(),
45
+ )
46
+ text = (
47
+ f"$ {command}\n"
48
+ f"exit_code={returncode}\n\n"
49
+ "===== STDOUT =====\n"
50
+ f"{stdout}"
51
+ "\n\n===== STDERR =====\n"
52
+ f"{stderr}"
53
+ )
54
+ record.path.write_text(text, encoding="utf-8", errors="replace")
55
+ write_artifact_metadata(record, {
56
+ "kind": "command_output",
57
+ "status": "complete" if returncode == 0 else "failed",
58
+ "created_at": datetime.now().isoformat(timespec="seconds"),
59
+ "command": command,
60
+ "exit_code": returncode,
61
+ "stdout_chars": len(stdout),
62
+ "stderr_chars": len(stderr),
63
+ "stdout_truncated_inline": len(stdout) > 5000,
64
+ "stderr_truncated_inline": len(stderr) > 2000,
65
+ })
66
+ return {"full_output_path": str(record.path)}
67
+ except Exception:
68
+ return {}
69
+
70
+
71
+ def _cprint(msg: str, *, console, has_rich: bool) -> None:
72
+ if has_rich and console is not None:
73
+ console.print(msg)
74
+ else:
75
+ plain = re.sub(r"\[/?[^\]]+\]", "", msg)
76
+ print(plain)
77
+
78
+
79
+ def tool_run_command(
80
+ params: dict,
81
+ *,
82
+ console=None,
83
+ has_rich: bool = True,
84
+ ) -> dict:
85
+ """Run a shell command and return output.
86
+
87
+ ``params`` should contain ``permission_mode`` and ``network_enabled``
88
+ (filled in by the aria_cli.py wrapper from the active globals).
89
+ """
90
+ command = params.get("command", "")
91
+ # LLMs sometimes send command as a list e.g. ['bash', '-lc', '...'] — normalize to string
92
+ if isinstance(command, list):
93
+ import shlex as _shlex
94
+ command = _shlex.join(str(c) for c in command)
95
+ params["command"] = command
96
+ if not command:
97
+ return {"success": False, "error": "Missing 'command' parameter"}
98
+
99
+ effective_policy = params.get("policy", "safe")
100
+ if params.get("user_approved") and effective_policy == "safe":
101
+ effective_policy = "balanced"
102
+
103
+ decision = evaluate_command_policy(
104
+ command,
105
+ effective_policy,
106
+ mode=params.get("permission_mode", "safe"),
107
+ network_enabled=bool(params.get("network_enabled", True)),
108
+ )
109
+ command = decision.normalized_command
110
+
111
+ dangerous = ["rm -rf /", "mkfs", "dd if=", "> /dev/", ":(){ :", "fork bomb"]
112
+ for d in dangerous:
113
+ if d in command:
114
+ return {"success": False, "error": f"Blocked dangerous command: {command}"}
115
+
116
+ # Prevent executing text/doc files as Python — they are analysis reports, not scripts
117
+ import re as _re_cmd
118
+ _py3_file = _re_cmd.search(r'\bpython3?\s+["\']?(\S+\.(?:txt|md|docx|csv|json|log))', command)
119
+ if _py3_file:
120
+ _bad_file = _py3_file.group(1)
121
+ return {
122
+ "success": False,
123
+ "error": (
124
+ f"拒绝执行: '{_bad_file}' 是文本/分析文件,不是 Python 脚本。\n"
125
+ "如需展示分析结果,请直接输出文字,或将分析结论写入 .py 文件后执行。"
126
+ ),
127
+ }
128
+
129
+ if params.get("dry_run"):
130
+ return {"success": True, "data": {
131
+ "command": command,
132
+ "risk": decision.risk,
133
+ "policy": decision.policy,
134
+ "requires_approval": getattr(decision, "requires_approval", False),
135
+ "network": getattr(decision, "network", False),
136
+ "dry_run": True,
137
+ }}
138
+ if not decision.allowed:
139
+ return {"success": False, "error": decision.reason}
140
+ try:
141
+ cwd = params.get("cwd", None)
142
+ timeout = min(params.get("timeout", 120), 300)
143
+ use_shell = True
144
+ argv = None
145
+ if decision.risk == "low":
146
+ has_shell_meta = any(ch in command for ch in ["|", "&", ";", "<", ">", "$", "`", "\n"])
147
+ if not has_shell_meta:
148
+ try:
149
+ argv = shlex.split(command)
150
+ if argv:
151
+ use_shell = False
152
+ except ValueError:
153
+ use_shell = True
154
+ argv = None
155
+
156
+ result = subprocess.run(
157
+ argv if (argv and not use_shell) else command,
158
+ shell=use_shell,
159
+ capture_output=True,
160
+ text=True,
161
+ timeout=timeout,
162
+ cwd=cwd,
163
+ )
164
+ full_stdout = result.stdout
165
+ full_stderr = result.stderr
166
+ output = full_stdout[-5000:] if len(full_stdout) > 5000 else full_stdout
167
+ stderr = full_stderr[-2000:] if len(full_stderr) > 2000 else full_stderr
168
+ output_artifact = _persist_command_output(command, full_stdout, full_stderr, result.returncode)
169
+
170
+ # ── Auto-fix loop (up to 3 rounds for python3 scripts) ──────────────
171
+ MAX_AUTO_FIX_ROUNDS = 3
172
+ _cmd_tail = (
173
+ command.strip().split("python3 ", 1)[-1].strip().split()
174
+ if command.strip().startswith("python3 ") else []
175
+ )
176
+ if result.returncode != 0 and _cmd_tail:
177
+ script_path = _cmd_tail[0]
178
+ script_p = pathlib.Path(script_path).expanduser().resolve()
179
+
180
+ for _fix_round in range(MAX_AUTO_FIX_ROUNDS):
181
+ combined_err = (output + " " + stderr).strip()
182
+ auto_fixed = False
183
+
184
+ if not (script_p.exists() and script_p.suffix == ".py"):
185
+ break
186
+ script_content = script_p.read_text(errors="replace")
187
+
188
+ name_match = re.search(r"NameError: name ['\"](\w+)['\"] is not defined", combined_err)
189
+ if name_match and not auto_fixed:
190
+ missing = name_match.group(1)
191
+ import_map = {
192
+ "os": "import os", "sys": "import sys", "re": "import re",
193
+ "json": "import json", "math": "import math", "time": "import time",
194
+ "np": "import numpy as np", "pd": "import pandas as pd",
195
+ "yf": "import yfinance as yf", "plt": "import matplotlib.pyplot as plt",
196
+ "mpf": "import mplfinance as mpf",
197
+ "datetime": "from datetime import datetime, timedelta",
198
+ "Path": "from pathlib import Path",
199
+ "timedelta": "from datetime import datetime, timedelta",
200
+ "go": "import plotly.graph_objects as go",
201
+ "px": "import plotly.express as px",
202
+ "ta": "import pandas_ta as ta", "warnings": "import warnings",
203
+ "make_subplots": "from plotly.subplots import make_subplots",
204
+ "bt": "import backtrader as bt", "vbt": "import vectorbt as vbt",
205
+ "ccxt": "import ccxt", "requests": "import requests",
206
+ "BeautifulSoup": "from bs4 import BeautifulSoup",
207
+ "tqdm": "from tqdm import tqdm",
208
+ "xgb": "import xgboost as xgb",
209
+ "Prophet": "from prophet import Prophet",
210
+ "arch": "from arch import arch_model",
211
+ "statsmodels": "import statsmodels.api as sm",
212
+ "sm": "import statsmodels.api as sm",
213
+ }
214
+ fix_import = import_map.get(missing)
215
+ if fix_import and fix_import not in script_content:
216
+ lines = script_content.split("\n")
217
+ insert_at = 0
218
+ for i, l in enumerate(lines):
219
+ if l.strip().startswith("#!") or l.strip().startswith("# -*-"):
220
+ insert_at = i + 1
221
+ else:
222
+ break
223
+ lines.insert(insert_at, fix_import)
224
+ if missing == "plt" and "matplotlib.use" not in script_content:
225
+ lines.insert(insert_at, "import matplotlib; matplotlib.use('Agg')")
226
+ script_p.write_text("\n".join(lines))
227
+ auto_fixed = True
228
+ _cprint(
229
+ f" [#C08050]Auto-fix[{_fix_round+1}/{MAX_AUTO_FIX_ROUNDS}]:"
230
+ f"[/#C08050] [dim]added '{fix_import}'[/dim]",
231
+ console=console, has_rich=has_rich,
232
+ )
233
+
234
+ if not auto_fixed and (
235
+ "cannot be resolved at runtime" in combined_err.lower()
236
+ or ("matplotlib" in combined_err and "backend" in combined_err.lower())
237
+ ):
238
+ if "matplotlib.use" not in script_content and "matplotlib.pyplot" in script_content:
239
+ script_content = script_content.replace(
240
+ "import matplotlib.pyplot as plt",
241
+ "import matplotlib; matplotlib.use('Agg')\nimport matplotlib.pyplot as plt",
242
+ )
243
+ script_p.write_text(script_content)
244
+ auto_fixed = True
245
+ _cprint(
246
+ f" [#C08050]Auto-fix[{_fix_round+1}]:[/#C08050]"
247
+ " [dim]added matplotlib.use('Agg')[/dim]",
248
+ console=console, has_rich=has_rich,
249
+ )
250
+
251
+ key_match = re.search(
252
+ r"KeyError: ['\"]?(Close|Open|High|Low|Volume|Adj Close)", combined_err
253
+ )
254
+ if key_match and not auto_fixed and "yfinance" in script_content:
255
+ if "columns.droplevel" not in script_content:
256
+ fix_line = (
257
+ "\n# Fix yfinance MultiIndex columns\n"
258
+ "if isinstance(df.columns, pd.MultiIndex):\n"
259
+ " df.columns = df.columns.droplevel(1)\n"
260
+ )
261
+ dl_match = re.search(r"(.*=\s*yf\.download\([^)]+\))", script_content)
262
+ if dl_match:
263
+ script_content = script_content.replace(
264
+ dl_match.group(0), dl_match.group(0) + fix_line
265
+ )
266
+ script_p.write_text(script_content)
267
+ auto_fixed = True
268
+ _cprint(
269
+ f" [#C08050]Auto-fix[{_fix_round+1}]:[/#C08050]"
270
+ " [dim]MultiIndex column fix[/dim]",
271
+ console=console, has_rich=has_rich,
272
+ )
273
+
274
+ attr_match = re.search(
275
+ r"AttributeError: '(\w+)' object has no attribute '(\w+)'", combined_err
276
+ )
277
+ if attr_match and not auto_fixed:
278
+ obj_type, attr_name = attr_match.group(1), attr_match.group(2)
279
+ if obj_type == "DataFrame" and attr_name == "append":
280
+ script_content = re.sub(
281
+ r"(\w+)\.append\(([^)]+)\)",
282
+ r"pd.concat([\1, \2], ignore_index=True)",
283
+ script_content,
284
+ )
285
+ script_p.write_text(script_content)
286
+ auto_fixed = True
287
+ _cprint(
288
+ f" [#C08050]Auto-fix[{_fix_round+1}]:[/#C08050]"
289
+ " [dim]DataFrame.append→pd.concat[/dim]",
290
+ console=console, has_rich=has_rich,
291
+ )
292
+
293
+ if not auto_fixed and "TypeError" in combined_err:
294
+ if "auto_adjust" in combined_err and "auto_adjust" in script_content:
295
+ script_content = re.sub(
296
+ r",\s*auto_adjust\s*=\s*(True|False)", "", script_content
297
+ )
298
+ script_p.write_text(script_content)
299
+ auto_fixed = True
300
+ _cprint(
301
+ f" [#C08050]Auto-fix[{_fix_round+1}]:[/#C08050]"
302
+ " [dim]removed deprecated auto_adjust param[/dim]",
303
+ console=console, has_rich=has_rich,
304
+ )
305
+
306
+ mod_match = re.search(r"No module named ['\"]?(\w+)", combined_err)
307
+ if mod_match and not auto_fixed:
308
+ missing_mod = mod_match.group(1)
309
+ pip_map = {
310
+ "mplfinance": "mplfinance", "plotly": "plotly",
311
+ "pandas_ta": "pandas_ta", "ta": "ta",
312
+ "sklearn": "scikit-learn", "cv2": "opencv-python",
313
+ "bs4": "beautifulsoup4", "PIL": "Pillow",
314
+ "backtrader": "backtrader", "vectorbt": "vectorbt",
315
+ "ccxt": "ccxt", "prophet": "prophet",
316
+ "arch": "arch", "xgboost": "xgboost",
317
+ "lightgbm": "lightgbm", "statsmodels": "statsmodels",
318
+ "akshare": "akshare", "tushare": "tushare",
319
+ "empyrical": "empyrical", "pyfolio": "pyfolio",
320
+ "seaborn": "seaborn", "openpyxl": "openpyxl",
321
+ }
322
+ pip_pkg = pip_map.get(missing_mod, missing_mod)
323
+ _cprint(
324
+ f" [#C08050]Auto-fix[{_fix_round+1}]:[/#C08050]"
325
+ f" [dim]pip3 install {pip_pkg}[/dim]",
326
+ console=console, has_rich=has_rich,
327
+ )
328
+ pip_result = subprocess.run(
329
+ f"pip3 install {pip_pkg}", shell=True, capture_output=True,
330
+ text=True, timeout=60,
331
+ )
332
+ if pip_result.returncode == 0:
333
+ auto_fixed = True
334
+
335
+ if auto_fixed:
336
+ _cprint(
337
+ f" [dim]Re-running after auto-fix (round {_fix_round+1}/{MAX_AUTO_FIX_ROUNDS})...[/dim]",
338
+ console=console, has_rich=has_rich,
339
+ )
340
+ result = subprocess.run(
341
+ command, shell=True, capture_output=True, text=True,
342
+ timeout=timeout, cwd=cwd,
343
+ )
344
+ full_stdout = result.stdout
345
+ full_stderr = result.stderr
346
+ output = full_stdout[-5000:] if len(full_stdout) > 5000 else full_stdout
347
+ stderr = full_stderr[-2000:] if len(full_stderr) > 2000 else full_stderr
348
+ output_artifact = _persist_command_output(
349
+ command, full_stdout, full_stderr, result.returncode
350
+ )
351
+ if result.returncode == 0:
352
+ break
353
+ else:
354
+ break
355
+ # ── End auto-fix ─────────────────────────────────────────────────────
356
+
357
+ if has_rich and console is not None:
358
+ if result.returncode == 0:
359
+ console.print(f" [green]Command completed[/green] [dim](exit {result.returncode})[/dim]")
360
+ else:
361
+ console.print(f" [dim]Command exited {result.returncode}[/dim]")
362
+ out_preview = output.strip().splitlines()[:6]
363
+ for ol in out_preview:
364
+ console.print(f" [dim]{ol[:120]}[/dim]")
365
+ if len(output.strip().splitlines()) > 6:
366
+ console.print(" [dim]...truncated[/dim]")
367
+ if output_artifact.get("full_output_path"):
368
+ console.print(" [dim]full output saved[/dim]")
369
+ if stderr.strip() and result.returncode != 0:
370
+ for el in stderr.strip().splitlines()[:3]:
371
+ console.print(f" [red]{el[:120]}[/red]")
372
+ else:
373
+ print(f" Command exit: {result.returncode}")
374
+ data = {
375
+ "command": command, "exit_code": result.returncode,
376
+ "stdout": output, "stderr": stderr,
377
+ "stdout_truncated": len(full_stdout) > 5000,
378
+ "stderr_truncated": len(full_stderr) > 2000,
379
+ }
380
+ data.update(output_artifact)
381
+ return {"success": True, "data": data}
382
+ except subprocess.TimeoutExpired:
383
+ return {"success": False, "error": f"Command timed out ({timeout}s)"}
384
+ except KeyboardInterrupt:
385
+ _cprint(" [dim]Command interrupted[/dim]", console=console, has_rich=has_rich)
386
+ return {"success": False, "error": "Command interrupted by user (Ctrl+C)"}
387
+ except Exception as e:
388
+ return {"success": False, "error": str(e)}
389
+
390
+
391
+ def tool_web_fetch(params: dict) -> dict:
392
+ """Fetch the text content of any URL."""
393
+ url = params.get("url", "").strip()
394
+ if not url:
395
+ return {"success": False, "error": "Missing 'url' parameter"}
396
+ if not url.startswith(("http://", "https://")):
397
+ url = "https://" + url
398
+ max_chars = min(int(params.get("max_chars", 4000)), 12000)
399
+ timeout = min(int(params.get("timeout", 15)), 30)
400
+ try:
401
+ import urllib.request as _ur
402
+ import ssl as _ssl
403
+ _prx = _ur.getproxies()
404
+ headers = {
405
+ "User-Agent": (
406
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
407
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
408
+ "Chrome/124.0 Safari/537.36"
409
+ ),
410
+ "Accept": "text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.8",
411
+ }
412
+ _gh_m = re.match(
413
+ r"https://github\.com/([^/]+/[^/]+)/blob/([^?#]+)", url
414
+ )
415
+ if _gh_m:
416
+ url = f"https://raw.githubusercontent.com/{_gh_m.group(1)}/{_gh_m.group(2)}"
417
+
418
+ import requests as _req
419
+ s = _req.Session()
420
+ s.proxies = _prx
421
+ s.verify = False
422
+ r = s.get(url, headers=headers, timeout=timeout)
423
+ r.raise_for_status()
424
+ raw = r.text
425
+
426
+ ct = r.headers.get("content-type", "")
427
+ if "json" in ct or raw.lstrip().startswith(("{", "[")):
428
+ return {"success": True, "data": {
429
+ "url": url, "content_type": ct,
430
+ "text": raw[:max_chars], "length": len(raw),
431
+ }}
432
+
433
+ text = re.sub(r"<script[^>]*>.*?</script>", " ", raw, flags=re.DOTALL | re.I)
434
+ text = re.sub(r"<style[^>]*>.*?</style>", " ", text, flags=re.DOTALL | re.I)
435
+ text = re.sub(r"<[^>]+>", " ", text)
436
+ text = re.sub(r"&nbsp;", " ", text)
437
+ text = re.sub(r"&amp;", "&", text)
438
+ text = re.sub(r"&lt;", "<", text)
439
+ text = re.sub(r"&gt;", ">", text)
440
+ text = re.sub(r"&quot;", '"', text)
441
+ text = re.sub(r"\s{3,}", "\n", text)
442
+ text = text.strip()
443
+
444
+ return {"success": True, "data": {
445
+ "url": url, "content_type": ct,
446
+ "text": text[:max_chars], "length": len(text),
447
+ "truncated": len(text) > max_chars,
448
+ }}
449
+ except Exception as e:
450
+ return {"success": False, "error": f"web_fetch failed: {e}"}
451
+
452
+
453
+ def tool_github(
454
+ params: dict,
455
+ *,
456
+ console=None,
457
+ has_rich: bool = True,
458
+ ) -> dict:
459
+ """GitHub API / gh CLI integration.
460
+
461
+ actions: list_prs, list_issues, view_pr, view_issue, create_pr,
462
+ list_commits, read_file, search, pr_diff, pr_checks
463
+ """
464
+ action = params.get("action", "list_prs").lower().replace("-", "_")
465
+ cwd = params.get("cwd") or None
466
+ policy = "safe"
467
+
468
+ def _gh(cmd: str, timeout: int = 20) -> dict:
469
+ import shutil
470
+ if not shutil.which("gh"):
471
+ return {"success": False,
472
+ "error": "gh CLI not found. Install: brew install gh && gh auth login"}
473
+ return tool_run_command(
474
+ {"command": cmd, "cwd": cwd, "timeout": timeout, "policy": policy},
475
+ console=console,
476
+ has_rich=has_rich,
477
+ )
478
+
479
+ if action in ("list_prs", "prs", "pull_requests"):
480
+ state = params.get("state", "open")
481
+ limit = int(params.get("limit", 20))
482
+ return _gh(f"gh pr list --state {state} --limit {limit} "
483
+ "--json number,title,author,state,headRefName,url")
484
+
485
+ if action in ("list_issues", "issues"):
486
+ state = params.get("state", "open")
487
+ limit = int(params.get("limit", 20))
488
+ label = f' --label "{params["label"]}"' if params.get("label") else ""
489
+ return _gh(f"gh issue list --state {state} --limit {limit}{label} "
490
+ "--json number,title,author,state,labels,url")
491
+
492
+ if action in ("view_pr", "pr"):
493
+ number = params.get("number") or params.get("pr")
494
+ if not number:
495
+ return {"success": False, "error": "Missing 'number' parameter"}
496
+ return _gh(f"gh pr view {number} "
497
+ "--json number,title,body,state,headRefName,baseRefName,additions,deletions,files,url")
498
+
499
+ if action in ("view_issue", "issue"):
500
+ number = params.get("number") or params.get("issue")
501
+ if not number:
502
+ return {"success": False, "error": "Missing 'number' parameter"}
503
+ return _gh(f"gh issue view {number} "
504
+ "--json number,title,body,state,labels,comments,url")
505
+
506
+ if action == "create_pr":
507
+ title = params.get("title", "")
508
+ body = params.get("body", "")
509
+ branch = params.get("branch", "")
510
+ base = params.get("base", "main")
511
+ if not title:
512
+ return {"success": False, "error": "Missing 'title' for create_pr"}
513
+ b_flag = f"--head {shlex.quote(branch)}" if branch else ""
514
+ cmd = (
515
+ f"gh pr create --title {shlex.quote(title)} "
516
+ f"--body {shlex.quote(body)} "
517
+ f"--base {shlex.quote(base)} {b_flag}"
518
+ )
519
+ return _gh(cmd, timeout=30)
520
+
521
+ if action in ("list_commits", "commits", "log"):
522
+ limit = int(params.get("limit", 10))
523
+ return _gh(
524
+ f"gh api repos/{{owner}}/{{repo}}/commits?per_page={limit} "
525
+ "--jq '[.[] | {sha: .sha[:7], "
526
+ 'message: .commit.message | split("\\n")[0], '
527
+ "author: .commit.author.name, date: .commit.author.date}]'"
528
+ )
529
+
530
+ if action == "search":
531
+ q = params.get("q") or params.get("query", "")
532
+ kind = params.get("kind", "code")
533
+ if not q:
534
+ return {"success": False, "error": "Missing 'q' parameter"}
535
+ return _gh(f"gh search {kind} {shlex.quote(q)} --limit 10 "
536
+ "--json url,path,textMatches", timeout=15)
537
+
538
+ if action in ("read_file", "file"):
539
+ ref = params.get("ref", "")
540
+ file_path = params.get("path", "")
541
+ if ref:
542
+ m = re.match(r"([^@:]+)@([^:]+):(.+)", ref)
543
+ if m:
544
+ repo, branch, fp = m.groups()
545
+ url = f"https://raw.githubusercontent.com/{repo}/{branch}/{fp}"
546
+ return tool_web_fetch({"url": url, "max_chars": 20000})
547
+ if file_path:
548
+ return _gh(f"gh api repos/{{owner}}/{{repo}}/contents/{file_path} "
549
+ "--jq '.content' | base64 -d")
550
+ return {"success": False, "error": "Provide 'ref' (owner/repo@branch:path) or 'path'"}
551
+
552
+ if action in ("pr_diff", "diff"):
553
+ number = params.get("number") or params.get("pr")
554
+ if not number:
555
+ return {"success": False, "error": "Missing 'number' parameter"}
556
+ return _gh(f"gh pr diff {number}", timeout=30)
557
+
558
+ if action in ("pr_checks", "checks", "ci"):
559
+ number = params.get("number") or params.get("pr")
560
+ return _gh(f"gh pr checks {number or ''}")
561
+
562
+ if action in ("git_status", "status"):
563
+ return tool_run_command(
564
+ {"command": "git status --short && echo '---' && git log --oneline -5",
565
+ "cwd": cwd, "policy": policy},
566
+ console=console, has_rich=has_rich,
567
+ )
568
+
569
+ if action in ("commit_and_push", "commit", "push"):
570
+ message = params.get("message", "")
571
+ branch = params.get("branch", "")
572
+ add_files = params.get("files", [])
573
+ repo = params.get("repo", "") # e.g. "artherahq/aria-code"
574
+ coauthor = params.get("coauthor", "")
575
+
576
+ if not message:
577
+ return {"success": False, "error": "Missing 'message' for commit_and_push"}
578
+
579
+ # ── Aria Code[bot] identity via GitHub App ─────────────────────────────
580
+ try:
581
+ from apps.cli.github_app_auth import (
582
+ get_installation_token, get_aria_git_url,
583
+ aria_bot_env, ARIA_BOT_NAME, ARIA_BOT_EMAIL,
584
+ )
585
+ owner = (repo.split("/")[0] if repo else None) or "artherahq"
586
+ token = get_installation_token(owner)
587
+ bot_env = aria_bot_env()
588
+ auth_available = True
589
+ except Exception as _auth_err:
590
+ # Fall back to local git credentials if App not configured
591
+ token = None
592
+ bot_env = {
593
+ "GIT_AUTHOR_NAME": "Aria Code",
594
+ "GIT_AUTHOR_EMAIL": "aria-code[bot]@artherahq.com",
595
+ "GIT_COMMITTER_NAME": "Aria Code",
596
+ "GIT_COMMITTER_EMAIL": "aria-code[bot]@artherahq.com",
597
+ }
598
+ auth_available = False
599
+ ARIA_BOT_NAME = "Aria Code"
600
+ ARIA_BOT_EMAIL = "aria-code[bot]@artherahq.com"
601
+
602
+ env_prefix = " ".join(f'{k}="{v}"' for k, v in bot_env.items()) + " "
603
+
604
+ # ── Build commit message with co-author attribution ────────────────────
605
+ body = message
606
+ if coauthor:
607
+ body += f"\n\nCo-Authored-By: {coauthor}"
608
+ # Always credit the Aria GitHub account so it appears in Contributors
609
+ if auth_available:
610
+ try:
611
+ from apps.cli.github_app_auth import ARIA_GITHUB_LOGIN, ARIA_GITHUB_EMAIL
612
+ body += f"\n\nCo-Authored-By: {ARIA_GITHUB_LOGIN} <{ARIA_GITHUB_EMAIL}>"
613
+ except ImportError:
614
+ pass
615
+ user_name = tool_run_command({"command": "git config user.name", "policy": "safe", "cwd": cwd}).get("output", "").strip()
616
+ user_email = tool_run_command({"command": "git config user.email", "policy": "safe", "cwd": cwd}).get("output", "").strip()
617
+ if user_name and user_email and user_email != ARIA_BOT_EMAIL:
618
+ body += f"\n\nCo-Authored-By: {user_name} <{user_email}>"
619
+
620
+ # ── Stage ──────────────────────────────────────────────────────────────
621
+ stage_cmd = ("git add " + " ".join(shlex.quote(f) for f in add_files)) if add_files else "git add -A"
622
+ stage_result = tool_run_command(
623
+ {"command": stage_cmd, "cwd": cwd, "policy": policy},
624
+ console=console, has_rich=has_rich,
625
+ )
626
+ if not stage_result.get("success"):
627
+ return stage_result
628
+
629
+ # ── Commit ─────────────────────────────────────────────────────────────
630
+ safe_body = body.replace('"', '\\"').replace('$', '\\$')
631
+ commit_result = tool_run_command(
632
+ {"command": f'{env_prefix}git commit -m "{safe_body}"',
633
+ "cwd": cwd, "policy": policy},
634
+ console=console, has_rich=has_rich,
635
+ )
636
+ if not commit_result.get("success"):
637
+ return commit_result
638
+
639
+ # ── Push (use App token remote if available) ───────────────────────────
640
+ push_branch = branch or tool_run_command(
641
+ {"command": "git rev-parse --abbrev-ref HEAD", "policy": "safe", "cwd": cwd}
642
+ ).get("output", "").strip() or "main"
643
+
644
+ if auth_available and token and repo:
645
+ auth_url = get_aria_git_url(repo, token)
646
+ push_cmd = f"git push {shlex.quote(auth_url)} {shlex.quote(push_branch)}"
647
+ else:
648
+ push_cmd = f"git push origin {shlex.quote(push_branch)}"
649
+
650
+ push_result = tool_run_command(
651
+ {"command": push_cmd, "cwd": cwd, "policy": policy, "timeout": 60},
652
+ console=console, has_rich=has_rich,
653
+ )
654
+ return {
655
+ **push_result,
656
+ "committed_as": f"{ARIA_BOT_NAME} <{ARIA_BOT_EMAIL}>",
657
+ "branch": push_branch,
658
+ "app_auth": auth_available,
659
+ }
660
+
661
+ return {
662
+ "success": False,
663
+ "error": (
664
+ f"Unknown GitHub action: '{action}'. "
665
+ "Use: list_prs, list_issues, view_pr, view_issue, create_pr, "
666
+ "list_commits, search, read_file, pr_diff, pr_checks, "
667
+ "git_status, commit_and_push"
668
+ ),
669
+ }