xapi-to 0.1.17 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/index.js +115 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,6 +122,15 @@ xapi-to get twitter.tweet_detail
|
|
|
122
122
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
For APIs that return binary data, use `--output` to request raw bytes and save
|
|
126
|
+
them directly. The CLI refuses to overwrite an existing file.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
xapi-to call openrouter.audio_speech \
|
|
130
|
+
--input '{"body":{"input":"Hello","model":"hexgrad/kokoro-82m","voice":"af_bella"}}' \
|
|
131
|
+
--output speech.mp3
|
|
132
|
+
```
|
|
133
|
+
|
|
125
134
|
## Output Formats
|
|
126
135
|
|
|
127
136
|
All output is JSON by default — designed for agent consumption.
|
package/dist/index.js
CHANGED
|
@@ -180,6 +180,10 @@ function showConfig() {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
// src/client.ts
|
|
183
|
+
import { open, rm } from "fs/promises";
|
|
184
|
+
import { resolve } from "path";
|
|
185
|
+
import { Readable, Transform } from "stream";
|
|
186
|
+
import { pipeline } from "stream/promises";
|
|
183
187
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
184
188
|
var EXECUTE_TIMEOUT_MS = 6e4;
|
|
185
189
|
var IDEMPOTENT_RETRIES = 2;
|
|
@@ -237,7 +241,7 @@ function parseRetryAfterMs(res) {
|
|
|
237
241
|
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : void 0;
|
|
238
242
|
}
|
|
239
243
|
function sleep(ms) {
|
|
240
|
-
return new Promise((
|
|
244
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
241
245
|
}
|
|
242
246
|
async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0) {
|
|
243
247
|
assertAllowedHost(url);
|
|
@@ -372,6 +376,84 @@ async function actionCall(actionId, input, opts, httpMethod, retries = 0, timeou
|
|
|
372
376
|
retries
|
|
373
377
|
);
|
|
374
378
|
}
|
|
379
|
+
async function actionDownload(actionId, input, opts, outputPath, httpMethod) {
|
|
380
|
+
const controller = new AbortController();
|
|
381
|
+
let timedOut = false;
|
|
382
|
+
const timer = setTimeout(() => {
|
|
383
|
+
timedOut = true;
|
|
384
|
+
controller.abort();
|
|
385
|
+
}, EXECUTE_TIMEOUT_MS);
|
|
386
|
+
const target = resolve(outputPath);
|
|
387
|
+
let file;
|
|
388
|
+
let complete = false;
|
|
389
|
+
try {
|
|
390
|
+
try {
|
|
391
|
+
file = await open(target, "wx");
|
|
392
|
+
} catch (error) {
|
|
393
|
+
if (error?.code === "EEXIST") {
|
|
394
|
+
throw new Error(`Output file already exists: ${target}`);
|
|
395
|
+
}
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
const url = `${baseUrl(opts)}/v1/actions/execute`;
|
|
399
|
+
assertAllowedHost(url);
|
|
400
|
+
const res = await fetch(url, {
|
|
401
|
+
method: "POST",
|
|
402
|
+
headers: headers(opts.apiKey),
|
|
403
|
+
body: JSON.stringify({
|
|
404
|
+
action_id: actionId,
|
|
405
|
+
...httpMethod ? { method: httpMethod } : {},
|
|
406
|
+
input,
|
|
407
|
+
response_mode: "raw"
|
|
408
|
+
}),
|
|
409
|
+
redirect: "manual",
|
|
410
|
+
signal: controller.signal
|
|
411
|
+
});
|
|
412
|
+
if (res.status >= 300 && res.status < 400) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if (!res.ok) {
|
|
418
|
+
const text = await res.text();
|
|
419
|
+
throw new HttpError(
|
|
420
|
+
res.status,
|
|
421
|
+
text.slice(0, 300),
|
|
422
|
+
isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
let bytes = 0;
|
|
426
|
+
if (res.body) {
|
|
427
|
+
const source = Readable.fromWeb(res.body);
|
|
428
|
+
const counter = new Transform({
|
|
429
|
+
transform(chunk, _encoding, callback) {
|
|
430
|
+
bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
|
|
431
|
+
callback(null, chunk);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
await pipeline(source, counter, file.createWriteStream());
|
|
435
|
+
} else {
|
|
436
|
+
await file.close();
|
|
437
|
+
}
|
|
438
|
+
complete = true;
|
|
439
|
+
return {
|
|
440
|
+
output: target,
|
|
441
|
+
bytes,
|
|
442
|
+
contentType: res.headers.get("content-type") || void 0,
|
|
443
|
+
contentDisposition: res.headers.get("content-disposition") || void 0,
|
|
444
|
+
status: res.status
|
|
445
|
+
};
|
|
446
|
+
} catch (error) {
|
|
447
|
+
if (timedOut) throw new RequestTimeoutError(EXECUTE_TIMEOUT_MS);
|
|
448
|
+
throw error;
|
|
449
|
+
} finally {
|
|
450
|
+
clearTimeout(timer);
|
|
451
|
+
if (!complete && file) {
|
|
452
|
+
await file.close().catch(() => void 0);
|
|
453
|
+
await rm(target, { force: true }).catch(() => void 0);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
375
457
|
async function actionServices(opts, params = {}) {
|
|
376
458
|
const url = new URL(`${baseUrl(opts)}/v1/actions/services`);
|
|
377
459
|
if (params.page) url.searchParams.set("page", String(params.page));
|
|
@@ -795,6 +877,7 @@ USAGE
|
|
|
795
877
|
FLAGS
|
|
796
878
|
--input <json> Input payload as JSON (required for execution)
|
|
797
879
|
--method GET|POST|... Override HTTP method
|
|
880
|
+
--output <path> Save a raw binary response to a new file
|
|
798
881
|
--code <target> Generate code snippet instead of executing
|
|
799
882
|
--format json|pretty|table Output format
|
|
800
883
|
|
|
@@ -817,6 +900,7 @@ CODE TARGETS
|
|
|
817
900
|
|
|
818
901
|
EXAMPLES
|
|
819
902
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
|
|
903
|
+
xapi-to call openrouter.audio_speech --input '{"body":{"input":"Hello"}}' --output speech.mp3
|
|
820
904
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code py
|
|
821
905
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code curl --format pretty
|
|
822
906
|
`;
|
|
@@ -985,6 +1069,10 @@ async function actionCall2(args, flags) {
|
|
|
985
1069
|
const id = args[0];
|
|
986
1070
|
if (!id) err(`usage: xapi-to call <id> --input '{"key":"val"}'`);
|
|
987
1071
|
if (flags.code) validateCodeFlag(flags);
|
|
1072
|
+
if (flags.output === "true") err("--output requires a file path");
|
|
1073
|
+
if (flags.code && flags.output) {
|
|
1074
|
+
err("--output cannot be combined with --code");
|
|
1075
|
+
}
|
|
988
1076
|
const cfg = getConfig();
|
|
989
1077
|
let input = {};
|
|
990
1078
|
if (flags.input) {
|
|
@@ -1006,6 +1094,27 @@ async function actionCall2(args, flags) {
|
|
|
1006
1094
|
}
|
|
1007
1095
|
requireApiKey(cfg);
|
|
1008
1096
|
try {
|
|
1097
|
+
if (flags.output) {
|
|
1098
|
+
const result = await actionDownload(
|
|
1099
|
+
id,
|
|
1100
|
+
cleanInput,
|
|
1101
|
+
cfg,
|
|
1102
|
+
flags.output,
|
|
1103
|
+
method
|
|
1104
|
+
);
|
|
1105
|
+
output(
|
|
1106
|
+
{
|
|
1107
|
+
success: true,
|
|
1108
|
+
output: result.output,
|
|
1109
|
+
bytes: result.bytes,
|
|
1110
|
+
status: result.status,
|
|
1111
|
+
content_type: result.contentType,
|
|
1112
|
+
content_disposition: result.contentDisposition
|
|
1113
|
+
},
|
|
1114
|
+
flags.format
|
|
1115
|
+
);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1009
1118
|
const res = await actionCall(id, cleanInput, cfg, method);
|
|
1010
1119
|
output(res, flags.format);
|
|
1011
1120
|
} catch (e) {
|
|
@@ -1283,7 +1392,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1283
1392
|
out.write("\x1B[J");
|
|
1284
1393
|
out.write(buildFrame());
|
|
1285
1394
|
};
|
|
1286
|
-
return new Promise((
|
|
1395
|
+
return new Promise((resolve2) => {
|
|
1287
1396
|
const { stdin } = process;
|
|
1288
1397
|
const wasRaw = stdin.isRaw;
|
|
1289
1398
|
stdin.setRawMode(true);
|
|
@@ -1294,7 +1403,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1294
1403
|
stdin.pause();
|
|
1295
1404
|
out.write("\x1B[?25h");
|
|
1296
1405
|
out.write("\n");
|
|
1297
|
-
|
|
1406
|
+
resolve2(result);
|
|
1298
1407
|
};
|
|
1299
1408
|
const onData = (buf) => {
|
|
1300
1409
|
const key = buf.toString();
|
|
@@ -1597,7 +1706,7 @@ function parsePositiveInt(raw, flagName) {
|
|
|
1597
1706
|
}
|
|
1598
1707
|
function sleep2(ms) {
|
|
1599
1708
|
if (ms <= 0) return Promise.resolve();
|
|
1600
|
-
return new Promise((
|
|
1709
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1601
1710
|
}
|
|
1602
1711
|
function extractTaskPayload(res) {
|
|
1603
1712
|
if (res && typeof res === "object") {
|
|
@@ -1763,6 +1872,7 @@ COMMANDS
|
|
|
1763
1872
|
--code <target> Generate code snippet (curl, py, js, ts, go)
|
|
1764
1873
|
call <id> --input '{"key":"val"}' Execute an action
|
|
1765
1874
|
--method GET|POST|... Override HTTP method
|
|
1875
|
+
--output <path> Save a raw binary response to a new file
|
|
1766
1876
|
--code <target> Generate code snippet instead of executing
|
|
1767
1877
|
Variants: python.requests, python.httpx, javascript.fetch, javascript.axios
|
|
1768
1878
|
|
|
@@ -1809,6 +1919,7 @@ EXAMPLES
|
|
|
1809
1919
|
xapi-to get twitter.tweet_detail --code curl
|
|
1810
1920
|
xapi-to get twitter.tweet_detail --code py --format pretty
|
|
1811
1921
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
|
|
1922
|
+
xapi-to call openrouter.audio_speech --input '{"body":{"input":"Hello"}}' --output speech.mp3
|
|
1812
1923
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' --code python
|
|
1813
1924
|
xapi-to task poll 550e8400-e29b-41d4-a716-446655440000
|
|
1814
1925
|
xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 2s --timeout 10m
|