Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ ALLOWED_USER_ID=your_discord_user_id

# Base folder path where Claude Code will operate
# Channel names will be appended to this path
BASE_FOLDER=/Users/your-user-name/repos
BASE_FOLDER=/Users/your-user-name/repos

# MCP Server Port (optional, defaults to 3001)
# Change this if port 3001 is already in use
MCP_SERVER_PORT=3001
Comment on lines +12 to +14
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a blank line at the end of the file.

The dotenv-linter correctly identifies that the file should end with a blank line, which is a common convention for text files.

Apply this diff:

 # MCP Server Port (optional, defaults to 3001)
 # Change this if port 3001 is already in use
 MCP_SERVER_PORT=3001
+
📝 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
# MCP Server Port (optional, defaults to 3001)
# Change this if port 3001 is already in use
MCP_SERVER_PORT=3001
# MCP Server Port (optional, defaults to 3001)
# Change this if port 3001 is already in use
MCP_SERVER_PORT=3001
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 14-14: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🤖 Prompt for AI Agents
In .env.example around lines 12 to 14, the file is missing a trailing blank line
at EOF; update the file by adding a single newline (blank line) at the end so
the file ends with an empty line.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ coverage
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
log.txt

# dotenv environment variable files
.env
Expand Down
98 changes: 95 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,13 @@ In your Discord server, create channels for each repository:

```bash
# Start the bot
bun run src/index.ts

# Or use the npm script
bun start

# Start in background (recommended for production)
nohup bun run src/index.ts > bot.log 2>&1 &

# Restart the bot (safely stops and restarts)
bun run restart
```

**Important**: Do not use hot reload (`bun --hot`) as it can cause issues with process management and spawn multiple Claude processes.
Expand All @@ -139,13 +142,41 @@ Bot is ready! Logged in as Claude Code Bot#1234
Successfully registered application commands.
```

### Managing the Bot

**Restarting the bot:**
```bash
bun run restart
```
This script safely stops any running bot instance and immediately starts a new one. Logs are written to `bot.log`.

**Viewing logs:**
```bash
tail -f bot.log
```

**Stopping the bot:**
```bash
# Find the bot process
ps aux | grep "bun run src/index.ts"

# Kill it
kill <PID>
```

## Usage

Type any message in a channel that corresponds to a repository folder. The bot will run Claude Code with your message as the prompt and stream the results.

**Notifications**: The bot will @mention you when:
- ✅ A session completes successfully
- ❌ A session fails or encounters an error
- ⏰ A session times out (after 5 minutes)

### Commands

- **Any message**: Runs Claude Code with your message as the prompt
- **stop**: Stops the currently running Claude Code process in the channel
- **/clear**: Resets the current channel's session (starts fresh next time)

### Example
Expand All @@ -165,6 +196,67 @@ Bot: 🔧 LS (path: .)
- Shows real-time tool usage and responses
- Only responds to the configured `ALLOWED_USER_ID`

## Development

### Running Tests

```bash
# Run all tests
bun run test:run

# Run tests in watch mode
bun test

# Run tests with coverage report
bun run test:coverage
```

### Test Coverage

Current test coverage (excluding MCP integration modules):

- **Overall**: 50.69%
- **database.ts**: 100% ✅
- **config.ts**: 100% ✅
- **shell.ts**: 93.58% ✅
- **commands.ts**: 76.19%
- **manager.ts**: 36.79%
- **client.ts**: 28.46%

Coverage focuses on unit-testable modules. MCP modules are excluded as they require full integration testing with Claude Code CLI and Discord bot running.

### Process Exit Code Handling

The bot properly handles Claude Code process exit codes:
- **Exit code 0**: Normal successful completion
- **Exit code 143**: SIGTERM shutdown (also treated as normal)
- **Other codes**: Displayed as errors in Discord

The bot no longer manually terminates Claude Code processes. It lets them exit naturally when tasks complete.

## Troubleshooting

### Claude Code Hooks

If you have Claude Code hooks configured (e.g., audio notifications in `~/.claude/settings.json`), they may interfere with the Discord bot. The bot automatically sets `CLAUDE_DISABLE_HOOKS=1` when spawning Claude processes.

To make your hooks respect this, add a check at the beginning of your hook scripts:

```bash
#!/bin/bash
# Example: ~/.claude/claude-sound-notification.sh

# Skip hook if running from Discord bot
if [ "$CLAUDE_DISABLE_HOOKS" = "1" ]; then
exit 0
fi

# Your existing hook code (e.g., play sound)
mplayer -really-quiet /path/to/sound.mp3
```

This ensures hooks run normally in your terminal but are skipped when using the Discord bot.

For detailed setup instructions, troubleshooting, and development information, see [CONTRIBUTING.md](CONTRIBUTING.md).

## License
Expand Down
141 changes: 131 additions & 10 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions mcp-bridge.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const { Transform } = require('stream');
// Debug: Log environment variables at startup
console.error(`MCP Bridge startup: DISCORD_CHANNEL_ID=${process.env.DISCORD_CHANNEL_ID}, DISCORD_CHANNEL_NAME=${process.env.DISCORD_CHANNEL_NAME}, DISCORD_USER_ID=${process.env.DISCORD_USER_ID}`);

const MCP_SERVER_URL = 'http://localhost:3001/mcp';
const MCP_SERVER_PORT = process.env.MCP_SERVER_PORT || '3001';
const MCP_SERVER_URL = `http://localhost:${MCP_SERVER_PORT}/mcp`;

// Transform stream to handle MCP messages
const mcpTransform = new Transform({
Expand Down Expand Up @@ -48,7 +49,7 @@ const mcpTransform = new Transform({

const options = {
hostname: 'localhost',
port: 3001,
port: parseInt(MCP_SERVER_PORT),
path: '/mcp',
method: 'POST',
headers
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
},
"scripts": {
"start": "bun run src/index.ts",
"restart": "./restart.sh",
"test": "vitest",
"test:run": "vitest run"
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"discord.js": "^14.19.3",
Expand All @@ -19,8 +21,9 @@
},
"devDependencies": {
"@types/bun": "latest",
"vitest": "^2.1.8",
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"@types/express": "^4.17.21"
"@vitest/coverage-v8": "^2.1.8",
"vitest": "^2.1.8"
}
}
44 changes: 44 additions & 0 deletions restart.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash
# Safe restart script for Claude Code Discord Bot
# Finds and kills the running bot, then immediately starts a new one

set -e

echo "🔄 Restarting Claude Code Discord Bot..."

# Find the bot process
BOT_PID=$(ps aux | grep "bun run src/index.ts" | grep -v grep | awk '{print $2}')

if [ -n "$BOT_PID" ]; then
echo "📍 Found bot running with PID: $BOT_PID"
echo "🛑 Stopping bot..."
kill -TERM $BOT_PID

# Wait for process to exit (max 5 seconds)
for i in {1..50}; do
if ! kill -0 $BOT_PID 2>/dev/null; then
echo "✅ Bot stopped"
break
fi
sleep 0.1
done

# Force kill if still running
if kill -0 $BOT_PID 2>/dev/null; then
echo "⚠️ Bot didn't stop gracefully, force killing..."
kill -9 $BOT_PID
fi
else
echo "ℹ️ No running bot found"
fi

echo "🚀 Starting bot..."
cd "$(dirname "$0")"
nohup bun run src/index.ts > bot.log 2>&1 &
NEW_PID=$!

echo "✅ Bot started with PID: $NEW_PID"
echo "📋 Logs available at: bot.log"
echo ""
echo "To view logs: tail -f bot.log"
echo "To stop: kill $NEW_PID"
17 changes: 17 additions & 0 deletions src/bot/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,23 @@ export class DiscordBot {

const channelId = message.channelId;

// Check if message is a stop command
const isStopCommand = message.content.trim().toLowerCase() === 'stop';

// If there's an active process and user says "stop", kill it
if (isStopCommand && this.claudeManager.hasActiveProcess(channelId)) {
console.log(`Stop command received for channel ${channelId}, killing process`);
this.claudeManager.killActiveProcess(channelId);

const stopEmbed = new EmbedBuilder()
.setTitle("🛑 Stopped")
.setDescription("Claude Code process has been stopped")
.setColor(0xFF6B6B); // Red-ish for stop

await message.channel.send({ embeds: [stopEmbed] });
return;
}
Comment on lines +108 to +123
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent stop command from launching new sessions

When no Claude process is active, a stop message slips past this guard and falls through to the session startup logic, spinning up a brand-new run with prompt "stop". That’s user-visible breakage: the command meant to halt work actually kicks off a new job. Please short-circuit all stop requests before the resume logic—respond with a “nothing to stop” message (or silence) instead of continuing into runClaudeCode.

-    // Check if message is a stop command
-    const isStopCommand = message.content.trim().toLowerCase() === 'stop';
-
-    // If there's an active process and user says "stop", kill it
-    if (isStopCommand && this.claudeManager.hasActiveProcess(channelId)) {
-      console.log(`Stop command received for channel ${channelId}, killing process`);
-      this.claudeManager.killActiveProcess(channelId);
-
-      const stopEmbed = new EmbedBuilder()
-        .setTitle("🛑 Stopped")
-        .setDescription("Claude Code process has been stopped")
-        .setColor(0xFF6B6B); // Red-ish for stop
-
-      await message.channel.send({ embeds: [stopEmbed] });
-      return;
-    }
+    const isStopCommand = message.content.trim().toLowerCase() === "stop";
+    if (isStopCommand) {
+      if (this.claudeManager.hasActiveProcess(channelId)) {
+        console.log(`Stop command received for channel ${channelId}, killing process`);
+        this.claudeManager.killActiveProcess(channelId);
+
+        const stopEmbed = new EmbedBuilder()
+          .setTitle("🛑 Stopped")
+          .setDescription("Claude Code process has been stopped")
+          .setColor(0xff6b6b);
+        await message.channel.send({ embeds: [stopEmbed] });
+      } else {
+        await message.channel.send("No Claude Code process is currently running.");
+      }
+      return;
+    }
📝 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
// Check if message is a stop command
const isStopCommand = message.content.trim().toLowerCase() === 'stop';
// If there's an active process and user says "stop", kill it
if (isStopCommand && this.claudeManager.hasActiveProcess(channelId)) {
console.log(`Stop command received for channel ${channelId}, killing process`);
this.claudeManager.killActiveProcess(channelId);
const stopEmbed = new EmbedBuilder()
.setTitle("🛑 Stopped")
.setDescription("Claude Code process has been stopped")
.setColor(0xFF6B6B); // Red-ish for stop
await message.channel.send({ embeds: [stopEmbed] });
return;
}
const isStopCommand = message.content.trim().toLowerCase() === "stop";
if (isStopCommand) {
if (this.claudeManager.hasActiveProcess(channelId)) {
console.log(`Stop command received for channel ${channelId}, killing process`);
this.claudeManager.killActiveProcess(channelId);
const stopEmbed = new EmbedBuilder()
.setTitle("🛑 Stopped")
.setDescription("Claude Code process has been stopped")
.setColor(0xff6b6b);
await message.channel.send({ embeds: [stopEmbed] });
} else {
await message.channel.send("No Claude Code process is currently running.");
}
return;
}
🤖 Prompt for AI Agents
In src/bot/client.ts around lines 100 to 115, the current handling only kills an
active Claude process but lets a plain "stop" message fall through and start a
new session; change the logic to short-circuit all "stop" requests by checking
isStopCommand before any session startup logic and, if no active process exists,
send an informative reply (e.g., "No active process to stop") or do nothing,
then return immediately so runClaudeCode (or any session-start code) is not
invoked with "stop" as the prompt.


// Atomic check-and-lock: if channel is already processing, skip
if (this.claudeManager.hasActiveProcess(channelId)) {
console.log(
Expand Down
Loading