ultrastiching-cli 0.0.0-stage → 0.1.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.
@@ -0,0 +1,124 @@
1
+ {
2
+ "datasets": {
3
+ "a_share_daily": {
4
+ "endpoint": "plasma_daily",
5
+ "keys": [
6
+ "code",
7
+ "trade_date"
8
+ ],
9
+ "kwargs": {}
10
+ },
11
+ "adj_factor": {
12
+ "endpoint": "plasma_adj_factor",
13
+ "keys": [
14
+ "code",
15
+ "trade_date"
16
+ ],
17
+ "kwargs": {}
18
+ },
19
+ "bond_daily": {
20
+ "endpoint": "plasma_bond_daily",
21
+ "keys": [
22
+ "code",
23
+ "trade_date"
24
+ ],
25
+ "kwargs": {}
26
+ },
27
+ "bond_dailyprice": {
28
+ "endpoint": "get_bond_dailyprice",
29
+ "keys": [
30
+ "code",
31
+ "trade_date"
32
+ ],
33
+ "kwargs": {
34
+ "count_type": "1",
35
+ "fields": []
36
+ }
37
+ },
38
+ "cb_daily": {
39
+ "endpoint": "plasma_cb_daily",
40
+ "keys": [
41
+ "code",
42
+ "trade_date"
43
+ ],
44
+ "kwargs": {}
45
+ },
46
+ "concept_index_daily": {
47
+ "endpoint": "plasma_concept_index_daily",
48
+ "keys": [
49
+ "concept_code",
50
+ "trade_date"
51
+ ],
52
+ "kwargs": {}
53
+ },
54
+ "fund_daily": {
55
+ "endpoint": "plasma_fund_daily",
56
+ "keys": [
57
+ "code",
58
+ "trade_date"
59
+ ],
60
+ "kwargs": {}
61
+ },
62
+ "fund_dailyprice": {
63
+ "endpoint": "get_fund_dailyprice",
64
+ "keys": [
65
+ "code",
66
+ "trade_date"
67
+ ],
68
+ "kwargs": {
69
+ "adjust_type": "N",
70
+ "count_type": "1",
71
+ "fields": []
72
+ }
73
+ },
74
+ "fut_daily": {
75
+ "endpoint": "plasma_fut_daily",
76
+ "keys": [
77
+ "code",
78
+ "trade_date"
79
+ ],
80
+ "kwargs": {}
81
+ },
82
+ "hk_daily": {
83
+ "endpoint": "plasma_hk_daily",
84
+ "keys": [
85
+ "code",
86
+ "trade_date"
87
+ ],
88
+ "kwargs": {}
89
+ },
90
+ "index_daily": {
91
+ "endpoint": "plasma_index_daily",
92
+ "keys": [
93
+ "code",
94
+ "trade_date"
95
+ ],
96
+ "kwargs": {}
97
+ },
98
+ "opt_daily": {
99
+ "endpoint": "plasma_opt_daily",
100
+ "keys": [
101
+ "code",
102
+ "trade_date"
103
+ ],
104
+ "kwargs": {}
105
+ },
106
+ "sge_daily": {
107
+ "endpoint": "plasma_sge_daily",
108
+ "keys": [
109
+ "code",
110
+ "trade_date"
111
+ ],
112
+ "kwargs": {}
113
+ },
114
+ "trade_cal": {
115
+ "endpoint": "plasma_trade_cal",
116
+ "keys": [
117
+ "exchange",
118
+ "calendar_date"
119
+ ],
120
+ "kwargs": {}
121
+ }
122
+ },
123
+ "schema_version": 1
124
+ }
package/src/args.js ADDED
@@ -0,0 +1,31 @@
1
+ export function parseArgs(argv) {
2
+ const flags = {};
3
+ const positional = [];
4
+ for (let i = 0; i < argv.length; i++) {
5
+ const arg = argv[i];
6
+ if (arg === '--') { positional.push(...argv.slice(i + 1)); break; }
7
+ if (!arg.startsWith('--') || arg === '--') { positional.push(arg); continue; }
8
+ const equal = arg.indexOf('=');
9
+ const name = arg.slice(2, equal < 0 ? undefined : equal).replaceAll('-', '_');
10
+ if (!name) throw new Error('Empty option name');
11
+ if (Object.hasOwn(flags, name)) throw new Error(`Duplicate option --${name}`);
12
+ if (equal >= 0) flags[name] = arg.slice(equal + 1);
13
+ else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) flags[name] = argv[++i];
14
+ else flags[name] = true;
15
+ }
16
+ return { flags, positional };
17
+ }
18
+
19
+ export function requireValue(flags, name) {
20
+ if (typeof flags[name] !== 'string' || !flags[name]) throw new Error(`--${name.replaceAll('_', '-')} requires a value`);
21
+ return flags[name];
22
+ }
23
+
24
+ export function jsonObject(raw, label = '--args') {
25
+ if (raw === undefined) return {};
26
+ if (typeof raw !== 'string') throw new Error(`${label} requires a JSON object`);
27
+ let value;
28
+ try { value = JSON.parse(raw); } catch { throw new Error(`${label} is not valid JSON`); }
29
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} requires a JSON object`);
30
+ return value;
31
+ }
package/src/config.js ADDED
@@ -0,0 +1,73 @@
1
+ import { closeSync, fstatSync, lstatSync, openSync, readFileSync, constants } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ export const DEFAULT_BASE_URL = 'https://api.ultrastiching.com';
6
+
7
+ function singleLine(label, value) {
8
+ if (typeof value !== 'string' || !value.trim() || value !== value.trim() || /[\r\n]/.test(value)) {
9
+ throw new Error(`${label} must be a non-empty single-line value without surrounding whitespace`);
10
+ }
11
+ return value;
12
+ }
13
+
14
+ export function normalizeOrigin(value, protocols = ['http:', 'https:']) {
15
+ const raw = singleLine('base URL', value);
16
+ let url;
17
+ try { url = new URL(raw); } catch { throw new Error('base URL must be an origin'); }
18
+ if (!protocols.includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
19
+ throw new Error(`base URL must be a ${protocols.join('/')} origin without path, query, or credentials`);
20
+ }
21
+ return url.origin;
22
+ }
23
+
24
+ export function resolveBaseUrl(explicit, env = process.env) {
25
+ return normalizeOrigin(explicit !== undefined ? explicit : env.MERIDIAN_BASE_URL !== undefined ? env.MERIDIAN_BASE_URL : DEFAULT_BASE_URL);
26
+ }
27
+
28
+ function credentialFile(home) {
29
+ if (process.platform === 'win32' || typeof process.getuid !== 'function' || constants.O_NOFOLLOW === undefined) {
30
+ throw new Error('Credential file fallback requires POSIX ownership and no-follow file access; use --key or MERIDIAN_API_KEY');
31
+ }
32
+ const dir = join(home, '.meridian');
33
+ let dirInfo;
34
+ try { dirInfo = lstatSync(dir); } catch (error) { if (error.code === 'ENOENT') return undefined; throw error; }
35
+ if (!dirInfo.isDirectory() || dirInfo.uid !== process.getuid() || (dirInfo.mode & 0o022)) {
36
+ throw new Error('~/.meridian must be an owned directory not writable by others');
37
+ }
38
+ let fd;
39
+ try {
40
+ fd = openSync(join(dir, 'credentials.env'), constants.O_RDONLY | constants.O_NOFOLLOW | (constants.O_NONBLOCK ?? 0));
41
+ } catch (error) { if (error.code === 'ENOENT') return undefined; throw error; }
42
+ try {
43
+ const info = fstatSync(fd);
44
+ if (!info.isFile() || info.uid !== process.getuid() || (info.mode & 0o077) || info.size > 65536) {
45
+ throw new Error('~/.meridian/credentials.env must be an owned regular file with mode 0600 or stricter, at most 64 KiB');
46
+ }
47
+ const content = readFileSync(fd, 'utf8');
48
+ let found;
49
+ for (const source of content.split(/\r?\n/)) {
50
+ let line = source.trim();
51
+ if (!line || line.startsWith('#')) continue;
52
+ if (line.startsWith('export ')) line = line.slice(7).trimStart();
53
+ const separator = line.indexOf('=');
54
+ const name = separator < 0 ? line : line.slice(0, separator).trim();
55
+ if (name !== 'MERIDIAN_API_KEY') continue;
56
+ if (separator < 0 || found !== undefined) throw new Error('Invalid or duplicate MERIDIAN_API_KEY in credentials.env');
57
+ let value = line.slice(separator + 1).trim();
58
+ if (value.startsWith('"') || value.startsWith("'")) {
59
+ if (value.length < 2 || value.at(-1) !== value[0]) throw new Error('Invalid quoted MERIDIAN_API_KEY in credentials.env');
60
+ value = value.slice(1, -1);
61
+ }
62
+ found = value;
63
+ }
64
+ return found;
65
+ } finally { closeSync(fd); }
66
+ }
67
+
68
+ export function resolveKey(explicit, { env = process.env, home = homedir(), optional = false } = {}) {
69
+ const key = explicit !== undefined ? explicit : Object.hasOwn(env, 'MERIDIAN_API_KEY') ? env.MERIDIAN_API_KEY : credentialFile(home);
70
+ if (key === undefined && optional) return undefined;
71
+ if (key === undefined) throw new Error('API Key required: use --key, MERIDIAN_API_KEY, or ~/.meridian/credentials.env');
72
+ return singleLine('API Key', key);
73
+ }
package/src/db-cli.js ADDED
@@ -0,0 +1,50 @@
1
+ import { jsonObject, requireValue } from './args.js';
2
+ import { callByName } from './http.js';
3
+ import { endpoint, parseParameter } from './registry.js';
4
+ import { datasets, MarketDB } from './marketdb.js';
5
+
6
+ const COMMON = new Set(['db', 'compact']);
7
+ function only(flags, allowed) {
8
+ const extra = Object.keys(flags).filter((name) => !allowed.has(name));
9
+ if (extra.length) throw new Error(`Unknown db option(s): ${extra.map((x) => `--${x.replaceAll('_', '-')}`).join(', ')}`);
10
+ }
11
+
12
+ export async function dbCommand(sub, flags, { fetchImpl } = {}) {
13
+ const path = flags.db === undefined ? undefined : requireValue(flags, 'db');
14
+ const requestOptions = { key: flags.key, baseUrl: flags.base_url, timeout: flags.timeout, fetchImpl };
15
+ const db = new MarketDB({ path, request: callByName, requestOptions });
16
+ if (sub === 'init') { only(flags, COMMON); return db.init(); }
17
+ if (sub === 'status') { only(flags, COMMON); return db.status(); }
18
+ if (sub === 'validate') { only(flags, COMMON); return db.validate(); }
19
+ if (sub === 'sync') {
20
+ const name = requireValue(flags, 'dataset');
21
+ const spec = datasets()[name];
22
+ if (!spec) throw new Error(`unknown dataset: ${name}`);
23
+ const meta = endpoint(spec.endpoint, 'http');
24
+ const values = jsonObject(flags.args);
25
+ const extra = new Set(['dataset', 'args', 'key', 'base_url', 'timeout', 'start', 'end', ...meta.parameters, ...COMMON]);
26
+ only(flags, extra);
27
+ if (flags.start !== undefined) values.start_date = requireValue(flags, 'start');
28
+ if (flags.end !== undefined) values.end_date = requireValue(flags, 'end');
29
+ for (const name of meta.parameters) {
30
+ if (!Object.hasOwn(flags, name)) continue;
31
+ const field = meta.request?.fields?.find((item) => item.name === name) ?? { name };
32
+ values[name] = parseParameter(requireValue(flags, name), field);
33
+ }
34
+ return db.sync(name, values);
35
+ }
36
+ if (sub === 'repair') {
37
+ only(flags, new Set([...COMMON, 'dataset', 'key', 'base_url', 'timeout']));
38
+ return db.repair(flags.dataset === undefined ? undefined : requireValue(flags, 'dataset'));
39
+ }
40
+ if (sub === 'sql') {
41
+ only(flags, new Set([...COMMON, 'query', 'limit']));
42
+ const limit = flags.limit === undefined ? undefined : Number(requireValue(flags, 'limit'));
43
+ return db.sql(requireValue(flags, 'query'), limit);
44
+ }
45
+ if (sub === 'export') {
46
+ only(flags, new Set([...COMMON, 'table', 'path', 'format']));
47
+ return db.export(requireValue(flags, 'table'), requireValue(flags, 'path'), flags.format === undefined ? undefined : requireValue(flags, 'format'));
48
+ }
49
+ throw new Error('db subcommands: init, sync, status, validate, repair, sql, export');
50
+ }
package/src/http.js ADDED
@@ -0,0 +1,129 @@
1
+ import { endpoint } from './registry.js';
2
+ import { normalizeOrigin, resolveBaseUrl, resolveKey } from './config.js';
3
+
4
+ export function redactKey(value, key) {
5
+ if (typeof value !== 'string' || !key) return value;
6
+ return value.replaceAll(key, '[REDACTED_API_KEY]');
7
+ }
8
+
9
+ function filteredHeaders(headers, key) {
10
+ const privateNames = new Set(['authorization', 'proxy-authorization', 'set-cookie', 'cookie', 'x-api-key']);
11
+ return Object.fromEntries(Object.entries(headers ?? {}).map(([name, value]) => [name,
12
+ privateNames.has(name.toLowerCase()) ? '[REDACTED]' : redactKey(value, key)]));
13
+ }
14
+
15
+ export class MeridianHttpError extends Error {
16
+ constructor({ status, statusText, errorCode, requestId, body, headers, url, key }) {
17
+ super(`UltraStiching API returned HTTP ${status}${errorCode ? ` (${errorCode})` : ''}`);
18
+ this.name = 'MeridianHttpError';
19
+ this.status = status;
20
+ this.statusText = statusText;
21
+ this.errorCode = errorCode;
22
+ this.requestId = requestId;
23
+ this.body = body; // Original body is preserved for programmatic callers.
24
+ this.headers = headers;
25
+ this.url = url;
26
+ this.key = key;
27
+ }
28
+ toDiagnostic() {
29
+ return {
30
+ type: this.name, status: this.status, status_text: redactKey(this.statusText, this.key),
31
+ error: redactKey(this.errorCode, this.key), request_id: redactKey(this.requestId, this.key),
32
+ raw_body: redactKey(this.body, this.key),
33
+ response: { status_code: this.status, reason: redactKey(this.statusText, this.key),
34
+ url: redactKey(this.url, this.key), headers: filteredHeaders(this.headers, this.key),
35
+ body: redactKey(this.body, this.key) },
36
+ };
37
+ }
38
+ }
39
+
40
+ export class MeridianTransportError extends Error {
41
+ constructor(cause, key) {
42
+ super('UltraStiching request could not be completed', { cause });
43
+ this.name = 'MeridianTransportError';
44
+ this.key = key;
45
+ }
46
+ toDiagnostic() {
47
+ return { type: this.name, message: this.message, cause: {
48
+ type: this.cause?.name ?? 'Error', message: redactKey(String(this.cause), this.key),
49
+ code: this.cause?.code, errno: this.cause?.errno,
50
+ } };
51
+ }
52
+ }
53
+
54
+ export class MeridianResponseError extends Error {
55
+ constructor({ status, requestId, body, key, cause }) {
56
+ super('UltraStiching API returned a successful response with invalid JSON', { cause });
57
+ this.name = 'MeridianResponseError';
58
+ this.status = status;
59
+ this.requestId = requestId;
60
+ this.body = body;
61
+ this.key = key;
62
+ }
63
+ toDiagnostic() {
64
+ return { type: this.name, status: this.status, request_id: redactKey(this.requestId, this.key), raw_body: redactKey(this.body, this.key) };
65
+ }
66
+ }
67
+
68
+ export async function requestEndpoint(meta, payload, { key, baseUrl, timeout = 60000, fetchImpl = fetch } = {}) {
69
+ if (!meta || meta.protocol !== 'http' || !meta.path?.startsWith('/') || meta.path.startsWith('//')) {
70
+ throw new Error('A known HTTP registry endpoint is required');
71
+ }
72
+ if (meta.response?.content_type === 'text/event-stream') {
73
+ throw new Error(`${meta.name} is SSE; synchronous call is unsupported. Read its contract with ultrastiching info ${meta.name}`);
74
+ }
75
+ const apiKey = resolveKey(key);
76
+ const origin = baseUrl === undefined ? resolveBaseUrl() : normalizeOrigin(baseUrl);
77
+ const ms = Number(timeout);
78
+ if (!Number.isFinite(ms) || ms <= 0) throw new Error('timeout must be a positive number of milliseconds');
79
+ let path = meta.path;
80
+ const query = new URLSearchParams();
81
+ if (meta.method === 'GET') {
82
+ for (const parameter of meta.request.parameters) {
83
+ const value = payload[parameter.name];
84
+ if (value === undefined) continue;
85
+ if (parameter.in === 'path') {
86
+ if (!Number.isSafeInteger(value) || value < 1) throw new Error(`${parameter.name} must be a positive integer`);
87
+ path = path.replace(`{${parameter.name}}`, encodeURIComponent(String(value)));
88
+ } else {
89
+ if (value === null) throw new Error(`${parameter.name} cannot be null in a GET query`);
90
+ query.set(parameter.name, String(value));
91
+ }
92
+ }
93
+ }
94
+ const url = `${origin}${path}${query.size ? `?${query}` : ''}`;
95
+ let response;
96
+ try {
97
+ response = await fetchImpl(url, {
98
+ method: meta.method,
99
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': meta.request?.content_type ?? 'application/json' },
100
+ ...(meta.method === 'GET' ? {} : { body: JSON.stringify(payload) }),
101
+ redirect: 'manual',
102
+ signal: AbortSignal.timeout(ms),
103
+ });
104
+ } catch (cause) { throw new MeridianTransportError(cause, apiKey); }
105
+ let buffer;
106
+ try { buffer = Buffer.from(await response.arrayBuffer()); }
107
+ catch (cause) { throw new MeridianTransportError(cause, apiKey); }
108
+ const requestId = response.headers.get('x-meridian-request-id');
109
+ if (!response.ok) {
110
+ const body = buffer.toString('utf8');
111
+ let errorCode;
112
+ try {
113
+ const parsed = JSON.parse(body);
114
+ errorCode = typeof parsed.error === 'string' ? parsed.error : parsed.error?.code;
115
+ } catch { /* raw body is retained */ }
116
+ throw new MeridianHttpError({ status: response.status, statusText: response.statusText, errorCode, requestId, body,
117
+ headers: Object.fromEntries(response.headers), url: response.url || url, key: apiKey });
118
+ }
119
+ const contentType = response.headers.get('content-type')?.split(';', 1)[0].trim().toLowerCase();
120
+ if (contentType === 'application/vnd.apache.arrow.stream') return { binary: true, data: buffer, requestId };
121
+ try { return { binary: false, data: JSON.parse(buffer.toString('utf8')), requestId }; }
122
+ catch (cause) { throw new MeridianResponseError({ status: response.status, requestId, body: buffer.toString('utf8'), key: apiKey, cause }); }
123
+ }
124
+
125
+ export async function callByName(name, payload, options) {
126
+ const meta = endpoint(name, 'http');
127
+ if (!meta) throw new Error(`Unknown HTTP endpoint ${name}; run ultrastiching list --search ${name}`);
128
+ return requestEndpoint(meta, payload, options);
129
+ }
package/src/main.js ADDED
@@ -0,0 +1,174 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { parseArgs, jsonObject, requireValue } from './args.js';
4
+ import { resolveBaseUrl, resolveKey } from './config.js';
5
+ import { callByName, redactKey } from './http.js';
6
+ import { buildPayload, endpoint, loadRegistry, parseParameter, searchEndpoints } from './registry.js';
7
+ import { streamNewsflash } from './websocket.js';
8
+
9
+ const HELP = `ultrastiching — UltraStiching data API CLI
10
+
11
+ ultrastiching doctor [--probe]
12
+ ultrastiching list [--search text] [--binary] [--verbose]
13
+ ultrastiching info <endpoint>
14
+ ultrastiching call <endpoint> [--parameter value ...] [--args JSON] [--out file]
15
+ ultrastiching arrow <get_* handler> --args '{"params":{...},"fields":[...]}' [--out file]
16
+ ultrastiching ws newsflash [--limit n] [--keyword text] [--count n]
17
+ ultrastiching datasets
18
+ ultrastiching db init|sync|status|validate|repair|sql|export [options]
19
+
20
+ Global: --key, --base-url, --timeout (milliseconds), --compact
21
+ `;
22
+
23
+ function emit(write, value, compact, key) {
24
+ write(`${redactKey(JSON.stringify(value, null, compact ? 0 : 2), key)}\n`);
25
+ }
26
+
27
+ function assertFlags(flags, allowed) {
28
+ const extra = Object.keys(flags).filter((name) => !allowed.has(name));
29
+ if (extra.length) throw new Error(`Unknown option(s): ${extra.map((name) => `--${name.replaceAll('_', '-')}`).join(', ')}`);
30
+ }
31
+
32
+ function valuesFor(meta, flags) {
33
+ const values = jsonObject(flags.args);
34
+ const names = new Set(meta.parameters);
35
+ const allowed = new Set([...names, ...Object.keys(meta.payload), 'args', 'out', 'key', 'base_url', 'timeout', 'compact']);
36
+ assertFlags(flags, allowed);
37
+ for (const name of names) {
38
+ const wireName = Object.entries(meta.payload).find(([, value]) => value?.$param === name)?.[0] ?? name;
39
+ const flagName = Object.hasOwn(flags, name) ? name : wireName;
40
+ if (!Object.hasOwn(flags, flagName)) continue;
41
+ const field = meta.request?.fields?.find((item) => item.name === wireName) ?? { name };
42
+ values[name] = parseParameter(requireValue(flags, flagName), field);
43
+ }
44
+ return values;
45
+ }
46
+
47
+ function outputResult(name, result, flags, write, key) {
48
+ if (result.binary) {
49
+ const target = resolve(flags.out ?? `meridian-out/${name}-${Date.now()}.arrow`);
50
+ mkdirSync(dirname(target), { recursive: true });
51
+ writeFileSync(target, result.data);
52
+ emit(write, { path: target, bytes: result.data.length, format: 'arrow_ipc', request_id: result.requestId }, flags.compact, key);
53
+ } else if (flags.out || rowCount(result.data) > 500) {
54
+ const target = resolve(flags.out ? requireValue(flags, 'out') : `meridian-out/${name}-${Date.now()}.json`);
55
+ mkdirSync(dirname(target), { recursive: true });
56
+ writeFileSync(target, `${JSON.stringify(result.data, null, flags.compact ? 0 : 2)}\n`);
57
+ emit(write, { path: target, format: 'json', request_id: result.requestId }, flags.compact, key);
58
+ } else emit(write, result.data, flags.compact, key);
59
+ }
60
+
61
+ function rowCount(data) {
62
+ if (Array.isArray(data)) return data.length;
63
+ if (!data || typeof data !== 'object') return 0;
64
+ if (Array.isArray(data.data_frame)) return data.data_frame.length;
65
+ if (Array.isArray(data.data)) return data.data.length;
66
+ if (Array.isArray(data.items)) return data.items.length;
67
+ return 0;
68
+ }
69
+
70
+ export async function main(argv, { write = (line) => process.stdout.write(line), fetchImpl, socketFactory } = {}) {
71
+ const { flags, positional } = parseArgs(argv);
72
+ const [command, name] = positional;
73
+ if (!command || flags.help) { write(HELP); return; }
74
+ if (command === 'list') {
75
+ assertFlags(flags, new Set(['search', 'binary', 'verbose', 'compact']));
76
+ const items = searchEndpoints({ search: flags.search === undefined ? '' : requireValue(flags, 'search'), binary: !!flags.binary });
77
+ emit(write, { total: items.length, endpoints: items.map((item) => flags.verbose ? {
78
+ name: item.name, title: item.title, protocol: item.protocol, path: item.path,
79
+ response_format: item.response?.return_format,
80
+ } : { name: item.name, title: item.title }) }, flags.compact);
81
+ return;
82
+ }
83
+ if (command === 'info') {
84
+ assertFlags(flags, new Set(['compact']));
85
+ const meta = endpoint(name);
86
+ if (!meta) throw new Error(`Unknown endpoint ${name}; run ultrastiching list --search ${name}`);
87
+ emit(write, meta, flags.compact);
88
+ return;
89
+ }
90
+ if (command === 'doctor') {
91
+ assertFlags(flags, new Set(['key', 'base_url', 'timeout', 'probe', 'compact']));
92
+ const registry = loadRegistry();
93
+ const key = resolveKey(flags.key, { optional: true });
94
+ const result = {
95
+ node: process.version, base_url: resolveBaseUrl(flags.base_url),
96
+ api_key_configured: !!key, registry: registry.protocol_counts,
97
+ duckdb: { available: false }, probe: 'not_requested',
98
+ };
99
+ try {
100
+ const duckdb = await import('@duckdb/node-api');
101
+ result.duckdb = { available: true, version: duckdb.default.version() };
102
+ } catch (error) {
103
+ result.duckdb = { available: false, error: error.message };
104
+ }
105
+ if (flags.probe) {
106
+ if (!key) throw new Error('doctor --probe requires an API Key');
107
+ const meta = endpoint('get_s_stockname', 'http');
108
+ const payload = buildPayload(meta, { code: ['600745.SH'] });
109
+ const response = await callByName(meta.name, payload, { key, baseUrl: flags.base_url, timeout: flags.timeout, fetchImpl });
110
+ result.probe = { status: 'ok', request_id: response.requestId };
111
+ }
112
+ emit(write, result, flags.compact, key);
113
+ return;
114
+ }
115
+ if (command === 'call') {
116
+ const meta = endpoint(name, 'http');
117
+ if (!meta) throw new Error(`Unknown HTTP endpoint ${name}; run ultrastiching list --search ${name}`);
118
+ const values = valuesFor(meta, flags);
119
+ const key = resolveKey(flags.key);
120
+ const response = await callByName(name, buildPayload(meta, values), {
121
+ key, baseUrl: flags.base_url, timeout: flags.timeout, fetchImpl,
122
+ });
123
+ outputResult(name, response, flags, write, key);
124
+ return;
125
+ }
126
+ if (command === 'arrow') {
127
+ assertFlags(flags, new Set(['args', 'out', 'key', 'base_url', 'timeout', 'compact']));
128
+ const handler = endpoint(name, 'http');
129
+ if (!handler || !name.startsWith('get_')) throw new Error('arrow requires a registered get_* handler; use call for native Arrow endpoints');
130
+ const args = jsonObject(flags.args);
131
+ const meta = endpoint('arrow', 'http');
132
+ const payload = buildPayload(meta, { handler: name, ...args });
133
+ const key = resolveKey(flags.key);
134
+ const response = await callByName('arrow', payload, {
135
+ key, baseUrl: flags.base_url, timeout: flags.timeout, fetchImpl,
136
+ });
137
+ if (!response.binary) throw new Error(`arrow returned ${typeof response.data}, expected Arrow IPC stream`);
138
+ outputResult(name, response, flags, write, key);
139
+ return;
140
+ }
141
+ if (command === 'ws') {
142
+ if (name !== 'newsflash') throw new Error('ws currently supports newsflash');
143
+ assertFlags(flags, new Set(['cursor', 'limit', 'keyword', 'min_importance', 'count', 'idle_timeout', 'key', 'ws_base_url', 'compact']));
144
+ const meta = endpoint('ws_newsflash', 'websocket');
145
+ const values = {};
146
+ for (const param of meta.parameters) {
147
+ if (!Object.hasOwn(flags, param)) continue;
148
+ const field = meta.request?.fields?.find((item) => item.name === param) ?? { name: param };
149
+ values[param] = parseParameter(requireValue(flags, param), field);
150
+ }
151
+ const key = resolveKey(flags.key);
152
+ const result = await streamNewsflash(values, {
153
+ key, websocketBaseUrl: flags.ws_base_url, count: flags.count,
154
+ idleTimeout: flags.idle_timeout, socketFactory, onMessage: (line) => write(`${redactKey(line, key)}\n`),
155
+ });
156
+ emit(write, result, flags.compact, key);
157
+ return;
158
+ }
159
+ if (command === 'datasets') {
160
+ assertFlags(flags, new Set(['compact']));
161
+ const { datasets } = await import('./marketdb.js');
162
+ emit(write, { datasets: datasets() }, flags.compact);
163
+ return;
164
+ }
165
+ if (command === 'db') {
166
+ const { dbCommand } = await import('./db-cli.js');
167
+ const result = await dbCommand(name, flags, { fetchImpl });
168
+ const key = flags.key === undefined ? undefined : resolveKey(flags.key);
169
+ emit(write, result, flags.compact, key);
170
+ return result.status === 'incomplete' || result.ok === false || result.repaired?.some((item) => item.status === 'incomplete')
171
+ ? { exitCode: 2 } : undefined;
172
+ }
173
+ throw new Error(`Unknown command ${command}; run ultrastiching --help`);
174
+ }