This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Canis is a multi-account WhatsApp chatbot built in TypeScript. It uses whatsapp-web.js (Puppeteer/Chrome automation) to connect to WhatsApp Web, dynamically loads commands, and integrates with MariaDB (via Prisma), Redis, multiple AI providers, and Sentry.
# Development (runs via ts-node, auto-restarts on exit)
npm run dev
# Build TypeScript to dist/ (also runs prisma generate)
npm run build
# Production (runs compiled JS, auto-restarts on exit)
npm run start
# PM2 production
npm run build && pm2 start
# Database migrations
npx prisma migrate dev
# Regenerate Prisma client (after schema changes)
npx prisma generate
# Test Sentry integration
npm run test:sentryThere are no unit tests. npm run build:clean rebuilds without incremental cache.
runner.ts → spawns index.ts as a child process and restarts it on exit → index.ts initializes PhishTank, cron jobs, memory monitor, all WhatsApp accounts, then loads commands.
src/components/client.ts maintains a Map<string, Client> of active WhatsApp clients. Each account has a clientId (string) and an isRoot flag. Accounts are persisted in the Account Prisma model and loaded on startup via getClientIds(). New accounts can be added at runtime via the connect command which calls addAccount().
Commands live in src/commands/ (and optionally src/commands/private/ for private commands not tracked in git). Each command file must export:
export const info = {
command: "name", // trigger keyword
description: "...",
usage: "name [args]",
example: "name foo",
role: "user", // "user" | "admin" | "super-admin"
cooldown: 5000, // ms
optOutAI?: boolean, // prevent AI from handling this trigger
dependencies?: [{ name: "pkg", version: "1.0.0" }], // auto-installed on load
};
export default async function (msg: Message): Promise<void> { ... }The loader (src/components/utils/cmd/loader.ts) scans both directories, dynamically require()s each file, and registers matching exports into the commands record. When AUTO_RELOAD=true, a file watcher hot-reloads commands on save.
message_create / message_edit events → src/components/events/message.ts:
- Filters (age, gif, status, broadcast, forwarded, bot senders)
- Checks block list and
pausedsetting (from Redis/DB viagetSetting) - Normalizes message body (NFKC, strips diacritics/zero-width chars)
- Strips command prefix (
COMMAND_PREFIX, default!) - Looks up command handler; if none found → runs quiz/riddle checks, InstantDownloader, auto-react, and AI on @mentions
- Rate limiting via
rateLimiter(Redis-backed) - Role check (
user/admin/super-admin) - Overrides
msg.reply()to apply bot font and log latency - Calls
handler.exec(msg)
| Path | Purpose |
|---|---|
src/components/client.ts |
WhatsApp client lifecycle, event wiring |
src/components/events/message.ts |
Core message dispatch and middleware |
src/components/utils/cmd/loader.ts |
Dynamic command loading |
src/components/services/ |
DB access layer (user, group, message, log, settings, account) |
src/components/utils/rateLimiter.ts |
Redis-backed per-user rate limiting |
src/components/utils/instantdl/ |
YouTube/Facebook instant download |
src/components/ai/ |
AI provider adapters (Groq, Gemini, OpenAI, OpenRouter, Ollama) |
src/components/phishtank.ts |
Phishing URL detection via PhishTank dataset |
src/components/redis.ts |
Shared Redis client |
src/components/prisma.ts |
Shared Prisma client |
src/cron.ts |
Cron job registry (add new jobs here) |
src/config.ts |
All env-var config with defaults |
src/generated/ |
Auto-generated Prisma client — do not edit |
Schema at prisma/schema.prisma. Models: User, Group, Message, Log, Account. Default provider is mysql (MariaDB). Change the provider in the schema if needed, then re-run prisma generate and prisma migrate dev.
Bot behaviour (e.g., paused, auto_react) is stored as key-value rows accessed via getSetting(key) / setSetting(key, value) in src/components/services/settings.ts, cached in Redis.
Copy .env.example to .env. Critical variables:
PUPPETEER_EXEC_PATH— path to Chrome/Chromium/Edge/Firefox/Brave binaryDATABASE_URL+PRISMA_MARIA_DB_*— MariaDB connectionREDIS_URL— Redis/Valkey connectionAI_PROVIDER— one ofopenrouter | groq | gemini | openai | ollama- Corresponding
*_API_KEYand*_MODELfor the chosen provider
- Create
src/jobs/yourjob.ts— exportinfo: CronJobInfoand a default async function. - Register it in
src/cron.tsby importing and adding to thejobsarray.
One unified agent — Mj — handles all AI interactions. The old personality commands (obi, naij, chad, sim) have been removed. mj and ai are both entry points to the same agent.
Three ways to trigger Mj in a chat:
- Direct command —
mj <query>orai <query> - Name mention — any message containing "mj" as a word (e.g. "hey mj what's up")
- @mention — @mention the bot in a group
Once activated, Mj continues replying to that user in that chat for 10 minutes of idle time — no need to re-trigger on every message. Session is tracked per-user-per-chat in Redis (agent:session:{chatId}:{lid}, 10 min TTL, refreshed on every reply).
src/components/ai/thread.ts stores conversation history per user per chat in Redis:
- Key:
agent:thread:{lid}:{chatId} - Value:
ThreadMessage[]— onlyuserandassistantturns - TTL:
AGENT_THREAD_TTL(default 3600 s); refreshed on every append - Oldest messages dropped when
AGENT_MAX_HISTORYis exceeded
src/components/ai/tools/ — each file exports a typed AgentTool definition. tools/index.ts is the registry.
| Tool | File | Purpose |
|---|---|---|
web_search |
tools/webSearch.ts |
DuckDuckGo search, returns top 5 results |
browse_page |
tools/browsePage.ts |
Headless Chrome page text extraction |
shell |
tools/shell.ts |
Shell execution (AGENT_SHELL_ENABLED=true, default on) |
send_file |
tools/sendFile.ts |
Send a local file to the WhatsApp chat |
run_command |
tools/botCommand.ts |
Execute a bot command by name |
list_commands |
tools/listCommands.ts |
Discover available bot commands |
get_user |
tools/userInfo.ts |
WhatsApp user info lookup |
get_group |
tools/groupInfo.ts |
WhatsApp group info lookup |
bot_stats |
tools/botStats.ts |
Memory / uptime / connected accounts |
run_command and send_file are intercepted in agentRunner.ts before tool execution — they set a result field in AgentResult and break the loop. personalityHandler.ts handles the result.
Agent creates files via shell (writes to /tmp/mj-workspace/), then calls send_file to deliver them to the user over WhatsApp. Multi-file projects get zipped first.
src/components/ai/agentRunner.ts runs the tool-calling loop for each provider:
- OpenAI, Groq, OpenRouter → shared
runOpenAILike()helper - Gemini →
runGemini()usingfunctionDeclarations/functionResponse - Ollama →
runOllama()
Loop cap: AGENT_MAX_TOOL_ITERATIONS (default 5).
- Create
src/components/ai/tools/myTool.ts— export anAgentToolconst and optionally arunMyTool()function. - In
tools/index.ts: import, add togetTools(), add acasetoexecuteTool().
AGENT_THREAD_TTL=3600
AGENT_MAX_HISTORY=20
AGENT_MAX_TOOL_ITERATIONS=5
AGENT_SHELL_ENABLED=true