Composable Bluesky data filtering and monitoring CLI built with Effect.
Sync posts from timelines, feeds, lists, authors, and the real-time Jetstream firehose into local SQLite stores. Query, filter, derive, and export data with a powerful filter DSL and multiple output formats.
bun add -g @mepuka/skygentgit clone https://github.com/mepuka/skygent-bsky.git
cd skygent-bsky
bun install
bun run index.ts --helpDownload a prebuilt binary from GitHub Releases, or build locally:
bun run build:binary
./skygent --helpCross-platform targets: linux-x64, linux-arm64, darwin-x64, darwin-arm64.
Skygent needs a Bluesky handle and app password. Credentials are resolved in this order:
- CLI flags:
--identifierand--password - Environment variables:
SKYGENT_IDENTIFIERandSKYGENT_PASSWORD - Encrypted credential file (
~/.skygent/credentials.json, requires a credentials key)
Credentials key resolution order:
- Environment:
SKYGENT_CREDENTIALS_KEY - Keyfile:
~/.skygent/credentials.key
Manage the encrypted credential file with skygent config credentials.
Manage the credentials key with skygent config credentials key set|status|clear.
The simplest setup:
cp .env.example .env
# Edit .env with your handle and app passwordBun loads .env automatically.
# Create a store
skygent store create my-store
# Sync your timeline
skygent sync timeline --store my-store
# Query recent posts
skygent query my-store --limit 10 --format table
# Stream live posts from Jetstream
skygent watch jetstream --store my-store
# Derive a filtered store
skygent derive my-store ai-posts --filter 'hashtag:#ai OR hashtag:#ml'
# Search posts locally
skygent search posts "effect typescript" --store my-store| Subcommand | Description |
|---|---|
store create <name> |
Create a new store |
store list |
List all stores |
store show <name> |
Show store config and metadata |
store rename <from> <to> |
Rename a store |
store delete <name> --force |
Delete a store |
store stats <name> |
Show store statistics |
store summary |
Summarize all stores |
store tree |
Visualize store lineage |
store materialize <name> |
Materialize filter outputs to disk |
| Subcommand | Description |
|---|---|
sync timeline |
Sync your timeline |
sync feed <uri> |
Sync a feed generator |
sync list <uri> |
Sync a list feed |
sync author <actor> |
Sync posts from an author |
sync thread <uri> |
Sync a thread (parents + replies) |
sync notifications |
Sync notifications |
sync jetstream |
Sync from Jetstream firehose |
All sync commands accept --store, --filter, --quiet, and --refresh.
Same subcommands as sync, with continuous polling. Supports --interval (default: 30s).
skygent watch timeline --store my-store --interval "5 minutes"skygent query my-store --limit 25 --format table
skygent query my-store --filter 'hashtag:#ai' --sort desc --format json
skygent query my-store --range 2024-01-01T00:00:00Z..2024-01-31T00:00:00Z
skygent query my-store --fields @minimal --newest-first
skygent query my-store --fields @images --resolve-images
skygent query my-store --extract-images --format json
skygent query store-a,store-b --format ndjsonFormats: json, ndjson, table, markdown, compact, card, thread
Field presets: @minimal, @social, @full, @images, @embeds, @media, or comma-separated field names with dot notation (use * to traverse arrays, e.g. images.*.alt).
Image options: --extract-images, --resolve-images, --cache-images (requires images in output), --no-cache-images-thumbnails.
Multi-store queries accept comma-separated store lists or repeated store arguments and include store names in output by default.
Apply a filter to a source store to produce a new filtered store:
skygent derive source-store target-store --filter 'hashtag:#ai'Modes:
event-time(default) -- Pure filters only, replayablederive-time-- Allows effectful filters (Trending, HasValidLinks)
Tip: run skygent filter help for a compact list of predicates and aliases.
| Subcommand | Description |
|---|---|
filter create <name> |
Save a named filter |
filter list |
List saved filters |
filter show <name> |
Show a saved filter |
filter delete <name> |
Delete a saved filter |
filter help |
Show filter DSL and JSON help |
filter validate |
Validate a filter expression |
filter test |
Test a filter against a post |
filter explain |
Explain why a post matches |
filter benchmark |
Benchmark filter performance |
filter describe |
Describe a filter in plain text |
| Subcommand | Description |
|---|---|
search posts <query> |
Search posts locally or --network |
search handles <query> |
Search Bluesky profiles |
search feeds <query> |
Search feed generators |
| Subcommand | Description |
|---|---|
graph followers <actor> |
List followers |
graph follows <actor> |
List follows |
graph known-followers <actor> |
Mutual followers |
graph relationships <actor> |
Relationship status |
graph lists <actor> |
Lists created by actor |
graph list <uri> |
View a list's members |
graph blocks |
Your blocked accounts |
graph mutes |
Your muted accounts |
| Subcommand | Description |
|---|---|
feed show <uri> |
Show feed details |
feed batch <uri>... |
Fetch multiple feeds |
feed by <actor> |
List feeds by an actor |
| Subcommand | Description |
|---|---|
post likes <uri> |
Who liked a post |
post reposted-by <uri> |
Who reposted |
post quotes <uri> |
Quote posts |
| Subcommand | Description |
|---|---|
view thread <uri> |
Display a thread |
view status <view> <source> |
Check if a derived view is stale |
skygent config check # Run health checksFilters are passed via --filter (DSL string) or --filter-json (JSON AST).
| Filter | Example |
|---|---|
hashtag:#tag |
Match posts with hashtag |
author:handle.bsky.social |
Match posts by author |
contains:"text" |
Text search (case-insensitive by default) |
regex:/pattern/i |
Regex match |
language:en,es |
Match languages |
date:<start>..<end> |
Date range (ISO 8601) |
engagement:minLikes=100 |
Engagement thresholds |
is:reply |
Post type (reply, quote, repost, original) |
has:images |
Media presence (images, video, links, media, embed) |
@saved-name |
Reference a saved filter |
from: = author:, tag: = hashtag:, text: = contains:, lang: = language:
authorin:alice,bob,charlie and hashtagin:#ai,#ml,#dl
hashtag:#ai AND author:user.bsky.social
hashtag:#ai OR hashtag:#ml
NOT hashtag:#spam
(hashtag:#ai OR hashtag:#ml) AND engagement:minLikes=10
Operators: AND / &&, OR / ||, NOT / !, parentheses for grouping.
| Variable | Default | Description |
|---|---|---|
SKYGENT_IDENTIFIER |
-- | Bluesky handle or DID |
SKYGENT_PASSWORD |
-- | App password |
SKYGENT_CREDENTIALS_KEY |
-- | Master key for encrypted credential storage (overrides ~/.skygent/credentials.key) |
SKYGENT_SERVICE |
https://bsky.social |
Bluesky service URL |
SKYGENT_STORE_ROOT |
~/.skygent |
Root storage directory |
SKYGENT_OUTPUT_FORMAT |
ndjson |
Default output format |
SKYGENT_BSKY_RATE_LIMIT |
250 millis |
Min delay between API calls |
SKYGENT_BSKY_RETRY_MAX |
5 |
Max retry attempts |
SKYGENT_SYNC_CONCURRENCY |
5 |
Concurrent sync workers |
--full-- Use verbose JSON output (compact is the default)--quiet-- Suppress progress output--log-format json|human-- Control log format
Skygent is built entirely on Effect with a layered service architecture:
- Domain (
src/domain/) -- Data models for posts, stores, filters, events, and derivations using Effect Schema - Services (
src/services/) -- Business logic: Bluesky API client, SQLite store, sync engine, filter runtime, derivation engine - CLI (
src/cli/) -- Command definitions, output formatting, error handling
Stores are local SQLite databases with an append-only event log. Derivations track lineage between stores and support incremental processing with checkpoints.
- Passwords are handled as
Redactedvalues and never logged - Encrypted credential storage uses AES-GCM with PBKDF2 (100,000 iterations)
- Avoid putting passwords in config files; use environment variables or the credential store
Detailed docs are in docs/:
MIT