echopai 2.0.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/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, 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,10 +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",
124
+ "search",
125
+ "financials",
123
126
  ]);
124
127
  const topLevelSpecDispatch = async (op, args) => {
125
128
  await dispatch(op, args);
@@ -134,18 +137,19 @@ for (const sub of topLevelStub.commands) {
134
137
  // Curated agent verbs (Phase 3.6+): task-level entries that wrap one or
135
138
  // more raw operations + map output for AI agents. Listed before tools so
136
139
  // `--help` shows them first.
137
- // Product priority (see feedback_views_over_news.md): views > research > news
140
+ // Product priority (see feedback_views_over_news.md): views > news
138
141
  program.addCommand(buildLookupCommand());
139
142
  program.addCommand(buildDigestCommand()); // one-shot fan-out (Phase 5.1)
143
+ program.addCommand(buildSearchCommand()); // hybrid semantic discovery (PLAN_SEARCH_SEMANTIC_UPGRADE)
140
144
  program.addCommand(buildQuoteCommand());
141
145
  program.addCommand(buildViewsCommand()); // PRIMARY research source
142
- program.addCommand(buildResearchCommand()); // views quality signal layer
143
146
  program.addCommand(buildNewsCommand()); // supplementary breadth
144
147
  program.addCommand(buildSentimentCommand());
145
148
  program.addCommand(buildHotCommand());
146
149
  program.addCommand(buildChartCommand()); // single-code K-line
147
150
  program.addCommand(buildBarsBatchCommand()); // multi-code batch K-line (PR 3.2/3.3 server)
148
151
  program.addCommand(buildScanCommand()); // full-market scan (PR 3.4 server)
152
+ program.addCommand(buildFinancialsCommand()); // fundamentals: quote-snapshot / pit / reports / series
149
153
  // hand-curated tools
150
154
  program.addCommand(buildLoginCommand());
151
155
  program.addCommand(buildLogoutCommand());
@@ -158,7 +162,14 @@ program.addCommand(buildWhoamiCommand());
158
162
  program.addCommand(buildDoctorCommand());
159
163
  program.addCommand(buildMcpCommand());
160
164
  program.addCommand(buildCompletionCommand());
165
+ program.addCommand(buildWelcomeCommand());
161
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
+ }
162
173
  program.parseAsync(process.argv).catch((e) => {
163
174
  process.stderr.write(JSON.stringify({
164
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,42 +8,50 @@
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
+ export { searchSpec, buildSearchCommand } from "./search.js";
17
18
  export { sentimentSpec, buildSentimentCommand } from "./sentiment.js";
18
19
  export { viewsSpec, buildViewsCommand } from "./views.js";
19
20
  import { barsBatchSpec } from "./bars_batch.js";
20
21
  import { chartSpec } from "./chart.js";
21
22
  import { digestSpec } from "./digest.js";
23
+ import { financialsQuoteSnapshotSpec, financialsPitSpec, financialsReportsSpec, financialsSeriesSpec, } from "./financials.js";
22
24
  import { hotSpec } from "./hot.js";
23
25
  import { lookupSpec } from "./lookup.js";
24
26
  import { newsSpec } from "./news.js";
25
27
  import { quoteSpec } from "./quote.js";
26
- import { researchSpec } from "./research.js";
27
28
  import { scanSpec } from "./scan.js";
29
+ import { searchSpec } from "./search.js";
28
30
  import { sentimentSpec } from "./sentiment.js";
29
31
  import { viewsSpec } from "./views.js";
30
32
  /**
31
33
  * Ordering follows product priority (feedback_views_over_news.md):
32
34
  * lookup (universal first step) → digest (one-shot fan-out, agent's opening
33
- * move once it has a canonical_code) → quote views/research (PRIMARY
34
- * research) news (supplementary) → sentimenthot chartbars_batch →
35
- * scan.
35
+ * move once it has a canonical_code) → search (hybrid semantic discovery
36
+ * for themes / concepts) → quoteviews (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).
36
40
  */
37
41
  export const ALL_VERB_SPECS = [
38
42
  lookupSpec,
39
43
  digestSpec,
44
+ searchSpec,
40
45
  quoteSpec,
41
46
  viewsSpec,
42
- researchSpec,
43
47
  newsSpec,
44
48
  sentimentSpec,
45
49
  hotSpec,
46
50
  chartSpec,
47
51
  barsBatchSpec,
48
52
  scanSpec,
53
+ financialsQuoteSnapshotSpec,
54
+ financialsPitSpec,
55
+ financialsReportsSpec,
56
+ financialsSeriesSpec,
49
57
  ];