xapi-to 0.1.14 → 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 +39 -38
  2. package/dist/index.js +293 -90
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,25 +21,25 @@ The published CLI runs on Node.js 18+. Bun is only required for local source dev
21
21
 
22
22
  ```bash
23
23
  # 1. Register a new account (apiKey saved automatically)
24
- xapi register
24
+ xapi-to register
25
25
 
26
26
  # 1b. Or register with an inviter's referral code (please replace xapito to your referral code)
27
- xapi register --referral-code xapito
27
+ xapi-to register --referral-code xapito
28
28
 
29
29
  # 2. Or set an existing key
30
- xapi config set apiKey=sk-xxx
30
+ xapi-to config set apiKey=sk-xxx
31
31
 
32
32
  # 3. Or via env var
33
33
  export XAPI_KEY=sk-xxx
34
34
 
35
35
  # 4. Verify connectivity
36
- xapi config health
36
+ xapi-to config health
37
37
  ```
38
38
 
39
39
  ## Usage
40
40
 
41
41
  ```
42
- xapi <command> [args] [flags]
42
+ xapi-to <command> [args] [flags]
43
43
  ```
44
44
 
45
45
  ### Action Commands
@@ -47,23 +47,23 @@ xapi <command> [args] [flags]
47
47
  Unified interface for capabilities (built-in) and APIs (third-party). Use `--source capability|api` to filter.
48
48
 
49
49
  ```bash
50
- xapi list # list all actions
51
- xapi list --source capability # only built-in capabilities
52
- xapi list --source api --category DeFi # filter by source and category
53
- xapi list --page 2 --page-size 20 # pagination
54
- xapi list --service-id <id> # filter by service
50
+ xapi-to list # list all actions
51
+ xapi-to list --source capability # only built-in capabilities
52
+ xapi-to list --source api --category DeFi # filter by source and category
53
+ xapi-to list --page 2 --page-size 20 # pagination
54
+ xapi-to list --service-id <id> # filter by service
55
55
 
56
- xapi search "twitter" # search by keyword
57
- xapi search "token price" --source api # search APIs only
56
+ xapi-to search "twitter" # search by keyword
57
+ xapi-to search "token price" --source api # search APIs only
58
58
 
59
- xapi categories # list all categories
60
- xapi categories --source capability # categories for capabilities only
59
+ xapi-to categories # list all categories
60
+ xapi-to categories --source capability # categories for capabilities only
61
61
 
62
- xapi services # list all services
63
- xapi services --category Social --page-size 10 # filter and paginate
62
+ xapi-to services # list all services
63
+ xapi-to services --category Social --page-size 10 # filter and paginate
64
64
 
65
- xapi get twitter.tweet_detail # get action schema
66
- xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' # execute
65
+ xapi-to get twitter.tweet_detail # get action schema
66
+ xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' # execute
67
67
  ```
68
68
 
69
69
  ### OAuth
@@ -71,30 +71,30 @@ xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' # execute
71
71
  Bind third-party OAuth accounts (e.g. Twitter) to your API key.
72
72
 
73
73
  ```bash
74
- xapi oauth bind --provider twitter # bind Twitter account
75
- xapi oauth status # list current bindings
76
- xapi oauth unbind <binding-id> # remove a binding
77
- xapi oauth providers # list available providers
74
+ xapi-to oauth bind --provider twitter # bind Twitter account
75
+ xapi-to oauth status # list current bindings
76
+ xapi-to oauth unbind <binding-id> # remove a binding
77
+ xapi-to oauth providers # list available providers
78
78
  ```
79
79
 
80
80
  ### Account
81
81
 
82
82
  ```bash
83
- xapi register # create account, saves apiKey automatically
84
- xapi register --referral-code xapito # register with an inviter's referral code (please replace xapito to your referral code)
85
- xapi register xapito # positional shorthand for --referral-code
86
- xapi balance # show USD balance
87
- xapi topup # generate payment URL
88
- xapi topup --method stripe --amount 10 # stripe, $10
89
- xapi topup --method x402 # x402 (USDC on Base)
83
+ xapi-to register # create account, saves apiKey automatically
84
+ xapi-to register --referral-code xapito # register with an inviter's referral code (please replace xapito to your referral code)
85
+ xapi-to register xapito # positional shorthand for --referral-code
86
+ xapi-to balance # show USD balance
87
+ xapi-to topup # generate payment URL
88
+ xapi-to topup --method stripe --amount 10 # stripe, $10
89
+ xapi-to topup --method x402 # x402 (USDC on Base)
90
90
  ```
91
91
 
92
92
  ### Config
93
93
 
94
94
  ```bash
95
- xapi config show # show current config
96
- xapi config set apiKey=sk-xxx # save API key
97
- xapi config health # check backend connectivity
95
+ xapi-to config show # show current config
96
+ xapi-to config set apiKey=sk-xxx # save API key
97
+ xapi-to config health # check backend connectivity
98
98
  ```
99
99
 
100
100
  ## Workflow: Always GET before CALL
@@ -103,13 +103,13 @@ Before calling any action, always read its schema first to understand required p
103
103
 
104
104
  ```bash
105
105
  # 1. Find the action
106
- xapi search "twitter"
106
+ xapi-to search "twitter"
107
107
 
108
108
  # 2. Read its schema
109
- xapi get twitter.tweet_detail
109
+ xapi-to get twitter.tweet_detail
110
110
 
111
111
  # 3. Call with correct parameters
112
- xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
112
+ xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
113
113
  ```
114
114
 
115
115
  ## Output Formats
@@ -117,9 +117,9 @@ xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
117
117
  All output is JSON by default — designed for agent consumption.
118
118
 
119
119
  ```bash
120
- xapi list --format json # default, machine-readable
121
- xapi list --format pretty # pretty-printed JSON
122
- xapi list --format table # human-readable table
120
+ xapi-to list --format json # default, machine-readable
121
+ xapi-to list --format pretty # pretty-printed JSON
122
+ xapi-to list --format table # human-readable table
123
123
  ```
124
124
 
125
125
  ## Environment Variables
@@ -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") {
@@ -131,7 +172,7 @@ async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS) {
131
172
  }
132
173
  if (data?.error === "OAuth Required" || data?.statusCode === 403 && data?.message?.includes("OAuth")) {
133
174
  throw new Error(
134
- (data.message || "OAuth authorization required") + '. Run "xapi oauth bind" to connect your account.'
175
+ (data.message || "OAuth authorization required") + '. Run "xapi-to oauth bind" to connect your account.'
135
176
  );
136
177
  }
137
178
  }
@@ -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
@@ -528,10 +572,10 @@ function generateCode(target, params) {
528
572
 
529
573
  // src/commands/action.ts
530
574
  var VALID_SOURCES = ["capability", "api"];
531
- var LIST_HELP = `xapi list - List all actions
575
+ var LIST_HELP = `xapi-to list - List all actions
532
576
 
533
577
  USAGE
534
- xapi list [flags]
578
+ xapi-to list [flags]
535
579
 
536
580
  FLAGS
537
581
  --source capability|api Filter by source type
@@ -542,14 +586,14 @@ FLAGS
542
586
  --format json|pretty|table Output format
543
587
 
544
588
  EXAMPLES
545
- xapi list
546
- xapi list --source api --format table
547
- xapi list --category social --page 2
589
+ xapi-to list
590
+ xapi-to list --source api --format table
591
+ xapi-to list --category social --page 2
548
592
  `;
549
- var SEARCH_HELP = `xapi search - Search actions by keyword
593
+ var SEARCH_HELP = `xapi-to search - Search actions by keyword
550
594
 
551
595
  USAGE
552
- xapi search <query> [flags]
596
+ xapi-to search <query> [flags]
553
597
 
554
598
  FLAGS
555
599
  --source capability|api Filter by source type
@@ -559,14 +603,14 @@ FLAGS
559
603
  --format json|pretty|table Output format
560
604
 
561
605
  EXAMPLES
562
- xapi search twitter
563
- xapi search "tweet detail" --source api
564
- xapi search weather --category utility --format table
606
+ xapi-to search twitter
607
+ xapi-to search "tweet detail" --source api
608
+ xapi-to search weather --category utility --format table
565
609
  `;
566
- var GET_HELP = `xapi get - Get action schema
610
+ var GET_HELP = `xapi-to get - Get action schema
567
611
 
568
612
  USAGE
569
- xapi get <id> [flags]
613
+ xapi-to get <id> [flags]
570
614
 
571
615
  FLAGS
572
616
  --method GET|POST|... Filter by HTTP method
@@ -591,15 +635,15 @@ CODE TARGETS
591
635
  go Go (net/http)
592
636
 
593
637
  EXAMPLES
594
- xapi get twitter.tweet_detail
595
- xapi get twitter.tweet_detail --method POST
596
- xapi get twitter.tweet_detail --code curl
597
- xapi get twitter.tweet_detail --code python.httpx --format pretty
638
+ xapi-to get twitter.tweet_detail
639
+ xapi-to get twitter.tweet_detail --method POST
640
+ xapi-to get twitter.tweet_detail --code curl
641
+ xapi-to get twitter.tweet_detail --code python.httpx --format pretty
598
642
  `;
599
- var CALL_HELP = `xapi call - Execute an action
643
+ var CALL_HELP = `xapi-to call - Execute an action
600
644
 
601
645
  USAGE
602
- xapi call <id> --input '{"key":"val"}' [flags]
646
+ xapi-to call <id> --input '{"key":"val"}' [flags]
603
647
 
604
648
  FLAGS
605
649
  --input <json> Input payload as JSON (required for execution)
@@ -625,9 +669,9 @@ CODE TARGETS
625
669
  go Go (net/http)
626
670
 
627
671
  EXAMPLES
628
- xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
629
- xapi call twitter.tweet_detail --input '{"tweet_id":"123"}' --code py
630
- xapi call twitter.tweet_detail --input '{"tweet_id":"123"}' --code curl --format pretty
672
+ xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
673
+ xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code py
674
+ xapi-to call twitter.tweet_detail --input '{"tweet_id":"123"}' --code curl --format pretty
631
675
  `;
632
676
  function showHelpIfRequested(flags, helpText) {
633
677
  if (flags.help) {
@@ -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 ?? "",
@@ -688,8 +733,9 @@ async function actionList2(args, flags) {
688
733
  async function actionSearch2(args, flags) {
689
734
  showHelpIfRequested(flags, SEARCH_HELP);
690
735
  const query = args[0];
691
- if (!query) err("usage: xapi search <query>");
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 ?? "",
@@ -756,7 +804,7 @@ async function actionServices2(args, flags) {
756
804
  async function actionGet2(args, flags) {
757
805
  showHelpIfRequested(flags, GET_HELP);
758
806
  const id = args[0];
759
- if (!id) err("usage: xapi get <id> [--method GET|POST|DELETE|...]");
807
+ if (!id) err("usage: xapi-to get <id> [--method GET|POST|DELETE|...]");
760
808
  if (flags.code) validateCodeFlag(flags);
761
809
  const cfg = getConfig();
762
810
  try {
@@ -788,7 +836,7 @@ async function actionGet2(args, flags) {
788
836
  async function actionCall2(args, flags) {
789
837
  showHelpIfRequested(flags, CALL_HELP);
790
838
  const id = args[0];
791
- if (!id) err(`usage: xapi call <id> --input '{"key":"val"}'`);
839
+ if (!id) err(`usage: xapi-to call <id> --input '{"key":"val"}'`);
792
840
  if (flags.code) validateCodeFlag(flags);
793
841
  const cfg = getConfig();
794
842
  let input = {};
@@ -826,29 +874,29 @@ __export(config_exports, {
826
874
  configSet: () => configSet,
827
875
  configShow: () => configShow
828
876
  });
829
- var CONFIG_HELP = `xapi config - Manage CLI configuration
877
+ var CONFIG_HELP = `xapi-to config - Manage CLI configuration
830
878
 
831
879
  USAGE
832
- xapi config <command> [flags]
880
+ xapi-to config <command> [flags]
833
881
 
834
882
  COMMANDS
835
883
  show Show current config (host, apiKey path, etc.)
836
884
  set apiKey=<key> Save API key to ~/.xapi/config.json
837
- health Check backend connectivity (alias: xapi health)
885
+ health Check backend connectivity (alias: xapi-to health)
838
886
 
839
887
  FLAGS
840
888
  --format json|pretty|table Output format
841
889
 
842
890
  EXAMPLES
843
- xapi config show
844
- xapi config set apiKey=xapi_abc123
845
- xapi config health
891
+ xapi-to config show
892
+ xapi-to config set apiKey=xapi_abc123
893
+ xapi-to config health
846
894
  `;
847
895
  async function configShow(args, flags) {
848
896
  showConfig();
849
897
  }
850
898
  async function configSet(args, flags) {
851
- if (args.length === 0) err("usage: xapi config set apiKey=<key>");
899
+ if (args.length === 0) err("usage: xapi-to config set apiKey=<key>");
852
900
  const updates = {};
853
901
  for (const arg of args) {
854
902
  const eq = arg.indexOf("=");
@@ -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,14 +1074,109 @@ 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
  }
1019
- var OAUTH_HELP = `xapi oauth - Manage OAuth bindings
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
+ }
1176
+ var OAUTH_HELP = `xapi-to oauth - Manage OAuth bindings
1020
1177
 
1021
1178
  USAGE
1022
- xapi oauth <command> [flags]
1179
+ xapi-to oauth <command> [flags]
1023
1180
 
1024
1181
  COMMANDS
1025
1182
  bind [--provider <name>] Bind an OAuth account to your API key
@@ -1029,15 +1186,17 @@ 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
- xapi oauth bind
1036
- xapi oauth bind --provider twitter
1037
- xapi oauth status
1038
- xapi oauth status --format pretty
1039
- xapi oauth unbind abc123
1040
- xapi oauth providers
1193
+ xapi-to oauth bind
1194
+ xapi-to oauth bind --provider twitter
1195
+ xapi-to oauth bind --scopes "tweet.read users.read"
1196
+ xapi-to oauth status
1197
+ xapi-to oauth status --format pretty
1198
+ xapi-to oauth unbind abc123
1199
+ xapi-to oauth providers
1041
1200
  `;
1042
1201
  async function oauthBind(args, flags) {
1043
1202
  const cfg = getConfig();
@@ -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,23 +1272,30 @@ 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
- err("oauth bind timed out", 'Authorization was not completed within 5 minutes. Run "xapi oauth bind" again.');
1290
+ err("oauth bind timed out", 'Authorization was not completed within 5 minutes. Run "xapi-to oauth bind" again.');
1090
1291
  }
1091
1292
  } else {
1092
1293
  output({
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) {
@@ -1110,7 +1312,7 @@ async function oauthStatus(args, flags) {
1110
1312
  if (!Array.isArray(bindings) || bindings.length === 0) {
1111
1313
  output({
1112
1314
  status: "no_bindings",
1113
- message: 'No OAuth bindings found. Run "xapi oauth bind" to connect an account.'
1315
+ message: 'No OAuth bindings found. Run "xapi-to oauth bind" to connect an account.'
1114
1316
  }, flags.format);
1115
1317
  return;
1116
1318
  }
@@ -1137,7 +1339,7 @@ async function oauthUnbind(args, flags) {
1137
1339
  const apiKey = cfg.apiKey;
1138
1340
  const bindingId = args[0];
1139
1341
  if (!bindingId) {
1140
- err("usage: xapi oauth unbind <binding-id>", 'Get the binding ID from "xapi oauth status"');
1342
+ err("usage: xapi-to oauth unbind <binding-id>", 'Get the binding ID from "xapi-to oauth status"');
1141
1343
  }
1142
1344
  try {
1143
1345
  const jwtToken = await loginAndGetJwt(apiKey);
@@ -1182,10 +1384,10 @@ function parseArgs(argv) {
1182
1384
  }
1183
1385
  return { positional, flags };
1184
1386
  }
1185
- var HELP = `xapi - agent-friendly CLI for xapi
1387
+ var HELP = `xapi-to - agent-friendly CLI for xapi
1186
1388
 
1187
1389
  USAGE
1188
- xapi <command> [args] [flags]
1390
+ xapi-to <command> [args] [flags]
1189
1391
 
1190
1392
  COMMANDS
1191
1393
  list List all actions
@@ -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
 
@@ -1223,11 +1426,11 @@ COMMANDS
1223
1426
 
1224
1427
  config show Show current config
1225
1428
  config set apiKey=<key> Save API key to ~/.xapi/config.json
1226
- config health Check backend connectivity (alias: xapi health)
1429
+ config health Check backend connectivity (alias: xapi-to health)
1227
1430
 
1228
1431
  GLOBAL FLAGS
1229
1432
  --format json|pretty|table Output format (default: json)
1230
- --help Show help (use with a command for details, e.g. xapi get --help)
1433
+ --help Show help (use with a command for details, e.g. xapi-to get --help)
1231
1434
 
1232
1435
  ENV VARS
1233
1436
  XAPI_KEY API key (header: XAPI-Key)
@@ -1235,21 +1438,21 @@ ENV VARS
1235
1438
  XAPI_OUTPUT Default output format
1236
1439
 
1237
1440
  EXAMPLES
1238
- xapi register
1239
- xapi register --referral-code xapito # register with an inviter's referral code
1240
- xapi register xapito # positional shorthand
1241
- xapi list --format table
1242
- xapi list --source capability
1243
- xapi search twitter --source api
1244
- xapi get twitter.tweet_detail
1245
- xapi get twitter.tweet_detail --code curl
1246
- xapi get twitter.tweet_detail --code py --format pretty
1247
- xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
1248
- xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' --code python
1249
- xapi categories
1250
- xapi services --format table
1251
- xapi config set apiKey=xapi_abc123
1252
- xapi health
1441
+ xapi-to register
1442
+ xapi-to register --referral-code xapito # register with an inviter's referral code
1443
+ xapi-to register xapito # positional shorthand
1444
+ xapi-to list --format table
1445
+ xapi-to list --source capability
1446
+ xapi-to search twitter --source api
1447
+ xapi-to get twitter.tweet_detail
1448
+ xapi-to get twitter.tweet_detail --code curl
1449
+ xapi-to get twitter.tweet_detail --code py --format pretty
1450
+ xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
1451
+ xapi-to call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' --code python
1452
+ xapi-to categories
1453
+ xapi-to services --format table
1454
+ xapi-to config set apiKey=xapi_abc123
1455
+ xapi-to health
1253
1456
  `;
1254
1457
  async function main() {
1255
1458
  const { positional, flags } = parseArgs(process.argv.slice(2));
@@ -1325,7 +1528,7 @@ async function main() {
1325
1528
  break;
1326
1529
  }
1327
1530
  default:
1328
- console.error(JSON.stringify({ error: `unknown command: ${cmd}`, hint: "run xapi --help" }));
1531
+ console.error(JSON.stringify({ error: `unknown command: ${cmd}`, hint: "run xapi-to --help" }));
1329
1532
  process.exit(1);
1330
1533
  }
1331
1534
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xapi-to",
3
- "version": "0.1.14",
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": {