echopai 2.1.0 → 2.2.0

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.
package/README.md CHANGED
@@ -125,13 +125,16 @@ Available MCP tools (subset shown to LLM depends on token scopes):
125
125
  | `digest` | One-shot research digest: 5 buckets in one call | `digest.get` + fan-out fallback |
126
126
  | `quote` | Real-time quote for 1-200 codes | `quote` |
127
127
  | `views` | **PRIMARY** research source (analyst views w/ entity attribution) | `views.recent` |
128
- | `research` | Research-entity performance (hit rate / coverage) | `research.entity-performance-list` |
129
128
  | `news` | **SUPPLEMENTARY** news / market briefs | `news.search` / `news.feed` |
130
129
  | `sentiment` | Aggregate market sentiment | `sentiment.overview` |
131
130
  | `hot` | Today's hot-stock leaderboard | `stocks.hot` |
132
131
  | `chart` | Single-security K-line (daily / minute) | `bars.daily` / `bars.minute` |
133
132
  | `bars_batch` | Batch K-line (≤100 codes × 1yr daily; ≤20 × 7d minute) | `bars.daily-batch` / `bars.minute-batch` |
134
133
  | `scan` | Full-market quote snapshot (~5800 securities) | `quote.scan` |
134
+ | `financials_quote_snapshot` | **Headline** valuation snapshot (PE / PB / PS / 换手率 / 股息率 / 量比 14 fields, TDX + Sina 自算) | `financials.quote-snapshot` |
135
+ | `financials_pit` | Point-in-time financial indicators at a given trade_date (anti-future-fn for backtests) | `financials.pit` |
136
+ | `financials_reports` | Last N report-period snapshots (~25 fields × period, filter by Q1/H1/Q3/annual) | `financials.reports` |
137
+ | `financials_series` | Time series of a single financial metric across report periods | `financials.series` |
135
138
 
136
139
  The agent should prefer `views` over `news` when forming an investment
137
140
  opinion (views carries research-entity attribution; news is breadth-only).
@@ -159,6 +162,15 @@ echopai raw views feed --since-days 7 --all | jq '.title'
159
162
 
160
163
  # One-shot research digest (server-side composite, 5 buckets, partial-failure tolerant)
161
164
  echopai digest --code SSE:600519
165
+
166
+ # Headline valuation snapshot: 14 fields (PE / PB / PS / 换手率 / 股息率 / 量比 ...)
167
+ echopai financials quote-snapshot --code SSE:600519
168
+ echopai financials quote-snapshot --code SSE:600519 --date 2026-05-13 # historical
169
+
170
+ # Deeper fundamentals
171
+ echopai financials pit --code SSE:600519 --date 2024-11-01
172
+ echopai financials reports --code SSE:600519 --kind annual --limit 5
173
+ echopai financials series --code SSE:600519 --metric roe_simple
162
174
  ```
163
175
 
164
176
  ## Output formats
@@ -83,22 +83,6 @@ export function buildCommandTree(program, dispatch) {
83
83
  attachOperation(cmd, OPERATIONS["auth.whoami"], dispatch);
84
84
  }
85
85
  }
86
- {
87
- const noun = program.command("backtest");
88
- noun.description("backtest commands");
89
- {
90
- const cmd = noun.command("excess-distribution");
91
- attachOperation(cmd, OPERATIONS["backtest.excess-distribution"], dispatch);
92
- }
93
- {
94
- const cmd = noun.command("rolling-win-rate");
95
- attachOperation(cmd, OPERATIONS["backtest.rolling-win-rate"], dispatch);
96
- }
97
- {
98
- const cmd = noun.command("summary");
99
- attachOperation(cmd, OPERATIONS["backtest.summary"], dispatch);
100
- }
101
- }
102
86
  {
103
87
  const noun = program.command("bars");
104
88
  noun.description("bars commands");
@@ -134,6 +118,10 @@ export function buildCommandTree(program, dispatch) {
134
118
  const cmd = noun.command("pit");
135
119
  attachOperation(cmd, OPERATIONS["financials.pit"], dispatch);
136
120
  }
121
+ {
122
+ const cmd = noun.command("quote-snapshot");
123
+ attachOperation(cmd, OPERATIONS["financials.quote-snapshot"], dispatch);
124
+ }
137
125
  {
138
126
  const cmd = noun.command("reports");
139
127
  attachOperation(cmd, OPERATIONS["financials.reports"], dispatch);
@@ -186,10 +174,6 @@ export function buildCommandTree(program, dispatch) {
186
174
  const cmd = noun.command("search");
187
175
  attachOperation(cmd, OPERATIONS["news.search"], dispatch);
188
176
  }
189
- {
190
- const cmd = noun.command("sources");
191
- attachOperation(cmd, OPERATIONS["news.sources"], dispatch);
192
- }
193
177
  }
194
178
  {
195
179
  const noun = program.command("payment");
@@ -211,18 +195,6 @@ export function buildCommandTree(program, dispatch) {
211
195
  attachOperation(cmd, OPERATIONS["quote.scan"], dispatch);
212
196
  }
213
197
  }
214
- {
215
- const noun = program.command("research");
216
- noun.description("research commands");
217
- {
218
- const cmd = noun.command("entity-performance");
219
- attachOperation(cmd, OPERATIONS["research.entity-performance"], dispatch);
220
- }
221
- {
222
- const cmd = noun.command("entity-performance-list");
223
- attachOperation(cmd, OPERATIONS["research.entity-performance-list"], dispatch);
224
- }
225
- }
226
198
  {
227
199
  const noun = program.command("search");
228
200
  noun.description("search commands");
@@ -267,18 +239,6 @@ export function buildCommandTree(program, dispatch) {
267
239
  attachOperation(cmd, OPERATIONS["sentiment.turnover"], dispatch);
268
240
  }
269
241
  }
270
- {
271
- const noun = program.command("signals");
272
- noun.description("signals commands");
273
- {
274
- const cmd = noun.command("outcome");
275
- attachOperation(cmd, OPERATIONS["signals.outcome"], dispatch);
276
- }
277
- {
278
- const cmd = noun.command("outcomes");
279
- attachOperation(cmd, OPERATIONS["signals.outcomes"], dispatch);
280
- }
281
- }
282
242
  {
283
243
  const noun = program.command("squawk");
284
244
  noun.description("squawk commands");
@@ -22,21 +22,6 @@ export const HELP = {
22
22
  "description": "Returns the calling token's kind, scopes, audience, app metadata,\nrate_limit, allowed_clients, agent_budget (if kind=agent), api_version,\nfeature_flags. Any valid JWT can call — no specific scope required.\n\nCLI/MCP call this once at startup, cache 5 minutes in-process, and use\nthe response to derive `verbs.available` (intersection of curated verb\nscopes with token scopes) and to populate `echopai doctor` checks.\n\nSee `docs/PLAN_CLI_V2_AGENT_SURFACE.md` §5.1.\n",
23
23
  "example": "echopai auth whoami"
24
24
  },
25
- "backtest.excess-distribution": {
26
- "summary": "Excess-return distribution over a benchmark",
27
- "description": "信号相对基准(沪深 300 / 中证 1000 等)持有期超额收益的直方分布。",
28
- "example": "echopai backtest excess-distribution"
29
- },
30
- "backtest.rolling-win-rate": {
31
- "summary": "Rolling win rate of a signal series",
32
- "description": "信号在滚动窗口(默认 60 个交易日)内的胜率时间序列。",
33
- "example": "echopai backtest rolling-win-rate"
34
- },
35
- "backtest.summary": {
36
- "summary": "Backtest summary metrics for a signal series",
37
- "description": "信号回测汇总:胜率、平均超额收益、Sharpe-like 评分、最大回撤等。",
38
- "example": "echopai backtest summary"
39
- },
40
25
  "bars.daily": {
41
26
  "summary": "Daily OHLC bars for one A-share security",
42
27
  "description": "Daily OHLC bars for one A-share security over a date range. Returns open / high / low / close / volume / turnover per trading day. Requires `bars:30d` (last 30 trading days) or `bars:full` (full history) scope.",
@@ -59,7 +44,7 @@ export const HELP = {
59
44
  },
60
45
  "digest.get": {
61
46
  "summary": "One-shot research digest for a single security (composite)",
62
- "description": "Composite endpoint: in one HTTP call returns views (PRIMARY research),\nresearch-entity performance (quality layer), real-time quote, market\nsentiment context, and supplementary news. Partial-failure tolerant:\neach bucket is independently fetched and per-bucket failures surface\nin `meta.partial_failures[]` rather than poisoning the response. If\nevery sub-bucket fails the endpoint returns 502.\n\nBucket scopes are checked *per bucket* (not at gateway level): a\ntoken with only `views:read` gets the views bucket populated and the\nrest reported as `scope_insufficient` partial failures. This mirrors\nthe CLI fan-out contract exactly so `echopai digest` can either call\nthis endpoint (preferred, single round-trip) or fall back to local\nfan-out without behavior drift.\n\nSee `docs/PLAN_CLI_V2_AGENT_SURFACE.md` §3.3 (digest spec) and §11\nPhase 5.2 (server endpoint).\n",
47
+ "description": "Composite endpoint: in one HTTP call returns views (PRIMARY research),\nreal-time quote, valuation snapshot (PE/PB/PS/换手率/股息率/量比 14 字段),\nmarket sentiment context, and supplementary news.\nPartial-failure tolerant:\neach bucket is independently fetched and per-bucket failures surface\nin `meta.partial_failures[]` rather than poisoning the response. If\nevery sub-bucket fails the endpoint returns 502.\n\nBucket scopes are checked *per bucket* (not at gateway level): a\ntoken with only `views:read` gets the views bucket populated and the\nrest reported as `scope_insufficient` partial failures. This mirrors\nthe CLI fan-out contract exactly so `echopai digest` can either call\nthis endpoint (preferred, single round-trip) or fall back to local\nfan-out without behavior drift.\n\nSee `docs/PLAN_CLI_V2_AGENT_SURFACE.md` §3.3 (digest spec) and §11\nPhase 5.2 (server endpoint).\n",
63
48
  "example": "echopai digest get VALUE"
64
49
  },
65
50
  "financials.pit": {
@@ -67,6 +52,11 @@ export const HELP = {
67
52
  "description": "基于 TDX 财务数据的 Point-in-time 查询。给定 `code` 和 `date`,返回该\n`trade_date` 当时市场上可见的最新一期财务报告(`announce_date <= date`,\n无公告日时保守延迟 90 天)。专为回测、AI agent、量化策略防未来函数泄漏。\n\n返回字段包括:EPS(基本/扣非/稀释)、BPS、ROE、毛利率、营收/净利润/总资产/\n归母权益等核心 ~25 字段。\n",
68
53
  "example": "echopai financials pit --code SSE:600519 --date 2024-11-01"
69
54
  },
55
+ "financials.quote-snapshot": {
56
+ "summary": "Real-time valuation / share / turnover snapshot for one A-share",
57
+ "description": "一站式估值快照 —— 一次返回 14 个字段,**全部基于 TDX 原始数据 + Sina 实时\n价自算,不依赖 Tushare**:\n\n- 估值: `pe` / `pe_ttm` / `pb` / `ps` / `ps_ttm`\n- 股本(万股): `total_share` / `float_share` / `free_share`\n- 市值(万元): `total_mv` / `circ_mv`\n- 流动性: `turnover_rate` / `turnover_rate_f`(%)/ `volume_ratio`(倍)\n- 分红: `dv_ratio` / `dv_ttm`(%)\n\n计算口径:\n- PE-TTM = 当前总市值 / 近 4 季度滚动净利润(PIT 表 `ni_parent_ttm`)\n- PE = 当前总市值 / 上年报净利润(`ni_parent_last1y`)\n- PB = 当前总市值 / 归母净资产(`equity_parent`,PIT 防穿越)\n- 换手率 = 当日累计成交量 / 流通股本 × 100\n- 量比 = 当日累计 / 近 5 日同时段累计平均(实时模式)\n / 近 5 日全日成交量平均(指定历史日期模式)\n- 股息率 = Σ(去年 / 近12月内派息事件 派现/10) / 现价 × 100\n\n默认 `date` 留空 → 实时(Sina 5s 快照价 + 当日累计成交量);\n给 `date=YYYY-MM-DD` 则按当日 close 取值(用于历史回测 / 校验)。\n\n财报数据走 `security_financial_indicators_pit` PIT 视图:保证 `visible_date`\n≤ trade_date,避免未来函数。\n\n准确性已对照 Tushare `daily_basic` 校验(见 `scripts/validation/validate_quote_snapshot_vs_tushare.py`)。\n",
58
+ "example": "echopai financials quote-snapshot --code SSE:600519 --date 2026-05-13"
59
+ },
70
60
  "financials.reports": {
71
61
  "summary": "Recent financial reports for one A-share security",
72
62
  "description": "返回该股票最近 N 期财务报告的核心指标(EPS/BPS/ROE/毛利率/营收/净利润/总\n资产/归母权益/经营现金流/总股本 等 ~25 字段)。可按 `kind` 过滤季报/半年报/\n年报。每期 `announce_date` 字段告知何时市场可见,建议结合回测时使用。\n",
@@ -79,18 +69,18 @@ export const HELP = {
79
69
  },
80
70
  "index.daily-bars": {
81
71
  "summary": "Daily OHLC bars for one A-share index",
82
- "description": "指数日线 OHLC。覆盖 v2 表中的指数(上证综指 000001.SH / 深证成指\n399001.SZ / 沪深300 000300.SH / 北证50 899050.BJ / 中证系列 000922.CSI 等)。\n与 `/v1/bars/daily` 同语义,但 index 没有 `turnover_rate` / `paused` 等股票特有字段。\n",
83
- "example": "echopai index daily-bars --code 000001.SH --from 2024-01-01 --to 2024-12-31"
72
+ "description": "指数日线 OHLC。覆盖 v2 表中的指数(上证综指 `SSE:000001` / 深证成指\n`SZSE:399001` / 沪深300 `SSE:000300` / 北证50 `BSE:899050` / 中证系列 `CSI:000922` 等)。\n与 `/v1/bars/daily` 同语义,但 index 没有 `turnover_rate` / `paused` 等股票特有字段。\n",
73
+ "example": "echopai index daily-bars --code SSE:000001 --from 2024-01-01 --to 2024-12-31"
84
74
  },
85
75
  "index.minute-bars": {
86
76
  "summary": "Minute OHLC bars for one A-share index",
87
77
  "description": "指数 1min OHLC。日期范围 ≤ 7 天(与 `/v1/bars/minute-batch` 对齐)。\n返回 `bar_time` / `trade_date` / OHLC / volume / amount / pct_change。\n",
88
- "example": "echopai index minute-bars --code 000001.SH"
78
+ "example": "echopai index minute-bars --code SSE:000001"
89
79
  },
90
80
  "index.snapshot": {
91
81
  "summary": "Real-time snapshot of all Sina-OK A-share indices (172 indices)",
92
- "description": "Returns the latest real-time snapshot for the 172 indices that Sina\nexposes a quote for (SSE / SZSE / BSE — includes 北证50 `899050.BJ`\nand 专精特新 `899601.BJ`). Source is the Redis hash\n`stockpulse:index_snapshots:latest` which the Rust collector refreshes\nevery ~15 seconds during market hours; outside trading hours the last\nintraday snapshot is returned.\n\nUse `codes` to narrow to a subset (canonical format like `000001.SH`).\nOmit to receive all 172.\n\nNote: CSI-series indices (e.g. `000922.CSI`) are not available here —\nSina has no quote feed for them. Daily/minute history for those still\nlives behind `/api/internal/index/{daily-bars,minute-bars-range}`.\n\nRequires `quote:l1`, `quote:l2`, or `quote:delayed` scope.\n",
93
- "example": "echopai index snapshot --codes 000001.SH,399001.SZ,899050.BJ"
82
+ "description": "Returns the latest real-time snapshot for the 172 indices that Sina\nexposes a quote for (SSE / SZSE / BSE — includes 北证50 `BSE:899050`\nand 专精特新 `899601.BJ`). Source is the Redis hash\n`stockpulse:index_snapshots:latest` which the Rust collector refreshes\nevery ~15 seconds during market hours; outside trading hours the last\nintraday snapshot is returned.\n\nUse `codes` to narrow to a subset (canonical format `SSE:000001` (exchange-prefix; legacy `000001.SH` is also accepted but discouraged)).\nOmit to receive all 172.\n\nNote: CSI-series indices (e.g. `CSI:000922`) are not available here —\nSina has no quote feed for them. Daily/minute history for those still\nlives behind `/api/internal/index/{daily-bars,minute-bars-range}`.\n\nRequires `quote:l1`, `quote:l2`, or `quote:delayed` scope.\n",
83
+ "example": "echopai index snapshot --codes SSE:000001,SZSE:399001,BSE:899050"
94
84
  },
95
85
  "market.status": {
96
86
  "summary": "Current A-share market session state",
@@ -117,11 +107,6 @@ export const HELP = {
117
107
  "description": "Full-text search recent news / market briefs. Returns title, published_at, snippet, tagged securities. (Internal collector identifiers — `source`/`source_id`/`source_url` — are NOT exposed publicly; admin tools see them via /v1/admin/*.) Requires `news:read` scope. Note: news fields are user-generated; meta.untrusted_text_fields lists fields that need sanitization before passing to an LLM.",
118
108
  "example": "echopai news search --query 光伏龙头"
119
109
  },
120
- "news.sources": {
121
- "summary": "List canonical news source identifiers",
122
- "description": "返回新闻 source 标识列表(如 `cls`、`jin10`、`twitter`、`wallstreetcn`),可用于 list/search 的 source 过滤。",
123
- "example": "echopai news sources"
124
- },
125
110
  "payment.plans": {
126
111
  "summary": "List subscription plans",
127
112
  "description": "返回订阅计划清单(plan_id + 价格 + 时长 + 描述)。",
@@ -137,16 +122,6 @@ export const HELP = {
137
122
  "description": "Dumps the entire Redis snapshot of A-share real-time quotes in a single\nround-trip. Use for \"全市场扫描\" / agent universe-of-interest discovery\nwhen you don't yet know which codes to query.\n\nFiltering:\n- `exchange=SSE|SZSE|BSE` narrows to one exchange (canonical_code prefix)\n- `app.allowed_securities` is ALWAYS applied; codes outside drop silently\n\nResponse size: ~3 MB unfiltered (~5800 items). CLI users should pair\nwith `--max-bytes` / `--fields` / `--query` from Phase 1.4.\n\n9:00–9:14 集合竞价时段返回空 items + `note` 字段说明。\n\nRequires `quote:l1`, `quote:l2`, or `quote:delayed` scope.\n",
138
123
  "example": "echopai quote scan"
139
124
  },
140
- "research.entity-performance": {
141
- "summary": "Performance of one research entity",
142
- "description": "单个研究实体的细化绩效指标,对照 `/v1/research-entities/performance`。",
143
- "example": "echopai research entity-performance VALUE"
144
- },
145
- "research.entity-performance-list": {
146
- "summary": "Aggregate performance of research entities",
147
- "description": "全体研究实体(analyst / team / institution)的命中率、平均目标价达成率、覆盖股票数等汇总。需要 `research:read` scope。",
148
- "example": "echopai research entity-performance-list"
149
- },
150
125
  "search.semantic": {
151
126
  "summary": "Hybrid semantic search across news + analyst views",
152
127
  "description": "语义 + 模糊泛化搜索。结合 entity 精确匹配(公司代码/分析师)、向量召回(pgvector\n+ DashScope embedding)、trigram 模糊、ILIKE 精确四路召回,RRF 融合后用 reranker\n精排,叠时间衰减 + views 加权(views 主源优先)。\n\n- 搜\"锂电池\"会带出新能源产业链(正极/负极/碳酸锂/宁德时代…)\n- 搜\"芯片\"会带出存储芯片/洁净室/晶圆代工…\n- mode=exact 兼容老 ILIKE 行为,仅在精确关键词命中时返回。\n",
@@ -182,16 +157,6 @@ export const HELP = {
182
157
  "description": "Intraday turnover time series. Requires `sentiment:read` scope.",
183
158
  "example": "echopai sentiment turnover"
184
159
  },
185
- "signals.outcome": {
186
- "summary": "Outcome trace for a single signal",
187
- "description": "单条 signal 的细化结果追踪。",
188
- "example": "echopai signals outcome VALUE"
189
- },
190
- "signals.outcomes": {
191
- "summary": "Outcomes of analyst-view-derived signals",
192
- "description": "返回分析师 views 派生信号的实际结果(目标价命中 / 止损 / 到期),含命中时长、最大回撤等。",
193
- "example": "echopai signals outcomes"
194
- },
195
160
  "squawk.audio": {
196
161
  "summary": "Stream squawk TTS audio",
197
162
  "description": "返回 squawk 项的 TTS 音频(MP3)。直接 stream,可作为 `<audio>` src 用。",
@@ -165,72 +165,6 @@ export const OPERATIONS = {
165
165
  "additionalProperties": false
166
166
  }
167
167
  },
168
- "backtest.excess-distribution": {
169
- "cliKey": "backtest.excess-distribution",
170
- "cliName": "backtest excess-distribution",
171
- "method": "GET",
172
- "path": "/v1/backtest/excess-distribution",
173
- "description": "信号相对基准(沪深 300 / 中证 1000 等)持有期超额收益的直方分布。",
174
- "summary": "Excess-return distribution over a benchmark",
175
- "positional": [],
176
- "outputDefault": "json",
177
- "pagination": "none",
178
- "stream": false,
179
- "billable": false,
180
- "idempotencyRequired": false,
181
- "scopesAny": [],
182
- "sideEffect": "read",
183
- "dryRunSupported": false,
184
- "inputSchema": {
185
- "type": "object",
186
- "properties": {},
187
- "additionalProperties": false
188
- }
189
- },
190
- "backtest.rolling-win-rate": {
191
- "cliKey": "backtest.rolling-win-rate",
192
- "cliName": "backtest rolling-win-rate",
193
- "method": "GET",
194
- "path": "/v1/backtest/rolling-win-rate",
195
- "description": "信号在滚动窗口(默认 60 个交易日)内的胜率时间序列。",
196
- "summary": "Rolling win rate of a signal series",
197
- "positional": [],
198
- "outputDefault": "json",
199
- "pagination": "none",
200
- "stream": false,
201
- "billable": false,
202
- "idempotencyRequired": false,
203
- "scopesAny": [],
204
- "sideEffect": "read",
205
- "dryRunSupported": false,
206
- "inputSchema": {
207
- "type": "object",
208
- "properties": {},
209
- "additionalProperties": false
210
- }
211
- },
212
- "backtest.summary": {
213
- "cliKey": "backtest.summary",
214
- "cliName": "backtest summary",
215
- "method": "GET",
216
- "path": "/v1/backtest/summary",
217
- "description": "信号回测汇总:胜率、平均超额收益、Sharpe-like 评分、最大回撤等。",
218
- "summary": "Backtest summary metrics for a signal series",
219
- "positional": [],
220
- "outputDefault": "json",
221
- "pagination": "none",
222
- "stream": false,
223
- "billable": false,
224
- "idempotencyRequired": false,
225
- "scopesAny": [],
226
- "sideEffect": "read",
227
- "dryRunSupported": false,
228
- "inputSchema": {
229
- "type": "object",
230
- "properties": {},
231
- "additionalProperties": false
232
- }
233
- },
234
168
  "bars.daily": {
235
169
  "cliKey": "bars.daily",
236
170
  "cliName": "bars daily",
@@ -437,7 +371,7 @@ export const OPERATIONS = {
437
371
  "cliName": "digest get",
438
372
  "method": "GET",
439
373
  "path": "/v1/digest/{code}",
440
- "description": "Composite endpoint: in one HTTP call returns views (PRIMARY research),\nresearch-entity performance (quality layer), real-time quote, market\nsentiment context, and supplementary news. Partial-failure tolerant:\neach bucket is independently fetched and per-bucket failures surface\nin `meta.partial_failures[]` rather than poisoning the response. If\nevery sub-bucket fails the endpoint returns 502.\n\nBucket scopes are checked *per bucket* (not at gateway level): a\ntoken with only `views:read` gets the views bucket populated and the\nrest reported as `scope_insufficient` partial failures. This mirrors\nthe CLI fan-out contract exactly so `echopai digest` can either call\nthis endpoint (preferred, single round-trip) or fall back to local\nfan-out without behavior drift.\n\nSee `docs/PLAN_CLI_V2_AGENT_SURFACE.md` §3.3 (digest spec) and §11\nPhase 5.2 (server endpoint).\n",
374
+ "description": "Composite endpoint: in one HTTP call returns views (PRIMARY research),\nreal-time quote, valuation snapshot (PE/PB/PS/换手率/股息率/量比 14 字段),\nmarket sentiment context, and supplementary news.\nPartial-failure tolerant:\neach bucket is independently fetched and per-bucket failures surface\nin `meta.partial_failures[]` rather than poisoning the response. If\nevery sub-bucket fails the endpoint returns 502.\n\nBucket scopes are checked *per bucket* (not at gateway level): a\ntoken with only `views:read` gets the views bucket populated and the\nrest reported as `scope_insufficient` partial failures. This mirrors\nthe CLI fan-out contract exactly so `echopai digest` can either call\nthis endpoint (preferred, single round-trip) or fall back to local\nfan-out without behavior drift.\n\nSee `docs/PLAN_CLI_V2_AGENT_SURFACE.md` §3.3 (digest spec) and §11\nPhase 5.2 (server endpoint).\n",
441
375
  "summary": "One-shot research digest for a single security (composite)",
442
376
  "positional": [
443
377
  "code"
@@ -526,6 +460,46 @@ export const OPERATIONS = {
526
460
  ]
527
461
  }
528
462
  },
463
+ "financials.quote-snapshot": {
464
+ "cliKey": "financials.quote-snapshot",
465
+ "cliName": "financials quote-snapshot",
466
+ "method": "GET",
467
+ "path": "/v1/financials/quote-snapshot",
468
+ "description": "一站式估值快照 —— 一次返回 14 个字段,**全部基于 TDX 原始数据 + Sina 实时\n价自算,不依赖 Tushare**:\n\n- 估值: `pe` / `pe_ttm` / `pb` / `ps` / `ps_ttm`\n- 股本(万股): `total_share` / `float_share` / `free_share`\n- 市值(万元): `total_mv` / `circ_mv`\n- 流动性: `turnover_rate` / `turnover_rate_f`(%)/ `volume_ratio`(倍)\n- 分红: `dv_ratio` / `dv_ttm`(%)\n\n计算口径:\n- PE-TTM = 当前总市值 / 近 4 季度滚动净利润(PIT 表 `ni_parent_ttm`)\n- PE = 当前总市值 / 上年报净利润(`ni_parent_last1y`)\n- PB = 当前总市值 / 归母净资产(`equity_parent`,PIT 防穿越)\n- 换手率 = 当日累计成交量 / 流通股本 × 100\n- 量比 = 当日累计 / 近 5 日同时段累计平均(实时模式)\n / 近 5 日全日成交量平均(指定历史日期模式)\n- 股息率 = Σ(去年 / 近12月内派息事件 派现/10) / 现价 × 100\n\n默认 `date` 留空 → 实时(Sina 5s 快照价 + 当日累计成交量);\n给 `date=YYYY-MM-DD` 则按当日 close 取值(用于历史回测 / 校验)。\n\n财报数据走 `security_financial_indicators_pit` PIT 视图:保证 `visible_date`\n≤ trade_date,避免未来函数。\n\n准确性已对照 Tushare `daily_basic` 校验(见 `scripts/validation/validate_quote_snapshot_vs_tushare.py`)。\n",
469
+ "summary": "Real-time valuation / share / turnover snapshot for one A-share",
470
+ "positional": [],
471
+ "outputDefault": "json",
472
+ "pagination": "none",
473
+ "stream": false,
474
+ "billable": true,
475
+ "idempotencyRequired": false,
476
+ "scopesAny": [
477
+ "financials:read"
478
+ ],
479
+ "sideEffect": "read",
480
+ "dryRunSupported": false,
481
+ "inputSchema": {
482
+ "type": "object",
483
+ "properties": {
484
+ "code": {
485
+ "type": "string",
486
+ "pattern": "^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$",
487
+ "description": "Canonical A-share code.",
488
+ "example": "SSE:600519"
489
+ },
490
+ "date": {
491
+ "type": "string",
492
+ "format": "date",
493
+ "description": "Trade date YYYY-MM-DD; omit for real-time (Redis snapshot price).\n",
494
+ "example": "2026-05-13"
495
+ }
496
+ },
497
+ "additionalProperties": false,
498
+ "required": [
499
+ "code"
500
+ ]
501
+ }
502
+ },
529
503
  "financials.reports": {
530
504
  "cliKey": "financials.reports",
531
505
  "cliName": "financials reports",
@@ -642,7 +616,7 @@ export const OPERATIONS = {
642
616
  "cliName": "index daily-bars",
643
617
  "method": "GET",
644
618
  "path": "/v1/index/bars/daily",
645
- "description": "指数日线 OHLC。覆盖 v2 表中的指数(上证综指 000001.SH / 深证成指\n399001.SZ / 沪深300 000300.SH / 北证50 899050.BJ / 中证系列 000922.CSI 等)。\n与 `/v1/bars/daily` 同语义,但 index 没有 `turnover_rate` / `paused` 等股票特有字段。\n",
619
+ "description": "指数日线 OHLC。覆盖 v2 表中的指数(上证综指 `SSE:000001` / 深证成指\n`SZSE:399001` / 沪深300 `SSE:000300` / 北证50 `BSE:899050` / 中证系列 `CSI:000922` 等)。\n与 `/v1/bars/daily` 同语义,但 index 没有 `turnover_rate` / `paused` 等股票特有字段。\n",
646
620
  "summary": "Daily OHLC bars for one A-share index",
647
621
  "positional": [],
648
622
  "outputDefault": "json",
@@ -663,7 +637,7 @@ export const OPERATIONS = {
663
637
  "code": {
664
638
  "type": "string",
665
639
  "description": "Canonical index code.",
666
- "example": "000001.SH"
640
+ "example": "SSE:000001"
667
641
  },
668
642
  "from": {
669
643
  "type": "string",
@@ -712,7 +686,7 @@ export const OPERATIONS = {
712
686
  "code": {
713
687
  "type": "string",
714
688
  "description": "Canonical index code.",
715
- "example": "000001.SH"
689
+ "example": "SSE:000001"
716
690
  },
717
691
  "from": {
718
692
  "type": "string",
@@ -738,7 +712,7 @@ export const OPERATIONS = {
738
712
  "cliName": "index snapshot",
739
713
  "method": "GET",
740
714
  "path": "/v1/index/snapshot",
741
- "description": "Returns the latest real-time snapshot for the 172 indices that Sina\nexposes a quote for (SSE / SZSE / BSE — includes 北证50 `899050.BJ`\nand 专精特新 `899601.BJ`). Source is the Redis hash\n`stockpulse:index_snapshots:latest` which the Rust collector refreshes\nevery ~15 seconds during market hours; outside trading hours the last\nintraday snapshot is returned.\n\nUse `codes` to narrow to a subset (canonical format like `000001.SH`).\nOmit to receive all 172.\n\nNote: CSI-series indices (e.g. `000922.CSI`) are not available here —\nSina has no quote feed for them. Daily/minute history for those still\nlives behind `/api/internal/index/{daily-bars,minute-bars-range}`.\n\nRequires `quote:l1`, `quote:l2`, or `quote:delayed` scope.\n",
715
+ "description": "Returns the latest real-time snapshot for the 172 indices that Sina\nexposes a quote for (SSE / SZSE / BSE — includes 北证50 `BSE:899050`\nand 专精特新 `899601.BJ`). Source is the Redis hash\n`stockpulse:index_snapshots:latest` which the Rust collector refreshes\nevery ~15 seconds during market hours; outside trading hours the last\nintraday snapshot is returned.\n\nUse `codes` to narrow to a subset (canonical format `SSE:000001` (exchange-prefix; legacy `000001.SH` is also accepted but discouraged)).\nOmit to receive all 172.\n\nNote: CSI-series indices (e.g. `CSI:000922`) are not available here —\nSina has no quote feed for them. Daily/minute history for those still\nlives behind `/api/internal/index/{daily-bars,minute-bars-range}`.\n\nRequires `quote:l1`, `quote:l2`, or `quote:delayed` scope.\n",
742
716
  "summary": "Real-time snapshot of all Sina-OK A-share indices (172 indices)",
743
717
  "positional": [],
744
718
  "outputDefault": "json",
@@ -758,8 +732,8 @@ export const OPERATIONS = {
758
732
  "properties": {
759
733
  "codes": {
760
734
  "type": "string",
761
- "description": "Comma-separated canonical index codes (e.g. `000001.SH,399001.SZ,899050.BJ`).\nOmit to return all 172.\n",
762
- "example": "000001.SH,399001.SZ,899050.BJ"
735
+ "description": "Comma-separated canonical index codes (e.g. `SSE:000001,SZSE:399001,BSE:899050`).\nOmit to return all 172.\n",
736
+ "example": "SSE:000001,SZSE:399001,BSE:899050"
763
737
  }
764
738
  },
765
739
  "additionalProperties": false
@@ -943,28 +917,6 @@ export const OPERATIONS = {
943
917
  ]
944
918
  }
945
919
  },
946
- "news.sources": {
947
- "cliKey": "news.sources",
948
- "cliName": "news sources",
949
- "method": "GET",
950
- "path": "/v1/news/sources",
951
- "description": "返回新闻 source 标识列表(如 `cls`、`jin10`、`twitter`、`wallstreetcn`),可用于 list/search 的 source 过滤。",
952
- "summary": "List canonical news source identifiers",
953
- "positional": [],
954
- "outputDefault": "json",
955
- "pagination": "none",
956
- "stream": false,
957
- "billable": false,
958
- "idempotencyRequired": false,
959
- "scopesAny": [],
960
- "sideEffect": "read",
961
- "dryRunSupported": false,
962
- "inputSchema": {
963
- "type": "object",
964
- "properties": {},
965
- "additionalProperties": false
966
- }
967
- },
968
920
  "payment.plans": {
969
921
  "cliKey": "payment.plans",
970
922
  "cliName": "payment plans",
@@ -1075,60 +1027,6 @@ export const OPERATIONS = {
1075
1027
  "additionalProperties": false
1076
1028
  }
1077
1029
  },
1078
- "research.entity-performance": {
1079
- "cliKey": "research.entity-performance",
1080
- "cliName": "research entity-performance",
1081
- "method": "GET",
1082
- "path": "/v1/research-entities/{entity_id}/performance",
1083
- "description": "单个研究实体的细化绩效指标,对照 `/v1/research-entities/performance`。",
1084
- "summary": "Performance of one research entity",
1085
- "positional": [
1086
- "entity_id"
1087
- ],
1088
- "outputDefault": "json",
1089
- "pagination": "none",
1090
- "stream": false,
1091
- "billable": false,
1092
- "idempotencyRequired": false,
1093
- "scopesAny": [],
1094
- "sideEffect": "read",
1095
- "dryRunSupported": false,
1096
- "inputSchema": {
1097
- "type": "object",
1098
- "properties": {
1099
- "entity_id": {
1100
- "type": "string",
1101
- "description": "Research-entity id (returned by research-entities.list / matcher)."
1102
- }
1103
- },
1104
- "additionalProperties": false,
1105
- "required": [
1106
- "entity_id"
1107
- ]
1108
- }
1109
- },
1110
- "research.entity-performance-list": {
1111
- "cliKey": "research.entity-performance-list",
1112
- "cliName": "research entity-performance-list",
1113
- "method": "GET",
1114
- "path": "/v1/research-entities/performance",
1115
- "description": "全体研究实体(analyst / team / institution)的命中率、平均目标价达成率、覆盖股票数等汇总。需要 `research:read` scope。",
1116
- "summary": "Aggregate performance of research entities",
1117
- "positional": [],
1118
- "outputDefault": "json",
1119
- "pagination": "none",
1120
- "stream": false,
1121
- "billable": false,
1122
- "idempotencyRequired": false,
1123
- "scopesAny": [],
1124
- "sideEffect": "read",
1125
- "dryRunSupported": false,
1126
- "inputSchema": {
1127
- "type": "object",
1128
- "properties": {},
1129
- "additionalProperties": false
1130
- }
1131
- },
1132
1030
  "search.semantic": {
1133
1031
  "cliKey": "search.semantic",
1134
1032
  "cliName": "search semantic",
@@ -1408,60 +1306,6 @@ export const OPERATIONS = {
1408
1306
  "additionalProperties": false
1409
1307
  }
1410
1308
  },
1411
- "signals.outcome": {
1412
- "cliKey": "signals.outcome",
1413
- "cliName": "signals outcome",
1414
- "method": "GET",
1415
- "path": "/v1/signals/{signal_id}/outcome",
1416
- "description": "单条 signal 的细化结果追踪。",
1417
- "summary": "Outcome trace for a single signal",
1418
- "positional": [
1419
- "signal_id"
1420
- ],
1421
- "outputDefault": "json",
1422
- "pagination": "none",
1423
- "stream": false,
1424
- "billable": false,
1425
- "idempotencyRequired": false,
1426
- "scopesAny": [],
1427
- "sideEffect": "read",
1428
- "dryRunSupported": false,
1429
- "inputSchema": {
1430
- "type": "object",
1431
- "properties": {
1432
- "signal_id": {
1433
- "type": "string",
1434
- "description": "Signal id (returned by signals/outcomes items[].signal_id)."
1435
- }
1436
- },
1437
- "additionalProperties": false,
1438
- "required": [
1439
- "signal_id"
1440
- ]
1441
- }
1442
- },
1443
- "signals.outcomes": {
1444
- "cliKey": "signals.outcomes",
1445
- "cliName": "signals outcomes",
1446
- "method": "GET",
1447
- "path": "/v1/signals/outcomes",
1448
- "description": "返回分析师 views 派生信号的实际结果(目标价命中 / 止损 / 到期),含命中时长、最大回撤等。",
1449
- "summary": "Outcomes of analyst-view-derived signals",
1450
- "positional": [],
1451
- "outputDefault": "json",
1452
- "pagination": "offset",
1453
- "stream": false,
1454
- "billable": false,
1455
- "idempotencyRequired": false,
1456
- "scopesAny": [],
1457
- "sideEffect": "read",
1458
- "dryRunSupported": false,
1459
- "inputSchema": {
1460
- "type": "object",
1461
- "properties": {},
1462
- "additionalProperties": false
1463
- }
1464
- },
1465
1309
  "squawk.audio": {
1466
1310
  "cliKey": "squawk.audio",
1467
1311
  "cliName": "squawk audio",
package/dist/bin.js CHANGED
@@ -20,20 +20,21 @@ import { invoke } from "./runtime/invoker.js";
20
20
  import { buildApiCommand } from "./tools/api.js";
21
21
  import { buildMcpCommand } from "./tools/mcp.js";
22
22
  import { buildRawCallCommand } from "./tools/raw.js";
23
- import { buildBarsBatchCommand, buildChartCommand, buildDigestCommand, buildHotCommand, buildLookupCommand, buildNewsCommand, buildQuoteCommand, buildResearchCommand, buildScanCommand, buildSearchCommand, buildSentimentCommand, buildViewsCommand, } from "./verbs/index.js";
23
+ import { buildBarsBatchCommand, buildChartCommand, buildDigestCommand, buildFinancialsCommand, buildHotCommand, buildLookupCommand, buildNewsCommand, buildQuoteCommand, buildScanCommand, buildSearchCommand, buildSentimentCommand, buildViewsCommand, } from "./verbs/index.js";
24
24
  import { buildCompletionCommand } from "./tools/completion.js";
25
25
  import { buildConfigCommand } from "./tools/config.js";
26
26
  import { buildLoginCommand, buildLogoutCommand, buildStatusCommand } from "./tools/login.js";
27
27
  import { buildDoctorCommand } from "./tools/doctor.js";
28
28
  import { buildSchemaCommand } from "./tools/schema.js";
29
29
  import { buildTraceCommand } from "./tools/trace.js";
30
+ import { buildWelcomeCommand, printWelcome, shouldShowWelcome } from "./tools/welcome.js";
30
31
  import { buildWhoamiCommand } from "./tools/whoami.js";
31
32
  import { CLI_VERSION } from "./version.js";
32
33
  const program = new Command();
33
34
  program
34
35
  .name("echopai")
35
36
  .description("EchoPai CLI v1 — partner-grade access to stock-market data, news, sentiment,\n" +
36
- "views, signals, backtest.\n\n" +
37
+ "and analyst views.\n\n" +
37
38
  "AI-First: JSON envelope on stdout, JSON errors on stderr, three-state exit\n" +
38
39
  "codes (0 success / 1 user error / 2 service error). Set ECHOPAI_KEY env\n" +
39
40
  "var or run `echopai login`.")
@@ -53,6 +54,7 @@ program
53
54
  .addHelpText("after", "\nAuthentication: ECHOPAI_KEY=<eps_live_<lookup>_<secret>> echopai <cmd>\n" +
54
55
  "Raw mirror: echopai raw <noun> <verb> # all OpenAPI ops, e.g. raw news search\n" +
55
56
  "Curated verbs: echopai <verb> # task-level entry (Phase 3.6+)\n" +
57
+ "Fundamentals: echopai financials quote-snapshot --code SSE:600519 # PE/PB/PS/换手率/股息率/量比 14-field snapshot\n" +
56
58
  "Capability: echopai whoami | echopai doctor\n" +
57
59
  "Schema dump: echopai schema export # AI agents pipe this for command surface\n" +
58
60
  "Profile: echopai config profile add prod --base-url https://api.echopai.com\n");
@@ -100,7 +102,7 @@ function applyAiFirstHooks(cmd) {
100
102
  //
101
103
  // Phase 3.7+ — `raw` is now the only place to find spec-driven operations.
102
104
  // Top-level previously held spec mirrors but starting this PR the curated
103
- // verbs (`views` / `news` / `quote` / `sentiment` / `research` / `hot` /
105
+ // verbs (`views` / `news` / `quote` / `sentiment` / `hot` /
104
106
  // `lookup` / `digest` etc.) take precedence at the top level. Use `echopai raw <noun>
105
107
  // <verb>` for the raw mirror (e.g. `raw views feed`, `raw quote`, etc.).
106
108
  const rawCmd = new Command("raw").description("Raw 1:1 mirror of OpenAPI operations (escape hatch for power users / scripts)");
@@ -108,7 +110,7 @@ buildCommandTree(rawCmd, dispatch);
108
110
  rawCmd.addCommand(buildRawCallCommand());
109
111
  program.addCommand(rawCmd);
110
112
  // Top-level spec commands kept ONLY for nouns that don't conflict with a
111
- // curated verb. Curated-verb nouns (views/news/quote/sentiment/research/hot
113
+ // curated verb. Curated-verb nouns (views/news/quote/sentiment/hot
112
114
  // /lookup/digest) are NOT generated at top level — they go below in their canonical
113
115
  // curated form. Use `echopai raw <noun>` for the spec mirror.
114
116
  const CURATED_NOUNS = new Set([
@@ -116,11 +118,11 @@ const CURATED_NOUNS = new Set([
116
118
  "news",
117
119
  "quote",
118
120
  "sentiment",
119
- "research",
120
121
  "hot",
121
122
  "lookup",
122
123
  "digest",
123
124
  "search",
125
+ "financials",
124
126
  ]);
125
127
  const topLevelSpecDispatch = async (op, args) => {
126
128
  await dispatch(op, args);
@@ -135,19 +137,19 @@ for (const sub of topLevelStub.commands) {
135
137
  // Curated agent verbs (Phase 3.6+): task-level entries that wrap one or
136
138
  // more raw operations + map output for AI agents. Listed before tools so
137
139
  // `--help` shows them first.
138
- // Product priority (see feedback_views_over_news.md): views > research > news
140
+ // Product priority (see feedback_views_over_news.md): views > news
139
141
  program.addCommand(buildLookupCommand());
140
142
  program.addCommand(buildDigestCommand()); // one-shot fan-out (Phase 5.1)
141
143
  program.addCommand(buildSearchCommand()); // hybrid semantic discovery (PLAN_SEARCH_SEMANTIC_UPGRADE)
142
144
  program.addCommand(buildQuoteCommand());
143
145
  program.addCommand(buildViewsCommand()); // PRIMARY research source
144
- program.addCommand(buildResearchCommand()); // views quality signal layer
145
146
  program.addCommand(buildNewsCommand()); // supplementary breadth
146
147
  program.addCommand(buildSentimentCommand());
147
148
  program.addCommand(buildHotCommand());
148
149
  program.addCommand(buildChartCommand()); // single-code K-line
149
150
  program.addCommand(buildBarsBatchCommand()); // multi-code batch K-line (PR 3.2/3.3 server)
150
151
  program.addCommand(buildScanCommand()); // full-market scan (PR 3.4 server)
152
+ program.addCommand(buildFinancialsCommand()); // fundamentals: quote-snapshot / pit / reports / series
151
153
  // hand-curated tools
152
154
  program.addCommand(buildLoginCommand());
153
155
  program.addCommand(buildLogoutCommand());
@@ -160,7 +162,14 @@ program.addCommand(buildWhoamiCommand());
160
162
  program.addCommand(buildDoctorCommand());
161
163
  program.addCommand(buildMcpCommand());
162
164
  program.addCommand(buildCompletionCommand());
165
+ program.addCommand(buildWelcomeCommand());
163
166
  applyAiFirstHooks(program);
167
+ // Bare `echopai` in a TTY → onboarding screen. Non-TTY / CI / piped output
168
+ // falls through to commander's plain help (preserves AI-first invariant).
169
+ if (shouldShowWelcome(process.argv)) {
170
+ printWelcome();
171
+ process.exit(0);
172
+ }
164
173
  program.parseAsync(process.argv).catch((e) => {
165
174
  process.stderr.write(JSON.stringify({
166
175
  error: {
@@ -0,0 +1,190 @@
1
+ /**
2
+ * `echopai` (bare) / `echopai welcome` — onboarding screen.
3
+ *
4
+ * Goal: give a TTY user a Claude-Code-style landing page when they type
5
+ * `echopai` with no args. Banner → auth status → grouped command examples
6
+ * → pointers to deeper help. AI / CI / non-TTY callers fall back to
7
+ * commander's plain help (handled by the bin.ts dispatcher).
8
+ *
9
+ * No network calls. We probe credentials locally (env / config file) only —
10
+ * a live `whoami` would slow the screen and is the wrong default for the
11
+ * "user just typed `echopai`" path. `echopai status` / `echopai whoami`
12
+ * cover the live-check use case.
13
+ */
14
+ import { Command } from "commander";
15
+ import { resolveCredentials, AuthMissingError } from "../runtime/auth.js";
16
+ import { bold, cyan, dim, green, isTtyHuman, yellow } from "../runtime/tty.js";
17
+ import { CLI_VERSION } from "../version.js";
18
+ function maskKey(key) {
19
+ // eps_live_<lookup>_<secret> → keep prefix + first 4 of lookup + masked tail.
20
+ if (key.length <= 16)
21
+ return "***";
22
+ return key.slice(0, 13) + "***";
23
+ }
24
+ function snapshotAuth() {
25
+ try {
26
+ const creds = resolveCredentials({});
27
+ if (creds.profile) {
28
+ return {
29
+ state: "profile",
30
+ profile: creds.profile,
31
+ keyHint: maskKey(creds.key),
32
+ baseUrl: creds.baseUrl,
33
+ };
34
+ }
35
+ return { state: "env", keyHint: maskKey(creds.key), baseUrl: creds.baseUrl };
36
+ }
37
+ catch (e) {
38
+ if (e instanceof AuthMissingError)
39
+ return { state: "missing" };
40
+ throw e;
41
+ }
42
+ }
43
+ /** Right-pad a string to width `w`, ignoring ANSI escapes for length. */
44
+ function padRight(s, w) {
45
+ // strip ANSI for length measurement
46
+ // eslint-disable-next-line no-control-regex
47
+ const visible = s.replace(/\x1b\[[0-9;]*m/g, "");
48
+ const pad = Math.max(0, w - visible.length);
49
+ return s + " ".repeat(pad);
50
+ }
51
+ const COMMAND_GROUPS = [
52
+ {
53
+ title: "🔍 快速检索",
54
+ items: [
55
+ { cmd: "echopai lookup --text 茅台", note: "中文名 / 拼音首字母 → canonical_code" },
56
+ { cmd: "echopai digest --code SSE:600519", note: "一键研究摘要(5 桶 fan-out,容忍部分失败)" },
57
+ { cmd: "echopai search --query \"AI 算力\"", note: "题材 / 概念语义搜索" },
58
+ ],
59
+ },
60
+ {
61
+ title: "📈 实时行情",
62
+ items: [
63
+ { cmd: "echopai quote --codes \"SSE:600519,SZSE:000001\"", note: "1-200 只实时报价" },
64
+ { cmd: "echopai sentiment", note: "市场情绪总览(涨跌停 / 宽度 / 分化)" },
65
+ { cmd: "echopai hot", note: "东方财富热门股榜(综合搜索 / 关注 / 评论)" },
66
+ { cmd: "echopai scan", note: "全市场快照(约 5800 只)" },
67
+ ],
68
+ },
69
+ {
70
+ title: "💰 基本面 / 估值",
71
+ items: [
72
+ { cmd: "echopai financials quote-snapshot --code SSE:600519", note: "估值快照 14 字段(PE/PB/PS/换手率/股息率/量比)★头牌★" },
73
+ { cmd: "echopai financials pit --code SSE:600519 --date 2024-11-01", note: "Point-in-time 财务指标(回测防穿越)" },
74
+ { cmd: "echopai financials reports --code SSE:600519 --kind annual", note: "最近 N 期财务报告(~25 字段/期)" },
75
+ { cmd: "echopai financials series --code SSE:600519 --metric roe_simple", note: "单指标历史时间序列" },
76
+ ],
77
+ },
78
+ {
79
+ title: "📰 研究 / 资讯",
80
+ items: [
81
+ { cmd: "echopai views --code SSE:600519", note: "★主源★ 卖方研报 / 分析师观点(含 research_entity_id 归因)" },
82
+ { cmd: "echopai news search --query AI --since-hours 24", note: "辅源新闻 / 简讯(短时窗)" },
83
+ ],
84
+ },
85
+ {
86
+ title: "📊 K 线",
87
+ items: [
88
+ { cmd: "echopai chart --code SSE:600519 --from 2026-01-01 --to 2026-05-01", note: "单只 K 线(日线 / 分钟线)" },
89
+ { cmd: "echopai bars-batch --codes A,B,C --from ... --to ...", note: "批量 K 线(≤100 只)" },
90
+ ],
91
+ },
92
+ {
93
+ title: "🤖 AI 集成",
94
+ items: [
95
+ { cmd: "echopai mcp serve", note: "启动 MCP stdio 服务(Claude Desktop / Cursor / Claude Code)" },
96
+ { cmd: "echopai schema export", note: "导出全部 operation schema 给 agent 喂养" },
97
+ ],
98
+ },
99
+ ];
100
+ const POINTERS = [
101
+ { cmd: "echopai --help", note: "完整命令树" },
102
+ { cmd: "echopai <verb> --help", note: "查看单个命令的参数和示例" },
103
+ { cmd: "echopai whoami", note: "在线检查 token 能力 / scopes" },
104
+ { cmd: "echopai doctor", note: "诊断鉴权 / 网络连通性" },
105
+ { cmd: "echopai status", note: "查看本地 profile + 鉴权状态" },
106
+ ];
107
+ const BANNER_PLAIN = [
108
+ " ███████ ██████ ██ ██ ██████ ██████ █████ ██",
109
+ " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██",
110
+ " █████ ██ ███████ ██ ██ ██████ ███████ ██",
111
+ " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██",
112
+ " ███████ ██████ ██ ██ ██████ ██ ██ ██ ██",
113
+ ];
114
+ export function renderWelcome(now = () => new Date()) {
115
+ const lines = [];
116
+ const auth = snapshotAuth();
117
+ // Banner
118
+ lines.push("");
119
+ for (const row of BANNER_PLAIN)
120
+ lines.push(cyan(row));
121
+ lines.push("");
122
+ lines.push(` ${bold("EchoPai CLI")} ${dim("v" + CLI_VERSION)} ${dim("·")} ${dim("面向 AI Agent 的 A 股数据接入终端")}`);
123
+ lines.push("");
124
+ // Auth status
125
+ if (auth.state === "missing") {
126
+ lines.push(` ${yellow("●")} ${bold("未登录")} ${dim("—")} 运行 ${cyan("`echopai login --key eps_live_<lookup>_<secret>`")} 或设置环境变量 ${cyan("ECHOPAI_KEY")}`);
127
+ }
128
+ else if (auth.state === "env") {
129
+ lines.push(` ${green("●")} ${bold("已登录")}(环境变量 ${cyan("ECHOPAI_KEY")})${dim(`(${auth.keyHint})`)} → ${dim(auth.baseUrl)}`);
130
+ }
131
+ else {
132
+ lines.push(` ${green("●")} ${bold("已登录")} profile ${cyan(auth.profile)} ${dim(`(${auth.keyHint})`)} → ${dim(auth.baseUrl)}`);
133
+ }
134
+ lines.push("");
135
+ // Command groups
136
+ lines.push(` ${dim("── 常用命令 ─────────────────────────────────────────────────")}`);
137
+ lines.push("");
138
+ // Reserve column 1 for cmd, column 2 for note. Compute padding from the
139
+ // widest cmd across all groups to keep alignment stable.
140
+ const widest = Math.min(72, COMMAND_GROUPS.flatMap((g) => g.items)
141
+ .map((it) => it.cmd.length)
142
+ .reduce((a, b) => Math.max(a, b), 0));
143
+ for (const group of COMMAND_GROUPS) {
144
+ lines.push(` ${bold(group.title)}`);
145
+ for (const it of group.items) {
146
+ const cmd = cyan(it.cmd);
147
+ lines.push(` ${padRight(cmd, widest + 4)} ${dim(it.note)}`);
148
+ }
149
+ lines.push("");
150
+ }
151
+ // Pointers
152
+ lines.push(` ${dim("── 更多 ─────────────────────────────────────────────────────")}`);
153
+ lines.push("");
154
+ for (const it of POINTERS) {
155
+ lines.push(` ${padRight(cyan(it.cmd), widest + 4)} ${dim(it.note)}`);
156
+ }
157
+ lines.push("");
158
+ lines.push(` ${dim("文档: https://docs.echopai.com · 反馈: https://github.com/evanzhangx/EchoPulse/issues")}`);
159
+ lines.push("");
160
+ // Timestamp footer so users know this isn't a stale cache
161
+ const ts = now().toISOString().replace("T", " ").slice(0, 19);
162
+ lines.push(` ${dim("生成于 " + ts + " UTC · Ctrl+C 退出")}`);
163
+ lines.push("");
164
+ return lines.join("\n");
165
+ }
166
+ /** Print welcome screen to stdout (no exit — caller decides). */
167
+ export function printWelcome() {
168
+ process.stdout.write(renderWelcome());
169
+ }
170
+ /**
171
+ * Decide whether bare `echopai` (no args, no flags) should show the welcome
172
+ * screen. False for AI / CI / piped output — they get commander's plain help.
173
+ */
174
+ export function shouldShowWelcome(argv) {
175
+ // node + script + (no extra args)
176
+ if (argv.length !== 2)
177
+ return false;
178
+ if (!isTtyHuman)
179
+ return false;
180
+ if (process.env.CI)
181
+ return false;
182
+ return true;
183
+ }
184
+ export function buildWelcomeCommand() {
185
+ return new Command("welcome")
186
+ .description("显示欢迎屏(banner + 常用命令 + 鉴权状态)")
187
+ .action(() => {
188
+ printWelcome();
189
+ });
190
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Killer "one-shot research" verb:
5
5
  *
6
- * one canonical_code → views + research + quote + sentiment + news
6
+ * one canonical_code → views + quote + snapshot + sentiment + news
7
7
  *
8
8
  * Plan-doc §3.3 makes this the "agent's opening move": one call returns the
9
9
  * full picture of a security. Buckets stay separated (no ranking imposed) so
@@ -24,11 +24,13 @@
24
24
  *
25
25
  * Why these 5 ops in the fallback:
26
26
  * - views.recent : PRIMARY research signal (analyst opinions on this code)
27
- * - research.entity-performance-list : quality layer for view authors
28
27
  * - quote : current price / change %
28
+ * - financials.quote-snapshot : 14-field valuation (PE/PB/PS/换手/股息/量比) on this code
29
29
  * - sentiment.overview : market regime context (not per-code; intentional —
30
30
  * no per-code sentiment op exists)
31
- * - news.search : SUPPLEMENTARY breadth (event stream filter on code)
31
+ * - news.list : SUPPLEMENTARY breadth (canonical_code-exact news,
32
+ * via news_item_security mapping table — NOT keyword search,
33
+ * since article text never contains 'SSE:600519' literally)
32
34
  *
33
35
  * Windows differ on purpose: views=168h (7d), news=24h. Encodes the rule
34
36
  * that research time-horizon > news time-horizon.
@@ -40,11 +42,11 @@ import { CallApiError } from "../runtime/errors.js";
40
42
  import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
41
43
  import { callOp } from "../runtime/verb_runner.js";
42
44
  /** Buckets in the order the description establishes (views first, news last). */
43
- const BUCKETS = ["views", "research", "quote", "sentiment", "news"];
45
+ const BUCKETS = ["views", "quote", "snapshot", "sentiment", "news"];
44
46
  const CODE_REGEX = /^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/;
45
47
  export const digestSpec = {
46
48
  name: "digest",
47
- description: "One-shot research digest for a single security: fan-out across views (PRIMARY), research-entity performance, quote, market sentiment, and supplementary news. Returns separated buckets — does NOT rank views vs news for the agent. Partial failures surface in meta.partial_failures[] without poisoning successful buckets. Use as the agent's first call when asked 'what's going on with <stock>'.",
49
+ description: "One-shot research digest for a single security: fan-out across views (PRIMARY), quote, 14-field valuation snapshot (PE/PB/PS/turnover/dividend/volume-ratio), market sentiment, and supplementary news. Returns separated buckets — does NOT rank views vs news for the agent. Partial failures surface in meta.partial_failures[] without poisoning successful buckets. Use as the agent's first call when asked 'what's going on with <stock>'.",
48
50
  inputSchema: {
49
51
  code: z
50
52
  .string()
@@ -70,7 +72,7 @@ export const digestSpec = {
70
72
  .min(1)
71
73
  .max(50)
72
74
  .default(10)
73
- .describe("Per-bucket item cap (views/news). Quote/sentiment/research aren't paginated here."),
75
+ .describe("Per-bucket item cap (views/news). Quote/snapshot/sentiment aren't paginated here."),
74
76
  },
75
77
  handler: async (args, ctx) => runDigest(args, ctx),
76
78
  // Backing ops are listed in two groups: digest.get is the primary
@@ -84,10 +86,10 @@ export const digestSpec = {
84
86
  backingOps: [
85
87
  "digest.get",
86
88
  "views.recent",
87
- "research.entity-performance-list",
88
89
  "quote",
90
+ "financials.quote-snapshot",
89
91
  "sentiment.overview",
90
- "news.search",
92
+ "news.list",
91
93
  ],
92
94
  };
93
95
  /**
@@ -153,10 +155,10 @@ function shouldFallbackToFanout(e) {
153
155
  function buildBucketCalls(args, ctx) {
154
156
  const opsRequired = {
155
157
  views: "views.recent",
156
- research: "research.entity-performance-list",
157
158
  quote: "quote",
159
+ snapshot: "financials.quote-snapshot",
158
160
  sentiment: "sentiment.overview",
159
- news: "news.search",
161
+ news: "news.list",
160
162
  };
161
163
  const calls = [];
162
164
  for (const bucket of BUCKETS) {
@@ -184,18 +186,18 @@ function buildBucketCalls(args, ctx) {
184
186
  limit: args.limit_per_bucket,
185
187
  };
186
188
  break;
187
- case "research":
188
- opArgs = {};
189
- break;
190
189
  case "quote":
191
190
  opArgs = { codes: [args.code] };
192
191
  break;
192
+ case "snapshot":
193
+ opArgs = { code: args.code };
194
+ break;
193
195
  case "sentiment":
194
196
  opArgs = { scope: "all_a_ex_st" };
195
197
  break;
196
198
  case "news":
197
199
  opArgs = {
198
- query: args.code,
200
+ security: args.code,
199
201
  since_hours: args.news_hours,
200
202
  limit: args.limit_per_bucket,
201
203
  };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * `echopai financials ...` + MCP tools `financials_quote_snapshot` / `financials_pit`
3
+ * / `financials_reports` / `financials_series`.
4
+ *
5
+ * Headline endpoint is **quote-snapshot** — one-call 14-field valuation snapshot
6
+ * (PE / PB / PS / 换手率 / 股息率 / 量比 等), TDX + Sina 自算口径,对照过 Tushare。
7
+ *
8
+ * Three sibling endpoints surface deeper financials access for backtests /
9
+ * fundamentals research:
10
+ * - pit — point-in-time indicators at a given trade_date (anti-future-fn)
11
+ * - reports — last N report-period snapshots (~25 fields each)
12
+ * - series — single-metric time series across periods
13
+ *
14
+ * MCP registers four discrete tools so an agent can pick the right granularity;
15
+ * CLI surfaces a single `financials` noun with four sub-commands (preserves
16
+ * the spec-driven shape from openapi.yaml).
17
+ */
18
+ import { Command, Option } from "commander";
19
+ import { z } from "zod";
20
+ import { OPERATIONS } from "../_generated/operations.js";
21
+ import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
22
+ import { callOp } from "../runtime/verb_runner.js";
23
+ const CODE_RE = /^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/;
24
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
25
+ const REPORT_KINDS = ["Q1", "H1", "Q3", "annual", "preliminary"];
26
+ export const financialsQuoteSnapshotSpec = {
27
+ name: "financials_quote_snapshot",
28
+ description: "One-call 14-field valuation snapshot for one A-share: PE / PE-TTM / PB / PS / PS-TTM, total/float/free share, total/circ market cap, turnover rate (raw & float), volume ratio, dividend yield (last-year & TTM). Computed from TDX + Sina real-time (no Tushare dependency), validated against Tushare daily_basic. Use `date` to fetch a historical snapshot; omit for real-time.",
29
+ inputSchema: {
30
+ code: z
31
+ .string()
32
+ .regex(CODE_RE)
33
+ .describe("Canonical A-share code (e.g. SSE:600519)"),
34
+ date: z
35
+ .string()
36
+ .regex(DATE_RE)
37
+ .optional()
38
+ .describe("Trade date YYYY-MM-DD; omit for real-time snapshot"),
39
+ },
40
+ handler: async (args, ctx) => {
41
+ const op = OPERATIONS["financials.quote-snapshot"];
42
+ if (!op)
43
+ throw new Error("financials.quote-snapshot op missing from codegen");
44
+ const callArgs = { code: args.code };
45
+ if (args.date)
46
+ callArgs.date = args.date;
47
+ return callOp(op, callArgs, ctx);
48
+ },
49
+ backingOps: ["financials.quote-snapshot"],
50
+ };
51
+ export const financialsPitSpec = {
52
+ name: "financials_pit",
53
+ description: "Point-in-time financial indicators for one A-share at a given trade_date. Returns the latest report visible AS OF that date (announce_date ≤ date; conservative 90-day fallback when announce_date is missing). Designed for backtests / AI agents to avoid look-ahead bias. ~25 fields incl. EPS / BPS / ROE / margins / revenue / net-income / equity.",
54
+ inputSchema: {
55
+ code: z.string().regex(CODE_RE).describe("Canonical A-share code"),
56
+ date: z
57
+ .string()
58
+ .regex(DATE_RE)
59
+ .optional()
60
+ .describe("Trade date YYYY-MM-DD; defaults to today"),
61
+ },
62
+ handler: async (args, ctx) => {
63
+ const op = OPERATIONS["financials.pit"];
64
+ if (!op)
65
+ throw new Error("financials.pit op missing from codegen");
66
+ const callArgs = { code: args.code };
67
+ if (args.date)
68
+ callArgs.date = args.date;
69
+ return callOp(op, callArgs, ctx);
70
+ },
71
+ backingOps: ["financials.pit"],
72
+ };
73
+ export const financialsReportsSpec = {
74
+ name: "financials_reports",
75
+ description: "Last N report-period snapshots for one A-share (~25 fields per period). Each item carries `announce_date` for visibility timing. Filter by `kind` to scope to Q1 / H1 / Q3 / annual / preliminary.",
76
+ inputSchema: {
77
+ code: z.string().regex(CODE_RE).describe("Canonical A-share code"),
78
+ limit: z
79
+ .number()
80
+ .int()
81
+ .min(1)
82
+ .max(100)
83
+ .default(12)
84
+ .describe("Max periods (1-100, default 12)"),
85
+ kind: z.enum(REPORT_KINDS).optional().describe("Filter by report kind"),
86
+ },
87
+ handler: async (args, ctx) => {
88
+ const op = OPERATIONS["financials.reports"];
89
+ if (!op)
90
+ throw new Error("financials.reports op missing from codegen");
91
+ const callArgs = { code: args.code, limit: args.limit };
92
+ if (args.kind)
93
+ callArgs.kind = args.kind;
94
+ return callOp(op, callArgs, ctx);
95
+ },
96
+ backingOps: ["financials.reports"],
97
+ };
98
+ export const financialsSeriesSpec = {
99
+ name: "financials_series",
100
+ description: "Time series of a single financial metric for one A-share across reporting periods. `metric` is an indicators-table field name (roe_simple / revenue / ni_parent / debt_asset_ratio / gross_margin / eps_basic, ~150 supported).",
101
+ inputSchema: {
102
+ code: z.string().regex(CODE_RE).describe("Canonical A-share code"),
103
+ metric: z
104
+ .string()
105
+ .min(1)
106
+ .max(64)
107
+ .describe("Indicator field name (e.g. roe_simple, revenue, ni_parent)"),
108
+ from: z.string().regex(DATE_RE).optional().describe("Inclusive earliest report_date"),
109
+ to: z.string().regex(DATE_RE).optional().describe("Inclusive latest report_date"),
110
+ limit: z
111
+ .number()
112
+ .int()
113
+ .min(1)
114
+ .max(200)
115
+ .default(40)
116
+ .describe("Max points (1-200, default 40)"),
117
+ },
118
+ handler: async (args, ctx) => {
119
+ const op = OPERATIONS["financials.series"];
120
+ if (!op)
121
+ throw new Error("financials.series op missing from codegen");
122
+ const callArgs = {
123
+ code: args.code,
124
+ metric: args.metric,
125
+ limit: args.limit,
126
+ };
127
+ if (args.from)
128
+ callArgs.from = args.from;
129
+ if (args.to)
130
+ callArgs.to = args.to;
131
+ return callOp(op, callArgs, ctx);
132
+ },
133
+ backingOps: ["financials.series"],
134
+ };
135
+ function clampInt(raw, min, max, fallback) {
136
+ const n = Math.floor(Number(raw));
137
+ if (!Number.isFinite(n))
138
+ return fallback;
139
+ if (n < min)
140
+ return min;
141
+ if (n > max)
142
+ return max;
143
+ return n;
144
+ }
145
+ export function buildFinancialsCommand() {
146
+ const cmd = new Command("financials").description("Fundamentals — valuation snapshot, point-in-time indicators, recent reports, single-metric time series.");
147
+ const qs = cmd
148
+ .command("quote-snapshot")
149
+ .description(financialsQuoteSnapshotSpec.description);
150
+ qs.addOption(new Option("--code <canonical_code>", "Canonical A-share code (e.g. SSE:600519)")
151
+ .makeOptionMandatory(true));
152
+ qs.addOption(new Option("--date <YYYY-MM-DD>", "Trade date; omit for real-time"));
153
+ qs.action(async (opts) => {
154
+ if (!OPERATIONS["financials.quote-snapshot"]) {
155
+ emitVerbError("internal_error", "financials.quote-snapshot missing", undefined, 2);
156
+ }
157
+ const args = { code: opts.code };
158
+ if (opts.date)
159
+ args.date = opts.date;
160
+ await executeVerb(async (ctx) => financialsQuoteSnapshotSpec.handler(args, ctx));
161
+ });
162
+ const pit = cmd.command("pit").description(financialsPitSpec.description);
163
+ pit.addOption(new Option("--code <canonical_code>", "Canonical A-share code").makeOptionMandatory(true));
164
+ pit.addOption(new Option("--date <YYYY-MM-DD>", "Trade date; defaults to today"));
165
+ pit.action(async (opts) => {
166
+ if (!OPERATIONS["financials.pit"]) {
167
+ emitVerbError("internal_error", "financials.pit missing", undefined, 2);
168
+ }
169
+ const args = { code: opts.code };
170
+ if (opts.date)
171
+ args.date = opts.date;
172
+ await executeVerb(async (ctx) => financialsPitSpec.handler(args, ctx));
173
+ });
174
+ const reports = cmd.command("reports").description(financialsReportsSpec.description);
175
+ reports.addOption(new Option("--code <canonical_code>", "Canonical A-share code").makeOptionMandatory(true));
176
+ reports.addOption(new Option("--limit <n>", "Max periods (1-100)").default("12"));
177
+ reports.addOption(new Option("--kind <kind>", "Filter by report kind").choices([...REPORT_KINDS]));
178
+ reports.action(async (opts) => {
179
+ if (!OPERATIONS["financials.reports"]) {
180
+ emitVerbError("internal_error", "financials.reports missing", undefined, 2);
181
+ }
182
+ const args = {
183
+ code: opts.code,
184
+ limit: clampInt(opts.limit, 1, 100, 12),
185
+ };
186
+ if (opts.kind)
187
+ args.kind = opts.kind;
188
+ await executeVerb(async (ctx) => financialsReportsSpec.handler(args, ctx));
189
+ });
190
+ const series = cmd.command("series").description(financialsSeriesSpec.description);
191
+ series.addOption(new Option("--code <canonical_code>", "Canonical A-share code").makeOptionMandatory(true));
192
+ series.addOption(new Option("--metric <name>", "Indicator field name (e.g. roe_simple)").makeOptionMandatory(true));
193
+ series.addOption(new Option("--from <YYYY-MM-DD>", "Inclusive earliest report_date"));
194
+ series.addOption(new Option("--to <YYYY-MM-DD>", "Inclusive latest report_date"));
195
+ series.addOption(new Option("--limit <n>", "Max points (1-200)").default("40"));
196
+ series.action(async (opts) => {
197
+ if (!OPERATIONS["financials.series"]) {
198
+ emitVerbError("internal_error", "financials.series missing", undefined, 2);
199
+ }
200
+ const args = {
201
+ code: opts.code,
202
+ metric: opts.metric,
203
+ limit: clampInt(opts.limit, 1, 200, 40),
204
+ };
205
+ if (opts.from)
206
+ args.from = opts.from;
207
+ if (opts.to)
208
+ args.to = opts.to;
209
+ await executeVerb(async (ctx) => financialsSeriesSpec.handler(args, ctx));
210
+ });
211
+ return cmd;
212
+ }
@@ -8,11 +8,11 @@
8
8
  export { barsBatchSpec, buildBarsBatchCommand } from "./bars_batch.js";
9
9
  export { chartSpec, buildChartCommand } from "./chart.js";
10
10
  export { digestSpec, buildDigestCommand } from "./digest.js";
11
+ export { financialsQuoteSnapshotSpec, financialsPitSpec, financialsReportsSpec, financialsSeriesSpec, buildFinancialsCommand, } from "./financials.js";
11
12
  export { hotSpec, buildHotCommand } from "./hot.js";
12
13
  export { lookupSpec, buildLookupCommand } from "./lookup.js";
13
14
  export { newsSpec, buildNewsCommand } from "./news.js";
14
15
  export { quoteSpec, buildQuoteCommand } from "./quote.js";
15
- export { researchSpec, buildResearchCommand } from "./research.js";
16
16
  export { scanSpec, buildScanCommand } from "./scan.js";
17
17
  export { searchSpec, buildSearchCommand } from "./search.js";
18
18
  export { sentimentSpec, buildSentimentCommand } from "./sentiment.js";
@@ -20,11 +20,11 @@ export { viewsSpec, buildViewsCommand } from "./views.js";
20
20
  import { barsBatchSpec } from "./bars_batch.js";
21
21
  import { chartSpec } from "./chart.js";
22
22
  import { digestSpec } from "./digest.js";
23
+ import { financialsQuoteSnapshotSpec, financialsPitSpec, financialsReportsSpec, financialsSeriesSpec, } from "./financials.js";
23
24
  import { hotSpec } from "./hot.js";
24
25
  import { lookupSpec } from "./lookup.js";
25
26
  import { newsSpec } from "./news.js";
26
27
  import { quoteSpec } from "./quote.js";
27
- import { researchSpec } from "./research.js";
28
28
  import { scanSpec } from "./scan.js";
29
29
  import { searchSpec } from "./search.js";
30
30
  import { sentimentSpec } from "./sentiment.js";
@@ -33,8 +33,10 @@ import { viewsSpec } from "./views.js";
33
33
  * Ordering follows product priority (feedback_views_over_news.md):
34
34
  * lookup (universal first step) → digest (one-shot fan-out, agent's opening
35
35
  * move once it has a canonical_code) → search (hybrid semantic discovery
36
- * for themes / concepts) → quote → views/research (PRIMARY research) → news
37
- * (supplementary) → sentiment → hot → chart → bars_batch → scan.
36
+ * for themes / concepts) → quote → views (PRIMARY research) → news
37
+ * (supplementary) → sentiment → hot → chart → bars_batch → scan
38
+ * financials (valuation snapshot is the headline; pit / reports / series
39
+ * cover deeper fundamentals access).
38
40
  */
39
41
  export const ALL_VERB_SPECS = [
40
42
  lookupSpec,
@@ -42,11 +44,14 @@ export const ALL_VERB_SPECS = [
42
44
  searchSpec,
43
45
  quoteSpec,
44
46
  viewsSpec,
45
- researchSpec,
46
47
  newsSpec,
47
48
  sentimentSpec,
48
49
  hotSpec,
49
50
  chartSpec,
50
51
  barsBatchSpec,
51
52
  scanSpec,
53
+ financialsQuoteSnapshotSpec,
54
+ financialsPitSpec,
55
+ financialsReportsSpec,
56
+ financialsSeriesSpec,
52
57
  ];
@@ -10,7 +10,7 @@ import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
10
10
  import { callOp } from "../runtime/verb_runner.js";
11
11
  export const viewsSpec = {
12
12
  name: "views",
13
- description: "PRIMARY research source for stock judgement: analyst views / sell-side reports / long-form opinion stream, with research_entity_id attribution (use `research` verb to query hit-rate). Prefer this over `news` when forming an investment opinion.",
13
+ description: "PRIMARY research source for stock judgement: analyst views / sell-side reports / long-form opinion stream, with research_entity_id attribution. Prefer this over `news` when forming an investment opinion.",
14
14
  inputSchema: {
15
15
  code: z
16
16
  .string()
package/dist/version.js CHANGED
@@ -2,4 +2,4 @@
2
2
  * Pinned at build time. Don't read package.json at runtime — adds startup latency
3
3
  * and breaks bundling.
4
4
  */
5
- export const CLI_VERSION = "2.1.0";
5
+ export const CLI_VERSION = "2.2.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echopai",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Command-line interface for the EchoPai Open Platform: stock-market data, news, analyst views, sentiment, signals, backtests.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://echopai.com",
@@ -1,44 +0,0 @@
1
- /**
2
- * `echopai research [--entity <id>]` + MCP tool `research`.
3
- *
4
- * Views quality signal layer.
5
- */
6
- import { Command, Option } from "commander";
7
- import { z } from "zod";
8
- import { OPERATIONS } from "../_generated/operations.js";
9
- import { executeVerb } from "../runtime/verb_cmd.js";
10
- import { callOp } from "../runtime/verb_runner.js";
11
- export const researchSpec = {
12
- name: "research",
13
- description: "Research-entity performance — hit rate / coverage / avg excess return. Pair with `views` (which returns research_entity_id per row) to assess analyst quality. Omit entity_id for aggregate top-performers list.",
14
- inputSchema: {
15
- entity_id: z
16
- .string()
17
- .optional()
18
- .describe("Specific research_entity_id; omit for aggregate top-performers list"),
19
- },
20
- handler: async (args, ctx) => {
21
- if (args.entity_id) {
22
- const op = OPERATIONS["research.entity-performance"];
23
- if (!op)
24
- throw new Error("research.entity-performance op missing");
25
- return callOp(op, { entity_id: args.entity_id }, ctx);
26
- }
27
- const op = OPERATIONS["research.entity-performance-list"];
28
- if (!op)
29
- throw new Error("research.entity-performance-list op missing");
30
- return callOp(op, {}, ctx);
31
- },
32
- backingOps: ["research.entity-performance-list", "research.entity-performance"],
33
- };
34
- export function buildResearchCommand() {
35
- const cmd = new Command(researchSpec.name).description(researchSpec.description);
36
- cmd.addOption(new Option("--entity <id>", "Specific research_entity_id; omit for aggregate list"));
37
- cmd.action(async (opts) => {
38
- const args = {};
39
- if (opts.entity)
40
- args.entity_id = opts.entity;
41
- await executeVerb(async (ctx) => researchSpec.handler(args, ctx));
42
- });
43
- return cmd;
44
- }