{"skill":{"slug":"bluesky-skill","displayName":"bluesky-skill","summary":"Manage a Bluesky (bsky) account — posting, replies, likes, reposts, follows, blocks, mutes, search, timeline, threads, notifications, DMs, and profile update...","description":"---\nname: bluesky-skill\ndescription: >-\n  Manage a Bluesky (bsky) account — posting, replies, likes, reposts, follows,\n  blocks, mutes, search, timeline, threads, notifications, DMs, and profile\n  updates via the AT Protocol.\nallowed-tools: Bash Read Edit Write Glob Grep\nmetadata:\n  openclaw:\n    requires:\n      env: [BLUESKY_HANDLE, BLUESKY_APP_PASSWORD]\n      bins: [python3]\n    primaryEnv: BLUESKY_HANDLE\n---\n\n# Bluesky Account Management\n\nOperate a Bluesky social media account via `./bsky <command> [args]`. All output is JSON. Run from the project root.\n\n## Setup\n\nInstall dependencies:\n```bash\npip install atproto python-dotenv\n```\n\nRequires `.env` at project root:\n```\nBLUESKY_HANDLE=your-handle.bsky.social\nBLUESKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx\n```\nApp passwords: `https://bsky.app/settings/app-passwords`. For DMs, enable \"Allow access to your direct messages\".\n\nAuth 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.\n\n## JSON Output\n\nEvery command prints one JSON object to stdout. Parse with `json.loads()`.\n\n### Response Schemas\n\n**Post object** (returned by `get`, and inside arrays from `timeline`, `search-posts`, `my-posts`, `thread`):\n```json\n{\n  \"uri\": \"at://did:plc:abc/app.bsky.feed.post/xyz\",\n  \"cid\": \"bafyrei...\",\n  \"author\": {\"handle\": \"alice.bsky.social\", \"did\": \"did:plc:abc\", \"display_name\": \"Alice\", \"avatar\": \"https://...\"},\n  \"text\": \"Post content here\",\n  \"created_at\": \"2026-03-14T10:00:00Z\",\n  \"like_count\": 5, \"repost_count\": 2, \"reply_count\": 1,\n  \"viewer\": {\"liked\": \"at://...like-uri or null\", \"reposted\": \"at://...repost-uri or null\"},\n  \"embed\": {\"images\": [{\"alt\": \"...\", \"thumb\": \"...\", \"fullsize\": \"...\"}], \"external\": {\"uri\": \"...\", \"title\": \"...\", \"description\": \"...\"}, \"record\": {\"uri\": \"...\", \"text\": \"...\", \"author\": {...}}} or null,\n  \"reply\": {\"parent_uri\": \"at://...\", \"root_uri\": \"at://...\"} // only present on replies\n}\n```\n\n**Profile object** (returned by `profile`, and inside arrays from `search-users`):\n```json\n{\n  \"handle\": \"alice.bsky.social\", \"did\": \"did:plc:abc\",\n  \"display_name\": \"Alice\", \"description\": \"Bio text\", \"avatar\": \"https://...\",\n  \"followers_count\": 100, \"follows_count\": 50, \"posts_count\": 200,\n  \"viewer\": {\"following\": \"at://...or null\", \"followed_by\": \"at://...or null\", \"blocking\": null, \"blocked_by\": null, \"muted\": null}\n}\n```\n\n**Actor object** (short profile, inside post authors, follower/following lists, notification authors):\n```json\n{\"handle\": \"alice.bsky.social\", \"did\": \"did:plc:abc\", \"display_name\": \"Alice\", \"avatar\": \"https://...\"}\n```\n\n**Notification object** (inside array from `notifications`):\n```json\n{\n  \"reason\": \"reply|like|repost|follow|mention|quote\",\n  \"uri\": \"at://...\", \"cid\": \"bafyrei...\", \"is_read\": false,\n  \"indexed_at\": \"2026-03-14T10:00:00Z\",\n  \"author\": {\"handle\": \"...\", \"did\": \"...\", \"display_name\": \"...\", \"avatar\": \"...\"},\n  \"record_text\": \"Their reply/post text (if applicable)\",\n  \"reason_subject\": \"at://...the post they liked/reposted/replied-to (if applicable)\",\n  \"subject_text\": \"Text of the subject post (if reason_subject exists)\"\n}\n```\n\n**Conversation object** (inside array from `dm-list`):\n```json\n{\n  \"id\": \"convo-id\", \"unread_count\": 2,\n  \"members\": [{\"handle\": \"...\", \"did\": \"...\", \"display_name\": \"...\", \"avatar\": \"...\"}],\n  \"last_message\": {\"id\": \"msg-id\", \"text\": \"...\", \"sent_at\": \"...\", \"sender_did\": \"did:plc:...\"} or null\n}\n```\n\n**DM message object** (inside array from `dm-read`):\n```json\n{\"id\": \"msg-id\", \"text\": \"Message text\", \"sent_at\": \"2026-03-14T10:00:00Z\", \"sender_did\": \"did:plc:...\"}\n```\n\n**Feed object** (inside array from `feeds`):\n```json\n{\n  \"uri\": \"at://did:plc:abc/app.bsky.feed.generator/whats-hot\",\n  \"cid\": \"bafyrei...\", \"did\": \"did:web:...\",\n  \"creator\": {\"handle\": \"...\", \"did\": \"...\", \"display_name\": \"...\", \"avatar\": \"...\"},\n  \"display_name\": \"What's Hot\", \"description\": \"Trending posts across Bluesky\",\n  \"avatar\": \"https://...\", \"like_count\": 12345, \"indexed_at\": \"2026-03-14T10:00:00Z\"\n}\n```\n\n### Command Response Keys\n\nEach command returns these top-level keys:\n\n| Command | Response keys |\n|---------|--------------|\n| `post` | `{\"uri\", \"cid\"}` |\n| `delete` | `{\"deleted\"}` (the URI) |\n| `like` | `{\"liked\", \"uri\"}` (post URI + like record URI) |\n| `unlike` | `{\"unliked\"}` |\n| `repost` | `{\"reposted\", \"uri\"}` (post URI + repost record URI) |\n| `unrepost` | `{\"unreposted\"}` |\n| `timeline` | `{\"feed\": [{\"post\": <post>, \"reason\": {\"type\": \"repost\", \"by\": <actor>} or null}], \"cursor\"}` |\n| `thread` | `{\"thread\": <post with nested \"replies\": [...]>}` |\n| `search-posts` | `{\"posts\": [<post>], \"cursor\"}` |\n| `search-users` | `{\"actors\": [<profile>], \"cursor\"}` |\n| `follow` | `{\"followed\", \"uri\"}` |\n| `unfollow` | `{\"unfollowed\"}` |\n| `followers` | `{\"followers\": [<actor>], \"cursor\"}` |\n| `following` | `{\"following\": [<actor>], \"cursor\"}` |\n| `mute` | `{\"muted\"}` |\n| `unmute` | `{\"unmuted\"}` |\n| `block` | `{\"blocked\", \"uri\"}` |\n| `unblock` | `{\"unblocked\"}` |\n| `profile` | `<profile>` (top-level, no wrapper) |\n| `get` | `<post>` (top-level, no wrapper) |\n| `my-posts` | `{\"posts\": [<post>], \"cursor\"}` |\n| `user-posts` | `{\"posts\": [<post>], \"cursor\"}` |\n| `likes` | `{\"likes\": [{\"actor\": <actor>, \"created_at\": \"...\"}], \"cursor\"}` |\n| `reposts` | `{\"reposted_by\": [<actor>], \"cursor\"}` |\n| `notifications` | `{\"notifications\": [<notification>], \"cursor\"}` |\n| `notif-read` | `{\"success\": true}` |\n| `dm-list` | `{\"conversations\": [<convo>], \"cursor\"}` |\n| `dm-read` | `{\"convo_id\", \"messages\": [<dm>], \"cursor\"}` |\n| `dm-send` | `{\"sent\": true, \"convo_id\", \"message_id\"}` |\n| `dm-mark-read` | `{\"success\": true}` |\n| `update-profile` | `<profile>` (top-level, no wrapper) |\n| `post-thread` | `{\"posts\": [{\"uri\", \"cid\"}, ...]}` |\n| `feeds` | `{\"feeds\": [<feed>], \"cursor\"}` |\n\n**Important:** Note that `timeline` wraps posts in `feed[].post` (with an optional `reason`), while `search-posts` and `my-posts` use `posts[]` directly.\n\n### Pagination\n\nList commands support `--cursor TOKEN`. The response includes `\"cursor\"` (null = no more results).\n1. First call: omit `--cursor`\n2. Next page: pass the returned cursor as `--cursor`\n3. Stop when cursor is null\n\n### Errors\n\nErrors return JSON with exit code 1:\n```json\n{\"error\": \"ERROR_TYPE\", \"message\": \"Human-readable description\", \"type\": \"SUBTYPE (for AUTH_ERROR)\"}\n```\n\nError types: `NOT_FOUND`, `NOT_LIKED`, `NOT_REPOSTED`, `NOT_FOLLOWING`, `NOT_BLOCKING`, `FILE_NOT_FOUND`, `INVALID_ARGS`, `AUTH_ERROR`.\n\n## Command Quick Reference\n\n### Posting\n| Command | Description |\n|---------|-------------|\n| `post \"text\"` | Create a text post (max 300 graphemes) |\n| `post \"text\" --image photo.jpg --alt \"description\"` | Post with image (repeat `--image`/`--alt` for up to 4) |\n| `post \"text\" --reply-to <uri>` | Reply to a post |\n| `post \"text\" --quote <uri>` | Quote a post |\n| `post \"text\" --quote <uri> --image photo.jpg --alt \"desc\"` | Quote with image |\n| `post-thread \"text1\" \"text2\" \"text3\"` | Create a multi-post thread |\n| `delete <uri>` | Delete a post |\n\n### Engagement\n| Command | Description |\n|---------|-------------|\n| `like <uri>` | Like a post |\n| `unlike <uri>` | Unlike (pass the post URI, not the like URI) |\n| `repost <uri>` | Repost a post |\n| `unrepost <uri>` | Undo repost (pass the post URI) |\n\n### Reading & Discovery\n| Command | Description |\n|---------|-------------|\n| `timeline [--limit N] [--cursor TOKEN]` | Home timeline (default 20) |\n| `thread <uri> [--depth N]` | Post thread with replies (default depth 6) |\n| `search-posts \"query\" [--limit N] [--cursor TOKEN]` | Search posts |\n| `search-users \"query\" [--limit N] [--cursor TOKEN]` | Search users |\n| `feeds [--query \"keyword\"] [--limit N] [--cursor TOKEN]` | Browse suggested feed generators (note: `--query` filters client-side, so results may be fewer than `--limit`) |\n\n### Social Graph\n| Command | Description |\n|---------|-------------|\n| `follow <handle>` | Follow |\n| `unfollow <handle>` | Unfollow |\n| `followers <handle> [--limit N] [--cursor TOKEN]` | List followers |\n| `following <handle> [--limit N] [--cursor TOKEN]` | List following |\n| `mute <handle>` / `unmute <handle>` | Mute/unmute |\n| `block <handle>` / `unblock <handle>` | Block/unblock |\n\n### Profile & Info\n| Command | Description |\n|---------|-------------|\n| `profile [handle]` | View profile (own if omitted) |\n| `update-profile [--name \"Name\"] [--bio \"Bio\"] [--avatar image.jpg]` | Update your profile |\n| `my-posts [--limit N] [--cursor TOKEN]` | Own recent posts |\n| `user-posts <handle> [--limit N] [--cursor TOKEN]` | A user's recent posts |\n| `get <uri>` | Fetch a single post |\n| `likes <uri> [--limit N] [--cursor TOKEN]` | Who liked a post |\n| `reposts <uri> [--limit N] [--cursor TOKEN]` | Who reposted a post |\n\n### Notifications\n| Command | Description |\n|---------|-------------|\n| `notifications [--limit N] [--unread-only] [--filter TYPE] [--cursor TOKEN]` | List notifications (filter: like, repost, follow, mention, reply, quote) |\n| `notif-read` | Mark all as read |\n\n### Direct Messages\n| Command | Description |\n|---------|-------------|\n| `dm-list [--limit N] [--cursor TOKEN]` | List conversations |\n| `dm-read --handle <handle> [--limit N] [--cursor TOKEN]` | Read messages with a user |\n| `dm-read --convo-id <id> [--limit N] [--cursor TOKEN]` | Read messages by convo ID |\n| `dm-send <handle> \"text\"` | Send a DM |\n| `dm-mark-read --convo-id <id>` | Mark convo as read |\n| `dm-mark-read --all` | Mark all as read |\n\nOnly text DMs are supported (no images).\n\n## Common Workflows\n\n### Check and respond to mentions\n```bash\n./bsky notifications --unread-only --filter mention\n# Parse → for each notification, extract reason_subject (the post they mentioned you in)\n./bsky get <reason_subject_uri>\n# Read context, then reply:\n./bsky post \"Your reply\" --reply-to <uri>\n./bsky notif-read\n```\n\n### Engage with timeline\n```bash\n./bsky timeline --limit 30\n# Parse → extract feed[].post objects\n# Like interesting posts:\n./bsky like <uri>\n# Reply to engage:\n./bsky post \"Your reply\" --reply-to <uri>\n```\n\n### Search and engage with a topic\n```bash\n./bsky search-posts \"topic keywords\" --limit 20\n# Parse → like/repost/reply to relevant posts[]\n./bsky like <uri>\n./bsky repost <uri>\n```\n\n### Join a conversation (read thread before replying)\n```bash\n./bsky thread <uri> --depth 6\n# Parse → read thread.text and thread.replies[] to understand context\n./bsky post \"Informed reply\" --reply-to <uri>\n```\n\n### Grow the network\n```bash\n./bsky search-users \"topic or niche\" --limit 20\n# Parse → review actors[] profiles for relevance\n./bsky profile <handle>\n# Check their posts before following:\n./bsky user-posts <handle> --limit 10\n# Avoid re-following — check viewer.following is null, then:\n./bsky follow <handle>\n```\n\n### Check engagement on own posts\n```bash\n./bsky my-posts --limit 10\n# Parse → find posts with high reply_count\n./bsky thread <uri>\n# Respond to replies, like engaged followers\n./bsky likes <uri>\n```\n\n### Check and respond to DMs\n```bash\n./bsky dm-list\n# Parse → find conversations[] with unread_count > 0\n./bsky dm-read --convo-id <id>\n# Parse → read messages[], reply:\n./bsky dm-send <handle> \"Your reply\"\n./bsky dm-mark-read --convo-id <id>\n```\n\n### Post a thread\n```bash\n./bsky post-thread \"First, let me explain the context...\" \"Second, here's the main point...\" \"Finally, the conclusion.\"\n# Returns: {\"posts\": [{\"uri\": \"...\", \"cid\": \"...\"}, ...]}\n```\n\n### Update your profile\n```bash\n./bsky update-profile --name \"New Display Name\" --bio \"Updated bio text\"\n./bsky update-profile --avatar new-avatar.jpg\n```\n\n### Discover feeds\n```bash\n./bsky feeds --limit 10\n# Filter by keyword:\n./bsky feeds --query \"news\"\n```\n\n### Check before posting (avoid duplicates)\n```bash\n./bsky my-posts --limit 5\n# Parse → check posts[].text for similar content\n./bsky post \"New post text\"\n```\n\n## Key Concepts\n\n- **Handles**: Always pass handles **without** the `@` prefix — use `user.bsky.social`, not `@user.bsky.social`.\n- **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.\n- **Rich text**: @mentions, #hashtags, URLs in post text are auto-converted to links. Write naturally.\n- **Character limit**: 300 graphemes per post.\n- **Unlike/unrepost**: Pass the **post URI**, not the like/repost record URI. Auto-resolved internally.\n- **Reply threading**: `--reply-to <uri>` auto-resolves the thread root.\n\n## Auth Troubleshooting\n\nAuth errors: `{\"error\": \"AUTH_ERROR\", \"type\": \"<TYPE>\", \"message\": \"...\"}` with exit code 1.\n\n1. **SESSION_CORRUPT** → `rm ~/.bsky_session.json` and retry\n2. **MISSING_ENV** → Ensure `.env` has `BLUESKY_HANDLE` and `BLUESKY_APP_PASSWORD`\n3. **INVALID_CREDENTIALS** → Handle: `user.bsky.social`, App password: `xxxx-xxxx-xxxx-xxxx` (19 chars)\n4. **NETWORK** → Retry up to 3 times with 10s delay\n5. **ACCOUNT_SUSPENDED** → Inform user, cannot fix programmatically\n","tags":{"latest":"1.0.1"},"stats":{"comments":0,"downloads":598,"installsAllTime":23,"installsCurrent":0,"stars":0,"versions":2},"createdAt":1773618509085,"updatedAt":1778491933006},"latestVersion":{"version":"1.0.1","createdAt":1773619147274,"changelog":"**Initial public release with improved setup instructions and dependency management.**\n\n- Added pip install instructions for required dependencies (`atproto`, `python-dotenv`)\n- Detailed location and handling of session cache for authentication\n- Clarified setup steps for `.env` usage and app passwords\n- All other command references and response schemas remain unchanged","license":"MIT-0"},"metadata":{"setup":[{"key":"BLUESKY_HANDLE","required":true},{"key":"BLUESKY_APP_PASSWORD","required":true}],"os":null,"systems":null},"owner":{"handle":"johannesseikowsky","userId":"s175yqw5ch7pd91mr5j7qd5sh9885css","displayName":"Johannes","image":"https://avatars.githubusercontent.com/u/5034739?v=4"},"moderation":{"isSuspicious":false,"isMalwareBlocked":false,"verdict":"clean","reasonCodes":["review.llm_review"],"summary":"Review: review.llm_review","engineVersion":"v2.4.24","updatedAt":1780089908111}}