echopai 2.2.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 (47) hide show
  1. package/README.md +63 -348
  2. package/dist/bin.js +8302 -149
  3. package/package.json +11 -13
  4. package/dist/_generated/commands.js +0 -282
  5. package/dist/_generated/help.js +0 -195
  6. package/dist/_generated/operations.js +0 -1529
  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 -387
  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/verb_cmd.js +0 -70
  20. package/dist/runtime/verb_runner.js +0 -152
  21. package/dist/runtime/whoami_cache.js +0 -109
  22. package/dist/tools/api.js +0 -81
  23. package/dist/tools/completion.js +0 -116
  24. package/dist/tools/config.js +0 -123
  25. package/dist/tools/doctor.js +0 -183
  26. package/dist/tools/login.js +0 -99
  27. package/dist/tools/mcp.js +0 -141
  28. package/dist/tools/raw.js +0 -96
  29. package/dist/tools/schema.js +0 -58
  30. package/dist/tools/trace.js +0 -54
  31. package/dist/tools/welcome.js +0 -190
  32. package/dist/tools/whoami.js +0 -132
  33. package/dist/verbs/_spec.js +0 -15
  34. package/dist/verbs/bars_batch.js +0 -66
  35. package/dist/verbs/chart.js +0 -110
  36. package/dist/verbs/digest.js +0 -344
  37. package/dist/verbs/financials.js +0 -212
  38. package/dist/verbs/hot.js +0 -29
  39. package/dist/verbs/index.js +0 -57
  40. package/dist/verbs/lookup.js +0 -72
  41. package/dist/verbs/news.js +0 -67
  42. package/dist/verbs/quote.js +0 -53
  43. package/dist/verbs/scan.js +0 -42
  44. package/dist/verbs/search.js +0 -105
  45. package/dist/verbs/sentiment.js +0 -46
  46. package/dist/verbs/views.js +0 -83
  47. package/dist/version.js +0 -5
@@ -1,344 +0,0 @@
1
- /**
2
- * `echopai digest --code <canonical_code>` + MCP tool `digest`.
3
- *
4
- * Killer "one-shot research" verb:
5
- *
6
- * one canonical_code → views + quote + snapshot + sentiment + news
7
- *
8
- * Plan-doc §3.3 makes this the "agent's opening move": one call returns the
9
- * full picture of a security. Buckets stay separated (no ranking imposed) so
10
- * the model can weigh each itself; the description already encodes the
11
- * product call that views > news.
12
- *
13
- * Phase 5.2 strategy — server-first with client-side fallback:
14
- * 1. Try `GET /v1/digest/{code}` (single HTTP call, server-side fan-out;
15
- * response shape already matches the CLI envelope below).
16
- * 2. If that returns 404 (endpoint not yet deployed) or `not_found`, fall
17
- * back to client-side Promise.allSettled fan-out (Phase 5.1).
18
- * 3. Other errors (4xx auth, 5xx server, network) propagate normally — we
19
- * don't double-call.
20
- *
21
- * Either path produces the same envelope shape, so MCP / agent code is
22
- * oblivious. Behavior parity is enforced by tests asserting the bucket order
23
- * (views first, news last) is identical in both modes.
24
- *
25
- * Why these 5 ops in the fallback:
26
- * - views.recent : PRIMARY research signal (analyst opinions on this code)
27
- * - quote : current price / change %
28
- * - financials.quote-snapshot : 14-field valuation (PE/PB/PS/换手/股息/量比) on this code
29
- * - sentiment.overview : market regime context (not per-code; intentional —
30
- * no per-code sentiment op exists)
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)
34
- *
35
- * Windows differ on purpose: views=168h (7d), news=24h. Encodes the rule
36
- * that research time-horizon > news time-horizon.
37
- */
38
- import { Command, Option } from "commander";
39
- import { z } from "zod";
40
- import { OPERATIONS } from "../_generated/operations.js";
41
- import { CallApiError } from "../runtime/errors.js";
42
- import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
43
- import { callOp } from "../runtime/verb_runner.js";
44
- /** Buckets in the order the description establishes (views first, news last). */
45
- const BUCKETS = ["views", "quote", "snapshot", "sentiment", "news"];
46
- const CODE_REGEX = /^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/;
47
- export const digestSpec = {
48
- name: "digest",
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>'.",
50
- inputSchema: {
51
- code: z
52
- .string()
53
- .regex(CODE_REGEX)
54
- .describe("Canonical code, e.g. SSE:600519. Use `lookup` first if you only have a name."),
55
- views_since_days: z
56
- .number()
57
- .int()
58
- .min(1)
59
- .max(90)
60
- .default(7)
61
- .describe("Lookback for views bucket (days, default 7 — research horizon)"),
62
- news_hours: z
63
- .number()
64
- .int()
65
- .min(1)
66
- .max(168)
67
- .default(24)
68
- .describe("Lookback for news bucket (hours, default 24 — event horizon)"),
69
- limit_per_bucket: z
70
- .number()
71
- .int()
72
- .min(1)
73
- .max(50)
74
- .default(10)
75
- .describe("Per-bucket item cap (views/news). Quote/snapshot/sentiment aren't paginated here."),
76
- },
77
- handler: async (args, ctx) => runDigest(args, ctx),
78
- // Backing ops are listed in two groups: digest.get is the primary
79
- // server-side endpoint we try first. The 5 fan-out ops are listed too
80
- // because MCP `tools/list` should still expose `digest` for tokens that
81
- // have *any* of these scopes — server-side `digest.get` exists for
82
- // routing, but on tokens lacking the relevant per-bucket scopes the
83
- // fan-out fallback (or server-side partial_failures) is what populates
84
- // the response. Including all backing ops keeps the scope-derivation
85
- // permissive; verbAvailable() returns true if any one is callable.
86
- backingOps: [
87
- "digest.get",
88
- "views.recent",
89
- "quote",
90
- "financials.quote-snapshot",
91
- "sentiment.overview",
92
- "news.list",
93
- ],
94
- };
95
- /**
96
- * Phase 5.2 entry: try server-side `/v1/digest/{code}` first; on 404
97
- * (endpoint not yet deployed) fall back to client-side fan-out.
98
- *
99
- * Exported for tests.
100
- */
101
- export async function runDigest(argsIn, ctx) {
102
- const op = OPERATIONS["digest.get"];
103
- if (op) {
104
- try {
105
- const args = argsIn;
106
- const env = await callOp(op, {
107
- code: args.code,
108
- views_since_days: args.views_since_days,
109
- news_hours: args.news_hours,
110
- limit_per_bucket: args.limit_per_bucket,
111
- }, ctx);
112
- // Stamp meta so callers can tell which path served the response. Useful
113
- // when debugging fallback decisions and when comparing latency.
114
- env.meta = { ...env.meta, digest_path: "server" };
115
- return env;
116
- }
117
- catch (e) {
118
- if (e instanceof CallApiError && shouldFallbackToFanout(e)) {
119
- // Server endpoint not yet rolled out on this gateway. Quietly drop
120
- // to fan-out and stamp meta so observability can track this.
121
- const env = await runDigestFanout(argsIn, ctx);
122
- env.meta = {
123
- ...env.meta,
124
- digest_path: "fanout_fallback",
125
- fallback_reason: e.code,
126
- };
127
- return env;
128
- }
129
- throw e;
130
- }
131
- }
132
- // Codegen pre-Phase 5.2: no server op available at all → fan-out only.
133
- const env = await runDigestFanout(argsIn, ctx);
134
- env.meta = { ...env.meta, digest_path: "fanout_only" };
135
- return env;
136
- }
137
- /**
138
- * Heuristic: when should we fall back to client-side fan-out instead of
139
- * propagating the server-side error?
140
- *
141
- * - 404 / not_found: endpoint isn't deployed yet (rolling release window).
142
- * - http_error with 404 status: same situation, no envelope returned.
143
- *
144
- * Auth / scope / rate-limit / 5xx upstream errors MUST propagate — those
145
- * indicate problems the fan-out path will hit anyway, and silently retrying
146
- * would mask them while spending 5× the upstream cost.
147
- */
148
- function shouldFallbackToFanout(e) {
149
- if (e.httpStatus === 404)
150
- return true;
151
- if (e.code === "not_found")
152
- return true;
153
- return false;
154
- }
155
- function buildBucketCalls(args, ctx) {
156
- const opsRequired = {
157
- views: "views.recent",
158
- quote: "quote",
159
- snapshot: "financials.quote-snapshot",
160
- sentiment: "sentiment.overview",
161
- news: "news.list",
162
- };
163
- const calls = [];
164
- for (const bucket of BUCKETS) {
165
- const opKey = opsRequired[bucket];
166
- const op = OPERATIONS[opKey];
167
- if (!op) {
168
- // Codegen drift — surface as a failure on that bucket rather than
169
- // exploding the whole digest; agent sees the gap in partial_failures.
170
- calls.push({
171
- bucket,
172
- endpoint: opKey,
173
- run: () => Promise.reject(new CallApiError({
174
- code: "internal_error",
175
- message: `Operation ${opKey} missing from codegen`,
176
- })),
177
- });
178
- continue;
179
- }
180
- let opArgs;
181
- switch (bucket) {
182
- case "views":
183
- opArgs = {
184
- security: args.code,
185
- since_days: args.views_since_days,
186
- limit: args.limit_per_bucket,
187
- };
188
- break;
189
- case "quote":
190
- opArgs = { codes: [args.code] };
191
- break;
192
- case "snapshot":
193
- opArgs = { code: args.code };
194
- break;
195
- case "sentiment":
196
- opArgs = { scope: "all_a_ex_st" };
197
- break;
198
- case "news":
199
- opArgs = {
200
- security: args.code,
201
- since_hours: args.news_hours,
202
- limit: args.limit_per_bucket,
203
- };
204
- break;
205
- }
206
- calls.push({
207
- bucket,
208
- endpoint: op.path,
209
- run: () => callOp(op, opArgs, ctx),
210
- });
211
- }
212
- return calls;
213
- }
214
- /**
215
- * Run the fan-out. Exported for tests; CLI/MCP enter via `digestSpec.handler`.
216
- */
217
- export async function runDigestFanout(argsIn, ctx) {
218
- const args = argsIn;
219
- const calls = buildBucketCalls(args, ctx);
220
- const results = await Promise.allSettled(calls.map((c) => c.run()));
221
- const data = { code: args.code };
222
- const partialFailures = [];
223
- let succeeded = 0;
224
- let totalDurationMs = 0;
225
- let earliestRequestId;
226
- results.forEach((result, idx) => {
227
- const { bucket, endpoint } = calls[idx];
228
- if (result.status === "fulfilled") {
229
- // Strip CLI-wrapper meta and keep just data + relevant server meta.
230
- // The outer digest envelope carries its own meta; per-bucket meta would
231
- // be noisy. Preserve server-provided pagination/total on the bucket.
232
- const env = result.value;
233
- const bucketServerMeta = stripCliMeta(env.meta);
234
- data[bucket] =
235
- bucketServerMeta && Object.keys(bucketServerMeta).length > 0
236
- ? { data: env.data, meta: bucketServerMeta }
237
- : env.data;
238
- succeeded++;
239
- const d = env.meta.duration_ms;
240
- if (typeof d === "number")
241
- totalDurationMs = Math.max(totalDurationMs, d);
242
- if (!earliestRequestId && typeof env.meta.request_id === "string") {
243
- earliestRequestId = env.meta.request_id;
244
- }
245
- }
246
- else {
247
- data[bucket] = null;
248
- const err = result.reason;
249
- if (err instanceof CallApiError) {
250
- partialFailures.push({
251
- bucket,
252
- code: err.code,
253
- message: err.message,
254
- endpoint,
255
- ...(err.requestId ? { request_id: err.requestId } : {}),
256
- });
257
- }
258
- else {
259
- partialFailures.push({
260
- bucket,
261
- code: "internal_error",
262
- message: err instanceof Error ? err.message : String(err),
263
- endpoint,
264
- });
265
- }
266
- }
267
- });
268
- // If literally every bucket failed, that's not a partial-fail situation —
269
- // surface it as a hard error so the agent doesn't act on an empty digest.
270
- if (succeeded === 0) {
271
- const first = partialFailures[0];
272
- throw new CallApiError({
273
- code: first?.code ?? "upstream_unavailable",
274
- message: `All ${calls.length} digest sub-ops failed; first: ${first?.message ?? "unknown"}`,
275
- ...(first?.request_id ? { requestId: first.request_id } : {}),
276
- });
277
- }
278
- return {
279
- data,
280
- meta: {
281
- endpoint: "digest (fanout)",
282
- method: "FANOUT",
283
- cli_version: ctx.cliVersion,
284
- duration_ms: totalDurationMs,
285
- truncated: false,
286
- fanout_total: calls.length,
287
- fanout_succeeded: succeeded,
288
- partial_failures: partialFailures,
289
- ...(earliestRequestId ? { request_id: earliestRequestId } : {}),
290
- },
291
- };
292
- }
293
- /** Drop CLI-injected meta fields; keep only server-originating keys. */
294
- function stripCliMeta(meta) {
295
- if (!meta)
296
- return undefined;
297
- const drop = new Set([
298
- "request_id",
299
- "endpoint",
300
- "method",
301
- "cli_version",
302
- "api_version",
303
- "duration_ms",
304
- "truncated",
305
- ]);
306
- const out = {};
307
- for (const [k, v] of Object.entries(meta)) {
308
- if (drop.has(k))
309
- continue;
310
- out[k] = v;
311
- }
312
- return out;
313
- }
314
- function clamp(raw, min, max, fallback) {
315
- const n = Math.floor(Number(raw));
316
- if (!Number.isFinite(n))
317
- return fallback;
318
- if (n < min)
319
- return min;
320
- if (n > max)
321
- return max;
322
- return n;
323
- }
324
- export function buildDigestCommand() {
325
- const cmd = new Command(digestSpec.name).description(digestSpec.description);
326
- cmd.addOption(new Option("--code <canonical_code>", "Canonical code, e.g. SSE:600519")
327
- .makeOptionMandatory(true));
328
- cmd.addOption(new Option("--views-since-days <n>", "Views lookback in days (1-90)").default("7"));
329
- cmd.addOption(new Option("--news-hours <n>", "News lookback in hours (1-168)").default("24"));
330
- cmd.addOption(new Option("--limit-per-bucket <n>", "Items per bucket (1-50)").default("10"));
331
- cmd.action(async (opts) => {
332
- if (!CODE_REGEX.test(opts.code)) {
333
- emitVerbError("invalid_args", `code ${JSON.stringify(opts.code)} is not a canonical_code (expected SSE|SZSE|BSE:NNNNNN)`, "Use `echopai lookup --text <name>` to resolve to a canonical_code first.", 1);
334
- }
335
- const args = {
336
- code: opts.code,
337
- views_since_days: clamp(opts.viewsSinceDays, 1, 90, 7),
338
- news_hours: clamp(opts.newsHours, 1, 168, 24),
339
- limit_per_bucket: clamp(opts.limitPerBucket, 1, 50, 10),
340
- };
341
- await executeVerb(async (ctx) => digestSpec.handler(args, ctx));
342
- });
343
- return cmd;
344
- }
@@ -1,212 +0,0 @@
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
- }
package/dist/verbs/hot.js DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * `echopai hot` + MCP tool `hot`.
3
- */
4
- import { Command } from "commander";
5
- import { OPERATIONS } from "../_generated/operations.js";
6
- import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
7
- import { callOp } from "../runtime/verb_runner.js";
8
- export const hotSpec = {
9
- name: "hot",
10
- description: "Today's hot-stock leaderboard (East-Money composite rank: search / follow / comment).",
11
- inputSchema: {},
12
- handler: async (_args, ctx) => {
13
- const op = OPERATIONS["stocks.hot"];
14
- if (!op)
15
- throw new Error("stocks.hot op missing");
16
- return callOp(op, {}, ctx);
17
- },
18
- backingOps: ["stocks.hot"],
19
- };
20
- export function buildHotCommand() {
21
- const cmd = new Command(hotSpec.name).description(hotSpec.description);
22
- cmd.action(async () => {
23
- if (!OPERATIONS["stocks.hot"]) {
24
- emitVerbError("internal_error", "stocks.hot missing", undefined, 2);
25
- }
26
- await executeVerb(async (ctx) => hotSpec.handler({}, ctx));
27
- });
28
- return cmd;
29
- }
@@ -1,57 +0,0 @@
1
- /**
2
- * Curated verb registry.
3
- *
4
- * Importing this module gives both:
5
- * - ALL_VERB_SPECS: 共用声明,MCP serve 枚举出 tools/list
6
- * - build*Command(): commander 端入口工厂
7
- */
8
- export { barsBatchSpec, buildBarsBatchCommand } from "./bars_batch.js";
9
- export { chartSpec, buildChartCommand } from "./chart.js";
10
- export { digestSpec, buildDigestCommand } from "./digest.js";
11
- export { financialsQuoteSnapshotSpec, financialsPitSpec, financialsReportsSpec, financialsSeriesSpec, buildFinancialsCommand, } from "./financials.js";
12
- export { hotSpec, buildHotCommand } from "./hot.js";
13
- export { lookupSpec, buildLookupCommand } from "./lookup.js";
14
- export { newsSpec, buildNewsCommand } from "./news.js";
15
- export { quoteSpec, buildQuoteCommand } from "./quote.js";
16
- export { scanSpec, buildScanCommand } from "./scan.js";
17
- export { searchSpec, buildSearchCommand } from "./search.js";
18
- export { sentimentSpec, buildSentimentCommand } from "./sentiment.js";
19
- export { viewsSpec, buildViewsCommand } from "./views.js";
20
- import { barsBatchSpec } from "./bars_batch.js";
21
- import { chartSpec } from "./chart.js";
22
- import { digestSpec } from "./digest.js";
23
- import { financialsQuoteSnapshotSpec, financialsPitSpec, financialsReportsSpec, financialsSeriesSpec, } from "./financials.js";
24
- import { hotSpec } from "./hot.js";
25
- import { lookupSpec } from "./lookup.js";
26
- import { newsSpec } from "./news.js";
27
- import { quoteSpec } from "./quote.js";
28
- import { scanSpec } from "./scan.js";
29
- import { searchSpec } from "./search.js";
30
- import { sentimentSpec } from "./sentiment.js";
31
- import { viewsSpec } from "./views.js";
32
- /**
33
- * Ordering follows product priority (feedback_views_over_news.md):
34
- * lookup (universal first step) → digest (one-shot fan-out, agent's opening
35
- * move once it has a canonical_code) → search (hybrid semantic discovery
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).
40
- */
41
- export const ALL_VERB_SPECS = [
42
- lookupSpec,
43
- digestSpec,
44
- searchSpec,
45
- quoteSpec,
46
- viewsSpec,
47
- newsSpec,
48
- sentimentSpec,
49
- hotSpec,
50
- chartSpec,
51
- barsBatchSpec,
52
- scanSpec,
53
- financialsQuoteSnapshotSpec,
54
- financialsPitSpec,
55
- financialsReportsSpec,
56
- financialsSeriesSpec,
57
- ];