@evatick/cli 0.3.2 → 0.4.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.
package/README.md CHANGED
@@ -1,7 +1,13 @@
1
- # EVA CLI
1
+ # EVA Tick CLI
2
2
 
3
- EVA CLI is a stable, machine-readable market-data command-line client for LLMs,
4
- AI agents, and automation. It connects to the versioned EVA HTTP API.
3
+ Deterministic, machine-readable financial data for AI agents, LLMs, and
4
+ automation.
5
+
6
+ EVA Tick gives agents canonical instrument IDs, stable JSON, structured errors,
7
+ and predictable commands instead of provider-specific identifiers and
8
+ functions.
9
+
10
+ ![Codex queries financial data through EVA Tick](https://raw.githubusercontent.com/xiaochaohit/evatick-cli/main/docs/assets/eva-tick-codex-demo.gif)
5
11
 
6
12
  ## Install
7
13
 
@@ -10,19 +16,81 @@ npm install --global @evatick/cli
10
16
  eva version
11
17
  ```
12
18
 
13
- It can also be run without a global installation:
19
+ Run it without installing:
14
20
 
15
21
  ```shell
16
22
  npx @evatick/cli health
17
23
  ```
18
24
 
19
- The npm package installs a native executable for the current platform. Python is
20
- not required. Supported targets are macOS arm64/x64, Linux x64 (glibc), and
21
- Windows x64.
25
+ The npm package is implemented in Node.js, has no runtime dependencies, and
26
+ does not require Python or platform-specific native executables.
27
+
28
+ ## First query
29
+
30
+ ```shell
31
+ eva health
32
+ eva instrument search --query 平安银行 --type equity --limit 3
33
+ eva instrument resolve --query 000001 --type equity
34
+ eva stock bars --symbol cn:equity:XSHE:000001 \
35
+ --interval 1d \
36
+ --start 2026-08-18 \
37
+ --end 2026-08-21 \
38
+ --adjustment forward \
39
+ --limit 3
40
+ ```
41
+
42
+ ## Common commands
43
+
44
+ - `eva instrument search` finds canonical financial instruments by name, code,
45
+ or alias.
46
+ - `eva stock bars` queries normalized OHLCV bars.
47
+ - `eva index constituents` queries index constituent memberships.
48
+
49
+ Run `eva COMMAND --help` or `eva GROUP COMMAND --help` for supported options.
50
+
51
+ ## Hosted or self-hosted
52
+
53
+ The CLI connects to the hosted EVA Tick service at `https://api.evatick.com` by
54
+ default. Point it at a self-hosted [EVA Tick Server](https://github.com/xiaochaohit/evatick-server)
55
+ temporarily with an environment variable:
56
+
57
+ ```shell
58
+ EVA_SERVER_URL=http://127.0.0.1:8765 eva health
59
+ ```
60
+
61
+ Or save the service URL:
62
+
63
+ ```shell
64
+ eva config set --base-url http://127.0.0.1:8765
65
+ eva config show
66
+ ```
67
+
68
+ When a service requires authentication, use `EVA_API_KEY` or
69
+ `eva config set --api-key 'eva_...'`. The CLI sends a key only when configured,
70
+ and `eva config show` masks stored keys.
71
+
72
+ ## Current availability
73
+
74
+ EVA Tick is designed as a provider-independent financial data layer. The
75
+ current public service starts with mainland China A-shares and indices published
76
+ by the Shanghai Stock Exchange, Shenzhen Stock Exchange, and China Securities
77
+ Index. Availability varies by deployed service and enabled providers.
78
+
79
+ ## Output and safety
80
+
81
+ - Successful results are compact JSON on stdout.
82
+ - Failures are structured JSON on stderr with stable error codes.
83
+ - JSON, JSONL, and CSV file exports are supported by the npm CLI.
84
+ - Existing files are never overwritten unless `--overwrite` is explicit.
85
+ - Parquet export is available in the Python distribution with
86
+ `pip install 'evatick[parquet]'`.
87
+
88
+ ## Documentation
22
89
 
23
- JSON, JSONL, and CSV exports are supported. Parquet export remains available in
24
- the Python distribution (`pip install 'evatick[parquet]'`) and is intentionally
25
- not bundled into the native npm executables.
90
+ - [Complete CLI documentation and examples](https://github.com/xiaochaohit/evatick-cli#readme)
91
+ - [EVA Tick Skill for Codex](https://www.npmjs.com/package/@evatick/skill)
92
+ - [Self-hosted server and OpenAPI contract](https://github.com/xiaochaohit/evatick-server)
93
+ - [License and data-source notices](https://github.com/xiaochaohit/evatick-cli#license)
26
94
 
27
- See the [EVA CLI repository](https://github.com/xiaochaohit/evatick-cli) for
28
- commands, configuration, source code, and license information.
95
+ Market data is provided for research and reference only and does not constitute
96
+ investment advice.
package/bin/eva.js CHANGED
@@ -2,42 +2,21 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { spawnSync } = require("node:child_process");
5
+ const { main } = require("../lib/cli.js");
6
6
 
7
- const targets = {
8
- "darwin-arm64": ["@evatick/cli-darwin-arm64", "eva"],
9
- "darwin-x64": ["@evatick/cli-darwin-x64", "eva"],
10
- "linux-x64": ["@evatick/cli-linux-x64", "eva"],
11
- "win32-x64": ["@evatick/cli-win32-x64", "eva.exe"],
12
- };
13
-
14
- const target = `${process.platform}-${process.arch}`;
15
- const selected = targets[target];
16
- if (!selected) {
17
- console.error(
18
- `EVA CLI does not support ${target}. Supported targets: ${Object.keys(targets).join(", ")}.`,
19
- );
20
- process.exit(1);
21
- }
22
-
23
- let executable;
24
- try {
25
- executable = require.resolve(`${selected[0]}/bin/${selected[1]}`);
26
- } catch (error) {
27
- console.error(
28
- `The EVA CLI executable for ${target} is missing. Reinstall @evatick/cli without omitting optional dependencies.`,
29
- );
30
- process.exit(1);
31
- }
32
-
33
- const result = spawnSync(executable, process.argv.slice(2), {
34
- stdio: "inherit",
7
+ main(process.argv.slice(2)).catch((error) => {
8
+ const problem = error && error.evaProblem
9
+ ? error.evaProblem
10
+ : {
11
+ code: "INTERNAL_ERROR",
12
+ message: "EVA CLI failed unexpectedly",
13
+ retryable: false,
14
+ exitCode: 1,
15
+ };
16
+ process.stderr.write(`${JSON.stringify({
17
+ code: problem.code,
18
+ message: problem.message,
19
+ retryable: problem.retryable,
20
+ })}\n`);
21
+ process.exitCode = problem.exitCode;
35
22
  });
36
- if (result.error) {
37
- console.error(`Unable to start EVA CLI: ${result.error.message}`);
38
- process.exit(1);
39
- }
40
- if (result.signal) {
41
- process.kill(process.pid, result.signal);
42
- }
43
- process.exit(result.status === null ? 1 : result.status);
package/lib/cli.js ADDED
@@ -0,0 +1,414 @@
1
+ "use strict";
2
+
3
+ const path = require("node:path");
4
+ const {
5
+ defaultConfigurationPath,
6
+ expandHome,
7
+ loadConfiguration,
8
+ saveConfiguration,
9
+ } = require("./config.js");
10
+ const { EvaError, usage } = require("./errors.js");
11
+ const { EvaHttpClient } = require("./http-client.js");
12
+ const { dumps, emit } = require("./output.js");
13
+
14
+ const DEFAULT_SERVER_URL = "https://api.evatick.com";
15
+ const VERSION = require("../package.json").version;
16
+ const INTERVALS = ["1m", "5m", "15m", "30m", "60m", "1d", "1w", "1mo"];
17
+
18
+ const ROOT_HELP = `NAME
19
+ eva
20
+
21
+ PURPOSE
22
+ 通过 EVA 查询股票和指数数据。
23
+
24
+ USAGE
25
+ eva [--config PATH] [--server-url URL] [--api-key KEY] COMMAND [OPTIONS]
26
+
27
+ OPTIONS
28
+ --config PATH
29
+ --server-url URL
30
+ --api-key KEY
31
+
32
+ COMMANDS
33
+ config
34
+ health
35
+ index
36
+ instrument
37
+ stock
38
+ version
39
+ `;
40
+
41
+ const HELP = {
42
+ config: "Usage: eva config [OPTIONS] COMMAND [ARGS]...\n\n Manage the unified CLI configuration file.\n\nOptions:\n --help Show this message and exit.\n\nCommands:\n set\n show\n",
43
+ "config set": "Usage: eva config set [OPTIONS]\n\nOptions:\n --base-url TEXT\n --help\n",
44
+ "config show": "Usage: eva config show [OPTIONS]\n\nOptions:\n --help\n",
45
+ health: "Usage: eva health [OPTIONS]\n\nOptions:\n --timeout FLOAT [default: 10]\n --retries INTEGER [default: 0]\n --help\n",
46
+ version: "Usage: eva version [OPTIONS]\n\nOptions:\n --help\n",
47
+ instrument: "Usage: eva instrument [OPTIONS] COMMAND [ARGS]...\n\n Discover and resolve canonical instruments.\n\nOptions:\n --help Show this message and exit.\n\nCommands:\n list\n resolve\n search\n show\n",
48
+ "instrument list": "Usage: eva instrument list [OPTIONS]\n\nOptions:\n --type [equity|index]\n --venue TEXT\n --publisher TEXT\n --capability [quote|bars|constituents]\n --cursor TEXT\n --limit INTEGER\n --output PATH\n --format [json|jsonl|csv|parquet]\n --overwrite\n --timeout FLOAT [default: 120]\n --retries INTEGER [default: 2]\n --help\n",
49
+ "instrument search": "Usage: eva instrument search [OPTIONS]\n\nOptions:\n --query TEXT [required]\n --type [equity|index]\n --venue TEXT\n --publisher TEXT\n --capability [quote|bars|constituents]\n --limit INTEGER\n --output PATH\n --format [json|jsonl|csv|parquet]\n --overwrite\n --timeout FLOAT [default: 120]\n --retries INTEGER [default: 2]\n --help\n",
50
+ "instrument resolve": "Usage: eva instrument resolve [OPTIONS]\n\nOptions:\n --query TEXT [required]\n --type [equity|index]\n --venue TEXT\n --publisher TEXT\n --capability [quote|bars|constituents]\n --timeout FLOAT [default: 120]\n --retries INTEGER [default: 2]\n --help\n",
51
+ "instrument show": "Usage: eva instrument show [OPTIONS]\n\nOptions:\n --id TEXT [required]\n --timeout FLOAT [default: 120]\n --retries INTEGER [default: 2]\n --help\n",
52
+ stock: "Usage: eva stock [OPTIONS] COMMAND [ARGS]...\n\n Query mainland A-share data.\n\nOptions:\n --help Show this message and exit.\n\nCommands:\n bars\n quotes\n",
53
+ index: "Usage: eva index [OPTIONS] COMMAND [ARGS]...\n\n Query SSE, SZSE, and CSI index data.\n\nOptions:\n --help Show this message and exit.\n\nCommands:\n bars\n constituents\n quotes\n",
54
+ "stock quotes": "Usage: eva stock quotes [OPTIONS]\n\nOptions:\n --symbol TEXT [required]\n --timeout FLOAT [default: 120]\n --retries INTEGER [default: 2]\n --help\n",
55
+ "index quotes": "Usage: eva index quotes [OPTIONS]\n\nOptions:\n --symbol TEXT [required]\n --timeout FLOAT [default: 120]\n --retries INTEGER [default: 2]\n --help\n",
56
+ "stock bars": "Usage: eva stock bars [OPTIONS]\n\nOptions:\n --symbol TEXT [required]\n --interval [1m|5m|15m|30m|60m|1d|1w|1mo] [default: 1d]\n --start TEXT\n --end TEXT\n --adjustment [none|forward|backward] [default: none]\n --limit INTEGER\n --output PATH\n --format [json|jsonl|csv|parquet]\n --overwrite\n --timeout FLOAT [default: 120]\n --retries INTEGER [default: 2]\n --help\n",
57
+ "index bars": "Usage: eva index bars [OPTIONS]\n\nOptions:\n --symbol TEXT [required]\n --interval [1m|5m|15m|30m|60m|1d|1w|1mo] [default: 1d]\n --start TEXT\n --end TEXT\n --adjustment [none|forward|backward] [default: none]\n --limit INTEGER\n --output PATH\n --format [json|jsonl|csv|parquet]\n --overwrite\n --timeout FLOAT [default: 120]\n --retries INTEGER [default: 2]\n --help\n",
58
+ "index constituents": "Usage: eva index constituents [OPTIONS]\n\nOptions:\n --symbol TEXT [required]\n --as-of TEXT\n --limit INTEGER\n --output PATH\n --format [json|jsonl|csv|parquet]\n --overwrite\n --timeout FLOAT [default: 120]\n --retries INTEGER [default: 2]\n --help\n",
59
+ };
60
+
61
+ const rootDefinitions = {
62
+ config: { type: "string" },
63
+ "server-url": { type: "string" },
64
+ "api-key": { type: "string" },
65
+ };
66
+ const requestDefinitions = {
67
+ timeout: { type: "number", min: 0.1, default: 120 },
68
+ retries: { type: "integer", min: 0, default: 2 },
69
+ };
70
+ const dataDefinitions = {
71
+ limit: { type: "integer", min: 1 },
72
+ output: { type: "string" },
73
+ format: { type: "choice", choices: ["json", "jsonl", "csv", "parquet"] },
74
+ overwrite: { type: "boolean", default: false },
75
+ ...requestDefinitions,
76
+ };
77
+ const discoveryDefinitions = {
78
+ type: { type: "choice", choices: ["equity", "index"] },
79
+ venue: { type: "string" },
80
+ publisher: { type: "string" },
81
+ capability: { type: "choice", choices: ["quote", "bars", "constituents"] },
82
+ };
83
+
84
+ function parseValue(name, raw, definition) {
85
+ if (definition.type === "string") return raw;
86
+ if (definition.type === "choice") {
87
+ if (!definition.choices.includes(raw)) {
88
+ throw usage(`Invalid value for '--${name}': '${raw}' is not one of ${definition.choices.join(", ")}.`);
89
+ }
90
+ return raw;
91
+ }
92
+ const value = Number(raw);
93
+ const valid = definition.type === "integer" ? Number.isInteger(value) : Number.isFinite(value);
94
+ if (!valid || value < definition.min) {
95
+ throw usage(`Invalid value for '--${name}': ${raw}`);
96
+ }
97
+ return value;
98
+ }
99
+
100
+ function parseOptions(arguments_, definitions, required = []) {
101
+ const values = {};
102
+ for (const [name, definition] of Object.entries(definitions)) {
103
+ if (definition.default !== undefined) values[toProperty(name)] = definition.default;
104
+ }
105
+ for (let index = 0; index < arguments_.length; index += 1) {
106
+ const argument = arguments_[index];
107
+ if (!argument.startsWith("--")) throw usage(`Got unexpected extra argument (${argument})`);
108
+ const separator = argument.indexOf("=");
109
+ const name = argument.slice(2, separator === -1 ? undefined : separator);
110
+ const definition = definitions[name];
111
+ if (!definition) throw usage(`No such option: --${name}`);
112
+ const property = toProperty(name);
113
+ if (definition.type === "boolean") {
114
+ if (separator !== -1) throw usage(`Option '--${name}' does not take a value.`);
115
+ values[property] = true;
116
+ continue;
117
+ }
118
+ let raw;
119
+ if (separator !== -1) raw = argument.slice(separator + 1);
120
+ else {
121
+ index += 1;
122
+ raw = arguments_[index];
123
+ if (raw === undefined || raw.startsWith("--")) throw usage(`Option '--${name}' requires an argument.`);
124
+ }
125
+ values[property] = parseValue(name, raw, definition);
126
+ }
127
+ for (const name of required) {
128
+ if (values[toProperty(name)] === undefined) throw usage(`Missing option '--${name}'.`);
129
+ }
130
+ return values;
131
+ }
132
+
133
+ function toProperty(name) {
134
+ return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
135
+ }
136
+
137
+ function parseRoot(arguments_) {
138
+ const optionArguments = [];
139
+ let index = 0;
140
+ while (index < arguments_.length && arguments_[index].startsWith("--")) {
141
+ const argument = arguments_[index];
142
+ if (argument === "--help") return { help: true };
143
+ optionArguments.push(argument);
144
+ const name = argument.slice(2).split("=", 1)[0];
145
+ if (!rootDefinitions[name]) throw usage(`No such option: --${name}`);
146
+ if (!argument.includes("=")) {
147
+ index += 1;
148
+ if (arguments_[index] === undefined) throw usage(`Option '--${name}' requires an argument.`);
149
+ optionArguments.push(arguments_[index]);
150
+ }
151
+ index += 1;
152
+ }
153
+ return {
154
+ options: parseOptions(optionArguments, rootDefinitions),
155
+ remaining: arguments_.slice(index),
156
+ };
157
+ }
158
+
159
+ function queryString(values) {
160
+ const query = new URLSearchParams();
161
+ for (const [name, value] of Object.entries(values)) {
162
+ if (value !== undefined && value !== null) query.set(name, String(value));
163
+ }
164
+ return query.toString();
165
+ }
166
+
167
+ function leaf(pathParts, arguments_) {
168
+ const key = pathParts.join(" ");
169
+ if (arguments_.includes("--help")) {
170
+ process.stdout.write(HELP[key]);
171
+ return null;
172
+ }
173
+ return key;
174
+ }
175
+
176
+ function contextFrom(rootOptions, environment) {
177
+ const configurationPath = path.resolve(expandHome(
178
+ rootOptions.config || defaultConfigurationPath(environment),
179
+ ));
180
+ const configuration = loadConfiguration(configurationPath);
181
+ return {
182
+ configurationPath,
183
+ configuration,
184
+ baseUrl: rootOptions.serverUrl || environment.EVA_SERVER_URL || configuration.baseUrl || DEFAULT_SERVER_URL,
185
+ apiKey: rootOptions.apiKey || environment.EVA_API_KEY || configuration.apiKey,
186
+ };
187
+ }
188
+
189
+ function client(context, options) {
190
+ return new EvaHttpClient(
191
+ context.baseUrl,
192
+ options.timeout,
193
+ options.retries,
194
+ context.apiKey,
195
+ VERSION,
196
+ );
197
+ }
198
+
199
+ async function dispatch(context, command, subcommand, arguments_) {
200
+ const key = leaf(subcommand ? [command, subcommand] : [command], arguments_);
201
+ if (key === null) return;
202
+
203
+ if (key === "version") {
204
+ parseOptions(arguments_, {});
205
+ process.stdout.write(`${dumps({
206
+ eva_cli: VERSION,
207
+ eva_server_api: "v1",
208
+ node: process.versions.node,
209
+ })}\n`);
210
+ return;
211
+ }
212
+ if (key === "config show") {
213
+ parseOptions(arguments_, {});
214
+ process.stdout.write(`${dumps({
215
+ path: context.configurationPath,
216
+ base_url: context.configuration.baseUrl || null,
217
+ api_key: context.configuration.apiKey ? "********" : null,
218
+ })}\n`);
219
+ return;
220
+ }
221
+ if (key === "config set") {
222
+ const options = parseOptions(arguments_, {
223
+ "base-url": { type: "string" },
224
+ "api-key": { type: "string" },
225
+ });
226
+ if (options.baseUrl === undefined && options.apiKey === undefined) {
227
+ throw usage("at least one of --base-url or --api-key is required");
228
+ }
229
+ const updated = {
230
+ baseUrl: options.baseUrl === undefined ? context.configuration.baseUrl : options.baseUrl,
231
+ apiKey: options.apiKey === undefined ? context.configuration.apiKey : options.apiKey,
232
+ };
233
+ if (updated.baseUrl && !(updated.baseUrl.startsWith("http://") || updated.baseUrl.startsWith("https://"))) {
234
+ throw usage("--base-url must start with http:// or https://");
235
+ }
236
+ saveConfiguration(context.configurationPath, updated);
237
+ process.stdout.write(`${dumps({ path: context.configurationPath, updated: true })}\n`);
238
+ return;
239
+ }
240
+ if (key === "health") {
241
+ const options = parseOptions(arguments_, {
242
+ timeout: { type: "number", min: 0.1, default: 10 },
243
+ retries: { type: "integer", min: 0, default: 0 },
244
+ });
245
+ const response = await client(context, options).request("GET", "/v1/health");
246
+ process.stdout.write(`${dumps(response.data ?? {})}\n`);
247
+ return;
248
+ }
249
+ if (key === "instrument list") {
250
+ const options = parseOptions(arguments_, {
251
+ ...discoveryDefinitions,
252
+ cursor: { type: "string" },
253
+ ...dataDefinitions,
254
+ });
255
+ const limit = options.limit || 100;
256
+ const response = await client(context, options).request(
257
+ "GET",
258
+ `/v1/instruments?${queryString({
259
+ instrument_type: options.type,
260
+ venue: options.venue,
261
+ publisher: options.publisher,
262
+ capability: options.capability,
263
+ cursor: options.cursor,
264
+ limit: Math.min(limit, 1000),
265
+ })}`,
266
+ );
267
+ emit(
268
+ { items: response.data ?? [], next_cursor: (response.page || {}).next_cursor ?? null },
269
+ { ...options, limit: undefined },
270
+ );
271
+ return;
272
+ }
273
+ if (key === "instrument search") {
274
+ const options = parseOptions(arguments_, {
275
+ query: { type: "string" }, ...discoveryDefinitions, ...dataDefinitions,
276
+ }, ["query"]);
277
+ const limit = Math.min(options.limit || 20, 200);
278
+ const response = await client(context, options).request(
279
+ "GET",
280
+ `/v1/instrument-search?${queryString({
281
+ q: options.query,
282
+ instrument_type: options.type,
283
+ venue: options.venue,
284
+ publisher: options.publisher,
285
+ capability: options.capability,
286
+ limit,
287
+ })}`,
288
+ );
289
+ emit(response.data ?? [], { ...options, limit });
290
+ return;
291
+ }
292
+ if (key === "instrument resolve") {
293
+ const options = parseOptions(arguments_, {
294
+ query: { type: "string" }, ...discoveryDefinitions, ...requestDefinitions,
295
+ }, ["query"]);
296
+ const resolutionContext = {};
297
+ if (options.type !== undefined) resolutionContext.instrument_type = options.type;
298
+ if (options.venue !== undefined) resolutionContext.venue = options.venue;
299
+ if (options.publisher !== undefined) resolutionContext.publisher = options.publisher;
300
+ if (options.capability !== undefined) resolutionContext.capability = options.capability;
301
+ const response = await client(context, options).request("POST", "/v1/instrument-resolve", {
302
+ query: options.query,
303
+ context: resolutionContext,
304
+ });
305
+ process.stdout.write(`${dumps(response.data ?? {})}\n`);
306
+ return;
307
+ }
308
+ if (key === "instrument show") {
309
+ const options = parseOptions(arguments_, {
310
+ id: { type: "string" }, ...requestDefinitions,
311
+ }, ["id"]);
312
+ const response = await client(context, options).request(
313
+ "GET",
314
+ `/v1/instruments/${encodeURIComponent(options.id)}`,
315
+ );
316
+ process.stdout.write(`${dumps(response.data ?? {})}\n`);
317
+ return;
318
+ }
319
+ if (key === "stock quotes" || key === "index quotes") {
320
+ const options = parseOptions(arguments_, {
321
+ symbol: { type: "string" }, ...requestDefinitions,
322
+ }, ["symbol"]);
323
+ const instrumentType = command === "stock" ? "equity" : "index";
324
+ const http = client(context, options);
325
+ const instrumentId = await http.resolve(options.symbol, instrumentType, "quote");
326
+ const response = await http.request(
327
+ "GET",
328
+ `/v1/instruments/${encodeURIComponent(instrumentId)}/quote`,
329
+ );
330
+ process.stdout.write(`${dumps(response.data ?? {})}\n`);
331
+ return;
332
+ }
333
+ if (key === "stock bars" || key === "index bars") {
334
+ const options = parseOptions(arguments_, {
335
+ symbol: { type: "string" },
336
+ interval: { type: "choice", choices: INTERVALS, default: "1d" },
337
+ start: { type: "string" },
338
+ end: { type: "string" },
339
+ adjustment: { type: "choice", choices: ["none", "forward", "backward"], default: "none" },
340
+ ...dataDefinitions,
341
+ }, ["symbol"]);
342
+ const instrumentType = command === "stock" ? "equity" : "index";
343
+ const http = client(context, options);
344
+ const instrumentId = await http.resolve(options.symbol, instrumentType, "bars");
345
+ const response = await http.request(
346
+ "GET",
347
+ `/v1/instruments/${encodeURIComponent(instrumentId)}/bars?${queryString({
348
+ interval: options.interval,
349
+ start: options.start,
350
+ end: options.end,
351
+ adjustment: options.adjustment,
352
+ })}`,
353
+ );
354
+ emit(response.data ?? [], options);
355
+ return;
356
+ }
357
+ if (key === "index constituents") {
358
+ const options = parseOptions(arguments_, {
359
+ symbol: { type: "string" }, "as-of": { type: "string" }, ...dataDefinitions,
360
+ }, ["symbol"]);
361
+ const http = client(context, options);
362
+ const instrumentId = await http.resolve(options.symbol, "index", "constituents");
363
+ const suffix = options.asOf ? `?${queryString({ as_of: options.asOf })}` : "";
364
+ const response = await http.request(
365
+ "GET",
366
+ `/v1/indices/${encodeURIComponent(instrumentId)}/constituents${suffix}`,
367
+ );
368
+ emit(response.data ?? [], options);
369
+ return;
370
+ }
371
+ throw usage(`No such command '${subcommand || command}'.`);
372
+ }
373
+
374
+ function commandParts(remaining) {
375
+ const command = remaining[0];
376
+ if (!command) return {};
377
+ if (!["config", "health", "version", "instrument", "stock", "index"].includes(command)) {
378
+ throw usage(`No such command '${command}'.`);
379
+ }
380
+ if (["health", "version"].includes(command)) {
381
+ return { command, arguments_: remaining.slice(1) };
382
+ }
383
+ const subcommand = remaining[1];
384
+ if (!subcommand) return { command, groupMissing: true };
385
+ if (subcommand === "--help") return { command, groupHelp: true };
386
+ if (subcommand.startsWith("--")) throw usage(`No such option: ${subcommand}`);
387
+ const valid = {
388
+ config: ["set", "show"],
389
+ instrument: ["list", "search", "resolve", "show"],
390
+ stock: ["quotes", "bars"],
391
+ index: ["quotes", "bars", "constituents"],
392
+ }[command];
393
+ if (!valid.includes(subcommand)) throw usage(`No such command '${subcommand}'.`);
394
+ return { command, subcommand, arguments_: remaining.slice(2) };
395
+ }
396
+
397
+ async function main(arguments_, environment = process.env) {
398
+ const root = parseRoot(arguments_);
399
+ if (root.help) {
400
+ process.stdout.write(ROOT_HELP);
401
+ return;
402
+ }
403
+ if (!root.remaining.length) throw usage(ROOT_HELP.trimEnd());
404
+ const parts = commandParts(root.remaining);
405
+ if (parts.groupHelp) {
406
+ process.stdout.write(HELP[parts.command]);
407
+ return;
408
+ }
409
+ if (parts.groupMissing) throw usage(HELP[parts.command].trimEnd());
410
+ const context = contextFrom(root.options, environment);
411
+ await dispatch(context, parts.command, parts.subcommand, parts.arguments_);
412
+ }
413
+
414
+ module.exports = { DEFAULT_SERVER_URL, main, parseOptions };
package/lib/config.js ADDED
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { EvaError } = require("./errors.js");
7
+
8
+ function defaultConfigurationPath(environment = process.env) {
9
+ if (environment.EVA_CONFIG) return path.resolve(expandHome(environment.EVA_CONFIG));
10
+ const root = process.platform === "win32"
11
+ ? environment.APPDATA || path.join(os.homedir(), "AppData", "Roaming")
12
+ : environment.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
13
+ return path.join(root, "eva", "config.json");
14
+ }
15
+
16
+ function expandHome(value) {
17
+ if (value === "~") return os.homedir();
18
+ if (value.startsWith(`~${path.sep}`) || value.startsWith("~/")) {
19
+ return path.join(os.homedir(), value.slice(2));
20
+ }
21
+ return value;
22
+ }
23
+
24
+ function configurationError(message) {
25
+ return new EvaError("INVALID_CONFIGURATION", message);
26
+ }
27
+
28
+ function loadConfiguration(configurationPath) {
29
+ let stats;
30
+ try {
31
+ stats = fs.statSync(configurationPath);
32
+ } catch (error) {
33
+ if (error.code === "ENOENT") return {};
34
+ throw configurationError(`cannot read configuration file: ${configurationPath}`);
35
+ }
36
+ if (!stats.isFile()) {
37
+ throw configurationError(`cannot read configuration file: ${configurationPath}`);
38
+ }
39
+ if (process.platform !== "win32" && (stats.mode & 0o077) !== 0) {
40
+ throw configurationError(
41
+ `configuration file must be owner-only (use chmod 600): ${configurationPath}`,
42
+ );
43
+ }
44
+ let value;
45
+ try {
46
+ value = JSON.parse(fs.readFileSync(configurationPath, "utf8"));
47
+ } catch {
48
+ throw configurationError(`cannot read configuration file: ${configurationPath}`);
49
+ }
50
+ if (!value || Array.isArray(value) || typeof value !== "object" || value.schema !== "eva.cli-config.v1") {
51
+ throw configurationError("configuration schema must be eva.cli-config.v1");
52
+ }
53
+ const allowed = new Set(["schema", "base_url", "api_key"]);
54
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key)).sort();
55
+ if (unknown.length) {
56
+ throw configurationError(`configuration contains unknown field: ${unknown[0]}`);
57
+ }
58
+ if (value.base_url !== undefined && (
59
+ typeof value.base_url !== "string" ||
60
+ !(value.base_url.startsWith("http://") || value.base_url.startsWith("https://"))
61
+ )) {
62
+ throw configurationError("configuration.base_url must be an HTTP(S) URL");
63
+ }
64
+ if (value.api_key !== undefined && (
65
+ typeof value.api_key !== "string" || !value.api_key.trim()
66
+ )) {
67
+ throw configurationError("configuration.api_key must be a non-empty string");
68
+ }
69
+ return { baseUrl: value.base_url, apiKey: value.api_key };
70
+ }
71
+
72
+ function saveConfiguration(configurationPath, configuration) {
73
+ fs.mkdirSync(path.dirname(configurationPath), { recursive: true });
74
+ const value = { schema: "eva.cli-config.v1" };
75
+ if (configuration.baseUrl) value.base_url = configuration.baseUrl;
76
+ if (configuration.apiKey) value.api_key = configuration.apiKey;
77
+ const temporary = path.join(
78
+ path.dirname(configurationPath),
79
+ `.${path.basename(configurationPath)}.${process.pid}.${Date.now()}.tmp`,
80
+ );
81
+ try {
82
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, {
83
+ encoding: "utf8",
84
+ flag: "wx",
85
+ mode: 0o600,
86
+ });
87
+ fs.chmodSync(temporary, 0o600);
88
+ fs.renameSync(temporary, configurationPath);
89
+ } finally {
90
+ try { fs.unlinkSync(temporary); } catch (error) {
91
+ if (error.code !== "ENOENT") throw error;
92
+ }
93
+ }
94
+ }
95
+
96
+ module.exports = {
97
+ defaultConfigurationPath,
98
+ expandHome,
99
+ loadConfiguration,
100
+ saveConfiguration,
101
+ };
package/lib/errors.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ class EvaError extends Error {
4
+ constructor(code, message, retryable = false, exitCode = 1) {
5
+ super(message);
6
+ this.name = "EvaError";
7
+ this.evaProblem = { code, message, retryable, exitCode };
8
+ }
9
+ }
10
+
11
+ function usage(message) {
12
+ return new EvaError("INVALID_ARGUMENT", message, false, 2);
13
+ }
14
+
15
+ module.exports = { EvaError, usage };
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+
3
+ const { EvaError } = require("./errors.js");
4
+
5
+ function sleep(milliseconds) {
6
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
7
+ }
8
+
9
+ class EvaHttpClient {
10
+ constructor(baseUrl, timeout, retries, apiKey, version) {
11
+ if (!(baseUrl.startsWith("http://") || baseUrl.startsWith("https://"))) {
12
+ throw new EvaError(
13
+ "INVALID_SERVER_URL",
14
+ "server URL must start with http:// or https://",
15
+ );
16
+ }
17
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
18
+ this.timeout = timeout;
19
+ this.retries = retries;
20
+ this.apiKey = apiKey;
21
+ this.version = version;
22
+ }
23
+
24
+ async request(method, requestPath, payload) {
25
+ const started = performance.now();
26
+ for (let attempt = 0; attempt <= this.retries; attempt += 1) {
27
+ const remaining = this.timeout * 1000 - (performance.now() - started);
28
+ if (remaining <= 0) {
29
+ throw new EvaError(
30
+ "TIMEOUT",
31
+ `server call exceeded the ${this.timeout} second total timeout`,
32
+ true,
33
+ );
34
+ }
35
+ const controller = new AbortController();
36
+ const timer = setTimeout(() => controller.abort(), remaining);
37
+ try {
38
+ const body = payload === undefined ? undefined : JSON.stringify(payload);
39
+ const response = await fetch(`${this.baseUrl}${requestPath}`, {
40
+ method,
41
+ body,
42
+ signal: controller.signal,
43
+ headers: {
44
+ Accept: "application/json",
45
+ "User-Agent": `eva/${this.version}`,
46
+ ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
47
+ ...(body ? { "Content-Type": "application/json" } : {}),
48
+ },
49
+ });
50
+ const text = await response.text();
51
+ let parsed;
52
+ try { parsed = JSON.parse(text); } catch {
53
+ if (response.ok) {
54
+ throw new EvaError(
55
+ "INVALID_SERVER_RESPONSE",
56
+ "server returned invalid JSON",
57
+ );
58
+ }
59
+ parsed = {};
60
+ }
61
+ if (!response.ok) {
62
+ const retryable = Boolean(parsed.retryable) || [429, 502, 503, 504].includes(response.status);
63
+ if (retryable && attempt < this.retries) {
64
+ await sleep(Math.min(250 * (2 ** attempt), remaining));
65
+ continue;
66
+ }
67
+ throw new EvaError(
68
+ String(parsed.code || "HTTP_ERROR"),
69
+ String(parsed.detail || `server returned HTTP ${response.status}`),
70
+ retryable,
71
+ );
72
+ }
73
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
74
+ throw new EvaError(
75
+ "INVALID_SERVER_RESPONSE",
76
+ "server returned a non-object JSON response",
77
+ );
78
+ }
79
+ return parsed;
80
+ } catch (error) {
81
+ if (error && error.evaProblem) throw error;
82
+ if (attempt < this.retries) {
83
+ await sleep(Math.min(250 * (2 ** attempt), remaining));
84
+ continue;
85
+ }
86
+ throw new EvaError("SERVER_UNAVAILABLE", "EVA service is unavailable", true);
87
+ } finally {
88
+ clearTimeout(timer);
89
+ }
90
+ }
91
+ throw new Error("HTTP retry loop exhausted");
92
+ }
93
+
94
+ async resolve(query, instrumentType, capability) {
95
+ const response = await this.request("POST", "/v1/instrument-resolve", {
96
+ query,
97
+ context: { instrument_type: instrumentType, capability },
98
+ });
99
+ const data = response.data || {};
100
+ const instrumentId = data.instrument && data.instrument.instrument_id;
101
+ if (data.status === "resolved" && typeof instrumentId === "string") return instrumentId;
102
+ throw new EvaError(
103
+ data.status === "ambiguous" ? "AMBIGUOUS_INSTRUMENT" : "INSTRUMENT_NOT_FOUND",
104
+ "instrument input did not resolve to one supported instrument",
105
+ );
106
+ }
107
+ }
108
+
109
+ module.exports = { EvaHttpClient };
package/lib/output.js ADDED
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { EvaError, usage } = require("./errors.js");
6
+
7
+ function serializationError(code, message) {
8
+ return new EvaError(code, message);
9
+ }
10
+
11
+ function applyLimit(value, limit) {
12
+ if (limit === undefined) return value;
13
+ if (!Array.isArray(value)) {
14
+ throw serializationError("LIMIT_NOT_APPLICABLE", "--limit requires a record sequence result");
15
+ }
16
+ return value.slice(0, limit);
17
+ }
18
+
19
+ function dumps(value, { limit, recordBudget = 200 } = {}) {
20
+ const normalized = applyLimit(value, limit);
21
+ if (recordBudget !== null && Array.isArray(normalized) && normalized.length > recordBudget) {
22
+ throw serializationError(
23
+ "RESULT_TOO_LARGE",
24
+ `result has ${normalized.length} records; use --limit or --output`,
25
+ );
26
+ }
27
+ return JSON.stringify(normalized);
28
+ }
29
+
30
+ function validateTarget(target, overwrite) {
31
+ let parent;
32
+ try { parent = fs.statSync(path.dirname(target)); } catch { parent = null; }
33
+ if (!parent || !parent.isDirectory()) {
34
+ throw serializationError("OUTPUT_PARENT_MISSING", "output parent directory does not exist");
35
+ }
36
+ let stats;
37
+ try { stats = fs.lstatSync(target); } catch (error) {
38
+ if (error.code === "ENOENT") return;
39
+ throw error;
40
+ }
41
+ if (!stats.isFile()) {
42
+ throw serializationError("OUTPUT_PATH_INVALID", "output path must be a regular file");
43
+ }
44
+ if (!overwrite) throw serializationError("OUTPUT_EXISTS", "output path already exists");
45
+ }
46
+
47
+ function atomicWrite(target, content, overwrite) {
48
+ validateTarget(target, overwrite);
49
+ const temporary = path.join(
50
+ path.dirname(target),
51
+ `.${path.basename(target)}.${process.pid}.${Date.now()}.tmp`,
52
+ );
53
+ let descriptor;
54
+ try {
55
+ descriptor = fs.openSync(temporary, "wx", 0o600);
56
+ fs.writeFileSync(descriptor, content, "utf8");
57
+ fs.fsyncSync(descriptor);
58
+ fs.closeSync(descriptor);
59
+ descriptor = undefined;
60
+ if (overwrite) fs.renameSync(temporary, target);
61
+ else {
62
+ try { fs.linkSync(temporary, target); } catch (error) {
63
+ if (error.code === "EEXIST") {
64
+ throw serializationError("OUTPUT_EXISTS", "output path already exists");
65
+ }
66
+ throw error;
67
+ }
68
+ fs.unlinkSync(temporary);
69
+ }
70
+ } finally {
71
+ if (descriptor !== undefined) fs.closeSync(descriptor);
72
+ try { fs.unlinkSync(temporary); } catch (error) {
73
+ if (error.code !== "ENOENT") throw error;
74
+ }
75
+ }
76
+ }
77
+
78
+ function csvCell(value) {
79
+ let text;
80
+ if (value === null || value === undefined) text = "";
81
+ else if (typeof value === "boolean") text = value ? "True" : "False";
82
+ else text = String(value);
83
+ if (/[",\r\n]/.test(text)) return `"${text.replaceAll('"', '""')}"`;
84
+ return text;
85
+ }
86
+
87
+ function recordsForFormat(value, format) {
88
+ if (!Array.isArray(value) || !value.every((record) => record && !Array.isArray(record) && typeof record === "object")) {
89
+ throw serializationError(
90
+ "INCOMPATIBLE_OUTPUT_FORMAT",
91
+ `${format} requires a sequence of record objects`,
92
+ );
93
+ }
94
+ return value;
95
+ }
96
+
97
+ function csvContent(value) {
98
+ const records = recordsForFormat(value, "csv");
99
+ if (!records.length) return "";
100
+ const fields = Object.keys(records[0]);
101
+ const expected = new Set(fields);
102
+ for (const record of records) {
103
+ const keys = Object.keys(record);
104
+ if (
105
+ keys.length !== fields.length ||
106
+ keys.some((key) => !expected.has(key)) ||
107
+ Object.values(record).some((item) => item !== null && typeof item === "object")
108
+ ) {
109
+ throw serializationError(
110
+ "INCOMPATIBLE_OUTPUT_FORMAT",
111
+ "csv requires consistent flat record fields",
112
+ );
113
+ }
114
+ }
115
+ const lines = [fields.map(csvCell).join(",")];
116
+ for (const record of records) lines.push(fields.map((field) => csvCell(record[field])).join(","));
117
+ return `${lines.join("\n")}\n`;
118
+ }
119
+
120
+ function exportResult(value, { output, format, overwrite, limit }) {
121
+ const target = path.resolve(output);
122
+ const extension = path.extname(target).slice(1).toLowerCase();
123
+ const known = new Set(["csv", "json", "jsonl", "parquet"]);
124
+ if (format && known.has(extension) && extension !== format) {
125
+ throw serializationError(
126
+ "OUTPUT_FORMAT_MISMATCH",
127
+ "explicit output format conflicts with the file extension",
128
+ );
129
+ }
130
+ const selected = format || extension;
131
+ if (!known.has(selected)) {
132
+ throw serializationError("UNSUPPORTED_OUTPUT_FORMAT", "output format is not supported");
133
+ }
134
+ const normalized = applyLimit(value, limit);
135
+ let content;
136
+ if (selected === "parquet") {
137
+ recordsForFormat(normalized, "parquet");
138
+ throw serializationError(
139
+ "PARQUET_UNAVAILABLE",
140
+ "Parquet output requires the evatick[parquet] extra",
141
+ );
142
+ } else if (selected === "csv") content = csvContent(normalized);
143
+ else if (selected === "jsonl") {
144
+ content = recordsForFormat(normalized, "jsonl").map((record) => JSON.stringify(record)).join("\n");
145
+ if (content) content += "\n";
146
+ } else content = `${JSON.stringify(normalized)}\n`;
147
+ atomicWrite(target, content, overwrite);
148
+ return { path: target, records: Array.isArray(normalized) ? normalized.length : 1 };
149
+ }
150
+
151
+ function emit(value, options = {}) {
152
+ if (!options.output) {
153
+ if (options.format) throw usage("--format requires --output");
154
+ process.stdout.write(`${dumps(value, { limit: options.limit })}\n`);
155
+ return;
156
+ }
157
+ process.stdout.write(`${dumps(exportResult(value, options))}\n`);
158
+ }
159
+
160
+ module.exports = { dumps, emit, exportResult };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@evatick/cli",
3
- "version": "0.3.2",
4
- "description": "EVA market data command-line client for LLMs, agents, and automation",
3
+ "version": "0.4.1",
4
+ "description": "EVA Tick CLI for deterministic, machine-readable financial data queries by AI agents, LLMs, and automation",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -17,16 +17,25 @@
17
17
  },
18
18
  "files": [
19
19
  "bin",
20
+ "lib",
20
21
  "README.md",
21
22
  "LICENSE"
22
23
  ],
23
24
  "engines": {
24
25
  "node": ">=18"
25
26
  },
26
- "optionalDependencies": {
27
- "@evatick/cli-darwin-arm64": "0.3.2",
28
- "@evatick/cli-darwin-x64": "0.3.2",
29
- "@evatick/cli-linux-x64": "0.3.2",
30
- "@evatick/cli-win32-x64": "0.3.2"
31
- }
27
+ "keywords": [
28
+ "eva-tick",
29
+ "financial-data",
30
+ "market-data",
31
+ "ai-agent",
32
+ "llm",
33
+ "codex",
34
+ "automation",
35
+ "quantitative-finance",
36
+ "openapi",
37
+ "akshare",
38
+ "a-share",
39
+ "china-stock-market"
40
+ ]
32
41
  }