Bluesky Account Management
Operate a Bluesky social media account via ./bsky <command> [args]. All output is JSON. Run from the project root.
Setup
Install dependencies:
CODEBLOCK0
Requires .env at project root:
BLUESKY_HANDLE=your-handle.bsky.social
BLUESKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
App passwords:
https://bsky.app/settings/app-passwords. For DMs, enable "Allow access to your direct messages".
Auth is automatic. A session cache is stored at ~/.bsky_session.json (contains an exported session token). Delete this file to force re-authentication or when revoking access.
JSON Output
Every command prints one JSON object to stdout. Parse with json.loads().
Response Schemas
Post object (returned by get, and inside arrays from timeline, search-posts, my-posts, thread):
CODEBLOCK2
Profile object (returned by profile, and inside arrays from search-users):
CODEBLOCK3
Actor object (short profile, inside post authors, follower/following lists, notification authors):
CODEBLOCK4
Notification object (inside array from notifications):
CODEBLOCK5
Conversation object (inside array from dm-list):
CODEBLOCK6
DM message object (inside array from dm-read):
CODEBLOCK7
Feed object (inside array from feeds):
CODEBLOCK8
Command Response Keys
Each command returns these top-level keys:
| Command | Response keys |
|---|
| INLINECODE16 | INLINECODE17 |
| INLINECODE18 |
{"deleted"} (the URI) |
|
like |
{"liked", "uri"} (post URI + like record URI) |
|
unlike |
{"unliked"} |
|
repost |
{"reposted", "uri"} (post URI + repost record URI) |
|
unrepost |
{"unreposted"} |
|
timeline |
{"feed": [{"post": <post>, "reason": {"type": "repost", "by": <actor>} or null}], "cursor"} |
|
thread |
{"thread": <post with nested "replies": [...]>} |
|
search-posts |
{"posts": [<post>], "cursor"} |
|
search-users |
{"actors": [<profile>], "cursor"} |
|
follow |
{"followed", "uri"} |
|
unfollow |
{"unfollowed"} |
|
followers |
{"followers": [<actor>], "cursor"} |
|
following |
{"following": [<actor>], "cursor"} |
|
mute |
{"muted"} |
|
unmute |
{"unmuted"} |
|
block |
{"blocked", "uri"} |
|
unblock |
{"unblocked"} |
|
profile |
<profile> (top-level, no wrapper) |
|
get |
<post> (top-level, no wrapper) |
|
my-posts |
{"posts": [<post>], "cursor"} |
|
user-posts |
{"posts": [<post>], "cursor"} |
|
likes |
{"likes": [{"actor": <actor>, "created_at": "..."}], "cursor"} |
|
reposts |
{"reposted_by": [<actor>], "cursor"} |
|
notifications |
{"notifications": [<notification>], "cursor"} |
|
notif-read |
{"success": true} |
|
dm-list |
{"conversations": [<convo>], "cursor"} |
|
dm-read |
{"convo_id", "messages": [<dm>], "cursor"} |
|
dm-send |
{"sent": true, "convo_id", "message_id"} |
|
dm-mark-read |
{"success": true} |
|
update-profile |
<profile> (top-level, no wrapper) |
|
post-thread |
{"posts": [{"uri", "cid"}, ...]} |
|
feeds |
{"feeds": [<feed>], "cursor"} |
Important: Note that timeline wraps posts in feed[].post (with an optional reason), while search-posts and my-posts use posts[] directly.
Pagination
List commands support --cursor TOKEN. The response includes "cursor" (null = no more results).
- 1. First call: omit INLINECODE90
- Next page: pass the returned cursor as INLINECODE91
- Stop when cursor is null
Errors
Errors return JSON with exit code 1:
CODEBLOCK9
Error types: NOT_FOUND, NOT_LIKED, NOT_REPOSTED, NOT_FOLLOWING, NOT_BLOCKING, FILE_NOT_FOUND, INVALID_ARGS, AUTH_ERROR.
Command Quick Reference
Posting
| Command | Description |
|---|
| INLINECODE100 | Create a text post (max 300 graphemes) |
| INLINECODE101 |
Post with image (repeat
--image/
--alt for up to 4) |
|
post "text" --reply-to <uri> | Reply to a post |
|
post "text" --quote <uri> | Quote a post |
|
post "text" --quote <uri> --image photo.jpg --alt "desc" | Quote with image |
|
post-thread "text1" "text2" "text3" | Create a multi-post thread |
|
delete <uri> | Delete a post |
Engagement
| Command | Description |
|---|
| INLINECODE109 | Like a post |
| INLINECODE110 |
Unlike (pass the post URI, not the like URI) |
|
repost <uri> | Repost a post |
|
unrepost <uri> | Undo repost (pass the post URI) |
Reading & Discovery
| Command | Description |
|---|
| INLINECODE113 | Home timeline (default 20) |
| INLINECODE114 |
Post thread with replies (default depth 6) |
|
search-posts "query" [--limit N] [--cursor TOKEN] | Search posts |
|
search-users "query" [--limit N] [--cursor TOKEN] | Search users |
|
feeds [--query "keyword"] [--limit N] [--cursor TOKEN] | Browse suggested feed generators (note:
--query filters client-side, so results may be fewer than
--limit) |
Social Graph
| Command | Description |
|---|
| INLINECODE120 | Follow |
| INLINECODE121 |
Unfollow |
|
followers <handle> [--limit N] [--cursor TOKEN] | List followers |
|
following <handle> [--limit N] [--cursor TOKEN] | List following |
|
mute <handle> /
unmute <handle> | Mute/unmute |
|
block <handle> /
unblock <handle> | Block/unblock |
Profile & Info
| Command | Description |
|---|
| INLINECODE128 | View profile (own if omitted) |
| INLINECODE129 |
Update your profile |
|
my-posts [--limit N] [--cursor TOKEN] | Own recent posts |
|
user-posts <handle> [--limit N] [--cursor TOKEN] | A user's recent posts |
|
get <uri> | Fetch a single post |
|
likes <uri> [--limit N] [--cursor TOKEN] | Who liked a post |
|
reposts <uri> [--limit N] [--cursor TOKEN] | Who reposted a post |
Notifications
| Command | Description |
|---|
| INLINECODE135 | List notifications (filter: like, repost, follow, mention, reply, quote) |
| INLINECODE136 |
Mark all as read |
Direct Messages
| Command | Description |
|---|
| INLINECODE137 | List conversations |
| INLINECODE138 |
Read messages with a user |
|
dm-read --convo-id <id> [--limit N] [--cursor TOKEN] | Read messages by convo ID |
|
dm-send <handle> "text" | Send a DM |
|
dm-mark-read --convo-id <id> | Mark convo as read |
|
dm-mark-read --all | Mark all as read |
Only text DMs are supported (no images).
Common Workflows
Check and respond to mentions
CODEBLOCK10
Engage with timeline
CODEBLOCK11
Search and engage with a topic
CODEBLOCK12
Join a conversation (read thread before replying)
CODEBLOCK13
Grow the network
CODEBLOCK14
Check engagement on own posts
CODEBLOCK15
Check and respond to DMs
CODEBLOCK16
Post a thread
CODEBLOCK17
Update your profile
CODEBLOCK18
Discover feeds
CODEBLOCK19
Check before posting (avoid duplicates)
CODEBLOCK20
Key Concepts
- - Handles: Always pass handles without the
@ prefix — use user.bsky.social, not @user.bsky.social. - URIs: Every post has an AT Protocol URI (
at://did:plc:abc/app.bsky.feed.post/xyz). Extract from the uri field in JSON. Used as arguments for like, reply, repost, thread, get, delete. - Rich text: @mentions, #hashtags, URLs in post text are auto-converted to links. Write naturally.
- Character limit: 300 graphemes per post.
- Unlike/unrepost: Pass the post URI, not the like/repost record URI. Auto-resolved internally.
- Reply threading:
--reply-to <uri> auto-resolves the thread root.
Auth Troubleshooting
Auth errors: {"error": "AUTH_ERROR", "type": "<TYPE>", "message": "..."} with exit code 1.
- 1. SESSIONCORRUPT →
rm ~/.bsky_session.json and retry - MISSINGENV → Ensure
.env has BLUESKY_HANDLE and INLINECODE153 - INVALIDCREDENTIALS → Handle:
user.bsky.social, App password: xxxx-xxxx-xxxx-xxxx (19 chars) - NETWORK → Retry up to 3 times with 10s delay
- ACCOUNTSUSPENDED → Inform user, cannot fix programmatically
Bluesky 账户管理
通过 ./bsky <命令> [参数] 操作 Bluesky 社交媒体账户。所有输出均为 JSON 格式。从项目根目录运行。
设置
安装依赖:
bash
pip install atproto python-dotenv
需要在项目根目录创建 .env 文件:
BLUESKY_HANDLE=你的用户名.bsky.social
BLUESKYAPPPASSWORD=xxxx-xxxx-xxxx-xxxx
应用密码获取地址:https://bsky.app/settings/app-passwords。如需使用私信功能,请启用允许访问您的私信。
认证过程自动完成。会话缓存存储在 ~/.bsky_session.json(包含导出的会话令牌)。删除此文件可强制重新认证或在撤销访问权限时使用。
JSON 输出
每个命令向标准输出打印一个 JSON 对象。使用 json.loads() 解析。
响应结构
帖子对象(由 get 返回,以及 timeline、search-posts、my-posts、thread 的数组内):
json
{
uri: at://did:plc:abc/app.bsky.feed.post/xyz,
cid: bafyrei...,
author: {handle: alice.bsky.social, did: did:plc:abc, display_name: Alice, avatar: https://...},
text: 帖子内容,
created_at: 2026-03-14T10:00:00Z,
likecount: 5, repostcount: 2, reply_count: 1,
viewer: {liked: at://...点赞URI或null, reposted: at://...转发URI或null},
embed: {images: [{alt: ..., thumb: ..., fullsize: ...}], external: {uri: ..., title: ..., description: ...}, record: {uri: ..., text: ..., author: {...}}} 或 null,
reply: {parenturi: at://..., rooturi: at://...} // 仅回复帖子时存在
}
个人资料对象(由 profile 返回,以及 search-users 的数组内):
json
{
handle: alice.bsky.social, did: did:plc:abc,
display_name: Alice, description: 个人简介文本, avatar: https://...,
followerscount: 100, followscount: 50, posts_count: 200,
viewer: {following: at://...或null, followedby: at://...或null, blocking: null, blockedby: null, muted: null}
}
用户对象(简短个人资料,出现在帖子作者、粉丝/关注列表、通知作者中):
json
{handle: alice.bsky.social, did: did:plc:abc, display_name: Alice, avatar: https://...}
通知对象(在 notifications 的数组内):
json
{
reason: reply|like|repost|follow|mention|quote,
uri: at://..., cid: bafyrei..., is_read: false,
indexed_at: 2026-03-14T10:00:00Z,
author: {handle: ..., did: ..., display_name: ..., avatar: ...},
record_text: 他们的回复/帖子文本(如适用),
reason_subject: at://...他们点赞/转发/回复的帖子(如适用),
subjecttext: 主题帖子的文本(如果存在 reasonsubject)
}
会话对象(在 dm-list 的数组内):
json
{
id: 会话ID, unread_count: 2,
members: [{handle: ..., did: ..., display_name: ..., avatar: ...}],
lastmessage: {id: 消息ID, text: ..., sentat: ..., sender_did: did:plc:...} 或 null
}
私信消息对象(在 dm-read 的数组内):
json
{id: 消息ID, text: 消息文本, sentat: 2026-03-14T10:00:00Z, senderdid: did:plc:...}
信息流对象(在 feeds 的数组内):
json
{
uri: at://did:plc:abc/app.bsky.feed.generator/whats-hot,
cid: bafyrei..., did: did:web:...,
creator: {handle: ..., did: ..., display_name: ..., avatar: ...},
display_name: 热门内容, description: Bluesky 上的热门帖子,
avatar: https://..., likecount: 12345, indexedat: 2026-03-14T10:00:00Z
}
命令响应键
每个命令返回以下顶级键:
| 命令 | 响应键 |
|---|
| post | {uri, cid} |
| delete |
{deleted}(URI) |
| like | {liked, uri}(帖子URI + 点赞记录URI) |
| unlike | {unliked} |
| repost | {reposted, uri}(帖子URI + 转发记录URI) |
| unrepost | {unreposted} |
| timeline | {feed: [{post: <帖子>, reason: {type: repost, by: <用户>} 或 null}], cursor} |
| thread | {thread: <帖子,包含嵌套的 replies: [...]>} |
| search-posts | {posts: [<帖子>], cursor} |
| search-users | {actors: [<个人资料>], cursor} |
| follow | {followed, uri} |
| unfollow | {unfollowed} |
| followers | {followers: [<用户>], cursor} |
| following | {following: [<用户>], cursor} |
| mute | {muted} |
| unmute | {unmuted} |
| block | {blocked, uri} |
| unblock | {unblocked} |
| profile | <个人资料>(顶级,无包装) |
| get | <帖子>(顶级,无包装) |
| my-posts | {posts: [<帖子>], cursor} |
| user-posts | {posts: [<帖子>], cursor} |
| likes | {likes: [{actor: <用户>, created_at: ...}], cursor} |
| reposts | {reposted_by: [<用户>], cursor} |
| notifications | {notifications: [<通知>], cursor} |
| notif-read | {success: true} |
| dm-list | {conversations: [<会话>], cursor} |
| dm-read | {convo_id, messages: [<私信>], cursor} |
| dm-send | {sent: true, convo
id, messageid} |
| dm-mark-read | {success: true} |
| update-profile | <个人资料>(顶级,无包装) |
| post-thread | {posts: [{uri, cid}, ...]} |
| feeds | {feeds: [<信息流>], cursor} |
重要提示: 注意 timeline 将帖子包装在 feed[].post 中(带有可选的 reason),而 search-posts 和 my-posts 直接使用 posts[]。
分页
列表命令支持 --cursor TOKEN。响应中包含 cursor(null = 没有更多结果)。
- 1. 首次调用:省略 --cursor
- 下一页:将返回的 cursor 作为