xapi-to 0.1.17 → 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 +60 -10
- package/dist/index.js +493 -44
- 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,24 +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
|
|
191
|
+
import { open, rm } from "fs/promises";
|
|
192
|
+
import { once } from "events";
|
|
193
|
+
import { resolve } from "path";
|
|
194
|
+
import { Readable, Transform } from "stream";
|
|
195
|
+
import { pipeline } from "stream/promises";
|
|
183
196
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
184
197
|
var EXECUTE_TIMEOUT_MS = 6e4;
|
|
198
|
+
var TRANSFER_IDLE_TIMEOUT_MS = 6e4;
|
|
185
199
|
var IDEMPOTENT_RETRIES = 2;
|
|
186
200
|
var RETRY_BASE_DELAY_MS = 500;
|
|
187
201
|
var RETRY_MAX_DELAY_MS = 8e3;
|
|
@@ -221,6 +235,10 @@ function retryBaseDelayMs() {
|
|
|
221
235
|
const override = Number(process.env.XAPI_RETRY_BASE_MS);
|
|
222
236
|
return Number.isFinite(override) && override > 0 ? override : RETRY_BASE_DELAY_MS;
|
|
223
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
|
+
}
|
|
224
242
|
function backoffDelayMs(attempt, retryAfterMs) {
|
|
225
243
|
if (retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
|
|
226
244
|
return Math.min(retryAfterMs, RETRY_MAX_DELAY_MS);
|
|
@@ -237,7 +255,7 @@ function parseRetryAfterMs(res) {
|
|
|
237
255
|
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : void 0;
|
|
238
256
|
}
|
|
239
257
|
function sleep(ms) {
|
|
240
|
-
return new Promise((
|
|
258
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
241
259
|
}
|
|
242
260
|
async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0) {
|
|
243
261
|
assertAllowedHost(url);
|
|
@@ -292,7 +310,13 @@ async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0
|
|
|
292
310
|
return body;
|
|
293
311
|
} catch (e) {
|
|
294
312
|
if (timedOut) {
|
|
295
|
-
|
|
313
|
+
const timeoutError = new RequestTimeoutError(timeoutMs);
|
|
314
|
+
if (attempt < retries) {
|
|
315
|
+
await sleep(backoffDelayMs(attempt));
|
|
316
|
+
attempt++;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
throw timeoutError;
|
|
296
320
|
}
|
|
297
321
|
if (isRetryableNetworkError(e) && attempt < retries) {
|
|
298
322
|
clearTimeout(timer);
|
|
@@ -335,6 +359,8 @@ async function actionSearch(query, opts, params = {}) {
|
|
|
335
359
|
if (params.source) url.searchParams.set("source", params.source);
|
|
336
360
|
if (params.page) url.searchParams.set("page", String(params.page));
|
|
337
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);
|
|
338
364
|
return request(
|
|
339
365
|
url.toString(),
|
|
340
366
|
{ method: "GET", headers: headers(opts.apiKey) },
|
|
@@ -360,6 +386,19 @@ async function actionGet(id, opts) {
|
|
|
360
386
|
IDEMPOTENT_RETRIES
|
|
361
387
|
);
|
|
362
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
|
+
}
|
|
363
402
|
async function actionCall(actionId, input, opts, httpMethod, retries = 0, timeoutMs = EXECUTE_TIMEOUT_MS) {
|
|
364
403
|
return request(
|
|
365
404
|
`${baseUrl(opts)}/v1/actions/execute`,
|
|
@@ -372,6 +411,161 @@ async function actionCall(actionId, input, opts, httpMethod, retries = 0, timeou
|
|
|
372
411
|
retries
|
|
373
412
|
);
|
|
374
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
|
+
}
|
|
481
|
+
async function actionDownload(actionId, input, opts, outputPath, httpMethod) {
|
|
482
|
+
const controller = new AbortController();
|
|
483
|
+
let timedOut = false;
|
|
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);
|
|
495
|
+
const target = resolve(outputPath);
|
|
496
|
+
let file;
|
|
497
|
+
let complete = false;
|
|
498
|
+
try {
|
|
499
|
+
try {
|
|
500
|
+
file = await open(target, "wx");
|
|
501
|
+
} catch (error) {
|
|
502
|
+
if (error?.code === "EEXIST") {
|
|
503
|
+
throw new Error(`Output file already exists: ${target}`);
|
|
504
|
+
}
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
const url = `${baseUrl(opts)}/v1/actions/execute`;
|
|
508
|
+
assertAllowedHost(url);
|
|
509
|
+
const res = await fetch(url, {
|
|
510
|
+
method: "POST",
|
|
511
|
+
headers: headers(opts.apiKey),
|
|
512
|
+
body: JSON.stringify({
|
|
513
|
+
action_id: actionId,
|
|
514
|
+
...httpMethod ? { method: httpMethod } : {},
|
|
515
|
+
input,
|
|
516
|
+
response_mode: "raw"
|
|
517
|
+
}),
|
|
518
|
+
redirect: "manual",
|
|
519
|
+
signal: controller.signal
|
|
520
|
+
});
|
|
521
|
+
if (res.status >= 300 && res.status < 400) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (!res.ok) {
|
|
527
|
+
const text = await res.text();
|
|
528
|
+
throw new HttpError(
|
|
529
|
+
res.status,
|
|
530
|
+
text.slice(0, 300),
|
|
531
|
+
isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
let bytes = 0;
|
|
535
|
+
if (res.body) {
|
|
536
|
+
const idleTimeoutMs = transferIdleTimeoutMs();
|
|
537
|
+
resetTimeout(idleTimeoutMs);
|
|
538
|
+
const source = Readable.fromWeb(res.body);
|
|
539
|
+
const counter = new Transform({
|
|
540
|
+
transform(chunk, _encoding, callback) {
|
|
541
|
+
resetTimeout(idleTimeoutMs);
|
|
542
|
+
bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
|
|
543
|
+
callback(null, chunk);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
await pipeline(source, counter, file.createWriteStream());
|
|
547
|
+
} else {
|
|
548
|
+
await file.close();
|
|
549
|
+
}
|
|
550
|
+
complete = true;
|
|
551
|
+
return {
|
|
552
|
+
output: target,
|
|
553
|
+
bytes,
|
|
554
|
+
contentType: res.headers.get("content-type") || void 0,
|
|
555
|
+
contentDisposition: res.headers.get("content-disposition") || void 0,
|
|
556
|
+
status: res.status
|
|
557
|
+
};
|
|
558
|
+
} catch (error) {
|
|
559
|
+
if (timedOut) throw new RequestTimeoutError(activeTimeoutMs);
|
|
560
|
+
throw error;
|
|
561
|
+
} finally {
|
|
562
|
+
if (timer) clearTimeout(timer);
|
|
563
|
+
if (!complete && file) {
|
|
564
|
+
await file.close().catch(() => void 0);
|
|
565
|
+
await rm(target, { force: true }).catch(() => void 0);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
375
569
|
async function actionServices(opts, params = {}) {
|
|
376
570
|
const url = new URL(`${baseUrl(opts)}/v1/actions/services`);
|
|
377
571
|
if (params.page) url.searchParams.set("page", String(params.page));
|
|
@@ -719,6 +913,7 @@ function generateCode(target, params) {
|
|
|
719
913
|
|
|
720
914
|
// src/commands/action.ts
|
|
721
915
|
var VALID_SOURCES = ["capability", "api"];
|
|
916
|
+
var VALID_SEARCH_SORTS = ["default", "relevance", "price"];
|
|
722
917
|
var LIST_HELP = `xapi-to list - List all actions
|
|
723
918
|
|
|
724
919
|
USAGE
|
|
@@ -747,12 +942,50 @@ FLAGS
|
|
|
747
942
|
--category <name> Filter by category
|
|
748
943
|
--page N Page number (default: 1)
|
|
749
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
|
|
750
949
|
--format json|pretty|table Output format
|
|
751
950
|
|
|
752
951
|
EXAMPLES
|
|
753
952
|
xapi-to search twitter
|
|
754
953
|
xapi-to search "tweet detail" --source api
|
|
954
|
+
xapi-to search "tweet detail" --sort relevance
|
|
955
|
+
xapi-to search weather --sort price
|
|
755
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
|
|
756
989
|
`;
|
|
757
990
|
var GET_HELP = `xapi-to get - Get action schema
|
|
758
991
|
|
|
@@ -795,6 +1028,8 @@ USAGE
|
|
|
795
1028
|
FLAGS
|
|
796
1029
|
--input <json> Input payload as JSON (required for execution)
|
|
797
1030
|
--method GET|POST|... Override HTTP method
|
|
1031
|
+
--output <path> Save a raw binary response to a new file
|
|
1032
|
+
--stream Forward the action's HTTP SSE response unchanged
|
|
798
1033
|
--code <target> Generate code snippet instead of executing
|
|
799
1034
|
--format json|pretty|table Output format
|
|
800
1035
|
|
|
@@ -817,6 +1052,8 @@ CODE TARGETS
|
|
|
817
1052
|
|
|
818
1053
|
EXAMPLES
|
|
819
1054
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
|
|
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
|
|
820
1057
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code py
|
|
821
1058
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code curl --format pretty
|
|
822
1059
|
`;
|
|
@@ -847,6 +1084,31 @@ function getSource(flags) {
|
|
|
847
1084
|
}
|
|
848
1085
|
return flags.source;
|
|
849
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
|
+
}
|
|
850
1112
|
async function actionList2(args, flags) {
|
|
851
1113
|
showHelpIfRequested(flags, LIST_HELP);
|
|
852
1114
|
const cfg = getConfig();
|
|
@@ -854,8 +1116,8 @@ async function actionList2(args, flags) {
|
|
|
854
1116
|
try {
|
|
855
1117
|
const res = await actionList(cfg, {
|
|
856
1118
|
source: getSource(flags),
|
|
857
|
-
page: flags.page
|
|
858
|
-
page_size: flags["page-size"]
|
|
1119
|
+
page: positiveIntegerFlag(flags.page, "--page"),
|
|
1120
|
+
page_size: positiveIntegerFlag(flags["page-size"], "--page-size"),
|
|
859
1121
|
category: flags.category,
|
|
860
1122
|
service_id: flags["service-id"]
|
|
861
1123
|
});
|
|
@@ -881,15 +1143,23 @@ async function actionSearch2(args, flags) {
|
|
|
881
1143
|
showHelpIfRequested(flags, SEARCH_HELP);
|
|
882
1144
|
const query = args[0];
|
|
883
1145
|
if (!query) err("usage: xapi-to search <query>");
|
|
1146
|
+
const requestedSort = getSearchSort(flags);
|
|
884
1147
|
const cfg = getConfig();
|
|
885
1148
|
const fmt = flags.format || getFormat();
|
|
886
1149
|
try {
|
|
887
1150
|
const res = await actionSearch(query, cfg, {
|
|
888
1151
|
source: getSource(flags),
|
|
889
1152
|
category: flags.category,
|
|
890
|
-
page: flags.page
|
|
891
|
-
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
|
|
892
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
|
+
}
|
|
893
1163
|
const results = res.results || [];
|
|
894
1164
|
if (fmt === "table") {
|
|
895
1165
|
output(results.map((a) => ({
|
|
@@ -899,7 +1169,8 @@ async function actionSearch2(args, flags) {
|
|
|
899
1169
|
source: a.source ?? "",
|
|
900
1170
|
category: a.meta?.category ?? "",
|
|
901
1171
|
status: a.status ?? "",
|
|
902
|
-
|
|
1172
|
+
price: a.meta?.pricing?.comparable ? a.meta.pricing.listed_price : "",
|
|
1173
|
+
pricing: a.meta?.pricing?.billing_type ?? ""
|
|
903
1174
|
})), "table");
|
|
904
1175
|
} else {
|
|
905
1176
|
output(res, flags.format);
|
|
@@ -909,6 +1180,7 @@ async function actionSearch2(args, flags) {
|
|
|
909
1180
|
}
|
|
910
1181
|
}
|
|
911
1182
|
async function actionCategories2(args, flags) {
|
|
1183
|
+
showHelpIfRequested(flags, CATEGORIES_HELP);
|
|
912
1184
|
const cfg = getConfig();
|
|
913
1185
|
const fmt = flags.format || getFormat();
|
|
914
1186
|
try {
|
|
@@ -923,12 +1195,13 @@ async function actionCategories2(args, flags) {
|
|
|
923
1195
|
}
|
|
924
1196
|
}
|
|
925
1197
|
async function actionServices2(args, flags) {
|
|
1198
|
+
showHelpIfRequested(flags, SERVICES_HELP);
|
|
926
1199
|
const cfg = getConfig();
|
|
927
1200
|
const fmt = flags.format || getFormat();
|
|
928
1201
|
try {
|
|
929
1202
|
const res = await actionServices(cfg, {
|
|
930
|
-
page: flags.page
|
|
931
|
-
page_size: flags["page-size"]
|
|
1203
|
+
page: positiveIntegerFlag(flags.page, "--page"),
|
|
1204
|
+
page_size: positiveIntegerFlag(flags["page-size"], "--page-size"),
|
|
932
1205
|
category: flags.category
|
|
933
1206
|
});
|
|
934
1207
|
const services = res.services || [];
|
|
@@ -953,11 +1226,11 @@ async function actionGet2(args, flags) {
|
|
|
953
1226
|
const id = args[0];
|
|
954
1227
|
if (!id) err("usage: xapi-to get <id> [--method GET|POST|DELETE|...]");
|
|
955
1228
|
if (flags.code) validateCodeFlag(flags);
|
|
1229
|
+
const methodFilter = httpMethodFlag(flags.method);
|
|
956
1230
|
const cfg = getConfig();
|
|
957
1231
|
try {
|
|
958
1232
|
const res = await actionGet(id, cfg);
|
|
959
1233
|
const actions = Array.isArray(res) ? res : [res];
|
|
960
|
-
const methodFilter = flags.method?.toUpperCase();
|
|
961
1234
|
const filtered = methodFilter ? actions.filter((a) => a.method?.toUpperCase() === methodFilter) : actions;
|
|
962
1235
|
if (filtered.length === 0) {
|
|
963
1236
|
err(`no endpoint found for method "${methodFilter}" in action "${id}"`);
|
|
@@ -980,11 +1253,30 @@ async function actionGet2(args, flags) {
|
|
|
980
1253
|
err("get failed", e.message);
|
|
981
1254
|
}
|
|
982
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
|
+
}
|
|
983
1268
|
async function actionCall2(args, flags) {
|
|
984
1269
|
showHelpIfRequested(flags, CALL_HELP);
|
|
985
1270
|
const id = args[0];
|
|
986
1271
|
if (!id) err(`usage: xapi-to call <id> --input '{"key":"val"}'`);
|
|
987
1272
|
if (flags.code) validateCodeFlag(flags);
|
|
1273
|
+
if (flags.output === "true") err("--output requires a file path");
|
|
1274
|
+
const stream = flags.stream === "true" || flags.stream === "1" || flags.stream === "yes";
|
|
1275
|
+
if (flags.code && flags.output) {
|
|
1276
|
+
err("--output cannot be combined with --code");
|
|
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");
|
|
988
1280
|
const cfg = getConfig();
|
|
989
1281
|
let input = {};
|
|
990
1282
|
if (flags.input) {
|
|
@@ -998,7 +1290,7 @@ async function actionCall2(args, flags) {
|
|
|
998
1290
|
}
|
|
999
1291
|
}
|
|
1000
1292
|
const { method: inputMethod, ...cleanInput } = input;
|
|
1001
|
-
const method = flags.method
|
|
1293
|
+
const method = httpMethodFlag(flags.method) || (typeof inputMethod === "string" ? inputMethod.toUpperCase() : void 0);
|
|
1002
1294
|
if (flags.code) {
|
|
1003
1295
|
const result = generateCode(flags.code, { actionId: id, input: cleanInput, actionHost: cfg.actionHost, method });
|
|
1004
1296
|
outputCode(result, flags);
|
|
@@ -1006,6 +1298,31 @@ async function actionCall2(args, flags) {
|
|
|
1006
1298
|
}
|
|
1007
1299
|
requireApiKey(cfg);
|
|
1008
1300
|
try {
|
|
1301
|
+
if (stream) {
|
|
1302
|
+
await actionStream(id, cleanInput, cfg, method);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (flags.output) {
|
|
1306
|
+
const result = await actionDownload(
|
|
1307
|
+
id,
|
|
1308
|
+
cleanInput,
|
|
1309
|
+
cfg,
|
|
1310
|
+
flags.output,
|
|
1311
|
+
method
|
|
1312
|
+
);
|
|
1313
|
+
output(
|
|
1314
|
+
{
|
|
1315
|
+
success: true,
|
|
1316
|
+
output: result.output,
|
|
1317
|
+
bytes: result.bytes,
|
|
1318
|
+
status: result.status,
|
|
1319
|
+
content_type: result.contentType,
|
|
1320
|
+
content_disposition: result.contentDisposition
|
|
1321
|
+
},
|
|
1322
|
+
flags.format
|
|
1323
|
+
);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1009
1326
|
const res = await actionCall(id, cleanInput, cfg, method);
|
|
1010
1327
|
output(res, flags.format);
|
|
1011
1328
|
} catch (e) {
|
|
@@ -1017,6 +1334,7 @@ async function actionCall2(args, flags) {
|
|
|
1017
1334
|
var config_exports = {};
|
|
1018
1335
|
__export(config_exports, {
|
|
1019
1336
|
CONFIG_HELP: () => CONFIG_HELP,
|
|
1337
|
+
HEALTH_HELP: () => HEALTH_HELP,
|
|
1020
1338
|
configHealth: () => configHealth,
|
|
1021
1339
|
configSet: () => configSet,
|
|
1022
1340
|
configShow: () => configShow
|
|
@@ -1035,14 +1353,23 @@ COMMANDS
|
|
|
1035
1353
|
FLAGS
|
|
1036
1354
|
--format json|pretty|table Output format
|
|
1037
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
|
+
|
|
1038
1360
|
EXAMPLES
|
|
1039
1361
|
xapi-to config show
|
|
1040
1362
|
xapi-to config set apiKey=xapi_abc123
|
|
1041
1363
|
echo "$XAPI_KEY" | xapi-to config set apiKey=- # keeps the key out of shell history
|
|
1042
1364
|
xapi-to config health
|
|
1043
1365
|
`;
|
|
1366
|
+
var HEALTH_HELP = `xapi-to health - Check backend connectivity
|
|
1367
|
+
|
|
1368
|
+
USAGE
|
|
1369
|
+
xapi-to health [--format json|pretty|table]
|
|
1370
|
+
`;
|
|
1044
1371
|
async function configShow(args, flags) {
|
|
1045
|
-
showConfig();
|
|
1372
|
+
output(showConfig(), flags.format);
|
|
1046
1373
|
}
|
|
1047
1374
|
async function configSet(args, flags) {
|
|
1048
1375
|
if (args.length === 0) err("usage: xapi-to config set apiKey=<key>");
|
|
@@ -1060,10 +1387,22 @@ async function configSet(args, flags) {
|
|
|
1060
1387
|
if (!value) err("apiKey is empty");
|
|
1061
1388
|
updates.apiKey = value;
|
|
1062
1389
|
}
|
|
1390
|
+
const sourceBeforeSave = getApiKeySource();
|
|
1063
1391
|
saveConfig(updates);
|
|
1064
|
-
|
|
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);
|
|
1065
1400
|
}
|
|
1066
1401
|
async function configHealth(args, flags) {
|
|
1402
|
+
if (flags.help) {
|
|
1403
|
+
console.log(HEALTH_HELP);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1067
1406
|
const cfg = getConfig();
|
|
1068
1407
|
const start = Date.now();
|
|
1069
1408
|
try {
|
|
@@ -1076,6 +1415,30 @@ async function configHealth(args, flags) {
|
|
|
1076
1415
|
}
|
|
1077
1416
|
|
|
1078
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
|
+
}
|
|
1079
1442
|
async function registerAccount(referralCode) {
|
|
1080
1443
|
assertAllowedHost(XAPI_API_HOST);
|
|
1081
1444
|
const controller = new AbortController();
|
|
@@ -1085,39 +1448,57 @@ async function registerAccount(referralCode) {
|
|
|
1085
1448
|
method: "POST",
|
|
1086
1449
|
headers: { "Content-Type": "application/json" },
|
|
1087
1450
|
body: JSON.stringify(referralCode ? { referralCode } : {}),
|
|
1088
|
-
signal: controller.signal
|
|
1451
|
+
signal: controller.signal,
|
|
1452
|
+
redirect: "manual"
|
|
1089
1453
|
});
|
|
1454
|
+
if (res.status >= 300 && res.status < 400) {
|
|
1455
|
+
throw new Error(`refusing to follow redirect to "${res.headers.get("location") ?? "?"}"`);
|
|
1456
|
+
}
|
|
1090
1457
|
if (!res.ok) {
|
|
1091
1458
|
const text = await res.text();
|
|
1092
1459
|
throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
1093
1460
|
}
|
|
1094
|
-
return res.json();
|
|
1461
|
+
return validateRegisterResponse(await res.json());
|
|
1095
1462
|
} finally {
|
|
1096
1463
|
clearTimeout(timer);
|
|
1097
1464
|
}
|
|
1098
1465
|
}
|
|
1099
1466
|
async function register(args, flags) {
|
|
1467
|
+
if (flags.help) {
|
|
1468
|
+
console.log(REGISTER_HELP);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1100
1471
|
try {
|
|
1101
1472
|
const cfg = getConfig();
|
|
1102
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
|
+
}
|
|
1103
1481
|
if (cfg.apiKey && !force) {
|
|
1104
1482
|
err("register would overwrite existing apiKey", 'Run "xapi-to register --force" to create a new account and replace the saved key.');
|
|
1105
1483
|
}
|
|
1106
1484
|
const rawReferral = flags["referral-code"] ?? flags["referralCode"] ?? args[0];
|
|
1107
|
-
|
|
1485
|
+
if (rawReferral === "true") {
|
|
1486
|
+
err("--referral-code requires a code");
|
|
1487
|
+
}
|
|
1488
|
+
const referralCode = typeof rawReferral === "string" && rawReferral.length > 0 ? rawReferral : void 0;
|
|
1108
1489
|
const res = await registerAccount(referralCode);
|
|
1490
|
+
const bindUrl = res.bindUrl || res.claimUrl;
|
|
1109
1491
|
saveConfig({ apiKey: res.apiKey });
|
|
1110
1492
|
output({
|
|
1111
1493
|
apiKey: res.apiKey,
|
|
1112
1494
|
user: res.user,
|
|
1113
1495
|
referralCode: res.referralCode,
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
...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 } : {},
|
|
1121
1502
|
note: force && cfg.apiKey ? "apiKey replaced in ~/.xapi/config.json" : "apiKey saved to ~/.xapi/config.json"
|
|
1122
1503
|
}, flags.format);
|
|
1123
1504
|
} catch (e) {
|
|
@@ -1127,23 +1508,50 @@ async function register(args, flags) {
|
|
|
1127
1508
|
|
|
1128
1509
|
// src/commands/topup.ts
|
|
1129
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
|
+
`;
|
|
1130
1518
|
async function topup(args, flags) {
|
|
1519
|
+
if (flags.help) {
|
|
1520
|
+
console.log(TOPUP_HELP);
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1131
1523
|
const cfg = getConfig();
|
|
1132
1524
|
const url = new URL(TOPUP_BASE_URL);
|
|
1133
1525
|
if (cfg.apiKey) url.searchParams.set("apikey", cfg.apiKey);
|
|
1134
|
-
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
|
+
}
|
|
1135
1532
|
const amountStr = flags.amount || args[0];
|
|
1136
1533
|
if (amountStr) {
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
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.");
|
|
1140
1538
|
}
|
|
1539
|
+
url.searchParams.set("amount", String(amountUsd));
|
|
1141
1540
|
}
|
|
1142
1541
|
output({ url: url.toString() }, flags.format);
|
|
1143
1542
|
}
|
|
1144
1543
|
|
|
1145
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
|
+
`;
|
|
1146
1550
|
async function balance(args, flags) {
|
|
1551
|
+
if (flags.help) {
|
|
1552
|
+
console.log(BALANCE_HELP);
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1147
1555
|
const cfg = getConfig();
|
|
1148
1556
|
requireApiKey(cfg);
|
|
1149
1557
|
let token;
|
|
@@ -1180,9 +1588,10 @@ __export(oauth_exports, {
|
|
|
1180
1588
|
});
|
|
1181
1589
|
import { spawnSync } from "child_process";
|
|
1182
1590
|
function openBrowser(url) {
|
|
1183
|
-
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];
|
|
1184
1593
|
try {
|
|
1185
|
-
spawnSync(cmd,
|
|
1594
|
+
spawnSync(cmd, args, { stdio: "ignore" });
|
|
1186
1595
|
} catch {
|
|
1187
1596
|
}
|
|
1188
1597
|
}
|
|
@@ -1226,9 +1635,11 @@ async function findCurrentKeyRecord(plaintextKey, jwtToken) {
|
|
|
1226
1635
|
throw new Error("No API keys found for this account");
|
|
1227
1636
|
}
|
|
1228
1637
|
const prefix = plaintextKey.substring(0, 7);
|
|
1229
|
-
const
|
|
1638
|
+
const suffix = plaintextKey.slice(-4);
|
|
1639
|
+
const expectedPreview = `${prefix}****${suffix}`;
|
|
1640
|
+
const match = keys.find((k) => k.keyPreview === expectedPreview);
|
|
1230
1641
|
if (match) return match;
|
|
1231
|
-
if (keys.length === 1) return keys[0];
|
|
1642
|
+
if (keys.length === 1 && !keys[0].keyPreview.includes("****")) return keys[0];
|
|
1232
1643
|
throw new Error(
|
|
1233
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.`
|
|
1234
1645
|
);
|
|
@@ -1283,7 +1694,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1283
1694
|
out.write("\x1B[J");
|
|
1284
1695
|
out.write(buildFrame());
|
|
1285
1696
|
};
|
|
1286
|
-
return new Promise((
|
|
1697
|
+
return new Promise((resolve2) => {
|
|
1287
1698
|
const { stdin } = process;
|
|
1288
1699
|
const wasRaw = stdin.isRaw;
|
|
1289
1700
|
stdin.setRawMode(true);
|
|
@@ -1294,7 +1705,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1294
1705
|
stdin.pause();
|
|
1295
1706
|
out.write("\x1B[?25h");
|
|
1296
1707
|
out.write("\n");
|
|
1297
|
-
|
|
1708
|
+
resolve2(result);
|
|
1298
1709
|
};
|
|
1299
1710
|
const onData = (buf) => {
|
|
1300
1711
|
const key = buf.toString();
|
|
@@ -1345,13 +1756,20 @@ FLAGS
|
|
|
1345
1756
|
EXAMPLES
|
|
1346
1757
|
xapi-to oauth bind
|
|
1347
1758
|
xapi-to oauth bind --provider twitter
|
|
1348
|
-
xapi-to oauth
|
|
1759
|
+
xapi-to oauth providers # inspect current default scopes
|
|
1760
|
+
xapi-to oauth bind --scopes "<scope list>" # override only when needed
|
|
1349
1761
|
xapi-to oauth status
|
|
1350
1762
|
xapi-to oauth status --format pretty
|
|
1351
1763
|
xapi-to oauth unbind abc123
|
|
1352
1764
|
xapi-to oauth providers
|
|
1353
1765
|
`;
|
|
1354
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
|
+
}
|
|
1355
1773
|
const cfg = getConfig();
|
|
1356
1774
|
requireApiKey(cfg);
|
|
1357
1775
|
const apiKey = cfg.apiKey;
|
|
@@ -1409,6 +1827,16 @@ async function oauthBind(args, flags) {
|
|
|
1409
1827
|
const authorizationStartedAt = /* @__PURE__ */ new Date();
|
|
1410
1828
|
const result = await initiateOAuth(keyRecord.id, provider.id, jwtToken, XAPI_API_HOST, scopes);
|
|
1411
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
|
+
}
|
|
1412
1840
|
if (isTTY) {
|
|
1413
1841
|
if (!headerPrinted) {
|
|
1414
1842
|
console.error(`
|
|
@@ -1597,7 +2025,7 @@ function parsePositiveInt(raw, flagName) {
|
|
|
1597
2025
|
}
|
|
1598
2026
|
function sleep2(ms) {
|
|
1599
2027
|
if (ms <= 0) return Promise.resolve();
|
|
1600
|
-
return new Promise((
|
|
2028
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1601
2029
|
}
|
|
1602
2030
|
function extractTaskPayload(res) {
|
|
1603
2031
|
if (res && typeof res === "object") {
|
|
@@ -1754,6 +2182,8 @@ COMMANDS
|
|
|
1754
2182
|
--source capability|api Filter by source type
|
|
1755
2183
|
--category <name> Filter by category
|
|
1756
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
|
|
1757
2187
|
categories List all action categories
|
|
1758
2188
|
--source capability|api Filter by source type
|
|
1759
2189
|
services List all services
|
|
@@ -1761,8 +2191,11 @@ COMMANDS
|
|
|
1761
2191
|
--category <name> Filter by category
|
|
1762
2192
|
get <id> [--method GET|POST|...] Get action schema (filter by HTTP method)
|
|
1763
2193
|
--code <target> Generate code snippet (curl, py, js, ts, go)
|
|
2194
|
+
get-batch <id> [id ...] Get up to 100 action schemas
|
|
1764
2195
|
call <id> --input '{"key":"val"}' Execute an action
|
|
1765
2196
|
--method GET|POST|... Override HTTP method
|
|
2197
|
+
--output <path> Save a raw binary response to a new file
|
|
2198
|
+
--stream Forward HTTP SSE frames unchanged
|
|
1766
2199
|
--code <target> Generate code snippet instead of executing
|
|
1767
2200
|
Variants: python.requests, python.httpx, javascript.fetch, javascript.axios
|
|
1768
2201
|
|
|
@@ -1779,7 +2212,7 @@ COMMANDS
|
|
|
1779
2212
|
|
|
1780
2213
|
register [referral-code] Create a new user account (apiKey saved automatically)
|
|
1781
2214
|
--referral-code <code> Register with an inviter's referral code (also: --referralCode, or as positional arg)
|
|
1782
|
-
--force Replace an existing
|
|
2215
|
+
--force Replace an existing file-based apiKey
|
|
1783
2216
|
balance Show current account balance
|
|
1784
2217
|
topup [--amount <usd>] [--method stripe|x402] Generate payment URL
|
|
1785
2218
|
|
|
@@ -1794,9 +2227,12 @@ GLOBAL FLAGS
|
|
|
1794
2227
|
--help Show help (use with a command for details, e.g. xapi-to get --help)
|
|
1795
2228
|
|
|
1796
2229
|
ENV VARS
|
|
1797
|
-
XAPI_KEY
|
|
2230
|
+
XAPI_KEY API key (highest precedence; header: XAPI-Key)
|
|
2231
|
+
XAPI_API_KEY Compatible API key alias
|
|
1798
2232
|
XAPI_ACTION_HOST Action service host (default: action.xapi.to)
|
|
2233
|
+
XAPI_API_HOST Auth/account service host (default: api.xapi.to)
|
|
1799
2234
|
XAPI_OUTPUT Default output format
|
|
2235
|
+
XAPI_TRANSFER_IDLE_TIMEOUT_MS SSE/download idle timeout (default: 60000)
|
|
1800
2236
|
|
|
1801
2237
|
EXAMPLES
|
|
1802
2238
|
xapi-to register
|
|
@@ -1806,9 +2242,11 @@ EXAMPLES
|
|
|
1806
2242
|
xapi-to list --source capability
|
|
1807
2243
|
xapi-to search twitter --source api
|
|
1808
2244
|
xapi-to get twitter.tweet_detail
|
|
2245
|
+
xapi-to get-batch twitter.tweet_detail crypto.token.price
|
|
1809
2246
|
xapi-to get twitter.tweet_detail --code curl
|
|
1810
2247
|
xapi-to get twitter.tweet_detail --code py --format pretty
|
|
1811
2248
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
|
|
2249
|
+
xapi-to call openrouter.audio_speech --input '{"body":{"input":"Hello"}}' --output speech.mp3
|
|
1812
2250
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' --code python
|
|
1813
2251
|
xapi-to task poll 550e8400-e29b-41d4-a716-446655440000
|
|
1814
2252
|
xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 2s --timeout 10m
|
|
@@ -1823,7 +2261,16 @@ async function main() {
|
|
|
1823
2261
|
console.log(HELP);
|
|
1824
2262
|
process.exit(0);
|
|
1825
2263
|
}
|
|
1826
|
-
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
|
+
}
|
|
1827
2274
|
const [cmd, ...rest] = positional;
|
|
1828
2275
|
switch (cmd) {
|
|
1829
2276
|
// ── Action commands (top-level) ──
|
|
@@ -1837,6 +2284,8 @@ async function main() {
|
|
|
1837
2284
|
return actionServices2(rest, flags);
|
|
1838
2285
|
case "get":
|
|
1839
2286
|
return actionGet2(rest, flags);
|
|
2287
|
+
case "get-batch":
|
|
2288
|
+
return actionBatchGet(rest, flags);
|
|
1840
2289
|
case "call":
|
|
1841
2290
|
return actionCall2(rest, flags);
|
|
1842
2291
|
case "task": {
|