xapi-to 0.1.15 → 0.1.16

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 +1 -0
  2. package/dist/index.js +230 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -140,6 +140,7 @@ Config is stored at `~/.xapi/config.json`.
140
140
  | `twitter.user_by_screen_name` | Get user profile by username |
141
141
  | `twitter.user_by_screen_names` | Batch get user profiles by usernames |
142
142
  | `twitter.user_tweets` | Get tweets from a user |
143
+ | `twitter.user_tweets_and_replies` | Get tweets and replies from a user |
143
144
  | `twitter.user_media` | Get media posts from a user |
144
145
  | `twitter.following` | Get user following list |
145
146
  | `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
  }
@@ -118,10 +152,17 @@ async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS) {
118
152
  try {
119
153
  const res = await fetch(url, { ...options, signal: controller.signal });
120
154
  if (!res.ok) {
121
- const text = await res.text();
122
- throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
155
+ const text2 = await res.text();
156
+ throw new Error(`HTTP ${res.status}: ${text2.slice(0, 300)}`);
157
+ }
158
+ if (res.status === 204) {
159
+ return void 0;
123
160
  }
124
- const body = await res.json();
161
+ const text = await res.text();
162
+ if (!text.trim()) {
163
+ return void 0;
164
+ }
165
+ const body = JSON.parse(text);
125
166
  if (body && typeof body === "object" && "success" in body && body.success === false) {
126
167
  const data = body.data;
127
168
  if (data?.statusCode === 401 || data?.error === "Unauthorized") {
@@ -249,13 +290,15 @@ async function listOAuthProviders(apiHost) {
249
290
  { method: "GET", headers: { "Content-Type": "application/json" } }
250
291
  );
251
292
  }
252
- async function initiateOAuth(apiKeyId, providerId, jwtToken, apiHost) {
293
+ async function initiateOAuth(apiKeyId, providerId, jwtToken, apiHost, scopes) {
294
+ const body = { apiKeyId, providerId };
295
+ if (scopes) body.scopes = scopes;
253
296
  return request(
254
297
  `${scheme(apiHost)}://${apiHost}/api/oauth/authorize`,
255
298
  {
256
299
  method: "POST",
257
300
  headers: jwtHeaders(jwtToken),
258
- body: JSON.stringify({ apiKeyId, providerId })
301
+ body: JSON.stringify(body)
259
302
  }
260
303
  );
261
304
  }
@@ -266,10 +309,11 @@ async function listOAuthBindings(jwtToken, apiHost) {
266
309
  );
267
310
  }
268
311
  async function deleteOAuthBinding(bindingId, jwtToken, apiHost) {
269
- return request(
312
+ const result = await request(
270
313
  `${scheme(apiHost)}://${apiHost}/api/oauth/bindings/${bindingId}`,
271
314
  { method: "DELETE", headers: jwtHeaders(jwtToken) }
272
315
  );
316
+ return result ?? { success: true };
273
317
  }
274
318
 
275
319
  // src/codegen.ts
@@ -659,6 +703,7 @@ function getSource(flags) {
659
703
  async function actionList2(args, flags) {
660
704
  showHelpIfRequested(flags, LIST_HELP);
661
705
  const cfg = getConfig();
706
+ const fmt = flags.format || getFormat();
662
707
  try {
663
708
  const res = await actionList(cfg, {
664
709
  source: getSource(flags),
@@ -668,7 +713,7 @@ async function actionList2(args, flags) {
668
713
  service_id: flags["service-id"]
669
714
  });
670
715
  const actions = res.actions || [];
671
- if (flags.format === "table") {
716
+ if (fmt === "table") {
672
717
  output(actions.map((a) => ({
673
718
  id: a.id,
674
719
  method: a.method ?? "",
@@ -690,6 +735,7 @@ async function actionSearch2(args, flags) {
690
735
  const query = args[0];
691
736
  if (!query) err("usage: xapi-to search <query>");
692
737
  const cfg = getConfig();
738
+ const fmt = flags.format || getFormat();
693
739
  try {
694
740
  const res = await actionSearch(query, cfg, {
695
741
  source: getSource(flags),
@@ -698,7 +744,7 @@ async function actionSearch2(args, flags) {
698
744
  page_size: flags["page-size"] ? parseInt(flags["page-size"]) : void 0
699
745
  });
700
746
  const results = res.results || [];
701
- if (flags.format === "table") {
747
+ if (fmt === "table") {
702
748
  output(results.map((a) => ({
703
749
  id: a.id,
704
750
  method: a.method ?? "",
@@ -717,9 +763,10 @@ async function actionSearch2(args, flags) {
717
763
  }
718
764
  async function actionCategories2(args, flags) {
719
765
  const cfg = getConfig();
766
+ const fmt = flags.format || getFormat();
720
767
  try {
721
768
  const res = await actionCategories(cfg, { source: getSource(flags) });
722
- if (flags.format === "table") {
769
+ if (fmt === "table") {
723
770
  output(res.categories.map((c) => ({ category: c })), "table");
724
771
  } else {
725
772
  output(res, flags.format);
@@ -730,6 +777,7 @@ async function actionCategories2(args, flags) {
730
777
  }
731
778
  async function actionServices2(args, flags) {
732
779
  const cfg = getConfig();
780
+ const fmt = flags.format || getFormat();
733
781
  try {
734
782
  const res = await actionServices(cfg, {
735
783
  page: flags.page ? parseInt(flags.page) : void 0,
@@ -737,7 +785,7 @@ async function actionServices2(args, flags) {
737
785
  category: flags.category
738
786
  });
739
787
  const services = res.services || [];
740
- if (flags.format === "table") {
788
+ if (fmt === "table") {
741
789
  output(services.map((s) => ({
742
790
  id: s.id,
743
791
  name: s.name ?? "",
@@ -895,6 +943,11 @@ async function registerAccount(referralCode) {
895
943
  }
896
944
  async function register(args, flags) {
897
945
  try {
946
+ const cfg = getConfig();
947
+ const force = flags.force === "true" || flags.force === "1" || flags.force === "yes";
948
+ if (cfg.apiKey && !force) {
949
+ err("register would overwrite existing apiKey", 'Run "xapi-to register --force" to create a new account and replace the saved key.');
950
+ }
898
951
  const rawReferral = flags["referral-code"] ?? flags["referralCode"] ?? args[0];
899
952
  const referralCode = typeof rawReferral === "string" && rawReferral !== "true" && rawReferral.length > 0 ? rawReferral : void 0;
900
953
  const res = await registerAccount(referralCode);
@@ -910,7 +963,7 @@ async function register(args, flags) {
910
963
  },
911
964
  tweetTemplate: res.tweetTemplate,
912
965
  ...referralCode ? { referredBy: referralCode } : {},
913
- note: "apiKey saved to ~/.xapi/config.json"
966
+ note: force && cfg.apiKey ? "apiKey replaced in ~/.xapi/config.json" : "apiKey saved to ~/.xapi/config.json"
914
967
  }, flags.format);
915
968
  } catch (e) {
916
969
  err("register failed", e.message);
@@ -967,7 +1020,8 @@ __export(oauth_exports, {
967
1020
  oauthBind: () => oauthBind,
968
1021
  oauthProviders: () => oauthProviders,
969
1022
  oauthStatus: () => oauthStatus,
970
- oauthUnbind: () => oauthUnbind
1023
+ oauthUnbind: () => oauthUnbind,
1024
+ pollForBinding: () => pollForBinding
971
1025
  });
972
1026
  import { spawnSync } from "child_process";
973
1027
  function openBrowser(url) {
@@ -977,14 +1031,22 @@ function openBrowser(url) {
977
1031
  } catch {
978
1032
  }
979
1033
  }
980
- async function pollForBinding(apiKeyId, providerId, jwtToken, timeoutMs = 5 * 60 * 1e3, intervalMs = 3e3) {
1034
+ function bindingChangedAfter(binding, startedAtMs, existingBindingIds) {
1035
+ const changedAt = Date.parse(binding.updatedAt || binding.createdAt || "");
1036
+ if (!Number.isFinite(changedAt)) return !existingBindingIds.has(binding.id);
1037
+ return changedAt >= startedAtMs;
1038
+ }
1039
+ async function pollForBinding(apiKeyId, providerId, jwtToken, startedAt, existingBindingIds = /* @__PURE__ */ new Set(), timeoutMs = 5 * 60 * 1e3, intervalMs = 3e3) {
981
1040
  const deadline = Date.now() + timeoutMs;
982
1041
  const isTTY = process.stdout.isTTY;
1042
+ const startedAtMs = startedAt.getTime() - 5e3;
983
1043
  while (Date.now() < deadline) {
984
1044
  await new Promise((r) => setTimeout(r, intervalMs));
985
1045
  try {
986
1046
  const bindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
987
- const match = Array.isArray(bindings) ? bindings.find((b) => b.apiKeyId === apiKeyId && b.providerId === providerId) : null;
1047
+ const match = Array.isArray(bindings) ? bindings.find(
1048
+ (b) => b.apiKeyId === apiKeyId && b.providerId === providerId && bindingChangedAfter(b, startedAtMs, existingBindingIds)
1049
+ ) : null;
988
1050
  if (match) return match;
989
1051
  } catch {
990
1052
  }
@@ -1012,10 +1074,105 @@ async function findCurrentKeyRecord(plaintextKey, jwtToken) {
1012
1074
  const prefix = plaintextKey.substring(0, 7);
1013
1075
  const match = keys.find((k) => k.keyPreview.startsWith(prefix));
1014
1076
  if (!match) {
1015
- return keys[0];
1077
+ throw new Error(
1078
+ `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.`
1079
+ );
1016
1080
  }
1017
1081
  return match;
1018
1082
  }
1083
+ function resolveScopeDefs(provider) {
1084
+ if (Array.isArray(provider.scopeDefinitions) && provider.scopeDefinitions.length > 0) {
1085
+ return provider.scopeDefinitions;
1086
+ }
1087
+ const raw = (provider.defaultScopes || "").split(/[\s,]+/).filter(Boolean);
1088
+ return raw.map((s) => ({
1089
+ scope: s,
1090
+ label: s,
1091
+ description: "",
1092
+ required: false,
1093
+ category: ""
1094
+ }));
1095
+ }
1096
+ async function selectScopesInteractive(provider) {
1097
+ const defs = resolveScopeDefs(provider);
1098
+ if (defs.length === 0) return "";
1099
+ const required = defs.filter((d) => d.required);
1100
+ const optional = defs.filter((d) => !d.required);
1101
+ const selected = new Set(defs.map((d) => d.scope));
1102
+ if (optional.length === 0) {
1103
+ return required.map((d) => d.scope).join(" ");
1104
+ }
1105
+ const out = process.stderr;
1106
+ let cursor = 0;
1107
+ const hint = " \u2191\u2193 navigate \xB7 space toggle \xB7 a all \xB7 n none \xB7 enter confirm";
1108
+ const buildFrame = () => {
1109
+ const lines = [];
1110
+ for (const d of required) {
1111
+ const desc = d.description ? ` \u2014 ${d.description}` : "";
1112
+ lines.push(` \x1B[2m[*] ${d.label}${desc} (required)\x1B[0m`);
1113
+ }
1114
+ for (let i = 0; i < optional.length; i++) {
1115
+ const d = optional[i];
1116
+ const ptr = cursor === i ? " \x1B[36m\u276F\x1B[0m" : " ";
1117
+ const chk = selected.has(d.scope) ? "\x1B[32m\u2714\x1B[0m" : " ";
1118
+ const desc = d.description ? ` \x1B[2m\u2014 ${d.description}\x1B[0m` : "";
1119
+ lines.push(` ${ptr} [${chk}] ${d.label}${desc}`);
1120
+ }
1121
+ lines.push(`\x1B[2m${hint}\x1B[0m`);
1122
+ return lines.join("\n");
1123
+ };
1124
+ out.write("\n Scopes:\n");
1125
+ out.write("\x1B[s");
1126
+ out.write("\x1B[?25l");
1127
+ out.write(buildFrame());
1128
+ const redraw = () => {
1129
+ out.write("\x1B[u");
1130
+ out.write("\x1B[J");
1131
+ out.write(buildFrame());
1132
+ };
1133
+ return new Promise((resolve) => {
1134
+ const { stdin } = process;
1135
+ const wasRaw = stdin.isRaw;
1136
+ stdin.setRawMode(true);
1137
+ stdin.resume();
1138
+ const finish = (result) => {
1139
+ stdin.removeListener("data", onData);
1140
+ stdin.setRawMode(wasRaw ?? false);
1141
+ stdin.pause();
1142
+ out.write("\x1B[?25h");
1143
+ out.write("\n");
1144
+ resolve(result);
1145
+ };
1146
+ const onData = (buf) => {
1147
+ const key = buf.toString();
1148
+ if (key === "\r" || key === "\n") {
1149
+ finish(Array.from(selected).join(" "));
1150
+ return;
1151
+ }
1152
+ if (key === "") {
1153
+ finish("");
1154
+ process.exit(130);
1155
+ }
1156
+ if (key === "\x1B[A" || key === "k") {
1157
+ cursor = (cursor - 1 + optional.length) % optional.length;
1158
+ } else if (key === "\x1B[B" || key === "j") {
1159
+ cursor = (cursor + 1) % optional.length;
1160
+ } else if (key === " ") {
1161
+ const scope = optional[cursor].scope;
1162
+ if (selected.has(scope)) selected.delete(scope);
1163
+ else selected.add(scope);
1164
+ } else if (key === "a") {
1165
+ for (const d of optional) selected.add(d.scope);
1166
+ } else if (key === "n") {
1167
+ for (const d of optional) selected.delete(d.scope);
1168
+ } else {
1169
+ return;
1170
+ }
1171
+ redraw();
1172
+ };
1173
+ stdin.on("data", onData);
1174
+ });
1175
+ }
1019
1176
  var OAUTH_HELP = `xapi-to oauth - Manage OAuth bindings
1020
1177
 
1021
1178
  USAGE
@@ -1029,11 +1186,13 @@ COMMANDS
1029
1186
 
1030
1187
  FLAGS
1031
1188
  --provider <name> OAuth provider (default: twitter)
1189
+ --scopes <scopes> Space-separated scopes (skips interactive selection)
1032
1190
  --format json|pretty|table Output format
1033
1191
 
1034
1192
  EXAMPLES
1035
1193
  xapi-to oauth bind
1036
1194
  xapi-to oauth bind --provider twitter
1195
+ xapi-to oauth bind --scopes "tweet.read users.read"
1037
1196
  xapi-to oauth status
1038
1197
  xapi-to oauth status --format pretty
1039
1198
  xapi-to oauth unbind abc123
@@ -1063,13 +1222,49 @@ async function oauthBind(args, flags) {
1063
1222
  `Provider "${providerName}" not found. Available: ${available}`
1064
1223
  );
1065
1224
  }
1066
- const result = await initiateOAuth(keyRecord.id, provider.id, jwtToken, XAPI_API_HOST);
1225
+ let scopes;
1226
+ let headerPrinted = false;
1227
+ const isTTY = Boolean(
1228
+ process.stdout.isTTY && process.stdin.isTTY && typeof process.stdin.setRawMode === "function"
1229
+ );
1230
+ if (flags.scopes) {
1231
+ scopes = flags.scopes;
1232
+ } else if (isTTY) {
1233
+ const defs = resolveScopeDefs(provider);
1234
+ if (defs.length > 0) {
1235
+ console.error(`
1236
+ Provider : ${provider.name}`);
1237
+ console.error(` API Key : ${keyRecord.keyPreview}`);
1238
+ headerPrinted = true;
1239
+ scopes = await selectScopesInteractive(provider) || void 0;
1240
+ }
1241
+ }
1242
+ const existingBindingIds = /* @__PURE__ */ new Set();
1243
+ if (isTTY) {
1244
+ try {
1245
+ const existingBindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
1246
+ if (Array.isArray(existingBindings)) {
1247
+ for (const binding of existingBindings) {
1248
+ if (binding.apiKeyId === keyRecord.id && binding.providerId === provider.id) {
1249
+ existingBindingIds.add(binding.id);
1250
+ }
1251
+ }
1252
+ }
1253
+ } catch {
1254
+ }
1255
+ }
1256
+ const authorizationStartedAt = /* @__PURE__ */ new Date();
1257
+ const result = await initiateOAuth(keyRecord.id, provider.id, jwtToken, XAPI_API_HOST, scopes);
1067
1258
  const { authorizationUrl } = result;
1068
- const isTTY = process.stdout.isTTY;
1069
1259
  if (isTTY) {
1070
- console.error(`
1260
+ if (!headerPrinted) {
1261
+ console.error(`
1071
1262
  Provider : ${provider.name}`);
1072
- console.error(` API Key : ${keyRecord.keyPreview}`);
1263
+ console.error(` API Key : ${keyRecord.keyPreview}`);
1264
+ }
1265
+ if (scopes) {
1266
+ console.error(` Scopes : ${scopes}`);
1267
+ }
1073
1268
  console.error(`
1074
1269
  Authorization URL:
1075
1270
  ${authorizationUrl}
@@ -1077,14 +1272,20 @@ async function oauthBind(args, flags) {
1077
1272
  console.error(" Opening browser...");
1078
1273
  openBrowser(authorizationUrl);
1079
1274
  console.error(" Waiting for you to complete authorization in the browser...\n");
1080
- const binding = await pollForBinding(keyRecord.id, provider.id, jwtToken);
1275
+ const binding = await pollForBinding(
1276
+ keyRecord.id,
1277
+ provider.id,
1278
+ jwtToken,
1279
+ authorizationStartedAt,
1280
+ existingBindingIds
1281
+ );
1081
1282
  if (process.stdout.isTTY) process.stdout.write("\n");
1082
1283
  if (binding) {
1083
1284
  const account = binding.providerAccountName || "unknown";
1084
1285
  console.error(`
1085
1286
  Authorization complete! Bound to @${account}
1086
1287
  `);
1087
- output({ status: "success", provider: provider.name, account }, flags.format);
1288
+ output({ status: "success", provider: provider.name, account, scopes }, flags.format);
1088
1289
  } else {
1089
1290
  err("oauth bind timed out", 'Authorization was not completed within 5 minutes. Run "xapi-to oauth bind" again.');
1090
1291
  }
@@ -1093,7 +1294,8 @@ async function oauthBind(args, flags) {
1093
1294
  status: "pending",
1094
1295
  provider: provider.name,
1095
1296
  apiKey: keyRecord.keyPreview,
1096
- authorizationUrl
1297
+ authorizationUrl,
1298
+ scopes
1097
1299
  }, flags.format);
1098
1300
  }
1099
1301
  } catch (e) {
@@ -1216,6 +1418,7 @@ COMMANDS
1216
1418
 
1217
1419
  register [referral-code] Create a new user account (apiKey saved automatically)
1218
1420
  --referral-code <code> Register with an inviter's referral code (also: --referralCode, or as positional arg)
1421
+ --force Replace an existing saved apiKey
1219
1422
  balance Show current account balance
1220
1423
  topup [--amount <usd>] [--method stripe|x402] Generate payment URL
1221
1424
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xapi-to",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Agent-friendly CLI for xapi - discover and call capabilities and APIs",
5
5
  "type": "module",
6
6
  "bin": {