Skip to content
NaichuanZhangPublic

About

A self-evolving thin OpenClaw-like implementation using Claude Agent SDK (claude-code sdk) controlled interfaced via discord, you can develop / add features on the fly via chat!

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Discordclaw

Discordclaw

A stripped-down Discord agent powered by Claude. Simplified fork of openclaw — keeps only Discord, replaces multi-provider AI with Anthropic SDK, adds a web dashboard.

Features

  • 💬 Conversational AI — @mention in channels or DM directly. Full conversation history per session.
  • 🧵 Thread-First Replies — In guild channels, every response goes into its own thread for clean session isolation. Monitored channels auto-respond without @mention.
  • 🎤 Voice Message Support — Send voice DMs and the bot transcribes them automatically, fully offline via whisper.cpp (falls back to NeMo Parakeet, then the OpenAI Whisper API if configured).
  • 🎙️ Voice Assistant — Join voice channels with /join. Listens via Silero VAD, transcribes locally with whisper.cpp, thinks with Claude, speaks back with ElevenLabs TTS. Supports interruptions, streaming TTS pipelining, and auto-disconnect. Auto-join mode tracks a configured user.
  • 🚴 Voice Coach — AI cycling coach that lives in a dedicated voice channel. Auto-joins when a tracked rider connects, monitors simulated cycling telemetry (power, heart rate, cadence), listens to rider speech via VAD+STT, and delivers real-time coaching via ElevenLabs TTS — all in the persona of a team sport director on race radio. Requires ELEVENLABS_API_KEY.
  • 📎 Artifacts — Persistent file storage for session inputs and outputs. Files uploaded to or generated by the agent are tracked with metadata, stored on disk, and browsable via the dashboard Artifacts page. Supports input/output tagging, Discord URL linking, and per-session browsing.
  • 🧠 Persistent Memory — Remembers things across conversations. Markdown files indexed with FTS5 full-text search. Optional mem9 cloud memory for semantic vector search.
  • 📜 Conversation History — Messages are archived across sessions. Query past conversations with get_conversation_history and get_conversation_stats tools.
  • 🎭 Customizable Personality — Edit SOUL.md to change how the bot behaves. Hot-reloads on save.
  • 🔧 Tool Use — Runs shell commands, reads/writes files, sends messages across channels, reacts to messages, attaches files, creates threads. Real-time tool call progress shown in Discord during agentic loops.
  • ↩️ Mid-turn Steering — A message sent while the bot is still working is injected into the running turn instead of queueing behind it; the bot reacts ↩️ and says which tool it interrupted. /interrupt cuts the turn but keeps the session, /stop kills it.
  • 📦 Skills — Drop a SKILL.md folder into data/skills/ and the bot learns new capabilities instantly. Install from GitHub or upload directly.
  • ⏰ Scheduled Tasks — Cron jobs that run agent turns on a schedule, delivering results in daily threads. Hot-reloads jobs.json without restart.
  • 🧬 Self-Evolution — The bot can modify its own source code via GitHub PRs. You approve the build plan up front; once CI passes, the PR is merged and deployed automatically (no diff review step). Optional Daytona sandbox CI for isolated validation. Deployment notifications posted automatically.
  • 🔍 Autonomous Reflection — Collects signals (errors, tool failures, duplicate loops) and structured logs, then periodically analyzes them to suggest improvements.
  • 📊 Web Dashboard — React SPA for managing sessions, channels, soul, memory, cron, skills, artifacts, evolution history, and logs.
  • 📝 Structured Logging — Three SQLite-backed log streams (application, error, tool calls) with dashboard viewer and automatic pruning.
  • 🔍 Web Search — Install the SearXNG skill for web, news, and package repository search.

Demo User Flow

You: Hey @Discordclaw, what did we talk about yesterday?
Bot: [searches memory] We discussed setting up the cron job
     for daily standups. Want me to finish that?

You: Yeah, set it up for 9am every weekday in #general
Bot: [creates cron job] Done! I'll post a standup prompt
     to #general at 9am Mon-Fri. ✅

You: 🎤 [sends voice message]
Bot: [transcribes audio] I heard you say "Can you search
     for the latest Node.js release?" Let me check...
     Node.js v22.5.0 was released on April 1, 2026.

You: Can you search the web for the latest Node.js release?
Bot: [reads searxng-search skill, runs search] Node.js v22.5.0
     was released on April 1, 2026 with...

You: I want you to add a /ping command that shows latency
Bot: Build plan:
     1. src/bot/commands.ts — register /ping, reply with gateway latency
     2. README.md — document the command
     Risk: needs a restart to re-register slash commands. OK to start?

You: yes
Bot: [evolve_start(plan, plan_approved) → evolve_write → evolve_propose]
     PR #8 validated, merged, deploying now... ✅
     /ping command is now live!

Slash Commands

Command Description
/ping Show bot health status, latency, and uptime
/help Show all commands and capabilities
/config Toggle bot on/off per channel, set custom instructions
/clear Stop this channel's session and drop its context (channel + the threads under it)
/soul View the bot's personality
/model Show the active model, or switch to another one from the proxy's list (persists across restarts)
/skills List, install (from GitHub or file upload), or remove skills
/cron View, add, enable/disable, force-run, set a per-job model, or show history of cron jobs
/caveman Toggle terse caveman-speak mode (lite/full/ultra/off) for the current channel
/restart Restart the bot process
/stop Stop every running session (kills the SDK child processes)
/interrupt Interrupt the current turn in this channel/thread, keeping the session and its context alive
/join Join your voice channel as a voice assistant
/leave Leave the voice channel

Getting Started

Each instance of Discordclaw is its own bot with its own personality, memory, and evolution history. To run your own, you'll fork the repo and deploy from your fork. This way the self-evolution feature creates PRs against your repo, not the upstream one.

Prerequisites

  • Node.js v20+
  • Git and GitHub CLI (gh) — required for the self-evolution engine
  • A Discord bot token (setup guide below)
  • An Anthropic API key (or a proxy endpoint)
  • (Optional) An OpenAI API key for voice message transcription
  • (Optional) An ElevenLabs API key for voice assistant and voice coach TTS
  • (Optional) An ElevenLabs API key for voice coach TTS
  • (Optional) A Daytona API key for sandbox CI during evolution validation

1. Fork & Clone

  1. Click Fork on the repo to create your own copy
  2. Clone your fork:
git clone https://github.com/YOUR_USERNAME/discord-claw.git
cd discord-claw

Why fork? The evolution engine pushes branches and creates PRs via gh. If you clone without forking, PRs would target the original repo. Your fork gives you full control — the bot evolves your codebase.

2. Create a Discord Bot

  1. Go to https://discord.com/developers/applications
  2. Create a new application → Bot tab → copy token
  3. Enable Message Content Intent and Server Members Intent
  4. OAuth2 > URL Generator → scopes: bot, applications.commands
  5. Permissions: Send Messages, Read Message History, Add Reactions, Attach Files, Use Slash Commands, Create Public Threads
  6. Invite bot to your server with the generated URL

3. Configure Environment

cp .env.example .env

Edit .env with your tokens:

DISCORD_BOT_TOKEN=your_discord_bot_token
ANTHROPIC_API_KEY=your_anthropic_api_key

# Optional
OPENAI_API_KEY=your_openai_key          # Voice message transcription
ELEVENLABS_API_KEY=your_elevenlabs_key  # Voice assistant + coach TTS
ELEVENLABS_API_KEY=your_elevenlabs_key  # Voice coach TTS
ELEVENLABS_VOICE_ID=your_voice_id       # Voice coach voice
GATEWAY_PORT=3000                        # Dashboard port
GATEWAY_TOKEN=your_secret_token          # Dashboard auth token
ANTHROPIC_MODEL=bedrock-claude-opus-5-1m # Fallback model (/model overrides it)
DAYTONA_API_KEY=your_daytona_key        # Sandbox CI for evolution validation

4. Install & Run

npm install
npm run build:ui    # Build the dashboard
npm run dev         # Start in development mode

The bot responds to @mentions in guild channels and all DMs. Dashboard at http://localhost:3000.

5. Production Deployment

For production, use the startup script which handles auto-pull, migrations, health checks, and rollback:

./start.sh

Or use the watchdog daemon for crash recovery and auto-restart:

npm run daemon

You can set up a systemd service, Docker container, or any process manager to keep it running. Point it at start.sh or the daemon as the entry point.

Tip: Set DISCORD_WEBHOOK_URL in .env to receive deploy/rollback notifications in a Discord channel.

6. Authenticate GitHub CLI (for Self-Evolution)

The evolution engine uses gh to create and merge PRs. Make sure it's authenticated:

gh auth login

Without this, the bot can still function normally — it just won't be able to create PRs to modify its own code.

7. Make It Yours

  • Personality — Edit data/SOUL.md to define how your bot talks and behaves. Hot-reloads on save.
  • Skills — Drop skill folders into data/skills/ or install from GitHub via the dashboard or /skills add-github.
  • Memory — The bot builds memory over time. You can also seed data/MEMORY.md with initial context.

Staying Up to Date

To pull improvements from the upstream repo into your fork:

git remote add upstream https://github.com/NaichuanZhang/discord-claw.git
git fetch upstream
git merge upstream/main

Environment Variables

Variable Required Description
DISCORD_BOT_TOKEN Yes Discord bot token
ANTHROPIC_API_KEY Yes* Anthropic API key
ANTHROPIC_BASE_URL No Proxy URL (overrides default API endpoint)
ANTHROPIC_AUTH_TOKEN No Auth token for proxy (used instead of API key)
ANTHROPIC_MODEL No Fallback model name (default: bedrock-claude-opus-5-1m). A /model selection is stored in the DB and takes precedence.
OPENAI_API_KEY No OpenAI API key for voice message transcription (Whisper)
ELEVENLABS_API_KEY No ElevenLabs API key for voice coach TTS
ELEVENLABS_VOICE_ID No ElevenLabs voice ID for voice coach
COACH_MODEL No LLM model for voice coach brain (default: bedrock-claude-sonnet-4-1m)
GATEWAY_PORT No Dashboard port (default: 3000)
GATEWAY_TOKEN No Auth token for dashboard API access
GATEWAY_PUBLIC_URL No Public URL for the gateway (used for artifact download links)
SESSION_TTL_HOURS No Session expiry (default: 24)
DISCORD_WEBHOOK_URL No Webhook for start.sh notifications (deploy, rollback alerts)
DAYTONA_API_KEY No Daytona API key for sandbox CI during evolution validation (falls back to local if not set)
DAYTONA_API_URL No Daytona API endpoint (default: https://app.daytona.io/api)
REFLECTION_CHANNEL_ID No Discord channel for reflection daemon proposals
REFLECTION_INTERVAL_HOURS No How often the reflection daemon runs (default: 6)
REFLECTION_LOOKBACK_HOURS No Signal lookback window (default: 24)
REFLECTION_MIN_SIGNALS No Minimum signals before reflection triggers (default: 3)
REFLECTION_MODEL No Claude model for reflection analysis (default: same as ANTHROPIC_MODEL)
VOICE_MODEL No Claude model for voice responses (default: claude-sonnet-4-20250514)
VOICE_SILENCE_MS No Silence duration to end utterance (default: 800)
VOICE_MIN_UTTERANCE_MS No Minimum utterance length, skip noise (default: 500)
VOICE_MAX_TOKENS No Max tokens for voice responses (default: 512)
VOICE_DEBUG No Voice debug logging (default: on, set 0 to disable)
VOICE_TTS_STREAM No Streaming TTS for lower TTFB (default: on, set 0 to disable)
VOICE_TOOLS_MODE No Voice agent tools: full (all tools except evolution) or minimal (memory + conversation history only). Default: full
VOICE_TTS_VOICE_ID No ElevenLabs voice for the assistant. Falls back to ELEVENLABS_VOICE_ID, then premade "Sarah"
VOICE_STT_REMOTE_FALLBACK No 1 = fall back to ElevenLabs Scribe when local whisper.cpp is unavailable (default off)
SDK_IDLE_MS No Session idle timeout before teardown (default: 1800000 = 30 min)
SDK_TURN_TIMEOUT_MS No Interrupt a turn that runs longer than this (default: 900000 = 15 min)
SDK_TURN_MAX_COST_USD No Soft cost cap per turn — over it, the usage footer says so. 0 (default) disables the warning
SDK_ATTACHMENT_MAX_BYTES No Per-file cap on attachments downloaded into the session inbox (default: 26214400 = 25 MB)
SDK_INBOX_MAX_AGE_MS No How long downloaded attachments are kept before pruning (default: 86400000 = 24 h)
SDK_ANTHROPIC_API_KEY No Opt into API-key auth for sessions (overrides inherited model auth)
SDK_ANTHROPIC_MODEL No Override the model used by sessions
SDK_ANTHROPIC_BASE_URL No Alternate Anthropic-compatible endpoint for sessions (e.g. a local gateway)

The SDK_* names above are also read under their pre-rename PILOT_* spellings, so an existing .env keeps working.

*Either ANTHROPIC_API_KEY or ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN required.

Key Systems

Soul — Bot personality defined in data/SOUL.md. Hot-reloads on file change. Editable via dashboard.

Memory — Hybrid search system. Local markdown files in data/ indexed with SQLite FTS5 (BM25-ranked), plus optional mem9 cloud memory for semantic vector search. Both sources are queried in parallel on every memory_search call. Queries are sanitized for FTS5 compatibility (special characters like hyphens and colons are handled automatically).

Sessions — Per-thread/DM/channel conversation tracking. The context the model reads lives inside its own SDK child process; our messages rows are the durable transcript, archived so get_conversation_history and get_conversation_stats can query across sessions. Ordering is handled by the session itself rather than a lock: a message arriving mid-turn is injected into the running turn (see Agent Runtime), so nothing has to wait for a mutex.

Cron — Scheduled tasks with three schedule types: one-shot (at), interval (every), cron expression (cron). agentTurn jobs run the agent and deliver results inside threads (the agent handles all delivery via tools — no duplicate top-level messages). Each one gets a fresh session in a thread of its own, so a scheduled run starts with clean context and a per-job /cron set-model override actually applies. systemEvent jobs deliver results directly to the configured channel. Auto-disables after 3 consecutive failures. Hot-reloads jobs.json on each tick cycle (up to every 60s) so externally-added jobs are picked up without a restart.

Skills — Modular capabilities defined as SKILL.md files with YAML frontmatter. Install from GitHub URL or upload directly. Uses SDK progressive loading pattern: only skill metadata (name, description, path) is injected into the system prompt; the agent reads full skill content on demand via read_skill tool. Skills can include companion files (scripts, references). Manageable via dashboard and /skills command.

Dashboard — Single-page React app at http://localhost:3000. Pages: Status, Sessions, Channels, Config, Cron, Skills, Artifacts, Evolution, Logs. Real-time message streaming via WebSocket.

Agent Runtime — Every message and every scheduled agentTurn is handled by a Claude Agent SDK session (src/sdk/) — one child process per Discord thread, DM or channel, keyed by target id. The tool loop, context compaction and duplicate-call handling are the SDK's; what this repo owns is the Discord side of it. See Agent Sessions below.

Thread-First Replies — In guild text channels, every bot response creates a thread on the user's message. Bot-created threads don't require @mentions for follow-up. Monitored channels auto-respond to all messages without @mention. DMs bypass threading entirely.

File Attachments — The agent can send files (PDFs, images, HTML, etc.) to Discord channels via the send_file tool. Files up to 25 MB are supported (Discord bot default tier).

Image Support — When the agent's response contains markdown images (![alt](url)), they are automatically extracted and rendered as Discord embeds (for web URLs) or file attachments (for local files). Image markdown is stripped from the text to avoid showing raw URLs.

Voice Messages — Discord voice DMs and audio attachments are automatically detected and transcribed. Backends are tried in order, so audio only leaves the machine as a last resort:

  1. whisper.cpp (default) — fully local, a native binary plus a GGML model. Needs no Python, pip, or root, which makes it the only local option on hosts with a broken/absent Python packaging stack. One-time setup: ./scripts/setup-whispercpp.sh (requires git, cmake, g++, ffmpeg). Override paths with WHISPER_CPP_BIN / WHISPER_CPP_MODEL, thread count with WHISPER_CPP_THREADS (default 4).
  2. NVIDIA NeMo Parakeet — also fully local, but lazily pip installs nemo_toolkit[asr] on first use, so it requires a working pip.
  3. OpenAI Whisper API — remote fallback, used only if OPENAI_API_KEY is set.

The transcribed text is passed to the agent as the message content. If every backend is unavailable the user gets a friendly message, while the logs record a combined per-backend diagnostic (e.g. whisper.cpp: binary missing at ...; parakeet: No usable pip ...; openai: not configured). Supports OGG, MP3, WAV, M4A, WebM, FLAC, and other common audio formats.

Voice Assistant — Real-time voice interaction in Discord voice channels. Pipeline: user speaks → opus decode → downsample to 16kHz mono → Silero VAD (ONNX, ~2MB model) detects speech boundaries → local whisper.cpp transcribes → Claude generates concise spoken response (1-3 sentences, no markdown) → ElevenLabs TTS synthesizes audio → bot speaks back. STT runs on-device (same backend as voice-message transcription), so audio never leaves the machine and there is no per-utterance cost; set VOICE_STT_REMOTE_FALLBACK=1 to fall back to ElevenLabs Scribe when whisper.cpp is unavailable. The LLM is Anthropic Claude (default: claude-sonnet-4-20250514, configurable via VOICE_MODEL) with full tool support. Max tokens default 512 (configurable via VOICE_MAX_TOKENS). Tools configurable via VOICE_TOOLS_MODE: full (memory, Discord, bash, file I/O, skills, conversation history — everything except evolution) or minimal (memory + conversation history only). Supports interruptions (cuts off bot when user starts speaking), streaming TTS pipelining (sentence-level), minimum utterance filtering (skips coughs/noise < 500ms), and auto-disconnect after 10 minutes idle. Auto-join mode tracks a configured user and joins/leaves their voice channel automatically. Requires ELEVENLABS_API_KEY.

Voice Coach — AI-powered cycling coach that operates independently of the voice assistant. Runs in a dedicated voice channel and auto-joins when a tracked rider connects. Every 7 seconds, it polls simulated cycling telemetry (power, heart rate, cadence, speed) from a mock server, feeds it to an LLM coach brain (configurable via COACH_MODEL, default: bedrock-claude-sonnet-4-1m), and if the coach has something to say, synthesizes speech via ElevenLabs TTS and plays it in the voice channel. Also listens to the rider's speech via VAD+STT (reusing the voice assistant's receiver/VAD/STT pipeline) and incorporates rider messages into coaching context. The coach persona is a German-accented team sport director on race radio. Requires ELEVENLABS_API_KEY and ELEVENLABS_VOICE_ID.

Artifacts — Persistent file storage system for tracking session inputs and outputs. When files are uploaded to or generated by the agent, they're registered as artifacts with metadata (direction, MIME type, size, Discord URL). Artifacts are stored on disk under data/artifacts/<sessionId>/ and tracked in SQLite. The dashboard Artifacts page provides per-session browsing with download links. API routes at /api/artifacts and /api/artifacts/:sessionId. Uses GATEWAY_PUBLIC_URL for generating download URLs in production.

Agent Sessions — Every Discord message and every scheduled agentTurn is served by a Claude Agent SDK session (src/sdk/): one child process per target, keyed by thread / DM / channel id. A top-level message spawns a thread via the normal createThreadForReply path and the session keys to that thread, so each thread is an isolated session with its own context. Sessions are resumed by SDK session id after a restart, reaped after SDK_IDLE_MS (default 30 min) idle, killed by /stop, and any child that outlives a hard kill is swept at boot by matching its cwd. If a stored session id can no longer be resumed, the session is cleared, whatever the CLI never answered is replayed, and a fresh one starts rather than every later turn failing.

Mid-conversation injection — the SDK prompt is an async iterable, so a message arriving mid-turn is handed to the turn already running instead of queueing behind it. When that happens the bot reacts ↩️ on the message and posts a marker line naming the tool it interrupted, so the transcript shows where the course changed.

Tools — an in-process MCP server (createSdkMcpServer) bridges send_message, send_file, add_reaction, ask_user, the memory tools, the context tools get_channel_history / create_thread / get_conversation_history / get_conversation_stats, the skill tools read_skill / list_skill_files (which honour /skills disable), and the full evolve_* set — so a session self-evolves through the same worktree → PR → CI → auto-merge path, with the plan-approval gate still enforced in code by evolve_start.

No sandbox, by operator choice — permissionMode: 'bypassPermissions' with allowDangerouslySkipPermissions: true: no permission prompts, no canUseTool gate. cwd is the session's own folder under data/sdk/workspace/sessions/ but nothing confines a session to it — paths and shell commands are unrestricted (a session can edit src/**, read .env, run git push) — and the child inherits the full environment (src/sdk/env.ts), so DISCORD_BOT_TOKEN, GH_TOKEN and every provider key are readable with one Bash call. Only convention holds: the system prompt says to route source changes through evolve_* — and since settingSources: ['project'], the workspace's own CLAUDE.md is loaded too, which is where the artifact-placement conventions live ('user' is still omitted, so the operator's ~/.claude settings stay un-inherited). To restore a boundary, filter the child env in env.ts and set permissionMode: 'default' with a canUseTool gate or a PreToolUse hook.

Identity and configuration — SOUL.md, the memory-recall instructions and /caveman are injected into the system prompt from src/shared/prompt-fragments.ts, and a channel prompt (/config set-prompt) goes in under a ## Channel Instructions heading. Both are read at session start, so a change mid-session applies from the next one (the replies say so with a -# 🧪 note) and both fall back to the parent channel, since the session lives on a thread while those commands write to the channel above it. /model (or a per-job cron model) sets the model a new session starts on; SDK_ANTHROPIC_MODEL still wins and a running child keeps the model it started with. Model auth arrives with the inherited environment — set SDK_ANTHROPIC_API_KEY / SDK_ANTHROPIC_BASE_URL to point sessions at a separate key or endpoint.

Per-session folders. Each session owns data/sdk/workspace/sessions/<channel-id>/, or sessions/<parent-id>/threads/<thread-id>/ when the session keys to a thread — nested rather than concatenated, because a thread's snowflake is already globally unique so the parent id buys hierarchy rather than uniqueness. Created on session start with a seeded CLAUDE.md (session id, parent, status, notes) that is never overwritten, since it outlives the CLI transcript.

That folder is the session's cwd, so artifacts land in it by default instead of by convention, attachments download to <session-folder>/inbox/, and CLAUDE.md discovery walks up — the session's file, the workspace one and the repo one all load. Two consequences were handled with it. The CLI keys transcripts by cwd, so a session id stored before this change points at a directory the CLI no longer reads: migrateTranscript() copies (never moves) the .jsonl and its subagent directory into the new location before a resume, and session.ts cross-checks the derived path against transcript_path from the init handshake so a wrong derivation is loud rather than silent. And the orphan sweep compares cwd by root + separator prefix instead of exactly — every session cwd is now below the workspace root, so the old exact match would have swept nothing, while the separator keeps a sibling like <workspace>-old from matching.

Attachments are handed over as paths, not content blocks: an image or document is downloaded to data/sdk/workspace/inbox/<messageId>/, size-capped (SDK_ATTACHMENT_MAX_BYTES, default 25 MB) with sanitised filenames, pruned after 24 h (SDK_INBOX_MAX_AGE_MS), and its absolute path is appended to the message so the session reads it with its own tools.

Output and limits — relayed output goes through a batching queue (src/sdk/relay-queue.ts): tool-progress lines are merged on a short debounce and a 5-sends/5s token bucket waits rather than dropping. Each turn ends with a -# 📊 usage footer (per-turn tokens, cost as the delta of the SDK's cumulative total, latency), a failed tool posts -# ⚠️ <tool> failed, and an idle reap says so in the channel. A wedged turn is interrupted by a watchdog (SDK_TURN_TIMEOUT_MS, default 15 min) rather than killed, so the session stays resumable and you can just say "continue"; SDK_TURN_MAX_COST_USD adds a -# 💸 line when a single turn passes a soft cost cap. Turns are logged like everything else — the user message and one assistant row per turn go into the normal messages tables, so /history, the archive, get_conversation_history and the log viewer all see them — and a failed turn records an error signal for the reflection daemon. /interrupt is the softer sibling of /stop: scoped to the channel or thread it is used in, it calls the SDK's native Query.interrupt(), aborting the running turn but keeping the child and its context, and its reply names the tool that was in flight. It also clears our pending queue; the shipped CLI does not implement cancel_queued, so anything already handed over may still run and is reported back once the CLI advertises interrupt_receipt_v1. An interrupt is not a rollback — files written and commands run stay. /clear, /stop and /restart act on the whole channel scope (the channel plus live sessions in threads under it), because sessions key to threads while the commands are usually typed in the parent. Known limit: no message-edit streaming.

Evolution Engine — The bot can modify its own source code through GitHub pull requests. All changes are isolated in a git worktree at worktrees/<id>/, validated, and submitted as PRs via gh CLI. Validation runs in two modes: Daytona Sandbox CI (preferred) spins up an ephemeral container via @daytona/sdk, clones the branch, runs npm ci, tsc --noEmit, and vitest run in full isolation — providing clean CI without interfering with the running bot; local fallback runs typecheck and tests in the worktree with symlinked node_modules when DAYTONA_API_KEY is not set. The agent has 9 evolution tools: evolve_start, evolve_read, evolve_write, evolve_bash, evolve_propose, evolve_suggest, evolve_cancel, evolve_review, and evolve_merge. The human gate is the build plan, not the diff: evolve_start requires a plan plus plan_approved: true, so the agent must post its plan and get explicit approval before writing any code. After that, evolve_propose validates, opens the PR, merges it (squash), posts a deployment notification thread, and restarts to deploy — fully automatic. Set EVOLUTION_AUTO_MERGE=false to restore manual evolve_review + evolve_merge. If auto-merge fails (merge conflict, GitHub error) the PR is left open and evolve_merge is the fallback. The bot also records ideas for improvements it can't yet make (evolve_suggest). Evolution history is tracked in SQLite and viewable in the dashboard.

Structured Logging — Three SQLite-backed log streams for observability:

  • Application log (application_log): General operational events with level (debug, info, warn, error), category, and optional metadata.
  • Error log (error_log): Errors with stack traces, categorized for breakdown analysis.
  • Tool call log (tool_call_log): Every agent tool invocation with tool name, input, output, duration, and success/failure status.

All logging functions (appLog(), errorLog(), toolCallLog()) are non-blocking and never throw — errors during log persistence are silently caught. Use createLogger(category) for scoped module loggers. Logs are viewable in the dashboard Logs page and automatically pruned during reflection cycles.

Reflection System — Autonomous self-improvement discovery. Two components:

  • Signal collection (reflection/signals.ts): Passively records events — errors, tool failures, duplicate loop patterns. Never throws, ensuring it can't crash the main pipeline. Auto-prunes signals older than 7 days.
  • Reflection daemon (reflection/daemon.ts): Runs on a configurable interval (default: every 6 hours). Gathers signals and structured logs (tool call statistics, error breakdowns, slowest tool calls), builds a structured analysis prompt, calls Claude, and if an improvement is found, records it as an evolution idea and posts a proposal to a Discord channel. Level 1 trust: never auto-implements — always requires human approval.

Watchdog Daemon — Standalone process (src/daemon/index.ts) that spawns the bot, monitors health, handles crash recovery with evolution rollback, and sends Discord webhook notifications. Exit code 100 from the bot triggers a deploy-restart (git pull + rebuild) rather than a simple respawn. Zero imports from the main bot.

Restart — The bot can restart itself via /restart command or automatically after merging an evolution PR. On restart, stale instances are automatically detected and killed to prevent duplicate bots. An idempotent startup script (start.sh) handles deploy: git pull → run migrations → build → start → health check → auto-rollback on failure.

Architecture

graph TB
    subgraph Discord
        DU[Discord Users]
        DG[Discord Gateway]
    end

    subgraph Discordclaw
        subgraph Bot["Bot (discord.js v14)"]
            CL[Client]
            MH[Message Handler]
            SC[Slash Commands]
        end

        subgraph Agent["Agent Runtime (Claude Agent SDK)"]
            PM[submitToSdkSession]
            SP[System Prompt Builder]
            TL[SDK child process<br/>+ MCP tool bridge]
        end

        subgraph Systems
            SOUL[Soul System<br/>SOUL.md]
            MEM[Memory System<br/>FTS5 + mem9]
            SESS[Session Manager<br/>+ Archiving]
            CRON[Cron Service]
            EVO[Evolution Engine<br/>Self-Modification]
            VOICE[Voice Transcription<br/>Whisper API]
            REFL[Reflection Daemon<br/>Signal Analysis]
            LOG[Structured Logging<br/>App / Error / Tool Calls]
            VCOACH[Voice Coach<br/>Cycling AI Coach]
            ART[Artifacts<br/>File Storage]
        end

        subgraph Storage
            DB[(SQLite)]
            FS[data/ Files]
        end

        subgraph Gateway["Gateway (Express)"]
            API[REST API]
            WS[WebSocket]
            UI[React Dashboard]
        end
    end

    CLAUDE[Claude API]
    OPENAI[OpenAI Whisper API]
    ELEVEN[ElevenLabs TTS API]
    DAYTONA[Daytona Sandbox CI]

    DU <-->|messages + voice| DG
    DG <-->|events| CL
    CL --> MH
    CL --> SC
    MH --> SESS
    SESS --> DB
    MH -->|voice messages| VOICE
    VOICE <-->|transcribe| OPENAI
    VOICE -->|text| MH
    MH --> PM
    MH -->|file attachments| ART
    PM --> SP
    SP --> SOUL
    SP --> MEM
    PM --> TL
    TL <-->|prompt + tool results| CLAUDE
    TL -->|memory_search, memory_get| MEM
    TL -->|send_message, send_file, add_reaction, create_thread, ask_user| CL
    TL -->|evolve_start, evolve_write, evolve_propose auto-merge| EVO
    TL -->|tool call logging| LOG
    EVO -->|git worktree, gh pr create| GH[GitHub]
    EVO -->|sandbox validation| DAYTONA
    EVO --> DB
    MH -->|log| DB
    MH -->|broadcast| WS
    LOG --> DB
    MEM --> DB
    MEM --> FS
    SOUL --> FS
    ART --> DB
    ART --> FS
    CRON -->|agentTurn → thread| PM
    CRON -->|deliver| CL
    CRON --> FS
    VCOACH <-->|telemetry + LLM| CLAUDE
    VCOACH <-->|TTS| ELEVEN
    VCOACH <-->|voice channel| DG
    REFL -->|collect signals + logs| DB
    REFL -->|analyze| CLAUDE
    REFL -->|propose ideas| EVO
    API --> DB
    API --> SESS
    API --> SOUL
    API --> MEM
    API --> CRON
    API --> ART
    UI -->|fetch| API
    UI -->|stream| WS
Loading

Data Flow

Message Flow

sequenceDiagram
    participant U as Discord User
    participant B as Bot
    participant V as Voice Transcription
    participant S as Session Manager
    participant K as SDK Session (child process)
    participant C as Claude
    participant M as Memory
    participant DB as SQLite

    U->>B: @mention, DM, or voice message
    B->>B: Filter (bot? mention? monitored? bot thread?)

    opt Voice Message Detected
        B->>V: transcribeAudio(attachment URL)
        V->>V: Download OGG → local whisper.cpp
        V-->>B: transcribed text
    end

    opt Attachments
        B->>B: Download to <workspace>/inbox, append absolute paths
    end

    B->>U: Create thread (top-level messages only)
    B->>S: resolveSession(channel, user)
    S->>DB: lookup / create session row
    B->>DB: log + archive user message

    B->>K: submitToSdkSession(threadId, text, modelOverride?)

    alt No live session
        K->>K: spawn child (cwd data/sdk/workspace, full env, resume stored id)
        K->>K: Build system prompt (soul + memory instructions + skills + channel prompt)
    else Turn already running
        K->>K: Inject into the running turn
        K->>U: ↩️ reaction + "-# steered <tool>" marker
    end

    K->>C: query() — streaming input, bypassPermissions

    loop SDK tool loop
        C-->>K: tool_use: mcp__discordclaw__memory_search
        K->>U: Tool progress line (debounced, 5/5s bucket)
        K->>M: searchMemory(query)
        M->>DB: FTS5 MATCH query (sanitized)
        M-->>K: ranked results
        K->>DB: toolCallLog(tool, input, result, duration)
        K->>C: tool_result + continue
    end

    C-->>K: text response + result (usage, cost)
    K->>U: Assistant text, then "-# 📊" usage footer
    K->>DB: log + archive assistant response (one row per turn)
    K->>WS: broadcastLog(entry)

    Note over K: Session stays warm — reaped after SDK_IDLE_MS, or on /stop
Loading

Cron Job Execution

sequenceDiagram
    participant T as Timer Loop
    participant CS as Cron Service
    participant ST as Cron Store
    participant R as Cron Route
    participant K as SDK Session
    participant D as Discord

    T->>CS: tick() — check due jobs
    CS->>ST: getJobs() where nextRunAtMs <= now

    alt agentTurn payload
        CS->>R: planSdkCronRoute(delivery channel, job)
        R-->>CS: session channel + needsThread
        Note over R: null route → throw, so the run record shows it
        CS->>D: ensureThread(job name) if needed
        CS->>K: submitToSdkSession(threadId, prompt, job model?)
        K-->>CS: fire-and-forget (session posts via tools)
    else systemEvent payload
        CS->>CS: log event text
        opt delivery configured
            CS->>D: channel.send(result)
        end
    end

    CS->>ST: updateJobState(lastRun, status)
    CS->>ST: appendRunEntry(history)
    CS->>CS: computeNextRun() + armTimer()
Loading

Dashboard Data Flow

graph LR
    subgraph Browser
        UI[React SPA]
    end

    subgraph Server
        API[REST API]
        WS[WebSocket /ws/logs]
    end

    subgraph Data
        DB[(SQLite)]
        FS[data/ files]
        CRON[Cron Store]
    end

    UI -->|GET /api/status| API
    UI -->|GET/DELETE /api/sessions| API
    UI -->|GET/PUT /api/channels| API
    UI -->|GET/PUT /api/soul| API
    UI -->|GET/PUT /api/memory| API
    UI -->|CRUD /api/cron| API
    UI -->|GET /api/evolutions| API
    UI -->|GET /api/artifacts| API
    UI -->|GET /api/logs/*| API
    UI <-->|real-time logs| WS

    API --> DB
    API --> FS
    API --> CRON
Loading

Voice Assistant Pipeline

The voice assistant is the most complex real-time data flow in the system. It processes live audio from Discord voice channels through a multi-stage pipeline: audio capture → speech detection → transcription → AI reasoning → speech synthesis → playback — all while handling interruptions and concurrent user streams.

High-Level Pipeline

graph LR
    subgraph "🎤 Capture"
        A1[Discord Opus Stream]
        A2[Opus Decode]
        A3[Downsample<br/>48kHz → 16kHz mono]
    end

    subgraph "🧠 Detect"
        B1[Silero VAD<br/>ONNX v4]
        B2{Speech<br/>prob > 0.5?}
        B3[Silence Timer<br/>800ms]
    end

    subgraph "📝 Understand"
        C1[Concatenate PCM<br/>Chunks]
        C2[Encode WAV<br/>16kHz mono]
        C3[whisper.cpp<br/>local, on-device]
    end

    subgraph "🤖 Think"
        D1[Voice Agent<br/>Claude]
        D2[Tools<br/>memory / discord / bash / skills]
    end

    subgraph "🔊 Speak"
        E1[ElevenLabs<br/>TTS]
        E2[AudioPlayer<br/>discord.js]
    end

    A1 --> A2 --> A3
    A3 -->|Float32 frames| B1
    A3 -->|Int16 chunks| C1
    B1 --> B2
    B2 -->|Yes| B3
    B2 -->|No speech| B1
    B3 -->|Timeout| C1
    C1 --> C2 --> C3
    C3 -->|text| D1
    D1 <-->|tool calls| D2
    D1 -->|response text| E1
    E1 -->|mp3 stream| E2
Loading

Detailed Sequence: Full Utterance Lifecycle

sequenceDiagram
    participant U as Discord User
    participant DC as Discord Gateway
    participant CMD as /join Command
    participant CONN as connection.ts
    participant RECV as receiver.ts
    participant VAD as vad.ts<br/>(Silero ONNX)
    participant ORCH as index.ts<br/>(Orchestrator)
    participant STT as stt.ts<br/>(local whisper.cpp)
    participant AGT as agent.ts<br/>(Claude)
    participant MEM as Memory System
    participant TTS as tts.ts<br/>(ElevenLabs)
    participant AP as AudioPlayer

    Note over CMD,CONN: ── Initialization ──

    U->>CMD: /join
    CMD->>CONN: joinChannel(voiceChannel)
    CONN->>DC: joinVoiceChannel(channelId, guildId)
    DC-->>CONN: VoiceConnectionStatus.Ready
    CONN-->>CMD: VoiceConnection
    CMD->>ORCH: startVoice(channel)
    ORCH->>AP: connection.subscribe(audioPlayer)
    ORCH->>ORCH: Start idle timer (10 min)

    Note over U,AP: ── User Speaks ──

    U->>DC: Speaks into mic
    DC->>ORCH: speaking:start(userId)
    ORCH->>ORCH: Initialize UserUtteranceState
    ORCH->>RECV: subscribeToUser(connection, userId)
    RECV->>RECV: receiver.subscribe(userId, AfterSilence 5s)

    Note over RECV: Auto-detect mono vs stereo<br/>on first opus packet

    loop Every 20ms Opus Packet
        DC->>RECV: opus packet (48kHz)
        RECV->>RECV: decodeOpus(packet, channels)
        RECV->>ORCH: onRawPcm(Int16Array)
        RECV->>RECV: downsampleToMono16k(pcm48k)
        RECV->>ORCH: onFrame(Float32Array)

        ORCH->>ORCH: Accumulate into VAD frame buffer

        opt VAD frame buffer full (480 samples = 30ms)
            ORCH->>VAD: process(frame)
            VAD-->>ORCH: probability [0.0-1.0]

            alt prob > 0.5 (speech detected)
                Note over ORCH: If first speech frame:<br/>set isSpeaking=true,<br/>start buffering rawChunks
                opt Bot is currently playing
                    ORCH->>AP: stop() — interrupt!
                    Note over AP: ⚡ Playback cut off
                end
                ORCH->>ORCH: Reset silence timer
                ORCH->>ORCH: Buffer rawChunks += pcm
            else prob ≤ 0.5 (silence) AND isSpeaking
                opt No active silence timer
                    ORCH->>ORCH: Start silence timer (800ms)
                end
            end
        end
    end

    Note over U,AP: ── Utterance Complete ──

    ORCH->>ORCH: Silence timer fires → onUtteranceComplete()

    alt utterance < 500ms
        ORCH->>ORCH: Discard (too short — noise/cough)
    else already processing another utterance
        ORCH->>ORCH: Skip (no queue yet)
    else valid utterance
        ORCH->>ORCH: processing = true

        Note over ORCH,STT: Step 1/5: Prepare Audio
        ORCH->>ORCH: Concatenate rawChunks → Int16Array
        ORCH->>RECV: downsampleToMono16kInt16(rawPcm, channels)
        RECV-->>ORCH: mono 16kHz Int16Array
        ORCH->>VAD: reset() — clear LSTM hidden states

        Note over ORCH,STT: Step 2/5: Speech-to-Text
        ORCH->>STT: transcribe(mono16kPcm)
        STT->>STT: write 16kHz mono WAV to tmp
        STT->>STT: spawn whisper-cli<br/>(local ggml model, ~1.65s)
        STT-->>ORCH: transcribed text

        alt empty transcription
            ORCH->>ORCH: Skip — nothing detected
        else has text
            Note over ORCH,AGT: Step 3/5: AI Response
            ORCH->>ORCH: getUserDisplayName(userId)
            ORCH->>AGT: processVoiceUtterance(text, userName)
            AGT->>AGT: Build system prompt<br/>(voice rules + soul brief + time + speaker)
            AGT->>AGT: Append to ephemeral voiceHistory (max 10 turns)
            AGT->>AGT: Claude messages.create()<br/>(VOICE_MODEL, 512 max_tokens, tools)

            opt Claude requests tool_use (up to 5 rounds)
                AGT->>MEM: handleVoiceTool(name, input)
                MEM-->>AGT: tool result
                AGT->>AGT: Follow-up Claude call
            end

            AGT-->>ORCH: response text (1-3 sentences, no markdown)

            Note over ORCH,AP: Step 4/5: Text-to-Speech
            ORCH->>TTS: synthesize(responseText)
            TTS->>TTS: POST /v1/text-to-speech/{voiceId}<br/>(model: eleven_turbo_v2_5)
            TTS-->>ORCH: mp3 audio buffer

            Note over ORCH,AP: Step 5/5: Playback
            ORCH->>AP: play(audioResource)
            AP-->>ORCH: AudioPlayerStatus.Idle

            ORCH->>ORCH: processing = false
        end
    end

    Note over U,AP: ── Cleanup ──

    alt Idle timeout (10 min no activity)
        ORCH->>ORCH: stopVoice()
        ORCH->>AGT: clearVoiceHistory()
        ORCH->>CONN: leaveChannel()
        CONN->>DC: connection.destroy()
    else User runs /leave
        CMD->>ORCH: stopVoice()
        ORCH->>AGT: clearVoiceHistory()
        ORCH->>CONN: leaveChannel()
    else Opus stream ends
        RECV->>ORCH: onStreamEnd()
        ORCH->>ORCH: Complete pending utterance
        ORCH->>ORCH: Clean up userState<br/>(re-subscribe on next speaking:start)
    end
Loading

Audio Format Transformations

graph TD
    subgraph "Discord Input"
        I1["Opus packets<br/>48kHz, mono or stereo<br/>20ms frames"]
    end

    subgraph "Decode (receiver.ts)"
        D1["OpusEncoder.decode()<br/>→ PCM Int16<br/>48kHz, 960 or 1920 samples/frame"]
    end

    subgraph "Dual Output Path"
        direction LR
        P1["<b>VAD Path</b><br/>downsampleToMono16k()<br/>→ Float32 [-1.0, 1.0]<br/>16kHz mono, 320 samples/frame"]
        P2["<b>Buffer Path</b><br/>Raw Int16 chunks<br/>48kHz, original channels<br/>accumulated while speaking"]
    end

    subgraph "STT Preparation"
        S1["Concatenate raw chunks<br/>→ single Int16Array"]
        S2["downsampleToMono16kInt16()<br/>→ Int16, 16kHz mono"]
        S3["wavHeader() + PCM<br/>→ temp WAV file<br/>44-byte header + PCM data"]
    end

    subgraph "STT Backend"
        A1["whisper.cpp (local)<br/>spawn whisper-cli -m ggml<br/>→ stdout transcript"]
    end

    subgraph "TTS Output"
        T1["ElevenLabs<br/>JSON POST { text, model_id }<br/>→ mp3 audio"]
    end

    subgraph "Playback"
        PL1["Readable.from(mp3Buffer)<br/>→ createAudioResource()<br/>StreamType.Arbitrary"]
    end

    I1 --> D1
    D1 --> P1
    D1 --> P2
    P1 -->|"30ms frames"| VAD["Silero VAD<br/>ONNX inference"]
    P2 -->|"on utterance complete"| S1
    S1 --> S2 --> S3
    S3 --> A1
    A1 -->|"text"| Claude["Claude<br/>→ spoken response"]
    Claude -->|"text"| T1
    T1 --> PL1
    PL1 --> Speaker["🔊 Discord Voice Channel"]
Loading

VAD State Machine

stateDiagram-v2
    [*] --> Listening: speaking:start event

    Listening --> SpeechDetected: VAD prob > 0.5
    Listening --> Listening: VAD prob ≤ 0.5

    SpeechDetected --> SpeechDetected: VAD prob > 0.5<br/>(reset silence timer)
    SpeechDetected --> SilenceTimer: VAD prob ≤ 0.5<br/>(start 800ms timer)

    SilenceTimer --> SpeechDetected: VAD prob > 0.5<br/>(cancel timer)
    SilenceTimer --> UtteranceComplete: Timer expires

    UtteranceComplete --> Discarded: duration < 500ms
    UtteranceComplete --> Skipped: already processing
    UtteranceComplete --> Pipeline: valid utterance

    Discarded --> Listening
    Skipped --> Listening

    Pipeline --> STT: transcribe audio
    STT --> AgentThink: text received
    STT --> Listening: empty transcription
    AgentThink --> TTS: response generated
    TTS --> Playing: audio synthesized
    Playing --> Listening: playback complete

    state "Interruption" as INT {
        [*] --> CheckPlaying
        CheckPlaying --> StopPlayer: bot is playing
        StopPlayer --> [*]: audioPlayer.stop()
    }

    SpeechDetected --> INT: new speech starts\nwhile bot playing
    INT --> SpeechDetected

    Listening --> [*]: stream ends / leave / idle timeout
Loading

Source File Responsibilities

graph TB
    subgraph "voice/index.ts — Orchestrator"
        INIT[initVoice / startVoice / stopVoice]
        STATE["Per-user state management<br/>UserUtteranceState map"]
        PIPELINE["5-step pipeline orchestration<br/>Audio → STT → Agent → TTS → Play"]
        INTERRUPT["Interruption handling<br/>audioPlayer.stop on new speech"]
        IDLE["Idle timeout (10 min)<br/>auto-disconnect"]
    end

    subgraph "voice/connection.ts"
        JOIN["joinChannel(VoiceBasedChannel)<br/>joinVoiceChannel + entersState(Ready)"]
        LEAVE["leaveChannel()<br/>connection.destroy()"]
        LIFECYCLE["Connection state tracking<br/>Disconnected / Destroyed handlers"]
    end

    subgraph "voice/receiver.ts"
        SUBSCRIBE["subscribeToUser()<br/>opus stream subscription"]
        DECODE["decodeOpus(packet, channels)<br/>@discordjs/opus"]
        DETECT["Auto-detect mono vs stereo<br/>first-packet heuristic"]
        DS_FLOAT["downsampleToMono16k()<br/>48kHz Int16 → 16kHz Float32"]
        DS_INT["downsampleToMono16kInt16()<br/>48kHz Int16 → 16kHz Int16"]
    end

    subgraph "voice/vad.ts"
        VAD_INIT["SileroVAD.init()<br/>Load ONNX model (v4, ~2MB)"]
        VAD_PROC["process(frame)<br/>480 samples → probability"]
        VAD_RESET["reset()<br/>Clear LSTM h/c states"]
    end

    subgraph "voice/stt.ts"
        WAV["encodeWav(samples, sampleRate)<br/>44-byte header + PCM<br/>(remote fallback upload only)"]
        TRANSCRIBE["transcribe(pcm16kMono)<br/>→ local whisper.cpp"]
    end

    subgraph "voice/agent.ts"
        VOICE_PROMPT["Voice system prompt<br/>1-3 sentences, no markdown"]
        HISTORY["Ephemeral voice history<br/>max 10 turns"]
        PROCESS["processVoiceUtterance(text, userName)<br/>Claude + tools (up to 5 rounds)"]
        CLEAR["clearVoiceHistory()<br/>reset on disconnect"]
    end

    subgraph "voice/autoJoin.ts"
        AUTOJOIN["Auto-join/leave voice channels<br/>tracks a configured user"]
    end

    subgraph "voice/tts.ts"
        SYNTH["synthesize(text)<br/>POST to ElevenLabs<br/>→ mp3 buffer"]
    end

    INIT --> JOIN
    INIT --> SUBSCRIBE
    INIT --> VAD_INIT
    STATE --> PIPELINE
    PIPELINE --> DS_INT
    PIPELINE --> TRANSCRIBE
    PIPELINE --> PROCESS
    PIPELINE --> SYNTH
    SUBSCRIBE --> DECODE
    DECODE --> DETECT
    DECODE --> DS_FLOAT
    PROCESS --> VOICE_PROMPT
    PROCESS --> HISTORY
    IDLE --> LEAVE
    IDLE --> CLEAR
    INTERRUPT -.->|"audioPlayer.stop()"| SYNTH
    AUTOJOIN --> INIT
Loading

Evolution Flow

sequenceDiagram
    participant U as Discord User
    participant A as Agent
    participant E as Evolution Engine
    participant W as Worktree
    participant S as Daytona Sandbox
    participant GH as GitHub
    participant DC as Deploy Channel

    U->>A: "Add feature X"
    A->>U: Build plan (files, changes, risks)
    U->>A: "yes" (plan approved)
    A->>E: evolve_start(reason, plan, plan_approved)
    E->>W: git worktree add worktrees/<id>/
    E->>W: symlink node_modules
    E-->>A: evolution ID + branch

    loop Make Changes
        A->>W: evolve_write(path, content)
        A->>W: evolve_read(path)
        A->>W: evolve_bash(command)
    end

    A->>E: evolve_propose(summary)

    alt DAYTONA_API_KEY set
        E->>S: Create ephemeral sandbox
        S->>S: git clone branch
        S->>S: npm ci (clean install)
        S->>S: tsc --noEmit
        S->>S: vitest run
        S-->>E: pass/fail results
        E->>S: Destroy sandbox
    else Local fallback
        E->>W: npm run typecheck
        E->>W: vitest run (120s timeout)
    end

    E->>W: git add + commit
    E->>GH: git push + gh pr create
    E->>E: git worktree remove
    E-->>A: PR URL

    A->>U: "PR created: github.com/.../pull/N"

    U->>A: "Show me the PR"
    A->>E: evolve_review(id)
    E->>GH: gh pr diff
    E-->>A: summary + diff
    A->>U: Shows changes for review

    U->>A: "Merge it"
    A->>E: evolve_merge(id)
    E->>GH: gh pr merge --squash
    E->>DC: Create deployment thread
    E->>E: triggerRestart()
    E->>E: start.sh: git pull → migrate → build → start
    E->>E: Health check ✓ (or auto-rollback)
Loading

Project Structure

discordclaw/
├── src/
│   ├── index.ts              # Entry point: start all systems, kill stale instances on restart
│   ├── restart.ts            # Shared restart trigger — avoids circular deps
│   ├── bot/                   # Discord bot (discord.js v14)
│   │   ├── client.ts          # Client setup, intents, event routing, DM raw fallback
│   │   ├── messages.ts        # Message pipeline: filter → voice transcribe → thread create → session dispatch
│   │   └── commands.ts        # Slash commands: /ping /help /config /clear /soul /skills /cron /caveman /restart /stop /interrupt /join /leave
│   ├── agent/                 # Tool definitions + session bookkeeping (bridged into SDK sessions)
│   │   ├── tools.ts           # Discord tools (send_message, send_file, add_reaction, get_channel_history, create_thread, ask_user)
│   │   ├── questions.ts       # Pending ask_user questions (embed + buttons, answer routing)
│   │   ├── dangerous-tools.ts # Powerful tools: bash, read_file, write_file
│   │   ├── tool-paths.ts      # Workspace path resolution for the file tools
│   │   └── sessions.ts        # Per-thread/DM session tracking + TTL + message archiving
│   ├── audio/                 # Voice message handling
│   │   ├── transcribe.ts      # Backend chain: whisper.cpp → Parakeet → OpenAI, with per-backend diagnostics
│   │   └── whispercpp-transcribe.ts # Local whisper.cpp backend (native binary, no Python)
│   ├── voice/                 # Voice assistant (real-time voice channel)
│   │   ├── connection.ts      # Join/leave voice channels, VoiceConnection lifecycle
│   │   ├── receiver.ts        # Opus decode, auto-detect mono/stereo, downsample 48kHz → 16kHz mono
│   │   ├── vad.ts             # Silero VAD wrapper (ONNX runtime, v4 model)
│   │   ├── stt.ts             # STT — local whisper.cpp, optional ElevenLabs Scribe fallback
│   │   ├── tts.ts             # ElevenLabs TTS client (full synthesis + HTTP chunked streaming)
│   │   ├── agent.ts           # Voice-optimized LLM (default: Sonnet, configurable via VOICE_MODEL, 512 tokens, spoken style)
│   │   ├── autoJoin.ts        # Auto-join/leave voice channels when tracked user joins/leaves
│   │   └── index.ts           # Orchestrator: wires receive → VAD → STT → agent → TTS → play
│   ├── voice-coach/           # AI cycling coach (independent voice pipeline)
│   │   ├── index.ts           # Orchestrator: auto-join on rider connect, 7s poll loop, session lifecycle
│   │   ├── coach-brain.ts     # LLM coach decision engine (COACH_MODEL, team radio persona)
│   │   ├── elevenlabs-tts.ts  # ElevenLabs TTS synthesis client
│   │   ├── player.ts          # Voice channel connection + audio playback for coach
│   │   ├── listener.ts        # Rider speech capture via VAD+STT, queued for coach brain
│   │   └── mock-server.ts     # Simulated cycling telemetry (power, HR, cadence, speed)
│   ├── sdk/                   # The agent runtime: one Claude Agent SDK session per thread/DM/channel
│   │   ├── session.ts         # Per-channel SDK session, streaming-input queue, relay, usage footer, resume self-heal, idle reap, orphan sweep
│   │   ├── relay-queue.ts     # Batching send queue: debounced merge + 5-per-5s token bucket (no dropped lines)
│   │   ├── attachments.ts     # Downloads Discord attachments into <session>/inbox and hands over paths
│   │   ├── session-dirs.ts    # Per-session folders + cwd, CLAUDE.md scaffolding, transcript relocation
│   │   ├── bridge.ts          # In-process MCP server re-exporting Discord/memory/skill/history/evolve_* tools
│   │   ├── cron-route.ts      # Routes a cron agentTurn to a session: fresh thread, the thread itself, or the DM
│   │   ├── env.ts             # Child-process env: full inheritance, secrets included (no allowlist)
│   │   └── index.ts           # Public surface
│   ├── artifacts/             # Persistent file storage for session I/O
│   │   └── index.ts           # Register, query, and serve artifacts (disk + SQLite metadata)
│   ├── logging/               # Structured logging system
│   │   ├── logger.ts          # Core: appLog(), errorLog(), toolCallLog(), createLogger(category) factory
│   │   └── queries.ts         # Read-side: getAppLogs(), getErrorLogs(), getToolCallLogs(), stats, pruning
│   ├── skills/                # Skills management (SDK pattern)
│   │   ├── types.ts           # Skill, SkillMeta, SkillSource types
│   │   ├── store.ts           # Filesystem-based discovery + per-skill .meta.json
│   │   ├── service.ts         # CRUD, GitHub install, prompt generation, file watcher
│   │   └── tools.ts           # read_skill + list_skill_files tool definitions
│   ├── soul/
│   │   └── soul.ts            # Load SOUL.md, file watcher, hot-reload
│   ├── memory/
│   │   ├── memory.ts          # File discovery, FTS5 indexing, BM25 search, query sanitization
│   │   ├── mem9.ts            # mem9 cloud memory API client (semantic vector search)
│   │   └── tools.ts           # memory_search + memory_get + mem9_store/update/delete tool definitions
│   ├── shared/                # Utilities shared across the runtime, the voice agents and the slash commands
│   │   ├── paths.ts           # Project root resolution
│   │   ├── format.ts          # Token/duration formatting for the -# usage footer
│   │   ├── prompt-fragments.ts # Memory-recall + caveman prompt text (system prompt + slash commands)
│   │   ├── anthropic.ts       # Anthropic SDK client factory
│   │   ├── discord-utils.ts   # Channel/guild helpers
│   │   └── conversation-history.ts # Cross-session message loading + conversation history tools
│   ├── cron/
│   │   ├── types.ts           # Job, schedule, payload, delivery types
│   │   ├── store.ts           # JSON persistence + JSONL run history + hot-reload
│   │   └── service.ts         # Timer loop, execution, retry, auto-disable, thread-only delivery for agentTurn
│   ├── reflection/            # Autonomous self-improvement
│   │   ├── signals.ts         # Passive signal collection (errors, tool failures, duplicate loops)
│   │   └── daemon.ts          # Periodic analysis, idea generation, channel proposals
│   ├── evolution/             # Self-evolution system
│   │   ├── engine.ts          # Git worktree lifecycle, PR creation via gh CLI, deployment notifications
│   │   ├── sandbox.ts         # Daytona sandbox CI — ephemeral container validation for evolution branches
│   │   ├── log.ts             # Evolution SQLite table + CRUD
│   │   ├── tools.ts           # Agent tools: evolve_start/read/write/bash/propose/suggest/cancel/review/merge
│   │   └── health.ts          # /api/health endpoint for start.sh
│   ├── daemon/                # Watchdog daemon (standalone process)
│   │   └── index.ts           # Spawns bot, health monitoring, crash recovery, evolution rollback
│   ├── db/
│   │   └── index.ts           # SQLite schema, migrations, query helpers
│   └── gateway/
│       ├── server.ts          # Express + WebSocket server
│       ├── api.ts             # REST API (status, sessions, channels, config, soul, memory, cron, skills, evolutions, logs)
│       ├── artifacts.ts       # Artifact API routes (/api/artifacts, /api/artifacts/:sessionId, file serving)
│       └── ui/                # React SPA (Vite)
│           ├── App.tsx         # Layout, routing, shared styles
│           ├── shared.ts       # Shared utilities (colors, apiFetch, formatters) — breaks circular deps
│           └── pages/          # Status, Sessions, Channels, Config, Cron, Skills, Artifacts, Evolution, Logs
├── tests/
│   └── integration/           # Integration tests (vitest)
│       └── boot.test.ts       # Critical boot path: DB, Soul, Memory, Skills, Images, Tools
├── data/                      # Runtime data (gitignored)
│   ├── discordclaw.db         # SQLite database
│   ├── SOUL.md                # Bot personality
│   ├── MEMORY.md              # Long-term memory
│   ├── memory/                # Daily memory notes
│   ├── artifacts/             # Stored artifacts organized by session ID
│   ├── cron/                  # Job store + run history (jobs.json gitignored, seed file tracked)
│   ├── skills/                # Installed skills (SKILL.md + companion files)
│   ├── models/                # ML models (Silero VAD ONNX, gitignored)
│   └── .migrations/           # Marker files for completed migrations
├── migrations/                # Idempotent migration scripts (run by start.sh)
├── start.sh                   # Production startup: pull → migrate → build → start → health check
├── .env                       # DISCORD_BOT_TOKEN, ANTHROPIC_* config, OPENAI_API_KEY
├── package.json
├── tsconfig.json
└── vite.config.ts

About

A self-evolving thin OpenClaw-like implementation using Claude Agent SDK (claude-code sdk) controlled interfaced via discord, you can develop / add features on the fly via chat!

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages