echopai 2.3.0 → 2.5.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/README.md +63 -348
- package/dist/bin.js +8388 -190
- package/package.json +11 -13
- package/dist/_generated/commands.js +0 -378
- package/dist/_generated/help.js +0 -295
- package/dist/_generated/operations.js +0 -2385
- package/dist/runtime/auth.js +0 -95
- package/dist/runtime/envelope.js +0 -52
- package/dist/runtime/errors.js +0 -186
- package/dist/runtime/filters.js +0 -153
- package/dist/runtime/format.js +0 -143
- package/dist/runtime/http.js +0 -65
- package/dist/runtime/idempotency.js +0 -18
- package/dist/runtime/invoker.js +0 -391
- package/dist/runtime/io.js +0 -16
- package/dist/runtime/paginator.js +0 -146
- package/dist/runtime/trace.js +0 -99
- package/dist/runtime/tty.js +0 -51
- package/dist/runtime/update_check.js +0 -120
- package/dist/runtime/update_worker.js +0 -63
- package/dist/runtime/verb_cmd.js +0 -72
- package/dist/runtime/verb_runner.js +0 -152
- package/dist/runtime/whoami_cache.js +0 -109
- package/dist/tools/api.js +0 -81
- package/dist/tools/completion.js +0 -116
- package/dist/tools/config.js +0 -123
- package/dist/tools/doctor.js +0 -183
- package/dist/tools/login.js +0 -99
- package/dist/tools/mcp.js +0 -141
- package/dist/tools/raw.js +0 -96
- package/dist/tools/schema.js +0 -58
- package/dist/tools/trace.js +0 -54
- package/dist/tools/upgrade.js +0 -103
- package/dist/tools/welcome.js +0 -225
- package/dist/tools/whoami.js +0 -132
- package/dist/verbs/_spec.js +0 -15
- package/dist/verbs/announcements.js +0 -195
- package/dist/verbs/bars_batch.js +0 -66
- package/dist/verbs/chart.js +0 -110
- package/dist/verbs/concepts.js +0 -393
- package/dist/verbs/digest.js +0 -351
- package/dist/verbs/financials.js +0 -212
- package/dist/verbs/hot.js +0 -29
- package/dist/verbs/index.js +0 -88
- package/dist/verbs/limit_up.js +0 -156
- package/dist/verbs/lookup.js +0 -72
- package/dist/verbs/market.js +0 -185
- package/dist/verbs/news.js +0 -81
- package/dist/verbs/quote.js +0 -53
- package/dist/verbs/scan.js +0 -42
- package/dist/verbs/search.js +0 -105
- package/dist/verbs/sentiment.js +0 -231
- package/dist/verbs/views.js +0 -85
- package/dist/version.js +0 -5
package/dist/tools/raw.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai raw call <method> <path> [--data '{...}'] [--query k=v ...]`
|
|
3
|
-
*
|
|
4
|
-
* Raw HTTP passthrough — 给未 codegen 的端点 / debug / 应急用。
|
|
5
|
-
* 跳过 Ajv pre-flight 校验、跳过 envelope 解析;行为 = curl + Bearer 注入 + base URL。
|
|
6
|
-
*
|
|
7
|
-
* 自动注入 X-Client / X-Client-Channel / X-Request-Id (与 spec-driven 调用同源),
|
|
8
|
-
* 这样 server 端 audit 能识别这是 CLI 流量。
|
|
9
|
-
*
|
|
10
|
-
* `echopai api call ...` 保留为 deprecated alias 一个 release (tools/api.ts);
|
|
11
|
-
* 下一个 major 移除。
|
|
12
|
-
*/
|
|
13
|
-
import { Command } from "commander";
|
|
14
|
-
import { fetch as undiciFetch } from "undici";
|
|
15
|
-
import { resolveCredentials, AuthMissingError } from "../runtime/auth.js";
|
|
16
|
-
import { buildHttpHeaders } from "../runtime/http.js";
|
|
17
|
-
import { CLI_VERSION } from "../version.js";
|
|
18
|
-
export function buildRawCallCommand() {
|
|
19
|
-
const call = new Command("call")
|
|
20
|
-
.description("Raw HTTP passthrough (arbitrary <method> <path>; debug / non-codegen endpoints)")
|
|
21
|
-
.argument("<method>", "HTTP method (GET|POST|PUT|PATCH|DELETE|HEAD)")
|
|
22
|
-
.argument("<path>", "URL path (relative; base URL injected from credential)")
|
|
23
|
-
.option("-d, --data <json>", "POST/PUT/PATCH body (JSON string)")
|
|
24
|
-
.option("-q, --query <kv...>", "Query string entries: k=v")
|
|
25
|
-
.option("--header <kv...>", "Extra headers: K=V");
|
|
26
|
-
call.action(async (method, urlPath, opts) => {
|
|
27
|
-
const m = method.toUpperCase();
|
|
28
|
-
if (!["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"].includes(m)) {
|
|
29
|
-
return die("invalid_args", `Unknown HTTP method: ${method}`, 1);
|
|
30
|
-
}
|
|
31
|
-
let creds;
|
|
32
|
-
try {
|
|
33
|
-
creds = resolveCredentials({});
|
|
34
|
-
}
|
|
35
|
-
catch (e) {
|
|
36
|
-
if (e instanceof AuthMissingError) {
|
|
37
|
-
return die("auth_missing", e.message, 1, e.recovery_hint);
|
|
38
|
-
}
|
|
39
|
-
throw e;
|
|
40
|
-
}
|
|
41
|
-
let url = creds.baseUrl + (urlPath.startsWith("/") ? urlPath : "/" + urlPath);
|
|
42
|
-
if (opts.query?.length) {
|
|
43
|
-
const qs = opts.query
|
|
44
|
-
.map((kv) => {
|
|
45
|
-
const idx = kv.indexOf("=");
|
|
46
|
-
if (idx === -1)
|
|
47
|
-
return null;
|
|
48
|
-
return `${encodeURIComponent(kv.slice(0, idx))}=${encodeURIComponent(kv.slice(idx + 1))}`;
|
|
49
|
-
})
|
|
50
|
-
.filter((s) => s !== null)
|
|
51
|
-
.join("&");
|
|
52
|
-
if (qs)
|
|
53
|
-
url += (url.includes("?") ? "&" : "?") + qs;
|
|
54
|
-
}
|
|
55
|
-
// Use centralized header builder (Phase 1.1) — automatically injects
|
|
56
|
-
// X-Client / X-Client-Channel / X-Request-Id. Raw passthrough should
|
|
57
|
-
// still be tagged as CLI traffic in server audit.
|
|
58
|
-
const { headers } = buildHttpHeaders({
|
|
59
|
-
bearer: creds.key,
|
|
60
|
-
cliVersion: CLI_VERSION,
|
|
61
|
-
});
|
|
62
|
-
if (opts.header) {
|
|
63
|
-
for (const kv of opts.header) {
|
|
64
|
-
const idx = kv.indexOf("=");
|
|
65
|
-
if (idx === -1)
|
|
66
|
-
continue;
|
|
67
|
-
headers.set(kv.slice(0, idx), kv.slice(idx + 1));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const init = { method: m, headers };
|
|
71
|
-
if (opts.data) {
|
|
72
|
-
if (!headers.has("content-type")) {
|
|
73
|
-
headers.set("content-type", "application/json");
|
|
74
|
-
}
|
|
75
|
-
init.body = opts.data;
|
|
76
|
-
}
|
|
77
|
-
const res = await undiciFetch(url, init);
|
|
78
|
-
const body = await res.text();
|
|
79
|
-
process.stdout.write(body + (body.endsWith("\n") ? "" : "\n"));
|
|
80
|
-
process.exit(res.status >= 400 && res.status < 500
|
|
81
|
-
? 1
|
|
82
|
-
: res.status >= 500
|
|
83
|
-
? 2
|
|
84
|
-
: 0);
|
|
85
|
-
});
|
|
86
|
-
return call;
|
|
87
|
-
}
|
|
88
|
-
function die(code, message, exitCode, recovery_hint) {
|
|
89
|
-
const env = {
|
|
90
|
-
error: { code, message, retryable: false },
|
|
91
|
-
};
|
|
92
|
-
if (recovery_hint)
|
|
93
|
-
env.error.recovery_hint = recovery_hint;
|
|
94
|
-
process.stderr.write(JSON.stringify(env) + "\n");
|
|
95
|
-
process.exit(exitCode);
|
|
96
|
-
}
|
package/dist/tools/schema.js
DELETED
|
@@ -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
|
-
}
|
package/dist/tools/trace.js
DELETED
|
@@ -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
|
-
}
|
package/dist/tools/upgrade.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,225 +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 { maybeEmitUpdateBanner } from "../runtime/update_check.js";
|
|
18
|
-
import { CLI_VERSION } from "../version.js";
|
|
19
|
-
function maskKey(key) {
|
|
20
|
-
// eps_live_<lookup>_<secret> → keep prefix + first 4 of lookup + masked tail.
|
|
21
|
-
if (key.length <= 16)
|
|
22
|
-
return "***";
|
|
23
|
-
return key.slice(0, 13) + "***";
|
|
24
|
-
}
|
|
25
|
-
function snapshotAuth() {
|
|
26
|
-
try {
|
|
27
|
-
const creds = resolveCredentials({});
|
|
28
|
-
if (creds.profile) {
|
|
29
|
-
return {
|
|
30
|
-
state: "profile",
|
|
31
|
-
profile: creds.profile,
|
|
32
|
-
keyHint: maskKey(creds.key),
|
|
33
|
-
baseUrl: creds.baseUrl,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
return { state: "env", keyHint: maskKey(creds.key), baseUrl: creds.baseUrl };
|
|
37
|
-
}
|
|
38
|
-
catch (e) {
|
|
39
|
-
if (e instanceof AuthMissingError)
|
|
40
|
-
return { state: "missing" };
|
|
41
|
-
throw e;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/** Right-pad a string to width `w`, ignoring ANSI escapes for length. */
|
|
45
|
-
function padRight(s, w) {
|
|
46
|
-
// strip ANSI for length measurement
|
|
47
|
-
// eslint-disable-next-line no-control-regex
|
|
48
|
-
const visible = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
49
|
-
const pad = Math.max(0, w - visible.length);
|
|
50
|
-
return s + " ".repeat(pad);
|
|
51
|
-
}
|
|
52
|
-
const COMMAND_GROUPS = [
|
|
53
|
-
{
|
|
54
|
-
title: "🔍 快速检索",
|
|
55
|
-
items: [
|
|
56
|
-
{ cmd: "echopai lookup --text 贵州茅台", note: "中文名 / 拼音首字母 → canonical_code" },
|
|
57
|
-
{ cmd: "echopai digest --code SSE:600519", note: "一键研究摘要(5 桶 fan-out,容忍部分失败)" },
|
|
58
|
-
{ cmd: "echopai search --query \"AI 算力\"", note: "题材 / 概念语义搜索" },
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
title: "📈 实时行情",
|
|
63
|
-
items: [
|
|
64
|
-
{ cmd: "echopai quote --codes \"SSE:600519,SZSE:000001\"", note: "1-200 只实时报价" },
|
|
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: "机构推荐热股榜" },
|
|
74
|
-
{ cmd: "echopai scan", note: "全市场快照(约 5800 只)" },
|
|
75
|
-
],
|
|
76
|
-
},
|
|
77
|
-
{
|
|
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: "💰 财务数据",
|
|
99
|
-
items: [
|
|
100
|
-
{ cmd: "echopai financials quote-snapshot --code SSE:600519", note: "估值快照 14 字段(PE/PB/PS/换手率/股息率/量比)★头牌★" },
|
|
101
|
-
{ cmd: "echopai financials pit --code SSE:600519 --date 2024-11-01", note: "Point-in-time 财务指标(回测防穿越)" },
|
|
102
|
-
{ cmd: "echopai financials reports --code SSE:600519 --kind annual", note: "最近 N 期财务报告(~25 字段/期)" },
|
|
103
|
-
{ cmd: "echopai financials series --code SSE:600519 --metric roe_simple", note: "单指标历史时间序列" },
|
|
104
|
-
],
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
title: "📰 市场观点/研究/资讯",
|
|
108
|
-
items: [
|
|
109
|
-
{ cmd: "echopai views --code SSE:600519", note: "★主源★ 卖方研报 / 分析师观点" },
|
|
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" },
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
title: "📊 K 线",
|
|
118
|
-
items: [
|
|
119
|
-
{ cmd: "echopai chart --code SSE:600519 --from 2026-01-01 --to 2026-05-01", note: "单只 K 线(日线 / 分钟线)" },
|
|
120
|
-
{ cmd: "echopai bars-batch --codes A,B,C --from ... --to ...", note: "批量 K 线(≤100 只)" },
|
|
121
|
-
],
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
title: "🤖 AI 集成",
|
|
125
|
-
items: [
|
|
126
|
-
{ cmd: "echopai mcp serve", note: "启动 MCP stdio 服务(Claude Desktop / Cursor / Claude Code)" },
|
|
127
|
-
{ cmd: "echopai schema export", note: "导出全部 operation schema 给 agent 喂养" },
|
|
128
|
-
],
|
|
129
|
-
},
|
|
130
|
-
];
|
|
131
|
-
const POINTERS = [
|
|
132
|
-
{ cmd: "echopai --help", note: "完整命令树" },
|
|
133
|
-
{ cmd: "echopai <verb> --help", note: "查看单个命令的参数和示例" },
|
|
134
|
-
{ cmd: "echopai whoami", note: "在线检查 token 能力 / scopes" },
|
|
135
|
-
{ cmd: "echopai doctor", note: "诊断鉴权 / 网络连通性" },
|
|
136
|
-
{ cmd: "echopai status", note: "查看本地 profile + 鉴权状态" },
|
|
137
|
-
{ cmd: "echopai upgrade [--exec]", note: "检查新版(24h cache),--exec 直接 npm i -g 升级" },
|
|
138
|
-
];
|
|
139
|
-
const BANNER_PLAIN = [
|
|
140
|
-
" ███████ ██████ ██ ██ ██████ ██████ █████ ██",
|
|
141
|
-
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██",
|
|
142
|
-
" █████ ██ ███████ ██ ██ ██████ ███████ ██",
|
|
143
|
-
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██",
|
|
144
|
-
" ███████ ██████ ██ ██ ██████ ██ ██ ██ ██",
|
|
145
|
-
];
|
|
146
|
-
export function renderWelcome(now = () => new Date()) {
|
|
147
|
-
const lines = [];
|
|
148
|
-
const auth = snapshotAuth();
|
|
149
|
-
// Banner
|
|
150
|
-
lines.push("");
|
|
151
|
-
for (const row of BANNER_PLAIN)
|
|
152
|
-
lines.push(cyan(row));
|
|
153
|
-
lines.push("");
|
|
154
|
-
lines.push(` ${bold("EchoPai CLI")} ${dim("v" + CLI_VERSION)} ${dim("·")} ${dim("面向 AI Agent 的 A 股数据接入终端")}`);
|
|
155
|
-
lines.push("");
|
|
156
|
-
// Auth status
|
|
157
|
-
if (auth.state === "missing") {
|
|
158
|
-
lines.push(` ${yellow("●")} ${bold("未登录")} ${dim("—")} 运行 ${cyan("`echopai login --key eps_live_<lookup>_<secret>`")} 或设置环境变量 ${cyan("ECHOPAI_KEY")}`);
|
|
159
|
-
}
|
|
160
|
-
else if (auth.state === "env") {
|
|
161
|
-
lines.push(` ${green("●")} ${bold("已登录")}(环境变量 ${cyan("ECHOPAI_KEY")})${dim(`(${auth.keyHint})`)} → ${dim(auth.baseUrl)}`);
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
lines.push(` ${green("●")} ${bold("已登录")} profile ${cyan(auth.profile)} ${dim(`(${auth.keyHint})`)} → ${dim(auth.baseUrl)}`);
|
|
165
|
-
}
|
|
166
|
-
lines.push("");
|
|
167
|
-
// Command groups
|
|
168
|
-
lines.push(` ${dim("── 常用命令 ─────────────────────────────────────────────────")}`);
|
|
169
|
-
lines.push("");
|
|
170
|
-
// Reserve column 1 for cmd, column 2 for note. Compute padding from the
|
|
171
|
-
// widest cmd across all groups to keep alignment stable.
|
|
172
|
-
const widest = Math.min(72, COMMAND_GROUPS.flatMap((g) => g.items)
|
|
173
|
-
.map((it) => it.cmd.length)
|
|
174
|
-
.reduce((a, b) => Math.max(a, b), 0));
|
|
175
|
-
for (const group of COMMAND_GROUPS) {
|
|
176
|
-
lines.push(` ${bold(group.title)}`);
|
|
177
|
-
for (const it of group.items) {
|
|
178
|
-
const cmd = cyan(it.cmd);
|
|
179
|
-
lines.push(` ${padRight(cmd, widest + 4)} ${dim(it.note)}`);
|
|
180
|
-
}
|
|
181
|
-
lines.push("");
|
|
182
|
-
}
|
|
183
|
-
// Pointers
|
|
184
|
-
lines.push(` ${dim("── 更多 ─────────────────────────────────────────────────────")}`);
|
|
185
|
-
lines.push("");
|
|
186
|
-
for (const it of POINTERS) {
|
|
187
|
-
lines.push(` ${padRight(cyan(it.cmd), widest + 4)} ${dim(it.note)}`);
|
|
188
|
-
}
|
|
189
|
-
lines.push("");
|
|
190
|
-
lines.push(` ${dim("官网: https://www.echopai.com · 反馈: service@echopai.com")}`);
|
|
191
|
-
lines.push("");
|
|
192
|
-
// Timestamp footer so users know this isn't a stale cache
|
|
193
|
-
const ts = now().toISOString().replace("T", " ").slice(0, 19);
|
|
194
|
-
lines.push(` ${dim("生成于 " + ts + " UTC · Ctrl+C 退出")}`);
|
|
195
|
-
lines.push("");
|
|
196
|
-
return lines.join("\n");
|
|
197
|
-
}
|
|
198
|
-
/** Print welcome screen to stdout (no exit — caller decides). */
|
|
199
|
-
export function printWelcome() {
|
|
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();
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Decide whether bare `echopai` (no args, no flags) should show the welcome
|
|
207
|
-
* screen. False for AI / CI / piped output — they get commander's plain help.
|
|
208
|
-
*/
|
|
209
|
-
export function shouldShowWelcome(argv) {
|
|
210
|
-
// node + script + (no extra args)
|
|
211
|
-
if (argv.length !== 2)
|
|
212
|
-
return false;
|
|
213
|
-
if (!isTtyHuman)
|
|
214
|
-
return false;
|
|
215
|
-
if (process.env.CI)
|
|
216
|
-
return false;
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
export function buildWelcomeCommand() {
|
|
220
|
-
return new Command("welcome")
|
|
221
|
-
.description("显示欢迎屏(banner + 常用命令 + 鉴权状态)")
|
|
222
|
-
.action(() => {
|
|
223
|
-
printWelcome();
|
|
224
|
-
});
|
|
225
|
-
}
|
package/dist/tools/whoami.js
DELETED
|
@@ -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
|
-
}
|