@spectratools/xapi-cli 0.5.0 → 0.6.1

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 +36 -9
  2. package/dist/cli.js +398 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,8 +27,8 @@ xapi-cli mcp add
27
27
  # Read-only endpoints (search, profiles, trends, list reads)
28
28
  export X_BEARER_TOKEN=your_app_bearer_token
29
29
 
30
- # Write endpoints (create/delete post, send DM)
31
- # OAuth 2.0 user context token with the required write scopes
30
+ # Write endpoints (post interactions, list mutations, user moderation, and send DM)
31
+ # OAuth 2.0 user context token with required write scopes
32
32
  export X_ACCESS_TOKEN=your_oauth2_user_access_token
33
33
  ```
34
34
 
@@ -38,10 +38,10 @@ Auth behavior:
38
38
 
39
39
  ## Command Group Intent Summary
40
40
 
41
- - `posts` — Read/search/create/delete posts and inspect social engagement
42
- - `users` — Profile lookup, social graph traversal, and user timelines
41
+ - `posts` — Read/search/create/delete posts, plus like/unlike, retweet, and bookmark workflows
42
+ - `users` — Profile lookup, social graph traversal, timelines, and follow/block/mute moderation actions
43
43
  - `timeline` — Home timeline and mention stream monitoring
44
- - `lists` — List discovery, member inspection, and list feed reads
44
+ - `lists` — List lookup, creation/deletion, member management, and list feed reads
45
45
  - `trends` — Trend place discovery and per-location trend fetch
46
46
  - `dm` — Conversation listing and outbound direct messages
47
47
 
@@ -63,24 +63,51 @@ xapi-cli users followers jack --max-results 100 --format json
63
63
  # Baseline file format: one follower ID per line
64
64
  xapi-cli users followers jack --new-only --seen-ids-file ./seen-followers.txt --format json
65
65
 
66
- # 3) Moderation helper flow
66
+ # 3) Engagement + moderation helper flow
67
67
  xapi-cli posts get 1234567890 --format json
68
+ xapi-cli posts like 1234567890 --format json
69
+ xapi-cli posts unlike 1234567890 --format json
70
+ xapi-cli posts retweet 1234567890 --format json
71
+ xapi-cli posts bookmark 1234567890 --format json
72
+ xapi-cli posts unbookmark 1234567890 --format json
68
73
  xapi-cli posts likes 1234567890 --max-results 100 --format json
69
74
  xapi-cli posts retweets 1234567890 --max-results 100 --format json
75
+ xapi-cli users follow interesting_dev --format json
76
+ xapi-cli users unfollow inactive_account --format json
77
+ xapi-cli users block spammer123 --format json
78
+ xapi-cli users unblock spammer123 --format json
79
+ xapi-cli users mute noisyaccount --format json
80
+ xapi-cli users unmute noisyaccount --format json
70
81
 
71
82
  # 4) Timeline monitor
72
83
  xapi-cli timeline home --max-results 50 --format json
73
84
  xapi-cli timeline mentions --max-results 50 --format json
74
85
 
86
+ # 4b) Timeline polling with --since-id (resume from last-seen post)
87
+ # Store the newest post ID from the previous fetch, then pass it on the next call
88
+ # to retrieve only new posts since that point.
89
+ xapi-cli timeline home --since-id 1900123456789012345 --max-results 50 --format json
90
+ xapi-cli timeline mentions --since-id 1900123456789012345 --max-results 50 --format json
91
+
75
92
  # 5) DM assistant loop
76
93
  xapi-cli dm conversations --max-results 20 --format json
77
94
  xapi-cli dm send 12345 --text "hello from agent" --format json
95
+
96
+ # 6) List curation loop
97
+ xapi-cli lists create --name "Core devs" --description "Builders only" --private --format json
98
+ xapi-cli lists get 1234567890 --format json
99
+ xapi-cli lists add-member 1234567890 jack --format json
100
+ xapi-cli lists remove-member 1234567890 jack --format json
101
+ xapi-cli lists members 1234567890 --max-results 100 --format json
102
+ xapi-cli lists posts 1234567890 --max-results 25 --format json
103
+ xapi-cli lists delete 1234567890 --format json
78
104
  ```
79
105
 
80
106
  ## Notes
81
107
 
82
108
  - All commands support JSON output with `--format json`.
83
- - `users followers --new-only` performs **client-side diffing** against `--seen-ids-file`; it does not use an API-native `since_id` filter for follower deltas.
84
- - Baseline files are read-only input (newline-delimited follower IDs) and are never mutated by the CLI.
109
+ - `timeline home` and `timeline mentions` support `--since-id <post-id>` for incremental polling only posts newer than the given ID are returned. Store the newest ID from each fetch and pass it on the next call to avoid re-processing.
110
+ - `users followers --new-only` performs **client-side diffing** against `--seen-ids-file`; it does not use an API-native `since_id` filter for follower deltas. The baseline file is a newline-delimited list of follower IDs, one per line.
111
+ - Baseline files are read-only input and are never mutated by the CLI. Your application is responsible for updating the baseline after processing new followers.
85
112
  - `X_BEARER_TOKEN` is for read-only app auth.
86
- - `X_ACCESS_TOKEN` is required for write actions (`posts create`, `posts delete`, `dm send`).
113
+ - `X_ACCESS_TOKEN` is required for write actions (`posts create|delete|like|unlike|bookmark|unbookmark|retweet`, `users follow|unfollow|block|unblock|mute|unmute`, `lists create|delete|add-member|remove-member`, `dm send`).
package/dist/cli.js CHANGED
@@ -522,10 +522,28 @@ import { Cli as Cli2, z as z3 } from "incur";
522
522
  var lists = Cli2.create("lists", {
523
523
  description: "Manage and browse X lists."
524
524
  });
525
+ var listIdSchema = z3.string().min(1).describe("List ID");
526
+ var memberSchema = z3.string().trim().min(1).describe("Member to target (username with or without @, or user ID)");
527
+ var createListOptionsSchema = z3.object({
528
+ name: z3.string().trim().min(1).max(25).describe("List name (1-25 characters)"),
529
+ description: z3.string().trim().min(1).max(100).optional().describe("Optional list description (1-100 characters)"),
530
+ private: z3.boolean().default(false).describe("Create as a private list")
531
+ });
532
+ async function resolveMemberTarget(client, usernameOrId) {
533
+ const normalized = usernameOrId.replace(/^@/, "");
534
+ if (/^\d+$/.test(normalized)) {
535
+ return { id: normalized };
536
+ }
537
+ const user = await client.getUserByUsername(normalized);
538
+ return {
539
+ id: user.data.id,
540
+ username: user.data.username
541
+ };
542
+ }
525
543
  lists.command("get", {
526
544
  description: "Get a list by ID.",
527
545
  args: z3.object({
528
- id: z3.string().describe("List ID")
546
+ id: listIdSchema
529
547
  }),
530
548
  env: xApiReadEnv,
531
549
  output: z3.object({
@@ -568,10 +586,180 @@ lists.command("get", {
568
586
  );
569
587
  }
570
588
  });
589
+ lists.command("create", {
590
+ description: "Create a new list.",
591
+ options: createListOptionsSchema,
592
+ env: xApiWriteEnv,
593
+ output: z3.object({
594
+ id: z3.string(),
595
+ name: z3.string()
596
+ }),
597
+ examples: [
598
+ {
599
+ options: { name: "Core devs", description: "Builders only", private: true },
600
+ description: "Create a private list"
601
+ }
602
+ ],
603
+ async run(c) {
604
+ try {
605
+ const client = createXApiClient(writeAuthToken(c.env));
606
+ const res = await client.createList(c.options.name, c.options.description, c.options.private);
607
+ return c.ok(
608
+ res.data,
609
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
610
+ cta: {
611
+ description: "Next steps:",
612
+ commands: [
613
+ {
614
+ command: "lists get",
615
+ args: { id: res.data.id },
616
+ description: "View the list details"
617
+ },
618
+ {
619
+ command: "lists add-member",
620
+ args: { id: res.data.id, member: "username-or-id" },
621
+ description: "Add members to the new list"
622
+ }
623
+ ]
624
+ }
625
+ }
626
+ );
627
+ } catch (error) {
628
+ const authError = toWriteAuthError("lists create", error);
629
+ if (authError) return c.error(authError);
630
+ throw error;
631
+ }
632
+ }
633
+ });
634
+ lists.command("delete", {
635
+ description: "Delete a list by ID.",
636
+ args: z3.object({
637
+ id: listIdSchema.describe("List ID to delete")
638
+ }),
639
+ env: xApiWriteEnv,
640
+ output: z3.object({
641
+ deleted: z3.boolean(),
642
+ id: z3.string()
643
+ }),
644
+ examples: [{ args: { id: "1234567890" }, description: "Delete a list" }],
645
+ async run(c) {
646
+ try {
647
+ const client = createXApiClient(writeAuthToken(c.env));
648
+ const res = await client.deleteList(c.args.id);
649
+ return c.ok({ deleted: res.data.deleted, id: c.args.id });
650
+ } catch (error) {
651
+ const authError = toWriteAuthError("lists delete", error);
652
+ if (authError) return c.error(authError);
653
+ throw error;
654
+ }
655
+ }
656
+ });
657
+ lists.command("add-member", {
658
+ description: "Add a member to an X list.",
659
+ args: z3.object({
660
+ id: listIdSchema,
661
+ member: memberSchema
662
+ }),
663
+ env: xApiWriteEnv,
664
+ output: z3.object({
665
+ id: z3.string(),
666
+ member_id: z3.string(),
667
+ member_username: z3.string().optional(),
668
+ is_member: z3.boolean()
669
+ }),
670
+ examples: [
671
+ {
672
+ args: { id: "1234567890", member: "jack" },
673
+ description: "Add @jack to a list"
674
+ }
675
+ ],
676
+ async run(c) {
677
+ try {
678
+ const client = createXApiClient(writeAuthToken(c.env));
679
+ const member = await resolveMemberTarget(client, c.args.member);
680
+ const res = await client.addListMember(c.args.id, member.id);
681
+ return c.ok(
682
+ {
683
+ id: c.args.id,
684
+ member_id: member.id,
685
+ member_username: member.username,
686
+ is_member: res.data.is_member
687
+ },
688
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
689
+ cta: {
690
+ description: "Verify list membership:",
691
+ commands: [
692
+ {
693
+ command: "lists members",
694
+ args: { id: c.args.id },
695
+ description: "View current list members"
696
+ }
697
+ ]
698
+ }
699
+ }
700
+ );
701
+ } catch (error) {
702
+ const authError = toWriteAuthError("lists add-member", error);
703
+ if (authError) return c.error(authError);
704
+ throw error;
705
+ }
706
+ }
707
+ });
708
+ lists.command("remove-member", {
709
+ description: "Remove a member from an X list.",
710
+ args: z3.object({
711
+ id: listIdSchema,
712
+ member: memberSchema
713
+ }),
714
+ env: xApiWriteEnv,
715
+ output: z3.object({
716
+ id: z3.string(),
717
+ member_id: z3.string(),
718
+ member_username: z3.string().optional(),
719
+ is_member: z3.boolean()
720
+ }),
721
+ examples: [
722
+ {
723
+ args: { id: "1234567890", member: "jack" },
724
+ description: "Remove @jack from a list"
725
+ }
726
+ ],
727
+ async run(c) {
728
+ try {
729
+ const client = createXApiClient(writeAuthToken(c.env));
730
+ const member = await resolveMemberTarget(client, c.args.member);
731
+ const res = await client.removeListMember(c.args.id, member.id);
732
+ return c.ok(
733
+ {
734
+ id: c.args.id,
735
+ member_id: member.id,
736
+ member_username: member.username,
737
+ is_member: res.data.is_member
738
+ },
739
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
740
+ cta: {
741
+ description: "Next steps:",
742
+ commands: [
743
+ {
744
+ command: "lists members",
745
+ args: { id: c.args.id },
746
+ description: "Confirm updated membership"
747
+ }
748
+ ]
749
+ }
750
+ }
751
+ );
752
+ } catch (error) {
753
+ const authError = toWriteAuthError("lists remove-member", error);
754
+ if (authError) return c.error(authError);
755
+ throw error;
756
+ }
757
+ }
758
+ });
571
759
  lists.command("members", {
572
760
  description: "List members of an X list.",
573
761
  args: z3.object({
574
- id: z3.string().describe("List ID")
762
+ id: listIdSchema
575
763
  }),
576
764
  options: z3.object({
577
765
  maxResults: z3.number().default(100).describe("Maximum members to return")
@@ -608,7 +796,7 @@ lists.command("members", {
608
796
  lists.command("posts", {
609
797
  description: "Get posts from an X list.",
610
798
  args: z3.object({
611
- id: z3.string().describe("List ID")
799
+ id: listIdSchema
612
800
  }),
613
801
  options: z3.object({
614
802
  maxResults: z3.number().default(25).describe("Maximum posts to return"),
@@ -869,6 +1057,78 @@ posts.command("like", {
869
1057
  }
870
1058
  }
871
1059
  });
1060
+ posts.command("unlike", {
1061
+ description: "Unlike a post by ID.",
1062
+ args: z4.object({
1063
+ id: z4.string().describe("Post ID to unlike")
1064
+ }),
1065
+ env: xApiWriteEnv,
1066
+ output: z4.object({
1067
+ liked: z4.boolean(),
1068
+ id: z4.string()
1069
+ }),
1070
+ examples: [{ args: { id: "1234567890" }, description: "Unlike a post" }],
1071
+ async run(c) {
1072
+ try {
1073
+ const client = createXApiClient(writeAuthToken(c.env));
1074
+ const me = await client.getMe();
1075
+ const res = await client.unlikePost(me.data.id, c.args.id);
1076
+ return c.ok({ liked: res.data.liked, id: c.args.id });
1077
+ } catch (error) {
1078
+ const authError = toWriteAuthError("posts unlike", error);
1079
+ if (authError) return c.error(authError);
1080
+ throw error;
1081
+ }
1082
+ }
1083
+ });
1084
+ posts.command("bookmark", {
1085
+ description: "Bookmark a post by ID.",
1086
+ args: z4.object({
1087
+ id: z4.string().describe("Post ID to bookmark")
1088
+ }),
1089
+ env: xApiWriteEnv,
1090
+ output: z4.object({
1091
+ bookmarked: z4.boolean(),
1092
+ id: z4.string()
1093
+ }),
1094
+ examples: [{ args: { id: "1234567890" }, description: "Bookmark a post" }],
1095
+ async run(c) {
1096
+ try {
1097
+ const client = createXApiClient(writeAuthToken(c.env));
1098
+ const me = await client.getMe();
1099
+ const res = await client.bookmarkPost(me.data.id, c.args.id);
1100
+ return c.ok({ bookmarked: res.data.bookmarked, id: c.args.id });
1101
+ } catch (error) {
1102
+ const authError = toWriteAuthError("posts bookmark", error);
1103
+ if (authError) return c.error(authError);
1104
+ throw error;
1105
+ }
1106
+ }
1107
+ });
1108
+ posts.command("unbookmark", {
1109
+ description: "Remove bookmark from a post by ID.",
1110
+ args: z4.object({
1111
+ id: z4.string().describe("Post ID to unbookmark")
1112
+ }),
1113
+ env: xApiWriteEnv,
1114
+ output: z4.object({
1115
+ bookmarked: z4.boolean(),
1116
+ id: z4.string()
1117
+ }),
1118
+ examples: [{ args: { id: "1234567890" }, description: "Remove a bookmark from a post" }],
1119
+ async run(c) {
1120
+ try {
1121
+ const client = createXApiClient(writeAuthToken(c.env));
1122
+ const me = await client.getMe();
1123
+ const res = await client.unbookmarkPost(me.data.id, c.args.id);
1124
+ return c.ok({ bookmarked: res.data.bookmarked, id: c.args.id });
1125
+ } catch (error) {
1126
+ const authError = toWriteAuthError("posts unbookmark", error);
1127
+ if (authError) return c.error(authError);
1128
+ throw error;
1129
+ }
1130
+ }
1131
+ });
872
1132
  posts.command("retweet", {
873
1133
  description: "Retweet a post by ID.",
874
1134
  args: z4.object({
@@ -1376,6 +1636,130 @@ users.command("unfollow", {
1376
1636
  }
1377
1637
  }
1378
1638
  });
1639
+ users.command("block", {
1640
+ description: "Block a user by username or ID.",
1641
+ args: z7.object({
1642
+ username: z7.string().describe("Username (with or without @) or user ID")
1643
+ }),
1644
+ env: xApiWriteEnv,
1645
+ output: z7.object({
1646
+ id: z7.string(),
1647
+ username: z7.string(),
1648
+ blocking: z7.boolean()
1649
+ }),
1650
+ examples: [{ args: { username: "jack" }, description: "Block @jack" }],
1651
+ async run(c) {
1652
+ try {
1653
+ const client = createXApiClient(writeAuthToken(c.env));
1654
+ const me = await client.getMe();
1655
+ const targetRes = await resolveUser(client, c.args.username);
1656
+ const target = targetRes.data;
1657
+ const res = await client.blockUser(me.data.id, target.id);
1658
+ return c.ok({
1659
+ id: target.id,
1660
+ username: target.username,
1661
+ blocking: res.data.blocking
1662
+ });
1663
+ } catch (error) {
1664
+ const authError = toWriteAuthError("users block", error);
1665
+ if (authError) return c.error(authError);
1666
+ throw error;
1667
+ }
1668
+ }
1669
+ });
1670
+ users.command("unblock", {
1671
+ description: "Unblock a user by username or ID.",
1672
+ args: z7.object({
1673
+ username: z7.string().describe("Username (with or without @) or user ID")
1674
+ }),
1675
+ env: xApiWriteEnv,
1676
+ output: z7.object({
1677
+ id: z7.string(),
1678
+ username: z7.string(),
1679
+ blocking: z7.boolean()
1680
+ }),
1681
+ examples: [{ args: { username: "jack" }, description: "Unblock @jack" }],
1682
+ async run(c) {
1683
+ try {
1684
+ const client = createXApiClient(writeAuthToken(c.env));
1685
+ const me = await client.getMe();
1686
+ const targetRes = await resolveUser(client, c.args.username);
1687
+ const target = targetRes.data;
1688
+ const res = await client.unblockUser(me.data.id, target.id);
1689
+ return c.ok({
1690
+ id: target.id,
1691
+ username: target.username,
1692
+ blocking: res.data.blocking
1693
+ });
1694
+ } catch (error) {
1695
+ const authError = toWriteAuthError("users unblock", error);
1696
+ if (authError) return c.error(authError);
1697
+ throw error;
1698
+ }
1699
+ }
1700
+ });
1701
+ users.command("mute", {
1702
+ description: "Mute a user by username or ID.",
1703
+ args: z7.object({
1704
+ username: z7.string().describe("Username (with or without @) or user ID")
1705
+ }),
1706
+ env: xApiWriteEnv,
1707
+ output: z7.object({
1708
+ id: z7.string(),
1709
+ username: z7.string(),
1710
+ muting: z7.boolean()
1711
+ }),
1712
+ examples: [{ args: { username: "jack" }, description: "Mute @jack" }],
1713
+ async run(c) {
1714
+ try {
1715
+ const client = createXApiClient(writeAuthToken(c.env));
1716
+ const me = await client.getMe();
1717
+ const targetRes = await resolveUser(client, c.args.username);
1718
+ const target = targetRes.data;
1719
+ const res = await client.muteUser(me.data.id, target.id);
1720
+ return c.ok({
1721
+ id: target.id,
1722
+ username: target.username,
1723
+ muting: res.data.muting
1724
+ });
1725
+ } catch (error) {
1726
+ const authError = toWriteAuthError("users mute", error);
1727
+ if (authError) return c.error(authError);
1728
+ throw error;
1729
+ }
1730
+ }
1731
+ });
1732
+ users.command("unmute", {
1733
+ description: "Unmute a user by username or ID.",
1734
+ args: z7.object({
1735
+ username: z7.string().describe("Username (with or without @) or user ID")
1736
+ }),
1737
+ env: xApiWriteEnv,
1738
+ output: z7.object({
1739
+ id: z7.string(),
1740
+ username: z7.string(),
1741
+ muting: z7.boolean()
1742
+ }),
1743
+ examples: [{ args: { username: "jack" }, description: "Unmute @jack" }],
1744
+ async run(c) {
1745
+ try {
1746
+ const client = createXApiClient(writeAuthToken(c.env));
1747
+ const me = await client.getMe();
1748
+ const targetRes = await resolveUser(client, c.args.username);
1749
+ const target = targetRes.data;
1750
+ const res = await client.unmuteUser(me.data.id, target.id);
1751
+ return c.ok({
1752
+ id: target.id,
1753
+ username: target.username,
1754
+ muting: res.data.muting
1755
+ });
1756
+ } catch (error) {
1757
+ const authError = toWriteAuthError("users unmute", error);
1758
+ if (authError) return c.error(authError);
1759
+ throw error;
1760
+ }
1761
+ }
1762
+ });
1379
1763
  users.command("followers", {
1380
1764
  description: "List followers of a user. Supports optional client-side baseline diffing for new follower detection.",
1381
1765
  args: z7.object({
@@ -1621,9 +2005,20 @@ var WRITE_OPERATIONS = /* @__PURE__ */ new Set([
1621
2005
  "posts create",
1622
2006
  "posts delete",
1623
2007
  "posts like",
2008
+ "posts unlike",
2009
+ "posts bookmark",
2010
+ "posts unbookmark",
1624
2011
  "posts retweet",
1625
2012
  "users follow",
1626
2013
  "users unfollow",
2014
+ "users block",
2015
+ "users unblock",
2016
+ "users mute",
2017
+ "users unmute",
2018
+ "lists create",
2019
+ "lists delete",
2020
+ "lists add-member",
2021
+ "lists remove-member",
1627
2022
  "dm send"
1628
2023
  ]);
1629
2024
  cli.use(async ({ command, error }, next) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectratools/xapi-cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "X (Twitter) API CLI for spectra-the-bot",
5
5
  "type": "module",
6
6
  "license": "MIT",