npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

unified-channel

v0.2.0

Published

Unified message middleware for Node.js — 19 channels, 1 API

Readme

unified-channel (Node.js/TypeScript)

The missing messaging layer for AI Agents. Connect your agent to Telegram, Discord, Slack, WhatsApp, and 15 more channels — with one unified API.

19 messaging channels, 1 unified API. TypeScript-first, zero required dependencies.

Whether you're building an AI assistant, a chatbot, or any agent that needs to talk to humans across platforms — unified-channel gives you a single interface to send and receive messages everywhere.

npm install unified-channel

Why AI Agents Need This

Most AI agent frameworks handle reasoning well but leave messaging as an afterthought. unified-channel fills that gap:

  • One integration, every platform — write your agent logic once, deploy to 19 channels
  • Middleware pipeline — add access control, command routing, rate limiting, or custom processing
  • Channel-agnostic — your agent code never touches platform-specific SDKs
  • Zero lock-in — add or remove channels without changing agent logic
┌─────────────┐     ┌──────────────────────────────────────────────┐     ┌─────────────┐
│  Telegram   │────▶│                                              │     │             │
│  Discord    │────▶│         unified-channel middleware           │────▶│  Your AI    │
│  Slack      │────▶│                                              │     │  Agent      │
│  WhatsApp   │────▶│  AccessMiddleware → CommandMiddleware → ...  │◀────│             │
│  + 15 more  │◀────│                                              │     │             │
└─────────────┘     └──────────────────────────────────────────────┘     └─────────────┘
     Users              Inbound ──────────────────────▶ Outbound           Your Code

Supported Channels

| Channel | Adapter | SDK/Protocol | |---------|---------|-------------| | Telegram | TelegramAdapter | grammy | | Discord | DiscordAdapter | discord.js | | Slack | SlackAdapter | @slack/bolt | | WhatsApp | WhatsAppAdapter | whatsapp-web.js | | iMessage | IMessageAdapter | macOS native (SQLite + AppleScript) | | Matrix | MatrixAdapter | matrix-bot-sdk | | MS Teams | MSTeamsAdapter | botbuilder + express | | LINE | LineAdapter | @line/bot-sdk + express | | Feishu/Lark | FeishuAdapter | @larksuiteoapi/node-sdk | | Mattermost | MattermostAdapter | WebSocket + fetch | | Google Chat | GoogleChatAdapter | Service account JWT + REST | | Nextcloud Talk | NextcloudAdapter | REST polling | | Synology Chat | SynologyAdapter | Webhook + REST | | Zalo | ZaloAdapter | Zalo OA API webhook | | Nostr | NostrAdapter | nostr-tools + WebSocket | | BlueBubbles | BlueBubblesAdapter | REST polling | | Twitch | TwitchAdapter | tmi.js | | IRC | IRCAdapter | irc-framework |

Quick Start

import { ChannelManager, CommandMiddleware, AccessMiddleware } from "unified-channel";
import { TelegramAdapter } from "unified-channel/adapters/telegram";
import { DiscordAdapter } from "unified-channel/adapters/discord";

const manager = new ChannelManager();

// Add channels
manager.addChannel(new TelegramAdapter("BOT_TOKEN"));
manager.addChannel(new DiscordAdapter("BOT_TOKEN"));

// Add middleware
const access = new AccessMiddleware(["admin_user_id"]);
const commands = new CommandMiddleware();

commands
  .command("ping", async () => "pong!")
  .command("status", async (msg) => `Channel: ${msg.channel}`);

manager
  .addMiddleware(access)
  .addMiddleware(commands)
  .onMessage(async (msg) => `Echo: ${msg.content.text}`);

// Start
await manager.run();

Architecture

Incoming Message → [AccessMiddleware] → [CommandMiddleware] → [FallbackHandler]
                                                                     ↓
                        OutboundMessage ← reply string or OutboundMessage

Core Types

interface UnifiedMessage {
  id: string;
  channel: string;           // "telegram", "discord", etc.
  sender: Identity;
  content: MessageContent;   // { type, text, command?, args? }
  timestamp: Date;
  chatId?: string;
  threadId?: string;
  raw?: unknown;             // Original platform event
}

interface OutboundMessage {
  chatId: string;
  text: string;
  replyToId?: string;
  buttons?: Button[][];
}

Middleware

Implement the Middleware interface:

interface Middleware {
  process(msg: UnifiedMessage, next: Handler): Promise<HandlerResult>;
}

Built-in middleware:

  • AccessMiddleware — allowlist by sender ID (silent drop on unauthorized)
  • CommandMiddleware — route /commands to registered handlers
  • ConversationMemory — track conversation history per chat (pluggable store)
  • StreamingMiddleware — typing indicators + streaming reply collection

ChannelAdapter

Each adapter implements:

interface ChannelAdapter {
  readonly channelId: string;
  connect(): Promise<void>;
  disconnect(): Promise<void>;
  onMessage(handler: (msg: UnifiedMessage) => void): void;
  send(msg: OutboundMessage): Promise<string | undefined>;
  getStatus(): Promise<ChannelStatus>;
}

Install Channel SDKs

Only install the SDKs you need:

# Telegram
npm install grammy

# Discord
npm install discord.js

# Slack
npm install @slack/bolt

# WhatsApp
npm install whatsapp-web.js

# Matrix
npm install matrix-bot-sdk

# MS Teams
npm install botbuilder express

# LINE
npm install @line/bot-sdk express

# Feishu/Lark
npm install @larksuiteoapi/node-sdk

# Twitch
npm install tmi.js

# IRC
npm install irc-framework

# Nostr
npm install nostr-tools ws

Channels like iMessage, Mattermost, Nextcloud, Synology, Zalo, BlueBubbles, and Google Chat use built-in fetch/http/WebSocket — no extra deps needed (Node 18+).

ServiceBridge — Remote Service Management

ServiceBridge makes it dead simple to expose any service's operations as IM commands. Think of it as a CLI for your service, but over Telegram/Discord/Slack.

import { ChannelManager, ServiceBridge } from "unified-channel";
import { TelegramAdapter } from "unified-channel/adapters/telegram";

const manager = new ChannelManager();
manager.addChannel(new TelegramAdapter(process.env.BOT_TOKEN!));

const bridge = new ServiceBridge(manager);

bridge
  .expose("deploy", async (args) => {
    const env = args[0] ?? "staging";
    // ... your deploy logic
    return `Deployed to ${env}`;
  }, { description: "Deploy the service" })

  .expose("restart", async () => {
    // ... restart logic
    return "Service restarted";
  }, { description: "Restart the service" })

  .exposeStatus(async () => {
    return "CPU: 23% | Memory: 512MB | Uptime: 3d";
  })

  .exposeLogs(async (args) => {
    const lines = args[0] ?? "20";
    // ... fetch logs
    return `Last ${lines} log lines...`;
  });

await bridge.run();

Now from Telegram:

/help              → lists all commands
/deploy prod       → "Deployed to prod"
/status            → "CPU: 23% | Memory: 512MB | Uptime: 3d"
/logs 50           → last 50 log lines

Features:

  • Auto /help generation from registered commands
  • Sync or async handlers — return a string or Promise<string>
  • Error handling — exceptions are caught and returned as error messages
  • Flag parsing — use parseFlags(args) for --key=value and --flag support
  • Fluent API — chain .expose() calls
  • Built-in /status, /logs, /help commands

Config File Support

Load a ChannelManager from a YAML or JSON config file with env var interpolation:

# unified-channel.yml
channels:
  telegram:
    token: "${UC_TELEGRAM_TOKEN}"
  discord:
    token: "${UC_DISCORD_TOKEN}"
import { loadConfig, ServiceBridge } from "unified-channel";

const manager = await loadConfig("./unified-channel.yml");
const bridge = new ServiceBridge(manager);
// ... expose commands ...
await bridge.run();

Supported formats: .yml, .yaml, .json. Environment variables use ${VAR} syntax with optional defaults: ${VAR:-fallback}.

API Reference

ChannelManager

manager.addChannel(adapter)       // Register a channel
manager.addMiddleware(mw)         // Add middleware (first-added runs first)
manager.onMessage(handler)        // Set fallback message handler
manager.send(channel, chatId, text, options?)  // Send to specific channel
manager.broadcast(text, { channel: chatId })   // Send to multiple channels
manager.getStatus()               // Get all channel statuses
manager.run()                     // Connect all channels and start listening
manager.shutdown()                // Disconnect all channels

ConversationMemory

Track conversation history per chat so your agent can maintain context across messages.

import { ChannelManager, ConversationMemory } from "unified-channel";

const manager = new ChannelManager();

// Add memory middleware (default: in-memory store, 50 turns max)
manager.addMiddleware(new ConversationMemory({ maxTurns: 20 }));

manager.onMessage(async (msg) => {
  // History is injected into metadata by the middleware
  const history = msg.metadata?.history as Array<{ role: string; content: string }>;
  console.log(`${history.length} previous messages in this chat`);
  return `You said: ${msg.content.text}`;
});

Bring your own store by implementing MemoryStore:

import { ConversationMemory, type MemoryStore, type HistoryEntry } from "unified-channel";

class RedisStore implements MemoryStore {
  async get(key: string): Promise<HistoryEntry[]> { /* ... */ }
  async append(key: string, entry: HistoryEntry): Promise<void> { /* ... */ }
  async trim(key: string, maxEntries: number): Promise<void> { /* ... */ }
  async clear(key: string): Promise<void> { /* ... */ }
}

manager.addMiddleware(new ConversationMemory({ store: new RedisStore() }));

RichReply

Build rich, cross-platform replies with a fluent API. Automatically degrades to plain text for unsupported channels.

import { RichReply } from "unified-channel";

const reply = new RichReply()
  .text("Server Status")
  .divider()
  .table(["Service", "Status"], [["API", "OK"], ["DB", "Slow"]])
  .code('const health = await check();', "typescript")
  .buttons([[
    { label: "Restart", callbackData: "restart" },
    { label: "Docs", url: "https://docs.example.com" },
  ]]);

// Platform-specific rendering
reply.toTelegram();   // { text: "...", parse_mode: "HTML", reply_markup: {...} }
reply.toDiscord();    // { content: "...", embeds: [...], components: [...] }
reply.toSlack();      // { blocks: [...] }
reply.toPlainText();  // Universal fallback

// Or auto-pick based on channel:
const outbound = reply.toOutbound("telegram"); // OutboundMessage

StreamingMiddleware

Handle streaming LLM responses with typing indicators and chunk-by-chunk delivery.

import { ChannelManager, StreamingMiddleware, StreamingReply } from "unified-channel";

const manager = new ChannelManager();
manager.addMiddleware(new StreamingMiddleware({ typingInterval: 3000 }));

manager.onMessage(async (msg) => {
  // Return a StreamingReply from an async generator
  async function* generate() {
    yield "Thinking";
    yield "...";
    yield " Here is the answer.";
  }
  return new StreamingReply(generate());
});

// Works with LLM SDKs too:
manager.onMessage(async (msg) => {
  const stream = await openai.chat.completions.create({
    model: "gpt-4",
    messages: [{ role: "user", content: msg.content.text }],
    stream: true,
  });
  return StreamingReply.fromLLM(stream);
});

The middleware collects all chunks into a final string reply. Optionally provide onTyping and onChunk callbacks via msg.metadata for real-time UI updates.

Testing

npm test

Also Available In

| Language | Repository | Status | |----------|-----------|--------| | Python | gambletan/unified-channel | 19 channels | | Java | gambletan/unified-channel-java | Coming soon | | TypeScript | You are here | 19 channels |

License

MIT