echopai 2.3.0 → 2.4.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.
Files changed (54) hide show
  1. package/README.md +63 -348
  2. package/dist/bin.js +8298 -190
  3. package/package.json +11 -13
  4. package/dist/_generated/commands.js +0 -378
  5. package/dist/_generated/help.js +0 -295
  6. package/dist/_generated/operations.js +0 -2385
  7. package/dist/runtime/auth.js +0 -95
  8. package/dist/runtime/envelope.js +0 -52
  9. package/dist/runtime/errors.js +0 -186
  10. package/dist/runtime/filters.js +0 -153
  11. package/dist/runtime/format.js +0 -143
  12. package/dist/runtime/http.js +0 -65
  13. package/dist/runtime/idempotency.js +0 -18
  14. package/dist/runtime/invoker.js +0 -391
  15. package/dist/runtime/io.js +0 -16
  16. package/dist/runtime/paginator.js +0 -146
  17. package/dist/runtime/trace.js +0 -99
  18. package/dist/runtime/tty.js +0 -51
  19. package/dist/runtime/update_check.js +0 -120
  20. package/dist/runtime/update_worker.js +0 -63
  21. package/dist/runtime/verb_cmd.js +0 -72
  22. package/dist/runtime/verb_runner.js +0 -152
  23. package/dist/runtime/whoami_cache.js +0 -109
  24. package/dist/tools/api.js +0 -81
  25. package/dist/tools/completion.js +0 -116
  26. package/dist/tools/config.js +0 -123
  27. package/dist/tools/doctor.js +0 -183
  28. package/dist/tools/login.js +0 -99
  29. package/dist/tools/mcp.js +0 -141
  30. package/dist/tools/raw.js +0 -96
  31. package/dist/tools/schema.js +0 -58
  32. package/dist/tools/trace.js +0 -54
  33. package/dist/tools/upgrade.js +0 -103
  34. package/dist/tools/welcome.js +0 -225
  35. package/dist/tools/whoami.js +0 -132
  36. package/dist/verbs/_spec.js +0 -15
  37. package/dist/verbs/announcements.js +0 -195
  38. package/dist/verbs/bars_batch.js +0 -66
  39. package/dist/verbs/chart.js +0 -110
  40. package/dist/verbs/concepts.js +0 -393
  41. package/dist/verbs/digest.js +0 -351
  42. package/dist/verbs/financials.js +0 -212
  43. package/dist/verbs/hot.js +0 -29
  44. package/dist/verbs/index.js +0 -88
  45. package/dist/verbs/limit_up.js +0 -156
  46. package/dist/verbs/lookup.js +0 -72
  47. package/dist/verbs/market.js +0 -185
  48. package/dist/verbs/news.js +0 -81
  49. package/dist/verbs/quote.js +0 -53
  50. package/dist/verbs/scan.js +0 -42
  51. package/dist/verbs/search.js +0 -105
  52. package/dist/verbs/sentiment.js +0 -231
  53. package/dist/verbs/views.js +0 -85
  54. package/dist/version.js +0 -5
@@ -1,156 +0,0 @@
1
- /**
2
- * `echopai limit-up ...` + MCP tools `limit_up_pool` / `limit_up_summary` /
3
- * `limit_up_history`.
4
- *
5
- * 涨停股池 / 涨停统计 / 涨停趋势 — 短线情绪与连板梯队的核心数据源,全部走
6
- * stockpulse-rs upstream(`market_limit_up_pool` ReplacingMergeTree + 与情绪页
7
- * 同源的 `market_breadth_intraday` 炸板/跌停 snapshot)。
8
- *
9
- * 盘前回退(与所有行情类页面一致):< 9:00 → 上一交易日;9:00-9:14 集合竞价
10
- * 前空窗期返回 items=[] / trade_date=null;≥ 9:15 起取今日。
11
- *
12
- * 用例:
13
- * - 短线 agent: limit-up pool → 当日涨停股逐股;按 consecutive_days desc 取
14
- * 最高连板;脉冲到 echopai chart 看 K 线
15
- * - 大盘情绪: limit-up summary → 涨停数 / 炸板数 / 连板梯队;配合
16
- * `sentiment overview` 形成情绪面板
17
- * - 趋势研判: limit-up history --days 30 → 近一月每日涨停数 / 最高连板
18
- */
19
- import { Command, Option } from "commander";
20
- import { z } from "zod";
21
- import { OPERATIONS } from "../_generated/operations.js";
22
- import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
23
- import { callOp } from "../runtime/verb_runner.js";
24
- const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
25
- const INCLUDE_VALUES = ["active", "all"];
26
- function clampInt(raw, min, max, fallback) {
27
- const n = Math.floor(Number(raw));
28
- if (!Number.isFinite(n))
29
- return fallback;
30
- if (n < min)
31
- return min;
32
- if (n > max)
33
- return max;
34
- return n;
35
- }
36
- export const limitUpPoolSpec = {
37
- name: "limit_up_pool",
38
- description: "A-share limit-up pool for a given trade date — per-stock detail: first/last seal time, final pct, seal amount/volume, broken count, consecutive_days (连板数), board_count_within (N-day 板数), one-word-board flag, current status (limit_up / broken), board classification. Sort: consecutive_days desc → board_count_within desc → seal_amount desc. Pre-open fallback applies. Use when an agent needs the concrete list of '今天涨停了哪些票'.",
39
- inputSchema: {
40
- trade_date: z
41
- .string()
42
- .regex(DATE_RE)
43
- .optional()
44
- .describe("ISO date YYYY-MM-DD; omit for latest (pre-open fallback)"),
45
- include: z
46
- .enum(INCLUDE_VALUES)
47
- .default("active")
48
- .describe("active = 排除 ST/退市(默认); all = 全部"),
49
- include_broken: z
50
- .boolean()
51
- .default(false)
52
- .describe("true 时连炸板(status=broken)也一起返回;默认只返回当前封板 status=limit_up"),
53
- },
54
- handler: async (args, ctx) => {
55
- const op = OPERATIONS["limit-up.pool"];
56
- if (!op)
57
- throw new Error("limit-up.pool op missing");
58
- const callArgs = {
59
- include: args.include,
60
- include_broken: args.include_broken,
61
- };
62
- if (args.trade_date)
63
- callArgs.trade_date = args.trade_date;
64
- return callOp(op, callArgs, ctx);
65
- },
66
- backingOps: ["limit-up.pool"],
67
- };
68
- export const limitUpSummarySpec = {
69
- name: "limit_up_summary",
70
- description: "Limit-up summary for a given trade date: 涨停数 (limit_up_count) / 炸板数 (broken_count) / 跌停数 (limit_down_count) / 最高连板 (max_height) / 连板梯队 ladder[{height, count}]. 炸板/跌停 与情绪页同源 (market_breadth_intraday 当日最新分钟行). Pre-open fallback applies.",
71
- inputSchema: {
72
- trade_date: z
73
- .string()
74
- .regex(DATE_RE)
75
- .optional()
76
- .describe("ISO date YYYY-MM-DD; omit for latest"),
77
- include: z
78
- .enum(INCLUDE_VALUES)
79
- .default("active")
80
- .describe("active = all_a_ex_st 口径(默认); all = all_a 口径"),
81
- },
82
- handler: async (args, ctx) => {
83
- const op = OPERATIONS["limit-up.summary"];
84
- if (!op)
85
- throw new Error("limit-up.summary op missing");
86
- const callArgs = { include: args.include };
87
- if (args.trade_date)
88
- callArgs.trade_date = args.trade_date;
89
- return callOp(op, callArgs, ctx);
90
- },
91
- backingOps: ["limit-up.summary"],
92
- };
93
- export const limitUpHistorySpec = {
94
- name: "limit_up_history",
95
- description: "Daily limit-up trend over the last N trading days: { trade_date, limit_up_count, broken_count, max_height }. 涨停数/最高连板 from market_limit_up_pool; 炸板数 from market_breadth_intraday (all_a_ex_st). Sorted by trade_date asc (oldest first).",
96
- inputSchema: {
97
- days: z
98
- .number()
99
- .int()
100
- .min(1)
101
- .max(250)
102
- .default(30)
103
- .describe("Lookback window in trading days (1-250, default 30)"),
104
- },
105
- handler: async (args, ctx) => {
106
- const op = OPERATIONS["limit-up.history"];
107
- if (!op)
108
- throw new Error("limit-up.history op missing");
109
- return callOp(op, { days: args.days }, ctx);
110
- },
111
- backingOps: ["limit-up.history"],
112
- };
113
- export function buildLimitUpCommand() {
114
- const cmd = new Command("limit-up").description("A-share limit-up board — pool (per-stock detail) / summary (counts + ladder) / history (daily trend).");
115
- const pool = cmd.command("pool").description(limitUpPoolSpec.description);
116
- pool.addOption(new Option("--trade-date <YYYY-MM-DD>", "Trade date; omit for latest"));
117
- pool.addOption(new Option("--include <s>", "active = 排除 ST/退市; all = 全部")
118
- .choices([...INCLUDE_VALUES])
119
- .default("active"));
120
- pool.addOption(new Option("--include-broken", "Also return 炸板 (status=broken); default only limit_up"));
121
- pool.action(async (opts) => {
122
- if (!OPERATIONS["limit-up.pool"]) {
123
- emitVerbError("internal_error", "limit-up.pool missing", undefined, 2);
124
- }
125
- const args = {
126
- include: opts.include,
127
- include_broken: Boolean(opts.includeBroken),
128
- };
129
- if (opts.tradeDate)
130
- args.trade_date = opts.tradeDate;
131
- await executeVerb(async (ctx) => limitUpPoolSpec.handler(args, ctx));
132
- });
133
- const summary = cmd.command("summary").description(limitUpSummarySpec.description);
134
- summary.addOption(new Option("--trade-date <YYYY-MM-DD>", "Trade date; omit for latest"));
135
- summary.addOption(new Option("--include <s>", "active = all_a_ex_st 口径; all = all_a 口径")
136
- .choices([...INCLUDE_VALUES])
137
- .default("active"));
138
- summary.action(async (opts) => {
139
- if (!OPERATIONS["limit-up.summary"]) {
140
- emitVerbError("internal_error", "limit-up.summary missing", undefined, 2);
141
- }
142
- const args = { include: opts.include };
143
- if (opts.tradeDate)
144
- args.trade_date = opts.tradeDate;
145
- await executeVerb(async (ctx) => limitUpSummarySpec.handler(args, ctx));
146
- });
147
- const history = cmd.command("history").description(limitUpHistorySpec.description);
148
- history.addOption(new Option("--days <n>", "Lookback window (1-250)").default("30"));
149
- history.action(async (opts) => {
150
- if (!OPERATIONS["limit-up.history"]) {
151
- emitVerbError("internal_error", "limit-up.history missing", undefined, 2);
152
- }
153
- await executeVerb(async (ctx) => limitUpHistorySpec.handler({ days: clampInt(opts.days, 1, 250, 30) }, ctx));
154
- });
155
- return cmd;
156
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * `echopai lookup --text <q> [--limit N]` + MCP tool `lookup`.
3
- *
4
- * Curated verb: 解析中文名 / 代码 / 拼音 → canonical_code 列表。Agent 几乎
5
- * 每个会话的第一步。底层调 raw OpenAPI op `semantic.find`。
6
- */
7
- import { Command, Option } from "commander";
8
- import { z } from "zod";
9
- import { OPERATIONS } from "../_generated/operations.js";
10
- import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
11
- import { callOp } from "../runtime/verb_runner.js";
12
- export function mapLookupResponse(raw) {
13
- if (!Array.isArray(raw))
14
- return [];
15
- const out = [];
16
- for (const item of raw) {
17
- if (!item || typeof item !== "object")
18
- continue;
19
- const r = item;
20
- if (typeof r.canonical_code !== "string")
21
- continue;
22
- out.push({
23
- canonical_code: r.canonical_code,
24
- name_cn: typeof r.name_cn === "string" ? r.name_cn : "",
25
- ticker: typeof r.ticker === "string" ? r.ticker : "",
26
- exchange: typeof r.exchange === "string" ? r.exchange : "",
27
- });
28
- }
29
- return out;
30
- }
31
- export const lookupSpec = {
32
- name: "lookup",
33
- description: "Resolve a Chinese name / A-share code / pinyin initials to canonical codes (e.g. SSE:600519). Use this first whenever the agent has a description but needs a canonical_code for downstream calls. AH dual-listing: when a company is listed on both A-share and HK (e.g. 工商银行 SSE:601398 / HK:01398), prefer the A-share canonical for all downstream analysis unless the user explicitly asks for the HK side.",
34
- inputSchema: {
35
- text: z
36
- .string()
37
- .min(1)
38
- .max(50)
39
- .describe("Search text: Chinese name (贵州茅台), code (600519), or pinyin (gzmt)"),
40
- limit: z.number().int().min(1).max(30).default(10).describe("Max matches (1-30)"),
41
- },
42
- handler: async (args, ctx) => {
43
- const op = OPERATIONS["semantic.find"];
44
- if (!op)
45
- throw new Error("semantic.find op missing from codegen");
46
- const env = await callOp(op, { query: args.text, limit: args.limit }, ctx);
47
- const matches = mapLookupResponse(env.data);
48
- return { data: { matches, count: matches.length }, meta: env.meta };
49
- },
50
- backingOps: ["semantic.find"],
51
- };
52
- function clampLimit(raw, min = 1, max = 30) {
53
- const n = Math.floor(Number(raw));
54
- if (!Number.isFinite(n) || n < min)
55
- return min;
56
- if (n > max)
57
- return max;
58
- return n;
59
- }
60
- export function buildLookupCommand() {
61
- const cmd = new Command(lookupSpec.name).description(lookupSpec.description);
62
- cmd.addOption(new Option("--text <text>", "Search text (Chinese name / code / pinyin)")
63
- .makeOptionMandatory(true));
64
- cmd.addOption(new Option("--limit <n>", "Max matches (1-30)").default("10"));
65
- cmd.action(async (opts) => {
66
- if (!OPERATIONS["semantic.find"]) {
67
- emitVerbError("internal_error", "semantic.find missing from codegen", undefined, 2);
68
- }
69
- await executeVerb(async (ctx) => lookupSpec.handler({ text: opts.text, limit: clampLimit(opts.limit) }, ctx));
70
- });
71
- return cmd;
72
- }
@@ -1,185 +0,0 @@
1
- /**
2
- * `echopai market ...` + MCP tools `market_status` / `market_movers`.
3
- *
4
- * - status — A 股市场会话状态:pre-open / open / lunch / closed +
5
- * 当前/下一交易日 + 节假日 flag。非计费,agent 在 quote / bars 前调一次
6
- * 决定盘前回退口径。
7
- * - movers — /market 行情榜单:客户端对 `quote.scan` 的 ~5800 只 A 股
8
- * snapshot 按用户选择的字段 (pct / speed / amount / turnover / total_mv /
9
- * pct_asc) 排序后取 top N。仅本地排序,agent 不需要自行下载 3 MB 再筛。
10
- *
11
- * `market_movers` 不是新的 partner endpoint,而是 CLI 端对 `quote.scan` 的
12
- * 客户端排序 + slice。背后 op 还是 `quote.scan`,scope 一致(quote:*)。
13
- */
14
- import { Command, Option } from "commander";
15
- import { z } from "zod";
16
- import { OPERATIONS } from "../_generated/operations.js";
17
- import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
18
- import { callOp } from "../runtime/verb_runner.js";
19
- export const marketStatusSpec = {
20
- name: "market_status",
21
- description: "Current A-share market session: pre-open / open / lunch / closed; current and next trading day; holiday flag. Cheap and non-billable — call it before any quote / bars fetch when the agent needs to decide pre-market fallback vs intraday vs after-close behavior.",
22
- inputSchema: {},
23
- handler: async (_args, ctx) => {
24
- const op = OPERATIONS["market.status"];
25
- if (!op)
26
- throw new Error("market.status op missing");
27
- return callOp(op, {}, ctx);
28
- },
29
- backingOps: ["market.status"],
30
- };
31
- const EXCHANGE_VALUES = ["SSE", "SZSE", "BSE"];
32
- const INCLUDE_VALUES = ["active", "all"];
33
- // `pct_asc` = 跌幅榜 (ascending pct); 其余字段均按 desc 取头部.
34
- const MOVERS_SORT_VALUES = [
35
- "pct",
36
- "pct_asc",
37
- "speed",
38
- "amount",
39
- "turnover",
40
- "total_mv",
41
- ];
42
- function moversFieldOf(sort) {
43
- switch (sort) {
44
- case "pct":
45
- case "pct_asc":
46
- return "pct";
47
- case "speed":
48
- return "speed_3min";
49
- case "turnover":
50
- return "turnover_rate";
51
- case "total_mv":
52
- return "total_mv";
53
- case "amount":
54
- default:
55
- return "amount";
56
- }
57
- }
58
- function numericOrNeg(v) {
59
- if (typeof v === "number" && Number.isFinite(v))
60
- return v;
61
- if (typeof v === "string") {
62
- const n = Number(v);
63
- if (Number.isFinite(n))
64
- return n;
65
- }
66
- return Number.NEGATIVE_INFINITY;
67
- }
68
- export const marketMoversSpec = {
69
- name: "market_movers",
70
- description: "A-share market movers — top N from the full real-time snapshot, sorted by chosen field. Sort keys: `pct` (涨幅榜) / `pct_asc` (跌幅榜) / `speed` (3-min 涨速 speed_3min) / `amount` (成交额) / `turnover` (换手率 turnover_rate) / `total_mv` (总市值). Backed by `quote.scan` so 9:00-9:14 集合竞价时段会返空 (with `note`). Defaults: sort=pct, top=20, include=active (排除 ST / 退市). Use this for /market-style 'who's running today' instead of pulling the full 3 MB scan and sorting client-side.",
71
- inputSchema: {
72
- sort: z
73
- .enum(MOVERS_SORT_VALUES)
74
- .default("pct")
75
- .describe("Sort field: pct / pct_asc / speed / amount / turnover / total_mv"),
76
- top: z
77
- .number()
78
- .int()
79
- .min(1)
80
- .max(500)
81
- .default(20)
82
- .describe("Top N items (1-500, default 20)"),
83
- exchange: z
84
- .enum(EXCHANGE_VALUES)
85
- .optional()
86
- .describe("Narrow to one exchange: SSE / SZSE / BSE"),
87
- include: z
88
- .enum(INCLUDE_VALUES)
89
- .default("active")
90
- .describe("active = 排除 ST/退市(默认); all = 全集(含 ST + 退市 + 未知 status)"),
91
- },
92
- handler: async (args, ctx) => {
93
- const op = OPERATIONS["quote.scan"];
94
- if (!op)
95
- throw new Error("quote.scan op missing");
96
- const callArgs = {};
97
- if (args.exchange)
98
- callArgs.exchange = args.exchange;
99
- // quote.scan input schema currently exposes only `exchange`; `include`
100
- // is supported by the server side (see stockpulse-rs stocks.rs) but
101
- // not yet declared in the partner inputSchema. Pass via raw URL anyway
102
- // by injecting before callOp — but to avoid ajv strict mode kicking,
103
- // we route through callOp which has additionalProperties:false. So we
104
- // currently DON'T forward `include` until the contract picks it up.
105
- // Default server behavior (`active`) matches our CLI default; if the
106
- // user explicitly wants `all`, fall through to verb-level filter below.
107
- const env = await callOp(op, callArgs, ctx);
108
- return sliceMoversEnvelope(env, args.sort, Number(args.top ?? 20), args.include === "all");
109
- },
110
- backingOps: ["quote.scan"],
111
- };
112
- function sliceMoversEnvelope(env, sort, top, _includeAll) {
113
- const body = env.data;
114
- if (!body || typeof body !== "object" || !Array.isArray(body.items)) {
115
- return env;
116
- }
117
- const items = body.items.slice();
118
- const field = moversFieldOf(sort);
119
- const asc = sort === "pct_asc";
120
- items.sort((a, b) => {
121
- const av = numericOrNeg(a[field]);
122
- const bv = numericOrNeg(b[field]);
123
- return asc ? av - bv : bv - av;
124
- });
125
- const sliced = items.slice(0, Math.max(1, Math.floor(top)));
126
- const data = {
127
- ...body,
128
- items: sliced,
129
- sort,
130
- sort_field: field,
131
- top,
132
- };
133
- const meta = {
134
- ...(env.meta ?? {}),
135
- client_sort: sort,
136
- client_sort_field: field,
137
- client_top: top,
138
- };
139
- return { data, meta };
140
- }
141
- function clampInt(raw, min, max, fallback) {
142
- const n = Math.floor(Number(raw));
143
- if (!Number.isFinite(n))
144
- return fallback;
145
- if (n < min)
146
- return min;
147
- if (n > max)
148
- return max;
149
- return n;
150
- }
151
- export function buildMarketCommand() {
152
- const cmd = new Command("market").description("Market session state + real-time movers (涨幅/涨速/成交额/换手率/总市值).");
153
- const status = cmd
154
- .command("status")
155
- .description(marketStatusSpec.description);
156
- status.action(async () => {
157
- if (!OPERATIONS["market.status"]) {
158
- emitVerbError("internal_error", "market.status missing", undefined, 2);
159
- }
160
- await executeVerb(async (ctx) => marketStatusSpec.handler({}, ctx));
161
- });
162
- const movers = cmd.command("movers").description(marketMoversSpec.description);
163
- movers.addOption(new Option("--sort <field>", "Sort field")
164
- .choices([...MOVERS_SORT_VALUES])
165
- .default("pct"));
166
- movers.addOption(new Option("--top <n>", "Top N items (1-500)").default("20"));
167
- movers.addOption(new Option("--exchange <ex>", "Filter by exchange").choices([...EXCHANGE_VALUES]));
168
- movers.addOption(new Option("--include <s>", "active (默认排除 ST/退市) | all")
169
- .choices([...INCLUDE_VALUES])
170
- .default("active"));
171
- movers.action(async (opts) => {
172
- if (!OPERATIONS["quote.scan"]) {
173
- emitVerbError("internal_error", "quote.scan missing", undefined, 2);
174
- }
175
- const args = {
176
- sort: opts.sort,
177
- top: clampInt(opts.top, 1, 500, 20),
178
- include: opts.include,
179
- };
180
- if (opts.exchange)
181
- args.exchange = opts.exchange;
182
- await executeVerb(async (ctx) => marketMoversSpec.handler(args, ctx));
183
- });
184
- return cmd;
185
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * `echopai news ...` + MCP tool `news`.
3
- *
4
- * SUPPLEMENTARY breadth source (feedback_views_over_news.md).
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 newsSpec = {
12
- name: "news",
13
- description: "SUPPLEMENTARY news / market briefs (short time-horizon event stream). Use ONLY to fill gaps not covered by `views`; prefer `views` as the primary research source. Three modes: `code` (canonical) → news mentioning that security; `query` (free text) → full-text search; neither → recent time-window feed. AH dual-listing: when filtering by `code` for an A+H listed company, pass the A-share canonical (e.g. SSE:601398) for consolidated coverage; HK side only if the user explicitly asks. Note: `query` is matched against news text, so passing a canonical_code as `query` will not work — use `code` for security filtering.",
14
- inputSchema: {
15
- code: z
16
- .string()
17
- .regex(/^((SSE|SZSE|BSE):[0-9]{6}|HK:[0-9]{5}|US:[A-Z][A-Z0-9.\-]{0,5})$/)
18
- .optional()
19
- .describe("Filter by security canonical_code (e.g. SSE:600519, HK:00700). Takes precedence over `query`."),
20
- query: z
21
- .string()
22
- .optional()
23
- .describe("Free-text query (Chinese or English) matched against news content; omit for time-window feed. Do NOT pass canonical_code here — use `code` instead."),
24
- hours: z
25
- .number()
26
- .int()
27
- .min(1)
28
- .max(168)
29
- .default(24)
30
- .describe("Lookback window in hours (default 24 — much narrower than views)"),
31
- limit: z.number().int().min(1).max(100).default(20).describe("Max items"),
32
- },
33
- handler: async (args, ctx) => {
34
- if (args.code) {
35
- const op = OPERATIONS["news.list"];
36
- if (!op)
37
- throw new Error("news.list op missing");
38
- return callOp(op, { security: args.code, since_hours: args.hours, limit: args.limit }, ctx);
39
- }
40
- if (args.query) {
41
- const op = OPERATIONS["news.search"];
42
- if (!op)
43
- throw new Error("news.search op missing");
44
- return callOp(op, { query: args.query, since_hours: args.hours, limit: args.limit }, ctx);
45
- }
46
- const op = OPERATIONS["news.feed"];
47
- if (!op)
48
- throw new Error("news.feed op missing");
49
- return callOp(op, {}, ctx);
50
- },
51
- backingOps: ["news.list", "news.search", "news.feed"],
52
- };
53
- function clamp(raw, min, max, fallback) {
54
- const n = Math.floor(Number(raw));
55
- if (!Number.isFinite(n))
56
- return fallback;
57
- if (n < min)
58
- return min;
59
- if (n > max)
60
- return max;
61
- return n;
62
- }
63
- export function buildNewsCommand() {
64
- const cmd = new Command(newsSpec.name).description(newsSpec.description);
65
- cmd.addOption(new Option("--code <canonical_code>", "Filter by security canonical_code (e.g. SSE:600519); takes precedence over --query"));
66
- cmd.addOption(new Option("--query <text>", "Free-text query; omit for time-window feed"));
67
- cmd.addOption(new Option("--hours <n>", "Lookback window (1-168 hours)").default("24"));
68
- cmd.addOption(new Option("--limit <n>", "Max items").default("20"));
69
- cmd.action(async (opts) => {
70
- const args = {
71
- hours: clamp(opts.hours, 1, 168, 24),
72
- limit: clamp(opts.limit, 1, 100, 20),
73
- };
74
- if (opts.code)
75
- args.code = opts.code;
76
- if (opts.query)
77
- args.query = opts.query;
78
- await executeVerb(async (ctx) => newsSpec.handler(args, ctx));
79
- });
80
- return cmd;
81
- }
@@ -1,53 +0,0 @@
1
- /**
2
- * `echopai quote --codes A,B,C` + MCP tool `quote`.
3
- */
4
- import { Command, Option } from "commander";
5
- import { z } from "zod";
6
- import { OPERATIONS } from "../_generated/operations.js";
7
- import { CallApiError } from "../runtime/errors.js";
8
- import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
9
- import { callOp } from "../runtime/verb_runner.js";
10
- export const quoteSpec = {
11
- name: "quote",
12
- description: "Real-time quote for 1-200 A-share securities (last price, volume, change %, bid/ask). For >200 codes use `scan` instead. AH dual-listing: HK codes are not supported here; for an A+H listed company use the A-share canonical (e.g. SSE:601398 not HK:01398).",
13
- inputSchema: {
14
- codes: z
15
- .array(z.string().regex(/^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/))
16
- .min(1)
17
- .max(200)
18
- .describe("Array of canonical codes (e.g. ['SSE:600519', 'SZSE:000001'])"),
19
- include_l2: z
20
- .boolean()
21
- .optional()
22
- .describe("Include L2 5-level order book (requires quote:l2 scope)"),
23
- },
24
- handler: async (args, ctx) => {
25
- const op = OPERATIONS["quote"];
26
- if (!op)
27
- throw new Error("quote op missing from codegen");
28
- return callOp(op, args, ctx);
29
- },
30
- backingOps: ["quote"],
31
- };
32
- export function buildQuoteCommand() {
33
- const cmd = new Command(quoteSpec.name).description(quoteSpec.description);
34
- cmd.addOption(new Option("--codes <csv>", "Canonical codes, comma-separated (e.g. SSE:600519,SZSE:000001)")
35
- .makeOptionMandatory(true));
36
- cmd.addOption(new Option("--include-l2", "Include L2 order book (requires quote:l2 scope)"));
37
- cmd.action(async (opts) => {
38
- const codes = opts.codes.split(",").map((s) => s.trim()).filter(Boolean);
39
- if (codes.length === 0 || codes.length > 200) {
40
- emitVerbError("invalid_args", `codes count ${codes.length} out of range; expected 1-200`, "Use `echopai scan` for full-market snapshot (>200 codes).", 1);
41
- }
42
- if (!OPERATIONS["quote"]) {
43
- emitVerbError("internal_error", "quote op missing from codegen", undefined, 2);
44
- }
45
- const args = { codes };
46
- if (opts.includeL2)
47
- args.include_l2 = true;
48
- await executeVerb(async (ctx) => quoteSpec.handler(args, ctx));
49
- });
50
- return cmd;
51
- }
52
- // Re-export CallApiError so handler call sites have a typed throw signature.
53
- export { CallApiError };
@@ -1,42 +0,0 @@
1
- /**
2
- * `echopai scan` + MCP tool `scan`.
3
- */
4
- import { Command, Option } from "commander";
5
- import { z } from "zod";
6
- import { OPERATIONS } from "../_generated/operations.js";
7
- import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
8
- import { callOp } from "../runtime/verb_runner.js";
9
- export const scanSpec = {
10
- name: "scan",
11
- description: "Full-market real-time quote scan (~5800 A-share securities in one round-trip). Use for universe discovery; agents should slim output with field/byte limits in their tool host or post-processing.",
12
- inputSchema: {
13
- exchange: z
14
- .enum(["SSE", "SZSE", "BSE"])
15
- .optional()
16
- .describe("Filter by exchange (omit for all)"),
17
- },
18
- handler: async (args, ctx) => {
19
- const op = OPERATIONS["quote.scan"];
20
- if (!op)
21
- throw new Error("quote.scan op missing");
22
- const callArgs = {};
23
- if (args.exchange)
24
- callArgs.exchange = args.exchange;
25
- return callOp(op, callArgs, ctx);
26
- },
27
- backingOps: ["quote.scan"],
28
- };
29
- export function buildScanCommand() {
30
- const cmd = new Command(scanSpec.name).description(scanSpec.description);
31
- cmd.addOption(new Option("--exchange <code>", "Filter by exchange").choices(["SSE", "SZSE", "BSE"]));
32
- cmd.action(async (opts) => {
33
- if (!OPERATIONS["quote.scan"]) {
34
- emitVerbError("internal_error", "quote.scan missing", undefined, 2);
35
- }
36
- const args = {};
37
- if (opts.exchange)
38
- args.exchange = opts.exchange;
39
- await executeVerb(async (ctx) => scanSpec.handler(args, ctx));
40
- });
41
- return cmd;
42
- }