bluesky
v1.0.0Post to Bluesky (AT Protocol). Use when the user asks to post a tweet/thread to Bluesky, share content on Bluesky, or post news from a URL.
How to install
Point your Sulala Agent at this store, then install this skill.
- Set the registry URL (e.g. in
.env):
SKILLS_REGISTRY_URL=https://hub.sulala.ai/api/sulalahub/registryThen run: sulala skill install bluesky or install from the dashboard Skills page.
Skill doc
---
name: bluesky
description: Post to Bluesky (AT Protocol). Use when the user asks to post a tweet/thread to Bluesky, share content on Bluesky, or post news/headlines from a source (URL or text) to Bluesky.
homepage: https://bsky.app
metadata:
{
"sulala": {
"emoji": "🦋",
"requires": { "bins": ["curl", "python3", "sh"], "env": ["BSKY_HANDLE", "BSKY_APP_PASSWORD"] },
"primaryEnv": "BSKY_APP_PASSWORD"
}
}
---
# Bluesky Posting
Post to Bluesky via the AT Protocol. Use **run_command** with `curl`, `python3`, and `sh`. Add `curl`, `python3`, and `sh` to ALLOWED_BINARIES.
Requires `BSKY_HANDLE` (e.g. `your.bsky.social`) and `BSKY_APP_PASSWORD` (app password from bsky.app → Settings → App Passwords). Set them in `.env` or in the skill config (`~/.sulala/config.json` or `.sulala/config.json` in the project; see Skill config in the dashboard). Config is re-read when the file changes; enable/disable takes effect without restart.
**IMPORTANT:** `run_command` does not run a shell — `$BSKY_HANDLE` and `$BSKY_APP_PASSWORD` will not expand if you call curl directly. Always use `binary: "sh"` and `args: ["-c", "curl ... $BSKY_HANDLE ... $BSKY_APP_PASSWORD ..."]` so the env vars expand.
## When to Use
- "Post this to Bluesky"
- "Share [content] on Bluesky"
- "Post news from [URL] to Bluesky"
- "Post a headline about [topic]"
## Post Flow (non-interactive)
### 1. Ensure credentials
```
BSKY_HANDLE="${BSKY_HANDLE:?Set BSKY_HANDLE}"
BSKY_APP_PASSWORD="${BSKY_APP_PASSWORD:?Set BSKY_APP_PASSWORD}"
```
If empty, read from Sulala config:
```
CONFIG_PATH="${SULALA_CONFIG_PATH:-$HOME/.sulala/config.json}"
# If using workspace config, use: CONFIG_PATH=".sulala/config.json" (when run from project root)
BSKY_HANDLE=$(cat "$CONFIG_PATH" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); e=d.get('skills',{}).get('entries',{}).get('bluesky',{}); print(e.get('handle','') or e.get('apiKey',''))" 2>/dev/null)
BSKY_APP_PASSWORD=$(cat "$CONFIG_PATH" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); e=d.get('skills',{}).get('entries',{}).get('bluesky',{}); print(e.get('apiKey','') or e.get('password',''))" 2>/dev/null)
```
(Config may use `handle` + `apiKey` for Bluesky. Adjust field names if your config differs.)
### 2. Create session (login)
**Use `run_command` with `binary: "sh"` and `args: ["-c", "..."]`** so `$BSKY_HANDLE` and `$BSKY_APP_PASSWORD` expand from the environment. Do NOT call curl directly.
```bash
sh -c 'SESSION=$(curl -s -X POST "https://bsky.social/xrpc/com.atproto.server.createSession" -H "Content-Type: application/json" -d "{\"identifier\": \"$BSKY_HANDLE\", \"password\": \"$BSKY_APP_PASSWORD\"}"); echo "$SESSION"'
```
Then parse the JSON output with `python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('accessJwt',''), d.get('did',''))"` to get `ACCESS_TOKEN` and `DID`.
If the session response contains `"error"` or `"AuthenticationRequired"`, report: "Bluesky login failed. Check BSKY_HANDLE and BSKY_APP_PASSWORD in .env."
### 3. Create post
Text must be JSON-escaped. Use python to build the payload. **Use `run_command` with `binary: "sh"`** so `$ACCESS_TOKEN`, `$DID`, and other vars expand.
Run a single `sh -c` that chains steps 2 and 3 so SESSION, ACCESS_TOKEN, DID persist in the same shell:
```bash
sh -c '
SESSION=$(curl -s -X POST "https://bsky.social/xrpc/com.atproto.server.createSession" \
-H "Content-Type: application/json" \
-d "{\"identifier\": \"$BSKY_HANDLE\", \"password\": \"$BSKY_APP_PASSWORD\"}");
ACCESS_TOKEN=$(echo "$SESSION" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get(\"accessJwt\",\"\"))");
DID=$(echo "$SESSION" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get(\"did\",\"\"))");
if [ -z "$ACCESS_TOKEN" ]; then echo "Login failed"; exit 1; fi;
POST_TEXT="Your post content here";
POST_JSON=$(python3 -c "import sys,json; print(json.dumps(sys.argv[1]))" "$POST_TEXT");
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z");
curl -s -X POST "https://bsky.social/xrpc/com.atproto.repo.createRecord" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"repo\": \"$DID\", \"collection\": \"app.bsky.feed.post\", \"record\": {\"\\$type\": \"app.bsky.feed.post\", \"text\": $POST_JSON, \"createdAt\": \"$NOW\"}}"
'
```
Replace `Your post content here` with the user's post text.
If the response contains `"uri"`, the post succeeded. Otherwise report the error.
## Posting from a news source
When the user wants to post **news** or **content from a URL**:
1. **Fetch the content** (e.g. with `curl -s <URL>` or read a file).
2. **Summarize** if the content is long — Bluesky posts are max 300 characters. Write a short headline or summary.
3. **Post** using the flow above. Optionally append the source URL if it fits within 300 chars.
Example for "post news from https://example.com/article":
- Fetch the page, extract title/lead.
- Build post: "Headline here — https://example.com/article"
- Post via the createRecord flow above.
## Character limit
Bluesky posts are limited to 300 characters. If the user's text or your summary exceeds that, truncate or split into multiple posts (thread). For a thread, create each post separately; the API does not natively support threads — you would need to reference the previous post's URI if linking them (advanced).
## Notes
- Use an **app password**, not the main account password. Create at bsky.app → Settings → App Passwords.
- `bsky.social` is the default PDS; custom PDS users would need a different host.
- Do not embed real credentials in the skill or in replies — use env or config.