Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
790d208
auto-claude: subtask-1-1 - Create health monitoring module with uptim…
BillChirico Feb 4, 2026
8eaad58
auto-claude: subtask-1-2 - Integrate health monitoring into main bot …
BillChirico Feb 4, 2026
285e153
auto-claude: subtask-2-1 - Create command deployment script to regist…
BillChirico Feb 4, 2026
99ff0b5
auto-claude: subtask-2-2 - Add interactionCreate event handler to mai…
BillChirico Feb 4, 2026
c16d0bf
auto-claude: subtask-3-1 - Create /status command handler with basic …
BillChirico Feb 4, 2026
f4dd508
auto-claude: subtask-3-2 - Wire status command into interaction handler
BillChirico Feb 4, 2026
c39a958
auto-claude: subtask-4-1 - End-to-end verification of /status command
BillChirico Feb 4, 2026
6f0c331
fix: add admin permission check for detailed diagnostics (qa-requested)
BillChirico Feb 4, 2026
d69c2c2
Merge branch 'main' into auto-claude/001-health-status-command
BillChirico Feb 4, 2026
eb89ea6
chore: remove auto-claude metadata and verification scripts from trac…
BillChirico Feb 4, 2026
bed8a8d
docs: add CLIENT_ID and GUILD_ID to .env.example
BillChirico Feb 4, 2026
05f9119
fix: throw error in HealthMonitor constructor instead of returning in…
BillChirico Feb 4, 2026
b09842d
fix: read process.uptime as property instead of function call
BillChirico Feb 4, 2026
30bece7
fix: use memberPermissions with PermissionFlagsBits for safer permiss…
BillChirico Feb 4, 2026
980ce42
refactor: use ApplicationCommandOptionType enum instead of magic number
BillChirico Feb 4, 2026
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
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Discord bot token
DISCORD_TOKEN=your_discord_bot_token

# Discord application client ID (for slash command registration)
CLIENT_ID=your_discord_client_id

# Discord guild/server ID (optional - for faster command deployment during development)
# If not set, commands deploy globally (takes up to 1 hour to propagate)
GUILD_ID=your_discord_guild_id

# OpenClaw API (routes through your Claude subscription)
OPENCLAW_URL=http://localhost:18789/v1/chat/completions
OPENCLAW_TOKEN=your_openclaw_gateway_token
11 changes: 6 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ node_modules/
.env
*.log

# Auto Claude data directory
# Auto Claude data directory and files
.auto-claude/

# Auto Claude generated files
.auto-claude-security.json
.auto-claude-status
.auto-claude-*
.claude_settings.json
.worktrees/
.security-key
logs/security/

# Verification scripts
verify-*.js
VERIFICATION_GUIDE.md
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"type": "module",
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js"
"dev": "node --watch src/index.js",
"deploy": "node src/deploy-commands.js"
},
"dependencies": {
"discord.js": "^14.25.1",
Expand Down
118 changes: 118 additions & 0 deletions src/commands/status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* Status Command - Display bot health metrics
*
* Shows uptime, memory usage, API status, and last AI request
* Admin mode (detailed: true) shows additional diagnostics
*/

import { EmbedBuilder, PermissionFlagsBits } from 'discord.js';
import { HealthMonitor } from '../utils/health.js';

/**
* Format timestamp as relative time
*/
function formatRelativeTime(timestamp) {
if (!timestamp) return 'Never';

const now = Date.now();
const diff = now - timestamp;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);

if (diff < 1000) return 'Just now';
if (seconds < 60) return `${seconds}s ago`;
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
return `${days}d ago`;
}

/**
* Get status emoji based on API status
*/
function getStatusEmoji(status) {
switch (status) {
case 'ok': return '🟢';
case 'error': return '🔴';
case 'unknown': return '🟡';
default: return '⚪';
}
}

/**
* Execute the status command
*/
export async function execute(interaction) {
try {
const detailed = interaction.options.getBoolean('detailed') || false;
const healthMonitor = HealthMonitor.getInstance();

if (detailed) {
// Check if user has admin permissions
if (!interaction.memberPermissions?.has(PermissionFlagsBits.Administrator)) {
await interaction.reply({
content: '❌ Detailed diagnostics are only available to administrators.',
ephemeral: true
});
return;
}

// Detailed mode - admin diagnostics
const status = healthMonitor.getDetailedStatus();

const embed = new EmbedBuilder()
.setColor(0x5865F2)
.setTitle('🔍 Bot Status - Detailed Diagnostics')
.addFields(
{ name: '⏱️ Uptime', value: status.uptimeFormatted, inline: true },
{ name: '🧠 Memory', value: status.memory.formatted, inline: true },
{ name: '🌐 API', value: `${getStatusEmoji(status.api.status)} ${status.api.status}`, inline: true },
{ name: '🤖 Last AI Request', value: formatRelativeTime(status.lastAIRequest), inline: true },
{ name: '📊 Process ID', value: `${status.process.pid}`, inline: true },
{ name: '🖥️ Platform', value: status.process.platform, inline: true },
{ name: '📦 Node Version', value: status.process.nodeVersion, inline: true },
{ name: '⚙️ Process Uptime', value: `${Math.floor(status.process.uptime)}s`, inline: true },
{ name: '🔢 Heap Used', value: `${status.memory.heapUsed}MB`, inline: true },
{ name: '💾 RSS', value: `${status.memory.rss}MB`, inline: true },
{ name: '📡 External', value: `${status.memory.external}MB`, inline: true },
{ name: '🔢 Array Buffers', value: `${status.memory.arrayBuffers}MB`, inline: true }
)
.setTimestamp()
.setFooter({ text: 'Detailed diagnostics mode' });

await interaction.reply({ embeds: [embed], ephemeral: true });
} else {
// Basic mode - user-friendly status
const status = healthMonitor.getStatus();

const embed = new EmbedBuilder()
.setColor(0x57F287)
.setTitle('📊 Bot Status')
.setDescription('Current health and performance metrics')
.addFields(
{ name: '⏱️ Uptime', value: status.uptimeFormatted, inline: true },
{ name: '🧠 Memory', value: status.memory.formatted, inline: true },
{ name: '🌐 API Status', value: `${getStatusEmoji(status.api.status)} ${status.api.status.toUpperCase()}`, inline: true },
{ name: '🤖 Last AI Request', value: formatRelativeTime(status.lastAIRequest), inline: false }
)
.setTimestamp()
.setFooter({ text: 'Use /status detailed:true for more info' });

await interaction.reply({ embeds: [embed] });
}
} catch (err) {
console.error('Status command error:', err.message);

const reply = {
content: 'Sorry, I couldn\'t retrieve the status. Try again in a moment!',
ephemeral: true
};

if (interaction.replied || interaction.deferred) {
await interaction.followUp(reply).catch(() => {});
} else {
await interaction.reply(reply).catch(() => {});
}
}
}
79 changes: 79 additions & 0 deletions src/deploy-commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Deploy Discord Slash Commands
*
* Registers bot commands with Discord API
* Run with: node src/deploy-commands.js
*/

import { REST, Routes, ApplicationCommandOptionType } from 'discord.js';
import { config as dotenvConfig } from 'dotenv';

dotenvConfig();

const DISCORD_TOKEN = process.env.DISCORD_TOKEN;
const CLIENT_ID = process.env.CLIENT_ID;
const GUILD_ID = process.env.GUILD_ID; // Optional: for faster guild-only deployment

if (!DISCORD_TOKEN) {
console.error('❌ Missing DISCORD_TOKEN in .env');
process.exit(1);
}

if (!CLIENT_ID) {
console.error('❌ Missing CLIENT_ID in .env');
process.exit(1);
}

// Define commands
const commands = [
{
name: 'status',
description: 'Check bot health and status',
options: [
{
name: 'detailed',
description: 'Show detailed diagnostics (admin only)',
type: ApplicationCommandOptionType.Boolean,
required: false,
},
],
},
];

const rest = new REST({ version: '10' }).setToken(DISCORD_TOKEN);

/**
* Deploy commands to Discord
*/
async function deployCommands() {
try {
console.log('🔄 Registering slash commands...');

let route;
let scope;

if (GUILD_ID) {
// Guild-specific deployment (faster for testing)
route = Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID);
scope = `guild ${GUILD_ID}`;
} else {
// Global deployment (takes up to 1 hour to propagate)
route = Routes.applicationCommands(CLIENT_ID);
scope = 'globally';
}

const data = await rest.put(route, { body: commands });

console.log(`✅ Successfully registered ${data.length} command(s) ${scope}`);
console.log(` Commands: ${data.map(cmd => `/${cmd.name}`).join(', ')}`);

if (!GUILD_ID) {
console.log('⏱️ Note: Global commands may take up to 1 hour to appear');
}
} catch (error) {
console.error('❌ Failed to register commands:', error);
process.exit(1);
}
}

deployCommands();
55 changes: 52 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { config as dotenvConfig } from 'dotenv';
import { readFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { HealthMonitor } from './utils/health.js';
import * as statusCommand from './commands/status.js';

dotenvConfig();

Expand Down Expand Up @@ -46,6 +48,9 @@ const client = new Client({
],
});

// Initialize health monitor
const healthMonitor = HealthMonitor.getInstance();

// Conversation history per channel (simple in-memory store)
const conversationHistory = new Map();
const MAX_HISTORY = 20;
Expand Down Expand Up @@ -126,19 +131,25 @@ You can use Discord markdown formatting.`;
});

if (!response.ok) {
healthMonitor.setAPIStatus('error');
throw new Error(`API error: ${response.status} ${response.statusText}`);
}

const data = await response.json();
const reply = data.choices?.[0]?.message?.content || "I got nothing. Try again?";


// Record successful AI request
healthMonitor.recordAIRequest();
healthMonitor.setAPIStatus('ok');

// Update history
addToHistory(channelId, 'user', `${username}: ${userMessage}`);
addToHistory(channelId, 'assistant', reply);

return reply;
} catch (err) {
console.error('OpenClaw API error:', err.message);
healthMonitor.setAPIStatus('error');
return "Sorry, I'm having trouble thinking right now. Try again in a moment!";
}
}
Expand Down Expand Up @@ -175,7 +186,10 @@ async function sendSpamAlert(message) {
client.once('ready', () => {
console.log(`✅ ${client.user.tag} is online!`);
console.log(`📡 Serving ${client.guilds.cache.size} server(s)`);


// Record bot start time
healthMonitor.recordStart();

if (config.welcome?.enabled) {
console.log(`👋 Welcome messages → #${config.welcome.channelId}`);
}
Expand Down Expand Up @@ -262,6 +276,41 @@ client.on('messageCreate', async (message) => {
}
});

// Handle slash commands
client.on('interactionCreate', async (interaction) => {
if (!interaction.isChatInputCommand()) return;

try {
console.log(`[INTERACTION] /${interaction.commandName} from ${interaction.user.tag}`);

// Route commands
switch (interaction.commandName) {
case 'status':
await statusCommand.execute(interaction);
break;
default:
await interaction.reply({
content: 'Unknown command!',
ephemeral: true
});
}
} catch (err) {
console.error('Interaction error:', err.message);

// Try to respond if we haven't already
const reply = {
content: 'Sorry, something went wrong with that command.',
ephemeral: true
};

if (interaction.replied || interaction.deferred) {
await interaction.followUp(reply).catch(() => {});
} else {
await interaction.reply(reply).catch(() => {});
}
}
});

// Error handling
client.on('error', (error) => {
console.error('Discord error:', error);
Expand Down
Loading