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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- 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" ", " ", text)
|
|
437
|
+
text = re.sub(r"&", "&", text)
|
|
438
|
+
text = re.sub(r"<", "<", text)
|
|
439
|
+
text = re.sub(r">", ">", text)
|
|
440
|
+
text = re.sub(r""", '"', 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
|
+
}
|