@spectratools/xapi-cli 0.6.1 → 0.6.2

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.
Files changed (2) hide show
  1. package/dist/index.js +2058 -0
  2. package/package.json +2 -2
package/dist/index.js ADDED
@@ -0,0 +1,2058 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync as readFileSync2, realpathSync } from "fs";
5
+ import { dirname, resolve } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { Cli as Cli7 } from "incur";
8
+
9
+ // src/auth.ts
10
+ import { HttpError } from "@spectratools/cli-shared";
11
+ import { z } from "incur";
12
+ var bearerTokenSchema = z.string().describe("X app-only bearer token (read-only endpoints)");
13
+ var accessTokenSchema = z.string().describe("X OAuth 2.0 user access token (required for write endpoints)");
14
+ var xApiReadEnv = z.object({
15
+ X_BEARER_TOKEN: bearerTokenSchema.optional(),
16
+ X_ACCESS_TOKEN: accessTokenSchema.optional()
17
+ }).refine((env) => Boolean(env.X_ACCESS_TOKEN || env.X_BEARER_TOKEN), {
18
+ message: "Set X_ACCESS_TOKEN or X_BEARER_TOKEN to authenticate X API requests."
19
+ });
20
+ var xApiWriteEnv = z.object({
21
+ X_ACCESS_TOKEN: accessTokenSchema,
22
+ X_BEARER_TOKEN: bearerTokenSchema.optional()
23
+ });
24
+ function readAuthToken(env) {
25
+ if (env.X_ACCESS_TOKEN) {
26
+ return env.X_ACCESS_TOKEN;
27
+ }
28
+ return env.X_BEARER_TOKEN;
29
+ }
30
+ function writeAuthToken(env) {
31
+ return env.X_ACCESS_TOKEN;
32
+ }
33
+ function parseXApiErrorDetail(body) {
34
+ try {
35
+ const parsed = JSON.parse(body);
36
+ if (typeof parsed !== "object" || parsed === null) {
37
+ return void 0;
38
+ }
39
+ const candidate = parsed;
40
+ if (typeof candidate.detail === "string" && candidate.detail.trim()) return candidate.detail;
41
+ if (typeof candidate.title === "string" && candidate.title.trim()) return candidate.title;
42
+ const firstError = candidate.errors?.[0];
43
+ if (typeof firstError?.message === "string" && firstError.message.trim())
44
+ return firstError.message;
45
+ if (typeof firstError?.detail === "string" && firstError.detail.trim())
46
+ return firstError.detail;
47
+ } catch {
48
+ }
49
+ return void 0;
50
+ }
51
+ function readEndpointNeedsUserToken(detail) {
52
+ if (!detail) return false;
53
+ const normalized = detail.toLowerCase();
54
+ return normalized.includes("application-only is forbidden") || normalized.includes("oauth 2.0 application-only") || normalized.includes("app-only") || normalized.includes("user context");
55
+ }
56
+ function toReadAuthError(operation, error) {
57
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
58
+ const detail = parseXApiErrorDetail(error.body);
59
+ const requiresUserToken = readEndpointNeedsUserToken(detail);
60
+ return {
61
+ code: "INSUFFICIENT_READ_AUTH",
62
+ message: [
63
+ "Insufficient auth for read endpoint:",
64
+ `- operation: ${operation}`,
65
+ `- status: ${error.status} ${error.statusText}`,
66
+ requiresUserToken ? "- required auth: X_ACCESS_TOKEN (OAuth 2.0 user token required for this endpoint)" : "- required auth: X_ACCESS_TOKEN or X_BEARER_TOKEN (with required read scopes)",
67
+ ...detail ? [`- x_api_detail: ${detail}`] : []
68
+ ].join("\n")
69
+ };
70
+ }
71
+ return void 0;
72
+ }
73
+ function toWriteAuthError(operation, error) {
74
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
75
+ const detail = parseXApiErrorDetail(error.body);
76
+ return {
77
+ code: "INSUFFICIENT_WRITE_AUTH",
78
+ message: [
79
+ "Insufficient auth for write endpoint:",
80
+ `- operation: ${operation}`,
81
+ `- status: ${error.status} ${error.statusText}`,
82
+ "- required auth: X_ACCESS_TOKEN (OAuth 2.0 user token with write scopes)",
83
+ "- note: app-only X_BEARER_TOKEN cannot perform write actions",
84
+ ...detail ? [`- x_api_detail: ${detail}`] : []
85
+ ].join("\n")
86
+ };
87
+ }
88
+ return void 0;
89
+ }
90
+ function toXApiHttpError(operation, error) {
91
+ if (!(error instanceof HttpError)) {
92
+ return void 0;
93
+ }
94
+ const detail = parseXApiErrorDetail(error.body);
95
+ return {
96
+ code: "X_API_REQUEST_FAILED",
97
+ message: [
98
+ "X API request failed:",
99
+ `- operation: ${operation}`,
100
+ `- status: ${error.status} ${error.statusText}`,
101
+ ...detail ? [`- x_api_detail: ${detail}`] : []
102
+ ].join("\n")
103
+ };
104
+ }
105
+ function toXApiCommandError(operation, error, authScope = "read") {
106
+ const authError = authScope === "write" ? toWriteAuthError(operation, error) : toReadAuthError(operation, error);
107
+ if (authError) {
108
+ return authError;
109
+ }
110
+ return toXApiHttpError(operation, error);
111
+ }
112
+
113
+ // src/commands/dm.ts
114
+ import { Cli, z as z2 } from "incur";
115
+
116
+ // src/api.ts
117
+ import { createHttpClient, withRetry } from "@spectratools/cli-shared";
118
+ var BASE_URL_V2 = "https://api.x.com/2";
119
+ var BASE_URL_V1_1 = "https://api.x.com/1.1";
120
+ var RETRY_OPTIONS = { maxRetries: 3, baseMs: 500, maxMs: 1e4 };
121
+ function createXApiClient(bearerToken) {
122
+ const defaultHeaders = {
123
+ Authorization: `Bearer ${bearerToken}`
124
+ };
125
+ const httpV2 = createHttpClient({
126
+ baseUrl: BASE_URL_V2,
127
+ defaultHeaders
128
+ });
129
+ const httpV1_1 = createHttpClient({
130
+ baseUrl: BASE_URL_V1_1,
131
+ defaultHeaders
132
+ });
133
+ function get(path, query) {
134
+ return withRetry(
135
+ () => httpV2.request(path, query !== void 0 ? { query } : {}),
136
+ RETRY_OPTIONS
137
+ );
138
+ }
139
+ function getV1_1(path, query) {
140
+ return withRetry(
141
+ () => httpV1_1.request(path, query !== void 0 ? { query } : {}),
142
+ RETRY_OPTIONS
143
+ );
144
+ }
145
+ function post(path, body) {
146
+ return withRetry(() => httpV2.request(path, { method: "POST", body }), RETRY_OPTIONS);
147
+ }
148
+ function del(path) {
149
+ return withRetry(() => httpV2.request(path, { method: "DELETE" }), RETRY_OPTIONS);
150
+ }
151
+ const POST_FIELDS = "id,text,author_id,created_at,public_metrics";
152
+ function getPost(id) {
153
+ return get(`/tweets/${id}`, {
154
+ "tweet.fields": POST_FIELDS
155
+ });
156
+ }
157
+ function searchPosts(query, maxResults, sort, nextToken) {
158
+ return get("/tweets/search/recent", {
159
+ query,
160
+ max_results: maxResults,
161
+ sort_order: sort,
162
+ "tweet.fields": POST_FIELDS,
163
+ ...nextToken ? { next_token: nextToken } : {}
164
+ });
165
+ }
166
+ function createPost(text, replyTo, quote) {
167
+ const body = { text };
168
+ if (replyTo) body.reply = { in_reply_to_tweet_id: replyTo };
169
+ if (quote) body.quote_tweet_id = quote;
170
+ return post("/tweets", body);
171
+ }
172
+ function deletePost(id) {
173
+ return del(`/tweets/${id}`);
174
+ }
175
+ function getPostLikes(id, maxResults, nextToken) {
176
+ return get(`/tweets/${id}/liking_users`, {
177
+ max_results: maxResults,
178
+ "user.fields": "id,name,username,public_metrics",
179
+ ...nextToken ? { pagination_token: nextToken } : {}
180
+ });
181
+ }
182
+ function getPostRetweets(id, maxResults, nextToken) {
183
+ return get(`/tweets/${id}/retweeted_by`, {
184
+ max_results: maxResults,
185
+ "user.fields": "id,name,username,public_metrics",
186
+ ...nextToken ? { pagination_token: nextToken } : {}
187
+ });
188
+ }
189
+ function likePost(userId, tweetId) {
190
+ return post(`/users/${userId}/likes`, { tweet_id: tweetId });
191
+ }
192
+ function unlikePost(userId, tweetId) {
193
+ return del(`/users/${userId}/likes/${tweetId}`);
194
+ }
195
+ function bookmarkPost(userId, tweetId) {
196
+ return post(`/users/${userId}/bookmarks`, { tweet_id: tweetId });
197
+ }
198
+ function unbookmarkPost(userId, tweetId) {
199
+ return del(`/users/${userId}/bookmarks/${tweetId}`);
200
+ }
201
+ function retweetPost(userId, tweetId) {
202
+ return post(`/users/${userId}/retweets`, { tweet_id: tweetId });
203
+ }
204
+ const USER_FIELDS = "id,name,username,description,public_metrics,created_at";
205
+ function getUserByUsername(username) {
206
+ return get(`/users/by/username/${username}`, {
207
+ "user.fields": USER_FIELDS
208
+ });
209
+ }
210
+ function getUserById(id) {
211
+ return get(`/users/${id}`, {
212
+ "user.fields": USER_FIELDS
213
+ });
214
+ }
215
+ function getUserFollowers(id, maxResults, nextToken) {
216
+ return get(`/users/${id}/followers`, {
217
+ max_results: maxResults,
218
+ "user.fields": USER_FIELDS,
219
+ ...nextToken ? { pagination_token: nextToken } : {}
220
+ });
221
+ }
222
+ function getUserFollowing(id, maxResults, nextToken) {
223
+ return get(`/users/${id}/following`, {
224
+ max_results: maxResults,
225
+ "user.fields": USER_FIELDS,
226
+ ...nextToken ? { pagination_token: nextToken } : {}
227
+ });
228
+ }
229
+ function followUser(sourceUserId, targetUserId) {
230
+ return post(`/users/${sourceUserId}/following`, {
231
+ target_user_id: targetUserId
232
+ });
233
+ }
234
+ function unfollowUser(sourceUserId, targetUserId) {
235
+ return del(`/users/${sourceUserId}/following/${targetUserId}`);
236
+ }
237
+ function blockUser(sourceUserId, targetUserId) {
238
+ return post(`/users/${sourceUserId}/blocking`, {
239
+ target_user_id: targetUserId
240
+ });
241
+ }
242
+ function unblockUser(sourceUserId, targetUserId) {
243
+ return del(`/users/${sourceUserId}/blocking/${targetUserId}`);
244
+ }
245
+ function muteUser(sourceUserId, targetUserId) {
246
+ return post(`/users/${sourceUserId}/muting`, {
247
+ target_user_id: targetUserId
248
+ });
249
+ }
250
+ function unmuteUser(sourceUserId, targetUserId) {
251
+ return del(`/users/${sourceUserId}/muting/${targetUserId}`);
252
+ }
253
+ function getUserPosts(id, maxResults, nextToken) {
254
+ return get(`/users/${id}/tweets`, {
255
+ max_results: maxResults,
256
+ "tweet.fields": POST_FIELDS,
257
+ ...nextToken ? { pagination_token: nextToken } : {}
258
+ });
259
+ }
260
+ function getUserMentions(id, maxResults, nextToken) {
261
+ return get(`/users/${id}/mentions`, {
262
+ max_results: maxResults,
263
+ "tweet.fields": POST_FIELDS,
264
+ ...nextToken ? { pagination_token: nextToken } : {}
265
+ });
266
+ }
267
+ function searchUsers(query) {
268
+ return get("/users/search", {
269
+ query,
270
+ "user.fields": USER_FIELDS
271
+ });
272
+ }
273
+ function getHomeTimeline(userId, maxResults, nextToken, sinceId) {
274
+ return get(`/users/${userId}/timelines/reverse_chronological`, {
275
+ max_results: maxResults,
276
+ "tweet.fields": POST_FIELDS,
277
+ ...nextToken ? { pagination_token: nextToken } : {},
278
+ ...sinceId ? { since_id: sinceId } : {}
279
+ });
280
+ }
281
+ function getMentionsTimeline(userId, maxResults, nextToken, sinceId) {
282
+ return get(`/users/${userId}/mentions`, {
283
+ max_results: maxResults,
284
+ "tweet.fields": POST_FIELDS,
285
+ ...nextToken ? { pagination_token: nextToken } : {},
286
+ ...sinceId ? { since_id: sinceId } : {}
287
+ });
288
+ }
289
+ function getList(id) {
290
+ return get(`/lists/${id}`, {
291
+ "list.fields": "id,name,description,owner_id,member_count"
292
+ });
293
+ }
294
+ function getListMembers(id, maxResults, nextToken) {
295
+ return get(`/lists/${id}/members`, {
296
+ max_results: maxResults,
297
+ "user.fields": USER_FIELDS,
298
+ ...nextToken ? { pagination_token: nextToken } : {}
299
+ });
300
+ }
301
+ function getListPosts(id, maxResults, nextToken) {
302
+ return get(`/lists/${id}/tweets`, {
303
+ max_results: maxResults,
304
+ "tweet.fields": POST_FIELDS,
305
+ ...nextToken ? { pagination_token: nextToken } : {}
306
+ });
307
+ }
308
+ function createList(name, description, isPrivate) {
309
+ return post("/lists", {
310
+ name,
311
+ ...description ? { description } : {},
312
+ ...isPrivate !== void 0 ? { private: isPrivate } : {}
313
+ });
314
+ }
315
+ function deleteList(id) {
316
+ return del(`/lists/${id}`);
317
+ }
318
+ function addListMember(id, userId) {
319
+ return post(`/lists/${id}/members`, {
320
+ user_id: userId
321
+ });
322
+ }
323
+ function removeListMember(id, userId) {
324
+ return del(`/lists/${id}/members/${userId}`);
325
+ }
326
+ function getTrendingPlaces() {
327
+ return getV1_1(
328
+ "/trends/available.json"
329
+ ).then((places) => ({ data: places }));
330
+ }
331
+ function getTrendsByLocation(woeid) {
332
+ return getV1_1("/trends/place.json", { id: woeid }).then((locations) => {
333
+ const trends2 = (locations[0]?.trends ?? []).map((trend) => ({
334
+ name: trend.name,
335
+ query: trend.query,
336
+ tweet_volume: trend.tweet_volume ?? void 0
337
+ }));
338
+ return { data: trends2 };
339
+ });
340
+ }
341
+ function getDmConversations(userId, maxResults, nextToken) {
342
+ return get(`/users/${userId}/dm_conversations`, {
343
+ max_results: maxResults,
344
+ ...nextToken ? { pagination_token: nextToken } : {}
345
+ });
346
+ }
347
+ function sendDm(participantId, text) {
348
+ return post(
349
+ `/dm_conversations/with/${participantId}/messages`,
350
+ { text }
351
+ );
352
+ }
353
+ function getMe() {
354
+ return get("/users/me", { "user.fields": USER_FIELDS });
355
+ }
356
+ return {
357
+ getPost,
358
+ searchPosts,
359
+ createPost,
360
+ deletePost,
361
+ getPostLikes,
362
+ getPostRetweets,
363
+ likePost,
364
+ unlikePost,
365
+ bookmarkPost,
366
+ unbookmarkPost,
367
+ retweetPost,
368
+ getUserByUsername,
369
+ getUserById,
370
+ getUserFollowers,
371
+ getUserFollowing,
372
+ followUser,
373
+ unfollowUser,
374
+ blockUser,
375
+ unblockUser,
376
+ muteUser,
377
+ unmuteUser,
378
+ getUserPosts,
379
+ getUserMentions,
380
+ searchUsers,
381
+ getHomeTimeline,
382
+ getMentionsTimeline,
383
+ getList,
384
+ getListMembers,
385
+ getListPosts,
386
+ createList,
387
+ deleteList,
388
+ addListMember,
389
+ removeListMember,
390
+ getTrendingPlaces,
391
+ getTrendsByLocation,
392
+ getDmConversations,
393
+ sendDm,
394
+ getMe
395
+ };
396
+ }
397
+ function relativeTime(iso) {
398
+ const ms = Date.now() - new Date(iso).getTime();
399
+ const s = Math.floor(ms / 1e3);
400
+ if (s < 60) return `${s}s ago`;
401
+ const m = Math.floor(s / 60);
402
+ if (m < 60) return `${m}m ago`;
403
+ const h = Math.floor(m / 60);
404
+ if (h < 24) return `${h}h ago`;
405
+ const d = Math.floor(h / 24);
406
+ return `${d}d ago`;
407
+ }
408
+ function truncateText(text, max = 100) {
409
+ if (text.length <= max) return text;
410
+ return `${text.slice(0, max - 3)}...`;
411
+ }
412
+
413
+ // src/collect-paged.ts
414
+ import { paginateCursor } from "@spectratools/cli-shared";
415
+ async function collectPaged(fetchFn, mapFn, maxResults, pageSize = 100) {
416
+ const results = [];
417
+ for await (const item of paginateCursor({
418
+ fetchPage: async (cursor) => {
419
+ const res = await fetchFn(
420
+ Math.min(maxResults - results.length, pageSize),
421
+ cursor ?? void 0
422
+ );
423
+ return {
424
+ items: res.data ?? [],
425
+ nextCursor: res.meta?.next_token ?? null
426
+ };
427
+ }
428
+ })) {
429
+ results.push(mapFn(item));
430
+ if (results.length >= maxResults) break;
431
+ }
432
+ return results;
433
+ }
434
+
435
+ // src/commands/dm.ts
436
+ var dm = Cli.create("dm", {
437
+ description: "Manage X direct messages."
438
+ });
439
+ dm.command("conversations", {
440
+ description: "List your DM conversations.",
441
+ options: z2.object({
442
+ maxResults: z2.number().default(20).describe("Maximum conversations to return")
443
+ }),
444
+ alias: { maxResults: "n" },
445
+ env: xApiReadEnv,
446
+ output: z2.object({
447
+ conversations: z2.array(
448
+ z2.object({
449
+ dm_conversation_id: z2.string(),
450
+ participant_ids: z2.array(z2.string())
451
+ })
452
+ ),
453
+ count: z2.number()
454
+ }),
455
+ examples: [{ description: "List your DM conversations" }],
456
+ async run(c) {
457
+ const client = createXApiClient(readAuthToken(c.env));
458
+ const meRes = await client.getMe();
459
+ const userId = meRes.data.id;
460
+ const allConvos = await collectPaged(
461
+ (limit, cursor) => client.getDmConversations(userId, limit, cursor),
462
+ (convo) => ({
463
+ dm_conversation_id: convo.dm_conversation_id,
464
+ participant_ids: convo.participant_ids
465
+ }),
466
+ c.options.maxResults
467
+ );
468
+ const firstParticipant = allConvos[0]?.participant_ids[0];
469
+ return c.ok(
470
+ { conversations: allConvos, count: allConvos.length },
471
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
472
+ cta: firstParticipant ? {
473
+ description: "Next steps:",
474
+ commands: [
475
+ {
476
+ command: "dm send",
477
+ args: { participantId: firstParticipant },
478
+ options: { text: "Hello!" },
479
+ description: "Send a message to the first conversation"
480
+ }
481
+ ]
482
+ } : void 0
483
+ }
484
+ );
485
+ }
486
+ });
487
+ dm.command("send", {
488
+ description: "Send a direct message to a user.",
489
+ args: z2.object({
490
+ participantId: z2.string().describe("User ID to send message to")
491
+ }),
492
+ options: z2.object({
493
+ text: z2.string().describe("Message text")
494
+ }),
495
+ env: xApiWriteEnv,
496
+ output: z2.object({
497
+ dm_conversation_id: z2.string(),
498
+ dm_event_id: z2.string()
499
+ }),
500
+ examples: [
501
+ {
502
+ args: { participantId: "12345" },
503
+ options: { text: "Hey there!" },
504
+ description: "Send a DM to a user"
505
+ }
506
+ ],
507
+ async run(c) {
508
+ try {
509
+ const client = createXApiClient(writeAuthToken(c.env));
510
+ const res = await client.sendDm(c.args.participantId, c.options.text);
511
+ return c.ok(res.data);
512
+ } catch (error) {
513
+ const authError = toWriteAuthError("dm send", error);
514
+ if (authError) return c.error(authError);
515
+ throw error;
516
+ }
517
+ }
518
+ });
519
+
520
+ // src/commands/lists.ts
521
+ import { Cli as Cli2, z as z3 } from "incur";
522
+ var lists = Cli2.create("lists", {
523
+ description: "Manage and browse X lists."
524
+ });
525
+ var listIdSchema = z3.string().min(1).describe("List ID");
526
+ var memberSchema = z3.string().trim().min(1).describe("Member to target (username with or without @, or user ID)");
527
+ var createListOptionsSchema = z3.object({
528
+ name: z3.string().trim().min(1).max(25).describe("List name (1-25 characters)"),
529
+ description: z3.string().trim().min(1).max(100).optional().describe("Optional list description (1-100 characters)"),
530
+ private: z3.boolean().default(false).describe("Create as a private list")
531
+ });
532
+ async function resolveMemberTarget(client, usernameOrId) {
533
+ const normalized = usernameOrId.replace(/^@/, "");
534
+ if (/^\d+$/.test(normalized)) {
535
+ return { id: normalized };
536
+ }
537
+ const user = await client.getUserByUsername(normalized);
538
+ return {
539
+ id: user.data.id,
540
+ username: user.data.username
541
+ };
542
+ }
543
+ lists.command("get", {
544
+ description: "Get a list by ID.",
545
+ args: z3.object({
546
+ id: listIdSchema
547
+ }),
548
+ env: xApiReadEnv,
549
+ output: z3.object({
550
+ id: z3.string(),
551
+ name: z3.string(),
552
+ description: z3.string().optional(),
553
+ owner_id: z3.string().optional(),
554
+ member_count: z3.number().optional()
555
+ }),
556
+ examples: [{ args: { id: "1234567890" }, description: "Get list details" }],
557
+ async run(c) {
558
+ const client = createXApiClient(readAuthToken(c.env));
559
+ const res = await client.getList(c.args.id);
560
+ const list = res.data;
561
+ return c.ok(
562
+ {
563
+ id: list.id,
564
+ name: list.name,
565
+ description: list.description,
566
+ owner_id: list.owner_id,
567
+ member_count: list.member_count
568
+ },
569
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
570
+ cta: {
571
+ description: "Explore this list:",
572
+ commands: [
573
+ {
574
+ command: "lists members",
575
+ args: { id: c.args.id },
576
+ description: "See list members"
577
+ },
578
+ {
579
+ command: "lists posts",
580
+ args: { id: c.args.id },
581
+ description: "See list posts"
582
+ }
583
+ ]
584
+ }
585
+ }
586
+ );
587
+ }
588
+ });
589
+ lists.command("create", {
590
+ description: "Create a new list.",
591
+ options: createListOptionsSchema,
592
+ env: xApiWriteEnv,
593
+ output: z3.object({
594
+ id: z3.string(),
595
+ name: z3.string()
596
+ }),
597
+ examples: [
598
+ {
599
+ options: { name: "Core devs", description: "Builders only", private: true },
600
+ description: "Create a private list"
601
+ }
602
+ ],
603
+ async run(c) {
604
+ try {
605
+ const client = createXApiClient(writeAuthToken(c.env));
606
+ const res = await client.createList(c.options.name, c.options.description, c.options.private);
607
+ return c.ok(
608
+ res.data,
609
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
610
+ cta: {
611
+ description: "Next steps:",
612
+ commands: [
613
+ {
614
+ command: "lists get",
615
+ args: { id: res.data.id },
616
+ description: "View the list details"
617
+ },
618
+ {
619
+ command: "lists add-member",
620
+ args: { id: res.data.id, member: "username-or-id" },
621
+ description: "Add members to the new list"
622
+ }
623
+ ]
624
+ }
625
+ }
626
+ );
627
+ } catch (error) {
628
+ const authError = toWriteAuthError("lists create", error);
629
+ if (authError) return c.error(authError);
630
+ throw error;
631
+ }
632
+ }
633
+ });
634
+ lists.command("delete", {
635
+ description: "Delete a list by ID.",
636
+ args: z3.object({
637
+ id: listIdSchema.describe("List ID to delete")
638
+ }),
639
+ env: xApiWriteEnv,
640
+ output: z3.object({
641
+ deleted: z3.boolean(),
642
+ id: z3.string()
643
+ }),
644
+ examples: [{ args: { id: "1234567890" }, description: "Delete a list" }],
645
+ async run(c) {
646
+ try {
647
+ const client = createXApiClient(writeAuthToken(c.env));
648
+ const res = await client.deleteList(c.args.id);
649
+ return c.ok({ deleted: res.data.deleted, id: c.args.id });
650
+ } catch (error) {
651
+ const authError = toWriteAuthError("lists delete", error);
652
+ if (authError) return c.error(authError);
653
+ throw error;
654
+ }
655
+ }
656
+ });
657
+ lists.command("add-member", {
658
+ description: "Add a member to an X list.",
659
+ args: z3.object({
660
+ id: listIdSchema,
661
+ member: memberSchema
662
+ }),
663
+ env: xApiWriteEnv,
664
+ output: z3.object({
665
+ id: z3.string(),
666
+ member_id: z3.string(),
667
+ member_username: z3.string().optional(),
668
+ is_member: z3.boolean()
669
+ }),
670
+ examples: [
671
+ {
672
+ args: { id: "1234567890", member: "jack" },
673
+ description: "Add @jack to a list"
674
+ }
675
+ ],
676
+ async run(c) {
677
+ try {
678
+ const client = createXApiClient(writeAuthToken(c.env));
679
+ const member = await resolveMemberTarget(client, c.args.member);
680
+ const res = await client.addListMember(c.args.id, member.id);
681
+ return c.ok(
682
+ {
683
+ id: c.args.id,
684
+ member_id: member.id,
685
+ member_username: member.username,
686
+ is_member: res.data.is_member
687
+ },
688
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
689
+ cta: {
690
+ description: "Verify list membership:",
691
+ commands: [
692
+ {
693
+ command: "lists members",
694
+ args: { id: c.args.id },
695
+ description: "View current list members"
696
+ }
697
+ ]
698
+ }
699
+ }
700
+ );
701
+ } catch (error) {
702
+ const authError = toWriteAuthError("lists add-member", error);
703
+ if (authError) return c.error(authError);
704
+ throw error;
705
+ }
706
+ }
707
+ });
708
+ lists.command("remove-member", {
709
+ description: "Remove a member from an X list.",
710
+ args: z3.object({
711
+ id: listIdSchema,
712
+ member: memberSchema
713
+ }),
714
+ env: xApiWriteEnv,
715
+ output: z3.object({
716
+ id: z3.string(),
717
+ member_id: z3.string(),
718
+ member_username: z3.string().optional(),
719
+ is_member: z3.boolean()
720
+ }),
721
+ examples: [
722
+ {
723
+ args: { id: "1234567890", member: "jack" },
724
+ description: "Remove @jack from a list"
725
+ }
726
+ ],
727
+ async run(c) {
728
+ try {
729
+ const client = createXApiClient(writeAuthToken(c.env));
730
+ const member = await resolveMemberTarget(client, c.args.member);
731
+ const res = await client.removeListMember(c.args.id, member.id);
732
+ return c.ok(
733
+ {
734
+ id: c.args.id,
735
+ member_id: member.id,
736
+ member_username: member.username,
737
+ is_member: res.data.is_member
738
+ },
739
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
740
+ cta: {
741
+ description: "Next steps:",
742
+ commands: [
743
+ {
744
+ command: "lists members",
745
+ args: { id: c.args.id },
746
+ description: "Confirm updated membership"
747
+ }
748
+ ]
749
+ }
750
+ }
751
+ );
752
+ } catch (error) {
753
+ const authError = toWriteAuthError("lists remove-member", error);
754
+ if (authError) return c.error(authError);
755
+ throw error;
756
+ }
757
+ }
758
+ });
759
+ lists.command("members", {
760
+ description: "List members of an X list.",
761
+ args: z3.object({
762
+ id: listIdSchema
763
+ }),
764
+ options: z3.object({
765
+ maxResults: z3.number().default(100).describe("Maximum members to return")
766
+ }),
767
+ alias: { maxResults: "n" },
768
+ env: xApiReadEnv,
769
+ output: z3.object({
770
+ users: z3.array(
771
+ z3.object({
772
+ id: z3.string(),
773
+ name: z3.string(),
774
+ username: z3.string(),
775
+ followers: z3.number().optional()
776
+ })
777
+ ),
778
+ count: z3.number()
779
+ }),
780
+ examples: [{ args: { id: "1234567890" }, description: "List all members" }],
781
+ async run(c) {
782
+ const client = createXApiClient(readAuthToken(c.env));
783
+ const allUsers = await collectPaged(
784
+ (limit, cursor) => client.getListMembers(c.args.id, limit, cursor),
785
+ (user) => ({
786
+ id: user.id,
787
+ name: user.name,
788
+ username: user.username,
789
+ followers: user.public_metrics?.followers_count
790
+ }),
791
+ c.options.maxResults
792
+ );
793
+ return c.ok({ users: allUsers, count: allUsers.length });
794
+ }
795
+ });
796
+ lists.command("posts", {
797
+ description: "Get posts from an X list.",
798
+ args: z3.object({
799
+ id: listIdSchema
800
+ }),
801
+ options: z3.object({
802
+ maxResults: z3.number().default(25).describe("Maximum posts to return"),
803
+ verbose: z3.boolean().optional().describe("Show full text")
804
+ }),
805
+ alias: { maxResults: "n" },
806
+ env: xApiReadEnv,
807
+ output: z3.object({
808
+ posts: z3.array(
809
+ z3.object({
810
+ id: z3.string(),
811
+ text: z3.string(),
812
+ author_id: z3.string().optional(),
813
+ created_at: z3.string().optional(),
814
+ likes: z3.number().optional()
815
+ })
816
+ ),
817
+ count: z3.number()
818
+ }),
819
+ examples: [{ args: { id: "1234567890" }, description: "Get posts from a list" }],
820
+ async run(c) {
821
+ const client = createXApiClient(readAuthToken(c.env));
822
+ const allPosts = await collectPaged(
823
+ (limit, cursor) => client.getListPosts(c.args.id, limit, cursor),
824
+ (post) => ({
825
+ id: post.id,
826
+ text: c.options.verbose ? post.text : truncateText(post.text),
827
+ author_id: post.author_id,
828
+ created_at: post.created_at ? relativeTime(post.created_at) : void 0,
829
+ likes: post.public_metrics?.like_count
830
+ }),
831
+ c.options.maxResults
832
+ );
833
+ const firstId = allPosts[0]?.id;
834
+ return c.ok(
835
+ { posts: allPosts, count: allPosts.length },
836
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
837
+ cta: firstId ? {
838
+ description: "Next steps:",
839
+ commands: [
840
+ {
841
+ command: "posts get",
842
+ args: { id: firstId },
843
+ description: "View top post in detail"
844
+ }
845
+ ]
846
+ } : void 0
847
+ }
848
+ );
849
+ }
850
+ });
851
+
852
+ // src/commands/posts.ts
853
+ import { Cli as Cli3, z as z4 } from "incur";
854
+ var posts = Cli3.create("posts", {
855
+ description: "Manage and search X posts."
856
+ });
857
+ posts.command("get", {
858
+ description: "Get a post by ID.",
859
+ args: z4.object({
860
+ id: z4.string().describe("Post ID")
861
+ }),
862
+ options: z4.object({
863
+ verbose: z4.boolean().optional().describe("Show full text without truncation")
864
+ }),
865
+ env: xApiReadEnv,
866
+ output: z4.object({
867
+ id: z4.string(),
868
+ text: z4.string(),
869
+ author_id: z4.string().optional(),
870
+ created_at: z4.string().optional(),
871
+ likes: z4.number().optional(),
872
+ retweets: z4.number().optional(),
873
+ replies: z4.number().optional()
874
+ }),
875
+ examples: [{ args: { id: "1234567890" }, description: "Get a post by ID" }],
876
+ async run(c) {
877
+ const client = createXApiClient(readAuthToken(c.env));
878
+ const res = await client.getPost(c.args.id);
879
+ const post = res.data;
880
+ const text = c.options.verbose ? post.text : truncateText(post.text);
881
+ return c.ok(
882
+ {
883
+ id: post.id,
884
+ text,
885
+ author_id: post.author_id,
886
+ created_at: post.created_at ? relativeTime(post.created_at) : void 0,
887
+ likes: post.public_metrics?.like_count,
888
+ retweets: post.public_metrics?.retweet_count,
889
+ replies: post.public_metrics?.reply_count
890
+ },
891
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
892
+ cta: {
893
+ description: "Explore this post:",
894
+ commands: [
895
+ {
896
+ command: "posts likes",
897
+ args: { id: c.args.id },
898
+ description: "See who liked this post"
899
+ },
900
+ {
901
+ command: "posts retweets",
902
+ args: { id: c.args.id },
903
+ description: "See who retweeted this post"
904
+ }
905
+ ]
906
+ }
907
+ }
908
+ );
909
+ }
910
+ });
911
+ posts.command("search", {
912
+ description: "Search recent posts.",
913
+ args: z4.object({
914
+ query: z4.string().describe("Search query")
915
+ }),
916
+ options: z4.object({
917
+ maxResults: z4.number().int().min(10).max(100).default(10).describe("Maximum results to return (10\u2013100)"),
918
+ sort: z4.enum(["recency", "relevancy"]).default("recency").describe("Sort order"),
919
+ verbose: z4.boolean().optional().describe("Show full text without truncation")
920
+ }),
921
+ alias: { maxResults: "n" },
922
+ env: xApiReadEnv,
923
+ output: z4.object({
924
+ posts: z4.array(
925
+ z4.object({
926
+ id: z4.string(),
927
+ text: z4.string(),
928
+ created_at: z4.string().optional(),
929
+ likes: z4.number().optional(),
930
+ retweets: z4.number().optional()
931
+ })
932
+ ),
933
+ count: z4.number()
934
+ }),
935
+ examples: [
936
+ { args: { query: "TypeScript" }, description: "Search for TypeScript posts" },
937
+ {
938
+ args: { query: "AI" },
939
+ options: { sort: "relevancy", maxResults: 20 },
940
+ description: "Search by relevance"
941
+ }
942
+ ],
943
+ async run(c) {
944
+ const client = createXApiClient(readAuthToken(c.env));
945
+ const res = await client.searchPosts(c.args.query, c.options.maxResults, c.options.sort);
946
+ const items = (res.data ?? []).map((p) => ({
947
+ id: p.id,
948
+ text: c.options.verbose ? p.text : truncateText(p.text),
949
+ created_at: p.created_at ? relativeTime(p.created_at) : void 0,
950
+ likes: p.public_metrics?.like_count,
951
+ retweets: p.public_metrics?.retweet_count
952
+ }));
953
+ const firstId = items[0]?.id;
954
+ return c.ok(
955
+ { posts: items, count: items.length },
956
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
957
+ cta: firstId ? {
958
+ description: "Next steps:",
959
+ commands: [
960
+ {
961
+ command: "posts get",
962
+ args: { id: firstId },
963
+ description: "View top result in detail"
964
+ }
965
+ ]
966
+ } : void 0
967
+ }
968
+ );
969
+ }
970
+ });
971
+ posts.command("create", {
972
+ description: "Create a new post.",
973
+ options: z4.object({
974
+ text: z4.string().describe("Post text"),
975
+ replyTo: z4.string().optional().describe("Reply to post ID"),
976
+ quote: z4.string().optional().describe("Quote post ID")
977
+ }),
978
+ env: xApiWriteEnv,
979
+ output: z4.object({
980
+ id: z4.string(),
981
+ text: z4.string()
982
+ }),
983
+ examples: [
984
+ { options: { text: "Hello world!" }, description: "Post a simple message" },
985
+ { options: { text: "Great point!", replyTo: "1234567890" }, description: "Reply to a post" }
986
+ ],
987
+ async run(c) {
988
+ try {
989
+ const client = createXApiClient(writeAuthToken(c.env));
990
+ const res = await client.createPost(c.options.text, c.options.replyTo, c.options.quote);
991
+ return c.ok(
992
+ res.data,
993
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
994
+ cta: {
995
+ description: "View your post:",
996
+ commands: [
997
+ {
998
+ command: "posts get",
999
+ args: { id: res.data.id },
1000
+ description: "See the created post"
1001
+ }
1002
+ ]
1003
+ }
1004
+ }
1005
+ );
1006
+ } catch (error) {
1007
+ const authError = toWriteAuthError("posts create", error);
1008
+ if (authError) return c.error(authError);
1009
+ throw error;
1010
+ }
1011
+ }
1012
+ });
1013
+ posts.command("delete", {
1014
+ description: "Delete a post by ID.",
1015
+ args: z4.object({
1016
+ id: z4.string().describe("Post ID to delete")
1017
+ }),
1018
+ env: xApiWriteEnv,
1019
+ output: z4.object({
1020
+ deleted: z4.boolean(),
1021
+ id: z4.string()
1022
+ }),
1023
+ examples: [{ args: { id: "1234567890" }, description: "Delete a post" }],
1024
+ async run(c) {
1025
+ try {
1026
+ const client = createXApiClient(writeAuthToken(c.env));
1027
+ const res = await client.deletePost(c.args.id);
1028
+ return c.ok({ deleted: res.data.deleted, id: c.args.id });
1029
+ } catch (error) {
1030
+ const authError = toWriteAuthError("posts delete", error);
1031
+ if (authError) return c.error(authError);
1032
+ throw error;
1033
+ }
1034
+ }
1035
+ });
1036
+ posts.command("like", {
1037
+ description: "Like a post by ID.",
1038
+ args: z4.object({
1039
+ id: z4.string().describe("Post ID to like")
1040
+ }),
1041
+ env: xApiWriteEnv,
1042
+ output: z4.object({
1043
+ liked: z4.boolean(),
1044
+ id: z4.string()
1045
+ }),
1046
+ examples: [{ args: { id: "1234567890" }, description: "Like a post" }],
1047
+ async run(c) {
1048
+ try {
1049
+ const client = createXApiClient(writeAuthToken(c.env));
1050
+ const me = await client.getMe();
1051
+ const res = await client.likePost(me.data.id, c.args.id);
1052
+ return c.ok({ liked: res.data.liked, id: c.args.id });
1053
+ } catch (error) {
1054
+ const authError = toWriteAuthError("posts like", error);
1055
+ if (authError) return c.error(authError);
1056
+ throw error;
1057
+ }
1058
+ }
1059
+ });
1060
+ posts.command("unlike", {
1061
+ description: "Unlike a post by ID.",
1062
+ args: z4.object({
1063
+ id: z4.string().describe("Post ID to unlike")
1064
+ }),
1065
+ env: xApiWriteEnv,
1066
+ output: z4.object({
1067
+ liked: z4.boolean(),
1068
+ id: z4.string()
1069
+ }),
1070
+ examples: [{ args: { id: "1234567890" }, description: "Unlike a post" }],
1071
+ async run(c) {
1072
+ try {
1073
+ const client = createXApiClient(writeAuthToken(c.env));
1074
+ const me = await client.getMe();
1075
+ const res = await client.unlikePost(me.data.id, c.args.id);
1076
+ return c.ok({ liked: res.data.liked, id: c.args.id });
1077
+ } catch (error) {
1078
+ const authError = toWriteAuthError("posts unlike", error);
1079
+ if (authError) return c.error(authError);
1080
+ throw error;
1081
+ }
1082
+ }
1083
+ });
1084
+ posts.command("bookmark", {
1085
+ description: "Bookmark a post by ID.",
1086
+ args: z4.object({
1087
+ id: z4.string().describe("Post ID to bookmark")
1088
+ }),
1089
+ env: xApiWriteEnv,
1090
+ output: z4.object({
1091
+ bookmarked: z4.boolean(),
1092
+ id: z4.string()
1093
+ }),
1094
+ examples: [{ args: { id: "1234567890" }, description: "Bookmark a post" }],
1095
+ async run(c) {
1096
+ try {
1097
+ const client = createXApiClient(writeAuthToken(c.env));
1098
+ const me = await client.getMe();
1099
+ const res = await client.bookmarkPost(me.data.id, c.args.id);
1100
+ return c.ok({ bookmarked: res.data.bookmarked, id: c.args.id });
1101
+ } catch (error) {
1102
+ const authError = toWriteAuthError("posts bookmark", error);
1103
+ if (authError) return c.error(authError);
1104
+ throw error;
1105
+ }
1106
+ }
1107
+ });
1108
+ posts.command("unbookmark", {
1109
+ description: "Remove bookmark from a post by ID.",
1110
+ args: z4.object({
1111
+ id: z4.string().describe("Post ID to unbookmark")
1112
+ }),
1113
+ env: xApiWriteEnv,
1114
+ output: z4.object({
1115
+ bookmarked: z4.boolean(),
1116
+ id: z4.string()
1117
+ }),
1118
+ examples: [{ args: { id: "1234567890" }, description: "Remove a bookmark from a post" }],
1119
+ async run(c) {
1120
+ try {
1121
+ const client = createXApiClient(writeAuthToken(c.env));
1122
+ const me = await client.getMe();
1123
+ const res = await client.unbookmarkPost(me.data.id, c.args.id);
1124
+ return c.ok({ bookmarked: res.data.bookmarked, id: c.args.id });
1125
+ } catch (error) {
1126
+ const authError = toWriteAuthError("posts unbookmark", error);
1127
+ if (authError) return c.error(authError);
1128
+ throw error;
1129
+ }
1130
+ }
1131
+ });
1132
+ posts.command("retweet", {
1133
+ description: "Retweet a post by ID.",
1134
+ args: z4.object({
1135
+ id: z4.string().describe("Post ID to retweet")
1136
+ }),
1137
+ env: xApiWriteEnv,
1138
+ output: z4.object({
1139
+ retweeted: z4.boolean(),
1140
+ id: z4.string()
1141
+ }),
1142
+ examples: [{ args: { id: "1234567890" }, description: "Retweet a post" }],
1143
+ async run(c) {
1144
+ try {
1145
+ const client = createXApiClient(writeAuthToken(c.env));
1146
+ const me = await client.getMe();
1147
+ const res = await client.retweetPost(me.data.id, c.args.id);
1148
+ return c.ok({ retweeted: res.data.retweeted, id: c.args.id });
1149
+ } catch (error) {
1150
+ const authError = toWriteAuthError("posts retweet", error);
1151
+ if (authError) return c.error(authError);
1152
+ throw error;
1153
+ }
1154
+ }
1155
+ });
1156
+ posts.command("likes", {
1157
+ description: "List users who liked a post.",
1158
+ args: z4.object({
1159
+ id: z4.string().describe("Post ID")
1160
+ }),
1161
+ options: z4.object({
1162
+ maxResults: z4.number().default(100).describe("Maximum users to return")
1163
+ }),
1164
+ alias: { maxResults: "n" },
1165
+ env: xApiReadEnv,
1166
+ output: z4.object({
1167
+ users: z4.array(
1168
+ z4.object({
1169
+ id: z4.string(),
1170
+ name: z4.string(),
1171
+ username: z4.string(),
1172
+ followers: z4.number().optional()
1173
+ })
1174
+ ),
1175
+ count: z4.number()
1176
+ }),
1177
+ examples: [{ args: { id: "1234567890" }, description: "See who liked a post" }],
1178
+ async run(c) {
1179
+ const client = createXApiClient(readAuthToken(c.env));
1180
+ const allUsers = await collectPaged(
1181
+ (limit, cursor) => client.getPostLikes(c.args.id, limit, cursor),
1182
+ (user) => ({
1183
+ id: user.id,
1184
+ name: user.name,
1185
+ username: user.username,
1186
+ followers: user.public_metrics?.followers_count
1187
+ }),
1188
+ c.options.maxResults
1189
+ );
1190
+ return c.ok(
1191
+ { users: allUsers, count: allUsers.length },
1192
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
1193
+ cta: {
1194
+ description: "Next steps:",
1195
+ commands: allUsers.slice(0, 1).map((u) => ({
1196
+ command: "users get",
1197
+ args: { username: u.username },
1198
+ description: `View profile of @${u.username}`
1199
+ }))
1200
+ }
1201
+ }
1202
+ );
1203
+ }
1204
+ });
1205
+ posts.command("retweets", {
1206
+ description: "List users who retweeted a post.",
1207
+ args: z4.object({
1208
+ id: z4.string().describe("Post ID")
1209
+ }),
1210
+ options: z4.object({
1211
+ maxResults: z4.number().default(100).describe("Maximum users to return")
1212
+ }),
1213
+ alias: { maxResults: "n" },
1214
+ env: xApiReadEnv,
1215
+ output: z4.object({
1216
+ users: z4.array(
1217
+ z4.object({
1218
+ id: z4.string(),
1219
+ name: z4.string(),
1220
+ username: z4.string(),
1221
+ followers: z4.number().optional()
1222
+ })
1223
+ ),
1224
+ count: z4.number()
1225
+ }),
1226
+ examples: [{ args: { id: "1234567890" }, description: "See who retweeted a post" }],
1227
+ async run(c) {
1228
+ const client = createXApiClient(readAuthToken(c.env));
1229
+ const allUsers = await collectPaged(
1230
+ (limit, cursor) => client.getPostRetweets(c.args.id, limit, cursor),
1231
+ (user) => ({
1232
+ id: user.id,
1233
+ name: user.name,
1234
+ username: user.username,
1235
+ followers: user.public_metrics?.followers_count
1236
+ }),
1237
+ c.options.maxResults
1238
+ );
1239
+ return c.ok({ users: allUsers, count: allUsers.length });
1240
+ }
1241
+ });
1242
+
1243
+ // src/commands/timeline.ts
1244
+ import { Cli as Cli4, z as z5 } from "incur";
1245
+ var timeline = Cli4.create("timeline", {
1246
+ description: "View your X timeline."
1247
+ });
1248
+ timeline.command("home", {
1249
+ description: "View your home timeline.",
1250
+ options: z5.object({
1251
+ maxResults: z5.number().default(25).describe("Maximum posts to return (5\u2013100)"),
1252
+ sinceId: z5.string().optional().describe("Only return posts newer than this post ID"),
1253
+ verbose: z5.boolean().optional().describe("Show full text without truncation")
1254
+ }),
1255
+ alias: { maxResults: "n" },
1256
+ env: xApiReadEnv,
1257
+ output: z5.object({
1258
+ posts: z5.array(
1259
+ z5.object({
1260
+ id: z5.string(),
1261
+ text: z5.string(),
1262
+ author_id: z5.string().optional(),
1263
+ created_at: z5.string().optional(),
1264
+ likes: z5.number().optional(),
1265
+ retweets: z5.number().optional()
1266
+ })
1267
+ ),
1268
+ count: z5.number()
1269
+ }),
1270
+ examples: [
1271
+ { description: "View your home timeline" },
1272
+ { options: { maxResults: 50 }, description: "View 50 posts" },
1273
+ {
1274
+ options: { sinceId: "1900123456789012345" },
1275
+ description: "Resume from last-seen post ID"
1276
+ }
1277
+ ],
1278
+ async run(c) {
1279
+ const client = createXApiClient(readAuthToken(c.env));
1280
+ const meRes = await client.getMe();
1281
+ const userId = meRes.data.id;
1282
+ const allPosts = await collectPaged(
1283
+ (limit, cursor) => client.getHomeTimeline(userId, limit, cursor, c.options.sinceId),
1284
+ (post) => ({
1285
+ id: post.id,
1286
+ text: c.options.verbose ? post.text : truncateText(post.text),
1287
+ author_id: post.author_id,
1288
+ created_at: post.created_at ? relativeTime(post.created_at) : void 0,
1289
+ likes: post.public_metrics?.like_count,
1290
+ retweets: post.public_metrics?.retweet_count
1291
+ }),
1292
+ c.options.maxResults
1293
+ );
1294
+ const firstId = allPosts[0]?.id;
1295
+ return c.ok(
1296
+ { posts: allPosts, count: allPosts.length },
1297
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
1298
+ cta: firstId ? {
1299
+ description: "Next steps:",
1300
+ commands: [
1301
+ {
1302
+ command: "posts get",
1303
+ args: { id: firstId },
1304
+ description: "View top post in detail"
1305
+ }
1306
+ ]
1307
+ } : void 0
1308
+ }
1309
+ );
1310
+ }
1311
+ });
1312
+ timeline.command("mentions", {
1313
+ description: "View your recent mentions.",
1314
+ options: z5.object({
1315
+ maxResults: z5.number().default(25).describe("Maximum mentions to return"),
1316
+ sinceId: z5.string().optional().describe("Only return mentions newer than this post ID"),
1317
+ verbose: z5.boolean().optional().describe("Show full text without truncation")
1318
+ }),
1319
+ alias: { maxResults: "n" },
1320
+ env: xApiReadEnv,
1321
+ output: z5.object({
1322
+ posts: z5.array(
1323
+ z5.object({
1324
+ id: z5.string(),
1325
+ text: z5.string(),
1326
+ author_id: z5.string().optional(),
1327
+ created_at: z5.string().optional()
1328
+ })
1329
+ ),
1330
+ count: z5.number()
1331
+ }),
1332
+ examples: [
1333
+ { description: "View your recent mentions" },
1334
+ {
1335
+ options: { sinceId: "1900123456789012345" },
1336
+ description: "Resume mentions from last-seen post ID"
1337
+ }
1338
+ ],
1339
+ async run(c) {
1340
+ const client = createXApiClient(readAuthToken(c.env));
1341
+ const meRes = await client.getMe();
1342
+ const userId = meRes.data.id;
1343
+ const allPosts = await collectPaged(
1344
+ (limit, cursor) => client.getMentionsTimeline(userId, limit, cursor, c.options.sinceId),
1345
+ (post) => ({
1346
+ id: post.id,
1347
+ text: c.options.verbose ? post.text : truncateText(post.text),
1348
+ author_id: post.author_id,
1349
+ created_at: post.created_at ? relativeTime(post.created_at) : void 0
1350
+ }),
1351
+ c.options.maxResults
1352
+ );
1353
+ return c.ok({ posts: allPosts, count: allPosts.length });
1354
+ }
1355
+ });
1356
+
1357
+ // src/commands/trends.ts
1358
+ import { Cli as Cli5, z as z6 } from "incur";
1359
+ var trends = Cli5.create("trends", {
1360
+ description: "Explore trending topics on X."
1361
+ });
1362
+ trends.command("places", {
1363
+ description: "List places where trending topics are available.",
1364
+ env: xApiReadEnv,
1365
+ output: z6.object({
1366
+ places: z6.array(
1367
+ z6.object({
1368
+ woeid: z6.number(),
1369
+ name: z6.string(),
1370
+ country: z6.string()
1371
+ })
1372
+ ),
1373
+ count: z6.number()
1374
+ }),
1375
+ examples: [{ description: "List all trending places" }],
1376
+ async run(c) {
1377
+ const client = createXApiClient(readAuthToken(c.env));
1378
+ const res = await client.getTrendingPlaces();
1379
+ const places = res.data ?? [];
1380
+ const first = places[0];
1381
+ return c.ok(
1382
+ { places, count: places.length },
1383
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
1384
+ cta: first ? {
1385
+ description: "Next steps:",
1386
+ commands: [
1387
+ {
1388
+ command: "trends location",
1389
+ args: { woeid: first.woeid },
1390
+ description: `View trends for ${first.name}`
1391
+ }
1392
+ ]
1393
+ } : void 0
1394
+ }
1395
+ );
1396
+ }
1397
+ });
1398
+ trends.command("location", {
1399
+ description: "Get trending topics for a specific location (WOEID).",
1400
+ args: z6.object({
1401
+ woeid: z6.string().describe("Where On Earth ID (from trends places)")
1402
+ }),
1403
+ env: xApiReadEnv,
1404
+ output: z6.object({
1405
+ trends: z6.array(
1406
+ z6.object({
1407
+ name: z6.string(),
1408
+ query: z6.string(),
1409
+ tweet_volume: z6.number().optional()
1410
+ })
1411
+ ),
1412
+ count: z6.number()
1413
+ }),
1414
+ examples: [
1415
+ { args: { woeid: "1" }, description: "Get worldwide trends" },
1416
+ { args: { woeid: "2459115" }, description: "Get trends for New York" }
1417
+ ],
1418
+ async run(c) {
1419
+ const client = createXApiClient(readAuthToken(c.env));
1420
+ const res = await client.getTrendsByLocation(Number(c.args.woeid));
1421
+ const trendItems = res.data ?? [];
1422
+ const firstTrend = trendItems[0];
1423
+ return c.ok(
1424
+ { trends: trendItems, count: trendItems.length },
1425
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
1426
+ cta: firstTrend ? {
1427
+ description: "Next steps:",
1428
+ commands: [
1429
+ {
1430
+ command: "posts search",
1431
+ args: { query: firstTrend.query },
1432
+ description: `Search posts about "${firstTrend.name}"`
1433
+ }
1434
+ ]
1435
+ } : void 0
1436
+ }
1437
+ );
1438
+ }
1439
+ });
1440
+
1441
+ // src/commands/users.ts
1442
+ import { readFileSync } from "fs";
1443
+ import { Cli as Cli6, z as z7 } from "incur";
1444
+ var users = Cli6.create("users", {
1445
+ description: "Look up X users."
1446
+ });
1447
+ async function resolveUser(client, usernameOrId) {
1448
+ if (/^\d+$/.test(usernameOrId)) {
1449
+ return client.getUserById(usernameOrId);
1450
+ }
1451
+ return client.getUserByUsername(usernameOrId.replace(/^@/, ""));
1452
+ }
1453
+ function formatUserProfile(user, verbose) {
1454
+ return {
1455
+ id: user.id,
1456
+ name: user.name,
1457
+ username: user.username,
1458
+ description: user.description ? verbose ? user.description : truncateText(user.description) : void 0,
1459
+ followers: user.public_metrics?.followers_count,
1460
+ following: user.public_metrics?.following_count,
1461
+ tweets: user.public_metrics?.tweet_count,
1462
+ joined: user.created_at ? relativeTime(user.created_at) : void 0
1463
+ };
1464
+ }
1465
+ function readSeenIds(filePath) {
1466
+ const fileContents = readFileSync(filePath, "utf8");
1467
+ return new Set(
1468
+ fileContents.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
1469
+ );
1470
+ }
1471
+ var followersOptionsSchema = z7.object({
1472
+ maxResults: z7.number().default(100).describe("Maximum followers to return"),
1473
+ seenIdsFile: z7.string().optional().describe(
1474
+ "Path to newline-delimited follower IDs used as a baseline for client-side diffing"
1475
+ ),
1476
+ newOnly: z7.boolean().default(false).describe(
1477
+ "Return only followers not found in --seen-ids-file (client-side baseline diff; not API-native since_id)"
1478
+ )
1479
+ }).refine((options) => !options.newOnly || Boolean(options.seenIdsFile), {
1480
+ path: ["seenIdsFile"],
1481
+ message: "--seen-ids-file is required when --new-only is set"
1482
+ });
1483
+ users.command("me", {
1484
+ description: "Get the authenticated user profile and metrics.",
1485
+ options: z7.object({
1486
+ verbose: z7.boolean().optional().describe("Show full bio without truncation")
1487
+ }),
1488
+ env: xApiReadEnv,
1489
+ output: z7.object({
1490
+ id: z7.string(),
1491
+ name: z7.string(),
1492
+ username: z7.string(),
1493
+ description: z7.string().optional(),
1494
+ followers: z7.number().optional(),
1495
+ following: z7.number().optional(),
1496
+ tweets: z7.number().optional(),
1497
+ joined: z7.string().optional()
1498
+ }),
1499
+ examples: [{ description: "Get your authenticated profile", options: {} }],
1500
+ async run(c) {
1501
+ const client = createXApiClient(readAuthToken(c.env));
1502
+ const res = await client.getMe();
1503
+ const user = res.data;
1504
+ return c.ok(
1505
+ formatUserProfile(user, c.options.verbose),
1506
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
1507
+ cta: {
1508
+ description: "Explore your account:",
1509
+ commands: [
1510
+ {
1511
+ command: "users followers",
1512
+ args: { username: user.username },
1513
+ description: "View your followers"
1514
+ },
1515
+ {
1516
+ command: "users posts",
1517
+ args: { username: user.username },
1518
+ description: "View your recent posts"
1519
+ }
1520
+ ]
1521
+ }
1522
+ }
1523
+ );
1524
+ }
1525
+ });
1526
+ users.command("get", {
1527
+ description: "Get a user by username or ID.",
1528
+ args: z7.object({
1529
+ username: z7.string().describe("Username (with or without @) or user ID")
1530
+ }),
1531
+ options: z7.object({
1532
+ verbose: z7.boolean().optional().describe("Show full bio without truncation")
1533
+ }),
1534
+ env: xApiReadEnv,
1535
+ output: z7.object({
1536
+ id: z7.string(),
1537
+ name: z7.string(),
1538
+ username: z7.string(),
1539
+ description: z7.string().optional(),
1540
+ followers: z7.number().optional(),
1541
+ following: z7.number().optional(),
1542
+ tweets: z7.number().optional(),
1543
+ joined: z7.string().optional()
1544
+ }),
1545
+ examples: [
1546
+ { args: { username: "jack" }, description: "Get a user by username" },
1547
+ { args: { username: "12345" }, description: "Get a user by ID" }
1548
+ ],
1549
+ async run(c) {
1550
+ const client = createXApiClient(readAuthToken(c.env));
1551
+ const res = await resolveUser(client, c.args.username);
1552
+ const user = res.data;
1553
+ return c.ok(
1554
+ formatUserProfile(user, c.options.verbose),
1555
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
1556
+ cta: {
1557
+ description: "Explore this user:",
1558
+ commands: [
1559
+ {
1560
+ command: "users posts",
1561
+ args: { username: user.username },
1562
+ description: "View their posts"
1563
+ },
1564
+ {
1565
+ command: "users followers",
1566
+ args: { username: user.username },
1567
+ description: "View their followers"
1568
+ }
1569
+ ]
1570
+ }
1571
+ }
1572
+ );
1573
+ }
1574
+ });
1575
+ users.command("follow", {
1576
+ description: "Follow a user by username or ID.",
1577
+ args: z7.object({
1578
+ username: z7.string().describe("Username (with or without @) or user ID")
1579
+ }),
1580
+ env: xApiWriteEnv,
1581
+ output: z7.object({
1582
+ id: z7.string(),
1583
+ username: z7.string(),
1584
+ following: z7.boolean(),
1585
+ pending_follow: z7.boolean().optional()
1586
+ }),
1587
+ examples: [{ args: { username: "jack" }, description: "Follow @jack" }],
1588
+ async run(c) {
1589
+ try {
1590
+ const client = createXApiClient(writeAuthToken(c.env));
1591
+ const me = await client.getMe();
1592
+ const targetRes = await resolveUser(client, c.args.username);
1593
+ const target = targetRes.data;
1594
+ const res = await client.followUser(me.data.id, target.id);
1595
+ return c.ok({
1596
+ id: target.id,
1597
+ username: target.username,
1598
+ following: res.data.following,
1599
+ pending_follow: res.data.pending_follow
1600
+ });
1601
+ } catch (error) {
1602
+ const authError = toWriteAuthError("users follow", error);
1603
+ if (authError) return c.error(authError);
1604
+ throw error;
1605
+ }
1606
+ }
1607
+ });
1608
+ users.command("unfollow", {
1609
+ description: "Unfollow a user by username or ID.",
1610
+ args: z7.object({
1611
+ username: z7.string().describe("Username (with or without @) or user ID")
1612
+ }),
1613
+ env: xApiWriteEnv,
1614
+ output: z7.object({
1615
+ id: z7.string(),
1616
+ username: z7.string(),
1617
+ following: z7.boolean()
1618
+ }),
1619
+ examples: [{ args: { username: "jack" }, description: "Unfollow @jack" }],
1620
+ async run(c) {
1621
+ try {
1622
+ const client = createXApiClient(writeAuthToken(c.env));
1623
+ const me = await client.getMe();
1624
+ const targetRes = await resolveUser(client, c.args.username);
1625
+ const target = targetRes.data;
1626
+ const res = await client.unfollowUser(me.data.id, target.id);
1627
+ return c.ok({
1628
+ id: target.id,
1629
+ username: target.username,
1630
+ following: res.data.following
1631
+ });
1632
+ } catch (error) {
1633
+ const authError = toWriteAuthError("users unfollow", error);
1634
+ if (authError) return c.error(authError);
1635
+ throw error;
1636
+ }
1637
+ }
1638
+ });
1639
+ users.command("block", {
1640
+ description: "Block a user by username or ID.",
1641
+ args: z7.object({
1642
+ username: z7.string().describe("Username (with or without @) or user ID")
1643
+ }),
1644
+ env: xApiWriteEnv,
1645
+ output: z7.object({
1646
+ id: z7.string(),
1647
+ username: z7.string(),
1648
+ blocking: z7.boolean()
1649
+ }),
1650
+ examples: [{ args: { username: "jack" }, description: "Block @jack" }],
1651
+ async run(c) {
1652
+ try {
1653
+ const client = createXApiClient(writeAuthToken(c.env));
1654
+ const me = await client.getMe();
1655
+ const targetRes = await resolveUser(client, c.args.username);
1656
+ const target = targetRes.data;
1657
+ const res = await client.blockUser(me.data.id, target.id);
1658
+ return c.ok({
1659
+ id: target.id,
1660
+ username: target.username,
1661
+ blocking: res.data.blocking
1662
+ });
1663
+ } catch (error) {
1664
+ const authError = toWriteAuthError("users block", error);
1665
+ if (authError) return c.error(authError);
1666
+ throw error;
1667
+ }
1668
+ }
1669
+ });
1670
+ users.command("unblock", {
1671
+ description: "Unblock a user by username or ID.",
1672
+ args: z7.object({
1673
+ username: z7.string().describe("Username (with or without @) or user ID")
1674
+ }),
1675
+ env: xApiWriteEnv,
1676
+ output: z7.object({
1677
+ id: z7.string(),
1678
+ username: z7.string(),
1679
+ blocking: z7.boolean()
1680
+ }),
1681
+ examples: [{ args: { username: "jack" }, description: "Unblock @jack" }],
1682
+ async run(c) {
1683
+ try {
1684
+ const client = createXApiClient(writeAuthToken(c.env));
1685
+ const me = await client.getMe();
1686
+ const targetRes = await resolveUser(client, c.args.username);
1687
+ const target = targetRes.data;
1688
+ const res = await client.unblockUser(me.data.id, target.id);
1689
+ return c.ok({
1690
+ id: target.id,
1691
+ username: target.username,
1692
+ blocking: res.data.blocking
1693
+ });
1694
+ } catch (error) {
1695
+ const authError = toWriteAuthError("users unblock", error);
1696
+ if (authError) return c.error(authError);
1697
+ throw error;
1698
+ }
1699
+ }
1700
+ });
1701
+ users.command("mute", {
1702
+ description: "Mute a user by username or ID.",
1703
+ args: z7.object({
1704
+ username: z7.string().describe("Username (with or without @) or user ID")
1705
+ }),
1706
+ env: xApiWriteEnv,
1707
+ output: z7.object({
1708
+ id: z7.string(),
1709
+ username: z7.string(),
1710
+ muting: z7.boolean()
1711
+ }),
1712
+ examples: [{ args: { username: "jack" }, description: "Mute @jack" }],
1713
+ async run(c) {
1714
+ try {
1715
+ const client = createXApiClient(writeAuthToken(c.env));
1716
+ const me = await client.getMe();
1717
+ const targetRes = await resolveUser(client, c.args.username);
1718
+ const target = targetRes.data;
1719
+ const res = await client.muteUser(me.data.id, target.id);
1720
+ return c.ok({
1721
+ id: target.id,
1722
+ username: target.username,
1723
+ muting: res.data.muting
1724
+ });
1725
+ } catch (error) {
1726
+ const authError = toWriteAuthError("users mute", error);
1727
+ if (authError) return c.error(authError);
1728
+ throw error;
1729
+ }
1730
+ }
1731
+ });
1732
+ users.command("unmute", {
1733
+ description: "Unmute a user by username or ID.",
1734
+ args: z7.object({
1735
+ username: z7.string().describe("Username (with or without @) or user ID")
1736
+ }),
1737
+ env: xApiWriteEnv,
1738
+ output: z7.object({
1739
+ id: z7.string(),
1740
+ username: z7.string(),
1741
+ muting: z7.boolean()
1742
+ }),
1743
+ examples: [{ args: { username: "jack" }, description: "Unmute @jack" }],
1744
+ async run(c) {
1745
+ try {
1746
+ const client = createXApiClient(writeAuthToken(c.env));
1747
+ const me = await client.getMe();
1748
+ const targetRes = await resolveUser(client, c.args.username);
1749
+ const target = targetRes.data;
1750
+ const res = await client.unmuteUser(me.data.id, target.id);
1751
+ return c.ok({
1752
+ id: target.id,
1753
+ username: target.username,
1754
+ muting: res.data.muting
1755
+ });
1756
+ } catch (error) {
1757
+ const authError = toWriteAuthError("users unmute", error);
1758
+ if (authError) return c.error(authError);
1759
+ throw error;
1760
+ }
1761
+ }
1762
+ });
1763
+ users.command("followers", {
1764
+ description: "List followers of a user. Supports optional client-side baseline diffing for new follower detection.",
1765
+ args: z7.object({
1766
+ username: z7.string().describe("Username or user ID")
1767
+ }),
1768
+ options: followersOptionsSchema,
1769
+ alias: { maxResults: "n", seenIdsFile: "s" },
1770
+ env: xApiReadEnv,
1771
+ output: z7.object({
1772
+ users: z7.array(
1773
+ z7.object({
1774
+ id: z7.string(),
1775
+ name: z7.string(),
1776
+ username: z7.string(),
1777
+ followers: z7.number().optional()
1778
+ })
1779
+ ),
1780
+ count: z7.number()
1781
+ }),
1782
+ examples: [
1783
+ { args: { username: "jack" }, description: "List followers of jack" },
1784
+ {
1785
+ args: { username: "jack" },
1786
+ options: { seenIdsFile: "./seen-followers.txt", newOnly: true },
1787
+ description: "Show only followers not in your baseline file (client-side diffing; the X API does not support since_id here)"
1788
+ }
1789
+ ],
1790
+ async run(c) {
1791
+ const client = createXApiClient(readAuthToken(c.env));
1792
+ const userRes = await resolveUser(client, c.args.username);
1793
+ const userId = userRes.data.id;
1794
+ const allUsers = await collectPaged(
1795
+ (limit, cursor) => client.getUserFollowers(userId, limit, cursor),
1796
+ (user) => ({
1797
+ id: user.id,
1798
+ name: user.name,
1799
+ username: user.username,
1800
+ followers: user.public_metrics?.followers_count
1801
+ }),
1802
+ c.options.maxResults,
1803
+ 1e3
1804
+ );
1805
+ if (!c.options.newOnly) {
1806
+ return c.ok({ users: allUsers, count: allUsers.length });
1807
+ }
1808
+ if (!c.options.seenIdsFile) {
1809
+ throw new Error("--seen-ids-file is required when --new-only is set");
1810
+ }
1811
+ const seenIds = readSeenIds(c.options.seenIdsFile);
1812
+ const newUsers = allUsers.filter((user) => !seenIds.has(user.id));
1813
+ return c.ok({ users: newUsers, count: newUsers.length });
1814
+ }
1815
+ });
1816
+ users.command("following", {
1817
+ description: "List accounts a user is following.",
1818
+ args: z7.object({
1819
+ username: z7.string().describe("Username or user ID")
1820
+ }),
1821
+ options: z7.object({
1822
+ maxResults: z7.number().default(100).describe("Maximum accounts to return")
1823
+ }),
1824
+ alias: { maxResults: "n" },
1825
+ env: xApiReadEnv,
1826
+ output: z7.object({
1827
+ users: z7.array(
1828
+ z7.object({
1829
+ id: z7.string(),
1830
+ name: z7.string(),
1831
+ username: z7.string(),
1832
+ followers: z7.number().optional()
1833
+ })
1834
+ ),
1835
+ count: z7.number()
1836
+ }),
1837
+ examples: [{ args: { username: "jack" }, description: "List accounts jack follows" }],
1838
+ async run(c) {
1839
+ const client = createXApiClient(readAuthToken(c.env));
1840
+ const userRes = await resolveUser(client, c.args.username);
1841
+ const userId = userRes.data.id;
1842
+ const allUsers = await collectPaged(
1843
+ (limit, cursor) => client.getUserFollowing(userId, limit, cursor),
1844
+ (user) => ({
1845
+ id: user.id,
1846
+ name: user.name,
1847
+ username: user.username,
1848
+ followers: user.public_metrics?.followers_count
1849
+ }),
1850
+ c.options.maxResults,
1851
+ 1e3
1852
+ );
1853
+ return c.ok({ users: allUsers, count: allUsers.length });
1854
+ }
1855
+ });
1856
+ users.command("posts", {
1857
+ description: "List a user's posts.",
1858
+ args: z7.object({
1859
+ username: z7.string().describe("Username or user ID")
1860
+ }),
1861
+ options: z7.object({
1862
+ maxResults: z7.number().default(10).describe("Maximum posts to return"),
1863
+ verbose: z7.boolean().optional().describe("Show full text without truncation")
1864
+ }),
1865
+ alias: { maxResults: "n" },
1866
+ env: xApiReadEnv,
1867
+ output: z7.object({
1868
+ posts: z7.array(
1869
+ z7.object({
1870
+ id: z7.string(),
1871
+ text: z7.string(),
1872
+ created_at: z7.string().optional(),
1873
+ likes: z7.number().optional(),
1874
+ retweets: z7.number().optional()
1875
+ })
1876
+ ),
1877
+ count: z7.number()
1878
+ }),
1879
+ examples: [{ args: { username: "jack" }, description: "Get jack's recent posts" }],
1880
+ async run(c) {
1881
+ const client = createXApiClient(readAuthToken(c.env));
1882
+ const userRes = await resolveUser(client, c.args.username);
1883
+ const userId = userRes.data.id;
1884
+ const allPosts = await collectPaged(
1885
+ (limit, cursor) => client.getUserPosts(userId, limit, cursor),
1886
+ (post) => ({
1887
+ id: post.id,
1888
+ text: c.options.verbose ? post.text : truncateText(post.text),
1889
+ created_at: post.created_at ? relativeTime(post.created_at) : void 0,
1890
+ likes: post.public_metrics?.like_count,
1891
+ retweets: post.public_metrics?.retweet_count
1892
+ }),
1893
+ c.options.maxResults
1894
+ );
1895
+ const firstId = allPosts[0]?.id;
1896
+ return c.ok(
1897
+ { posts: allPosts, count: allPosts.length },
1898
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
1899
+ cta: firstId ? {
1900
+ description: "Next steps:",
1901
+ commands: [
1902
+ {
1903
+ command: "posts get",
1904
+ args: { id: firstId },
1905
+ description: "View top post in detail"
1906
+ }
1907
+ ]
1908
+ } : void 0
1909
+ }
1910
+ );
1911
+ }
1912
+ });
1913
+ users.command("mentions", {
1914
+ description: "List recent mentions of a user.",
1915
+ args: z7.object({
1916
+ username: z7.string().describe("Username or user ID")
1917
+ }),
1918
+ options: z7.object({
1919
+ maxResults: z7.number().default(10).describe("Maximum mentions to return"),
1920
+ verbose: z7.boolean().optional().describe("Show full text")
1921
+ }),
1922
+ alias: { maxResults: "n" },
1923
+ env: xApiReadEnv,
1924
+ output: z7.object({
1925
+ posts: z7.array(
1926
+ z7.object({
1927
+ id: z7.string(),
1928
+ text: z7.string(),
1929
+ created_at: z7.string().optional()
1930
+ })
1931
+ ),
1932
+ count: z7.number()
1933
+ }),
1934
+ examples: [{ args: { username: "jack" }, description: "Get mentions of jack" }],
1935
+ async run(c) {
1936
+ const client = createXApiClient(readAuthToken(c.env));
1937
+ const userRes = await resolveUser(client, c.args.username);
1938
+ const userId = userRes.data.id;
1939
+ const allPosts = await collectPaged(
1940
+ (limit, cursor) => client.getUserMentions(userId, limit, cursor),
1941
+ (post) => ({
1942
+ id: post.id,
1943
+ text: c.options.verbose ? post.text : truncateText(post.text),
1944
+ created_at: post.created_at ? relativeTime(post.created_at) : void 0
1945
+ }),
1946
+ c.options.maxResults
1947
+ );
1948
+ return c.ok({ posts: allPosts, count: allPosts.length });
1949
+ }
1950
+ });
1951
+ users.command("search", {
1952
+ description: "Search for users by keyword.",
1953
+ args: z7.object({
1954
+ query: z7.string().describe("Search query")
1955
+ }),
1956
+ env: xApiReadEnv,
1957
+ output: z7.object({
1958
+ users: z7.array(
1959
+ z7.object({
1960
+ id: z7.string(),
1961
+ name: z7.string(),
1962
+ username: z7.string(),
1963
+ followers: z7.number().optional()
1964
+ })
1965
+ ),
1966
+ count: z7.number()
1967
+ }),
1968
+ examples: [{ args: { query: "TypeScript" }, description: "Search for users about TypeScript" }],
1969
+ async run(c) {
1970
+ const client = createXApiClient(readAuthToken(c.env));
1971
+ const res = await client.searchUsers(c.args.query);
1972
+ const items = (res.data ?? []).map((u) => ({
1973
+ id: u.id,
1974
+ name: u.name,
1975
+ username: u.username,
1976
+ followers: u.public_metrics?.followers_count
1977
+ }));
1978
+ const first = items[0];
1979
+ return c.ok(
1980
+ { users: items, count: items.length },
1981
+ c.format === "json" || c.format === "jsonl" ? void 0 : {
1982
+ cta: first ? {
1983
+ description: "Next steps:",
1984
+ commands: [
1985
+ {
1986
+ command: "users get",
1987
+ args: { username: first.username },
1988
+ description: `View @${first.username}'s profile`
1989
+ }
1990
+ ]
1991
+ } : void 0
1992
+ }
1993
+ );
1994
+ }
1995
+ });
1996
+
1997
+ // src/cli.ts
1998
+ var __dirname = dirname(fileURLToPath(import.meta.url));
1999
+ var pkg = JSON.parse(readFileSync2(resolve(__dirname, "../package.json"), "utf8"));
2000
+ var cli = Cli7.create("xapi", {
2001
+ version: pkg.version,
2002
+ description: "X (Twitter) API CLI for spectra-the-bot."
2003
+ });
2004
+ var WRITE_OPERATIONS = /* @__PURE__ */ new Set([
2005
+ "posts create",
2006
+ "posts delete",
2007
+ "posts like",
2008
+ "posts unlike",
2009
+ "posts bookmark",
2010
+ "posts unbookmark",
2011
+ "posts retweet",
2012
+ "users follow",
2013
+ "users unfollow",
2014
+ "users block",
2015
+ "users unblock",
2016
+ "users mute",
2017
+ "users unmute",
2018
+ "lists create",
2019
+ "lists delete",
2020
+ "lists add-member",
2021
+ "lists remove-member",
2022
+ "dm send"
2023
+ ]);
2024
+ cli.use(async ({ command, error }, next) => {
2025
+ try {
2026
+ return await next();
2027
+ } catch (cause) {
2028
+ const authScope = WRITE_OPERATIONS.has(command) ? "write" : "read";
2029
+ const mapped = toXApiCommandError(command, cause, authScope);
2030
+ if (mapped) {
2031
+ return error(mapped);
2032
+ }
2033
+ throw cause;
2034
+ }
2035
+ });
2036
+ cli.command(posts);
2037
+ cli.command(users);
2038
+ cli.command(timeline);
2039
+ cli.command(lists);
2040
+ cli.command(trends);
2041
+ cli.command(dm);
2042
+ var isMain = (() => {
2043
+ const entrypoint = process.argv[1];
2044
+ if (!entrypoint) {
2045
+ return false;
2046
+ }
2047
+ try {
2048
+ return realpathSync(entrypoint) === realpathSync(fileURLToPath(import.meta.url));
2049
+ } catch {
2050
+ return false;
2051
+ }
2052
+ })();
2053
+ if (isMain) {
2054
+ cli.serve();
2055
+ }
2056
+ export {
2057
+ cli
2058
+ };