echopai 2.3.0 → 2.4.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 +8298 -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/runtime/auth.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credential / profile resolution.
|
|
3
|
-
*
|
|
4
|
-
* Precedence(高 → 低):
|
|
5
|
-
* 1. --key <eps_live_X_Y> 命令行
|
|
6
|
-
* 2. ECHOPAI_KEY env
|
|
7
|
-
* 3. ~/.config/echopai/config.toml 默认 profile
|
|
8
|
-
* 4. ~/.config/echopai/config.toml 指定 profile(--profile <name> 或 ECHOPAI_PROFILE)
|
|
9
|
-
* 5. 报错 auth_missing
|
|
10
|
-
*
|
|
11
|
-
* 配置文件格式(TOML):
|
|
12
|
-
*
|
|
13
|
-
* default_profile = "prod"
|
|
14
|
-
*
|
|
15
|
-
* [profiles.prod]
|
|
16
|
-
* key = "eps_live_xxx_yyy"
|
|
17
|
-
* base_url = "https://api.echopai.com"
|
|
18
|
-
*
|
|
19
|
-
* [profiles.staging]
|
|
20
|
-
* key = "eps_live_aaa_bbb"
|
|
21
|
-
* base_url = "https://staging.echopai.com"
|
|
22
|
-
*
|
|
23
|
-
* XDG paths:
|
|
24
|
-
* Linux/macOS: $XDG_CONFIG_HOME/echopai/config.toml or ~/.config/echopai/config.toml
|
|
25
|
-
* Windows: %APPDATA%/echopai/config.toml
|
|
26
|
-
*/
|
|
27
|
-
import * as fs from "node:fs";
|
|
28
|
-
import * as os from "node:os";
|
|
29
|
-
import * as path from "node:path";
|
|
30
|
-
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
31
|
-
export class AuthMissingError extends Error {
|
|
32
|
-
recovery_hint;
|
|
33
|
-
constructor(message, recovery_hint) {
|
|
34
|
-
super(message);
|
|
35
|
-
this.recovery_hint = recovery_hint;
|
|
36
|
-
this.name = "AuthMissingError";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
export function configDir() {
|
|
40
|
-
if (process.platform === "win32") {
|
|
41
|
-
const appdata = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
42
|
-
return path.join(appdata, "echopai");
|
|
43
|
-
}
|
|
44
|
-
const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
45
|
-
return path.join(xdg, "echopai");
|
|
46
|
-
}
|
|
47
|
-
export function configPath() {
|
|
48
|
-
return path.join(configDir(), "config.toml");
|
|
49
|
-
}
|
|
50
|
-
export function readConfigFile() {
|
|
51
|
-
const p = configPath();
|
|
52
|
-
if (!fs.existsSync(p))
|
|
53
|
-
return {};
|
|
54
|
-
try {
|
|
55
|
-
const raw = fs.readFileSync(p, "utf-8");
|
|
56
|
-
return parseToml(raw);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return {};
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
export function writeConfigFile(cfg) {
|
|
63
|
-
const dir = configDir();
|
|
64
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
65
|
-
const data = stringifyToml(cfg);
|
|
66
|
-
fs.writeFileSync(configPath(), data, { mode: 0o600 });
|
|
67
|
-
}
|
|
68
|
-
export function resolveCredentials(opts = {}) {
|
|
69
|
-
const DEFAULT_BASE_URL = "https://api.echopai.com";
|
|
70
|
-
// 1. --key arg
|
|
71
|
-
if (opts.key) {
|
|
72
|
-
return { key: opts.key, baseUrl: process.env.ECHOPAI_BASE_URL || DEFAULT_BASE_URL, profile: null };
|
|
73
|
-
}
|
|
74
|
-
// 2. env var
|
|
75
|
-
const envKey = process.env.ECHOPAI_KEY;
|
|
76
|
-
if (envKey) {
|
|
77
|
-
return { key: envKey, baseUrl: process.env.ECHOPAI_BASE_URL || DEFAULT_BASE_URL, profile: null };
|
|
78
|
-
}
|
|
79
|
-
// 3+4. config file
|
|
80
|
-
const cfg = readConfigFile();
|
|
81
|
-
const profileName = opts.profile || process.env.ECHOPAI_PROFILE || cfg.default_profile;
|
|
82
|
-
if (profileName && cfg.profiles && cfg.profiles[profileName]) {
|
|
83
|
-
const p = cfg.profiles[profileName];
|
|
84
|
-
if (p.key) {
|
|
85
|
-
return {
|
|
86
|
-
key: p.key,
|
|
87
|
-
baseUrl: p.base_url || process.env.ECHOPAI_BASE_URL || DEFAULT_BASE_URL,
|
|
88
|
-
profile: profileName,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
throw new AuthMissingError(profileName
|
|
93
|
-
? `No key configured for profile '${profileName}'.`
|
|
94
|
-
: "No credential found.", "Set ECHOPAI_KEY env var, or run `echopai login --key eps_live_<lookup>_<secret>`.");
|
|
95
|
-
}
|
package/dist/runtime/envelope.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Standard CLI success envelope.
|
|
3
|
-
*
|
|
4
|
-
* 服务端响应有时含 `{data, meta}`(pagination cursors / has_more / total 等),
|
|
5
|
-
* 有时只返 data。本模块统一对外输出:
|
|
6
|
-
*
|
|
7
|
-
* { data, meta: { ...serverMeta, request_id, endpoint, method, cli_version,
|
|
8
|
-
* api_version?, duration_ms, truncated } }
|
|
9
|
-
*
|
|
10
|
-
* CLI 字段永远覆盖 server 同名字段——CLI 是观察者,知道真相(例如 server 没 echo
|
|
11
|
-
* X-Request-Id 时,CLI 端的 uuid 才是 canonical)。
|
|
12
|
-
*
|
|
13
|
-
* truncated 默认 false;PR 1.4 引入 --max-bytes / --fields 等截断逻辑后,
|
|
14
|
-
* 触发时由 invoker / paginator 写为 true 并附 truncation_reason。
|
|
15
|
-
*
|
|
16
|
-
* 与 http.ts 关系:http.ts 出站(request headers),envelope.ts 入站(response meta)。
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Merge server-provided meta with CLI-known truth. CLI fields win on collision.
|
|
20
|
-
*/
|
|
21
|
-
export function mergeMeta(serverMeta, cli) {
|
|
22
|
-
const merged = {
|
|
23
|
-
...(serverMeta ?? {}),
|
|
24
|
-
request_id: cli.requestId,
|
|
25
|
-
endpoint: cli.endpoint,
|
|
26
|
-
method: cli.method,
|
|
27
|
-
cli_version: cli.cliVersion,
|
|
28
|
-
duration_ms: cli.durationMs,
|
|
29
|
-
truncated: false,
|
|
30
|
-
};
|
|
31
|
-
if (cli.apiVersion)
|
|
32
|
-
merged.api_version = cli.apiVersion;
|
|
33
|
-
return merged;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Build a complete response envelope from a raw response body.
|
|
37
|
-
*
|
|
38
|
-
* - 若 body 是 `{data, meta}` 形态:data 透传,meta 合并 CLI 字段。
|
|
39
|
-
* - 否则:整 body 当 data,meta 全部由 CLI 提供。
|
|
40
|
-
* - body 为 null/字符串/数组时也走"整 body 当 data"分支。
|
|
41
|
-
*/
|
|
42
|
-
export function buildResponseEnvelope(body, cli) {
|
|
43
|
-
if (typeof body === "object" &&
|
|
44
|
-
body !== null &&
|
|
45
|
-
!Array.isArray(body) &&
|
|
46
|
-
"data" in body &&
|
|
47
|
-
"meta" in body) {
|
|
48
|
-
const e = body;
|
|
49
|
-
return { data: e.data, meta: mergeMeta(e.meta, cli) };
|
|
50
|
-
}
|
|
51
|
-
return { data: body, meta: mergeMeta(undefined, cli) };
|
|
52
|
-
}
|
package/dist/runtime/errors.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 错误层:解析 API envelope,分级 exit code,输出 stderr JSON。
|
|
3
|
-
*
|
|
4
|
-
* Canonical envelope (server + CLI uniform):
|
|
5
|
-
* { "error": {
|
|
6
|
-
* "code": "<machine_code>",
|
|
7
|
-
* "message": "<human>",
|
|
8
|
-
* "retryable": <bool>,
|
|
9
|
-
* "recovery_hint": "<actionable>",
|
|
10
|
-
* "request_id": "<uuid>"
|
|
11
|
-
* } }
|
|
12
|
-
*
|
|
13
|
-
* `code` must be a member of KNOWN_ERROR_CODES (see docs/api-contract/error-codes.md).
|
|
14
|
-
* Server may emit codes outside this set; CLI accepts them but logs a warning under
|
|
15
|
-
* --debug. `recovery_hint` falls back to DEFAULT_RECOVERY_HINTS when server omits it.
|
|
16
|
-
*
|
|
17
|
-
* Exit code 三态:
|
|
18
|
-
* 0 success
|
|
19
|
-
* 1 user error (4xx — auth_missing / scope_insufficient / kind_not_allowed /
|
|
20
|
-
* invalid_args / validation_failed / not_found / confirmation_required ...)
|
|
21
|
-
* 2 service error (5xx, 429 retryable, network failure, internal_error, stream_error)
|
|
22
|
-
*/
|
|
23
|
-
export const KNOWN_ERROR_CODES = [
|
|
24
|
-
// auth / identity
|
|
25
|
-
"auth_missing",
|
|
26
|
-
"auth_invalid",
|
|
27
|
-
"auth_expired",
|
|
28
|
-
// permission / scope
|
|
29
|
-
"scope_insufficient",
|
|
30
|
-
"kind_not_allowed",
|
|
31
|
-
"channel_not_allowed",
|
|
32
|
-
// input / validation
|
|
33
|
-
"invalid_args",
|
|
34
|
-
"validation_failed",
|
|
35
|
-
"not_found",
|
|
36
|
-
"idempotency_conflict",
|
|
37
|
-
// write / confirmation
|
|
38
|
-
"confirmation_required",
|
|
39
|
-
"dry_run_unsupported",
|
|
40
|
-
"unsafe_command",
|
|
41
|
-
// rate / quota
|
|
42
|
-
"rate_limited",
|
|
43
|
-
"quota_exhausted",
|
|
44
|
-
"agent_budget_exhausted",
|
|
45
|
-
// transport / infrastructure
|
|
46
|
-
"network_error",
|
|
47
|
-
"timeout",
|
|
48
|
-
"stream_error",
|
|
49
|
-
"upstream_unavailable",
|
|
50
|
-
// server / internal
|
|
51
|
-
"internal_error",
|
|
52
|
-
"http_error",
|
|
53
|
-
];
|
|
54
|
-
const KNOWN_CODE_SET = new Set(KNOWN_ERROR_CODES);
|
|
55
|
-
export function isKnownErrorCode(code) {
|
|
56
|
-
return KNOWN_CODE_SET.has(code);
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Codes that are safe to retry with the same input. Server may also set
|
|
60
|
-
* `retryable` per-response; if it does, server wins. This map is the
|
|
61
|
-
* fallback when server omits `retryable`.
|
|
62
|
-
*/
|
|
63
|
-
const RETRYABLE_CODES = new Set([
|
|
64
|
-
"rate_limited",
|
|
65
|
-
"network_error",
|
|
66
|
-
"timeout",
|
|
67
|
-
"stream_error",
|
|
68
|
-
"upstream_unavailable",
|
|
69
|
-
"internal_error",
|
|
70
|
-
]);
|
|
71
|
-
export function isRetryableByCode(code) {
|
|
72
|
-
return RETRYABLE_CODES.has(code);
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Default recovery hints when server omits `recovery_hint`. Keep in sync with
|
|
76
|
-
* docs/api-contract/error-codes.md.
|
|
77
|
-
*/
|
|
78
|
-
export const DEFAULT_RECOVERY_HINTS = {
|
|
79
|
-
auth_missing: "Run `echopai login`, or set `ECHOPAI_KEY=<token>`.",
|
|
80
|
-
auth_invalid: "Token rejected. Re-issue via `echopai login` or check token revocation.",
|
|
81
|
-
auth_expired: "Token expired. Re-issue via `echopai login`.",
|
|
82
|
-
scope_insufficient: "Run `echopai whoami` to see available scopes; request the missing scope from your app admin.",
|
|
83
|
-
kind_not_allowed: "Endpoint not available for this token kind. See `echopai whoami` for your kind.",
|
|
84
|
-
channel_not_allowed: "This CLI/MCP channel is not authorized for the current token. Check `allowed_clients` on your app config.",
|
|
85
|
-
invalid_args: "Check parameter types/format. Run with `--help` for the schema.",
|
|
86
|
-
validation_failed: "Server rejected the request body. Check the `message` for the offending field.",
|
|
87
|
-
not_found: "Resource does not exist or is not visible to this token.",
|
|
88
|
-
idempotency_conflict: "Same Idempotency-Key seen with a different body. Use a new key or replay the original request exactly.",
|
|
89
|
-
confirmation_required: "Non-TTY write requires `--yes`. Re-run with `--yes` after verifying the operation.",
|
|
90
|
-
dry_run_unsupported: "This endpoint does not support `--dry-run`. Run without `--dry-run` (and `--yes` if it's a write).",
|
|
91
|
-
unsafe_command: "This command is gated. See `docs/PLAN_CLI_V2_AGENT_SURFACE.md` §6 for safety model.",
|
|
92
|
-
rate_limited: "Slow down. Honor `Retry-After` header if present; back off and retry.",
|
|
93
|
-
quota_exhausted: "Monthly quota for this endpoint class exceeded. Upgrade plan or wait until reset.",
|
|
94
|
-
agent_budget_exhausted: "Agent budget exhausted. Start a new session via `agent session-start` or request a budget bump.",
|
|
95
|
-
network_error: "Verify network reachability and `base_url`. Use `--debug` for full trace.",
|
|
96
|
-
timeout: "Server took too long. Retry; if persistent, check `echopai doctor`.",
|
|
97
|
-
stream_error: "WebSocket closed abnormally. Use `--debug` to see the close code/reason; `--reconnect` to auto-retry.",
|
|
98
|
-
upstream_unavailable: "Upstream service is temporarily unavailable. Retry with backoff.",
|
|
99
|
-
internal_error: "Server-side error. Retry; include the `request_id` when reporting.",
|
|
100
|
-
http_error: "Server did not return a typed error envelope. See `message` and `request_id`.",
|
|
101
|
-
};
|
|
102
|
-
/**
|
|
103
|
-
* Resolve effective recovery hint:
|
|
104
|
-
* 1. Explicit hint (server-provided or CLI-passed)
|
|
105
|
-
* 2. DEFAULT_RECOVERY_HINTS[code] fallback
|
|
106
|
-
* 3. undefined if code unknown and no hint
|
|
107
|
-
*/
|
|
108
|
-
export function resolveRecoveryHint(code, explicit) {
|
|
109
|
-
if (explicit && explicit.length > 0)
|
|
110
|
-
return explicit;
|
|
111
|
-
if (isKnownErrorCode(code))
|
|
112
|
-
return DEFAULT_RECOVERY_HINTS[code];
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
export class CallApiError extends Error {
|
|
116
|
-
code;
|
|
117
|
-
retryable;
|
|
118
|
-
recovery_hint;
|
|
119
|
-
httpStatus;
|
|
120
|
-
requestId;
|
|
121
|
-
raw;
|
|
122
|
-
constructor(args) {
|
|
123
|
-
super(args.message);
|
|
124
|
-
this.name = "CallApiError";
|
|
125
|
-
this.code = args.code;
|
|
126
|
-
this.retryable = args.retryable ?? isRetryableByCode(args.code);
|
|
127
|
-
this.recovery_hint = resolveRecoveryHint(args.code, args.recovery_hint);
|
|
128
|
-
this.httpStatus = args.httpStatus;
|
|
129
|
-
this.requestId = args.requestId;
|
|
130
|
-
this.raw = args.raw;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Parse possibly-envelope JSON body. Returns CallApiError if it looks like a
|
|
135
|
-
* standard EchoPai error, else null.
|
|
136
|
-
*/
|
|
137
|
-
export function tryParseErrorEnvelope(body, httpStatus, requestId) {
|
|
138
|
-
if (typeof body !== "object" || body === null)
|
|
139
|
-
return null;
|
|
140
|
-
const obj = body;
|
|
141
|
-
const direct = errorFromObject(obj.error, body, httpStatus, requestId);
|
|
142
|
-
if (direct)
|
|
143
|
-
return direct;
|
|
144
|
-
// FastAPI sometimes wraps the service envelope as {detail: {error: {...}}}.
|
|
145
|
-
if (typeof obj.detail === "object" && obj.detail !== null) {
|
|
146
|
-
const detail = obj.detail;
|
|
147
|
-
const nested = errorFromObject(detail.error, body, httpStatus, requestId);
|
|
148
|
-
if (nested)
|
|
149
|
-
return nested;
|
|
150
|
-
}
|
|
151
|
-
// FastAPI fallback {detail: "..."}
|
|
152
|
-
if (typeof obj.detail === "string") {
|
|
153
|
-
return new CallApiError({
|
|
154
|
-
code: httpStatus === 401 ? "auth_missing" : "http_error",
|
|
155
|
-
message: obj.detail,
|
|
156
|
-
httpStatus,
|
|
157
|
-
requestId,
|
|
158
|
-
raw: body,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
function errorFromObject(value, raw, httpStatus, requestId) {
|
|
164
|
-
if (typeof value !== "object" || value === null)
|
|
165
|
-
return null;
|
|
166
|
-
const env = value;
|
|
167
|
-
if (typeof env.code !== "string" || typeof env.message !== "string") {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
return new CallApiError({
|
|
171
|
-
code: env.code,
|
|
172
|
-
message: env.message,
|
|
173
|
-
...(typeof env.retryable === "boolean" ? { retryable: env.retryable } : {}),
|
|
174
|
-
...(typeof env.recovery_hint === "string"
|
|
175
|
-
? { recovery_hint: env.recovery_hint }
|
|
176
|
-
: {}),
|
|
177
|
-
httpStatus,
|
|
178
|
-
requestId,
|
|
179
|
-
raw,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
export function exitCodeForStatus(httpStatus) {
|
|
183
|
-
if (httpStatus >= 400 && httpStatus < 500)
|
|
184
|
-
return 1;
|
|
185
|
-
return 2;
|
|
186
|
-
}
|
package/dist/runtime/filters.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output filter pipeline applied between response envelope and render.
|
|
3
|
-
*
|
|
4
|
-
* Pipeline order (each step optional, --flag triggers):
|
|
5
|
-
* 1. --query <jmespath> data = jmespath.search(data, expr)
|
|
6
|
-
* 2. --fields a,b,c keep only these keys per item/object
|
|
7
|
-
* 3. --max-bytes N binary-search items truncation; meta.truncated=true
|
|
8
|
-
*
|
|
9
|
-
* 设计原则:
|
|
10
|
-
* - JMESPath 错误算 user error(exit 1),调用方负责捕获并 writeError("invalid_args")。
|
|
11
|
-
* - --fields 对 array / {items:[]} / 普通 object 都做 best-effort projection。
|
|
12
|
-
* - --max-bytes 是 serialized envelope 的字节数;items 数组形态可智能裁剪,
|
|
13
|
-
* 其他形态退化为整 data 替换为 omitted 占位符。
|
|
14
|
-
* - 截断时 meta.truncated 翻为 true,加 truncation_reason / truncated_items_dropped。
|
|
15
|
-
*/
|
|
16
|
-
import jmespathPkg from "jmespath";
|
|
17
|
-
const jmes = jmespathPkg;
|
|
18
|
-
const jmesSearch = jmes.search ?? (jmes.default ? jmes.default.search : (() => null));
|
|
19
|
-
export class FilterError extends Error {
|
|
20
|
-
constructor(message) {
|
|
21
|
-
super(message);
|
|
22
|
-
this.name = "FilterError";
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
export function applyFilters(envelope, opts) {
|
|
26
|
-
let data = envelope.data;
|
|
27
|
-
let meta = { ...envelope.meta };
|
|
28
|
-
if (opts.query && opts.query.length > 0) {
|
|
29
|
-
try {
|
|
30
|
-
data = jmesSearch(data, opts.query);
|
|
31
|
-
}
|
|
32
|
-
catch (e) {
|
|
33
|
-
throw new FilterError(`Invalid --query expression: ${e instanceof Error ? e.message : String(e)}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (opts.fields && opts.fields.length > 0) {
|
|
37
|
-
data = pickFields(data, opts.fields);
|
|
38
|
-
}
|
|
39
|
-
if (opts.maxBytes !== undefined && opts.maxBytes > 0) {
|
|
40
|
-
return enforceMaxBytes({ data, meta }, opts.maxBytes);
|
|
41
|
-
}
|
|
42
|
-
return { data, meta };
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Recursively project: array → map; {items:[...]} → recurse into items;
|
|
46
|
-
* plain object → pick listed keys; primitive → unchanged.
|
|
47
|
-
*/
|
|
48
|
-
export function pickFields(value, fields) {
|
|
49
|
-
if (Array.isArray(value)) {
|
|
50
|
-
return value.map((item) => pickFields(item, fields));
|
|
51
|
-
}
|
|
52
|
-
if (value !== null && typeof value === "object") {
|
|
53
|
-
const obj = value;
|
|
54
|
-
if (Array.isArray(obj.items)) {
|
|
55
|
-
return {
|
|
56
|
-
...obj,
|
|
57
|
-
items: obj.items.map((it) => pickFields(it, fields)),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
const out = {};
|
|
61
|
-
for (const f of fields) {
|
|
62
|
-
if (f in obj)
|
|
63
|
-
out[f] = obj[f];
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
return value;
|
|
68
|
-
}
|
|
69
|
-
function enforceMaxBytes(env, maxBytes) {
|
|
70
|
-
const initialSize = JSON.stringify(env).length;
|
|
71
|
-
if (initialSize <= maxBytes)
|
|
72
|
-
return env;
|
|
73
|
-
const data = env.data;
|
|
74
|
-
// Truncatable shape 1: object with {items: [...]} (envelope-style)
|
|
75
|
-
if (data !== null && typeof data === "object" && !Array.isArray(data)) {
|
|
76
|
-
const obj = data;
|
|
77
|
-
if (Array.isArray(obj.items)) {
|
|
78
|
-
const items = obj.items;
|
|
79
|
-
const total = items.length;
|
|
80
|
-
const buildEnv = (n) => ({
|
|
81
|
-
data: { ...obj, items: items.slice(0, n) },
|
|
82
|
-
meta: {
|
|
83
|
-
...env.meta,
|
|
84
|
-
truncated: true,
|
|
85
|
-
truncation_reason: "max_bytes",
|
|
86
|
-
truncated_items_dropped: total - n,
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
const fit = binarySearchFit(buildEnv, total, maxBytes);
|
|
90
|
-
return buildEnv(fit);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// Truncatable shape 2: top-level array
|
|
94
|
-
if (Array.isArray(data)) {
|
|
95
|
-
const total = data.length;
|
|
96
|
-
const buildEnv = (n) => ({
|
|
97
|
-
data: data.slice(0, n),
|
|
98
|
-
meta: {
|
|
99
|
-
...env.meta,
|
|
100
|
-
truncated: true,
|
|
101
|
-
truncation_reason: "max_bytes",
|
|
102
|
-
truncated_items_dropped: total - n,
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
const fit = binarySearchFit(buildEnv, total, maxBytes);
|
|
106
|
-
return buildEnv(fit);
|
|
107
|
-
}
|
|
108
|
-
// Non-truncatable: replace data with placeholder
|
|
109
|
-
return {
|
|
110
|
-
data: {
|
|
111
|
-
omitted: true,
|
|
112
|
-
reason: "response exceeded --max-bytes; data is not paginatable",
|
|
113
|
-
},
|
|
114
|
-
meta: {
|
|
115
|
-
...env.meta,
|
|
116
|
-
truncated: true,
|
|
117
|
-
truncation_reason: "max_bytes",
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
function binarySearchFit(shape, total, maxBytes) {
|
|
122
|
-
// Find largest n in [0, total] where JSON.stringify(shape(n)).length <= maxBytes
|
|
123
|
-
let lo = 0;
|
|
124
|
-
let hi = total;
|
|
125
|
-
while (lo < hi) {
|
|
126
|
-
const mid = Math.floor((lo + hi + 1) / 2);
|
|
127
|
-
const size = JSON.stringify(shape(mid)).length;
|
|
128
|
-
if (size <= maxBytes)
|
|
129
|
-
lo = mid;
|
|
130
|
-
else
|
|
131
|
-
hi = mid - 1;
|
|
132
|
-
}
|
|
133
|
-
return lo;
|
|
134
|
-
}
|
|
135
|
-
export function parseFieldsFlag(raw) {
|
|
136
|
-
if (typeof raw !== "string" || raw.length === 0)
|
|
137
|
-
return undefined;
|
|
138
|
-
const fields = raw
|
|
139
|
-
.split(",")
|
|
140
|
-
.map((s) => s.trim())
|
|
141
|
-
.filter((s) => s.length > 0);
|
|
142
|
-
return fields.length > 0 ? fields : undefined;
|
|
143
|
-
}
|
|
144
|
-
export function parseMaxBytesFlag(raw) {
|
|
145
|
-
if (typeof raw === "number" && raw > 0)
|
|
146
|
-
return raw;
|
|
147
|
-
if (typeof raw === "string" && raw.length > 0) {
|
|
148
|
-
const n = Number(raw);
|
|
149
|
-
if (Number.isFinite(n) && n > 0)
|
|
150
|
-
return n;
|
|
151
|
-
}
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
package/dist/runtime/format.js
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output format renderer。
|
|
3
|
-
*
|
|
4
|
-
* - json JSON.stringify (multi-line in TTY, single-line otherwise)
|
|
5
|
-
* - ndjson one JSON object per line(流式友好)
|
|
6
|
-
* - table ASCII table(仅 array of flat object)
|
|
7
|
-
* - csv RFC-4180 quoted
|
|
8
|
-
* - tsv tab-separated
|
|
9
|
-
* - yaml minimal YAML(不依赖 lib,仅 flat / array of flat)
|
|
10
|
-
*
|
|
11
|
-
* 自动从 data 推断列名(取 array[0] 的 keys);若不是 array of object 则
|
|
12
|
-
* fallback 到 json(避免格式错误)。
|
|
13
|
-
*/
|
|
14
|
-
export function isOutputFormat(s) {
|
|
15
|
-
return ["json", "ndjson", "table", "csv", "tsv", "yaml"].includes(s);
|
|
16
|
-
}
|
|
17
|
-
export function render(env, fmt, isTty) {
|
|
18
|
-
if (fmt === "json") {
|
|
19
|
-
// TTY: pretty 2-space; non-TTY: single line (NDJSON-friendly pipelines)
|
|
20
|
-
return JSON.stringify(env, null, isTty ? 2 : 0);
|
|
21
|
-
}
|
|
22
|
-
if (fmt === "ndjson") {
|
|
23
|
-
return ndjson(env);
|
|
24
|
-
}
|
|
25
|
-
// table / csv / tsv / yaml — only useful on array of flat object data。
|
|
26
|
-
// server envelope 常见形态 {items: [...], total, has_more} —— 对 items 表格化。
|
|
27
|
-
let target = env.data;
|
|
28
|
-
if (typeof target === "object" &&
|
|
29
|
-
target !== null &&
|
|
30
|
-
!Array.isArray(target) &&
|
|
31
|
-
Array.isArray(target.items)) {
|
|
32
|
-
target = target.items;
|
|
33
|
-
}
|
|
34
|
-
const arr = arrayOfFlatObjects(target);
|
|
35
|
-
if (arr === null) {
|
|
36
|
-
// fallback to json
|
|
37
|
-
return JSON.stringify(env, null, isTty ? 2 : 0);
|
|
38
|
-
}
|
|
39
|
-
if (fmt === "table")
|
|
40
|
-
return renderTable(arr);
|
|
41
|
-
if (fmt === "csv")
|
|
42
|
-
return renderDelimited(arr, ",", true);
|
|
43
|
-
if (fmt === "tsv")
|
|
44
|
-
return renderDelimited(arr, "\t", false);
|
|
45
|
-
if (fmt === "yaml")
|
|
46
|
-
return renderYaml(arr);
|
|
47
|
-
return JSON.stringify(env, null, isTty ? 2 : 0);
|
|
48
|
-
}
|
|
49
|
-
function arrayOfFlatObjects(data) {
|
|
50
|
-
if (!Array.isArray(data))
|
|
51
|
-
return null;
|
|
52
|
-
if (data.length === 0)
|
|
53
|
-
return [];
|
|
54
|
-
// 允许嵌套字段(cell 渲染时 JSON.stringify),但顶层每条必须是 object。
|
|
55
|
-
for (const item of data) {
|
|
56
|
-
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
return data;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Pick a stable subset of columns for table / CSV — the union of keys from
|
|
63
|
-
* the first few rows, capped at COLUMN_LIMIT. 防超宽表格 + 缺字段 row 不破列。
|
|
64
|
-
*/
|
|
65
|
-
const COLUMN_LIMIT = 8;
|
|
66
|
-
function pickColumns(arr) {
|
|
67
|
-
const seen = new Set();
|
|
68
|
-
for (let i = 0; i < Math.min(arr.length, 5); i++) {
|
|
69
|
-
for (const k of Object.keys(arr[i])) {
|
|
70
|
-
if (!seen.has(k)) {
|
|
71
|
-
seen.add(k);
|
|
72
|
-
if (seen.size >= COLUMN_LIMIT)
|
|
73
|
-
return [...seen];
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return [...seen];
|
|
78
|
-
}
|
|
79
|
-
function ndjson(env) {
|
|
80
|
-
if (Array.isArray(env.data)) {
|
|
81
|
-
return env.data.map((d) => JSON.stringify(d)).join("\n");
|
|
82
|
-
}
|
|
83
|
-
return JSON.stringify(env);
|
|
84
|
-
}
|
|
85
|
-
function renderTable(arr) {
|
|
86
|
-
if (arr.length === 0)
|
|
87
|
-
return "(empty)";
|
|
88
|
-
const cols = pickColumns(arr);
|
|
89
|
-
const CELL_MAX = 60; // truncate long cells (URLs, content)
|
|
90
|
-
const truncate = (s) => (s.length > CELL_MAX ? s.slice(0, CELL_MAX - 1) + "…" : s);
|
|
91
|
-
const rows = arr.map((r) => cols.map((c) => truncate(stringify(r[c]))));
|
|
92
|
-
const widths = cols.map((c, i) => Math.max(c.length, ...rows.map((r) => r[i].length)));
|
|
93
|
-
const sep = widths.map((w) => "-".repeat(w)).join("-+-");
|
|
94
|
-
const headerRow = cols.map((c, i) => c.padEnd(widths[i])).join(" | ");
|
|
95
|
-
const dataRows = rows.map((r) => r.map((cell, i) => cell.padEnd(widths[i])).join(" | "));
|
|
96
|
-
return [headerRow, sep, ...dataRows].join("\n");
|
|
97
|
-
}
|
|
98
|
-
function renderDelimited(arr, delim, csvQuote) {
|
|
99
|
-
if (arr.length === 0)
|
|
100
|
-
return "";
|
|
101
|
-
const cols = pickColumns(arr);
|
|
102
|
-
const escape = (s) => {
|
|
103
|
-
if (!csvQuote)
|
|
104
|
-
return s.replace(/\t/g, " ").replace(/\n/g, " ");
|
|
105
|
-
if (s.includes(delim) || s.includes('"') || s.includes("\n")) {
|
|
106
|
-
return `"${s.replace(/"/g, '""')}"`;
|
|
107
|
-
}
|
|
108
|
-
return s;
|
|
109
|
-
};
|
|
110
|
-
const header = cols.map(escape).join(delim);
|
|
111
|
-
const rows = arr.map((r) => cols.map((c) => escape(stringify(r[c]))).join(delim));
|
|
112
|
-
return [header, ...rows].join("\n");
|
|
113
|
-
}
|
|
114
|
-
function renderYaml(arr) {
|
|
115
|
-
if (arr.length === 0)
|
|
116
|
-
return "[]";
|
|
117
|
-
return arr
|
|
118
|
-
.map((r) => {
|
|
119
|
-
const lines = Object.entries(r).map(([k, v]) => ` ${k}: ${yamlScalar(v)}`);
|
|
120
|
-
return ["-", ...lines].join("\n");
|
|
121
|
-
})
|
|
122
|
-
.join("\n");
|
|
123
|
-
}
|
|
124
|
-
function yamlScalar(v) {
|
|
125
|
-
if (v === null || v === undefined)
|
|
126
|
-
return "null";
|
|
127
|
-
if (typeof v === "string") {
|
|
128
|
-
// quote if contains special chars
|
|
129
|
-
if (v === "" || /[:#\n"']|^\s|\s$/.test(v))
|
|
130
|
-
return JSON.stringify(v);
|
|
131
|
-
return v;
|
|
132
|
-
}
|
|
133
|
-
return String(v);
|
|
134
|
-
}
|
|
135
|
-
function stringify(v) {
|
|
136
|
-
if (v === null || v === undefined)
|
|
137
|
-
return "";
|
|
138
|
-
if (typeof v === "string")
|
|
139
|
-
return v;
|
|
140
|
-
if (typeof v === "number" || typeof v === "boolean")
|
|
141
|
-
return String(v);
|
|
142
|
-
return JSON.stringify(v);
|
|
143
|
-
}
|
package/dist/runtime/http.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Centralized header construction for all CLI outbound requests (HTTP + WS).
|
|
3
|
-
*
|
|
4
|
-
* 强制注入:
|
|
5
|
-
* - Authorization (HTTP only; WS 走 Sec-WebSocket-Protocol subprotocol)
|
|
6
|
-
* - Accept: application/json (HTTP)
|
|
7
|
-
* - User-Agent: echopai-cli/<v> Node/<v> <platform>-<arch>
|
|
8
|
-
* - X-Client: echopai-cli/<v>
|
|
9
|
-
* - X-Client-Channel: cli | mcp (默认 cli;mcp serve 注入 mcp)
|
|
10
|
-
* - X-Request-Id: client-generated uuidv4 (server 若 echo 同名 header 以 server 为准)
|
|
11
|
-
*
|
|
12
|
-
* 每次构造生成新的 X-Request-Id(除非显式传 requestId)。分页 / WS 重连场景每次
|
|
13
|
-
* 迭代都应该重新调一次 builder,以拿到独立的 request_id。
|
|
14
|
-
*
|
|
15
|
-
* 与 envelope.ts 的关系:本模块只负责出站;入站 / meta 拼装由 envelope.ts 做。
|
|
16
|
-
*/
|
|
17
|
-
import { randomUUID } from "node:crypto";
|
|
18
|
-
import { Headers } from "undici";
|
|
19
|
-
export function buildUserAgent(cliVersion) {
|
|
20
|
-
// User-Agent identifier matches X-Client (also `echopai-cli/<v>`) so
|
|
21
|
-
// server-side metrics/audit can match on either header uniformly.
|
|
22
|
-
// Renamed from `@echopai/cli/<v>` in v2.0.0 alongside the npm rename
|
|
23
|
-
// from @echopai/cli → echopai. X-Client itself was already this form.
|
|
24
|
-
return `echopai-cli/${cliVersion} Node/${process.version} ${process.platform}-${process.arch}`;
|
|
25
|
-
}
|
|
26
|
-
export function buildHttpHeaders(ctx) {
|
|
27
|
-
const requestId = ctx.requestId ?? randomUUID();
|
|
28
|
-
const headers = new Headers({
|
|
29
|
-
Authorization: `Bearer ${ctx.bearer}`,
|
|
30
|
-
Accept: "application/json",
|
|
31
|
-
"User-Agent": buildUserAgent(ctx.cliVersion),
|
|
32
|
-
"X-Client": `echopai-cli/${ctx.cliVersion}`,
|
|
33
|
-
"X-Client-Channel": ctx.channel ?? "cli",
|
|
34
|
-
"X-Request-Id": requestId,
|
|
35
|
-
});
|
|
36
|
-
return { headers, requestId };
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* WebSocket upgrade headers. NO Authorization — WS auth flows via
|
|
40
|
-
* Sec-WebSocket-Protocol `bearer.<token>` subprotocol.
|
|
41
|
-
*
|
|
42
|
-
* Plain object because `ws` lib's `headers` option is Record<string,string>,
|
|
43
|
-
* not undici Headers.
|
|
44
|
-
*/
|
|
45
|
-
export function buildWsHeaders(ctx) {
|
|
46
|
-
const requestId = ctx.requestId ?? randomUUID();
|
|
47
|
-
const headers = {
|
|
48
|
-
"User-Agent": buildUserAgent(ctx.cliVersion),
|
|
49
|
-
"X-Client": `echopai-cli/${ctx.cliVersion}`,
|
|
50
|
-
"X-Client-Channel": ctx.channel ?? "cli",
|
|
51
|
-
"X-Request-Id": requestId,
|
|
52
|
-
};
|
|
53
|
-
return { headers, requestId };
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Effective request_id for meta envelope / logging:
|
|
57
|
-
* server echo wins (server can rewrite for correlation in its own audit);
|
|
58
|
-
* otherwise client-generated uuid is canonical.
|
|
59
|
-
*
|
|
60
|
-
* After Phase 1 lands, both ends will have a request_id even for endpoints
|
|
61
|
-
* the server doesn't echo yet — they just won't agree until server adopts echo.
|
|
62
|
-
*/
|
|
63
|
-
export function resolveRequestId(serverHeader, clientGenerated) {
|
|
64
|
-
return serverHeader && serverHeader.length > 0 ? serverHeader : clientGenerated;
|
|
65
|
-
}
|