xapi-to 0.1.16 → 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 +19 -0
- package/dist/index.js +548 -57
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -66,6 +66,16 @@ xapi-to get twitter.tweet_detail # get action schema
|
|
|
66
66
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' # execute
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### Async Task Commands
|
|
70
|
+
|
|
71
|
+
Task helpers built on top of the `task.poll` capability.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
xapi-to task poll 550e8400-e29b-41d4-a716-446655440000 # poll once
|
|
75
|
+
xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 # wait until terminal status
|
|
76
|
+
xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 1s --timeout 10m
|
|
77
|
+
```
|
|
78
|
+
|
|
69
79
|
### OAuth
|
|
70
80
|
|
|
71
81
|
Bind third-party OAuth accounts (e.g. Twitter) to your API key.
|
|
@@ -112,6 +122,15 @@ xapi-to get twitter.tweet_detail
|
|
|
112
122
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
|
|
113
123
|
```
|
|
114
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
|
+
|
|
115
134
|
## Output Formats
|
|
116
135
|
|
|
117
136
|
All output is JSON by default — designed for agent consumption.
|
package/dist/index.js
CHANGED
|
@@ -100,7 +100,43 @@ import { join } from "path";
|
|
|
100
100
|
var XAPI_ACTION_HOST = process.env.XAPI_ACTION_HOST || "action.xapi.to";
|
|
101
101
|
var XAPI_API_HOST = process.env.XAPI_API_HOST || "api.xapi.to";
|
|
102
102
|
function scheme(host) {
|
|
103
|
-
return
|
|
103
|
+
return isLoopbackHost(host) ? "http" : "https";
|
|
104
|
+
}
|
|
105
|
+
var ALLOWED_HOST_EXACT = ["xapi.to", "xapi.xyz"];
|
|
106
|
+
var ALLOWED_HOST_SUFFIXES = [".xapi.to", ".xapi.xyz"];
|
|
107
|
+
function hostnameOf(hostOrUrl) {
|
|
108
|
+
const raw = hostOrUrl.includes("://") ? hostOrUrl : `http://${hostOrUrl}`;
|
|
109
|
+
try {
|
|
110
|
+
return new URL(raw).hostname.toLowerCase();
|
|
111
|
+
} catch {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function isLoopbackIPv4(h) {
|
|
116
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
117
|
+
if (!m) return false;
|
|
118
|
+
const octets = m.slice(1).map(Number);
|
|
119
|
+
return octets.every((o) => o <= 255) && octets[0] === 127;
|
|
120
|
+
}
|
|
121
|
+
function isLoopbackHostname(h) {
|
|
122
|
+
return h === "localhost" || h.endsWith(".localhost") || h === "::1" || h === "[::1]" || isLoopbackIPv4(h);
|
|
123
|
+
}
|
|
124
|
+
function isLoopbackHost(hostOrUrl) {
|
|
125
|
+
return isLoopbackHostname(hostnameOf(hostOrUrl));
|
|
126
|
+
}
|
|
127
|
+
function isAllowedHost(hostOrUrl) {
|
|
128
|
+
const h = hostnameOf(hostOrUrl);
|
|
129
|
+
if (!h) return false;
|
|
130
|
+
if (isLoopbackHostname(h)) return true;
|
|
131
|
+
if (ALLOWED_HOST_EXACT.includes(h)) return true;
|
|
132
|
+
return ALLOWED_HOST_SUFFIXES.some((suffix) => h.endsWith(suffix));
|
|
133
|
+
}
|
|
134
|
+
function assertAllowedHost(hostOrUrl) {
|
|
135
|
+
if (!isAllowedHost(hostOrUrl)) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`refusing to contact untrusted host "${hostnameOf(hostOrUrl) || hostOrUrl}": the xapi API key may only be sent to *.xapi.to, *.xapi.xyz, or localhost`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
104
140
|
}
|
|
105
141
|
var CONFIG_DIR = join(homedir(), ".xapi");
|
|
106
142
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
@@ -144,41 +180,134 @@ function showConfig() {
|
|
|
144
180
|
}
|
|
145
181
|
|
|
146
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";
|
|
147
187
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
148
188
|
var EXECUTE_TIMEOUT_MS = 6e4;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
189
|
+
var IDEMPOTENT_RETRIES = 2;
|
|
190
|
+
var RETRY_BASE_DELAY_MS = 500;
|
|
191
|
+
var RETRY_MAX_DELAY_MS = 8e3;
|
|
192
|
+
var HttpError = class extends Error {
|
|
193
|
+
constructor(status, detail, retryAfterMs) {
|
|
194
|
+
super(`HTTP ${status}: ${detail}`);
|
|
195
|
+
this.status = status;
|
|
196
|
+
this.retryAfterMs = retryAfterMs;
|
|
197
|
+
this.name = "HttpError";
|
|
198
|
+
}
|
|
199
|
+
status;
|
|
200
|
+
retryAfterMs;
|
|
201
|
+
};
|
|
202
|
+
var RequestTimeoutError = class extends Error {
|
|
203
|
+
constructor(timeoutMs) {
|
|
204
|
+
super(`request timed out after ${timeoutMs}ms`);
|
|
205
|
+
this.timeoutMs = timeoutMs;
|
|
206
|
+
this.name = "RequestTimeoutError";
|
|
207
|
+
}
|
|
208
|
+
timeoutMs;
|
|
209
|
+
};
|
|
210
|
+
function isRetryableStatus(status) {
|
|
211
|
+
return status === 408 || status === 429 || status === 502 || status === 503 || status === 504;
|
|
212
|
+
}
|
|
213
|
+
function isRetryableNetworkError(e) {
|
|
214
|
+
if (!(e instanceof Error)) return false;
|
|
215
|
+
if (e instanceof HttpError || e instanceof RequestTimeoutError) return false;
|
|
216
|
+
if (e.name === "AbortError") return false;
|
|
217
|
+
return e instanceof TypeError || /network|fetch failed|econn|etimedout|eai_again|socket|dns/i.test(e.message);
|
|
218
|
+
}
|
|
219
|
+
function isRetryableRequestError(e) {
|
|
220
|
+
if (e instanceof HttpError) return isRetryableStatus(e.status);
|
|
221
|
+
if (e instanceof RequestTimeoutError) return true;
|
|
222
|
+
return isRetryableNetworkError(e);
|
|
223
|
+
}
|
|
224
|
+
function retryBaseDelayMs() {
|
|
225
|
+
const override = Number(process.env.XAPI_RETRY_BASE_MS);
|
|
226
|
+
return Number.isFinite(override) && override > 0 ? override : RETRY_BASE_DELAY_MS;
|
|
227
|
+
}
|
|
228
|
+
function backoffDelayMs(attempt, retryAfterMs) {
|
|
229
|
+
if (retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
|
|
230
|
+
return Math.min(retryAfterMs, RETRY_MAX_DELAY_MS);
|
|
231
|
+
}
|
|
232
|
+
const capped = Math.min(retryBaseDelayMs() * 2 ** attempt, RETRY_MAX_DELAY_MS);
|
|
233
|
+
return capped / 2 + Math.random() * (capped / 2);
|
|
234
|
+
}
|
|
235
|
+
function parseRetryAfterMs(res) {
|
|
236
|
+
const header = res.headers.get("retry-after");
|
|
237
|
+
if (!header) return void 0;
|
|
238
|
+
const seconds = Number(header);
|
|
239
|
+
if (Number.isFinite(seconds)) return seconds * 1e3;
|
|
240
|
+
const at = Date.parse(header);
|
|
241
|
+
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : void 0;
|
|
242
|
+
}
|
|
243
|
+
function sleep(ms) {
|
|
244
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
245
|
+
}
|
|
246
|
+
async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0) {
|
|
247
|
+
assertAllowedHost(url);
|
|
248
|
+
let attempt = 0;
|
|
249
|
+
while (true) {
|
|
250
|
+
const controller = new AbortController();
|
|
251
|
+
let timedOut = false;
|
|
252
|
+
const timer = setTimeout(() => {
|
|
253
|
+
timedOut = true;
|
|
254
|
+
controller.abort();
|
|
255
|
+
}, timeoutMs);
|
|
256
|
+
try {
|
|
257
|
+
const res = await fetch(url, { ...options, redirect: "manual", signal: controller.signal });
|
|
258
|
+
if (res.status >= 300 && res.status < 400) {
|
|
169
259
|
throw new Error(
|
|
170
|
-
|
|
260
|
+
`refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
|
|
171
261
|
);
|
|
172
262
|
}
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
263
|
+
if (!res.ok) {
|
|
264
|
+
const retryAfterMs = isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0;
|
|
265
|
+
if (isRetryableStatus(res.status) && attempt < retries) {
|
|
266
|
+
await res.text().catch(() => "");
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
await sleep(backoffDelayMs(attempt, retryAfterMs));
|
|
269
|
+
attempt++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const text2 = await res.text();
|
|
273
|
+
throw new HttpError(res.status, text2.slice(0, 300), retryAfterMs);
|
|
274
|
+
}
|
|
275
|
+
if (res.status === 204) {
|
|
276
|
+
return void 0;
|
|
277
|
+
}
|
|
278
|
+
const text = await res.text();
|
|
279
|
+
if (!text.trim()) {
|
|
280
|
+
return void 0;
|
|
281
|
+
}
|
|
282
|
+
const body = JSON.parse(text);
|
|
283
|
+
if (body && typeof body === "object" && "success" in body && body.success === false) {
|
|
284
|
+
const data = body.data;
|
|
285
|
+
if (data?.statusCode === 401 || data?.error === "Unauthorized") {
|
|
286
|
+
throw new Error(
|
|
287
|
+
"Authentication failed: " + (data.message || "Invalid or missing API key") + '. Run "npx xapi-to config set apiKey=<key>" to update your key.'
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (data?.error === "OAuth Required" || data?.statusCode === 403 && data?.message?.includes("OAuth")) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
(data.message || "OAuth authorization required") + '. Run "xapi-to oauth bind" to connect your account.'
|
|
293
|
+
);
|
|
294
|
+
}
|
|
177
295
|
}
|
|
296
|
+
return body;
|
|
297
|
+
} catch (e) {
|
|
298
|
+
if (timedOut) {
|
|
299
|
+
throw new RequestTimeoutError(timeoutMs);
|
|
300
|
+
}
|
|
301
|
+
if (isRetryableNetworkError(e) && attempt < retries) {
|
|
302
|
+
clearTimeout(timer);
|
|
303
|
+
await sleep(backoffDelayMs(attempt));
|
|
304
|
+
attempt++;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
throw e;
|
|
308
|
+
} finally {
|
|
309
|
+
clearTimeout(timer);
|
|
178
310
|
}
|
|
179
|
-
return body;
|
|
180
|
-
} finally {
|
|
181
|
-
clearTimeout(timer);
|
|
182
311
|
}
|
|
183
312
|
}
|
|
184
313
|
function headers(apiKey) {
|
|
@@ -198,7 +327,9 @@ async function actionList(opts, params = {}) {
|
|
|
198
327
|
if (params.service_id) url.searchParams.set("service_id", params.service_id);
|
|
199
328
|
return request(
|
|
200
329
|
url.toString(),
|
|
201
|
-
{ method: "GET", headers: headers(opts.apiKey) }
|
|
330
|
+
{ method: "GET", headers: headers(opts.apiKey) },
|
|
331
|
+
DEFAULT_TIMEOUT_MS,
|
|
332
|
+
IDEMPOTENT_RETRIES
|
|
202
333
|
);
|
|
203
334
|
}
|
|
204
335
|
async function actionSearch(query, opts, params = {}) {
|
|
@@ -210,7 +341,9 @@ async function actionSearch(query, opts, params = {}) {
|
|
|
210
341
|
if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
|
|
211
342
|
return request(
|
|
212
343
|
url.toString(),
|
|
213
|
-
{ method: "GET", headers: headers(opts.apiKey) }
|
|
344
|
+
{ method: "GET", headers: headers(opts.apiKey) },
|
|
345
|
+
DEFAULT_TIMEOUT_MS,
|
|
346
|
+
IDEMPOTENT_RETRIES
|
|
214
347
|
);
|
|
215
348
|
}
|
|
216
349
|
async function actionCategories(opts, params = {}) {
|
|
@@ -218,16 +351,20 @@ async function actionCategories(opts, params = {}) {
|
|
|
218
351
|
if (params.source) url.searchParams.set("source", params.source);
|
|
219
352
|
return request(
|
|
220
353
|
url.toString(),
|
|
221
|
-
{ method: "GET", headers: headers(opts.apiKey) }
|
|
354
|
+
{ method: "GET", headers: headers(opts.apiKey) },
|
|
355
|
+
DEFAULT_TIMEOUT_MS,
|
|
356
|
+
IDEMPOTENT_RETRIES
|
|
222
357
|
);
|
|
223
358
|
}
|
|
224
359
|
async function actionGet(id, opts) {
|
|
225
360
|
return request(
|
|
226
361
|
`${baseUrl(opts)}/v1/actions/${encodeURIComponent(id)}`,
|
|
227
|
-
{ method: "GET", headers: headers(opts.apiKey) }
|
|
362
|
+
{ method: "GET", headers: headers(opts.apiKey) },
|
|
363
|
+
DEFAULT_TIMEOUT_MS,
|
|
364
|
+
IDEMPOTENT_RETRIES
|
|
228
365
|
);
|
|
229
366
|
}
|
|
230
|
-
async function actionCall(actionId, input, opts, httpMethod) {
|
|
367
|
+
async function actionCall(actionId, input, opts, httpMethod, retries = 0, timeoutMs = EXECUTE_TIMEOUT_MS) {
|
|
231
368
|
return request(
|
|
232
369
|
`${baseUrl(opts)}/v1/actions/execute`,
|
|
233
370
|
{
|
|
@@ -235,9 +372,88 @@ async function actionCall(actionId, input, opts, httpMethod) {
|
|
|
235
372
|
headers: headers(opts.apiKey),
|
|
236
373
|
body: JSON.stringify({ action_id: actionId, ...httpMethod ? { method: httpMethod } : {}, input })
|
|
237
374
|
},
|
|
238
|
-
EXECUTE_TIMEOUT_MS
|
|
375
|
+
Math.min(timeoutMs, EXECUTE_TIMEOUT_MS),
|
|
376
|
+
retries
|
|
239
377
|
);
|
|
240
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
|
+
}
|
|
241
457
|
async function actionServices(opts, params = {}) {
|
|
242
458
|
const url = new URL(`${baseUrl(opts)}/v1/actions/services`);
|
|
243
459
|
if (params.page) url.searchParams.set("page", String(params.page));
|
|
@@ -245,14 +461,18 @@ async function actionServices(opts, params = {}) {
|
|
|
245
461
|
if (params.category) url.searchParams.set("category", params.category);
|
|
246
462
|
return request(
|
|
247
463
|
url.toString(),
|
|
248
|
-
{ method: "GET", headers: headers(opts.apiKey) }
|
|
464
|
+
{ method: "GET", headers: headers(opts.apiKey) },
|
|
465
|
+
DEFAULT_TIMEOUT_MS,
|
|
466
|
+
IDEMPOTENT_RETRIES
|
|
249
467
|
);
|
|
250
468
|
}
|
|
251
469
|
async function healthCheck(opts) {
|
|
252
470
|
return request(
|
|
253
471
|
`${baseUrl(opts)}/health`,
|
|
254
472
|
{ method: "GET", headers: headers(opts.apiKey) },
|
|
255
|
-
5e3
|
|
473
|
+
5e3,
|
|
474
|
+
0
|
|
475
|
+
// health is a quick connectivity probe — fail fast, don't retry
|
|
256
476
|
);
|
|
257
477
|
}
|
|
258
478
|
async function loginWithApiKey(apiKey, apiHost) {
|
|
@@ -262,7 +482,10 @@ async function loginWithApiKey(apiKey, apiHost) {
|
|
|
262
482
|
method: "POST",
|
|
263
483
|
headers: { "Content-Type": "application/json" },
|
|
264
484
|
body: JSON.stringify({ apiKey })
|
|
265
|
-
}
|
|
485
|
+
},
|
|
486
|
+
DEFAULT_TIMEOUT_MS,
|
|
487
|
+
IDEMPOTENT_RETRIES
|
|
488
|
+
// auth exchange has no side effect — safe to retry
|
|
266
489
|
);
|
|
267
490
|
}
|
|
268
491
|
function jwtHeaders(jwtToken) {
|
|
@@ -271,7 +494,9 @@ function jwtHeaders(jwtToken) {
|
|
|
271
494
|
async function listKeys(jwtToken, apiHost) {
|
|
272
495
|
return request(
|
|
273
496
|
`${scheme(apiHost)}://${apiHost}/api/keys`,
|
|
274
|
-
{ method: "GET", headers: jwtHeaders(jwtToken) }
|
|
497
|
+
{ method: "GET", headers: jwtHeaders(jwtToken) },
|
|
498
|
+
DEFAULT_TIMEOUT_MS,
|
|
499
|
+
IDEMPOTENT_RETRIES
|
|
275
500
|
);
|
|
276
501
|
}
|
|
277
502
|
async function enableOAuthForKey(keyId, plaintextKey, jwtToken, apiHost) {
|
|
@@ -287,7 +512,9 @@ async function enableOAuthForKey(keyId, plaintextKey, jwtToken, apiHost) {
|
|
|
287
512
|
async function listOAuthProviders(apiHost) {
|
|
288
513
|
return request(
|
|
289
514
|
`${scheme(apiHost)}://${apiHost}/api/oauth/providers`,
|
|
290
|
-
{ method: "GET", headers: { "Content-Type": "application/json" } }
|
|
515
|
+
{ method: "GET", headers: { "Content-Type": "application/json" } },
|
|
516
|
+
DEFAULT_TIMEOUT_MS,
|
|
517
|
+
IDEMPOTENT_RETRIES
|
|
291
518
|
);
|
|
292
519
|
}
|
|
293
520
|
async function initiateOAuth(apiKeyId, providerId, jwtToken, apiHost, scopes) {
|
|
@@ -305,7 +532,9 @@ async function initiateOAuth(apiKeyId, providerId, jwtToken, apiHost, scopes) {
|
|
|
305
532
|
async function listOAuthBindings(jwtToken, apiHost) {
|
|
306
533
|
return request(
|
|
307
534
|
`${scheme(apiHost)}://${apiHost}/api/oauth/bindings`,
|
|
308
|
-
{ method: "GET", headers: jwtHeaders(jwtToken) }
|
|
535
|
+
{ method: "GET", headers: jwtHeaders(jwtToken) },
|
|
536
|
+
DEFAULT_TIMEOUT_MS,
|
|
537
|
+
IDEMPOTENT_RETRIES
|
|
309
538
|
);
|
|
310
539
|
}
|
|
311
540
|
async function deleteOAuthBinding(bindingId, jwtToken, apiHost) {
|
|
@@ -648,6 +877,7 @@ USAGE
|
|
|
648
877
|
FLAGS
|
|
649
878
|
--input <json> Input payload as JSON (required for execution)
|
|
650
879
|
--method GET|POST|... Override HTTP method
|
|
880
|
+
--output <path> Save a raw binary response to a new file
|
|
651
881
|
--code <target> Generate code snippet instead of executing
|
|
652
882
|
--format json|pretty|table Output format
|
|
653
883
|
|
|
@@ -670,6 +900,7 @@ CODE TARGETS
|
|
|
670
900
|
|
|
671
901
|
EXAMPLES
|
|
672
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
|
|
673
904
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code py
|
|
674
905
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code curl --format pretty
|
|
675
906
|
`;
|
|
@@ -838,6 +1069,10 @@ async function actionCall2(args, flags) {
|
|
|
838
1069
|
const id = args[0];
|
|
839
1070
|
if (!id) err(`usage: xapi-to call <id> --input '{"key":"val"}'`);
|
|
840
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
|
+
}
|
|
841
1076
|
const cfg = getConfig();
|
|
842
1077
|
let input = {};
|
|
843
1078
|
if (flags.input) {
|
|
@@ -859,6 +1094,27 @@ async function actionCall2(args, flags) {
|
|
|
859
1094
|
}
|
|
860
1095
|
requireApiKey(cfg);
|
|
861
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
|
+
}
|
|
862
1118
|
const res = await actionCall(id, cleanInput, cfg, method);
|
|
863
1119
|
output(res, flags.format);
|
|
864
1120
|
} catch (e) {
|
|
@@ -874,6 +1130,7 @@ __export(config_exports, {
|
|
|
874
1130
|
configSet: () => configSet,
|
|
875
1131
|
configShow: () => configShow
|
|
876
1132
|
});
|
|
1133
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
877
1134
|
var CONFIG_HELP = `xapi-to config - Manage CLI configuration
|
|
878
1135
|
|
|
879
1136
|
USAGE
|
|
@@ -881,7 +1138,7 @@ USAGE
|
|
|
881
1138
|
|
|
882
1139
|
COMMANDS
|
|
883
1140
|
show Show current config (host, apiKey path, etc.)
|
|
884
|
-
set apiKey=<key> Save API key to ~/.xapi/config.json
|
|
1141
|
+
set apiKey=<key> Save API key to ~/.xapi/config.json (apiKey=- reads from stdin)
|
|
885
1142
|
health Check backend connectivity (alias: xapi-to health)
|
|
886
1143
|
|
|
887
1144
|
FLAGS
|
|
@@ -890,6 +1147,7 @@ FLAGS
|
|
|
890
1147
|
EXAMPLES
|
|
891
1148
|
xapi-to config show
|
|
892
1149
|
xapi-to config set apiKey=xapi_abc123
|
|
1150
|
+
echo "$XAPI_KEY" | xapi-to config set apiKey=- # keeps the key out of shell history
|
|
893
1151
|
xapi-to config health
|
|
894
1152
|
`;
|
|
895
1153
|
async function configShow(args, flags) {
|
|
@@ -904,7 +1162,12 @@ async function configSet(args, flags) {
|
|
|
904
1162
|
const key = arg.slice(0, eq);
|
|
905
1163
|
if (key === "host") err("host is built-in and cannot be configured");
|
|
906
1164
|
if (key !== "apiKey") err(`unknown config key: ${key} (only apiKey is configurable)`);
|
|
907
|
-
|
|
1165
|
+
let value = arg.slice(eq + 1);
|
|
1166
|
+
if (value === "-") {
|
|
1167
|
+
value = readFileSync2(0, "utf-8").trim();
|
|
1168
|
+
}
|
|
1169
|
+
if (!value) err("apiKey is empty");
|
|
1170
|
+
updates.apiKey = value;
|
|
908
1171
|
}
|
|
909
1172
|
saveConfig(updates);
|
|
910
1173
|
console.log(JSON.stringify({ ok: true, updated: Object.keys(updates) }));
|
|
@@ -923,6 +1186,7 @@ async function configHealth(args, flags) {
|
|
|
923
1186
|
|
|
924
1187
|
// src/commands/register.ts
|
|
925
1188
|
async function registerAccount(referralCode) {
|
|
1189
|
+
assertAllowedHost(XAPI_API_HOST);
|
|
926
1190
|
const controller = new AbortController();
|
|
927
1191
|
const timer = setTimeout(() => controller.abort(), 15e3);
|
|
928
1192
|
try {
|
|
@@ -1070,15 +1334,13 @@ async function findCurrentKeyRecord(plaintextKey, jwtToken) {
|
|
|
1070
1334
|
if (!Array.isArray(keys) || keys.length === 0) {
|
|
1071
1335
|
throw new Error("No API keys found for this account");
|
|
1072
1336
|
}
|
|
1073
|
-
if (keys.length === 1) return keys[0];
|
|
1074
1337
|
const prefix = plaintextKey.substring(0, 7);
|
|
1075
1338
|
const match = keys.find((k) => k.keyPreview.startsWith(prefix));
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
)
|
|
1080
|
-
|
|
1081
|
-
return match;
|
|
1339
|
+
if (match) return match;
|
|
1340
|
+
if (keys.length === 1) return keys[0];
|
|
1341
|
+
throw new Error(
|
|
1342
|
+
`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
|
+
);
|
|
1082
1344
|
}
|
|
1083
1345
|
function resolveScopeDefs(provider) {
|
|
1084
1346
|
if (Array.isArray(provider.scopeDefinitions) && provider.scopeDefinitions.length > 0) {
|
|
@@ -1130,7 +1392,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1130
1392
|
out.write("\x1B[J");
|
|
1131
1393
|
out.write(buildFrame());
|
|
1132
1394
|
};
|
|
1133
|
-
return new Promise((
|
|
1395
|
+
return new Promise((resolve2) => {
|
|
1134
1396
|
const { stdin } = process;
|
|
1135
1397
|
const wasRaw = stdin.isRaw;
|
|
1136
1398
|
stdin.setRawMode(true);
|
|
@@ -1141,7 +1403,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1141
1403
|
stdin.pause();
|
|
1142
1404
|
out.write("\x1B[?25h");
|
|
1143
1405
|
out.write("\n");
|
|
1144
|
-
|
|
1406
|
+
resolve2(result);
|
|
1145
1407
|
};
|
|
1146
1408
|
const onData = (buf) => {
|
|
1147
1409
|
const key = buf.toString();
|
|
@@ -1358,19 +1620,217 @@ async function oauthProviders(args, flags) {
|
|
|
1358
1620
|
}
|
|
1359
1621
|
}
|
|
1360
1622
|
|
|
1361
|
-
// src/
|
|
1362
|
-
var
|
|
1363
|
-
|
|
1623
|
+
// src/commands/task.ts
|
|
1624
|
+
var POLL_HELP = `xapi-to task poll - Poll an async task once
|
|
1625
|
+
|
|
1626
|
+
USAGE
|
|
1627
|
+
xapi-to task poll <task_id> [flags]
|
|
1628
|
+
|
|
1629
|
+
FLAGS
|
|
1630
|
+
--format json|pretty|table Output format
|
|
1631
|
+
|
|
1632
|
+
EXAMPLES
|
|
1633
|
+
xapi-to task poll 550e8400-e29b-41d4-a716-446655440000
|
|
1634
|
+
`;
|
|
1635
|
+
var WAIT_HELP = `xapi-to task wait - Wait until async task reaches terminal status
|
|
1636
|
+
|
|
1637
|
+
USAGE
|
|
1638
|
+
xapi-to task wait <task_id> [flags]
|
|
1639
|
+
|
|
1640
|
+
FLAGS
|
|
1641
|
+
--interval <duration> Poll interval (default: 2s). Supports ms/s/m/h
|
|
1642
|
+
--timeout <duration> Max wait duration (optional). Supports ms/s/m/h
|
|
1643
|
+
--max-attempts <number> Max poll attempts (optional)
|
|
1644
|
+
--format json|pretty|table Output format
|
|
1645
|
+
|
|
1646
|
+
EXAMPLES
|
|
1647
|
+
xapi-to task wait 550e8400-e29b-41d4-a716-446655440000
|
|
1648
|
+
xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 1s --timeout 10m
|
|
1649
|
+
`;
|
|
1650
|
+
var TASK_HELP = `xapi-to task - Async task helpers
|
|
1651
|
+
|
|
1652
|
+
USAGE
|
|
1653
|
+
xapi-to task <command> [args] [flags]
|
|
1654
|
+
|
|
1655
|
+
COMMANDS
|
|
1656
|
+
poll <task_id> Poll task status once (wraps action: task.poll)
|
|
1657
|
+
wait <task_id> Poll repeatedly until terminal status
|
|
1658
|
+
|
|
1659
|
+
EXAMPLES
|
|
1660
|
+
xapi-to task poll 550e8400-e29b-41d4-a716-446655440000
|
|
1661
|
+
xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 2s --timeout 10m
|
|
1662
|
+
`;
|
|
1663
|
+
function showHelpIfRequested2(flags, helpText) {
|
|
1664
|
+
if (flags.help) {
|
|
1665
|
+
console.log(helpText);
|
|
1666
|
+
process.exit(0);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
function parseDurationMs(raw) {
|
|
1670
|
+
const text = raw.trim().toLowerCase();
|
|
1671
|
+
const match = text.match(/^(\d+)(ms|s|m|h)?$/);
|
|
1672
|
+
if (!match) {
|
|
1673
|
+
err(`invalid duration "${raw}". Use formats like 500ms, 2s, 5m, 1h.`);
|
|
1674
|
+
}
|
|
1675
|
+
const value = Number(match[1]);
|
|
1676
|
+
const unit = match[2] || "ms";
|
|
1677
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1678
|
+
err(`invalid duration "${raw}". Duration must be >= 0.`);
|
|
1679
|
+
}
|
|
1680
|
+
switch (unit) {
|
|
1681
|
+
case "ms":
|
|
1682
|
+
return value;
|
|
1683
|
+
case "s":
|
|
1684
|
+
return value * 1e3;
|
|
1685
|
+
case "m":
|
|
1686
|
+
return value * 6e4;
|
|
1687
|
+
case "h":
|
|
1688
|
+
return value * 36e5;
|
|
1689
|
+
default:
|
|
1690
|
+
return value;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
function parsePositiveDurationMs(raw, flagName) {
|
|
1694
|
+
const ms = parseDurationMs(raw);
|
|
1695
|
+
if (ms <= 0) {
|
|
1696
|
+
err(`${flagName} must be greater than 0`);
|
|
1697
|
+
}
|
|
1698
|
+
return ms;
|
|
1699
|
+
}
|
|
1700
|
+
function parsePositiveInt(raw, flagName) {
|
|
1701
|
+
const n = Number(raw);
|
|
1702
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
1703
|
+
err(`${flagName} must be a positive integer`);
|
|
1704
|
+
}
|
|
1705
|
+
return n;
|
|
1706
|
+
}
|
|
1707
|
+
function sleep2(ms) {
|
|
1708
|
+
if (ms <= 0) return Promise.resolve();
|
|
1709
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1710
|
+
}
|
|
1711
|
+
function extractTaskPayload(res) {
|
|
1712
|
+
if (res && typeof res === "object") {
|
|
1713
|
+
const obj = res;
|
|
1714
|
+
if (typeof obj.status === "string") return obj;
|
|
1715
|
+
if (obj.data && typeof obj.data === "object" && !Array.isArray(obj.data)) {
|
|
1716
|
+
const dataObj = obj.data;
|
|
1717
|
+
if (typeof dataObj.status === "string") return dataObj;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
return res ?? {};
|
|
1721
|
+
}
|
|
1722
|
+
function getStatus(payload) {
|
|
1723
|
+
const status = payload.status;
|
|
1724
|
+
if (status === "pending" || status === "processing" || status === "succeeded" || status === "failed" || status === "expired") {
|
|
1725
|
+
return status;
|
|
1726
|
+
}
|
|
1727
|
+
return void 0;
|
|
1728
|
+
}
|
|
1729
|
+
async function taskPoll(args, flags) {
|
|
1730
|
+
showHelpIfRequested2(flags, POLL_HELP);
|
|
1731
|
+
const taskId = args[0];
|
|
1732
|
+
if (!taskId) err("usage: xapi-to task poll <task_id>");
|
|
1733
|
+
const cfg = getConfig();
|
|
1734
|
+
requireApiKey(cfg);
|
|
1735
|
+
try {
|
|
1736
|
+
const res = await actionCall("task.poll", { task_id: taskId }, cfg, void 0, 2);
|
|
1737
|
+
const payload = extractTaskPayload(res);
|
|
1738
|
+
output(payload, flags.format);
|
|
1739
|
+
} catch (e) {
|
|
1740
|
+
err("task poll failed", e.message);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async function taskWait(args, flags) {
|
|
1744
|
+
showHelpIfRequested2(flags, WAIT_HELP);
|
|
1745
|
+
const taskId = args[0];
|
|
1746
|
+
if (!taskId) err("usage: xapi-to task wait <task_id>");
|
|
1747
|
+
const intervalMs = parsePositiveDurationMs(flags.interval || "2s", "--interval");
|
|
1748
|
+
const timeoutMs = flags.timeout ? parseDurationMs(flags.timeout) : void 0;
|
|
1749
|
+
const maxAttempts = flags["max-attempts"] ? parsePositiveInt(flags["max-attempts"], "--max-attempts") : void 0;
|
|
1750
|
+
const cfg = getConfig();
|
|
1751
|
+
requireApiKey(cfg);
|
|
1752
|
+
const startedAt = Date.now();
|
|
1753
|
+
const deadline = timeoutMs !== void 0 ? startedAt + timeoutMs : void 0;
|
|
1754
|
+
let attempts = 0;
|
|
1755
|
+
while (true) {
|
|
1756
|
+
if (deadline !== void 0 && Date.now() >= deadline) {
|
|
1757
|
+
err(
|
|
1758
|
+
"task wait timeout",
|
|
1759
|
+
`task_id=${taskId}, elapsed_ms=${Date.now() - startedAt}, timeout_ms=${timeoutMs}`
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
attempts += 1;
|
|
1763
|
+
let retryDelayMs;
|
|
1764
|
+
try {
|
|
1765
|
+
const remaining = deadline !== void 0 ? Math.max(1, deadline - Date.now()) : void 0;
|
|
1766
|
+
const res = await actionCall("task.poll", { task_id: taskId }, cfg, void 0, 0, remaining);
|
|
1767
|
+
const payload = extractTaskPayload(res);
|
|
1768
|
+
const status = getStatus(payload);
|
|
1769
|
+
if (!status) {
|
|
1770
|
+
err("task wait failed", "task.poll returned invalid response: missing status");
|
|
1771
|
+
}
|
|
1772
|
+
if (status === "succeeded") {
|
|
1773
|
+
output(payload, flags.format);
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
if (status === "failed" || status === "expired") {
|
|
1777
|
+
output(payload, flags.format);
|
|
1778
|
+
process.exit(1);
|
|
1779
|
+
}
|
|
1780
|
+
} catch (e) {
|
|
1781
|
+
if (e instanceof Error && e.message === "process.exit") {
|
|
1782
|
+
throw e;
|
|
1783
|
+
}
|
|
1784
|
+
if (deadline !== void 0 && Date.now() >= deadline) {
|
|
1785
|
+
err(
|
|
1786
|
+
"task wait timeout",
|
|
1787
|
+
`task_id=${taskId}, elapsed_ms=${Date.now() - startedAt}, timeout_ms=${timeoutMs}`
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
if (isRetryableRequestError(e)) {
|
|
1791
|
+
retryDelayMs = e instanceof HttpError ? e.retryAfterMs : void 0;
|
|
1792
|
+
} else {
|
|
1793
|
+
err("task wait failed", e.message);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (maxAttempts && attempts >= maxAttempts) {
|
|
1797
|
+
err(
|
|
1798
|
+
"task wait exceeded max attempts",
|
|
1799
|
+
`task_id=${taskId}, attempts=${attempts}, max_attempts=${maxAttempts}`
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
const desiredWaitMs = retryDelayMs ?? intervalMs;
|
|
1803
|
+
const waitMs = deadline !== void 0 ? Math.min(desiredWaitMs, Math.max(0, deadline - Date.now())) : desiredWaitMs;
|
|
1804
|
+
await sleep2(waitMs);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
function taskHelp() {
|
|
1808
|
+
return TASK_HELP;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/args.ts
|
|
1364
1812
|
function parseArgs(argv) {
|
|
1365
1813
|
const positional = [];
|
|
1366
1814
|
const flags = {};
|
|
1367
1815
|
let i = 0;
|
|
1816
|
+
let onlyPositional = false;
|
|
1368
1817
|
while (i < argv.length) {
|
|
1369
1818
|
const arg = argv[i];
|
|
1370
|
-
if (arg
|
|
1819
|
+
if (!onlyPositional && arg === "--") {
|
|
1820
|
+
onlyPositional = true;
|
|
1821
|
+
i++;
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
if (!onlyPositional && arg.startsWith("--")) {
|
|
1825
|
+
const eq = arg.indexOf("=");
|
|
1826
|
+
if (eq !== -1) {
|
|
1827
|
+
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
1828
|
+
i++;
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1371
1831
|
const key = arg.slice(2);
|
|
1372
1832
|
const next = argv[i + 1];
|
|
1373
|
-
if (next && !next.startsWith("--")) {
|
|
1833
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
1374
1834
|
flags[key] = next;
|
|
1375
1835
|
i += 2;
|
|
1376
1836
|
} else {
|
|
@@ -1384,6 +1844,10 @@ function parseArgs(argv) {
|
|
|
1384
1844
|
}
|
|
1385
1845
|
return { positional, flags };
|
|
1386
1846
|
}
|
|
1847
|
+
|
|
1848
|
+
// src/index.ts
|
|
1849
|
+
var { CONFIG_HELP: CONFIG_HELP2 } = config_exports;
|
|
1850
|
+
var { OAUTH_HELP: OAUTH_HELP2 } = oauth_exports;
|
|
1387
1851
|
var HELP = `xapi-to - agent-friendly CLI for xapi
|
|
1388
1852
|
|
|
1389
1853
|
USAGE
|
|
@@ -1408,9 +1872,16 @@ COMMANDS
|
|
|
1408
1872
|
--code <target> Generate code snippet (curl, py, js, ts, go)
|
|
1409
1873
|
call <id> --input '{"key":"val"}' Execute an action
|
|
1410
1874
|
--method GET|POST|... Override HTTP method
|
|
1875
|
+
--output <path> Save a raw binary response to a new file
|
|
1411
1876
|
--code <target> Generate code snippet instead of executing
|
|
1412
1877
|
Variants: python.requests, python.httpx, javascript.fetch, javascript.axios
|
|
1413
1878
|
|
|
1879
|
+
task poll <task_id> Poll async task once (wraps task.poll)
|
|
1880
|
+
task wait <task_id> Wait until async task completes
|
|
1881
|
+
--interval <duration> Poll interval, e.g. 2s (default: 2s)
|
|
1882
|
+
--timeout <duration> Max wait duration, e.g. 10m
|
|
1883
|
+
--max-attempts <number> Max poll attempts
|
|
1884
|
+
|
|
1414
1885
|
oauth bind [--provider twitter] Bind Twitter OAuth to your API key
|
|
1415
1886
|
oauth status List current OAuth bindings
|
|
1416
1887
|
oauth unbind <binding-id> Remove an OAuth binding
|
|
@@ -1448,7 +1919,10 @@ EXAMPLES
|
|
|
1448
1919
|
xapi-to get twitter.tweet_detail --code curl
|
|
1449
1920
|
xapi-to get twitter.tweet_detail --code py --format pretty
|
|
1450
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
|
|
1451
1923
|
xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' --code python
|
|
1924
|
+
xapi-to task poll 550e8400-e29b-41d4-a716-446655440000
|
|
1925
|
+
xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 2s --timeout 10m
|
|
1452
1926
|
xapi-to categories
|
|
1453
1927
|
xapi-to services --format table
|
|
1454
1928
|
xapi-to config set apiKey=xapi_abc123
|
|
@@ -1476,6 +1950,23 @@ async function main() {
|
|
|
1476
1950
|
return actionGet2(rest, flags);
|
|
1477
1951
|
case "call":
|
|
1478
1952
|
return actionCall2(rest, flags);
|
|
1953
|
+
case "task": {
|
|
1954
|
+
if (rest.length === 0) {
|
|
1955
|
+
console.log(taskHelp());
|
|
1956
|
+
process.exit(0);
|
|
1957
|
+
}
|
|
1958
|
+
const [subCmd, ...subRest] = rest;
|
|
1959
|
+
switch (subCmd) {
|
|
1960
|
+
case "poll":
|
|
1961
|
+
return taskPoll(subRest, flags);
|
|
1962
|
+
case "wait":
|
|
1963
|
+
return taskWait(subRest, flags);
|
|
1964
|
+
default:
|
|
1965
|
+
console.error(JSON.stringify({ error: `unknown task command: ${subCmd}`, hint: "valid commands: poll, wait" }));
|
|
1966
|
+
process.exit(1);
|
|
1967
|
+
}
|
|
1968
|
+
break;
|
|
1969
|
+
}
|
|
1479
1970
|
// ── OAuth commands ──
|
|
1480
1971
|
case "oauth": {
|
|
1481
1972
|
if (flags.help || rest.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xapi-to",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Agent-friendly CLI for xapi - discover and call capabilities and APIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsup src/index.ts --format esm --target node18 --clean --out-dir dist --tsconfig tsconfig.build.json",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
16
17
|
"start": "node dist/index.js",
|
|
17
18
|
"dev": "XAPI_ACTION_HOST=localhost:3003 bun run src/index.ts",
|
|
18
19
|
"prepublishOnly": "npm run build",
|