Skip to content
← Public packages

@kentcdodds/bluesky

Bluesky and AT Protocol helpers using package-storage handle plus blueskyAppPassword.

AGENTS.md

135 lines · 4.9 KB · Markdown

@kentcdodds/bluesky — agent notes

Human setup and intent live in README.md. This file is for agents: imports, smoke/dryRun snippets, and edge cases. Secrets by name only — never paste values. Do not disable live webhooks or jobs.

Secrets / storage

KindNameNotes
User secretblueskyAppPasswordAT Protocol session create
Package storageblueskyHandleDefault actor/handle

Defaults: auth service https://bsky.social; public AppView https://public.api.bsky.app. Pass service to override, or authenticated: false to skip the session on reads that allow it.

Import paths

ExportImport
overviewkody:@kentcdodds/bluesky
smoke-testkody:@kentcdodds/bluesky/smoke-test
requestkody:@kentcdodds/bluesky/request
resolve-handlekody:@kentcdodds/bluesky/resolve-handle
get-profilekody:@kentcdodds/bluesky/get-profile
search-postskody:@kentcdodds/bluesky/search-posts
get-author-feedkody:@kentcdodds/bluesky/get-author-feed
get-post-threadkody:@kentcdodds/bluesky/get-post-thread
list-notificationskody:@kentcdodds/bluesky/list-notifications
create-postkody:@kentcdodds/bluesky/create-post
reply-postkody:@kentcdodds/bluesky/reply-post
delete-postkody:@kentcdodds/bluesky/delete-post
like-postkody:@kentcdodds/bluesky/like-post
repostkody:@kentcdodds/bluesky/repost
followkody:@kentcdodds/bluesky/follow
unfollowkody:@kentcdodds/bluesky/unfollow

Prefer static kody:@kentcdodds/bluesky/... imports from execute. Do not lead with packages.invoke.

Smoke test (read-only)

import smokeTest from 'kody:@kentcdodds/bluesky/smoke-test'

export default async function main() {
	return await smokeTest()
	// => { ok: true, did, handle, profile }
}

Read helper:

import getProfile from 'kody:@kentcdodds/bluesky/get-profile'

export default async function main() {
	return await getProfile({ actor: 'kentcdodds.com' })
}

dryRun / confirm (writes)

Write helpers dry-run unless confirm: true after explicit user approval:

import createPost from 'kody:@kentcdodds/bluesky/create-post'

export default async function main() {
	return await createPost({
		text: 'Watch this: https://example.com',
		externalCard: {
			uri: 'https://example.com',
			title: 'Example video',
			description: 'A useful video.',
			thumbnailUrl: 'https://example.com/thumbnail.jpg',
		},
		dryRun: true,
	})
	// => { dryRun: true, action: 'create-post', payload: { ... } }
}

Same guard pattern for delete-post, like-post, repost, follow, unfollow. Mutating request bodies also dry-run without confirm: true.

Inbound webhooks

Declared webhooks (do not disable):

Webhook nameExport
create-post./create-post
delete-post./delete-post
get-post-thread./get-post-thread

Inbound POSTs map to those exports with inputMode: params. Mutating webhooks still honor confirm / dryRun — never treat a webhook hit as auto-approval to publish. Minted webhook URLs are credentials; never paste them in chat.

Edge cases

  • create-post auto-adds clickable facets for HTTP(S) URLs unless callers pass facets or autoLinkFacets: false. Facet indexes are UTF-8 byte offsets.
  • externalCard.thumbnailUrl: helper fetches, fits under Bluesky’s 1 MB blob limit (Cloudinary JPEG derivative when needed), then uploads. Dry-runs report originalBytes, uploadBytes, mimeType, fitted instead of uploading.
  • search-posts defaults to authenticated search: session on bsky.social, then api.bsky.app with the JWT (public AppView search is blocked from Kody workers; do not createSession against api.bsky.app — the app password is not approved for that host).
  • Smoke/auth must never return session JWTs or app-password values.
  • Always prefer dry-run before any live write; smoke is not permission to post.

Replies (do not silently post standalone)

Bluesky expects reply: { root: { uri, cid }, parent: { uri, cid } }. create-post maps aliases (replyTo, parent+root, parentUri/parentCid/ rootUri/rootCid, replyToUri/replyToCid) and rejects unknown keys. Dry-run / confirm results include mode: 'reply' | 'standalone'. Prefer ./reply-post when the user asked to reply — it refuses without parent refs.

Input schema (delete/like/follow/repost)

./delete-post, ./like-post, ./repost, ./follow, and ./unfollow parse with Remix Schema (unknownKeys: 'error'). Aliases:

  • delete: uri ← postUri / atUri / post (or repo+rkey)
  • like/repost: uri+cid ← postUri/atUri/post + postCid, or subject: {uri,cid}
  • follow/unfollow: actor ← handle / did / user / username (unfollow also uri/followUri)

Unknown keys get did-you-mean hints. Prefer dryRun: true before live writes.