echopai 2.8.0 → 2.8.1

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 (2) hide show
  1. package/dist/bin.js +67 -21
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -3430,6 +3430,22 @@ function buildHttpHeaders(ctx) {
3430
3430
  function resolveRequestId(serverHeader, clientGenerated) {
3431
3431
  return serverHeader && serverHeader.length > 0 ? serverHeader : clientGenerated;
3432
3432
  }
3433
+ var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
3434
+ var WHOAMI_TIMEOUT_MS = 8000;
3435
+ async function fetchWithTimeout(fn, url, init, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
3436
+ const ac = new AbortController;
3437
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
3438
+ try {
3439
+ return await fn(url, { ...init, signal: ac.signal });
3440
+ } catch (e) {
3441
+ if (ac.signal.aborted) {
3442
+ throw new Error(`request timed out after ${timeoutMs}ms: ${url}`);
3443
+ }
3444
+ throw e;
3445
+ } finally {
3446
+ clearTimeout(timer);
3447
+ }
3448
+ }
3433
3449
 
3434
3450
  // src/runtime/trace.ts
3435
3451
  import {
@@ -3560,7 +3576,10 @@ async function paginate(op, initialParams, ctx, write = writeStdout) {
3560
3576
  process.stderr.write(`> [page ${pages + 1}] ${op.method} ${url}
3561
3577
  `);
3562
3578
  }
3563
- const res = await fetchFn(url, { method: op.method, headers });
3579
+ const res = await fetchWithTimeout(fetchFn, url, {
3580
+ method: op.method,
3581
+ headers
3582
+ });
3564
3583
  const body = await res.text();
3565
3584
  let json = null;
3566
3585
  try {
@@ -3700,7 +3719,7 @@ import os2 from "node:os";
3700
3719
  import path2 from "node:path";
3701
3720
 
3702
3721
  // src/version.ts
3703
- var CLI_VERSION = "2.8.0";
3722
+ var CLI_VERSION = "2.8.1";
3704
3723
 
3705
3724
  // src/runtime/update_check.ts
3706
3725
  var UPDATE_CACHE_PATH = path2.join(os2.homedir(), ".config", "echopai", "update_cache.json");
@@ -3943,7 +3962,7 @@ async function invoke(op, args, ctx) {
3943
3962
  const startedAt = Date.now();
3944
3963
  let res;
3945
3964
  try {
3946
- res = await fetch(url, init);
3965
+ res = await fetchWithTimeout(fetch, url, init);
3947
3966
  } catch (e) {
3948
3967
  trace(op, { exit_code: 2, error_code: "network_error" });
3949
3968
  writeError("network_error", e instanceof Error ? e.message : String(e), 2);
@@ -4172,7 +4191,7 @@ async function getWhoami(ctx, opts) {
4172
4191
  if (inflight && inflight.key === key) {
4173
4192
  return inflight.promise;
4174
4193
  }
4175
- const promise = doFetch(ctx, opts?.fetchImpl).then((resp) => {
4194
+ const promise = doFetch(ctx, opts?.fetchImpl, opts?.requestTimeoutMs).then((resp) => {
4176
4195
  cache = { key, resp, expiresAt: Date.now() + ttlMs };
4177
4196
  return resp;
4178
4197
  }).finally(() => {
@@ -4182,7 +4201,7 @@ async function getWhoami(ctx, opts) {
4182
4201
  inflight = { key, promise };
4183
4202
  return promise;
4184
4203
  }
4185
- async function doFetch(ctx, fetchImpl) {
4204
+ async function doFetch(ctx, fetchImpl, requestTimeoutMs = WHOAMI_TIMEOUT_MS) {
4186
4205
  const fn = fetchImpl ?? fetch;
4187
4206
  const url = ctx.baseUrl.replace(/\/+$/, "") + "/v1/auth/whoami";
4188
4207
  const { headers, requestId: clientRequestId } = buildHttpHeaders({
@@ -4192,7 +4211,7 @@ async function doFetch(ctx, fetchImpl) {
4192
4211
  });
4193
4212
  let res;
4194
4213
  try {
4195
- res = await fn(url, { method: "GET", headers });
4214
+ res = await fetchWithTimeout(fn, url, { method: "GET", headers }, requestTimeoutMs);
4196
4215
  } catch (e) {
4197
4216
  throw new CallApiError({
4198
4217
  code: "network_error",
@@ -4338,7 +4357,7 @@ async function callOp(op, args, ctx) {
4338
4357
  const startedAt = Date.now();
4339
4358
  let res;
4340
4359
  try {
4341
- res = await fetchFn(url, init);
4360
+ res = await fetchWithTimeout(fetchFn, url, init);
4342
4361
  } catch (e) {
4343
4362
  throw new CallApiError({
4344
4363
  code: "network_error",
@@ -6606,24 +6625,51 @@ function buildMcpCommand() {
6606
6625
  }
6607
6626
  throw e;
6608
6627
  }
6609
- let whoami;
6610
- try {
6611
- whoami = await getWhoami({
6612
- baseUrl: creds.baseUrl,
6613
- bearer: creds.key,
6614
- cliVersion: CLI_VERSION,
6615
- channel: "mcp"
6616
- });
6617
- } catch (e) {
6618
- process.stderr.write(`[mcp] whoami failed: ${e instanceof Error ? e.message : String(e)}
6628
+ const WHOAMI_MAX_ATTEMPTS = 3;
6629
+ const WHOAMI_BACKOFF_MS = [500, 1500];
6630
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6631
+ let whoami = null;
6632
+ let lastErr = null;
6633
+ for (let attempt = 1;attempt <= WHOAMI_MAX_ATTEMPTS; attempt++) {
6634
+ try {
6635
+ whoami = await getWhoami({
6636
+ baseUrl: creds.baseUrl,
6637
+ bearer: creds.key,
6638
+ cliVersion: CLI_VERSION,
6639
+ channel: "mcp"
6640
+ });
6641
+ break;
6642
+ } catch (e) {
6643
+ lastErr = e;
6644
+ const status = e instanceof CallApiError ? e.httpStatus : undefined;
6645
+ const code = e instanceof CallApiError ? e.code : undefined;
6646
+ const isAuthError = status === 401 || status === 403 || code === "auth_missing" || code === "forbidden";
6647
+ if (isAuthError) {
6648
+ process.stderr.write(`[mcp] whoami auth failed${status ? ` (HTTP ${status})` : ""}: ` + `${e instanceof Error ? e.message : String(e)}
6649
+ ` + "[mcp] hint: key 失效或无权限,请 `echopai login` 或检查 ECHOPAI_KEY。\n");
6650
+ process.exit(1);
6651
+ }
6652
+ if (attempt < WHOAMI_MAX_ATTEMPTS) {
6653
+ const backoffMs = WHOAMI_BACKOFF_MS[attempt - 1] ?? 1500;
6654
+ process.stderr.write(`[mcp] whoami attempt ${attempt}/${WHOAMI_MAX_ATTEMPTS} failed ` + `(${e instanceof Error ? e.message : String(e)}); ` + `retrying in ${backoffMs}ms
6619
6655
  `);
6620
- process.exit(1);
6656
+ await sleep(backoffMs);
6657
+ }
6658
+ }
6621
6659
  }
6622
- const tokenScopes = new Set(whoami.scopes);
6623
- const availableVerbs = filterAvailableVerbs(ALL_VERB_SPECS, tokenScopes);
6624
- process.stderr.write(`[mcp] kind=${whoami.kind} scopes=[${whoami.scopes.join(",")}]
6660
+ let availableVerbs;
6661
+ if (whoami) {
6662
+ const tokenScopes = new Set(whoami.scopes);
6663
+ availableVerbs = filterAvailableVerbs(ALL_VERB_SPECS, tokenScopes);
6664
+ process.stderr.write(`[mcp] kind=${whoami.kind} scopes=[${whoami.scopes.join(",")}]
6625
6665
  ` + `[mcp] exposing ${availableVerbs.length}/${ALL_VERB_SPECS.length} curated verbs as MCP tools
6626
6666
  `);
6667
+ } else {
6668
+ availableVerbs = [...ALL_VERB_SPECS];
6669
+ process.stderr.write(`[mcp] whoami failed after ${WHOAMI_MAX_ATTEMPTS} attempts: ` + `${lastErr instanceof Error ? lastErr.message : String(lastErr)}
6670
+ ` + `[mcp] degraded start: exposing all ${availableVerbs.length} curated verbs; ` + `scopes enforced per-call by the server
6671
+ `);
6672
+ }
6627
6673
  const server = new McpServer({
6628
6674
  name: "echopai",
6629
6675
  version: CLI_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echopai",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "Command-line interface for the EchoPai Open Platform: stock-market data, news, analyst views, sentiment, signals, backtests.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://echopai.com",