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,179 @@
1
+ """SessionCommandsMixin — session list/load/save/export commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from apps.cli.session_export import build_session_export_payload
8
+
9
+
10
+ class SessionCommandsMixin:
11
+ """Mixin: session list/load/save/export commands."""
12
+
13
+ def cmd_sessions(self, args: str):
14
+ keyword = args.strip().lower()
15
+ sessions = self.terminal.session_mgr.list_sessions()
16
+ if keyword:
17
+ sessions = [s for s in sessions if keyword in s["title"].lower()]
18
+ if not sessions:
19
+ msg = f"No sessions matching '{keyword}'" if keyword else "No saved sessions"
20
+ console.print(f"[dim]{msg}[/dim]" if HAS_RICH else msg)
21
+ return
22
+ if HAS_RICH:
23
+ console.print()
24
+ header = f" [bold]Sessions[/bold] [dim]({len(sessions)} found)[/dim]" if keyword else " [bold]Sessions[/bold]"
25
+ console.print(header)
26
+ for i, s in enumerate(sessions, 1):
27
+ updated = s["updated"][:16] if s["updated"] else "-"
28
+ console.print(f" [dim]{i}.[/dim] [bold]{s['title']}[/bold] "
29
+ f"[dim]{s['id'][:8]} {s['messages']} msgs {updated}[/dim]")
30
+ console.print()
31
+ console.print(" [dim]Use /load <number> to resume · /sessions <keyword> to search[/dim]")
32
+ else:
33
+ for i, s in enumerate(sessions, 1):
34
+ print(f" {i}. [{s['id'][:8]}] {s['title']} ({s['messages']} msgs)")
35
+
36
+ def cmd_save(self, args: str):
37
+ if not self.terminal.conversation:
38
+ console.print("[dim]Nothing to save[/dim]" if HAS_RICH else "Nothing to save")
39
+ return
40
+ sid = self.terminal.session_id
41
+ title = args.strip().strip('"').strip("'") if args.strip() else None
42
+ meta = {}
43
+ if title:
44
+ meta["title"] = title
45
+ self.terminal.session_mgr.save_session(sid, self.terminal.conversation, metadata=meta)
46
+ self.terminal.config["last_session_id"] = sid
47
+ save_config(self.terminal.config)
48
+ display = f"{title} ({sid[:8]})" if title else f"{sid[:8]}..."
49
+ console.print(f"[green]Session saved: {display}[/green]" if HAS_RICH
50
+ else f"Saved: {display}")
51
+
52
+ def cmd_rename(self, args: str):
53
+ """Rename current session."""
54
+ title = args.strip().strip('"').strip("'")
55
+ if not title:
56
+ console.print("[dim]Usage: /rename <title>[/dim]" if HAS_RICH else "Usage: /rename <title>")
57
+ return
58
+ sid = self.terminal.session_id
59
+ data = self.terminal.session_mgr.load_session(sid)
60
+ if data:
61
+ meta = data.get("metadata", {})
62
+ meta["title"] = title
63
+ self.terminal.session_mgr.save_session(sid, self.terminal.conversation, metadata=meta)
64
+ else:
65
+ self.terminal.session_mgr.save_session(sid, self.terminal.conversation, metadata={"title": title})
66
+ console.print(f"[green]Renamed: {title}[/green]" if HAS_RICH else f"Renamed: {title}")
67
+
68
+ def cmd_load(self, args: str):
69
+ session_id = args.strip()
70
+ if not session_id:
71
+ sessions = self.terminal.session_mgr.list_sessions()
72
+ if not sessions:
73
+ console.print("[dim]No sessions. Usage: /load <session_id>[/dim]" if HAS_RICH
74
+ else "No sessions")
75
+ return
76
+ options = []
77
+ for s in sessions[:20]:
78
+ title = s.get("metadata", {}).get("title", s["id"][:8])
79
+ ts = s.get("updated", "")[:10]
80
+ options.append((title, ts))
81
+ choice = _arrow_select(options, selected=0, title="Load Session")
82
+ if 0 <= choice < len(sessions):
83
+ session_id = sessions[choice]["id"]
84
+ else:
85
+ if HAS_RICH:
86
+ console.print("[dim]Cancelled[/dim]")
87
+ else:
88
+ print("Cancelled")
89
+ return
90
+
91
+ data = self.terminal.session_mgr.load_session(session_id)
92
+ if data:
93
+ self.terminal.conversation = data.get("messages", [])
94
+ self.terminal.session_id = data["id"]
95
+ title = data.get("metadata", {}).get("title", "Untitled")
96
+ n = len(self.terminal.conversation)
97
+ console.print(f"[green]Loaded: {title} ({n} messages)[/green]" if HAS_RICH
98
+ else f"Loaded: {title} ({n} msgs)")
99
+ else:
100
+ _print_error(f"Session not found: {session_id}", "session")
101
+
102
+ def cmd_recall(self, args: str):
103
+ """Full-text search across all saved sessions: /recall <query>"""
104
+ query = args.strip()
105
+ if not query:
106
+ console.print("[dim]Usage: /recall <query>[/dim]" if HAS_RICH else "Usage: /recall <query>")
107
+ return
108
+ results = self.terminal.session_mgr.search_sessions(query)
109
+ if not results:
110
+ msg = f"No sessions found matching '{query}'"
111
+ console.print(f"[dim]{msg}[/dim]" if HAS_RICH else msg)
112
+ return
113
+ if HAS_RICH:
114
+ console.print()
115
+ console.print(f" [bold]Recall[/bold] [dim]{len(results)} session(s) match '{query}'[/dim]")
116
+ console.print()
117
+ for r in results[:10]:
118
+ updated = r["updated"][:16] if r["updated"] else ""
119
+ console.print(
120
+ f" [bold]{r['title']}[/bold] "
121
+ f"[dim]{r['id'][:8]} {r['match_count']} hit(s) {updated}[/dim]"
122
+ )
123
+ preview = r["preview"].replace("\n", " ")[:100]
124
+ console.print(f" [dim]…{preview}…[/dim]")
125
+ console.print()
126
+ console.print(" [dim]Use /load <id> to resume a session[/dim]")
127
+ else:
128
+ print(f"\n{len(results)} session(s) found:")
129
+ for r in results[:10]:
130
+ print(f" [{r['id'][:8]}] {r['title']} ({r['match_count']} hits)")
131
+ print(f" ...{r['preview'][:80]}...")
132
+
133
+ async def cmd_export(self, args: str):
134
+ parts = args.split()
135
+ fmt = parts[0].lower() if parts else "json"
136
+ filename = parts[1] if len(parts) > 1 else None
137
+
138
+ if not self.terminal.conversation:
139
+ console.print("[dim]Nothing to export[/dim]" if HAS_RICH else "Nothing to export")
140
+ return
141
+
142
+ try:
143
+ provider_health = []
144
+ if fmt == "bundle":
145
+ try:
146
+ from packages.aria_services.provider_health import GLOBAL_PROVIDER_HEALTH
147
+ provider_health = GLOBAL_PROVIDER_HEALTH.snapshot()
148
+ except Exception:
149
+ provider_health = []
150
+ content, ext, prefix = build_session_export_payload(
151
+ fmt,
152
+ self.terminal.conversation,
153
+ session_id=self.terminal.session_id,
154
+ config=self.terminal.config,
155
+ trace=getattr(self.terminal, "runtime_trace", None),
156
+ provider_health=provider_health,
157
+ )
158
+ except ValueError as exc:
159
+ if fmt == "sft" and "No user→assistant pairs" in str(exc):
160
+ console.print("[dim]No user→assistant pairs to export[/dim]" if HAS_RICH else "No pairs to export")
161
+ return
162
+ console.print("[dim]Format: json, csv, md, sft, or bundle[/dim]" if HAS_RICH
163
+ else "Format: json, csv, md, sft, bundle")
164
+ return
165
+
166
+ if fmt == "sft":
167
+ pairs = json.loads(content)
168
+ if HAS_RICH:
169
+ console.print(f"[dim]{len(pairs)} training pairs extracted[/dim]")
170
+ else:
171
+ print(f"{len(pairs)} training pairs")
172
+
173
+ if not filename:
174
+ from datetime import datetime
175
+ filename = f"{prefix}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{ext}"
176
+ with open(filename, "w", encoding="utf-8") as f:
177
+ f.write(content)
178
+ console.print(f"[green]Exported to {filename}[/green]" if HAS_RICH
179
+ else f"Exported: {filename}")
@@ -0,0 +1,280 @@
1
+ """SessionUxCommandsMixin — clear, recap, history, compact, fork, copy commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+
8
+ class SessionUxCommandsMixin:
9
+ """Mixin: session UX and conversation management commands."""
10
+
11
+ def cmd_clear(self, args: str):
12
+ self.terminal.conversation = []
13
+ os.system("clear" if os.name == "posix" else "cls")
14
+ console.print("[dim]Conversation cleared[/dim]" if HAS_RICH else "Cleared")
15
+
16
+ def cmd_btw(self, args: str):
17
+ q = args.strip()
18
+ if not q:
19
+ console.print("[dim]/btw <question> — quick question without polluting history[/dim]" if HAS_RICH else "/btw <question>")
20
+ return
21
+ conv = self.terminal.conversation
22
+ if not conv:
23
+ console.print("[dim](no conversation context yet)[/dim]" if HAS_RICH else "(no context)")
24
+ return
25
+ _ctx_slice = conv[-6:] if len(conv) >= 6 else conv
26
+ _ctx = "\n".join(
27
+ f"{m['role'].upper()}: {str(m.get('content', ''))[:300]}"
28
+ for m in _ctx_slice
29
+ )
30
+ _btw_prompt = (
31
+ f"[Side question — answer briefly, do not reference this note]\n"
32
+ f"Context from conversation:\n{_ctx}\n\nQuestion: {q}"
33
+ )
34
+ if HAS_RICH:
35
+ from rich.panel import Panel as _Panel
36
+ from rich import box as _rbox
37
+ console.print(_Panel(f"[dim]{q}[/dim]", title="[dim]/btw[/dim]", box=_rbox.ROUNDED, border_style="dim"))
38
+ import asyncio as _aio
39
+
40
+ async def _ask_btw():
41
+ try:
42
+ _result = await stream_provider_result(
43
+ OllamaProvider(
44
+ self.terminal.config.get("ollama_url", "http://localhost:11434"),
45
+ self.terminal.config.get("model", "qwen2.5:7b"),
46
+ show_market_prefetch_status=False,
47
+ ),
48
+ _btw_prompt,
49
+ tools=[],
50
+ )
51
+ if _result.get("success"):
52
+ return _result.get("response", "")
53
+ return f"(error: {_result.get('error', '')})"
54
+ except Exception as _e:
55
+ return f"(error: {_e})"
56
+
57
+ try:
58
+ loop = _aio.get_event_loop()
59
+ answer = loop.run_until_complete(_ask_btw()) if not loop.is_running() else "(run /btw from interactive prompt)"
60
+ except Exception:
61
+ answer = "(could not get answer)"
62
+ if HAS_RICH:
63
+ from rich.panel import Panel as _Panel
64
+ from rich import box as _rbox
65
+ console.print(_Panel(answer.strip(), title="[dim]↩ btw[/dim]", box=_rbox.ROUNDED, border_style="dim #C08050"))
66
+ else:
67
+ print(f"\n [btw] {answer.strip()}\n")
68
+
69
+ def cmd_recap(self, args: str):
70
+ conv = self.terminal.conversation
71
+ if not conv:
72
+ console.print("[dim]No conversation yet[/dim]" if HAS_RICH else "No conversation")
73
+ return
74
+ turns = len([m for m in conv if m.get("role") == "user"])
75
+ topics: list[str] = []
76
+ for m in conv:
77
+ if m.get("role") == "user":
78
+ snippet = str(m.get("content", ""))[:60].strip()
79
+ if snippet:
80
+ topics.append(snippet)
81
+ if HAS_RICH:
82
+ from rich.panel import Panel as _Panel
83
+ from rich import box as _rbox
84
+ body = f"[dim]{turns} 轮对话[/dim]\n"
85
+ for i, t in enumerate(topics[-6:], 1):
86
+ body += f" [dim]{i}.[/dim] {t}…\n"
87
+ console.print(_Panel(body.rstrip(), title="[bold]会话摘要[/bold]", box=_rbox.ROUNDED, border_style="dim"))
88
+ else:
89
+ print(f"Session: {turns} turns")
90
+ for i, t in enumerate(topics[-6:], 1):
91
+ print(f" {i}. {t}…")
92
+
93
+ def cmd_history(self, args: str):
94
+ if not self.terminal.conversation:
95
+ console.print("[dim]No conversation history[/dim]" if HAS_RICH else "No history")
96
+ return
97
+ for msg in self.terminal.conversation[-10:]:
98
+ role = msg["role"]
99
+ content = msg["content"][:120]
100
+ if HAS_RICH:
101
+ prefix = "You" if role == "user" else "Aria"
102
+ style = "bold" if role == "user" else "bold"
103
+ console.print(f"[{style}]{prefix}:[/{style}] [dim]{content}[/dim]")
104
+ else:
105
+ print(f"{'You' if role == 'user' else 'Aria'}: {content}")
106
+
107
+ def cmd_compact(self, args: str):
108
+ if "--hard" in args:
109
+ if len(self.terminal.conversation) > 10:
110
+ kept = self.terminal.conversation[-6:]
111
+ self.terminal.conversation = kept
112
+ console.print(f"[dim]Hard-compacted to last {len(kept)} messages[/dim]" if HAS_RICH
113
+ else f"Hard-compacted to {len(kept)} messages")
114
+ else:
115
+ console.print("[dim]Context small enough, no compaction needed[/dim]" if HAS_RICH
116
+ else "No compaction needed")
117
+ return
118
+ import asyncio as _asyncio
119
+ try:
120
+ loop = _asyncio.get_event_loop()
121
+ loop.run_until_complete(self._smart_compact_async(silent=False))
122
+ except RuntimeError:
123
+ if len(self.terminal.conversation) > 6:
124
+ from apps.cli.message_processing import compact_messages
125
+ compacted = compact_messages(
126
+ self.terminal.conversation,
127
+ model_key=self.terminal.config.get("model", "qwen2.5:7b"),
128
+ )
129
+ self.terminal.conversation = (
130
+ compacted
131
+ if len(compacted) < len(self.terminal.conversation)
132
+ else self.terminal.conversation[-8:]
133
+ )
134
+ console.print("[dim]Compacted (fallback)[/dim]")
135
+
136
+ async def _smart_compact_async(self, silent: bool = False):
137
+ conv = self.terminal.conversation
138
+ if len(conv) <= 4:
139
+ if not silent:
140
+ console.print("[dim]Context small enough — no compaction needed[/dim]" if HAS_RICH
141
+ else "Context small enough")
142
+ return
143
+
144
+ if not silent and HAS_RICH:
145
+ console.print("[dim]Summarising conversation...[/dim]")
146
+
147
+ try:
148
+ max_ctx = int(get_model_cfg(self.terminal.config.get("model", "qwen2.5:7b")).get("num_ctx", 16384) or 16384)
149
+ except Exception:
150
+ max_ctx = 16384
151
+ from packages.aria_services.context import build_context_service
152
+ context_service = build_context_service(max_tokens=max_ctx)
153
+ summary_prompt = context_service.build_summary_prompt(conv)
154
+
155
+ summary = ""
156
+ try:
157
+ result = await stream_provider_result(
158
+ OllamaProvider(
159
+ self.terminal.config.get("ollama_url", "http://localhost:11434"),
160
+ self.terminal.config.get("model", "qwen2.5:7b"),
161
+ show_market_prefetch_status=False,
162
+ ),
163
+ summary_prompt,
164
+ [],
165
+ tools=[],
166
+ )
167
+ if result.get("success") and result.get("response"):
168
+ summary = result["response"].strip()
169
+ except Exception:
170
+ pass
171
+
172
+ if not summary:
173
+ try:
174
+ compacted = context_service.compact_messages(conv)
175
+ except Exception:
176
+ compacted = []
177
+ self.terminal.conversation = compacted if compacted and len(compacted) < len(conv) else conv[-8:]
178
+ if not silent:
179
+ console.print("[dim]Compacted (summary failed, used local fallback)[/dim]" if HAS_RICH
180
+ else "Compacted (summary fallback)")
181
+ return
182
+
183
+ envelope = context_service.build_summary_envelope(conv, summary)
184
+ self.terminal.conversation = envelope.messages
185
+ new_count = len(self.terminal.conversation)
186
+ old_count = len(conv)
187
+ if not silent:
188
+ if HAS_RICH:
189
+ console.print(
190
+ f" [dim]✓ Compacted {old_count} → {new_count} messages "
191
+ f"(summary preserved context)[/dim]"
192
+ )
193
+ else:
194
+ print(f"Compacted {old_count} → {new_count} messages")
195
+
196
+ def cmd_fork(self, args: str):
197
+ import time as _t
198
+ name = args.strip() or f"fork-{_t.strftime('%H%M%S')}"
199
+ snapshot = {
200
+ "name": name,
201
+ "ts": _t.strftime("%Y-%m-%d %H:%M:%S"),
202
+ "conv": [dict(m) for m in self.terminal.conversation],
203
+ "config": dict(self.terminal.config),
204
+ }
205
+ self.terminal._forks.append(snapshot)
206
+ idx = len(self.terminal._forks) - 1
207
+ if HAS_RICH:
208
+ console.print(
209
+ f" [dim]↳ Forked as [bold]{name}[/bold] "
210
+ f"(fork #{idx}, {len(snapshot['conv'])} messages). "
211
+ f"Restore with /load-fork {idx}[/dim]"
212
+ )
213
+ else:
214
+ print(f"Forked as '{name}' (#{idx}). Restore with /load-fork {idx}")
215
+
216
+ def cmd_load_fork(self, args: str):
217
+ forks = self.terminal._forks
218
+ if not forks:
219
+ console.print("[dim]No forks yet — use /fork to create one[/dim]" if HAS_RICH else "No forks")
220
+ return
221
+ try:
222
+ idx = int(args.strip())
223
+ except (ValueError, IndexError):
224
+ if HAS_RICH:
225
+ for i, f in enumerate(forks):
226
+ console.print(f" [dim]#{i}[/dim] {f['name']} [dim]{f['ts']} {len(f['conv'])} msgs[/dim]")
227
+ else:
228
+ for i, f in enumerate(forks):
229
+ print(f" #{i} {f['name']} {f['ts']}")
230
+ return
231
+ if idx < 0 or idx >= len(forks):
232
+ console.print(f"[dim]Fork #{idx} not found[/dim]" if HAS_RICH else "Invalid index")
233
+ return
234
+ snap = forks[idx]
235
+ self.terminal.conversation = [dict(m) for m in snap["conv"]]
236
+ console.print(
237
+ f" [dim]✓ Restored fork [bold]{snap['name']}[/bold] "
238
+ f"({len(snap['conv'])} messages)[/dim]"
239
+ if HAS_RICH else f"Restored fork '{snap['name']}'"
240
+ )
241
+
242
+ def cmd_copy(self, args: str):
243
+ text = self.terminal._last_response
244
+ if not text:
245
+ console.print("[dim]No response to copy yet[/dim]" if HAS_RICH else "Nothing to copy")
246
+ return
247
+ copied = False
248
+ try:
249
+ import subprocess as _sp
250
+ _sp.run(["pbcopy"], input=text.encode(), check=True, timeout=3)
251
+ copied = True
252
+ except Exception:
253
+ pass
254
+ if not copied:
255
+ try:
256
+ import subprocess as _sp
257
+ _sp.run(["xclip", "-selection", "clipboard"], input=text.encode(), check=True, timeout=3)
258
+ copied = True
259
+ except Exception:
260
+ pass
261
+ if not copied:
262
+ try:
263
+ import subprocess as _sp
264
+ _sp.run(["xdotool", "type", "--clearmodifiers", text], check=True, timeout=3)
265
+ copied = True
266
+ except Exception:
267
+ pass
268
+ if copied:
269
+ self.terminal._record_feedback("copy", text)
270
+ preview = text[:60].replace("\n", " ")
271
+ console.print(
272
+ f" [dim]✓ Copied to clipboard: \"{preview}{'…' if len(text) > 60 else ''}\"[/dim]"
273
+ if HAS_RICH else f"Copied: \"{preview}\""
274
+ )
275
+ else:
276
+ console.print(
277
+ "[yellow]Could not reach clipboard (pbcopy/xclip not found). "
278
+ "Here is the response:[/yellow]\n" + text
279
+ if HAS_RICH else "Clipboard unavailable. Response:\n" + text
280
+ )