···287 defaultValue: "7",
288 placeholder: "7",
289 validate: (value) => {
290- const num = parseInt(value, 10);
000291 if (Number.isNaN(num) || num < 1) {
292 return "Please enter a positive number";
293 }
···287 defaultValue: "7",
288 placeholder: "7",
289 validate: (value) => {
290+ if (!value) {
291+ return "Please enter a number";
292+ }
293+ const num = Number.parseInt(value, 10);
294 if (Number.isNaN(num) || num < 1) {
295 return "Please enter a positive number";
296 }
-1
packages/cli/src/commands/login.ts
···4import { resolveHandleToDid } from "../lib/atproto";
5import {
6 getCallbackPort,
7- getCallbackUrl,
8 getOAuthClient,
9 getOAuthScope,
10} from "../lib/oauth-client";
···4import { resolveHandleToDid } from "../lib/atproto";
5import {
6 getCallbackPort,
07 getOAuthClient,
8 getOAuthScope,
9} from "../lib/oauth-client";
+1-1
packages/cli/src/commands/publish.ts
···209 let agent: Awaited<ReturnType<typeof createAgent>> | undefined;
210 try {
211 agent = await createAgent(credentials);
212- s.stop(`Logged in as ${agent.session?.handle}`);
213 } catch (error) {
214 s.stop("Failed to login");
215 log.error(`Failed to login: ${error}`);
···209 let agent: Awaited<ReturnType<typeof createAgent>> | undefined;
210 try {
211 agent = await createAgent(credentials);
212+ s.stop(`Logged in as ${agent.did}`);
213 } catch (error) {
214 s.stop("Failed to login");
215 log.error(`Failed to login: ${error}`);
+1-1
packages/cli/src/commands/sync.ts
···76 let agent: Awaited<ReturnType<typeof createAgent>> | undefined;
77 try {
78 agent = await createAgent(credentials);
79- s.stop(`Logged in as ${agent.session?.handle}`);
80 } catch (error) {
81 s.stop("Failed to login");
82 log.error(`Failed to login: ${error}`);
···76 let agent: Awaited<ReturnType<typeof createAgent>> | undefined;
77 try {
78 agent = await createAgent(credentials);
79+ s.stop(`Logged in as ${agent.did}`);
80 } catch (error) {
81 s.stop("Failed to login");
82 log.error(`Failed to login: ${error}`);
+35-29
packages/cli/src/lib/atproto.ts
···13} from "./types";
14import { isAppPasswordCredentials, isOAuthCredentials } from "./types";
15000000000000000016async function fileExists(filePath: string): Promise<boolean> {
17 try {
18 await fs.access(filePath);
···96 showInDiscover?: boolean;
97}
9899-export async function createAgent(credentials: Credentials): Promise<AtpAgent> {
100 if (isOAuthCredentials(credentials)) {
101 // OAuth flow - restore session from stored tokens
102 const client = await getOAuthClient();
103 try {
104 const oauthSession = await client.restore(credentials.did);
105 // Wrap the OAuth session in an Agent which provides the atproto API
106- const agent = new Agent(oauthSession) as unknown as AtpAgent;
107-108- // The Agent class doesn't have session.did like AtpAgent does
109- // We need to set up a compatible session object for the rest of our code
110- agent.session = {
111- did: oauthSession.did,
112- handle: credentials.handle,
113- accessJwt: "",
114- refreshJwt: "",
115- active: true,
116- };
117-118- return agent;
119 } catch (error) {
120 if (error instanceof Error) {
121 // Check for common OAuth errors
···147}
148149export async function uploadImage(
150- agent: AtpAgent,
151 imagePath: string,
152): Promise<BlobObject | undefined> {
153 if (!(await fileExists(imagePath))) {
···216}
217218export async function createDocument(
219- agent: AtpAgent,
220 post: BlogPost,
221 config: PublisherConfig,
222 coverImage?: BlobObject,
···259 }
260261 const response = await agent.com.atproto.repo.createRecord({
262- repo: agent.session!.did,
263 collection: "site.standard.document",
264 record,
265 });
···268}
269270export async function updateDocument(
271- agent: AtpAgent,
272 post: BlogPost,
273 atUri: string,
274 config: PublisherConfig,
···321 }
322323 await agent.com.atproto.repo.putRecord({
324- repo: agent.session!.did,
325 collection: collection!,
326 rkey: rkey!,
327 record,
···361}
362363export async function listDocuments(
364- agent: AtpAgent,
365 publicationUri?: string,
366): Promise<ListDocumentsResult[]> {
367 const documents: ListDocumentsResult[] = [];
···369370 do {
371 const response = await agent.com.atproto.repo.listRecords({
372- repo: agent.session!.did,
373 collection: "site.standard.document",
374 limit: 100,
375 cursor,
376 });
377378 for (const record of response.data.records) {
379- const value = record.value as unknown as DocumentRecord;
00380381 // If publicationUri is specified, only include documents from that publication
382- if (publicationUri && value.site !== publicationUri) {
383 continue;
384 }
385386 documents.push({
387 uri: record.uri,
388 cid: record.cid,
389- value,
390 });
391 }
392···397}
398399export async function createPublication(
400- agent: AtpAgent,
401 options: CreatePublicationOptions,
402): Promise<string> {
403 let icon: BlobObject | undefined;
···428 }
429430 const response = await agent.com.atproto.repo.createRecord({
431- repo: agent.session!.did,
432 collection: "site.standard.publication",
433 record,
434 });
···481 * Create a Bluesky post with external link embed
482 */
483export async function createBlueskyPost(
484- agent: AtpAgent,
485 options: CreateBlueskyPostOptions,
486): Promise<StrongRef> {
487 const { title, description, canonicalUrl, coverImage, publishedAt } = options;
···576 };
577578 const response = await agent.com.atproto.repo.createRecord({
579- repo: agent.session!.did,
580 collection: "app.bsky.feed.post",
581 record,
582 });
···591 * Add bskyPostRef to an existing document record
592 */
593export async function addBskyPostRefToDocument(
594- agent: AtpAgent,
595 documentAtUri: string,
596 bskyPostRef: StrongRef,
597): Promise<void> {
···13} from "./types";
14import { isAppPasswordCredentials, isOAuthCredentials } from "./types";
1516+/**
17+ * Type guard to check if a record value is a DocumentRecord
18+ */
19+function isDocumentRecord(value: unknown): value is DocumentRecord {
20+ if (!value || typeof value !== "object") return false;
21+ const v = value as Record<string, unknown>;
22+ return (
23+ v.$type === "site.standard.document" &&
24+ typeof v.title === "string" &&
25+ typeof v.site === "string" &&
26+ typeof v.path === "string" &&
27+ typeof v.textContent === "string" &&
28+ typeof v.publishedAt === "string"
29+ );
30+}
31+32async function fileExists(filePath: string): Promise<boolean> {
33 try {
34 await fs.access(filePath);
···112 showInDiscover?: boolean;
113}
114115+export async function createAgent(credentials: Credentials): Promise<Agent> {
116 if (isOAuthCredentials(credentials)) {
117 // OAuth flow - restore session from stored tokens
118 const client = await getOAuthClient();
119 try {
120 const oauthSession = await client.restore(credentials.did);
121 // Wrap the OAuth session in an Agent which provides the atproto API
122+ return new Agent(oauthSession);
000000000000123 } catch (error) {
124 if (error instanceof Error) {
125 // Check for common OAuth errors
···151}
152153export async function uploadImage(
154+ agent: Agent,
155 imagePath: string,
156): Promise<BlobObject | undefined> {
157 if (!(await fileExists(imagePath))) {
···220}
221222export async function createDocument(
223+ agent: Agent,
224 post: BlogPost,
225 config: PublisherConfig,
226 coverImage?: BlobObject,
···263 }
264265 const response = await agent.com.atproto.repo.createRecord({
266+ repo: agent.did!,
267 collection: "site.standard.document",
268 record,
269 });
···272}
273274export async function updateDocument(
275+ agent: Agent,
276 post: BlogPost,
277 atUri: string,
278 config: PublisherConfig,
···325 }
326327 await agent.com.atproto.repo.putRecord({
328+ repo: agent.did!,
329 collection: collection!,
330 rkey: rkey!,
331 record,
···365}
366367export async function listDocuments(
368+ agent: Agent,
369 publicationUri?: string,
370): Promise<ListDocumentsResult[]> {
371 const documents: ListDocumentsResult[] = [];
···373374 do {
375 const response = await agent.com.atproto.repo.listRecords({
376+ repo: agent.did!,
377 collection: "site.standard.document",
378 limit: 100,
379 cursor,
380 });
381382 for (const record of response.data.records) {
383+ if (!isDocumentRecord(record.value)) {
384+ continue;
385+ }
386387 // If publicationUri is specified, only include documents from that publication
388+ if (publicationUri && record.value.site !== publicationUri) {
389 continue;
390 }
391392 documents.push({
393 uri: record.uri,
394 cid: record.cid,
395+ value: record.value,
396 });
397 }
398···403}
404405export async function createPublication(
406+ agent: Agent,
407 options: CreatePublicationOptions,
408): Promise<string> {
409 let icon: BlobObject | undefined;
···434 }
435436 const response = await agent.com.atproto.repo.createRecord({
437+ repo: agent.did!,
438 collection: "site.standard.publication",
439 record,
440 });
···487 * Create a Bluesky post with external link embed
488 */
489export async function createBlueskyPost(
490+ agent: Agent,
491 options: CreateBlueskyPostOptions,
492): Promise<StrongRef> {
493 const { title, description, canonicalUrl, coverImage, publishedAt } = options;
···582 };
583584 const response = await agent.com.atproto.repo.createRecord({
585+ repo: agent.did!,
586 collection: "app.bsky.feed.post",
587 record,
588 });
···597 * Add bskyPostRef to an existing document record
598 */
599export async function addBskyPostRefToDocument(
600+ agent: Agent,
601 documentAtUri: string,
602 bskyPostRef: StrongRef,
603): Promise<void> {
+3-8
packages/cli/src/lib/credentials.ts
···95 }
96 }
9798- // Otherwise, check all OAuth sessions to find a matching handle
99- // (This is a fallback - handle matching isn't perfect without storing handles)
100- const sessions = await listOAuthSessions();
101- for (const did of sessions) {
102- // Could enhance this by storing handle with session, but for now
103- // just return null if profile isn't a DID
104- }
105-106 return null;
107}
108
···95 }
96 }
9798+ // Otherwise, we would need to check all OAuth sessions to find a matching handle,
99+ // but handle matching isn't perfect without storing handles alongside sessions.
100+ // For now, just return null if profile isn't a DID.
00000101 return null;
102}
103
+5-2
packages/cli/src/lib/oauth-client.ts
···18// This prevents the "No lock mechanism provided" warning
19const locks = new Map<string, Promise<void>>();
2021-async function requestLock(key: string, fn: () => Promise<void>): Promise<void> {
00022 // Wait for any existing lock on this key
23 while (locks.has(key)) {
24 await locks.get(key);
···32 locks.set(key, lockPromise);
3334 try {
35- await fn();
36 } finally {
37 locks.delete(key);
38 resolve!();
···18// This prevents the "No lock mechanism provided" warning
19const locks = new Map<string, Promise<void>>();
2021+async function requestLock<T>(
22+ key: string,
23+ fn: () => T | PromiseLike<T>,
24+): Promise<T> {
25 // Wait for any existing lock on this key
26 while (locks.has(key)) {
27 await locks.get(key);
···35 locks.set(key, lockPromise);
3637 try {
38+ return await fn();
39 } finally {
40 locks.delete(key);
41 resolve!();