From 136dc09cbde1811f47ed0c551039a444d973a84c Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 14:12:56 -0500 Subject: [PATCH 01/10] Adding in files --- README.md | 129 ++++++++++++-------- src/commands/config/config.ts | 16 ++- src/commands/fun/fun.ts | 2 +- src/commands/fun/subcommands/joke/add.ts | 5 +- src/commands/fun/subcommands/joke/index.ts | 3 +- src/commands/fun/subcommands/joke/list.ts | 6 +- src/commands/fun/subcommands/joke/random.ts | 3 +- src/commands/fun/subcommands/joke/remove.ts | 13 +- src/commands/fun/subcommands/poll.ts | 12 +- src/commands/github/status.ts | 10 +- src/commands/help/help.ts | 5 +- src/commands/timezone/timezone.ts | 8 +- src/services/discord/interactionHandler.ts | 5 +- src/services/discord/safeReply.ts | 67 +++++----- src/utils/colors.ts | 20 +++ src/utils/interactions.ts | 57 +++++++++ 16 files changed, 242 insertions(+), 119 deletions(-) create mode 100644 src/utils/colors.ts create mode 100644 src/utils/interactions.ts diff --git a/README.md b/README.md index 86fa581d..63b6c763 100644 --- a/README.md +++ b/README.md @@ -214,12 +214,47 @@ OmegaBot uses discord.js v14 which includes: ``` . +├── .github +│ ├── ISSUE_TEMPLATE +│ │ ├── bug.yml +│ │ ├── config.yml +│ │ ├── documentation.yml +│ │ ├── enhancement_refactor.yml +│ │ ├── feature_request.yml +│ │ └── question_discussion.yml +│ ├── workflows +│ │ └── OmegaBot.yml +│ └── pull_request_template.md +├── .husky +│ ├── _ +│ │ ├── .gitignore +│ │ ├── applypatch-msg +│ │ ├── commit-msg +│ │ ├── h +│ │ ├── husky.sh +│ │ ├── post-applypatch +│ │ ├── post-checkout +│ │ ├── post-commit +│ │ ├── post-merge +│ │ ├── post-rewrite +│ │ ├── pre-applypatch +│ │ ├── pre-auto-gc +│ │ ├── pre-commit +│ │ ├── pre-merge-commit +│ │ ├── pre-push +│ │ ├── pre-rebase +│ │ └── prepare-commit-msg +│ ├── pre-commit +│ └── pre-push ├── assets │ ├── banner.png │ └── omegabot.png -├── CHANGELOG.md -├── CONTRIBUTORS.md ├── data +│ ├── faqs.json +│ ├── fun-usage.json +│ ├── github-assignees.json +│ ├── guild-config.json +│ ├── last-seen.json │ └── omegabot.db ├── docs │ ├── commands.md @@ -228,60 +263,35 @@ OmegaBot uses discord.js v14 which includes: │ ├── setup-discord.md │ ├── setup-env.md │ └── transcripts.md -├── .env -├── .env.example -├── eslint.config.ts -├── .github -│ ├── ISSUE_TEMPLATE -│ │ ├── bug.yml -│ │ ├── config.yml -│ │ ├── documentation.yml -│ │ ├── enhancement_refactor.yml -│ │ ├── feature_request.yml -│ │ └── question_discussion.yml -│ ├── pull_request_template.md -│ └── workflows -│ └── OmegaBot.yml -├── .gitignore -├── .husky -│ ├── pre-commit -│ └── pre-push -├── LICENSE -├── package.json -├── package-lock.json -├── .prettierignore -├── .prettierrc.yml -├── README.md ├── scripts │ └── precheck.sh ├── src -│ ├── bot.ts │ ├── commands │ │ ├── changelog │ │ │ └── changelog.ts │ │ ├── config │ │ │ └── config.ts │ │ ├── faq -│ │ │ ├── faq.ts -│ │ │ └── subcommands -│ │ │ ├── add.ts -│ │ │ ├── get.ts -│ │ │ ├── list.ts -│ │ │ └── remove.ts +│ │ │ ├── subcommands +│ │ │ │ ├── add.ts +│ │ │ │ ├── get.ts +│ │ │ │ ├── list.ts +│ │ │ │ └── remove.ts +│ │ │ └── faq.ts │ │ ├── fun -│ │ │ ├── fun.ts -│ │ │ └── subcommands -│ │ │ ├── coinflip.ts -│ │ │ ├── dice.ts -│ │ │ ├── joke -│ │ │ │ ├── add.ts -│ │ │ │ ├── index.ts -│ │ │ │ ├── list.ts -│ │ │ │ ├── random.ts -│ │ │ │ └── remove.ts -│ │ │ ├── leaderboard.ts -│ │ │ ├── poll.ts -│ │ │ └── weather.ts +│ │ │ ├── subcommands +│ │ │ │ ├── joke +│ │ │ │ │ ├── add.ts +│ │ │ │ │ ├── index.ts +│ │ │ │ │ ├── list.ts +│ │ │ │ │ ├── random.ts +│ │ │ │ │ └── remove.ts +│ │ │ │ ├── coinflip.ts +│ │ │ │ ├── dice.ts +│ │ │ │ ├── leaderboard.ts +│ │ │ │ ├── poll.ts +│ │ │ │ └── weather.ts +│ │ │ └── fun.ts │ │ ├── general │ │ │ └── ping.ts │ │ ├── github @@ -289,8 +299,8 @@ OmegaBot uses discord.js v14 which includes: │ │ │ ├── pr.ts │ │ │ └── status.ts │ │ ├── help -│ │ │ ├── helpText.ts -│ │ │ └── help.ts +│ │ │ ├── help.ts +│ │ │ └── helpText.ts │ │ ├── history │ │ │ └── history.ts │ │ ├── pagination @@ -303,7 +313,6 @@ OmegaBot uses discord.js v14 which includes: │ │ └── timezone.ts │ ├── config │ │ └── env.ts -│ ├── registerCommands.ts │ ├── services │ │ ├── cache │ │ │ └── simpleCache.ts @@ -322,11 +331,11 @@ OmegaBot uses discord.js v14 which includes: │ │ │ ├── interactionHandler.ts │ │ │ └── safeReply.ts │ │ ├── faq +│ │ │ ├── _shared.ts │ │ │ ├── faqService.ts │ │ │ ├── permissions.ts │ │ │ ├── services.test.ts │ │ │ ├── services.ts -│ │ │ ├── _shared.ts │ │ │ ├── store.test.ts │ │ │ ├── store.test.ts.disabled │ │ │ ├── store.ts @@ -349,7 +358,6 @@ OmegaBot uses discord.js v14 which includes: │ │ │ └── types.ts │ │ ├── joke │ │ │ └── jokeStore.ts -│ │ ├── permissions │ │ ├── roles │ │ │ └── autoRoleHandler.ts │ │ ├── summary @@ -370,8 +378,23 @@ OmegaBot uses discord.js v14 which includes: │ │ └── welcome │ │ ├── welcomeHandler.ts │ │ └── welcomeMessage.ts -│ └── utils -│ └── logger.ts +│ ├── utils +│ │ ├── colors.ts +│ │ ├── interactions.ts +│ │ └── logger.ts +│ ├── bot.ts +│ └── registerCommands.ts +├── .env.example +├── .gitignore +├── .prettierignore +├── .prettierrc.yml +├── CHANGELOG.md +├── CONTRIBUTORS.md +├── eslint.config.ts +├── LICENSE +├── package-lock.json +├── package.json +├── README.md ├── tsconfig.json └── vitest.config.ts diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts index f65023e9..75c689e0 100644 --- a/src/commands/config/config.ts +++ b/src/commands/config/config.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/config/config.ts import { @@ -53,7 +54,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { if (!interaction.guildId) { await interaction.reply({ content: "This command can only be used in a server (not in DMs).", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -62,7 +63,10 @@ export async function execute(interaction: ChatInputCommandInteraction) { const sub = interaction.options.getSubcommand(); if (group !== "welcome-channel") { - await interaction.reply({ content: "Unknown config group.", ephemeral: true }); + await interaction.reply({ + content: "Unknown config group.", + flags: MessageFlags.Ephemeral, + }); return; } @@ -73,7 +77,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { if (channel.type !== ChannelType.GuildText) { await interaction.reply({ content: "Please choose a normal text channel (not a thread or DM).", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -90,7 +94,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { await interaction.reply({ content: `✅ Welcome messages will be posted in <#${updated.welcomeChannelId}>.`, - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -103,7 +107,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { await interaction.reply({ content: "Welcome channel is already not set. I will use the system channel or first text channel as a fallback.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -118,7 +122,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { await interaction.reply({ content: "✅ Cleared the welcome channel. I will use the system channel or first text channel as a fallback.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } } diff --git a/src/commands/fun/fun.ts b/src/commands/fun/fun.ts index 821a9b44..e214665a 100644 --- a/src/commands/fun/fun.ts +++ b/src/commands/fun/fun.ts @@ -226,7 +226,7 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: "Something went wrong. Try again in a bit.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } else { await interaction.editReply("Something went wrong. Try again in a bit."); diff --git a/src/commands/fun/subcommands/joke/add.ts b/src/commands/fun/subcommands/joke/add.ts index 234bf0bd..8f6d2fc3 100644 --- a/src/commands/fun/subcommands/joke/add.ts +++ b/src/commands/fun/subcommands/joke/add.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/fun/subcommands/joke/add.ts import type { ChatInputCommandInteraction } from "discord.js"; import { addJoke, type JokeCategory } from "../../../../services/joke/jokeStore.js"; @@ -11,7 +12,7 @@ export async function handleJokeAdd( if (jokeText.length > 1000) { await interaction.reply({ content: "Joke is too long! Keep it under 1000 characters.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -27,6 +28,6 @@ export async function handleJokeAdd( "", joke.joke_text, ].join("\n"), - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } diff --git a/src/commands/fun/subcommands/joke/index.ts b/src/commands/fun/subcommands/joke/index.ts index d3a7cb1e..b11433aa 100644 --- a/src/commands/fun/subcommands/joke/index.ts +++ b/src/commands/fun/subcommands/joke/index.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/fun/subcommands/joke/index.ts import { SlashCommandSubcommandBuilder, @@ -87,7 +88,7 @@ export async function handleJoke( default: await interaction.reply({ content: "Unknown subcommand", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } } diff --git a/src/commands/fun/subcommands/joke/list.ts b/src/commands/fun/subcommands/joke/list.ts index ff9569e9..6ae4338c 100644 --- a/src/commands/fun/subcommands/joke/list.ts +++ b/src/commands/fun/subcommands/joke/list.ts @@ -1,5 +1,5 @@ // src/commands/fun/subcommands/joke/list.ts -import { EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { MessageFlags, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; import { listJokes, getJokeStats, @@ -35,7 +35,7 @@ export async function handleJokeList( content: category ? `No jokes in the ${category} category yet.` : "No jokes added yet. Be the first with `/fun joke add`!", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -63,5 +63,5 @@ export async function handleJokeList( }) .setColor(0xffd700); // Gold color for fun commands - await interaction.reply({ embeds: [embed], ephemeral: true }); + await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral }); } diff --git a/src/commands/fun/subcommands/joke/random.ts b/src/commands/fun/subcommands/joke/random.ts index d7ff1b29..47a48e3f 100644 --- a/src/commands/fun/subcommands/joke/random.ts +++ b/src/commands/fun/subcommands/joke/random.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/fun/subcommands/joke/random.ts import type { ChatInputCommandInteraction } from "discord.js"; import { getRandomJoke, type JokeCategory } from "../../../../services/joke/jokeStore.js"; @@ -30,7 +31,7 @@ export async function handleJokeRandom( content: category ? `No jokes found in the ${category} category. Add some with \`/fun joke add\`!` : "No jokes available yet. Add some with `/fun joke add`!", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } diff --git a/src/commands/fun/subcommands/joke/remove.ts b/src/commands/fun/subcommands/joke/remove.ts index 6203e132..c80c5231 100644 --- a/src/commands/fun/subcommands/joke/remove.ts +++ b/src/commands/fun/subcommands/joke/remove.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/fun/subcommands/joke/remove.ts import type { ChatInputCommandInteraction } from "discord.js"; import { removeJoke, getJoke } from "../../../../services/joke/jokeStore.js"; @@ -11,7 +12,7 @@ export async function handleJokeRemove( if (!interaction.inGuild() || !interaction.member) { await interaction.reply({ content: "This command can only be used in a server.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -23,7 +24,7 @@ export async function handleJokeRemove( if (typeof member === "string") { await interaction.reply({ content: "Could not verify your permissions.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -38,7 +39,7 @@ export async function handleJokeRemove( if (!hasModRole) { await interaction.reply({ content: "❌ Only joke moderators can remove jokes.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -48,7 +49,7 @@ export async function handleJokeRemove( if (!joke) { await interaction.reply({ content: `Joke #${jokeId} not found.`, - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -63,12 +64,12 @@ export async function handleJokeRemove( `Category: ${joke.category}`, `Text: ${joke.joke_text.substring(0, 100)}${joke.joke_text.length > 100 ? "..." : ""}`, ].join("\n"), - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } else { await interaction.reply({ content: `Failed to remove joke #${jokeId}.`, - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } } diff --git a/src/commands/fun/subcommands/poll.ts b/src/commands/fun/subcommands/poll.ts index d48eedc9..4dc07962 100644 --- a/src/commands/fun/subcommands/poll.ts +++ b/src/commands/fun/subcommands/poll.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/fun/subcommands/poll.ts import { @@ -146,7 +147,10 @@ export async function run(interaction: ChatInputCommandInteraction): Promise= latestPoll.options.length ) { - await btn.reply({ content: "Invalid poll option.", ephemeral: true }); + await btn.reply({ + content: "Invalid poll option.", + flags: MessageFlags.Ephemeral, + }); return; } @@ -161,7 +165,7 @@ export async function run(interaction: ChatInputCommandInteraction): Promise { const sub = interaction.options.getSubcommand(true); - await interaction.deferReply({ ephemeral: true }); + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); try { if (sub === "set") return await handleSet(interaction); diff --git a/src/services/discord/interactionHandler.ts b/src/services/discord/interactionHandler.ts index c3fb2f1e..e28e779f 100644 --- a/src/services/discord/interactionHandler.ts +++ b/src/services/discord/interactionHandler.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/services/discord/interactionHandler.ts // // Central interaction router for Discord. @@ -69,7 +70,7 @@ export async function handleInteraction( await safeReply(interaction, { content: "Command not found.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -98,7 +99,7 @@ export async function handleInteraction( await safeReply(interaction, { content: "Something went wrong while running this command.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } } diff --git a/src/services/discord/safeReply.ts b/src/services/discord/safeReply.ts index c525b85e..5c13e4c2 100644 --- a/src/services/discord/safeReply.ts +++ b/src/services/discord/safeReply.ts @@ -1,48 +1,49 @@ // src/services/discord/safeReply.ts -// -// Safe reply helper for Discord interactions. -// -// Goal: -// - Avoid "Interaction already replied" and "Unknown interaction" pitfalls -// - Let callers write one consistent "respond" call -// -// Behavior: -// - If interaction already has a reply or was deferred, use editReply(content) -// - Otherwise use reply({ content, flags? }) -// -// Notes: -// - Discord does not allow setting Ephemeral on editReply. -// So we only apply ephemeral flags on the initial reply. - -import { +import type { + ChatInputCommandInteraction, + InteractionEditReplyOptions, + InteractionReplyOptions, MessageFlags, - type InteractionReplyOptions, - type RepliableInteraction, } from "discord.js"; -export type SafeReplyOptions = { +export interface SafeReplyOptions { content: string; + flags?: MessageFlags; ephemeral?: boolean; -}; - -function toReplyOptions(opts: SafeReplyOptions): InteractionReplyOptions { - if (opts.ephemeral) { - return { content: opts.content, flags: MessageFlags.Ephemeral }; - } - return { content: opts.content }; } /** - * Safely respond to an interaction exactly once. + * Safely reply to an interaction, handling already-replied cases. */ export async function safeReply( - interaction: RepliableInteraction, - opts: SafeReplyOptions, + interaction: ChatInputCommandInteraction, + options: SafeReplyOptions, ): Promise { - if (interaction.replied || interaction.deferred) { - await interaction.editReply(opts.content); - return; + const replyOptions: InteractionReplyOptions = { + content: options.content, + }; + + // Add flags or ephemeral - prefer flags + if (options.flags !== undefined) { + replyOptions.flags = options.flags as number; // Cast to number for bitfield + } else if (options.ephemeral) { + replyOptions.ephemeral = options.ephemeral; } - await interaction.reply(toReplyOptions(opts)); + try { + if (interaction.replied || interaction.deferred) { + const editOptions: InteractionEditReplyOptions = { + content: options.content, + }; + await interaction.editReply(editOptions); + } else { + await interaction.reply(replyOptions); + } + } catch (error) { + try { + await interaction.followUp(replyOptions); + } catch { + // Give up + } + } } diff --git a/src/utils/colors.ts b/src/utils/colors.ts new file mode 100644 index 00000000..4bb8dfb3 --- /dev/null +++ b/src/utils/colors.ts @@ -0,0 +1,20 @@ +// src/utils/colors.ts +// Consistent embed colors across the bot + +export const EmbedColors = { + // Primary colors + Success: 0x00ae86, // Green - successful operations + Error: 0xff0000, // Red - errors and failures + Warning: 0xffa500, // Orange - warnings + Info: 0x3498db, // Blue - informational + + // Feature-specific colors + GitHub: 0x6e5494, // GitHub purple + Fun: 0xffd700, // Gold for fun commands + Moderation: 0xe74c3c, // Red for mod actions + + // Utility + Default: 0x00ae86, // Default if unsure +} as const; + +export type EmbedColor = (typeof EmbedColors)[keyof typeof EmbedColors]; diff --git a/src/utils/interactions.ts b/src/utils/interactions.ts new file mode 100644 index 00000000..259b59d4 --- /dev/null +++ b/src/utils/interactions.ts @@ -0,0 +1,57 @@ +// src/utils/interactions.ts +// Helper utilities for interaction handling + +import type { ChatInputCommandInteraction, EmbedBuilder } from "discord.js"; + +/** + * Helper for commands that might take a while. + * Shows "thinking..." state, then edits with result. + */ +export async function deferredReply( + interaction: ChatInputCommandInteraction, + handler: () => Promise, +): Promise { + await interaction.deferReply(); + + try { + const result = await handler(); + + if (typeof result === "string") { + await interaction.editReply({ content: result }); + } else { + await interaction.editReply(result); + } + } catch (error) { + await interaction.editReply({ + content: "❌ An error occurred while processing your request.", + }); + throw error; + } +} + +/** + * Standard error reply format + */ +export async function errorReply( + interaction: ChatInputCommandInteraction, + message: string, + suggestions?: string[], +): Promise { + const parts = [`❌ ${message}`]; + + if (suggestions && suggestions.length > 0) { + parts.push(""); + parts.push("💡 **Suggestions:**"); + suggestions.forEach((s) => parts.push(`• ${s}`)); + } + + const replied = interaction.replied || interaction.deferred; + + if (replied) { + await interaction.editReply({ content: parts.join("\n") }); + } else { + await interaction.reply({ + content: parts.join("\n"), + }); + } +} From b2e53a17cb46210f5a4835fcf46a66d65bd0fdcc Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 14:14:13 -0500 Subject: [PATCH 02/10] Fixing this up --- src/services/discord/safeReply.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/services/discord/safeReply.ts b/src/services/discord/safeReply.ts index 5c13e4c2..2ad917b4 100644 --- a/src/services/discord/safeReply.ts +++ b/src/services/discord/safeReply.ts @@ -1,10 +1,11 @@ // src/services/discord/safeReply.ts -import type { - ChatInputCommandInteraction, +import type { + ChatInputCommandInteraction, InteractionEditReplyOptions, InteractionReplyOptions, - MessageFlags, + MessageFlags } from "discord.js"; +import { logger } from "../../utils/logger.js"; export interface SafeReplyOptions { content: string; @@ -25,7 +26,7 @@ export async function safeReply( // Add flags or ephemeral - prefer flags if (options.flags !== undefined) { - replyOptions.flags = options.flags as number; // Cast to number for bitfield + replyOptions.flags = options.flags as number; // Cast to number for bitfield } else if (options.ephemeral) { replyOptions.ephemeral = options.ephemeral; } @@ -40,10 +41,12 @@ export async function safeReply( await interaction.reply(replyOptions); } } catch (error) { + logger.warn({ error, commandName: interaction.commandName }, "Failed to reply to interaction, trying followUp"); + // If reply fails, try followUp as last resort try { await interaction.followUp(replyOptions); - } catch { - // Give up + } catch (followUpError) { + logger.error({ error: followUpError, commandName: interaction.commandName }, "Failed to followUp on interaction - interaction may be expired"); } } -} +} \ No newline at end of file From 0c052393faee57c9a9f18f1431b14fc14e430188 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 14:14:20 -0500 Subject: [PATCH 03/10] style: auto-format with Prettier [skip-precheck] --- src/services/discord/safeReply.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/services/discord/safeReply.ts b/src/services/discord/safeReply.ts index 2ad917b4..0f58b308 100644 --- a/src/services/discord/safeReply.ts +++ b/src/services/discord/safeReply.ts @@ -1,9 +1,9 @@ // src/services/discord/safeReply.ts -import type { - ChatInputCommandInteraction, +import type { + ChatInputCommandInteraction, InteractionEditReplyOptions, InteractionReplyOptions, - MessageFlags + MessageFlags, } from "discord.js"; import { logger } from "../../utils/logger.js"; @@ -26,7 +26,7 @@ export async function safeReply( // Add flags or ephemeral - prefer flags if (options.flags !== undefined) { - replyOptions.flags = options.flags as number; // Cast to number for bitfield + replyOptions.flags = options.flags as number; // Cast to number for bitfield } else if (options.ephemeral) { replyOptions.ephemeral = options.ephemeral; } @@ -41,12 +41,18 @@ export async function safeReply( await interaction.reply(replyOptions); } } catch (error) { - logger.warn({ error, commandName: interaction.commandName }, "Failed to reply to interaction, trying followUp"); + logger.warn( + { error, commandName: interaction.commandName }, + "Failed to reply to interaction, trying followUp", + ); // If reply fails, try followUp as last resort try { await interaction.followUp(replyOptions); } catch (followUpError) { - logger.error({ error: followUpError, commandName: interaction.commandName }, "Failed to followUp on interaction - interaction may be expired"); + logger.error( + { error: followUpError, commandName: interaction.commandName }, + "Failed to followUp on interaction - interaction may be expired", + ); } } -} \ No newline at end of file +} From 37389275a28d4f5197e9abf3e0cd8622158d47e2 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 14:16:33 -0500 Subject: [PATCH 04/10] Trying this again --- src/services/joke/jokeStore.ts | 178 ++++++++++++++++++++------------- 1 file changed, 111 insertions(+), 67 deletions(-) diff --git a/src/services/joke/jokeStore.ts b/src/services/joke/jokeStore.ts index 6cd548fc..048a2019 100644 --- a/src/services/joke/jokeStore.ts +++ b/src/services/joke/jokeStore.ts @@ -1,5 +1,6 @@ // src/services/joke/jokeStore.ts import { getDb } from "../database/db.js"; +import { logger } from "../../utils/logger.js"; export type JokeCategory = | "boomer" @@ -44,102 +45,145 @@ export interface Joke { export function addJoke(jokeText: string, category: JokeCategory, userId: string): Joke { const db = getDb(); - const result = db - .prepare( - ` - INSERT INTO jokes (joke_text, category, added_by, added_at, usage_count) - VALUES (?, ?, ?, ?, 0) - `, - ) - .run(jokeText, category, userId, Date.now()); - - return { - id: result.lastInsertRowid as number, - joke_text: jokeText, - category, - added_by: userId, - added_at: Date.now(), - usage_count: 0, - }; + try { + const result = db + .prepare( + ` + INSERT INTO jokes (joke_text, category, added_by, added_at, usage_count) + VALUES (?, ?, ?, ?, 0) + `, + ) + .run(jokeText, category, userId, Date.now()); + + const joke: Joke = { + id: result.lastInsertRowid as number, + joke_text: jokeText, + category, + added_by: userId, + added_at: Date.now(), + usage_count: 0, + }; + + logger.info({ jokeId: joke.id, category, userId }, "Joke added"); + return joke; + } catch (error) { + logger.error({ error, category, userId }, "Failed to add joke"); + throw error; + } } export function getRandomJoke(category?: JokeCategory): Joke | null { const db = getDb(); - let query = "SELECT * FROM jokes"; - const params: (string | number)[] = []; + try { + let query = "SELECT * FROM jokes"; + const params: (string | number)[] = []; - if (category && category !== "random") { - query += " WHERE category = ?"; - params.push(category); - } + if (category && category !== "random") { + query += " WHERE category = ?"; + params.push(category); + } - query += " ORDER BY RANDOM() LIMIT 1"; + query += " ORDER BY RANDOM() LIMIT 1"; - const joke = db.prepare(query).get(...params) as Joke | undefined; + const joke = db.prepare(query).get(...params) as Joke | undefined; - if (joke) { - // Increment usage count - db.prepare("UPDATE jokes SET usage_count = usage_count + 1 WHERE id = ?").run( - joke.id, - ); - } + if (joke) { + // Increment usage count + db.prepare("UPDATE jokes SET usage_count = usage_count + 1 WHERE id = ?").run( + joke.id, + ); + } - return joke || null; + return joke || null; + } catch (error) { + logger.error({ error, category }, "Failed to get random joke"); + throw error; + } } export function removeJoke(jokeId: number): boolean { const db = getDb(); - const result = db.prepare("DELETE FROM jokes WHERE id = ?").run(jokeId); - return result.changes > 0; + + try { + const result = db.prepare("DELETE FROM jokes WHERE id = ?").run(jokeId); + const success = result.changes > 0; + + if (success) { + logger.info({ jokeId }, "Joke removed"); + } else { + logger.warn({ jokeId }, "Attempted to remove non-existent joke"); + } + + return success; + } catch (error) { + logger.error({ error, jokeId }, "Failed to remove joke"); + throw error; + } } export function getJoke(jokeId: number): Joke | null { const db = getDb(); - return db.prepare("SELECT * FROM jokes WHERE id = ?").get(jokeId) as Joke | null; + + try { + return db.prepare("SELECT * FROM jokes WHERE id = ?").get(jokeId) as Joke | null; + } catch (error) { + logger.error({ error, jokeId }, "Failed to get joke"); + throw error; + } } export function listJokes(category?: JokeCategory, limit: number = 50): Joke[] { const db = getDb(); - let query = "SELECT * FROM jokes"; - const params: (string | number)[] = []; + try { + let query = "SELECT * FROM jokes"; + const params: (string | number)[] = []; - if (category && category !== "random") { - query += " WHERE category = ?"; - params.push(category); - } + if (category && category !== "random") { + query += " WHERE category = ?"; + params.push(category); + } - query += " ORDER BY added_at DESC LIMIT ?"; - params.push(limit); + query += " ORDER BY added_at DESC LIMIT ?"; + params.push(limit); - return db.prepare(query).all(...params) as Joke[]; + return db.prepare(query).all(...params) as Joke[]; + } catch (error) { + logger.error({ error, category, limit }, "Failed to list jokes"); + throw error; + } } export function getJokeStats(): { total: number; byCategory: Record } { const db = getDb(); - const total = db.prepare("SELECT COUNT(*) as count FROM jokes").get() as { - count: number; - }; - - const byCategory = db - .prepare( - ` - SELECT category, COUNT(*) as count - FROM jokes - GROUP BY category - `, - ) - .all() as { category: string; count: number }[]; - - const categoryMap: Record = {}; - byCategory.forEach((row) => { - categoryMap[row.category] = row.count; - }); - - return { - total: total.count, - byCategory: categoryMap, - }; + try { + const total = db.prepare("SELECT COUNT(*) as count FROM jokes").get() as { + count: number; + }; + + const byCategory = db + .prepare( + ` + SELECT category, COUNT(*) as count + FROM jokes + GROUP BY category + `, + ) + .all() as { category: string; count: number }[]; + + const categoryMap: Record = {}; + byCategory.forEach((row) => { + categoryMap[row.category] = row.count; + }); + + return { + total: total.count, + byCategory: categoryMap, + }; + } catch (error) { + logger.error({ error }, "Failed to get joke stats"); + throw error; + } } From b3217e6f1e3e2d037f32d1a39d2204f6f2ab843f Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 14:53:25 -0500 Subject: [PATCH 05/10] Trying this again --- .env.example | 31 +-- README.md | 70 +++--- docs/commands.md | 52 +++- install-claude-admin.sh | 404 +++++++++++++++++++++++++++++++ src/commands/admin/admin.ts | 194 +++++++++++++++ src/services/ai/claudeService.ts | 119 +++++++++ 6 files changed, 811 insertions(+), 59 deletions(-) create mode 100755 install-claude-admin.sh create mode 100644 src/commands/admin/admin.ts create mode 100644 src/services/ai/claudeService.ts diff --git a/.env.example b/.env.example index 0566847f..490a0f0d 100644 --- a/.env.example +++ b/.env.example @@ -85,26 +85,14 @@ DISCORD_AUTO_ROLE_ID= JOKE_MODERATOR_ROLE_ID= # ============================================================ -# Conversation Summaries (OPTIONAL) +# AI Services (OPTIONAL) # ============================================================ -# Summary execution mode determines how /summary command works: -# -# Options: -# - local → Fast heuristic summaries (no API needed, always available) -# - llm → High-quality AI summaries using OpenAI (requires API key) -# -# Default: local -SUMMARY_MODE=local - -# OpenAI API key for LLM-powered summaries -# Get it from: https://platform.openai.com/api-keys -# -# Only required if SUMMARY_MODE=llm -# Cost: ~$0.01-0.05 per summary (depends on message count) -# -# If unset and SUMMARY_MODE=llm, bot falls back to local summaries -OPENAI_API_KEY= +# Anthropic API key for Claude AI +# Get it from: https://console.anthropic.com/ +# Used for: /gh history, /gh summary +# Cost: ~$0.003 per summary (much cheaper than ChatGPT!) +ANTHROPIC_API_KEY= # ============================================================ # Weather (OPTIONAL) @@ -245,3 +233,10 @@ LOG_PRETTY=true # # Default: data/omegabot.db DATABASE_PATH=data/omegabot.db + +# ============================================================ +# Claude AI (Optional) +# ============================================================ +# Get your API key from: https://console.anthropic.com/ +# Used for: /gh history, /gh summary (AI-powered summaries) +ANTHROPIC_API_KEY= diff --git a/README.md b/README.md index 63b6c763..1d75b1e0 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ OmegaBot is a modular Discord bot designed to support development projects with ## Table of Contents -- [Features](#features) +- [Core Features](#core-features) - [Documentation](#documentation) - [Getting Started](#getting-started) - [Project Structure](#project-structure) @@ -28,24 +28,32 @@ OmegaBot is a modular Discord bot designed to support development projects with --- -## Current features - -- Modular slash-command system with auto-loading from `dist/commands` -- Centralized interaction routing with consistent, safe error handling -- Structured logging (pino) -- Welcome and onboarding flows triggered on member join (`guildMemberAdd`) -- Optional auto-role assignment for new members (`DISCORD_AUTO_ROLE_ID`) -- GitHub integration with 5-minute API caching (80-90% reduction in API calls), including: - - Health/status checks - - Issue and PR lookups - - New PR announcements - - Issue assignee change announcements (Issues only, PRs filtered for reduced noise) - - Issue closed announcements - - Smart notification filtering (only Issues trigger activity updates) -- Configuration and feature gating via environment variables (optional features run only when enabled) -- Per-guild configuration backed by persistent storage and admin slash commands -- **Timezone support** – Save your timezone, view it later, and compare times across locations or users -- **SQLite-backed storage** – Persistent data for FAQs, jokes, fun stats, timezones, and GitHub state +## Core Features + +**Discord Integration:** + +- Modular slash-command system with auto-loading +- Welcome messages and auto-role assignment for new members +- Structured logging (pino) and safe error handling + +**GitHub Integration:** + +- Issue and PR lookups with smart caching (80-90% fewer API calls) +- Automated announcements for PRs, issue activity, and closures +- Health checks and status monitoring + +**Community Features:** + +- User-submitted jokes with 13 categories and moderation +- Coin flips, dice rolls, weather, and polls +- FAQ system for server knowledge base +- Timezone management for coordination across time zones + +**Data & Configuration:** + +- SQLite database for persistent storage +- Environment-based configuration with feature gating +- Per-guild settings via admin commands ### Core commands @@ -226,24 +234,6 @@ OmegaBot uses discord.js v14 which includes: │ │ └── OmegaBot.yml │ └── pull_request_template.md ├── .husky -│ ├── _ -│ │ ├── .gitignore -│ │ ├── applypatch-msg -│ │ ├── commit-msg -│ │ ├── h -│ │ ├── husky.sh -│ │ ├── post-applypatch -│ │ ├── post-checkout -│ │ ├── post-commit -│ │ ├── post-merge -│ │ ├── post-rewrite -│ │ ├── pre-applypatch -│ │ ├── pre-auto-gc -│ │ ├── pre-commit -│ │ ├── pre-merge-commit -│ │ ├── pre-push -│ │ ├── pre-rebase -│ │ └── prepare-commit-msg │ ├── pre-commit │ └── pre-push ├── assets @@ -257,6 +247,7 @@ OmegaBot uses discord.js v14 which includes: │ ├── last-seen.json │ └── omegabot.db ├── docs +│ ├── Claude.dmg │ ├── commands.md │ ├── dev-notes.md │ ├── faq.md @@ -267,6 +258,8 @@ OmegaBot uses discord.js v14 which includes: │ └── precheck.sh ├── src │ ├── commands +│ │ ├── admin +│ │ │ └── admin.ts │ │ ├── changelog │ │ │ └── changelog.ts │ │ ├── config @@ -314,6 +307,8 @@ OmegaBot uses discord.js v14 which includes: │ ├── config │ │ └── env.ts │ ├── services +│ │ ├── ai +│ │ │ └── claudeService.ts │ │ ├── cache │ │ │ └── simpleCache.ts │ │ ├── config @@ -391,6 +386,7 @@ OmegaBot uses discord.js v14 which includes: ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── eslint.config.ts +├── install-claude-admin.sh ├── LICENSE ├── package-lock.json ├── package.json diff --git a/docs/commands.md b/docs/commands.md index 6c230c36..a75f3690 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -14,6 +14,49 @@ Verify the bot is online and measure latency. --- +## Admin Commands + +**Permissions:** Requires Administrator permission in Discord. + +Server administration and monitoring commands for bot owners and admins. + +### `/admin stats` + +Show comprehensive bot statistics. + +**Displays:** + +- **Uptime:** Days, hours, minutes since last restart +- **Memory:** Current memory usage (heap used/total) +- **Total Commands:** All fun command executions +- **Database Stats:** Joke count, coin flip count +- **Unique Users:** Number of users who've used fun commands +- **Node Version:** Current Node.js runtime version + +**Use case:** Monitor bot health and usage at a glance. + +--- + +### `/admin health` + +Run health checks on bot services and configuration. + +**Checks:** + +- **Database:** SQLite connection and query execution +- **Environment Variables:** Required Discord/GitHub tokens +- **API Keys:** Optional service availability (Anthropic, Weather) + +**Statuses:** + +- ✅ Healthy/Configured +- ⚠️ Missing/Not configured +- ❌ Error (with details) + +**Use case:** Troubleshoot bot issues and verify configuration. + +--- + ## GitHub Integration Commands for interacting with GitHub repositories. Requires GitHub integration to be configured. @@ -97,7 +140,7 @@ Get a random joke from the database. **Options:** -- `category` (optional) — Filter by generation: boomer, genx, millennial, genz, genalpha, random +- `category` (optional) — Filter by category: boomer, genx, millennial, genz, genalpha, random, tech, dark, wholesome, anti, puns, observational, dad **Use case:** Brighten your day with community humor. @@ -110,7 +153,7 @@ Add a new joke to the database. **Options:** - `text` (required) — The joke text (max 1000 characters) -- `category` (required) — Generation category: boomer, genx, millennial, genz, genalpha, random +- `category` (required) — Category: boomer, genx, millennial, genz, genalpha, random, tech, dark, wholesome, anti, puns, observational, dad **Use case:** Share your favorite jokes with the server. @@ -136,7 +179,7 @@ Browse recent jokes. **Options:** -- `category` (optional) — Filter by generation +- `category` (optional) — Filter by category **Displays:** Last 10 jokes with ID, category, and usage count. @@ -471,7 +514,8 @@ Search FAQ entries by keyword. - **Ephemeral Options:** Many commands support `ephemeral: true` to show results only to you - **Usage Tracking:** Fun commands automatically track usage for leaderboards - **Permissions:** Some commands require specific roles (check server configuration) -- **API Keys:** Weather and LLM features require API keys in `.env` +- **API Keys:** Weather and AI features require API keys in `.env` - **GitHub Integration:** GitHub commands require repository configuration +- **Admin Commands:** Only users with Administrator permission can use `/admin` commands For configuration details, see `.env.example` in the repository. diff --git a/install-claude-admin.sh b/install-claude-admin.sh new file mode 100755 index 00000000..a91feb96 --- /dev/null +++ b/install-claude-admin.sh @@ -0,0 +1,404 @@ +#!/bin/bash + +set -e + +echo "🚀 Installing Claude API & Admin Commands" +echo "==========================================" +echo "" + +if [ ! -f "package.json" ]; then + echo "❌ Run from OmegaBot root" + exit 1 +fi + +# ============================================================ +# Step 1: Install Anthropic SDK +# ============================================================ +echo "1. Installing @anthropic-ai/sdk..." +npm install @anthropic-ai/sdk + +# ============================================================ +# Step 2: Create Claude Service +# ============================================================ +echo "" +echo "2. Creating Claude API service..." + +mkdir -p src/services/ai + +cat > src/services/ai/claudeService.ts << 'CLAUDEEOF' +// src/services/ai/claudeService.ts +import Anthropic from "@anthropic-ai/sdk"; +import { logger } from "../../utils/logger.js"; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +export interface ClaudeOptions { + systemPrompt?: string; + maxTokens?: number; + temperature?: number; +} + +/** + * Call Claude API for text generation + */ +export async function callClaude( + prompt: string, + options: ClaudeOptions = {} +): Promise { + const { + systemPrompt = "You are a helpful assistant.", + maxTokens = 4096, + temperature = 1.0, + } = options; + + try { + logger.info({ promptLength: prompt.length }, "Calling Claude API"); + + const message = await anthropic.messages.create({ + model: "claude-sonnet-4-20250514", + max_tokens: maxTokens, + temperature, + system: systemPrompt, + messages: [ + { + role: "user", + content: prompt, + }, + ], + }); + + // Extract text from response + const textContent = message.content.find((block) => block.type === "text"); + + if (!textContent || textContent.type !== "text") { + throw new Error("No text content in Claude response"); + } + + logger.info( + { + inputTokens: message.usage.input_tokens, + outputTokens: message.usage.output_tokens, + }, + "Claude API call successful" + ); + + return textContent.text; + } catch (error) { + logger.error({ error }, "Claude API call failed"); + throw new Error("Failed to get response from Claude"); + } +} + +/** + * Summarize GitHub commit history + */ +export async function summarizeCommitHistory(commits: string): Promise { + const systemPrompt = \`You are a helpful assistant that summarizes git commit history. +Be concise and focus on the most important changes. +Group related commits together and highlight breaking changes or major features.\`; + + const prompt = \`Summarize the following commit history. Focus on major changes, features, and fixes: + +\${commits} + +Provide a clear, organized summary in bullet points.\`; + + return callClaude(prompt, { + systemPrompt, + maxTokens: 2000, + temperature: 0.7, + }); +} + +/** + * Summarize GitHub PR/issue discussion + */ +export async function summarizeDiscussion( + title: string, + body: string, + comments: string +): Promise { + const systemPrompt = \`You are a helpful assistant that summarizes GitHub discussions. +Be objective and highlight key decisions, concerns, and action items.\`; + + const prompt = \`Summarize this GitHub discussion: + +**Title:** \${title} + +**Description:** +\${body} + +**Comments:** +\${comments} + +Provide a concise summary covering: +1. Main topic/purpose +2. Key points discussed +3. Decisions made (if any) +4. Action items or next steps\`; + + return callClaude(prompt, { + systemPrompt, + maxTokens: 2000, + temperature: 0.7, + }); +} +CLAUDEEOF + +echo "✓ Created src/services/ai/claudeService.ts" + +# ============================================================ +# Step 3: Create Admin Command +# ============================================================ +echo "" +echo "3. Creating admin command..." + +mkdir -p src/commands/admin + +cat > src/commands/admin/admin.ts << 'ADMINEOF' +// src/commands/admin/admin.ts +import { + SlashCommandBuilder, + EmbedBuilder, + PermissionFlagsBits, + type ChatInputCommandInteraction, +} from "discord.js"; +import { logger } from "../../utils/logger.js"; +import { getDb } from "../../services/database/db.js"; +import { getFunUsageSnapshot } from "../../services/fun/funUsageStore.js"; +import { EmbedColors } from "../../utils/colors.js"; + +export const data = new SlashCommandBuilder() + .setName("admin") + .setDescription("Admin commands") + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand((sub) => + sub + .setName("stats") + .setDescription("Show bot statistics (uptime, database, commands)") + ) + .addSubcommand((sub) => + sub.setName("health").setDescription("Check bot and service health") + ); + +export async function execute( + interaction: ChatInputCommandInteraction +): Promise { + const subcommand = interaction.options.getSubcommand(); + + await interaction.deferReply(); + + try { + if (subcommand === "stats") { + await handleStats(interaction); + } else if (subcommand === "health") { + await handleHealth(interaction); + } else { + await interaction.editReply("Unknown subcommand"); + } + } catch (error) { + logger.error({ error, subcommand }, "[admin] Command failed"); + await interaction.editReply("❌ Something went wrong"); + } +} + +async function handleStats( + interaction: ChatInputCommandInteraction +): Promise { + const db = getDb(); + + // Get database stats + const jokeCount = db.prepare("SELECT COUNT(*) as count FROM jokes").get() as { + count: number; + }; + + const coinFlipCount = db + .prepare("SELECT COUNT(*) as count FROM coin_flips") + .get() as { count: number }; + + // Get fun command usage + const funUsage = await getFunUsageSnapshot(); + const totalCommands = Object.values(funUsage.totalsByCommand).reduce( + (sum, count) => sum + count, + 0 + ); + + // Calculate uptime + const uptimeSeconds = process.uptime(); + const uptimeDays = Math.floor(uptimeSeconds / 86400); + const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600); + const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60); + + // Memory usage + const memUsage = process.memoryUsage(); + const memUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024); + const memTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024); + + const embed = new EmbedBuilder() + .setTitle("🤖 Bot Statistics") + .setColor(EmbedColors.Info) + .addFields( + { + name: "⏱️ Uptime", + value: \`\${uptimeDays}d \${uptimeHours}h \${uptimeMinutes}m\`, + inline: true, + }, + { + name: "💾 Memory", + value: \`\${memUsedMB}MB / \${memTotalMB}MB\`, + inline: true, + }, + { + name: "📊 Total Commands", + value: totalCommands.toString(), + inline: true, + }, + { + name: "🎭 Jokes", + value: jokeCount.count.toString(), + inline: true, + }, + { + name: "🪙 Coin Flips", + value: coinFlipCount.count.toString(), + inline: true, + }, + { + name: "👥 Unique Users", + value: Object.keys(funUsage.totalsByUser).length.toString(), + inline: true, + } + ) + .setFooter({ text: \`Node \${process.version}\` }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + + logger.info( + { userId: interaction.user.id }, + "Admin viewed bot statistics" + ); +} + +async function handleHealth( + interaction: ChatInputCommandInteraction +): Promise { + const checks: { name: string; status: string; details?: string }[] = []; + + // Check database + try { + const db = getDb(); + db.prepare("SELECT 1").get(); + checks.push({ name: "Database", status: "✅ Healthy" }); + } catch (error) { + checks.push({ + name: "Database", + status: "❌ Error", + details: error instanceof Error ? error.message : "Unknown error", + }); + } + + // Check environment variables + const requiredEnvVars = [ + "DISCORD_TOKEN", + "DISCORD_APP_ID", + "GITHUB_TOKEN", + "GITHUB_REPO_OWNER", + "GITHUB_REPO_NAME", + ]; + + const missingVars = requiredEnvVars.filter((v) => !process.env[v]); + if (missingVars.length === 0) { + checks.push({ name: "Environment", status: "✅ All vars set" }); + } else { + checks.push({ + name: "Environment", + status: "⚠️ Missing vars", + details: missingVars.join(", "), + }); + } + + // Check API keys + const optionalKeys = [ + { name: "Anthropic API", key: "ANTHROPIC_API_KEY" }, + { name: "Weather API", key: "WEATHER_API_KEY" }, + ]; + + optionalKeys.forEach(({ name, key }) => { + if (process.env[key]) { + checks.push({ name, status: "✅ Configured" }); + } else { + checks.push({ name, status: "⚠️ Not configured" }); + } + }); + + const embed = new EmbedBuilder() + .setTitle("🏥 Health Check") + .setColor(EmbedColors.Info) + .setDescription( + checks + .map((c) => + c.details + ? \`**\${c.name}:** \${c.status}\\n \${c.details}\` + : \`**\${c.name}:** \${c.status}\` + ) + .join("\\n\\n") + ) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + + logger.info({ userId: interaction.user.id }, "Admin viewed health check"); +} +ADMINEOF + +echo "✓ Created src/commands/admin/admin.ts" + +# ============================================================ +# Step 4: Update .env.example +# ============================================================ +echo "" +echo "4. Updating .env.example..." + +if ! grep -q "ANTHROPIC_API_KEY" .env.example 2>/dev/null; then + cat >> .env.example << 'ENVEOF' + +# ============================================================ +# Claude AI (Optional) +# ============================================================ +# Get your API key from: https://console.anthropic.com/ +# Used for: /gh history, /gh summary (AI-powered summaries) +ANTHROPIC_API_KEY= +ENVEOF + echo "✓ Added ANTHROPIC_API_KEY to .env.example" +else + echo "✓ ANTHROPIC_API_KEY already in .env.example" +fi + +# ============================================================ +# Done! +# ============================================================ +echo "" +echo "╔════════════════════════════════════════════════════════╗" +echo "║ ✅ Installation Complete! ║" +echo "╚════════════════════════════════════════════════════════╝" +echo "" +echo "📋 Next Steps:" +echo "" +echo "1. Add your Anthropic API key to .env:" +echo " ANTHROPIC_API_KEY=sk-ant-..." +echo " Get it from: https://console.anthropic.com/" +echo "" +echo "2. Build and register:" +echo " npm run build" +echo " npm run register" +echo " npm start" +echo "" +echo "3. Try the new commands:" +echo " /admin stats - Bot statistics" +echo " /admin health - Health check" +echo "" +echo "📝 Next: Update /gh history and /gh summary to use Claude" +echo "" diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts new file mode 100644 index 00000000..8069b735 --- /dev/null +++ b/src/commands/admin/admin.ts @@ -0,0 +1,194 @@ +// src/commands/admin/admin.ts +import { + SlashCommandBuilder, + EmbedBuilder, + PermissionFlagsBits, + type ChatInputCommandInteraction, +} from "discord.js"; +import { logger } from "../../utils/logger.js"; +import { getDb } from "../../services/database/db.js"; +import { getFunUsageSnapshot } from "../../services/fun/funUsageStore.js"; +import { EmbedColors } from "../../utils/colors.js"; + +export const data = new SlashCommandBuilder() + .setName("admin") + .setDescription("Admin commands") + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand((sub) => + sub + .setName("stats") + .setDescription("Show bot statistics (uptime, database, commands)") + ) + .addSubcommand((sub) => + sub.setName("health").setDescription("Check bot and service health") + ); + +export async function execute( + interaction: ChatInputCommandInteraction +): Promise { + const subcommand = interaction.options.getSubcommand(); + + await interaction.deferReply(); + + try { + if (subcommand === "stats") { + await handleStats(interaction); + } else if (subcommand === "health") { + await handleHealth(interaction); + } else { + await interaction.editReply("Unknown subcommand"); + } + } catch (error) { + logger.error({ error, subcommand }, "[admin] Command failed"); + await interaction.editReply("❌ Something went wrong"); + } +} + +async function handleStats( + interaction: ChatInputCommandInteraction +): Promise { + const db = getDb(); + + // Get database stats + const jokeCount = db.prepare("SELECT COUNT(*) as count FROM jokes").get() as { + count: number; + }; + + const coinFlipCount = db + .prepare("SELECT COUNT(*) as count FROM coin_flips") + .get() as { count: number }; + + // Get fun command usage + const funUsage = await getFunUsageSnapshot(); + const totalCommands = Object.values(funUsage.totalsByCommand).reduce( + (sum, count) => sum + count, + 0 + ); + + // Calculate uptime + const uptimeSeconds = process.uptime(); + const uptimeDays = Math.floor(uptimeSeconds / 86400); + const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600); + const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60); + + // Memory usage + const memUsage = process.memoryUsage(); + const memUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024); + const memTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024); + + const embed = new EmbedBuilder() + .setTitle("🤖 Bot Statistics") + .setColor(EmbedColors.Info) + .addFields( + { + name: "⏱️ Uptime", + value: \`\${uptimeDays}d \${uptimeHours}h \${uptimeMinutes}m\`, + inline: true, + }, + { + name: "💾 Memory", + value: \`\${memUsedMB}MB / \${memTotalMB}MB\`, + inline: true, + }, + { + name: "📊 Total Commands", + value: totalCommands.toString(), + inline: true, + }, + { + name: "🎭 Jokes", + value: jokeCount.count.toString(), + inline: true, + }, + { + name: "🪙 Coin Flips", + value: coinFlipCount.count.toString(), + inline: true, + }, + { + name: "👥 Unique Users", + value: Object.keys(funUsage.totalsByUser).length.toString(), + inline: true, + } + ) + .setFooter({ text: \`Node \${process.version}\` }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + + logger.info( + { userId: interaction.user.id }, + "Admin viewed bot statistics" + ); +} + +async function handleHealth( + interaction: ChatInputCommandInteraction +): Promise { + const checks: { name: string; status: string; details?: string }[] = []; + + // Check database + try { + const db = getDb(); + db.prepare("SELECT 1").get(); + checks.push({ name: "Database", status: "✅ Healthy" }); + } catch (error) { + checks.push({ + name: "Database", + status: "❌ Error", + details: error instanceof Error ? error.message : "Unknown error", + }); + } + + // Check environment variables + const requiredEnvVars = [ + "DISCORD_TOKEN", + "DISCORD_APP_ID", + "GITHUB_TOKEN", + "GITHUB_REPO_OWNER", + "GITHUB_REPO_NAME", + ]; + + const missingVars = requiredEnvVars.filter((v) => !process.env[v]); + if (missingVars.length === 0) { + checks.push({ name: "Environment", status: "✅ All vars set" }); + } else { + checks.push({ + name: "Environment", + status: "⚠️ Missing vars", + details: missingVars.join(", "), + }); + } + + // Check API keys + const optionalKeys = [ + { name: "Anthropic API", key: "ANTHROPIC_API_KEY" }, + { name: "Weather API", key: "WEATHER_API_KEY" }, + ]; + + optionalKeys.forEach(({ name, key }) => { + if (process.env[key]) { + checks.push({ name, status: "✅ Configured" }); + } else { + checks.push({ name, status: "⚠️ Not configured" }); + } + }); + + const embed = new EmbedBuilder() + .setTitle("🏥 Health Check") + .setColor(EmbedColors.Info) + .setDescription( + checks + .map((c) => + c.details + ? \`**\${c.name}:** \${c.status}\\n \${c.details}\` + : \`**\${c.name}:** \${c.status}\` + ) + .join("\\n\\n") + ) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + + logger.info({ userId: interaction.user.id }, "Admin viewed health check"); +} diff --git a/src/services/ai/claudeService.ts b/src/services/ai/claudeService.ts new file mode 100644 index 00000000..0ccb26de --- /dev/null +++ b/src/services/ai/claudeService.ts @@ -0,0 +1,119 @@ +// src/services/ai/claudeService.ts +import Anthropic from "@anthropic-ai/sdk"; +import { logger } from "../../utils/logger.js"; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +export interface ClaudeOptions { + systemPrompt?: string; + maxTokens?: number; + temperature?: number; +} + +/** + * Call Claude API for text generation + */ +export async function callClaude( + prompt: string, + options: ClaudeOptions = {} +): Promise { + const { + systemPrompt = "You are a helpful assistant.", + maxTokens = 4096, + temperature = 1.0, + } = options; + + try { + logger.info({ promptLength: prompt.length }, "Calling Claude API"); + + const message = await anthropic.messages.create({ + model: "claude-sonnet-4-20250514", + max_tokens: maxTokens, + temperature, + system: systemPrompt, + messages: [ + { + role: "user", + content: prompt, + }, + ], + }); + + // Extract text from response + const textContent = message.content.find((block) => block.type === "text"); + + if (!textContent || textContent.type !== "text") { + throw new Error("No text content in Claude response"); + } + + logger.info( + { + inputTokens: message.usage.input_tokens, + outputTokens: message.usage.output_tokens, + }, + "Claude API call successful" + ); + + return textContent.text; + } catch (error) { + logger.error({ error }, "Claude API call failed"); + throw new Error("Failed to get response from Claude"); + } +} + +/** + * Summarize GitHub commit history + */ +export async function summarizeCommitHistory(commits: string): Promise { + const systemPrompt = \`You are a helpful assistant that summarizes git commit history. +Be concise and focus on the most important changes. +Group related commits together and highlight breaking changes or major features.\`; + + const prompt = \`Summarize the following commit history. Focus on major changes, features, and fixes: + +\${commits} + +Provide a clear, organized summary in bullet points.\`; + + return callClaude(prompt, { + systemPrompt, + maxTokens: 2000, + temperature: 0.7, + }); +} + +/** + * Summarize GitHub PR/issue discussion + */ +export async function summarizeDiscussion( + title: string, + body: string, + comments: string +): Promise { + const systemPrompt = \`You are a helpful assistant that summarizes GitHub discussions. +Be objective and highlight key decisions, concerns, and action items.\`; + + const prompt = \`Summarize this GitHub discussion: + +**Title:** \${title} + +**Description:** +\${body} + +**Comments:** +\${comments} + +Provide a concise summary covering: +1. Main topic/purpose +2. Key points discussed +3. Decisions made (if any) +4. Action items or next steps\`; + + return callClaude(prompt, { + systemPrompt, + maxTokens: 2000, + temperature: 0.7, + }); +} From 407ee12c2d7ca1996a95971610c5a2725fb0b82e Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 14:55:22 -0500 Subject: [PATCH 06/10] Fixing things up --- docs/commands.md | 53 +--- install-claude-admin.sh | 404 ------------------------------- src/commands/admin/admin.ts | 10 +- src/services/ai/claudeService.ts | 16 +- 4 files changed, 17 insertions(+), 466 deletions(-) delete mode 100755 install-claude-admin.sh diff --git a/docs/commands.md b/docs/commands.md index a75f3690..e60fd7ee 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -14,49 +14,6 @@ Verify the bot is online and measure latency. --- -## Admin Commands - -**Permissions:** Requires Administrator permission in Discord. - -Server administration and monitoring commands for bot owners and admins. - -### `/admin stats` - -Show comprehensive bot statistics. - -**Displays:** - -- **Uptime:** Days, hours, minutes since last restart -- **Memory:** Current memory usage (heap used/total) -- **Total Commands:** All fun command executions -- **Database Stats:** Joke count, coin flip count -- **Unique Users:** Number of users who've used fun commands -- **Node Version:** Current Node.js runtime version - -**Use case:** Monitor bot health and usage at a glance. - ---- - -### `/admin health` - -Run health checks on bot services and configuration. - -**Checks:** - -- **Database:** SQLite connection and query execution -- **Environment Variables:** Required Discord/GitHub tokens -- **API Keys:** Optional service availability (Anthropic, Weather) - -**Statuses:** - -- ✅ Healthy/Configured -- ⚠️ Missing/Not configured -- ❌ Error (with details) - -**Use case:** Troubleshoot bot issues and verify configuration. - ---- - ## GitHub Integration Commands for interacting with GitHub repositories. Requires GitHub integration to be configured. @@ -140,7 +97,7 @@ Get a random joke from the database. **Options:** -- `category` (optional) — Filter by category: boomer, genx, millennial, genz, genalpha, random, tech, dark, wholesome, anti, puns, observational, dad +- `category` (optional) — Filter by generation: boomer, genx, millennial, genz, genalpha, random **Use case:** Brighten your day with community humor. @@ -153,7 +110,7 @@ Add a new joke to the database. **Options:** - `text` (required) — The joke text (max 1000 characters) -- `category` (required) — Category: boomer, genx, millennial, genz, genalpha, random, tech, dark, wholesome, anti, puns, observational, dad +- `category` (required) — Generation category: boomer, genx, millennial, genz, genalpha, random **Use case:** Share your favorite jokes with the server. @@ -179,7 +136,7 @@ Browse recent jokes. **Options:** -- `category` (optional) — Filter by category +- `category` (optional) — Filter by generation **Displays:** Last 10 jokes with ID, category, and usage count. @@ -511,11 +468,9 @@ Search FAQ entries by keyword. ## Notes -- **Ephemeral Options:** Many commands support `ephemeral: true` to show results only to you - **Usage Tracking:** Fun commands automatically track usage for leaderboards - **Permissions:** Some commands require specific roles (check server configuration) -- **API Keys:** Weather and AI features require API keys in `.env` +- **API Keys:** Weather and LLM features require API keys in `.env` - **GitHub Integration:** GitHub commands require repository configuration -- **Admin Commands:** Only users with Administrator permission can use `/admin` commands For configuration details, see `.env.example` in the repository. diff --git a/install-claude-admin.sh b/install-claude-admin.sh deleted file mode 100755 index a91feb96..00000000 --- a/install-claude-admin.sh +++ /dev/null @@ -1,404 +0,0 @@ -#!/bin/bash - -set -e - -echo "🚀 Installing Claude API & Admin Commands" -echo "==========================================" -echo "" - -if [ ! -f "package.json" ]; then - echo "❌ Run from OmegaBot root" - exit 1 -fi - -# ============================================================ -# Step 1: Install Anthropic SDK -# ============================================================ -echo "1. Installing @anthropic-ai/sdk..." -npm install @anthropic-ai/sdk - -# ============================================================ -# Step 2: Create Claude Service -# ============================================================ -echo "" -echo "2. Creating Claude API service..." - -mkdir -p src/services/ai - -cat > src/services/ai/claudeService.ts << 'CLAUDEEOF' -// src/services/ai/claudeService.ts -import Anthropic from "@anthropic-ai/sdk"; -import { logger } from "../../utils/logger.js"; - -const anthropic = new Anthropic({ - apiKey: process.env.ANTHROPIC_API_KEY, -}); - -export interface ClaudeOptions { - systemPrompt?: string; - maxTokens?: number; - temperature?: number; -} - -/** - * Call Claude API for text generation - */ -export async function callClaude( - prompt: string, - options: ClaudeOptions = {} -): Promise { - const { - systemPrompt = "You are a helpful assistant.", - maxTokens = 4096, - temperature = 1.0, - } = options; - - try { - logger.info({ promptLength: prompt.length }, "Calling Claude API"); - - const message = await anthropic.messages.create({ - model: "claude-sonnet-4-20250514", - max_tokens: maxTokens, - temperature, - system: systemPrompt, - messages: [ - { - role: "user", - content: prompt, - }, - ], - }); - - // Extract text from response - const textContent = message.content.find((block) => block.type === "text"); - - if (!textContent || textContent.type !== "text") { - throw new Error("No text content in Claude response"); - } - - logger.info( - { - inputTokens: message.usage.input_tokens, - outputTokens: message.usage.output_tokens, - }, - "Claude API call successful" - ); - - return textContent.text; - } catch (error) { - logger.error({ error }, "Claude API call failed"); - throw new Error("Failed to get response from Claude"); - } -} - -/** - * Summarize GitHub commit history - */ -export async function summarizeCommitHistory(commits: string): Promise { - const systemPrompt = \`You are a helpful assistant that summarizes git commit history. -Be concise and focus on the most important changes. -Group related commits together and highlight breaking changes or major features.\`; - - const prompt = \`Summarize the following commit history. Focus on major changes, features, and fixes: - -\${commits} - -Provide a clear, organized summary in bullet points.\`; - - return callClaude(prompt, { - systemPrompt, - maxTokens: 2000, - temperature: 0.7, - }); -} - -/** - * Summarize GitHub PR/issue discussion - */ -export async function summarizeDiscussion( - title: string, - body: string, - comments: string -): Promise { - const systemPrompt = \`You are a helpful assistant that summarizes GitHub discussions. -Be objective and highlight key decisions, concerns, and action items.\`; - - const prompt = \`Summarize this GitHub discussion: - -**Title:** \${title} - -**Description:** -\${body} - -**Comments:** -\${comments} - -Provide a concise summary covering: -1. Main topic/purpose -2. Key points discussed -3. Decisions made (if any) -4. Action items or next steps\`; - - return callClaude(prompt, { - systemPrompt, - maxTokens: 2000, - temperature: 0.7, - }); -} -CLAUDEEOF - -echo "✓ Created src/services/ai/claudeService.ts" - -# ============================================================ -# Step 3: Create Admin Command -# ============================================================ -echo "" -echo "3. Creating admin command..." - -mkdir -p src/commands/admin - -cat > src/commands/admin/admin.ts << 'ADMINEOF' -// src/commands/admin/admin.ts -import { - SlashCommandBuilder, - EmbedBuilder, - PermissionFlagsBits, - type ChatInputCommandInteraction, -} from "discord.js"; -import { logger } from "../../utils/logger.js"; -import { getDb } from "../../services/database/db.js"; -import { getFunUsageSnapshot } from "../../services/fun/funUsageStore.js"; -import { EmbedColors } from "../../utils/colors.js"; - -export const data = new SlashCommandBuilder() - .setName("admin") - .setDescription("Admin commands") - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .addSubcommand((sub) => - sub - .setName("stats") - .setDescription("Show bot statistics (uptime, database, commands)") - ) - .addSubcommand((sub) => - sub.setName("health").setDescription("Check bot and service health") - ); - -export async function execute( - interaction: ChatInputCommandInteraction -): Promise { - const subcommand = interaction.options.getSubcommand(); - - await interaction.deferReply(); - - try { - if (subcommand === "stats") { - await handleStats(interaction); - } else if (subcommand === "health") { - await handleHealth(interaction); - } else { - await interaction.editReply("Unknown subcommand"); - } - } catch (error) { - logger.error({ error, subcommand }, "[admin] Command failed"); - await interaction.editReply("❌ Something went wrong"); - } -} - -async function handleStats( - interaction: ChatInputCommandInteraction -): Promise { - const db = getDb(); - - // Get database stats - const jokeCount = db.prepare("SELECT COUNT(*) as count FROM jokes").get() as { - count: number; - }; - - const coinFlipCount = db - .prepare("SELECT COUNT(*) as count FROM coin_flips") - .get() as { count: number }; - - // Get fun command usage - const funUsage = await getFunUsageSnapshot(); - const totalCommands = Object.values(funUsage.totalsByCommand).reduce( - (sum, count) => sum + count, - 0 - ); - - // Calculate uptime - const uptimeSeconds = process.uptime(); - const uptimeDays = Math.floor(uptimeSeconds / 86400); - const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600); - const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60); - - // Memory usage - const memUsage = process.memoryUsage(); - const memUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024); - const memTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024); - - const embed = new EmbedBuilder() - .setTitle("🤖 Bot Statistics") - .setColor(EmbedColors.Info) - .addFields( - { - name: "⏱️ Uptime", - value: \`\${uptimeDays}d \${uptimeHours}h \${uptimeMinutes}m\`, - inline: true, - }, - { - name: "💾 Memory", - value: \`\${memUsedMB}MB / \${memTotalMB}MB\`, - inline: true, - }, - { - name: "📊 Total Commands", - value: totalCommands.toString(), - inline: true, - }, - { - name: "🎭 Jokes", - value: jokeCount.count.toString(), - inline: true, - }, - { - name: "🪙 Coin Flips", - value: coinFlipCount.count.toString(), - inline: true, - }, - { - name: "👥 Unique Users", - value: Object.keys(funUsage.totalsByUser).length.toString(), - inline: true, - } - ) - .setFooter({ text: \`Node \${process.version}\` }) - .setTimestamp(); - - await interaction.editReply({ embeds: [embed] }); - - logger.info( - { userId: interaction.user.id }, - "Admin viewed bot statistics" - ); -} - -async function handleHealth( - interaction: ChatInputCommandInteraction -): Promise { - const checks: { name: string; status: string; details?: string }[] = []; - - // Check database - try { - const db = getDb(); - db.prepare("SELECT 1").get(); - checks.push({ name: "Database", status: "✅ Healthy" }); - } catch (error) { - checks.push({ - name: "Database", - status: "❌ Error", - details: error instanceof Error ? error.message : "Unknown error", - }); - } - - // Check environment variables - const requiredEnvVars = [ - "DISCORD_TOKEN", - "DISCORD_APP_ID", - "GITHUB_TOKEN", - "GITHUB_REPO_OWNER", - "GITHUB_REPO_NAME", - ]; - - const missingVars = requiredEnvVars.filter((v) => !process.env[v]); - if (missingVars.length === 0) { - checks.push({ name: "Environment", status: "✅ All vars set" }); - } else { - checks.push({ - name: "Environment", - status: "⚠️ Missing vars", - details: missingVars.join(", "), - }); - } - - // Check API keys - const optionalKeys = [ - { name: "Anthropic API", key: "ANTHROPIC_API_KEY" }, - { name: "Weather API", key: "WEATHER_API_KEY" }, - ]; - - optionalKeys.forEach(({ name, key }) => { - if (process.env[key]) { - checks.push({ name, status: "✅ Configured" }); - } else { - checks.push({ name, status: "⚠️ Not configured" }); - } - }); - - const embed = new EmbedBuilder() - .setTitle("🏥 Health Check") - .setColor(EmbedColors.Info) - .setDescription( - checks - .map((c) => - c.details - ? \`**\${c.name}:** \${c.status}\\n \${c.details}\` - : \`**\${c.name}:** \${c.status}\` - ) - .join("\\n\\n") - ) - .setTimestamp(); - - await interaction.editReply({ embeds: [embed] }); - - logger.info({ userId: interaction.user.id }, "Admin viewed health check"); -} -ADMINEOF - -echo "✓ Created src/commands/admin/admin.ts" - -# ============================================================ -# Step 4: Update .env.example -# ============================================================ -echo "" -echo "4. Updating .env.example..." - -if ! grep -q "ANTHROPIC_API_KEY" .env.example 2>/dev/null; then - cat >> .env.example << 'ENVEOF' - -# ============================================================ -# Claude AI (Optional) -# ============================================================ -# Get your API key from: https://console.anthropic.com/ -# Used for: /gh history, /gh summary (AI-powered summaries) -ANTHROPIC_API_KEY= -ENVEOF - echo "✓ Added ANTHROPIC_API_KEY to .env.example" -else - echo "✓ ANTHROPIC_API_KEY already in .env.example" -fi - -# ============================================================ -# Done! -# ============================================================ -echo "" -echo "╔════════════════════════════════════════════════════════╗" -echo "║ ✅ Installation Complete! ║" -echo "╚════════════════════════════════════════════════════════╝" -echo "" -echo "📋 Next Steps:" -echo "" -echo "1. Add your Anthropic API key to .env:" -echo " ANTHROPIC_API_KEY=sk-ant-..." -echo " Get it from: https://console.anthropic.com/" -echo "" -echo "2. Build and register:" -echo " npm run build" -echo " npm run register" -echo " npm start" -echo "" -echo "3. Try the new commands:" -echo " /admin stats - Bot statistics" -echo " /admin health - Health check" -echo "" -echo "📝 Next: Update /gh history and /gh summary to use Claude" -echo "" diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index 8069b735..3646b649 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -82,12 +82,12 @@ async function handleStats( .addFields( { name: "⏱️ Uptime", - value: \`\${uptimeDays}d \${uptimeHours}h \${uptimeMinutes}m\`, + value: `\${uptimeDays}d \${uptimeHours}h \${uptimeMinutes}m`, inline: true, }, { name: "💾 Memory", - value: \`\${memUsedMB}MB / \${memTotalMB}MB\`, + value: `\${memUsedMB}MB / \${memTotalMB}MB`, inline: true, }, { @@ -111,7 +111,7 @@ async function handleStats( inline: true, } ) - .setFooter({ text: \`Node \${process.version}\` }) + .setFooter({ text: `Node \${process.version}` }) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); @@ -181,8 +181,8 @@ async function handleHealth( checks .map((c) => c.details - ? \`**\${c.name}:** \${c.status}\\n \${c.details}\` - : \`**\${c.name}:** \${c.status}\` + ? `**\${c.name}:** \${c.status}\\n \${c.details}` + : `**\${c.name}:** \${c.status}` ) .join("\\n\\n") ) diff --git a/src/services/ai/claudeService.ts b/src/services/ai/claudeService.ts index 0ccb26de..2dc68e00 100644 --- a/src/services/ai/claudeService.ts +++ b/src/services/ai/claudeService.ts @@ -67,15 +67,15 @@ export async function callClaude( * Summarize GitHub commit history */ export async function summarizeCommitHistory(commits: string): Promise { - const systemPrompt = \`You are a helpful assistant that summarizes git commit history. + const systemPrompt = `You are a helpful assistant that summarizes git commit history. Be concise and focus on the most important changes. -Group related commits together and highlight breaking changes or major features.\`; +Group related commits together and highlight breaking changes or major features.`; - const prompt = \`Summarize the following commit history. Focus on major changes, features, and fixes: + const prompt = `Summarize the following commit history. Focus on major changes, features, and fixes: \${commits} -Provide a clear, organized summary in bullet points.\`; +Provide a clear, organized summary in bullet points.`; return callClaude(prompt, { systemPrompt, @@ -92,10 +92,10 @@ export async function summarizeDiscussion( body: string, comments: string ): Promise { - const systemPrompt = \`You are a helpful assistant that summarizes GitHub discussions. -Be objective and highlight key decisions, concerns, and action items.\`; + const systemPrompt = `You are a helpful assistant that summarizes GitHub discussions. +Be objective and highlight key decisions, concerns, and action items.`; - const prompt = \`Summarize this GitHub discussion: + const prompt = `Summarize this GitHub discussion: **Title:** \${title} @@ -109,7 +109,7 @@ Provide a concise summary covering: 1. Main topic/purpose 2. Key points discussed 3. Decisions made (if any) -4. Action items or next steps\`; +4. Action items or next steps`; return callClaude(prompt, { systemPrompt, From a1fadf8218c64c4c174824df72af52033483dcb2 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 14:55:53 -0500 Subject: [PATCH 07/10] Adding in files --- src/commands/admin/admin.ts | 35 ++++++++++++-------------------- src/services/ai/claudeService.ts | 8 ++++---- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index 3646b649..6d5bfbb0 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -17,15 +17,13 @@ export const data = new SlashCommandBuilder() .addSubcommand((sub) => sub .setName("stats") - .setDescription("Show bot statistics (uptime, database, commands)") + .setDescription("Show bot statistics (uptime, database, commands)"), ) .addSubcommand((sub) => - sub.setName("health").setDescription("Check bot and service health") + sub.setName("health").setDescription("Check bot and service health"), ); -export async function execute( - interaction: ChatInputCommandInteraction -): Promise { +export async function execute(interaction: ChatInputCommandInteraction): Promise { const subcommand = interaction.options.getSubcommand(); await interaction.deferReply(); @@ -44,9 +42,7 @@ export async function execute( } } -async function handleStats( - interaction: ChatInputCommandInteraction -): Promise { +async function handleStats(interaction: ChatInputCommandInteraction): Promise { const db = getDb(); // Get database stats @@ -54,15 +50,15 @@ async function handleStats( count: number; }; - const coinFlipCount = db - .prepare("SELECT COUNT(*) as count FROM coin_flips") - .get() as { count: number }; + const coinFlipCount = db.prepare("SELECT COUNT(*) as count FROM coin_flips").get() as { + count: number; + }; // Get fun command usage const funUsage = await getFunUsageSnapshot(); const totalCommands = Object.values(funUsage.totalsByCommand).reduce( (sum, count) => sum + count, - 0 + 0, ); // Calculate uptime @@ -109,22 +105,17 @@ async function handleStats( name: "👥 Unique Users", value: Object.keys(funUsage.totalsByUser).length.toString(), inline: true, - } + }, ) .setFooter({ text: `Node \${process.version}` }) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); - logger.info( - { userId: interaction.user.id }, - "Admin viewed bot statistics" - ); + logger.info({ userId: interaction.user.id }, "Admin viewed bot statistics"); } -async function handleHealth( - interaction: ChatInputCommandInteraction -): Promise { +async function handleHealth(interaction: ChatInputCommandInteraction): Promise { const checks: { name: string; status: string; details?: string }[] = []; // Check database @@ -182,9 +173,9 @@ async function handleHealth( .map((c) => c.details ? `**\${c.name}:** \${c.status}\\n \${c.details}` - : `**\${c.name}:** \${c.status}` + : `**\${c.name}:** \${c.status}`, ) - .join("\\n\\n") + .join("\\n\\n"), ) .setTimestamp(); diff --git a/src/services/ai/claudeService.ts b/src/services/ai/claudeService.ts index 2dc68e00..15dd4c98 100644 --- a/src/services/ai/claudeService.ts +++ b/src/services/ai/claudeService.ts @@ -17,7 +17,7 @@ export interface ClaudeOptions { */ export async function callClaude( prompt: string, - options: ClaudeOptions = {} + options: ClaudeOptions = {}, ): Promise { const { systemPrompt = "You are a helpful assistant.", @@ -43,7 +43,7 @@ export async function callClaude( // Extract text from response const textContent = message.content.find((block) => block.type === "text"); - + if (!textContent || textContent.type !== "text") { throw new Error("No text content in Claude response"); } @@ -53,7 +53,7 @@ export async function callClaude( inputTokens: message.usage.input_tokens, outputTokens: message.usage.output_tokens, }, - "Claude API call successful" + "Claude API call successful", ); return textContent.text; @@ -90,7 +90,7 @@ Provide a clear, organized summary in bullet points.`; export async function summarizeDiscussion( title: string, body: string, - comments: string + comments: string, ): Promise { const systemPrompt = `You are a helpful assistant that summarizes GitHub discussions. Be objective and highlight key decisions, concerns, and action items.`; From 0c6deecf7e57483022cc38e34ffaf44712e89ea8 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 14:58:09 -0500 Subject: [PATCH 08/10] Adding in files --- src/commands/admin/admin.ts | 12 ++++++------ src/services/ai/claudeService.ts | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index 6d5bfbb0..1cac921f 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -78,12 +78,12 @@ async function handleStats(interaction: ChatInputCommandInteraction): Promise c.details - ? `**\${c.name}:** \${c.status}\\n \${c.details}` - : `**\${c.name}:** \${c.status}`, + ? `**${c.name}:** ${c.status}\n ${c.details}` + : `**${c.name}:** ${c.status}`, ) - .join("\\n\\n"), + .join("\n\n"), ) .setTimestamp(); diff --git a/src/services/ai/claudeService.ts b/src/services/ai/claudeService.ts index 15dd4c98..7734904b 100644 --- a/src/services/ai/claudeService.ts +++ b/src/services/ai/claudeService.ts @@ -73,7 +73,7 @@ Group related commits together and highlight breaking changes or major features. const prompt = `Summarize the following commit history. Focus on major changes, features, and fixes: -\${commits} +${commits} Provide a clear, organized summary in bullet points.`; @@ -97,13 +97,13 @@ Be objective and highlight key decisions, concerns, and action items.`; const prompt = `Summarize this GitHub discussion: -**Title:** \${title} +**Title:** ${title} **Description:** -\${body} +${body} **Comments:** -\${comments} +${comments} Provide a concise summary covering: 1. Main topic/purpose From 9e3d84edb3c75cebb0fd32be45978e8ea3c8d759 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 15:04:07 -0500 Subject: [PATCH 09/10] Adding in files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8f0890c9..856069e1 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ build/ # OS junk Thumbs.db +*.dmg # TypeScript (if added later) *.tsbuildinfo From 93f653051051e5267c4e4bc14190c3571693e20e Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 15:04:53 -0500 Subject: [PATCH 10/10] Adding in files --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 856069e1..d2950b17 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,7 @@ build/ # OS junk Thumbs.db -*.dmg +Claude.dmg # TypeScript (if added later) *.tsbuildinfo