Skip to content

feat(framework): implement enterprise bot framework - #2710

Open
LuferOS wants to merge 1 commit into
WhiskeySockets:masterfrom
LuferOS:luferos
Open

feat(framework): implement enterprise bot framework#2710
LuferOS wants to merge 1 commit into
WhiskeySockets:masterfrom
LuferOS:luferos

Conversation

@LuferOS

@LuferOS LuferOS commented Jul 14, 2026

Copy link
Copy Markdown

Summary by cubic

Adds an enterprise-ready bot framework with a high-level Bot API, durable SQLite-backed state, media conversion tools, and group analytics to build resilient production bots.

  • New Features

    • Bot: middleware and bot.command(), message queue with exponential backoff reconnect.
    • Context: reply, react, replySticker, replyVoiceNote, and simple per-chat session API.
    • SQLiteStore: KV store via better-sqlite3 used for msgRetryCounterCache to reduce RAM use.
    • MediaManager: image/video to WebP stickers with metadata; audio to OGG voice notes.
    • StatsManager: group activity tracking and getGhosts(groupId) for inactive members.
    • Example Example/bot-example.ts, README migration notes, and public exports via src/index.ts.
  • Dependencies

    • Add better-sqlite3, ffmpeg-static, fluent-ffmpeg, node-webpmux.
    • Add types: @types/better-sqlite3, @types/fluent-ffmpeg.

Written for commit 4bb3bc8. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a bot framework with middleware, command routing, message handling, reconnection, and queued sending.
    • Added session persistence and SQLite-backed storage.
    • Added media conversion helpers for stickers and voice notes.
    • Added group statistics, rankings, and inactive-member insights.
    • Added convenient reply, reaction, sticker, and voice-note helpers.
    • Added a complete sticker bot example.
  • Documentation

    • Added Spanish feature overview, integration guidance, migration notes, and usage examples.
    • Centralized framework exports for simpler imports.

@whiskeysockets-bot

Copy link
Copy Markdown
Contributor

Thanks for opening this pull request and contributing to the project!

The next step is for the maintainers to review your changes. If everything looks good, it will be approved and merged into the main branch.

In the meantime, anyone in the community is encouraged to test this pull request and provide feedback.

✅ How to confirm it works

If you’ve tested this PR, please comment below with:

Tested and working ✅

This helps us speed up the review and merge process.

📦 To test this PR locally:

# NPM
npm install @whiskeysockets/baileys@LuferOS/Baileys-next#luferos

# Yarn (v2+)
yarn add @whiskeysockets/baileys@LuferOS/Baileys-next#luferos

# PNPM
pnpm add @whiskeysockets/baileys@LuferOS/Baileys-next#luferos

If you encounter any issues or have feedback, feel free to comment as well.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new SQLite-backed bot framework with middleware routing, reconnection and message queuing, session and group statistics management, media conversion helpers, public exports, documentation, and a sticker-command example.

Changes

Enterprise bot framework

Layer / File(s) Summary
Persistent storage, sessions, and statistics
src/Framework/Store/SQLiteStore.ts, src/Framework/SessionManager.ts, src/Framework/StatsManager.ts
Adds SQLite key-value persistence, per-JID session CRUD, group message and sticker counts, rankings, and ghost detection.
Message context and media conversion
src/Framework/Context.ts, src/Framework/MediaManager.ts
Adds quoted replies, reactions, session access, sticker conversion, and voice-note conversion helpers.
Bot orchestration and public API
src/Framework/Bot.ts, src/Framework/index.ts, src/index.ts, package.json
Adds middleware routing, queued sends, socket reconnection, statistics wiring, media dependencies, and framework re-exports.
Example and migration documentation
Example/bot-example.ts, README.md
Adds a multi-file-authenticated sticker bot example and Spanish framework integration guidance.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WhatsApp
  participant Bot
  participant Context
  participant MediaManager
  participant SQLiteStore
  WhatsApp->>Bot: messages.upsert
  Bot->>Context: create message context
  Bot->>SQLiteStore: record session or group statistics
  Bot->>Context: execute registered middleware
  Context->>MediaManager: convert media when needed
  MediaManager-->>Context: return converted buffer
  Context->>Bot: send quoted reply
  Bot->>WhatsApp: sendMessage
Loading

Poem

I’m a bunny with a bot in my burrow tonight,
SQLite keeps my carrot-counts tidy and right.
Middleware hops through messages with flair,
Stickers bloom softly from media in air.
Queue up the sends, let reconnections be sweet—
This framework makes every hop complete!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a new framework-level enterprise bot implementation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Example/bot-example.ts`:
- Around line 50-53: Move the `creds.update` listener registration in the bot
startup flow to after `await bot.start()`, when `bot.socket` exists. Update the
code around `saveCreds` so credential updates are consistently persisted, while
preserving the existing handler.

In `@src/Framework/Bot.ts`:
- Around line 115-128: Move the analytics block calling stats.observeMessage
inside the try boundary that currently protects executeMiddlewares in the
message handler. Ensure synchronous SQLite failures are caught and reported by
the existing middleware error handling while preserving the current participant,
group, and sticker conditions.
- Around line 59-79: Add unit tests covering the public routing APIs use,
command, and onText, including middleware ordering, duplicate next() behavior,
and command-boundary matching; also test queue resolution/rejection and socket
reconnect decisions. Add integration tests for the new incoming event handlers
and protocol/stanza paths introduced in the related changes.
- Around line 59-175: For src/Framework/Bot.ts lines 59-175, add unit and
integration tests covering middleware routing, command boundary matching, queued
sendMessage behavior and draining, statistics observation, and reconnect
cleanup. For src/Framework/StatsManager.ts lines 61-107, add tests for persisted
counters, rankings, JID normalization, and ghost detection. For
src/Framework/Context.ts lines 19-65, add tests covering message-text variants,
sessions, replies, reactions, stickers, and voice notes, using protocol/event
integration coverage where applicable.
- Around line 81-88: Replace console-based diagnostics with the configured
pino-compatible logger across Bot.sendMessage, queue-drain handling, and
middleware/connection handling in src/Framework/Bot.ts (81-88, 93-103, 124-159),
logging queued-message metadata without interpolating JIDs, using objects first
and messages last, and reporting middleware failures under err. Apply the same
logger contract in Example/bot-example.ts (44-54) by replacing its console
calls; preserve the existing diagnostic events and connection fields as
structured data.
- Around line 81-90: Update Bot.sendMessage and the loggedOut handling around
the referenced reconnection logic to reject every queued message when
reconnection is terminal, then clear messageQueue so payloads and promises are
released. Ensure subsequent queued sends do not remain pending after loggedOut;
add a bounded queue or enqueue timeout only if supported by the existing queue
design.
- Around line 63-69: Update the command matching in Bot.command so
startsWith(cmd) only matches an exact command or when the character immediately
after cmd is whitespace. Preserve handler and next execution for valid matches,
while preventing longer command names such as !stickerSpam from matching
!sticker.
- Around line 107-108: Update Framework.Bot.start to prevent duplicate socket
creation: retain and reuse the active socket when appropriate, cancel and clear
any pending reconnect timer before replacing it, and detach listeners from the
previous socket during teardown or restart. Ensure reconnect scheduling stores
its timer handle so restart and cleanup can cancel it.
- Around line 18-24: The framework surface still exposes unsafe any types. In
src/Framework/Bot.ts lines 18-24 and 81, derive EnqueuedMessage’s resolve
callback and sendMessage’s return type from WASocket['sendMessage']; in
src/Framework/Context.ts lines 25-31, default session generics to unknown; and
in Example/bot-example.ts lines 31-38, replace the any cast with a typed
WAMessage/quoted-message shape.

In `@src/Framework/Context.ts`:
- Around line 19-23: Update the Context.text getter to fall back to captions on
supported media messages, including image and video captions, after checking
conversation and extended text. Preserve the existing undefined result when no
text or media caption is present so Bot.command can receive direct media
commands.

In `@src/Framework/MediaManager.ts`:
- Around line 28-32: Replace synchronous filesystem calls throughout
MediaManager with awaited promise-based operations: at
src/Framework/MediaManager.ts lines 28-32 and 86-90, use fs.promises.writeFile
or copyFile; at lines 75-79 and 103-107, use fs.promises.readFile and replace
existence checks plus unlinkSync with fs.promises.unlink(path).catch(() => {}).
Ensure the containing methods remain async and preserve existing cleanup and
data flow.

In `@src/Framework/SessionManager.ts`:
- Line 14: Replace the default any types in SessionManager.get and the other
affected SessionManager methods with unknown, and update both MediaManager
error-handler parameters to use Error or unknown. Apply these changes at
src/Framework/SessionManager.ts lines 14-14, 18-18, and 26-26, plus
src/Framework/MediaManager.ts lines 51-51 and 100-100, without changing the
surrounding behavior.

In `@src/Framework/StatsManager.ts`:
- Around line 61-66: Normalize participant JIDs to a canonical user key in
observeMessage before insertStmt.run, and apply the same normalization to
participant IDs used by getGhosts when building or querying its Map. Replace
raw-string matching with the shared canonicalization utility so PN/LID/device
variants resolve to the same stats entry and active members are not classified
as ghosts.

In `@src/Framework/Store/SQLiteStore.ts`:
- Around line 28-43: Update SQLiteStore.set so it always JSON.stringify values,
including strings, before passing them to setStmt.run. Remove the typeof value
=== 'string' special case, preserving get’s JSON.parse-based round-trip behavior
and string types.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 97921d22-d208-4758-9423-4ada1a3b6606

📥 Commits

Reviewing files that changed from the base of the PR and between 731cd6b and 4bb3bc8.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (11)
  • Example/bot-example.ts
  • README.md
  • package.json
  • src/Framework/Bot.ts
  • src/Framework/Context.ts
  • src/Framework/MediaManager.ts
  • src/Framework/SessionManager.ts
  • src/Framework/StatsManager.ts
  • src/Framework/Store/SQLiteStore.ts
  • src/Framework/index.ts
  • src/index.ts

Comment thread Example/bot-example.ts
Comment on lines +50 to +53
// Guardar credenciales al conectarse/actualizarse
bot.socket?.ev.on('creds.update', saveCreds)

await bot.start()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Register creds.update after the socket exists.

bot.socket is always undefined before bot.start(), so optional chaining silently skips this handler and credential updates are never persisted.

-	bot.socket?.ev.on('creds.update', saveCreds)
-
 	await bot.start()
+	bot.socket?.ev.on('creds.update', saveCreds)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Guardar credenciales al conectarse/actualizarse
bot.socket?.ev.on('creds.update', saveCreds)
await bot.start()
// Guardar credenciales al conectarse/actualizarse
await bot.start()
bot.socket?.ev.on('creds.update', saveCreds)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Example/bot-example.ts` around lines 50 - 53, Move the `creds.update`
listener registration in the bot startup flow to after `await bot.start()`, when
`bot.socket` exists. Update the code around `saveCreds` so credential updates
are consistently persisted, while preserving the existing handler.

Comment thread src/Framework/Bot.ts
Comment on lines +18 to +24
type EnqueuedMessage = {
jid: string
content: AnyMessageContent
options: MiscMessageGenerationOptions
resolve: (value: any) => void
reject: (reason?: any) => void
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Bot.ts outline ==\n'
ast-grep outline src/Framework/Bot.ts --view expanded || true
printf '\n== Context.ts outline ==\n'
ast-grep outline src/Framework/Context.ts --view expanded || true
printf '\n== Example/bot-example.ts outline ==\n'
ast-grep outline Example/bot-example.ts --view expanded || true

printf '\n== Relevant lines in Bot.ts ==\n'
nl -ba src/Framework/Bot.ts | sed -n '1,180p'

printf '\n== Relevant lines in Context.ts ==\n'
nl -ba src/Framework/Context.ts | sed -n '1,120p'

printf '\n== Relevant lines in Example/bot-example.ts ==\n'
nl -ba Example/bot-example.ts | sed -n '1,120p'

printf '\n== Search for sendMessage typings / downloadMediaMessage usage ==\n'
rg -n "sendMessage\\(|downloadMediaMessage|WASocket" src Example node_modules --glob '!**/dist/**' --glob '!**/build/**' || true

Repository: WhiskeySockets/Baileys

Length of output: 2231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Bot.ts\n'
nl -ba src/Framework/Bot.ts | sed -n '1,130p'

printf '\nContext.ts\n'
nl -ba src/Framework/Context.ts | sed -n '1,80p'

printf '\nExample/bot-example.ts\n'
nl -ba Example/bot-example.ts | sed -n '1,70p'

Repository: WhiskeySockets/Baileys

Length of output: 207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== Bot.ts ==\n'
sed -n '1,140p' src/Framework/Bot.ts | cat -n

printf '\n== Context.ts ==\n'
sed -n '1,90p' src/Framework/Context.ts | cat -n

printf '\n== Example/bot-example.ts ==\n'
sed -n '1,90p' Example/bot-example.ts | cat -n

printf '\n== Search for sendMessage / downloadMediaMessage type definitions ==\n'
rg -n "sendMessage\\s*[:(]|downloadMediaMessage|type .*WASocket|interface .*WASocket|export type WASocket" src . --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' || true

Repository: WhiskeySockets/Baileys

Length of output: 25162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== Socket sendMessage signature ==\n'
sed -n '1310,1355p' src/Socket/messages-send.ts | cat -n

printf '\n== downloadMediaMessage signature ==\n'
sed -n '1040,1085p' src/Utils/messages.ts | cat -n

printf '\n== index exports for Context/Bot/Media helpers ==\n'
sed -n '1,80p' src/index.ts | cat -n

Repository: WhiskeySockets/Baileys

Length of output: 4032


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== SessionManager signatures ==\n'
sed -n '1,220p' src/Framework/SessionManager.ts | cat -n

printf '\n== Search for SessionManager get/set/update defaults ==\n'
rg -n "class SessionManager|get<|set<|update<|delete\\(" src/Framework src --glob '!**/dist/**' --glob '!**/build/**' || true

Repository: WhiskeySockets/Baileys

Length of output: 5791


Remove the remaining any types from the framework surface.

  • src/Framework/Bot.ts: derive the queue callback and sendMessage return type from WASocket['sendMessage'].
  • src/Framework/SessionManager.ts and src/Framework/Context.ts: default the session generics to unknown instead of any.
  • Example/bot-example.ts: replace the as any cast with a typed WAMessage/quoted-message shape.
📍 Affects 3 files
  • src/Framework/Bot.ts#L18-L24 (this comment)
  • src/Framework/Bot.ts#L81-L81
  • src/Framework/Context.ts#L25-L31
  • Example/bot-example.ts#L31-L38
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Framework/Bot.ts` around lines 18 - 24, The framework surface still
exposes unsafe any types. In src/Framework/Bot.ts lines 18-24 and 81, derive
EnqueuedMessage’s resolve callback and sendMessage’s return type from
WASocket['sendMessage']; in src/Framework/Context.ts lines 25-31, default
session generics to unknown; and in Example/bot-example.ts lines 31-38, replace
the any cast with a typed WAMessage/quoted-message shape.

Source: Coding guidelines

Comment thread src/Framework/Bot.ts
Comment on lines +59 to +79
public use(middleware: MiddlewareFn) {
this.middlewares.push(middleware)
}

public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) {
this.use(async (ctx, next) => {
if (ctx.text?.startsWith(cmd)) {
await handler(ctx)
}
await next()
})
}

public onText(handler: (ctx: Context) => Promise<void> | void) {
this.use(async (ctx, next) => {
if (ctx.text) {
await handler(ctx)
}
await next()
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Add tests for routing, queuing, and socket lifecycle paths.

Unit-test middleware ordering/duplicate next(), command boundaries, queue resolution/rejection, and reconnect decisions. Add integration tests for the new incoming event handlers.

As per coding guidelines, new public APIs require unit tests and new protocol paths or stanza handlers require integration tests.

Also applies to: 107-175

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Framework/Bot.ts` around lines 59 - 79, Add unit tests covering the
public routing APIs use, command, and onText, including middleware ordering,
duplicate next() behavior, and command-boundary matching; also test queue
resolution/rejection and socket reconnect decisions. Add integration tests for
the new incoming event handlers and protocol/stanza paths introduced in the
related changes.

Source: Coding guidelines

Comment thread src/Framework/Bot.ts
Comment on lines +59 to +175
public use(middleware: MiddlewareFn) {
this.middlewares.push(middleware)
}

public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) {
this.use(async (ctx, next) => {
if (ctx.text?.startsWith(cmd)) {
await handler(ctx)
}
await next()
})
}

public onText(handler: (ctx: Context) => Promise<void> | void) {
this.use(async (ctx, next) => {
if (ctx.text) {
await handler(ctx)
}
await next()
})
}

public async sendMessage(jid: string, content: AnyMessageContent, options: MiscMessageGenerationOptions = {}): Promise<any> {
if (this.isConnected && this.socket) {
return this.socket.sendMessage(jid, content, options)
} else {
// Queue the message
return new Promise((resolve, reject) => {
this.messageQueue.push({ jid, content, options, resolve, reject })
console.log(`[Bot] Socket no conectado. Mensaje para ${jid} encolado. (Cola: ${this.messageQueue.length})`)
})
}
}

private drainQueue() {
if (!this.isConnected || !this.socket || this.messageQueue.length === 0) return

console.log(`[Bot] Drenando cola de mensajes: ${this.messageQueue.length} pendientes.`)
const queueToProcess = [...this.messageQueue]
this.messageQueue = []

for (const msg of queueToProcess) {
this.socket.sendMessage(msg.jid, msg.content, msg.options)
.then(msg.resolve)
.catch(msg.reject)
}
}

public async start() {
this.socket = makeWASocket(this.config)

this.socket.ev.on('messages.upsert', async ({ messages, type }) => {
if (type !== 'notify') return
for (const msg of messages) {
const ctx = new Context(this, msg)

// Analíticas: Observar el mensaje antes de los middlewares
if (this.stats && ctx.remoteJid && isJidGroup(ctx.remoteJid)) {
const participant = msg.key.participant || msg.participant
if (participant) {
const isSticker = !!msg.message?.stickerMessage
this.stats.observeMessage(ctx.remoteJid, participant, isSticker)
}
}

try {
await this.executeMiddlewares(ctx)
} catch (err) {
console.error('Error executing middleware:', err)
}
}
})

this.socket.ev.on('connection.update', (update) => {
const { connection, lastDisconnect } = update

if (connection === 'open') {
console.log('[Bot] Conectado exitosamente.')
this.isConnected = true
this.reconnectAttempts = 0
this.drainQueue()
}

if (connection === 'close') {
this.isConnected = false
const error = lastDisconnect?.error as Boom | undefined
const statusCode = error?.output?.statusCode
const shouldReconnect = statusCode !== DisconnectReason.loggedOut

console.log(`[Bot] Conexión cerrada. Razón: ${statusCode}`)

if (shouldReconnect) {
this.reconnectAttempts++
const delay = Math.min(this.MAX_RECONNECT_DELAY, this.BASE_RECONNECT_DELAY * (2 ** (this.reconnectAttempts - 1)))
console.log(`[Bot] Intentando reconectar en ${delay}ms (Intento ${this.reconnectAttempts})...`)

setTimeout(() => {
this.start()
}, delay)
} else {
console.log('[Bot] Sesión cerrada (LoggedOut). No se reconectará automáticamente.')
}
}
})
}

private async executeMiddlewares(ctx: Context) {
let index = -1
const dispatch = async (i: number): Promise<void> => {
if (i <= index) throw new Error('next() called multiple times')
index = i
const middleware = this.middlewares[i]
if (middleware) {
await middleware(ctx, () => dispatch(i + 1))
}
}
await dispatch(0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

The new public framework and event paths lack required test coverage.

  • src/Framework/Bot.ts#L59-L175: test middleware routing, command boundaries, queue behavior, statistics wiring, and reconnect cleanup.
  • src/Framework/StatsManager.ts#L61-L107: test persisted counters, rankings, JID normalization, and ghost detection.
  • src/Framework/Context.ts#L19-L65: test message-text variants, sessions, replies, reactions, stickers, and voice notes.

As per coding guidelines, public APIs require unit tests and protocol/event paths require integration tests.

📍 Affects 3 files
  • src/Framework/Bot.ts#L59-L175 (this comment)
  • src/Framework/StatsManager.ts#L61-L107
  • src/Framework/Context.ts#L19-L65
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Framework/Bot.ts` around lines 59 - 175, For src/Framework/Bot.ts lines
59-175, add unit and integration tests covering middleware routing, command
boundary matching, queued sendMessage behavior and draining, statistics
observation, and reconnect cleanup. For src/Framework/StatsManager.ts lines
61-107, add tests for persisted counters, rankings, JID normalization, and ghost
detection. For src/Framework/Context.ts lines 19-65, add tests covering
message-text variants, sessions, replies, reactions, stickers, and voice notes,
using protocol/event integration coverage where applicable.

Source: Coding guidelines

Comment thread src/Framework/Bot.ts
Comment on lines +63 to +69
public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) {
this.use(async (ctx, next) => {
if (ctx.text?.startsWith(cmd)) {
await handler(ctx)
}
await next()
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require a command boundary after the prefix.

startsWith(cmd) also invokes !sticker for inputs such as !stickerSpam. Match the exact command or require whitespace after it.

-			if (ctx.text?.startsWith(cmd)) {
+			if (ctx.text === cmd || ctx.text?.startsWith(`${cmd} `)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) {
this.use(async (ctx, next) => {
if (ctx.text?.startsWith(cmd)) {
await handler(ctx)
}
await next()
})
public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) {
this.use(async (ctx, next) => {
if (ctx.text === cmd || ctx.text?.startsWith(`${cmd} `)) {
await handler(ctx)
}
await next()
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Framework/Bot.ts` around lines 63 - 69, Update the command matching in
Bot.command so startsWith(cmd) only matches an exact command or when the
character immediately after cmd is whitespace. Preserve handler and next
execution for valid matches, while preventing longer command names such as
!stickerSpam from matching !sticker.

Comment thread src/Framework/Context.ts
Comment on lines +19 to +23
public get text(): string | undefined {
return this.message.message?.conversation ||
this.message.message?.extendedTextMessage?.text ||
undefined
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Include media captions in Context.text.

The sticker example expects !sticker on an image or video, but Bot.command only sees Context.text; direct media captions are currently ignored, so the handler never runs.

Proposed fix
 	public get text(): string | undefined {
-		return this.message.message?.conversation || 
-			this.message.message?.extendedTextMessage?.text || 
+		return this.message.message?.conversation ||
+			this.message.message?.extendedTextMessage?.text ||
+			this.message.message?.imageMessage?.caption ||
+			this.message.message?.videoMessage?.caption ||
 			undefined
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public get text(): string | undefined {
return this.message.message?.conversation ||
this.message.message?.extendedTextMessage?.text ||
undefined
}
public get text(): string | undefined {
return this.message.message?.conversation ||
this.message.message?.extendedTextMessage?.text ||
this.message.message?.imageMessage?.caption ||
this.message.message?.videoMessage?.caption ||
undefined
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Framework/Context.ts` around lines 19 - 23, Update the Context.text
getter to fall back to captions on supported media messages, including image and
video captions, after checking conversation and extended text. Preserve the
existing undefined result when no text or media caption is present so
Bot.command can receive direct media commands.

Comment on lines +28 to +32
if (Buffer.isBuffer(inputPathOrBuffer)) {
fs.writeFileSync(tempInput, inputPathOrBuffer)
} else {
fs.copyFileSync(inputPathOrBuffer, tempInput)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔴 Critical | 🏗️ Heavy lift

Replace synchronous filesystem operations with async equivalents.

Using synchronous filesystem operations (fs.writeFileSync, fs.readFileSync, fs.copyFileSync, etc.) blocks the main Node.js event loop. In a high-scalability bot framework, this will delay the processing of concurrent messages and can cause the WebSocket connection to drop due to missed pings.

  • src/Framework/MediaManager.ts#L28-L32: Replace fs.writeFileSync and fs.copyFileSync with await fs.promises.writeFile and await fs.promises.copyFile.
  • src/Framework/MediaManager.ts#L75-L79: Replace fs.readFileSync with await fs.promises.readFile. Replace fs.existsSync and fs.unlinkSync with await fs.promises.unlink(path).catch(() => {}).
  • src/Framework/MediaManager.ts#L86-L90: Replace fs.writeFileSync and fs.copyFileSync with await fs.promises.writeFile and await fs.promises.copyFile.
  • src/Framework/MediaManager.ts#L103-L107: Replace fs.readFileSync with await fs.promises.readFile. Replace fs.existsSync and fs.unlinkSync with await fs.promises.unlink(path).catch(() => {}).
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 28-28: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(tempInput, inputPathOrBuffer)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

📍 Affects 1 file
  • src/Framework/MediaManager.ts#L28-L32 (this comment)
  • src/Framework/MediaManager.ts#L75-L79
  • src/Framework/MediaManager.ts#L86-L90
  • src/Framework/MediaManager.ts#L103-L107
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Framework/MediaManager.ts` around lines 28 - 32, Replace synchronous
filesystem calls throughout MediaManager with awaited promise-based operations:
at src/Framework/MediaManager.ts lines 28-32 and 86-90, use
fs.promises.writeFile or copyFile; at lines 75-79 and 103-107, use
fs.promises.readFile and replace existence checks plus unlinkSync with
fs.promises.unlink(path).catch(() => {}). Ensure the containing methods remain
async and preserve existing cleanup and data flow.

return `session_${jid}`
}

public get<T = any>(jid: string): T | undefined {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid using any in new code.

As per coding guidelines, do not use any in new code. Use unknown or a more specific type to ensure type safety.

  • src/Framework/SessionManager.ts#L14-L14: Replace T = any with T = unknown.
  • src/Framework/SessionManager.ts#L18-L18: Replace T = any with T = unknown.
  • src/Framework/SessionManager.ts#L26-L26: Replace T = any with T = unknown.
  • src/Framework/MediaManager.ts#L51-L51: Replace (err: any) with (err: Error) or (err: unknown).
  • src/Framework/MediaManager.ts#L100-L100: Replace (err: any) with (err: Error) or (err: unknown).
📍 Affects 2 files
  • src/Framework/SessionManager.ts#L14-L14 (this comment)
  • src/Framework/SessionManager.ts#L18-L18
  • src/Framework/SessionManager.ts#L26-L26
  • src/Framework/MediaManager.ts#L51-L51
  • src/Framework/MediaManager.ts#L100-L100
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Framework/SessionManager.ts` at line 14, Replace the default any types in
SessionManager.get and the other affected SessionManager methods with unknown,
and update both MediaManager error-handler parameters to use Error or unknown.
Apply these changes at src/Framework/SessionManager.ts lines 14-14, 18-18, and
26-26, plus src/Framework/MediaManager.ts lines 51-51 and 100-100, without
changing the surrounding behavior.

Source: Coding guidelines

Comment on lines +61 to +66
public observeMessage(groupJid: string, userJid: string, isSticker: boolean) {
const msgCount = 1
const stickerCount = isSticker ? 1 : 0
const now = Date.now()

this.insertStmt.run(groupJid, userJid, msgCount, stickerCount, now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/Framework/StatsManager.ts --view expanded

printf '\n--- file ---\n'
cat -n src/Framework/StatsManager.ts | sed -n '1,220p'

printf '\n--- JID helper usage in this file ---\n'
rg -n "jidDecode|jidNormalizedUser|areJidsSameUser|isJidUser|split\\('@'\\)|===" src/Framework/StatsManager.ts

printf '\n--- related references ---\n'
rg -n "getGhosts|observeMessage|StatsManager" src

Repository: WhiskeySockets/Baileys

Length of output: 5301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- call sites ---\n'
rg -n "observeMessage\\(|getGhosts\\(" src

printf '\n--- participant id handling ---\n'
rg -n "participants\\.map\\(|groupMetadata\\(|jidNormalizedUser|jidDecode|areJidsSameUser|isJidUser|participant\\.id|msg\\.key\\.participant|remoteJid" src

printf '\n--- StatsManager neighbors ---\n'
ast-grep outline src/Framework --view expanded

Repository: WhiskeySockets/Baileys

Length of output: 33736


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- StatsManager references ---\n'
rg -n "observeMessage\\(|getGhosts\\(" src

printf '\n--- JID helper / metadata references ---\n'
rg -n "jidNormalizedUser|jidDecode|areJidsSameUser|isJidUser|groupMetadata\\(|participants\\.map\\(|participant\\.id|msg\\.key\\.participant|remoteJid" src

printf '\n--- file list in src/Framework ---\n'
git ls-files src/Framework

Repository: WhiskeySockets/Baileys

Length of output: 29705


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- StatsManager call sites ---\n'
rg -n "observeMessage\\(|getGhosts\\(" src

printf '\n--- JID and metadata handling ---\n'
rg -n "jidNormalizedUser|jidDecode|areJidsSameUser|isJidUser|groupMetadata\\(|participants\\.map\\(|participant\\.id|msg\\.key\\.participant|remoteJid" src

printf '\n--- related Framework files ---\n'
git ls-files src/Framework

Repository: WhiskeySockets/Baileys

Length of output: 29695


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- Bot.ts slice ---\n'
sed -n '100,130p' src/Framework/Bot.ts | cat -n

printf '\n--- process-message normalization slice ---\n'
sed -n '110,140p' src/Utils/process-message.ts | cat -n

printf '\n--- group metadata consumers around participant ids ---\n'
sed -n '90,120p' src/Socket/groups.ts | cat -n
sed -n '210,245p' src/Socket/groups.ts | cat -n

Repository: WhiskeySockets/Baileys

Length of output: 5400


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- messages.upsert emission path ---\n'
rg -n "messages\\.upsert|cleanMessage\\(|processMessage\\(" src/Socket src/Utils

printf '\n--- group metadata normalization ---\n'
rg -n "extractGroupMetadata|groupMetadata\\(|participants.*id|jidNormalizedUser\\(.*participants" src/Socket src/Utils src/WABinary

printf '\n--- relevant slices ---\n'
sed -n '1480,1525p' src/Socket/messages-recv.ts | cat -n
sed -n '1,120p' src/Socket/groups.ts | cat -n

Repository: WhiskeySockets/Baileys

Length of output: 8934


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- StatsManager tests ---\n'
rg -n "StatsManager|observeMessage|getTopUsers|getTopStickers|getGhosts" src/__tests__ src

printf '\n--- extractGroupMetadata implementation ---\n'
sed -n '304,420p' src/Socket/groups.ts | cat -n

printf '\n--- cleanMessage call site and message emission ---\n'
sed -n '1758,1785p' src/Socket/messages-recv.ts | cat -n
sed -n '480,520p' src/Utils/process-message.ts | cat -n

Repository: WhiskeySockets/Baileys

Length of output: 8643


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- upsertMessage definition ---\n'
rg -n "const upsertMessage|function upsertMessage|upsertMessage =" src/Socket src/Utils

printf '\n--- messages.upsert emitters with final message shape ---\n'
sed -n '1180,1235p' src/Socket/chats.ts | cat -n
sed -n '1590,1645p' src/Socket/messages-recv.ts | cat -n

Repository: WhiskeySockets/Baileys

Length of output: 4951


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- jid-utils slice ---\n'
sed -n '1,160p' src/WABinary/jid-utils.ts | cat -n

printf '\n--- StatsManager slice ---\n'
sed -n '61,106p' src/Framework/StatsManager.ts | cat -n

Repository: WhiskeySockets/Baileys

Length of output: 7139


Normalize participant JIDs before storing or matching stats. observeMessage writes the raw participant JID, and getGhosts compares it with raw groupMetadata participant IDs using exact Map keys. PN/LID/device variants can split one user across buckets and flag active members as ghosts. Use a canonical JID/user key on both paths instead of raw string equality.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Framework/StatsManager.ts` around lines 61 - 66, Normalize participant
JIDs to a canonical user key in observeMessage before insertStmt.run, and apply
the same normalization to participant IDs used by getGhosts when building or
querying its Map. Replace raw-string matching with the shared canonicalization
utility so PN/LID/device variants resolve to the same stats entry and active
members are not classified as ghosts.

Source: Coding guidelines

Comment on lines +28 to +43
get<T>(key: string): T | undefined {
const row = this.getStmt.get(key) as { value: string } | undefined
if (row) {
try {
return JSON.parse(row.value) as T
} catch {
return row.value as unknown as T
}
}
return undefined
}

set<T>(key: string, value: T): void {
const strValue = typeof value === 'string' ? value : JSON.stringify(value)
this.setStmt.run(key, strValue)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fix data corruption caused by string serialization logic.

When typeof value === 'string', the value is stored as a raw string. Upon retrieval, JSON.parse will attempt to parse it. If the saved string happens to be a number (e.g., "123"), a boolean (e.g., "true"), or valid JSON (e.g., "{\"a\":1}"), it will be incorrectly parsed into those types instead of remaining a string. This breaks the key-value store's data integrity guarantees.

Unconditionally stringify the value on set to ensure all types (including strings) safely round-trip through JSON encoding.

🐛 Proposed fix
	get<T>(key: string): T | undefined {
		const row = this.getStmt.get(key) as { value: string } | undefined
		if (row) {
			try {
				return JSON.parse(row.value) as T
			} catch {
				return row.value as unknown as T
			}
		}
		return undefined
	}

	set<T>(key: string, value: T): void {
+		if (value === undefined) {
+			this.del(key)
+			return
+		}
-		const strValue = typeof value === 'string' ? value : JSON.stringify(value)
-		this.setStmt.run(key, strValue)
+		this.setStmt.run(key, JSON.stringify(value))
	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
get<T>(key: string): T | undefined {
const row = this.getStmt.get(key) as { value: string } | undefined
if (row) {
try {
return JSON.parse(row.value) as T
} catch {
return row.value as unknown as T
}
}
return undefined
}
set<T>(key: string, value: T): void {
const strValue = typeof value === 'string' ? value : JSON.stringify(value)
this.setStmt.run(key, strValue)
}
get<T>(key: string): T | undefined {
const row = this.getStmt.get(key) as { value: string } | undefined
if (row) {
try {
return JSON.parse(row.value) as T
} catch {
return row.value as unknown as T
}
}
return undefined
}
set<T>(key: string, value: T): void {
if (value === undefined) {
this.del(key)
return
}
this.setStmt.run(key, JSON.stringify(value))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Framework/Store/SQLiteStore.ts` around lines 28 - 43, Update
SQLiteStore.set so it always JSON.stringify values, including strings, before
passing them to setStmt.run. Remove the typeof value === 'string' special case,
preserving get’s JSON.parse-based round-trip behavior and string types.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

22 issues found across 13 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/Framework/Store/SQLiteStore.ts">

<violation number="1" location="src/Framework/Store/SQLiteStore.ts:41">
P2: String cache values that are valid JSON change type on retrieval because `get` parses every stored value while `set` leaves strings unencoded. Serialize strings too so `get<string>` round-trips its declared type.</violation>
</file>

<file name="src/Framework/MediaManager.ts">

<violation number="1" location="src/Framework/MediaManager.ts:9">
P1: Loading the framework fails under this package's ESM runtime because `require` is undefined. Use an ESM import for `node-webpmux` (or `createRequire`) so importing `Context` can initialize.</violation>

<violation number="2" location="src/Framework/MediaManager.ts:25">
P3: The temp file management pattern (write input → process → read output → cleanup) is duplicated identically across both conversion methods. Extracting it into a shared helper (e.g., `private static async processWithFfmpeg(input, processor)`) would reduce duplication and prevent cleanup bugs in future conversion methods.</violation>

<violation number="3" location="src/Framework/MediaManager.ts:29">
P2: Synchronous filesystem operations (`fs.writeFileSync`, `fs.readFileSync`, `fs.copyFileSync`, `fs.unlinkSync`) block the event loop in these `async` methods. Under concurrent message load, this can stall WebSocket ping/pong and cause disconnects. Since the methods are already `async`, replace with `fs.promises.*` equivalents (`writeFile`, `readFile`, `copyFile`, `unlink`).</violation>

<violation number="4" location="src/Framework/MediaManager.ts:36">
P1: The `convertToVoiceNote` method is missing critical audio encoding parameters that WhatsApp expects for voice notes. Without specifying mono channel (`-ac 1`), sample rate, bitrate, and `-application voip`, the output OGG Opus file may be rejected by WhatsApp or play incorrectly on the receiving end. Add these options to ensure the output is a standard mono Opus voice note at an appropriate bitrate.</violation>

<violation number="5" location="src/Framework/MediaManager.ts:66">
P2: Sticker pack metadata is encoded with a zero EXIF payload length, so WhatsApp can ignore `packname` and `author`. Write the serialized JSON length into offset 14 before saving the image.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:50">
P1: The `creds.update` listener in the example will never fire because `bot.socket` is `undefined` before `bot.start()` is called. The optional chaining `?.` makes this a silent no-op — credentials are never persisted, which means the authentication state is lost on restart and the user will be forced to re-authenticate. Move `bot.socket.ev.on('creds.update', saveCreds)` to **after** `await bot.start()` to ensure the socket exists. Note that reconnection will create a new socket without the listener, so an internal hook in `Bot.start()` or a wrapper around the socket creation would be more robust.</violation>
</file>

<file name="src/Framework/StatsManager.ts">

<violation number="1" location="src/Framework/StatsManager.ts:66">
P1: JIDs stored/queried without normalization — device suffixes can cause ghost-detection false positives.

`observeMessage` stores the raw `userJid` parameter into the `group_stats` table. But `getGhosts` compares stored JIDs against `groupMetadata().participants.map(p => p.id)`, which returns device-stripped JIDs. When a `userJid` carries a device suffix (`:0`, `:42`), the `Map.get(jid)` lookup will miss the participant, classifying them as a never-spoken ghost even though they have sent messages.

**Recommendation:** Normalize all JIDs with `jidNormalizedUser` before storing and querying.

```ts
// In observeMessage:
const normalizedUserJid = jidNormalizedUser(userJid)
this.insertStmt.run(jidNormalizedUser(groupJid), normalizedUserJid, msgCount, stickerCount, now)

// In getGhosts:
const normalizedJid = jidNormalizedUser(jid)
const lastActive = statsMap.get(normalizedJid)
```</violation>

<violation number="2" location="src/Framework/StatsManager.ts:69">
P2: Negative `limit` returns every member instead of a bounded leaderboard, since SQLite treats negative `LIMIT` as unlimited. Validate a non-negative integer before executing either leaderboard query.</violation>

<violation number="3" location="src/Framework/StatsManager.ts:78">
P2: Calling `getGhosts` while reconnecting reaches `groupMetadata` on a closed socket rather than failing as "not connected." Include `isConnected` in the availability guard.</violation>

<violation number="4" location="src/Framework/StatsManager.ts:79">
P2: Plain `Error` for socket-disconnected state — downstream code can't distinguish this from internal assertion failures.

AGENTS.md specifies that protocol/connection errors should use `Boom` with a `statusCode` so callers can branch on `error.output.statusCode`. Currently, `getGhosts` throws a bare `Error`, and any code that tries to retry on connection errors has no programmatic way to identify the failure type.

**Recommendation:** Throw `new Boom('Socket not connected', { statusCode: 503 })` instead, consistent with the project convention.</violation>
</file>

<file name="src/Framework/Bot.ts">

<violation number="1" location="src/Framework/Bot.ts:46">
P2: Multiple `Bot` instances in one working directory silently share sessions, analytics, and retry counters through `baileys_store.db`; one bot can overwrite another's state. Make the store/path configurable or derive an isolated path/namespace per bot.</violation>

<violation number="2" location="src/Framework/Bot.ts:65">
P2: `startsWith(cmd)` matches partial overlaps — e.g., registering `!sticker` would also fire for `!stickerSpam` or `!stickerset`. Require an exact match or a word boundary (whitespace) after the command prefix to avoid unintended handler invocations.</violation>

<violation number="3" location="src/Framework/Bot.ts:87">
P2: Queued `sendMessage` calls remain pending forever after `DisconnectReason.loggedOut`, so callers awaiting a reply/send can hang indefinitely. Reject and clear queued entries on terminal close (and define a bounded/discard policy for prolonged disconnection).</violation>

<violation number="4" location="src/Framework/Bot.ts:88">
P2: Disconnected sends expose recipient JIDs in unstructured stdout logs and bypass the configured logger. Use structured logging and avoid logging the JID unless required for an approved diagnostic path.</violation>

<violation number="5" location="src/Framework/Bot.ts:88">
P2: Bot.ts uses `console.log` for all logging output, but the codebase convention requires pino-compatible structured logging via a `logger: ILogger` instance. Every async-boundary path should accept and use a logger. Without it, log processors can't index or filter these messages, and downstream users can't control log levels. Accept a `logger` in BotConfig and use `this.logger.info(...)` / `this.logger.warn(...)` with structured payloads (e.g. `{ queueLength: this.messageQueue.length }`) instead of console.log string interpolation.</violation>

<violation number="6" location="src/Framework/Bot.ts:120">
P1: `this.stats.observeMessage(...)` executes synchronous SQLite I/O outside the `try` block. If it throws (e.g., database locked, disk full), the entire async event handler rejects and all middleware for that message is skipped. Move this call inside the `try` block so a stats failure doesn't prevent message processing.</violation>

<violation number="7" location="src/Framework/Bot.ts:156">
P2: Reconnect scheduling fails lint and can leave a failed `start()` rejection unhandled because its Promise is dropped. Explicitly handle the reconnect promise.</violation>
</file>

<file name="src/Framework/Context.ts">

<violation number="1" location="src/Framework/Context.ts:3">
P1: `makeWASocket` is only used in a type context (`typeof makeWASocket` inside a `ReturnType<>`) — never called or used as a runtime value. With `verbatimModuleSyntax`, this must be `import type` to avoid emitting an unnecessary runtime import.</violation>

<violation number="2" location="src/Framework/Context.ts:8">
P3: The new public Context helpers have no unit coverage for reply quoting, session delegation, reactions, or media conversion failures. Add colocated Context tests so this API contract is regression-tested.</violation>

<violation number="3" location="src/Framework/Context.ts:19">
P2: The `text` getter only extracts text from `conversation` and `extendedTextMessage.text`, missing captions on image, video, document, and other media messages. When a user sends an image with a caption like "Check this out", `ctx.text` returns `undefined` and text-based handlers silently skip the message. Add media caption fields to the getter, for example `message?.imageMessage?.caption`, `message?.videoMessage?.caption`, `message?.documentMessage?.caption`.</violation>
</file>

<file name="Example/bot-example.ts">

<violation number="1" location="Example/bot-example.ts:51">
P0: The `saveCreds` listener is registered on `bot.socket?.ev` before `bot.start()` is called, but `bot.socket` is only assigned during `start()`. Because of the `?.` optional chaining, this line silently becomes a no-op, and `saveCreds` is never actually registered — the bot will lose its session on restart because `creds.update` events are never persisted. Move the listener registration after `await bot.start()`, or have the `Bot` class handle creds saving internally.</violation>
</file>

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

Comment thread Example/bot-example.ts
})

// Guardar credenciales al conectarse/actualizarse
bot.socket?.ev.on('creds.update', saveCreds)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: The saveCreds listener is registered on bot.socket?.ev before bot.start() is called, but bot.socket is only assigned during start(). Because of the ?. optional chaining, this line silently becomes a no-op, and saveCreds is never actually registered — the bot will lose its session on restart because creds.update events are never persisted. Move the listener registration after await bot.start(), or have the Bot class handle creds saving internally.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Example/bot-example.ts, line 51:

<comment>The `saveCreds` listener is registered on `bot.socket?.ev` before `bot.start()` is called, but `bot.socket` is only assigned during `start()`. Because of the `?.` optional chaining, this line silently becomes a no-op, and `saveCreds` is never actually registered — the bot will lose its session on restart because `creds.update` events are never persisted. Move the listener registration after `await bot.start()`, or have the `Bot` class handle creds saving internally.</comment>

<file context>
@@ -0,0 +1,57 @@
+    })
+
+    // Guardar credenciales al conectarse/actualizarse
+    bot.socket?.ev.on('creds.update', saveCreds)
+
+    await bot.start()
</file context>

import ffmpegStatic from 'ffmpeg-static'

// node-webpmux usa commonjs en su mayoría
const webpmux = require('node-webpmux')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Loading the framework fails under this package's ESM runtime because require is undefined. Use an ESM import for node-webpmux (or createRequire) so importing Context can initialize.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/MediaManager.ts, line 9:

<comment>Loading the framework fails under this package's ESM runtime because `require` is undefined. Use an ESM import for `node-webpmux` (or `createRequire`) so importing `Context` can initialize.</comment>

<file context>
@@ -0,0 +1,109 @@
+import ffmpegStatic from 'ffmpeg-static'
+
+// node-webpmux usa commonjs en su mayoría
+const webpmux = require('node-webpmux')
+
+// Configurar la ruta de FFmpeg automáticamente
</file context>

Comment thread README.md
Comment on lines +50 to +51
bot.socket?.ev.on('creds.update', saveCreds)
await bot.start()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The creds.update listener in the example will never fire because bot.socket is undefined before bot.start() is called. The optional chaining ?. makes this a silent no-op — credentials are never persisted, which means the authentication state is lost on restart and the user will be forced to re-authenticate. Move bot.socket.ev.on('creds.update', saveCreds) to after await bot.start() to ensure the socket exists. Note that reconnection will create a new socket without the listener, so an internal hook in Bot.start() or a wrapper around the socket creation would be more robust.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 50:

<comment>The `creds.update` listener in the example will never fire because `bot.socket` is `undefined` before `bot.start()` is called. The optional chaining `?.` makes this a silent no-op — credentials are never persisted, which means the authentication state is lost on restart and the user will be forced to re-authenticate. Move `bot.socket.ev.on('creds.update', saveCreds)` to **after** `await bot.start()` to ensure the socket exists. Note that reconnection will create a new socket without the listener, so an internal hook in `Bot.start()` or a wrapper around the socket creation would be more robust.</comment>

<file context>
@@ -10,6 +10,51 @@
+        await ctx.replySticker(buffer, { packname: 'SuperBot', author: '@luis' })
+    })
+
+    bot.socket?.ev.on('creds.update', saveCreds)
+    await bot.start()
+}
</file context>
Suggested change
bot.socket?.ev.on('creds.update', saveCreds)
await bot.start()
await bot.start()
bot.socket?.ev.on('creds.update', saveCreds)


try {
await new Promise<void>((resolve, reject) => {
ffmpeg(tempInput)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The convertToVoiceNote method is missing critical audio encoding parameters that WhatsApp expects for voice notes. Without specifying mono channel (-ac 1), sample rate, bitrate, and -application voip, the output OGG Opus file may be rejected by WhatsApp or play incorrectly on the receiving end. Add these options to ensure the output is a standard mono Opus voice note at an appropriate bitrate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/MediaManager.ts, line 36:

<comment>The `convertToVoiceNote` method is missing critical audio encoding parameters that WhatsApp expects for voice notes. Without specifying mono channel (`-ac 1`), sample rate, bitrate, and `-application voip`, the output OGG Opus file may be rejected by WhatsApp or play incorrectly on the receiving end. Add these options to ensure the output is a standard mono Opus voice note at an appropriate bitrate.</comment>

<file context>
@@ -0,0 +1,109 @@
+
+		try {
+			await new Promise<void>((resolve, reject) => {
+				ffmpeg(tempInput)
+					.inputOptions(['-y'])
+					.outputOptions([
</file context>

Comment thread src/Framework/Context.ts
@@ -0,0 +1,67 @@
import type { WAMessage, AnyMessageContent, MiscMessageGenerationOptions } from '../Types'
import type { Bot } from './Bot'
import makeWASocket from '../Socket'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: makeWASocket is only used in a type context (typeof makeWASocket inside a ReturnType<>) — never called or used as a runtime value. With verbatimModuleSyntax, this must be import type to avoid emitting an unnecessary runtime import.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/Context.ts, line 3:

<comment>`makeWASocket` is only used in a type context (`typeof makeWASocket` inside a `ReturnType<>`) — never called or used as a runtime value. With `verbatimModuleSyntax`, this must be `import type` to avoid emitting an unnecessary runtime import.</comment>

<file context>
@@ -0,0 +1,67 @@
+import type { WAMessage, AnyMessageContent, MiscMessageGenerationOptions } from '../Types'
+import type { Bot } from './Bot'
+import makeWASocket from '../Socket'
+import { MediaManager, type StickerMetadata } from './MediaManager'
+
</file context>


public async getGhosts(groupJid: string, inactiveDays: number = 30): Promise<{ jid: string, isTotalGhost: boolean, lastActive?: number }[]> {
if (!this.bot.socket) {
throw new Error('Socket not connected')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Plain Error for socket-disconnected state — downstream code can't distinguish this from internal assertion failures.

AGENTS.md specifies that protocol/connection errors should use Boom with a statusCode so callers can branch on error.output.statusCode. Currently, getGhosts throws a bare Error, and any code that tries to retry on connection errors has no programmatic way to identify the failure type.

Recommendation: Throw new Boom('Socket not connected', { statusCode: 503 }) instead, consistent with the project convention.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/StatsManager.ts, line 79:

<comment>Plain `Error` for socket-disconnected state — downstream code can't distinguish this from internal assertion failures.

AGENTS.md specifies that protocol/connection errors should use `Boom` with a `statusCode` so callers can branch on `error.output.statusCode`. Currently, `getGhosts` throws a bare `Error`, and any code that tries to retry on connection errors has no programmatic way to identify the failure type.

**Recommendation:** Throw `new Boom('Socket not connected', { statusCode: 503 })` instead, consistent with the project convention.</comment>

<file context>
@@ -0,0 +1,108 @@
+
+	public async getGhosts(groupJid: string, inactiveDays: number = 30): Promise<{ jid: string, isTotalGhost: boolean, lastActive?: number }[]> {
+		if (!this.bot.socket) {
+			throw new Error('Socket not connected')
+		}
+
</file context>

Comment thread src/Framework/Bot.ts

public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) {
this.use(async (ctx, next) => {
if (ctx.text?.startsWith(cmd)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: startsWith(cmd) matches partial overlaps — e.g., registering !sticker would also fire for !stickerSpam or !stickerset. Require an exact match or a word boundary (whitespace) after the command prefix to avoid unintended handler invocations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/Bot.ts, line 65:

<comment>`startsWith(cmd)` matches partial overlaps — e.g., registering `!sticker` would also fire for `!stickerSpam` or `!stickerset`. Require an exact match or a word boundary (whitespace) after the command prefix to avoid unintended handler invocations.</comment>

<file context>
@@ -0,0 +1,177 @@
+
+	public command(cmd: string, handler: (ctx: Context) => Promise<void> | void) {
+		this.use(async (ctx, next) => {
+			if (ctx.text?.startsWith(cmd)) {
+				await handler(ctx)
+			}
</file context>

@@ -0,0 +1,109 @@
import * as fs from 'fs'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Synchronous filesystem operations (fs.writeFileSync, fs.readFileSync, fs.copyFileSync, fs.unlinkSync) block the event loop in these async methods. Under concurrent message load, this can stall WebSocket ping/pong and cause disconnects. Since the methods are already async, replace with fs.promises.* equivalents (writeFile, readFile, copyFile, unlink).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/MediaManager.ts, line 29:

<comment>Synchronous filesystem operations (`fs.writeFileSync`, `fs.readFileSync`, `fs.copyFileSync`, `fs.unlinkSync`) block the event loop in these `async` methods. Under concurrent message load, this can stall WebSocket ping/pong and cause disconnects. Since the methods are already `async`, replace with `fs.promises.*` equivalents (`writeFile`, `readFile`, `copyFile`, `unlink`).</comment>

<file context>
@@ -0,0 +1,109 @@
+		const tempOutput = this.getTempFile('webp')
+
+		if (Buffer.isBuffer(inputPathOrBuffer)) {
+			fs.writeFileSync(tempInput, inputPathOrBuffer)
+		} else {
+			fs.copyFileSync(inputPathOrBuffer, tempInput)
</file context>

Comment thread src/Framework/Context.ts
@@ -0,0 +1,67 @@
import type { WAMessage, AnyMessageContent, MiscMessageGenerationOptions } from '../Types'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new public Context helpers have no unit coverage for reply quoting, session delegation, reactions, or media conversion failures. Add colocated Context tests so this API contract is regression-tested.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/Context.ts, line 8:

<comment>The new public Context helpers have no unit coverage for reply quoting, session delegation, reactions, or media conversion failures. Add colocated Context tests so this API contract is regression-tested.</comment>

<file context>
@@ -0,0 +1,67 @@
+
+export type WASocket = ReturnType<typeof makeWASocket>
+
+export class Context {
+	public readonly message: WAMessage
+	public readonly bot: Bot
</file context>

}

public static async convertToSticker(inputPathOrBuffer: string | Buffer, metadata?: StickerMetadata): Promise<Buffer> {
const tempInput = this.getTempFile('in')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The temp file management pattern (write input → process → read output → cleanup) is duplicated identically across both conversion methods. Extracting it into a shared helper (e.g., private static async processWithFfmpeg(input, processor)) would reduce duplication and prevent cleanup bugs in future conversion methods.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Framework/MediaManager.ts, line 25:

<comment>The temp file management pattern (write input → process → read output → cleanup) is duplicated identically across both conversion methods. Extracting it into a shared helper (e.g., `private static async processWithFfmpeg(input, processor)`) would reduce duplication and prevent cleanup bugs in future conversion methods.</comment>

<file context>
@@ -0,0 +1,109 @@
+	}
+
+	public static async convertToSticker(inputPathOrBuffer: string | Buffer, metadata?: StickerMetadata): Promise<Buffer> {
+		const tempInput = this.getTempFile('in')
+		const tempOutput = this.getTempFile('webp')
+
</file context>

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for 14 days with no activity. Remove the stale label or comment or this will be closed in 14 days

@github-actions github-actions Bot added the Stale label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants