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,1414 @@
1
+ """
2
+ WorkspaceCommandsMixin — Workspace commands: packages, file, project, init, setup, memory.
3
+
4
+ Extracted from aria_cli.py. Methods' __globals__ are rebound to aria_cli's namespace
5
+ by _rebind_mixin_globals() called at module load time.
6
+ """
7
+ from __future__ import annotations
8
+
9
+
10
+ class WorkspaceCommandsMixin:
11
+ """Mixin: Workspace commands: packages, file, project, init, setup, memory."""
12
+
13
+ async def cmd_packages(self, args: str):
14
+ """Show Aria Code package facades and Arthera package bridge status."""
15
+ try:
16
+ from packages.aria_agents import list_agent_manifests
17
+ from packages.aria_core import (
18
+ build_package_manifest,
19
+ list_architecture_layers,
20
+ required_architecture_layer_names,
21
+ write_package_manifest,
22
+ )
23
+ from packages.aria_infra import (
24
+ aria_code_identity,
25
+ build_package_doctor_report,
26
+ discover_arthera_packages,
27
+ )
28
+ from packages.aria_mcp import (
29
+ arthera_quant_engine_server_config,
30
+ default_exposures,
31
+ load_mcp_config,
32
+ merge_server_config,
33
+ mcp_server_status,
34
+ mcp_tools_to_specs,
35
+ write_mcp_config,
36
+ )
37
+ from packages.aria_services import (
38
+ list_service_specs,
39
+ list_service_usage_specs,
40
+ required_service_names,
41
+ )
42
+ from packages.aria_services.provider_health import GLOBAL_PROVIDER_HEALTH
43
+ from packages.aria_skills import builtin_skill_specs
44
+ from packages.aria_tools import build_registry_from_legacy
45
+ except Exception as exc:
46
+ _print_error(f"packages facade unavailable: {exc}")
47
+ return
48
+
49
+ sub = args.strip().lower()
50
+ identity = aria_code_identity(__version__)
51
+ arthera = discover_arthera_packages()
52
+ tool_registry = build_registry_from_legacy(LOCAL_TOOLS, LOCAL_TOOL_SCHEMAS)
53
+ services = list_service_specs()
54
+ agent_count = len(list_agent_manifests())
55
+ skill_count = len(builtin_skill_specs())
56
+ mcp_exposure_count = len(default_exposures())
57
+ server_cfg = arthera_quant_engine_server_config()
58
+ mcp_config_path = MCP_CONFIG_PATH if _HAS_MCP else pathlib.Path.home() / ".arthera" / "mcp_servers.json"
59
+
60
+ if sub.startswith("export-manifest") or sub.startswith("manifest"):
61
+ raw_parts = args.strip().split(maxsplit=1)
62
+ out_path = None
63
+ if len(raw_parts) > 1:
64
+ out_path = pathlib.Path(raw_parts[1]).expanduser()
65
+ else:
66
+ from artifacts import artifact_dir
67
+ out_path = artifact_dir("manifests", "packages") / "aria_package_manifest.json"
68
+
69
+ reg = getattr(self.terminal, "_mcp_registry", None)
70
+ mcp_tools = []
71
+ if reg:
72
+ try:
73
+ mcp_tools = [
74
+ tool for tool in reg.all_tools()
75
+ if tool.get("server") == "arthera_quant_engine"
76
+ ]
77
+ except Exception:
78
+ mcp_tools = []
79
+ arthera_specs = mcp_tools_to_specs(mcp_tools, "arthera_quant_engine")
80
+ manifest = build_package_manifest(
81
+ identity=identity,
82
+ services=services,
83
+ tools=tool_registry.list(),
84
+ agents=list_agent_manifests(),
85
+ skills=builtin_skill_specs(),
86
+ mcp_exposures=default_exposures(),
87
+ arthera_packages=arthera,
88
+ arthera_mcp_tools=arthera_specs,
89
+ )
90
+ write_package_manifest(out_path, manifest)
91
+ if HAS_RICH:
92
+ console.print()
93
+ console.print(f" [green]Package manifest exported[/green] [dim]{out_path}[/dim]")
94
+ console.print(
95
+ f" [dim]tools={len(manifest['capabilities']['tools'])} "
96
+ f"services={len(manifest['capabilities']['services'])} "
97
+ f"agents={len(manifest['capabilities']['agents'])} "
98
+ f"skills={len(manifest['capabilities']['skills'])} "
99
+ f"arthera_mcp_tools={len(manifest['capabilities']['arthera_mcp_tools'])}[/dim]\n"
100
+ )
101
+ else:
102
+ print(f"Package manifest exported: {out_path}")
103
+ return
104
+
105
+ if sub in ("doctor", "doctor arthera", "arthera doctor"):
106
+ reg = getattr(self.terminal, "_mcp_registry", None)
107
+ runtime_status = []
108
+ mcp_tools = []
109
+ if reg:
110
+ try:
111
+ runtime_status = reg.status()
112
+ mcp_tools = [
113
+ tool for tool in reg.all_tools()
114
+ if tool.get("server") == "arthera_quant_engine"
115
+ ]
116
+ except Exception:
117
+ runtime_status = []
118
+ mcp_tools = []
119
+ status = mcp_server_status(mcp_config_path, "arthera_quant_engine", runtime_status)
120
+ specs = mcp_tools_to_specs(mcp_tools, "arthera_quant_engine")
121
+ from artifacts import artifact_dir
122
+ manifest_path = artifact_dir("manifests", "packages", create=False) / "aria_package_manifest.json"
123
+ report = build_package_doctor_report(
124
+ arthera=arthera,
125
+ mcp_status=status,
126
+ tool_count=len(specs),
127
+ manifest_can_export=True,
128
+ manifest_path=manifest_path,
129
+ services=services,
130
+ required_services=required_service_names(),
131
+ architecture_layers=list_architecture_layers(),
132
+ required_architecture_layers=required_architecture_layer_names(),
133
+ provider_health=GLOBAL_PROVIDER_HEALTH.snapshot(),
134
+ )
135
+ if HAS_RICH:
136
+ from rich.table import Table as _Table
137
+ color = "green" if report.status == "ok" else "yellow" if report.status == "warn" else "red"
138
+ console.print()
139
+ console.print(
140
+ f" [bold]{identity.product}[/bold] "
141
+ f"[dim]· {identity.company} packages doctor ·[/dim] [{color}]{report.status}[/{color}]\n"
142
+ )
143
+ tbl = _Table(
144
+ box=rich_box.ROUNDED,
145
+ border_style="dim",
146
+ show_header=True,
147
+ header_style="bold dim",
148
+ )
149
+ tbl.add_column("Check", width=22)
150
+ tbl.add_column("Status", width=8)
151
+ tbl.add_column("Detail")
152
+ tbl.add_column("Next")
153
+ for check in report.checks:
154
+ st_color = "green" if check.status == "ok" else "yellow" if check.status == "warn" else "red"
155
+ tbl.add_row(
156
+ check.name,
157
+ f"[{st_color}]{check.status}[/{st_color}]",
158
+ check.detail,
159
+ check.remediation,
160
+ )
161
+ console.print(tbl)
162
+ console.print()
163
+ else:
164
+ print(f"{identity.product} packages doctor: {report.status}")
165
+ for check in report.checks:
166
+ print(f"{check.status:5s} {check.name}: {check.detail} {check.remediation}")
167
+ return
168
+
169
+ if sub in ("status", "status arthera", "arthera status"):
170
+ runtime_status = []
171
+ reg = getattr(self.terminal, "_mcp_registry", None)
172
+ if reg:
173
+ try:
174
+ runtime_status = reg.status()
175
+ except Exception:
176
+ runtime_status = []
177
+ status = mcp_server_status(mcp_config_path, "arthera_quant_engine", runtime_status)
178
+
179
+ if HAS_RICH:
180
+ console.print()
181
+ console.print(
182
+ f" [bold]{identity.product}[/bold] "
183
+ f"[dim]· {identity.company} package bridge status[/dim]\n"
184
+ )
185
+ rows = [
186
+ ("config", status["config_path"]),
187
+ ("configured", "yes" if status["configured"] else "no"),
188
+ ("server", status["server_path"] or "—"),
189
+ ("server file", "found" if status["server_file_exists"] else "missing"),
190
+ ("runtime", "running" if status["running"] else "not running"),
191
+ ("tools", str(status["tool_count"])),
192
+ ]
193
+ for key, value in rows:
194
+ style = "green" if value in ("yes", "found", "running") else "yellow" if value in ("no", "missing", "not running") else "dim"
195
+ console.print(f" [dim]{key:12s}[/dim] [{style}]{value}[/{style}]")
196
+ if status["tools"]:
197
+ console.print(" [dim]tool names:[/dim] " + ", ".join(status["tools"][:12]))
198
+ if not status["configured"]:
199
+ console.print(" [dim]Run /packages connect arthera to write the MCP bridge.[/dim]")
200
+ elif not status["running"]:
201
+ console.print(" [dim]Run /mcp reload or /packages connect arthera --reload.[/dim]")
202
+ console.print()
203
+ else:
204
+ print(f"{identity.product} · {identity.company} package bridge status")
205
+ for key, value in status.items():
206
+ if key != "tools":
207
+ print(f"{key}: {value}")
208
+ return
209
+
210
+ if sub in ("tools arthera", "arthera tools", "tools"):
211
+ reg = getattr(self.terminal, "_mcp_registry", None)
212
+ mcp_tools = []
213
+ if reg:
214
+ try:
215
+ mcp_tools = [
216
+ tool for tool in reg.all_tools()
217
+ if tool.get("server") == "arthera_quant_engine"
218
+ ]
219
+ except Exception:
220
+ mcp_tools = []
221
+ specs = mcp_tools_to_specs(mcp_tools, "arthera_quant_engine")
222
+
223
+ if HAS_RICH:
224
+ from rich.table import Table as _Table
225
+ console.print()
226
+ console.print(
227
+ f" [bold]{identity.product}[/bold] "
228
+ f"[dim]· {identity.company} MCP tool manifests[/dim]\n"
229
+ )
230
+ if not specs:
231
+ console.print(" [yellow]No Arthera MCP tools discovered.[/yellow]")
232
+ console.print(" [dim]Run /packages connect arthera --reload, then retry /packages tools arthera.[/dim]\n")
233
+ return
234
+ tbl = _Table(
235
+ title="[bold]Arthera QuantEngine Tools[/bold]",
236
+ box=rich_box.ROUNDED,
237
+ border_style="dim",
238
+ show_header=True,
239
+ header_style="bold dim",
240
+ )
241
+ tbl.add_column("Tool", width=34)
242
+ tbl.add_column("Permissions", width=24)
243
+ tbl.add_column("Capabilities", width=30)
244
+ tbl.add_column("Schema")
245
+ for spec in specs:
246
+ perms = ", ".join(p.value for p in spec.permissions)
247
+ caps = ", ".join(spec.capabilities)
248
+ schema_state = "yes" if spec.schema else "no"
249
+ tbl.add_row(spec.name, perms, caps, schema_state)
250
+ console.print(tbl)
251
+ console.print()
252
+ else:
253
+ if not specs:
254
+ print("No Arthera MCP tools discovered. Run /packages connect arthera --reload.")
255
+ return
256
+ for spec in specs:
257
+ print(f"{spec.name}: {', '.join(spec.capabilities)}")
258
+ return
259
+
260
+ if sub in ("services", "service", "usage", "use", "map"):
261
+ usage_specs = list_service_usage_specs()
262
+ if HAS_RICH:
263
+ from rich.table import Table as _Table
264
+ console.print()
265
+ console.print(
266
+ f" [bold]{identity.product}[/bold] "
267
+ f"[dim]· service usage map[/dim]\n"
268
+ )
269
+ tbl = _Table(
270
+ title="[bold]Project Services[/bold]",
271
+ box=rich_box.ROUNDED,
272
+ border_style="dim",
273
+ show_header=True,
274
+ header_style="bold dim",
275
+ )
276
+ tbl.add_column("Service", width=22)
277
+ tbl.add_column("Purpose", width=34)
278
+ tbl.add_column("CLI", width=32)
279
+ tbl.add_column("Arthera / MCP", width=34)
280
+ tbl.add_column("Next")
281
+ for spec in usage_specs:
282
+ mcp = ", ".join(spec.mcp_tools[:4]) if spec.mcp_tools else "—"
283
+ packages = ", ".join(spec.package_sources[:2])
284
+ tbl.add_row(
285
+ spec.name,
286
+ spec.purpose,
287
+ ", ".join(spec.cli_entrypoints),
288
+ f"{packages}\n[dim]{mcp}[/dim]",
289
+ spec.next_step,
290
+ )
291
+ console.print(tbl)
292
+ console.print("[dim]连接 Arthera MCP: /packages connect arthera --reload;券商接入: /broker guide[/dim]\n")
293
+ else:
294
+ for spec in usage_specs:
295
+ print(f"{spec.name}: {spec.purpose} -> {', '.join(spec.cli_entrypoints)}")
296
+ return
297
+
298
+ if sub.startswith("connect arthera") or sub in ("connect", "connect-quant", "connect quant"):
299
+ existing = load_mcp_config(mcp_config_path)
300
+ updated = merge_server_config(existing, server_cfg)
301
+ write_mcp_config(mcp_config_path, updated)
302
+ server_path = pathlib.Path(str(server_cfg["args"][0]))
303
+ ready = server_path.exists()
304
+ if HAS_RICH:
305
+ console.print()
306
+ console.print(
307
+ f" [bold]{identity.product}[/bold] "
308
+ f"[dim]connected to {identity.company} package bridge[/dim]"
309
+ )
310
+ status = "[green]ready[/green]" if ready else "[yellow]configured, server file not found[/yellow]"
311
+ console.print(f" {status} [dim]{server_cfg['name']}[/dim]")
312
+ console.print(f" [dim]config:[/dim] {mcp_config_path}")
313
+ console.print(f" [dim]server:[/dim] {server_path}")
314
+ else:
315
+ print(f"Connected {identity.product} -> {server_cfg['name']}")
316
+ print(f"config: {mcp_config_path}")
317
+ print(f"server: {server_path} ({'ready' if ready else 'missing'})")
318
+
319
+ if "--reload" in sub or " reload" in sub:
320
+ await self.cmd_mcp("reload")
321
+ else:
322
+ if HAS_RICH:
323
+ console.print(" [dim]Run /mcp reload to start the server.[/dim]\n")
324
+ else:
325
+ print("Run /mcp reload to start the server.")
326
+ return
327
+
328
+ if HAS_RICH:
329
+ from rich.table import Table as _Table
330
+
331
+ console.print()
332
+ console.print(
333
+ f" [bold]{identity.product}[/bold] "
334
+ f"[dim]v{identity.version} · {identity.company} product[/dim]"
335
+ )
336
+ console.print(f" [dim]{identity.description}[/dim]\n")
337
+
338
+ tbl = _Table(
339
+ title="[bold]Package Facades[/bold]",
340
+ box=rich_box.ROUNDED,
341
+ border_style="dim",
342
+ show_header=True,
343
+ header_style="bold dim",
344
+ )
345
+ tbl.add_column("Package")
346
+ tbl.add_column("Status")
347
+ tbl.add_column("Surface")
348
+ tbl.add_row("aria_core", "ready", "CapabilityManifest, permissions")
349
+ tbl.add_row("aria_services", "ready", f"{len(services)} service boundaries")
350
+ tbl.add_row("aria_tools", "ready", f"{len(tool_registry.list())} tools")
351
+ tbl.add_row("aria_agents", "ready", f"{agent_count} agents")
352
+ tbl.add_row("aria_skills", "ready", f"{skill_count} skills")
353
+ tbl.add_row("aria_mcp", "ready", f"{mcp_exposure_count} planned exposures")
354
+ tbl.add_row("aria_infra", "ready", "Arthera package discovery")
355
+ console.print(tbl)
356
+
357
+ console.print()
358
+ console.print("[bold]Arthera Packages[/bold]")
359
+ from ui.render.output import display_path as _display_path
360
+ if arthera.available:
361
+ console.print(f" [green]found[/green] [dim]{_display_path(arthera.root, fallback='package root')}[/dim]")
362
+ for name, path in sorted(arthera.packages.items()):
363
+ console.print(f" [dim]·[/dim] [bold]{name:14s}[/bold] [dim]{_display_path(path, fallback='package')}[/dim]")
364
+ if arthera.mcp_servers:
365
+ console.print(" [dim]MCP server candidates:[/dim]")
366
+ for path in arthera.mcp_servers[:5]:
367
+ console.print(f" [dim]{_display_path(path, fallback='server')}[/dim]")
368
+ else:
369
+ console.print(f" [yellow]not found[/yellow] [dim]{_display_path(arthera.root, fallback='package root')}[/dim]")
370
+
371
+ console.print()
372
+ console.print("[bold]Recommended MCP bridge[/bold]")
373
+ console.print(f" [dim]name:[/dim] {server_cfg['name']}")
374
+ console.print(f" [dim]command:[/dim] {server_cfg['command']} {' '.join(server_cfg['args'])}")
375
+ console.print(f" [dim]env PYTHONPATH:[/dim] {server_cfg['env']['PYTHONPATH']}")
376
+ console.print(f" [dim]config:[/dim] {mcp_config_path}")
377
+ console.print(" [dim]Run /packages connect arthera to write this MCP bridge.[/dim]\n")
378
+ else:
379
+ print(f"{identity.product} v{identity.version} · {identity.company} product")
380
+ print(f"services={len(services)} tools={len(tool_registry.list())} agents={agent_count} skills={skill_count} mcp={mcp_exposure_count}")
381
+ print(f"Arthera packages: {'found' if arthera.available else 'not found'} {arthera.root}")
382
+ print(f"Recommended MCP: {server_cfg}")
383
+
384
+ async def cmd_file(self, args: str):
385
+ """
386
+ /file load <路径> — 加载文件到会话
387
+ /file analyze [1|2|3|4] — 分层分析(1=摘要 2=深度 3=领域 4=建议)
388
+ /file ask <问题> — 就已加载文件提问
389
+ /file list — 列出会话中的所有文件
390
+ /file switch <文件名> — 切换活跃文件
391
+ /file clear [文件名] — 清除文件
392
+ /file check — 检查可用解析器
393
+ """
394
+ import asyncio as _asyncio
395
+ loop = _asyncio.get_event_loop()
396
+ parts = args.strip().split(None, 1) if args.strip() else []
397
+ sub = parts[0].lower() if parts else "help"
398
+ rest = parts[1].strip() if len(parts) > 1 else ""
399
+
400
+ # ── 确保 file_session 已初始化 ────────────────────────────────────────
401
+ if self.terminal._file_session is None:
402
+ try:
403
+ from file_analysis_tools import FileSession
404
+ self.terminal._file_session = FileSession()
405
+ except ImportError as e:
406
+ if HAS_RICH:
407
+ console.print(f"[red]file_analysis_tools 未加载: {e}[/red]")
408
+ return
409
+
410
+ fs = self.terminal._file_session
411
+
412
+ # ────────────────── /file load ────────────────────────────────────────
413
+ if sub == "load":
414
+ if not rest:
415
+ if HAS_RICH:
416
+ console.print("[dim]用法: /file load <文件路径>[/dim]")
417
+ console.print("[dim]支持: PDF DOCX XLSX CSV JSON TXT MD 图片 代码文件[/dim]")
418
+ return
419
+
420
+ if HAS_RICH:
421
+ console.print(f"[dim]正在解析 {rest}...[/dim]")
422
+
423
+ # Include images only for vision-capable models
424
+ _curr_model = self.terminal.config.get("model", "")
425
+ include_img = False
426
+ if _HAS_MODEL_CAP:
427
+ try:
428
+ _mc = get_model_capability(_curr_model)
429
+ include_img = bool(_mc.vision)
430
+ except Exception:
431
+ pass
432
+
433
+ from file_analysis_tools import parse_file, check_parsers
434
+ fc = await loop.run_in_executor(
435
+ None, lambda: parse_file(rest, include_images=include_img))
436
+
437
+ if not fc.success:
438
+ if HAS_RICH:
439
+ console.print(f"[red]解析失败: {fc.error}[/red]")
440
+ # Show which parsers are available
441
+ parsers = check_parsers()
442
+ missing = [k for k, v in parsers.items() if not v]
443
+ if missing:
444
+ console.print(f"[yellow]⚠ 未安装解析器: {', '.join(missing)}[/yellow]")
445
+ console.print(f"[dim]安装命令: pip install {' '.join(missing)}[/dim]")
446
+ return
447
+
448
+ fs.load(rest, include_images=include_img)
449
+ self.terminal._file_ctx_injected = False # Reset so next msg injects file
450
+
451
+ if HAS_RICH:
452
+ from rich.panel import Panel as _P
453
+ from rich import box as _box
454
+ info_lines = [
455
+ f"[green]✓[/green] [bold]{fc.filename}[/bold]",
456
+ f"[dim]类型: {fc.file_type.upper()} 大小: {fc.size_kb:.1f} KB "
457
+ f"提取: {fc.char_count:,} 字符[/dim]",
458
+ ]
459
+ for k, v in fc.metadata.items():
460
+ if k in ("pages","rows","columns","lines","language",
461
+ "sheets","title","author","symbols"):
462
+ val = v[:5] if isinstance(v, list) else v
463
+ info_lines.append(f"[dim]{k}: {val}[/dim]")
464
+ if fc.truncated:
465
+ info_lines.append(f"[yellow]⚠ 内容已截断(文件较大)[/yellow]")
466
+ if fc.tables:
467
+ info_lines.append(f"[dim]包含 {len(fc.tables)} 个表格[/dim]")
468
+ info_lines.append(f"\n[dim]发送任何消息即可开始分析,或使用 /file analyze 1-4[/dim]")
469
+ console.print(_P("\n".join(info_lines),
470
+ title="[bold]📄 文件已加载[/bold]",
471
+ border_style="green", box=_box.ROUNDED))
472
+
473
+ # ────────────────── /file analyze ─────────────────────────────────────
474
+ elif sub == "analyze":
475
+ fc = fs.get_active()
476
+ if not fc:
477
+ if HAS_RICH: console.print("[dim]请先使用 /file load <路径> 加载文件[/dim]")
478
+ return
479
+
480
+ # Determine layer(s) to run
481
+ layer_arg = rest.strip()
482
+ if layer_arg == "all":
483
+ layers_to_run = [1, 2, 3, 4]
484
+ else:
485
+ try:
486
+ layers_to_run = [int(layer_arg)] if layer_arg else [1, 2]
487
+ except ValueError:
488
+ layers_to_run = [1, 2]
489
+
490
+ layer_names = {1: "📌 快速摘要", 2: "🔍 深度分析", 3: "💡 领域洞察", 4: "✅ 行动建议"}
491
+
492
+ from file_analysis_tools import build_analysis_prompt
493
+
494
+ for layer in layers_to_run:
495
+ if HAS_RICH:
496
+ console.print(f"\n[bold]{layer_names.get(layer, f'层{layer}')}[/bold]")
497
+ console.print(f"[dim]{'─'*50}[/dim]")
498
+
499
+ prompt = build_analysis_prompt(fc, layer=layer)
500
+ # Send to LLM via the normal message pipeline
501
+ await self.terminal.send_message(
502
+ prompt,
503
+ system_override=(
504
+ "你是专业文档分析助手,具备金融、法律、技术、不动产等多领域知识。"
505
+ "分析要精确、结构化,优先使用数字和具体事实。"
506
+ ),
507
+ )
508
+
509
+ if len(layers_to_run) > 1 and layer < layers_to_run[-1]:
510
+ if HAS_RICH:
511
+ console.print(f"\n[dim]{'═'*60}[/dim]")
512
+ console.print(f"[dim]进入下一层分析...[/dim]\n")
513
+
514
+ # ────────────────── /file ask ──────────────────────────────────────────
515
+ elif sub == "ask":
516
+ fc = fs.get_active()
517
+ if not fc:
518
+ if HAS_RICH: console.print("[dim]请先使用 /file load <路径> 加载文件[/dim]")
519
+ return
520
+ question = rest.strip()
521
+ if not question:
522
+ if HAS_RICH:
523
+ from rich.prompt import Prompt as _Prompt
524
+ question = _Prompt.ask(" 请输入问题")
525
+ else:
526
+ question = input("请输入问题: ")
527
+ if not question:
528
+ return
529
+
530
+ from file_analysis_tools import build_analysis_prompt
531
+ prompt = build_analysis_prompt(fc, layer=0, question=question)
532
+ await self.terminal.send_message(
533
+ prompt,
534
+ system_override=(
535
+ "你是专业文档分析助手。请基于用户提供的文件内容准确回答问题,"
536
+ "若文件中无法找到答案,请如实说明。"
537
+ ),
538
+ )
539
+
540
+ # ────────────────── /file list ─────────────────────────────────────────
541
+ elif sub == "list":
542
+ files = fs.list_files()
543
+ if not files:
544
+ if HAS_RICH: console.print("[dim]会话中暂无已加载文件。使用 /file load <路径>[/dim]")
545
+ return
546
+ if HAS_RICH:
547
+ from rich.table import Table as _T
548
+ from rich import box as _box
549
+ tb = _T(title="[bold]📂 已加载文件[/bold]", box=_box.ROUNDED)
550
+ tb.add_column("状态", width=4); tb.add_column("文件名")
551
+ tb.add_column("类型", style="dim"); tb.add_column("大小KB", justify="right", style="dim")
552
+ tb.add_column("字符数", justify="right", style="dim"); tb.add_column("截断", style="dim")
553
+ for f in files:
554
+ status = "[green]●[/green]" if f["active"] else "[dim]○[/dim]"
555
+ tb.add_row(status, f["filename"], f["type"].upper(),
556
+ str(f["size_kb"]), f"{f['chars']:,}",
557
+ "[yellow]是[/yellow]" if f["truncated"] else "否")
558
+ console.print(tb)
559
+ console.print("[dim]/file ask <问题> 向活跃文件提问[/dim]")
560
+
561
+ # ────────────────── /file switch ──────────────────────────────────────
562
+ elif sub == "switch":
563
+ if not rest:
564
+ if HAS_RICH: console.print("[dim]用法: /file switch <文件名>[/dim]")
565
+ return
566
+ if fs.set_active(rest):
567
+ fc = fs.get_active()
568
+ self.terminal._file_ctx_injected = False
569
+ if HAS_RICH:
570
+ console.print(f"[green]✓ 已切换到: {fc.filename}[/green]")
571
+ else:
572
+ if HAS_RICH: console.print(f"[red]未找到文件: {rest}[/red]")
573
+
574
+ # ────────────────── /file clear ──────────────────────────────────────
575
+ elif sub == "clear":
576
+ fs.clear(rest if rest else None)
577
+ self.terminal._file_ctx_injected = False
578
+ msg = f"已清除文件: {rest}" if rest else "已清除所有已加载文件"
579
+ if HAS_RICH: console.print(f"[dim]{msg}[/dim]")
580
+
581
+ # ────────────────── /file check ───────────────────────────────────────
582
+ elif sub == "check":
583
+ from file_analysis_tools import check_parsers
584
+ parsers = check_parsers()
585
+ if HAS_RICH:
586
+ from rich.table import Table as _T
587
+ from rich import box as _box
588
+ tb = _T(title="[bold]📦 文件解析器状态[/bold]", box=_box.ROUNDED)
589
+ tb.add_column("库"); tb.add_column("状态"); tb.add_column("安装命令", style="dim")
590
+ _CMDS = {
591
+ "pdfplumber": "pip install pdfplumber",
592
+ "pypdf": "pip install pypdf",
593
+ "python-docx":"pip install python-docx",
594
+ "pandas": "pip install pandas",
595
+ "openpyxl": "pip install openpyxl",
596
+ "beautifulsoup4": "pip install beautifulsoup4",
597
+ "Pillow": "pip install Pillow",
598
+ }
599
+ for lib, ok in parsers.items():
600
+ status = "[green]✓ 已安装[/green]" if ok else "[red]✗ 未安装[/red]"
601
+ tb.add_row(lib, status, "" if ok else _CMDS.get(lib,""))
602
+ console.print(tb)
603
+ formats_ok = []
604
+ if parsers.get("pdfplumber") or parsers.get("pypdf"):
605
+ formats_ok.append("PDF")
606
+ if parsers.get("python-docx"):
607
+ formats_ok.append("Word/DOCX")
608
+ if parsers.get("pandas") and parsers.get("openpyxl"):
609
+ formats_ok.append("Excel/CSV")
610
+ formats_ok.extend(["JSON", "TXT/MD", "代码文件"])
611
+ console.print(f"[dim]可解析格式: {', '.join(formats_ok)}[/dim]")
612
+
613
+ # ────────────────── /file help ─────────────────────────────────────────
614
+ else:
615
+ if HAS_RICH:
616
+ console.print("[bold]📄 /file 文件分析命令[/bold]")
617
+ rows = [
618
+ ("/file load <路径>", "加载文件 (PDF/DOCX/XLSX/CSV/JSON/TXT/代码/图片)"),
619
+ ("/file analyze 1", "快速摘要 (300字)"),
620
+ ("/file analyze 2", "深度内容分析 (结构/要点/异常)"),
621
+ ("/file analyze 3", "领域专项分析 (财务/法律/技术/不动产)"),
622
+ ("/file analyze 4", "行动建议与风险清单"),
623
+ ("/file analyze all", "依次运行 4 层分析"),
624
+ ("/file ask <问题>", "就文件内容多轮提问"),
625
+ ("/file list", "查看已加载文件"),
626
+ ("/file switch <文件名>", "切换分析目标文件"),
627
+ ("/file clear", "清除所有已加载文件"),
628
+ ("/file check", "检查已安装的解析器"),
629
+ ]
630
+ from rich.table import Table as _T
631
+ from rich import box as _box
632
+ tb = _T(box=_box.MINIMAL)
633
+ tb.add_column("命令", style="cyan"); tb.add_column("说明", style="dim")
634
+ for cmd, desc in rows:
635
+ tb.add_row(cmd, desc)
636
+ console.print(tb)
637
+
638
+ async def cmd_project(self, args: str):
639
+ """项目分析 (Claude Code / Codex 风格): /project load|tree|grep|ask|task|status|info <参数>"""
640
+ try:
641
+ from project_tools import ProjectSession, scan_project, format_grep_results
642
+ _HAS_PT = True
643
+ except ImportError:
644
+ _HAS_PT = False
645
+
646
+ if not _HAS_PT:
647
+ console.print("[red]❌ project_tools.py 未找到,请确保文件存在。[/red]")
648
+ return
649
+
650
+ parts = args.strip().split(maxsplit=1)
651
+ sub = parts[0].lower() if parts else "info"
652
+ rest = parts[1].strip() if len(parts) > 1 else ""
653
+ ps = self.terminal._project_session # type: ignore[attr-defined]
654
+
655
+ # ── load ──────────────────────────────────────────────────────────────
656
+ if sub == "load":
657
+ if not rest:
658
+ console.print("[yellow]用法: /project load <目录路径>[/yellow]")
659
+ return
660
+ from pathlib import Path as _Path
661
+ target = _Path(rest).expanduser().resolve()
662
+ if not target.exists():
663
+ console.print(f"[red]路径不存在: {target}[/red]")
664
+ return
665
+
666
+ console.print(f"[dim]正在扫描项目: {target} …[/dim]")
667
+ try:
668
+ new_ps = scan_project(str(target), max_files=2000)
669
+ except Exception as e:
670
+ console.print(f"[red]扫描失败: {e}[/red]")
671
+ return
672
+
673
+ self.terminal._project_session = new_ps # type: ignore[attr-defined]
674
+ self.terminal._project_ctx_injected = False # type: ignore[attr-defined]
675
+
676
+ # Auto-archive project into global memory
677
+ if getattr(self.terminal, "memory_mgr", None):
678
+ try:
679
+ _ps_s = new_ps.summary()
680
+ self.terminal.memory_mgr.upsert_project(_ps_s["name"], {
681
+ "root": _ps_s["root"],
682
+ "type": _ps_s["type"],
683
+ "languages": _ps_s.get("languages", []),
684
+ })
685
+ except Exception:
686
+ pass
687
+
688
+ s = new_ps.summary()
689
+ tb = _T(box=_box.ROUNDED, show_header=False, padding=(0, 1))
690
+ tb.add_column("k", style="dim", width=14)
691
+ tb.add_column("v", style="cyan")
692
+ tb.add_row("项目名", s["name"])
693
+ tb.add_row("路径", s["root"])
694
+ tb.add_row("类型", s["type"])
695
+ tb.add_row("语言", ", ".join(s["languages"][:4]))
696
+ tb.add_row("文件数", str(s["total_files"]))
697
+ tb.add_row("代码行", f"{s['total_lines']:,}")
698
+ tb.add_row("总大小", f"{s['total_size_kb']} KB")
699
+ if s["git"].get("branch"):
700
+ tb.add_row("Git 分支", s["git"]["branch"])
701
+ if s["git"].get("changed_count"):
702
+ tb.add_row("变更文件", str(s["git"]["changed_count"]))
703
+ console.print(f"\n[bold]项目已加载 ✓[/bold]")
704
+ console.print(tb)
705
+ console.print(f"\n[dim]关键文件: {', '.join(s['key_files'][:6])}[/dim]")
706
+ console.print("[dim]现在可以直接对话,Aria 将根据项目上下文回答。[/dim]\n")
707
+ return
708
+
709
+ # ── 未加载时提示 ──────────────────────────────────────────────────────
710
+ if ps is None:
711
+ console.print("[yellow]请先加载项目: /project load <目录路径>[/yellow]")
712
+ return
713
+
714
+ # ── tree ──────────────────────────────────────────────────────────────
715
+ if sub == "tree":
716
+ depth_arg = rest.strip()
717
+ max_lines = 120
718
+ if depth_arg.isdigit():
719
+ max_lines = int(depth_arg) * 30 # rough approximation
720
+ tree_str = ps.get_tree(max_lines=max_lines)
721
+ console.print(f"\n[bold]{ps.name}/[/bold]")
722
+ console.print(f"[dim]{tree_str}[/dim]")
723
+ console.print(f"\n[dim]共 {ps.stats.get('total_files', 0)} 个文件[/dim]\n")
724
+
725
+ # ── grep / search ─────────────────────────────────────────────────────
726
+ elif sub in ("grep", "search"):
727
+ if not rest:
728
+ console.print("[yellow]用法: /project grep <正则表达式> [glob模式][/yellow]")
729
+ return
730
+ parts2 = rest.split(maxsplit=1)
731
+ pattern = parts2[0]
732
+ glob = parts2[1] if len(parts2) > 1 else "**/*"
733
+ console.print(f"[dim]搜索 \"{pattern}\" …[/dim]")
734
+ results = ps.grep(pattern, glob=glob, max_results=60)
735
+ console.print(format_grep_results(results, pattern))
736
+
737
+ # ── status ────────────────────────────────────────────────────────────
738
+ elif sub == "status":
739
+ gi = ps.git_info
740
+ if not gi:
741
+ console.print("[dim]当前项目不是 Git 仓库[/dim]")
742
+ return
743
+ console.print(f"\n[bold]Git 状态[/bold] — {ps.name}")
744
+ console.print(f" 分支: [cyan]{gi.get('branch','?')}[/cyan] "
745
+ f"变更: [yellow]{gi.get('changed_count', 0)}[/yellow] 个文件")
746
+ if gi.get("changed_files"):
747
+ for f in gi["changed_files"][:15]:
748
+ console.print(f" [dim]{f}[/dim]")
749
+ if gi.get("recent_commits"):
750
+ console.print("\n[bold]最近提交:[/bold]")
751
+ for c in gi["recent_commits"][:5]:
752
+ console.print(f" [dim]{c}[/dim]")
753
+ console.print()
754
+
755
+ # ── info ──────────────────────────────────────────────────────────────
756
+ elif sub in ("info", "summary", ""):
757
+ s = ps.summary()
758
+ tb = _T(box=_box.ROUNDED, show_header=False, padding=(0, 1))
759
+ tb.add_column("k", style="dim", width=14)
760
+ tb.add_column("v", style="cyan")
761
+ tb.add_row("项目名", s["name"])
762
+ tb.add_row("路径", s["root"])
763
+ tb.add_row("类型", s["type"])
764
+ tb.add_row("主要语言", ", ".join(s["languages"][:4]))
765
+ tb.add_row("文件数", str(s["total_files"]))
766
+ tb.add_row("代码行", f"{s['total_lines']:,}")
767
+ tb.add_row("大小", f"{s['total_size_kb']} KB")
768
+ if s["git"].get("branch"):
769
+ tb.add_row("Git 分支", s["git"]["branch"])
770
+ console.print(tb)
771
+ console.print(f"\n[dim]关键文件: {', '.join(s['key_files'][:8])}[/dim]\n")
772
+
773
+ # ── read ──────────────────────────────────────────────────────────────
774
+ elif sub == "read":
775
+ if not rest:
776
+ console.print("[yellow]用法: /project read <文件路径>[/yellow]")
777
+ return
778
+ ok, content = ps.read_file(rest)
779
+ if not ok:
780
+ console.print(f"[red]{content}[/red]")
781
+ return
782
+ lang = rest.rsplit(".", 1)[-1] if "." in rest else "text"
783
+ console.print(f"\n[bold]{rest}[/bold]")
784
+ if HAS_RICH:
785
+ from rich.syntax import Syntax
786
+ console.print(Syntax(content, lang, theme="monokai", line_numbers=True,
787
+ word_wrap=False))
788
+ else:
789
+ print(content)
790
+
791
+ # ── clear ─────────────────────────────────────────────────────────────
792
+ elif sub == "clear":
793
+ self.terminal._project_session = None # type: ignore[attr-defined]
794
+ self.terminal._project_ctx_injected = False # type: ignore[attr-defined]
795
+ console.print("[dim]项目上下文已清除[/dim]")
796
+
797
+ # ── ask / task → forward to AI with project context ───────────────────
798
+ elif sub in ("ask", "task"):
799
+ if not rest:
800
+ console.print(f"[yellow]用法: /project {sub} <问题或任务描述>[/yellow]")
801
+ return
802
+ # Delegate to the AI; project context is injected automatically via send_message
803
+ prefix = "请基于当前项目完成以下任务:\n" if sub == "task" else ""
804
+ await self.terminal.send_message(prefix + rest) # type: ignore[attr-defined]
805
+
806
+ # ── help ─────────────────────────────────────────────────────────────
807
+ else:
808
+ rows = [
809
+ ("/project load <path>", "加载项目目录,构建文件索引"),
810
+ ("/project tree [depth]", "显示文件树结构"),
811
+ ("/project grep <pattern>", "跨文件正则搜索"),
812
+ ("/project read <file>", "查看项目中的文件内容"),
813
+ ("/project status", "Git 状态 + 最近提交"),
814
+ ("/project info", "项目摘要(类型/语言/规模)"),
815
+ ("/project ask <question>", "向 AI 提问(使用项目上下文)"),
816
+ ("/project task <description>", "让 AI 执行任务(工具调用模式)"),
817
+ ("/project clear", "卸载当前项目上下文"),
818
+ ]
819
+ tb = _T(box=_box.MINIMAL)
820
+ tb.add_column("命令", style="cyan")
821
+ tb.add_column("说明", style="dim")
822
+ for cmd, desc in rows:
823
+ tb.add_row(cmd, desc)
824
+ console.print(f"\n[bold]/project — 项目分析命令[/bold]\n")
825
+ console.print(tb)
826
+ console.print()
827
+
828
+ async def cmd_init(self, args: str):
829
+ """Bootstrap an ARIA.md memory file, or scaffold a new project.
830
+
831
+ Without arguments: scans the current directory and generates ARIA.md.
832
+ With a template name: creates a fully-runnable project scaffold.
833
+
834
+ Usage:
835
+ /init — generate ARIA.md for current project
836
+ /init --force — regenerate even if ARIA.md already exists
837
+ /init list — list available scaffold templates
838
+ /init quant [dir] — quantitative strategy project
839
+ /init analysis [dir] — data analysis project
840
+ /init fastapi [dir] — FastAPI financial data service
841
+ /init dashboard [dir] — Plotly Dash interactive dashboard
842
+ """
843
+ global _PROJECT_CONTEXT
844
+ cwd = pathlib.Path.cwd()
845
+
846
+ # ── /init <template> — project scaffold ──────────────────────────────
847
+ _tmpl_key = args.strip().lower().split()[0] if args.strip() else ""
848
+ if _tmpl_key == "list":
849
+ rows = [(k, v["desc"]) for k, v in self._SCAFFOLD_TEMPLATES.items()]
850
+ if HAS_RICH:
851
+ from rich.table import Table as _Table
852
+ t = _Table(box=None, show_header=True, header_style="bold cyan", padding=(0,2))
853
+ t.add_column("模板", style="green")
854
+ t.add_column("说明")
855
+ for k, d in rows:
856
+ t.add_row(k, d)
857
+ console.print("\n [bold]可用脚手架模板[/bold]")
858
+ console.print(t)
859
+ console.print("\n 用法: [cyan]/init <模板名> [目录名][/cyan]\n")
860
+ else:
861
+ print("可用模板:"); [print(f" {k}: {d}") for k, d in rows]
862
+ return
863
+
864
+ if _tmpl_key in self._SCAFFOLD_TEMPLATES:
865
+ tmpl = self._SCAFFOLD_TEMPLATES[_tmpl_key]
866
+ # Optional second arg: target directory name
867
+ _args_parts = args.strip().split()
868
+ _target_name = _args_parts[1] if len(_args_parts) > 1 else f"{_tmpl_key}_project"
869
+ _target_path = pathlib.Path(_target_name).expanduser()
870
+ if _target_path.is_absolute():
871
+ target_dir = _target_path
872
+ else:
873
+ from artifacts import user_projects_dir as _user_projects_dir
874
+ target_dir = _user_projects_dir() / _target_name
875
+ if target_dir.exists():
876
+ console.print(f"[yellow]目录已存在: {target_dir}[/yellow]") if HAS_RICH else print(f"目录已存在: {target_dir}")
877
+ else:
878
+ target_dir.mkdir(parents=True)
879
+ created = self._create_scaffold(target_dir, tmpl)
880
+ if HAS_RICH:
881
+ from rich.panel import Panel as _SPanel
882
+ from rich import box as _sbox
883
+ lines = "\n".join(f" [dim]{pathlib.Path(p)}[/dim]" for p in created)
884
+ console.print(_SPanel(
885
+ f"[green]✅ 项目脚手架已创建[/green] [bold]{_target_name}[/bold]\n\n{lines}\n\n"
886
+ f"[dim]cd \"{target_dir}\" && pip install -r requirements.txt[/dim]",
887
+ title=f"[bold cyan]/init {_tmpl_key}[/bold cyan]",
888
+ border_style="cyan",
889
+ box=_sbox.ROUNDED,
890
+ padding=(1, 2),
891
+ ))
892
+ else:
893
+ print(f"✅ 创建: {target_dir}"); [print(f" {p}") for p in created]
894
+ # Optionally generate ARIA.md inside the new project
895
+ try:
896
+ from apps.cli.project_aria import build_project_aria_md
897
+ _aria_tgt = target_dir / "ARIA.md"
898
+ if not _aria_tgt.exists():
899
+ _aria_tgt.write_text(
900
+ build_project_aria_md(
901
+ project_name=_target_name,
902
+ stack=tmpl["desc"],
903
+ entry="main.py",
904
+ purpose=f"由 /init {_tmpl_key} 生成的脚手架项目",
905
+ notes=[
906
+ "脚手架目录已经就绪,后续功能与服务应继续按模块拆分。",
907
+ ],
908
+ ),
909
+ encoding="utf-8",
910
+ )
911
+ except Exception:
912
+ pass
913
+ return
914
+ # ── /init [--force] — generate ARIA.md for current project ──────────
915
+
916
+ aria_md = cwd / "ARIA.md"
917
+ force = "--force" in args
918
+
919
+ if aria_md.exists() and not force:
920
+ msg = f"ARIA.md already exists. Use /init --force to regenerate."
921
+ console.print(f"[yellow]{msg}[/yellow]") if HAS_RICH else print(msg)
922
+ return
923
+
924
+ # Scan for common project signal files
925
+ _SCAN_FILES = [
926
+ "README.md", "README.rst", "README.txt",
927
+ "package.json", "pyproject.toml", "setup.py", "setup.cfg",
928
+ "requirements.txt", "Pipfile", "poetry.lock",
929
+ "Cargo.toml", "go.mod", "pom.xml", "build.gradle",
930
+ "Makefile", "Dockerfile", ".env.example",
931
+ "CLAUDE.md", ".ariarc",
932
+ ]
933
+ snippets, found_files = [], []
934
+ for fname in _SCAN_FILES:
935
+ fp = cwd / fname
936
+ if fp.exists():
937
+ found_files.append(fname)
938
+ try:
939
+ snippets.append(f"### {fname}\n{fp.read_text(errors='replace')[:1200]}")
940
+ except Exception:
941
+ pass
942
+
943
+ code_exts = {".py", ".ts", ".js", ".go", ".rs", ".java", ".cpp", ".c"}
944
+ code_files = sorted(
945
+ f.name for f in cwd.iterdir()
946
+ if f.is_file() and f.suffix in code_exts
947
+ )[:10]
948
+
949
+ scan_summary = "\n\n".join(snippets[:5])
950
+
951
+ prompt = (
952
+ f"分析以下项目信息,生成一个 ARIA.md 记忆文件。\n\n"
953
+ f"目录: {cwd}\n"
954
+ f"发现的配置文件: {', '.join(found_files) or '无'}\n"
955
+ f"代码文件: {', '.join(code_files) or '无'}\n\n"
956
+ f"文件内容:\n{scan_summary}\n\n"
957
+ f"请生成符合以下格式的 ARIA.md(只输出文件内容本身,不加任何解释):\n\n"
958
+ f"# Memory\n\n"
959
+ f"- **Project**: <项目名称>\n"
960
+ f"- **Stack**: <语言/框架>\n"
961
+ f"- **Entry**: <主入口文件>\n"
962
+ f"- **Conventions**: <代码规范或约定>\n"
963
+ f"- **Notes**: <其他重要信息>\n"
964
+ )
965
+
966
+ console.print("[dim]分析项目结构中...[/dim]") if HAS_RICH else print("Analyzing project...")
967
+ await self.terminal.send_message(prompt)
968
+
969
+ # Extract the last assistant response and write to ARIA.md
970
+ if self.terminal.conversation:
971
+ last_ai = next(
972
+ (m["content"] for m in reversed(self.terminal.conversation)
973
+ if m.get("role") == "assistant"),
974
+ None,
975
+ )
976
+ if last_ai:
977
+ content = _strip_markdown_fences(last_ai).strip()
978
+ # Strip injected market-data blocks (lines starting with ## 📊 or *⚠️*)
979
+ # that the market-data prefetch may have appended to the AI response.
980
+ import re as _re_init
981
+ content = _re_init.sub(
982
+ r'\n*## 📊.*?(?=\n#|\Z)', '', content, flags=_re_init.DOTALL
983
+ ).strip()
984
+ content = _re_init.sub(r'\n*\*⚠️.*?\*\n*', '\n', content).strip()
985
+ if not content.startswith("# Memory"):
986
+ content = "# Memory\n\n" + content
987
+ aria_md.write_text(content + "\n", encoding="utf-8")
988
+ _PROJECT_CONTEXT = _load_project_context()
989
+ msg = f"ARIA.md created at {aria_md}"
990
+ console.print(f"\n[green]{msg}[/green]") if HAS_RICH else print(f"\n{msg}")
991
+
992
+ async def cmd_setup(self, args: str):
993
+ """Guided first-run setup wizard (Open Interpreter style).
994
+
995
+ Usage: /setup
996
+ """
997
+ import getpass as _gp
998
+
999
+ _is_interactive = sys.stdin.isatty()
1000
+
1001
+ if HAS_RICH:
1002
+ console.print()
1003
+ console.print("[bold cyan]━━ Aria Setup Wizard ━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]")
1004
+ console.print()
1005
+
1006
+ # ── Step 1: Detect LOCAL backends only (not cloud LLM providers) ───────
1007
+ _LOCAL_BACKENDS_ONLY = {"ollama", "lmstudio", "vllm", "llamacpp", "jan"}
1008
+ try:
1009
+ from local_llm_provider import probe_all_backends, BACKEND_DEFAULTS
1010
+ _all_backends = probe_all_backends()
1011
+ # Filter to only true local backends — cloud providers appear in Step 3
1012
+ backends = {k: v for k, v in _all_backends.items() if k in _LOCAL_BACKENDS_ONLY}
1013
+ except ImportError:
1014
+ backends = {}
1015
+
1016
+ console.print(" [bold]Step 1/4 · 本地 Backend[/bold]") if HAS_RICH else print("Step 1: Local Backends")
1017
+ ollama_online = backends.get("ollama", False)
1018
+ for name, ok in backends.items():
1019
+ icon = "✅" if ok else "○"
1020
+ color = "green" if ok else "dim"
1021
+ url = BACKEND_DEFAULTS.get(name, {}).get("default_url", "") if "BACKEND_DEFAULTS" in dir() else ""
1022
+ if HAS_RICH:
1023
+ console.print(f" {icon} [{color}]{name:12s}[/{color}] [dim]{url}[/dim]")
1024
+ else:
1025
+ print(f" {'✓' if ok else '✗'} {name:12s} {url}")
1026
+ console.print() if HAS_RICH else print()
1027
+
1028
+ # ── Step 2: Pick default Ollama model (if Ollama online) ────────────
1029
+ if ollama_online and _is_interactive:
1030
+ console.print(" [bold]Step 2/4 · 选择默认本地模型[/bold]") if HAS_RICH else print("Step 2: Default model")
1031
+ rich_models, _ = detect_ollama_models_rich(
1032
+ self.terminal.config.get("ollama_url", "http://localhost:11434")
1033
+ )
1034
+ if rich_models:
1035
+ model_names = [m["name"] for m in rich_models]
1036
+ current_id = self.terminal.config.get("model", "")
1037
+ sel_idx = next((i for i, n in enumerate(model_names) if n == current_id), 0)
1038
+ options = [(f" {n}", "") for n in model_names]
1039
+ picked = _arrow_select(options, sel_idx, "选择默认模型")
1040
+ if picked is not None:
1041
+ chosen = model_names[picked]
1042
+ self.terminal.config["model"] = chosen
1043
+ save_config(self.terminal.config)
1044
+ msg = f"✓ 默认模型设为 {chosen}"
1045
+ console.print(f" [green]{msg}[/green]") if HAS_RICH else print(f" {msg}")
1046
+ console.print() if HAS_RICH else print()
1047
+ else:
1048
+ console.print(" [dim]Step 2/4 · (Ollama 未运行,跳过模型选择)[/dim]") if HAS_RICH else print(" Skipping model select (Ollama offline)")
1049
+ console.print() if HAS_RICH else print()
1050
+
1051
+ # ── Step 3: Cloud API keys ───────────────────────────────────────────
1052
+ console.print(" [bold]Step 3/4 · Cloud API Key 配置[/bold]") if HAS_RICH else print("Step 3: Cloud API Keys")
1053
+ _SETUP_PROVIDERS = [
1054
+ ("deepseek", "DeepSeek", "推荐:deepseek-chat,性价比最高"),
1055
+ ("openai", "OpenAI", "GPT-4o,o1等"),
1056
+ ("groq", "Groq", "免费 llama/mixtral 推理,极速"),
1057
+ ("anthropic", "Anthropic", "Claude 3.5/3.7"),
1058
+ ]
1059
+ for prov, label, desc in _SETUP_PROVIDERS:
1060
+ existing_key = _get_provider_key(prov)
1061
+ if existing_key:
1062
+ masked = existing_key[:6] + "****" + existing_key[-4:]
1063
+ console.print(f" 🔑 {label:12s} [dim]已配置 ({masked})[/dim]") if HAS_RICH else print(f" {label}: 已配置")
1064
+ continue
1065
+ if _is_interactive:
1066
+ console.print(f" [cyan]{label}[/cyan] [dim]({desc})[/dim]") if HAS_RICH else print(f" {label}: {desc}")
1067
+ try:
1068
+ key = _gp.getpass(f" Enter {label} API key (留空跳过): ").strip()
1069
+ except Exception:
1070
+ key = ""
1071
+ if key:
1072
+ self.cmd_apikey(f"set {prov} {key}")
1073
+ else:
1074
+ console.print(f" ○ {label:12s} [dim]未配置 → /apikey set {prov} <key>[/dim]") if HAS_RICH else print(f" {label}: not configured")
1075
+ console.print() if HAS_RICH else print()
1076
+
1077
+ # ── Step 3.5: Data Service API keys ──────────────────────────────────
1078
+ console.print(" [bold]Step 3.5/4 · 市场数据服务 Key(后端离线时使用)[/bold]") if HAS_RICH else print("Step 3.5: Data Service Keys")
1079
+ _SETUP_DATA = [
1080
+ ("finnhub", "Finnhub", "股票实时行情+新闻", "https://finnhub.io/register"),
1081
+ ("newsapi", "NewsAPI", "财经新闻聚合", "https://newsapi.org/register"),
1082
+ ("brave", "Brave Search", "网页搜索", "https://api.search.brave.com/app/keys"),
1083
+ ("alphavantage", "Alpha Vantage", "股票历史数据", "https://www.alphavantage.co/support/#api-key"),
1084
+ ]
1085
+ _existing_data = _load_data_keys()
1086
+ for svc, label, desc, signup_url in _SETUP_DATA:
1087
+ existing_key = _existing_data.get(svc, "")
1088
+ if existing_key:
1089
+ masked = existing_key[:6] + "****" + existing_key[-4:]
1090
+ console.print(f" 🔑 {label:16s} [dim]已配置 ({masked})[/dim]") if HAS_RICH else print(f" {label}: configured")
1091
+ continue
1092
+ if _is_interactive:
1093
+ console.print(f" [cyan]{label}[/cyan] [dim]({desc})[/dim]") if HAS_RICH else print(f" {label}: {desc}")
1094
+ console.print(f" [dim]注册:{signup_url}[/dim]") if HAS_RICH else print(f" Register: {signup_url}")
1095
+ try:
1096
+ key = _gp.getpass(f" Enter {label} API key (留空跳过): ").strip()
1097
+ except Exception:
1098
+ key = ""
1099
+ if key:
1100
+ self.cmd_apikey(f"set {svc} {key}")
1101
+ else:
1102
+ if HAS_RICH:
1103
+ console.print(f" ○ {label:16s} [dim]未配置 → /apikey set {svc} <key>[/dim]")
1104
+ console.print(f" [dim]注册:{signup_url}[/dim]")
1105
+ else:
1106
+ print(f" {label}: not configured → /apikey set {svc} <key>")
1107
+ console.print() if HAS_RICH else print()
1108
+
1109
+ # ── Step 3.8: MCP servers ────────────────────────────────────────────
1110
+ sub = args.strip().lower()
1111
+ if sub in ("mcp", "all"):
1112
+ console.print(" [bold]Step 3.8/4 · MCP 服务器[/bold]") if HAS_RICH else print("Step 3.8: MCP Servers")
1113
+ _mcp_cfg_path = Path.home() / ".arthera" / "mcp_servers.json"
1114
+ if _mcp_cfg_path.exists():
1115
+ try:
1116
+ import json as _j2
1117
+ _mcp_data = _j2.loads(_mcp_cfg_path.read_text())
1118
+ _servers = _mcp_data.get("servers", [])
1119
+ enabled_srv = [s for s in _servers if s.get("enabled", False)]
1120
+ disabled_srv = [s for s in _servers if not s.get("enabled", True)]
1121
+ for s in enabled_srv:
1122
+ console.print(f" ✅ {s['name']:16s} [dim]{s.get('description','')[:50]}[/dim]") if HAS_RICH else print(f" ✓ {s['name']}")
1123
+ for s in disabled_srv:
1124
+ note = s.get("_setup", "")
1125
+ console.print(f" ○ {s['name']:16s} [dim]{s.get('description','')[:50]}[/dim]") if HAS_RICH else print(f" ✗ {s['name']}")
1126
+ if note:
1127
+ console.print(f" [dim]安装: {note}[/dim]") if HAS_RICH else print(f" Setup: {note}")
1128
+ if disabled_srv:
1129
+ console.print() if HAS_RICH else print()
1130
+ console.print(" [dim]安装后编辑 ~/.arthera/mcp_servers.json 将对应项 enabled 改为 true[/dim]") if HAS_RICH else print(" Edit mcp_servers.json: set enabled=true after installing")
1131
+ except Exception:
1132
+ pass
1133
+ console.print() if HAS_RICH else print()
1134
+
1135
+ # ── Step 4: Messaging channels (Feishu / Telegram) ──────────────────
1136
+ if sub in ("feishu", "telegram", "notify", "all", ""):
1137
+ console.print(" [bold]Step 4/5 · 消息通知连接[/bold]") if HAS_RICH else print("Step 4: Messaging")
1138
+ _env_path = Path.home() / ".aria" / ".env"
1139
+ _env_vars: dict = {}
1140
+ if _env_path.exists():
1141
+ for _line in _env_path.read_text().splitlines():
1142
+ if "=" in _line and not _line.startswith("#"):
1143
+ k, _, v = _line.partition("=")
1144
+ _env_vars[k.strip()] = v.strip()
1145
+
1146
+ # Feishu status
1147
+ _fs_mode = _env_vars.get("ARIA_RELAY_MODE", "")
1148
+ _fs_id = _env_vars.get("ARIA_RELAY_CLIENT_ID", "")
1149
+ _fs_app = _env_vars.get("FEISHU_APP_ID", "")
1150
+ if _fs_mode == "relay" and _fs_id:
1151
+ _fs_status = f"[green]✓ 中继模式[/green] ID: {_fs_id[:12]}…"
1152
+ elif _fs_mode == "own_app" and _fs_app:
1153
+ _fs_status = f"[green]✓ 自建应用[/green] {_fs_app}"
1154
+ else:
1155
+ _fs_status = "[dim]未配置[/dim] → /setup feishu"
1156
+
1157
+ # Telegram status
1158
+ _tg_token = _env_vars.get("TELEGRAM_BOT_TOKEN", "")
1159
+ _tg_ids = _env_vars.get("TELEGRAM_ALLOWED_IDS", "")
1160
+ if _tg_token and _tg_token != "your_bot_token_here":
1161
+ _tg_status = f"[green]✓ 已配置[/green] Chat IDs: {_tg_ids or '(未设置)'}"
1162
+ else:
1163
+ _tg_status = "[dim]未配置[/dim] → /setup telegram"
1164
+
1165
+ if HAS_RICH:
1166
+ console.print(f" 飞书 {_fs_status}")
1167
+ console.print(f" Telegram {_tg_status}")
1168
+ else:
1169
+ print(f" Feishu: {_fs_mode or 'not configured'}")
1170
+ print(f" Telegram: {'configured' if _tg_token else 'not configured'}")
1171
+
1172
+ # Sub-command: launch wizard for just this channel
1173
+ if sub in ("feishu", "telegram"):
1174
+ console.print() if HAS_RICH else print()
1175
+ try:
1176
+ import importlib.util as _ilu
1177
+ _wiz_path = Path(__file__).parent.parent.parent.parent / "setup_wizard.py"
1178
+ _spec = _ilu.spec_from_file_location("_aria_setup_wizard", str(_wiz_path))
1179
+ _wiz = _ilu.module_from_spec(_spec)
1180
+ _spec.loader.exec_module(_wiz)
1181
+ _e = _wiz._load_env()
1182
+ if sub == "feishu":
1183
+ _wiz.setup_feishu(_e)
1184
+ else:
1185
+ _wiz.setup_telegram(_e)
1186
+ _wiz._save_env(_e)
1187
+ except Exception as _we:
1188
+ _fallback_flag = "--feishu" if sub == "feishu" else "--telegram"
1189
+ if HAS_RICH:
1190
+ console.print(f" [yellow]请运行: python3 setup_wizard.py {_fallback_flag}[/yellow]")
1191
+ else:
1192
+ print(f" Run: python3 setup_wizard.py {_fallback_flag}")
1193
+ return
1194
+
1195
+ console.print() if HAS_RICH else print()
1196
+
1197
+ # ── Step 5: Summary ─────────────────────────────────────────────────
1198
+ console.print(" [bold]Step 5/5 · 配置完成[/bold]") if HAS_RICH else print("Step 5: Done")
1199
+ model = self.terminal.config.get("model", "?")
1200
+ provider = self.terminal.config.get("local_provider", "ollama")
1201
+ console.print(f" 模型: [cyan]{model}[/cyan] Provider: [cyan]{provider}[/cyan]") if HAS_RICH else print(f" Model: {model} Provider: {provider}")
1202
+ console.print() if HAS_RICH else print()
1203
+ console.print(
1204
+ " [dim]提示: /model — 切换模型 /providers — 查看所有 provider\n"
1205
+ " /setup feishu — 配置飞书 /setup telegram — 配置 Telegram[/dim]"
1206
+ ) if HAS_RICH else print(" Tip: /model /providers /setup feishu /setup telegram")
1207
+ console.print("[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]") if HAS_RICH else print("─" * 50)
1208
+ console.print() if HAS_RICH else print()
1209
+
1210
+ def cmd_memory(self, args: str):
1211
+ """Manage persistent memory: project ARIA.md and global user profile.
1212
+
1213
+ Usage:
1214
+ /memory show — display current project ARIA.md
1215
+ /memory add <fact> — append fact to project ARIA.md
1216
+ /memory clear — wipe project ARIA.md memory section
1217
+ /memory search <query> — search across ARIA.md + sessions
1218
+ /memory profile — show global ~/.arthera/ARIA.md (injected every session)
1219
+ /memory profile add <text> — append to global profile
1220
+ /memory profile clear — reset global profile
1221
+ /memory global — legacy global Memory entries
1222
+ """
1223
+ global _PROJECT_CONTEXT
1224
+ aria_md = pathlib.Path.cwd() / "ARIA.md"
1225
+ parts = args.strip().split(maxsplit=1)
1226
+ sub = parts[0].lower() if parts else "show"
1227
+ rest = parts[1].strip() if len(parts) > 1 else ""
1228
+
1229
+ if sub == "show":
1230
+ if not aria_md.exists():
1231
+ msg = f"No ARIA.md in {pathlib.Path.cwd()}"
1232
+ console.print(f"[dim]{msg}[/dim]") if HAS_RICH else print(msg)
1233
+ return
1234
+ content = aria_md.read_text(encoding="utf-8")
1235
+ if HAS_RICH:
1236
+ try:
1237
+ from rich.markdown import Markdown as _RMd
1238
+ console.print(_RMd(content))
1239
+ except Exception:
1240
+ console.print(content)
1241
+ else:
1242
+ print(content)
1243
+
1244
+ elif sub == "add":
1245
+ if not rest:
1246
+ console.print("[dim]Usage: /memory add <fact>[/dim]") if HAS_RICH else print("Usage: /memory add <fact>")
1247
+ return
1248
+ self.cmd_note(rest)
1249
+
1250
+ elif sub == "clear":
1251
+ if aria_md.exists():
1252
+ aria_md.write_text("# Memory\n\n", encoding="utf-8")
1253
+ _PROJECT_CONTEXT = _load_project_context()
1254
+ console.print("[dim]Memory cleared.[/dim]") if HAS_RICH else print("Memory cleared.")
1255
+ else:
1256
+ console.print("[dim]Nothing to clear.[/dim]") if HAS_RICH else print("Nothing to clear.")
1257
+
1258
+ elif sub == "search":
1259
+ # Semantic search in ARIA.md and strategy vault using simple grep
1260
+ # (ChromaDB RAG upgrade planned for Phase 2)
1261
+ if not rest:
1262
+ console.print("[dim]Usage: /memory search <query>[/dim]") if HAS_RICH else print("Usage: /memory search <query>")
1263
+ return
1264
+ query_low = rest.lower()
1265
+ results = []
1266
+ # 1. Search ARIA.md
1267
+ if aria_md.exists():
1268
+ for line in aria_md.read_text(encoding="utf-8").splitlines():
1269
+ if query_low in line.lower() and line.strip():
1270
+ results.append(("ARIA.md", line.strip()))
1271
+ # 2. Search session history titles
1272
+ for sess_file in sorted(SESSIONS_DIR.glob("*.json"), key=lambda p: -p.stat().st_mtime)[:20]:
1273
+ try:
1274
+ sess = json.loads(sess_file.read_text(encoding="utf-8"))
1275
+ title = sess.get("metadata", {}).get("title", "")
1276
+ if query_low in title.lower():
1277
+ results.append(("Session", title[:80]))
1278
+ except Exception:
1279
+ pass
1280
+ # 3. Search strategy vault
1281
+ try:
1282
+ from strategy_vault import get_vault as _gv
1283
+ vault = _gv()
1284
+ for s in (vault.list() or []):
1285
+ name = str(s.get("name", ""))
1286
+ msg = str(s.get("message", ""))
1287
+ if query_low in name.lower() or query_low in msg.lower():
1288
+ results.append(("Strategy", f"{name}: {msg[:60]}"))
1289
+ except Exception:
1290
+ pass
1291
+
1292
+ if results:
1293
+ if HAS_RICH:
1294
+ console.print()
1295
+ console.print(f" [bold]记忆搜索: '{rest}'[/bold] [dim]{len(results)} 条结果[/dim]")
1296
+ console.print()
1297
+ for src, text in results[:15]:
1298
+ console.print(f" [dim]{src:<12s}[/dim] {text}")
1299
+ console.print()
1300
+ else:
1301
+ print(f" Search '{rest}': {len(results)} results")
1302
+ for src, text in results[:15]:
1303
+ print(f" [{src}] {text}")
1304
+ else:
1305
+ msg = f"未找到与 '{rest}' 相关的记忆"
1306
+ console.print(f"[dim]{msg}[/dim]") if HAS_RICH else print(msg)
1307
+
1308
+ elif sub == "profile":
1309
+ # Per-user ARIA.md at ~/.arthera/ARIA.md — injected into every session
1310
+ _profile_path = pathlib.Path.home() / ".arthera" / "ARIA.md"
1311
+ gparts = rest.strip().split(maxsplit=1)
1312
+ gsub = gparts[0].lower() if gparts else "show"
1313
+ grest = gparts[1].strip() if len(gparts) > 1 else ""
1314
+
1315
+ if gsub == "show":
1316
+ if not _profile_path.exists():
1317
+ if HAS_RICH:
1318
+ console.print(f"[dim]~/.arthera/ARIA.md 还不存在。用 /memory profile add <内容> 创建。[/dim]")
1319
+ else:
1320
+ print("~/.arthera/ARIA.md not found. Use /memory profile add <text> to create.")
1321
+ return
1322
+ content = _profile_path.read_text(encoding="utf-8")
1323
+ if HAS_RICH:
1324
+ try:
1325
+ from rich.markdown import Markdown as _RMd3
1326
+ console.print()
1327
+ console.print(f" [dim]~/.arthera/ARIA.md[/dim]")
1328
+ console.print(_RMd3(content))
1329
+ except Exception:
1330
+ console.print(content)
1331
+ else:
1332
+ print(content)
1333
+
1334
+ elif gsub == "add":
1335
+ if not grest:
1336
+ console.print("[dim]Usage: /memory profile add <内容>[/dim]") if HAS_RICH else print("Usage: /memory profile add <text>")
1337
+ return
1338
+ _profile_path.parent.mkdir(parents=True, exist_ok=True)
1339
+ now_str = datetime.now().strftime("%Y-%m-%d")
1340
+ if _profile_path.exists():
1341
+ existing = _profile_path.read_text(encoding="utf-8")
1342
+ if "## 偏好与背景" not in existing and "## Preferences" not in existing:
1343
+ existing += "\n\n## 偏好与背景\n"
1344
+ existing += f"\n- [{now_str}] {grest}"
1345
+ _profile_path.write_text(existing, encoding="utf-8")
1346
+ else:
1347
+ _profile_path.write_text(
1348
+ f"# 用户背景\n\n## 偏好与背景\n\n- [{now_str}] {grest}\n",
1349
+ encoding="utf-8",
1350
+ )
1351
+ # Refresh project context so change takes effect immediately
1352
+ _PROJECT_CONTEXT = _load_project_context()
1353
+ if HAS_RICH:
1354
+ console.print(f" [dim]✓ 已写入 ~/.arthera/ARIA.md — 下次对话自动注入[/dim]")
1355
+ else:
1356
+ print(f"Saved to ~/.arthera/ARIA.md")
1357
+
1358
+ elif gsub == "clear":
1359
+ if _profile_path.exists():
1360
+ _profile_path.write_text("# 用户背景\n\n", encoding="utf-8")
1361
+ _PROJECT_CONTEXT = _load_project_context()
1362
+ console.print("[dim]~/.arthera/ARIA.md 已清空。[/dim]") if HAS_RICH else print("Profile cleared.")
1363
+ else:
1364
+ console.print("[dim]文件不存在,无需清空。[/dim]") if HAS_RICH else print("Nothing to clear.")
1365
+
1366
+ else:
1367
+ if HAS_RICH:
1368
+ console.print("[dim]Usage: /memory profile [show|add <内容>|clear][/dim]")
1369
+ else:
1370
+ print("Usage: /memory profile [show|add <text>|clear]")
1371
+
1372
+ elif sub == "global":
1373
+ # Global user memory (cross-project, cross-session)
1374
+ if not self.memory_mgr:
1375
+ console.print("[dim]Memory manager not available.[/dim]") if HAS_RICH else print("Memory manager not available.")
1376
+ return
1377
+ gparts = rest.strip().split(maxsplit=1)
1378
+ gsub = gparts[0].lower() if gparts else "show"
1379
+ grest = gparts[1].strip() if len(gparts) > 1 else ""
1380
+
1381
+ if gsub == "show":
1382
+ entries = self.memory_mgr.list_all()
1383
+ if not entries:
1384
+ console.print("[dim]全局 Memory 为空。用 /memory global add <内容> 添加。[/dim]") if HAS_RICH else print("Global memory is empty.")
1385
+ return
1386
+ if HAS_RICH:
1387
+ from rich.markdown import Markdown as _RMd2
1388
+ for e in entries:
1389
+ console.print(f"\n[bold cyan]{e['title']}[/bold cyan] [dim]{e['file']}[/dim]")
1390
+ console.print(_RMd2(e["content"]) if e["content"] else "[dim](empty)[/dim]")
1391
+ else:
1392
+ for e in entries:
1393
+ print(f"\n## {e['title']}\n{e['content']}")
1394
+
1395
+ elif gsub == "add":
1396
+ if not grest:
1397
+ console.print("[dim]Usage: /memory global add <内容>[/dim]") if HAS_RICH else print("Usage: /memory global add <content>")
1398
+ return
1399
+ self.memory_mgr.append("user_profile", grest, title="User Profile")
1400
+ console.print(f"[dim]已写入全局 Memory: {grest[:60]}[/dim]") if HAS_RICH else print(f"Saved: {grest[:60]}")
1401
+
1402
+ elif gsub == "clear":
1403
+ n = self.memory_mgr.clear_all()
1404
+ console.print(f"[dim]全局 Memory 已清空(删除 {n} 个文件)。[/dim]") if HAS_RICH else print(f"Global memory cleared ({n} files).")
1405
+
1406
+ else:
1407
+ console.print("[dim]Usage: /memory global [show|add <内容>|clear][/dim]") if HAS_RICH else print("Usage: /memory global [show|add|clear]")
1408
+
1409
+ else:
1410
+ if HAS_RICH:
1411
+ console.print("[dim]Usage: /memory [show|add <fact>|clear|search <query>|profile|global][/dim]")
1412
+ console.print("[dim] /memory profile add <内容> — 写入全局用户背景(每次会话自动注入)[/dim]")
1413
+ else:
1414
+ print("Usage: /memory [show|add <fact>|clear|search <query>|profile|global]")