bskybot#
Create custom Bluesky bots through simple configuration. This TypeScript library provides a robust foundation for building bots that can respond to keywords, run on schedules, or execute custom actions on the Bluesky social network.
Features#
- Multiple Bot Types: ActionBot, CronBot, and KeywordBot for different use cases
- Schema-Validated WebSocket: Real-time Bluesky firehose connection powered by
@vinerima/wahwith Zod-based message validation - Automatic Failover: Multi-service reconnection with exponential backoff
- Structured Logging: Correlation IDs and timing for better debugging
- Health Monitoring: Built-in health checks and metrics collection
- TypeScript Support: Full type safety with Zod schema inference
- Error Handling: Robust retry strategies and graceful failure handling
Installation#
pnpm add bskybot
npm install bskybot
Quick Start#
import { KeywordBot, useKeywordBotAgent } from "bskybot";
const myBot: KeywordBot = {
identifier: "your-bot.bsky.social",
password: "your-app-password", // Generate at https://bsky.app/settings/app-passwords
service: "https://bsky.social",
replies: [{
keyword: "hello bot",
messages: ["Hello there!", "Hi! How can I help?"]
}]
};
const agent = await useKeywordBotAgent(myBot);
Bot Types#
ActionBot#
Execute custom logic in response to posts or other triggers.
import { ActionBot, ActionBotAgent, useActionBotAgent, Post } from "bskybot";
const actionBot: ActionBot = {
identifier: "my-action-bot.bsky.social",
password: "your-app-password",
username: "My Action Bot",
service: "https://bsky.social",
action: async (agent: ActionBotAgent, post: Post) => {
if (post.text.includes("weather")) {
const weatherData = await getWeatherData();
await agent.post({ text: `Current weather: ${weatherData}` });
agent.logAction("info", "Posted weather update", {
postUri: post.uri,
weather: weatherData
});
}
}
};
const agent = await useActionBotAgent(actionBot);
await agent?.doAction();
CronBot#
Run scheduled tasks using cron expressions.
import { CronBot, CronBotAgent, useCronBotAgent } from "bskybot";
const cronBot: CronBot = {
identifier: "my-cron-bot.bsky.social",
password: "your-app-password",
username: "Daily Bot",
service: "https://bsky.social",
cronJob: {
scheduleExpression: "0 9 * * *", // Daily at 9 AM
callback: null,
timeZone: "America/New_York"
},
action: async (agent: CronBotAgent) => {
const dailyTip = await getDailyTip();
await agent.post({ text: `Daily tip: ${dailyTip}` });
agent.logAction("info", "Posted daily tip", { tip: dailyTip });
}
};
const agent = await useCronBotAgent(cronBot);
// Cron job starts automatically
KeywordBot#
Respond to posts containing specific keywords.
import { KeywordBot, useKeywordBotAgent } from "bskybot";
const keywordBot: KeywordBot = {
identifier: "my-keyword-bot.bsky.social",
password: "your-app-password",
username: "Helper Bot",
service: "https://bsky.social",
replies: [
{
keyword: "help",
exclude: ["helpless", "unhelpful"],
messages: [
"I'm here to help!",
"What do you need assistance with?",
"How can I help you today?"
]
},
{
keyword: "documentation",
messages: ["Check out our docs at https://example.com/docs"]
}
]
};
const agent = await useKeywordBotAgent(keywordBot);
Jetstream Integration#
Connect to the Bluesky Jetstream firehose for real-time post processing using createJetstreamClient. Messages are validated at runtime against Zod schemas, so your handler only receives type-safe, fully validated data.
import { createJetstreamClient, useKeywordBotAgent } from "bskybot";
const keywordBot = { /* configuration */ };
const agent = await useKeywordBotAgent(keywordBot);
if (agent) {
const client = createJetstreamClient({
service: "wss://jetstream2.us-east.bsky.network/subscribe",
queryParams: {
wantedCollections: "app.bsky.feed.post",
},
onPost: (post) => {
agent.likeAndReplyIfFollower(post);
},
});
// Graceful shutdown
process.on("SIGINT", () => client.close());
}
Multi-service failover#
Pass an array of service URLs. The client cycles through them on connection failure with exponential backoff.
const client = createJetstreamClient({
service: [
"wss://jetstream2.us-east.bsky.network/subscribe",
"wss://jetstream1.us-west.bsky.network/subscribe",
"wss://jetstream2.us-west.bsky.network/subscribe",
],
reconnect: {
initialDelay: 5000,
maxDelay: 30000,
backoffFactor: 1.5,
maxAttempts: 3,
maxServiceCycles: 2,
},
onPost: (post) => {
// process post
},
});
Custom schema handlers#
For advanced use cases, you can use the underlying WebSocketClient from @vinerima/wah directly and register your own Zod schemas.
import { WebSocketClient } from "bskybot";
import { z } from "zod";
const MyCustomSchema = z.object({
kind: z.literal("identity"),
did: z.string(),
identity: z.object({
handle: z.string(),
}),
});
const client = new WebSocketClient({
service: "wss://jetstream2.us-east.bsky.network/subscribe",
});
client.handle(MyCustomSchema, (ctx) => {
console.log("Identity update:", ctx.data.identity.handle);
});
client.connect();
Advanced Configuration#
Logging Configuration#
import { Logger } from "bskybot";
// Configure global log level
Logger.setLogLevel(LogLevel.INFO); // DEBUG, INFO, WARN, ERROR
// In bot actions, use agent.logAction for correlation tracking
agent.logAction("info", "Operation completed", {
customField: "value",
metrics: { processingTime: "150ms" }
});
Health Monitoring#
import { healthMonitor } from "bskybot";
healthMonitor.registerHealthCheck("myCheck", async () => {
return checkSomething();
});
healthMonitor.start();
const status = await healthMonitor.getHealthStatus();
console.log("Healthy:", status.healthy);
console.log("Checks:", status.checks);
console.log("Metrics:", status.metrics);
Exported Classes and Functions#
Core Classes#
BotAgent- Abstract base class for all bot agents with correlation trackingActionBotAgent- ExtendsBotAgentfor action-based botsCronBotAgent- ExtendsBotAgentfor scheduled botsKeywordBotAgent- ExtendsBotAgentfor keyword-responding bots
Initialization Functions#
useActionBotAgent(actionBot: ActionBot): Promise<ActionBotAgent | null>useCronBotAgent(cronBot: CronBot): Promise<CronBotAgent | null>useKeywordBotAgent(keywordBot: KeywordBot): Promise<KeywordBotAgent | null>initializeBotAgent<T>(botType: string, bot: Bot, createAgent: Function): Promise<T | null>- Generic initialization helper
Jetstream#
createJetstreamClient(options: JetstreamClientOptions): WebSocketClient- Factory for a firehose-connected WebSocket client with post schema validationcommitToPost(data: JetstreamPostCreateCommit): Post | null- Converts a validated commit message to aPost
Zod Schemas#
JetstreamPostCreateCommitSchema- Validates Jetstream commit messages for new post creationPostRecordSchema- Validatesapp.bsky.feed.postrecord dataUriCidSchema- Validates URI/CID pairs
Re-exported from @vinerima/wah#
WebSocketClient- Generic WebSocket client with schema-based handlers, failover, and reconnection- Types:
WebSocketClientOptions,ConnectionInfo,ConnectionState,HandlerContext,MessageHandler,ReconnectOptions,LoggerInterface
Utility Functions#
buildReplyToPost(root: UriCid, parent: UriCid, message: string)- Helper for creating post repliesfilterBotReplies(text: string, replies: BotReply[]): BotReply[]- Filter applicable bot replies
Type Definitions#
// Bot configuration types
interface Bot {
identifier: string;
password: string;
username?: string;
service: string;
}
interface ActionBot extends Bot {
action: (agent: ActionBotAgent, params?: unknown) => Promise<void>;
}
interface CronBot extends Bot {
cronJob: {
scheduleExpression: string;
callback?: (() => void) | null;
timeZone: string;
};
action: (agent: CronBotAgent) => Promise<void>;
}
interface KeywordBot extends Bot {
replies: BotReply[];
}
interface BotReply {
keyword: string;
exclude?: string[];
messages: string[];
}
// Post types
interface Post {
uri: string;
cid: string;
rootUri: string;
rootCid: string;
text: string;
authorDid: string;
createdAt?: Date;
}
interface UriCid {
uri: string;
cid: string;
}
Migration Guide#
Migrating from v2.x to v3.0.0#
v3.0.0 replaces the custom WebSocket layer with @vinerima/wah, a generic WebSocket action handler with Zod-based schema validation. This brings runtime message validation, typed handlers, and a smaller API surface.
Breaking Changes#
-
WebSocketClientis now from@vinerima/wahThe custom
WebSocketClientclass has been removed. The re-exportedWebSocketClientfrom@vinerima/wahhas a different API: it useshandle(schema, handler)for message processing instead of subclass overrides.// v2.x import { WebSocketClient } from "bskybot"; const client = new WebSocketClient({ service: "wss://jetstream2.us-east.bsky.network/subscribe", reconnectInterval: 5000, maxReconnectAttempts: 3, }); // v3.x import { WebSocketClient } from "bskybot"; const client = new WebSocketClient({ service: "wss://jetstream2.us-east.bsky.network/subscribe", reconnect: { initialDelay: 5000, maxAttempts: 3, }, }); client.connect(); // Connection is no longer automatic on construction -
JetstreamSubscriptionreplaced bycreateJetstreamClientThe class-based
JetstreamSubscriptionis replaced by a factory function.// v2.x import { JetstreamSubscription, websocketToFeedEntry } from "bskybot"; const sub = new JetstreamSubscription( "wss://jetstream2.us-east.bsky.network/subscribe", 5000, (data) => { const post = websocketToFeedEntry(data); if (post) processPost(post); } ); // v3.x import { createJetstreamClient } from "bskybot"; const client = createJetstreamClient({ service: "wss://jetstream2.us-east.bsky.network/subscribe", onPost: (post) => { processPost(post); }, }); -
websocketToFeedEntryreplaced bycommitToPostThe function no longer accepts raw
WebSocket.Data. It accepts a Zod-validatedJetstreamPostCreateCommitobject. If you usecreateJetstreamClient, you don't need to call this function at all - it's handled internally.// v2.x import { websocketToFeedEntry } from "bskybot"; const post = websocketToFeedEntry(rawWebSocketData); // v3.x - handled automatically by createJetstreamClient // Or if you need manual conversion: import { commitToPost, JetstreamPostCreateCommitSchema } from "bskybot"; const parsed = JetstreamPostCreateCommitSchema.safeParse(jsonData); if (parsed.success) { const post = commitToPost(parsed.data); } -
Type guards removed, Zod schemas replace them
isCommitMessage(),isPostCommitMessage(), andisCreateOperation()are removed. Use the Zod schemas for validation instead.// v2.x import { isCommitMessage, isPostCommitMessage, isCreateOperation } from "bskybot"; if (isCommitMessage(msg) && isCreateOperation(msg) && isPostCommitMessage(msg)) { // process post } // v3.x import { JetstreamPostCreateCommitSchema } from "bskybot"; const result = JetstreamPostCreateCommitSchema.safeParse(msg); if (result.success) { // result.data is fully typed as JetstreamPostCreateCommit } -
Deprecated
WebsocketMessagetype removedUse
JetstreamPostCreateCommitinstead. -
Health check integration removed from WebSocket layer
The WebSocket client no longer auto-registers health checks. Use the
@vinerima/wahevent system to wire up health monitoring if needed.import { createJetstreamClient, healthMonitor } from "bskybot"; const client = createJetstreamClient({ /* ... */ }); healthMonitor.registerHealthCheck("jetstream", async () => { return client.getConnectionInfo().state === "connected"; });
New Dependencies#
@vinerima/wah- WebSocket action handler (replaces custom WebSocket layer)zod- Schema validation (used for Jetstream message schemas)ws- Removed as direct dependency (now a transitive dependency of@vinerima/wah)
Migration Steps#
-
Update the package:
pnpm add bskybot@^3.0.0 -
Replace
JetstreamSubscriptionwithcreateJetstreamClient(see example above). -
Replace
websocketToFeedEntryusage. If you usecreateJetstreamClient, theonPostcallback receives ready-to-usePostobjects. -
Replace type guard imports with Zod schema imports if you were using them directly.
-
If you were extending
WebSocketClient, switch to the@vinerima/wahcomposition model usinghandle(schema, handler). -
If you relied on automatic health check registration, add manual registration using the event-based approach shown above.
All bot types (ActionBotAgent, CronBotAgent, KeywordBotAgent), their initialization functions, and the Post/UriCid types remain unchanged.
Migrating from v1.x to v2.x#
Breaking Changes#
-
Bot Agent Architecture
All bot agents now extend
BotAgentbase class instead ofAtpAgentdirectly:// v1.x - ActionBotAgent extended AtpAgent directly // v2.x - ActionBotAgent extends BotAgent (which extends AtpAgent) // New logAction() method available on all agents -
Logging Method Changes
Replace manual logging with the
logAction()method for correlation tracking:// v1.x Logger.info("Message", context); // v2.x agent.logAction("info", "Message", context); -
WebSocket Configuration
The
WebSocketClientconstructor parameter changed fromurltoservice:// v1.x const client = new WebSocketClient({ url: "wss://example.com" }); // v2.x const client = new WebSocketClient({ service: "wss://example.com" });
License#
MIT License - see LICENSE file for details.
Support#
- Issues: GitHub Issues
- Discussions: GitHub Discussions