xapi-to 0.1.15 → 0.1.17

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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/dist/index.js +651 -68
  3. 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.
@@ -140,6 +150,7 @@ Config is stored at `~/.xapi/config.json`.
140
150
  | `twitter.user_by_screen_name` | Get user profile by username |
141
151
  | `twitter.user_by_screen_names` | Batch get user profiles by usernames |
142
152
  | `twitter.user_tweets` | Get tweets from a user |
153
+ | `twitter.user_tweets_and_replies` | Get tweets and replies from a user |
143
154
  | `twitter.user_media` | Get media posts from a user |
144
155
  | `twitter.following` | Get user following list |
145
156
  | `twitter.followers` | Get user followers |
package/dist/index.js CHANGED
@@ -24,12 +24,46 @@ function output(data, format) {
24
24
  console.log(JSON.stringify(data, null, 2));
25
25
  return;
26
26
  }
27
- if (fmt === "table" && Array.isArray(data)) {
28
- printTable(data);
29
- return;
27
+ if (fmt === "table") {
28
+ const rows = tableRows(data);
29
+ if (rows) {
30
+ printTable(rows);
31
+ return;
32
+ }
30
33
  }
31
34
  console.log(JSON.stringify(data, null, 2));
32
35
  }
36
+ function tableRows(data) {
37
+ if (Array.isArray(data)) return normalizeRows(data, "value");
38
+ if (!data || typeof data !== "object") return null;
39
+ const obj = data;
40
+ const preferredKeys = ["items", "actions", "results", "services", "categories", "bindings", "providers"];
41
+ for (const key of preferredKeys) {
42
+ const value = obj[key];
43
+ if (Array.isArray(value)) return normalizeRows(value, singularKey(key));
44
+ }
45
+ const firstArray = Object.entries(obj).find(([, value]) => Array.isArray(value));
46
+ return firstArray ? normalizeRows(firstArray[1], singularKey(firstArray[0])) : null;
47
+ }
48
+ function normalizeRows(rows, primitiveKey) {
49
+ return rows.map((row) => {
50
+ if (row && typeof row === "object" && !Array.isArray(row)) {
51
+ return row;
52
+ }
53
+ return { [primitiveKey]: row };
54
+ });
55
+ }
56
+ function singularKey(key) {
57
+ if (key === "categories") return "category";
58
+ if (key.endsWith("ies")) return `${key.slice(0, -3)}y`;
59
+ if (key.endsWith("s")) return key.slice(0, -1);
60
+ return "value";
61
+ }
62
+ function formatCell(value) {
63
+ if (value === null || value === void 0) return "";
64
+ if (typeof value === "object") return JSON.stringify(value);
65
+ return String(value);
66
+ }
33
67
  function printTable(rows) {
34
68
  if (rows.length === 0) {
35
69
  console.log("(empty)");
@@ -37,14 +71,14 @@ function printTable(rows) {
37
71
  }
38
72
  const keys = Object.keys(rows[0]);
39
73
  const widths = keys.map(
40
- (k) => Math.min(40, Math.max(k.length, ...rows.map((r) => String(r[k] ?? "").length)))
74
+ (k) => Math.min(40, Math.max(k.length, ...rows.map((r) => formatCell(r[k]).length)))
41
75
  );
42
76
  const sep = widths.map((w) => "-".repeat(w)).join(" ");
43
77
  const header = keys.map((k, i) => k.padEnd(widths[i])).join(" ");
44
78
  console.log(header);
45
79
  console.log(sep);
46
80
  for (const row of rows) {
47
- const line = keys.map((k, i) => String(row[k] ?? "").slice(0, widths[i]).padEnd(widths[i])).join(" ");
81
+ const line = keys.map((k, i) => formatCell(row[k]).slice(0, widths[i]).padEnd(widths[i])).join(" ");
48
82
  console.log(line);
49
83
  }
50
84
  }
@@ -66,7 +100,43 @@ import { join } from "path";
66
100
  var XAPI_ACTION_HOST = process.env.XAPI_ACTION_HOST || "action.xapi.to";
67
101
  var XAPI_API_HOST = process.env.XAPI_API_HOST || "api.xapi.to";
68
102
  function scheme(host) {
69
- return host.startsWith("localhost") || host.startsWith("127.") ? "http" : "https";
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
+ }
70
140
  }
71
141
  var CONFIG_DIR = join(homedir(), ".xapi");
72
142
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
@@ -112,32 +182,128 @@ function showConfig() {
112
182
  // src/client.ts
113
183
  var DEFAULT_TIMEOUT_MS = 3e4;
114
184
  var EXECUTE_TIMEOUT_MS = 6e4;
115
- async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS) {
116
- const controller = new AbortController();
117
- const timer = setTimeout(() => controller.abort(), timeoutMs);
118
- try {
119
- const res = await fetch(url, { ...options, signal: controller.signal });
120
- if (!res.ok) {
121
- const text = await res.text();
122
- throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
123
- }
124
- const body = await res.json();
125
- if (body && typeof body === "object" && "success" in body && body.success === false) {
126
- const data = body.data;
127
- if (data?.statusCode === 401 || data?.error === "Unauthorized") {
185
+ var IDEMPOTENT_RETRIES = 2;
186
+ var RETRY_BASE_DELAY_MS = 500;
187
+ var RETRY_MAX_DELAY_MS = 8e3;
188
+ var HttpError = class extends Error {
189
+ constructor(status, detail, retryAfterMs) {
190
+ super(`HTTP ${status}: ${detail}`);
191
+ this.status = status;
192
+ this.retryAfterMs = retryAfterMs;
193
+ this.name = "HttpError";
194
+ }
195
+ status;
196
+ retryAfterMs;
197
+ };
198
+ var RequestTimeoutError = class extends Error {
199
+ constructor(timeoutMs) {
200
+ super(`request timed out after ${timeoutMs}ms`);
201
+ this.timeoutMs = timeoutMs;
202
+ this.name = "RequestTimeoutError";
203
+ }
204
+ timeoutMs;
205
+ };
206
+ function isRetryableStatus(status) {
207
+ return status === 408 || status === 429 || status === 502 || status === 503 || status === 504;
208
+ }
209
+ function isRetryableNetworkError(e) {
210
+ if (!(e instanceof Error)) return false;
211
+ if (e instanceof HttpError || e instanceof RequestTimeoutError) return false;
212
+ if (e.name === "AbortError") return false;
213
+ return e instanceof TypeError || /network|fetch failed|econn|etimedout|eai_again|socket|dns/i.test(e.message);
214
+ }
215
+ function isRetryableRequestError(e) {
216
+ if (e instanceof HttpError) return isRetryableStatus(e.status);
217
+ if (e instanceof RequestTimeoutError) return true;
218
+ return isRetryableNetworkError(e);
219
+ }
220
+ function retryBaseDelayMs() {
221
+ const override = Number(process.env.XAPI_RETRY_BASE_MS);
222
+ return Number.isFinite(override) && override > 0 ? override : RETRY_BASE_DELAY_MS;
223
+ }
224
+ function backoffDelayMs(attempt, retryAfterMs) {
225
+ if (retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
226
+ return Math.min(retryAfterMs, RETRY_MAX_DELAY_MS);
227
+ }
228
+ const capped = Math.min(retryBaseDelayMs() * 2 ** attempt, RETRY_MAX_DELAY_MS);
229
+ return capped / 2 + Math.random() * (capped / 2);
230
+ }
231
+ function parseRetryAfterMs(res) {
232
+ const header = res.headers.get("retry-after");
233
+ if (!header) return void 0;
234
+ const seconds = Number(header);
235
+ if (Number.isFinite(seconds)) return seconds * 1e3;
236
+ const at = Date.parse(header);
237
+ return Number.isFinite(at) ? Math.max(0, at - Date.now()) : void 0;
238
+ }
239
+ function sleep(ms) {
240
+ return new Promise((resolve) => setTimeout(resolve, ms));
241
+ }
242
+ async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0) {
243
+ assertAllowedHost(url);
244
+ let attempt = 0;
245
+ while (true) {
246
+ const controller = new AbortController();
247
+ let timedOut = false;
248
+ const timer = setTimeout(() => {
249
+ timedOut = true;
250
+ controller.abort();
251
+ }, timeoutMs);
252
+ try {
253
+ const res = await fetch(url, { ...options, redirect: "manual", signal: controller.signal });
254
+ if (res.status >= 300 && res.status < 400) {
128
255
  throw new Error(
129
- "Authentication failed: " + (data.message || "Invalid or missing API key") + '. Run "npx xapi-to config set apiKey=<key>" to update your key.'
256
+ `refusing to follow redirect to "${res.headers.get("location") ?? "?"}" (would forward the API key past the host allowlist)`
130
257
  );
131
258
  }
132
- if (data?.error === "OAuth Required" || data?.statusCode === 403 && data?.message?.includes("OAuth")) {
133
- throw new Error(
134
- (data.message || "OAuth authorization required") + '. Run "xapi-to oauth bind" to connect your account.'
135
- );
259
+ if (!res.ok) {
260
+ const retryAfterMs = isRetryableStatus(res.status) ? parseRetryAfterMs(res) : void 0;
261
+ if (isRetryableStatus(res.status) && attempt < retries) {
262
+ await res.text().catch(() => "");
263
+ clearTimeout(timer);
264
+ await sleep(backoffDelayMs(attempt, retryAfterMs));
265
+ attempt++;
266
+ continue;
267
+ }
268
+ const text2 = await res.text();
269
+ throw new HttpError(res.status, text2.slice(0, 300), retryAfterMs);
270
+ }
271
+ if (res.status === 204) {
272
+ return void 0;
273
+ }
274
+ const text = await res.text();
275
+ if (!text.trim()) {
276
+ return void 0;
277
+ }
278
+ const body = JSON.parse(text);
279
+ if (body && typeof body === "object" && "success" in body && body.success === false) {
280
+ const data = body.data;
281
+ if (data?.statusCode === 401 || data?.error === "Unauthorized") {
282
+ throw new Error(
283
+ "Authentication failed: " + (data.message || "Invalid or missing API key") + '. Run "npx xapi-to config set apiKey=<key>" to update your key.'
284
+ );
285
+ }
286
+ if (data?.error === "OAuth Required" || data?.statusCode === 403 && data?.message?.includes("OAuth")) {
287
+ throw new Error(
288
+ (data.message || "OAuth authorization required") + '. Run "xapi-to oauth bind" to connect your account.'
289
+ );
290
+ }
136
291
  }
292
+ return body;
293
+ } catch (e) {
294
+ if (timedOut) {
295
+ throw new RequestTimeoutError(timeoutMs);
296
+ }
297
+ if (isRetryableNetworkError(e) && attempt < retries) {
298
+ clearTimeout(timer);
299
+ await sleep(backoffDelayMs(attempt));
300
+ attempt++;
301
+ continue;
302
+ }
303
+ throw e;
304
+ } finally {
305
+ clearTimeout(timer);
137
306
  }
138
- return body;
139
- } finally {
140
- clearTimeout(timer);
141
307
  }
142
308
  }
143
309
  function headers(apiKey) {
@@ -157,7 +323,9 @@ async function actionList(opts, params = {}) {
157
323
  if (params.service_id) url.searchParams.set("service_id", params.service_id);
158
324
  return request(
159
325
  url.toString(),
160
- { method: "GET", headers: headers(opts.apiKey) }
326
+ { method: "GET", headers: headers(opts.apiKey) },
327
+ DEFAULT_TIMEOUT_MS,
328
+ IDEMPOTENT_RETRIES
161
329
  );
162
330
  }
163
331
  async function actionSearch(query, opts, params = {}) {
@@ -169,7 +337,9 @@ async function actionSearch(query, opts, params = {}) {
169
337
  if (params.page_size) url.searchParams.set("page_size", String(params.page_size));
170
338
  return request(
171
339
  url.toString(),
172
- { method: "GET", headers: headers(opts.apiKey) }
340
+ { method: "GET", headers: headers(opts.apiKey) },
341
+ DEFAULT_TIMEOUT_MS,
342
+ IDEMPOTENT_RETRIES
173
343
  );
174
344
  }
175
345
  async function actionCategories(opts, params = {}) {
@@ -177,16 +347,20 @@ async function actionCategories(opts, params = {}) {
177
347
  if (params.source) url.searchParams.set("source", params.source);
178
348
  return request(
179
349
  url.toString(),
180
- { method: "GET", headers: headers(opts.apiKey) }
350
+ { method: "GET", headers: headers(opts.apiKey) },
351
+ DEFAULT_TIMEOUT_MS,
352
+ IDEMPOTENT_RETRIES
181
353
  );
182
354
  }
183
355
  async function actionGet(id, opts) {
184
356
  return request(
185
357
  `${baseUrl(opts)}/v1/actions/${encodeURIComponent(id)}`,
186
- { method: "GET", headers: headers(opts.apiKey) }
358
+ { method: "GET", headers: headers(opts.apiKey) },
359
+ DEFAULT_TIMEOUT_MS,
360
+ IDEMPOTENT_RETRIES
187
361
  );
188
362
  }
189
- async function actionCall(actionId, input, opts, httpMethod) {
363
+ async function actionCall(actionId, input, opts, httpMethod, retries = 0, timeoutMs = EXECUTE_TIMEOUT_MS) {
190
364
  return request(
191
365
  `${baseUrl(opts)}/v1/actions/execute`,
192
366
  {
@@ -194,7 +368,8 @@ async function actionCall(actionId, input, opts, httpMethod) {
194
368
  headers: headers(opts.apiKey),
195
369
  body: JSON.stringify({ action_id: actionId, ...httpMethod ? { method: httpMethod } : {}, input })
196
370
  },
197
- EXECUTE_TIMEOUT_MS
371
+ Math.min(timeoutMs, EXECUTE_TIMEOUT_MS),
372
+ retries
198
373
  );
199
374
  }
200
375
  async function actionServices(opts, params = {}) {
@@ -204,14 +379,18 @@ async function actionServices(opts, params = {}) {
204
379
  if (params.category) url.searchParams.set("category", params.category);
205
380
  return request(
206
381
  url.toString(),
207
- { method: "GET", headers: headers(opts.apiKey) }
382
+ { method: "GET", headers: headers(opts.apiKey) },
383
+ DEFAULT_TIMEOUT_MS,
384
+ IDEMPOTENT_RETRIES
208
385
  );
209
386
  }
210
387
  async function healthCheck(opts) {
211
388
  return request(
212
389
  `${baseUrl(opts)}/health`,
213
390
  { method: "GET", headers: headers(opts.apiKey) },
214
- 5e3
391
+ 5e3,
392
+ 0
393
+ // health is a quick connectivity probe — fail fast, don't retry
215
394
  );
216
395
  }
217
396
  async function loginWithApiKey(apiKey, apiHost) {
@@ -221,7 +400,10 @@ async function loginWithApiKey(apiKey, apiHost) {
221
400
  method: "POST",
222
401
  headers: { "Content-Type": "application/json" },
223
402
  body: JSON.stringify({ apiKey })
224
- }
403
+ },
404
+ DEFAULT_TIMEOUT_MS,
405
+ IDEMPOTENT_RETRIES
406
+ // auth exchange has no side effect — safe to retry
225
407
  );
226
408
  }
227
409
  function jwtHeaders(jwtToken) {
@@ -230,7 +412,9 @@ function jwtHeaders(jwtToken) {
230
412
  async function listKeys(jwtToken, apiHost) {
231
413
  return request(
232
414
  `${scheme(apiHost)}://${apiHost}/api/keys`,
233
- { method: "GET", headers: jwtHeaders(jwtToken) }
415
+ { method: "GET", headers: jwtHeaders(jwtToken) },
416
+ DEFAULT_TIMEOUT_MS,
417
+ IDEMPOTENT_RETRIES
234
418
  );
235
419
  }
236
420
  async function enableOAuthForKey(keyId, plaintextKey, jwtToken, apiHost) {
@@ -246,30 +430,37 @@ async function enableOAuthForKey(keyId, plaintextKey, jwtToken, apiHost) {
246
430
  async function listOAuthProviders(apiHost) {
247
431
  return request(
248
432
  `${scheme(apiHost)}://${apiHost}/api/oauth/providers`,
249
- { method: "GET", headers: { "Content-Type": "application/json" } }
433
+ { method: "GET", headers: { "Content-Type": "application/json" } },
434
+ DEFAULT_TIMEOUT_MS,
435
+ IDEMPOTENT_RETRIES
250
436
  );
251
437
  }
252
- async function initiateOAuth(apiKeyId, providerId, jwtToken, apiHost) {
438
+ async function initiateOAuth(apiKeyId, providerId, jwtToken, apiHost, scopes) {
439
+ const body = { apiKeyId, providerId };
440
+ if (scopes) body.scopes = scopes;
253
441
  return request(
254
442
  `${scheme(apiHost)}://${apiHost}/api/oauth/authorize`,
255
443
  {
256
444
  method: "POST",
257
445
  headers: jwtHeaders(jwtToken),
258
- body: JSON.stringify({ apiKeyId, providerId })
446
+ body: JSON.stringify(body)
259
447
  }
260
448
  );
261
449
  }
262
450
  async function listOAuthBindings(jwtToken, apiHost) {
263
451
  return request(
264
452
  `${scheme(apiHost)}://${apiHost}/api/oauth/bindings`,
265
- { method: "GET", headers: jwtHeaders(jwtToken) }
453
+ { method: "GET", headers: jwtHeaders(jwtToken) },
454
+ DEFAULT_TIMEOUT_MS,
455
+ IDEMPOTENT_RETRIES
266
456
  );
267
457
  }
268
458
  async function deleteOAuthBinding(bindingId, jwtToken, apiHost) {
269
- return request(
459
+ const result = await request(
270
460
  `${scheme(apiHost)}://${apiHost}/api/oauth/bindings/${bindingId}`,
271
461
  { method: "DELETE", headers: jwtHeaders(jwtToken) }
272
462
  );
463
+ return result ?? { success: true };
273
464
  }
274
465
 
275
466
  // src/codegen.ts
@@ -659,6 +850,7 @@ function getSource(flags) {
659
850
  async function actionList2(args, flags) {
660
851
  showHelpIfRequested(flags, LIST_HELP);
661
852
  const cfg = getConfig();
853
+ const fmt = flags.format || getFormat();
662
854
  try {
663
855
  const res = await actionList(cfg, {
664
856
  source: getSource(flags),
@@ -668,7 +860,7 @@ async function actionList2(args, flags) {
668
860
  service_id: flags["service-id"]
669
861
  });
670
862
  const actions = res.actions || [];
671
- if (flags.format === "table") {
863
+ if (fmt === "table") {
672
864
  output(actions.map((a) => ({
673
865
  id: a.id,
674
866
  method: a.method ?? "",
@@ -690,6 +882,7 @@ async function actionSearch2(args, flags) {
690
882
  const query = args[0];
691
883
  if (!query) err("usage: xapi-to search <query>");
692
884
  const cfg = getConfig();
885
+ const fmt = flags.format || getFormat();
693
886
  try {
694
887
  const res = await actionSearch(query, cfg, {
695
888
  source: getSource(flags),
@@ -698,7 +891,7 @@ async function actionSearch2(args, flags) {
698
891
  page_size: flags["page-size"] ? parseInt(flags["page-size"]) : void 0
699
892
  });
700
893
  const results = res.results || [];
701
- if (flags.format === "table") {
894
+ if (fmt === "table") {
702
895
  output(results.map((a) => ({
703
896
  id: a.id,
704
897
  method: a.method ?? "",
@@ -717,9 +910,10 @@ async function actionSearch2(args, flags) {
717
910
  }
718
911
  async function actionCategories2(args, flags) {
719
912
  const cfg = getConfig();
913
+ const fmt = flags.format || getFormat();
720
914
  try {
721
915
  const res = await actionCategories(cfg, { source: getSource(flags) });
722
- if (flags.format === "table") {
916
+ if (fmt === "table") {
723
917
  output(res.categories.map((c) => ({ category: c })), "table");
724
918
  } else {
725
919
  output(res, flags.format);
@@ -730,6 +924,7 @@ async function actionCategories2(args, flags) {
730
924
  }
731
925
  async function actionServices2(args, flags) {
732
926
  const cfg = getConfig();
927
+ const fmt = flags.format || getFormat();
733
928
  try {
734
929
  const res = await actionServices(cfg, {
735
930
  page: flags.page ? parseInt(flags.page) : void 0,
@@ -737,7 +932,7 @@ async function actionServices2(args, flags) {
737
932
  category: flags.category
738
933
  });
739
934
  const services = res.services || [];
740
- if (flags.format === "table") {
935
+ if (fmt === "table") {
741
936
  output(services.map((s) => ({
742
937
  id: s.id,
743
938
  name: s.name ?? "",
@@ -826,6 +1021,7 @@ __export(config_exports, {
826
1021
  configSet: () => configSet,
827
1022
  configShow: () => configShow
828
1023
  });
1024
+ import { readFileSync as readFileSync2 } from "fs";
829
1025
  var CONFIG_HELP = `xapi-to config - Manage CLI configuration
830
1026
 
831
1027
  USAGE
@@ -833,7 +1029,7 @@ USAGE
833
1029
 
834
1030
  COMMANDS
835
1031
  show Show current config (host, apiKey path, etc.)
836
- set apiKey=<key> Save API key to ~/.xapi/config.json
1032
+ set apiKey=<key> Save API key to ~/.xapi/config.json (apiKey=- reads from stdin)
837
1033
  health Check backend connectivity (alias: xapi-to health)
838
1034
 
839
1035
  FLAGS
@@ -842,6 +1038,7 @@ FLAGS
842
1038
  EXAMPLES
843
1039
  xapi-to config show
844
1040
  xapi-to config set apiKey=xapi_abc123
1041
+ echo "$XAPI_KEY" | xapi-to config set apiKey=- # keeps the key out of shell history
845
1042
  xapi-to config health
846
1043
  `;
847
1044
  async function configShow(args, flags) {
@@ -856,7 +1053,12 @@ async function configSet(args, flags) {
856
1053
  const key = arg.slice(0, eq);
857
1054
  if (key === "host") err("host is built-in and cannot be configured");
858
1055
  if (key !== "apiKey") err(`unknown config key: ${key} (only apiKey is configurable)`);
859
- updates.apiKey = arg.slice(eq + 1);
1056
+ let value = arg.slice(eq + 1);
1057
+ if (value === "-") {
1058
+ value = readFileSync2(0, "utf-8").trim();
1059
+ }
1060
+ if (!value) err("apiKey is empty");
1061
+ updates.apiKey = value;
860
1062
  }
861
1063
  saveConfig(updates);
862
1064
  console.log(JSON.stringify({ ok: true, updated: Object.keys(updates) }));
@@ -875,6 +1077,7 @@ async function configHealth(args, flags) {
875
1077
 
876
1078
  // src/commands/register.ts
877
1079
  async function registerAccount(referralCode) {
1080
+ assertAllowedHost(XAPI_API_HOST);
878
1081
  const controller = new AbortController();
879
1082
  const timer = setTimeout(() => controller.abort(), 15e3);
880
1083
  try {
@@ -895,6 +1098,11 @@ async function registerAccount(referralCode) {
895
1098
  }
896
1099
  async function register(args, flags) {
897
1100
  try {
1101
+ const cfg = getConfig();
1102
+ const force = flags.force === "true" || flags.force === "1" || flags.force === "yes";
1103
+ if (cfg.apiKey && !force) {
1104
+ err("register would overwrite existing apiKey", 'Run "xapi-to register --force" to create a new account and replace the saved key.');
1105
+ }
898
1106
  const rawReferral = flags["referral-code"] ?? flags["referralCode"] ?? args[0];
899
1107
  const referralCode = typeof rawReferral === "string" && rawReferral !== "true" && rawReferral.length > 0 ? rawReferral : void 0;
900
1108
  const res = await registerAccount(referralCode);
@@ -910,7 +1118,7 @@ async function register(args, flags) {
910
1118
  },
911
1119
  tweetTemplate: res.tweetTemplate,
912
1120
  ...referralCode ? { referredBy: referralCode } : {},
913
- note: "apiKey saved to ~/.xapi/config.json"
1121
+ note: force && cfg.apiKey ? "apiKey replaced in ~/.xapi/config.json" : "apiKey saved to ~/.xapi/config.json"
914
1122
  }, flags.format);
915
1123
  } catch (e) {
916
1124
  err("register failed", e.message);
@@ -967,7 +1175,8 @@ __export(oauth_exports, {
967
1175
  oauthBind: () => oauthBind,
968
1176
  oauthProviders: () => oauthProviders,
969
1177
  oauthStatus: () => oauthStatus,
970
- oauthUnbind: () => oauthUnbind
1178
+ oauthUnbind: () => oauthUnbind,
1179
+ pollForBinding: () => pollForBinding
971
1180
  });
972
1181
  import { spawnSync } from "child_process";
973
1182
  function openBrowser(url) {
@@ -977,14 +1186,22 @@ function openBrowser(url) {
977
1186
  } catch {
978
1187
  }
979
1188
  }
980
- async function pollForBinding(apiKeyId, providerId, jwtToken, timeoutMs = 5 * 60 * 1e3, intervalMs = 3e3) {
1189
+ function bindingChangedAfter(binding, startedAtMs, existingBindingIds) {
1190
+ const changedAt = Date.parse(binding.updatedAt || binding.createdAt || "");
1191
+ if (!Number.isFinite(changedAt)) return !existingBindingIds.has(binding.id);
1192
+ return changedAt >= startedAtMs;
1193
+ }
1194
+ async function pollForBinding(apiKeyId, providerId, jwtToken, startedAt, existingBindingIds = /* @__PURE__ */ new Set(), timeoutMs = 5 * 60 * 1e3, intervalMs = 3e3) {
981
1195
  const deadline = Date.now() + timeoutMs;
982
1196
  const isTTY = process.stdout.isTTY;
1197
+ const startedAtMs = startedAt.getTime() - 5e3;
983
1198
  while (Date.now() < deadline) {
984
1199
  await new Promise((r) => setTimeout(r, intervalMs));
985
1200
  try {
986
1201
  const bindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
987
- const match = Array.isArray(bindings) ? bindings.find((b) => b.apiKeyId === apiKeyId && b.providerId === providerId) : null;
1202
+ const match = Array.isArray(bindings) ? bindings.find(
1203
+ (b) => b.apiKeyId === apiKeyId && b.providerId === providerId && bindingChangedAfter(b, startedAtMs, existingBindingIds)
1204
+ ) : null;
988
1205
  if (match) return match;
989
1206
  } catch {
990
1207
  }
@@ -1008,13 +1225,106 @@ async function findCurrentKeyRecord(plaintextKey, jwtToken) {
1008
1225
  if (!Array.isArray(keys) || keys.length === 0) {
1009
1226
  throw new Error("No API keys found for this account");
1010
1227
  }
1011
- if (keys.length === 1) return keys[0];
1012
1228
  const prefix = plaintextKey.substring(0, 7);
1013
1229
  const match = keys.find((k) => k.keyPreview.startsWith(prefix));
1014
- if (!match) {
1015
- return keys[0];
1230
+ if (match) return match;
1231
+ if (keys.length === 1) return keys[0];
1232
+ throw new Error(
1233
+ `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
+ );
1235
+ }
1236
+ function resolveScopeDefs(provider) {
1237
+ if (Array.isArray(provider.scopeDefinitions) && provider.scopeDefinitions.length > 0) {
1238
+ return provider.scopeDefinitions;
1239
+ }
1240
+ const raw = (provider.defaultScopes || "").split(/[\s,]+/).filter(Boolean);
1241
+ return raw.map((s) => ({
1242
+ scope: s,
1243
+ label: s,
1244
+ description: "",
1245
+ required: false,
1246
+ category: ""
1247
+ }));
1248
+ }
1249
+ async function selectScopesInteractive(provider) {
1250
+ const defs = resolveScopeDefs(provider);
1251
+ if (defs.length === 0) return "";
1252
+ const required = defs.filter((d) => d.required);
1253
+ const optional = defs.filter((d) => !d.required);
1254
+ const selected = new Set(defs.map((d) => d.scope));
1255
+ if (optional.length === 0) {
1256
+ return required.map((d) => d.scope).join(" ");
1016
1257
  }
1017
- return match;
1258
+ const out = process.stderr;
1259
+ let cursor = 0;
1260
+ const hint = " \u2191\u2193 navigate \xB7 space toggle \xB7 a all \xB7 n none \xB7 enter confirm";
1261
+ const buildFrame = () => {
1262
+ const lines = [];
1263
+ for (const d of required) {
1264
+ const desc = d.description ? ` \u2014 ${d.description}` : "";
1265
+ lines.push(` \x1B[2m[*] ${d.label}${desc} (required)\x1B[0m`);
1266
+ }
1267
+ for (let i = 0; i < optional.length; i++) {
1268
+ const d = optional[i];
1269
+ const ptr = cursor === i ? " \x1B[36m\u276F\x1B[0m" : " ";
1270
+ const chk = selected.has(d.scope) ? "\x1B[32m\u2714\x1B[0m" : " ";
1271
+ const desc = d.description ? ` \x1B[2m\u2014 ${d.description}\x1B[0m` : "";
1272
+ lines.push(` ${ptr} [${chk}] ${d.label}${desc}`);
1273
+ }
1274
+ lines.push(`\x1B[2m${hint}\x1B[0m`);
1275
+ return lines.join("\n");
1276
+ };
1277
+ out.write("\n Scopes:\n");
1278
+ out.write("\x1B[s");
1279
+ out.write("\x1B[?25l");
1280
+ out.write(buildFrame());
1281
+ const redraw = () => {
1282
+ out.write("\x1B[u");
1283
+ out.write("\x1B[J");
1284
+ out.write(buildFrame());
1285
+ };
1286
+ return new Promise((resolve) => {
1287
+ const { stdin } = process;
1288
+ const wasRaw = stdin.isRaw;
1289
+ stdin.setRawMode(true);
1290
+ stdin.resume();
1291
+ const finish = (result) => {
1292
+ stdin.removeListener("data", onData);
1293
+ stdin.setRawMode(wasRaw ?? false);
1294
+ stdin.pause();
1295
+ out.write("\x1B[?25h");
1296
+ out.write("\n");
1297
+ resolve(result);
1298
+ };
1299
+ const onData = (buf) => {
1300
+ const key = buf.toString();
1301
+ if (key === "\r" || key === "\n") {
1302
+ finish(Array.from(selected).join(" "));
1303
+ return;
1304
+ }
1305
+ if (key === "") {
1306
+ finish("");
1307
+ process.exit(130);
1308
+ }
1309
+ if (key === "\x1B[A" || key === "k") {
1310
+ cursor = (cursor - 1 + optional.length) % optional.length;
1311
+ } else if (key === "\x1B[B" || key === "j") {
1312
+ cursor = (cursor + 1) % optional.length;
1313
+ } else if (key === " ") {
1314
+ const scope = optional[cursor].scope;
1315
+ if (selected.has(scope)) selected.delete(scope);
1316
+ else selected.add(scope);
1317
+ } else if (key === "a") {
1318
+ for (const d of optional) selected.add(d.scope);
1319
+ } else if (key === "n") {
1320
+ for (const d of optional) selected.delete(d.scope);
1321
+ } else {
1322
+ return;
1323
+ }
1324
+ redraw();
1325
+ };
1326
+ stdin.on("data", onData);
1327
+ });
1018
1328
  }
1019
1329
  var OAUTH_HELP = `xapi-to oauth - Manage OAuth bindings
1020
1330
 
@@ -1029,11 +1339,13 @@ COMMANDS
1029
1339
 
1030
1340
  FLAGS
1031
1341
  --provider <name> OAuth provider (default: twitter)
1342
+ --scopes <scopes> Space-separated scopes (skips interactive selection)
1032
1343
  --format json|pretty|table Output format
1033
1344
 
1034
1345
  EXAMPLES
1035
1346
  xapi-to oauth bind
1036
1347
  xapi-to oauth bind --provider twitter
1348
+ xapi-to oauth bind --scopes "tweet.read users.read"
1037
1349
  xapi-to oauth status
1038
1350
  xapi-to oauth status --format pretty
1039
1351
  xapi-to oauth unbind abc123
@@ -1063,13 +1375,49 @@ async function oauthBind(args, flags) {
1063
1375
  `Provider "${providerName}" not found. Available: ${available}`
1064
1376
  );
1065
1377
  }
1066
- const result = await initiateOAuth(keyRecord.id, provider.id, jwtToken, XAPI_API_HOST);
1378
+ let scopes;
1379
+ let headerPrinted = false;
1380
+ const isTTY = Boolean(
1381
+ process.stdout.isTTY && process.stdin.isTTY && typeof process.stdin.setRawMode === "function"
1382
+ );
1383
+ if (flags.scopes) {
1384
+ scopes = flags.scopes;
1385
+ } else if (isTTY) {
1386
+ const defs = resolveScopeDefs(provider);
1387
+ if (defs.length > 0) {
1388
+ console.error(`
1389
+ Provider : ${provider.name}`);
1390
+ console.error(` API Key : ${keyRecord.keyPreview}`);
1391
+ headerPrinted = true;
1392
+ scopes = await selectScopesInteractive(provider) || void 0;
1393
+ }
1394
+ }
1395
+ const existingBindingIds = /* @__PURE__ */ new Set();
1396
+ if (isTTY) {
1397
+ try {
1398
+ const existingBindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
1399
+ if (Array.isArray(existingBindings)) {
1400
+ for (const binding of existingBindings) {
1401
+ if (binding.apiKeyId === keyRecord.id && binding.providerId === provider.id) {
1402
+ existingBindingIds.add(binding.id);
1403
+ }
1404
+ }
1405
+ }
1406
+ } catch {
1407
+ }
1408
+ }
1409
+ const authorizationStartedAt = /* @__PURE__ */ new Date();
1410
+ const result = await initiateOAuth(keyRecord.id, provider.id, jwtToken, XAPI_API_HOST, scopes);
1067
1411
  const { authorizationUrl } = result;
1068
- const isTTY = process.stdout.isTTY;
1069
1412
  if (isTTY) {
1070
- console.error(`
1413
+ if (!headerPrinted) {
1414
+ console.error(`
1071
1415
  Provider : ${provider.name}`);
1072
- console.error(` API Key : ${keyRecord.keyPreview}`);
1416
+ console.error(` API Key : ${keyRecord.keyPreview}`);
1417
+ }
1418
+ if (scopes) {
1419
+ console.error(` Scopes : ${scopes}`);
1420
+ }
1073
1421
  console.error(`
1074
1422
  Authorization URL:
1075
1423
  ${authorizationUrl}
@@ -1077,14 +1425,20 @@ async function oauthBind(args, flags) {
1077
1425
  console.error(" Opening browser...");
1078
1426
  openBrowser(authorizationUrl);
1079
1427
  console.error(" Waiting for you to complete authorization in the browser...\n");
1080
- const binding = await pollForBinding(keyRecord.id, provider.id, jwtToken);
1428
+ const binding = await pollForBinding(
1429
+ keyRecord.id,
1430
+ provider.id,
1431
+ jwtToken,
1432
+ authorizationStartedAt,
1433
+ existingBindingIds
1434
+ );
1081
1435
  if (process.stdout.isTTY) process.stdout.write("\n");
1082
1436
  if (binding) {
1083
1437
  const account = binding.providerAccountName || "unknown";
1084
1438
  console.error(`
1085
1439
  Authorization complete! Bound to @${account}
1086
1440
  `);
1087
- output({ status: "success", provider: provider.name, account }, flags.format);
1441
+ output({ status: "success", provider: provider.name, account, scopes }, flags.format);
1088
1442
  } else {
1089
1443
  err("oauth bind timed out", 'Authorization was not completed within 5 minutes. Run "xapi-to oauth bind" again.');
1090
1444
  }
@@ -1093,7 +1447,8 @@ async function oauthBind(args, flags) {
1093
1447
  status: "pending",
1094
1448
  provider: provider.name,
1095
1449
  apiKey: keyRecord.keyPreview,
1096
- authorizationUrl
1450
+ authorizationUrl,
1451
+ scopes
1097
1452
  }, flags.format);
1098
1453
  }
1099
1454
  } catch (e) {
@@ -1156,19 +1511,217 @@ async function oauthProviders(args, flags) {
1156
1511
  }
1157
1512
  }
1158
1513
 
1159
- // src/index.ts
1160
- var { CONFIG_HELP: CONFIG_HELP2 } = config_exports;
1161
- var { OAUTH_HELP: OAUTH_HELP2 } = oauth_exports;
1514
+ // src/commands/task.ts
1515
+ var POLL_HELP = `xapi-to task poll - Poll an async task once
1516
+
1517
+ USAGE
1518
+ xapi-to task poll <task_id> [flags]
1519
+
1520
+ FLAGS
1521
+ --format json|pretty|table Output format
1522
+
1523
+ EXAMPLES
1524
+ xapi-to task poll 550e8400-e29b-41d4-a716-446655440000
1525
+ `;
1526
+ var WAIT_HELP = `xapi-to task wait - Wait until async task reaches terminal status
1527
+
1528
+ USAGE
1529
+ xapi-to task wait <task_id> [flags]
1530
+
1531
+ FLAGS
1532
+ --interval <duration> Poll interval (default: 2s). Supports ms/s/m/h
1533
+ --timeout <duration> Max wait duration (optional). Supports ms/s/m/h
1534
+ --max-attempts <number> Max poll attempts (optional)
1535
+ --format json|pretty|table Output format
1536
+
1537
+ EXAMPLES
1538
+ xapi-to task wait 550e8400-e29b-41d4-a716-446655440000
1539
+ xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 1s --timeout 10m
1540
+ `;
1541
+ var TASK_HELP = `xapi-to task - Async task helpers
1542
+
1543
+ USAGE
1544
+ xapi-to task <command> [args] [flags]
1545
+
1546
+ COMMANDS
1547
+ poll <task_id> Poll task status once (wraps action: task.poll)
1548
+ wait <task_id> Poll repeatedly until terminal status
1549
+
1550
+ EXAMPLES
1551
+ xapi-to task poll 550e8400-e29b-41d4-a716-446655440000
1552
+ xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 2s --timeout 10m
1553
+ `;
1554
+ function showHelpIfRequested2(flags, helpText) {
1555
+ if (flags.help) {
1556
+ console.log(helpText);
1557
+ process.exit(0);
1558
+ }
1559
+ }
1560
+ function parseDurationMs(raw) {
1561
+ const text = raw.trim().toLowerCase();
1562
+ const match = text.match(/^(\d+)(ms|s|m|h)?$/);
1563
+ if (!match) {
1564
+ err(`invalid duration "${raw}". Use formats like 500ms, 2s, 5m, 1h.`);
1565
+ }
1566
+ const value = Number(match[1]);
1567
+ const unit = match[2] || "ms";
1568
+ if (!Number.isFinite(value) || value < 0) {
1569
+ err(`invalid duration "${raw}". Duration must be >= 0.`);
1570
+ }
1571
+ switch (unit) {
1572
+ case "ms":
1573
+ return value;
1574
+ case "s":
1575
+ return value * 1e3;
1576
+ case "m":
1577
+ return value * 6e4;
1578
+ case "h":
1579
+ return value * 36e5;
1580
+ default:
1581
+ return value;
1582
+ }
1583
+ }
1584
+ function parsePositiveDurationMs(raw, flagName) {
1585
+ const ms = parseDurationMs(raw);
1586
+ if (ms <= 0) {
1587
+ err(`${flagName} must be greater than 0`);
1588
+ }
1589
+ return ms;
1590
+ }
1591
+ function parsePositiveInt(raw, flagName) {
1592
+ const n = Number(raw);
1593
+ if (!Number.isInteger(n) || n <= 0) {
1594
+ err(`${flagName} must be a positive integer`);
1595
+ }
1596
+ return n;
1597
+ }
1598
+ function sleep2(ms) {
1599
+ if (ms <= 0) return Promise.resolve();
1600
+ return new Promise((resolve) => setTimeout(resolve, ms));
1601
+ }
1602
+ function extractTaskPayload(res) {
1603
+ if (res && typeof res === "object") {
1604
+ const obj = res;
1605
+ if (typeof obj.status === "string") return obj;
1606
+ if (obj.data && typeof obj.data === "object" && !Array.isArray(obj.data)) {
1607
+ const dataObj = obj.data;
1608
+ if (typeof dataObj.status === "string") return dataObj;
1609
+ }
1610
+ }
1611
+ return res ?? {};
1612
+ }
1613
+ function getStatus(payload) {
1614
+ const status = payload.status;
1615
+ if (status === "pending" || status === "processing" || status === "succeeded" || status === "failed" || status === "expired") {
1616
+ return status;
1617
+ }
1618
+ return void 0;
1619
+ }
1620
+ async function taskPoll(args, flags) {
1621
+ showHelpIfRequested2(flags, POLL_HELP);
1622
+ const taskId = args[0];
1623
+ if (!taskId) err("usage: xapi-to task poll <task_id>");
1624
+ const cfg = getConfig();
1625
+ requireApiKey(cfg);
1626
+ try {
1627
+ const res = await actionCall("task.poll", { task_id: taskId }, cfg, void 0, 2);
1628
+ const payload = extractTaskPayload(res);
1629
+ output(payload, flags.format);
1630
+ } catch (e) {
1631
+ err("task poll failed", e.message);
1632
+ }
1633
+ }
1634
+ async function taskWait(args, flags) {
1635
+ showHelpIfRequested2(flags, WAIT_HELP);
1636
+ const taskId = args[0];
1637
+ if (!taskId) err("usage: xapi-to task wait <task_id>");
1638
+ const intervalMs = parsePositiveDurationMs(flags.interval || "2s", "--interval");
1639
+ const timeoutMs = flags.timeout ? parseDurationMs(flags.timeout) : void 0;
1640
+ const maxAttempts = flags["max-attempts"] ? parsePositiveInt(flags["max-attempts"], "--max-attempts") : void 0;
1641
+ const cfg = getConfig();
1642
+ requireApiKey(cfg);
1643
+ const startedAt = Date.now();
1644
+ const deadline = timeoutMs !== void 0 ? startedAt + timeoutMs : void 0;
1645
+ let attempts = 0;
1646
+ while (true) {
1647
+ if (deadline !== void 0 && Date.now() >= deadline) {
1648
+ err(
1649
+ "task wait timeout",
1650
+ `task_id=${taskId}, elapsed_ms=${Date.now() - startedAt}, timeout_ms=${timeoutMs}`
1651
+ );
1652
+ }
1653
+ attempts += 1;
1654
+ let retryDelayMs;
1655
+ try {
1656
+ const remaining = deadline !== void 0 ? Math.max(1, deadline - Date.now()) : void 0;
1657
+ const res = await actionCall("task.poll", { task_id: taskId }, cfg, void 0, 0, remaining);
1658
+ const payload = extractTaskPayload(res);
1659
+ const status = getStatus(payload);
1660
+ if (!status) {
1661
+ err("task wait failed", "task.poll returned invalid response: missing status");
1662
+ }
1663
+ if (status === "succeeded") {
1664
+ output(payload, flags.format);
1665
+ return;
1666
+ }
1667
+ if (status === "failed" || status === "expired") {
1668
+ output(payload, flags.format);
1669
+ process.exit(1);
1670
+ }
1671
+ } catch (e) {
1672
+ if (e instanceof Error && e.message === "process.exit") {
1673
+ throw e;
1674
+ }
1675
+ if (deadline !== void 0 && Date.now() >= deadline) {
1676
+ err(
1677
+ "task wait timeout",
1678
+ `task_id=${taskId}, elapsed_ms=${Date.now() - startedAt}, timeout_ms=${timeoutMs}`
1679
+ );
1680
+ }
1681
+ if (isRetryableRequestError(e)) {
1682
+ retryDelayMs = e instanceof HttpError ? e.retryAfterMs : void 0;
1683
+ } else {
1684
+ err("task wait failed", e.message);
1685
+ }
1686
+ }
1687
+ if (maxAttempts && attempts >= maxAttempts) {
1688
+ err(
1689
+ "task wait exceeded max attempts",
1690
+ `task_id=${taskId}, attempts=${attempts}, max_attempts=${maxAttempts}`
1691
+ );
1692
+ }
1693
+ const desiredWaitMs = retryDelayMs ?? intervalMs;
1694
+ const waitMs = deadline !== void 0 ? Math.min(desiredWaitMs, Math.max(0, deadline - Date.now())) : desiredWaitMs;
1695
+ await sleep2(waitMs);
1696
+ }
1697
+ }
1698
+ function taskHelp() {
1699
+ return TASK_HELP;
1700
+ }
1701
+
1702
+ // src/args.ts
1162
1703
  function parseArgs(argv) {
1163
1704
  const positional = [];
1164
1705
  const flags = {};
1165
1706
  let i = 0;
1707
+ let onlyPositional = false;
1166
1708
  while (i < argv.length) {
1167
1709
  const arg = argv[i];
1168
- if (arg.startsWith("--")) {
1710
+ if (!onlyPositional && arg === "--") {
1711
+ onlyPositional = true;
1712
+ i++;
1713
+ continue;
1714
+ }
1715
+ if (!onlyPositional && arg.startsWith("--")) {
1716
+ const eq = arg.indexOf("=");
1717
+ if (eq !== -1) {
1718
+ flags[arg.slice(2, eq)] = arg.slice(eq + 1);
1719
+ i++;
1720
+ continue;
1721
+ }
1169
1722
  const key = arg.slice(2);
1170
1723
  const next = argv[i + 1];
1171
- if (next && !next.startsWith("--")) {
1724
+ if (next !== void 0 && !next.startsWith("--")) {
1172
1725
  flags[key] = next;
1173
1726
  i += 2;
1174
1727
  } else {
@@ -1182,6 +1735,10 @@ function parseArgs(argv) {
1182
1735
  }
1183
1736
  return { positional, flags };
1184
1737
  }
1738
+
1739
+ // src/index.ts
1740
+ var { CONFIG_HELP: CONFIG_HELP2 } = config_exports;
1741
+ var { OAUTH_HELP: OAUTH_HELP2 } = oauth_exports;
1185
1742
  var HELP = `xapi-to - agent-friendly CLI for xapi
1186
1743
 
1187
1744
  USAGE
@@ -1209,6 +1766,12 @@ COMMANDS
1209
1766
  --code <target> Generate code snippet instead of executing
1210
1767
  Variants: python.requests, python.httpx, javascript.fetch, javascript.axios
1211
1768
 
1769
+ task poll <task_id> Poll async task once (wraps task.poll)
1770
+ task wait <task_id> Wait until async task completes
1771
+ --interval <duration> Poll interval, e.g. 2s (default: 2s)
1772
+ --timeout <duration> Max wait duration, e.g. 10m
1773
+ --max-attempts <number> Max poll attempts
1774
+
1212
1775
  oauth bind [--provider twitter] Bind Twitter OAuth to your API key
1213
1776
  oauth status List current OAuth bindings
1214
1777
  oauth unbind <binding-id> Remove an OAuth binding
@@ -1216,6 +1779,7 @@ COMMANDS
1216
1779
 
1217
1780
  register [referral-code] Create a new user account (apiKey saved automatically)
1218
1781
  --referral-code <code> Register with an inviter's referral code (also: --referralCode, or as positional arg)
1782
+ --force Replace an existing saved apiKey
1219
1783
  balance Show current account balance
1220
1784
  topup [--amount <usd>] [--method stripe|x402] Generate payment URL
1221
1785
 
@@ -1246,6 +1810,8 @@ EXAMPLES
1246
1810
  xapi-to get twitter.tweet_detail --code py --format pretty
1247
1811
  xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
1248
1812
  xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' --code python
1813
+ xapi-to task poll 550e8400-e29b-41d4-a716-446655440000
1814
+ xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 2s --timeout 10m
1249
1815
  xapi-to categories
1250
1816
  xapi-to services --format table
1251
1817
  xapi-to config set apiKey=xapi_abc123
@@ -1273,6 +1839,23 @@ async function main() {
1273
1839
  return actionGet2(rest, flags);
1274
1840
  case "call":
1275
1841
  return actionCall2(rest, flags);
1842
+ case "task": {
1843
+ if (rest.length === 0) {
1844
+ console.log(taskHelp());
1845
+ process.exit(0);
1846
+ }
1847
+ const [subCmd, ...subRest] = rest;
1848
+ switch (subCmd) {
1849
+ case "poll":
1850
+ return taskPoll(subRest, flags);
1851
+ case "wait":
1852
+ return taskWait(subRest, flags);
1853
+ default:
1854
+ console.error(JSON.stringify({ error: `unknown task command: ${subCmd}`, hint: "valid commands: poll, wait" }));
1855
+ process.exit(1);
1856
+ }
1857
+ break;
1858
+ }
1276
1859
  // ── OAuth commands ──
1277
1860
  case "oauth": {
1278
1861
  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.15",
3
+ "version": "0.1.17",
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",