Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ vitest.config.js
.editorconfig
biome.json

# Auto Claude / OpenClaw
# Auto Claude
.auto-claude/
.auto-claude-*
.auto-claude-security.json
Expand Down
21 changes: 14 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ DISCORD_REDIRECT_URI=http://localhost:3001/api/v1/auth/discord/callback
# Generate with: openssl rand -base64 32
SESSION_SECRET=your_session_secret

# ── OpenClaw ─────────────────────────────────
# ── Anthropic ───────────────────────────────

# OpenClaw chat completions endpoint (required)
# Local: http://localhost:18789/v1/chat/completions
# Remote: https://your-tailscale-hostname.ts.net/v1/chat/completions
OPENCLAW_API_URL=http://localhost:18789/v1/chat/completions
# Anthropic API key for Claude Agent SDK (required for AI features unless using OAuth)
# Standard API keys (sk-ant-api03-*): set ANTHROPIC_API_KEY only.
# OAuth access tokens (sk-ant-oat01-*): set CLAUDE_CODE_OAUTH_TOKEN only
# and leave ANTHROPIC_API_KEY blank.
ANTHROPIC_API_KEY=your_anthropic_api_key

# OpenClaw API key / gateway token (required)
OPENCLAW_API_KEY=your_openclaw_gateway_token
# Claude Code OAuth token (required when using OAuth access tokens)
# The SDK subprocess sends this as Bearer auth. If both this and ANTHROPIC_API_KEY
# are set, the SDK sends conflicting auth headers and the API rejects the request.
# CLAUDE_CODE_OAUTH_TOKEN=your_oauth_token

# ── Database ─────────────────────────────────

Expand Down Expand Up @@ -83,6 +86,10 @@ NEXT_PUBLIC_DISCORD_CLIENT_ID=your_discord_client_id
# Get your API key from https://app.mem0.ai
MEM0_API_KEY=your_mem0_api_key

# ── Router (optional) ────────────────────────
# OpenRouter API key (required when using claude-code-router)
# OPENROUTER_API_KEY=your_openrouter_api_key

# ── Logging ──────────────────────────────────

# Logging level (optional: debug, info, warn, error — default: info)
Expand Down
15 changes: 11 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

## Project Overview

**Bill Bot** is a Discord bot for the Volvox developer community. It provides AI chat (via OpenClaw/Claude), dynamic welcome messages, spam detection, and runtime configuration management backed by PostgreSQL.
**Bill Bot** is a Discord bot for the Volvox developer community. It provides AI chat (via Claude CLI in headless mode with split Haiku classifier + Sonnet responder triage), dynamic welcome messages, spam detection, and runtime configuration management backed by PostgreSQL.

## Stack

- **Runtime:** Node.js 22 (ESM modules, `"type": "module"`)
- **Framework:** discord.js v14
- **Database:** PostgreSQL (via `pg` — raw SQL, no ORM)
- **Logging:** Winston with daily file rotation
- **AI:** Claude via OpenClaw chat completions API
- **AI:** Claude via CLI (`claude` binary in headless mode, wrapped by `CLIProcess`)
- **Linting:** Biome
- **Testing:** Vitest
- **Hosting:** Railway
Expand All @@ -25,8 +25,9 @@
| `src/db.js` | PostgreSQL pool management (init, query, close) |
| `src/logger.js` | Winston logger setup with file + console transports |
| `src/commands/*.js` | Slash commands (auto-loaded) |
| `src/modules/ai.js` | AI chat handler — conversation history, OpenClaw API calls |
| `src/modules/chimeIn.js` | Organic conversation joining logic |
| `src/modules/ai.js` | AI chat handler — conversation history, Claude CLI calls |
| `src/modules/triage.js` | Per-channel message triage — Haiku classifier + Sonnet responder via CLIProcess |
| `src/modules/cli-process.js` | Claude CLI subprocess manager with dual-mode (short-lived / long-lived) support and token-based recycling |
| `src/modules/welcome.js` | Dynamic welcome message generation |
| `src/modules/spam.js` | Spam/scam pattern detection |
| `src/modules/moderation.js` | Moderation — case creation, DM notifications, mod log embeds, escalation, tempban scheduler |
Expand All @@ -50,6 +51,7 @@
| `src/utils/sanitizeMentions.js` | Mention sanitization — strips @everyone/@here from outgoing text via zero-width space insertion |
| `src/utils/registerCommands.js` | Discord REST API command registration |
| `src/utils/splitMessage.js` | Message splitting for Discord's 2000-char limit |
| `src/utils/debugFooter.js` | Debug stats footer builder and Discord embed wrapper for AI responses |
| `src/utils/duration.js` | Duration parsing — "1h", "7d" ↔ ms with human-readable formatting |
| `config.json` | Default configuration (seeded to DB on first run) |
| `.env.example` | Environment variable template |
Expand Down Expand Up @@ -221,3 +223,8 @@ Edit `.gitleaks.toml` — add paths to `[allowlist].paths` or add inline `# gitl
9. **Duration caps** — Discord timeouts max at 28 days; slowmode caps at 6 hours (21600s). Both are enforced in command logic
10. **Tempban scheduler** — runs on a 60s interval; started in `index.js` startup and stopped in graceful shutdown. Catches up on missed unbans after restart
11. **Case numbering** — per-guild sequential and assigned atomically inside `createCase()` using `COALESCE(MAX(case_number), 0) + 1` in a single INSERT
12. **Triage budget limits** — `classifyBudget` caps Haiku classifier spend; `respondBudget` caps Sonnet responder spend per call. If exceeded, the CLI returns an error result (`is_error: true`), which the code catches and logs. Monitor `total_cost_usd` in logs
13. **Triage timeout behavior** — `timeout` controls the deadline for evaluation calls. On timeout the call is aborted and no response is sent
14. **Channel buffer eviction** — triage tracks at most 100 channels; channels inactive for 30 minutes are evicted. If a channel is evicted mid-conversation, the buffer is lost and evaluation restarts from scratch
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

find src -name "triage.js" -type f

Repository: BillChirico/bills-bot

Length of output: 86


🏁 Script executed:

rg -n --type=js '100|30.*min|evict|MAX_CHANNEL|EVICTION' src/modules/triage.js

Repository: BillChirico/bills-bot

Length of output: 631


🏁 Script executed:

rg -n --type=js 'channels?.*100|30.*minut|inactive|evict' src/

Repository: BillChirico/bills-bot

Length of output: 1848


🏁 Script executed:

sed -n '110,125p' src/modules/triage.js

Repository: BillChirico/bills-bot

Length of output: 503


🏁 Script executed:

sed -n '185,210p' src/modules/triage.js

Repository: BillChirico/bills-bot

Length of output: 1011


🏁 Script executed:

rg -n 'module.exports|export' src/modules/triage.js | head -20

Repository: BillChirico/bills-bot

Length of output: 303


Consider making the 100-channel cap and 30-minute eviction timeout configurable.

These values are hardcoded as MAX_TRACKED_CHANNELS and CHANNEL_INACTIVE_MS at lines 115–116 in src/modules/triage.js. While they have inline comments, they bypass the config system used elsewhere in the module (e.g., startTriage(), accumulateMessage(), evaluateNow()). Exposing them as config parameters would make future tuning easier without code changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` at line 181, The hardcoded limits MAX_TRACKED_CHANNELS and
CHANNEL_INACTIVE_MS in src/modules/triage.js should be exposed as configurable
parameters; modify the triage module to read these values from the existing
config interface (or accept them as options to startTriage) instead of using the
constants, update usages in startTriage, accumulateMessage, and evaluateNow to
reference the config-backed values, and ensure sensible defaults (100 and
30*60*1000) are preserved when the config entries are absent.

15. **Split triage evaluation** — two-step flow: Haiku classifies (cheap, ~80% are "ignore" and stop here), then Sonnet responds only when needed. CLIProcess wraps the `claude` CLI binary with token-based recycling (default 20k accumulated tokens) to bound context growth. Both processes use JSON schema structured output
16. **Token recycling** — each CLIProcess tracks accumulated input+output tokens. When `tokenRecycleLimit` is exceeded, the process is transparently replaced. Recycling is non-blocking — the current caller gets their result, the next caller waits for the fresh process
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ COPY --chown=botuser:botgroup src/ ./src/
# Create data directory for state persistence
RUN mkdir -p data && chown botuser:botgroup data

# Pre-seed Claude Code config with cached GrowthBook feature flags so the CLI
# does not attempt a slow/hanging network fetch on first invocation inside Docker.
# The userID and firstStartTime are placeholders; the CLI updates them at runtime.
RUN mkdir -p /home/botuser/.claude && \
printf '{\n "cachedGrowthBookFeatures": {\n "tengu_mcp_tool_search": false,\n "tengu_scratch": false,\n "tengu_disable_bypass_permissions_mode": false,\n "tengu_1p_event_batch_config": {"scheduledDelayMillis": 5000, "maxExportBatchSize": 200, "maxQueueSize": 8192},\n "tengu_claudeai_mcp_connectors": true,\n "tengu_event_sampling_config": {},\n "tengu_log_segment_events": false,\n "tengu_log_datadog_events": true,\n "tengu_marble_anvil": true,\n "tengu_tool_pear": false,\n "tengu_scarf_coffee": false,\n "tengu_keybinding_customization_release": true,\n "tengu_penguins_enabled": true,\n "tengu_thinkback": false,\n "tengu_oboe": true,\n "tengu_chomp_inflection": true,\n "tengu_copper_lantern": false,\n "tengu_marble_lantern_disabled": false,\n "tengu_vinteuil_phrase": true,\n "tengu_system_prompt_global_cache": false,\n "enhanced_telemetry_beta": false,\n "tengu_cache_plum_violet": false,\n "tengu_streaming_tool_execution2": true,\n "tengu_tool_search_unsupported_models": ["haiku"],\n "tengu_plan_mode_interview_phase": false,\n "tengu_fgts": false,\n "tengu_attribution_header": false,\n "tengu_prompt_cache_1h_config": {"allowlist": ["repl_main_thread*", "sdk"]},\n "tengu_tst_names_in_messages": false,\n "tengu_mulberry_fog": false,\n "tengu_coral_fern": false,\n "tengu_bergotte_lantern": false,\n "tengu_moth_copse": false\n },\n "opusProMigrationComplete": true,\n "sonnet1m45MigrationComplete": true,\n "cachedExtraUsageDisabledReason": null\n}\n' > /home/botuser/.claude.json && \
chown -R botuser:botgroup /home/botuser/.claude /home/botuser/.claude.json && \
chmod 600 /home/botuser/.claude.json
Comment on lines +31 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded internal GrowthBook feature flags will drift with CLI updates.

This JSON payload contains ~30 internal tengu_* feature flags that are implementation details of the Claude CLI. Since package.json uses ^2.1.44 (accepting minor/patch bumps), a CLI update may add, rename, or remove flags, causing the pre-seeded config to become stale or counterproductive. This will be a recurring maintenance burden.

Consider documenting a process to regenerate this file (e.g., "run claude --version in a fresh container and copy ~/.claude.json"), or generate it at build time by invoking the CLI briefly. At minimum, add a comment linking the seeded config to the specific CLI version it was captured from.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 32 - 38, Add provenance and an
automated/regeneration approach for the pre-seeded ~/.claude.json to avoid
drift: update the Dockerfile block that creates /home/botuser/.claude.json to
include a comment stating the Claude CLI version used to capture these flags
(the package.json dependency ^2.1.44) and either (a) replace the hardcoded JSON
at build time by invoking the CLI inside the image (run `claude --version` and
copy the runtime ~/.claude.json into /home/botuser/.claude.json) or (b) add a
short documented step in the repo README explaining how to regenerate the file
(run the CLI in a fresh container and copy ~/.claude.json), and ensure the
Dockerfile preserves chown/chmod on /home/botuser/.claude.json (the existing
chown -R botuser:botgroup and chmod 600 lines should remain).


USER botuser

CMD ["node", "src/index.js"]
54 changes: 37 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Node.js](https://img.shields.io/badge/Node.js-22-green.svg)](https://nodejs.org)

AI-powered Discord bot for the [Volvox](https://volvox.dev) developer community. Built with discord.js v14 and powered by Claude via [OpenClaw](https://openclaw.com).
AI-powered Discord bot for the [Volvox](https://volvox.dev) developer community. Built with discord.js v14 and powered by Claude via the Claude CLI in headless mode.

## ✨ Features

- **🧠 AI Chat** — Mention the bot to chat with Claude. Maintains per-channel conversation history with intelligent context management.
- **🎯 Chime-In** — Bot can organically join conversations when it has something relevant to add (configurable per-channel).
- **🎯 Smart Triage** — Two-step evaluation (fast classifier + responder) that drives chime-ins and community rule enforcement.
- **👋 Dynamic Welcome Messages** — Contextual onboarding with time-of-day greetings, community activity snapshots, member milestones, and highlight channels.
- **🛡️ Spam Detection** — Pattern-based scam/spam detection with mod alerts and optional auto-delete.
- **⚔️ Moderation Suite** — Full-featured mod toolkit: warn, kick, ban, tempban, softban, timeout, purge, lock/unlock, slowmode. Includes case management, mod log routing, DM notifications, auto-escalation, and tempban scheduling.
Expand All @@ -25,8 +25,8 @@ Discord User
┌─────────────┐ ┌──────────────┐ ┌─────────┐
│ Bill Bot │────▶│ OpenClaw │────▶│ Claude │
│ (Node.js) │◀────│ Gateway │◀────│ (AI) │
│ Bill Bot │────▶│ Claude CLI │────▶│ Claude │
│ (Node.js) │◀────│ (headless) │◀────│ (AI) │
└──────┬──────┘ └──────────────┘ └─────────┘
Expand All @@ -40,7 +40,7 @@ Discord User
- [Node.js](https://nodejs.org) 22+
- [pnpm](https://pnpm.io) (`npm install -g pnpm`)
- [PostgreSQL](https://www.postgresql.org/) database
- [OpenClaw](https://openclaw.com) gateway (for AI chat features)
- An [Anthropic API key](https://console.anthropic.com) (for AI chat features)
- A [Discord application](https://discord.com/developers/applications) with bot token

## 🚀 Setup
Expand Down Expand Up @@ -96,8 +96,8 @@ pnpm dev
| `DISCORD_TOKEN` | ✅ | Discord bot token |
| `DISCORD_CLIENT_ID` | ✅* | Discord application/client ID for slash-command deployment (`pnpm deploy`) |
| `GUILD_ID` | ❌ | Guild ID for faster dev command deployment (omit for global) |
| `OPENCLAW_API_URL` | ✅ | OpenClaw chat completions endpoint |
| `OPENCLAW_API_KEY` | | OpenClaw gateway authentication token |
| `ANTHROPIC_API_KEY` | ✅ | Anthropic API key for Claude AI |
| `CLAUDE_CODE_OAUTH_TOKEN` | | Required when using OAuth access tokens (`sk-ant-oat01-*`). Leave `ANTHROPIC_API_KEY` blank when using this. |
| `DATABASE_URL` | ✅** | PostgreSQL connection string for persistent config/state |
| `MEM0_API_KEY` | ❌ | Mem0 API key for long-term memory |
| `BOT_API_SECRET` | ✅*** | Shared secret for web dashboard API authentication |
Expand All @@ -107,7 +107,6 @@ pnpm dev
\** Bot can run without DB, but persistent config is strongly recommended in production.
\*** Required when running with the web dashboard. Can be omitted for bot-only deployments.

Legacy OpenClaw aliases are also supported for backwards compatibility: `OPENCLAW_URL`, `OPENCLAW_TOKEN`.

### Web Dashboard

Expand All @@ -130,20 +129,41 @@ All configuration lives in `config.json` and can be updated at runtime via the `
| Key | Type | Description |
|-----|------|-------------|
| `enabled` | boolean | Enable/disable AI responses |
| `model` | string | Claude model to use (e.g. `claude-sonnet-4-20250514`) |
| `maxTokens` | number | Max tokens per AI response |
| `systemPrompt` | string | System prompt defining bot personality |
| `channels` | string[] | Channel IDs to respond in (empty = all channels) |
| `historyLength` | number | Max conversation history entries per channel (default: 20) |
| `historyTTLDays` | number | Days before old history is cleaned up (default: 30) |
| `threadMode.enabled` | boolean | Enable threaded responses (default: false) |
| `threadMode.autoArchiveMinutes` | number | Thread auto-archive timeout (default: 60) |
| `threadMode.reuseWindowMinutes` | number | Window for reusing existing threads (default: 30) |

### Chime-In (`chimeIn`)
### Triage (`triage`)

| Key | Type | Description |
|-----|------|-------------|
| `enabled` | boolean | Enable organic conversation joining |
| `evaluateEvery` | number | Evaluate every N messages |
| `model` | string | Model for evaluation (e.g. `claude-haiku-4-5`) |
| `enabled` | boolean | Enable triage-based message evaluation |
| `defaultInterval` | number | Base evaluation interval in ms (default: 5000) |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

defaultInterval default documented as 5000 but config.json has 3000.

The README says "default: 5000" but config.json ships with 3000. Additionally, getDynamicInterval in triage.js defaults to 5000, and the scheduleEvaluation function defaults to 0. These should all agree on a single canonical default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 145, Documentation and code disagree on the canonical
default for the evaluation interval: README lists defaultInterval as 5000 while
config.json sets 3000 and triage.js's getDynamicInterval defaults to 5000 but
scheduleEvaluation defaults to 0; pick one canonical default (suggest aligning
everything to the value in config.json or vice versa) and update all places to
match: update README's table entry `defaultInterval`, the value in config.json,
the default parameter/return in getDynamicInterval (triage.js), and the default
used by scheduleEvaluation so they all use the same numeric constant (refer to
symbols defaultInterval, config.json, getDynamicInterval, and scheduleEvaluation
when making the edits).

| `maxBufferSize` | number | Max messages per channel buffer (default: 30) |
| `triggerWords` | string[] | Words that force instant evaluation (default: `["volvox"]`) |
| `moderationKeywords` | string[] | Words that flag for moderation |
| `classifyModel` | string | Model for classification step (default: `claude-haiku-4-5`) |
| `respondModel` | string | Model for response step (default: `claude-sonnet-4-6`) |
| `classifyBudget` | number | Max USD per classify call (default: 0.05) |
| `respondBudget` | number | Max USD per respond call (default: 0.20) |
| `thinkingTokens` | number | Thinking token budget for responder (default: 4096) |
| `contextMessages` | number | Channel history messages fetched for context (default: 10) |
| `streaming` | boolean | Enable streaming responses (default: false) |
| `tokenRecycleLimit` | number | Token threshold before recycling CLI process (default: 20000) |
| `timeout` | number | Evaluation timeout in ms (default: 30000) |
| `classifyBaseUrl` | string | Custom API base URL for classifier (default: null) |
| `respondBaseUrl` | string | Custom API base URL for responder (default: null) |
| `classifyApiKey` | string | Custom API key for classifier (default: null) |
| `respondApiKey` | string | Custom API key for responder (default: null) |
| `moderationResponse` | boolean | Send moderation nudge messages (default: true) |
| `channels` | string[] | Channels to monitor (empty = all) |
| `excludeChannels` | string[] | Channels to never chime into |
| `excludeChannels` | string[] | Channels to never triage |
| `debugFooter` | boolean | Show debug stats footer on AI responses (default: false) |
| `debugFooterLevel` | string | Footer density: `"verbose"`, `"compact"`, or `"split"` (default: `"verbose"`) |

### Welcome Messages (`welcome`)

Expand Down Expand Up @@ -351,8 +371,8 @@ Set these in the Railway dashboard for the Bot service:
| `DISCORD_TOKEN` | Yes | Discord bot token |
| `DISCORD_CLIENT_ID` | Yes | Discord application/client ID |
| `GUILD_ID` | No | Guild ID for faster dev command deployment (omit for global) |
| `OPENCLAW_API_URL` | Yes | OpenClaw chat completions endpoint |
| `OPENCLAW_API_KEY` | Yes | OpenClaw gateway authentication token |
| `ANTHROPIC_API_KEY` | Yes | Anthropic API key for Claude AI |
| `CLAUDE_CODE_OAUTH_TOKEN` | No | Required when using OAuth access tokens (`sk-ant-oat01-*`). Leave `ANTHROPIC_API_KEY` blank when using this. |
| `DATABASE_URL` | Yes | `${{Postgres.DATABASE_URL}}` — Railway variable reference |
| `MEM0_API_KEY` | No | Mem0 API key for long-term memory |
| `LOG_LEVEL` | No | `debug`, `info`, `warn`, or `error` (default: `info`) |
Expand Down
33 changes: 24 additions & 9 deletions config.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
{
"ai": {
"enabled": true,
"model": "claude-sonnet-4-20250514",
"maxTokens": 1024,
"systemPrompt": "You are Volvox Bot, the friendly AI assistant for the Volvox developer community Discord server.\n\nYou're witty, snarky (but warm), and deeply knowledgeable about programming, software development, and tech.\n\nKey traits:\n- Helpful but not boring\n- Can roast people lightly when appropriate\n- Enthusiastic about cool tech and projects\n- Supportive of beginners learning to code\n- Concise - this is Discord, not an essay\n\n⚠️ CRITICAL RULES:\n- NEVER type @.everyone or @.here (remove the dots) - these ping hundreds of people\n- NEVER use mass mention pings under any circumstances\n- If you need to address the group, say \"everyone\" or \"folks\" without the @ symbol\n\nKeep responses under 2000 chars. Use Discord markdown when helpful.",
"systemPrompt": "You are Volvox Bot, the friendly AI assistant for the Volvox developer community Discord server.\n\nYou're witty, snarky (but warm), and deeply knowledgeable about programming, software development, and tech.\n\nKey traits:\n- Helpful but not boring\n- Can roast people lightly when appropriate\n- Enthusiastic about cool tech and projects\n- Supportive of beginners learning to code\n- Concise - this is Discord, not an essay\n\nIf asked about your own infrastructure, model, or internals — say you don't know the specifics\nand suggest asking a server admin. Don't guess or speculate about what you run on.\n\nCRITICAL RULES:\n- NEVER type @everyone or @here — these ping hundreds of people\n- NEVER use mass mention pings under any circumstances\n- If you need to address the group, say \"everyone\" or \"folks\" without the @ symbol\n\nKeep responses under 2000 chars. Use Discord markdown when helpful.",
"channels": [],
"historyLength": 20,
"historyTTLDays": 30,
Expand All @@ -13,13 +11,30 @@
"reuseWindowMinutes": 30
}
},
"chimeIn": {
"enabled": false,
"evaluateEvery": 10,
"model": "claude-haiku-4-5",
"maxBufferSize": 10,
"triage": {
"enabled": true,
"defaultInterval": 3000,
"maxBufferSize": 30,
Comment on lines +16 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dynamic interval defaults don’t match the 10s/5s/2s requirement.

With defaultInterval: 3000, the current interval logic produces ~3s/1.5s/0.6s tiers, which is far more aggressive than the specified 10s/5s/2s and will increase triage cost. If the requirement still stands, bump the base interval to 10000 (or adjust the interval logic).

💡 Suggested fix
-    "defaultInterval": 3000,
+    "defaultInterval": 10000,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"defaultInterval": 3000,
"maxBufferSize": 30,
"defaultInterval": 10000,
"maxBufferSize": 30,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config.json` around lines 16 - 17, The configured defaultInterval value is
wrong for the required 10s/5s/2s tiers; update the "defaultInterval" setting
from 3000 to 10000 in config.json so the dynamic interval logic yields
~10s/5s/2s (or alternatively adjust the interval computation code to scale from
3000 to produce those tiers), and keep "maxBufferSize" as-is unless you
deliberately want to change buffering behavior.

"triggerWords": ["volvox"],
"moderationKeywords": [],
"classifyModel": "claude-haiku-4-5",
"classifyBudget": 0.05,
"respondModel": "claude-sonnet-4-6",
"respondBudget": 0.20,
"thinkingTokens": 4096,
"classifyBaseUrl": null,
"classifyApiKey": null,
"respondBaseUrl": null,
"respondApiKey": null,
"streaming": false,
"tokenRecycleLimit": 20000,
"contextMessages": 10,
"timeout": 30000,
"moderationResponse": true,
"channels": [],
"excludeChannels": []
"excludeChannels": [],
"debugFooter": true,
"debugFooterLevel": "verbose"
Comment on lines +36 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

debugFooter: true exposes internal AI stats to all users by default.

The default config enables debug footers with verbose level, which shows triage/response model names, token counts, and cost data to end users. This should default to false for production; operators can enable it explicitly when debugging.

🔧 Suggested fix
-    "debugFooter": true,
-    "debugFooterLevel": "verbose"
+    "debugFooter": false,
+    "debugFooterLevel": "compact"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"debugFooter": true,
"debugFooterLevel": "verbose"
"debugFooter": false,
"debugFooterLevel": "compact"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config.json` around lines 36 - 37, Change the production defaults so
debugFooter is disabled and verbose metadata is not exposed: set "debugFooter"
to false in the config (and optionally change "debugFooterLevel" from "verbose"
to a less-detailed level such as "info" or remove it), ensuring only operators
who explicitly enable debugFooters in non-production environments will surface
internal AI stats.

},
"welcome": {
"enabled": true,
Expand Down
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,20 @@ services:
profiles:
- full

router:
build:
context: ./router
dockerfile: Dockerfile
restart: unless-stopped
environment:
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3456 || exit 1"]
interval: 5s
timeout: 3s
retries: 5
profiles:
- router
Comment on lines +53 to +66
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Verify router service network accessibility.

The router service has no ports mapping and no depends_on declarations. If other services (e.g., bot) need to reach the router internally, consider adding:

  1. A networks configuration for inter-service communication, or
  2. Document that services can reach it via Docker's default bridge network at http://router:3456.

If the router is only used externally (e.g., for development tooling), the current setup is fine.

Also, ensure OPENROUTER_API_KEY is documented in your .env.example or README to guide deployment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose.yml` around lines 53 - 66, The router service is not reachable
by name or ports by other containers; update the router service definition
(service name: router) to attach it to the shared Docker network used by other
services (add a networks: entry and ensure other services join the same network)
and, if service startup order matters, add a depends_on: [bot] or the
appropriate dependent service(s); if you intend external access instead, add a
ports: ["3456:3456"] mapping to expose http://localhost:3456; finally, document
the required OPENROUTER_API_KEY in .env.example or the README so deployments
have the variable set.


volumes:
pgdata:
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"prepare": "git config core.hooksPath .hooks"
},
"dependencies": {
"@anthropic-ai/claude-code": "^2.1.44",
"discord.js": "^14.25.1",
"dotenv": "^17.3.1",
"express": "^5.2.1",
Expand Down
5 changes: 5 additions & 0 deletions router/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM node:22-alpine
RUN npm install -g @musistudio/claude-code-router
COPY config.json /root/.claude-code-router/config.json
EXPOSE 3456
CMD ["ccr", "start"]
Comment on lines +1 to +5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@musistudio/claude-code-router npm package latest version

💡 Result:

The latest published version of the npm package @musistudio/claude-code-router is 1.0.47. [1]


🏁 Script executed:

# Check for docker-compose.yml and any existing configuration
fd -e "docker-compose" -e "docker-compose.yml" -e ".dockerignore"

# Look for any package version references or pinning in the codebase
fd -e "package.json" | head -20

Repository: BillChirico/bills-bot

Length of output: 47


🏁 Script executed:

# Get repository overview
git ls-files | head -30

# Look for Dockerfile and docker-compose in the repo
find . -name "Dockerfile" -o -name "docker-compose.yml" 2>/dev/null

# Check directory structure
ls -la

Repository: BillChirico/bills-bot

Length of output: 2397


🏁 Script executed:

# Read the docker-compose.yml to understand how router service is configured
cat -n docker-compose.yml

# Check router/config.json
cat -n router/config.json

# Check package.json for any package version references
cat -n package.json

Repository: BillChirico/bills-bot

Length of output: 4148


Run as a non-root user and pin the package version.

The container runs a network-facing service as root, which widens the blast radius if the process is compromised. Additionally, omitting a version pin on the npm package means builds are not reproducible and could silently pick up breaking changes.

Pin the version to @musistudio/claude-code-router@1.0.47 (or your preferred version). When switching to a non-root user, adjust the config destination from /root/.claude-code-router/config.json to a path owned by the new user (e.g., /home/appuser/.claude-code-router/config.json).

🧰 Tools
🪛 Checkov (3.2.334)

[low] 1-5: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)


[low] 1-5: Ensure that a user for the container has been created

(CKV_DOCKER_3)

🪛 Hadolint (2.14.0)

[warning] 2-2: Pin versions in npm. Instead of npm install <package> use npm install <package>@<version>

(DL3016)

🪛 Trivy (0.69.1)

[info] 1-1: No HEALTHCHECK defined

Add HEALTHCHECK instruction in your Dockerfile

Rule: DS-0026

Learn more

(IaC/Dockerfile)


[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)


[info] 1-1: No HEALTHCHECK defined

Add HEALTHCHECK instruction in your Dockerfile

Rule: DS-0026

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@router/Dockerfile` around lines 1 - 5, Pin the npm package and run the
process as a non-root user: change the global install to install
`@musistudio/claude-code-router`@1.0.47 in the RUN line, create a non-root user
(e.g., appuser) and home dir, adjust the COPY destination from
/root/.claude-code-router/config.json to the non-root user's home like
/home/appuser/.claude-code-router/config.json and ensure ownership (chown) of
that directory to appuser, and add a USER appuser before the CMD ["ccr",
"start"] so the service (CMD) runs unprivileged.

Loading
Loading