@spectratools/xapi-cli 0.1.1 → 0.1.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 (3) hide show
  1. package/README.md +55 -50
  2. package/dist/cli.js +381 -293
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,74 +1,79 @@
1
- # @spectra-the-bot/xapi-cli
1
+ # @spectratools/xapi-cli
2
2
 
3
- X (Twitter) API v2 CLI for spectra-the-bot, built with [incur](https://github.com/wevm/incur).
3
+ X (Twitter) API v2 CLI for post, user, list, trend, timeline, and DM workflows.
4
4
 
5
- ## Setup
5
+ ## Install
6
6
 
7
7
  ```bash
8
- export X_BEARER_TOKEN=your_bearer_token_here
9
- npx @spectra-the-bot/xapi-cli --help
8
+ pnpm add -g @spectratools/xapi-cli
10
9
  ```
11
10
 
12
- ## Commands
13
-
14
- ### Posts
11
+ ## LLM / Agent Discovery
15
12
 
16
13
  ```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
- ```
14
+ # Emit machine-readable command metadata
15
+ xapi-cli --llms
24
16
 
25
- ### Users
17
+ # Register as a reusable local skill for agent runtimes
18
+ xapi-cli skills add
26
19
 
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>
20
+ # Register as an MCP server entry
21
+ xapi-cli mcp add
34
22
  ```
35
23
 
36
- ### Timeline
24
+ ## Configuration
37
25
 
38
26
  ```bash
39
- xapi timeline home [-n 25]
40
- xapi timeline mentions [-n 25]
41
- ```
42
-
43
- ### Lists
27
+ # Read-only endpoints (search, profiles, trends, list reads)
28
+ export X_BEARER_TOKEN=your_app_bearer_token
44
29
 
45
- ```bash
46
- xapi lists get <id>
47
- xapi lists members <id>
48
- xapi lists posts <id> [-n 25]
30
+ # Write endpoints (create/delete post, send DM)
31
+ # OAuth 2.0 user context token with the required write scopes
32
+ export X_ACCESS_TOKEN=your_oauth2_user_access_token
49
33
  ```
50
34
 
51
- ### Trends
35
+ Auth behavior:
36
+ - Reads use `X_ACCESS_TOKEN` when present, otherwise fall back to `X_BEARER_TOKEN`.
37
+ - Writes require `X_ACCESS_TOKEN` and will return a structured auth error if missing/insufficient.
52
38
 
53
- ```bash
54
- xapi trends places
55
- xapi trends location <woeid>
56
- ```
39
+ ## Command Group Intent Summary
40
+
41
+ - `posts` Read/search/create/delete posts and inspect social engagement
42
+ - `users` — Profile lookup, social graph traversal, and user timelines
43
+ - `timeline` — Home timeline and mention stream monitoring
44
+ - `lists` — List discovery, member inspection, and list feed reads
45
+ - `trends` — Trend place discovery and per-location trend fetch
46
+ - `dm` — Conversation listing and outbound direct messages
57
47
 
58
- ### DMs
48
+ ## Agent-Oriented Examples
59
49
 
60
50
  ```bash
61
- xapi dm conversations [-n 20]
62
- xapi dm send <participant-id> --text "Hello!"
51
+ # 1) Trend-to-content pipeline
52
+ xapi-cli trends places --format json
53
+ xapi-cli trends location 1 --format json
54
+ xapi-cli posts search "AI agents" --sort relevancy --max-results 20 --format json
55
+
56
+ # 2) User intelligence pass
57
+ xapi-cli users get jack --format json
58
+ xapi-cli users posts jack --max-results 20 --format json
59
+ xapi-cli users followers jack --max-results 100 --format json
60
+
61
+ # 3) Moderation helper flow
62
+ xapi-cli posts get 1234567890 --format json
63
+ xapi-cli posts likes 1234567890 --max-results 100 --format json
64
+ xapi-cli posts retweets 1234567890 --max-results 100 --format json
65
+
66
+ # 4) Timeline monitor
67
+ xapi-cli timeline home --max-results 50 --format json
68
+ xapi-cli timeline mentions --max-results 50 --format json
69
+
70
+ # 5) DM assistant loop
71
+ xapi-cli dm conversations --max-results 20 --format json
72
+ xapi-cli dm send 12345 --text "hello from agent" --format json
63
73
  ```
64
74
 
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
75
+ ## Notes
73
76
 
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.
77
+ - All commands support JSON output with `--format json`.
78
+ - `X_BEARER_TOKEN` is for read-only app auth.
79
+ - `X_ACCESS_TOKEN` is required for write actions (`posts create`, `posts delete`, `dm send`).
package/dist/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import { realpathSync } from "fs";
4
5
  import { fileURLToPath } from "url";
5
6
  import { Cli as Cli7 } from "incur";
6
7
 
7
8
  // src/commands/dm.ts
8
- import { apiKeyAuth } from "@spectratools/cli-shared";
9
- import { Cli, z } from "incur";
9
+ import { Cli, z as z2 } from "incur";
10
10
 
11
11
  // src/api.ts
12
12
  import { createHttpClient, withRetry } from "@spectratools/cli-shared";
@@ -214,6 +214,66 @@ function truncateText(text, max = 100) {
214
214
  return `${text.slice(0, max - 3)}...`;
215
215
  }
216
216
 
217
+ // src/auth.ts
218
+ import { HttpError } from "@spectratools/cli-shared";
219
+ import { z } from "incur";
220
+ var bearerTokenSchema = z.string().describe("X app-only bearer token (read-only endpoints)");
221
+ var accessTokenSchema = z.string().describe("X OAuth 2.0 user access token (required for write endpoints)");
222
+ var xApiReadEnv = z.object({
223
+ X_BEARER_TOKEN: bearerTokenSchema.optional(),
224
+ X_ACCESS_TOKEN: accessTokenSchema.optional()
225
+ }).refine((env) => Boolean(env.X_ACCESS_TOKEN || env.X_BEARER_TOKEN), {
226
+ message: "Set X_ACCESS_TOKEN or X_BEARER_TOKEN to authenticate X API requests."
227
+ });
228
+ var xApiWriteEnv = z.object({
229
+ X_ACCESS_TOKEN: accessTokenSchema,
230
+ X_BEARER_TOKEN: bearerTokenSchema.optional()
231
+ });
232
+ function readAuthToken(env) {
233
+ if (env.X_ACCESS_TOKEN) {
234
+ return env.X_ACCESS_TOKEN;
235
+ }
236
+ return env.X_BEARER_TOKEN;
237
+ }
238
+ function writeAuthToken(env) {
239
+ return env.X_ACCESS_TOKEN;
240
+ }
241
+ function parseXApiErrorDetail(body) {
242
+ try {
243
+ const parsed = JSON.parse(body);
244
+ if (typeof parsed !== "object" || parsed === null) {
245
+ return void 0;
246
+ }
247
+ const candidate = parsed;
248
+ if (typeof candidate.detail === "string" && candidate.detail.trim()) return candidate.detail;
249
+ if (typeof candidate.title === "string" && candidate.title.trim()) return candidate.title;
250
+ const firstError = candidate.errors?.[0];
251
+ if (typeof firstError?.message === "string" && firstError.message.trim())
252
+ return firstError.message;
253
+ if (typeof firstError?.detail === "string" && firstError.detail.trim())
254
+ return firstError.detail;
255
+ } catch {
256
+ }
257
+ return void 0;
258
+ }
259
+ function toWriteAuthError(operation, error) {
260
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
261
+ const detail = parseXApiErrorDetail(error.body);
262
+ return {
263
+ code: "INSUFFICIENT_WRITE_AUTH",
264
+ message: [
265
+ "Insufficient auth for write endpoint:",
266
+ `- operation: ${operation}`,
267
+ `- status: ${error.status} ${error.statusText}`,
268
+ "- required auth: X_ACCESS_TOKEN (OAuth 2.0 user token with write scopes)",
269
+ "- note: app-only X_BEARER_TOKEN cannot perform write actions",
270
+ ...detail ? [`- x_api_detail: ${detail}`] : []
271
+ ].join("\n")
272
+ };
273
+ }
274
+ return void 0;
275
+ }
276
+
217
277
  // src/collect-paged.ts
218
278
  import { paginateCursor } from "@spectratools/cli-shared";
219
279
  async function collectPaged(fetchFn, mapFn, maxResults, pageSize = 100) {
@@ -242,23 +302,23 @@ var dm = Cli.create("dm", {
242
302
  });
243
303
  dm.command("conversations", {
244
304
  description: "List your DM conversations.",
245
- options: z.object({
246
- maxResults: z.number().default(20).describe("Maximum conversations to return")
305
+ options: z2.object({
306
+ maxResults: z2.number().default(20).describe("Maximum conversations to return")
247
307
  }),
248
308
  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())
309
+ env: xApiReadEnv,
310
+ output: z2.object({
311
+ conversations: z2.array(
312
+ z2.object({
313
+ dm_conversation_id: z2.string(),
314
+ participant_ids: z2.array(z2.string())
254
315
  })
255
316
  ),
256
- count: z.number()
317
+ count: z2.number()
257
318
  }),
258
319
  examples: [{ description: "List your DM conversations" }],
259
320
  async run(c) {
260
- const { apiKey } = apiKeyAuth("X_BEARER_TOKEN");
261
- const client = createXApiClient(apiKey);
321
+ const client = createXApiClient(readAuthToken(c.env));
262
322
  const meRes = await client.getMe();
263
323
  const userId = meRes.data.id;
264
324
  const allConvos = await collectPaged(
@@ -290,15 +350,16 @@ dm.command("conversations", {
290
350
  });
291
351
  dm.command("send", {
292
352
  description: "Send a direct message to a user.",
293
- args: z.object({
294
- participantId: z.string().describe("User ID to send message to")
353
+ args: z2.object({
354
+ participantId: z2.string().describe("User ID to send message to")
295
355
  }),
296
- options: z.object({
297
- text: z.string().describe("Message text")
356
+ options: z2.object({
357
+ text: z2.string().describe("Message text")
298
358
  }),
299
- output: z.object({
300
- dm_conversation_id: z.string(),
301
- dm_event_id: z.string()
359
+ env: xApiWriteEnv,
360
+ output: z2.object({
361
+ dm_conversation_id: z2.string(),
362
+ dm_event_id: z2.string()
302
363
  }),
303
364
  examples: [
304
365
  {
@@ -308,35 +369,39 @@ dm.command("send", {
308
369
  }
309
370
  ],
310
371
  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);
372
+ try {
373
+ const client = createXApiClient(writeAuthToken(c.env));
374
+ const res = await client.sendDm(c.args.participantId, c.options.text);
375
+ return c.ok(res.data);
376
+ } catch (error) {
377
+ const authError = toWriteAuthError("dm send", error);
378
+ if (authError) return c.error(authError);
379
+ throw error;
380
+ }
315
381
  }
316
382
  });
317
383
 
318
384
  // src/commands/lists.ts
319
- import { apiKeyAuth as apiKeyAuth2 } from "@spectratools/cli-shared";
320
- import { Cli as Cli2, z as z2 } from "incur";
385
+ import { Cli as Cli2, z as z3 } from "incur";
321
386
  var lists = Cli2.create("lists", {
322
387
  description: "Manage and browse X lists."
323
388
  });
324
389
  lists.command("get", {
325
390
  description: "Get a list by ID.",
326
- args: z2.object({
327
- id: z2.string().describe("List ID")
391
+ args: z3.object({
392
+ id: z3.string().describe("List ID")
328
393
  }),
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()
394
+ env: xApiReadEnv,
395
+ output: z3.object({
396
+ id: z3.string(),
397
+ name: z3.string(),
398
+ description: z3.string().optional(),
399
+ owner_id: z3.string().optional(),
400
+ member_count: z3.number().optional()
335
401
  }),
336
402
  examples: [{ args: { id: "1234567890" }, description: "Get list details" }],
337
403
  async run(c) {
338
- const { apiKey } = apiKeyAuth2("X_BEARER_TOKEN");
339
- const client = createXApiClient(apiKey);
404
+ const client = createXApiClient(readAuthToken(c.env));
340
405
  const res = await client.getList(c.args.id);
341
406
  const list = res.data;
342
407
  return c.ok(
@@ -361,28 +426,28 @@ lists.command("get", {
361
426
  });
362
427
  lists.command("members", {
363
428
  description: "List members of an X list.",
364
- args: z2.object({
365
- id: z2.string().describe("List ID")
429
+ args: z3.object({
430
+ id: z3.string().describe("List ID")
366
431
  }),
367
- options: z2.object({
368
- maxResults: z2.number().default(100).describe("Maximum members to return")
432
+ options: z3.object({
433
+ maxResults: z3.number().default(100).describe("Maximum members to return")
369
434
  }),
370
435
  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()
436
+ env: xApiReadEnv,
437
+ output: z3.object({
438
+ users: z3.array(
439
+ z3.object({
440
+ id: z3.string(),
441
+ name: z3.string(),
442
+ username: z3.string(),
443
+ followers: z3.number().optional()
378
444
  })
379
445
  ),
380
- count: z2.number()
446
+ count: z3.number()
381
447
  }),
382
448
  examples: [{ args: { id: "1234567890" }, description: "List all members" }],
383
449
  async run(c) {
384
- const { apiKey } = apiKeyAuth2("X_BEARER_TOKEN");
385
- const client = createXApiClient(apiKey);
450
+ const client = createXApiClient(readAuthToken(c.env));
386
451
  const allUsers = await collectPaged(
387
452
  (limit, cursor) => client.getListMembers(c.args.id, limit, cursor),
388
453
  (user) => ({
@@ -398,30 +463,30 @@ lists.command("members", {
398
463
  });
399
464
  lists.command("posts", {
400
465
  description: "Get posts from an X list.",
401
- args: z2.object({
402
- id: z2.string().describe("List ID")
466
+ args: z3.object({
467
+ id: z3.string().describe("List ID")
403
468
  }),
404
- options: z2.object({
405
- maxResults: z2.number().default(25).describe("Maximum posts to return"),
406
- verbose: z2.boolean().optional().describe("Show full text")
469
+ options: z3.object({
470
+ maxResults: z3.number().default(25).describe("Maximum posts to return"),
471
+ verbose: z3.boolean().optional().describe("Show full text")
407
472
  }),
408
473
  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()
474
+ env: xApiReadEnv,
475
+ output: z3.object({
476
+ posts: z3.array(
477
+ z3.object({
478
+ id: z3.string(),
479
+ text: z3.string(),
480
+ author_id: z3.string().optional(),
481
+ created_at: z3.string().optional(),
482
+ likes: z3.number().optional()
417
483
  })
418
484
  ),
419
- count: z2.number()
485
+ count: z3.number()
420
486
  }),
421
487
  examples: [{ args: { id: "1234567890" }, description: "Get posts from a list" }],
422
488
  async run(c) {
423
- const { apiKey } = apiKeyAuth2("X_BEARER_TOKEN");
424
- const client = createXApiClient(apiKey);
489
+ const client = createXApiClient(readAuthToken(c.env));
425
490
  const allPosts = await collectPaged(
426
491
  (limit, cursor) => client.getListPosts(c.args.id, limit, cursor),
427
492
  (post) => ({
@@ -453,32 +518,31 @@ lists.command("posts", {
453
518
  });
454
519
 
455
520
  // src/commands/posts.ts
456
- import { apiKeyAuth as apiKeyAuth3 } from "@spectratools/cli-shared";
457
- import { Cli as Cli3, z as z3 } from "incur";
521
+ import { Cli as Cli3, z as z4 } from "incur";
458
522
  var posts = Cli3.create("posts", {
459
523
  description: "Manage and search X posts."
460
524
  });
461
525
  posts.command("get", {
462
526
  description: "Get a post by ID.",
463
- args: z3.object({
464
- id: z3.string().describe("Post ID")
527
+ args: z4.object({
528
+ id: z4.string().describe("Post ID")
465
529
  }),
466
- options: z3.object({
467
- verbose: z3.boolean().optional().describe("Show full text without truncation")
530
+ options: z4.object({
531
+ verbose: z4.boolean().optional().describe("Show full text without truncation")
468
532
  }),
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()
533
+ env: xApiReadEnv,
534
+ output: z4.object({
535
+ id: z4.string(),
536
+ text: z4.string(),
537
+ author_id: z4.string().optional(),
538
+ created_at: z4.string().optional(),
539
+ likes: z4.number().optional(),
540
+ retweets: z4.number().optional(),
541
+ replies: z4.number().optional()
477
542
  }),
478
543
  examples: [{ args: { id: "1234567890" }, description: "Get a post by ID" }],
479
544
  async run(c) {
480
- const { apiKey } = apiKeyAuth3("X_BEARER_TOKEN");
481
- const client = createXApiClient(apiKey);
545
+ const client = createXApiClient(readAuthToken(c.env));
482
546
  const res = await client.getPost(c.args.id);
483
547
  const post = res.data;
484
548
  const text = c.options.verbose ? post.text : truncateText(post.text);
@@ -514,26 +578,27 @@ posts.command("get", {
514
578
  });
515
579
  posts.command("search", {
516
580
  description: "Search recent posts.",
517
- args: z3.object({
518
- query: z3.string().describe("Search query")
581
+ args: z4.object({
582
+ query: z4.string().describe("Search query")
519
583
  }),
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")
584
+ options: z4.object({
585
+ maxResults: z4.number().default(10).describe("Maximum results to return (10\u2013100)"),
586
+ sort: z4.enum(["recency", "relevancy"]).default("recency").describe("Sort order"),
587
+ verbose: z4.boolean().optional().describe("Show full text without truncation")
524
588
  }),
525
589
  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()
590
+ env: xApiReadEnv,
591
+ output: z4.object({
592
+ posts: z4.array(
593
+ z4.object({
594
+ id: z4.string(),
595
+ text: z4.string(),
596
+ created_at: z4.string().optional(),
597
+ likes: z4.number().optional(),
598
+ retweets: z4.number().optional()
534
599
  })
535
600
  ),
536
- count: z3.number()
601
+ count: z4.number()
537
602
  }),
538
603
  examples: [
539
604
  { args: { query: "TypeScript" }, description: "Search for TypeScript posts" },
@@ -544,8 +609,7 @@ posts.command("search", {
544
609
  }
545
610
  ],
546
611
  async run(c) {
547
- const { apiKey } = apiKeyAuth3("X_BEARER_TOKEN");
548
- const client = createXApiClient(apiKey);
612
+ const client = createXApiClient(readAuthToken(c.env));
549
613
  const res = await client.searchPosts(c.args.query, c.options.maxResults, c.options.sort);
550
614
  const items = (res.data ?? []).map((p) => ({
551
615
  id: p.id,
@@ -574,74 +638,90 @@ posts.command("search", {
574
638
  });
575
639
  posts.command("create", {
576
640
  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")
641
+ options: z4.object({
642
+ text: z4.string().describe("Post text"),
643
+ replyTo: z4.string().optional().describe("Reply to post ID"),
644
+ quote: z4.string().optional().describe("Quote post ID")
581
645
  }),
582
- output: z3.object({
583
- id: z3.string(),
584
- text: z3.string()
646
+ env: xApiWriteEnv,
647
+ output: z4.object({
648
+ id: z4.string(),
649
+ text: z4.string()
585
650
  }),
586
651
  examples: [
587
652
  { options: { text: "Hello world!" }, description: "Post a simple message" },
588
653
  { options: { text: "Great point!", replyTo: "1234567890" }, description: "Reply to a post" }
589
654
  ],
590
655
  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
- });
656
+ try {
657
+ const client = createXApiClient(writeAuthToken(c.env));
658
+ const res = await client.createPost(c.options.text, c.options.replyTo, c.options.quote);
659
+ return c.ok(res.data, {
660
+ cta: {
661
+ description: "View your post:",
662
+ commands: [
663
+ {
664
+ command: "posts get",
665
+ args: { id: res.data.id },
666
+ description: "See the created post"
667
+ }
668
+ ]
669
+ }
670
+ });
671
+ } catch (error) {
672
+ const authError = toWriteAuthError("posts create", error);
673
+ if (authError) return c.error(authError);
674
+ throw error;
675
+ }
602
676
  }
603
677
  });
604
678
  posts.command("delete", {
605
679
  description: "Delete a post by ID.",
606
- args: z3.object({
607
- id: z3.string().describe("Post ID to delete")
680
+ args: z4.object({
681
+ id: z4.string().describe("Post ID to delete")
608
682
  }),
609
- output: z3.object({
610
- deleted: z3.boolean(),
611
- id: z3.string()
683
+ env: xApiWriteEnv,
684
+ output: z4.object({
685
+ deleted: z4.boolean(),
686
+ id: z4.string()
612
687
  }),
613
688
  examples: [{ args: { id: "1234567890" }, description: "Delete a post" }],
614
689
  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 });
690
+ try {
691
+ const client = createXApiClient(writeAuthToken(c.env));
692
+ const res = await client.deletePost(c.args.id);
693
+ return c.ok({ deleted: res.data.deleted, id: c.args.id });
694
+ } catch (error) {
695
+ const authError = toWriteAuthError("posts delete", error);
696
+ if (authError) return c.error(authError);
697
+ throw error;
698
+ }
619
699
  }
620
700
  });
621
701
  posts.command("likes", {
622
702
  description: "List users who liked a post.",
623
- args: z3.object({
624
- id: z3.string().describe("Post ID")
703
+ args: z4.object({
704
+ id: z4.string().describe("Post ID")
625
705
  }),
626
- options: z3.object({
627
- maxResults: z3.number().default(100).describe("Maximum users to return")
706
+ options: z4.object({
707
+ maxResults: z4.number().default(100).describe("Maximum users to return")
628
708
  }),
629
709
  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()
710
+ env: xApiReadEnv,
711
+ output: z4.object({
712
+ users: z4.array(
713
+ z4.object({
714
+ id: z4.string(),
715
+ name: z4.string(),
716
+ username: z4.string(),
717
+ followers: z4.number().optional()
637
718
  })
638
719
  ),
639
- count: z3.number()
720
+ count: z4.number()
640
721
  }),
641
722
  examples: [{ args: { id: "1234567890" }, description: "See who liked a post" }],
642
723
  async run(c) {
643
- const { apiKey } = apiKeyAuth3("X_BEARER_TOKEN");
644
- const client = createXApiClient(apiKey);
724
+ const client = createXApiClient(readAuthToken(c.env));
645
725
  const allUsers = await collectPaged(
646
726
  (limit, cursor) => client.getPostLikes(c.args.id, limit, cursor),
647
727
  (user) => ({
@@ -669,28 +749,28 @@ posts.command("likes", {
669
749
  });
670
750
  posts.command("retweets", {
671
751
  description: "List users who retweeted a post.",
672
- args: z3.object({
673
- id: z3.string().describe("Post ID")
752
+ args: z4.object({
753
+ id: z4.string().describe("Post ID")
674
754
  }),
675
- options: z3.object({
676
- maxResults: z3.number().default(100).describe("Maximum users to return")
755
+ options: z4.object({
756
+ maxResults: z4.number().default(100).describe("Maximum users to return")
677
757
  }),
678
758
  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()
759
+ env: xApiReadEnv,
760
+ output: z4.object({
761
+ users: z4.array(
762
+ z4.object({
763
+ id: z4.string(),
764
+ name: z4.string(),
765
+ username: z4.string(),
766
+ followers: z4.number().optional()
686
767
  })
687
768
  ),
688
- count: z3.number()
769
+ count: z4.number()
689
770
  }),
690
771
  examples: [{ args: { id: "1234567890" }, description: "See who retweeted a post" }],
691
772
  async run(c) {
692
- const { apiKey } = apiKeyAuth3("X_BEARER_TOKEN");
693
- const client = createXApiClient(apiKey);
773
+ const client = createXApiClient(readAuthToken(c.env));
694
774
  const allUsers = await collectPaged(
695
775
  (limit, cursor) => client.getPostRetweets(c.args.id, limit, cursor),
696
776
  (user) => ({
@@ -706,38 +786,37 @@ posts.command("retweets", {
706
786
  });
707
787
 
708
788
  // src/commands/timeline.ts
709
- import { apiKeyAuth as apiKeyAuth4 } from "@spectratools/cli-shared";
710
- import { Cli as Cli4, z as z4 } from "incur";
789
+ import { Cli as Cli4, z as z5 } from "incur";
711
790
  var timeline = Cli4.create("timeline", {
712
791
  description: "View your X timeline."
713
792
  });
714
793
  timeline.command("home", {
715
794
  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")
795
+ options: z5.object({
796
+ maxResults: z5.number().default(25).describe("Maximum posts to return (5\u2013100)"),
797
+ verbose: z5.boolean().optional().describe("Show full text without truncation")
719
798
  }),
720
799
  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()
800
+ env: xApiReadEnv,
801
+ output: z5.object({
802
+ posts: z5.array(
803
+ z5.object({
804
+ id: z5.string(),
805
+ text: z5.string(),
806
+ author_id: z5.string().optional(),
807
+ created_at: z5.string().optional(),
808
+ likes: z5.number().optional(),
809
+ retweets: z5.number().optional()
730
810
  })
731
811
  ),
732
- count: z4.number()
812
+ count: z5.number()
733
813
  }),
734
814
  examples: [
735
815
  { description: "View your home timeline" },
736
816
  { options: { maxResults: 50 }, description: "View 50 posts" }
737
817
  ],
738
818
  async run(c) {
739
- const { apiKey } = apiKeyAuth4("X_BEARER_TOKEN");
740
- const client = createXApiClient(apiKey);
819
+ const client = createXApiClient(readAuthToken(c.env));
741
820
  const meRes = await client.getMe();
742
821
  const userId = meRes.data.id;
743
822
  const allPosts = await collectPaged(
@@ -772,26 +851,26 @@ timeline.command("home", {
772
851
  });
773
852
  timeline.command("mentions", {
774
853
  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")
854
+ options: z5.object({
855
+ maxResults: z5.number().default(25).describe("Maximum mentions to return"),
856
+ verbose: z5.boolean().optional().describe("Show full text without truncation")
778
857
  }),
779
858
  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()
859
+ env: xApiReadEnv,
860
+ output: z5.object({
861
+ posts: z5.array(
862
+ z5.object({
863
+ id: z5.string(),
864
+ text: z5.string(),
865
+ author_id: z5.string().optional(),
866
+ created_at: z5.string().optional()
787
867
  })
788
868
  ),
789
- count: z4.number()
869
+ count: z5.number()
790
870
  }),
791
871
  examples: [{ description: "View your recent mentions" }],
792
872
  async run(c) {
793
- const { apiKey } = apiKeyAuth4("X_BEARER_TOKEN");
794
- const client = createXApiClient(apiKey);
873
+ const client = createXApiClient(readAuthToken(c.env));
795
874
  const meRes = await client.getMe();
796
875
  const userId = meRes.data.id;
797
876
  const allPosts = await collectPaged(
@@ -809,27 +888,26 @@ timeline.command("mentions", {
809
888
  });
810
889
 
811
890
  // src/commands/trends.ts
812
- import { apiKeyAuth as apiKeyAuth5 } from "@spectratools/cli-shared";
813
- import { Cli as Cli5, z as z5 } from "incur";
891
+ import { Cli as Cli5, z as z6 } from "incur";
814
892
  var trends = Cli5.create("trends", {
815
893
  description: "Explore trending topics on X."
816
894
  });
817
895
  trends.command("places", {
818
896
  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()
897
+ env: xApiReadEnv,
898
+ output: z6.object({
899
+ places: z6.array(
900
+ z6.object({
901
+ woeid: z6.number(),
902
+ name: z6.string(),
903
+ country: z6.string()
825
904
  })
826
905
  ),
827
- count: z5.number()
906
+ count: z6.number()
828
907
  }),
829
908
  examples: [{ description: "List all trending places" }],
830
909
  async run(c) {
831
- const { apiKey } = apiKeyAuth5("X_BEARER_TOKEN");
832
- const client = createXApiClient(apiKey);
910
+ const client = createXApiClient(readAuthToken(c.env));
833
911
  const res = await client.getTrendingPlaces();
834
912
  const places = res.data ?? [];
835
913
  const first = places[0];
@@ -852,26 +930,26 @@ trends.command("places", {
852
930
  });
853
931
  trends.command("location", {
854
932
  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)")
933
+ args: z6.object({
934
+ woeid: z6.string().describe("Where On Earth ID (from trends places)")
857
935
  }),
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()
936
+ env: xApiReadEnv,
937
+ output: z6.object({
938
+ trends: z6.array(
939
+ z6.object({
940
+ name: z6.string(),
941
+ query: z6.string(),
942
+ tweet_volume: z6.number().optional()
864
943
  })
865
944
  ),
866
- count: z5.number()
945
+ count: z6.number()
867
946
  }),
868
947
  examples: [
869
948
  { args: { woeid: "1" }, description: "Get worldwide trends" },
870
949
  { args: { woeid: "2459115" }, description: "Get trends for New York" }
871
950
  ],
872
951
  async run(c) {
873
- const { apiKey } = apiKeyAuth5("X_BEARER_TOKEN");
874
- const client = createXApiClient(apiKey);
952
+ const client = createXApiClient(readAuthToken(c.env));
875
953
  const res = await client.getTrendsByLocation(Number(c.args.woeid));
876
954
  const trendItems = res.data ?? [];
877
955
  const firstTrend = trendItems[0];
@@ -894,8 +972,7 @@ trends.command("location", {
894
972
  });
895
973
 
896
974
  // src/commands/users.ts
897
- import { apiKeyAuth as apiKeyAuth6 } from "@spectratools/cli-shared";
898
- import { Cli as Cli6, z as z6 } from "incur";
975
+ import { Cli as Cli6, z as z7 } from "incur";
899
976
  var users = Cli6.create("users", {
900
977
  description: "Look up X users."
901
978
  });
@@ -907,29 +984,29 @@ async function resolveUser(client, usernameOrId) {
907
984
  }
908
985
  users.command("get", {
909
986
  description: "Get a user by username or ID.",
910
- args: z6.object({
911
- username: z6.string().describe("Username (with or without @) or user ID")
987
+ args: z7.object({
988
+ username: z7.string().describe("Username (with or without @) or user ID")
912
989
  }),
913
- options: z6.object({
914
- verbose: z6.boolean().optional().describe("Show full bio without truncation")
990
+ options: z7.object({
991
+ verbose: z7.boolean().optional().describe("Show full bio without truncation")
915
992
  }),
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()
993
+ env: xApiReadEnv,
994
+ output: z7.object({
995
+ id: z7.string(),
996
+ name: z7.string(),
997
+ username: z7.string(),
998
+ description: z7.string().optional(),
999
+ followers: z7.number().optional(),
1000
+ following: z7.number().optional(),
1001
+ tweets: z7.number().optional(),
1002
+ joined: z7.string().optional()
925
1003
  }),
926
1004
  examples: [
927
1005
  { args: { username: "jack" }, description: "Get a user by username" },
928
1006
  { args: { username: "12345" }, description: "Get a user by ID" }
929
1007
  ],
930
1008
  async run(c) {
931
- const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
932
- const client = createXApiClient(apiKey);
1009
+ const client = createXApiClient(readAuthToken(c.env));
933
1010
  const res = await resolveUser(client, c.args.username);
934
1011
  const user = res.data;
935
1012
  return c.ok(
@@ -965,28 +1042,28 @@ users.command("get", {
965
1042
  });
966
1043
  users.command("followers", {
967
1044
  description: "List followers of a user.",
968
- args: z6.object({
969
- username: z6.string().describe("Username or user ID")
1045
+ args: z7.object({
1046
+ username: z7.string().describe("Username or user ID")
970
1047
  }),
971
- options: z6.object({
972
- maxResults: z6.number().default(100).describe("Maximum followers to return")
1048
+ options: z7.object({
1049
+ maxResults: z7.number().default(100).describe("Maximum followers to return")
973
1050
  }),
974
1051
  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()
1052
+ env: xApiReadEnv,
1053
+ output: z7.object({
1054
+ users: z7.array(
1055
+ z7.object({
1056
+ id: z7.string(),
1057
+ name: z7.string(),
1058
+ username: z7.string(),
1059
+ followers: z7.number().optional()
982
1060
  })
983
1061
  ),
984
- count: z6.number()
1062
+ count: z7.number()
985
1063
  }),
986
1064
  examples: [{ args: { username: "jack" }, description: "List followers of jack" }],
987
1065
  async run(c) {
988
- const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
989
- const client = createXApiClient(apiKey);
1066
+ const client = createXApiClient(readAuthToken(c.env));
990
1067
  const userRes = await resolveUser(client, c.args.username);
991
1068
  const userId = userRes.data.id;
992
1069
  const allUsers = await collectPaged(
@@ -1005,28 +1082,28 @@ users.command("followers", {
1005
1082
  });
1006
1083
  users.command("following", {
1007
1084
  description: "List accounts a user is following.",
1008
- args: z6.object({
1009
- username: z6.string().describe("Username or user ID")
1085
+ args: z7.object({
1086
+ username: z7.string().describe("Username or user ID")
1010
1087
  }),
1011
- options: z6.object({
1012
- maxResults: z6.number().default(100).describe("Maximum accounts to return")
1088
+ options: z7.object({
1089
+ maxResults: z7.number().default(100).describe("Maximum accounts to return")
1013
1090
  }),
1014
1091
  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()
1092
+ env: xApiReadEnv,
1093
+ output: z7.object({
1094
+ users: z7.array(
1095
+ z7.object({
1096
+ id: z7.string(),
1097
+ name: z7.string(),
1098
+ username: z7.string(),
1099
+ followers: z7.number().optional()
1022
1100
  })
1023
1101
  ),
1024
- count: z6.number()
1102
+ count: z7.number()
1025
1103
  }),
1026
1104
  examples: [{ args: { username: "jack" }, description: "List accounts jack follows" }],
1027
1105
  async run(c) {
1028
- const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
1029
- const client = createXApiClient(apiKey);
1106
+ const client = createXApiClient(readAuthToken(c.env));
1030
1107
  const userRes = await resolveUser(client, c.args.username);
1031
1108
  const userId = userRes.data.id;
1032
1109
  const allUsers = await collectPaged(
@@ -1045,30 +1122,30 @@ users.command("following", {
1045
1122
  });
1046
1123
  users.command("posts", {
1047
1124
  description: "List a user's posts.",
1048
- args: z6.object({
1049
- username: z6.string().describe("Username or user ID")
1125
+ args: z7.object({
1126
+ username: z7.string().describe("Username or user ID")
1050
1127
  }),
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")
1128
+ options: z7.object({
1129
+ maxResults: z7.number().default(10).describe("Maximum posts to return"),
1130
+ verbose: z7.boolean().optional().describe("Show full text without truncation")
1054
1131
  }),
1055
1132
  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()
1133
+ env: xApiReadEnv,
1134
+ output: z7.object({
1135
+ posts: z7.array(
1136
+ z7.object({
1137
+ id: z7.string(),
1138
+ text: z7.string(),
1139
+ created_at: z7.string().optional(),
1140
+ likes: z7.number().optional(),
1141
+ retweets: z7.number().optional()
1064
1142
  })
1065
1143
  ),
1066
- count: z6.number()
1144
+ count: z7.number()
1067
1145
  }),
1068
1146
  examples: [{ args: { username: "jack" }, description: "Get jack's recent posts" }],
1069
1147
  async run(c) {
1070
- const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
1071
- const client = createXApiClient(apiKey);
1148
+ const client = createXApiClient(readAuthToken(c.env));
1072
1149
  const userRes = await resolveUser(client, c.args.username);
1073
1150
  const userId = userRes.data.id;
1074
1151
  const allPosts = await collectPaged(
@@ -1102,28 +1179,28 @@ users.command("posts", {
1102
1179
  });
1103
1180
  users.command("mentions", {
1104
1181
  description: "List recent mentions of a user.",
1105
- args: z6.object({
1106
- username: z6.string().describe("Username or user ID")
1182
+ args: z7.object({
1183
+ username: z7.string().describe("Username or user ID")
1107
1184
  }),
1108
- options: z6.object({
1109
- maxResults: z6.number().default(10).describe("Maximum mentions to return"),
1110
- verbose: z6.boolean().optional().describe("Show full text")
1185
+ options: z7.object({
1186
+ maxResults: z7.number().default(10).describe("Maximum mentions to return"),
1187
+ verbose: z7.boolean().optional().describe("Show full text")
1111
1188
  }),
1112
1189
  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()
1190
+ env: xApiReadEnv,
1191
+ output: z7.object({
1192
+ posts: z7.array(
1193
+ z7.object({
1194
+ id: z7.string(),
1195
+ text: z7.string(),
1196
+ created_at: z7.string().optional()
1119
1197
  })
1120
1198
  ),
1121
- count: z6.number()
1199
+ count: z7.number()
1122
1200
  }),
1123
1201
  examples: [{ args: { username: "jack" }, description: "Get mentions of jack" }],
1124
1202
  async run(c) {
1125
- const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
1126
- const client = createXApiClient(apiKey);
1203
+ const client = createXApiClient(readAuthToken(c.env));
1127
1204
  const userRes = await resolveUser(client, c.args.username);
1128
1205
  const userId = userRes.data.id;
1129
1206
  const allPosts = await collectPaged(
@@ -1140,24 +1217,24 @@ users.command("mentions", {
1140
1217
  });
1141
1218
  users.command("search", {
1142
1219
  description: "Search for users by keyword.",
1143
- args: z6.object({
1144
- query: z6.string().describe("Search query")
1220
+ args: z7.object({
1221
+ query: z7.string().describe("Search query")
1145
1222
  }),
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()
1223
+ env: xApiReadEnv,
1224
+ output: z7.object({
1225
+ users: z7.array(
1226
+ z7.object({
1227
+ id: z7.string(),
1228
+ name: z7.string(),
1229
+ username: z7.string(),
1230
+ followers: z7.number().optional()
1153
1231
  })
1154
1232
  ),
1155
- count: z6.number()
1233
+ count: z7.number()
1156
1234
  }),
1157
1235
  examples: [{ args: { query: "TypeScript" }, description: "Search for users about TypeScript" }],
1158
1236
  async run(c) {
1159
- const { apiKey } = apiKeyAuth6("X_BEARER_TOKEN");
1160
- const client = createXApiClient(apiKey);
1237
+ const client = createXApiClient(readAuthToken(c.env));
1161
1238
  const res = await client.searchUsers(c.args.query);
1162
1239
  const items = (res.data ?? []).map((u) => ({
1163
1240
  id: u.id,
@@ -1194,7 +1271,18 @@ cli.command(timeline);
1194
1271
  cli.command(lists);
1195
1272
  cli.command(trends);
1196
1273
  cli.command(dm);
1197
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
1274
+ var isMain = (() => {
1275
+ const entrypoint = process.argv[1];
1276
+ if (!entrypoint) {
1277
+ return false;
1278
+ }
1279
+ try {
1280
+ return realpathSync(entrypoint) === realpathSync(fileURLToPath(import.meta.url));
1281
+ } catch {
1282
+ return false;
1283
+ }
1284
+ })();
1285
+ if (isMain) {
1198
1286
  cli.serve();
1199
1287
  }
1200
1288
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectratools/xapi-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "X (Twitter) API CLI for spectra-the-bot",
5
5
  "type": "module",
6
6
  "license": "MIT",