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,358 @@
1
+ """UiCommandsMixin — vision, browser, screenshot, input, and context commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import io
7
+ import pathlib
8
+ from urllib.parse import urlsplit
9
+
10
+
11
+ class UiCommandsMixin:
12
+ """Mixin: visual input and terminal UI commands."""
13
+
14
+ @staticmethod
15
+ def _short_url_label(url: str) -> str:
16
+ try:
17
+ from urllib.parse import urlsplit
18
+ parsed = urlsplit(url if url.startswith(("http://", "https://")) else f"https://{url}")
19
+ host = parsed.netloc or parsed.path
20
+ path = parsed.path.rstrip("/")
21
+ if len(path) > 32:
22
+ path = path[:29] + "..."
23
+ return f"{host}{path}" if path and path != "/" else host
24
+ except Exception:
25
+ return url[:48]
26
+
27
+ @staticmethod
28
+ def _load_image_source(path_or_url: str) -> dict:
29
+ """Load an image from a local path, URL, or clipboard."""
30
+ raw = (path_or_url or "").strip().strip("\"'")
31
+ if not raw:
32
+ raise ValueError("Missing image source")
33
+
34
+ mime_map = {
35
+ "png": "image/png",
36
+ "jpg": "image/jpeg",
37
+ "jpeg": "image/jpeg",
38
+ "gif": "image/gif",
39
+ "webp": "image/webp",
40
+ }
41
+
42
+ def _from_bytes(data: bytes, mime: str, label: str) -> dict:
43
+ if not data:
44
+ raise ValueError("Empty image data")
45
+ return {
46
+ "label": label,
47
+ "mime": mime,
48
+ "b64": base64.b64encode(data).decode(),
49
+ "size_kb": max(1, len(data) // 1024),
50
+ }
51
+
52
+ if raw.lower() in {"clipboard", "clip", "paste"}:
53
+ try:
54
+ from PIL import ImageGrab
55
+ img = ImageGrab.grabclipboard()
56
+ if img is None:
57
+ raise ValueError("Clipboard does not contain an image")
58
+ if isinstance(img, list):
59
+ for item in img:
60
+ p = pathlib.Path(str(item))
61
+ if p.is_file() and p.suffix.lstrip(".").lower() in mime_map:
62
+ return UiCommandsMixin._load_image_source(str(p))
63
+ raise ValueError("Clipboard does not contain a supported image file")
64
+ if hasattr(img, "save"):
65
+ buf = io.BytesIO()
66
+ img.save(buf, format="PNG")
67
+ return _from_bytes(buf.getvalue(), "image/png", "clipboard")
68
+ raise ValueError("Clipboard image format not supported")
69
+ except Exception as exc:
70
+ raise ValueError(str(exc)) from exc
71
+
72
+ if raw.startswith(("http://", "https://", "www.")):
73
+ try:
74
+ import requests
75
+ url = raw if raw.startswith(("http://", "https://")) else f"https://{raw}"
76
+ resp = requests.get(url, timeout=20, headers={"User-Agent": "Mozilla/5.0"})
77
+ resp.raise_for_status()
78
+ content_type = (resp.headers.get("content-type") or "").split(";", 1)[0].strip().lower()
79
+ mime = content_type if content_type.startswith("image/") else "image/png"
80
+ if mime == "application/octet-stream":
81
+ mime = "image/png"
82
+ return _from_bytes(resp.content, mime, UiCommandsMixin._short_url_label(url))
83
+ except Exception as exc:
84
+ raise ValueError(f"Cannot download image: {exc}") from exc
85
+
86
+ path = pathlib.Path(raw).expanduser().resolve()
87
+ if not path.exists():
88
+ raise FileNotFoundError(f"File not found: {path}")
89
+ suffix = path.suffix.lstrip(".").lower()
90
+ mime = mime_map.get(suffix)
91
+ if not mime:
92
+ raise ValueError(f"Unsupported image type: .{suffix}")
93
+ return _from_bytes(path.read_bytes(), mime, path.name)
94
+
95
+ def cmd_vision(self, args: str):
96
+ _curr_model = self.terminal.config.get("model", "")
97
+ if _curr_model and _HAS_MODEL_CAP:
98
+ _vcap = get_model_capability(_curr_model)
99
+ if not _vcap.vision:
100
+ _warn = (
101
+ f"[yellow]⚠[/yellow] 当前模型 [bold]{_curr_model}[/bold] 不支持图片输入。\n"
102
+ f"[dim]支持视觉的模型:llama3.2:11b · gemma3 · llava · qwen2-vl · moondream[/dim]"
103
+ )
104
+ if HAS_RICH:
105
+ console.print(Panel(_warn, border_style="yellow", box=rich_box.ROUNDED, padding=(0, 1)))
106
+ else:
107
+ print(f"Warning: model {_curr_model} does not support vision input.")
108
+ return
109
+
110
+ path_str = args.strip().strip("\"'")
111
+ if not path_str:
112
+ msg = "Usage: /vision <image_path|image_url|clipboard> (e.g. /vision ~/Pictures/chart.png)"
113
+ console.print(f"[dim]{msg}[/dim]" if HAS_RICH else msg)
114
+ return
115
+
116
+ try:
117
+ payload = self._load_image_source(path_str)
118
+ except Exception as e:
119
+ _print_error(str(e), "vision")
120
+ return
121
+
122
+ self.terminal._pending_image = {
123
+ "type": "image_url",
124
+ "image_url": {"url": f"data:{payload['mime']};base64,{payload['b64']}"},
125
+ }
126
+ size_kb = payload["size_kb"]
127
+ if HAS_RICH:
128
+ console.print(Panel(
129
+ f"[green]✓[/green] [dim]{payload['label']}[/dim] [dim]{size_kb} KB · {payload['mime']}[/dim]\n"
130
+ f"[dim]Image queued — ask your question now[/dim]",
131
+ border_style="dim",
132
+ box=rich_box.ROUNDED,
133
+ padding=(0, 1),
134
+ ))
135
+ else:
136
+ print(f"Image loaded: {payload['label']} ({size_kb} KB) — send your question now")
137
+
138
+ async def cmd_browser(self, args: str):
139
+ """Open a URL in a headless browser."""
140
+ if not _HAS_COMPUTER_USE:
141
+ _print_error(
142
+ "computer_use_tools not available.",
143
+ "Install: pip install playwright mss pyautogui pillow && playwright install chromium",
144
+ )
145
+ return
146
+ from computer_use_tools import _tool_browser_navigate, _tool_browser_screenshot
147
+
148
+ parts = args.strip().split(maxsplit=1)
149
+ if not parts:
150
+ if HAS_RICH:
151
+ console.print("[dim]Usage: /browser <url> or /browser screenshot <url>[/dim]")
152
+ return
153
+
154
+ if parts[0].lower() == "screenshot" and len(parts) > 1:
155
+ url = parts[1].strip()
156
+ if HAS_RICH:
157
+ with console.status(f"[dim]Screenshotting {self._short_url_label(url)}…[/dim]", spinner="dots"):
158
+ result = _tool_browser_screenshot({"url": url})
159
+ else:
160
+ result = _tool_browser_screenshot({"url": url})
161
+ if result.get("success"):
162
+ d = result["data"]
163
+ from computer_use_tools import pop_pending_vision_image
164
+ b64 = pop_pending_vision_image()
165
+ if b64:
166
+ self.terminal._pending_image = {
167
+ "type": "image_url",
168
+ "image_url": {"url": f"data:image/png;base64,{b64}"},
169
+ }
170
+ if HAS_RICH:
171
+ console.print(Panel(
172
+ f"[green]✓[/green] [bold]{d.get('title','')[:60]}[/bold]\n"
173
+ f"[dim]{self._short_url_label(url)} · {d.get('size_kb', 0)} KB[/dim]\n"
174
+ f"[dim]Screenshot queued — ask your question now[/dim]",
175
+ border_style="dim", box=rich_box.ROUNDED, padding=(0, 1),
176
+ ))
177
+ else:
178
+ print(f"Screenshot ready ({d.get('size_kb', 0)} KB) — send your question")
179
+ else:
180
+ _print_error(result.get("error", "Screenshot failed"), "browser screenshot")
181
+ else:
182
+ url = parts[0].strip()
183
+ if HAS_RICH:
184
+ with console.status(f"[dim]Opening {self._short_url_label(url)}…[/dim]", spinner="dots"):
185
+ result = _tool_browser_navigate({"url": url})
186
+ else:
187
+ result = _tool_browser_navigate({"url": url})
188
+ if result.get("success"):
189
+ d = result["data"]
190
+ title = d.get("title", "")
191
+ text = d.get("text", "")[:2000]
192
+ links = d.get("links", [])[:5]
193
+ engine = d.get("engine", "")
194
+ if HAS_RICH:
195
+ link_str = "\n".join(f" {l}" for l in links) if links else " (none)"
196
+ console.print(Panel(
197
+ f"[bold]{title[:80]}[/bold] [dim]({engine})[/dim]\n\n"
198
+ f"{text}\n\n[dim]Links:[/dim]\n{link_str}",
199
+ border_style="dim", box=rich_box.ROUNDED, padding=(0, 1),
200
+ title=f"[dim]{self._short_url_label(url)}[/dim]", title_align="left",
201
+ ))
202
+ else:
203
+ print(f"Title: {title}\n{text[:500]}")
204
+ else:
205
+ _print_error(result.get("error", "Navigation failed"), "browser")
206
+
207
+ async def cmd_screenshot(self, args: str):
208
+ if not _HAS_COMPUTER_USE:
209
+ _print_error(
210
+ "computer_use_tools not available.",
211
+ "Install: pip install mss pillow",
212
+ )
213
+ return
214
+ from computer_use_tools import _tool_computer_screenshot, pop_pending_vision_image
215
+
216
+ monitor = int(args.strip()) if args.strip().isdigit() else 1
217
+ if HAS_RICH:
218
+ with console.status("[dim]Capturing screen…[/dim]", spinner="dots"):
219
+ result = _tool_computer_screenshot({"monitor": monitor})
220
+ else:
221
+ result = _tool_computer_screenshot({"monitor": monitor})
222
+
223
+ if result.get("success"):
224
+ d = result["data"]
225
+ b64 = pop_pending_vision_image()
226
+ if b64:
227
+ self.terminal._pending_image = {
228
+ "type": "image_url",
229
+ "image_url": {"url": f"data:image/png;base64,{b64}"},
230
+ }
231
+ if HAS_RICH:
232
+ console.print(Panel(
233
+ f"[green]✓[/green] [dim]{d['width']}×{d['height']} · {d['size_kb']} KB[/dim]\n"
234
+ f"[dim]Screenshot queued — ask your question now[/dim]",
235
+ border_style="dim", box=rich_box.ROUNDED, padding=(0, 1),
236
+ ))
237
+ else:
238
+ print(f"Screenshot {d['width']}×{d['height']} ({d['size_kb']} KB) — send your question")
239
+ else:
240
+ _print_error(result.get("error", "Screenshot failed"), "screenshot")
241
+
242
+ def cmd_input(self, args: str):
243
+ raw = args.strip().lower()
244
+ cfg = self.terminal.config
245
+ valid_styles = {"panel", "box", "plain"}
246
+ valid_themes = {"auto", "dark", "light"}
247
+
248
+ def _save_and_show(message: str) -> None:
249
+ save_config(cfg)
250
+ if HAS_RICH:
251
+ console.print(f"[green]✓[/green] {message}")
252
+ console.print(
253
+ f" [dim]style[/dim] {cfg.get('input_style', 'panel')} "
254
+ f"[dim]theme[/dim] {cfg.get('input_theme', 'auto')}"
255
+ )
256
+ else:
257
+ print(message)
258
+ print(f" style {cfg.get('input_style', 'panel')} theme {cfg.get('input_theme', 'auto')}")
259
+
260
+ if not raw or raw in {"status", "show"}:
261
+ style = cfg.get("input_style", "panel")
262
+ theme = cfg.get("input_theme", "auto")
263
+ if HAS_RICH:
264
+ console.print(Panel(
265
+ f"[bold]style[/bold] {style}\n"
266
+ f"[bold]theme[/bold] {theme}\n\n"
267
+ "[dim]Use[/dim] /input panel [dim]for the Codex-style input block[/dim]\n"
268
+ "[dim]Use[/dim] /input theme auto [dim]to follow the terminal/system theme[/dim]",
269
+ title="Input UI",
270
+ border_style="dim",
271
+ box=rich_box.ROUNDED,
272
+ padding=(0, 1),
273
+ ))
274
+ else:
275
+ print(f"input style: {style}")
276
+ print(f"input theme: {theme}")
277
+ print("Usage: /input panel|box|plain | /input theme auto|dark|light")
278
+ return
279
+
280
+ if raw == "reset":
281
+ cfg["input_style"] = "panel"
282
+ cfg["input_theme"] = "auto"
283
+ _save_and_show("input UI reset to panel · auto")
284
+ return
285
+
286
+ parts = raw.split()
287
+ if parts[0] == "theme":
288
+ if len(parts) != 2 or parts[1] not in valid_themes:
289
+ msg = "Usage: /input theme auto|dark|light"
290
+ console.print(f"[red]{msg}[/red]" if HAS_RICH else msg)
291
+ return
292
+ cfg["input_theme"] = parts[1]
293
+ _save_and_show(f"input theme set to {parts[1]}")
294
+ return
295
+
296
+ if parts[0] in valid_themes and len(parts) == 1:
297
+ cfg["input_theme"] = parts[0]
298
+ _save_and_show(f"input theme set to {parts[0]}")
299
+ return
300
+
301
+ if parts[0] in valid_styles and len(parts) == 1:
302
+ cfg["input_style"] = parts[0]
303
+ _save_and_show(f"input style set to {parts[0]}")
304
+ return
305
+
306
+ msg = "Usage: /input panel|box|plain | /input theme auto|dark|light | /input reset"
307
+ console.print(f"[red]{msg}[/red]" if HAS_RICH else msg)
308
+
309
+ def cmd_context(self, args: str):
310
+ cfg = self.terminal.config
311
+ conv = self.terminal.conversation
312
+ conv_len = len(conv)
313
+ model_id = cfg.get("model", "qwen2.5:7b")
314
+ thinking = cfg.get("thinking_mode", "auto")
315
+ has_auth = bool(cfg.get("auth_token"))
316
+ local_mode = cfg.get("local_mode", False)
317
+ auto_compact = bool(cfg.get("auto_compact_context", True))
318
+ try:
319
+ auto_compact_threshold = float(cfg.get("auto_compact_threshold", 0.78))
320
+ except Exception:
321
+ auto_compact_threshold = 0.78
322
+ auto_compact_count = int(getattr(self.terminal, "_auto_compact_count", 0) or 0)
323
+
324
+ total_chars = sum(len(m.get("content", "")) for m in conv)
325
+ est_tokens = total_chars // 3
326
+ max_ctx = get_model_cfg(model_id).get("num_ctx", 16384)
327
+ ctx_pct = min(100, int(est_tokens / max_ctx * 100))
328
+ ctx_color = "green" if ctx_pct < 60 else ("yellow" if ctx_pct < 85 else "red")
329
+
330
+ if HAS_RICH:
331
+ console.print()
332
+ console.print("[bold]Current Context[/bold]")
333
+ console.print()
334
+ console.print(f" [dim]{'Model':<20s}[/dim]{model_id}")
335
+ console.print(f" [dim]{'Provider':<20s}[/dim]{'[green]Local (Ollama)[/green]' if local_mode else 'AWS → Ollama fallback'}")
336
+ console.print(f" [dim]{'Thinking':<20s}[/dim]{thinking}")
337
+ console.print(f" [dim]{'Messages':<20s}[/dim]{conv_len}")
338
+ console.print(f" [dim]{'Est. tokens':<20s}[/dim][{ctx_color}]{est_tokens:,} / {max_ctx:,} ({ctx_pct}%)[/{ctx_color}]")
339
+ console.print(f" [dim]{'Authenticated':<20s}[/dim]{'yes' if has_auth else 'no'}")
340
+ console.print(
341
+ f" [dim]{'Auto compact':<20s}[/dim]"
342
+ f"{'on' if auto_compact else 'off'}"
343
+ f" · threshold {int(auto_compact_threshold * 100)}%"
344
+ f" · runs {auto_compact_count}"
345
+ )
346
+ console.print(f" [dim]{'Session':<20s}[/dim]{self.terminal.session_id}")
347
+ console.print(f" [dim]{'Project context':<20s}[/dim]{'loaded' if _PROJECT_CONTEXT else 'none'}")
348
+ wl = cfg.get("watchlist", [])
349
+ if wl:
350
+ console.print(f" [dim]{'Watchlist':<20s}[/dim]{', '.join(wl)}")
351
+ if ctx_pct >= 80:
352
+ console.print(f"\n [yellow]⚠ Context {ctx_pct}% full — use /compact to free space[/yellow]")
353
+ console.print()
354
+ else:
355
+ print(f" Model: {model_id} ({'local' if local_mode else 'aws'})")
356
+ print(f" Messages: {conv_len} Tokens: ~{est_tokens:,}/{max_ctx:,} ({ctx_pct}%)")
357
+ print(f" Auto compact: {'on' if auto_compact else 'off'} threshold={int(auto_compact_threshold * 100)}% runs={auto_compact_count}")
358
+ print(f" Session: {self.terminal.session_id}")
@@ -0,0 +1,279 @@
1
+ """WorkflowCommandsMixin — hooks, regen, undo, retry, note, review commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import pathlib
7
+ from datetime import datetime
8
+
9
+
10
+ class WorkflowCommandsMixin:
11
+ """Mixin: interactive workflow and edit-review commands."""
12
+
13
+ def cmd_hooks(self, args: str):
14
+ global _JSON_HOOKS
15
+ hooks_dirs = [
16
+ CONFIG_DIR / "hooks",
17
+ pathlib.Path.cwd() / ".aria" / "hooks",
18
+ ]
19
+ parts = args.strip().split(maxsplit=1)
20
+ sub = parts[0].lower() if parts else "list"
21
+ rest = parts[1].strip() if len(parts) > 1 else ""
22
+
23
+ if sub == "reload":
24
+ if _HAS_JSON_HOOKS:
25
+ try:
26
+ _JSON_HOOKS = _load_hooks()
27
+ n = sum(len(v) for v in _JSON_HOOKS.values())
28
+ if HAS_RICH:
29
+ console.print(f" [green]✓[/green] [dim]hooks.json reloaded ({n} entries)[/dim]")
30
+ else:
31
+ print(f" hooks.json reloaded ({n} entries)")
32
+ except Exception as exc:
33
+ if HAS_RICH:
34
+ console.print(f" [red]✗ reload failed: {exc}[/red]")
35
+ else:
36
+ print(f" reload failed: {exc}")
37
+ return
38
+
39
+ if sub == "list":
40
+ if _HAS_JSON_HOOKS:
41
+ try:
42
+ from apps.cli.hooks import list_hooks as _list_json_hooks
43
+ _json_rows = _list_json_hooks()
44
+ if _json_rows:
45
+ if HAS_RICH:
46
+ console.print()
47
+ console.print(" [bold]JSON Hooks[/bold] [dim](~/.arthera/hooks.json)[/dim]")
48
+ for r in _json_rows:
49
+ _block = " [red][blocking][/red]" if r["blocking"] else ""
50
+ _tool = f"[{r['tool']}]" if r["tool"] != "*" else ""
51
+ console.print(
52
+ f" [cyan]{r['event']:<16}[/cyan]{_tool:<14} "
53
+ f"[dim]{r['command']}[/dim]{_block}"
54
+ )
55
+ else:
56
+ for r in _json_rows:
57
+ print(f" {r['event']:<16} {r['tool']:<12} {r['command']}")
58
+ except Exception:
59
+ pass
60
+
61
+ found: list[tuple] = []
62
+ for hdir in hooks_dirs:
63
+ if hdir.exists():
64
+ for f in sorted(hdir.iterdir()):
65
+ if f.is_file() and not f.name.startswith("."):
66
+ found.append((str(hdir), f.name, str(f)))
67
+ if not found:
68
+ if HAS_RICH:
69
+ console.print(f" [dim]No hooks found.[/dim]")
70
+ console.print(f" [dim]Hook dirs:[/dim]")
71
+ for d in hooks_dirs:
72
+ console.print(f" [dim]{_display_path(d, fallback='hook dir')}[/dim]")
73
+ console.print(f" [dim]Events: prompt_submit response_done tool_use compact[/dim]")
74
+ else:
75
+ print("No hooks. Dirs:", [str(d) for d in hooks_dirs])
76
+ return
77
+ if HAS_RICH:
78
+ console.print()
79
+ for hdir, name, path in found:
80
+ console.print(f" [dim]{name:<28}[/dim] {_display_path(path, fallback='hook')}")
81
+ console.print()
82
+ else:
83
+ for hdir, name, path in found:
84
+ print(f" {name} {_display_path(path, fallback='hook')}")
85
+
86
+ elif sub == "edit":
87
+ if not rest:
88
+ if _HAS_JSON_HOOKS:
89
+ from apps.cli.hooks import hooks_file_path, create_example_hooks
90
+ _hpath = hooks_file_path("global")
91
+ create_example_hooks(_hpath)
92
+ editor = os.getenv("EDITOR", "nano")
93
+ try:
94
+ import subprocess as _sp
95
+ _sp.run([editor, str(_hpath)])
96
+ _JSON_HOOKS = _load_hooks()
97
+ except Exception as exc:
98
+ if HAS_RICH:
99
+ console.print(f"[red]Could not open editor: {exc}[/red]")
100
+ else:
101
+ print(f"Could not open editor: {exc}")
102
+ return
103
+ event = rest
104
+ hdir = CONFIG_DIR / "hooks"
105
+ hdir.mkdir(parents=True, exist_ok=True)
106
+ script = hdir / f"{event}.sh"
107
+ if not script.exists():
108
+ script.write_text(
109
+ f"#!/bin/bash\n# Aria hook: {event}\n# "
110
+ f"Env vars: ARIA_EVENT ARIA_TOOL ARIA_TOOL_PARAMS ARIA_RESPONSE ARIA_SESSION\n\n"
111
+ f'echo "Hook {event} fired"\n',
112
+ encoding="utf-8"
113
+ )
114
+ script.chmod(0o755)
115
+ editor = os.getenv("EDITOR", "nano")
116
+ try:
117
+ import subprocess as _sp
118
+ _sp.run([editor, str(script)])
119
+ except Exception as exc:
120
+ console.print(f"[red]Could not open editor: {exc}[/red]" if HAS_RICH else str(exc))
121
+
122
+ elif sub == "run":
123
+ event = rest or "ResponseDone"
124
+ if _HAS_JSON_HOOKS:
125
+ _fire_json_hook(event, session_id=getattr(self.terminal, "session_id", ""), hooks=_JSON_HOOKS)
126
+ _run_event_hook(event, {"ARIA_EVENT": event, "ARIA_SESSION": getattr(self.terminal, "session_id", "")})
127
+ if HAS_RICH:
128
+ console.print(f" [dim]Hook '{event}' triggered[/dim]")
129
+ else:
130
+ print(f"Hook '{event}' triggered")
131
+
132
+ else:
133
+ if HAS_RICH:
134
+ console.print("[dim]Usage: /hooks list|edit [event]|reload|run [event][/dim]")
135
+ else:
136
+ print("Usage: /hooks list|edit [event]|reload|run [event]")
137
+
138
+ async def cmd_regen(self, args: str):
139
+ last_user_msg = None
140
+ for i in range(len(self.terminal.conversation) - 1, -1, -1):
141
+ if self.terminal.conversation[i]["role"] == "assistant":
142
+ self.terminal.conversation.pop(i)
143
+ break
144
+ for msg in reversed(self.terminal.conversation):
145
+ if msg["role"] == "user":
146
+ last_user_msg = msg["content"]
147
+ break
148
+ if last_user_msg:
149
+ for i in range(len(self.terminal.conversation) - 1, -1, -1):
150
+ if self.terminal.conversation[i]["role"] == "user" and self.terminal.conversation[i]["content"] == last_user_msg:
151
+ self.terminal.conversation.pop(i)
152
+ break
153
+ console.print("[dim]Regenerating...[/dim]" if HAS_RICH else "Regenerating...")
154
+ await self.terminal.send_message(last_user_msg)
155
+ else:
156
+ console.print("[dim]No message to regenerate[/dim]" if HAS_RICH else "Nothing to regenerate")
157
+
158
+ def cmd_undo(self, args: str):
159
+ if len(self.terminal.conversation) < 2:
160
+ console.print("[dim]Nothing to undo[/dim]" if HAS_RICH else "Nothing to undo")
161
+ return
162
+ removed = 0
163
+ for role in ("assistant", "user"):
164
+ for i in range(len(self.terminal.conversation) - 1, -1, -1):
165
+ if self.terminal.conversation[i]["role"] == role:
166
+ self.terminal.conversation.pop(i)
167
+ removed += 1
168
+ break
169
+ if HAS_RICH:
170
+ console.print(f"[dim]Undone ({removed} messages removed, {len(self.terminal.conversation)} remaining)[/dim]")
171
+ else:
172
+ print(f"Undone ({removed} removed)")
173
+
174
+ async def cmd_retry(self, args: str):
175
+ last_user_msg = None
176
+ for i in range(len(self.terminal.conversation) - 1, -1, -1):
177
+ if self.terminal.conversation[i]["role"] == "assistant":
178
+ self.terminal.conversation.pop(i)
179
+ break
180
+ for msg in reversed(self.terminal.conversation):
181
+ if msg["role"] == "user":
182
+ last_user_msg = msg["content"]
183
+ break
184
+ if not last_user_msg:
185
+ console.print("[dim]No message to retry[/dim]" if HAS_RICH else "Nothing to retry")
186
+ return
187
+ for i in range(len(self.terminal.conversation) - 1, -1, -1):
188
+ if self.terminal.conversation[i]["role"] == "user" and self.terminal.conversation[i]["content"] == last_user_msg:
189
+ self.terminal.conversation.pop(i)
190
+ break
191
+ orig_model_key = resolve_model_key(self.terminal.config.get("model", "qwen2.5:7b"))
192
+ _fallback_model = MODELS.get("qwen-fast") or MODELS.get("qwen7b") or next(iter(MODELS.values()))
193
+ orig_temp = MODELS.get(orig_model_key, _fallback_model).get("temperature", 0.3)
194
+ MODELS[orig_model_key]["temperature"] = min(0.9, orig_temp + 0.3)
195
+ if HAS_RICH:
196
+ console.print(f"[dim]Retrying with temperature {MODELS[orig_model_key]['temperature']:.1f}...[/dim]")
197
+ else:
198
+ print(f"Retrying (temp +0.3)...")
199
+ try:
200
+ await self.terminal.send_message(last_user_msg)
201
+ finally:
202
+ MODELS[orig_model_key]["temperature"] = orig_temp
203
+
204
+ def cmd_note(self, args: str):
205
+ text = args.strip()
206
+ if not text:
207
+ console.print("[dim]Usage: /note <text>[/dim]" if HAS_RICH else "Usage: /note <text>")
208
+ return
209
+ aria_md = pathlib.Path.cwd() / "ARIA.md"
210
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
211
+ entry = f"\n- [{now_str}] {text}"
212
+ if aria_md.exists():
213
+ content = aria_md.read_text(encoding="utf-8")
214
+ if "## Notes" not in content:
215
+ content += "\n\n## Notes\n"
216
+ content += entry
217
+ else:
218
+ content = f"# Aria Project Notes\n\n## Notes\n{entry}\n"
219
+ aria_md.write_text(content, encoding="utf-8")
220
+ global _PROJECT_CONTEXT
221
+ _PROJECT_CONTEXT = _load_project_context()
222
+ if HAS_RICH:
223
+ console.print(f"[dim]Note saved to {aria_md.name}[/dim]")
224
+ else:
225
+ print(f"Saved to {aria_md.name}")
226
+
227
+ async def cmd_review(self, args: str):
228
+ raw = args.strip()
229
+ policy = self.terminal.config.get("command_policy", "safe")
230
+
231
+ if raw and not raw.startswith("--"):
232
+ p = pathlib.Path(raw).expanduser()
233
+ if not p.exists():
234
+ msg = f"File not found: {raw}"
235
+ console.print(f"[red]{msg}[/red]") if HAS_RICH else print(msg)
236
+ return
237
+ _print_phase("Reading file")
238
+ try:
239
+ content = p.read_text(errors="replace")[:12000]
240
+ except Exception as e:
241
+ console.print(f"[red]Cannot read file: {e}[/red]") if HAS_RICH else print(f"Cannot read: {e}")
242
+ return
243
+ line_count = content.count("\n")
244
+ if HAS_RICH:
245
+ console.print(f" [dim]↳ {p.name} · {line_count} lines[/dim]")
246
+ _print_phase("AI Review")
247
+ prompt = (
248
+ f"请对以下 `{p.name}` 的代码进行专业审查,查找 Bug、安全问题和改进点。\n"
249
+ f"每条发现用严重程度标签开头:**BUG**、**IMPROVEMENT**、**NIT**。\n"
250
+ f"按文件组织输出,直接给结论,不要重复贴出全部代码。\n\n"
251
+ f"```\n{content}\n```"
252
+ )
253
+ else:
254
+ diff_cmd = "git diff --staged" if raw.startswith("--staged") else "git diff HEAD"
255
+ _print_phase("Reading diff")
256
+ tr = _tool_run_command({"command": diff_cmd})
257
+ if not tr.get("success"):
258
+ msg = tr.get("error", "git diff failed")
259
+ console.print(f"[red]{msg}[/red]") if HAS_RICH else print(msg)
260
+ return
261
+ diff_text = (tr.get("data") or {}).get("stdout", "").strip()
262
+ if not diff_text:
263
+ console.print("[dim]No changes to review.[/dim]") if HAS_RICH else print("No changes to review.")
264
+ return
265
+ _adds = diff_text.count("\n+") - diff_text.count("\n+++")
266
+ _dels = diff_text.count("\n-") - diff_text.count("\n---")
267
+ _files = diff_text.count("\ndiff --git")
268
+ if HAS_RICH:
269
+ console.print(f" [dim]↳ {_files} files · +{_adds} −{_dels} lines[/dim]")
270
+ diff_text = diff_text[:12000]
271
+ _print_phase("AI Review")
272
+ prompt = (
273
+ "请审查以下 git diff,找出 Bug、潜在回归、安全问题和代码质量问题。\n"
274
+ "每条发现用严重程度标签开头:**BUG**、**IMPROVEMENT**、**NIT**。\n"
275
+ "按文件分组,直接给出结论。\n\n"
276
+ f"```diff\n{diff_text}\n```"
277
+ )
278
+
279
+ await self.terminal.send_message(prompt)