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,58 +0,0 @@
1
- /**
2
- * `echopai schema [list | get <key> | export]`
3
- *
4
- * AI agent ingestion 路径:`echopai schema export | ai-tool` 一次拿全部命令的
5
- * input JSON Schema + path + method + 示例。
6
- */
7
- import { Command } from "commander";
8
- import { OPERATIONS, listOperations } from "../_generated/operations.js";
9
- import { HELP } from "../_generated/help.js";
10
- export function buildSchemaCommand() {
11
- const cmd = new Command("schema").description("Inspect / export the CLI command surface");
12
- cmd
13
- .command("list")
14
- .description("Print one line per command: cliKey, method, path, summary")
15
- .action(() => {
16
- for (const op of listOperations()) {
17
- process.stdout.write(JSON.stringify({
18
- cliKey: op.cliKey,
19
- method: op.method,
20
- path: op.path,
21
- summary: op.summary,
22
- }) + "\n");
23
- }
24
- process.exit(0);
25
- });
26
- cmd
27
- .command("get <cliKey>")
28
- .description("Print full operation def + help (input schema + example)")
29
- .action((cliKey) => {
30
- const op = OPERATIONS[cliKey];
31
- if (!op) {
32
- process.stderr.write(JSON.stringify({
33
- error: {
34
- code: "schema_not_found",
35
- message: `No command with cliKey '${cliKey}'.`,
36
- recovery_hint: "Run `echopai schema list` for available keys.",
37
- },
38
- }) + "\n");
39
- process.exit(1);
40
- }
41
- const help = HELP[cliKey] ?? null;
42
- const out = { ...op, ...(help ? { example: help.example } : {}) };
43
- process.stdout.write(JSON.stringify(out, null, 2) + "\n");
44
- process.exit(0);
45
- });
46
- cmd
47
- .command("export")
48
- .description("NDJSON dump of full surface (one operation per line) — for AI agents")
49
- .action(() => {
50
- for (const op of listOperations()) {
51
- const help = HELP[op.cliKey];
52
- const merged = { ...op, ...(help ? { example: help.example } : {}) };
53
- process.stdout.write(JSON.stringify(merged) + "\n");
54
- }
55
- process.exit(0);
56
- });
57
- return cmd;
58
- }
@@ -1,54 +0,0 @@
1
- /**
2
- * `echopai trace [tail | get <request_id>]`
3
- *
4
- * 本地调用追溯 ring buffer (~/.echopai/trace.ndjson)。每次 CLI 调用结束都会
5
- * 写一条 NDJSON。用途:
6
- * - 看最近 N 次调用 (tail)
7
- * - 用 request_id 反查具体一次 (get)
8
- *
9
- * `ECHOPAI_NO_TRACE=1` 关闭写入;`ECHOPAI_TRACE_FILE=/path` 改路径(测试用)。
10
- */
11
- import { Command, Option } from "commander";
12
- import { getTraceByRequestId, resolveTraceFile, tailTraces, } from "../runtime/trace.js";
13
- export function buildTraceCommand() {
14
- const cmd = new Command("trace").description("Inspect local CLI invocation trace (~/.echopai/trace.ndjson)");
15
- cmd
16
- .command("tail")
17
- .description("Print last N trace records (NDJSON)")
18
- .addOption(new Option("--lines <n>", "Number of records").default("20"))
19
- .action((opts) => {
20
- const n = Math.max(1, Math.floor(Number(opts.lines) || 20));
21
- const records = tailTraces(n);
22
- for (const r of records) {
23
- process.stdout.write(JSON.stringify(r) + "\n");
24
- }
25
- process.exit(0);
26
- });
27
- cmd
28
- .command("get <request_id>")
29
- .description("Find a single trace record by request_id")
30
- .action((requestId) => {
31
- const r = getTraceByRequestId(requestId);
32
- if (!r) {
33
- process.stderr.write(JSON.stringify({
34
- error: {
35
- code: "not_found",
36
- message: `No trace record with request_id '${requestId}'.`,
37
- retryable: false,
38
- recovery_hint: "Trace ring buffer holds ~50MB. Older records rotate to `trace.ndjson.1` and beyond that are lost.",
39
- },
40
- }) + "\n");
41
- process.exit(1);
42
- }
43
- process.stdout.write(JSON.stringify(r, null, 2) + "\n");
44
- process.exit(0);
45
- });
46
- cmd
47
- .command("path")
48
- .description("Print the resolved trace file path")
49
- .action(() => {
50
- process.stdout.write(resolveTraceFile() + "\n");
51
- process.exit(0);
52
- });
53
- return cmd;
54
- }
@@ -1,190 +0,0 @@
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
- }
@@ -1,132 +0,0 @@
1
- /**
2
- * `echopai whoami`
3
- *
4
- * 调 /v1/auth/whoami 拿 token 自省 + 派生 operation 可用性:
5
- * available: token 至少有一个 op.scopesAny 中的 scope (或 scopesAny 为空)
6
- * unavailable: op.scopesAny 非空且全部 miss → missing_scopes_any 全部列出
7
- *
8
- * 输出标准 envelope;缓存 5min in-process (whoami_cache.ts)。
9
- *
10
- * 该命令是 Phase 4 MCP `tools/list` 推导逻辑的同源——MCP server 启动时调用
11
- * 同一个 getWhoami() 拿能力图。
12
- */
13
- import { Command, Option } from "commander";
14
- import { listOperations } from "../_generated/operations.js";
15
- import { resolveCredentials, AuthMissingError } from "../runtime/auth.js";
16
- import { CallApiError } from "../runtime/errors.js";
17
- import { isTtyHuman, renderError } from "../runtime/tty.js";
18
- import { clearWhoamiCache, getWhoami } from "../runtime/whoami_cache.js";
19
- import { CLI_VERSION } from "../version.js";
20
- export function deriveOperationAvailability(scopes, ops = listOperations()) {
21
- const available = [];
22
- const unavailable = [];
23
- for (const op of ops) {
24
- const required = op.scopesAny;
25
- const summary = { cliKey: op.cliKey, method: op.method, path: op.path };
26
- if (!required || required.length === 0) {
27
- available.push(summary);
28
- continue;
29
- }
30
- const hit = required.some((s) => scopes.has(s));
31
- if (hit) {
32
- available.push(summary);
33
- }
34
- else {
35
- unavailable.push({ ...summary, missing_scopes_any: required.slice() });
36
- }
37
- }
38
- return { available, unavailable };
39
- }
40
- export function buildWhoamiOutput(whoami, channel, ops = listOperations()) {
41
- const scopeSet = new Set(whoami.scopes);
42
- const { available, unavailable } = deriveOperationAvailability(scopeSet, ops);
43
- const out = {
44
- kind: whoami.kind,
45
- scopes: whoami.scopes,
46
- channel,
47
- operations: { available, unavailable },
48
- };
49
- if (whoami.app_id)
50
- out.app_id = whoami.app_id;
51
- if (whoami.org_id)
52
- out.org_id = whoami.org_id;
53
- if (whoami.app_slug)
54
- out.app_slug = whoami.app_slug;
55
- if (whoami.audience)
56
- out.audience = whoami.audience;
57
- if (whoami.api_version)
58
- out.api_version = whoami.api_version;
59
- if (whoami.allowed_clients)
60
- out.allowed_clients = whoami.allowed_clients;
61
- if (whoami.feature_flags && Object.keys(whoami.feature_flags).length > 0) {
62
- out.feature_flags = whoami.feature_flags;
63
- }
64
- if (whoami.rate_limit?.qps != null)
65
- out.rate_limit_qps = whoami.rate_limit.qps;
66
- if (whoami.rate_limit?.monthly_quota != null) {
67
- out.monthly_quota = whoami.rate_limit.monthly_quota;
68
- }
69
- return out;
70
- }
71
- export function buildWhoamiCommand() {
72
- const cmd = new Command("whoami").description("Show current token capabilities (kind / scopes / available operations)");
73
- cmd.addOption(new Option("--no-cache", "Bypass 5-minute whoami cache"));
74
- cmd.addOption(new Option("--key <key>", "Override credential (env / config)"));
75
- cmd.addOption(new Option("--profile <name>", "Use a specific profile"));
76
- cmd.action(async (opts) => {
77
- let creds;
78
- try {
79
- creds = resolveCredentials({
80
- ...(opts.key ? { key: opts.key } : {}),
81
- ...(opts.profile ? { profile: opts.profile } : {}),
82
- });
83
- }
84
- catch (e) {
85
- if (e instanceof AuthMissingError) {
86
- emitError("auth_missing", e.message, e.recovery_hint, 1);
87
- }
88
- throw e;
89
- }
90
- if (opts.cache === false)
91
- clearWhoamiCache();
92
- let whoami;
93
- try {
94
- whoami = await getWhoami({ baseUrl: creds.baseUrl, bearer: creds.key, cliVersion: CLI_VERSION }, { force: opts.cache === false });
95
- }
96
- catch (e) {
97
- if (e instanceof CallApiError) {
98
- emitError(e.code, e.message, e.recovery_hint, e.httpStatus && e.httpStatus < 500 ? 1 : 2, e.requestId);
99
- }
100
- throw e;
101
- }
102
- const data = buildWhoamiOutput(whoami, "cli");
103
- const envelope = {
104
- data,
105
- meta: {
106
- cli_version: CLI_VERSION,
107
- ...(whoami.api_version ? { api_version: whoami.api_version } : {}),
108
- },
109
- };
110
- process.stdout.write(JSON.stringify(envelope) + "\n");
111
- process.exit(0);
112
- });
113
- return cmd;
114
- }
115
- function emitError(code, message, recoveryHint, exitCode, requestId) {
116
- const env = {
117
- error: {
118
- code,
119
- message,
120
- retryable: false,
121
- ...(recoveryHint ? { recovery_hint: recoveryHint } : {}),
122
- ...(requestId ? { request_id: requestId } : {}),
123
- },
124
- };
125
- if (isTtyHuman) {
126
- process.stderr.write(renderError(env) + "\n");
127
- }
128
- else {
129
- process.stderr.write(JSON.stringify(env) + "\n");
130
- }
131
- process.exit(exitCode);
132
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * VerbSpec —— curated verb 的源声明,CLI 与 MCP 两条入口共享。
3
- *
4
- * - CLI (commander): build*Command() 内部把 spec.handler 接到 commander.action
5
- * 外加 executeVerb (process.exit / stderr / 错误 envelope) 包装。
6
- * - MCP: tools/mcp.ts 启动时枚举所有 spec,把 inputSchema 直接喂 SDK
7
- * registerTool,handler 包成 MCP tool callback。
8
- *
9
- * 单源声明保证两个入口的 verb 名称 / 参数语义 / 输出形态完全同步。
10
- *
11
- * inputSchema 用 Zod raw shape (Record<string, ZodType>) —— MCP SDK 原生
12
- * 接受;CLI 端不重复用它做校验 (commander Option + 我们 verb 内部的
13
- * clamp/parse 已足够)。
14
- */
15
- export {};
@@ -1,66 +0,0 @@
1
- /**
2
- * `echopai bars-batch` + MCP tool `bars_batch`.
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
- const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
10
- const CODE_RE = /^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/;
11
- export const barsBatchSpec = {
12
- name: "bars_batch",
13
- description: "Batch K-line for multiple A-share securities in one round-trip. Daily: ≤100 codes × ≤1yr; minute: ≤20 codes × ≤7d. Returns partial-success envelope {items, errors}.",
14
- inputSchema: {
15
- codes: z
16
- .array(z.string().regex(CODE_RE))
17
- .min(1)
18
- .max(100)
19
- .describe("Canonical codes (daily ≤100; minute ≤20)"),
20
- from: z.string().regex(DATE_RE).describe("Inclusive start date YYYY-MM-DD"),
21
- to: z.string().regex(DATE_RE).describe("Inclusive end date YYYY-MM-DD"),
22
- minute: z
23
- .boolean()
24
- .optional()
25
- .describe("Minute-level bars instead of daily (tighter caps apply)"),
26
- },
27
- handler: async (args, ctx) => {
28
- const isMinute = Boolean(args.minute);
29
- const cap = isMinute ? 20 : 100;
30
- const codes = args.codes;
31
- if (codes.length > cap) {
32
- throw new Error(`codes count ${codes.length} exceeds cap ${cap} for ${isMinute ? "minute" : "daily"} bars_batch`);
33
- }
34
- const cliKey = isMinute ? "bars.minute-batch" : "bars.daily-batch";
35
- const op = OPERATIONS[cliKey];
36
- if (!op)
37
- throw new Error(`${cliKey} op missing from codegen`);
38
- return callOp(op, { codes, from: args.from, to: args.to }, ctx);
39
- },
40
- backingOps: ["bars.daily-batch", "bars.minute-batch"],
41
- };
42
- export function buildBarsBatchCommand() {
43
- // CLI alias keeps the hyphenated form. MCP tool name uses underscore (MCP
44
- // convention; underscores parse cleaner across hosts).
45
- const cmd = new Command("bars-batch").description(barsBatchSpec.description);
46
- cmd.addOption(new Option("--codes <csv>", "Canonical codes, comma-separated")
47
- .makeOptionMandatory(true));
48
- cmd.addOption(new Option("--from <date>", "Inclusive start YYYY-MM-DD").makeOptionMandatory(true));
49
- cmd.addOption(new Option("--to <date>", "Inclusive end YYYY-MM-DD").makeOptionMandatory(true));
50
- cmd.addOption(new Option("--minute", "Minute-level bars instead of daily"));
51
- cmd.action(async (opts) => {
52
- const codes = opts.codes.split(",").map((s) => s.trim()).filter(Boolean);
53
- const isMinute = Boolean(opts.minute);
54
- const cap = isMinute ? 20 : 100;
55
- if (codes.length === 0 || codes.length > cap) {
56
- emitVerbError("invalid_args", `codes count ${codes.length} out of range; expected 1-${cap} for ${isMinute ? "minute" : "daily"} bars-batch`, isMinute
57
- ? "Daily mode (default) allows up to 100 codes; drop --minute or chunk the request."
58
- : "Chunk the request into batches of ≤100 codes.", 1);
59
- }
60
- const args = { codes, from: opts.from, to: opts.to };
61
- if (opts.minute)
62
- args.minute = true;
63
- await executeVerb(async (ctx) => barsBatchSpec.handler(args, ctx));
64
- });
65
- return cmd;
66
- }
@@ -1,110 +0,0 @@
1
- /**
2
- * `echopai chart` + MCP tool `chart`.
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
- const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
10
- export const chartSpec = {
11
- name: "chart",
12
- description: "Single-security K-line (daily by default; set minute=true for intraday with a single date). For multi-code use `bars_batch` instead.",
13
- inputSchema: {
14
- code: z
15
- .string()
16
- .regex(/^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/)
17
- .describe("Single canonical code (e.g. SSE:600519)"),
18
- days: z
19
- .number()
20
- .int()
21
- .min(1)
22
- .max(365)
23
- .default(30)
24
- .describe("Daily lookback (ignored when from/to or minute mode are set)"),
25
- from: z
26
- .string()
27
- .regex(DATE_RE)
28
- .optional()
29
- .describe("YYYY-MM-DD inclusive start (daily mode; overrides days)"),
30
- to: z
31
- .string()
32
- .regex(DATE_RE)
33
- .optional()
34
- .describe("YYYY-MM-DD inclusive end (daily mode; overrides days)"),
35
- minute: z.boolean().optional().describe("Switch to minute-level bars (requires date)"),
36
- date: z.string().regex(DATE_RE).optional().describe("Single trade date YYYY-MM-DD (minute mode)"),
37
- },
38
- handler: async (args, ctx) => {
39
- if (args.minute) {
40
- if (!args.date) {
41
- throw new Error("minute=true requires date (YYYY-MM-DD)");
42
- }
43
- const op = OPERATIONS["bars.minute"];
44
- if (!op)
45
- throw new Error("bars.minute op missing");
46
- // bars.minute expects a single `date` (YYYY-MM-DD), not from/to —
47
- // see stockpulse-rs MinuteBarsQuery + OpenAPI 2026-05-12 correction.
48
- return callOp(op, { code: args.code, date: args.date }, ctx);
49
- }
50
- const op = OPERATIONS["bars.daily"];
51
- if (!op)
52
- throw new Error("bars.daily op missing");
53
- const callArgs = { code: args.code };
54
- if (args.from && args.to) {
55
- callArgs.from = args.from;
56
- callArgs.to = args.to;
57
- }
58
- else {
59
- // bars.daily requires from/to (additionalProperties:false; no `days`).
60
- // Derive a from/to window from --days client-side. Includes today;
61
- // server skips non-trading days automatically.
62
- const days = Math.max(1, Number(args.days ?? 30));
63
- const to = new Date();
64
- const from = new Date(to.getTime() - (days - 1) * 24 * 3600 * 1000);
65
- const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
66
- callArgs.from = fmt(from);
67
- callArgs.to = fmt(to);
68
- }
69
- return callOp(op, callArgs, ctx);
70
- },
71
- backingOps: ["bars.daily", "bars.minute"],
72
- };
73
- function clamp(raw, min, max, fallback) {
74
- const n = Math.floor(Number(raw));
75
- if (!Number.isFinite(n))
76
- return fallback;
77
- if (n < min)
78
- return min;
79
- if (n > max)
80
- return max;
81
- return n;
82
- }
83
- export function buildChartCommand() {
84
- const cmd = new Command(chartSpec.name).description(chartSpec.description);
85
- cmd.addOption(new Option("--code <canonical_code>", "Single canonical code").makeOptionMandatory(true));
86
- cmd.addOption(new Option("--days <n>", "Daily lookback (1-365, default 30)").default("30"));
87
- cmd.addOption(new Option("--from <date>", "Inclusive start date YYYY-MM-DD (overrides --days)"));
88
- cmd.addOption(new Option("--to <date>", "Inclusive end date YYYY-MM-DD (overrides --days)"));
89
- cmd.addOption(new Option("--minute", "Switch to minute-level bars (requires --date)"));
90
- cmd.addOption(new Option("--date <date>", "Single trade date YYYY-MM-DD (minute mode)"));
91
- cmd.action(async (opts) => {
92
- if (opts.minute && !opts.date) {
93
- emitVerbError("invalid_args", "--minute requires --date YYYY-MM-DD (single trade date)", "Example: echopai chart --minute --code SSE:600519 --date 2026-05-09", 1);
94
- }
95
- const args = {
96
- code: opts.code,
97
- days: clamp(opts.days, 1, 365, 30),
98
- };
99
- if (opts.from)
100
- args.from = opts.from;
101
- if (opts.to)
102
- args.to = opts.to;
103
- if (opts.minute)
104
- args.minute = true;
105
- if (opts.date)
106
- args.date = opts.date;
107
- await executeVerb(async (ctx) => chartSpec.handler(args, ctx));
108
- });
109
- return cmd;
110
- }