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,42 @@
1
+ # xbird Listing Guide
2
+
3
+ Where to list xbird for discovery by AI agents and developers.
4
+
5
+ ## 1. x402.org/ecosystem
6
+
7
+ Main x402 agent catalog. Submit via the website form.
8
+
9
+ - URL: https://x402.org/ecosystem
10
+ - Include: agent card URL, description, pricing tiers
11
+ - Category: "Data API" / "Social Media"
12
+
13
+ ## 2. x402scan.com
14
+
15
+ On-chain agent explorer with metrics (revenue, requests, uptime).
16
+
17
+ - Auto-indexes agents registered via ERC-8004 Identity Registry
18
+ - After running `bun run register`, xbird should appear automatically
19
+ - Verify at: https://x402scan.com/agent/{agentId}
20
+
21
+ ## 3. x402.eco
22
+
23
+ SDK registry for agent integration.
24
+
25
+ - URL: https://x402.eco
26
+ - Submit package/agent for inclusion in SDK discovery
27
+
28
+ ## 4. GitHub topic
29
+
30
+ Add the `x402` topic to the xbird GitHub repository:
31
+
32
+ 1. Go to repo Settings > Topics
33
+ 2. Add: `x402`, `erc-8004`, `twitter-api`, `micropayments`
34
+
35
+ ## Checklist
36
+
37
+ - [ ] Deploy to Railway (`railway up`)
38
+ - [ ] Register on-chain (`bun run register`)
39
+ - [ ] Submit to x402.org/ecosystem
40
+ - [ ] Verify on x402scan.com
41
+ - [ ] Submit to x402.eco
42
+ - [ ] Add GitHub topics
@@ -0,0 +1,146 @@
1
+ # xbird MCP Server Setup
2
+
3
+ xbird MCP server runs locally on your machine, executing Twitter API requests from your residential IP while paying per-operation via x402 protocol.
4
+
5
+ ## Prerequisites
6
+
7
+ - [Bun](https://bun.sh) runtime
8
+ - Twitter account cookies (`auth_token` and `ct0`)
9
+ - USDC wallet private key (Base network) for x402 payments
10
+
11
+ ## Getting Twitter Cookies
12
+
13
+ 1. Open Twitter/X in your browser
14
+ 2. Open DevTools → Application → Cookies → `https://x.com`
15
+ 3. Copy `auth_token` and `ct0` values
16
+
17
+ ## Configuration
18
+
19
+ ### Claude Code
20
+
21
+ Add to `~/.claude.json` or project `.claude/settings.json`:
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "xbird": {
27
+ "command": "bunx",
28
+ "args": ["xbird-mcp"],
29
+ "env": {
30
+ "XBIRD_AUTH_TOKEN": "your_auth_token",
31
+ "XBIRD_CT0": "your_ct0",
32
+ "XBIRD_PRIVATE_KEY": "0x_your_private_key"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Cursor
40
+
41
+ Add to Cursor settings (MCP section):
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "xbird": {
47
+ "command": "bunx",
48
+ "args": ["xbird-mcp"],
49
+ "env": {
50
+ "XBIRD_AUTH_TOKEN": "your_auth_token",
51
+ "XBIRD_CT0": "your_ct0",
52
+ "XBIRD_PRIVATE_KEY": "0x_your_private_key"
53
+ }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### Windsurf
60
+
61
+ Add to Windsurf MCP config:
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "xbird": {
67
+ "command": "bunx",
68
+ "args": ["xbird-mcp"],
69
+ "env": {
70
+ "XBIRD_AUTH_TOKEN": "your_auth_token",
71
+ "XBIRD_CT0": "your_ct0",
72
+ "XBIRD_PRIVATE_KEY": "0x_your_private_key"
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Environment Variables
80
+
81
+ | Variable | Required | Description |
82
+ |----------|----------|-------------|
83
+ | `XBIRD_AUTH_TOKEN` | Yes | Twitter `auth_token` cookie |
84
+ | `XBIRD_CT0` | Yes | Twitter `ct0` cookie |
85
+ | `XBIRD_PRIVATE_KEY` | Yes | Wallet private key for USDC payments (Base) |
86
+ | `XBIRD_SERVER_URL` | No | Payment server URL (default: `https://xbirdapi.up.railway.app`) |
87
+
88
+ ## Available Tools (30)
89
+
90
+ ### Read ($0.001/call)
91
+ - `get_tweet` — Get a tweet by ID
92
+ - `get_thread` — Get a tweet thread
93
+ - `get_replies` — Get replies to a tweet
94
+ - `get_user` — Get user profile by handle
95
+ - `get_user_about` — Get detailed user info
96
+ - `get_current_user` — Get authenticated user
97
+ - `get_home_timeline` — Get home timeline
98
+ - `get_news` — Get trending topics
99
+ - `get_lists` — Get owned lists
100
+ - `get_list_timeline` — Get tweets from a list
101
+
102
+ ### Search ($0.005/call)
103
+ - `search_tweets` — Search for tweets
104
+ - `get_mentions` — Get mentions for a user
105
+
106
+ ### Bulk ($0.01/call)
107
+ - `get_user_tweets` — Get user's tweets
108
+ - `get_followers` — Get user's followers
109
+ - `get_following` — Get user's following
110
+ - `get_likes` — Get user's liked tweets
111
+ - `get_bookmarks` — Get bookmarked tweets
112
+ - `get_list_memberships` — Get list memberships
113
+
114
+ ### Write ($0.01/call)
115
+ - `post_tweet` — Post a tweet
116
+ - `reply_to_tweet` — Reply to a tweet
117
+ - `post_thread` — Post a thread
118
+ - `like_tweet` / `unlike_tweet` — Like/unlike
119
+ - `retweet` / `unretweet` — Retweet/unretweet
120
+ - `bookmark_tweet` / `unbookmark_tweet` — Bookmark/unbookmark
121
+ - `follow_user` / `unfollow_user` — Follow/unfollow
122
+
123
+ ### Media ($0.05/call)
124
+ - `upload_media` — Upload image/video for tweets
125
+
126
+ ## Manual Testing
127
+
128
+ ```bash
129
+ # List available tools
130
+ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | bun src/mcp/server.ts
131
+
132
+ # Run directly
133
+ bun src/mcp/server.ts
134
+ ```
135
+
136
+ ## How It Works
137
+
138
+ ```
139
+ AI Agent (Claude/Cursor)
140
+ ↓ MCP stdio
141
+ xbird-mcp (local process)
142
+ ├─ Pay x402 → Railway /api/authorize/:tier
143
+ └─ Execute → Twitter API (local residential IP)
144
+ ```
145
+
146
+ The Railway server acts as a payment gateway only — it verifies x402 payments but does not make Twitter requests. Twitter API calls happen locally from your machine, avoiding datacenter IP blocks.
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "xbird-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Twitter/X — 30 tools with x402 micropayments, runs locally from residential IP",
5
+ "module": "src/index.ts",
6
+ "type": "module",
7
+ "keywords": ["mcp", "twitter", "x", "x402", "ai", "agent", "claude", "cursor"],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/checkra1neth/xbird"
11
+ },
12
+ "license": "MIT",
13
+ "bin": {
14
+ "xbird": "src/cli.ts",
15
+ "xbird-mcp": "src/mcp/server.ts"
16
+ },
17
+ "files": [
18
+ "src/**/*.ts",
19
+ "docs/",
20
+ "package.json",
21
+ "tsconfig.json"
22
+ ],
23
+ "scripts": {
24
+ "dev": "bun run src/cli.ts",
25
+ "build": "bun build --compile --minify src/cli.ts --outfile xbird",
26
+ "test": "bun test",
27
+ "check": "bun run --bun tsc --noEmit",
28
+ "server": "bun --hot src/server.ts",
29
+ "server:dev": "bun --watch src/server.ts",
30
+ "register": "bun run src/server/erc8004/register.ts",
31
+ "mcp": "bun src/mcp/server.ts"
32
+ },
33
+ "devDependencies": {
34
+ "@types/bun": "latest",
35
+ "@x402/fetch": "^2.3.0",
36
+ "typescript": "^5.9.0"
37
+ },
38
+ "dependencies": {
39
+ "@coinbase/cdp-sdk": "^1.44.0",
40
+ "@steipete/sweet-cookie": "^0.1.0",
41
+ "@x402/core": "^2.3.0",
42
+ "@x402/evm": "^2.3.0",
43
+ "@x402/hono": "^2.3.0",
44
+ "commander": "^14.0.3",
45
+ "@modelcontextprotocol/sdk": "^1.12.0",
46
+ "hono": "^4.11.9",
47
+ "kleur": "^4.1.5",
48
+ "viem": "^2.45.2"
49
+ }
50
+ }
@@ -0,0 +1,80 @@
1
+ import type { Command } from "commander";
2
+ import type { OutputFormat, CLIContext, ExtendedBrowserName } from "../lib/types.ts";
3
+ import { resolveCredentials } from "../lib/auth.ts";
4
+ import { loadConfig } from "../lib/config.ts";
5
+ import { mapBrowserName } from "../lib/cookies.ts";
6
+ import { setEmojiEnabled } from "../formatters/common.ts";
7
+ import { setQuoteDepth } from "../formatters/tweet.ts";
8
+ import { error, warn } from "./output.ts";
9
+ import kleur from "kleur";
10
+
11
+ export async function createContext(cmd: Command): Promise<CLIContext> {
12
+ const opts = cmd.optsWithGlobals();
13
+ const config = await loadConfig();
14
+
15
+ // --- Format: CLI flags > env > config > default ---
16
+ const envFormat = process.env.XBIRD_FORMAT as OutputFormat | undefined;
17
+ const format: OutputFormat = opts.jsonFull
18
+ ? "json-full"
19
+ : opts.json
20
+ ? "json"
21
+ : opts.plain
22
+ ? "plain"
23
+ : envFormat ?? config.format ?? "human";
24
+
25
+ // --- No color: CLI --no-color > NO_COLOR env > config > default ---
26
+ const noColor = opts.color === false
27
+ || process.env.NO_COLOR !== undefined
28
+ || config.noColor === true;
29
+
30
+ // --- No emoji: CLI --no-emoji > env > config > default ---
31
+ // Commander.js: --no-emoji sets opts.emoji = false (not opts.noEmoji = true)
32
+ const noEmoji = opts.emoji === false
33
+ || process.env.XBIRD_NO_EMOJI !== undefined
34
+ || config.noEmoji === true;
35
+
36
+ // --- Quote depth: CLI > env > config > default ---
37
+ const envQuoteDepth = process.env.XBIRD_QUOTE_DEPTH;
38
+ const quoteDepth = opts.quoteDepth !== undefined
39
+ ? Number(opts.quoteDepth)
40
+ : envQuoteDepth !== undefined
41
+ ? Number(envQuoteDepth)
42
+ : config.quoteDepth ?? 1;
43
+
44
+ // --- Timeout: CLI > env > config > default ---
45
+ const envTimeout = process.env.XBIRD_TIMEOUT;
46
+ const timeoutMs = opts.timeout !== undefined
47
+ ? Number(opts.timeout)
48
+ : envTimeout !== undefined
49
+ ? Number(envTimeout)
50
+ : config.timeout ?? 30_000;
51
+
52
+ // --- Cookie source: CLI > env > config ---
53
+ const envCookieSource = process.env.XBIRD_COOKIE_SOURCE as ExtendedBrowserName | undefined;
54
+ const cookieSource: ExtendedBrowserName | undefined =
55
+ opts.cookieSource ?? envCookieSource ?? config.cookieSource;
56
+
57
+ // Apply settings
58
+ kleur.enabled = !noColor;
59
+ setEmojiEnabled(!noEmoji);
60
+ setQuoteDepth(quoteDepth);
61
+
62
+ // Resolve browser mapping for Arc/Brave
63
+ const mapping = mapBrowserName(cookieSource);
64
+ const { credentials, warnings } = await resolveCredentials({
65
+ authToken: opts.authToken ?? config.authToken,
66
+ ct0: opts.ct0 ?? config.ct0,
67
+ cookieSource: mapping?.browserName,
68
+ chromeProfile: mapping?.chromeProfile,
69
+ });
70
+
71
+ for (const w of warnings) {
72
+ if (format !== "json") warn(w);
73
+ }
74
+
75
+ if (!credentials) {
76
+ error("Could not resolve Twitter credentials. See warnings above.");
77
+ }
78
+
79
+ return { credentials: credentials!, format, noColor, noEmoji, quoteDepth, timeoutMs };
80
+ }
@@ -0,0 +1,33 @@
1
+ import type { OutputFormat } from "../lib/types.ts";
2
+
3
+ export function output(data: unknown, format: OutputFormat, humanFormatter?: (data: unknown) => string): void {
4
+ if (format === "json") {
5
+ console.log(JSON.stringify(data, null, 2));
6
+ return;
7
+ }
8
+ if (format === "plain") {
9
+ if (typeof data === "string") {
10
+ console.log(data);
11
+ } else {
12
+ console.log(JSON.stringify(data));
13
+ }
14
+ return;
15
+ }
16
+ // human format
17
+ if (humanFormatter) {
18
+ console.log(humanFormatter(data));
19
+ } else if (typeof data === "string") {
20
+ console.log(data);
21
+ } else {
22
+ console.log(JSON.stringify(data, null, 2));
23
+ }
24
+ }
25
+
26
+ export function error(message: string): void {
27
+ console.error(`Error: ${message}`);
28
+ process.exit(1);
29
+ }
30
+
31
+ export function warn(message: string): void {
32
+ console.error(`Warning: ${message}`);
33
+ }
@@ -0,0 +1,63 @@
1
+ import { Command } from "commander";
2
+
3
+ // Commands will be imported and registered here
4
+ import { registerWhoami } from "../commands/whoami.ts";
5
+ import { registerCheck } from "../commands/check.ts";
6
+ import { registerRead } from "../commands/read.ts";
7
+ import { registerTweet } from "../commands/tweet.ts";
8
+ import { registerSearch } from "../commands/search.ts";
9
+ import { registerHome } from "../commands/home.ts";
10
+ import { registerMentions } from "../commands/mentions.ts";
11
+ import { registerUser } from "../commands/user.ts";
12
+ import { registerEngagement } from "../commands/engagement.ts";
13
+ import { registerBookmark } from "../commands/bookmark.ts";
14
+ import { registerFollow } from "../commands/follow.ts";
15
+ import { registerLists } from "../commands/lists.ts";
16
+ import { registerNews } from "../commands/news.ts";
17
+ import { registerUserTweets } from "../commands/user-tweets.ts";
18
+ import { registerBookmarksList } from "../commands/bookmarks-list.ts";
19
+ import { registerLikes } from "../commands/likes.ts";
20
+ import { registerFollowers } from "../commands/followers.ts";
21
+ import { registerReplies } from "../commands/replies.ts";
22
+ import { registerAbout } from "../commands/about.ts";
23
+
24
+ export function createProgram(): Command {
25
+ const program = new Command();
26
+
27
+ program
28
+ .name("xbird")
29
+ .description("Personal Twitter/X CLI")
30
+ .version("0.1.0")
31
+ .option("--auth-token <token>", "Twitter auth_token cookie")
32
+ .option("--ct0 <token>", "Twitter ct0 cookie")
33
+ .option("--cookie-source <browser>", "Browser for cookie extraction (safari/chrome/firefox/arc/brave)")
34
+ .option("--json", "Output as JSON")
35
+ .option("--json-full", "Full JSON output with raw data")
36
+ .option("--plain", "Output as plain text")
37
+ .option("--no-color", "Disable colored output")
38
+ .option("--no-emoji", "Replace emojis with text alternatives")
39
+ .option("--quote-depth <n>", "Depth for rendering quoted tweets (0-3, default: 1)")
40
+ .option("--timeout <ms>", "Request timeout in milliseconds (default: 30000)");
41
+
42
+ registerWhoami(program);
43
+ registerCheck(program);
44
+ registerRead(program);
45
+ registerTweet(program);
46
+ registerSearch(program);
47
+ registerHome(program);
48
+ registerMentions(program);
49
+ registerUser(program);
50
+ registerEngagement(program);
51
+ registerBookmark(program);
52
+ registerFollow(program);
53
+ registerLists(program);
54
+ registerNews(program);
55
+ registerUserTweets(program);
56
+ registerBookmarksList(program);
57
+ registerLikes(program);
58
+ registerFollowers(program);
59
+ registerReplies(program);
60
+ registerAbout(program);
61
+
62
+ return program;
63
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bun
2
+ import { createProgram } from "./cli/program.ts";
3
+
4
+ const program = createProgram();
5
+ program.parseAsync(process.argv).catch((err: Error) => {
6
+ console.error(`Error: ${err.message}`);
7
+ process.exit(1);
8
+ });
@@ -0,0 +1,30 @@
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
+
7
+ export function registerAbout(program: Command): void {
8
+ program
9
+ .command("about <handle>")
10
+ .description("Show account origin/metadata")
11
+ .action(async function (this: Command, handle: string) {
12
+ const ctx = await createContext(this);
13
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
14
+ const screenName = normalizeHandle(handle);
15
+ const result = await client.getAbout(screenName);
16
+ if (!result.success) error(result.error ?? "Failed to load account info");
17
+
18
+ if (ctx.format === "json" || ctx.format === "json-full") {
19
+ output(result.about, "json");
20
+ } else {
21
+ const a = result.about;
22
+ if (a?.accountBasedIn) console.log(`Based in: ${a.accountBasedIn}`);
23
+ if (a?.createdAt) console.log(`Created: ${a.createdAt}`);
24
+ if (a?.source) console.log(`Source: ${a.source}`);
25
+ if (!a?.accountBasedIn && !a?.createdAt && !a?.source) {
26
+ console.log("No about info available.");
27
+ }
28
+ }
29
+ });
30
+ }
@@ -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 { extractTweetId } from "../lib/utils.ts";
6
+ import { emoji, green } from "../formatters/common.ts";
7
+
8
+ export function registerBookmark(program: Command): void {
9
+ program
10
+ .command("bookmark <id>")
11
+ .description("Bookmark a tweet")
12
+ .action(async function (this: Command, id: string) {
13
+ const ctx = await createContext(this);
14
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
15
+ const tweetId = extractTweetId(id);
16
+ const result = await client.bookmark(tweetId);
17
+ if (!result.success) error(result.error ?? "Failed to bookmark");
18
+ if (ctx.format === "json") {
19
+ output({ success: true, action: "bookmark", tweetId }, ctx.format);
20
+ } else {
21
+ console.log(`${green(emoji("✓"))} Bookmarked tweet ${tweetId}`);
22
+ }
23
+ });
24
+
25
+ program
26
+ .command("unbookmark <id>")
27
+ .description("Remove a bookmark")
28
+ .action(async function (this: Command, id: string) {
29
+ const ctx = await createContext(this);
30
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
31
+ const tweetId = extractTweetId(id);
32
+ const result = await client.unbookmark(tweetId);
33
+ if (!result.success) error(result.error ?? "Failed to unbookmark");
34
+ if (ctx.format === "json") {
35
+ output({ success: true, action: "unbookmark", tweetId }, ctx.format);
36
+ } else {
37
+ console.log(`${green(emoji("✓"))} Removed bookmark ${tweetId}`);
38
+ }
39
+ });
40
+ }
@@ -0,0 +1,108 @@
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
+ import type { Tweet } from "../lib/types.ts";
8
+
9
+ export function registerBookmarksList(program: Command): void {
10
+ program
11
+ .command("bookmarks")
12
+ .description("Show your bookmarked tweets")
13
+ .option("-n, --count <number>", "Number of bookmarks", "20")
14
+ .option("--cursor <cursor>", "Pagination cursor")
15
+ .option("--folder-id <id>", "Bookmark folder ID")
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
+ .option("--expand-root-only", "No thread expansion, just show the tweet")
20
+ .option("--author-chain", "For each bookmark, show only tweets by same author in thread")
21
+ .option("--author-only", "Filter all results to only bookmarked author's tweets")
22
+ .option("--full-chain-only", "Show full thread chain for each bookmark")
23
+ .option("--sort-chronological", "Sort all output by createdAt ascending")
24
+ .action(async function (this: Command) {
25
+ const ctx = await createContext(this);
26
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
27
+ const opts = this.opts();
28
+ const count = parseInt(opts.count, 10);
29
+
30
+ let tweets: Tweet[];
31
+ let cursor: string | undefined;
32
+
33
+ if (opts.all || opts.maxPages) {
34
+ const result = await autoPaginate<Tweet>(
35
+ async (c?) => {
36
+ const r = await client.getBookmarks(count, c, opts.folderId);
37
+ return { items: r.tweets, cursor: r.cursor };
38
+ },
39
+ {
40
+ all: opts.all,
41
+ maxPages: opts.maxPages ? parseInt(opts.maxPages, 10) : undefined,
42
+ delay: opts.delay ? parseInt(opts.delay, 10) : undefined,
43
+ }
44
+ );
45
+ tweets = result.items;
46
+ cursor = result.cursor;
47
+ } else {
48
+ const result = await client.getBookmarks(count, opts.cursor, opts.folderId);
49
+ if (!result.success) error(result.error ?? "Failed to load bookmarks");
50
+ tweets = result.tweets;
51
+ cursor = result.cursor;
52
+ }
53
+
54
+ // Thread expansion flags (client-side filtering)
55
+ if (opts.fullChainOnly) {
56
+ const expanded: Tweet[] = [];
57
+ for (const tweet of tweets) {
58
+ const thread = await client.getThread(tweet.id);
59
+ if (thread.success) {
60
+ expanded.push(...thread.tweets);
61
+ } else {
62
+ expanded.push(tweet);
63
+ }
64
+ }
65
+ tweets = expanded;
66
+ } else if (opts.authorChain) {
67
+ const expanded: Tweet[] = [];
68
+ for (const tweet of tweets) {
69
+ const thread = await client.getThread(tweet.id);
70
+ if (thread.success) {
71
+ const authorTweets = thread.tweets.filter(t => t.authorId === tweet.authorId);
72
+ expanded.push(...authorTweets);
73
+ } else {
74
+ expanded.push(tweet);
75
+ }
76
+ }
77
+ tweets = expanded;
78
+ } else if (opts.authorOnly) {
79
+ // Collect unique author IDs from bookmarked tweets
80
+ const authorIds = new Set(tweets.map(t => t.authorId));
81
+ tweets = tweets.filter(t => authorIds.has(t.authorId));
82
+ }
83
+ // expand-root-only: no expansion needed, tweets are already root-only
84
+
85
+ // Deduplicate by tweet ID
86
+ const seen = new Set<string>();
87
+ tweets = tweets.filter(t => {
88
+ if (seen.has(t.id)) return false;
89
+ seen.add(t.id);
90
+ return true;
91
+ });
92
+
93
+ if (opts.sortChronological) {
94
+ tweets.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
95
+ }
96
+
97
+ if (ctx.format === "json" || ctx.format === "json-full") {
98
+ output({ tweets, cursor }, "json");
99
+ } else {
100
+ for (const tweet of tweets) {
101
+ console.log(formatTweet(tweet));
102
+ console.log("---");
103
+ }
104
+ if (tweets.length === 0) console.log("No bookmarks found.");
105
+ if (cursor && !opts.all) console.log(`\nNext cursor: ${cursor}`);
106
+ }
107
+ });
108
+ }
@@ -0,0 +1,27 @@
1
+ import type { Command } from "commander";
2
+ import { createContext } from "../cli/context.ts";
3
+ import { output } from "../cli/output.ts";
4
+ import { bold, dim, emoji, green } from "../formatters/common.ts";
5
+
6
+ export function registerCheck(program: Command): void {
7
+ program
8
+ .command("check")
9
+ .description("Check credential source")
10
+ .action(async function (this: Command) {
11
+ const ctx = await createContext(this);
12
+ const info = {
13
+ source: ctx.credentials.source,
14
+ browser: ctx.credentials.browser,
15
+ authTokenPrefix: ctx.credentials.authToken.slice(0, 8) + "...",
16
+ ct0Prefix: ctx.credentials.ct0.slice(0, 8) + "...",
17
+ };
18
+ if (ctx.format === "json") {
19
+ output(info, ctx.format);
20
+ } else {
21
+ console.log(`${green(emoji("✓"))} Credentials found`);
22
+ console.log(` Source: ${bold(ctx.credentials.source)}${ctx.credentials.browser ? ` (${ctx.credentials.browser})` : ""}`);
23
+ console.log(` auth_token: ${dim(info.authTokenPrefix)}`);
24
+ console.log(` ct0: ${dim(info.ct0Prefix)}`);
25
+ }
26
+ });
27
+ }
@@ -0,0 +1,31 @@
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 { emoji, green } from "../formatters/common.ts";
7
+
8
+ export function registerEngagement(program: Command): void {
9
+ for (const [cmd, method, label, desc] of [
10
+ ["like", "like", "Liked", "Like a tweet"],
11
+ ["unlike", "unlike", "Unliked", "Unlike a tweet"],
12
+ ["retweet", "retweet", "Retweeted", "Retweet a tweet"],
13
+ ["unretweet", "unretweet", "Unretweeted", "Unretweet a tweet"],
14
+ ] as const) {
15
+ program
16
+ .command(`${cmd} <id>`)
17
+ .description(desc)
18
+ .action(async function (this: Command, id: string) {
19
+ const ctx = await createContext(this);
20
+ const client = new XClient(ctx.credentials, ctx.timeoutMs);
21
+ const tweetId = extractTweetId(id);
22
+ const result = await (client as any)[method](tweetId);
23
+ if (!result.success) error(result.error ?? `Failed to ${cmd}`);
24
+ if (ctx.format === "json") {
25
+ output({ success: true, action: cmd, tweetId }, ctx.format);
26
+ } else {
27
+ console.log(`${green(emoji("✓"))} ${label} tweet ${tweetId}`);
28
+ }
29
+ });
30
+ }
31
+ }