Create bluesky bots via configuration.
TypeScript 64.4%
JavaScript 35.4%
Shell 0.1%
Other 0.1%
33 1 0

Clone this repository

https://tangled.org/eric.wien/bskybot https://tangled.org/did:plc:4cvte3gr2l65lukolfy5rgma/bskybot
git@knot.eric.wien:eric.wien/bskybot git@knot.eric.wien:did:plc:4cvte3gr2l65lukolfy5rgma/bskybot

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

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/wah with 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 tracking
  • ActionBotAgent - Extends BotAgent for action-based bots
  • CronBotAgent - Extends BotAgent for scheduled bots
  • KeywordBotAgent - Extends BotAgent for 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 validation
  • commitToPost(data: JetstreamPostCreateCommit): Post | null - Converts a validated commit message to a Post

Zod Schemas#

  • JetstreamPostCreateCommitSchema - Validates Jetstream commit messages for new post creation
  • PostRecordSchema - Validates app.bsky.feed.post record data
  • UriCidSchema - 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 replies
  • filterBotReplies(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#

  1. WebSocketClient is now from @vinerima/wah

    The custom WebSocketClient class has been removed. The re-exported WebSocketClient from @vinerima/wah has a different API: it uses handle(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
    
  2. JetstreamSubscription replaced by createJetstreamClient

    The class-based JetstreamSubscription is 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);
      },
    });
    
  3. websocketToFeedEntry replaced by commitToPost

    The function no longer accepts raw WebSocket.Data. It accepts a Zod-validated JetstreamPostCreateCommit object. If you use createJetstreamClient, 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);
    }
    
  4. Type guards removed, Zod schemas replace them

    isCommitMessage(), isPostCommitMessage(), and isCreateOperation() 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
    }
    
  5. Deprecated WebsocketMessage type removed

    Use JetstreamPostCreateCommit instead.

  6. Health check integration removed from WebSocket layer

    The WebSocket client no longer auto-registers health checks. Use the @vinerima/wah event 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#

  1. Update the package:

    pnpm add bskybot@^3.0.0
    
  2. Replace JetstreamSubscription with createJetstreamClient (see example above).

  3. Replace websocketToFeedEntry usage. If you use createJetstreamClient, the onPost callback receives ready-to-use Post objects.

  4. Replace type guard imports with Zod schema imports if you were using them directly.

  5. If you were extending WebSocketClient, switch to the @vinerima/wah composition model using handle(schema, handler).

  6. 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#

  1. Bot Agent Architecture

    All bot agents now extend BotAgent base class instead of AtpAgent directly:

    // v1.x - ActionBotAgent extended AtpAgent directly
    // v2.x - ActionBotAgent extends BotAgent (which extends AtpAgent)
    // New logAction() method available on all agents
    
  2. 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);
    
  3. WebSocket Configuration

    The WebSocketClient constructor parameter changed from url to service:

    // 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#