Skip to content

HIROBOT

by darkshinra154-lgtm Node.js Other

Lightweight Whatsapp Bot with Voip Call, Website, and AI Agent. — Built with Baileys and NodeJS 24.

README.md

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.

Env
         ---Project Structure---
HIROBOT
├── 📁lib
│   ├── 📁package
│   │   ├── 📁ai            
│   │   ├── 📁voip           # Call
│   │   └── 📁website
│   │       ├── 📁views      # HTML folder
│   │       └── 📄server.js
│   ├── 📁scrapers
│   ├── 📁utils
│   ├── 📄config.js        # bot's preference
│   ├── 📄main.js
│   └── 📄start.js
├── 📁data
│   ├── 📁sessions
│   ├── 📁tunnel
│   └── 📁tmp
├── 📁plugins
├── 📄.env                 # your tokens
├── 📄CHANGELOG.md
├── 📄LICENSE
├── 📄package.json
└── 📄README.md

About AI Agent

──────────────

How AutoHeal Works?

MERMAID
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)
JavaScript
/*
  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 trycloudflare.com URL that changes every time it restarts. No setup required.

Message types

──────────────

📖 Basic
JavaScript
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
JavaScript
conn.sendLocation(m.chat, 'https://example.com/thumb.jpg','Title','Address',m)

🧾 Product
JavaScript
/**@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
JavaScript
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
JavaScript
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
JavaScript
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
JavaScript
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
JavaScript
/** @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
JavaScript
/** @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
})

Install and Run
REQUIREMENT Server 100% CPU Limit, 700MB RAM, 1GB Storage
-
NodeJS 24 or higher
pkg install nodejs
Python Python 3.10+
pkg install python
FFMPEG latest
pkg install ffmpeg
Bash
$ git clone https://github.com/HirooSy/HIROBOT.git
$ cd HIROBOT
$ mv .env.example .env
$ nano .env
$ node .