echopai 2.2.0 → 2.3.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
@@ -14,22 +14,52 @@
14
14
  *
15
15
  * See docs/PLAN_CLI_V2_REWRITE.md for design details.
16
16
  */
17
+ import { spawn } from "node:child_process";
18
+ import { fileURLToPath } from "node:url";
19
+ import path from "node:path";
17
20
  import { Command, Option } from "commander";
18
21
  import { buildCommandTree } from "./_generated/commands.js";
19
22
  import { invoke } from "./runtime/invoker.js";
20
23
  import { buildApiCommand } from "./tools/api.js";
21
24
  import { buildMcpCommand } from "./tools/mcp.js";
22
25
  import { buildRawCallCommand } from "./tools/raw.js";
23
- import { buildBarsBatchCommand, buildChartCommand, buildDigestCommand, buildFinancialsCommand, buildHotCommand, buildLookupCommand, buildNewsCommand, buildQuoteCommand, buildScanCommand, buildSearchCommand, buildSentimentCommand, buildViewsCommand, } from "./verbs/index.js";
26
+ import { buildAnnouncementsCommand, buildBarsBatchCommand, buildChartCommand, buildConceptsCommand, buildDigestCommand, buildFinancialsCommand, buildHotCommand, buildLimitUpCommand, buildLookupCommand, buildMarketCommand, buildNewsCommand, buildQuoteCommand, buildScanCommand, buildSearchCommand, buildSentimentCommand, buildViewsCommand, } from "./verbs/index.js";
24
27
  import { buildCompletionCommand } from "./tools/completion.js";
25
28
  import { buildConfigCommand } from "./tools/config.js";
26
29
  import { buildLoginCommand, buildLogoutCommand, buildStatusCommand } from "./tools/login.js";
27
30
  import { buildDoctorCommand } from "./tools/doctor.js";
28
31
  import { buildSchemaCommand } from "./tools/schema.js";
29
32
  import { buildTraceCommand } from "./tools/trace.js";
33
+ import { buildUpgradeCommand } from "./tools/upgrade.js";
30
34
  import { buildWelcomeCommand, printWelcome, shouldShowWelcome } from "./tools/welcome.js";
31
35
  import { buildWhoamiCommand } from "./tools/whoami.js";
32
36
  import { CLI_VERSION } from "./version.js";
37
+ // ─── Background update check ────────────────────────────────────────────────
38
+ // Spawn a detached child that hits npm registry and writes a 24h cache file.
39
+ // Parent exits immediately (child.unref). Banner is emitted on next CLI run
40
+ // from the cache (zero hot-path network cost). Skipped in CI / non-TTY /
41
+ // opt-out env so AI agents and scripts pay nothing.
42
+ try {
43
+ const optedOut = process.env.CI ||
44
+ process.env.ECHOPAI_DISABLE_UPDATE_CHECK ||
45
+ !process.stderr.isTTY;
46
+ // Don't recurse: the worker entry sets this env so we skip spawning again
47
+ // from inside the worker process itself.
48
+ if (!optedOut && !process.env._ECHOPAI_UPDATE_WORKER) {
49
+ const here = path.dirname(fileURLToPath(import.meta.url));
50
+ const workerPath = path.join(here, "runtime", "update_worker.js");
51
+ const child = spawn(process.execPath, [workerPath, CLI_VERSION], {
52
+ detached: true,
53
+ stdio: "ignore",
54
+ windowsHide: true,
55
+ env: { ...process.env, _ECHOPAI_UPDATE_WORKER: "1" },
56
+ });
57
+ child.unref();
58
+ }
59
+ }
60
+ catch {
61
+ // Worker spawn is best-effort; never let it break the CLI.
62
+ }
33
63
  const program = new Command();
34
64
  program
35
65
  .name("echopai")
@@ -51,13 +81,19 @@ program
51
81
  .addOption(new Option("--max-bytes <n>", "Truncate serialized envelope to N bytes (sets meta.truncated)"))
52
82
  .addOption(new Option("--yes", "Confirm a write op in non-TTY mode (required for sideEffect=write in agents / CI)"))
53
83
  .addOption(new Option("--dry-run", "Send X-Dry-Run:1 on write ops where the server advertises dry-run support"))
54
- .addHelpText("after", "\nAuthentication: ECHOPAI_KEY=<eps_live_<lookup>_<secret>> echopai <cmd>\n" +
55
- "Raw mirror: echopai raw <noun> <verb> # all OpenAPI ops, e.g. raw news search\n" +
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" +
58
- "Capability: echopai whoami | echopai doctor\n" +
59
- "Schema dump: echopai schema export # AI agents pipe this for command surface\n" +
60
- "Profile: echopai config profile add prod --base-url https://api.echopai.com\n");
84
+ .addHelpText("after", "\nAuthentication: ECHOPAI_KEY=<eps_live_<lookup>_<secret>> echopai <cmd>\n" +
85
+ "Raw mirror: echopai raw <noun> <verb> # all OpenAPI ops, e.g. raw news search\n" +
86
+ "Curated verbs: echopai <verb> # task-level entry (Phase 3.6+)\n" +
87
+ "Market session: echopai market status # pre-open/open/lunch/closed + next trading day\n" +
88
+ "Market movers: echopai market movers --sort pct --top 20 # /market 行情榜(pct / pct_asc / speed / amount / turnover / total_mv)\n" +
89
+ "Announcements: echopai announcements stock --code SSE:600519 # cninfo A-share disclosures\n" +
90
+ "Concepts: echopai concepts alerts # 当前激活的概念异动(big_move / limit_up_cluster)\n" +
91
+ "Limit-up: echopai limit-up summary # 涨停数 / 炸板数 / 连板梯队\n" +
92
+ "Fundamentals: echopai financials quote-snapshot --code SSE:600519 # PE/PB/PS/换手率/股息率/量比 14-field snapshot\n" +
93
+ "Capability: echopai whoami | echopai doctor\n" +
94
+ "Schema dump: echopai schema export # AI agents pipe this for command surface\n" +
95
+ "Profile: echopai config profile add prod --base-url https://api.echopai.com\n" +
96
+ "Self-update: echopai upgrade [--check] [--exec] # check npm registry; --exec runs `npm i -g echopai@latest`\n");
61
97
  const dispatch = async (op, args) => {
62
98
  // commander stores global flags on the *root* command's opts in camelCase
63
99
  // (e.g. --max-bytes → maxBytes). Subcommand-level opts go through
@@ -123,6 +159,10 @@ const CURATED_NOUNS = new Set([
123
159
  "digest",
124
160
  "search",
125
161
  "financials",
162
+ "announcements",
163
+ "market",
164
+ "concepts",
165
+ "limit-up",
126
166
  ]);
127
167
  const topLevelSpecDispatch = async (op, args) => {
128
168
  await dispatch(op, args);
@@ -142,10 +182,14 @@ program.addCommand(buildLookupCommand());
142
182
  program.addCommand(buildDigestCommand()); // one-shot fan-out (Phase 5.1)
143
183
  program.addCommand(buildSearchCommand()); // hybrid semantic discovery (PLAN_SEARCH_SEMANTIC_UPGRADE)
144
184
  program.addCommand(buildQuoteCommand());
185
+ program.addCommand(buildMarketCommand()); // session state (pre-open / open / lunch / closed)
145
186
  program.addCommand(buildViewsCommand()); // PRIMARY research source
146
187
  program.addCommand(buildNewsCommand()); // supplementary breadth
188
+ program.addCommand(buildAnnouncementsCommand()); // cninfo A-share disclosures (feed/stock/detail)
147
189
  program.addCommand(buildSentimentCommand());
148
190
  program.addCommand(buildHotCommand());
191
+ program.addCommand(buildConceptsCommand()); // concept indices + alerts + bars + news/views
192
+ program.addCommand(buildLimitUpCommand()); // 涨停股池 / 涨停统计 / 涨停趋势
149
193
  program.addCommand(buildChartCommand()); // single-code K-line
150
194
  program.addCommand(buildBarsBatchCommand()); // multi-code batch K-line (PR 3.2/3.3 server)
151
195
  program.addCommand(buildScanCommand()); // full-market scan (PR 3.4 server)
@@ -163,6 +207,7 @@ program.addCommand(buildDoctorCommand());
163
207
  program.addCommand(buildMcpCommand());
164
208
  program.addCommand(buildCompletionCommand());
165
209
  program.addCommand(buildWelcomeCommand());
210
+ program.addCommand(buildUpgradeCommand());
166
211
  applyAiFirstHooks(program);
167
212
  // Bare `echopai` in a TTY → onboarding screen. Non-TTY / CI / piped output
168
213
  // falls through to commander's plain help (preserves AI-first invariant).
@@ -23,6 +23,7 @@ import { generateIdempotencyKey, announceKey } from "./idempotency.js";
23
23
  import { writeStdout, writeStderr } from "./io.js";
24
24
  import { paginate, exitCodeForPaginateError } from "./paginator.js";
25
25
  import { renderError, isTtyHuman } from "./tty.js";
26
+ import { maybeEmitUpdateBanner } from "./update_check.js";
26
27
  const Ajv = (AjvPkg.default ??
27
28
  AjvPkg);
28
29
  const addFormats = (addFormatsPkg.default ??
@@ -180,6 +181,7 @@ export async function invoke(op, args, ctx) {
180
181
  });
181
182
  await writeStderr(`[paginate] ${result.pages} pages, ${result.items} items\n`);
182
183
  trace(op, { exit_code: 0 });
184
+ maybeEmitUpdateBanner();
183
185
  process.exit(0);
184
186
  }
185
187
  catch (e) {
@@ -293,6 +295,7 @@ export async function invoke(op, args, ctx) {
293
295
  // 8. success path — render
294
296
  if (raw) {
295
297
  await writeStdout(bodyText + "\n");
298
+ maybeEmitUpdateBanner();
296
299
  process.exit(0);
297
300
  }
298
301
  const rawEnvelope = buildResponseEnvelope(bodyJson ?? bodyText, {
@@ -337,6 +340,7 @@ export async function invoke(op, args, ctx) {
337
340
  exit_code: 0,
338
341
  truncated: envelope.meta.truncated === true,
339
342
  });
343
+ maybeEmitUpdateBanner();
340
344
  process.exit(0);
341
345
  }
342
346
  function buildQueryString(params) {
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Update-availability check — passive, AI-safe.
3
+ *
4
+ * 设计:
5
+ * - 真正发网络的活儿在 detached child (`update_worker.ts`) 干,由 bin.ts
6
+ * 启动时 spawn + unref,父进程立刻继续。Worker 写 ~/.config/echopai/
7
+ * update_cache.json,下次 CLI 启动时 sync 读 cache → 决定是否打 banner。
8
+ * - 这里只做 sync reader + 比较 + emit banner。任何 IO / 解析失败都吞掉
9
+ * 当作"没有可用更新"——CLI 自更新链路任何环节都不该让正常命令失败。
10
+ *
11
+ * AI-safe / agent-safe 规则:
12
+ * - 默认只在 stderr.isTTY 时 emit banner(agent 走 pipe 不 TTY,无污染)
13
+ * - 环境变量 `ECHOPAI_DISABLE_UPDATE_CHECK=1` / `CI=1` 一律静默
14
+ * - banner 是单行 dim ANSI,不破坏 NDJSON / JSON envelope(stdout 不动)
15
+ */
16
+ import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
17
+ import os from "node:os";
18
+ import path from "node:path";
19
+ import { CLI_VERSION } from "../version.js";
20
+ export const UPDATE_CACHE_PATH = path.join(os.homedir(), ".config", "echopai", "update_cache.json");
21
+ /** Pure semver-lite comparator. Pre-release suffixes are best-effort sorted
22
+ * lexicographically AFTER the numeric tuple, so `2.3.0-beta.1 < 2.3.0`. */
23
+ export function compareSemver(a, b) {
24
+ const parse = (s) => {
25
+ const [core, pre = ""] = s.replace(/^v/, "").split("-", 2);
26
+ const nums = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
27
+ return { nums, pre };
28
+ };
29
+ const pa = parse(a);
30
+ const pb = parse(b);
31
+ const len = Math.max(pa.nums.length, pb.nums.length);
32
+ for (let i = 0; i < len; i++) {
33
+ const d = (pa.nums[i] ?? 0) - (pb.nums[i] ?? 0);
34
+ if (d !== 0)
35
+ return d > 0 ? 1 : -1;
36
+ }
37
+ // pre-release: empty > non-empty (release > prerelease per semver)
38
+ if (pa.pre === pb.pre)
39
+ return 0;
40
+ if (pa.pre === "")
41
+ return 1;
42
+ if (pb.pre === "")
43
+ return -1;
44
+ return pa.pre < pb.pre ? -1 : 1;
45
+ }
46
+ export function isNewer(latest, current) {
47
+ try {
48
+ return compareSemver(latest, current) > 0;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ /** Sync read; returns null on any error (missing file / parse fail / etc.). */
55
+ export function readCachedUpdate() {
56
+ try {
57
+ const raw = readFileSync(UPDATE_CACHE_PATH, "utf-8");
58
+ const obj = JSON.parse(raw);
59
+ if (typeof obj.checked_at === "string" &&
60
+ typeof obj.current === "string" &&
61
+ typeof obj.latest === "string") {
62
+ return obj;
63
+ }
64
+ return null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ /** Best-effort cache write (atomic via tmp + rename). Returns true on success. */
71
+ export function writeCachedUpdate(info) {
72
+ try {
73
+ mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true });
74
+ const tmp = UPDATE_CACHE_PATH + "." + process.pid + ".tmp";
75
+ writeFileSync(tmp, JSON.stringify(info) + "\n", { encoding: "utf-8" });
76
+ // Atomic rename within same dir
77
+ renameSync(tmp, UPDATE_CACHE_PATH);
78
+ return true;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ /** Returns whether banner emission is currently allowed (TTY + no opt-out). */
85
+ export function isUpdateBannerAllowed() {
86
+ if (process.env.ECHOPAI_DISABLE_UPDATE_CHECK)
87
+ return false;
88
+ if (process.env.CI)
89
+ return false;
90
+ // banner goes to stderr; only emit if user is at an interactive terminal
91
+ if (!process.stderr.isTTY)
92
+ return false;
93
+ return true;
94
+ }
95
+ /**
96
+ * Emit a single-line banner to stderr if cache shows a newer version.
97
+ * Idempotent within a single process: only emits once even if called many
98
+ * times (e.g. from both executeVerb and welcome screen).
99
+ */
100
+ let bannerEmittedThisProcess = false;
101
+ export function maybeEmitUpdateBanner(stream = process.stderr) {
102
+ if (bannerEmittedThisProcess)
103
+ return;
104
+ if (!isUpdateBannerAllowed())
105
+ return;
106
+ const info = readCachedUpdate();
107
+ if (!info)
108
+ return;
109
+ if (!isNewer(info.latest, CLI_VERSION))
110
+ return;
111
+ bannerEmittedThisProcess = true;
112
+ // dim cyan, single line, prefixed with sparkles
113
+ const dim = "\x1b[2m";
114
+ const reset = "\x1b[0m";
115
+ stream.write(`${dim}✨ echopai v${info.latest} available (you have v${CLI_VERSION}) — run \`echopai upgrade\`${reset}\n`);
116
+ }
117
+ /** Test hook: reset the once-per-process gate. */
118
+ export function _resetBannerGateForTests() {
119
+ bannerEmittedThisProcess = false;
120
+ }
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Detached update-check worker. Spawned by `bin.ts` at startup
4
+ * with `detached: true, stdio: 'ignore'` + `child.unref()` so the parent
5
+ * exits immediately while this process finishes the registry fetch in the
6
+ * background.
7
+ *
8
+ * Contract:
9
+ * - argv[2] = current CLI version (informational, stored in cache)
10
+ * - reads cache mtime; if < 24h old, exits 0 without network
11
+ * - else GET https://registry.npmjs.org/echopai/latest with 5s timeout,
12
+ * parses { version }, writes ~/.config/echopai/update_cache.json
13
+ * - all errors swallowed (worker exit 0); next invocation retries
14
+ *
15
+ * Never writes to stdout/stderr. Never throws. Cap process time at ~6s
16
+ * via AbortController to avoid lingering processes on flaky networks.
17
+ */
18
+ import { statSync } from "node:fs";
19
+ import { fetch } from "undici";
20
+ import { UPDATE_CACHE_PATH, writeCachedUpdate, } from "./update_check.js";
21
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
22
+ const FETCH_TIMEOUT_MS = 5_000;
23
+ const REGISTRY_URL = "https://registry.npmjs.org/echopai/latest";
24
+ async function main() {
25
+ const current = process.argv[2] ?? "0.0.0";
26
+ // Skip if cache fresh
27
+ try {
28
+ const stat = statSync(UPDATE_CACHE_PATH);
29
+ if (Date.now() - stat.mtimeMs < CACHE_TTL_MS)
30
+ return;
31
+ }
32
+ catch {
33
+ // missing cache — proceed to fetch
34
+ }
35
+ const ac = new AbortController();
36
+ const timer = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
37
+ try {
38
+ const res = await fetch(REGISTRY_URL, {
39
+ headers: { Accept: "application/json" },
40
+ signal: ac.signal,
41
+ });
42
+ if (!res.ok)
43
+ return;
44
+ const body = (await res.json());
45
+ const latest = body?.version;
46
+ if (!latest || typeof latest !== "string")
47
+ return;
48
+ const info = {
49
+ checked_at: new Date().toISOString(),
50
+ current,
51
+ latest,
52
+ };
53
+ writeCachedUpdate(info);
54
+ }
55
+ catch {
56
+ // network / abort / JSON parse — silent
57
+ }
58
+ finally {
59
+ clearTimeout(timer);
60
+ }
61
+ }
62
+ // Don't propagate errors; this worker is best-effort.
63
+ main().catch(() => { });
@@ -12,6 +12,7 @@
12
12
  import { resolveCredentials, AuthMissingError } from "./auth.js";
13
13
  import { CallApiError } from "./errors.js";
14
14
  import { isTtyHuman, renderError } from "./tty.js";
15
+ import { maybeEmitUpdateBanner } from "./update_check.js";
15
16
  import { CLI_VERSION } from "../version.js";
16
17
  /**
17
18
  * Run a verb handler end-to-end:
@@ -38,6 +39,7 @@ export async function executeVerb(handler) {
38
39
  cliVersion: CLI_VERSION,
39
40
  });
40
41
  process.stdout.write(JSON.stringify(env) + "\n");
42
+ maybeEmitUpdateBanner();
41
43
  process.exit(0);
42
44
  }
43
45
  catch (e) {
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `echopai upgrade` — explicit "check for + show upgrade command" entry.
3
+ *
4
+ * Three flavors:
5
+ * - `echopai upgrade` → 用 cache(无网络)+ 打印 envelope
6
+ * (current / latest / command)
7
+ * - `echopai upgrade --check` → 强制走 npm registry,刷新 cache
8
+ * - `echopai upgrade --check --exec` → 刷新 cache 后真跑 `npm i -g echopai@latest`
9
+ * (透传 stdio;非 0 退出码传递)
10
+ *
11
+ * AI-first envelope on stdout(带 meta.checked_at),错误走 stderr JSON。
12
+ * 永远不会 exec npm 除非显式 --exec。
13
+ */
14
+ import { spawnSync } from "node:child_process";
15
+ import { Command, Option } from "commander";
16
+ import { fetch } from "undici";
17
+ import { CLI_VERSION, } from "../version.js";
18
+ import { isNewer, readCachedUpdate, writeCachedUpdate, } from "../runtime/update_check.js";
19
+ const REGISTRY_URL = "https://registry.npmjs.org/echopai/latest";
20
+ const FETCH_TIMEOUT_MS = 5_000;
21
+ async function fetchLatest() {
22
+ const ac = new AbortController();
23
+ const timer = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
24
+ try {
25
+ const res = await fetch(REGISTRY_URL, {
26
+ headers: { Accept: "application/json" },
27
+ signal: ac.signal,
28
+ });
29
+ if (!res.ok)
30
+ return null;
31
+ const body = (await res.json());
32
+ return typeof body?.version === "string" ? body.version : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ finally {
38
+ clearTimeout(timer);
39
+ }
40
+ }
41
+ function emitJson(data, meta = {}) {
42
+ process.stdout.write(JSON.stringify({ data, meta }) + "\n");
43
+ }
44
+ function emitErr(code, message, exit = 2) {
45
+ process.stderr.write(JSON.stringify({ error: { code, message } }) + "\n");
46
+ process.exit(exit);
47
+ }
48
+ export function buildUpgradeCommand() {
49
+ const cmd = new Command("upgrade").description("Check for + show install command for the latest echopai release (no auto-exec unless --exec).");
50
+ cmd.addOption(new Option("--check", "Force fresh registry fetch (skip 24h cache)"));
51
+ cmd.addOption(new Option("--exec", "Actually run `npm i -g echopai@latest` (default: print command only)"));
52
+ cmd.action(async (opts) => {
53
+ let info = opts.check ? null : readCachedUpdate();
54
+ if (!info || opts.check) {
55
+ const latest = await fetchLatest();
56
+ if (!latest) {
57
+ emitErr("network_error", "Failed to reach npm registry (timeout or non-200). Try again later.");
58
+ }
59
+ info = {
60
+ checked_at: new Date().toISOString(),
61
+ current: CLI_VERSION,
62
+ latest,
63
+ };
64
+ writeCachedUpdate(info);
65
+ }
66
+ const newer = isNewer(info.latest, CLI_VERSION);
67
+ const installCmd = "npm i -g echopai@latest";
68
+ if (!newer) {
69
+ emitJson({
70
+ current: CLI_VERSION,
71
+ latest: info.latest,
72
+ status: "up_to_date",
73
+ message: `Already on the latest version (v${CLI_VERSION}).`,
74
+ }, { checked_at: info.checked_at, source: opts.check ? "registry" : "cache" });
75
+ process.exit(0);
76
+ }
77
+ // Newer available
78
+ if (opts.exec) {
79
+ // Run npm i -g with inherited stdio so user sees real-time progress.
80
+ // Don't shell-out — use direct args to avoid quoting issues.
81
+ const r = spawnSync("npm", ["i", "-g", `echopai@${info.latest}`], { stdio: "inherit" });
82
+ if (r.status === 0) {
83
+ emitJson({
84
+ current: CLI_VERSION,
85
+ latest: info.latest,
86
+ status: "upgraded",
87
+ message: `Upgraded to v${info.latest}. Re-run any command to load the new version.`,
88
+ }, { checked_at: info.checked_at, exec: true });
89
+ process.exit(0);
90
+ }
91
+ emitErr("exec_failed", `npm i -g failed with exit ${r.status ?? "unknown"}. Run manually: ${installCmd}`);
92
+ }
93
+ emitJson({
94
+ current: CLI_VERSION,
95
+ latest: info.latest,
96
+ status: "update_available",
97
+ command: installCmd,
98
+ message: `v${info.latest} is available. Run: ${installCmd} (or pass --exec to run it now)`,
99
+ }, { checked_at: info.checked_at, source: opts.check ? "registry" : "cache" });
100
+ process.exit(0);
101
+ });
102
+ return cmd;
103
+ }
@@ -14,6 +14,7 @@
14
14
  import { Command } from "commander";
15
15
  import { resolveCredentials, AuthMissingError } from "../runtime/auth.js";
16
16
  import { bold, cyan, dim, green, isTtyHuman, yellow } from "../runtime/tty.js";
17
+ import { maybeEmitUpdateBanner } from "../runtime/update_check.js";
17
18
  import { CLI_VERSION } from "../version.js";
18
19
  function maskKey(key) {
19
20
  // eps_live_<lookup>_<secret> → keep prefix + first 4 of lookup + masked tail.
@@ -52,7 +53,7 @@ const COMMAND_GROUPS = [
52
53
  {
53
54
  title: "🔍 快速检索",
54
55
  items: [
55
- { cmd: "echopai lookup --text 茅台", note: "中文名 / 拼音首字母 → canonical_code" },
56
+ { cmd: "echopai lookup --text 贵州茅台", note: "中文名 / 拼音首字母 → canonical_code" },
56
57
  { cmd: "echopai digest --code SSE:600519", note: "一键研究摘要(5 桶 fan-out,容忍部分失败)" },
57
58
  { cmd: "echopai search --query \"AI 算力\"", note: "题材 / 概念语义搜索" },
58
59
  ],
@@ -61,13 +62,40 @@ const COMMAND_GROUPS = [
61
62
  title: "📈 实时行情",
62
63
  items: [
63
64
  { cmd: "echopai quote --codes \"SSE:600519,SZSE:000001\"", note: "1-200 只实时报价" },
64
- { cmd: "echopai sentiment", note: "市场情绪总览(涨跌停 / 宽度 / 分化)" },
65
- { cmd: "echopai hot", note: "东方财富热门股榜(综合搜索 / 关注 / 评论)" },
65
+ { cmd: "echopai market status", note: "A 股会话状态(pre-open/open/lunch/closed)+ 节假日" },
66
+ { cmd: "echopai market movers --sort pct --top 20", note: "/market 涨幅榜 top 20(pct/pct_asc/speed/amount/turnover/total_mv)" },
67
+ { cmd: "echopai market movers --sort speed --top 30", note: "3 分钟涨速榜(短线脉冲)" },
68
+ { cmd: "echopai market movers --sort amount --exchange SSE", note: "上证成交额榜" },
69
+ { cmd: "echopai sentiment", note: "市场情绪总览(实时;涨跌停 / 宽度 / 分化)" },
70
+ { cmd: "echopai sentiment overview --date 2026-05-20", note: "任意历史日的情绪聚合(最后一分钟)" },
71
+ { cmd: "echopai sentiment breadth --date 2026-05-20", note: "全天 ~241 分钟时序(任意日)" },
72
+ { cmd: "echopai sentiment turnover --date 2026-05-20 --days 5", note: "成交额时序 + 5 日同分钟同比" },
73
+ { cmd: "echopai hot", note: "机构推荐热股榜" },
66
74
  { cmd: "echopai scan", note: "全市场快照(约 5800 只)" },
67
75
  ],
68
76
  },
69
77
  {
70
- title: "💰 基本面 / 估值",
78
+ title: "🚀 涨停板",
79
+ items: [
80
+ { cmd: "echopai limit-up summary", note: "涨停数 / 炸板数 / 跌停数 / 连板梯队" },
81
+ { cmd: "echopai limit-up pool", note: "当日涨停股逐股明细(连板 / 封单 / 一字板)" },
82
+ { cmd: "echopai limit-up history --days 30", note: "近 N 日涨停 / 最高连板趋势" },
83
+ ],
84
+ },
85
+ {
86
+ title: "🧩 概念板块",
87
+ items: [
88
+ { cmd: "echopai concepts list --sort pct", note: "概念涨幅榜(pct/amount/limit_up/stock_count/speed)" },
89
+ { cmd: "echopai concepts list --sort speed", note: "概念 3 分钟涨速榜(客户端排序 speed_3min)" },
90
+ { cmd: "echopai concepts alerts", note: "当前激活概念异动(big_move / limit_up_cluster)" },
91
+ { cmd: "echopai concepts show <concept_id>", note: "单概念 meta + 成份股 + 最新行情" },
92
+ { cmd: "echopai concepts daily-bars <concept_id> --from ... --to ...", note: "概念指数 K 线(链式等权 base 1000)" },
93
+ { cmd: "echopai concepts news <concept_id>", note: "概念关联新闻(ILIKE)" },
94
+ { cmd: "echopai concepts views <concept_id>", note: "概念关联券商观点(主源研究)" },
95
+ ],
96
+ },
97
+ {
98
+ title: "💰 财务数据",
71
99
  items: [
72
100
  { cmd: "echopai financials quote-snapshot --code SSE:600519", note: "估值快照 14 字段(PE/PB/PS/换手率/股息率/量比)★头牌★" },
73
101
  { cmd: "echopai financials pit --code SSE:600519 --date 2024-11-01", note: "Point-in-time 财务指标(回测防穿越)" },
@@ -76,10 +104,13 @@ const COMMAND_GROUPS = [
76
104
  ],
77
105
  },
78
106
  {
79
- title: "📰 研究 / 资讯",
107
+ title: "📰 市场观点/研究/资讯",
80
108
  items: [
81
- { cmd: "echopai views --code SSE:600519", note: "★主源★ 卖方研报 / 分析师观点(含 research_entity_id 归因)" },
109
+ { cmd: "echopai views --code SSE:600519", note: "★主源★ 卖方研报 / 分析师观点" },
82
110
  { cmd: "echopai news search --query AI --since-hours 24", note: "辅源新闻 / 简讯(短时窗)" },
111
+ { cmd: "echopai announcements feed --type annual_report", note: "全市场公告 feed(cninfo,按 published_at desc)" },
112
+ { cmd: "echopai announcements stock --code SSE:600519 --since-days 30", note: "单股公告历史窗口(最长 5 年)" },
113
+ { cmd: "echopai announcements detail <uuid>", note: "单条公告完整正文 + meta" },
83
114
  ],
84
115
  },
85
116
  {
@@ -103,6 +134,7 @@ const POINTERS = [
103
134
  { cmd: "echopai whoami", note: "在线检查 token 能力 / scopes" },
104
135
  { cmd: "echopai doctor", note: "诊断鉴权 / 网络连通性" },
105
136
  { cmd: "echopai status", note: "查看本地 profile + 鉴权状态" },
137
+ { cmd: "echopai upgrade [--exec]", note: "检查新版(24h cache),--exec 直接 npm i -g 升级" },
106
138
  ];
107
139
  const BANNER_PLAIN = [
108
140
  " ███████ ██████ ██ ██ ██████ ██████ █████ ██",
@@ -155,7 +187,7 @@ export function renderWelcome(now = () => new Date()) {
155
187
  lines.push(` ${padRight(cyan(it.cmd), widest + 4)} ${dim(it.note)}`);
156
188
  }
157
189
  lines.push("");
158
- lines.push(` ${dim("文档: https://docs.echopai.com · 反馈: https://github.com/evanzhangx/EchoPulse/issues")}`);
190
+ lines.push(` ${dim("官网: https://www.echopai.com · 反馈: service@echopai.com")}`);
159
191
  lines.push("");
160
192
  // Timestamp footer so users know this isn't a stale cache
161
193
  const ts = now().toISOString().replace("T", " ").slice(0, 19);
@@ -166,6 +198,9 @@ export function renderWelcome(now = () => new Date()) {
166
198
  /** Print welcome screen to stdout (no exit — caller decides). */
167
199
  export function printWelcome() {
168
200
  process.stdout.write(renderWelcome());
201
+ // Surface available update from local cache (worker refreshes it in the
202
+ // background). No network call here — fast even when there's nothing to say.
203
+ maybeEmitUpdateBanner();
169
204
  }
170
205
  /**
171
206
  * Decide whether bare `echopai` (no args, no flags) should show the welcome