@harveyrandall/bsky-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1620 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/config.ts
7
+ import { readFile, writeFile, mkdir } from "fs/promises";
8
+ import { homedir } from "os";
9
+ import { join, dirname } from "path";
10
+ import { existsSync } from "fs";
11
+ import { readdirSync } from "fs";
12
+ function configDir() {
13
+ return join(homedir(), ".config");
14
+ }
15
+ function bskyDir() {
16
+ return join(configDir(), "bsky");
17
+ }
18
+ function configPath(profile) {
19
+ if (profile) {
20
+ return join(bskyDir(), `config-${profile}.json`);
21
+ }
22
+ return join(bskyDir(), "config.json");
23
+ }
24
+ function authPath(handle, prefix = "") {
25
+ return join(bskyDir(), `${prefix}${handle}.auth`);
26
+ }
27
+ async function loadConfig(profile) {
28
+ if (profile === "?") {
29
+ listProfiles();
30
+ process.exit(0);
31
+ }
32
+ const fp = configPath(profile);
33
+ await mkdir(dirname(fp), { recursive: true });
34
+ let cfg = {
35
+ host: "https://bsky.social",
36
+ bgs: "https://bsky.network",
37
+ handle: "",
38
+ password: ""
39
+ };
40
+ try {
41
+ const data = await readFile(fp, "utf-8");
42
+ const fileCfg = JSON.parse(data);
43
+ cfg = { ...cfg, ...fileCfg };
44
+ } catch {
45
+ }
46
+ if (process.env.BSKY_HANDLE) cfg.handle = process.env.BSKY_HANDLE;
47
+ if (process.env.BSKY_PASSWORD) cfg.password = process.env.BSKY_PASSWORD;
48
+ if (process.env.BSKY_HOST) cfg.host = process.env.BSKY_HOST;
49
+ if (process.env.BSKY_BGS) cfg.bgs = process.env.BSKY_BGS;
50
+ if (!cfg.handle || !cfg.password) {
51
+ throw new Error(
52
+ "No credentials found. Run 'bsky login' or set BSKY_HANDLE and BSKY_PASSWORD."
53
+ );
54
+ }
55
+ return cfg;
56
+ }
57
+ async function saveConfig(config, profile) {
58
+ const fp = configPath(profile);
59
+ await mkdir(dirname(fp), { recursive: true });
60
+ await writeFile(fp, JSON.stringify(config, null, " ") + "\n", {
61
+ mode: 420
62
+ });
63
+ }
64
+ function listProfiles() {
65
+ const dir = bskyDir();
66
+ if (!existsSync(dir)) return;
67
+ const files = readdirSync(dir);
68
+ for (const file of files) {
69
+ if (file.startsWith("config-") && file.endsWith(".json")) {
70
+ const name = file.slice(7, -5);
71
+ console.log(name);
72
+ }
73
+ }
74
+ }
75
+
76
+ // src/client.ts
77
+ import { AtpAgent } from "@atproto/api";
78
+
79
+ // src/auth.ts
80
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
81
+ import { createInterface } from "readline/promises";
82
+ import { createWriteStream } from "fs";
83
+ import { stdin, stdout, stderr } from "process";
84
+ async function readAuth(handle, prefix = "") {
85
+ try {
86
+ const fp = authPath(handle, prefix);
87
+ const data = await readFile2(fp, "utf-8");
88
+ return JSON.parse(data);
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+ async function writeAuth(auth, handle, prefix = "") {
94
+ const fp = authPath(handle, prefix);
95
+ await writeFile2(fp, JSON.stringify(auth, null, " ") + "\n", {
96
+ mode: 384
97
+ });
98
+ }
99
+ async function promptPassword() {
100
+ if (stdin.isTTY) {
101
+ stderr.write("Password: ");
102
+ const muted = createWriteStream("/dev/null");
103
+ const rl2 = createInterface({ input: stdin, output: muted });
104
+ const password2 = await rl2.question("");
105
+ rl2.close();
106
+ stderr.write("\n");
107
+ return password2.trim();
108
+ }
109
+ const rl = createInterface({ input: stdin });
110
+ const password = await rl.question("");
111
+ rl.close();
112
+ return password.trim();
113
+ }
114
+ async function prompt2FA() {
115
+ stderr.write(
116
+ "2FA is enabled. A sign-in code has been sent to your email.\nEnter the code: "
117
+ );
118
+ const rl = createInterface({ input: stdin, output: stdout });
119
+ const code = await rl.question("");
120
+ rl.close();
121
+ return code.trim();
122
+ }
123
+
124
+ // src/client.ts
125
+ async function createClient(config, prefix = "") {
126
+ const agent = new AtpAgent({ service: config.host });
127
+ const auth = await readAuth(config.handle, prefix);
128
+ if (auth) {
129
+ try {
130
+ await agent.resumeSession({
131
+ did: auth.did,
132
+ handle: auth.handle,
133
+ accessJwt: auth.refreshJwt,
134
+ refreshJwt: auth.refreshJwt,
135
+ active: true
136
+ });
137
+ const refreshed = await agent.com.atproto.server.refreshSession(
138
+ void 0,
139
+ {
140
+ headers: {
141
+ authorization: `Bearer ${auth.refreshJwt}`
142
+ }
143
+ }
144
+ );
145
+ await agent.resumeSession({
146
+ did: refreshed.data.did,
147
+ handle: refreshed.data.handle,
148
+ accessJwt: refreshed.data.accessJwt,
149
+ refreshJwt: refreshed.data.refreshJwt,
150
+ active: true
151
+ });
152
+ await writeAuth(
153
+ {
154
+ did: refreshed.data.did,
155
+ handle: refreshed.data.handle,
156
+ accessJwt: refreshed.data.accessJwt,
157
+ refreshJwt: refreshed.data.refreshJwt
158
+ },
159
+ config.handle,
160
+ prefix
161
+ );
162
+ return agent;
163
+ } catch {
164
+ }
165
+ }
166
+ try {
167
+ const loginResponse = await agent.login({
168
+ identifier: config.handle,
169
+ password: config.password
170
+ });
171
+ await writeAuth(
172
+ {
173
+ did: loginResponse.data.did,
174
+ handle: loginResponse.data.handle,
175
+ accessJwt: loginResponse.data.accessJwt,
176
+ refreshJwt: loginResponse.data.refreshJwt
177
+ },
178
+ config.handle,
179
+ prefix
180
+ );
181
+ return agent;
182
+ } catch (err) {
183
+ if (err instanceof Error && err.message.includes("AuthFactorTokenRequired")) {
184
+ const token = await prompt2FA();
185
+ const loginResponse = await agent.login({
186
+ identifier: config.handle,
187
+ password: config.password,
188
+ authFactorToken: token
189
+ });
190
+ await writeAuth(
191
+ {
192
+ did: loginResponse.data.did,
193
+ handle: loginResponse.data.handle,
194
+ accessJwt: loginResponse.data.accessJwt,
195
+ refreshJwt: loginResponse.data.refreshJwt
196
+ },
197
+ config.handle,
198
+ prefix
199
+ );
200
+ return agent;
201
+ }
202
+ throw new Error(`Cannot create session: ${err}`);
203
+ }
204
+ }
205
+
206
+ // src/commands/login.ts
207
+ function registerLogin(program2) {
208
+ program2.command("login").description("Login to Bluesky").argument("<handle>", "Your Bluesky handle").argument("[password]", "Your app password (prompts if omitted)").option("--host <url>", "PDS host URL", "https://bsky.social").option("--bgs <url>", "BGS host URL", "https://bsky.network").action(
209
+ async (handle, password, opts) => {
210
+ const resolvedPassword = password ?? process.env.BSKY_PASSWORD ?? await promptPassword();
211
+ const profile = program2.opts().profile;
212
+ const config = {
213
+ host: opts.host,
214
+ bgs: opts.bgs,
215
+ handle,
216
+ password: resolvedPassword
217
+ };
218
+ await saveConfig(config, profile);
219
+ }
220
+ );
221
+ }
222
+
223
+ // src/commands/timeline.ts
224
+ import chalk2 from "chalk";
225
+ import WebSocket from "ws";
226
+
227
+ // src/lib/format.ts
228
+ import chalk from "chalk";
229
+ function printPost(post) {
230
+ const rec = post.record;
231
+ process.stdout.write(chalk.redBright(post.author.handle));
232
+ process.stdout.write(` [${post.author.displayName ?? ""}]`);
233
+ console.log(` (${formatTime(rec.createdAt)})`);
234
+ console.log(rec.text);
235
+ if (post.embed) {
236
+ const images = post.embed;
237
+ if (images.images) {
238
+ for (const img of images.images) {
239
+ console.log(` {${img.fullsize}}`);
240
+ }
241
+ }
242
+ }
243
+ console.log(
244
+ ` \u{1F44D}(${post.likeCount ?? 0})\u26A1(${post.repostCount ?? 0})\u21A9\uFE0F (${post.replyCount ?? 0})`
245
+ );
246
+ if (rec.reply?.parent) {
247
+ process.stdout.write(" > ");
248
+ console.log(chalk.blue(rec.reply.parent.uri));
249
+ }
250
+ process.stdout.write(" - ");
251
+ console.log(chalk.blue(post.uri));
252
+ console.log();
253
+ }
254
+ function printActor(actor) {
255
+ process.stdout.write(chalk.redBright(actor.handle));
256
+ process.stdout.write(` [${actor.displayName ?? ""}] `);
257
+ console.log(chalk.blue(actor.did));
258
+ }
259
+ function formatTime(dateStr) {
260
+ const date = new Date(dateStr);
261
+ return date.toISOString();
262
+ }
263
+ function printStreamPost(did, handle, text, rkey, collection) {
264
+ const author = handle ?? did;
265
+ process.stdout.write(chalk.redBright(author));
266
+ console.log(`: ${text}`);
267
+ console.log(` at://${did}/${collection}/${rkey}`);
268
+ console.log();
269
+ }
270
+ function outputJson(data) {
271
+ console.log(JSON.stringify(data));
272
+ }
273
+
274
+ // src/commands/timeline.ts
275
+ var DEFAULT_JETSTREAM = "wss://jetstream1.us-east.bsky.network/subscribe";
276
+ function registerTimeline(program2) {
277
+ program2.command("timeline").alias("tl").description("Show timeline").option("-H, --handle <handle>", "User handle").option("-n <count>", "Number of items", "30").action(async (opts) => {
278
+ const agent = await getClient(program2);
279
+ const json = isJson(program2);
280
+ const n = parseInt(opts.n, 10);
281
+ let feed = [];
282
+ let cursor;
283
+ while (true) {
284
+ if (opts.handle) {
285
+ const handle = opts.handle === "self" ? agent.session.did : opts.handle;
286
+ const resp = await agent.getAuthorFeed({
287
+ actor: handle,
288
+ cursor,
289
+ limit: n
290
+ });
291
+ feed.push(...resp.data.feed);
292
+ cursor = resp.data.cursor;
293
+ } else {
294
+ const resp = await agent.getTimeline({
295
+ cursor,
296
+ limit: n
297
+ });
298
+ feed.push(...resp.data.feed);
299
+ cursor = resp.data.cursor;
300
+ }
301
+ if (!cursor || feed.length > n) break;
302
+ }
303
+ feed.sort((a, b) => {
304
+ const ta = new Date(
305
+ a.post.record.createdAt
306
+ ).getTime();
307
+ const tb = new Date(
308
+ b.post.record.createdAt
309
+ ).getTime();
310
+ return ta - tb;
311
+ });
312
+ if (feed.length > n) {
313
+ feed = feed.slice(feed.length - n);
314
+ }
315
+ if (json) {
316
+ for (const p of feed) outputJson(p);
317
+ } else {
318
+ for (const p of feed) printPost(p.post);
319
+ }
320
+ });
321
+ }
322
+ function registerStream(program2) {
323
+ program2.command("stream").description("Stream live posts from the network").option("--cursor <cursor>", "Resume cursor (Unix microseconds)").option("-H, --handle <handle>", "Filter to a specific user").option("--pattern <regex>", "Filter post text by regex").option("--pattern-flags <flags>", "Regex flags for --pattern", "gi").option("--jetstream <url>", "Override Jetstream endpoint").action(
324
+ async (opts) => {
325
+ const json = isJson(program2);
326
+ const VALID_FLAGS = /* @__PURE__ */ new Set(["g", "i", "m", "s", "u", "v", "d", "y"]);
327
+ if (!opts.pattern && opts.patternFlags !== "gi") {
328
+ program2.error(`${chalk2.bgRed.white("Fatal error:")} --pattern-flags requires --pattern`);
329
+ }
330
+ if (opts.pattern) {
331
+ let flagChars = opts.patternFlags.split("");
332
+ const unknown = flagChars.filter((f) => !VALID_FLAGS.has(f));
333
+ if (unknown.length > 0) {
334
+ program2.error(`unknown regex flag(s): ${unknown.join(", ")}`);
335
+ }
336
+ const uniqueFlags = new Set(flagChars);
337
+ if (uniqueFlags.size !== flagChars.length) {
338
+ const dupes = flagChars.filter((f, i) => flagChars.indexOf(f) !== i);
339
+ console.warn(
340
+ `
341
+ ${chalk2.bgYellow.black("Warning:")} duplicate regex flag(s) removed: ${[...new Set(dupes)].join(", ")}`
342
+ );
343
+ flagChars = [...uniqueFlags];
344
+ opts.patternFlags = flagChars.join("");
345
+ }
346
+ if (flagChars.includes("u") && flagChars.includes("v")) {
347
+ program2.error(`${chalk2.bgRed.white("Fatal error:")}regex flags u and v cannot be used together`);
348
+ }
349
+ if (flagChars.includes("y") && flagChars.includes("g")) {
350
+ console.warn(`${chalk2.bgYellow.black("Warning:")} sticky flag (y) makes global flag (g) meaningless`);
351
+ }
352
+ if (flagChars.includes("u") && flagChars.includes("d")) {
353
+ console.warn(`${chalk2.bgYellow.black("Warning:")} unicode (u) with hasIndices (d) is valid but rarely needed`);
354
+ }
355
+ }
356
+ const wsUrl = new URL(opts.jetstream ?? DEFAULT_JETSTREAM);
357
+ wsUrl.searchParams.set("wantedCollections", "app.bsky.feed.post");
358
+ let filterHandle = null;
359
+ if (opts.handle) {
360
+ const agent = await getClient(program2);
361
+ if (opts.handle === "self") {
362
+ wsUrl.searchParams.set("wantedDids", agent.session.did);
363
+ filterHandle = agent.session.handle;
364
+ } else if (opts.handle.startsWith("did:")) {
365
+ wsUrl.searchParams.set("wantedDids", opts.handle);
366
+ } else {
367
+ const profile = await agent.getProfile({ actor: opts.handle });
368
+ wsUrl.searchParams.set("wantedDids", profile.data.did);
369
+ filterHandle = opts.handle;
370
+ }
371
+ }
372
+ if (opts.cursor) {
373
+ wsUrl.searchParams.set("cursor", opts.cursor);
374
+ }
375
+ let re = null;
376
+ if (opts.pattern) {
377
+ try {
378
+ re = new RegExp(opts.pattern, opts.patternFlags);
379
+ } catch (err) {
380
+ program2.error(`invalid regex pattern: ${err.message}`);
381
+ }
382
+ }
383
+ let lastCursor = "";
384
+ const ws = new WebSocket(wsUrl.toString());
385
+ process.on("SIGINT", () => {
386
+ if (lastCursor) {
387
+ console.error(`
388
+ Cursor: ${lastCursor}`);
389
+ }
390
+ ws.close();
391
+ process.exit(0);
392
+ });
393
+ ws.on("message", (data) => {
394
+ try {
395
+ const event = JSON.parse(data.toString());
396
+ lastCursor = String(event.time_us);
397
+ if (event.kind !== "commit") return;
398
+ const commit = event.commit;
399
+ if (commit.operation !== "create") return;
400
+ if (commit.collection !== "app.bsky.feed.post") return;
401
+ const text = commit.record?.text;
402
+ if (!text) return;
403
+ if (re && !re.test(text)) return;
404
+ if (json) {
405
+ outputJson(event);
406
+ } else {
407
+ printStreamPost(
408
+ event.did,
409
+ filterHandle,
410
+ text,
411
+ commit.rkey,
412
+ commit.collection
413
+ );
414
+ }
415
+ } catch {
416
+ }
417
+ });
418
+ ws.on("error", (err) => {
419
+ console.error("WebSocket error:", err.message);
420
+ process.exit(1);
421
+ });
422
+ ws.on("close", () => {
423
+ if (lastCursor) {
424
+ console.error(`Cursor: ${lastCursor}`);
425
+ }
426
+ process.exit(0);
427
+ });
428
+ await new Promise(() => {
429
+ });
430
+ }
431
+ );
432
+ }
433
+
434
+ // src/commands/thread.ts
435
+ function normalizeUri(uri) {
436
+ if (!uri.startsWith("at://did:plc:")) {
437
+ return "at://did:plc:" + uri;
438
+ }
439
+ return uri;
440
+ }
441
+ function registerThread(program2) {
442
+ program2.command("thread").description("Show thread").argument("<uri>", "Post URI").option("-n <count>", "Number of items", "30").action(async (uri, opts) => {
443
+ const agent = await getClient(program2);
444
+ const json = isJson(program2);
445
+ const depth = parseInt(opts.n, 10);
446
+ const resp = await agent.getPostThread({
447
+ uri: normalizeUri(uri),
448
+ depth
449
+ });
450
+ const thread = resp.data.thread;
451
+ if (json) {
452
+ outputJson(thread);
453
+ if (thread.replies) {
454
+ for (const r of thread.replies) outputJson(r);
455
+ }
456
+ return;
457
+ }
458
+ const replies = thread.replies ?? [];
459
+ replies.reverse();
460
+ printPost(thread.post);
461
+ for (const r of replies) {
462
+ if (r.post) printPost(r.post);
463
+ }
464
+ });
465
+ }
466
+
467
+ // src/commands/post.ts
468
+ import { readFile as readFile3 } from "fs/promises";
469
+ import { basename } from "path";
470
+ import { stdin as stdin2 } from "process";
471
+ import { load as cheerioLoad } from "cheerio";
472
+
473
+ // src/lib/extract.ts
474
+ var URL_PATTERN = /https?:\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;()]+/g;
475
+ var MENTION_PATTERN = /@[a-zA-Z0-9.]+/g;
476
+ var TAG_PATTERN = /\B#\S+/g;
477
+ function byteLength(str) {
478
+ return new TextEncoder().encode(str).byteLength;
479
+ }
480
+ function extractWithByteOffsets(text, pattern, stripPrefix) {
481
+ const results = [];
482
+ let match;
483
+ pattern.lastIndex = 0;
484
+ while ((match = pattern.exec(text)) !== null) {
485
+ const before = text.slice(0, match.index);
486
+ const matchText = match[0];
487
+ const start = byteLength(before);
488
+ const end = start + byteLength(matchText);
489
+ const entryText = stripPrefix ? matchText.replace(new RegExp(`^${stripPrefix}`), "") : matchText;
490
+ results.push({ start, end, text: entryText });
491
+ }
492
+ return results;
493
+ }
494
+ function extractLinks(text) {
495
+ return extractWithByteOffsets(text, URL_PATTERN);
496
+ }
497
+ function extractMentions(text) {
498
+ return extractWithByteOffsets(text, MENTION_PATTERN, "@");
499
+ }
500
+ function extractTags(text) {
501
+ return extractWithByteOffsets(text, TAG_PATTERN, "#");
502
+ }
503
+
504
+ // src/commands/post.ts
505
+ async function readStdin() {
506
+ const chunks = [];
507
+ for await (const chunk of stdin2) {
508
+ chunks.push(chunk);
509
+ }
510
+ return Buffer.concat(chunks).toString("utf-8");
511
+ }
512
+ function detectMimeType(data) {
513
+ if (data[0] === 255 && data[1] === 216) return "image/jpeg";
514
+ if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71)
515
+ return "image/png";
516
+ if (data[0] === 71 && data[1] === 73 && data[2] === 70)
517
+ return "image/gif";
518
+ if (data[0] === 82 && data[1] === 73 && data[2] === 70 && data[3] === 70)
519
+ return "image/webp";
520
+ if (data[4] === 102 && data[5] === 116 && data[6] === 121)
521
+ return "video/mp4";
522
+ return "application/octet-stream";
523
+ }
524
+ async function buildFacets(agent, text) {
525
+ const facets = [];
526
+ for (const entry of extractLinks(text)) {
527
+ facets.push({
528
+ index: { byteStart: entry.start, byteEnd: entry.end },
529
+ features: [
530
+ { $type: "app.bsky.richtext.facet#link", uri: entry.text }
531
+ ]
532
+ });
533
+ }
534
+ for (const entry of extractMentions(text)) {
535
+ try {
536
+ const profile = await agent.getProfile({ actor: entry.text });
537
+ facets.push({
538
+ index: { byteStart: entry.start, byteEnd: entry.end },
539
+ features: [
540
+ { $type: "app.bsky.richtext.facet#mention", did: profile.data.did }
541
+ ]
542
+ });
543
+ } catch {
544
+ }
545
+ }
546
+ for (const entry of extractTags(text)) {
547
+ facets.push({
548
+ index: { byteStart: entry.start, byteEnd: entry.end },
549
+ features: [
550
+ { $type: "app.bsky.richtext.facet#tag", tag: entry.text }
551
+ ]
552
+ });
553
+ }
554
+ return facets;
555
+ }
556
+ async function fetchLinkCard(agent, url) {
557
+ try {
558
+ const resp = await fetch(url);
559
+ if (!resp.ok) return null;
560
+ const html = await resp.text();
561
+ const $ = cheerioLoad(html);
562
+ let title = $("title").text();
563
+ let description = $('meta[property="description"]').attr("content") ?? "";
564
+ const imgURL = $('meta[property="og:image"]').attr("content");
565
+ if (!title) title = $('meta[property="og:title"]').attr("content") ?? url;
566
+ if (!description)
567
+ description = $('meta[property="og:description"]').attr("content") ?? url;
568
+ const external = {
569
+ uri: url,
570
+ title,
571
+ description
572
+ };
573
+ if (imgURL) {
574
+ try {
575
+ const imgResp = await fetch(imgURL);
576
+ if (imgResp.ok) {
577
+ const imgData = new Uint8Array(await imgResp.arrayBuffer());
578
+ const uploadResp = await agent.uploadBlob(imgData, {
579
+ encoding: detectMimeType(imgData)
580
+ });
581
+ external.thumb = uploadResp.data.blob;
582
+ }
583
+ } catch {
584
+ }
585
+ }
586
+ return { $type: "app.bsky.embed.external", external };
587
+ } catch {
588
+ return null;
589
+ }
590
+ }
591
+ async function createPost(agent, text, opts) {
592
+ const facets = await buildFacets(agent, text);
593
+ const post = {
594
+ $type: "app.bsky.feed.post",
595
+ text,
596
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
597
+ facets: facets.length > 0 ? facets : void 0
598
+ };
599
+ if (opts.reply) {
600
+ post.reply = {
601
+ root: opts.replyRoot ?? opts.reply,
602
+ parent: opts.reply
603
+ };
604
+ }
605
+ if (opts.images && opts.images.length > 0) {
606
+ const images = [];
607
+ for (let i = 0; i < opts.images.length; i++) {
608
+ const data = await readFile3(opts.images[i]);
609
+ const uploadResp = await agent.uploadBlob(data, {
610
+ encoding: detectMimeType(data)
611
+ });
612
+ images.push({
613
+ alt: opts.imageAlts?.[i] ?? basename(opts.images[i]),
614
+ image: uploadResp.data.blob
615
+ });
616
+ }
617
+ post.embed = { $type: "app.bsky.embed.images", images };
618
+ }
619
+ if (opts.video) {
620
+ const data = await readFile3(opts.video);
621
+ const uploadResp = await agent.uploadBlob(data, {
622
+ encoding: detectMimeType(data)
623
+ });
624
+ post.embed = {
625
+ $type: "app.bsky.embed.video",
626
+ video: uploadResp.data.blob,
627
+ alt: opts.videoAlt ?? basename(opts.video)
628
+ };
629
+ }
630
+ if (opts.quote) {
631
+ post.embed = {
632
+ $type: "app.bsky.embed.record",
633
+ record: opts.quote
634
+ };
635
+ }
636
+ if (!post.embed) {
637
+ const links = extractLinks(text);
638
+ if (links.length > 0) {
639
+ const card = await fetchLinkCard(agent, links[0].text);
640
+ if (card) post.embed = card;
641
+ }
642
+ }
643
+ const resp = await agent.post(post);
644
+ return resp.uri;
645
+ }
646
+ function registerPost(program2) {
647
+ program2.command("post").description("Create a new post").argument("[text...]", "Post text").option("--stdin", "Read text from stdin").option("-i, --image <files...>", "Image files to attach").option("--image-alt <alts...>", "Alt text for images").option("--video <file>", "Video file to attach").option("--video-alt <alt>", "Alt text for video").action(
648
+ async (textParts, opts) => {
649
+ let text = textParts.join(" ");
650
+ if (opts.stdin) {
651
+ text = await readStdin();
652
+ }
653
+ if (!text.trim()) {
654
+ console.error("Error: post text is required");
655
+ process.exit(1);
656
+ }
657
+ const agent = await getClient(program2);
658
+ const uri = await createPost(agent, text, {
659
+ images: opts.image,
660
+ imageAlts: opts.imageAlt,
661
+ video: opts.video,
662
+ videoAlt: opts.videoAlt
663
+ });
664
+ console.log(uri);
665
+ }
666
+ );
667
+ }
668
+ function registerReply(program2) {
669
+ program2.command("reply").description("Reply to a post").argument("<uri>", "URI of the post to reply to").argument("<text...>", "Reply text").action(async (uri, textParts) => {
670
+ const text = textParts.join(" ");
671
+ const agent = await getClient(program2);
672
+ const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
673
+ const parts = atUri.split("/");
674
+ const rkey = parts[parts.length - 1];
675
+ const collection = parts[parts.length - 2];
676
+ const did = parts[2];
677
+ const record = await agent.com.atproto.repo.getRecord({
678
+ repo: did,
679
+ collection,
680
+ rkey
681
+ });
682
+ const parent = {
683
+ uri: record.data.uri,
684
+ cid: record.data.cid
685
+ };
686
+ const parentPost = record.data.value;
687
+ const root = parentPost.reply?.root ?? parent;
688
+ const postUri = await createPost(agent, text, {
689
+ reply: parent,
690
+ replyRoot: root
691
+ });
692
+ console.log(postUri);
693
+ });
694
+ }
695
+ function registerQuote(program2) {
696
+ program2.command("quote").description("Quote a post").argument("<uri>", "URI of the post to quote").argument("<text...>", "Quote text").action(async (uri, textParts) => {
697
+ const text = textParts.join(" ");
698
+ const agent = await getClient(program2);
699
+ const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
700
+ const parts = atUri.split("/");
701
+ const rkey = parts[parts.length - 1];
702
+ const collection = parts[parts.length - 2];
703
+ const did = parts[2];
704
+ const record = await agent.com.atproto.repo.getRecord({
705
+ repo: did,
706
+ collection,
707
+ rkey
708
+ });
709
+ const postUri = await createPost(agent, text, {
710
+ quote: {
711
+ uri: record.data.uri,
712
+ cid: record.data.cid
713
+ }
714
+ });
715
+ console.log(postUri);
716
+ });
717
+ }
718
+
719
+ // src/commands/delete.ts
720
+ function registerDelete(program2) {
721
+ program2.command("delete").description("Delete a post").argument("<uri...>", "Post URI(s) to delete").action(async (uris) => {
722
+ const agent = await getClient(program2);
723
+ for (const uri of uris) {
724
+ const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
725
+ const parts = atUri.split("/");
726
+ if (parts.length < 3) {
727
+ console.error(`Invalid post URI: ${uri}`);
728
+ continue;
729
+ }
730
+ const rkey = parts[parts.length - 1];
731
+ const collection = parts[parts.length - 2];
732
+ await agent.com.atproto.repo.deleteRecord({
733
+ repo: agent.session.did,
734
+ collection,
735
+ rkey
736
+ });
737
+ }
738
+ });
739
+ }
740
+
741
+ // src/commands/like.ts
742
+ import chalk3 from "chalk";
743
+ function registerLike(program2) {
744
+ program2.command("like").description("Like a post").argument("<uri...>", "Post URI(s) to like").action(async (uris) => {
745
+ const agent = await getClient(program2);
746
+ for (const uri of uris) {
747
+ const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
748
+ const parts = atUri.split("/");
749
+ const rkey = parts[parts.length - 1];
750
+ const collection = parts[parts.length - 2];
751
+ const did = parts[2];
752
+ const record = await agent.com.atproto.repo.getRecord({
753
+ repo: did,
754
+ collection,
755
+ rkey
756
+ });
757
+ const resp = await agent.like(record.data.uri, record.data.cid);
758
+ console.log(resp.uri);
759
+ }
760
+ });
761
+ }
762
+ function registerLikes(program2) {
763
+ program2.command("likes").description("Show likes on a post").argument("<uri>", "Post URI").action(async (uri) => {
764
+ const agent = await getClient(program2);
765
+ const json = isJson(program2);
766
+ const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
767
+ const parts = atUri.split("/");
768
+ const rkey = parts[parts.length - 1];
769
+ const collection = parts[parts.length - 2];
770
+ const did = parts[2];
771
+ const record = await agent.com.atproto.repo.getRecord({
772
+ repo: did,
773
+ collection,
774
+ rkey
775
+ });
776
+ const resp = await agent.getLikes({
777
+ uri: record.data.uri,
778
+ cid: record.data.cid,
779
+ limit: 50
780
+ });
781
+ if (json) {
782
+ for (const v of resp.data.likes) outputJson(v);
783
+ return;
784
+ }
785
+ for (const v of resp.data.likes) {
786
+ process.stdout.write("\u{1F44D} ");
787
+ process.stdout.write(chalk3.redBright(v.actor.handle));
788
+ process.stdout.write(` [${v.actor.displayName ?? ""}]`);
789
+ console.log(` (${v.createdAt})`);
790
+ }
791
+ });
792
+ }
793
+
794
+ // src/commands/repost.ts
795
+ import chalk4 from "chalk";
796
+ function registerRepost(program2) {
797
+ program2.command("repost").description("Repost a post").argument("<uri...>", "Post URI(s) to repost").action(async (uris) => {
798
+ const agent = await getClient(program2);
799
+ for (const uri of uris) {
800
+ const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
801
+ const parts = atUri.split("/");
802
+ const rkey = parts[parts.length - 1];
803
+ const collection = parts[parts.length - 2];
804
+ const did = parts[2];
805
+ const record = await agent.com.atproto.repo.getRecord({
806
+ repo: did,
807
+ collection,
808
+ rkey
809
+ });
810
+ const resp = await agent.repost(record.data.uri, record.data.cid);
811
+ console.log(resp.uri);
812
+ }
813
+ });
814
+ }
815
+ function registerReposts(program2) {
816
+ program2.command("reposts").description("Show reposts of a post").argument("<uri>", "Post URI").action(async (uri) => {
817
+ const agent = await getClient(program2);
818
+ const json = isJson(program2);
819
+ const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
820
+ const parts = atUri.split("/");
821
+ const rkey = parts[parts.length - 1];
822
+ const collection = parts[parts.length - 2];
823
+ const did = parts[2];
824
+ const record = await agent.com.atproto.repo.getRecord({
825
+ repo: did,
826
+ collection,
827
+ rkey
828
+ });
829
+ const resp = await agent.getRepostedBy({
830
+ uri: record.data.uri,
831
+ cid: record.data.cid,
832
+ limit: 50
833
+ });
834
+ if (json) {
835
+ for (const r of resp.data.repostedBy) outputJson(r);
836
+ return;
837
+ }
838
+ for (const r of resp.data.repostedBy) {
839
+ process.stdout.write("\u26A1 ");
840
+ process.stdout.write(chalk4.redBright(r.handle));
841
+ console.log(` [${r.displayName ?? ""}]`);
842
+ }
843
+ });
844
+ }
845
+
846
+ // src/commands/social.ts
847
+ async function resolveDid(agent, handleOrDid) {
848
+ if (handleOrDid.startsWith("did:")) return handleOrDid;
849
+ const profile = await agent.getProfile({ actor: handleOrDid });
850
+ return profile.data.did;
851
+ }
852
+ function registerFollow(program2) {
853
+ program2.command("follow").description("Follow user(s)").argument("<handles...>", "Handle(s) to follow").action(async (handles) => {
854
+ const agent = await getClient(program2);
855
+ for (const handle of handles) {
856
+ const profile = await agent.getProfile({ actor: handle });
857
+ const resp = await agent.follow(profile.data.did);
858
+ console.log(resp.uri);
859
+ }
860
+ });
861
+ }
862
+ function registerUnfollow(program2) {
863
+ program2.command("unfollow").description("Unfollow user(s)").argument("<handles...>", "Handle(s) to unfollow").action(async (handles) => {
864
+ const agent = await getClient(program2);
865
+ for (const handle of handles) {
866
+ const profile = await agent.getProfile({ actor: handle });
867
+ const followUri = profile.data.viewer?.following;
868
+ if (!followUri) continue;
869
+ const parts = followUri.split("/");
870
+ const rkey = parts[parts.length - 1];
871
+ const collection = parts[parts.length - 2];
872
+ console.log(followUri);
873
+ await agent.com.atproto.repo.deleteRecord({
874
+ repo: agent.session.did,
875
+ collection,
876
+ rkey
877
+ });
878
+ }
879
+ });
880
+ }
881
+ function registerFollows(program2) {
882
+ program2.command("follows").description("Show follows").option("-H, --handle <handle>", "User handle").action(async (opts) => {
883
+ const agent = await getClient(program2);
884
+ const json = isJson(program2);
885
+ const handle = opts.handle ?? agent.session.handle;
886
+ let cursor;
887
+ while (true) {
888
+ const resp = await agent.getFollows({
889
+ actor: handle,
890
+ cursor,
891
+ limit: 100
892
+ });
893
+ if (json) {
894
+ for (const f of resp.data.follows) outputJson(f);
895
+ } else {
896
+ for (const f of resp.data.follows) printActor(f);
897
+ }
898
+ cursor = resp.data.cursor;
899
+ if (!cursor) break;
900
+ }
901
+ });
902
+ }
903
+ function registerFollowers(program2) {
904
+ program2.command("followers").description("Show followers").option("-H, --handle <handle>", "User handle").action(async (opts) => {
905
+ const agent = await getClient(program2);
906
+ const json = isJson(program2);
907
+ const handle = opts.handle ?? agent.session.handle;
908
+ let cursor;
909
+ while (true) {
910
+ const resp = await agent.getFollowers({
911
+ actor: handle,
912
+ cursor,
913
+ limit: 100
914
+ });
915
+ if (json) {
916
+ for (const f of resp.data.followers) outputJson(f);
917
+ } else {
918
+ for (const f of resp.data.followers) printActor(f);
919
+ }
920
+ cursor = resp.data.cursor;
921
+ if (!cursor) break;
922
+ }
923
+ });
924
+ }
925
+ function registerBlock(program2) {
926
+ program2.command("block").description("Block user(s)").argument("<handles...>", "Handle(s) or DID(s) to block").action(async (handles) => {
927
+ const agent = await getClient(program2);
928
+ for (const handle of handles) {
929
+ const did = await resolveDid(agent, handle);
930
+ const resp = await agent.com.atproto.repo.createRecord({
931
+ repo: agent.session.did,
932
+ collection: "app.bsky.graph.block",
933
+ record: {
934
+ $type: "app.bsky.graph.block",
935
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
936
+ subject: did
937
+ }
938
+ });
939
+ console.log(resp.data.uri);
940
+ }
941
+ });
942
+ }
943
+ function registerUnblock(program2) {
944
+ program2.command("unblock").description("Unblock user(s)").argument("<handles...>", "Handle(s) to unblock").action(async (handles) => {
945
+ const agent = await getClient(program2);
946
+ for (const handle of handles) {
947
+ const profile = await agent.getProfile({ actor: handle });
948
+ const blockUri = profile.data.viewer?.blocking;
949
+ if (!blockUri) continue;
950
+ const parts = blockUri.split("/");
951
+ const rkey = parts[parts.length - 1];
952
+ const collection = parts[parts.length - 2];
953
+ console.log(blockUri);
954
+ await agent.com.atproto.repo.deleteRecord({
955
+ repo: agent.session.did,
956
+ collection,
957
+ rkey
958
+ });
959
+ }
960
+ });
961
+ }
962
+ function registerBlocks(program2) {
963
+ program2.command("blocks").description("Show blocked users").option("-H, --handle <handle>", "User handle").action(async () => {
964
+ const agent = await getClient(program2);
965
+ const json = isJson(program2);
966
+ let cursor;
967
+ while (true) {
968
+ const resp = await agent.app.bsky.graph.getBlocks({
969
+ cursor,
970
+ limit: 100
971
+ });
972
+ if (json) {
973
+ for (const b of resp.data.blocks) outputJson(b);
974
+ } else {
975
+ for (const b of resp.data.blocks) printActor(b);
976
+ }
977
+ cursor = resp.data.cursor;
978
+ if (!cursor) break;
979
+ }
980
+ });
981
+ }
982
+ function registerMute(program2) {
983
+ program2.command("mute").description("Mute user(s)").argument("<handles...>", "Handle(s) or DID(s) to mute").action(async (handles) => {
984
+ const agent = await getClient(program2);
985
+ for (const handle of handles) {
986
+ const did = await resolveDid(agent, handle);
987
+ await agent.mute(did);
988
+ }
989
+ });
990
+ }
991
+
992
+ // src/commands/search.ts
993
+ function registerSearch(program2) {
994
+ program2.command("search").description("Search posts").argument("<terms...>", "Search terms").option("-n <count>", "Number of results", "100").action(async (terms, opts) => {
995
+ const agent = await getClient(program2);
996
+ const json = isJson(program2);
997
+ const n = parseInt(opts.n, 10);
998
+ const query = terms.join(" ");
999
+ let results = [];
1000
+ let cursor;
1001
+ while (true) {
1002
+ const resp = await agent.app.bsky.feed.searchPosts({
1003
+ q: query,
1004
+ cursor,
1005
+ limit: 100
1006
+ });
1007
+ results.push(...resp.data.posts);
1008
+ cursor = resp.data.cursor;
1009
+ if (!cursor || results.length > n) break;
1010
+ }
1011
+ results.sort((a, b) => {
1012
+ const ta = new Date(
1013
+ a.record.createdAt
1014
+ ).getTime();
1015
+ const tb = new Date(
1016
+ b.record.createdAt
1017
+ ).getTime();
1018
+ return ta - tb;
1019
+ });
1020
+ if (results.length > n) {
1021
+ results = results.slice(results.length - n);
1022
+ }
1023
+ if (json) {
1024
+ for (const p of results) outputJson(p);
1025
+ } else {
1026
+ for (const p of results) printPost(p);
1027
+ }
1028
+ });
1029
+ }
1030
+ function registerSearchUsers(program2) {
1031
+ program2.command("search-users").description("Search for users").argument("<terms...>", "Search terms").option("-n <count>", "Number of results", "100").action(async (terms, opts) => {
1032
+ const agent = await getClient(program2);
1033
+ const n = parseInt(opts.n, 10);
1034
+ const query = terms.join(" ");
1035
+ const resp = await agent.searchActors({
1036
+ term: query,
1037
+ limit: n
1038
+ });
1039
+ for (const actor of resp.data.actors) {
1040
+ outputJson(actor);
1041
+ }
1042
+ });
1043
+ }
1044
+
1045
+ // src/commands/profile.ts
1046
+ import { readFile as readFile4 } from "fs/promises";
1047
+ function detectMimeType2(data) {
1048
+ if (data[0] === 255 && data[1] === 216) return "image/jpeg";
1049
+ if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71)
1050
+ return "image/png";
1051
+ if (data[0] === 71 && data[1] === 73 && data[2] === 70)
1052
+ return "image/gif";
1053
+ return "application/octet-stream";
1054
+ }
1055
+ function registerProfile(program2) {
1056
+ program2.command("profile").description("Show profile").option("-H, --handle <handle>", "User handle").action(async (opts) => {
1057
+ const agent = await getClient(program2);
1058
+ const json = isJson(program2);
1059
+ const handle = opts.handle ?? agent.session.handle;
1060
+ const resp = await agent.getProfile({ actor: handle });
1061
+ const profile = resp.data;
1062
+ if (json) {
1063
+ outputJson(profile);
1064
+ return;
1065
+ }
1066
+ console.log(`Did: ${profile.did}`);
1067
+ console.log(`Handle: ${profile.handle}`);
1068
+ console.log(`DisplayName: ${profile.displayName ?? ""}`);
1069
+ console.log(`Description: ${profile.description ?? ""}`);
1070
+ console.log(`Follows: ${profile.followsCount ?? 0}`);
1071
+ console.log(`Followers: ${profile.followersCount ?? 0}`);
1072
+ console.log(`Avatar: ${profile.avatar ?? ""}`);
1073
+ console.log(`Banner: ${profile.banner ?? ""}`);
1074
+ });
1075
+ }
1076
+ function registerProfileUpdate(program2) {
1077
+ program2.command("profile-update").description("Update profile").argument("[displayname]", "Display name").argument("[description]", "Description").option("--avatar <file>", "Avatar image file").option("--banner <file>", "Banner image file").action(
1078
+ async (displayName, description, opts) => {
1079
+ if (!displayName && !description && !opts.avatar && !opts.banner) {
1080
+ console.error("Error: provide at least one field to update");
1081
+ process.exit(1);
1082
+ }
1083
+ const agent = await getClient(program2);
1084
+ const current = await agent.getProfile({
1085
+ actor: agent.session.handle
1086
+ });
1087
+ const name = displayName ?? current.data.displayName;
1088
+ const desc = description ?? current.data.description;
1089
+ let avatar = void 0;
1090
+ if (opts.avatar) {
1091
+ const data = await readFile4(opts.avatar);
1092
+ const uploadResp = await agent.uploadBlob(data, {
1093
+ encoding: detectMimeType2(data)
1094
+ });
1095
+ avatar = uploadResp.data.blob;
1096
+ }
1097
+ let banner = void 0;
1098
+ if (opts.banner) {
1099
+ const data = await readFile4(opts.banner);
1100
+ const uploadResp = await agent.uploadBlob(data, {
1101
+ encoding: detectMimeType2(data)
1102
+ });
1103
+ banner = uploadResp.data.blob;
1104
+ }
1105
+ const currentRecord = await agent.com.atproto.repo.getRecord({
1106
+ repo: agent.session.did,
1107
+ collection: "app.bsky.actor.profile",
1108
+ rkey: "self"
1109
+ });
1110
+ await agent.com.atproto.repo.putRecord({
1111
+ repo: agent.session.did,
1112
+ collection: "app.bsky.actor.profile",
1113
+ rkey: "self",
1114
+ record: {
1115
+ $type: "app.bsky.actor.profile",
1116
+ displayName: name,
1117
+ description: desc,
1118
+ ...avatar ? { avatar } : {},
1119
+ ...banner ? { banner } : {}
1120
+ },
1121
+ swapRecord: currentRecord.data.cid
1122
+ });
1123
+ }
1124
+ );
1125
+ }
1126
+ function registerSession(program2) {
1127
+ program2.command("session").description("Show session info").action(async () => {
1128
+ const agent = await getClient(program2);
1129
+ const json = isJson(program2);
1130
+ const resp = await agent.com.atproto.server.getSession();
1131
+ const session = resp.data;
1132
+ if (json) {
1133
+ outputJson(session);
1134
+ return;
1135
+ }
1136
+ console.log(`Did: ${session.did}`);
1137
+ console.log(`Email: ${session.email ?? ""}`);
1138
+ console.log(`Handle: ${session.handle}`);
1139
+ });
1140
+ }
1141
+
1142
+ // src/commands/notification.ts
1143
+ import chalk5 from "chalk";
1144
+ function registerNotifs(program2) {
1145
+ program2.command("notifs").alias("notification").description("Show notifications").option("-a, --all", "Show all (including read)").action(async (opts) => {
1146
+ const agent = await getClient(program2);
1147
+ const json = isJson(program2);
1148
+ const resp = await agent.listNotifications({ limit: 50 });
1149
+ const notifs = resp.data.notifications;
1150
+ if (json) {
1151
+ for (const n of notifs) outputJson(n);
1152
+ return;
1153
+ }
1154
+ for (const n of notifs) {
1155
+ if (!opts.all && n.isRead) continue;
1156
+ process.stdout.write(chalk5.redBright(n.author.handle));
1157
+ process.stdout.write(` [${n.author.displayName ?? ""}] `);
1158
+ console.log(chalk5.blue(n.author.did));
1159
+ const record = n.record;
1160
+ switch (record.$type) {
1161
+ case "app.bsky.feed.post":
1162
+ console.log(` ${n.reason} to ${n.uri}`);
1163
+ break;
1164
+ case "app.bsky.feed.repost": {
1165
+ const repost = record;
1166
+ console.log(` reposted ${repost.subject.uri}`);
1167
+ break;
1168
+ }
1169
+ case "app.bsky.feed.like": {
1170
+ const like = record;
1171
+ console.log(` liked ${like.subject.uri}`);
1172
+ break;
1173
+ }
1174
+ case "app.bsky.graph.follow":
1175
+ console.log(" followed you");
1176
+ break;
1177
+ }
1178
+ }
1179
+ await agent.app.bsky.notification.updateSeen({
1180
+ seenAt: (/* @__PURE__ */ new Date()).toISOString()
1181
+ });
1182
+ });
1183
+ }
1184
+
1185
+ // src/commands/moderation.ts
1186
+ async function resolveDid2(agent, handleOrDid) {
1187
+ if (handleOrDid.startsWith("did:")) return handleOrDid;
1188
+ const profile = await agent.getProfile({ actor: handleOrDid });
1189
+ return profile.data.did;
1190
+ }
1191
+ function registerReport(program2) {
1192
+ program2.command("report").description("Report a user").argument("<handle>", "Handle or DID to report").option("--comment <text>", "Report comment").action(async (handle, opts) => {
1193
+ const agent = await getClient(program2);
1194
+ const did = await resolveDid2(agent, handle);
1195
+ const resp = await agent.com.atproto.moderation.createReport({
1196
+ reasonType: "com.atproto.moderation.defs#reasonSpam",
1197
+ subject: {
1198
+ $type: "com.atproto.admin.defs#repoRef",
1199
+ did
1200
+ },
1201
+ reason: opts.comment
1202
+ });
1203
+ console.log("Report created successfully:", JSON.stringify(resp.data));
1204
+ });
1205
+ }
1206
+ function registerModList(program2) {
1207
+ program2.command("mod-list").description("Create a moderation list with user(s)").argument("<handles...>", "Handle(s) or DID(s) to add").option("--name <name>", "List name", "NewList").option("--desc <description>", "List description", "").action(
1208
+ async (handles, opts) => {
1209
+ const agent = await getClient(program2);
1210
+ const listResp = await agent.com.atproto.repo.createRecord({
1211
+ repo: agent.session.did,
1212
+ collection: "app.bsky.graph.list",
1213
+ record: {
1214
+ $type: "app.bsky.graph.list",
1215
+ name: opts.name,
1216
+ purpose: "app.bsky.graph.defs#modlist",
1217
+ description: opts.desc,
1218
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1219
+ }
1220
+ });
1221
+ console.log("List created successfully. URI:", listResp.data.uri);
1222
+ for (const handle of handles) {
1223
+ const did = await resolveDid2(agent, handle);
1224
+ await agent.com.atproto.repo.createRecord({
1225
+ repo: agent.session.did,
1226
+ collection: "app.bsky.graph.listitem",
1227
+ record: {
1228
+ $type: "app.bsky.graph.listitem",
1229
+ subject: did,
1230
+ list: listResp.data.uri,
1231
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1232
+ }
1233
+ });
1234
+ console.log("User added to moderation list successfully.");
1235
+ }
1236
+ }
1237
+ );
1238
+ }
1239
+
1240
+ // src/commands/bookmark.ts
1241
+ import { AppBskyFeedDefs } from "@atproto/api";
1242
+ function registerBookmarks(program2) {
1243
+ const bookmarks = program2.command("bookmarks").description("Manage bookmarks");
1244
+ bookmarks.command("create").description("Bookmark a post").argument("<uri...>", "Post URI(s) to bookmark").action(async (uris) => {
1245
+ const agent = await getClient(program2);
1246
+ for (const uri of uris) {
1247
+ const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
1248
+ const parts = atUri.split("/");
1249
+ const rkey = parts[parts.length - 1];
1250
+ const collection = parts[parts.length - 2];
1251
+ const did = parts[2];
1252
+ const record = await agent.com.atproto.repo.getRecord({
1253
+ repo: did,
1254
+ collection,
1255
+ rkey
1256
+ });
1257
+ await agent.app.bsky.bookmark.createBookmark({
1258
+ uri: record.data.uri,
1259
+ cid: record.data.cid
1260
+ });
1261
+ console.log(record.data.uri);
1262
+ }
1263
+ });
1264
+ bookmarks.command("delete").description("Remove a bookmark").argument("<uri...>", "Post URI(s) to unbookmark").action(async (uris) => {
1265
+ const agent = await getClient(program2);
1266
+ for (const uri of uris) {
1267
+ const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
1268
+ await agent.app.bsky.bookmark.deleteBookmark({ uri: atUri });
1269
+ console.log(atUri);
1270
+ }
1271
+ });
1272
+ bookmarks.command("get").description("List bookmarked posts").option("-n, --count <number>", "Number of bookmarks to show", "50").action(async (opts) => {
1273
+ const agent = await getClient(program2);
1274
+ const json = isJson(program2);
1275
+ const limit = parseInt(opts.count, 10);
1276
+ let cursor;
1277
+ let remaining = limit;
1278
+ do {
1279
+ const resp = await agent.app.bsky.bookmark.getBookmarks({
1280
+ limit: Math.min(remaining, 50),
1281
+ cursor
1282
+ });
1283
+ for (const bookmark of resp.data.bookmarks) {
1284
+ if (json) {
1285
+ outputJson(bookmark);
1286
+ } else if (AppBskyFeedDefs.isPostView(bookmark.item)) {
1287
+ printPost(bookmark.item);
1288
+ }
1289
+ }
1290
+ remaining -= resp.data.bookmarks.length;
1291
+ cursor = resp.data.cursor;
1292
+ } while (cursor && remaining > 0);
1293
+ });
1294
+ }
1295
+
1296
+ // src/commands/password.ts
1297
+ function registerAppPassword(program2) {
1298
+ const appPw = program2.command("app-password").description("Manage app passwords");
1299
+ appPw.command("list").description("List app passwords").action(async () => {
1300
+ const agent = await getClient(program2);
1301
+ const json = isJson(program2);
1302
+ const resp = await agent.com.atproto.server.listAppPasswords();
1303
+ const passwords = resp.data.passwords;
1304
+ if (json) {
1305
+ for (const pw of passwords) outputJson(pw);
1306
+ return;
1307
+ }
1308
+ for (const pw of passwords) {
1309
+ console.log(`${pw.name} (${pw.createdAt})`);
1310
+ }
1311
+ });
1312
+ appPw.command("add").description("Create an app password").argument("<name>", "Password name").action(async (name) => {
1313
+ const agent = await getClient(program2);
1314
+ const json = isJson(program2);
1315
+ const resp = await agent.com.atproto.server.createAppPassword({
1316
+ name
1317
+ });
1318
+ if (json) {
1319
+ outputJson(resp.data);
1320
+ } else {
1321
+ console.log(`${resp.data.name}: ${resp.data.password}`);
1322
+ }
1323
+ });
1324
+ appPw.command("revoke").description("Revoke an app password").argument("<name>", "Password name").action(async (name) => {
1325
+ const agent = await getClient(program2);
1326
+ await agent.com.atproto.server.revokeAppPassword({ name });
1327
+ });
1328
+ }
1329
+
1330
+ // src/commands/invite.ts
1331
+ import chalk6 from "chalk";
1332
+ function registerInviteCodes(program2) {
1333
+ program2.command("invite-codes").description("Show invite codes").option("--used", "Show used codes too").action(async (opts) => {
1334
+ const agent = await getClient(program2);
1335
+ const json = isJson(program2);
1336
+ const resp = await agent.com.atproto.server.getAccountInviteCodes({
1337
+ includeUsed: opts.used ?? false
1338
+ });
1339
+ const codes = resp.data.codes;
1340
+ if (json) {
1341
+ for (const c of codes) outputJson(c);
1342
+ return;
1343
+ }
1344
+ for (const c of codes) {
1345
+ if (c.uses.length >= c.available) {
1346
+ console.log(chalk6.magentaBright(`${c.code} (used)`));
1347
+ } else {
1348
+ console.log(c.code);
1349
+ }
1350
+ }
1351
+ });
1352
+ }
1353
+
1354
+ // src/commands/completions.ts
1355
+ function getCommands(program2) {
1356
+ return program2.commands.map((cmd) => ({
1357
+ name: cmd.name(),
1358
+ aliases: cmd.aliases(),
1359
+ description: cmd.description(),
1360
+ options: cmd.options.map((opt) => ({
1361
+ short: opt.short?.replace(/^-/, "") || void 0,
1362
+ long: opt.long?.replace(/^--/, "") || void 0,
1363
+ description: opt.description
1364
+ }))
1365
+ }));
1366
+ }
1367
+ function getGlobalOptions(program2) {
1368
+ return program2.options.map((opt) => ({
1369
+ short: opt.short?.replace(/^-/, "") || void 0,
1370
+ long: opt.long?.replace(/^--/, "") || void 0,
1371
+ description: opt.description
1372
+ }));
1373
+ }
1374
+ function optionFlag(opt) {
1375
+ return opt.long ? `--${opt.long}` : `-${opt.short}`;
1376
+ }
1377
+ function generateBash(program2) {
1378
+ const commands = getCommands(program2);
1379
+ const globalOpts = getGlobalOptions(program2);
1380
+ const cmdNames = commands.flatMap((c) => [c.name, ...c.aliases]).join(" ");
1381
+ const globalFlags = globalOpts.map((o) => optionFlag(o)).join(" ");
1382
+ const caseClauses = commands.map((cmd) => {
1383
+ const names = [cmd.name, ...cmd.aliases];
1384
+ const opts = cmd.options.map((o) => optionFlag(o)).join(" ");
1385
+ return names.map((n) => ` ${n}) opts="${opts}" ;;`).join("\n");
1386
+ }).join("\n");
1387
+ const subcommandClauses = commands.filter((cmd) => {
1388
+ const programCmd = program2.commands.find((c) => c.name() === cmd.name);
1389
+ return programCmd && programCmd.commands.length > 0;
1390
+ }).map((cmd) => {
1391
+ const programCmd = program2.commands.find((c) => c.name() === cmd.name);
1392
+ const subNames = programCmd.commands.map((sub) => sub.name()).join(" ");
1393
+ return ` ${cmd.name}) opts="${subNames}" ;;`;
1394
+ }).join("\n");
1395
+ return `#!/bin/bash
1396
+
1397
+ _bsky_completions() {
1398
+ local cur prev commands global_opts opts
1399
+ COMPREPLY=()
1400
+ cur="\${COMP_WORDS[COMP_CWORD]}"
1401
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
1402
+ commands="${cmdNames}"
1403
+ global_opts="${globalFlags} --help --version"
1404
+
1405
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
1406
+ COMPREPLY=( $(compgen -W "\${commands} \${global_opts}" -- "\${cur}") )
1407
+ return 0
1408
+ fi
1409
+
1410
+ case "\${COMP_WORDS[1]}" in
1411
+ ${caseClauses}
1412
+ ${subcommandClauses}
1413
+ *) opts="" ;;
1414
+ esac
1415
+
1416
+ COMPREPLY=( $(compgen -W "\${opts} --help" -- "\${cur}") )
1417
+ return 0
1418
+ }
1419
+
1420
+ complete -F _bsky_completions bsky
1421
+ `;
1422
+ }
1423
+ function generateZsh(program2) {
1424
+ const commands = getCommands(program2);
1425
+ const globalOpts = getGlobalOptions(program2);
1426
+ const cmdList = commands.map((c) => {
1427
+ const desc = c.description.replace(/'/g, "'\\''");
1428
+ return ` '${c.name}:${desc}'`;
1429
+ }).join("\n");
1430
+ const globalOptsList = globalOpts.map((o) => {
1431
+ const desc = o.description.replace(/'/g, "'\\''");
1432
+ return ` '${optionFlag(o)}[${desc}]'`;
1433
+ }).join("\n");
1434
+ const subcmdFunctions = commands.map((cmd) => {
1435
+ const opts = cmd.options.map((o) => {
1436
+ const desc = o.description.replace(/'/g, "'\\''");
1437
+ return ` '${optionFlag(o)}[${desc}]'`;
1438
+ }).join("\n");
1439
+ const programCmd = program2.commands.find((c) => c.name() === cmd.name);
1440
+ const subCommands = programCmd?.commands ?? [];
1441
+ if (subCommands.length > 0) {
1442
+ const subList = subCommands.map((sub) => {
1443
+ const desc = sub.description().replace(/'/g, "'\\''");
1444
+ return ` '${sub.name()}:${desc}'`;
1445
+ }).join("\n");
1446
+ return ` ${cmd.name})
1447
+ local -a subcommands
1448
+ subcommands=(
1449
+ ${subList}
1450
+ )
1451
+ _describe 'subcommand' subcommands
1452
+ ;;`;
1453
+ }
1454
+ if (!opts) return ` ${cmd.name}) ;;`;
1455
+ return ` ${cmd.name})
1456
+ _arguments \\
1457
+ ${opts}
1458
+ ;;`;
1459
+ }).join("\n");
1460
+ return `#compdef bsky
1461
+
1462
+ _bsky() {
1463
+ local -a commands
1464
+ commands=(
1465
+ ${cmdList}
1466
+ )
1467
+
1468
+ _arguments \\
1469
+ ${globalOptsList} \\
1470
+ '1:command:->command' \\
1471
+ '*::arg:->args'
1472
+
1473
+ case $state in
1474
+ command)
1475
+ _describe 'command' commands
1476
+ ;;
1477
+ args)
1478
+ case $words[1] in
1479
+ ${subcmdFunctions}
1480
+ esac
1481
+ ;;
1482
+ esac
1483
+ }
1484
+
1485
+ _bsky "$@"
1486
+ `;
1487
+ }
1488
+ function generateFish(program2) {
1489
+ const commands = getCommands(program2);
1490
+ const globalOpts = getGlobalOptions(program2);
1491
+ const lines = [
1492
+ "# Fish completions for bsky",
1493
+ "# Generated by: bsky completions fish",
1494
+ "",
1495
+ "# Disable file completions by default",
1496
+ "complete -c bsky -f",
1497
+ ""
1498
+ ];
1499
+ for (const opt of globalOpts) {
1500
+ const desc = opt.description.replace(/'/g, "\\'");
1501
+ const parts = [`complete -c bsky -n '__fish_use_subcommand'`];
1502
+ if (opt.long) parts.push(`-l ${opt.long}`);
1503
+ if (opt.short) parts.push(`-s ${opt.short}`);
1504
+ parts.push(`-d '${desc}'`);
1505
+ lines.push(parts.join(" "));
1506
+ }
1507
+ lines.push("");
1508
+ for (const cmd of commands) {
1509
+ const desc = cmd.description.replace(/'/g, "\\'");
1510
+ lines.push(
1511
+ `complete -c bsky -n '__fish_use_subcommand' -a ${cmd.name} -d '${desc}'`
1512
+ );
1513
+ for (const alias of cmd.aliases) {
1514
+ lines.push(
1515
+ `complete -c bsky -n '__fish_use_subcommand' -a ${alias} -d '${desc}'`
1516
+ );
1517
+ }
1518
+ for (const opt of cmd.options) {
1519
+ const optDesc = opt.description.replace(/'/g, "\\'");
1520
+ const parts = [`complete -c bsky -n '__fish_seen_subcommand_from ${cmd.name}'`];
1521
+ if (opt.long) parts.push(`-l ${opt.long}`);
1522
+ if (opt.short) parts.push(`-s ${opt.short}`);
1523
+ parts.push(`-d '${optDesc}'`);
1524
+ lines.push(parts.join(" "));
1525
+ }
1526
+ const programCmd = program2.commands.find((c) => c.name() === cmd.name);
1527
+ if (programCmd && programCmd.commands.length > 0) {
1528
+ for (const sub of programCmd.commands) {
1529
+ const subDesc = sub.description().replace(/'/g, "\\'");
1530
+ lines.push(
1531
+ `complete -c bsky -n '__fish_seen_subcommand_from ${cmd.name}' -a ${sub.name()} -d '${subDesc}'`
1532
+ );
1533
+ }
1534
+ }
1535
+ }
1536
+ lines.push("");
1537
+ return lines.join("\n");
1538
+ }
1539
+ function registerCompletions(program2) {
1540
+ program2.command("completions").description("Generate shell completion script").argument("<shell>", "Shell type: bash, zsh, or fish").action((shell) => {
1541
+ switch (shell) {
1542
+ case "bash":
1543
+ process.stdout.write(generateBash(program2));
1544
+ break;
1545
+ case "zsh":
1546
+ process.stdout.write(generateZsh(program2));
1547
+ break;
1548
+ case "fish":
1549
+ process.stdout.write(generateFish(program2));
1550
+ break;
1551
+ default:
1552
+ console.error(
1553
+ `Unknown shell: ${shell}. Supported: bash, zsh, fish`
1554
+ );
1555
+ process.exit(1);
1556
+ }
1557
+ });
1558
+ }
1559
+
1560
+ // src/index.ts
1561
+ var program = new Command();
1562
+ program.name("bsky").description("A CLI client for Bluesky").version("1.0.0").option("--json", "Output as JSON").option("-p, --profile <name>", "Profile name").option("-v, --verbose", "Verbose output");
1563
+ function resolveProfile(program2) {
1564
+ return program2.opts().profile ?? process.env.BSKY_PROFILE;
1565
+ }
1566
+ async function getClient(program2) {
1567
+ const profile = resolveProfile(program2);
1568
+ const config = await loadConfig(profile);
1569
+ const prefix = profile ? `${profile}-` : "";
1570
+ return createClient(config, prefix);
1571
+ }
1572
+ async function getConfig(program2) {
1573
+ const profile = resolveProfile(program2);
1574
+ return loadConfig(profile);
1575
+ }
1576
+ function isJson(program2) {
1577
+ return program2.opts().json === true;
1578
+ }
1579
+ registerLogin(program);
1580
+ registerTimeline(program);
1581
+ registerStream(program);
1582
+ registerThread(program);
1583
+ registerPost(program);
1584
+ registerReply(program);
1585
+ registerQuote(program);
1586
+ registerDelete(program);
1587
+ registerLike(program);
1588
+ registerLikes(program);
1589
+ registerRepost(program);
1590
+ registerReposts(program);
1591
+ registerFollow(program);
1592
+ registerUnfollow(program);
1593
+ registerFollows(program);
1594
+ registerFollowers(program);
1595
+ registerBlock(program);
1596
+ registerUnblock(program);
1597
+ registerBlocks(program);
1598
+ registerMute(program);
1599
+ registerSearch(program);
1600
+ registerSearchUsers(program);
1601
+ registerProfile(program);
1602
+ registerProfileUpdate(program);
1603
+ registerSession(program);
1604
+ registerNotifs(program);
1605
+ registerReport(program);
1606
+ registerModList(program);
1607
+ registerBookmarks(program);
1608
+ registerAppPassword(program);
1609
+ registerInviteCodes(program);
1610
+ registerCompletions(program);
1611
+ program.parseAsync(process.argv).catch((err) => {
1612
+ console.error(err.message ?? err);
1613
+ process.exit(1);
1614
+ });
1615
+ export {
1616
+ getClient,
1617
+ getConfig,
1618
+ isJson
1619
+ };
1620
+ //# sourceMappingURL=index.js.map