Discord AI Companion

Nova is a friendly, slightly witty Discord companion that chats naturally in DMs or when mentioned in servers. It runs on Node.js, uses discord.js v14, and supports OpenRouter (recommended) or OpenAI backends for model access, plus lightweight local memory for persistent personality.

Features

  • Conversational replies in DMs automatically; replies in servers when mentioned or in a pinned channel.
  • Chat model (defaults to meta-llama/llama-3-8b-instruct when using OpenRouter) for dialogue and a low-cost embedding model (nvidia/llama-nemotron-embed-vl-1b-v2 by default). OpenAI keys/models may be used as a fallback.
  • Short-term, long-term, and summarized memory layers with cosine-similarity retrieval.
  • Automatic memory pruning, importance scoring, and transcript summarization when chats grow long.
  • Local SQLite memory file (no extra infrastructure) powered by sql.js, plus graceful retries for the model API (OpenRouter/OpenAI).
  • Optional "miss u" pings that DM your coder at random intervals (06h) when CODER_USER_ID is set.
  • Dynamic per-message prompt directives that tune Nova's tone (empathetic, hype, roleplay, etc.) before every OpenAI call.
  • Lightweight Google scraping for fresh answers without paid APIs (locally cached).
  • Guard rails that refuse "ignore previous instructions"-style jailbreak attempts plus a configurable search blacklist.
  • The same blacklist applies to everyday conversation—if a user message contains a banned term, Nova declines the topic outright.

Prerequisites

  • Node.js 18+ (tested up through Node 25)
  • Discord bot token with Message Content Intent enabled
  • OpenRouter or OpenAI API key

Setup

  1. Install dependencies:

    npm install
    

    Prerequisites

    • Node.js 18+ (tested up through Node 25)
    • Discord bot token with Message Content Intent enabled
    • OpenRouter or OpenAI API key

    Setup

    1. Install dependencies:
      npm install
      
    2. Copy the environment template:
      cp .env.example .env
      
    3. Fill .env with your secrets:
      • DISCORD_TOKEN: Discord bot token
      • USE_OPENROUTER: Set to true to route requests through OpenRouter (recommended).
      • OPENROUTER_API_KEY: OpenRouter API key (when USE_OPENROUTER=true).
      • OPENROUTER_MODEL: Optional chat model override for OpenRouter (default meta-llama/llama-3-8b-instruct).
      • OPENROUTER_EMBED_MODEL: Optional embed model override for OpenRouter (default nvidia/llama-nemotron-embed-vl-1b-v2).
      • OPENAI_API_KEY: Optional OpenAI key (used as fallback when USE_OPENROUTER is not true).
      • BOT_CHANNEL_ID: Optional guild channel ID where the bot can reply without mentions
      • CODER_USER_ID: Optional Discord user ID to receive surprise DMs every 68 hours (configurable)
      • ENABLE_WEB_SEARCH: Set to false to disable Google lookups (default true)
      • CONTINUATION_INTERVAL_MS: (optional) ms between proactive follow-ups (default 15000)
      • CONTINUATION_MAX_PROACTIVE: (optional) max number of proactive follow-ups (default 10)
      • CODER_PING_MIN_MS / CODER_PING_MAX_MS: (optional) override min/max coder ping window in ms (defaults 68 hours)

    Running

    • Development: npm run dev
    • Production: npm start

    Optional PM2 Setup

    npm install -g pm2
    pm2 start npm --name nova-bot -- run start
    pm2 save
    

    PM2 restarts the bot if it crashes and keeps logs (pm2 logs nova-bot).

    File Structure

    src/
      bot.js        # Discord client + routing logic
      config.js     # Environment and tuning knobs
      openai.js     # Chat + embedding helpers with retry logic
      memory.js     # Multi-layer memory engine
    .env.example
    README.md
    
    • Short-term (recency buffer): Last 10 conversation turns kept verbatim for style and continuity. Stored per user inside data/memory.sqlite.

    • Long-term (vector store): Every user message + bot reply pair becomes an embedding via text-embedding-3-small. Embeddings, raw text, timestamps, and heuristic importance scores live in the same SQLite file. Retrieval uses cosine similarity plus a small importance boost; top 5 results feed the prompt.

    • Summary layer: When the recency buffer grows past ~3000 characters, Nova asks OpenAI to condense the transcript to <120 words, keeps the summary, and trims the raw buffer down to the last few turns. This keeps token usage low while retaining story arcs.

    • Importance scoring: Messages mentioning intent words ("plan", "remember", etc.), showing length, or emotional weight receive higher scores. When the store exceeds its cap, the lowest-importance/oldest memories are pruned. You can also call pruneLowImportanceMemories() manually if needed.

    • Embedding math: text-embedding-3-small returns 1,536 floating-point numbers for each text chunk. That giant array is a vector map of the messages meaning; similar moments land near each other in 1,536-dimensional space.

    • What gets embedded: After every user→bot turn, recordInteraction() (see src/memory.js) bundles the pair, scores its importance, asks OpenAI for an embedding, and stores { content, embedding, importance, timestamp } inside the SQLite tables.

    • Why so many numbers: Cosine similarity needs raw vectors to compare new thoughts to past ones. When a fresh message arrives, retrieveRelevantMemories() embeds it too, calculates cosine similarity against every stored vector, adds a small importance boost, and returns the top five memories to inject into the system prompt.

    • Self-cleaning: If the DB grows past the configured limits, low-importance items are trimmed, summaries compress the short-term transcript, and you can delete data/memory.sqlite to reset everything cleanly.

    Migrating legacy memory.json

    • Keep your original data/memory.json in place and delete/rename data/memory.sqlite before launching the bot.
    • On the next start, the new SQL engine auto-imports every user record from the JSON file, logs a migration message, and writes the populated .sqlite file.
    • After confirming the data landed, archive or remove the JSON backup if you no longer need it.

    Conversation Flow

    1. Incoming message triggers only if it is a DM, mentions the bot, or appears in the configured channel.
    2. The user turn is appended to short-term memory immediately.
    3. The memory engine retrieves relevant long-term memories and summary text.
    4. A compact system prompt injects personality, summary, and relevant memories before passing short-term history to the model API (OpenRouter/OpenAI).
    5. The reply is sent back to Discord. If Nova wants to send a burst of thoughts, she emits the <SPLIT> token and the runtime fans it out into multiple sequential Discord messages.
    6. Long chats automatically summarize; low-value memories eventually get pruned.

    Nova may also enter a proactive continuation mode after replying: if you stay quiet, she can send short, context-aware follow-ups at the configured interval until you stop her with a short phrase like "gotta go" or after the configured maximum number of follow-ups.

    Dynamic Prompting

    • Each turn, Nova inspects the fresh user message (tone, instructions, roleplay cues, explicit “split this” requests) plus the last few utterances.
    • A helper (composeDynamicPrompt in src/bot.js) emits short directives like “User mood: fragile, be gentle” or “They asked for roleplay—stay in character.”
    • These directives slot into the system prompt ahead of memories, so OpenAI gets real-time guidance tailored to the latest vibe without losing the core persona.
    • src/search.js grabs the standard Google results page with a real browser user-agent, extracts the top titles/links/snippets, and caches them for 10 minutes to stay polite.
    • bot.js detects when a question sounds “live” (mentions today/news/google/etc.) and injects the formatted snippets into the prompt as "Live intel". No paid APIs involved—its just outbound HTTPS from your machine.
    • Toggle this via ENABLE_WEB_SEARCH=false if you dont want Nova to look things up.
    • Edit data/filter.txt to maintain a newline-delimited list of banned keywords/phrases; matching queries are blocked before hitting Google and Nova refuses to discuss them in normal chat.
    • Every entry in data/search.log records which transport (direct or cache) served the lookup so you can audit traffic paths quickly.

    Proactive Pings

    • When CODER_USER_ID is provided, Nova spins up a timer on startup that waits a random duration between the configured min/max interval before DMing that user (defaults to 68 hours). Override the window with CODER_PING_MIN_MS and CODER_PING_MAX_MS in milliseconds.

    • Each ping goes through the configured model API (OpenRouter/OpenAI) with the prompt "you havent messaged your coder in a while, and you wanna chat with him!" so responses stay playful and unscripted.

    • The ping gets typed out (sendTyping) for realism and is stored back into the memory layers so the next incoming reply has context.

    • The bot retries OpenAI requests up to 3 times with incremental backoff when rate limited.

    • data/memory.sqlite is ignored by git but will grow with usage; back it up if you want persistent personality (and keep data/memory.json around only if you need legacy migrations).

    • To reset persona, delete data/memory.sqlite while the bot is offline.

    Happy chatting!

Description
Open-source AI Discord chatbot built with modular architecture, memory handling etc.
Readme MIT 163 KiB
Languages
JavaScript 70.4%
HTML 29.6%