@spectratools/xapi-cli 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.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/cli.js +1202 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 spectra-the-bot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @spectra-the-bot/xapi-cli
|
|
2
|
+
|
|
3
|
+
X (Twitter) API v2 CLI for spectra-the-bot, built with [incur](https://github.com/wevm/incur).
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
export X_BEARER_TOKEN=your_bearer_token_here
|
|
9
|
+
npx @spectra-the-bot/xapi-cli --help
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
### Posts
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
xapi posts get <id>
|
|
18
|
+
xapi posts search <query> [-n 10] [--sort recency|relevancy]
|
|
19
|
+
xapi posts create --text "Hello world!" [--reply-to <id>] [--quote <id>]
|
|
20
|
+
xapi posts delete <id>
|
|
21
|
+
xapi posts likes <id>
|
|
22
|
+
xapi posts retweets <id>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Users
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
xapi users get <username|id>
|
|
29
|
+
xapi users followers <username> [-n 100]
|
|
30
|
+
xapi users following <username>
|
|
31
|
+
xapi users posts <username> [-n 10]
|
|
32
|
+
xapi users mentions <username>
|
|
33
|
+
xapi users search <query>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Timeline
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
xapi timeline home [-n 25]
|
|
40
|
+
xapi timeline mentions [-n 25]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Lists
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
xapi lists get <id>
|
|
47
|
+
xapi lists members <id>
|
|
48
|
+
xapi lists posts <id> [-n 25]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Trends
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
xapi trends places
|
|
55
|
+
xapi trends location <woeid>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### DMs
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
xapi dm conversations [-n 20]
|
|
62
|
+
xapi dm send <participant-id> --text "Hello!"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Common Options
|
|
66
|
+
|
|
67
|
+
- `--verbose` — Show full text without truncation
|
|
68
|
+
- `-n, --max-results` — Control result count
|
|
69
|
+
- `--format json` — JSON output
|
|
70
|
+
- `--help` — Show help
|
|
71
|
+
|
|
72
|
+
## Auth
|
|
73
|
+
|
|
74
|
+
All read endpoints use `X_BEARER_TOKEN`. Write endpoints (create post, delete, DMs) require OAuth 2.0 user context. Requests are automatically retried on 429 rate limit responses.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { Cli as Cli7 } from "incur";
|
|
6
|
+
|
|
7
|
+
// src/commands/dm.ts
|
|
8
|
+
import { apiKeyAuth } from "@spectratools/cli-shared";
|
|
9
|
+
import { Cli, z } from "incur";
|
|
10
|
+
|
|
11
|
+
// src/api.ts
|
|
12
|
+
import { createHttpClient, withRetry } from "@spectratools/cli-shared";
|
|
13
|
+
var BASE_URL = "https://api.x.com/2";
|
|
14
|
+
var RETRY_OPTIONS = { maxRetries: 3, baseMs: 500, maxMs: 1e4 };
|
|
15
|
+
function createXApiClient(bearerToken) {
|
|
16
|
+
const http = createHttpClient({
|
|
17
|
+
baseUrl: BASE_URL,
|
|
18
|
+
defaultHeaders: {
|
|
19
|
+
Authorization: `Bearer ${bearerToken}`
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
function get(path, query) {
|
|
23
|
+
return withRetry(
|
|
24
|
+
() => http.request(path, query !== void 0 ? { query } : {}),
|
|
25
|
+
RETRY_OPTIONS
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
function post(path, body) {
|
|
29
|
+
return withRetry(() => http.request(path, { method: "POST", body }), RETRY_OPTIONS);
|
|
30
|
+
}
|
|
31
|
+
function del(path) {
|
|
32
|
+
return withRetry(() => http.request(path, { method: "DELETE" }), RETRY_OPTIONS);
|
|
33
|
+
}
|
|
34
|
+
const POST_FIELDS = "id,text,author_id,created_at,public_metrics";
|
|
35
|
+
function getPost(id) {
|
|
36
|
+
return get(`/tweets/${id}`, {
|
|
37
|
+
"tweet.fields": POST_FIELDS
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function searchPosts(query, maxResults, sort, nextToken) {
|
|
41
|
+
return get("/tweets/search/recent", {
|
|
42
|
+
query,
|
|
43
|
+
max_results: maxResults,
|
|
44
|
+
sort_order: sort,
|
|
45
|
+
"tweet.fields": POST_FIELDS,
|
|
46
|
+
...nextToken ? { next_token: nextToken } : {}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function createPost(text, replyTo, quote) {
|
|
50
|
+
const body = { text };
|
|
51
|
+
if (replyTo) body.reply = { in_reply_to_tweet_id: replyTo };
|
|
52
|
+
if (quote) body.quote_tweet_id = quote;
|
|
53
|
+
return post("/tweets", body);
|
|
54
|
+
}
|
|
55
|
+
function deletePost(id) {
|
|
56
|
+
return del(`/tweets/${id}`);
|
|
57
|
+
}
|
|
58
|
+
function getPostLikes(id, maxResults, nextToken) {
|
|
59
|
+
return get(`/tweets/${id}/liking_users`, {
|
|
60
|
+
max_results: maxResults,
|
|
61
|
+
"user.fields": "id,name,username,public_metrics",
|
|
62
|
+
...nextToken ? { pagination_token: nextToken } : {}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function getPostRetweets(id, maxResults, nextToken) {
|
|
66
|
+
return get(`/tweets/${id}/retweeted_by`, {
|
|
67
|
+
max_results: maxResults,
|
|
68
|
+
"user.fields": "id,name,username,public_metrics",
|
|
69
|
+
...nextToken ? { pagination_token: nextToken } : {}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const USER_FIELDS = "id,name,username,description,public_metrics,created_at";
|
|
73
|
+
function getUserByUsername(username) {
|
|
74
|
+
return get(`/users/by/username/${username}`, {
|
|
75
|
+
"user.fields": USER_FIELDS
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function getUserById(id) {
|
|
79
|
+
return get(`/users/${id}`, {
|
|
80
|
+
"user.fields": USER_FIELDS
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function getUserFollowers(id, maxResults, nextToken) {
|
|
84
|
+
return get(`/users/${id}/followers`, {
|
|
85
|
+
max_results: maxResults,
|
|
86
|
+
"user.fields": USER_FIELDS,
|
|
87
|
+
...nextToken ? { pagination_token: nextToken } : {}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function getUserFollowing(id, maxResults, nextToken) {
|
|
91
|
+
return get(`/users/${id}/following`, {
|
|
92
|
+
max_results: maxResults,
|
|
93
|
+
"user.fields": USER_FIELDS,
|
|
94
|
+
...nextToken ? { pagination_token: nextToken } : {}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function getUserPosts(id, maxResults, nextToken) {
|
|
98
|
+
return get(`/users/${id}/tweets`, {
|
|
99
|
+
max_results: maxResults,
|
|
100
|
+
"tweet.fields": POST_FIELDS,
|
|
101
|
+
...nextToken ? { pagination_token: nextToken } : {}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function getUserMentions(id, maxResults, nextToken) {
|
|
105
|
+
return get(`/users/${id}/mentions`, {
|
|
106
|
+
max_results: maxResults,
|
|
107
|
+
"tweet.fields": POST_FIELDS,
|
|
108
|
+
...nextToken ? { pagination_token: nextToken } : {}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function searchUsers(query) {
|
|
112
|
+
return get("/users/search", {
|
|
113
|
+
query,
|
|
114
|
+
"user.fields": USER_FIELDS
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function getHomeTimeline(userId, maxResults, nextToken) {
|
|
118
|
+
return get(`/users/${userId}/timelines/reverse_chronological`, {
|
|
119
|
+
max_results: maxResults,
|
|
120
|
+
"tweet.fields": POST_FIELDS,
|
|
121
|
+
...nextToken ? { pagination_token: nextToken } : {}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function getMentionsTimeline(userId, maxResults, nextToken) {
|
|
125
|
+
return get(`/users/${userId}/mentions`, {
|
|
126
|
+
max_results: maxResults,
|
|
127
|
+
"tweet.fields": POST_FIELDS,
|
|
128
|
+
...nextToken ? { pagination_token: nextToken } : {}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function getList(id) {
|
|
132
|
+
return get(`/lists/${id}`, {
|
|
133
|
+
"list.fields": "id,name,description,owner_id,member_count"
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function getListMembers(id, maxResults, nextToken) {
|
|
137
|
+
return get(`/lists/${id}/members`, {
|
|
138
|
+
max_results: maxResults,
|
|
139
|
+
"user.fields": USER_FIELDS,
|
|
140
|
+
...nextToken ? { pagination_token: nextToken } : {}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function getListPosts(id, maxResults, nextToken) {
|
|
144
|
+
return get(`/lists/${id}/tweets`, {
|
|
145
|
+
max_results: maxResults,
|
|
146
|
+
"tweet.fields": POST_FIELDS,
|
|
147
|
+
...nextToken ? { pagination_token: nextToken } : {}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function getTrendingPlaces() {
|
|
151
|
+
return get(
|
|
152
|
+
"/trends/available"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
function getTrendsByLocation(woeid) {
|
|
156
|
+
return get(
|
|
157
|
+
`/trends/place/${woeid}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
function getDmConversations(userId, maxResults, nextToken) {
|
|
161
|
+
return get(`/users/${userId}/dm_conversations`, {
|
|
162
|
+
max_results: maxResults,
|
|
163
|
+
...nextToken ? { pagination_token: nextToken } : {}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function sendDm(participantId, text) {
|
|
167
|
+
return post(
|
|
168
|
+
`/dm_conversations/with/${participantId}/messages`,
|
|
169
|
+
{ text }
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
function getMe() {
|
|
173
|
+
return get("/users/me", { "user.fields": USER_FIELDS });
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
getPost,
|
|
177
|
+
searchPosts,
|
|
178
|
+
createPost,
|
|
179
|
+
deletePost,
|
|
180
|
+
getPostLikes,
|
|
181
|
+
getPostRetweets,
|
|
182
|
+
getUserByUsername,
|
|
183
|
+
getUserById,
|
|
184
|
+
getUserFollowers,
|
|
185
|
+
getUserFollowing,
|
|
186
|
+
getUserPosts,
|
|
187
|
+
getUserMentions,
|
|
188
|
+
searchUsers,
|
|
189
|
+
getHomeTimeline,
|
|
190
|
+
getMentionsTimeline,
|
|
191
|
+
getList,
|
|
192
|
+
getListMembers,
|
|
193
|
+
getListPosts,
|
|
194
|
+
getTrendingPlaces,
|
|
195
|
+
getTrendsByLocation,
|
|
196
|
+
getDmConversations,
|
|
197
|
+
sendDm,
|
|
198
|
+
getMe
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function relativeTime(iso) {
|
|
202
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
203
|
+
const s = Math.floor(ms / 1e3);
|
|
204
|
+
if (s < 60) return `${s}s ago`;
|
|
205
|
+
const m = Math.floor(s / 60);
|
|
206
|
+
if (m < 60) return `${m}m ago`;
|
|
207
|
+
const h = Math.floor(m / 60);
|
|
208
|
+
if (h < 24) return `${h}h ago`;
|
|
209
|
+
const d = Math.floor(h / 24);
|
|
210
|
+
return `${d}d ago`;
|
|
211
|
+
}
|
|
212
|
+
function truncateText(text, max = 100) {
|
|
213
|
+
if (text.length <= max) return text;
|
|
214
|
+
return `${text.slice(0, max - 3)}...`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/collect-paged.ts
|
|
218
|
+
import { paginateCursor } from "@spectratools/cli-shared";
|
|
219
|
+
async function collectPaged(fetchFn, mapFn, maxResults, pageSize = 100) {
|
|
220
|
+
const results = [];
|
|
221
|
+
for await (const item of paginateCursor({
|
|
222
|
+
fetchPage: async (cursor) => {
|
|
223
|
+
const res = await fetchFn(
|
|
224
|
+
Math.min(maxResults - results.length, pageSize),
|
|
225
|
+
cursor ?? void 0
|
|
226
|
+
);
|
|
227
|
+
return {
|
|
228
|
+
items: res.data ?? [],
|
|
229
|
+
nextCursor: res.meta?.next_token ?? null
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
})) {
|
|
233
|
+
results.push(mapFn(item));
|
|
234
|
+
if (results.length >= maxResults) break;
|
|
235
|
+
}
|
|
236
|
+
return results;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/commands/dm.ts
|
|
240
|
+
var dm = Cli.create("dm", {
|
|
241
|
+
description: "Manage X direct messages."
|
|
242
|
+
});
|
|
243
|
+
dm.command("conversations", {
|
|
244
|
+
description: "List your DM conversations.",
|
|
245
|
+
options: z.object({
|
|
246
|
+
maxResults: z.number().default(20).describe("Maximum conversations to return")
|
|
247
|
+
}),
|
|
248
|
+
alias: { maxResults: "n" },
|
|
249
|
+
output: z.object({
|
|
250
|
+
conversations: z.array(
|
|
251
|
+
z.object({
|
|
252
|
+
dm_conversation_id: z.string(),
|
|
253
|
+
participant_ids: z.array(z.string())
|
|
254
|
+
})
|
|
255
|
+
),
|
|
256
|
+
count: z.number()
|
|
257
|
+
}),
|
|
258
|
+
examples: [{ description: "List your DM conversations" }],
|
|
259
|
+
async run(c) {
|
|
260
|
+
const { apiKey } = apiKeyAuth("X_BEARER_TOKEN");
|
|
261
|
+
const client = createXApiClient(apiKey);
|
|
262
|
+
const meRes = await client.getMe();
|
|
263
|
+
const userId = meRes.data.id;
|
|
264
|
+
const allConvos = await collectPaged(
|
|
265
|
+
(limit, cursor) => client.getDmConversations(userId, limit, cursor),
|
|
266
|
+
(convo) => ({
|
|
267
|
+
dm_conversation_id: convo.dm_conversation_id,
|
|
268
|
+
participant_ids: convo.participant_ids
|
|
269
|
+
}),
|
|
270
|
+
c.options.maxResults
|
|
271
|
+
);
|
|
272
|
+
const firstParticipant = allConvos[0]?.participant_ids[0];
|
|
273
|
+
return c.ok(
|
|
274
|
+
{ conversations: allConvos, count: allConvos.length },
|
|
275
|
+
{
|
|
276
|
+
cta: firstParticipant ? {
|
|
277
|
+
description: "Next steps:",
|
|
278
|
+
commands: [
|
|
279
|
+
{
|
|
280
|
+
command: "dm send",
|
|
281
|
+
args: { participantId: firstParticipant },
|
|
282
|
+
options: { text: "Hello!" },
|
|
283
|
+
description: "Send a message to the first conversation"
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
} : void 0
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
dm.command("send", {
|
|
292
|
+
description: "Send a direct message to a user.",
|
|
293
|
+
args: z.object({
|
|
294
|
+
participantId: z.string().describe("User ID to send message to")
|
|
295
|
+
}),
|
|
296
|
+
options: z.object({
|
|
297
|
+
text: z.string().describe("Message text")
|
|
298
|
+
}),
|
|
299
|
+
output: z.object({
|
|
300
|
+
dm_conversation_id: z.string(),
|
|
301
|
+
dm_event_id: z.string()
|
|
302
|
+
}),
|
|
303
|
+
examples: [
|
|
304
|
+
{
|
|
305
|
+
args: { participantId: "12345" },
|
|
306
|
+
options: { text: "Hey there!" },
|
|
307
|
+
description: "Send a DM to a user"
|
|
308
|
+
}
|
|
309
|
+
],
|
|
310
|
+
async run(c) {
|
|
311
|
+
const { apiKey } = apiKeyAuth("X_BEARER_TOKEN");
|
|
312
|
+
const client = createXApiClient(apiKey);
|
|
313
|
+
const res = await client.sendDm(c.args.participantId, c.options.text);
|
|
314
|
+
return c.ok(res.data);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// src/commands/lists.ts
|
|
319
|
+
import { apiKeyAuth as apiKeyAuth2 } from "@spectratools/cli-shared";
|
|
320
|
+
import { Cli as Cli2, z as z2 } from "incur";
|
|
321
|
+
var lists = Cli2.create("lists", {
|
|
322
|
+
description: "Manage and browse X lists."
|
|
323
|
+
});
|
|
324
|
+
lists.command("get", {
|
|
325
|
+
description: "Get a list by ID.",
|
|
326
|
+
args: z2.object({
|
|
327
|
+
id: z2.string().describe("List ID")
|
|
328
|
+
}),
|
|
329
|
+
output: z2.object({
|
|
330
|
+
id: z2.string(),
|
|
331
|
+
name: z2.string(),
|
|
332
|
+
description: z2.string().optional(),
|
|
333
|
+
owner_id: z2.string().optional(),
|
|
334
|
+
member_count: z2.number().optional()
|
|
335
|
+
}),
|
|
336
|
+
examples: [{ args: { id: "1234567890" }, description: "Get list details" }],
|
|
337
|
+
async run(c) {
|
|
338
|
+
const { apiKey } = apiKeyAuth2("X_BEARER_TOKEN");
|
|
339
|
+
const client = createXApiClient(apiKey);
|
|
340
|
+
const res = await client.getList(c.args.id);
|
|
341
|
+
const list = res.data;
|
|
342
|
+
return c.ok(
|
|
343
|
+
{
|
|
344
|
+
id: list.id,
|
|
345
|
+
name: list.name,
|
|
346
|
+
description: list.description,
|
|
347
|
+
owner_id: list.owner_id,
|
|
348
|
+
member_count: list.member_count
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
cta: {
|
|
352
|
+
description: "Explore this list:",
|
|
353
|
+
commands: [
|
|
354
|
+
{ command: "lists members", args: { id: c.args.id }, description: "See list members" },
|
|
355
|
+
{ command: "lists posts", args: { id: c.args.id }, description: "See list posts" }
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
lists.command("members", {
|
|
363
|
+
description: "List members of an X list.",
|
|
364
|
+
args: z2.object({
|
|
365
|
+
id: z2.string().describe("List ID")
|
|
366
|
+
}),
|
|
367
|
+
options: z2.object({
|
|
368
|
+
maxResults: z2.number().default(100).describe("Maximum members to return")
|
|
369
|
+
}),
|
|
370
|
+
alias: { maxResults: "n" },
|
|
371
|
+
output: z2.object({
|
|
372
|
+
users: z2.array(
|
|
373
|
+
z2.object({
|
|
374
|
+
id: z2.string(),
|
|
375
|
+
name: z2.string(),
|
|
376
|
+
username: z2.string(),
|
|
377
|
+
followers: z2.number().optional()
|
|
378
|
+
})
|
|
379
|
+
),
|
|
380
|
+
count: z2.number()
|
|
381
|
+
}),
|
|
382
|
+
examples: [{ args: { id: "1234567890" }, description: "List all members" }],
|
|
383
|
+
async run(c) {
|
|
384
|
+
const { apiKey } = apiKeyAuth2("X_BEARER_TOKEN");
|
|
385
|
+
const client = createXApiClient(apiKey);
|
|
386
|
+
const allUsers = await collectPaged(
|
|
387
|
+
(limit, cursor) => client.getListMembers(c.args.id, limit, cursor),
|
|
388
|
+
(user) => ({
|
|
389
|
+
id: user.id,
|
|
390
|
+
name: user.name,
|
|
391
|
+
username: user.username,
|
|
392
|
+
followers: user.public_metrics?.followers_count
|
|
393
|
+
}),
|
|
394
|
+
c.options.maxResults
|
|
395
|
+
);
|
|
396
|
+
return c.ok({ users: allUsers, count: allUsers.length });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
lists.command("posts", {
|
|
400
|
+
description: "Get posts from an X list.",
|
|
401
|
+
args: z2.object({
|
|
402
|
+
id: z2.string().describe("List ID")
|
|
403
|
+
}),
|
|
404
|
+
options: z2.object({
|
|
405
|
+
maxResults: z2.number().default(25).describe("Maximum posts to return"),
|
|
406
|
+
verbose: z2.boolean().optional().describe("Show full text")
|
|
407
|
+
}),
|
|
408
|
+
alias: { maxResults: "n" },
|
|
409
|
+
output: z2.object({
|
|
410
|
+
posts: z2.array(
|
|
411
|
+
z2.object({
|
|
412
|
+
id: z2.string(),
|
|
413
|
+
text: z2.string(),
|
|
414
|
+
author_id: z2.string().optional(),
|
|
415
|
+
created_at: z2.string().optional(),
|
|
416
|
+
likes: z2.number().optional()
|
|
417
|
+
})
|
|
418
|
+
),
|
|
419
|
+
count: z2.number()
|
|
420
|
+
}),
|
|
421
|
+
examples: [{ args: { id: "1234567890" }, description: "Get posts from a list" }],
|
|
422
|
+
async run(c) {
|
|
423
|
+
const { apiKey } = apiKeyAuth2("X_BEARER_TOKEN");
|
|
424
|
+
const client = createXApiClient(apiKey);
|
|
425
|
+
const allPosts = await collectPaged(
|
|
426
|
+
(limit, cursor) => client.getListPosts(c.args.id, limit, cursor),
|
|
427
|
+
(post) => ({
|
|
428
|
+
id: post.id,
|
|
429
|
+
text: c.options.verbose ? post.text : truncateText(post.text),
|
|
430
|
+
author_id: post.author_id,
|
|
431
|
+
created_at: post.created_at ? relativeTime(post.created_at) : void 0,
|
|
432
|
+
likes: post.public_metrics?.like_count
|
|
433
|
+
}),
|
|
434
|
+
c.options.maxResults
|
|
435
|
+
);
|
|
436
|
+
const firstId = allPosts[0]?.id;
|
|
437
|
+
return c.ok(
|
|
438
|
+
{ posts: allPosts, count: allPosts.length },
|
|
439
|
+
{
|
|
440
|
+
cta: firstId ? {
|
|
441
|
+
description: "Next steps:",
|
|
442
|
+
commands: [
|
|
443
|
+
{
|
|
444
|
+
command: "posts get",
|
|
445
|
+
args: { id: firstId },
|
|
446
|
+
description: "View top post in detail"
|
|
447
|
+
}
|
|
448
|
+
]
|
|
449
|
+
} : void 0
|
|
450
|
+
}
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// src/commands/posts.ts
|
|
456
|
+
import { apiKeyAuth as apiKeyAuth3 } from "@spectratools/cli-shared";
|
|
457
|
+
import { Cli as Cli3, z as z3 } from "incur";
|
|
458
|
+
var posts = Cli3.create("posts", {
|
|
459
|
+
description: "Manage and search X posts."
|
|
460
|
+
});
|
|
461
|
+
posts.command("get", {
|
|
462
|
+
description: "Get a post by ID.",
|
|
463
|
+
args: z3.object({
|
|
464
|
+
id: z3.string().describe("Post ID")
|
|
465
|
+
}),
|
|
466
|
+
options: z3.object({
|
|
467
|
+
verbose: z3.boolean().optional().describe("Show full text without truncation")
|
|
468
|
+
}),
|
|
469
|
+
output: z3.object({
|
|
470
|
+
id: z3.string(),
|
|
471
|
+
text: z3.string(),
|
|
472
|
+
author_id: z3.string().optional(),
|
|
473
|
+
created_at: z3.string().optional(),
|
|
474
|
+
likes: z3.number().optional(),
|
|
475
|
+
retweets: z3.number().optional(),
|
|
476
|
+
replies: z3.number().optional()
|
|
477
|
+
}),
|
|
478
|
+
examples: [{ args: { id: "1234567890" }, description: "Get a post by ID" }],
|
|
479
|
+
async run(c) {
|
|
480
|
+
const { apiKey } = apiKeyAuth3("X_BEARER_TOKEN");
|
|
481
|
+
const client = createXApiClient(apiKey);
|
|
482
|
+
const res = await client.getPost(c.args.id);
|
|
483
|
+
const post = res.data;
|
|
484
|
+
const text = c.options.verbose ? post.text : truncateText(post.text);
|
|
485
|
+
return c.ok(
|
|
486
|
+
{
|
|
487
|
+
id: post.id,
|
|
488
|
+
text,
|
|
489
|
+
author_id: post.author_id,
|
|
490
|
+
created_at: post.created_at ? relativeTime(post.created_at) : void 0,
|
|
491
|
+
likes: post.public_metrics?.like_count,
|
|
492
|
+
retweets: post.public_metrics?.retweet_count,
|
|
493
|
+
replies: post.public_metrics?.reply_count
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
cta: {
|
|
497
|
+
description: "Explore this post:",
|
|
498
|
+
commands: [
|
|
499
|
+
{
|
|
500
|
+
command: "posts likes",
|
|
501
|
+
args: { id: c.args.id },
|
|
502
|
+
description: "See who liked this post"
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
command: "posts retweets",
|
|
506
|
+
args: { id: c.args.id },
|
|
507
|
+
description: "See who retweeted this post"
|
|
508
|
+
}
|
|
509
|
+
]
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
posts.command("search", {
|
|
516
|
+
description: "Search recent posts.",
|
|
517
|
+
args: z3.object({
|
|
518
|
+
query: z3.string().describe("Search query")
|
|
519
|
+
}),
|
|
520
|
+
options: z3.object({
|
|
521
|
+
maxResults: z3.number().default(10).describe("Maximum results to return (10\u2013100)"),
|
|
522
|
+
sort: z3.enum(["recency", "relevancy"]).default("recency").describe("Sort order"),
|
|
523
|
+
verbose: z3.boolean().optional().describe("Show full text without truncation")
|
|
524
|
+
}),
|
|
525
|
+
alias: { maxResults: "n" },
|
|
526
|
+
output: z3.object({
|
|
527
|
+
posts: z3.array(
|
|
528
|
+
z3.object({
|
|
529
|
+
id: z3.string(),
|
|
530
|
+
text: z3.string(),
|
|
531
|
+
created_at: z3.string().optional(),
|
|
532
|
+
likes: z3.number().optional(),
|
|
533
|
+
retweets: z3.number().optional()
|
|
534
|
+
})
|
|
535
|
+
),
|
|
536
|
+
count: z3.number()
|
|
537
|
+
}),
|
|
538
|
+
examples: [
|
|
539
|
+
{ args: { query: "TypeScript" }, description: "Search for TypeScript posts" },
|
|
540
|
+
{
|
|
541
|
+
args: { query: "AI" },
|
|
542
|
+
options: { sort: "relevancy", maxResults: 20 },
|
|
543
|
+
description: "Search by relevance"
|
|
544
|
+
}
|
|
545
|
+
],
|
|
546
|
+
async run(c) {
|
|
547
|
+
const { apiKey } = apiKeyAuth3("X_BEARER_TOKEN");
|
|
548
|
+
const client = createXApiClient(apiKey);
|
|
549
|
+
const res = await client.searchPosts(c.args.query, c.options.maxResults, c.options.sort);
|
|
550
|
+
const items = (res.data ?? []).map((p) => ({
|
|
551
|
+
id: p.id,
|
|
552
|
+
text: c.options.verbose ? p.text : truncateText(p.text),
|
|
553
|
+
created_at: p.created_at ? relativeTime(p.created_at) : void 0,
|
|
554
|
+
likes: p.public_metrics?.like_count,
|
|
555
|
+
retweets: p.public_metrics?.retweet_count
|
|
556
|
+
}));
|
|
557
|
+
const firstId = items[0]?.id;
|
|
558
|
+
return c.ok(
|
|
559
|
+
{ posts: items, count: items.length },
|
|
560
|
+
{
|
|
561
|
+
cta: firstId ? {
|
|
562
|
+
description: "Next steps:",
|
|
563
|
+
commands: [
|
|
564
|
+
{
|
|
565
|
+
command: "posts get",
|
|
566
|
+
args: { id: firstId },
|
|
567
|
+
description: "View top result in detail"
|
|
568
|
+
}
|
|
569
|
+
]
|
|
570
|
+
} : void 0
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
posts.command("create", {
|
|
576
|
+
description: "Create a new post.",
|
|
577
|
+
options: z3.object({
|
|
578
|
+
text: z3.string().describe("Post text"),
|
|
579
|
+
replyTo: z3.string().optional().describe("Reply to post ID"),
|
|
580
|
+
quote: z3.string().optional().describe("Quote post ID")
|
|
581
|
+
}),
|
|
582
|
+
output: z3.object({
|
|
583
|
+
id: z3.string(),
|
|
584
|
+
text: z3.string()
|
|
585
|
+
}),
|
|
586
|
+
examples: [
|
|
587
|
+
{ options: { text: "Hello world!" }, description: "Post a simple message" },
|
|
588
|
+
{ options: { text: "Great point!", replyTo: "1234567890" }, description: "Reply to a post" }
|
|
589
|
+
],
|
|
590
|
+
async run(c) {
|
|
591
|
+
const { apiKey } = apiKeyAuth3("X_BEARER_TOKEN");
|
|
592
|
+
const client = createXApiClient(apiKey);
|
|
593
|
+
const res = await client.createPost(c.options.text, c.options.replyTo, c.options.quote);
|
|
594
|
+
return c.ok(res.data, {
|
|
595
|
+
cta: {
|
|
596
|
+
description: "View your post:",
|
|
597
|
+
commands: [
|
|
598
|
+
{ command: "posts get", args: { id: res.data.id }, description: "See the created post" }
|
|
599
|
+
]
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
posts.command("delete", {
|
|
605
|
+
description: "Delete a post by ID.",
|
|
606
|
+
args: z3.object({
|
|
607
|
+
id: z3.string().describe("Post ID to delete")
|
|
608
|
+
}),
|
|
609
|
+
output: z3.object({
|
|
610
|
+
deleted: z3.boolean(),
|
|
611
|
+
id: z3.string()
|
|
612
|
+
}),
|
|
613
|
+
examples: [{ args: { id: "1234567890" }, description: "Delete a post" }],
|
|
614
|
+
async run(c) {
|
|
615
|
+
const { apiKey } = apiKeyAuth3("X_BEARER_TOKEN");
|
|
616
|
+
const client = createXApiClient(apiKey);
|
|
617
|
+
const res = await client.deletePost(c.args.id);
|
|
618
|
+
return c.ok({ deleted: res.data.deleted, id: c.args.id });
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
posts.command("likes", {
|
|
622
|
+
description: "List users who liked a post.",
|
|
623
|
+
args: z3.object({
|
|
624
|
+
id: z3.string().describe("Post ID")
|
|
625
|
+
}),
|
|
626
|
+
options: z3.object({
|
|
627
|
+
maxResults: z3.number().default(100).describe("Maximum users to return")
|
|
628
|
+
}),
|
|
629
|
+
alias: { maxResults: "n" },
|
|
630
|
+
output: z3.object({
|
|
631
|
+
users: z3.array(
|
|
632
|
+
z3.object({
|
|
633
|
+
id: z3.string(),
|
|
634
|
+
name: z3.string(),
|
|
635
|
+
username: z3.string(),
|
|
636
|
+
followers: z3.number().optional()
|
|
637
|
+
})
|
|
638
|
+
),
|
|
639
|
+
count: z3.number()
|
|
640
|
+
}),
|
|
641
|
+
examples: [{ args: { id: "1234567890" }, description: "See who liked a post" }],
|
|
642
|
+
async run(c) {
|
|
643
|
+
const { apiKey } = apiKeyAuth3("X_BEARER_TOKEN");
|
|
644
|
+
const client = createXApiClient(apiKey);
|
|
645
|
+
const allUsers = await collectPaged(
|
|
646
|
+
(limit, cursor) => client.getPostLikes(c.args.id, limit, cursor),
|
|
647
|
+
(user) => ({
|
|
648
|
+
id: user.id,
|
|
649
|
+
name: user.name,
|
|
650
|
+
username: user.username,
|
|
651
|
+
followers: user.public_metrics?.followers_count
|
|
652
|
+
}),
|
|
653
|
+
c.options.maxResults
|
|
654
|
+
);
|
|
655
|
+
return c.ok(
|
|
656
|
+
{ users: allUsers, count: allUsers.length },
|
|
657
|
+
{
|
|
658
|
+
cta: {
|
|
659
|
+
description: "Next steps:",
|
|
660
|
+
commands: allUsers.slice(0, 1).map((u) => ({
|
|
661
|
+
command: "users get",
|
|
662
|
+
args: { username: u.username },
|
|
663
|
+
description: `View profile of @${u.username}`
|
|
664
|
+
}))
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
posts.command("retweets", {
|
|
671
|
+
description: "List users who retweeted a post.",
|
|
672
|
+
args: z3.object({
|
|
673
|
+
id: z3.string().describe("Post ID")
|
|
674
|
+
}),
|
|
675
|
+
options: z3.object({
|
|
676
|
+
maxResults: z3.number().default(100).describe("Maximum users to return")
|
|
677
|
+
}),
|
|
678
|
+
alias: { maxResults: "n" },
|
|
679
|
+
output: z3.object({
|
|
680
|
+
users: z3.array(
|
|
681
|
+
z3.object({
|
|
682
|
+
id: z3.string(),
|
|
683
|
+
name: z3.string(),
|
|
684
|
+
username: z3.string(),
|
|
685
|
+
followers: z3.number().optional()
|
|
686
|
+
})
|
|
687
|
+
),
|
|
688
|
+
count: z3.number()
|
|
689
|
+
}),
|
|
690
|
+
examples: [{ args: { id: "1234567890" }, description: "See who retweeted a post" }],
|
|
691
|
+
async run(c) {
|
|
692
|
+
const { apiKey } = apiKeyAuth3("X_BEARER_TOKEN");
|
|
693
|
+
const client = createXApiClient(apiKey);
|
|
694
|
+
const allUsers = await collectPaged(
|
|
695
|
+
(limit, cursor) => client.getPostRetweets(c.args.id, limit, cursor),
|
|
696
|
+
(user) => ({
|
|
697
|
+
id: user.id,
|
|
698
|
+
name: user.name,
|
|
699
|
+
username: user.username,
|
|
700
|
+
followers: user.public_metrics?.followers_count
|
|
701
|
+
}),
|
|
702
|
+
c.options.maxResults
|
|
703
|
+
);
|
|
704
|
+
return c.ok({ users: allUsers, count: allUsers.length });
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// src/commands/timeline.ts
|
|
709
|
+
import { apiKeyAuth as apiKeyAuth4 } from "@spectratools/cli-shared";
|
|
710
|
+
import { Cli as Cli4, z as z4 } from "incur";
|
|
711
|
+
var timeline = Cli4.create("timeline", {
|
|
712
|
+
description: "View your X timeline."
|
|
713
|
+
});
|
|
714
|
+
timeline.command("home", {
|
|
715
|
+
description: "View your home timeline.",
|
|
716
|
+
options: z4.object({
|
|
717
|
+
maxResults: z4.number().default(25).describe("Maximum posts to return (5\u2013100)"),
|
|
718
|
+
verbose: z4.boolean().optional().describe("Show full text without truncation")
|
|
719
|
+
}),
|
|
720
|
+
alias: { maxResults: "n" },
|
|
721
|
+
output: z4.object({
|
|
722
|
+
posts: z4.array(
|
|
723
|
+
z4.object({
|
|
724
|
+
id: z4.string(),
|
|
725
|
+
text: z4.string(),
|
|
726
|
+
author_id: z4.string().optional(),
|
|
727
|
+
created_at: z4.string().optional(),
|
|
728
|
+
likes: z4.number().optional(),
|
|
729
|
+
retweets: z4.number().optional()
|
|
730
|
+
})
|
|
731
|
+
),
|
|
732
|
+
count: z4.number()
|
|
733
|
+
}),
|
|
734
|
+
examples: [
|
|
735
|
+
{ description: "View your home timeline" },
|
|
736
|
+
{ options: { maxResults: 50 }, description: "View 50 posts" }
|
|
737
|
+
],
|
|
738
|
+
async run(c) {
|
|
739
|
+
const { apiKey } = apiKeyAuth4("X_BEARER_TOKEN");
|
|
740
|
+
const client = createXApiClient(apiKey);
|
|
741
|
+
const meRes = await client.getMe();
|
|
742
|
+
const userId = meRes.data.id;
|
|
743
|
+
const allPosts = await collectPaged(
|
|
744
|
+
(limit, cursor) => client.getHomeTimeline(userId, limit, cursor),
|
|
745
|
+
(post) => ({
|
|
746
|
+
id: post.id,
|
|
747
|
+
text: c.options.verbose ? post.text : truncateText(post.text),
|
|
748
|
+
author_id: post.author_id,
|
|
749
|
+
created_at: post.created_at ? relativeTime(post.created_at) : void 0,
|
|
750
|
+
likes: post.public_metrics?.like_count,
|
|
751
|
+
retweets: post.public_metrics?.retweet_count
|
|
752
|
+
}),
|
|
753
|
+
c.options.maxResults
|
|
754
|
+
);
|
|
755
|
+
const firstId = allPosts[0]?.id;
|
|
756
|
+
return c.ok(
|
|
757
|
+
{ posts: allPosts, count: allPosts.length },
|
|
758
|
+
{
|
|
759
|
+
cta: firstId ? {
|
|
760
|
+
description: "Next steps:",
|
|
761
|
+
commands: [
|
|
762
|
+
{
|
|
763
|
+
command: "posts get",
|
|
764
|
+
args: { id: firstId },
|
|
765
|
+
description: "View top post in detail"
|
|
766
|
+
}
|
|
767
|
+
]
|
|
768
|
+
} : void 0
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
timeline.command("mentions", {
|
|
774
|
+
description: "View your recent mentions.",
|
|
775
|
+
options: z4.object({
|
|
776
|
+
maxResults: z4.number().default(25).describe("Maximum mentions to return"),
|
|
777
|
+
verbose: z4.boolean().optional().describe("Show full text without truncation")
|
|
778
|
+
}),
|
|
779
|
+
alias: { maxResults: "n" },
|
|
780
|
+
output: z4.object({
|
|
781
|
+
posts: z4.array(
|
|
782
|
+
z4.object({
|
|
783
|
+
id: z4.string(),
|
|
784
|
+
text: z4.string(),
|
|
785
|
+
author_id: z4.string().optional(),
|
|
786
|
+
created_at: z4.string().optional()
|
|
787
|
+
})
|
|
788
|
+
),
|
|
789
|
+
count: z4.number()
|
|
790
|
+
}),
|
|
791
|
+
examples: [{ description: "View your recent mentions" }],
|
|
792
|
+
async run(c) {
|
|
793
|
+
const { apiKey } = apiKeyAuth4("X_BEARER_TOKEN");
|
|
794
|
+
const client = createXApiClient(apiKey);
|
|
795
|
+
const meRes = await client.getMe();
|
|
796
|
+
const userId = meRes.data.id;
|
|
797
|
+
const allPosts = await collectPaged(
|
|
798
|
+
(limit, cursor) => client.getMentionsTimeline(userId, limit, cursor),
|
|
799
|
+
(post) => ({
|
|
800
|
+
id: post.id,
|
|
801
|
+
text: c.options.verbose ? post.text : truncateText(post.text),
|
|
802
|
+
author_id: post.author_id,
|
|
803
|
+
created_at: post.created_at ? relativeTime(post.created_at) : void 0
|
|
804
|
+
}),
|
|
805
|
+
c.options.maxResults
|
|
806
|
+
);
|
|
807
|
+
return c.ok({ posts: allPosts, count: allPosts.length });
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// src/commands/trends.ts
|
|
812
|
+
import { apiKeyAuth as apiKeyAuth5 } from "@spectratools/cli-shared";
|
|
813
|
+
import { Cli as Cli5, z as z5 } from "incur";
|
|
814
|
+
var trends = Cli5.create("trends", {
|
|
815
|
+
description: "Explore trending topics on X."
|
|
816
|
+
});
|
|
817
|
+
trends.command("places", {
|
|
818
|
+
description: "List places where trending topics are available.",
|
|
819
|
+
output: z5.object({
|
|
820
|
+
places: z5.array(
|
|
821
|
+
z5.object({
|
|
822
|
+
woeid: z5.number(),
|
|
823
|
+
name: z5.string(),
|
|
824
|
+
country: z5.string()
|
|
825
|
+
})
|
|
826
|
+
),
|
|
827
|
+
count: z5.number()
|
|
828
|
+
}),
|
|
829
|
+
examples: [{ description: "List all trending places" }],
|
|
830
|
+
async run(c) {
|
|
831
|
+
const { apiKey } = apiKeyAuth5("X_BEARER_TOKEN");
|
|
832
|
+
const client = createXApiClient(apiKey);
|
|
833
|
+
const res = await client.getTrendingPlaces();
|
|
834
|
+
const places = res.data ?? [];
|
|
835
|
+
const first = places[0];
|
|
836
|
+
return c.ok(
|
|
837
|
+
{ places, count: places.length },
|
|
838
|
+
{
|
|
839
|
+
cta: first ? {
|
|
840
|
+
description: "Next steps:",
|
|
841
|
+
commands: [
|
|
842
|
+
{
|
|
843
|
+
command: "trends location",
|
|
844
|
+
args: { woeid: first.woeid },
|
|
845
|
+
description: `View trends for ${first.name}`
|
|
846
|
+
}
|
|
847
|
+
]
|
|
848
|
+
} : void 0
|
|
849
|
+
}
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
trends.command("location", {
|
|
854
|
+
description: "Get trending topics for a specific location (WOEID).",
|
|
855
|
+
args: z5.object({
|
|
856
|
+
woeid: z5.string().describe("Where On Earth ID (from trends places)")
|
|
857
|
+
}),
|
|
858
|
+
output: z5.object({
|
|
859
|
+
trends: z5.array(
|
|
860
|
+
z5.object({
|
|
861
|
+
name: z5.string(),
|
|
862
|
+
query: z5.string(),
|
|
863
|
+
tweet_volume: z5.number().optional()
|
|
864
|
+
})
|
|
865
|
+
),
|
|
866
|
+
count: z5.number()
|
|
867
|
+
}),
|
|
868
|
+
examples: [
|
|
869
|
+
{ args: { woeid: "1" }, description: "Get worldwide trends" },
|
|
870
|
+
{ args: { woeid: "2459115" }, description: "Get trends for New York" }
|
|
871
|
+
],
|
|
872
|
+
async run(c) {
|
|
873
|
+
const { apiKey } = apiKeyAuth5("X_BEARER_TOKEN");
|
|
874
|
+
const client = createXApiClient(apiKey);
|
|
875
|
+
const res = await client.getTrendsByLocation(Number(c.args.woeid));
|
|
876
|
+
const trendItems = res.data ?? [];
|
|
877
|
+
const firstTrend = trendItems[0];
|
|
878
|
+
return c.ok(
|
|
879
|
+
{ trends: trendItems, count: trendItems.length },
|
|
880
|
+
{
|
|
881
|
+
cta: firstTrend ? {
|
|
882
|
+
description: "Next steps:",
|
|
883
|
+
commands: [
|
|
884
|
+
{
|
|
885
|
+
command: "posts search",
|
|
886
|
+
args: { query: firstTrend.query },
|
|
887
|
+
description: `Search posts about "${firstTrend.name}"`
|
|
888
|
+
}
|
|
889
|
+
]
|
|
890
|
+
} : void 0
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// src/commands/users.ts
|
|
897
|
+
import { apiKeyAuth as apiKeyAuth6 } from "@spectratools/cli-shared";
|
|
898
|
+
import { Cli as Cli6, z as z6 } from "incur";
|
|
899
|
+
var users = Cli6.create("users", {
|
|
900
|
+
description: "Look up X users."
|
|
901
|
+
});
|
|
902
|
+
async function resolveUser(client, usernameOrId) {
|
|
903
|
+
if (/^\d+$/.test(usernameOrId)) {
|
|
904
|
+
return client.getUserById(usernameOrId);
|
|
905
|
+
}
|
|
906
|
+
return client.getUserByUsername(usernameOrId.replace(/^@/, ""));
|
|
907
|
+
}
|
|
908
|
+
users.command("get", {
|
|
909
|
+
description: "Get a user by username or ID.",
|
|
910
|
+
args: z6.object({
|
|
911
|
+
username: z6.string().describe("Username (with or without @) or user ID")
|
|
912
|
+
}),
|
|
913
|
+
options: z6.object({
|
|
914
|
+
verbose: z6.boolean().optional().describe("Show full bio without truncation")
|
|
915
|
+
}),
|
|
916
|
+
output: z6.object({
|
|
917
|
+
id: z6.string(),
|
|
918
|
+
name: z6.string(),
|
|
919
|
+
username: z6.string(),
|
|
920
|
+
description: z6.string().optional(),
|
|
921
|
+
followers: z6.number().optional(),
|
|
922
|
+
following: z6.number().optional(),
|
|
923
|
+
tweets: z6.number().optional(),
|
|
924
|
+
joined: z6.string().optional()
|
|
925
|
+
}),
|
|
926
|
+
examples: [
|
|
927
|
+
{ args: { username: "jack" }, description: "Get a user by username" },
|
|
928
|
+
{ args: { username: "12345" }, description: "Get a user by ID" }
|
|
929
|
+
],
|
|
930
|
+
async run(c) {
|
|
931
|
+
const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
|
|
932
|
+
const client = createXApiClient(apiKey);
|
|
933
|
+
const res = await resolveUser(client, c.args.username);
|
|
934
|
+
const user = res.data;
|
|
935
|
+
return c.ok(
|
|
936
|
+
{
|
|
937
|
+
id: user.id,
|
|
938
|
+
name: user.name,
|
|
939
|
+
username: user.username,
|
|
940
|
+
description: user.description ? c.options.verbose ? user.description : truncateText(user.description) : void 0,
|
|
941
|
+
followers: user.public_metrics?.followers_count,
|
|
942
|
+
following: user.public_metrics?.following_count,
|
|
943
|
+
tweets: user.public_metrics?.tweet_count,
|
|
944
|
+
joined: user.created_at ? relativeTime(user.created_at) : void 0
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
cta: {
|
|
948
|
+
description: "Explore this user:",
|
|
949
|
+
commands: [
|
|
950
|
+
{
|
|
951
|
+
command: "users posts",
|
|
952
|
+
args: { username: user.username },
|
|
953
|
+
description: "View their posts"
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
command: "users followers",
|
|
957
|
+
args: { username: user.username },
|
|
958
|
+
description: "View their followers"
|
|
959
|
+
}
|
|
960
|
+
]
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
users.command("followers", {
|
|
967
|
+
description: "List followers of a user.",
|
|
968
|
+
args: z6.object({
|
|
969
|
+
username: z6.string().describe("Username or user ID")
|
|
970
|
+
}),
|
|
971
|
+
options: z6.object({
|
|
972
|
+
maxResults: z6.number().default(100).describe("Maximum followers to return")
|
|
973
|
+
}),
|
|
974
|
+
alias: { maxResults: "n" },
|
|
975
|
+
output: z6.object({
|
|
976
|
+
users: z6.array(
|
|
977
|
+
z6.object({
|
|
978
|
+
id: z6.string(),
|
|
979
|
+
name: z6.string(),
|
|
980
|
+
username: z6.string(),
|
|
981
|
+
followers: z6.number().optional()
|
|
982
|
+
})
|
|
983
|
+
),
|
|
984
|
+
count: z6.number()
|
|
985
|
+
}),
|
|
986
|
+
examples: [{ args: { username: "jack" }, description: "List followers of jack" }],
|
|
987
|
+
async run(c) {
|
|
988
|
+
const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
|
|
989
|
+
const client = createXApiClient(apiKey);
|
|
990
|
+
const userRes = await resolveUser(client, c.args.username);
|
|
991
|
+
const userId = userRes.data.id;
|
|
992
|
+
const allUsers = await collectPaged(
|
|
993
|
+
(limit, cursor) => client.getUserFollowers(userId, limit, cursor),
|
|
994
|
+
(user) => ({
|
|
995
|
+
id: user.id,
|
|
996
|
+
name: user.name,
|
|
997
|
+
username: user.username,
|
|
998
|
+
followers: user.public_metrics?.followers_count
|
|
999
|
+
}),
|
|
1000
|
+
c.options.maxResults,
|
|
1001
|
+
1e3
|
|
1002
|
+
);
|
|
1003
|
+
return c.ok({ users: allUsers, count: allUsers.length });
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
users.command("following", {
|
|
1007
|
+
description: "List accounts a user is following.",
|
|
1008
|
+
args: z6.object({
|
|
1009
|
+
username: z6.string().describe("Username or user ID")
|
|
1010
|
+
}),
|
|
1011
|
+
options: z6.object({
|
|
1012
|
+
maxResults: z6.number().default(100).describe("Maximum accounts to return")
|
|
1013
|
+
}),
|
|
1014
|
+
alias: { maxResults: "n" },
|
|
1015
|
+
output: z6.object({
|
|
1016
|
+
users: z6.array(
|
|
1017
|
+
z6.object({
|
|
1018
|
+
id: z6.string(),
|
|
1019
|
+
name: z6.string(),
|
|
1020
|
+
username: z6.string(),
|
|
1021
|
+
followers: z6.number().optional()
|
|
1022
|
+
})
|
|
1023
|
+
),
|
|
1024
|
+
count: z6.number()
|
|
1025
|
+
}),
|
|
1026
|
+
examples: [{ args: { username: "jack" }, description: "List accounts jack follows" }],
|
|
1027
|
+
async run(c) {
|
|
1028
|
+
const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
|
|
1029
|
+
const client = createXApiClient(apiKey);
|
|
1030
|
+
const userRes = await resolveUser(client, c.args.username);
|
|
1031
|
+
const userId = userRes.data.id;
|
|
1032
|
+
const allUsers = await collectPaged(
|
|
1033
|
+
(limit, cursor) => client.getUserFollowing(userId, limit, cursor),
|
|
1034
|
+
(user) => ({
|
|
1035
|
+
id: user.id,
|
|
1036
|
+
name: user.name,
|
|
1037
|
+
username: user.username,
|
|
1038
|
+
followers: user.public_metrics?.followers_count
|
|
1039
|
+
}),
|
|
1040
|
+
c.options.maxResults,
|
|
1041
|
+
1e3
|
|
1042
|
+
);
|
|
1043
|
+
return c.ok({ users: allUsers, count: allUsers.length });
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
users.command("posts", {
|
|
1047
|
+
description: "List a user's posts.",
|
|
1048
|
+
args: z6.object({
|
|
1049
|
+
username: z6.string().describe("Username or user ID")
|
|
1050
|
+
}),
|
|
1051
|
+
options: z6.object({
|
|
1052
|
+
maxResults: z6.number().default(10).describe("Maximum posts to return"),
|
|
1053
|
+
verbose: z6.boolean().optional().describe("Show full text without truncation")
|
|
1054
|
+
}),
|
|
1055
|
+
alias: { maxResults: "n" },
|
|
1056
|
+
output: z6.object({
|
|
1057
|
+
posts: z6.array(
|
|
1058
|
+
z6.object({
|
|
1059
|
+
id: z6.string(),
|
|
1060
|
+
text: z6.string(),
|
|
1061
|
+
created_at: z6.string().optional(),
|
|
1062
|
+
likes: z6.number().optional(),
|
|
1063
|
+
retweets: z6.number().optional()
|
|
1064
|
+
})
|
|
1065
|
+
),
|
|
1066
|
+
count: z6.number()
|
|
1067
|
+
}),
|
|
1068
|
+
examples: [{ args: { username: "jack" }, description: "Get jack's recent posts" }],
|
|
1069
|
+
async run(c) {
|
|
1070
|
+
const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
|
|
1071
|
+
const client = createXApiClient(apiKey);
|
|
1072
|
+
const userRes = await resolveUser(client, c.args.username);
|
|
1073
|
+
const userId = userRes.data.id;
|
|
1074
|
+
const allPosts = await collectPaged(
|
|
1075
|
+
(limit, cursor) => client.getUserPosts(userId, limit, cursor),
|
|
1076
|
+
(post) => ({
|
|
1077
|
+
id: post.id,
|
|
1078
|
+
text: c.options.verbose ? post.text : truncateText(post.text),
|
|
1079
|
+
created_at: post.created_at ? relativeTime(post.created_at) : void 0,
|
|
1080
|
+
likes: post.public_metrics?.like_count,
|
|
1081
|
+
retweets: post.public_metrics?.retweet_count
|
|
1082
|
+
}),
|
|
1083
|
+
c.options.maxResults
|
|
1084
|
+
);
|
|
1085
|
+
const firstId = allPosts[0]?.id;
|
|
1086
|
+
return c.ok(
|
|
1087
|
+
{ posts: allPosts, count: allPosts.length },
|
|
1088
|
+
{
|
|
1089
|
+
cta: firstId ? {
|
|
1090
|
+
description: "Next steps:",
|
|
1091
|
+
commands: [
|
|
1092
|
+
{
|
|
1093
|
+
command: "posts get",
|
|
1094
|
+
args: { id: firstId },
|
|
1095
|
+
description: "View top post in detail"
|
|
1096
|
+
}
|
|
1097
|
+
]
|
|
1098
|
+
} : void 0
|
|
1099
|
+
}
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
users.command("mentions", {
|
|
1104
|
+
description: "List recent mentions of a user.",
|
|
1105
|
+
args: z6.object({
|
|
1106
|
+
username: z6.string().describe("Username or user ID")
|
|
1107
|
+
}),
|
|
1108
|
+
options: z6.object({
|
|
1109
|
+
maxResults: z6.number().default(10).describe("Maximum mentions to return"),
|
|
1110
|
+
verbose: z6.boolean().optional().describe("Show full text")
|
|
1111
|
+
}),
|
|
1112
|
+
alias: { maxResults: "n" },
|
|
1113
|
+
output: z6.object({
|
|
1114
|
+
posts: z6.array(
|
|
1115
|
+
z6.object({
|
|
1116
|
+
id: z6.string(),
|
|
1117
|
+
text: z6.string(),
|
|
1118
|
+
created_at: z6.string().optional()
|
|
1119
|
+
})
|
|
1120
|
+
),
|
|
1121
|
+
count: z6.number()
|
|
1122
|
+
}),
|
|
1123
|
+
examples: [{ args: { username: "jack" }, description: "Get mentions of jack" }],
|
|
1124
|
+
async run(c) {
|
|
1125
|
+
const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
|
|
1126
|
+
const client = createXApiClient(apiKey);
|
|
1127
|
+
const userRes = await resolveUser(client, c.args.username);
|
|
1128
|
+
const userId = userRes.data.id;
|
|
1129
|
+
const allPosts = await collectPaged(
|
|
1130
|
+
(limit, cursor) => client.getUserMentions(userId, limit, cursor),
|
|
1131
|
+
(post) => ({
|
|
1132
|
+
id: post.id,
|
|
1133
|
+
text: c.options.verbose ? post.text : truncateText(post.text),
|
|
1134
|
+
created_at: post.created_at ? relativeTime(post.created_at) : void 0
|
|
1135
|
+
}),
|
|
1136
|
+
c.options.maxResults
|
|
1137
|
+
);
|
|
1138
|
+
return c.ok({ posts: allPosts, count: allPosts.length });
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
users.command("search", {
|
|
1142
|
+
description: "Search for users by keyword.",
|
|
1143
|
+
args: z6.object({
|
|
1144
|
+
query: z6.string().describe("Search query")
|
|
1145
|
+
}),
|
|
1146
|
+
output: z6.object({
|
|
1147
|
+
users: z6.array(
|
|
1148
|
+
z6.object({
|
|
1149
|
+
id: z6.string(),
|
|
1150
|
+
name: z6.string(),
|
|
1151
|
+
username: z6.string(),
|
|
1152
|
+
followers: z6.number().optional()
|
|
1153
|
+
})
|
|
1154
|
+
),
|
|
1155
|
+
count: z6.number()
|
|
1156
|
+
}),
|
|
1157
|
+
examples: [{ args: { query: "TypeScript" }, description: "Search for users about TypeScript" }],
|
|
1158
|
+
async run(c) {
|
|
1159
|
+
const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
|
|
1160
|
+
const client = createXApiClient(apiKey);
|
|
1161
|
+
const res = await client.searchUsers(c.args.query);
|
|
1162
|
+
const items = (res.data ?? []).map((u) => ({
|
|
1163
|
+
id: u.id,
|
|
1164
|
+
name: u.name,
|
|
1165
|
+
username: u.username,
|
|
1166
|
+
followers: u.public_metrics?.followers_count
|
|
1167
|
+
}));
|
|
1168
|
+
const first = items[0];
|
|
1169
|
+
return c.ok(
|
|
1170
|
+
{ users: items, count: items.length },
|
|
1171
|
+
{
|
|
1172
|
+
cta: first ? {
|
|
1173
|
+
description: "Next steps:",
|
|
1174
|
+
commands: [
|
|
1175
|
+
{
|
|
1176
|
+
command: "users get",
|
|
1177
|
+
args: { username: first.username },
|
|
1178
|
+
description: `View @${first.username}'s profile`
|
|
1179
|
+
}
|
|
1180
|
+
]
|
|
1181
|
+
} : void 0
|
|
1182
|
+
}
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
// src/cli.ts
|
|
1188
|
+
var cli = Cli7.create("xapi", {
|
|
1189
|
+
description: "X (Twitter) API CLI for spectra-the-bot."
|
|
1190
|
+
});
|
|
1191
|
+
cli.command(posts);
|
|
1192
|
+
cli.command(users);
|
|
1193
|
+
cli.command(timeline);
|
|
1194
|
+
cli.command(lists);
|
|
1195
|
+
cli.command(trends);
|
|
1196
|
+
cli.command(dm);
|
|
1197
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
1198
|
+
cli.serve();
|
|
1199
|
+
}
|
|
1200
|
+
export {
|
|
1201
|
+
cli
|
|
1202
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spectratools/xapi-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "X (Twitter) API CLI for spectra-the-bot",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "spectra-the-bot",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"xapi-cli": "./dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"incur": "^0.2.2",
|
|
22
|
+
"@spectratools/cli-shared": "0.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "5.7.3",
|
|
26
|
+
"vitest": "2.1.8"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"main": "./dist/cli.js",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
36
|
+
"test": "vitest run"
|
|
37
|
+
}
|
|
38
|
+
}
|