echopai 2.8.0 → 2.9.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 (2) hide show
  1. package/dist/bin.js +271 -243
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -2928,97 +2928,43 @@ async function downloadAndVerify(url, expectedSha) {
2928
2928
  }
2929
2929
  }
2930
2930
 
2931
- // src/runtime/invoker.ts
2932
- import AjvPkg from "ajv";
2933
- import addFormatsPkg from "ajv-formats";
2934
-
2935
- // src/runtime/auth.ts
2936
- import * as fs from "node:fs";
2937
- import * as os from "node:os";
2938
- import * as path from "node:path";
2939
- import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
2940
-
2941
- class AuthMissingError extends Error {
2942
- recovery_hint;
2943
- constructor(message, recovery_hint) {
2944
- super(message);
2945
- this.recovery_hint = recovery_hint;
2946
- this.name = "AuthMissingError";
2947
- }
2948
- }
2949
- function configDir() {
2950
- if (process.platform === "win32") {
2951
- const appdata = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
2952
- return path.join(appdata, "echopai");
2953
- }
2954
- const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
2955
- return path.join(xdg, "echopai");
2956
- }
2957
- function configPath() {
2958
- return path.join(configDir(), "config.toml");
2959
- }
2960
- function readConfigFile() {
2961
- const p = configPath();
2962
- if (!fs.existsSync(p))
2963
- return {};
2964
- try {
2965
- const raw = fs.readFileSync(p, "utf-8");
2966
- return parseToml(raw);
2967
- } catch {
2968
- return {};
2969
- }
2970
- }
2971
- function writeConfigFile(cfg) {
2972
- const dir = configDir();
2973
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
2974
- const data = stringifyToml(cfg);
2975
- fs.writeFileSync(configPath(), data, { mode: 384 });
2976
- }
2977
- function resolveCredentials(opts = {}) {
2978
- const DEFAULT_BASE_URL = "https://api.echopai.com";
2979
- if (opts.key) {
2980
- return { key: opts.key, baseUrl: process.env.ECHOPAI_BASE_URL || DEFAULT_BASE_URL, profile: null };
2981
- }
2982
- const envKey = process.env.ECHOPAI_KEY;
2983
- if (envKey) {
2984
- return { key: envKey, baseUrl: process.env.ECHOPAI_BASE_URL || DEFAULT_BASE_URL, profile: null };
2985
- }
2986
- const cfg = readConfigFile();
2987
- const profileName = opts.profile || process.env.ECHOPAI_PROFILE || cfg.default_profile;
2988
- if (profileName && cfg.profiles && cfg.profiles[profileName]) {
2989
- const p = cfg.profiles[profileName];
2990
- if (p.key) {
2991
- return {
2992
- key: p.key,
2993
- baseUrl: p.base_url || process.env.ECHOPAI_BASE_URL || DEFAULT_BASE_URL,
2994
- profile: profileName
2995
- };
2996
- }
2997
- }
2998
- throw new AuthMissingError(profileName ? `No key configured for profile '${profileName}'.` : "No credential found.", "Set ECHOPAI_KEY env var, or run `echopai login --key eps_live_<lookup>_<secret>`.");
2999
- }
3000
-
3001
- // src/runtime/envelope.ts
3002
- function mergeMeta(serverMeta, cli) {
3003
- const merged = {
3004
- ...serverMeta ?? {},
3005
- request_id: cli.requestId,
3006
- endpoint: cli.endpoint,
3007
- method: cli.method,
3008
- cli_version: cli.cliVersion,
3009
- duration_ms: cli.durationMs,
3010
- truncated: false
3011
- };
3012
- if (cli.apiVersion)
3013
- merged.api_version = cli.apiVersion;
3014
- return merged;
2931
+ // src/runtime/tty.ts
2932
+ var enable = (() => {
2933
+ if (!process.stderr.isTTY)
2934
+ return false;
2935
+ if (process.env.CI)
2936
+ return false;
2937
+ if (process.env.NO_COLOR)
2938
+ return false;
2939
+ if (process.env.TERM === "dumb")
2940
+ return false;
2941
+ return true;
2942
+ })();
2943
+ var isTtyHuman = enable;
2944
+ var ESC = "\x1B[";
2945
+ function ansi(code, s) {
2946
+ if (!enable)
2947
+ return s;
2948
+ return `${ESC}${code}m${s}${ESC}0m`;
3015
2949
  }
3016
- function buildResponseEnvelope(body, cli) {
3017
- if (typeof body === "object" && body !== null && !Array.isArray(body) && "data" in body && "meta" in body) {
3018
- const e = body;
3019
- return { data: e.data, meta: mergeMeta(e.meta, cli) };
3020
- }
3021
- return { data: body, meta: mergeMeta(undefined, cli) };
2950
+ var red = (s) => ansi("31", s);
2951
+ var green = (s) => ansi("32", s);
2952
+ var yellow = (s) => ansi("33", s);
2953
+ var cyan = (s) => ansi("36", s);
2954
+ var dim = (s) => ansi("2", s);
2955
+ var bold = (s) => ansi("1", s);
2956
+ function renderError(env) {
2957
+ if (!enable)
2958
+ return JSON.stringify(env);
2959
+ const e = env.error;
2960
+ const label = `${red(bold("✗ " + e.code))}${e.retryable ? " " + dim("(retryable)") : ""}`;
2961
+ const lines = [`${label} ${dim("—")} ${e.message}`];
2962
+ if (e.recovery_hint)
2963
+ lines.push(` ${cyan("hint:")} ${e.recovery_hint}`);
2964
+ if (e.request_id)
2965
+ lines.push(` ${dim("request_id: " + e.request_id)}`);
2966
+ return lines.join(`
2967
+ `);
3022
2968
  }
3023
2969
 
3024
2970
  // src/runtime/errors.ts
@@ -3157,6 +3103,128 @@ function exitCodeForStatus(httpStatus) {
3157
3103
  return 1;
3158
3104
  return 2;
3159
3105
  }
3106
+ var SERVICE_ERROR_CODES = new Set([
3107
+ "network_error",
3108
+ "timeout",
3109
+ "stream_error",
3110
+ "upstream_unavailable",
3111
+ "internal_error",
3112
+ "http_error"
3113
+ ]);
3114
+ function classifyExitCode(code) {
3115
+ return SERVICE_ERROR_CODES.has(code) ? 2 : 1;
3116
+ }
3117
+ function emitErrorEnvelope(args, exitCode) {
3118
+ const retryable = args.retryable ?? isRetryableByCode(args.code);
3119
+ const hint = resolveRecoveryHint(args.code, args.recovery_hint);
3120
+ const env = { error: { code: args.code, message: args.message, retryable } };
3121
+ if (hint)
3122
+ env.error.recovery_hint = hint;
3123
+ if (args.request_id)
3124
+ env.error.request_id = args.request_id;
3125
+ const ec = exitCode ?? classifyExitCode(args.code);
3126
+ if (isTtyHuman) {
3127
+ process.stderr.write(renderError(env) + `
3128
+ `);
3129
+ } else {
3130
+ process.stdout.write(JSON.stringify(env) + `
3131
+ `);
3132
+ }
3133
+ process.exit(ec);
3134
+ }
3135
+
3136
+ // src/runtime/invoker.ts
3137
+ import AjvPkg from "ajv";
3138
+ import addFormatsPkg from "ajv-formats";
3139
+
3140
+ // src/runtime/auth.ts
3141
+ import * as fs from "node:fs";
3142
+ import * as os from "node:os";
3143
+ import * as path from "node:path";
3144
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
3145
+
3146
+ class AuthMissingError extends Error {
3147
+ recovery_hint;
3148
+ constructor(message, recovery_hint) {
3149
+ super(message);
3150
+ this.recovery_hint = recovery_hint;
3151
+ this.name = "AuthMissingError";
3152
+ }
3153
+ }
3154
+ function configDir() {
3155
+ if (process.platform === "win32") {
3156
+ const appdata = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
3157
+ return path.join(appdata, "echopai");
3158
+ }
3159
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
3160
+ return path.join(xdg, "echopai");
3161
+ }
3162
+ function configPath() {
3163
+ return path.join(configDir(), "config.toml");
3164
+ }
3165
+ function readConfigFile() {
3166
+ const p = configPath();
3167
+ if (!fs.existsSync(p))
3168
+ return {};
3169
+ try {
3170
+ const raw = fs.readFileSync(p, "utf-8");
3171
+ return parseToml(raw);
3172
+ } catch {
3173
+ return {};
3174
+ }
3175
+ }
3176
+ function writeConfigFile(cfg) {
3177
+ const dir = configDir();
3178
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
3179
+ const data = stringifyToml(cfg);
3180
+ fs.writeFileSync(configPath(), data, { mode: 384 });
3181
+ }
3182
+ function resolveCredentials(opts = {}) {
3183
+ const DEFAULT_BASE_URL = "https://api.echopai.com";
3184
+ if (opts.key) {
3185
+ return { key: opts.key, baseUrl: process.env.ECHOPAI_BASE_URL || DEFAULT_BASE_URL, profile: null };
3186
+ }
3187
+ const envKey = process.env.ECHOPAI_KEY;
3188
+ if (envKey) {
3189
+ return { key: envKey, baseUrl: process.env.ECHOPAI_BASE_URL || DEFAULT_BASE_URL, profile: null };
3190
+ }
3191
+ const cfg = readConfigFile();
3192
+ const profileName = opts.profile || process.env.ECHOPAI_PROFILE || cfg.default_profile;
3193
+ if (profileName && cfg.profiles && cfg.profiles[profileName]) {
3194
+ const p = cfg.profiles[profileName];
3195
+ if (p.key) {
3196
+ return {
3197
+ key: p.key,
3198
+ baseUrl: p.base_url || process.env.ECHOPAI_BASE_URL || DEFAULT_BASE_URL,
3199
+ profile: profileName
3200
+ };
3201
+ }
3202
+ }
3203
+ throw new AuthMissingError(profileName ? `No key configured for profile '${profileName}'.` : "No credential found.", "Set ECHOPAI_KEY env var, or run `echopai login --key eps_live_<lookup>_<secret>`.");
3204
+ }
3205
+
3206
+ // src/runtime/envelope.ts
3207
+ function mergeMeta(serverMeta, cli) {
3208
+ const merged = {
3209
+ ...serverMeta ?? {},
3210
+ request_id: cli.requestId,
3211
+ endpoint: cli.endpoint,
3212
+ method: cli.method,
3213
+ cli_version: cli.cliVersion,
3214
+ duration_ms: cli.durationMs,
3215
+ truncated: false
3216
+ };
3217
+ if (cli.apiVersion)
3218
+ merged.api_version = cli.apiVersion;
3219
+ return merged;
3220
+ }
3221
+ function buildResponseEnvelope(body, cli) {
3222
+ if (typeof body === "object" && body !== null && !Array.isArray(body) && "data" in body && "meta" in body) {
3223
+ const e = body;
3224
+ return { data: e.data, meta: mergeMeta(e.meta, cli) };
3225
+ }
3226
+ return { data: body, meta: mergeMeta(undefined, cli) };
3227
+ }
3160
3228
 
3161
3229
  // src/runtime/filters.ts
3162
3230
  import jmespathPkg from "jmespath";
@@ -3430,6 +3498,22 @@ function buildHttpHeaders(ctx) {
3430
3498
  function resolveRequestId(serverHeader, clientGenerated) {
3431
3499
  return serverHeader && serverHeader.length > 0 ? serverHeader : clientGenerated;
3432
3500
  }
3501
+ var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
3502
+ var WHOAMI_TIMEOUT_MS = 8000;
3503
+ async function fetchWithTimeout(fn, url, init, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
3504
+ const ac = new AbortController;
3505
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
3506
+ try {
3507
+ return await fn(url, { ...init, signal: ac.signal });
3508
+ } catch (e) {
3509
+ if (ac.signal.aborted) {
3510
+ throw new Error(`request timed out after ${timeoutMs}ms: ${url}`);
3511
+ }
3512
+ throw e;
3513
+ } finally {
3514
+ clearTimeout(timer);
3515
+ }
3516
+ }
3433
3517
 
3434
3518
  // src/runtime/trace.ts
3435
3519
  import {
@@ -3560,7 +3644,10 @@ async function paginate(op, initialParams, ctx, write = writeStdout) {
3560
3644
  process.stderr.write(`> [page ${pages + 1}] ${op.method} ${url}
3561
3645
  `);
3562
3646
  }
3563
- const res = await fetchFn(url, { method: op.method, headers });
3647
+ const res = await fetchWithTimeout(fetchFn, url, {
3648
+ method: op.method,
3649
+ headers
3650
+ });
3564
3651
  const body = await res.text();
3565
3652
  let json = null;
3566
3653
  try {
@@ -3655,52 +3742,13 @@ function exitCodeForPaginateError(e) {
3655
3742
  return 2;
3656
3743
  }
3657
3744
 
3658
- // src/runtime/tty.ts
3659
- var enable = (() => {
3660
- if (!process.stderr.isTTY)
3661
- return false;
3662
- if (process.env.CI)
3663
- return false;
3664
- if (process.env.NO_COLOR)
3665
- return false;
3666
- if (process.env.TERM === "dumb")
3667
- return false;
3668
- return true;
3669
- })();
3670
- var isTtyHuman = enable;
3671
- var ESC = "\x1B[";
3672
- function ansi(code, s) {
3673
- if (!enable)
3674
- return s;
3675
- return `${ESC}${code}m${s}${ESC}0m`;
3676
- }
3677
- var red = (s) => ansi("31", s);
3678
- var green = (s) => ansi("32", s);
3679
- var yellow = (s) => ansi("33", s);
3680
- var cyan = (s) => ansi("36", s);
3681
- var dim = (s) => ansi("2", s);
3682
- var bold = (s) => ansi("1", s);
3683
- function renderError(env) {
3684
- if (!enable)
3685
- return JSON.stringify(env);
3686
- const e = env.error;
3687
- const label = `${red(bold("✗ " + e.code))}${e.retryable ? " " + dim("(retryable)") : ""}`;
3688
- const lines = [`${label} ${dim("—")} ${e.message}`];
3689
- if (e.recovery_hint)
3690
- lines.push(` ${cyan("hint:")} ${e.recovery_hint}`);
3691
- if (e.request_id)
3692
- lines.push(` ${dim("request_id: " + e.request_id)}`);
3693
- return lines.join(`
3694
- `);
3695
- }
3696
-
3697
3745
  // src/runtime/update_check.ts
3698
3746
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "node:fs";
3699
3747
  import os2 from "node:os";
3700
3748
  import path2 from "node:path";
3701
3749
 
3702
3750
  // src/version.ts
3703
- var CLI_VERSION = "2.8.0";
3751
+ var CLI_VERSION = "2.9.0";
3704
3752
 
3705
3753
  // src/runtime/update_check.ts
3706
3754
  var UPDATE_CACHE_PATH = path2.join(os2.homedir(), ".config", "echopai", "update_cache.json");
@@ -3943,7 +3991,7 @@ async function invoke(op, args, ctx) {
3943
3991
  const startedAt = Date.now();
3944
3992
  let res;
3945
3993
  try {
3946
- res = await fetch(url, init);
3994
+ res = await fetchWithTimeout(fetch, url, init);
3947
3995
  } catch (e) {
3948
3996
  trace(op, { exit_code: 2, error_code: "network_error" });
3949
3997
  writeError("network_error", e instanceof Error ? e.message : String(e), 2);
@@ -4060,23 +4108,13 @@ function trace(op, t) {
4060
4108
  });
4061
4109
  }
4062
4110
  function writeError(code, message, exitCode, extras) {
4063
- const retryable = extras?.retryable ?? isRetryableByCode(code);
4064
- const hint = resolveRecoveryHint(code, extras?.recovery_hint);
4065
- const env = {
4066
- error: { code, message, retryable }
4067
- };
4068
- if (hint)
4069
- env.error.recovery_hint = hint;
4070
- if (extras?.request_id)
4071
- env.error.request_id = extras.request_id;
4072
- if (isTtyHuman) {
4073
- process.stderr.write(renderError(env) + `
4074
- `);
4075
- } else {
4076
- process.stderr.write(JSON.stringify(env) + `
4077
- `);
4078
- }
4079
- process.exit(exitCode);
4111
+ emitErrorEnvelope({
4112
+ code,
4113
+ message,
4114
+ ...extras?.retryable !== undefined ? { retryable: extras.retryable } : {},
4115
+ ...extras?.recovery_hint ? { recovery_hint: extras.recovery_hint } : {},
4116
+ ...extras?.request_id ? { request_id: extras.request_id } : {}
4117
+ }, exitCode);
4080
4118
  }
4081
4119
 
4082
4120
  // src/tools/api.ts
@@ -4138,12 +4176,7 @@ function buildApiCommand() {
4138
4176
  return api;
4139
4177
  }
4140
4178
  function die(code, message, exitCode, recovery_hint) {
4141
- const env = { error: { code, message } };
4142
- if (recovery_hint)
4143
- env.error.recovery_hint = recovery_hint;
4144
- process.stderr.write(JSON.stringify(env) + `
4145
- `);
4146
- process.exit(exitCode);
4179
+ emitErrorEnvelope({ code, message, ...recovery_hint ? { recovery_hint } : {} }, exitCode);
4147
4180
  }
4148
4181
 
4149
4182
  // src/tools/mcp.ts
@@ -4172,7 +4205,7 @@ async function getWhoami(ctx, opts) {
4172
4205
  if (inflight && inflight.key === key) {
4173
4206
  return inflight.promise;
4174
4207
  }
4175
- const promise = doFetch(ctx, opts?.fetchImpl).then((resp) => {
4208
+ const promise = doFetch(ctx, opts?.fetchImpl, opts?.requestTimeoutMs).then((resp) => {
4176
4209
  cache = { key, resp, expiresAt: Date.now() + ttlMs };
4177
4210
  return resp;
4178
4211
  }).finally(() => {
@@ -4182,7 +4215,7 @@ async function getWhoami(ctx, opts) {
4182
4215
  inflight = { key, promise };
4183
4216
  return promise;
4184
4217
  }
4185
- async function doFetch(ctx, fetchImpl) {
4218
+ async function doFetch(ctx, fetchImpl, requestTimeoutMs = WHOAMI_TIMEOUT_MS) {
4186
4219
  const fn = fetchImpl ?? fetch;
4187
4220
  const url = ctx.baseUrl.replace(/\/+$/, "") + "/v1/auth/whoami";
4188
4221
  const { headers, requestId: clientRequestId } = buildHttpHeaders({
@@ -4192,7 +4225,7 @@ async function doFetch(ctx, fetchImpl) {
4192
4225
  });
4193
4226
  let res;
4194
4227
  try {
4195
- res = await fn(url, { method: "GET", headers });
4228
+ res = await fetchWithTimeout(fn, url, { method: "GET", headers }, requestTimeoutMs);
4196
4229
  } catch (e) {
4197
4230
  throw new CallApiError({
4198
4231
  code: "network_error",
@@ -4256,29 +4289,18 @@ async function executeVerb(handler) {
4256
4289
  process.exit(0);
4257
4290
  } catch (e) {
4258
4291
  if (e instanceof CallApiError) {
4259
- emitVerbError(e.code, e.message, e.recovery_hint, e.httpStatus && e.httpStatus < 500 ? 1 : 2, e.requestId);
4292
+ emitVerbError(e.code, e.message, e.recovery_hint, e.httpStatus ? e.httpStatus < 500 ? 1 : 2 : classifyExitCode(e.code), e.requestId);
4260
4293
  }
4261
4294
  emitVerbError("internal_error", e instanceof Error ? e.message : String(e), undefined, 2);
4262
4295
  }
4263
4296
  }
4264
4297
  function emitVerbError(code, message, recoveryHint, exitCode, requestId) {
4265
- const env = {
4266
- error: {
4267
- code,
4268
- message,
4269
- retryable: false,
4270
- ...recoveryHint ? { recovery_hint: recoveryHint } : {},
4271
- ...requestId ? { request_id: requestId } : {}
4272
- }
4273
- };
4274
- if (isTtyHuman) {
4275
- process.stderr.write(renderError(env) + `
4276
- `);
4277
- } else {
4278
- process.stderr.write(JSON.stringify(env) + `
4279
- `);
4280
- }
4281
- process.exit(exitCode);
4298
+ emitErrorEnvelope({
4299
+ code,
4300
+ message,
4301
+ ...recoveryHint ? { recovery_hint: recoveryHint } : {},
4302
+ ...requestId ? { request_id: requestId } : {}
4303
+ }, exitCode);
4282
4304
  }
4283
4305
 
4284
4306
  // src/runtime/verb_runner.ts
@@ -4338,7 +4360,7 @@ async function callOp(op, args, ctx) {
4338
4360
  const startedAt = Date.now();
4339
4361
  let res;
4340
4362
  try {
4341
- res = await fetchFn(url, init);
4363
+ res = await fetchWithTimeout(fetchFn, url, init);
4342
4364
  } catch (e) {
4343
4365
  throw new CallApiError({
4344
4366
  code: "network_error",
@@ -6606,24 +6628,51 @@ function buildMcpCommand() {
6606
6628
  }
6607
6629
  throw e;
6608
6630
  }
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)}
6631
+ const WHOAMI_MAX_ATTEMPTS = 3;
6632
+ const WHOAMI_BACKOFF_MS = [500, 1500];
6633
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6634
+ let whoami = null;
6635
+ let lastErr = null;
6636
+ for (let attempt = 1;attempt <= WHOAMI_MAX_ATTEMPTS; attempt++) {
6637
+ try {
6638
+ whoami = await getWhoami({
6639
+ baseUrl: creds.baseUrl,
6640
+ bearer: creds.key,
6641
+ cliVersion: CLI_VERSION,
6642
+ channel: "mcp"
6643
+ });
6644
+ break;
6645
+ } catch (e) {
6646
+ lastErr = e;
6647
+ const status = e instanceof CallApiError ? e.httpStatus : undefined;
6648
+ const code = e instanceof CallApiError ? e.code : undefined;
6649
+ const isAuthError = status === 401 || status === 403 || code === "auth_missing" || code === "forbidden";
6650
+ if (isAuthError) {
6651
+ process.stderr.write(`[mcp] whoami auth failed${status ? ` (HTTP ${status})` : ""}: ` + `${e instanceof Error ? e.message : String(e)}
6652
+ ` + "[mcp] hint: key 失效或无权限,请 `echopai login` 或检查 ECHOPAI_KEY。\n");
6653
+ process.exit(1);
6654
+ }
6655
+ if (attempt < WHOAMI_MAX_ATTEMPTS) {
6656
+ const backoffMs = WHOAMI_BACKOFF_MS[attempt - 1] ?? 1500;
6657
+ process.stderr.write(`[mcp] whoami attempt ${attempt}/${WHOAMI_MAX_ATTEMPTS} failed ` + `(${e instanceof Error ? e.message : String(e)}); ` + `retrying in ${backoffMs}ms
6619
6658
  `);
6620
- process.exit(1);
6659
+ await sleep(backoffMs);
6660
+ }
6661
+ }
6621
6662
  }
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(",")}]
6663
+ let availableVerbs;
6664
+ if (whoami) {
6665
+ const tokenScopes = new Set(whoami.scopes);
6666
+ availableVerbs = filterAvailableVerbs(ALL_VERB_SPECS, tokenScopes);
6667
+ process.stderr.write(`[mcp] kind=${whoami.kind} scopes=[${whoami.scopes.join(",")}]
6625
6668
  ` + `[mcp] exposing ${availableVerbs.length}/${ALL_VERB_SPECS.length} curated verbs as MCP tools
6626
6669
  `);
6670
+ } else {
6671
+ availableVerbs = [...ALL_VERB_SPECS];
6672
+ process.stderr.write(`[mcp] whoami failed after ${WHOAMI_MAX_ATTEMPTS} attempts: ` + `${lastErr instanceof Error ? lastErr.message : String(lastErr)}
6673
+ ` + `[mcp] degraded start: exposing all ${availableVerbs.length} curated verbs; ` + `scopes enforced per-call by the server
6674
+ `);
6675
+ }
6627
6676
  const server = new McpServer({
6628
6677
  name: "echopai",
6629
6678
  version: CLI_VERSION,
@@ -6762,14 +6811,7 @@ function buildRawCallCommand() {
6762
6811
  return call;
6763
6812
  }
6764
6813
  function die2(code, message, exitCode, recovery_hint) {
6765
- const env = {
6766
- error: { code, message, retryable: false }
6767
- };
6768
- if (recovery_hint)
6769
- env.error.recovery_hint = recovery_hint;
6770
- process.stderr.write(JSON.stringify(env) + `
6771
- `);
6772
- process.exit(exitCode);
6814
+ emitErrorEnvelope({ code, message, ...recovery_hint ? { recovery_hint } : {} }, exitCode);
6773
6815
  }
6774
6816
 
6775
6817
  // src/tools/completion.ts
@@ -6960,12 +7002,7 @@ function redactKey(key) {
6960
7002
  return `eps_live_${parts[2]}_***${last.slice(-4)}`;
6961
7003
  }
6962
7004
  function writeError2(code, message, recovery_hint, exitCode) {
6963
- const env = { error: { code, message } };
6964
- if (recovery_hint)
6965
- env.error.recovery_hint = recovery_hint;
6966
- process.stderr.write(JSON.stringify(env) + `
6967
- `);
6968
- process.exit(exitCode);
7005
+ emitErrorEnvelope({ code, message, ...recovery_hint ? { recovery_hint } : {} }, exitCode);
6969
7006
  }
6970
7007
 
6971
7008
  // src/tools/login.ts
@@ -7145,23 +7182,12 @@ function buildWhoamiCommand() {
7145
7182
  return cmd;
7146
7183
  }
7147
7184
  function emitError(code, message, recoveryHint, exitCode, requestId) {
7148
- const env = {
7149
- error: {
7150
- code,
7151
- message,
7152
- retryable: false,
7153
- ...recoveryHint ? { recovery_hint: recoveryHint } : {},
7154
- ...requestId ? { request_id: requestId } : {}
7155
- }
7156
- };
7157
- if (isTtyHuman) {
7158
- process.stderr.write(renderError(env) + `
7159
- `);
7160
- } else {
7161
- process.stderr.write(JSON.stringify(env) + `
7162
- `);
7163
- }
7164
- process.exit(exitCode);
7185
+ emitErrorEnvelope({
7186
+ code,
7187
+ message,
7188
+ ...recoveryHint ? { recovery_hint: recoveryHint } : {},
7189
+ ...requestId ? { request_id: requestId } : {}
7190
+ }, exitCode);
7165
7191
  }
7166
7192
 
7167
7193
  // src/tools/doctor.ts
@@ -8118,10 +8144,10 @@ var program = new Command31;
8118
8144
  program.name("echopai").description(`Command-line access to the EchoPai market-data platform: quotes, news,
8119
8145
  ` + `analyst research, fundamentals, and sentiment.
8120
8146
 
8121
- ` + `Machine-readable by design — a JSON envelope on stdout, JSON errors on
8122
- ` + `stderr, and three-state exit codes (0 success / 1 user error / 2 service
8123
- ` + `error) built for AI agents and scripts. Authenticate with the
8124
- ` + "ECHOPAI_KEY environment variable or `echopai login`.").version(CLI_VERSION, "-V, --version").addOption(new Option23("--debug", "Print HTTP wire trace to stderr (Bearer redacted)")).addOption(new Option23("--raw", "Pass through raw response body (skip envelope wrap)")).addOption(new Option23("--jq <jmespath>", "Filter or transform output with a JMESPath expression.")).addOption(new Option23("--fields <a,b,c>", "Keep only listed top-level fields per item")).addOption(new Option23("--max-bytes <n>", "Truncate serialized envelope to N bytes (sets meta.truncated)")).addOption(new Option23("--yes", "Confirm a write op in non-TTY mode (required for sideEffect=write in agents / CI)")).addOption(new Option23("--dry-run", "Send X-Dry-Run:1 on write ops where the server advertises dry-run support")).addHelpText("after", `
8147
+ ` + `Machine-readable by design — in non-TTY (agent/script) mode both success
8148
+ ` + "`{data:...}` and error `{error:{code,retryable,...}}` envelopes go to stdout\n" + `(one stream to parse), with three-state exit codes (0 success / 1 user error
8149
+ ` + `/ 2 service error). On a TTY, errors render to stderr for humans. Built for AI
8150
+ ` + "agents and scripts. Authenticate with ECHOPAI_KEY or `echopai login`.").version(CLI_VERSION, "-V, --version").addOption(new Option23("--debug", "Print HTTP wire trace to stderr (Bearer redacted)")).addOption(new Option23("--raw", "Pass through raw response body (skip envelope wrap)")).addOption(new Option23("--jq <jmespath>", "Filter or transform output with a JMESPath expression.")).addOption(new Option23("--fields <a,b,c>", "Keep only listed top-level fields per item")).addOption(new Option23("--max-bytes <n>", "Truncate serialized envelope to N bytes (sets meta.truncated)")).addOption(new Option23("--yes", "Confirm a write op in non-TTY mode (required for sideEffect=write in agents / CI)")).addOption(new Option23("--dry-run", "Send X-Dry-Run:1 on write ops where the server advertises dry-run support")).addHelpText("after", `
8125
8151
  Authentication: ECHOPAI_KEY=<eps_live_...> echopai <command>
8126
8152
  ` + `Curated verbs: echopai <verb> # task-level commands (recommended)
8127
8153
  ` + `Raw operations: echopai raw <noun> <verb> # low-level access to every API operation
@@ -8145,21 +8171,30 @@ var dispatch = async (op, args) => {
8145
8171
  }
8146
8172
  await invoke(op, { ...globalsSnake, ...args }, { cliVersion: CLI_VERSION });
8147
8173
  };
8174
+ function detectStrayCodeArgs(args) {
8175
+ if (!Array.isArray(args))
8176
+ return [];
8177
+ const codeLike = /^((SSE|SZSE|BSE|SH|SZ|BJ):)?[0-9]{6}$/i;
8178
+ return args.filter((a) => typeof a === "string" && codeLike.test(a));
8179
+ }
8148
8180
  function applyAiFirstHooks(cmd) {
8149
8181
  cmd.configureOutput({ writeErr: () => {} });
8150
8182
  cmd.exitOverride((err) => {
8151
8183
  if (err.code === "commander.helpDisplayed" || err.code === "commander.help" || err.code === "commander.version") {
8152
8184
  process.exit(0);
8153
8185
  }
8154
- process.stderr.write(JSON.stringify({
8155
- error: {
8156
- code: "invalid_args",
8157
- message: err.message,
8158
- recovery_hint: "Run with `--help` for usage."
8159
- }
8160
- }) + `
8161
- `);
8162
- process.exit(1);
8186
+ let message = err.message;
8187
+ const stray = detectStrayCodeArgs(cmd.args);
8188
+ if (stray.length > 0) {
8189
+ const opt = /code\b/i.test(err.message) ? "--code" : "--codes";
8190
+ message += `. Detected positional argument(s) [${stray.join(", ")}] — these commands ` + `take codes via an option, not positionally; did you mean ` + `\`${opt} ${stray.join(",")}\`?`;
8191
+ }
8192
+ emitErrorEnvelope({
8193
+ code: "invalid_args",
8194
+ message,
8195
+ retryable: false,
8196
+ recovery_hint: "Run with `--help` for usage."
8197
+ }, 1);
8163
8198
  });
8164
8199
  for (const sub of cmd.commands)
8165
8200
  applyAiFirstHooks(sub);
@@ -8231,12 +8266,5 @@ if (shouldShowWelcome(process.argv)) {
8231
8266
  process.exit(0);
8232
8267
  }
8233
8268
  program.parseAsync(process.argv).catch((e) => {
8234
- process.stderr.write(JSON.stringify({
8235
- error: {
8236
- code: "internal_error",
8237
- message: e instanceof Error ? e.message : String(e)
8238
- }
8239
- }) + `
8240
- `);
8241
- process.exit(2);
8269
+ emitErrorEnvelope({ code: "internal_error", message: e instanceof Error ? e.message : String(e) }, 2);
8242
8270
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echopai",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
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",