Common patterns and shared utilities for eevee modules.
@eeveebot:registry=https://npm.pkg.github.com/
@thehonker:registry=https://npm.pkg.github.com/npm install @eeveebot/libeeveePublished to the eeveebot GitHub Packages registry.
A minimal module using libeevee looks like this:
import {
createNatsConnection,
registerGracefulShutdown,
createModuleMetrics,
loadModuleConfig,
RateLimitConfig,
defaultRateLimit,
initializeSystemMetrics,
setupHttpServer,
registerCommand,
sendChatMessage,
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: string, message: Nats.Msg) => {
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 });That's it.
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.
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(); });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 });Sets up an Express server for Prometheus metrics scraping and health checks.
setupHttpServer({ port: '9000', serviceName: 'mymod' });Options:
| Field | Type | Default | Description |
|---|---|---|---|
port |
string |
'9000' |
Port for the HTTP server |
serviceName |
string |
— | Service name included in health responses |
natsClients |
NatsClient[] |
[] |
NATS clients to check for connectivity |
Health endpoint behavior:
GET /healthchecks connectivity of all providednatsClients- Returns 200 if all clients are connected (
isClosed()returnsfalse) - Returns 503 if any client is disconnected (
isClosed()returnstrue) - If
natsClientsis not provided or empty, always returns 200 (backward compatible)
// With NATS health checking:
const nats = await createNatsConnection();
setupHttpServer({ port: '9000', serviceName: 'mymod', natsClients: [nats] });Initializes the standard system metrics (uptime gauge, memory usage gauge) for the given module. Call once at startup.
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:
| Method | Description |
|---|---|
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 |
Direct access to shared Prometheus primitives — use these when createModuleMetrics isn't enough:
Counter,Gauge,Histogram— prom-client constructorsregister— shared Prometheus registrycommandCounter,commandProcessingTime,commandErrorCounter— pre-defined instrumentsnatsPublishCounter,natsSubscribeCounter— NATS operation trackingmessageCounter,messageProcessingTime— message-level metricsconnectionCounter,connectionGauge,channelCounter,channelGauge— connector metricsuptimeGauge,memoryUsageGauge— system metricserrorCounter,httpRequestCounter,httpRequestDuration— infra metricsrecordMessage(),recordConnection(),recordChannel(),recordCommand(),recordCommandError()— convenience wrappers
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:
| Field | Type | Default | Description |
|---|---|---|---|
commandUUID |
string |
— | Unique command identifier |
commandDisplayName |
string |
— | Human-readable name (also used for control re-sub) |
regex |
string |
— | Trigger regex |
platformPrefixAllowed |
boolean |
— | Allow platform prefix before command |
ratelimit |
RateLimitConfig |
— | Rate limiting config |
platform |
string |
'.*' |
Platform filter |
network |
string |
'.*' |
Network filter |
instance |
string |
'.*' |
Instance filter |
channel |
string |
'.*' |
Channel filter |
user |
string |
'.*' |
User filter |
Returns: Array of subscription promises (for the control re-registration subs).
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.
Same as sendChatMessage but with type: 'action.outgoing' for IRC actions (/me).
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? }
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:
| Field | Type | Description |
|---|---|---|
nats |
NatsClient |
Connected NATS client |
moduleName |
string |
Module name for responses |
startTime |
number |
Date.now() captured at startup |
metrics |
ModuleMetrics? |
For recording pub/sub metrics |
prometheusRegister |
any? |
Custom prom-client register |
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 booleanQueries 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 }Platform-aware IRC color helpers. All functions are no-ops on non-IRC platforms — they return the original text unchanged.
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' unchangedIrcColorName 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
Apply a named IRC background color. Same naming convention with bg prefix: bgwhite, bgblack, bgnavy, etc.
Apply an IRC text style. IrcStyleName values: normal, underline, bold, italic, inverse, strikethrough, monospace
styleForPlatform('important', 'irc', 'bold');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:
| Type | Color |
|---|---|
user |
cyan |
date |
green |
action |
yellow |
warning |
olive |
info |
blue |
title |
cyan |
error |
red |
success |
green |
highlight |
yellow |
muted |
gray |
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)Pick a random foreground color and apply it. Used by the emote module.
Apply rainbow colorization using irc-colors.rainbow(). Optionally provide a custom color array.
stripColors(text)— Remove IRC color codesstripStyle(text)— Remove IRC style codesstripColorsAndStyle(text)— Remove both
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');interface RateLimitConfig {
mode: 'enqueue' | 'drop';
level: 'platform' | 'instance' | 'channel' | 'user' | 'global';
limit: number;
interval: string; // e.g. "30s", "1m", "5m"
}Also exported as defaultRateLimit — { mode: 'drop', level: 'user', limit: 5, interval: '1m' }.
Type alias for Nats.Subscription | false. Used as the return type for NatsClient.subscribe() and the natsSubscriptions array type in consumer modules.
import { NatsSubscriptionResult } from '@eeveebot/libeevee';
const natsSubscriptions: Array<Promise<NatsSubscriptionResult>> = [];Mirror the registration helpers. Publish unregistration requests and subscribe to the corresponding control.* subjects for re-unregistration on demand.
Unregisters a command by publishing to command.unregister.
await unregisterCommand(nats, {
commandUUID: '9e5c1e0c-...',
commandDisplayName: 'echo',
}, metrics);CommandUnregistrationOptions:
| Field | Type | Description |
|---|---|---|
commandUUID |
string |
UUID of the command to unregister |
commandDisplayName |
string |
Display name (used for control re-sub) |
Unregisters a broadcast by publishing to broadcast.unregister.
BroadcastUnregistrationOptions: same fields as CommandUnregistrationOptions.
Removes help entries for a module by publishing to help.remove.
Compiles a regex pattern safely with ReDoS protection. Limits input to 500 characters and falls back to /.^/ (never-matching) on failure.
import { compileRegex } from '@eeveebot/libeevee';
const re = compileRegex('^!weather\\s+(.*)', { flags: 'i' });Formats milliseconds since startTime into a human-readable uptime string (e.g. "2d 3h 15m"). Exported via registerStatsHandlers but also available directly.
interface ChatMessage {
channel: string;
network: string;
instance: string;
platform: string;
text: string;
trace: string;
}interface HelpEntry {
command: string;
descr: string;
params: Array<{ param: string; required: boolean; descr: string }>;
aliases?: string[];
}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).
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).
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
}interface ValueColorRange {
lt?: { threshold: number; color: IrcColorName };
ranges?: Array<{ max: number; color: IrcColorName }>;
fallback: IrcColorName;
}A pre-configured winston logger instance. All eevee modules use this for structured logging — never console.log.
Format depends on NODE_ENV:
| Environment | Format | Example Output |
|---|---|---|
| Non-production | Colored, human-readable | 14: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.
These are re-exported from their original libraries for convenience:
ircColors— fullirc-colorsAPI (foreground/background colors, styles, rainbow, strip)NatsClient— NATS client classNatsClient.isClosed()— Returnstrueif the NATS connection is closed/disconnected. Used by the health endpoint to report connectivity status.handleSIG— Double-SIGINT force-exit handler
| Variable | Used By | Description |
|---|---|---|
NATS_HOST |
createNatsConnection() |
NATS server hostname |
NATS_TOKEN |
createNatsConnection() |
NATS auth token |
MODULE_CONFIG_PATH |
loadModuleConfig() |
Path to YAML config file |
HTTP_API_PORT |
setupHttpServer() |
Port for metrics/health HTTP server |
Contributions are welcome! Please see the eevee contributing guide for details.
CC BY-NC-SA 4.0 — see LICENSE for the full text.