xapi-to 0.1.18 → 0.1.19
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/LICENSE +21 -0
- package/README.md +51 -10
- package/dist/index.js +384 -46
- package/package.json +9 -2
- package/skills/xapi/SKILL.md +485 -0
- package/skills/xapi/guides/ai.md +200 -0
- package/skills/xapi/guides/ai_gateway.md +263 -0
- package/skills/xapi/guides/crypto.md +197 -0
- package/skills/xapi/guides/douyin.md +297 -0
- package/skills/xapi/guides/google_search.md +194 -0
- package/skills/xapi/guides/linkedin.md +198 -0
- package/skills/xapi/guides/reddit.md +312 -0
- package/skills/xapi/guides/sms.md +186 -0
- package/skills/xapi/guides/tiktok.md +322 -0
- package/skills/xapi/guides/twitter.md +276 -0
- package/skills/xapi/guides/weibo.md +301 -0
- package/skills/xapi/guides/ws_gateway.md +206 -0
- package/skills/xapi/guides/xiaohongshu.md +315 -0
- package/skills/xapi/scripts/download_tweet_videos.sh +125 -0
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/config.ts
|
|
9
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
|
|
10
10
|
|
|
11
11
|
// src/format.ts
|
|
12
12
|
function getFormat() {
|
|
@@ -143,11 +143,18 @@ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
|
143
143
|
function loadFileConfig() {
|
|
144
144
|
if (!existsSync(CONFIG_FILE)) return {};
|
|
145
145
|
try {
|
|
146
|
-
|
|
146
|
+
const parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
147
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
148
|
+
return typeof parsed.apiKey === "string" && parsed.apiKey.trim() ? { apiKey: parsed.apiKey } : {};
|
|
147
149
|
} catch {
|
|
148
150
|
return {};
|
|
149
151
|
}
|
|
150
152
|
}
|
|
153
|
+
function getApiKeySource() {
|
|
154
|
+
if (process.env.XAPI_KEY) return "XAPI_KEY";
|
|
155
|
+
if (process.env.XAPI_API_KEY) return "XAPI_API_KEY";
|
|
156
|
+
return loadFileConfig().apiKey ? "file" : "none";
|
|
157
|
+
}
|
|
151
158
|
function getConfig() {
|
|
152
159
|
const file = loadFileConfig();
|
|
153
160
|
return {
|
|
@@ -164,28 +171,31 @@ function saveConfig(updates) {
|
|
|
164
171
|
const current = loadFileConfig();
|
|
165
172
|
const merged = { ...current, ...updates };
|
|
166
173
|
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
174
|
+
if (process.platform !== "win32") chmodSync(CONFIG_DIR, 448);
|
|
167
175
|
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 384 });
|
|
176
|
+
if (process.platform !== "win32") chmodSync(CONFIG_FILE, 384);
|
|
168
177
|
}
|
|
169
178
|
function showConfig() {
|
|
170
179
|
const cfg = getConfig();
|
|
171
|
-
|
|
172
|
-
console.log(JSON.stringify({
|
|
180
|
+
return {
|
|
173
181
|
actionHost: cfg.actionHost,
|
|
174
182
|
apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : void 0,
|
|
175
183
|
source: {
|
|
176
|
-
apiKey:
|
|
184
|
+
apiKey: getApiKeySource()
|
|
177
185
|
},
|
|
178
186
|
configFile: CONFIG_FILE
|
|
179
|
-
}
|
|
187
|
+
};
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
// src/client.ts
|
|
183
191
|
import { open, rm } from "fs/promises";
|
|
192
|
+
import { once } from "events";
|
|
184
193
|
import { resolve } from "path";
|
|
185
194
|
import { Readable, Transform } from "stream";
|
|
186
195
|
import { pipeline } from "stream/promises";
|
|
187
196
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
188
197
|
var EXECUTE_TIMEOUT_MS = 6e4;
|
|
198
|
+
var TRANSFER_IDLE_TIMEOUT_MS = 6e4;
|
|
189
199
|
var IDEMPOTENT_RETRIES = 2;
|
|
190
200
|
var RETRY_BASE_DELAY_MS = 500;
|
|
191
201
|
var RETRY_MAX_DELAY_MS = 8e3;
|
|
@@ -225,6 +235,10 @@ function retryBaseDelayMs() {
|
|
|
225
235
|
const override = Number(process.env.XAPI_RETRY_BASE_MS);
|
|
226
236
|
return Number.isFinite(override) && override > 0 ? override : RETRY_BASE_DELAY_MS;
|
|
227
237
|
}
|
|
238
|
+
function transferIdleTimeoutMs() {
|
|
239
|
+
const override = Number(process.env.XAPI_TRANSFER_IDLE_TIMEOUT_MS);
|
|
240
|
+
return Number.isFinite(override) && override > 0 ? override : TRANSFER_IDLE_TIMEOUT_MS;
|
|
241
|
+
}
|
|
228
242
|
function backoffDelayMs(attempt, retryAfterMs) {
|
|
229
243
|
if (retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
|
|
230
244
|
return Math.min(retryAfterMs, RETRY_MAX_DELAY_MS);
|
|
@@ -296,7 +310,13 @@ async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0
|
|
|
296
310
|
return body;
|
|
297
311
|
} catch (e) {
|
|
298
312
|
if (timedOut) {
|
|
299
|
-
|
|
313
|
+
const timeoutError = new RequestTimeoutError(timeoutMs);
|
|
314
|
+
if (attempt < retries) {
|
|
315
|
+
await sleep(backoffDelayMs(attempt));
|
|
316
|
+
attempt++;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
throw timeoutError;
|
|
300
320
|
}
|
|
301
321
|
if (isRetryableNetworkError(e) && attempt < retries) {
|
|
302
322
|
clearTimeout(timer);
|
|
@@ -339,6 +359,8 @@ async function actionSearch(query, opts, params = {}) {
|
|
|
339
359
|
if (params.source) url.searchParams.set("source", params.source);
|
|
340
360
|
if (params.page) url.searchParams.set("page", String(params.page));
|
|
341
361
|
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
362
|
+
if (params.include_all_versions) url.searchParams.set("include_all_versions", "true");
|
|
363
|
+
if (params.sort) url.searchParams.set("sort", params.sort);
|
|
342
364
|
return request(
|
|
343
365
|
url.toString(),
|
|
344
366
|
{ method: "GET", headers: headers(opts.apiKey) },
|
|
@@ -364,6 +386,19 @@ async function actionGet(id, opts) {
|
|
|
364
386
|
IDEMPOTENT_RETRIES
|
|
365
387
|
);
|
|
366
388
|
}
|
|
389
|
+
async function actionBatch(ids, opts) {
|
|
390
|
+
return request(
|
|
391
|
+
`${baseUrl(opts)}/v1/actions/batch`,
|
|
392
|
+
{
|
|
393
|
+
method: "POST",
|
|
394
|
+
headers: headers(opts.apiKey),
|
|
395
|
+
body: JSON.stringify({ ids })
|
|
396
|
+
},
|
|
397
|
+
DEFAULT_TIMEOUT_MS,
|
|
398
|
+
IDEMPOTENT_RETRIES
|
|
399
|
+
// read-only metadata fetch — safe to retry
|
|
400
|
+
);
|
|
401
|
+
}
|
|
367
402
|
async function actionCall(actionId, input, opts, httpMethod, retries = 0, timeoutMs = EXECUTE_TIMEOUT_MS) {
|
|
368
403
|
return request(
|
|
369
404
|
`${baseUrl(opts)}/v1/actions/execute`,
|
|
@@ -376,13 +411,87 @@ async function actionCall(actionId, input, opts, httpMethod, retries = 0, timeou
|
|
|
376
411
|
retries
|
|
377
412
|
);
|
|
378
413
|
}
|
|
414
|
+
async function actionStream(actionId, input, opts, httpMethod) {
|
|
415
|
+
const controller = new AbortController();
|
|
416
|
+
let timedOut = false;
|
|
417
|
+
let activeTimeoutMs = EXECUTE_TIMEOUT_MS;
|
|
418
|
+
let timer;
|
|
419
|
+
const resetTimeout = (timeoutMs) => {
|
|
420
|
+
if (timer) clearTimeout(timer);
|
|
421
|
+
activeTimeoutMs = timeoutMs;
|
|
422
|
+
timer = setTimeout(() => {
|
|
423
|
+
timedOut = true;
|
|
424
|
+
controller.abort();
|
|
425
|
+
}, timeoutMs);
|
|
426
|
+
};
|
|
427
|
+
resetTimeout(EXECUTE_TIMEOUT_MS);
|
|
428
|
+
const url = `${baseUrl(opts)}/v1/actions/execute`;
|
|
429
|
+
assertAllowedHost(url);
|
|
430
|
+
try {
|
|
431
|
+
const res = await fetch(url, {
|
|
432
|
+
method: "POST",
|
|
433
|
+
headers: {
|
|
434
|
+
...headers(opts.apiKey),
|
|
435
|
+
Accept: "text/event-stream"
|
|
436
|
+
},
|
|
437
|
+
body: JSON.stringify({
|
|
438
|
+
action_id: actionId,
|
|
439
|
+
...httpMethod ? { method: httpMethod } : {},
|
|
440
|
+
input,
|
|
441
|
+
stream: true
|
|
442
|
+
}),
|
|
443
|
+
redirect: "manual",
|
|
444
|
+
signal: controller.signal
|
|
445
|
+
});
|
|
446
|
+
if (res.status >= 300 && res.status < 400) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
if (!res.ok) {
|
|
452
|
+
const text = await res.text();
|
|
453
|
+
throw new HttpError(
|
|
454
|
+
res.status,
|
|
455
|
+
text.slice(0, 300),
|
|
456
|
+
isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const contentType = res.headers.get("content-type") || "";
|
|
460
|
+
if (!contentType.toLowerCase().includes("text/event-stream")) {
|
|
461
|
+
const text = await res.text();
|
|
462
|
+
throw new Error(
|
|
463
|
+
`expected an SSE response but received "${contentType || "unknown"}": ${text.slice(0, 300)}`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
if (!res.body) return;
|
|
467
|
+
const idleTimeoutMs = transferIdleTimeoutMs();
|
|
468
|
+
resetTimeout(idleTimeoutMs);
|
|
469
|
+
const source = Readable.fromWeb(res.body);
|
|
470
|
+
for await (const chunk of source) {
|
|
471
|
+
resetTimeout(idleTimeoutMs);
|
|
472
|
+
if (!process.stdout.write(chunk)) await once(process.stdout, "drain");
|
|
473
|
+
}
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (timedOut) throw new RequestTimeoutError(activeTimeoutMs);
|
|
476
|
+
throw error;
|
|
477
|
+
} finally {
|
|
478
|
+
if (timer) clearTimeout(timer);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
379
481
|
async function actionDownload(actionId, input, opts, outputPath, httpMethod) {
|
|
380
482
|
const controller = new AbortController();
|
|
381
483
|
let timedOut = false;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
484
|
+
let activeTimeoutMs = EXECUTE_TIMEOUT_MS;
|
|
485
|
+
let timer;
|
|
486
|
+
const resetTimeout = (timeoutMs) => {
|
|
487
|
+
if (timer) clearTimeout(timer);
|
|
488
|
+
activeTimeoutMs = timeoutMs;
|
|
489
|
+
timer = setTimeout(() => {
|
|
490
|
+
timedOut = true;
|
|
491
|
+
controller.abort();
|
|
492
|
+
}, timeoutMs);
|
|
493
|
+
};
|
|
494
|
+
resetTimeout(EXECUTE_TIMEOUT_MS);
|
|
386
495
|
const target = resolve(outputPath);
|
|
387
496
|
let file;
|
|
388
497
|
let complete = false;
|
|
@@ -424,9 +533,12 @@ async function actionDownload(actionId, input, opts, outputPath, httpMethod) {
|
|
|
424
533
|
}
|
|
425
534
|
let bytes = 0;
|
|
426
535
|
if (res.body) {
|
|
536
|
+
const idleTimeoutMs = transferIdleTimeoutMs();
|
|
537
|
+
resetTimeout(idleTimeoutMs);
|
|
427
538
|
const source = Readable.fromWeb(res.body);
|
|
428
539
|
const counter = new Transform({
|
|
429
540
|
transform(chunk, _encoding, callback) {
|
|
541
|
+
resetTimeout(idleTimeoutMs);
|
|
430
542
|
bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
|
|
431
543
|
callback(null, chunk);
|
|
432
544
|
}
|
|
@@ -444,10 +556,10 @@ async function actionDownload(actionId, input, opts, outputPath, httpMethod) {
|
|
|
444
556
|
status: res.status
|
|
445
557
|
};
|
|
446
558
|
} catch (error) {
|
|
447
|
-
if (timedOut) throw new RequestTimeoutError(
|
|
559
|
+
if (timedOut) throw new RequestTimeoutError(activeTimeoutMs);
|
|
448
560
|
throw error;
|
|
449
561
|
} finally {
|
|
450
|
-
clearTimeout(timer);
|
|
562
|
+
if (timer) clearTimeout(timer);
|
|
451
563
|
if (!complete && file) {
|
|
452
564
|
await file.close().catch(() => void 0);
|
|
453
565
|
await rm(target, { force: true }).catch(() => void 0);
|
|
@@ -801,6 +913,7 @@ function generateCode(target, params) {
|
|
|
801
913
|
|
|
802
914
|
// src/commands/action.ts
|
|
803
915
|
var VALID_SOURCES = ["capability", "api"];
|
|
916
|
+
var VALID_SEARCH_SORTS = ["default", "relevance", "price"];
|
|
804
917
|
var LIST_HELP = `xapi-to list - List all actions
|
|
805
918
|
|
|
806
919
|
USAGE
|
|
@@ -829,12 +942,50 @@ FLAGS
|
|
|
829
942
|
--category <name> Filter by category
|
|
830
943
|
--page N Page number (default: 1)
|
|
831
944
|
--page-size N Results per page
|
|
945
|
+
--sort default|relevance|price
|
|
946
|
+
Recommended (default), strongest match, or lowest
|
|
947
|
+
comparable price after exact-id/local-match guards
|
|
948
|
+
--include-all-versions Include active non-default major versions
|
|
832
949
|
--format json|pretty|table Output format
|
|
833
950
|
|
|
834
951
|
EXAMPLES
|
|
835
952
|
xapi-to search twitter
|
|
836
953
|
xapi-to search "tweet detail" --source api
|
|
954
|
+
xapi-to search "tweet detail" --sort relevance
|
|
955
|
+
xapi-to search weather --sort price
|
|
837
956
|
xapi-to search weather --category utility --format table
|
|
957
|
+
xapi-to search twitter --include-all-versions
|
|
958
|
+
`;
|
|
959
|
+
var CATEGORIES_HELP = `xapi-to categories - List action categories
|
|
960
|
+
|
|
961
|
+
USAGE
|
|
962
|
+
xapi-to categories [flags]
|
|
963
|
+
|
|
964
|
+
FLAGS
|
|
965
|
+
--source capability|api Filter by source type
|
|
966
|
+
--format json|pretty|table Output format
|
|
967
|
+
`;
|
|
968
|
+
var SERVICES_HELP = `xapi-to services - List services
|
|
969
|
+
|
|
970
|
+
USAGE
|
|
971
|
+
xapi-to services [flags]
|
|
972
|
+
|
|
973
|
+
FLAGS
|
|
974
|
+
--category <name> Filter by category
|
|
975
|
+
--page N Page number
|
|
976
|
+
--page-size N Results per page
|
|
977
|
+
--format json|pretty|table Output format
|
|
978
|
+
`;
|
|
979
|
+
var GET_BATCH_HELP = `xapi-to get-batch - Get multiple action schemas
|
|
980
|
+
|
|
981
|
+
USAGE
|
|
982
|
+
xapi-to get-batch <id> [id ...] [flags]
|
|
983
|
+
|
|
984
|
+
FLAGS
|
|
985
|
+
--format json|pretty|table Output format
|
|
986
|
+
|
|
987
|
+
EXAMPLES
|
|
988
|
+
xapi-to get-batch twitter.tweet_detail crypto.token.price
|
|
838
989
|
`;
|
|
839
990
|
var GET_HELP = `xapi-to get - Get action schema
|
|
840
991
|
|
|
@@ -878,6 +1029,7 @@ FLAGS
|
|
|
878
1029
|
--input <json> Input payload as JSON (required for execution)
|
|
879
1030
|
--method GET|POST|... Override HTTP method
|
|
880
1031
|
--output <path> Save a raw binary response to a new file
|
|
1032
|
+
--stream Forward the action's HTTP SSE response unchanged
|
|
881
1033
|
--code <target> Generate code snippet instead of executing
|
|
882
1034
|
--format json|pretty|table Output format
|
|
883
1035
|
|
|
@@ -901,6 +1053,7 @@ CODE TARGETS
|
|
|
901
1053
|
EXAMPLES
|
|
902
1054
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
|
|
903
1055
|
xapi-to call openrouter.audio_speech --input '{"body":{"input":"Hello"}}' --output speech.mp3
|
|
1056
|
+
xapi-to call ai.text.chat.fast --input '{"messages":[{"role":"user","content":"Hello"}]}' --stream
|
|
904
1057
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code py
|
|
905
1058
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code curl --format pretty
|
|
906
1059
|
`;
|
|
@@ -931,6 +1084,31 @@ function getSource(flags) {
|
|
|
931
1084
|
}
|
|
932
1085
|
return flags.source;
|
|
933
1086
|
}
|
|
1087
|
+
function getSearchSort(flags) {
|
|
1088
|
+
const value = flags.sort;
|
|
1089
|
+
if (value === void 0) return void 0;
|
|
1090
|
+
if (value === "true") {
|
|
1091
|
+
err("--sort requires a value: default, relevance, or price");
|
|
1092
|
+
}
|
|
1093
|
+
if (!VALID_SEARCH_SORTS.includes(value)) {
|
|
1094
|
+
err(`invalid --sort value: "${value}". Must be default, relevance, or price.`);
|
|
1095
|
+
}
|
|
1096
|
+
return value;
|
|
1097
|
+
}
|
|
1098
|
+
function positiveIntegerFlag(value, name) {
|
|
1099
|
+
if (value === void 0) return void 0;
|
|
1100
|
+
if (!/^\d+$/.test(value) || Number(value) < 1) {
|
|
1101
|
+
err(`${name} must be a positive integer`);
|
|
1102
|
+
}
|
|
1103
|
+
return Number(value);
|
|
1104
|
+
}
|
|
1105
|
+
function httpMethodFlag(value) {
|
|
1106
|
+
if (value === void 0) return void 0;
|
|
1107
|
+
if (value === "true" || !/^[A-Za-z]+$/.test(value)) {
|
|
1108
|
+
err("--method requires an HTTP method, e.g. --method POST");
|
|
1109
|
+
}
|
|
1110
|
+
return value.toUpperCase();
|
|
1111
|
+
}
|
|
934
1112
|
async function actionList2(args, flags) {
|
|
935
1113
|
showHelpIfRequested(flags, LIST_HELP);
|
|
936
1114
|
const cfg = getConfig();
|
|
@@ -938,8 +1116,8 @@ async function actionList2(args, flags) {
|
|
|
938
1116
|
try {
|
|
939
1117
|
const res = await actionList(cfg, {
|
|
940
1118
|
source: getSource(flags),
|
|
941
|
-
page: flags.page
|
|
942
|
-
page_size: flags["page-size"]
|
|
1119
|
+
page: positiveIntegerFlag(flags.page, "--page"),
|
|
1120
|
+
page_size: positiveIntegerFlag(flags["page-size"], "--page-size"),
|
|
943
1121
|
category: flags.category,
|
|
944
1122
|
service_id: flags["service-id"]
|
|
945
1123
|
});
|
|
@@ -965,15 +1143,23 @@ async function actionSearch2(args, flags) {
|
|
|
965
1143
|
showHelpIfRequested(flags, SEARCH_HELP);
|
|
966
1144
|
const query = args[0];
|
|
967
1145
|
if (!query) err("usage: xapi-to search <query>");
|
|
1146
|
+
const requestedSort = getSearchSort(flags);
|
|
968
1147
|
const cfg = getConfig();
|
|
969
1148
|
const fmt = flags.format || getFormat();
|
|
970
1149
|
try {
|
|
971
1150
|
const res = await actionSearch(query, cfg, {
|
|
972
1151
|
source: getSource(flags),
|
|
973
1152
|
category: flags.category,
|
|
974
|
-
page: flags.page
|
|
975
|
-
page_size: flags["page-size"]
|
|
1153
|
+
page: positiveIntegerFlag(flags.page, "--page"),
|
|
1154
|
+
page_size: positiveIntegerFlag(flags["page-size"], "--page-size"),
|
|
1155
|
+
include_all_versions: flags["include-all-versions"] === "true",
|
|
1156
|
+
sort: requestedSort
|
|
976
1157
|
});
|
|
1158
|
+
if (requestedSort && res.sort !== requestedSort) {
|
|
1159
|
+
throw new Error(
|
|
1160
|
+
res.sort ? `backend applied sort "${res.sort}" instead of requested "${requestedSort}"` : "backend does not support search sorting yet; deploy the updated backend before using --sort"
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
977
1163
|
const results = res.results || [];
|
|
978
1164
|
if (fmt === "table") {
|
|
979
1165
|
output(results.map((a) => ({
|
|
@@ -983,7 +1169,8 @@ async function actionSearch2(args, flags) {
|
|
|
983
1169
|
source: a.source ?? "",
|
|
984
1170
|
category: a.meta?.category ?? "",
|
|
985
1171
|
status: a.status ?? "",
|
|
986
|
-
|
|
1172
|
+
price: a.meta?.pricing?.comparable ? a.meta.pricing.listed_price : "",
|
|
1173
|
+
pricing: a.meta?.pricing?.billing_type ?? ""
|
|
987
1174
|
})), "table");
|
|
988
1175
|
} else {
|
|
989
1176
|
output(res, flags.format);
|
|
@@ -993,6 +1180,7 @@ async function actionSearch2(args, flags) {
|
|
|
993
1180
|
}
|
|
994
1181
|
}
|
|
995
1182
|
async function actionCategories2(args, flags) {
|
|
1183
|
+
showHelpIfRequested(flags, CATEGORIES_HELP);
|
|
996
1184
|
const cfg = getConfig();
|
|
997
1185
|
const fmt = flags.format || getFormat();
|
|
998
1186
|
try {
|
|
@@ -1007,12 +1195,13 @@ async function actionCategories2(args, flags) {
|
|
|
1007
1195
|
}
|
|
1008
1196
|
}
|
|
1009
1197
|
async function actionServices2(args, flags) {
|
|
1198
|
+
showHelpIfRequested(flags, SERVICES_HELP);
|
|
1010
1199
|
const cfg = getConfig();
|
|
1011
1200
|
const fmt = flags.format || getFormat();
|
|
1012
1201
|
try {
|
|
1013
1202
|
const res = await actionServices(cfg, {
|
|
1014
|
-
page: flags.page
|
|
1015
|
-
page_size: flags["page-size"]
|
|
1203
|
+
page: positiveIntegerFlag(flags.page, "--page"),
|
|
1204
|
+
page_size: positiveIntegerFlag(flags["page-size"], "--page-size"),
|
|
1016
1205
|
category: flags.category
|
|
1017
1206
|
});
|
|
1018
1207
|
const services = res.services || [];
|
|
@@ -1037,11 +1226,11 @@ async function actionGet2(args, flags) {
|
|
|
1037
1226
|
const id = args[0];
|
|
1038
1227
|
if (!id) err("usage: xapi-to get <id> [--method GET|POST|DELETE|...]");
|
|
1039
1228
|
if (flags.code) validateCodeFlag(flags);
|
|
1229
|
+
const methodFilter = httpMethodFlag(flags.method);
|
|
1040
1230
|
const cfg = getConfig();
|
|
1041
1231
|
try {
|
|
1042
1232
|
const res = await actionGet(id, cfg);
|
|
1043
1233
|
const actions = Array.isArray(res) ? res : [res];
|
|
1044
|
-
const methodFilter = flags.method?.toUpperCase();
|
|
1045
1234
|
const filtered = methodFilter ? actions.filter((a) => a.method?.toUpperCase() === methodFilter) : actions;
|
|
1046
1235
|
if (filtered.length === 0) {
|
|
1047
1236
|
err(`no endpoint found for method "${methodFilter}" in action "${id}"`);
|
|
@@ -1064,15 +1253,30 @@ async function actionGet2(args, flags) {
|
|
|
1064
1253
|
err("get failed", e.message);
|
|
1065
1254
|
}
|
|
1066
1255
|
}
|
|
1256
|
+
async function actionBatchGet(args, flags) {
|
|
1257
|
+
showHelpIfRequested(flags, GET_BATCH_HELP);
|
|
1258
|
+
if (args.length === 0) err("usage: xapi-to get-batch <id> [id ...]");
|
|
1259
|
+
if (args.length > 100) err("get-batch accepts at most 100 action IDs");
|
|
1260
|
+
const cfg = getConfig();
|
|
1261
|
+
try {
|
|
1262
|
+
const res = await actionBatch(args, cfg);
|
|
1263
|
+
output(res, flags.format);
|
|
1264
|
+
} catch (e) {
|
|
1265
|
+
err("get-batch failed", e.message);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1067
1268
|
async function actionCall2(args, flags) {
|
|
1068
1269
|
showHelpIfRequested(flags, CALL_HELP);
|
|
1069
1270
|
const id = args[0];
|
|
1070
1271
|
if (!id) err(`usage: xapi-to call <id> --input '{"key":"val"}'`);
|
|
1071
1272
|
if (flags.code) validateCodeFlag(flags);
|
|
1072
1273
|
if (flags.output === "true") err("--output requires a file path");
|
|
1274
|
+
const stream = flags.stream === "true" || flags.stream === "1" || flags.stream === "yes";
|
|
1073
1275
|
if (flags.code && flags.output) {
|
|
1074
1276
|
err("--output cannot be combined with --code");
|
|
1075
1277
|
}
|
|
1278
|
+
if (stream && flags.output) err("--stream cannot be combined with --output");
|
|
1279
|
+
if (stream && flags.code) err("--stream cannot be combined with --code");
|
|
1076
1280
|
const cfg = getConfig();
|
|
1077
1281
|
let input = {};
|
|
1078
1282
|
if (flags.input) {
|
|
@@ -1086,7 +1290,7 @@ async function actionCall2(args, flags) {
|
|
|
1086
1290
|
}
|
|
1087
1291
|
}
|
|
1088
1292
|
const { method: inputMethod, ...cleanInput } = input;
|
|
1089
|
-
const method = flags.method
|
|
1293
|
+
const method = httpMethodFlag(flags.method) || (typeof inputMethod === "string" ? inputMethod.toUpperCase() : void 0);
|
|
1090
1294
|
if (flags.code) {
|
|
1091
1295
|
const result = generateCode(flags.code, { actionId: id, input: cleanInput, actionHost: cfg.actionHost, method });
|
|
1092
1296
|
outputCode(result, flags);
|
|
@@ -1094,6 +1298,10 @@ async function actionCall2(args, flags) {
|
|
|
1094
1298
|
}
|
|
1095
1299
|
requireApiKey(cfg);
|
|
1096
1300
|
try {
|
|
1301
|
+
if (stream) {
|
|
1302
|
+
await actionStream(id, cleanInput, cfg, method);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1097
1305
|
if (flags.output) {
|
|
1098
1306
|
const result = await actionDownload(
|
|
1099
1307
|
id,
|
|
@@ -1126,6 +1334,7 @@ async function actionCall2(args, flags) {
|
|
|
1126
1334
|
var config_exports = {};
|
|
1127
1335
|
__export(config_exports, {
|
|
1128
1336
|
CONFIG_HELP: () => CONFIG_HELP,
|
|
1337
|
+
HEALTH_HELP: () => HEALTH_HELP,
|
|
1129
1338
|
configHealth: () => configHealth,
|
|
1130
1339
|
configSet: () => configSet,
|
|
1131
1340
|
configShow: () => configShow
|
|
@@ -1144,14 +1353,23 @@ COMMANDS
|
|
|
1144
1353
|
FLAGS
|
|
1145
1354
|
--format json|pretty|table Output format
|
|
1146
1355
|
|
|
1356
|
+
ENVIRONMENT OVERRIDES
|
|
1357
|
+
XAPI_KEY takes precedence over XAPI_API_KEY, which takes precedence over the file.
|
|
1358
|
+
Saving a file key does not replace an active environment-variable key.
|
|
1359
|
+
|
|
1147
1360
|
EXAMPLES
|
|
1148
1361
|
xapi-to config show
|
|
1149
1362
|
xapi-to config set apiKey=xapi_abc123
|
|
1150
1363
|
echo "$XAPI_KEY" | xapi-to config set apiKey=- # keeps the key out of shell history
|
|
1151
1364
|
xapi-to config health
|
|
1152
1365
|
`;
|
|
1366
|
+
var HEALTH_HELP = `xapi-to health - Check backend connectivity
|
|
1367
|
+
|
|
1368
|
+
USAGE
|
|
1369
|
+
xapi-to health [--format json|pretty|table]
|
|
1370
|
+
`;
|
|
1153
1371
|
async function configShow(args, flags) {
|
|
1154
|
-
showConfig();
|
|
1372
|
+
output(showConfig(), flags.format);
|
|
1155
1373
|
}
|
|
1156
1374
|
async function configSet(args, flags) {
|
|
1157
1375
|
if (args.length === 0) err("usage: xapi-to config set apiKey=<key>");
|
|
@@ -1169,10 +1387,22 @@ async function configSet(args, flags) {
|
|
|
1169
1387
|
if (!value) err("apiKey is empty");
|
|
1170
1388
|
updates.apiKey = value;
|
|
1171
1389
|
}
|
|
1390
|
+
const sourceBeforeSave = getApiKeySource();
|
|
1172
1391
|
saveConfig(updates);
|
|
1173
|
-
|
|
1392
|
+
const source = sourceBeforeSave === "XAPI_KEY" || sourceBeforeSave === "XAPI_API_KEY" ? sourceBeforeSave : "file";
|
|
1393
|
+
output({
|
|
1394
|
+
ok: true,
|
|
1395
|
+
updated: Object.keys(updates),
|
|
1396
|
+
effective: source === "file",
|
|
1397
|
+
source,
|
|
1398
|
+
...source === "XAPI_KEY" || source === "XAPI_API_KEY" ? { warning: `${source} still overrides the saved file key` } : {}
|
|
1399
|
+
}, flags.format);
|
|
1174
1400
|
}
|
|
1175
1401
|
async function configHealth(args, flags) {
|
|
1402
|
+
if (flags.help) {
|
|
1403
|
+
console.log(HEALTH_HELP);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1176
1406
|
const cfg = getConfig();
|
|
1177
1407
|
const start = Date.now();
|
|
1178
1408
|
try {
|
|
@@ -1185,6 +1415,30 @@ async function configHealth(args, flags) {
|
|
|
1185
1415
|
}
|
|
1186
1416
|
|
|
1187
1417
|
// src/commands/register.ts
|
|
1418
|
+
var REGISTER_HELP = `xapi-to register - Create a new xAPI account
|
|
1419
|
+
|
|
1420
|
+
USAGE
|
|
1421
|
+
xapi-to register [referral-code] [flags]
|
|
1422
|
+
|
|
1423
|
+
FLAGS
|
|
1424
|
+
--referral-code <code> Submit an inviter's referral code
|
|
1425
|
+
--referralCode <code> Alias for --referral-code
|
|
1426
|
+
--force Replace an existing file-based API key
|
|
1427
|
+
--format json|pretty|table Output format
|
|
1428
|
+
|
|
1429
|
+
The API key is saved to ~/.xapi/config.json. If XAPI_KEY or XAPI_API_KEY is set,
|
|
1430
|
+
unset it before registering because environment variables override the saved file.
|
|
1431
|
+
`;
|
|
1432
|
+
function validateRegisterResponse(value) {
|
|
1433
|
+
const res = value;
|
|
1434
|
+
if (!res || typeof res.apiKey !== "string" || !res.apiKey.trim()) {
|
|
1435
|
+
throw new Error("invalid register response: missing apiKey");
|
|
1436
|
+
}
|
|
1437
|
+
if (typeof res.referralCode !== "string" || !res.user || typeof res.user.id !== "string") {
|
|
1438
|
+
throw new Error("invalid register response: missing account details");
|
|
1439
|
+
}
|
|
1440
|
+
return res;
|
|
1441
|
+
}
|
|
1188
1442
|
async function registerAccount(referralCode) {
|
|
1189
1443
|
assertAllowedHost(XAPI_API_HOST);
|
|
1190
1444
|
const controller = new AbortController();
|
|
@@ -1194,39 +1448,57 @@ async function registerAccount(referralCode) {
|
|
|
1194
1448
|
method: "POST",
|
|
1195
1449
|
headers: { "Content-Type": "application/json" },
|
|
1196
1450
|
body: JSON.stringify(referralCode ? { referralCode } : {}),
|
|
1197
|
-
signal: controller.signal
|
|
1451
|
+
signal: controller.signal,
|
|
1452
|
+
redirect: "manual"
|
|
1198
1453
|
});
|
|
1454
|
+
if (res.status >= 300 && res.status < 400) {
|
|
1455
|
+
throw new Error(`refusing to follow redirect to "${res.headers.get("location") ?? "?"}"`);
|
|
1456
|
+
}
|
|
1199
1457
|
if (!res.ok) {
|
|
1200
1458
|
const text = await res.text();
|
|
1201
1459
|
throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
1202
1460
|
}
|
|
1203
|
-
return res.json();
|
|
1461
|
+
return validateRegisterResponse(await res.json());
|
|
1204
1462
|
} finally {
|
|
1205
1463
|
clearTimeout(timer);
|
|
1206
1464
|
}
|
|
1207
1465
|
}
|
|
1208
1466
|
async function register(args, flags) {
|
|
1467
|
+
if (flags.help) {
|
|
1468
|
+
console.log(REGISTER_HELP);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1209
1471
|
try {
|
|
1210
1472
|
const cfg = getConfig();
|
|
1211
1473
|
const force = flags.force === "true" || flags.force === "1" || flags.force === "yes";
|
|
1474
|
+
const source = getApiKeySource();
|
|
1475
|
+
if (source === "XAPI_KEY" || source === "XAPI_API_KEY") {
|
|
1476
|
+
err(
|
|
1477
|
+
"register cannot replace an API key supplied by an environment variable",
|
|
1478
|
+
`Unset ${source} first; it would continue to override the newly saved key.`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1212
1481
|
if (cfg.apiKey && !force) {
|
|
1213
1482
|
err("register would overwrite existing apiKey", 'Run "xapi-to register --force" to create a new account and replace the saved key.');
|
|
1214
1483
|
}
|
|
1215
1484
|
const rawReferral = flags["referral-code"] ?? flags["referralCode"] ?? args[0];
|
|
1216
|
-
|
|
1485
|
+
if (rawReferral === "true") {
|
|
1486
|
+
err("--referral-code requires a code");
|
|
1487
|
+
}
|
|
1488
|
+
const referralCode = typeof rawReferral === "string" && rawReferral.length > 0 ? rawReferral : void 0;
|
|
1217
1489
|
const res = await registerAccount(referralCode);
|
|
1490
|
+
const bindUrl = res.bindUrl || res.claimUrl;
|
|
1218
1491
|
saveConfig({ apiKey: res.apiKey });
|
|
1219
1492
|
output({
|
|
1220
1493
|
apiKey: res.apiKey,
|
|
1221
1494
|
user: res.user,
|
|
1222
1495
|
referralCode: res.referralCode,
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
...referralCode ? { referredBy: referralCode } : {},
|
|
1496
|
+
bindUrl,
|
|
1497
|
+
// Keep the backend's legacy field visible while clients migrate to bindUrl.
|
|
1498
|
+
claimUrl: res.claimUrl || bindUrl,
|
|
1499
|
+
// The backend may accept the registration while ignoring an invalid code,
|
|
1500
|
+
// so only report that the code was submitted, not that a referral exists.
|
|
1501
|
+
...referralCode ? { referralCodeProvided: referralCode } : {},
|
|
1230
1502
|
note: force && cfg.apiKey ? "apiKey replaced in ~/.xapi/config.json" : "apiKey saved to ~/.xapi/config.json"
|
|
1231
1503
|
}, flags.format);
|
|
1232
1504
|
} catch (e) {
|
|
@@ -1236,23 +1508,50 @@ async function register(args, flags) {
|
|
|
1236
1508
|
|
|
1237
1509
|
// src/commands/topup.ts
|
|
1238
1510
|
var TOPUP_BASE_URL = "https://www.xapi.to/topup/payment";
|
|
1511
|
+
var TOPUP_HELP = `xapi-to topup - Generate a private payment URL
|
|
1512
|
+
|
|
1513
|
+
USAGE
|
|
1514
|
+
xapi-to topup [--amount <usd>] [--method stripe|x402]
|
|
1515
|
+
|
|
1516
|
+
The generated URL can contain your API key. Do not log or share it.
|
|
1517
|
+
`;
|
|
1239
1518
|
async function topup(args, flags) {
|
|
1519
|
+
if (flags.help) {
|
|
1520
|
+
console.log(TOPUP_HELP);
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1240
1523
|
const cfg = getConfig();
|
|
1241
1524
|
const url = new URL(TOPUP_BASE_URL);
|
|
1242
1525
|
if (cfg.apiKey) url.searchParams.set("apikey", cfg.apiKey);
|
|
1243
|
-
if (flags.method)
|
|
1526
|
+
if (flags.method) {
|
|
1527
|
+
if (!["stripe", "x402"].includes(flags.method)) {
|
|
1528
|
+
err("invalid --method value", "Expected stripe or x402.");
|
|
1529
|
+
}
|
|
1530
|
+
url.searchParams.set("method", flags.method);
|
|
1531
|
+
}
|
|
1244
1532
|
const amountStr = flags.amount || args[0];
|
|
1245
1533
|
if (amountStr) {
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1534
|
+
const normalizedAmount = amountStr.trim();
|
|
1535
|
+
const amountUsd = Number(normalizedAmount);
|
|
1536
|
+
if (!/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(normalizedAmount) || !Number.isFinite(amountUsd) || amountUsd <= 0) {
|
|
1537
|
+
err("invalid top-up amount", "Expected a positive USD number, e.g. --amount 10.");
|
|
1249
1538
|
}
|
|
1539
|
+
url.searchParams.set("amount", String(amountUsd));
|
|
1250
1540
|
}
|
|
1251
1541
|
output({ url: url.toString() }, flags.format);
|
|
1252
1542
|
}
|
|
1253
1543
|
|
|
1254
1544
|
// src/commands/balance.ts
|
|
1545
|
+
var BALANCE_HELP = `xapi-to balance - Show the current account balance
|
|
1546
|
+
|
|
1547
|
+
USAGE
|
|
1548
|
+
xapi-to balance [--format json|pretty|table]
|
|
1549
|
+
`;
|
|
1255
1550
|
async function balance(args, flags) {
|
|
1551
|
+
if (flags.help) {
|
|
1552
|
+
console.log(BALANCE_HELP);
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1256
1555
|
const cfg = getConfig();
|
|
1257
1556
|
requireApiKey(cfg);
|
|
1258
1557
|
let token;
|
|
@@ -1289,9 +1588,10 @@ __export(oauth_exports, {
|
|
|
1289
1588
|
});
|
|
1290
1589
|
import { spawnSync } from "child_process";
|
|
1291
1590
|
function openBrowser(url) {
|
|
1292
|
-
const cmd = process.platform === "win32" ? "
|
|
1591
|
+
const cmd = process.platform === "win32" ? "rundll32.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
1592
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
1293
1593
|
try {
|
|
1294
|
-
spawnSync(cmd,
|
|
1594
|
+
spawnSync(cmd, args, { stdio: "ignore" });
|
|
1295
1595
|
} catch {
|
|
1296
1596
|
}
|
|
1297
1597
|
}
|
|
@@ -1335,9 +1635,11 @@ async function findCurrentKeyRecord(plaintextKey, jwtToken) {
|
|
|
1335
1635
|
throw new Error("No API keys found for this account");
|
|
1336
1636
|
}
|
|
1337
1637
|
const prefix = plaintextKey.substring(0, 7);
|
|
1338
|
-
const
|
|
1638
|
+
const suffix = plaintextKey.slice(-4);
|
|
1639
|
+
const expectedPreview = `${prefix}****${suffix}`;
|
|
1640
|
+
const match = keys.find((k) => k.keyPreview === expectedPreview);
|
|
1339
1641
|
if (match) return match;
|
|
1340
|
-
if (keys.length === 1) return keys[0];
|
|
1642
|
+
if (keys.length === 1 && !keys[0].keyPreview.includes("****")) return keys[0];
|
|
1341
1643
|
throw new Error(
|
|
1342
1644
|
`Current API key (${prefix}...) was not found in your account keys. Run "xapi-to config set apiKey=<key>" with a valid key before binding OAuth.`
|
|
1343
1645
|
);
|
|
@@ -1454,13 +1756,20 @@ FLAGS
|
|
|
1454
1756
|
EXAMPLES
|
|
1455
1757
|
xapi-to oauth bind
|
|
1456
1758
|
xapi-to oauth bind --provider twitter
|
|
1457
|
-
xapi-to oauth
|
|
1759
|
+
xapi-to oauth providers # inspect current default scopes
|
|
1760
|
+
xapi-to oauth bind --scopes "<scope list>" # override only when needed
|
|
1458
1761
|
xapi-to oauth status
|
|
1459
1762
|
xapi-to oauth status --format pretty
|
|
1460
1763
|
xapi-to oauth unbind abc123
|
|
1461
1764
|
xapi-to oauth providers
|
|
1462
1765
|
`;
|
|
1463
1766
|
async function oauthBind(args, flags) {
|
|
1767
|
+
if (flags.provider === "true") {
|
|
1768
|
+
err("--provider requires a provider name, e.g. --provider twitter");
|
|
1769
|
+
}
|
|
1770
|
+
if (flags.scopes === "true") {
|
|
1771
|
+
err("--scopes requires a space-separated scope list");
|
|
1772
|
+
}
|
|
1464
1773
|
const cfg = getConfig();
|
|
1465
1774
|
requireApiKey(cfg);
|
|
1466
1775
|
const apiKey = cfg.apiKey;
|
|
@@ -1518,6 +1827,16 @@ async function oauthBind(args, flags) {
|
|
|
1518
1827
|
const authorizationStartedAt = /* @__PURE__ */ new Date();
|
|
1519
1828
|
const result = await initiateOAuth(keyRecord.id, provider.id, jwtToken, XAPI_API_HOST, scopes);
|
|
1520
1829
|
const { authorizationUrl } = result;
|
|
1830
|
+
let authorizationTarget;
|
|
1831
|
+
try {
|
|
1832
|
+
authorizationTarget = new URL(authorizationUrl);
|
|
1833
|
+
} catch {
|
|
1834
|
+
throw new Error("OAuth provider returned an invalid authorization URL");
|
|
1835
|
+
}
|
|
1836
|
+
const localHttp = authorizationTarget.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(authorizationTarget.hostname);
|
|
1837
|
+
if (authorizationTarget.protocol !== "https:" && !localHttp) {
|
|
1838
|
+
throw new Error(`OAuth provider returned an unsupported authorization URL protocol: ${authorizationTarget.protocol}`);
|
|
1839
|
+
}
|
|
1521
1840
|
if (isTTY) {
|
|
1522
1841
|
if (!headerPrinted) {
|
|
1523
1842
|
console.error(`
|
|
@@ -1863,6 +2182,8 @@ COMMANDS
|
|
|
1863
2182
|
--source capability|api Filter by source type
|
|
1864
2183
|
--category <name> Filter by category
|
|
1865
2184
|
--page N --page-size N Pagination
|
|
2185
|
+
--sort default|relevance|price Recommended, strongest match, or comparable price
|
|
2186
|
+
--include-all-versions Include active non-default major versions
|
|
1866
2187
|
categories List all action categories
|
|
1867
2188
|
--source capability|api Filter by source type
|
|
1868
2189
|
services List all services
|
|
@@ -1870,9 +2191,11 @@ COMMANDS
|
|
|
1870
2191
|
--category <name> Filter by category
|
|
1871
2192
|
get <id> [--method GET|POST|...] Get action schema (filter by HTTP method)
|
|
1872
2193
|
--code <target> Generate code snippet (curl, py, js, ts, go)
|
|
2194
|
+
get-batch <id> [id ...] Get up to 100 action schemas
|
|
1873
2195
|
call <id> --input '{"key":"val"}' Execute an action
|
|
1874
2196
|
--method GET|POST|... Override HTTP method
|
|
1875
2197
|
--output <path> Save a raw binary response to a new file
|
|
2198
|
+
--stream Forward HTTP SSE frames unchanged
|
|
1876
2199
|
--code <target> Generate code snippet instead of executing
|
|
1877
2200
|
Variants: python.requests, python.httpx, javascript.fetch, javascript.axios
|
|
1878
2201
|
|
|
@@ -1889,7 +2212,7 @@ COMMANDS
|
|
|
1889
2212
|
|
|
1890
2213
|
register [referral-code] Create a new user account (apiKey saved automatically)
|
|
1891
2214
|
--referral-code <code> Register with an inviter's referral code (also: --referralCode, or as positional arg)
|
|
1892
|
-
--force Replace an existing
|
|
2215
|
+
--force Replace an existing file-based apiKey
|
|
1893
2216
|
balance Show current account balance
|
|
1894
2217
|
topup [--amount <usd>] [--method stripe|x402] Generate payment URL
|
|
1895
2218
|
|
|
@@ -1904,9 +2227,12 @@ GLOBAL FLAGS
|
|
|
1904
2227
|
--help Show help (use with a command for details, e.g. xapi-to get --help)
|
|
1905
2228
|
|
|
1906
2229
|
ENV VARS
|
|
1907
|
-
XAPI_KEY
|
|
2230
|
+
XAPI_KEY API key (highest precedence; header: XAPI-Key)
|
|
2231
|
+
XAPI_API_KEY Compatible API key alias
|
|
1908
2232
|
XAPI_ACTION_HOST Action service host (default: action.xapi.to)
|
|
2233
|
+
XAPI_API_HOST Auth/account service host (default: api.xapi.to)
|
|
1909
2234
|
XAPI_OUTPUT Default output format
|
|
2235
|
+
XAPI_TRANSFER_IDLE_TIMEOUT_MS SSE/download idle timeout (default: 60000)
|
|
1910
2236
|
|
|
1911
2237
|
EXAMPLES
|
|
1912
2238
|
xapi-to register
|
|
@@ -1916,6 +2242,7 @@ EXAMPLES
|
|
|
1916
2242
|
xapi-to list --source capability
|
|
1917
2243
|
xapi-to search twitter --source api
|
|
1918
2244
|
xapi-to get twitter.tweet_detail
|
|
2245
|
+
xapi-to get-batch twitter.tweet_detail crypto.token.price
|
|
1919
2246
|
xapi-to get twitter.tweet_detail --code curl
|
|
1920
2247
|
xapi-to get twitter.tweet_detail --code py --format pretty
|
|
1921
2248
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
|
|
@@ -1934,7 +2261,16 @@ async function main() {
|
|
|
1934
2261
|
console.log(HELP);
|
|
1935
2262
|
process.exit(0);
|
|
1936
2263
|
}
|
|
1937
|
-
if (flags.format)
|
|
2264
|
+
if (flags.format) {
|
|
2265
|
+
if (!["json", "pretty", "table"].includes(flags.format)) {
|
|
2266
|
+
console.error(JSON.stringify({
|
|
2267
|
+
error: `invalid --format value: ${flags.format}`,
|
|
2268
|
+
hint: "expected json, pretty, or table"
|
|
2269
|
+
}));
|
|
2270
|
+
process.exit(1);
|
|
2271
|
+
}
|
|
2272
|
+
process.env.XAPI_OUTPUT = flags.format;
|
|
2273
|
+
}
|
|
1938
2274
|
const [cmd, ...rest] = positional;
|
|
1939
2275
|
switch (cmd) {
|
|
1940
2276
|
// ── Action commands (top-level) ──
|
|
@@ -1948,6 +2284,8 @@ async function main() {
|
|
|
1948
2284
|
return actionServices2(rest, flags);
|
|
1949
2285
|
case "get":
|
|
1950
2286
|
return actionGet2(rest, flags);
|
|
2287
|
+
case "get-batch":
|
|
2288
|
+
return actionBatchGet(rest, flags);
|
|
1951
2289
|
case "call":
|
|
1952
2290
|
return actionCall2(rest, flags);
|
|
1953
2291
|
case "task": {
|