xbird-mcp 0.1.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.

Potentially problematic release.


This version of xbird-mcp might be problematic. Click here for more details.

Files changed (85) hide show
  1. package/docs/listing.md +42 -0
  2. package/docs/mcp-setup.md +146 -0
  3. package/package.json +50 -0
  4. package/src/cli/context.ts +80 -0
  5. package/src/cli/output.ts +33 -0
  6. package/src/cli/program.ts +63 -0
  7. package/src/cli.ts +8 -0
  8. package/src/commands/about.ts +30 -0
  9. package/src/commands/bookmark.ts +40 -0
  10. package/src/commands/bookmarks-list.ts +108 -0
  11. package/src/commands/check.ts +27 -0
  12. package/src/commands/engagement.ts +31 -0
  13. package/src/commands/follow.ts +40 -0
  14. package/src/commands/followers.ts +132 -0
  15. package/src/commands/home.ts +59 -0
  16. package/src/commands/likes.ts +73 -0
  17. package/src/commands/lists.ts +101 -0
  18. package/src/commands/mentions.ts +32 -0
  19. package/src/commands/news.ts +38 -0
  20. package/src/commands/read.ts +36 -0
  21. package/src/commands/replies.ts +59 -0
  22. package/src/commands/search.ts +60 -0
  23. package/src/commands/tweet.ts +128 -0
  24. package/src/commands/user-tweets.ts +73 -0
  25. package/src/commands/user.ts +20 -0
  26. package/src/commands/whoami.ts +26 -0
  27. package/src/formatters/common.ts +49 -0
  28. package/src/formatters/list.ts +22 -0
  29. package/src/formatters/news.ts +23 -0
  30. package/src/formatters/tweet.ts +38 -0
  31. package/src/formatters/user.ts +20 -0
  32. package/src/index.ts +26 -0
  33. package/src/lib/auth.ts +105 -0
  34. package/src/lib/client-bookmarks.ts +62 -0
  35. package/src/lib/client-engagement.ts +152 -0
  36. package/src/lib/client-follow.ts +90 -0
  37. package/src/lib/client-full.ts +48 -0
  38. package/src/lib/client-likes.ts +62 -0
  39. package/src/lib/client-lists.ts +227 -0
  40. package/src/lib/client-media.ts +162 -0
  41. package/src/lib/client-news.ts +185 -0
  42. package/src/lib/client-posting.ts +163 -0
  43. package/src/lib/client-reading.ts +452 -0
  44. package/src/lib/client-search.ts +156 -0
  45. package/src/lib/client-timeline.ts +98 -0
  46. package/src/lib/client-user.ts +518 -0
  47. package/src/lib/client.ts +134 -0
  48. package/src/lib/config.ts +22 -0
  49. package/src/lib/constants.ts +55 -0
  50. package/src/lib/cookies.ts +132 -0
  51. package/src/lib/features.ts +175 -0
  52. package/src/lib/headers.ts +28 -0
  53. package/src/lib/paginate.ts +39 -0
  54. package/src/lib/query-ids.ts +190 -0
  55. package/src/lib/types.ts +147 -0
  56. package/src/lib/utils.ts +176 -0
  57. package/src/mcp/executor.ts +178 -0
  58. package/src/mcp/payment.ts +38 -0
  59. package/src/mcp/server.ts +53 -0
  60. package/src/mcp/tools.ts +389 -0
  61. package/src/server/app.ts +117 -0
  62. package/src/server/config/accounts.ts +137 -0
  63. package/src/server/config/pricing.ts +217 -0
  64. package/src/server/erc8004/register.ts +77 -0
  65. package/src/server/middleware/account-pool.ts +101 -0
  66. package/src/server/middleware/error-handler.ts +27 -0
  67. package/src/server/middleware/payer-extract.ts +43 -0
  68. package/src/server/middleware/x402.ts +61 -0
  69. package/src/server/routes/accounts.ts +93 -0
  70. package/src/server/routes/authorize.ts +19 -0
  71. package/src/server/routes/bookmarks.ts +21 -0
  72. package/src/server/routes/engagement.ts +84 -0
  73. package/src/server/routes/follow.ts +32 -0
  74. package/src/server/routes/health.ts +14 -0
  75. package/src/server/routes/lists.ts +38 -0
  76. package/src/server/routes/media.ts +44 -0
  77. package/src/server/routes/mentions.ts +20 -0
  78. package/src/server/routes/news.ts +23 -0
  79. package/src/server/routes/search.ts +26 -0
  80. package/src/server/routes/timeline.ts +21 -0
  81. package/src/server/routes/tweets.ts +82 -0
  82. package/src/server/routes/users.ts +92 -0
  83. package/src/server/storage/accounts-db.ts +84 -0
  84. package/src/server.ts +34 -0
  85. package/tsconfig.json +29 -0
@@ -0,0 +1,40 @@
1
+ import type { Command } from "commander";
2
+ import { createContext } from "../cli/context.ts";
3
+ import { XClient } from "../lib/client-full.ts";
4
+ import { output, error } from "../cli/output.ts";
5
+ import { normalizeHandle } from "../lib/utils.ts";
6
+ import { emoji, green } from "../formatters/common.ts";
7
+
8
+ export function registerFollow(program: Command): void {
9
+ program
10
+ .command("follow <handle>")
11
+ .description("Follow a user")
12
+ .action(async function (this: Command, handle: string) {
13
+ const ctx = await createContext(this);
14
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
15
+ const normalized = normalizeHandle(handle);
16
+ const result = await client.follow(normalized);
17
+ if (!result.success) error(result.error ?? "Failed to follow");
18
+ if (ctx.format === "json") {
19
+ output({ success: true, action: "follow", handle: normalized }, ctx.format);
20
+ } else {
21
+ console.log(`${green(emoji("✓"))} Now following @${normalized}`);
22
+ }
23
+ });
24
+
25
+ program
26
+ .command("unfollow <handle>")
27
+ .description("Unfollow a user")
28
+ .action(async function (this: Command, handle: string) {
29
+ const ctx = await createContext(this);
30
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
31
+ const normalized = normalizeHandle(handle);
32
+ const result = await client.unfollow(normalized);
33
+ if (!result.success) error(result.error ?? "Failed to unfollow");
34
+ if (ctx.format === "json") {
35
+ output({ success: true, action: "unfollow", handle: normalized }, ctx.format);
36
+ } else {
37
+ console.log(`${green(emoji("✓"))} Unfollowed @${normalized}`);
38
+ }
39
+ });
40
+ }
@@ -0,0 +1,132 @@
1
+ import type { Command } from "commander";
2
+ import { createContext } from "../cli/context.ts";
3
+ import { XClient } from "../lib/client-full.ts";
4
+ import { output, error } from "../cli/output.ts";
5
+ import { normalizeHandle } from "../lib/utils.ts";
6
+ import { formatUserCompact } from "../formatters/user.ts";
7
+ import { autoPaginate } from "../lib/paginate.ts";
8
+ import type { TwitterUser } from "../lib/types.ts";
9
+
10
+ export function registerFollowers(program: Command): void {
11
+ program
12
+ .command("followers <handle>")
13
+ .description("Show followers of a user")
14
+ .option("-n, --count <number>", "Number of users", "20")
15
+ .option("--cursor <cursor>", "Pagination cursor")
16
+ .option("--all", "Fetch all pages")
17
+ .option("--max-pages <n>", "Max pages to fetch")
18
+ .option("--delay <ms>", "Delay between pages in ms")
19
+ .action(async function (this: Command, handle: string) {
20
+ const ctx = await createContext(this);
21
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
22
+ const opts = this.opts();
23
+ const count = parseInt(opts.count, 10);
24
+ const screenName = normalizeHandle(handle);
25
+
26
+ const userResult = await client.getUserByHandle(screenName);
27
+ if (!userResult.success || !userResult.user) {
28
+ error(userResult.error ?? `User @${screenName} not found`);
29
+ }
30
+
31
+ if (opts.all || opts.maxPages) {
32
+ const result = await autoPaginate<TwitterUser>(
33
+ async (cursor?) => {
34
+ const r = await client.getFollowers(userResult.user!.id, count, cursor);
35
+ return { items: r.users, cursor: r.cursor };
36
+ },
37
+ {
38
+ all: opts.all,
39
+ maxPages: opts.maxPages ? parseInt(opts.maxPages, 10) : undefined,
40
+ delay: opts.delay ? parseInt(opts.delay, 10) : undefined,
41
+ }
42
+ );
43
+
44
+ if (ctx.format === "json" || ctx.format === "json-full") {
45
+ output({ users: result.items, cursor: result.cursor }, "json");
46
+ } else {
47
+ for (const user of result.items) {
48
+ console.log(formatUserCompact(user));
49
+ }
50
+ if (result.items.length === 0) console.log("No followers found.");
51
+ }
52
+ } else {
53
+ const result = await client.getFollowers(
54
+ userResult.user!.id,
55
+ count,
56
+ opts.cursor
57
+ );
58
+ if (!result.success) error(result.error ?? "Failed to load followers");
59
+
60
+ if (ctx.format === "json") {
61
+ output({ users: result.users, cursor: result.cursor }, ctx.format);
62
+ } else {
63
+ for (const user of result.users) {
64
+ console.log(formatUserCompact(user));
65
+ }
66
+ if (result.users.length === 0) console.log("No followers found.");
67
+ if (result.cursor) console.log(`\nNext cursor: ${result.cursor}`);
68
+ }
69
+ }
70
+ });
71
+
72
+ program
73
+ .command("following <handle>")
74
+ .description("Show users that a user is following")
75
+ .option("-n, --count <number>", "Number of users", "20")
76
+ .option("--cursor <cursor>", "Pagination cursor")
77
+ .option("--all", "Fetch all pages")
78
+ .option("--max-pages <n>", "Max pages to fetch")
79
+ .option("--delay <ms>", "Delay between pages in ms")
80
+ .action(async function (this: Command, handle: string) {
81
+ const ctx = await createContext(this);
82
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
83
+ const opts = this.opts();
84
+ const count = parseInt(opts.count, 10);
85
+ const screenName = normalizeHandle(handle);
86
+
87
+ const userResult = await client.getUserByHandle(screenName);
88
+ if (!userResult.success || !userResult.user) {
89
+ error(userResult.error ?? `User @${screenName} not found`);
90
+ }
91
+
92
+ if (opts.all || opts.maxPages) {
93
+ const result = await autoPaginate<TwitterUser>(
94
+ async (cursor?) => {
95
+ const r = await client.getFollowing(userResult.user!.id, count, cursor);
96
+ return { items: r.users, cursor: r.cursor };
97
+ },
98
+ {
99
+ all: opts.all,
100
+ maxPages: opts.maxPages ? parseInt(opts.maxPages, 10) : undefined,
101
+ delay: opts.delay ? parseInt(opts.delay, 10) : undefined,
102
+ }
103
+ );
104
+
105
+ if (ctx.format === "json" || ctx.format === "json-full") {
106
+ output({ users: result.items, cursor: result.cursor }, "json");
107
+ } else {
108
+ for (const user of result.items) {
109
+ console.log(formatUserCompact(user));
110
+ }
111
+ if (result.items.length === 0) console.log("No following found.");
112
+ }
113
+ } else {
114
+ const result = await client.getFollowing(
115
+ userResult.user!.id,
116
+ count,
117
+ opts.cursor
118
+ );
119
+ if (!result.success) error(result.error ?? "Failed to load following");
120
+
121
+ if (ctx.format === "json") {
122
+ output({ users: result.users, cursor: result.cursor }, ctx.format);
123
+ } else {
124
+ for (const user of result.users) {
125
+ console.log(formatUserCompact(user));
126
+ }
127
+ if (result.users.length === 0) console.log("No following found.");
128
+ if (result.cursor) console.log(`\nNext cursor: ${result.cursor}`);
129
+ }
130
+ }
131
+ });
132
+ }
@@ -0,0 +1,59 @@
1
+ import type { Command } from "commander";
2
+ import { createContext } from "../cli/context.ts";
3
+ import { XClient } from "../lib/client-full.ts";
4
+ import { output, error } from "../cli/output.ts";
5
+ import { formatTweet } from "../formatters/tweet.ts";
6
+ import { autoPaginate } from "../lib/paginate.ts";
7
+
8
+ export function registerHome(program: Command): void {
9
+ program
10
+ .command("home")
11
+ .description("Show home timeline")
12
+ .option("-n, --count <number>", "Number of tweets", "20")
13
+ .option("--cursor <cursor>", "Pagination cursor")
14
+ .option("--following", "Show following-only timeline")
15
+ .option("--all", "Fetch all pages")
16
+ .option("--max-pages <n>", "Max pages to fetch")
17
+ .option("--delay <ms>", "Delay between pages in ms")
18
+ .action(async function (this: Command) {
19
+ const ctx = await createContext(this);
20
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
21
+ const opts = this.opts();
22
+ const count = parseInt(opts.count, 10);
23
+
24
+ if (opts.all || opts.maxPages) {
25
+ const result = await autoPaginate(
26
+ async (cursor?) => {
27
+ const r = await client.home(count, cursor, opts.following);
28
+ return { items: r.tweets, cursor: r.cursor };
29
+ },
30
+ {
31
+ all: opts.all,
32
+ maxPages: opts.maxPages ? parseInt(opts.maxPages, 10) : undefined,
33
+ delay: opts.delay ? parseInt(opts.delay, 10) : undefined,
34
+ }
35
+ );
36
+
37
+ if (ctx.format === "json" || ctx.format === "json-full") {
38
+ output({ tweets: result.items, cursor: result.cursor }, "json");
39
+ } else {
40
+ for (const tweet of result.items) {
41
+ console.log(formatTweet(tweet));
42
+ console.log("---");
43
+ }
44
+ }
45
+ } else {
46
+ const result = await client.home(count, opts.cursor, opts.following);
47
+ if (!result.success || !result.tweets) error(result.error ?? "Failed to load timeline");
48
+ if (ctx.format === "json") {
49
+ output({ tweets: result.tweets, cursor: result.cursor }, ctx.format);
50
+ } else {
51
+ for (const tweet of result.tweets!) {
52
+ console.log(formatTweet(tweet));
53
+ console.log("---");
54
+ }
55
+ if (result.cursor) console.log(`\nNext cursor: ${result.cursor}`);
56
+ }
57
+ }
58
+ });
59
+ }
@@ -0,0 +1,73 @@
1
+ import type { Command } from "commander";
2
+ import { createContext } from "../cli/context.ts";
3
+ import { XClient } from "../lib/client-full.ts";
4
+ import { output, error } from "../cli/output.ts";
5
+ import { normalizeHandle } from "../lib/utils.ts";
6
+ import { formatTweet } from "../formatters/tweet.ts";
7
+ import { autoPaginate } from "../lib/paginate.ts";
8
+
9
+ export function registerLikes(program: Command): void {
10
+ program
11
+ .command("likes <handle>")
12
+ .description("Show tweets liked by a user")
13
+ .option("-n, --count <number>", "Number of likes", "20")
14
+ .option("--cursor <cursor>", "Pagination cursor")
15
+ .option("--all", "Fetch all pages")
16
+ .option("--max-pages <n>", "Max pages to fetch")
17
+ .option("--delay <ms>", "Delay between pages in ms")
18
+ .action(async function (this: Command, handle: string) {
19
+ const ctx = await createContext(this);
20
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
21
+ const opts = this.opts();
22
+ const count = parseInt(opts.count, 10);
23
+ const screenName = normalizeHandle(handle);
24
+
25
+ // Resolve user ID
26
+ const userResult = await client.getUserByHandle(screenName);
27
+ if (!userResult.success || !userResult.user) {
28
+ error(userResult.error ?? `User @${screenName} not found`);
29
+ }
30
+
31
+ if (opts.all || opts.maxPages) {
32
+ const result = await autoPaginate(
33
+ async (cursor?) => {
34
+ const r = await client.getLikes(userResult.user!.id, count, cursor);
35
+ return { items: r.tweets, cursor: r.cursor };
36
+ },
37
+ {
38
+ all: opts.all,
39
+ maxPages: opts.maxPages ? parseInt(opts.maxPages, 10) : undefined,
40
+ delay: opts.delay ? parseInt(opts.delay, 10) : undefined,
41
+ }
42
+ );
43
+
44
+ if (ctx.format === "json" || ctx.format === "json-full") {
45
+ output({ tweets: result.items, cursor: result.cursor }, "json");
46
+ } else {
47
+ for (const tweet of result.items) {
48
+ console.log(formatTweet(tweet));
49
+ console.log("---");
50
+ }
51
+ if (result.items.length === 0) console.log("No liked tweets found.");
52
+ }
53
+ } else {
54
+ const result = await client.getLikes(
55
+ userResult.user!.id,
56
+ count,
57
+ opts.cursor
58
+ );
59
+ if (!result.success) error(result.error ?? "Failed to load likes");
60
+
61
+ if (ctx.format === "json") {
62
+ output({ tweets: result.tweets, cursor: result.cursor }, ctx.format);
63
+ } else {
64
+ for (const tweet of result.tweets) {
65
+ console.log(formatTweet(tweet));
66
+ console.log("---");
67
+ }
68
+ if (result.tweets.length === 0) console.log("No liked tweets found.");
69
+ if (result.cursor) console.log(`\nNext cursor: ${result.cursor}`);
70
+ }
71
+ }
72
+ });
73
+ }
@@ -0,0 +1,101 @@
1
+ import type { Command } from "commander";
2
+ import { createContext } from "../cli/context.ts";
3
+ import { XClient } from "../lib/client-full.ts";
4
+ import { output, error } from "../cli/output.ts";
5
+ import { formatList, formatListCompact } from "../formatters/list.ts";
6
+ import { formatTweet } from "../formatters/tweet.ts";
7
+ import { autoPaginate } from "../lib/paginate.ts";
8
+ import type { Tweet } from "../lib/types.ts";
9
+
10
+ export function registerLists(program: Command): void {
11
+ program
12
+ .command("lists")
13
+ .description("Show your owned lists and memberships")
14
+ .action(async function (this: Command) {
15
+ const ctx = await createContext(this);
16
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
17
+
18
+ const owned = await client.getOwnedLists();
19
+ const memberships = await client.getListMemberships();
20
+
21
+ if (!owned.success && !memberships.success) {
22
+ error(owned.error ?? memberships.error ?? "Failed to load lists");
23
+ }
24
+
25
+ if (ctx.format === "json") {
26
+ output({
27
+ owned: owned.lists,
28
+ memberships: memberships.lists,
29
+ }, ctx.format);
30
+ } else {
31
+ if (owned.lists.length > 0) {
32
+ console.log("Owned lists:");
33
+ for (const list of owned.lists) {
34
+ console.log(formatListCompact(list));
35
+ }
36
+ } else {
37
+ console.log("No owned lists.");
38
+ }
39
+
40
+ if (memberships.lists.length > 0) {
41
+ console.log("\nMemberships:");
42
+ for (const list of memberships.lists) {
43
+ console.log(formatListCompact(list));
44
+ }
45
+ }
46
+ }
47
+ });
48
+
49
+ program
50
+ .command("list-timeline <id>")
51
+ .description("Show tweets from a list")
52
+ .option("-n, --count <number>", "Number of tweets", "20")
53
+ .option("--cursor <cursor>", "Pagination cursor")
54
+ .option("--all", "Fetch all pages")
55
+ .option("--max-pages <n>", "Max pages to fetch")
56
+ .option("--delay <ms>", "Delay between pages in ms")
57
+ .action(async function (this: Command, id: string) {
58
+ const ctx = await createContext(this);
59
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
60
+ const opts = this.opts();
61
+ const count = parseInt(opts.count, 10);
62
+
63
+ if (opts.all || opts.maxPages) {
64
+ const result = await autoPaginate<Tweet>(
65
+ async (cursor?) => {
66
+ const r = await client.getListTimeline(id, count, cursor);
67
+ return { items: r.tweets, cursor: r.cursor };
68
+ },
69
+ {
70
+ all: opts.all,
71
+ maxPages: opts.maxPages ? parseInt(opts.maxPages, 10) : undefined,
72
+ delay: opts.delay ? parseInt(opts.delay, 10) : undefined,
73
+ }
74
+ );
75
+
76
+ if (ctx.format === "json" || ctx.format === "json-full") {
77
+ output({ tweets: result.items, cursor: result.cursor }, "json");
78
+ } else {
79
+ for (const tweet of result.items) {
80
+ console.log(formatTweet(tweet));
81
+ console.log("---");
82
+ }
83
+ if (result.items.length === 0) console.log("No tweets found.");
84
+ }
85
+ } else {
86
+ const result = await client.getListTimeline(id, count, opts.cursor);
87
+ if (!result.success) error(result.error ?? "Failed to load list timeline");
88
+
89
+ if (ctx.format === "json") {
90
+ output({ tweets: result.tweets, cursor: result.cursor }, ctx.format);
91
+ } else {
92
+ for (const tweet of result.tweets) {
93
+ console.log(formatTweet(tweet));
94
+ console.log("---");
95
+ }
96
+ if (result.tweets.length === 0) console.log("No tweets found.");
97
+ if (result.cursor) console.log(`\nNext cursor: ${result.cursor}`);
98
+ }
99
+ }
100
+ });
101
+ }
@@ -0,0 +1,32 @@
1
+ import type { Command } from "commander";
2
+ import { createContext } from "../cli/context.ts";
3
+ import { XClient } from "../lib/client-full.ts";
4
+ import { output, error } from "../cli/output.ts";
5
+ import { formatTweet } from "../formatters/tweet.ts";
6
+
7
+ export function registerMentions(program: Command): void {
8
+ program
9
+ .command("mentions")
10
+ .description("Show recent mentions")
11
+ .option("-n, --count <number>", "Number of mentions", "20")
12
+ .action(async function (this: Command) {
13
+ const ctx = await createContext(this);
14
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
15
+ const opts = this.opts();
16
+ const count = parseInt(opts.count, 10);
17
+ // First get current user handle, then search mentions
18
+ const me = await client.getCurrentUser();
19
+ if (!me.success || !me.user) error("Could not determine current user for mentions");
20
+ const result = await client.mentions(me.user!.screenName, count);
21
+ if (!result.success || !result.tweets) error(result.error ?? "Failed to load mentions");
22
+ if (ctx.format === "json") {
23
+ output(result.tweets, ctx.format);
24
+ } else {
25
+ for (const tweet of result.tweets!) {
26
+ console.log(formatTweet(tweet));
27
+ console.log("---");
28
+ }
29
+ if (result.tweets!.length === 0) console.log("No recent mentions.");
30
+ }
31
+ });
32
+ }
@@ -0,0 +1,38 @@
1
+ import type { Command } from "commander";
2
+ import { createContext } from "../cli/context.ts";
3
+ import { XClient } from "../lib/client-full.ts";
4
+ import { output, error } from "../cli/output.ts";
5
+ import { formatNewsItem, formatNewsItemCompact } from "../formatters/news.ts";
6
+
7
+ export function registerNews(program: Command): void {
8
+ program
9
+ .command("news")
10
+ .description("Show trending topics and news")
11
+ .option("--tab <tab>", "News tab: trending, forYou, news, sports, entertainment", "trending")
12
+ .option("-n, --count <number>", "Number of items", "20")
13
+ .option("--ai-only", "Only show AI-summarized items")
14
+ .option("--with-tweets", "Include related tweets")
15
+ .option("--trending-only", "Only show trending items")
16
+ .action(async function (this: Command) {
17
+ const ctx = await createContext(this);
18
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
19
+ const opts = this.opts();
20
+ const count = parseInt(opts.count, 10);
21
+ const result = await client.getNews(count, {
22
+ tab: opts.tab,
23
+ aiOnly: opts.aiOnly,
24
+ withTweets: opts.withTweets,
25
+ trendingOnly: opts.trendingOnly,
26
+ });
27
+ if (!result.success) error(result.error ?? "Failed to load news");
28
+
29
+ if (ctx.format === "json") {
30
+ output(result.items, ctx.format);
31
+ } else {
32
+ for (const item of result.items) {
33
+ console.log(formatNewsItemCompact(item));
34
+ }
35
+ if (result.items.length === 0) console.log("No trending topics found.");
36
+ }
37
+ });
38
+ }
@@ -0,0 +1,36 @@
1
+ import type { Command } from "commander";
2
+ import { createContext } from "../cli/context.ts";
3
+ import { XClient } from "../lib/client-full.ts";
4
+ import { output, error } from "../cli/output.ts";
5
+ import { extractTweetId } from "../lib/utils.ts";
6
+ import { formatTweet } from "../formatters/tweet.ts";
7
+
8
+ export function registerRead(program: Command): void {
9
+ program
10
+ .command("read <id>")
11
+ .description("Read a tweet by ID or URL")
12
+ .option("-t, --thread", "Show full thread")
13
+ .action(async function (this: Command, id: string) {
14
+ const ctx = await createContext(this);
15
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
16
+ const tweetId = extractTweetId(id);
17
+ const opts = this.opts();
18
+
19
+ if (opts.thread) {
20
+ const result = await client.getThread(tweetId);
21
+ if (!result.success || !result.tweets) error(result.error ?? "Failed to get thread");
22
+ if (ctx.format === "json") {
23
+ output(result.tweets, ctx.format);
24
+ } else {
25
+ for (const tweet of result.tweets!) {
26
+ console.log(formatTweet(tweet));
27
+ console.log("---");
28
+ }
29
+ }
30
+ } else {
31
+ const result = await client.getTweet(tweetId);
32
+ if (!result.success || !result.tweet) error(result.error ?? "Failed to get tweet");
33
+ output(result.tweet, ctx.format, (data) => formatTweet(data as any));
34
+ }
35
+ });
36
+ }
@@ -0,0 +1,59 @@
1
+ import type { Command } from "commander";
2
+ import { createContext } from "../cli/context.ts";
3
+ import { XClient } from "../lib/client-full.ts";
4
+ import { output, error } from "../cli/output.ts";
5
+ import { formatTweet, formatTweetCompact } from "../formatters/tweet.ts";
6
+ import { autoPaginate } from "../lib/paginate.ts";
7
+
8
+ export function registerReplies(program: Command): void {
9
+ program
10
+ .command("replies <id>")
11
+ .description("Show replies to a tweet")
12
+ .option("-n, --count <number>", "Number per page", "20")
13
+ .option("--cursor <cursor>", "Pagination cursor")
14
+ .option("--all", "Fetch all pages")
15
+ .option("--max-pages <n>", "Max pages to fetch")
16
+ .option("--delay <ms>", "Delay between pages in ms")
17
+ .action(async function (this: Command, id: string) {
18
+ const ctx = await createContext(this);
19
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
20
+ const opts = this.opts();
21
+ const count = parseInt(opts.count, 10);
22
+
23
+ if (opts.all || opts.maxPages) {
24
+ const result = await autoPaginate(
25
+ async (cursor?) => {
26
+ const r = await client.getReplies(id, count, cursor);
27
+ return { items: r.tweets, cursor: r.cursor };
28
+ },
29
+ {
30
+ all: opts.all,
31
+ maxPages: opts.maxPages ? parseInt(opts.maxPages, 10) : undefined,
32
+ delay: opts.delay ? parseInt(opts.delay, 10) : undefined,
33
+ }
34
+ );
35
+
36
+ if (ctx.format === "json" || ctx.format === "json-full") {
37
+ output({ tweets: result.items, cursor: result.cursor }, "json");
38
+ } else {
39
+ for (const t of result.items) {
40
+ console.log(formatTweetCompact(t));
41
+ }
42
+ }
43
+ } else {
44
+ const result = await client.getReplies(id, count, opts.cursor);
45
+ if (!result.success) error(result.error ?? "Failed to load replies");
46
+
47
+ if (ctx.format === "json" || ctx.format === "json-full") {
48
+ output({ tweets: result.tweets, cursor: result.cursor }, "json");
49
+ } else {
50
+ for (const t of result.tweets) {
51
+ console.log(formatTweet(t));
52
+ console.log("---");
53
+ }
54
+ if (result.tweets.length === 0) console.log("No replies found.");
55
+ if (result.cursor) console.log(`\nNext cursor: ${result.cursor}`);
56
+ }
57
+ }
58
+ });
59
+ }
@@ -0,0 +1,60 @@
1
+ import type { Command } from "commander";
2
+ import { createContext } from "../cli/context.ts";
3
+ import { XClient } from "../lib/client-full.ts";
4
+ import { output, error } from "../cli/output.ts";
5
+ import { formatTweet, formatTweetCompact } from "../formatters/tweet.ts";
6
+ import { autoPaginate } from "../lib/paginate.ts";
7
+
8
+ export function registerSearch(program: Command): void {
9
+ program
10
+ .command("search <query>")
11
+ .description("Search tweets")
12
+ .option("-n, --count <number>", "Number of results", "20")
13
+ .option("--cursor <cursor>", "Pagination cursor")
14
+ .option("--all", "Fetch all pages")
15
+ .option("--max-pages <n>", "Max pages to fetch")
16
+ .option("--delay <ms>", "Delay between pages in ms")
17
+ .action(async function (this: Command, query: string) {
18
+ const ctx = await createContext(this);
19
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
20
+ const opts = this.opts();
21
+ const count = parseInt(opts.count, 10);
22
+
23
+ if (opts.all || opts.maxPages) {
24
+ const result = await autoPaginate(
25
+ async (cursor?) => {
26
+ const r = await client.search(query, count, cursor);
27
+ return { items: r.tweets ?? [], cursor: r.cursor };
28
+ },
29
+ {
30
+ all: opts.all,
31
+ maxPages: opts.maxPages ? parseInt(opts.maxPages, 10) : undefined,
32
+ delay: opts.delay ? parseInt(opts.delay, 10) : undefined,
33
+ }
34
+ );
35
+
36
+ if (ctx.format === "json" || ctx.format === "json-full") {
37
+ output({ tweets: result.items, cursor: result.cursor }, "json");
38
+ } else {
39
+ for (const tweet of result.items) {
40
+ console.log(formatTweet(tweet));
41
+ console.log("---");
42
+ }
43
+ if (result.items.length === 0) console.log("No results found.");
44
+ }
45
+ } else {
46
+ const result = await client.search(query, count, opts.cursor);
47
+ if (!result.success || !result.tweets) error(result.error ?? "Search failed");
48
+ if (ctx.format === "json") {
49
+ output({ tweets: result.tweets, cursor: result.cursor }, ctx.format);
50
+ } else {
51
+ for (const tweet of result.tweets!) {
52
+ console.log(formatTweet(tweet));
53
+ console.log("---");
54
+ }
55
+ if (result.tweets!.length === 0) console.log("No results found.");
56
+ if (result.cursor) console.log(`\nNext cursor: ${result.cursor}`);
57
+ }
58
+ }
59
+ });
60
+ }