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
package/docs/listing.md
ADDED
|
@@ -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,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
|
+
}
|