HIROBOT
Lightweight Whatsapp Bot with Voip Call, Website, and AI Agent. — Built with Baileys and NodeJS 24.
Note
Hirobot is A Lightweight WhatsApp bot that integrates an AI agent, VoIP calling capabilities, and a dedicated web portal for users. Built with Baileys and NodeJS v24+.
Features:
- AI Agent Using Gemini.
- 1:1 Voice & Video Call.
- Multi Sessions.
- Database Node:Sqlite / Mongodb.
- Support AI Rich and Button Message.
- Cloudflared Tunnel Website.
- Minimal Depedencies.
|
About AI Agent
──────────────
How AutoHeal Works?
flowchart LR A@{ shape: odd, label: "User Command" } --> Error Error process@==> C@{ shape: diamond, label: "Gemini Server Gemma-4-31b-it"} C --> D@{ shape: circle, label: "⏳" } D ==> E[✅ Write and save] D ==> F[❌ Stop autoheal] E --> G[Done] F --> H[Note it as failure] process@{ animate: true } style Error stroke:#f00
| Gemini 3.1 Lite-Flash | Daily Conversation |
| Gemini 3.1 Lite | Daily conversation but more complex |
| Gemma-4-31b-it | AutoHeal system and coding |
| Gemma-4-26b-a4b-it | AutoHeal system and coding |
How to add a new tool
All MCP Helper
| Category | Function | Description |
|---|---|---|
| Session & Chat History | getSession(jid) |
get chat history array for a chat |
resetSession(jid) |
clear chat history for a chat | |
getPinnedNotesReadOnly(jid) |
get notes pinned to a chat | |
| Talking to the AI / Agent Loop | runAgent(conn, m, text, opts) |
run a full AI turn, get a reply |
runAgentConfirmed(conn, m, opts) |
resume an agent turn awaiting confirmation | |
callTool(name, args) |
call another registered tool by name | |
listTools() / countTools() |
list / count registered tools | |
| Identity & Permissions | getUserIdentity(jid, db, conn) |
get sender's name/number/owner/timezone |
checkGroupAdminOrOwner(groupJid) |
check if sender is group admin/owner | |
readGroupSettings(groupJid) |
read group settings from brain storage | |
readOwnerList() |
list registered bot owners | |
| Persistent Storage ("brain") | loadBrain() / saveBrain(brain) |
read/write ai-brain.json |
ensureBrainGroupSlot(brain, jid) |
ensure a group slot exists in brain | |
| Web & Media | searchWebGrounded(query) |
grounded web search |
captureWebsiteScreenshot(url) |
screenshot a webpage | |
fetchWebsiteHtmlFallback(url) |
fetch raw HTML of a page | |
peekFetchBuffer(url, headers) |
peek a file buffer from a URL | |
peekfetchVideoBuffer(url, maxBytes, headers) |
peek a video buffer from a URL | |
detectPlatform(url) |
detect platform (YouTube/TikTok/etc) | |
peekAnalyzeWithVision(mediaItems, platform, url, context) |
analyze media with vision model | |
buildMediaPart(m) |
extract image/video/audio from a message | |
fetchSocialMulti(url) |
download helper for social media | |
downloadUserImageAsUrl(m) |
upload user's image, get back a URL | |
| File & Data Tools | readFileToolCore(file_path, offset) |
core logic behind "read file" tool |
buildSimpleDiff(oldStr, newStr) |
build a text diff between two strings | |
parseDbKeyPath(key_path) |
parse a dotted key path for db access | |
| Plugin Execution (Advanced/Internal) | resolvePlugin(command) |
find which plugin matches a command |
resolveCustomPrefixPlugin(rawInput) |
same, for custom-prefix commands | |
execPluginCommand(command, argsStr, opts) |
run an existing bot plugin/command | |
execEval(code, opts) |
evaluate raw JS code (owner-only, dangerous) | |
classifyPluginRisk(name, plugin) |
classify a plugin's risk level | |
accessLabel(level) / riskBadge(level) |
risk-level label/badge helpers | |
pluginRequirements(plugin) |
get a plugin's access requirements | |
getDangerousDocReason(m) |
check if a message/doc looks risky | |
| Error Handling & Internals | handleError(conn, m, err, pluginName) |
central error handler/reporter |
isTransientApiError(err) |
check if an API error is transient | |
getApiKeys() / getNextKey() / rotateKey() / resetRateLimit(jid) |
API key pool management | |
normalizeApiKeys(input) |
format/clean a raw API key list | |
getPersonality() |
get bot's configured personality/system prompt | |
MODELS |
map of available AI models | |
setCurrentContext(...) |
internal turn/state management (used by mcp.js itself) | |
hasPending() / confirmPending() / cancelPending() |
internal turn/state management (used by mcp.js itself) |
/*
ctx() -> Returns the current chat state (always fresh, backed by an
internal module-level object in mcp.js). Common fields:
- currentJid : the id of the chat/user sending the message
- conn : the active WhatsApp connection (for manual sendMessage)
- isOwner : true if the sender is the bot owner
- isROwner : true if the sender is a "real" owner (not fromMe)
- timezone : sender's configured timezone, e.g. "Asia/Jakarta"
Tools import helpers straight from '../mcp.js'. There's no circular-import
issue: mcp.js never statically imports files in ./tools -- it loads them
with a dynamic import() at runtime (see loadToolsDir), so importing mcp.js
from a tool file at the top level is completely safe.
*/
import { ctx, searchWebGrounded } from '../mcp.js'
export default [
{
name: 'check_weather',
description: 'Check the weather for a specific city. Use it when a user asks for the weather, e.g., "What\'s the weather like in Jakarta?"',
parameters: {
city: { type: 'string', description: 'City name, e.g. "Jakarta"', required: true }
},
execute: async ({ city }) => {
const { currentJid } = ctx()
if (!currentJid) return 'Chat context not available'
// const result = await searchWebGrounded(`current weather in ${city}`) // only if you need a helper from mcp.js
return `Weather in ${city}: sunny, 30°C`
}
}
]
About Website
──────────────
.env Settings for Website
| SCENARIO 1 No public address, using own domain |
Note Your panel does not give a real public address/IP (e.g. Pterodactyl, where the address is just name:port, internal-only). The bot still runs cloudflared (binary auto-downloaded, no manual install) to open a quick tunnel — your custom domain sits in front of it via a Cloudflare Worker + KV Namespace, instead of a regular DNS record. |
|
CF_KV_TOKEN |
Cloudflare API Token with Account > Workers KV Storage > Edit permission, from dash.cloudflare.com/profile/api-tokens | |
CF_ACCOUNT_ID |
Found on the Workers & Pages overview page | |
CF_KV_NAMESPACE_ID |
Found on your KV Namespace's page | |
CF_HOSTNAME |
Required for the domain that becomes your bot's main address, e.g. bot.yourdomain.com |
|
| SCENARIO 2 DNS-pointable server, using own domain |
Note Your panel/server can be pointed to via a DNS record (a VPS with a fixed IP, or a panel supporting Cloudflare Named Tunnel / Zero Trust). Requires card verification on Cloudflare Zero Trust (free forever, no charge unless you exceed free-tier limits). |
|
CF_TOKEN |
Named Tunnel token from Cloudflare Zero Trust | |
CF_HOSTNAME |
Required for the domain that becomes your bot's main address, e.g. bot.yourdomain.com |
|
| SCENARIO 3 No own domain, free provider |
Note HOSTNAME_PUBLIC — you don't own a custom domain, but have a free public address from another provider (e.g. my.zone.id, is.dev) already pointed (A/CNAME) at your panel's real public address. Fill in only this one, leave everything above empty — the bot will not run cloudflared/any tunnel at all.
|
|
| SCENARIO 4 Default, nothing configured |
Note Nothing above is filled in — the bot automatically uses a free |
|
Message types
──────────────
📖 Basic
conn.reply(m.chat, 'Hello world!', m)
/** @Media
URL — 'https://example.com/audio.mp3'
Local — '/path/to/video.mp4'
@Options
send as document — { document:true }
send as voicenote — { ptt: true }
**/
conn.sendFile(m.chat, media, "file.png", "hello world!", m, { options })
conn.sendContact(m.chat, [
['6281234567890', 'HirooSy'],
['6289876543210', 'Hiro']
], m)
conn.react(m.chat, '👍', m.key)
📍 Location
conn.sendLocation(m.chat, 'https://example.com/thumb.jpg','Title','Address',m)
🧾 Product
/**@Media
String: "https://example.com/img.png" or "./image.png"
Array: [ "https://example.com/", "./img.png" ]
**/
conn.sendProduct(m.chat, media, 'Title', 'Description, m, {
businessOwnerJid: "[email protected]",
currencyCode: 'USD',
priceAmount1000: 20,
retailerId: 'Code Promo',
})
🖼️ Url Preview
conn.sendUrlPreview(
m.chat,
'https://example.com/thumb.jpg',
'https://example.com Hello World!',
'Url Preview Title',
'Url Description',
'IMAGE', // true for highQuality, or ['IMAGE', true]
m
)
🛒 Carousel
conn.sendButton(m.chat, {
text: 'Interactive with Carousel!',
footer: 'HirooSy',
cards: [
{
image: { url: './path/to/image.jpg' },
caption: 'Image 1',
footer: 'Image 1',
nativeFlow: [{ text: 'Source', url: 'https://example.com', useWebview: true }]
},
{
image: { url: 'https://example.com/image.png' },
caption: 'Image 2',
footer: 'Image 2',
ltoText: 'New Coupon!',
ltoCode: 'HiroBot',
ltoUrl: 'https://example.com',
nativeFlow: [{ text: 'Source', url: 'https://example.com' }]
}
]
}, m)
🔖 NativeFlow Button
conn.sendButton(m.chat, {
image: { url: './path/to/image.jpg' },
caption: 'Interactive!',
footer: 'My Bot',
optionText: 'Select Options',
optionTitle: 'Select Options',
ltoText: 'HirooSy',
ltoCode: 'Hiro bot',
ltoUrl: 'https://example.com',
nativeFlow: [
{ text: '👋🏻 Greeting', id: '#Greeting' },
{ text: '📞 Call', call: '628123456789' },
{ text: '📋 Copy', copy: 'Hiro bot' },
{ text: '🌐 Source', url: 'https://example.com', useWebview: true },
{
text: '📋 Select',
sections: [
{ title: '✨ Section 1', rows: [{ header: '', title: '🏷️ Coupon', description: '', id: '#CouponCode' }] },
{ title: '✨ Section 2', highlight_label: '🔥 Popular', rows: [{ header: '', title: '💭 Secret Ingredient', description: '', id: '#SecretIngredient' }] }
],
}
]
}, m)
🗓️ AI Rich
await conn.aiRich()
.setTitle('Ai Rich Message')
.addText('[HyperLink](https://example.com)\nCitation [](https://example.com)'\n[x^2+y^2=r^2|100|100](https://example.com/latex.png))
.addImage('https://example.com/image.png')
.addCode('javascript', `console.log('Hello World')`)
.addHtml(["<html>Hello world</html>", "Tab 1"], ["<html>Hi twin</html>", "Tab 2"]),
.addTable([
['Name', 'HirooSy'],
['Bio', 'Im developer'],
['Age', '67']
])
.addSource([['https://example.com/favicon.ico', 'https://example.com', 'Source']])
.addTip('Tip Text')
.addSuggest(['Continue', 'Cancel'])
.send(m.chat, { quoted: m })
// animated progress
await conn.aiRich()
.addProcess("Loading...")
.send(m.chat)
📦 Sticker
/** @Media
URL — 'https://example.com/image.png'
Local — '/path/to/image.png'
**/
// Sticker
conn.sendSticker(m.chat, media, { packname: "Hiro", author: "Bot" }, m)
// StickerPack
conn.sendStickerPack(m.chat, {
cover: { url: media },
stickers: [
{ data: { url: media } },
{ data: { url: media } },
],
name: 'My Sticker Pack',
publisher: 'Publisher stickerpack',
description: 'Description pack'
})
📞 Call
/** @Media
URL — 'https://example.com/audio.mp3'
Local — '/path/to/video.mp4'
**/
// Audio
const call = await conn.call('628123456789', media)
// Video
const call = await conn.call('628123456789', media, {
videoSource: media
})
// Silent
const call = await conn.call('628123456789', 'silence')
// Audio as video call
const call = await conn.call('628123456789', Audio, {
isVideo: true
})
