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.
- package/docs/listing.md +42 -0
- package/docs/mcp-setup.md +146 -0
- package/package.json +50 -0
- package/src/cli/context.ts +80 -0
- package/src/cli/output.ts +33 -0
- package/src/cli/program.ts +63 -0
- package/src/cli.ts +8 -0
- package/src/commands/about.ts +30 -0
- package/src/commands/bookmark.ts +40 -0
- package/src/commands/bookmarks-list.ts +108 -0
- package/src/commands/check.ts +27 -0
- package/src/commands/engagement.ts +31 -0
- package/src/commands/follow.ts +40 -0
- package/src/commands/followers.ts +132 -0
- package/src/commands/home.ts +59 -0
- package/src/commands/likes.ts +73 -0
- package/src/commands/lists.ts +101 -0
- package/src/commands/mentions.ts +32 -0
- package/src/commands/news.ts +38 -0
- package/src/commands/read.ts +36 -0
- package/src/commands/replies.ts +59 -0
- package/src/commands/search.ts +60 -0
- package/src/commands/tweet.ts +128 -0
- package/src/commands/user-tweets.ts +73 -0
- package/src/commands/user.ts +20 -0
- package/src/commands/whoami.ts +26 -0
- package/src/formatters/common.ts +49 -0
- package/src/formatters/list.ts +22 -0
- package/src/formatters/news.ts +23 -0
- package/src/formatters/tweet.ts +38 -0
- package/src/formatters/user.ts +20 -0
- package/src/index.ts +26 -0
- package/src/lib/auth.ts +105 -0
- package/src/lib/client-bookmarks.ts +62 -0
- package/src/lib/client-engagement.ts +152 -0
- package/src/lib/client-follow.ts +90 -0
- package/src/lib/client-full.ts +48 -0
- package/src/lib/client-likes.ts +62 -0
- package/src/lib/client-lists.ts +227 -0
- package/src/lib/client-media.ts +162 -0
- package/src/lib/client-news.ts +185 -0
- package/src/lib/client-posting.ts +163 -0
- package/src/lib/client-reading.ts +452 -0
- package/src/lib/client-search.ts +156 -0
- package/src/lib/client-timeline.ts +98 -0
- package/src/lib/client-user.ts +518 -0
- package/src/lib/client.ts +134 -0
- package/src/lib/config.ts +22 -0
- package/src/lib/constants.ts +55 -0
- package/src/lib/cookies.ts +132 -0
- package/src/lib/features.ts +175 -0
- package/src/lib/headers.ts +28 -0
- package/src/lib/paginate.ts +39 -0
- package/src/lib/query-ids.ts +190 -0
- package/src/lib/types.ts +147 -0
- package/src/lib/utils.ts +176 -0
- package/src/mcp/executor.ts +178 -0
- package/src/mcp/payment.ts +38 -0
- package/src/mcp/server.ts +53 -0
- package/src/mcp/tools.ts +389 -0
- package/src/server/app.ts +117 -0
- package/src/server/config/accounts.ts +137 -0
- package/src/server/config/pricing.ts +217 -0
- package/src/server/erc8004/register.ts +77 -0
- package/src/server/middleware/account-pool.ts +101 -0
- package/src/server/middleware/error-handler.ts +27 -0
- package/src/server/middleware/payer-extract.ts +43 -0
- package/src/server/middleware/x402.ts +61 -0
- package/src/server/routes/accounts.ts +93 -0
- package/src/server/routes/authorize.ts +19 -0
- package/src/server/routes/bookmarks.ts +21 -0
- package/src/server/routes/engagement.ts +84 -0
- package/src/server/routes/follow.ts +32 -0
- package/src/server/routes/health.ts +14 -0
- package/src/server/routes/lists.ts +38 -0
- package/src/server/routes/media.ts +44 -0
- package/src/server/routes/mentions.ts +20 -0
- package/src/server/routes/news.ts +23 -0
- package/src/server/routes/search.ts +26 -0
- package/src/server/routes/timeline.ts +21 -0
- package/src/server/routes/tweets.ts +82 -0
- package/src/server/routes/users.ts +92 -0
- package/src/server/storage/accounts-db.ts +84 -0
- package/src/server.ts +34 -0
- 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
|
+
}
|