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/_generated/commands.js +96 -0
- package/dist/_generated/help.js +107 -7
- package/dist/_generated/operations.js +883 -27
- package/dist/bin.js +53 -8
- package/dist/runtime/invoker.js +4 -0
- package/dist/runtime/update_check.js +120 -0
- package/dist/runtime/update_worker.js +63 -0
- package/dist/runtime/verb_cmd.js +2 -0
- package/dist/tools/upgrade.js +103 -0
- package/dist/tools/welcome.js +42 -7
- package/dist/verbs/announcements.js +195 -0
- package/dist/verbs/concepts.js +393 -0
- package/dist/verbs/digest.js +9 -2
- package/dist/verbs/index.js +37 -6
- package/dist/verbs/limit_up.js +156 -0
- package/dist/verbs/lookup.js +1 -1
- package/dist/verbs/market.js +185 -0
- package/dist/verbs/news.js +17 -3
- package/dist/verbs/quote.js +1 -1
- package/dist/verbs/sentiment.js +191 -6
- package/dist/verbs/views.js +5 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
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:
|
|
55
|
-
"Raw mirror:
|
|
56
|
-
"Curated verbs:
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
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).
|
package/dist/runtime/invoker.js
CHANGED
|
@@ -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(() => { });
|
package/dist/runtime/verb_cmd.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/tools/welcome.js
CHANGED
|
@@ -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
|
|
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
|
|
65
|
-
{ cmd: "echopai
|
|
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: "★主源★ 卖方研报 /
|
|
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("
|
|
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
|