@eeveebot/libeevee is the shared TypeScript library that provides common functionality for all eevee.bot modules. Every module imports it for NATS messaging, structured logging, metrics, and signal handling.

Installation

npmrc config

  @eeveebot:registry=https://npm.pkg.github.com/
@thehonker:registry=https://npm.pkg.github.com/
  

install

  npm install @eeveebot/libeevee
  

Published to the eeveebot GitHub Packages registry.

Quick Start

A minimal module using libeevee looks like this:

  import {
  createNatsConnection,
  registerGracefulShutdown,
  createModuleMetrics,
  loadModuleConfig,
  RateLimitConfig,
  defaultRateLimit,
  initializeSystemMetrics,
  setupHttpServer,
  registerCommand,
  sendChatMessage,
  registerBroadcast,
  registerHelp,
  registerStatsHandlers,
  HelpEntry,
} from '@eeveebot/libeevee';

// Config
interface MyConfig { ratelimit?: RateLimitConfig }
const config = loadModuleConfig<MyConfig>({});

// Bootstrap
const natsClients = [];
registerGracefulShutdown(natsClients);
const nats = await createNatsConnection();
natsClients.push(nats);

// Metrics & HTTP
const metrics = createModuleMetrics('mymod');
initializeSystemMetrics('mymod');
setupHttpServer({ port: process.env.HTTP_API_PORT || '9000', serviceName: 'mymod' });
const moduleStartTime = Date.now();

// Register a command
const subs = await registerCommand(nats, {
  commandUUID: '...',
  commandDisplayName: 'mymod',
  regex: '^mymod\\s+',
  ratelimit: config.ratelimit || defaultRateLimit,
}, metrics);

// Handle command execution
nats.subscribe('command.execute.<uuid>', (subject, message) => {
  const data = JSON.parse(message.string());
  sendChatMessage(nats, {
    channel: data.channel,
    network: data.network,
    instance: data.instance,
    platform: data.platform,
    text: `You said: ${data.text}`,
    trace: data.trace,
  }, metrics);
});

// Help & stats
const helpSubs = await registerHelp(nats, 'mymod', [
  { command: 'mymod', descr: 'Does the thing', params: [{ param: 'text', required: true, descr: 'Text to echo' }] },
], metrics);

const statsSubs = registerStatsHandlers({ nats, moduleName: 'mymod', startTime: moduleStartTime, metrics });
  

API Reference

Core Bootstrap

createNatsConnection(options?)

Reads NATS_HOST and NATS_TOKEN from environment, validates them, creates a NatsClient, and connects. Throws with a clear message on missing vars.

  const nats = await createNatsConnection();
// With custom env var names:
const nats = await createNatsConnection({ hostEnvVar: 'MY_NATS_HOST', tokenEnvVar: 'MY_NATS_TOKEN' });
  

Returns: Connected NatsClient instance.

registerGracefulShutdown(natsClients, cleanup?)

Registers SIGINT/SIGTERM handlers that drain all NATS clients, run optional cleanup, then delegate to handleSIG() for double-signal force-exit.

  const natsClients = [];
registerGracefulShutdown(natsClients);
// With DB cleanup:
registerGracefulShutdown(natsClients, async () => { if (db) db.close(); });
  

loadModuleConfig<T>(defaults)

Reads MODULE_CONFIG_PATH from env, parses the YAML file, returns the result. Falls back to defaults on missing path or parse errors.

  interface MyConfig { ratelimit?: RateLimitConfig; maxRetries?: number }
const config = loadModuleConfig<MyConfig>({ maxRetries: 3 });
  

setupHttpServer(options)

Sets up an Express server for Prometheus metrics scraping and health checks.

  setupHttpServer({ port: '9000', serviceName: 'mymod' });
  

Options:

FieldTypeDefaultDescription
portstring'9000'Port for the HTTP server
serviceNamestringService name included in health responses
natsClientsNatsClient[][]NATS clients to check for connectivity

Health endpoint behavior:

  • GET /health checks connectivity of all provided natsClients
  • Returns 200 if all clients are connected (isClosed() returns false)
  • Returns 503 if any client is disconnected (isClosed() returns true)
  • If natsClients is not provided or empty, always returns 200 (backward compatible)
  // With NATS health checking:
const nats = await createNatsConnection();
setupHttpServer({ port: '9000', serviceName: 'mymod', natsClients: [nats] });
  

initializeSystemMetrics(moduleName)

Initializes the standard system metrics (uptime gauge, memory usage gauge) for the given module. Call once at startup.


Metrics

createModuleMetrics(moduleName)

Factory that returns a ModuleMetrics object with pre-bound methods. Eliminates the need for per-module lib/metrics.mts files.

  const metrics = createModuleMetrics('dice');
metrics.recordCommand(platform, network, channel, 'success');
metrics.recordError('parse_error');
metrics.recordProcessingTime(0.025);
metrics.recordNatsPublish('command.register');
metrics.recordNatsSubscribe(subject);
  

ModuleMetrics methods:

MethodDescription
recordCommand(platform, network, channel, result)Increment command counter
recordError(errorType)Increment error counter
recordProcessingTime(seconds)Observe command processing time
recordNatsPublish(messageType)Increment NATS publish counter
recordNatsSubscribe(subject)Increment NATS subscribe counter

Low-level Metrics

Direct access to shared Prometheus primitives — use these when createModuleMetrics isn’t enough:

  • Counter, Gauge, Histogram — prom-client constructors
  • register — shared Prometheus registry
  • commandCounter, commandProcessingTime, commandErrorCounter — pre-defined instruments
  • natsPublishCounter, natsSubscribeCounter — NATS operation tracking
  • messageCounter, messageProcessingTime — message-level metrics
  • connectionCounter, connectionGauge, channelCounter, channelGauge — connector metrics
  • uptimeGauge, memoryUsageGauge — system metrics
  • errorCounter, httpRequestCounter, httpRequestDuration — infra metrics
  • recordMessage(), recordConnection(), recordChannel(), recordCommand(), recordCommandError() — convenience wrappers

Command & Message Helpers

registerCommand(nats, options, metrics?, autoControlSub?)

Registers a command with the router by publishing to command.register. By default, also subscribes to control.registerCommands and control.registerCommands.<displayName> for automatic re-registration.

  const subs = await registerCommand(nats, {
  commandUUID: '9e5c1e0c-...',
  commandDisplayName: 'echo',
  regex: '^echo\\s+',
  platformPrefixAllowed: true,
  ratelimit: { mode: 'drop', level: 'user', limit: 5, interval: '1m' },
  // Optional overrides (default: '.*' for all)
  platform: 'irc',
  network: 'libera',
}, metrics);
  

CommandRegistrationOptions:

FieldTypeDefaultDescription
commandUUIDstringUnique command identifier
commandDisplayNamestringHuman-readable name (also used for control re-sub)
regexstringTrigger regex
platformPrefixAllowedbooleanAllow platform prefix before command
ratelimitRateLimitConfigRate limiting config
platformstring'.*'Platform filter
networkstring'.*'Network filter
instancestring'.*'Instance filter
channelstring'.*'Channel filter
userstring'.*'User filter

Returns: Array of subscription promises (for the control re-registration subs).

sendChatMessage(nats, message, metrics?, type?)

Constructs the standard outgoing message envelope and publishes to chat.message.outgoing.<platform>.<instance>.<channel>.

  sendChatMessage(nats, {
  channel, network, instance, platform,
  text: 'Hello!',
  trace: data.trace,
}, metrics);
  

ChatMessage fields: channel, network, instance, platform, text, trace.

sendAction(nats, message, metrics?)

Same as sendChatMessage but with type: 'action.outgoing' for IRC actions (/me).

registerHelp(nats, moduleName, helpData, metrics?)

Publishes help data to help.update immediately, then subscribes to help.updateRequest and help.updateRequest.<moduleName> to re-publish when requested.

  const helpSubs = await registerHelp(nats, 'dice', [
  {
    command: 'roll',
    descr: 'Roll dice like a D&D nerd',
    params: [{ param: 'dicenotation', required: true, descr: 'XdY+Z or XdF or 4d6k3' }],
    aliases: ['r'],
  },
], metrics);
  

HelpEntry: { command, descr, params: Array<{ param, required, descr }>, aliases? }

registerBroadcast(nats, options, metrics?, autoControlSub?)

Registers a broadcast with the router by publishing to broadcast.register. By default, also subscribes to control.registerBroadcasts and control.registerBroadcasts.<displayName> for automatic re-registration.

  const broadcastSubs = await registerBroadcast(nats, {
  broadcastUUID: 'c3d4e5f6-...',
  broadcastDisplayName: 'seen',
}, metrics);
  

With a message filter (e.g., urltitle only wants messages with URLs):

  const broadcastSubs = await registerBroadcast(nats, {
  broadcastUUID: 'd4e5f6a7-...',
  broadcastDisplayName: 'urltitle',
  messageFilterRegex: 'https?://',
}, metrics);
  

BroadcastRegistrationOptions:

FieldTypeDefaultDescription
broadcastUUIDstringUnique broadcast identifier
broadcastDisplayNamestringHuman-readable name (used for control re-sub)
platformstring'.*'Platform filter
networkstring'.*'Network filter
instancestring'.*'Instance filter
channelstring'.*'Channel filter
userstring'.*'User filter
nickstring'.*'Nick filter
messageFilterRegexstring'.*'Message text filter

Returns: Array of subscription promises (for the control re-registration subs).


Stats & RPC

registerStatsHandlers(options)

Subscribes to stats.uptime and stats.emit.request and responds with module uptime, memory usage, and Prometheus metrics. Returns subscription objects.

  const statsSubs = registerStatsHandlers({
  nats,
  moduleName: 'dice',
  startTime: moduleStartTime,
  metrics,
  // Optional: custom Prometheus register (defaults to libeevee's shared register)
  // prometheusRegister: customRegister,
});
  

StatsHandlersOptions:

FieldTypeDescription
natsNatsClientConnected NATS client
moduleNamestringModule name for responses
startTimenumberDate.now() captured at startup
metricsModuleMetrics?For recording pub/sub metrics
prometheusRegisterany?Custom prom-client register

queryChannelUsers(nats, platform, instance, channel, options?)

Queries the IRC connector for the user list in a channel via NATS RPC. Sends a list-users-in-channel control command and waits for a reply on a unique channel (5s timeout).

  const users = await queryChannelUsers(nats, 'irc', 'libera', '#eevee', {
  metrics,
  producer: 'seen',       // for log messages
  timeoutMs: 5000,        // default: 5000
});
// users: Array<ChannelUser> — each user includes isChannelAdmin boolean
  

queryUserModes(nats, platform, instance, channel, nick, options?)

Queries the IRC connector for a specific user’s channel modes via NATS RPC. Sends a get-modes-for-user control command and waits for a reply on a unique channel (5s timeout). The server is polled fresh every time (no caching).

  const result = await queryUserModes(nats, 'irc', 'libera', '#eevee', 'alice', {
  metrics,
  producer: 'seen',       // for log messages
  timeoutMs: 5000,        // default: 5000
});
// result: UserModes — { channel, nick, modes, isChannelAdmin }
  

Colorization

Platform-aware IRC color helpers. All functions are no-ops on non-IRC platforms — they return the original text unchanged.

colorizeForPlatform(text, platform, color)

Apply a named IRC foreground color to text. Supports all 26 irc-colors foreground colors.

  colorizeForPlatform('hello', 'irc', 'cyan');   // → colored on IRC
colorizeForPlatform('hello', 'discord', 'cyan'); // → 'hello' unchanged
  

IrcColorName values: white, black, navy, green, red, brown, maroon, purple, violet, olive, yellow, lightgreen, lime, teal, bluecyan, cyan, aqua, blue, royal, pink, lightpurple, fuchsia, gray, grey, lightgray, lightgrey, silver

colorizeBgForPlatform(text, platform, color)

Apply a named IRC background color. Same naming convention with bg prefix: bgwhite, bgblack, bgnavy, etc.

styleForPlatform(text, platform, style)

Apply an IRC text style. IrcStyleName values: normal, underline, bold, italic, inverse, strikethrough, monospace

  styleForPlatform('important', 'irc', 'bold');
  

colorizeByType(text, platform, type, colorMap?)

Semantic color mapping — pick a color by meaning rather than name. Uses a default map, overrideable per module.

  colorizeByType('goos', 'irc', 'user'); // → cyan
colorizeByType('2d 3h ago', 'irc', 'date'); // → green

// Custom map:
const myMap = { user: 'pink', date: 'yellow', warning: 'red' };
colorizeByType(text, platform, 'user', myMap);
  

Default semantic map:

TypeColor
usercyan
dategreen
actionyellow
warningolive
infoblue
titlecyan
errorred
successgreen
highlightyellow
mutedgray

colorizeByValue(text, platform, value, definition)

Pick a color based on a numeric value and range thresholds. Perfect for temperature, wind speed, humidity, etc.

  colorizeByValue('72°F', 'irc', 72, {
  ranges: [
    { max: 32, color: 'blue' },
    { max: 50, color: 'cyan' },
    { max: 70, color: 'green' },
    { max: 80, color: 'yellow' },
    { max: 90, color: 'olive' },
  ],
  fallback: 'red',
});
// → yellow (72 is between 70 and 80)
  

randomColorForPlatform(text, platform)

Pick a random foreground color and apply it. Used by the emote module.

rainbowForPlatform(text, platform, colorArr?)

Apply rainbow colorization using irc-colors.rainbow(). Optionally provide a custom color array.

Strip Functions

  • stripColors(text) — Remove IRC color codes
  • stripStyle(text) — Remove IRC style codes
  • stripColorsAndStyle(text) — Remove both

Direct Color Maps

If you need raw access to validated color functions:

  import { fgColors, bgColors, styles } from '@eeveebot/libeevee';

fgColors.cyan('hello');  // same as colorizeForPlatform but without the platform check
bgColors.bgcyan('hello');
styles.bold('hello');
  

Types

RateLimitConfig

  interface RateLimitConfig {
  mode: 'enqueue' | 'drop';
  level: 'channel' | 'user' | 'global';
  limit: number;
  interval: string; // e.g. "30s", "1m", "5m"
}
  

Also exported as defaultRateLimit{ mode: 'drop', level: 'user', limit: 5, interval: '1m' }.

ChatMessage

  interface ChatMessage {
  channel: string;
  network: string;
  instance: string;
  platform: string;
  text: string;
  trace: string;
}
  

HelpEntry

  interface HelpEntry {
  command: string;
  descr: string;
  params: Array<{ param: string; required: boolean; descr: string }>;
  aliases?: string[];
}
  

ChannelUser

  interface ChannelUser {
  nick: string;
  ident: string;
  hostname: string;
  modes: string[];
  isChannelAdmin: boolean;
}
  

isChannelAdmin is true if the user has channel mode +h (halfop), +o (op), +a (admin/protect), or +q (owner).

UserModes

  interface UserModes {
  channel: string;
  nick: string;
  modes: string[];
  isChannelAdmin: boolean;
}
  

isChannelAdmin is true if the user has channel mode +h (halfop), +o (op), +a (admin/protect), or +q (owner).

BroadcastRegistrationOptions

  interface BroadcastRegistrationOptions {
  broadcastUUID: string;
  broadcastDisplayName: string;
  platform?: string;          // default '.*'
  network?: string;           // default '.*'
  instance?: string;          // default '.*'
  channel?: string;           // default '.*'
  user?: string;              // default '.*'
  nick?: string;              // default '.*'
  messageFilterRegex?: string; // default '.*'
}
  

SemanticColorMap

  interface SemanticColorMap {
  user?: IrcColorName;
  date?: IrcColorName;
  action?: IrcColorName;
  warning?: IrcColorName;
  info?: IrcColorName;
  title?: IrcColorName;
  error?: IrcColorName;
  success?: IrcColorName;
  highlight?: IrcColorName;
  muted?: IrcColorName;
  [key: string]: IrcColorName | undefined; // extensible
}
  

ValueColorRange

  interface ValueColorRange {
  lt?: { threshold: number; color: IrcColorName };
  ranges?: Array<{ max: number; color: IrcColorName }>;
  fallback: IrcColorName;
}
  

Logging

log

A pre-configured winston logger instance. All eevee modules use this for structured logging — never console.log.

Format depends on NODE_ENV:

EnvironmentFormatExample Output
Non-productionColored, human-readable14:32:01 [info] [seen] Module started
Production (NODE_ENV=production)JSON with ISO timestamps{"timestamp":"2026-05-07T14:32:01Z","level":"info","producer":"seen","message":"Module started"}

Both formats include errors({ stack: true }) (Error objects render their stack trace) and splat() (printf-style interpolation).

Log levels:

  import { log } from '@eeveebot/libeevee';

log.debug('Detailed tracing info', { producer: 'seen' });
log.info('Module started', { producer: 'seen' });
log.warn('Rate limit exceeded', { producer: 'seen', channel: '#eevee' });
log.error('Failed to connect', { producer: 'seen', error: err.message });
  

The producer convention:

Every log call should include a producer field in the metadata object identifying the subsystem that generated the message. This is not enforced by winston — it’s an eevee convention — but it makes filtering logs across a running deployment far more useful.

  log.info('Incoming message published', { producer: 'ircClient', channel: '#eevee', user: 'goos' });
// Non-production: 14:32:01 [info] [ircClient] Incoming message published
// Production: {"timestamp":"...","level":"info","producer":"ircClient","message":"Incoming message published","channel":"#eevee","user":"goos"}
  

Structured metadata:

Pass any key-value pairs as the second argument. They become fields in the log output (JSON in production, embedded in the formatted string in dev):

  log.info('Command executed', {
  producer: 'dice',
  platform: 'irc',
  channel: '#eevee',
  user: 'goos',
  result: '4d6k3 → 3, 5, 2, 6 (keep 3) → 14',
});
  

Error logging pattern:

Use log.error() with the error message (not the Error object) in the error field:

  try {
  await someOperation();
} catch (error) {
  log.error('Operation failed', {
    producer: 'seen',
    error: error instanceof Error ? error.message : String(error),
  });
}
  

This keeps the output structured and searchable. The errors({ stack: true }) transform handles stack traces when you do pass an Error object directly.


Passthrough Exports

These are re-exported from their original libraries for convenience:

  • ircColors — full irc-colors API (foreground/background colors, styles, rainbow, strip)
  • NatsClient — NATS client class
  • NatsClient.isClosed() — Returns true if the NATS connection is closed/disconnected. Used by the health endpoint to report connectivity status.
  • handleSIG — Double-SIGINT force-exit handler

Environment Variables

VariableUsed ByDescription
NATS_HOSTcreateNatsConnection()NATS server hostname
NATS_TOKENcreateNatsConnection()NATS auth token
MODULE_CONFIG_PATHloadModuleConfig()Path to YAML config file
HTTP_API_PORTsetupHttpServer()Port for metrics/health HTTP server

Source

The source code lives at github.com/eeveebot/libeevee-js.


Related: See Writing a Module for a full tutorial on using these APIs, and Module Lifecycle for health checks and shutdown behavior.