@spectratools/xapi-cli 0.4.0 → 0.6.0

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 +20 -7
  2. package/dist/cli.js +468 -7
  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,10 +63,18 @@ 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 bookmark 1234567890 --format json
71
+ xapi-cli posts unbookmark 1234567890 --format json
68
72
  xapi-cli posts likes 1234567890 --max-results 100 --format json
69
73
  xapi-cli posts retweets 1234567890 --max-results 100 --format json
74
+ xapi-cli users block spammer123 --format json
75
+ xapi-cli users unblock spammer123 --format json
76
+ xapi-cli users mute noisyaccount --format json
77
+ xapi-cli users unmute noisyaccount --format json
70
78
 
71
79
  # 4) Timeline monitor
72
80
  xapi-cli timeline home --max-results 50 --format json
@@ -75,6 +83,11 @@ xapi-cli timeline mentions --max-results 50 --format json
75
83
  # 5) DM assistant loop
76
84
  xapi-cli dm conversations --max-results 20 --format json
77
85
  xapi-cli dm send 12345 --text "hello from agent" --format json
86
+
87
+ # 6) List curation loop
88
+ xapi-cli lists create --name "Core devs" --description "Builders only" --private --format json
89
+ xapi-cli lists add-member 1234567890 jack --format json
90
+ xapi-cli lists members 1234567890 --max-results 100 --format json
78
91
  ```
79
92
 
80
93
  ## Notes
@@ -83,4 +96,4 @@ xapi-cli dm send 12345 --text "hello from agent" --format json
83
96
  - `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
97
  - Baseline files are read-only input (newline-delimited follower IDs) and are never mutated by the CLI.
85
98
  - `X_BEARER_TOKEN` is for read-only app auth.
86
- - `X_ACCESS_TOKEN` is required for write actions (`posts create`, `posts delete`, `dm send`).
99
+ - `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
@@ -189,6 +189,15 @@ function createXApiClient(bearerToken) {
189
189
  function likePost(userId, tweetId) {
190
190
  return post(`/users/${userId}/likes`, { tweet_id: tweetId });
191
191
  }
192
+ function unlikePost(userId, tweetId) {
193
+ return del(`/users/${userId}/likes/${tweetId}`);
194
+ }
195
+ function bookmarkPost(userId, tweetId) {
196
+ return post(`/users/${userId}/bookmarks`, { tweet_id: tweetId });
197
+ }
198
+ function unbookmarkPost(userId, tweetId) {
199
+ return del(`/users/${userId}/bookmarks/${tweetId}`);
200
+ }
192
201
  function retweetPost(userId, tweetId) {
193
202
  return post(`/users/${userId}/retweets`, { tweet_id: tweetId });
194
203
  }
@@ -225,6 +234,22 @@ function createXApiClient(bearerToken) {
225
234
  function unfollowUser(sourceUserId, targetUserId) {
226
235
  return del(`/users/${sourceUserId}/following/${targetUserId}`);
227
236
  }
237
+ function blockUser(sourceUserId, targetUserId) {
238
+ return post(`/users/${sourceUserId}/blocking`, {
239
+ target_user_id: targetUserId
240
+ });
241
+ }
242
+ function unblockUser(sourceUserId, targetUserId) {
243
+ return del(`/users/${sourceUserId}/blocking/${targetUserId}`);
244
+ }
245
+ function muteUser(sourceUserId, targetUserId) {
246
+ return post(`/users/${sourceUserId}/muting`, {
247
+ target_user_id: targetUserId
248
+ });
249
+ }
250
+ function unmuteUser(sourceUserId, targetUserId) {
251
+ return del(`/users/${sourceUserId}/muting/${targetUserId}`);
252
+ }
228
253
  function getUserPosts(id, maxResults, nextToken) {
229
254
  return get(`/users/${id}/tweets`, {
230
255
  max_results: maxResults,
@@ -280,6 +305,24 @@ function createXApiClient(bearerToken) {
280
305
  ...nextToken ? { pagination_token: nextToken } : {}
281
306
  });
282
307
  }
308
+ function createList(name, description, isPrivate) {
309
+ return post("/lists", {
310
+ name,
311
+ ...description ? { description } : {},
312
+ ...isPrivate !== void 0 ? { private: isPrivate } : {}
313
+ });
314
+ }
315
+ function deleteList(id) {
316
+ return del(`/lists/${id}`);
317
+ }
318
+ function addListMember(id, userId) {
319
+ return post(`/lists/${id}/members`, {
320
+ user_id: userId
321
+ });
322
+ }
323
+ function removeListMember(id, userId) {
324
+ return del(`/lists/${id}/members/${userId}`);
325
+ }
283
326
  function getTrendingPlaces() {
284
327
  return getV1_1(
285
328
  "/trends/available.json"
@@ -318,6 +361,9 @@ function createXApiClient(bearerToken) {
318
361
  getPostLikes,
319
362
  getPostRetweets,
320
363
  likePost,
364
+ unlikePost,
365
+ bookmarkPost,
366
+ unbookmarkPost,
321
367
  retweetPost,
322
368
  getUserByUsername,
323
369
  getUserById,
@@ -325,6 +371,10 @@ function createXApiClient(bearerToken) {
325
371
  getUserFollowing,
326
372
  followUser,
327
373
  unfollowUser,
374
+ blockUser,
375
+ unblockUser,
376
+ muteUser,
377
+ unmuteUser,
328
378
  getUserPosts,
329
379
  getUserMentions,
330
380
  searchUsers,
@@ -333,6 +383,10 @@ function createXApiClient(bearerToken) {
333
383
  getList,
334
384
  getListMembers,
335
385
  getListPosts,
386
+ createList,
387
+ deleteList,
388
+ addListMember,
389
+ removeListMember,
336
390
  getTrendingPlaces,
337
391
  getTrendsByLocation,
338
392
  getDmConversations,
@@ -468,10 +522,28 @@ import { Cli as Cli2, z as z3 } from "incur";
468
522
  var lists = Cli2.create("lists", {
469
523
  description: "Manage and browse X lists."
470
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
+ }
471
543
  lists.command("get", {
472
544
  description: "Get a list by ID.",
473
545
  args: z3.object({
474
- id: z3.string().describe("List ID")
546
+ id: listIdSchema
475
547
  }),
476
548
  env: xApiReadEnv,
477
549
  output: z3.object({
@@ -514,10 +586,180 @@ lists.command("get", {
514
586
  );
515
587
  }
516
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
+ });
517
759
  lists.command("members", {
518
760
  description: "List members of an X list.",
519
761
  args: z3.object({
520
- id: z3.string().describe("List ID")
762
+ id: listIdSchema
521
763
  }),
522
764
  options: z3.object({
523
765
  maxResults: z3.number().default(100).describe("Maximum members to return")
@@ -554,7 +796,7 @@ lists.command("members", {
554
796
  lists.command("posts", {
555
797
  description: "Get posts from an X list.",
556
798
  args: z3.object({
557
- id: z3.string().describe("List ID")
799
+ id: listIdSchema
558
800
  }),
559
801
  options: z3.object({
560
802
  maxResults: z3.number().default(25).describe("Maximum posts to return"),
@@ -815,6 +1057,78 @@ posts.command("like", {
815
1057
  }
816
1058
  }
817
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
+ });
818
1132
  posts.command("retweet", {
819
1133
  description: "Retweet a post by ID.",
820
1134
  args: z4.object({
@@ -935,6 +1249,7 @@ timeline.command("home", {
935
1249
  description: "View your home timeline.",
936
1250
  options: z5.object({
937
1251
  maxResults: z5.number().default(25).describe("Maximum posts to return (5\u2013100)"),
1252
+ sinceId: z5.string().optional().describe("Only return posts newer than this post ID"),
938
1253
  verbose: z5.boolean().optional().describe("Show full text without truncation")
939
1254
  }),
940
1255
  alias: { maxResults: "n" },
@@ -954,14 +1269,18 @@ timeline.command("home", {
954
1269
  }),
955
1270
  examples: [
956
1271
  { description: "View your home timeline" },
957
- { options: { maxResults: 50 }, description: "View 50 posts" }
1272
+ { options: { maxResults: 50 }, description: "View 50 posts" },
1273
+ {
1274
+ options: { sinceId: "1900123456789012345" },
1275
+ description: "Resume from last-seen post ID"
1276
+ }
958
1277
  ],
959
1278
  async run(c) {
960
1279
  const client = createXApiClient(readAuthToken(c.env));
961
1280
  const meRes = await client.getMe();
962
1281
  const userId = meRes.data.id;
963
1282
  const allPosts = await collectPaged(
964
- (limit, cursor) => client.getHomeTimeline(userId, limit, cursor),
1283
+ (limit, cursor) => client.getHomeTimeline(userId, limit, cursor, c.options.sinceId),
965
1284
  (post) => ({
966
1285
  id: post.id,
967
1286
  text: c.options.verbose ? post.text : truncateText(post.text),
@@ -994,6 +1313,7 @@ timeline.command("mentions", {
994
1313
  description: "View your recent mentions.",
995
1314
  options: z5.object({
996
1315
  maxResults: z5.number().default(25).describe("Maximum mentions to return"),
1316
+ sinceId: z5.string().optional().describe("Only return mentions newer than this post ID"),
997
1317
  verbose: z5.boolean().optional().describe("Show full text without truncation")
998
1318
  }),
999
1319
  alias: { maxResults: "n" },
@@ -1009,13 +1329,19 @@ timeline.command("mentions", {
1009
1329
  ),
1010
1330
  count: z5.number()
1011
1331
  }),
1012
- examples: [{ description: "View your recent mentions" }],
1332
+ examples: [
1333
+ { description: "View your recent mentions" },
1334
+ {
1335
+ options: { sinceId: "1900123456789012345" },
1336
+ description: "Resume mentions from last-seen post ID"
1337
+ }
1338
+ ],
1013
1339
  async run(c) {
1014
1340
  const client = createXApiClient(readAuthToken(c.env));
1015
1341
  const meRes = await client.getMe();
1016
1342
  const userId = meRes.data.id;
1017
1343
  const allPosts = await collectPaged(
1018
- (limit, cursor) => client.getMentionsTimeline(userId, limit, cursor),
1344
+ (limit, cursor) => client.getMentionsTimeline(userId, limit, cursor, c.options.sinceId),
1019
1345
  (post) => ({
1020
1346
  id: post.id,
1021
1347
  text: c.options.verbose ? post.text : truncateText(post.text),
@@ -1310,6 +1636,130 @@ users.command("unfollow", {
1310
1636
  }
1311
1637
  }
1312
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
+ });
1313
1763
  users.command("followers", {
1314
1764
  description: "List followers of a user. Supports optional client-side baseline diffing for new follower detection.",
1315
1765
  args: z7.object({
@@ -1555,9 +2005,20 @@ var WRITE_OPERATIONS = /* @__PURE__ */ new Set([
1555
2005
  "posts create",
1556
2006
  "posts delete",
1557
2007
  "posts like",
2008
+ "posts unlike",
2009
+ "posts bookmark",
2010
+ "posts unbookmark",
1558
2011
  "posts retweet",
1559
2012
  "users follow",
1560
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",
1561
2022
  "dm send"
1562
2023
  ]);
1563
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.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "X (Twitter) API CLI for spectra-the-bot",
5
5
  "type": "module",
6
6
  "license": "MIT",