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.
Files changed (54) hide show
  1. package/README.md +63 -348
  2. package/dist/bin.js +8388 -190
  3. package/package.json +11 -13
  4. package/dist/_generated/commands.js +0 -378
  5. package/dist/_generated/help.js +0 -295
  6. package/dist/_generated/operations.js +0 -2385
  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 -391
  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/update_check.js +0 -120
  20. package/dist/runtime/update_worker.js +0 -63
  21. package/dist/runtime/verb_cmd.js +0 -72
  22. package/dist/runtime/verb_runner.js +0 -152
  23. package/dist/runtime/whoami_cache.js +0 -109
  24. package/dist/tools/api.js +0 -81
  25. package/dist/tools/completion.js +0 -116
  26. package/dist/tools/config.js +0 -123
  27. package/dist/tools/doctor.js +0 -183
  28. package/dist/tools/login.js +0 -99
  29. package/dist/tools/mcp.js +0 -141
  30. package/dist/tools/raw.js +0 -96
  31. package/dist/tools/schema.js +0 -58
  32. package/dist/tools/trace.js +0 -54
  33. package/dist/tools/upgrade.js +0 -103
  34. package/dist/tools/welcome.js +0 -225
  35. package/dist/tools/whoami.js +0 -132
  36. package/dist/verbs/_spec.js +0 -15
  37. package/dist/verbs/announcements.js +0 -195
  38. package/dist/verbs/bars_batch.js +0 -66
  39. package/dist/verbs/chart.js +0 -110
  40. package/dist/verbs/concepts.js +0 -393
  41. package/dist/verbs/digest.js +0 -351
  42. package/dist/verbs/financials.js +0 -212
  43. package/dist/verbs/hot.js +0 -29
  44. package/dist/verbs/index.js +0 -88
  45. package/dist/verbs/limit_up.js +0 -156
  46. package/dist/verbs/lookup.js +0 -72
  47. package/dist/verbs/market.js +0 -185
  48. package/dist/verbs/news.js +0 -81
  49. package/dist/verbs/quote.js +0 -53
  50. package/dist/verbs/scan.js +0 -42
  51. package/dist/verbs/search.js +0 -105
  52. package/dist/verbs/sentiment.js +0 -231
  53. package/dist/verbs/views.js +0 -85
  54. package/dist/version.js +0 -5
@@ -1,51 +0,0 @@
1
- /**
2
- * TTY 检测 + 极小 ANSI 着色(无依赖,避免 chalk/ora 拖慢 CLI 启动)。
3
- *
4
- * 启用规则:
5
- * - process.stdout.isTTY === true
6
- * - !process.env.CI
7
- * - !process.env.NO_COLOR
8
- * - process.env.TERM !== 'dumb'
9
- *
10
- * 任一失败 → color() / dim() / errSym() 返回原文(脚本/AI 场景纯洁)。
11
- */
12
- const enable = (() => {
13
- if (!process.stderr.isTTY)
14
- return false;
15
- if (process.env.CI)
16
- return false;
17
- if (process.env.NO_COLOR)
18
- return false;
19
- if (process.env.TERM === "dumb")
20
- return false;
21
- return true;
22
- })();
23
- export const isTtyHuman = enable;
24
- const ESC = "[";
25
- function ansi(code, s) {
26
- if (!enable)
27
- return s;
28
- return `${ESC}${code}m${s}${ESC}0m`;
29
- }
30
- export const red = (s) => ansi("31", s);
31
- export const green = (s) => ansi("32", s);
32
- export const yellow = (s) => ansi("33", s);
33
- export const cyan = (s) => ansi("36", s);
34
- export const dim = (s) => ansi("2", s);
35
- export const bold = (s) => ansi("1", s);
36
- /**
37
- * 把 error envelope 渲染成多行人性化文字(仅 TTY)。
38
- * 非 TTY 直接 JSON.stringify。
39
- */
40
- export function renderError(env) {
41
- if (!enable)
42
- return JSON.stringify(env);
43
- const e = env.error;
44
- const label = `${red(bold("✗ " + e.code))}${e.retryable ? " " + dim("(retryable)") : ""}`;
45
- const lines = [`${label} ${dim("—")} ${e.message}`];
46
- if (e.recovery_hint)
47
- lines.push(` ${cyan("hint:")} ${e.recovery_hint}`);
48
- if (e.request_id)
49
- lines.push(` ${dim("request_id: " + e.request_id)}`);
50
- return lines.join("\n");
51
- }
@@ -1,120 +0,0 @@
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
- }
@@ -1,63 +0,0 @@
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(() => { });
@@ -1,72 +0,0 @@
1
- /**
2
- * Shared scaffolding for curated agent verbs.
3
- *
4
- * 每个 verb 都做同样三件事:解析 credential → 调一次或多次 callOp → emit
5
- * envelope + exit。本模块把样板抽出,让 verb 实现只关心"参数 → 派生 args → 映射
6
- * 输出"。
7
- *
8
- * 与 verb_runner.ts 的区别:
9
- * - verb_runner: 纯 HTTP/Ajv 层,不知道 commander / exit / stderr renderer
10
- * - verb_cmd: process / commander 层 ergonomics,把 runner 包成"一行起 verb"
11
- */
12
- import { resolveCredentials, AuthMissingError } from "./auth.js";
13
- import { CallApiError } from "./errors.js";
14
- import { isTtyHuman, renderError } from "./tty.js";
15
- import { maybeEmitUpdateBanner } from "./update_check.js";
16
- import { CLI_VERSION } from "../version.js";
17
- /**
18
- * Run a verb handler end-to-end:
19
- * 1. Resolve credentials (auth_missing → exit 1)
20
- * 2. Invoke handler with ctx
21
- * 3. Print stdout envelope + exit 0 on success
22
- * 4. Map CallApiError → structured stderr + exit 1/2 by HTTP class
23
- */
24
- export async function executeVerb(handler) {
25
- let creds;
26
- try {
27
- creds = resolveCredentials({});
28
- }
29
- catch (e) {
30
- if (e instanceof AuthMissingError) {
31
- emitVerbError("auth_missing", e.message, e.recovery_hint, 1);
32
- }
33
- throw e;
34
- }
35
- try {
36
- const env = await handler({
37
- baseUrl: creds.baseUrl,
38
- bearer: creds.key,
39
- cliVersion: CLI_VERSION,
40
- });
41
- process.stdout.write(JSON.stringify(env) + "\n");
42
- maybeEmitUpdateBanner();
43
- process.exit(0);
44
- }
45
- catch (e) {
46
- if (e instanceof CallApiError) {
47
- emitVerbError(e.code, e.message, e.recovery_hint, e.httpStatus && e.httpStatus < 500 ? 1 : 2, e.requestId);
48
- }
49
- // unexpected error: structured emit, exit 2
50
- emitVerbError("internal_error", e instanceof Error ? e.message : String(e), undefined, 2);
51
- }
52
- }
53
- export function emitVerbError(code, message, recoveryHint, exitCode, requestId) {
54
- const env = {
55
- error: {
56
- code,
57
- message,
58
- retryable: false,
59
- ...(recoveryHint ? { recovery_hint: recoveryHint } : {}),
60
- ...(requestId ? { request_id: requestId } : {}),
61
- },
62
- };
63
- if (isTtyHuman) {
64
- process.stderr.write(renderError(env) + "\n");
65
- }
66
- else {
67
- process.stderr.write(JSON.stringify(env) + "\n");
68
- }
69
- process.exit(exitCode);
70
- }
71
- /** Re-export for verb-local convenience. */
72
- export { CLI_VERSION };
@@ -1,152 +0,0 @@
1
- /**
2
- * Programmatic operation runner — shared core for curated agent verbs.
3
- *
4
- * 与 invoker.ts 的关系:invoker.ts 是 commander 入口 (强耦合 process.exit / 错误
5
- * stderr 输出 / trace 记录);本模块只做"调一次操作 → 返回 envelope",便于 verb
6
- * 实现里组合多次调用 (e.g. digest fan-out) 或在调完后映射 output。
7
- *
8
- * 不做的事:
9
- * - --all 分页 (verb 自己决定要不要)
10
- * - WebSocket streaming (单帧场景不存在)
11
- * - Idempotency-Key 自动注入 (Phase 6 写 verb 显式控制)
12
- * - trace 写入 (调用方自己 trace)
13
- *
14
- * 错误:抛 CallApiError;调用方负责 catch + 决定 exit / 部分失败容错。
15
- */
16
- import AjvPkg from "ajv";
17
- import addFormatsPkg from "ajv-formats";
18
- import { fetch as undiciFetch } from "undici";
19
- import { buildResponseEnvelope } from "./envelope.js";
20
- import { CallApiError, tryParseErrorEnvelope } from "./errors.js";
21
- import { buildHttpHeaders, resolveRequestId } from "./http.js";
22
- const Ajv = (AjvPkg.default ??
23
- AjvPkg);
24
- const addFormats = (addFormatsPkg.default ??
25
- addFormatsPkg);
26
- const ajv = new Ajv({
27
- allErrors: true,
28
- useDefaults: true,
29
- coerceTypes: "array",
30
- strict: false,
31
- });
32
- addFormats(ajv);
33
- /**
34
- * Invoke a single OpenAPI operation programmatically. Returns the standard
35
- * response envelope; throws CallApiError on failure (network / HTTP 4xx-5xx /
36
- * malformed response).
37
- *
38
- * Validation:
39
- * - Pre-flight Ajv validates `args` against `op.inputSchema`. On validation
40
- * failure, throws CallApiError(code="invalid_args").
41
- *
42
- * Path templating: occurrences of `{name}` in `op.path` are substituted from
43
- * `args[name]` and removed from query.
44
- */
45
- export async function callOp(op, args, ctx) {
46
- const params = { ...args };
47
- // Pre-flight schema validation
48
- const validate = ajv.compile(op.inputSchema);
49
- if (!validate(params)) {
50
- const errs = (validate.errors || [])
51
- .map((e) => `${e.instancePath || "(root)"}: ${e.message ?? "invalid"}`)
52
- .join("; ");
53
- throw new CallApiError({
54
- code: "invalid_args",
55
- message: `Input validation failed: ${errs}`,
56
- });
57
- }
58
- // Path templating
59
- let url = ctx.baseUrl.replace(/\/+$/, "") + op.path;
60
- const pathParamRegex = /\{([^}]+)\}/g;
61
- const missingPathParam = [];
62
- url = url.replace(pathParamRegex, (_full, name) => {
63
- const v = params[name];
64
- if (v === undefined) {
65
- missingPathParam.push(name);
66
- return _full;
67
- }
68
- delete params[name];
69
- return encodeURIComponent(String(v));
70
- });
71
- if (missingPathParam.length > 0) {
72
- throw new CallApiError({
73
- code: "invalid_args",
74
- message: `Path parameter(s) required: ${missingPathParam.join(", ")}`,
75
- });
76
- }
77
- // Query / body
78
- const qs = buildQueryString(params, op.method);
79
- if (op.method === "GET" && qs)
80
- url += "?" + qs;
81
- const { headers, requestId: clientRequestId } = buildHttpHeaders({
82
- bearer: ctx.bearer,
83
- cliVersion: ctx.cliVersion,
84
- ...(ctx.channel ? { channel: ctx.channel } : {}),
85
- });
86
- const init = { method: op.method, headers };
87
- if (op.method === "POST") {
88
- headers.set("Content-Type", "application/json");
89
- init.body = JSON.stringify(params);
90
- }
91
- const fetchFn = ctx.fetchImpl ?? undiciFetch;
92
- const startedAt = Date.now();
93
- let res;
94
- try {
95
- res = await fetchFn(url, init);
96
- }
97
- catch (e) {
98
- throw new CallApiError({
99
- code: "network_error",
100
- message: e instanceof Error ? e.message : String(e),
101
- });
102
- }
103
- const requestId = resolveRequestId(res.headers.get("x-request-id"), clientRequestId);
104
- const apiVersion = res.headers.get("x-api-version") || undefined;
105
- const ct = res.headers.get("content-type") || "";
106
- const bodyText = await res.text();
107
- let bodyJson = null;
108
- if (ct.includes("application/json") && bodyText) {
109
- try {
110
- bodyJson = JSON.parse(bodyText);
111
- }
112
- catch {
113
- // non-JSON body — let error path below handle
114
- }
115
- }
116
- const durationMs = Date.now() - startedAt;
117
- if (res.status >= 400) {
118
- const apiErr = tryParseErrorEnvelope(bodyJson, res.status, requestId);
119
- if (apiErr)
120
- throw apiErr;
121
- throw new CallApiError({
122
- code: "http_error",
123
- message: `HTTP ${res.status} ${res.statusText}: ${bodyText.slice(0, 200)}`,
124
- httpStatus: res.status,
125
- requestId,
126
- });
127
- }
128
- return buildResponseEnvelope(bodyJson ?? bodyText, {
129
- requestId,
130
- endpoint: op.path,
131
- method: op.method,
132
- cliVersion: ctx.cliVersion,
133
- durationMs,
134
- ...(apiVersion ? { apiVersion } : {}),
135
- });
136
- }
137
- function buildQueryString(params, method) {
138
- if (method !== "GET")
139
- return "";
140
- const parts = [];
141
- for (const [k, v] of Object.entries(params)) {
142
- if (v === undefined || v === null || v === "")
143
- continue;
144
- if (Array.isArray(v)) {
145
- parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(v.join(","))}`);
146
- }
147
- else {
148
- parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
149
- }
150
- }
151
- return parts.join("&");
152
- }
@@ -1,109 +0,0 @@
1
- /**
2
- * /v1/auth/whoami client + 5-minute in-process cache.
3
- *
4
- * 设计原则:
5
- * - **单飞 (single-flight)**: 并发调用时只发一次真请求;多调用者共享 in-flight
6
- * Promise。MCP serve 长 session 多并发 list-tools 场景下避免请求风暴。
7
- * - **TTL 5 分钟**: cache hit 后跨命令复用 (process 退出即失效)。shell one-shot
8
- * 每次多一次 whoami 调用;MCP server 长进程几乎永远 cache hit。
9
- * - **bearer 切换不复用**: cache key 含 bearer prefix,换 token 自动 miss。
10
- * - **best-effort 失败传播**: 网络 / auth 错误抛 CallApiError 上去由 caller
11
- * 决定 (doctor 显示 fail,whoami exit 2)。
12
- *
13
- * 不在这里做 verb 派生 / unavailable 列表计算——那是 tools/whoami.ts 的职责。
14
- */
15
- import { fetch as undiciFetch } from "undici";
16
- import { CallApiError, tryParseErrorEnvelope } from "./errors.js";
17
- import { buildHttpHeaders, resolveRequestId } from "./http.js";
18
- const DEFAULT_TTL_MS = 5 * 60 * 1000;
19
- let cache = null;
20
- let inflight = null;
21
- function cacheKey(ctx) {
22
- // bearer 前 8 char + baseUrl 足以区分;不存全 bearer 避免日志 / dump 泄露。
23
- return `${ctx.baseUrl}|${ctx.bearer.slice(0, 8)}|${ctx.channel ?? "cli"}`;
24
- }
25
- /** Test-only hook: reset module state between specs. */
26
- export function clearWhoamiCache() {
27
- cache = null;
28
- inflight = null;
29
- }
30
- /** Public read of cache without triggering fetch (returns null if miss/expired). */
31
- export function peekWhoamiCache(ctx) {
32
- const k = cacheKey(ctx);
33
- if (cache && cache.key === k && cache.expiresAt > Date.now())
34
- return cache.resp;
35
- return null;
36
- }
37
- export async function getWhoami(ctx, opts) {
38
- const key = cacheKey(ctx);
39
- const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
40
- if (!opts?.force && cache && cache.key === key && cache.expiresAt > Date.now()) {
41
- return cache.resp;
42
- }
43
- if (inflight && inflight.key === key) {
44
- return inflight.promise;
45
- }
46
- const promise = doFetch(ctx, opts?.fetchImpl)
47
- .then((resp) => {
48
- cache = { key, resp, expiresAt: Date.now() + ttlMs };
49
- return resp;
50
- })
51
- .finally(() => {
52
- if (inflight && inflight.key === key)
53
- inflight = null;
54
- });
55
- inflight = { key, promise };
56
- return promise;
57
- }
58
- async function doFetch(ctx, fetchImpl) {
59
- const fn = fetchImpl ?? undiciFetch;
60
- const url = ctx.baseUrl.replace(/\/+$/, "") + "/v1/auth/whoami";
61
- const { headers, requestId: clientRequestId } = buildHttpHeaders({
62
- bearer: ctx.bearer,
63
- cliVersion: ctx.cliVersion,
64
- ...(ctx.channel ? { channel: ctx.channel } : {}),
65
- });
66
- let res;
67
- try {
68
- res = await fn(url, { method: "GET", headers });
69
- }
70
- catch (e) {
71
- throw new CallApiError({
72
- code: "network_error",
73
- message: e instanceof Error ? e.message : String(e),
74
- });
75
- }
76
- const requestId = resolveRequestId(res.headers.get("x-request-id"), clientRequestId);
77
- const text = await res.text();
78
- let json = null;
79
- if (text) {
80
- try {
81
- json = JSON.parse(text);
82
- }
83
- catch {
84
- // body not JSON — error path below will surface http_error
85
- }
86
- }
87
- if (res.status >= 400) {
88
- const env = tryParseErrorEnvelope(json, res.status, requestId);
89
- if (env)
90
- throw env;
91
- throw new CallApiError({
92
- code: "http_error",
93
- message: `HTTP ${res.status} ${res.statusText}: ${text.slice(0, 200)}`,
94
- httpStatus: res.status,
95
- requestId,
96
- });
97
- }
98
- if (typeof json !== "object" ||
99
- json === null ||
100
- !("data" in json)) {
101
- throw new CallApiError({
102
- code: "internal_error",
103
- message: "whoami: response missing `data` envelope",
104
- httpStatus: res.status,
105
- requestId,
106
- });
107
- }
108
- return json.data;
109
- }
package/dist/tools/api.js DELETED
@@ -1,81 +0,0 @@
1
- /**
2
- * `echopai api call <method> <path>` — DEPRECATED alias of `echopai raw call`.
3
- *
4
- * 保留一个 release 给老脚本,新代码请用 `echopai raw call`。
5
- * 行为同 raw call,但在调用时往 stderr 打 deprecation 提示一行。
6
- */
7
- import { Command } from "commander";
8
- import { fetch as undiciFetch, Headers } from "undici";
9
- import { resolveCredentials, AuthMissingError } from "../runtime/auth.js";
10
- import { CLI_VERSION } from "../version.js";
11
- export function buildApiCommand() {
12
- const api = new Command("api").description("[DEPRECATED] Use `echopai raw call` instead — alias kept for one release");
13
- api
14
- .command("call <method> <path>")
15
- .description("[DEPRECATED] Call <method> <path> — use `echopai raw call` instead")
16
- .option("-d, --data <json>", "POST/PUT/PATCH body (JSON string)")
17
- .option("-q, --query <kv...>", "Query string entries: k=v")
18
- .option("--header <kv...>", "Extra headers: K=V")
19
- .action(async (method, urlPath, opts) => {
20
- process.stderr.write("[deprecated] `echopai api call` will be removed next major. Use `echopai raw call` instead.\n");
21
- const m = method.toUpperCase();
22
- if (!["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"].includes(m)) {
23
- return die("invalid_args", `Unknown HTTP method: ${method}`, 1);
24
- }
25
- let creds;
26
- try {
27
- creds = resolveCredentials({});
28
- }
29
- catch (e) {
30
- if (e instanceof AuthMissingError) {
31
- return die("auth_missing", e.message, 1, e.recovery_hint);
32
- }
33
- throw e;
34
- }
35
- let url = creds.baseUrl + (urlPath.startsWith("/") ? urlPath : "/" + urlPath);
36
- if (opts.query?.length) {
37
- const qs = opts.query
38
- .map((kv) => {
39
- const idx = kv.indexOf("=");
40
- if (idx === -1)
41
- return null;
42
- return `${encodeURIComponent(kv.slice(0, idx))}=${encodeURIComponent(kv.slice(idx + 1))}`;
43
- })
44
- .filter((s) => s !== null)
45
- .join("&");
46
- if (qs)
47
- url += (url.includes("?") ? "&" : "?") + qs;
48
- }
49
- const headers = new Headers({
50
- Authorization: `Bearer ${creds.key}`,
51
- "User-Agent": `echopai-cli/${CLI_VERSION}`,
52
- Accept: "application/json",
53
- });
54
- if (opts.header) {
55
- for (const kv of opts.header) {
56
- const idx = kv.indexOf("=");
57
- if (idx === -1)
58
- continue;
59
- headers.set(kv.slice(0, idx), kv.slice(idx + 1));
60
- }
61
- }
62
- const init = { method: m, headers };
63
- if (opts.data) {
64
- if (!headers.has("content-type"))
65
- headers.set("content-type", "application/json");
66
- init.body = opts.data;
67
- }
68
- const res = await undiciFetch(url, init);
69
- const body = await res.text();
70
- process.stdout.write(body + (body.endsWith("\n") ? "" : "\n"));
71
- process.exit(res.status >= 400 && res.status < 500 ? 1 : res.status >= 500 ? 2 : 0);
72
- });
73
- return api;
74
- }
75
- function die(code, message, exitCode, recovery_hint) {
76
- const env = { error: { code, message } };
77
- if (recovery_hint)
78
- env.error.recovery_hint = recovery_hint;
79
- process.stderr.write(JSON.stringify(env) + "\n");
80
- process.exit(exitCode);
81
- }