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,114 @@
1
+ """Deterministic real-estate query handler extracted from aria_cli.py."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Callable
5
+
6
+
7
+ def handle_realty_query(
8
+ message: str,
9
+ *,
10
+ is_realty_query: Callable[[str], bool],
11
+ cn_cities: tuple | list,
12
+ intl_cities: tuple | list,
13
+ ) -> dict:
14
+ """Deterministic handler for natural-language real-estate / housing questions.
15
+
16
+ Detects city names + real-estate keywords and calls the realty data tools
17
+ directly, returning a formatted response without needing the LLM to parse it.
18
+ """
19
+ if not is_realty_query(message):
20
+ return {"success": False, "error": "not_realty_query"}
21
+
22
+ try:
23
+ from realty_data_tools import get_house_price_index, get_re_investment
24
+ except ImportError:
25
+ return {"success": False, "error": "realty_data_tools_not_available"}
26
+
27
+ cities_found: list[str] = []
28
+ for city in cn_cities:
29
+ if city in message:
30
+ cities_found.append(city)
31
+ if len(cities_found) >= 2:
32
+ break
33
+
34
+ _low_msg = message.lower()
35
+ _intl_found = [c for c in intl_cities if c in _low_msg]
36
+
37
+ if _intl_found and not cities_found:
38
+ _intl_name = _intl_found[0]
39
+ return {
40
+ "success": True,
41
+ "response": (
42
+ f"## 🌍 {_intl_name.title()} 房地产市场\n\n"
43
+ f"国际城市房价数据目前依赖 LLM 知识库分析(无实时数据接入)。\n\n"
44
+ f"**当前支持实时数据的城市:** 中国大陆 70 个主要城市(北上广深等)\n\n"
45
+ f"如需国际市场数据,建议查阅:\n"
46
+ f"- 美国:`/realty us` — 联邦住房数据(Case-Shiller 指数、新屋开工)\n"
47
+ f"- 其他国际城市:可直接提问,Aria 将基于训练知识回答(数据截止至知识库更新时间)\n\n"
48
+ f"---\n\n"
49
+ f"请问您具体想了解 **{_intl_name.title()}** 哪方面的房地产信息?"
50
+ ),
51
+ "tools_used": ["realty_query"],
52
+ }
53
+
54
+ city1 = cities_found[0] if cities_found else "全国"
55
+ city2 = cities_found[1] if len(cities_found) > 1 else ("上海" if city1 != "上海" else "北京")
56
+
57
+ lines: list[str] = []
58
+ lines.append(f"## 🏠 {city1} 房地产市场")
59
+ if len(cities_found) > 1:
60
+ lines[-1] += f" vs {city2}"
61
+ lines.append("")
62
+
63
+ try:
64
+ r = get_house_price_index(city1, city2)
65
+ if r.get("success"):
66
+ for lbl, cd in (
67
+ (city1, r.get("latest_city1", {})),
68
+ (city2, r.get("latest_city2", {})),
69
+ ):
70
+ if not cd:
71
+ continue
72
+ lines.append(f"### {lbl}")
73
+ if cd.get("new_yoy") is not None:
74
+ lines.append(f"- **新房同比**:{float(cd['new_yoy']):+.2f}%")
75
+ if cd.get("new_mom") is not None:
76
+ lines.append(f"- **新房环比**:{float(cd['new_mom']):+.2f}%")
77
+ if cd.get("second_yoy") is not None:
78
+ lines.append(f"- **二手房同比**:{float(cd['second_yoy']):+.2f}%")
79
+ if cd.get("second_mom") is not None:
80
+ lines.append(f"- **二手房环比**:{float(cd['second_mom']):+.2f}%")
81
+ if cd.get("date"):
82
+ lines.append(f"- **数据期**:{cd['date']}")
83
+ lines.append("")
84
+ else:
85
+ lines.append(f"房价指数数据暂时不可用({r.get('error', '数据源未响应')})")
86
+ lines.append("")
87
+ except Exception as _e:
88
+ lines.append(f"获取房价数据失败: {_e}")
89
+ lines.append("")
90
+
91
+ try:
92
+ ri = get_re_investment()
93
+ if ri.get("success") and ri.get("latest"):
94
+ lt = ri["latest"]
95
+ lines.append("### 全国房地产开发投资")
96
+ lines.append(f"- **最新值**:{lt.get('最新值', 'N/A')}")
97
+ lines.append(f"- **日期**:{lt.get('日期', 'N/A')}")
98
+ lines.append(f"- **涨跌幅**:{lt.get('涨跌幅', 'N/A')}")
99
+ lines.append(f"- **近1年涨跌幅**:{lt.get('近1年涨跌幅', 'N/A')}")
100
+ lines.append("")
101
+ except Exception:
102
+ pass
103
+
104
+ lines.append("**更多操作**")
105
+ lines.append(f"- `/realty market {city1}` — 完整城市房价走势图")
106
+ lines.append(f"- `/realty compare {city1} {city2}` — 城市横向对比")
107
+ lines.append(f"- `/realty rent` — 租金收益率计算")
108
+ lines.append(f"- `/realty reit` — REIT 市场数据")
109
+
110
+ return {
111
+ "success": True,
112
+ "response": "\n".join(lines),
113
+ "tools_used": ["realty_query"],
114
+ }
@@ -0,0 +1,82 @@
1
+ """Deterministic strategy-advice responses.
2
+
3
+ This keeps advisory questions from being treated as code-generation tasks.
4
+ When the user asks "how should I write a strategy" or "from which angles",
5
+ the CLI should answer with a framework and explicit next commands, not create
6
+ files or run backtests without a generation/execution intent.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+
12
+ _STRATEGY_TERMS = (
13
+ "量化策略", "交易策略", "美股策略", "回测策略", "策略框架",
14
+ "quant strategy", "trading strategy", "backtest strategy",
15
+ )
16
+
17
+ _ADVICE_TERMS = (
18
+ "几个角度", "哪些角度", "从几个方面", "怎么写", "如何写",
19
+ "思路", "框架", "结构", "要考虑", "你觉得", "建议",
20
+ "how", "what should", "framework", "approach", "angles",
21
+ )
22
+
23
+ _EXECUTION_TERMS = (
24
+ "开始写", "帮我写", "生成", "实现", "创建", "保存", "运行",
25
+ "回测一下", "直接做", "落地", "完善代码",
26
+ "write", "generate", "implement", "create", "save", "run",
27
+ )
28
+
29
+
30
+ def is_strategy_advice_request(message: str) -> bool:
31
+ text = (message or "").strip()
32
+ if not text or text.startswith("/"):
33
+ return False
34
+ low = text.lower()
35
+ has_strategy = any(term in text or term in low for term in _STRATEGY_TERMS)
36
+ has_advice = any(term in text or term in low for term in _ADVICE_TERMS)
37
+ has_execution = any(term in text or term in low for term in _EXECUTION_TERMS)
38
+ return bool(has_strategy and has_advice and not has_execution)
39
+
40
+
41
+ def handle_strategy_advice(message: str) -> dict:
42
+ if not is_strategy_advice_request(message):
43
+ return {"success": False, "error": "not_strategy_advice"}
44
+
45
+ response = """## 美股量化策略写作框架
46
+
47
+ 建议从 8 个层级写,先定义假设,再验证和风控,最后才进入交易执行。
48
+ 当前是咨询型问题,不需要先写文件,也不会自动运行回测。
49
+
50
+ | 层级 | 要解决的问题 | 产物 |
51
+ |---|---|---|
52
+ | 1. 市场假设 | 为什么这个信号可能赚钱 | 策略假设、适用市场、失效条件 |
53
+ | 2. 标的池 | 交易哪些股票/ETF,如何剔除不可交易标的 | universe 规则、流动性/价格过滤 |
54
+ | 3. 数据层 | OHLCV、财报、因子、宏观、新闻是否可得且可信 | 数据源、清洗、复权、缺失处理 |
55
+ | 4. 信号层 | 入场/出场依据是什么 | 因子、阈值、信号延迟、防未来函数 |
56
+ | 5. 组合层 | 单票仓位、行业暴露、相关性怎么控制 | 权重、再平衡、最大持仓、现金规则 |
57
+ | 6. 交易成本 | 手续费、滑点、冲击成本是否吞噬收益 | 成本模型、成交假设 |
58
+ | 7. 回测评估 | 策略是否稳健,而不是只在样本内好看 | CAGR、Sharpe、最大回撤、胜率、换手、分阶段表现 |
59
+ | 8. 风控部署 | 实盘如何限制损失并监控失效 | 止损、熔断、告警、paper/live 分层 |
60
+
61
+ **推荐落地顺序**
62
+
63
+ 1. 先写一页策略说明:假设、标的池、调仓频率、风险边界。
64
+ 2. 再写最小回测脚本:只包含数据、信号、仓位、绩效。
65
+ 3. 加 walk-forward、不同市场阶段、参数敏感性测试。
66
+ 4. 通过后接 paper trading,不直接上实盘。
67
+ 5. 实盘前加入订单预览、资金上限、熔断、审计日志。
68
+
69
+ **下一步命令**
70
+
71
+ - `/scaffold my_us_strategy --template strategy`:生成策略项目骨架。
72
+ - `/backtest momentum SPY --period 1y`:先跑一个内置动量回测。
73
+ - `/auto-strategy momentum SPY --target sharpe=1.2`:进入自动优化流程。
74
+
75
+ *这只是策略设计框架,不构成投资建议。需要我生成代码时,请明确说“生成/实现/写文件”。*"""
76
+
77
+ return {
78
+ "success": True,
79
+ "response": response,
80
+ "tools_used": ["strategy_advice"],
81
+ "analysis_complete": True,
82
+ }
apps/cli/hooks.py ADDED
@@ -0,0 +1,180 @@
1
+ """Aria Code lifecycle hooks — JSON-configurable, shell-executable.
2
+
3
+ Config file: ~/.arthera/hooks.json
4
+ Project-local override: .aria/hooks.json (takes precedence)
5
+
6
+ Schema
7
+ ------
8
+ {
9
+ "PreToolUse": [{"tool": "run_command", "command": "echo $ARIA_TOOL"}],
10
+ "PostToolUse": [{"command": "notify-send 'done'"}],
11
+ "ResponseDone": [{"command": "afplay /System/Sounds/Glass.aiff"}],
12
+ "SessionStart": [{"command": "echo starting $ARIA_SESSION"}],
13
+ "SessionEnd": [{"command": "echo ended"}]
14
+ }
15
+
16
+ Each hook entry:
17
+ command — shell command to execute (required)
18
+ tool — if set, only fires when tool name matches (PreToolUse / PostToolUse)
19
+ timeout — seconds before the hook is killed (default 10)
20
+ blocking — if true AND exit code != 0, execution is blocked (PreToolUse only)
21
+
22
+ Env vars injected into every hook:
23
+ ARIA_EVENT — hook event name
24
+ ARIA_TOOL — tool name (Pre/PostToolUse only)
25
+ ARIA_TOOL_PARAMS — JSON-encoded params (Pre/PostToolUse only)
26
+ ARIA_RESULT — JSON-encoded result (PostToolUse only)
27
+ ARIA_RESPONSE — first 500 chars of response (ResponseDone)
28
+ ARIA_SESSION — session ID
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import logging
34
+ import os
35
+ import subprocess
36
+ from pathlib import Path
37
+ from typing import Optional
38
+
39
+ from apps.cli.config_paths import resolve_config_dir
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ _CONFIG_DIR = resolve_config_dir()
44
+ _GLOBAL_HOOKS_FILE = _CONFIG_DIR / "hooks.json"
45
+ _LOCAL_HOOKS_FILE = Path.cwd() / ".aria" / "hooks.json"
46
+
47
+ _VALID_EVENTS = frozenset([
48
+ "PreToolUse", "PostToolUse",
49
+ "ResponseDone",
50
+ "SessionStart", "SessionEnd",
51
+ ])
52
+
53
+
54
+ def load_hooks() -> dict:
55
+ """Load and merge global + project-local hooks config."""
56
+ merged: dict[str, list] = {e: [] for e in _VALID_EVENTS}
57
+
58
+ for path in (_GLOBAL_HOOKS_FILE, _LOCAL_HOOKS_FILE):
59
+ if path.exists():
60
+ try:
61
+ data = json.loads(path.read_text(encoding="utf-8"))
62
+ for event, entries in data.items():
63
+ if event in _VALID_EVENTS and isinstance(entries, list):
64
+ merged[event].extend(entries)
65
+ except Exception as exc:
66
+ logger.debug("hooks.json parse error (%s): %s", path, exc)
67
+
68
+ return merged
69
+
70
+
71
+ def fire(
72
+ event: str,
73
+ *,
74
+ tool: Optional[str] = None,
75
+ params: Optional[dict] = None,
76
+ result: Optional[dict] = None,
77
+ response: str = "",
78
+ session_id: str = "",
79
+ hooks: Optional[dict] = None,
80
+ ) -> bool:
81
+ """Fire all matching hooks for an event.
82
+
83
+ Returns False if any *blocking* PreToolUse hook exits non-zero
84
+ (meaning the tool call should be suppressed). Returns True otherwise.
85
+ """
86
+ if hooks is None:
87
+ hooks = load_hooks()
88
+
89
+ entries = hooks.get(event, [])
90
+ if not entries:
91
+ return True
92
+
93
+ base_env = dict(os.environ)
94
+ base_env["ARIA_EVENT"] = event
95
+ base_env["ARIA_SESSION"] = session_id or ""
96
+ if tool:
97
+ base_env["ARIA_TOOL"] = tool
98
+ if params is not None:
99
+ base_env["ARIA_TOOL_PARAMS"] = json.dumps(params, ensure_ascii=False)[:2000]
100
+ if result is not None:
101
+ base_env["ARIA_RESULT"] = json.dumps(result, ensure_ascii=False)[:2000]
102
+ if response:
103
+ base_env["ARIA_RESPONSE"] = response[:500]
104
+
105
+ blocked = False
106
+ for entry in entries:
107
+ if not isinstance(entry, dict):
108
+ continue
109
+ cmd = entry.get("command", "").strip()
110
+ if not cmd:
111
+ continue
112
+
113
+ # Tool filter: skip if hook is scoped to a different tool
114
+ if entry.get("tool") and tool and entry["tool"] != tool:
115
+ continue
116
+
117
+ timeout = int(entry.get("timeout", 10))
118
+ is_blocking = bool(entry.get("blocking", False))
119
+
120
+ try:
121
+ proc = subprocess.run(
122
+ cmd,
123
+ shell=True,
124
+ env=base_env,
125
+ timeout=timeout,
126
+ capture_output=True,
127
+ text=True,
128
+ )
129
+ if is_blocking and proc.returncode != 0 and event == "PreToolUse":
130
+ logger.info(
131
+ "Blocking hook vetoed tool '%s': exit %d — %s",
132
+ tool, proc.returncode, proc.stderr[:120],
133
+ )
134
+ blocked = True
135
+ except subprocess.TimeoutExpired:
136
+ logger.debug("Hook timed out after %ds: %s", timeout, cmd[:60])
137
+ except Exception as exc:
138
+ logger.debug("Hook error: %s", exc)
139
+
140
+ return not blocked
141
+
142
+
143
+ def list_hooks() -> list[dict]:
144
+ """Return a flat list of configured hooks for display."""
145
+ hooks = load_hooks()
146
+ rows = []
147
+ for event, entries in hooks.items():
148
+ for entry in entries:
149
+ if isinstance(entry, dict) and entry.get("command"):
150
+ rows.append({
151
+ "event": event,
152
+ "tool": entry.get("tool", "*"),
153
+ "command": entry["command"][:80],
154
+ "blocking": entry.get("blocking", False),
155
+ "timeout": entry.get("timeout", 10),
156
+ })
157
+ return rows
158
+
159
+
160
+ def hooks_file_path(scope: str = "global") -> Path:
161
+ """Return the hooks.json path for a given scope."""
162
+ if scope == "local":
163
+ return Path.cwd() / ".aria" / "hooks.json"
164
+ return _GLOBAL_HOOKS_FILE
165
+
166
+
167
+ def create_example_hooks(path: Path) -> None:
168
+ """Write an annotated example hooks.json if it doesn't exist."""
169
+ if path.exists():
170
+ return
171
+ path.parent.mkdir(parents=True, exist_ok=True)
172
+ example = {
173
+ "_comment": "Aria Code hooks — https://aria.code/docs/hooks",
174
+ "PreToolUse": [],
175
+ "PostToolUse": [],
176
+ "ResponseDone": [],
177
+ "SessionStart": [],
178
+ "SessionEnd": [],
179
+ }
180
+ path.write_text(json.dumps(example, indent=2, ensure_ascii=False), encoding="utf-8")
apps/cli/i18n.py ADDED
@@ -0,0 +1,284 @@
1
+ """i18n.py — System language detection and UI string translations for Aria Code.
2
+
3
+ Priority order for UI language:
4
+ 1. config["ui_lang"] (user explicitly set via /config set ui_lang=zh)
5
+ 2. OS locale at first-run (written into config automatically)
6
+ 3. Fallback: "en"
7
+
8
+ Supported languages: "zh" (Simplified Chinese), "en" (English)
9
+ Future: "ja", "ko" — extend STRINGS dict below.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from typing import Optional
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # String catalogue
19
+ # ---------------------------------------------------------------------------
20
+
21
+ STRINGS: dict[str, dict[str, str]] = {
22
+ # Banner labels
23
+ "local_first_agent": {
24
+ "zh": "本地优先智能体",
25
+ "en": "local-first agent",
26
+ },
27
+ "model": {
28
+ "zh": "模型",
29
+ "en": "model",
30
+ },
31
+ "workspace": {
32
+ "zh": "工作区",
33
+ "en": "workspace",
34
+ },
35
+ "mode": {
36
+ "zh": "模式",
37
+ "en": "mode",
38
+ },
39
+ "status": {
40
+ "zh": "状态",
41
+ "en": "status",
42
+ },
43
+ "tools": {
44
+ "zh": "工具",
45
+ "en": "tools",
46
+ },
47
+ "skills": {
48
+ "zh": "技能",
49
+ "en": "skills",
50
+ },
51
+ "quant": {
52
+ "zh": "量化",
53
+ "en": "quant",
54
+ },
55
+ # Status labels
56
+ "sharing_on": {
57
+ "zh": "数据共享",
58
+ "en": "sharing on",
59
+ },
60
+ "local_only": {
61
+ "zh": "纯本地",
62
+ "en": "local-only",
63
+ },
64
+ "cloud": {
65
+ "zh": "云端",
66
+ "en": "cloud",
67
+ },
68
+ "local": {
69
+ "zh": "本地",
70
+ "en": "local",
71
+ },
72
+ "lite": {
73
+ "zh": "轻量",
74
+ "en": "lite",
75
+ },
76
+ "ollama_online": {
77
+ "zh": "Ollama 在线",
78
+ "en": "Ollama online",
79
+ },
80
+ "ollama_offline": {
81
+ "zh": "Ollama 离线",
82
+ "en": "Ollama offline",
83
+ },
84
+ "cloud_connected": {
85
+ "zh": "云端已连接",
86
+ "en": "cloud ✓",
87
+ },
88
+ "model_singular": {
89
+ "zh": "个模型",
90
+ "en": "model",
91
+ },
92
+ "model_plural": {
93
+ "zh": "个模型",
94
+ "en": "models",
95
+ },
96
+ # Hint line
97
+ "try": {
98
+ "zh": "试试",
99
+ "en": "try",
100
+ },
101
+ # Model picker
102
+ "select_model": {
103
+ "zh": "选择模型",
104
+ "en": "Select Model",
105
+ },
106
+ "installed": {
107
+ "zh": "已安装",
108
+ "en": "installed",
109
+ },
110
+ "not_installed": {
111
+ "zh": "未安装",
112
+ "en": "not installed",
113
+ },
114
+ "cancelled": {
115
+ "zh": "已取消",
116
+ "en": "Cancelled",
117
+ },
118
+ "no_change": {
119
+ "zh": "未更改",
120
+ "en": "No change",
121
+ },
122
+ "community_models": {
123
+ "zh": "社区模型 (Ollama)",
124
+ "en": "Community (Ollama)",
125
+ },
126
+ "ollama_unreachable": {
127
+ "zh": "Ollama 无法连接",
128
+ "en": "Ollama unreachable",
129
+ },
130
+ "switch_model_hint": {
131
+ "zh": "使用 /model <id> 切换模型",
132
+ "en": "Use /model <id> to switch",
133
+ },
134
+ "current": {
135
+ "zh": "当前",
136
+ "en": "current",
137
+ },
138
+ # Permission / mode labels
139
+ "network_on": {
140
+ "zh": "网络开",
141
+ "en": "network on",
142
+ },
143
+ "network_off": {
144
+ "zh": "网络关",
145
+ "en": "network off",
146
+ },
147
+ "privacy": {
148
+ "zh": "隐私",
149
+ "en": "privacy",
150
+ },
151
+ # Setup wizard
152
+ "detecting_lang": {
153
+ "zh": "检测到系统语言:中文",
154
+ "en": "Detected system language: English",
155
+ },
156
+ "first_run_welcome": {
157
+ "zh": "欢迎使用 Aria Code!首次运行,正在自动配置...",
158
+ "en": "Welcome to Aria Code! First run — auto-configuring...",
159
+ },
160
+ "auto_model_selected": {
161
+ "zh": "已自动选择本地模型:",
162
+ "en": "Auto-selected local model: ",
163
+ },
164
+ "no_ollama_models": {
165
+ "zh": "未发现本地模型,使用默认配置",
166
+ "en": "No local models found, using default config",
167
+ },
168
+ # Thinking mode
169
+ "thinking_mode": {
170
+ "zh": "思考模式",
171
+ "en": "Thinking Mode",
172
+ },
173
+ # Misc
174
+ "tip": {
175
+ "zh": "提示",
176
+ "en": "tip",
177
+ },
178
+ "auto_matched": {
179
+ "zh": "自动匹配",
180
+ "en": "auto-matched",
181
+ },
182
+ }
183
+
184
+
185
+ # ---------------------------------------------------------------------------
186
+ # Detection
187
+ # ---------------------------------------------------------------------------
188
+
189
+ def detect_system_lang() -> str:
190
+ """Detect OS language. Returns 'zh' for Chinese systems, 'en' for everything else.
191
+
192
+ Checks (in order): $LANGUAGE, $LANG, $LC_ALL, $LC_MESSAGES, Python locale module.
193
+ Treats zh_CN, zh_TW, zh_HK, zh_SG as 'zh'.
194
+ """
195
+ for var in ("LANGUAGE", "LANG", "LC_ALL", "LC_MESSAGES"):
196
+ raw = os.environ.get(var, "").strip()
197
+ if not raw or raw in ("C", "POSIX", "C.UTF-8"):
198
+ continue
199
+ # LANGUAGE may be colon-separated list: zh_CN:en
200
+ code = raw.split(":")[0].split(".")[0].split("_")[0].lower()
201
+ if code == "zh":
202
+ return "zh"
203
+ if code in ("en", "fr", "de", "es", "pt", "ja", "ko", "ru", "ar"):
204
+ return code if code in ("ja", "ko") else "en"
205
+
206
+ # Fallback: Python locale module (works on macOS/Windows when env vars absent)
207
+ try:
208
+ import locale as _locale
209
+ lang_code, _ = _locale.getdefaultlocale()
210
+ if lang_code:
211
+ prefix = lang_code.split("_")[0].lower()
212
+ if prefix == "zh":
213
+ return "zh"
214
+ if prefix in ("ja", "ko"):
215
+ return prefix
216
+ except Exception:
217
+ pass
218
+
219
+ return "en"
220
+
221
+
222
+ def get_ui_lang(config: Optional[dict] = None) -> str:
223
+ """Return effective UI language.
224
+
225
+ 1. config["ui_lang"] if explicitly set
226
+ 2. detect_system_lang()
227
+ """
228
+ if config:
229
+ saved = config.get("ui_lang", "")
230
+ if saved in STRINGS.get("model", {}): # valid lang code
231
+ return saved
232
+ return detect_system_lang()
233
+
234
+
235
+ def t(key: str, lang: Optional[str] = None, config: Optional[dict] = None) -> str:
236
+ """Translate a UI key to the given language (or auto-detect).
237
+
238
+ Falls back to 'en' if key/lang not found.
239
+ """
240
+ if lang is None:
241
+ lang = get_ui_lang(config)
242
+ entry = STRINGS.get(key, {})
243
+ return entry.get(lang) or entry.get("en") or key
244
+
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # Ollama model helpers (used by load_config on first run)
248
+ # ---------------------------------------------------------------------------
249
+
250
+ _MODEL_PRIORITY = [
251
+ "qwen2.5:7b",
252
+ "qwen2.5-coder:7b",
253
+ "deepseek-r1:7b",
254
+ "qwen2.5:3b",
255
+ "qwen2.5-coder:3b",
256
+ "llama3.2:3b",
257
+ "mistral:7b",
258
+ "qwen2.5-coder:1.5b",
259
+ "qwen2.5:1.5b",
260
+ "llama3.2:1b",
261
+ ]
262
+
263
+
264
+ def auto_select_model(ollama_url: str = "http://localhost:11434",
265
+ fallback: str = "qwen2.5-coder:1.5b") -> str:
266
+ """Query Ollama and return the best locally available model.
267
+
268
+ Returns *fallback* when Ollama is unreachable or no models are installed.
269
+ """
270
+ try:
271
+ import urllib.request as _ur
272
+ import json as _json
273
+ with _ur.urlopen(f"{ollama_url}/api/tags", timeout=3) as resp:
274
+ data = _json.loads(resp.read())
275
+ installed = {m["name"] for m in data.get("models", [])}
276
+ if not installed:
277
+ return fallback
278
+ for pref in _MODEL_PRIORITY:
279
+ if pref in installed:
280
+ return pref
281
+ # Return alphabetically first installed model
282
+ return sorted(installed)[0]
283
+ except Exception:
284
+ return fallback