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,696 @@
1
+ """DiagnosticOpsCommandsMixin — bug, accuracy, cost, todo, doctor, datasource commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import pathlib
8
+
9
+ # Status glyph + colour per architecture-layer status.
10
+ _ARCH_ICON = {
11
+ "done": ("✓", "#3fb950"),
12
+ "partial": ("◐", "#d29922"),
13
+ "planned": ("○", "dim"),
14
+ "blocked": ("✗", "#f85149"),
15
+ }
16
+
17
+
18
+ def format_architecture_report(layers, counts, *, gaps_only: bool = False,
19
+ rich: bool = True) -> list:
20
+ """Render the architecture contract as display lines (pure / testable).
21
+
22
+ ``layers`` are ArchitectureLayer objects; ``counts`` is the status→n map.
23
+ With ``gaps_only`` the DONE layers are dropped so only work-to-do shows.
24
+ """
25
+ total = sum(counts.values()) or len(layers)
26
+ done = counts.get("done", 0)
27
+ lines = []
28
+ if rich:
29
+ lines.append(f"[bold]架构契约[/bold] [dim]{done}/{total} 层完成[/dim]")
30
+ lines.append(" " + " ".join(
31
+ f"[{_ARCH_ICON[s][1]}]{_ARCH_ICON[s][0]}[/{_ARCH_ICON[s][1]}] {s} {counts.get(s, 0)}"
32
+ for s in ("done", "partial", "planned", "blocked")
33
+ ))
34
+ else:
35
+ lines.append(f"架构契约 {done}/{total} 层完成")
36
+ lines.append("")
37
+ for layer in layers:
38
+ st = getattr(layer.status, "value", str(layer.status))
39
+ if gaps_only and st == "done":
40
+ continue
41
+ icon, color = _ARCH_ICON.get(st, ("•", "dim"))
42
+ if rich:
43
+ lines.append(f"[{color}]{icon}[/{color}] [bold]{layer.name}[/bold] "
44
+ f"[dim]{layer.responsibility}[/dim]")
45
+ else:
46
+ lines.append(f"{icon} {layer.name} {layer.responsibility}")
47
+ if st != "done":
48
+ for ns in (layer.next_steps or [])[:2]:
49
+ lines.append(f" → {ns}")
50
+ for bl in (layer.blockers or [])[:1]:
51
+ lines.append(f" [#f85149]⚠ {bl}[/#f85149]" if rich else f" ⚠ {bl}")
52
+ return lines
53
+
54
+
55
+ class DiagnosticOpsCommandsMixin:
56
+ """Mixin: diagnostics, feedback, usage, and source inspection commands."""
57
+
58
+ def cmd_architecture(self, args: str):
59
+ """显示分层架构契约(各层状态 + 每层下一步)。用法: /architecture [--gaps]"""
60
+ try:
61
+ from packages.aria_core import (
62
+ list_architecture_layers, architecture_status_counts)
63
+ except Exception as exc: # pragma: no cover - import guard
64
+ msg = f"架构契约不可用: {exc}"
65
+ console.print(f"[red]{msg}[/red]") if HAS_RICH else print(msg)
66
+ return
67
+ gaps_only = "gap" in args.lower()
68
+ lines = format_architecture_report(
69
+ list_architecture_layers(), architecture_status_counts(),
70
+ gaps_only=gaps_only, rich=HAS_RICH)
71
+ if HAS_RICH:
72
+ console.print(Panel("\n".join(lines), title="[bold]Aria 架构[/bold]",
73
+ border_style="#C08050", box=rich_box.ROUNDED,
74
+ padding=(0, 1)))
75
+ else:
76
+ print("\n".join(lines))
77
+
78
+ def cmd_bug(self, args: str):
79
+ desc = args.strip()
80
+ if not desc:
81
+ console.print("[dim]用法: /bug <描述你遇到的问题>[/dim]" if HAS_RICH
82
+ else "Usage: /bug <description>")
83
+ return
84
+ ctx_parts = []
85
+ for m in self.terminal.conversation[-6:]:
86
+ _c = (m.get("content", "") or "")[:300]
87
+ ctx_parts.append(f"{m.get('role','')}: {_c}")
88
+ ctx = "\n".join(ctx_parts)
89
+ import platform as _pf
90
+ env = (f"v{__version__} · {_pf.system()} · py{_pf.python_version()} · "
91
+ f"model={self.terminal.config.get('model','')}")
92
+ self.terminal._record_feedback("bug", ctx, comment=f"{desc}\n\n[env] {env}")
93
+ gh = "https://github.com/artherahq/aria-code/issues"
94
+ if HAS_RICH:
95
+ console.print(" [#C08050]✓ 已记录问题(本地)[/#C08050]")
96
+ console.print(f" [dim]上传需 /privacy opt-in · 或直接提 issue: {gh}[/dim]")
97
+ else:
98
+ print(f" ✓ Bug recorded locally. Upload via /privacy opt-in, or file: {gh}")
99
+
100
+ def cmd_accuracy(self, args: str):
101
+ res = self.terminal._verify_predictions(min_age_hours=24.0)
102
+ try:
103
+ from apps.cli.prediction_feedback import PredictionTracker
104
+ acc = PredictionTracker(CONFIG_DIR).accuracy()
105
+ except Exception:
106
+ acc = {}
107
+ if HAS_RICH:
108
+ console.print()
109
+ console.print(" [bold]预测战绩[/bold] [dim]LLM 方向判断 vs 实际行情[/dim]")
110
+ if res.get("settled"):
111
+ console.print(f" [dim]本次结算 {res['settled']} 笔:"
112
+ f"命中 [green]{res['correct']}[/green] / "
113
+ f"落空 [red]{res['wrong']}[/red][/dim]")
114
+ _acc = acc.get("accuracy")
115
+ _acc_str = f"{_acc:.0%}" if _acc is not None else "—"
116
+ console.print(
117
+ f" 累计:已结算 [bold]{acc.get('settled',0)}[/bold] · "
118
+ f"命中率 [#C08050]{_acc_str}[/#C08050] · "
119
+ f"待结算 [dim]{acc.get('pending',0)}[/dim]"
120
+ )
121
+ if not acc.get("total"):
122
+ console.print(" [dim]暂无记录 — 用 /team 或 /analyze 让 AI 给出方向判断后会自动追踪[/dim]")
123
+ else:
124
+ print(f" 预测战绩: 结算{res.get('settled',0)} 命中率"
125
+ f"{acc.get('accuracy')} 待结算{acc.get('pending',0)}")
126
+
127
+ def cmd_cost(self, args: str):
128
+ import time as _t
129
+ elapsed = _t.time() - self.terminal._session_start
130
+ inp = self.terminal._session_input_tokens
131
+ out = self.terminal._session_output_tokens
132
+ think = self.terminal._session_thinking_tokens
133
+ turns = self.terminal._session_turns
134
+ total = inp + out + think
135
+
136
+ is_local = self.terminal._last_provider in ("ollama", "ollama_cache", "local")
137
+ cost_usd = 0.0
138
+ if not is_local:
139
+ cost_usd = (inp * 0.14 + out * 0.28 + think * 1.10) / 1_000_000
140
+
141
+ hh = int(elapsed // 3600)
142
+ mm = int((elapsed % 3600) // 60)
143
+ ss = int(elapsed % 60)
144
+ duration = f"{hh}h {mm:02d}m {ss:02d}s" if hh else f"{mm}m {ss:02d}s"
145
+
146
+ if HAS_RICH:
147
+ console.print()
148
+ console.print("[bold]Session Usage[/bold]")
149
+ console.print()
150
+ console.print(f" [dim]{'Duration':<22}[/dim]{duration}")
151
+ console.print(f" [dim]{'Turns':<22}[/dim]{turns}")
152
+ console.print(f" [dim]{'Input tokens':<22}[/dim]{inp:,}")
153
+ console.print(f" [dim]{'Output tokens':<22}[/dim]{out:,}")
154
+ if think:
155
+ console.print(f" [dim]{'Thinking tokens':<22}[/dim]{think:,}")
156
+ console.print(f" [dim]{'Total tokens':<22}[/dim][bold]{total:,}[/bold]")
157
+ if is_local:
158
+ console.print(f" [dim]{'Est. cost':<22}[/dim][green]$0.00 (local)[/green]")
159
+ elif total > 0:
160
+ console.print(f" [dim]{'Est. cost':<22}[/dim]${cost_usd:.4f} USD")
161
+ console.print(f" [dim]{'Provider':<22}[/dim]{self.terminal._last_provider}")
162
+ console.print()
163
+ else:
164
+ print(f" Session: {duration} Turns: {turns}")
165
+ print(f" Tokens: {inp:,} in / {out:,} out / {total:,} total")
166
+ if not is_local and total > 0:
167
+ print(f" Est. cost: ${cost_usd:.4f}")
168
+
169
+ def cmd_todo(self, args: str):
170
+ import json as _json
171
+ todo_file = CONFIG_DIR / "todos.json"
172
+
173
+ def _load():
174
+ try:
175
+ if todo_file.exists():
176
+ return _json.loads(todo_file.read_text(encoding="utf-8"))
177
+ except Exception:
178
+ pass
179
+ return []
180
+
181
+ def _save(tasks):
182
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
183
+ todo_file.write_text(_json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8")
184
+
185
+ parts = args.strip().split(maxsplit=1)
186
+ sub = parts[0].lower() if parts else "list"
187
+ rest = parts[1].strip() if len(parts) > 1 else ""
188
+ tasks = _load()
189
+
190
+ if sub in ("", "list", "ls"):
191
+ if not tasks:
192
+ console.print("[dim]No tasks. Add with: /todo add <task>[/dim]" if HAS_RICH else "No tasks")
193
+ return
194
+ if HAS_RICH:
195
+ console.print()
196
+ for i, t in enumerate(tasks):
197
+ status_icon = "[green]✓[/green]" if t.get("done") else "[yellow]○[/yellow]"
198
+ style = "dim" if t.get("done") else ""
199
+ text = t.get("text", "")
200
+ console.print(f" {status_icon} [dim]{i}[/dim] [{style}]{text}[/{style}]" if style
201
+ else f" {status_icon} [dim]{i}[/dim] {text}")
202
+ pending = sum(1 for t in tasks if not t.get("done"))
203
+ console.print(f"\n [dim]{pending}/{len(tasks)} pending[/dim]")
204
+ console.print()
205
+ else:
206
+ for i, t in enumerate(tasks):
207
+ mark = "✓" if t.get("done") else "○"
208
+ print(f" {mark} {i} {t.get('text', '')}")
209
+ elif sub == "add":
210
+ if not rest:
211
+ console.print("[dim]Usage: /todo add <task text>[/dim]" if HAS_RICH else "Usage: /todo add <task>")
212
+ return
213
+ task = {"text": rest, "done": False, "id": len(tasks)}
214
+ tasks.append(task)
215
+ _save(tasks)
216
+ console.print(f" [dim]✓ Added: {rest}[/dim]" if HAS_RICH else f"Added: {rest}")
217
+ elif sub in ("done", "check", "complete"):
218
+ try:
219
+ idx = int(rest)
220
+ tasks[idx]["done"] = True
221
+ _save(tasks)
222
+ console.print(f" [dim]✓ Done: {tasks[idx]['text']}[/dim]" if HAS_RICH
223
+ else f"Done: {tasks[idx]['text']}")
224
+ except (ValueError, IndexError):
225
+ console.print("[dim]Usage: /todo done <id>[/dim]" if HAS_RICH else "Usage: /todo done <id>")
226
+ elif sub in ("remove", "rm", "delete", "del"):
227
+ try:
228
+ idx = int(rest)
229
+ removed = tasks.pop(idx)
230
+ _save(tasks)
231
+ console.print(f" [dim]Removed: {removed['text']}[/dim]" if HAS_RICH
232
+ else f"Removed: {removed['text']}")
233
+ except (ValueError, IndexError):
234
+ console.print("[dim]Usage: /todo remove <id>[/dim]" if HAS_RICH else "bad index")
235
+ elif sub == "clear":
236
+ _save([])
237
+ console.print("[dim]All tasks cleared[/dim]" if HAS_RICH else "Cleared")
238
+ else:
239
+ full_text = (sub + " " + rest).strip()
240
+ task = {"text": full_text, "done": False, "id": len(tasks)}
241
+ tasks.append(task)
242
+ _save(tasks)
243
+ console.print(f" [dim]✓ Added: {full_text}[/dim]" if HAS_RICH else f"Added: {full_text}")
244
+
245
+ def cmd_doctor(self, args: str):
246
+ try:
247
+ from doctor import run_doctor
248
+
249
+ report = run_doctor(
250
+ self.terminal.config,
251
+ check_network="--network" in (args or "").split(),
252
+ )
253
+ if HAS_RICH:
254
+ from rich.table import Table as _DoctorTable
255
+ table = _DoctorTable(title="Aria Code doctor", box=rich_box.ROUNDED)
256
+ table.add_column("Status", width=8)
257
+ table.add_column("Check", style="bold")
258
+ table.add_column("Detail", style="dim")
259
+ table.add_column("Suggestion", style="dim")
260
+ icons = {"ok": "[green]OK[/green]", "warn": "[yellow]WARN[/yellow]", "err": "[red]ERR[/red]"}
261
+ for check in report.checks:
262
+ table.add_row(
263
+ icons.get(check.status, check.status.upper()),
264
+ check.name,
265
+ check.detail,
266
+ check.suggestion,
267
+ )
268
+ console.print()
269
+ console.print(table)
270
+ color = "green" if report.errors == 0 and report.warnings == 0 else ("yellow" if report.errors == 0 else "red")
271
+ console.print(f"[{color}]{report.passed} passed · {report.warnings} warnings · {report.errors} errors[/{color}]")
272
+ console.print()
273
+ else:
274
+ from doctor import format_doctor_plain
275
+ print(format_doctor_plain(report))
276
+ return
277
+ except Exception as exc:
278
+ console.print(f"[yellow]doctor module unavailable, using legacy checks: {exc}[/yellow]" if HAS_RICH else f"doctor module unavailable: {exc}")
279
+
280
+ import importlib as _il, subprocess as _sp, shutil as _sh
281
+ cfg = self.terminal.config
282
+ ollama_url = cfg.get("ollama_url", "http://localhost:11434")
283
+ api_url = cfg.get("api_url", "http://localhost:8000")
284
+
285
+ checks: list[tuple] = []
286
+
287
+ def _ok(label, detail=""): checks.append(("ok", label, detail))
288
+ def _warn(label, detail=""): checks.append(("warn", label, detail))
289
+ def _err(label, detail=""): checks.append(("err", label, detail))
290
+
291
+ import sys as _sys
292
+ pyver = f"{_sys.version_info.major}.{_sys.version_info.minor}.{_sys.version_info.micro}"
293
+ if _sys.version_info >= (3, 9):
294
+ _ok("Python", pyver)
295
+ else:
296
+ _warn("Python", f"{pyver} (3.9+ recommended)")
297
+
298
+ try:
299
+ import urllib.request as _ur
300
+ _opener = _ur.build_opener(_ur.ProxyHandler({}))
301
+ _r = _opener.open(f"{ollama_url}/api/tags", timeout=3)
302
+ _data = json.loads(_r.read())
303
+ models = [m["name"] for m in _data.get("models", [])]
304
+ if models:
305
+ _ok("Ollama", f"{len(models)} models: {', '.join(models[:4])}")
306
+ else:
307
+ _warn("Ollama", "running but no models installed (ollama pull qwen2.5-coder:1.5b)")
308
+ except Exception as e:
309
+ _err("Ollama", f"not reachable at {ollama_url} ({e})")
310
+
311
+ try:
312
+ import urllib.request as _ur
313
+ _opener = _ur.build_opener(_ur.ProxyHandler({}))
314
+ _r = _opener.open(f"{api_url}/health", timeout=3)
315
+ _ok("Backend", f"running at {api_url}")
316
+ except Exception:
317
+ _warn("Backend", f"offline at {api_url} — local Ollama mode will be used")
318
+
319
+ key_checks = [
320
+ ("finnhub", "股票行情"),
321
+ ("alphavantage", "历史数据"),
322
+ ("newsapi", "新闻"),
323
+ ("brave", "网络搜索"),
324
+ ("coingecko", "加密货币"),
325
+ ]
326
+ for svc, desc in key_checks:
327
+ k = _get_provider_key(svc)
328
+ if k:
329
+ _ok(f"API key: {svc}", f"{desc} ({'*'*6}{k[-4:]})")
330
+ else:
331
+ _warn(f"API key: {svc}", f"{desc} 未配置 (/apikey set {svc} <key>)")
332
+
333
+ llm_keys = [("deepseek", "DeepSeek"), ("openai", "OpenAI"),
334
+ ("siliconflow", "SiliconFlow"), ("moonshot", "Moonshot")]
335
+ _has_any_llm = False
336
+ for svc, name in llm_keys:
337
+ k = _get_provider_key(svc)
338
+ if k:
339
+ _ok(f"LLM key: {svc}", f"{name} configured")
340
+ _has_any_llm = True
341
+ if not _has_any_llm:
342
+ _warn("LLM keys", "No cloud LLM keys — Ollama must be running for AI responses")
343
+
344
+ _pkgs = [
345
+ ("aiohttp", "async HTTP"),
346
+ ("rich", "terminal UI"),
347
+ ("prompt_toolkit", "autocomplete"),
348
+ ("yfinance", "market data"),
349
+ ("pandas", "data processing"),
350
+ ("requests", "HTTP client"),
351
+ ]
352
+ for pkg, desc in _pkgs:
353
+ try:
354
+ m = _il.import_module(pkg)
355
+ ver = getattr(m, "__version__", "?")
356
+ _ok(f"pkg: {pkg}", f"{desc} v{ver}")
357
+ except ImportError:
358
+ _warn(f"pkg: {pkg}", f"{desc} not installed (pip install {pkg})")
359
+
360
+ aria_md = pathlib.Path.cwd() / "ARIA.md"
361
+ if aria_md.exists():
362
+ lines = len(aria_md.read_text(encoding="utf-8").splitlines())
363
+ _ok("ARIA.md", f"{lines} lines of project context")
364
+ else:
365
+ _warn("ARIA.md", f"not found in {pathlib.Path.cwd()} (use /init to create)")
366
+
367
+ if _HAS_MCP:
368
+ try:
369
+ reg = self.terminal._mcp_registry
370
+ if reg and hasattr(reg, "list_tools"):
371
+ tools = reg.list_tools()
372
+ _ok("MCP", f"{len(tools)} tools from MCP servers")
373
+ else:
374
+ _warn("MCP", "registry not started yet")
375
+ except Exception:
376
+ _warn("MCP", "loaded but no active servers")
377
+ else:
378
+ _warn("MCP", "mcp_client not found — MCP support disabled")
379
+
380
+ tool_count = len(ARIA_TOOLS) + len(LOCAL_TOOLS)
381
+ _ok("Aria tools", f"{tool_count} tools loaded")
382
+
383
+ console.print() if HAS_RICH else None
384
+ if HAS_RICH:
385
+ console.print("[bold]Aria Code — Diagnostics[/bold]")
386
+ console.print()
387
+ icons = {"ok": "[green]✓[/green]", "warn": "[yellow]⚠[/yellow]", "err": "[red]✗[/red]"}
388
+ for status, label, detail in checks:
389
+ icon = icons[status]
390
+ detail_str = f" [dim]{detail}[/dim]" if detail else ""
391
+ console.print(f" {icon} {label:<28}{detail_str}")
392
+ console.print()
393
+ n_ok = sum(1 for s, *_ in checks if s == "ok")
394
+ n_w = sum(1 for s, *_ in checks if s == "warn")
395
+ n_e = sum(1 for s, *_ in checks if s == "err")
396
+ summary_color = "green" if n_e == 0 and n_w == 0 else ("yellow" if n_e == 0 else "red")
397
+ console.print(f" [{summary_color}]{n_ok} passed · {n_w} warnings · {n_e} errors[/{summary_color}]")
398
+ console.print()
399
+
400
+ _fh_ok = bool(_get_provider_key("finnhub"))
401
+ _av_ok = bool(_get_provider_key("alphavantage"))
402
+ _na_ok = bool(_get_provider_key("newsapi"))
403
+ _ak_ok = True
404
+ _llm_ok = any(_get_provider_key(p) for p in ("deepseek", "openai", "anthropic", "groq"))
405
+
406
+ _guide_needed = not (_fh_ok and _av_ok and _na_ok and _llm_ok)
407
+ if _guide_needed:
408
+ console.print("[bold]数据源配置指南[/bold] [dim](完整功能需要以下 key)[/dim]")
409
+ console.print()
410
+ console.print(" [dim]Use /doctor --network for network checks[/dim]")
411
+ else:
412
+ print("Diagnostics complete")
413
+
414
+ async def cmd_install(self, args: str):
415
+ """
416
+ 检测并安装缺失的依赖包(环境补全)。
417
+
418
+ /install 扫描全部已知依赖,列出缺失并询问是否安装
419
+ /install <pkg> [pkg] 直接安装指定 Python 包
420
+ /install --auto 根据最近一次提问的意图检测缺失项
421
+ /install --required 仅安装"必需"包,跳过可选项
422
+ /install --plan 只展示安装计划,不安装
423
+ /install --yes 非交互确认,直接安装选中包
424
+ """
425
+ import shlex as _shlex
426
+ import subprocess as _sp
427
+ import sys as _sys
428
+ from apps.cli.preflight import (
429
+ build_full_dependency_report,
430
+ build_intent_preflight,
431
+ build_install_plan,
432
+ package_to_module,
433
+ select_install_packages,
434
+ )
435
+
436
+ raw = (args or "").strip()
437
+ tokens = raw.split()
438
+ flags = {t for t in tokens if t.startswith("--")}
439
+ explicit_pkgs = [t for t in tokens if not t.startswith("--")]
440
+
441
+ # ── Resolve what to install ───────────────────────────────────────────
442
+ report = None
443
+ if explicit_pkgs:
444
+ # Direct package install — no detection needed
445
+ pip_packages = explicit_pkgs
446
+ command_hints: tuple = ()
447
+ env_hints: tuple = ()
448
+ else:
449
+ if "--auto" in flags:
450
+ # Detect from the user's last real question
451
+ last_msg = ""
452
+ for m in reversed(self.terminal.conversation):
453
+ if m.get("role") == "user" and m.get("content"):
454
+ last_msg = m["content"] if isinstance(m["content"], str) else ""
455
+ break
456
+ if not last_msg:
457
+ console.print("[yellow]没有可分析的历史提问,改用全量扫描[/yellow]" if HAS_RICH
458
+ else "No history; full scan")
459
+ report = build_full_dependency_report(include_optional="--required" not in flags)
460
+ else:
461
+ report = build_intent_preflight(last_msg)
462
+ else:
463
+ report = build_full_dependency_report(include_optional="--required" not in flags)
464
+
465
+ plan = build_install_plan(report)
466
+ pip_packages = list(plan.pip_packages)
467
+ command_hints = plan.command_hints
468
+ env_hints = plan.env_hints
469
+
470
+ # ── Nothing missing ───────────────────────────────────────────────────
471
+ if not pip_packages and not (report and (report.missing_commands or report.missing_env)):
472
+ console.print("[green]✓ 环境完整,没有检测到缺失的 Python 包[/green]" if HAS_RICH
473
+ else "All dependencies satisfied")
474
+ return
475
+
476
+ # ── Show findings ─────────────────────────────────────────────────────
477
+ if HAS_RICH:
478
+ console.print()
479
+ console.print("[bold]环境检测结果[/bold]")
480
+ if pip_packages:
481
+ console.print(f" [yellow]缺少 {len(pip_packages)} 个 Python 包:[/yellow]")
482
+ for p in pip_packages:
483
+ purpose = ""
484
+ if report:
485
+ for r in report.missing_python:
486
+ if r.package == p:
487
+ purpose = f" [dim]— {r.purpose}{'(可选)' if not r.required else ''}[/dim]"
488
+ break
489
+ console.print(f" • [cyan]{p}[/cyan]{purpose}")
490
+ if command_hints:
491
+ console.print(" [yellow]缺少命令行工具:[/yellow]")
492
+ for h in command_hints:
493
+ console.print(f" • [dim]{h}[/dim]")
494
+ if env_hints:
495
+ console.print(" [dim]未配置的环境变量(可选,不自动处理):[/dim]")
496
+ for h in env_hints:
497
+ console.print(f" • [dim]{h}[/dim]")
498
+ console.print()
499
+ else:
500
+ print(f"Missing packages: {', '.join(pip_packages)}")
501
+ for h in command_hints:
502
+ print(f" tool: {h}")
503
+
504
+ if not pip_packages:
505
+ if command_hints:
506
+ console.print("[dim]命令行工具需手动安装(见上方提示),Aria 不会自动执行系统级安装[/dim]"
507
+ if HAS_RICH else "Install CLI tools manually (see hints above)")
508
+ return
509
+
510
+ # ── Select packages ───────────────────────────────────────────────────
511
+ if report:
512
+ if "--plan" in flags:
513
+ selection = select_install_packages(plan, report, mode="plan")
514
+ elif "--yes" in flags:
515
+ _mode = "required" if "--required" in flags else "all"
516
+ selection = select_install_packages(plan, report, mode=_mode)
517
+ else:
518
+ required_pkgs = [
519
+ r.package for r in report.missing_python
520
+ if r.required and r.package in pip_packages
521
+ ]
522
+ optional_pkgs = [
523
+ r.package for r in report.missing_python
524
+ if not r.required and r.package in pip_packages
525
+ ]
526
+ if HAS_RICH:
527
+ console.print("[bold]选择安装范围[/bold]")
528
+ console.print(" [cyan]all[/cyan] 安装全部缺失 Python 包")
529
+ console.print(" [cyan]required[/cyan] 只安装必需包")
530
+ console.print(" [cyan]optional[/cyan] 只安装可选增强包")
531
+ console.print(" [cyan]custom[/cyan] 手动输入包名或编号")
532
+ console.print(" [cyan]plan[/cyan] 只显示计划,不安装")
533
+ console.print(" [cyan]skip[/cyan] 跳过")
534
+ for idx, pkg in enumerate(pip_packages, 1):
535
+ kind = "required" if pkg in required_pkgs else "optional"
536
+ console.print(f" [dim]{idx}.[/dim] {pkg} [dim]({kind})[/dim]")
537
+ else:
538
+ print("Select install scope: all | required | optional | custom | plan | skip")
539
+ for idx, pkg in enumerate(pip_packages, 1):
540
+ kind = "required" if pkg in required_pkgs else "optional"
541
+ print(f" {idx}. {pkg} ({kind})")
542
+ default_mode = "required" if required_pkgs and optional_pkgs else "all"
543
+ try:
544
+ choice = console.input(f"安装范围 [{default_mode}]: ") if HAS_RICH else input(f"Install scope [{default_mode}]: ")
545
+ except (EOFError, KeyboardInterrupt):
546
+ console.print("\n[dim]已取消[/dim]" if HAS_RICH else "Cancelled")
547
+ return
548
+ choice = (choice or default_mode).strip().lower()
549
+ custom_items: list[str] = []
550
+ if choice == "custom":
551
+ try:
552
+ raw_custom = console.input("输入包名或编号(空格/逗号分隔): ") if HAS_RICH else input("Packages or numbers: ")
553
+ except (EOFError, KeyboardInterrupt):
554
+ console.print("\n[dim]已取消[/dim]" if HAS_RICH else "Cancelled")
555
+ return
556
+ for item in raw_custom.replace(",", " ").split():
557
+ if item.isdigit():
558
+ idx = int(item)
559
+ if 1 <= idx <= len(pip_packages):
560
+ custom_items.append(pip_packages[idx - 1])
561
+ else:
562
+ custom_items.append(item)
563
+ selection = select_install_packages(
564
+ plan, report, mode=choice, custom=custom_items
565
+ )
566
+ pip_packages = list(selection.pip_packages)
567
+ if selection.mode in {"plan", "dry-run", "dry_run"}:
568
+ console.print("[dim]已生成安装计划,未执行安装。[/dim]" if HAS_RICH else "Install plan only; no changes made.")
569
+ return
570
+ if not pip_packages:
571
+ console.print("[dim]没有选择任何 Python 包,未安装。[/dim]" if HAS_RICH else "No packages selected.")
572
+ return
573
+ if selection.skipped_packages and HAS_RICH:
574
+ console.print(f"[dim]跳过: {', '.join(selection.skipped_packages)}[/dim]")
575
+ elif "--plan" in flags:
576
+ console.print("[dim]显式包安装计划已显示,未执行安装。[/dim]" if HAS_RICH else "Install plan only; no changes made.")
577
+ return
578
+
579
+ # ── Confirm ───────────────────────────────────────────────────────────
580
+ pip_cmd = [_sys.executable, "-m", "pip", "install", *pip_packages]
581
+ pretty = " ".join(_shlex.quote(c) for c in pip_cmd)
582
+ if "--yes" not in flags:
583
+ prompt = f"将运行: {pretty}\n确认安装? [y/N]: "
584
+ try:
585
+ answer = console.input(prompt) if HAS_RICH else input(prompt)
586
+ except (EOFError, KeyboardInterrupt):
587
+ console.print("\n[dim]已取消[/dim]" if HAS_RICH else "Cancelled")
588
+ return
589
+ if answer.strip().lower() not in {"y", "yes"}:
590
+ console.print("[dim]已取消,未安装任何包[/dim]" if HAS_RICH else "Cancelled")
591
+ return
592
+ elif HAS_RICH:
593
+ console.print(f"[dim]Auto install: {pretty}[/dim]")
594
+
595
+ # ── Install ───────────────────────────────────────────────────────────
596
+ console.print(f"\n[dim]⏳ 安装中: {' '.join(pip_packages)}…[/dim]" if HAS_RICH
597
+ else f"Installing {' '.join(pip_packages)}...")
598
+ try:
599
+ proc = await asyncio.get_event_loop().run_in_executor(
600
+ None,
601
+ lambda: _sp.run(pip_cmd, capture_output=True, text=True, timeout=300),
602
+ )
603
+ except Exception as exc:
604
+ console.print(f"[red]安装失败: {exc}[/red]" if HAS_RICH else f"Install failed: {exc}")
605
+ return
606
+
607
+ if proc.returncode == 0:
608
+ # Verify each package now imports
609
+ import importlib as _il
610
+ _il.invalidate_caches()
611
+ ok_list, fail_list = [], []
612
+ for p in pip_packages:
613
+ mod = package_to_module(p)
614
+ try:
615
+ _il.import_module(mod)
616
+ ok_list.append(p)
617
+ except Exception:
618
+ fail_list.append(p)
619
+ if HAS_RICH:
620
+ console.print(f" [green]✓ 安装完成: {', '.join(ok_list) or '—'}[/green]")
621
+ if fail_list:
622
+ console.print(f" [yellow]⚠ 已安装但当前会话需重启才能加载: {', '.join(fail_list)}[/yellow]")
623
+ console.print(" [dim]提示: 部分包需重启 Aria 才能被工具加载[/dim]")
624
+ else:
625
+ print(f"Installed: {', '.join(ok_list)}")
626
+ else:
627
+ err_tail = (proc.stderr or proc.stdout or "")[-400:]
628
+ console.print(f"[red]pip 安装失败 (code {proc.returncode}):[/red]\n[dim]{err_tail}[/dim]"
629
+ if HAS_RICH else f"pip failed: {err_tail}")
630
+
631
+ async def cmd_datasource(self, args: str):
632
+ sub = args.strip().lower()
633
+ if sub.startswith("test "):
634
+ src_name = sub[5:].strip()
635
+ await asyncio.get_event_loop().run_in_executor(
636
+ None, lambda: _test_datasource(src_name)
637
+ )
638
+ return
639
+
640
+ if sub == "config":
641
+ paths = [
642
+ "~/.aria/datasources.yaml",
643
+ "~/.aria/.env",
644
+ str(CONFIG_DIR / "providers.json"),
645
+ ]
646
+ if HAS_RICH:
647
+ console.print(" [bold]数据源配置文件:[/bold]")
648
+ for p in paths:
649
+ import pathlib
650
+ full = pathlib.Path(p).expanduser()
651
+ exists = "[green]✓[/green]" if full.exists() else "[dim]✗ (未创建)[/dim]"
652
+ console.print(f" {exists} [dim]{p}[/dim]")
653
+ console.print("\n [dim]环境变量: TUSHARE_TOKEN FRED_API_KEY ALPHA_VANTAGE_KEY[/dim]")
654
+ return
655
+
656
+ try:
657
+ from datasources.router import _SOURCE_REGISTRY, DataRouter
658
+ router = DataRouter()
659
+ except ImportError:
660
+ _print_error("datasources 模块未找到")
661
+ return
662
+
663
+ if HAS_RICH:
664
+ from rich.table import Table
665
+ from rich import box as rich_box
666
+ table = Table(title="数据源状态", box=rich_box.SIMPLE, header_style="bold dim")
667
+ table.add_column("名称", width=16)
668
+ table.add_column("市场", width=20)
669
+ table.add_column("需要Key", width=8)
670
+ table.add_column("状态", width=8)
671
+ table.add_column("说明")
672
+ _DESC = {
673
+ "yfinance": "Yahoo Finance (免费)",
674
+ "akshare": "AkShare A股 (免费)",
675
+ "tushare": "Tushare Pro (需Token)",
676
+ "fred": "美联储经济数据 (免费)",
677
+ "edgar": "SEC EDGAR 财报 (免费)",
678
+ "alpha_vantage": "Alpha Vantage (免费Key)",
679
+ "world_bank": "世界银行 (免费)",
680
+ }
681
+ for name, cls in _SOURCE_REGISTRY.items():
682
+ try:
683
+ src = cls()
684
+ configured = src.is_configured()
685
+ status = "[green]✓ 就绪[/green]" if configured else "[dim]✗ 未配置[/dim]"
686
+ needs_key = "是" if cls.requires_key else "否"
687
+ markets = ", ".join(getattr(cls, "markets", []))
688
+ except Exception:
689
+ status, needs_key, markets = "[red]错误[/red]", "?", "?"
690
+ table.add_row(name, markets, needs_key, status, _DESC.get(name, ""))
691
+ console.print(table)
692
+ console.print(" [dim]/datasource config — 配置文件路径[/dim]")
693
+ else:
694
+ for name, cls in _SOURCE_REGISTRY.items():
695
+ src = cls()
696
+ print(f" {name}: {'ready' if src.is_configured() else 'not configured'}")