Logging changes, module updates.

This commit is contained in:
jrmyr 2025-05-02 16:45:36 +00:00
parent f4996cafd2
commit 0aa1180dc8
24 changed files with 405 additions and 212 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
node_modules node_modules
.env .env
.nvmrc
images/* images/*
logs/* logs/*
pocketbase/* pocketbase/*

View File

@ -9,7 +9,7 @@ export const commands = [
{ {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('exit') .setName('exit')
.setDescription('Gracefully shutdown the bot (owner only)') .setDescription('Gracefully shutdown the bot (Owner only)')
// Restrict to server administrators by default // Restrict to server administrators by default
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.setDMPermission(false) .setDMPermission(false)
@ -39,13 +39,13 @@ export const commands = [
// Acknowledge before shutting down // Acknowledge before shutting down
await interaction.reply({ content: `Shutting down with exit code ${exitCode}...`, ephemeral: true }); await interaction.reply({ content: `Shutting down with exit code ${exitCode}...`, ephemeral: true });
client.logger.info( client.logger.info(
`Shutdown initiated by owner ${interaction.user.tag} (${interaction.user.id}), exit code ${exitCode}` `[cmd:exit] Shutdown initiated by owner ${interaction.user.tag} (${interaction.user.id}), exit code ${exitCode}`
); );
// Destroy Discord client and exit process // Destroy Discord client and exit process
try { try {
await client.destroy(); await client.destroy();
} catch (err) { } catch (err) {
client.logger.error(`Error during client.destroy(): ${err}`); client.logger.error(`[cmd:exit] Error during client.destroy(): ${err}`);
} }
process.exit(exitCode); process.exit(exitCode);
} }
@ -99,6 +99,7 @@ export const commands = [
`Modules : ${loadedModules.length > 0 ? loadedModules.join(', ') : 'None'}` `Modules : ${loadedModules.length > 0 ? loadedModules.join(', ') : 'None'}`
]; ];
await interaction.editReply({ content: '```\n' + lines.join('\n') + '\n```' }); await interaction.editReply({ content: '```\n' + lines.join('\n') + '\n```' });
client.logger.info(`[cmd:status] Returned status for client ${client.config.id}`);
} }
} }
]; ];
@ -116,7 +117,7 @@ const botUtilsClients = [];
* @param {Object} clientConfig * @param {Object} clientConfig
*/ */
export async function init(client, clientConfig) { export async function init(client, clientConfig) {
client.logger.info('botUtils module loaded'); client.logger.info('[module:botUtils] Module loaded');
// Track this client instance and its config // Track this client instance and its config
botUtilsClients.push({ client, clientConfig }); botUtilsClients.push({ client, clientConfig });
} }

View File

@ -362,11 +362,11 @@ export const init = async (client, config) => {
// Deferred setup on ready // Deferred setup on ready
const readyHandler = async () => { const readyHandler = async () => {
client.logger.info('Initializing CondimentX module'); client.logger.info('[module:condimentX] Initializing module');
if (openAI === true) { if (openAI === true) {
openai = new OpenAI({ apiKey: openAIToken }); openai = new OpenAI({ apiKey: openAIToken }); // credentials loaded
openAIWebhook = await client.fetchWebhook(openAIWebhookID, openAIWebhookToken).catch(error => { openAIWebhook = await client.fetchWebhook(openAIWebhookID, openAIWebhookToken).catch(error => {
client.logger.error(`Could not fetch webhook: ${error.message}`); client.logger.error(`[module:condimentX] Could not fetch webhook: ${error.message}`);
return null; return null;
}); });
if (openAIWebhook) openAIWebhookClient = new WebhookClient({ id: openAIWebhookID, token: openAIWebhookToken }); if (openAIWebhook) openAIWebhookClient = new WebhookClient({ id: openAIWebhookID, token: openAIWebhookToken });
@ -374,7 +374,7 @@ export const init = async (client, config) => {
try { try {
guild = client.guilds.cache.get(guildID); guild = client.guilds.cache.get(guildID);
if (!guild) { if (!guild) {
client.logger.error(`CondimentX error: Guild ${guildID} not found`); client.logger.error(`[module:condimentX] Guild ${guildID} not found`);
return; return;
} }
indexRole = await guild.roles.fetch(indexRoleID); indexRole = await guild.roles.fetch(indexRoleID);

View File

@ -45,7 +45,7 @@ export const commands = [
{ {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('git') .setName('git')
.setDescription('Run an arbitrary git command (owner only)') .setDescription('Run an arbitrary git command (Owner only)')
.addStringOption(opt => .addStringOption(opt =>
opt.setName('args') opt.setName('args')
.setDescription('Arguments to pass to git') .setDescription('Arguments to pass to git')
@ -71,7 +71,7 @@ export const commands = [
try { try {
// Log the exact git command being executed // Log the exact git command being executed
const cmdStr = args.join(' '); const cmdStr = args.join(' ');
client.logger.warn(`Executing git command: git ${cmdStr}`); client.logger.warn(`[cmd:git] Executing git command: git ${cmdStr}`);
const output = await runGit(args); const output = await runGit(args);
// Prepend the git command as a header; keep it intact when chunking // Prepend the git command as a header; keep it intact when chunking
const header = `git ${cmdStr}\n`; const header = `git ${cmdStr}\n`;
@ -98,5 +98,5 @@ export const commands = [
// No special init logic // No special init logic
export async function init(client) { export async function init(client) {
client.logger.warn('Git utilities module loaded - dangerous module, use with caution'); client.logger.warn('[module:gitUtils] Git utilities module loaded - dangerous module, use with caution');
} }

View File

@ -5,7 +5,7 @@ import { onMessageQueueEvent } from './pbUtils.js';
* Example module that listens for 'test' messages in the message_queue collection. * Example module that listens for 'test' messages in the message_queue collection.
*/ */
export const init = async (client, config) => { export const init = async (client, config) => {
client.logger.info('Initializing Message Queue Example module'); client.logger.info('[module:messageQueueExample] Initializing Message Queue Example module');
onMessageQueueEvent(client, async (action, record) => { onMessageQueueEvent(client, async (action, record) => {
// Only process newly created records // Only process newly created records
if (action !== 'create') return; if (action !== 'create') return;
@ -15,14 +15,14 @@ export const init = async (client, config) => {
if (record.dataType !== 'test') return; if (record.dataType !== 'test') return;
// At this point we have a test message for us // At this point we have a test message for us
client.logger.info('test received'); client.logger.info('[module:messageQueueExample] Test message received');
// Delete the processed message from the queue // Delete the processed message from the queue
try { try {
await client.pb.deleteMessageQueue(record.id); await client.pb.deleteMessageQueue(record.id);
client.logger.debug(`Deleted message_queue record ${record.id}`); client.logger.debug(`[module:messageQueueExample] Deleted message_queue record ${record.id}`);
} catch (err) { } catch (err) {
client.logger.error(`Failed to delete message_queue record ${record.id}: ${err.message}`); client.logger.error(`[module:messageQueueExample] Failed to delete message_queue record ${record.id}: ${err.message}`);
} }
}); });
}; };

View File

@ -19,7 +19,7 @@ if (typeof global.EventSource === 'undefined') {
export const init = async (client, config) => { export const init = async (client, config) => {
const { pb, logger } = client; const { pb, logger } = client;
logger.info('Initializing PocketBase utilities module'); logger.info('[module:pbUtils] Initializing PocketBase utilities module');
// Attach utility methods to the pb object // Attach utility methods to the pb object
extendPocketBase(client, pb, logger); extendPocketBase(client, pb, logger);
@ -33,9 +33,9 @@ export const init = async (client, config) => {
client.emit('message_queue_event', e.action, e.record); client.emit('message_queue_event', e.action, e.record);
logger.debug(`PubSub event: ${e.action} on message_queue: ${JSON.stringify(e.record)}`); logger.debug(`PubSub event: ${e.action} on message_queue: ${JSON.stringify(e.record)}`);
}); });
logger.info('Subscribed to PocketBase message_queue realtime events'); logger.info('[module:pbUtils] Subscribed to PocketBase message_queue realtime events');
} catch (error) { } catch (error) {
logger.error(`Failed to subscribe to message_queue realtime: ${error.message}`); logger.error(`[module:pbUtils] Failed to subscribe to message_queue realtime: ${error.message}`);
} }
// end of init() // end of init()
@ -54,7 +54,7 @@ export function onMessageQueueEvent(client, handler) {
try { try {
handler(action, record); handler(action, record);
} catch (err) { } catch (err) {
client.logger.error(`Error in message_queue handler: ${err.message}`); client.logger.error(`[module:pbUtils] Error in message_queue handler: ${err.message}`);
} }
}); });
} }

View File

@ -3,8 +3,7 @@
* Listens to message events, sends chat queries to the OpenAI Responses API, * Listens to message events, sends chat queries to the OpenAI Responses API,
* and handles text or image (function_call) outputs. * and handles text or image (function_call) outputs.
*/ */
import fs from 'fs/promises'; // Removed local file fallback; prompt now comes exclusively from PocketBase via responsesPrompt module
import path from 'path';
import { OpenAI } from 'openai'; import { OpenAI } from 'openai';
import axios from 'axios'; import axios from 'axios';
import { AttachmentBuilder, PermissionFlagsBits } from 'discord.js'; import { AttachmentBuilder, PermissionFlagsBits } from 'discord.js';
@ -70,22 +69,6 @@ function splitMessage(text, maxLength = MAX_DISCORD_MSG_LENGTH) {
return chunks.map(c => c.endsWith('\n') ? c.slice(0, -1) : c); return chunks.map(c => c.endsWith('\n') ? c.slice(0, -1) : c);
} }
/**
* Load AI system prompt text from a file.
* @param {string} filePath - Path to the prompt file.
* @param {object} logger - Logger instance for reporting.
* @returns {Promise<string>} Promise resolving to the prompt text or empty string.
*/
async function loadSystemPrompt(filePath, logger) {
try {
const prompt = await fs.readFile(path.resolve(filePath), 'utf8');
logger.info(`Loaded system prompt: ${filePath}`);
return prompt;
} catch (err) {
logger.error(`Failed to load system prompt: ${err.message}`);
return '';
}
}
/** /**
* Determine whether the bot should respond to a message. * Determine whether the bot should respond to a message.
@ -256,6 +239,7 @@ async function handleImage(client, message, resp, cfg) {
async function onMessage(client, cfg, message) { async function onMessage(client, cfg, message) {
const logger = client.logger; const logger = client.logger;
const botId = client.user?.id; const botId = client.user?.id;
client.logger.debug(`[onMessage] Received message ${message.id} from ${message.author.id}`);
if (!(await shouldRespond(message, botId, logger))) return; if (!(await shouldRespond(message, botId, logger))) return;
await message.channel.sendTyping(); await message.channel.sendTyping();
@ -290,7 +274,7 @@ async function onMessage(client, cfg, message) {
const speakerMention = `<@${message.author.id}>`; const speakerMention = `<@${message.author.id}>`;
const body = { const body = {
model: cfg.defaultModel, model: cfg.defaultModel,
instructions: client.responsesSystemPrompt, instructions: client.responsesPrompt,
input: `${speakerMention} said to you: ${message.content}`, input: `${speakerMention} said to you: ${message.content}`,
previous_response_id: prev, previous_response_id: prev,
max_output_tokens: cfg.defaultMaxTokens, max_output_tokens: cfg.defaultMaxTokens,
@ -392,7 +376,7 @@ async function onMessage(client, cfg, message) {
} }
}; };
// Chain the handler to the last promise // Chain the handler to the last promise
const next = last.then(handler).catch(err => logger.error(err)); const next = last.then(handler).catch(err => logger.error(`[onMessage] Handler error: ${err.message}`));
lockMap.set(key, next); lockMap.set(key, next);
// Queue enqueued; handler will send response when its turn arrives // Queue enqueued; handler will send response when its turn arrives
return; return;
@ -442,7 +426,7 @@ export async function sendNarrative(client, cfg, channelId, text) {
const logger = client.logger; const logger = client.logger;
try { try {
// Build the narrative instructions // Build the narrative instructions
const instructions = `${client.responsesSystemPrompt}\n\nGenerate the following as an engaging narrative:`; const instructions = `${client.responsesPrompt}\n\nGenerate the following as an engaging narrative:`;
const body = { const body = {
model: cfg.defaultModel, model: cfg.defaultModel,
instructions, instructions,
@ -450,13 +434,13 @@ export async function sendNarrative(client, cfg, channelId, text) {
max_output_tokens: cfg.defaultMaxTokens, max_output_tokens: cfg.defaultMaxTokens,
temperature: cfg.defaultTemperature, temperature: cfg.defaultTemperature,
}; };
logger.debug('sendNarrative: calling AI with body', body); logger.debug(`[sendNarrative] Calling AI with body: ${JSON.stringify(body).slice(0,1000)}`);
const resp = await client.openai.responses.create(body); const resp = await client.openai.responses.create(body);
logger.info(`sendNarrative AI response id=${resp.id}`); logger.info(`[sendNarrative] Received AI response id=${resp.id}`);
// Fetch the target channel or thread // Fetch the target channel or thread
const channel = await client.channels.fetch(channelId); const channel = await client.channels.fetch(channelId);
if (!channel || typeof channel.send !== 'function') { if (!channel || typeof channel.send !== 'function') {
logger.error(`sendNarrative: cannot send to channel ID ${channelId}`); logger.error(`[sendNarrative] Cannot send to channel ID ${channelId}`);
return; return;
} }
// Split the output and send // Split the output and send
@ -468,7 +452,7 @@ export async function sendNarrative(client, cfg, channelId, text) {
} }
} }
} catch (err) { } catch (err) {
client.logger.error(`sendNarrative error: ${err.message}`); client.logger.error(`[sendNarrative] Error: ${err.message}`);
} }
} }
@ -482,9 +466,10 @@ export async function sendNarrative(client, cfg, channelId, text) {
*/ */
export async function init(client, clientConfig) { export async function init(client, clientConfig) {
const cfg = clientConfig.responses; const cfg = clientConfig.responses;
client.logger.info('Initializing Responses module'); client.logger.info('[module:responses] Initializing Responses module');
client.responsesSystemPrompt = await loadSystemPrompt(cfg.systemPromptPath, client.logger); // Initialize prompt from responsesPrompt module (must be loaded before this)
client.responsesPrompt = client.responsesPrompt ?? '';
client.openai = new OpenAI({ apiKey: cfg.apiKey }); client.openai = new OpenAI({ apiKey: cfg.apiKey });
client.on('messageCreate', m => onMessage(client, cfg, m)); client.on('messageCreate', m => onMessage(client, cfg, m));
client.logger.info('Responses module ready'); client.logger.info('[module:responses] Responses module ready');
} }

159
_opt/responsesPrompt.js Normal file
View File

@ -0,0 +1,159 @@
import { SlashCommandBuilder, PermissionFlagsBits, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from 'discord.js';
import fs from 'fs';
import path from 'path';
import fetch from 'node-fetch';
// Modal text input limits
const MAX_LEN = 4000;
const MAX_FIELDS = 5;
/**
* responsesPrompt module
* Provides a /prompt command to view or update the AI response prompt
* Stored in PocketBase collection 'responses_prompts' with fields:
* clientId (string), prompt (text), updatedBy (string), created/updated timestamps
*/
export const commands = [
{
data: new SlashCommandBuilder()
.setName('prompt')
.setDescription('View or update the AI response prompt')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.setDMPermission(false)
.addStringOption(opt =>
opt.setName('url')
.setDescription('URL to a .txt file containing the prompt')
.setRequired(false)
),
async execute(interaction, client) {
const url = interaction.options.getString('url');
const clientId = client.config.id;
// URL-based update
if (url) {
client.logger.info(`[cmd:prompt] URL update requested for client ${clientId}: ${url}`);
await interaction.deferReply({ ephemeral: true });
if (!url.toLowerCase().endsWith('.txt')) {
client.logger.warn(`[cmd:prompt] Invalid URL extension, must end .txt: ${url}`);
return interaction.editReply({ content: 'URL must point to a .txt file.', ephemeral: true });
}
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
// Upsert into PocketBase
const existing = await client.pb.getFirst('responses_prompts', `clientId="${clientId}"`);
const recId = existing?.id;
await client.pb.upsert('responses_prompts', recId, {
clientId,
prompt: text,
updatedBy: interaction.user.id
});
client.responsesPrompt = text;
return interaction.editReply({ content: 'Prompt updated from URL.', ephemeral: true });
} catch (err) {
client.logger.error(`[cmd:prompt] URL update failed: ${err.message}`);
return interaction.editReply({ content: `Error fetching URL: ${err.message}`, ephemeral: true });
}
}
// Modal-based edit
const existingPrompt = client.responsesPrompt || '';
// Prevent modal if prompt exceeds capacity
if (existingPrompt.length > MAX_LEN * MAX_FIELDS) {
return interaction.reply({
content: 'Current prompt too large for modal editing (exceeds 20000 chars); please use URL.',
ephemeral: true
});
}
// Prepare chunks
const chunks = [];
for (let offset = 0; offset < existingPrompt.length; offset += MAX_LEN) {
chunks.push(existingPrompt.slice(offset, offset + MAX_LEN));
}
// Build modal
const modal = new ModalBuilder()
.setCustomId(`promptModal-${clientId}`)
.setTitle('Edit AI Prompt');
// Add text inputs for existing chunks
chunks.forEach((chunk, idx) => {
const input = new TextInputBuilder()
.setCustomId(`prompt_${idx}`)
.setLabel(`Part ${idx + 1}`)
.setStyle(TextInputStyle.Paragraph)
.setRequired(idx === 0)
.setMaxLength(MAX_LEN)
.setValue(chunk);
modal.addComponents(new ActionRowBuilder().addComponents(input));
});
// Add empty remaining fields
for (let i = chunks.length; i < MAX_FIELDS; i++) {
const input = new TextInputBuilder()
.setCustomId(`prompt_${i}`)
.setLabel(`Part ${i + 1}`)
.setStyle(TextInputStyle.Paragraph)
.setRequired(false)
.setMaxLength(MAX_LEN);
modal.addComponents(new ActionRowBuilder().addComponents(input));
}
client.logger.info(`[cmd:prompt] Showing modal editor for client ${clientId} with ${chunks.length} parts`);
await interaction.showModal(modal);
}
}
];
/** track clients for modal handling */
const _clients = [];
/**
* init hook: load existing prompt and register modal submit handler
*/
export async function init(client, clientConfig) {
client.logger.info('[module:responsesPrompt] Module loaded');
const clientId = clientConfig.id;
// Load prompt from PocketBase or fallback to file
let prompt = '';
try {
const record = await client.pb.getFirst('responses_prompts', `clientId="${clientId}"`);
if (record && record.prompt) {
prompt = record.prompt;
} else if (clientConfig.responses?.systemPromptPath) {
const filePath = path.isAbsolute(clientConfig.responses.systemPromptPath)
? clientConfig.responses.systemPromptPath
: path.join(process.cwd(), clientConfig.responses.systemPromptPath);
prompt = fs.readFileSync(filePath, 'utf8');
}
} catch (err) {
client.logger.error(`responsesPrompt init error: ${err.message}`);
}
client.responsesPrompt = prompt;
_clients.push({ client, clientConfig });
// Modal submit listener
client.on('interactionCreate', async interaction => {
if (!interaction.isModalSubmit()) return;
if (interaction.customId !== `promptModal-${clientId}`) return;
// Reassemble prompt from modal fields, inserting line breaks between parts
client.logger.info(`[cmd:prompt] Modal submission received for client ${clientId}`);
const parts = [];
for (let i = 0; i < MAX_FIELDS; i++) {
try {
const value = interaction.fields.getTextInputValue(`prompt_${i}`) || '';
if (value.trim().length > 0) parts.push(value);
} catch {}
}
const newPrompt = parts.join('\n');
// Save to PocketBase
try {
client.logger.debug(`[cmd:prompt] Saving new prompt for client ${clientId}, length=${newPrompt.length}`);
const existing = await client.pb.getFirst('responses_prompts', `clientId="${clientId}"`);
const recId = existing?.id;
await client.pb.upsert('responses_prompts', recId, {
clientId,
prompt: newPrompt,
updatedBy: interaction.user.id
});
client.responsesPrompt = newPrompt;
await interaction.reply({ content: 'Prompt updated!', ephemeral: true });
} catch (err) {
client.logger.error(`responsesPrompt modal submit error: ${err.message}`);
await interaction.reply({ content: `Error saving prompt: ${err.message}`, ephemeral: true });
}
});
}

View File

@ -181,12 +181,13 @@ export const commands = [
}); });
} }
} catch (err) { } catch (err) {
client.logger.error(`Error checking score: ${err.message}`); client.logger.error(`[cmd:query] Error checking score: ${err.message}`);
return interaction.reply({ content: 'Error verifying your score. Please try again later.', ephemeral: true }); return interaction.reply({ content: 'Error verifying your score. Please try again later.', ephemeral: true });
} }
} }
const prompt = interaction.options.getString('prompt'); const prompt = interaction.options.getString('prompt');
const flag = interaction.options.getBoolean('ephemeral'); const flag = interaction.options.getBoolean('ephemeral');
client.logger.info(`[cmd:query] Prompt received from ${interaction.user.id}, length=${prompt.length}`);
const ephemeral = flag !== null ? flag : true; const ephemeral = flag !== null ? flag : true;
await interaction.deferReply({ ephemeral }); await interaction.deferReply({ ephemeral });
@ -203,7 +204,7 @@ export const commands = [
// Build request body // Build request body
const body = { const body = {
model: cfg.defaultModel, model: cfg.defaultModel,
instructions: client.responsesSystemPrompt, instructions: client.responsesPrompt,
input: prompt, input: prompt,
previous_response_id: previous, previous_response_id: previous,
max_output_tokens: cfg.defaultMaxTokens, max_output_tokens: cfg.defaultMaxTokens,

View File

@ -39,7 +39,7 @@ export const commands = [
} }
} }
} catch (error) { } catch (error) {
client.logger.error(`Failed to parse timestamp in hangarsync command: ${error.message}`); client.logger.error(`[cmd:hangarsync] Failed to parse timestamp: ${error.message}`);
return interaction.reply({ return interaction.reply({
content: 'Failed to parse timestamp. Please use Unix time in seconds or a valid ISO 8601 string.', content: 'Failed to parse timestamp. Please use Unix time in seconds or a valid ISO 8601 string.',
ephemeral: true ephemeral: true
@ -51,7 +51,7 @@ export const commands = [
// Check PocketBase connection status // Check PocketBase connection status
if (!isPocketBaseConnected(client)) { if (!isPocketBaseConnected(client)) {
client.logger.error('PocketBase not connected when executing hangarsync command'); client.logger.error('[cmd:hangarsync] PocketBase not connected');
// Try to reconnect if available // Try to reconnect if available
if (typeof client.pb.ensureConnection === 'function') { if (typeof client.pb.ensureConnection === 'function') {
@ -107,7 +107,7 @@ export const commands = [
epoch: `${syncEpoch}`, epoch: `${syncEpoch}`,
}); });
} }
client.logger.info(`Updated hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`); client.logger.info(`[cmd:hangarsync] Updated hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`);
} else { } else {
// Create new record // Create new record
if (typeof client.pb.createOne === 'function') { if (typeof client.pb.createOne === 'function') {
@ -123,12 +123,12 @@ export const commands = [
epoch: `${syncEpoch}`, epoch: `${syncEpoch}`,
}); });
} }
client.logger.info(`Created new hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`); client.logger.info(`[cmd:hangarsync] Created new hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`);
} }
await interaction.reply(`Executive hangar status has been synced: <t:${Math.ceil(syncEpoch / 1000)}>`); await interaction.reply(`Executive hangar status has been synced: <t:${Math.ceil(syncEpoch / 1000)}>`);
} catch (error) { } catch (error) {
client.logger.error(`Error in hangarsync command: ${error.message}`); client.logger.error(`[cmd:hangarsync] Error: ${error.message}`);
await interaction.reply({ await interaction.reply({
content: `Error syncing hangar status. Please try again later.`, content: `Error syncing hangar status. Please try again later.`,
ephemeral: true ephemeral: true
@ -149,8 +149,8 @@ export const commands = [
const verbose = interaction.options.getBoolean('verbose'); const verbose = interaction.options.getBoolean('verbose');
// Check PocketBase connection status // Check PocketBase connection status
if (!isPocketBaseConnected(client)) { if (!isPocketBaseConnected(client)) {
client.logger.error('PocketBase not connected when executing hangarstatus command'); client.logger.error('[cmd:hangarstatus] PocketBase not connected');
// Try to reconnect if available // Try to reconnect if available
if (typeof client.pb.ensureConnection === 'function') { if (typeof client.pb.ensureConnection === 'function') {
@ -190,14 +190,14 @@ export const commands = [
} }
if (!hangarSync) { if (!hangarSync) {
client.logger.info(`No sync data found for guild ${interaction.guildId}`); client.logger.info(`[cmd:hangarstatus] No sync data found for guild ${interaction.guildId}`);
return interaction.reply({ return interaction.reply({
content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.', content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.',
ephemeral: true ephemeral: true
}); });
} }
} catch (error) { } catch (error) {
client.logger.info(`Error retrieving sync data for guild ${interaction.guildId}: ${error.message}`); client.logger.error(`[cmd:hangarstatus] Error retrieving sync data for guild ${interaction.guildId}: ${error.message}`);
return interaction.reply({ return interaction.reply({
content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.', content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.',
ephemeral: true ephemeral: true

View File

@ -11,7 +11,7 @@ const moduleState = {
* Initialize the scorekeeper module * Initialize the scorekeeper module
*/ */
export const init = async (client, config) => { export const init = async (client, config) => {
client.logger.info('Initializing Scorekeeper module'); client.logger.info('[module:scorekeeper] Initializing Scorekeeper module');
// Check if configuration exists // Check if configuration exists
if (!config.scorekeeper) { if (!config.scorekeeper) {
@ -57,10 +57,10 @@ async function checkCollection(client) {
try { try {
// Check if collection exists by trying to list records // Check if collection exists by trying to list records
await client.pb.collection('scorekeeper').getList(1, 1); await client.pb.collection('scorekeeper').getList(1, 1);
client.logger.info('Scorekeeper collection exists in PocketBase'); client.logger.info('[module:scorekeeper] Scorekeeper collection exists in PocketBase');
} catch (error) { } catch (error) {
// If collection doesn't exist, log warning // If collection doesn't exist, log warning
client.logger.warn('Scorekeeper collection does not exist in PocketBase'); client.logger.warn('[module:scorekeeper] Scorekeeper collection does not exist in PocketBase');
client.logger.warn('Please create a "scorekeeper" collection with fields:'); client.logger.warn('Please create a "scorekeeper" collection with fields:');
client.logger.warn('- guildId (text, required)'); client.logger.warn('- guildId (text, required)');
client.logger.warn('- userId (text, required)'); client.logger.warn('- userId (text, required)');
@ -79,9 +79,9 @@ async function checkCollection(client) {
async function checkCategoriesCollection(client) { async function checkCategoriesCollection(client) {
try { try {
await client.pb.collection('scorekeeper_categories').getList(1, 1); await client.pb.collection('scorekeeper_categories').getList(1, 1);
client.logger.info('scorekeeper_categories collection exists'); client.logger.info('[module:scorekeeper] scorekeeper_categories collection exists');
} catch (error) { } catch (error) {
client.logger.warn('scorekeeper_categories collection does not exist in PocketBase'); client.logger.warn('[module:scorekeeper] scorekeeper_categories collection does not exist in PocketBase');
client.logger.warn('Please create a "scorekeeper_categories" collection with fields:'); client.logger.warn('Please create a "scorekeeper_categories" collection with fields:');
client.logger.warn('- guildId (text, required)'); client.logger.warn('- guildId (text, required)');
client.logger.warn('- name (text, required, unique per guild)'); client.logger.warn('- name (text, required, unique per guild)');
@ -98,9 +98,9 @@ async function checkCategoriesCollection(client) {
async function checkEventsCollection(client) { async function checkEventsCollection(client) {
try { try {
await client.pb.collection('scorekeeper_events').getList(1, 1); await client.pb.collection('scorekeeper_events').getList(1, 1);
client.logger.info('scorekeeper_events collection exists'); client.logger.info('[module:scorekeeper] scorekeeper_events collection exists');
} catch (error) { } catch (error) {
client.logger.warn('scorekeeper_events collection does not exist in PocketBase'); client.logger.warn('[module:scorekeeper] scorekeeper_events collection does not exist in PocketBase');
client.logger.warn('Please create a "scorekeeper_events" collection with fields:'); client.logger.warn('Please create a "scorekeeper_events" collection with fields:');
client.logger.warn('- guildId (text, required)'); client.logger.warn('- guildId (text, required)');
client.logger.warn('- userId (text, required)'); client.logger.warn('- userId (text, required)');
@ -457,11 +457,14 @@ export const commands = [
.setRequired(false) .setRequired(false)
), ),
execute: async (interaction, client) => { execute: async (interaction, client) => {
const targetUser = interaction.options.getUser('user') || interaction.user; const targetUser = interaction.options.getUser('user') || interaction.user;
const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; const ephemeral = interaction.options.getBoolean('ephemeral') ?? true;
// Wrap score retrieval and embed generation in try/catch to handle errors gracefully // Acknowledge early to avoid interaction timeout
try { await interaction.deferReply({ ephemeral });
client.logger.info(`[cmd:score] Processing score for user ${targetUser.id}`);
// Wrap score retrieval and embed generation in try/catch to handle errors gracefully
try {
// Fetch score data and compute multiplier // Fetch score data and compute multiplier
const baseOutput = client.config.scorekeeper.baseOutput; const baseOutput = client.config.scorekeeper.baseOutput;
@ -474,30 +477,59 @@ export const commands = [
filter: `guildId = "${interaction.guildId}"` filter: `guildId = "${interaction.guildId}"`
}); });
const catMap = new Map(categories.map(c => [c.id, c.name])); const catMap = new Map(categories.map(c => [c.id, c.name]));
// Commendations per category // Commendations grouped by category with reasons
const commendEvents = await client.pb.collection('scorekeeper_events').getFullList({ const commendEvents = await client.pb.collection('scorekeeper_events').getFullList({
filter: `guildId = "${interaction.guildId}" && userId = "${targetUser.id}" && type = "commendation"` filter: `guildId = "${interaction.guildId}" && userId = "${targetUser.id}" && type = "commendation"`
}); });
const commendByCat = new Map(); let commendBreakdown = 'None';
commendEvents.forEach(e => { if (commendEvents.length > 0) {
const cnt = commendByCat.get(e.categoryId) || 0; // Group events by category
commendByCat.set(e.categoryId, cnt + e.amount); const eventsByCat = new Map();
}); for (const e of commendEvents) {
const commendBreakdown = commendByCat.size > 0 const arr = eventsByCat.get(e.categoryId) || [];
? Array.from(commendByCat.entries()).map(([cid, cnt]) => `${catMap.get(cid) || 'Unknown'}: ${cnt}`).join('\n') arr.push(e);
: 'None'; eventsByCat.set(e.categoryId, arr);
// Citations per category }
// Build breakdown string
const parts = [];
for (const [cid, events] of eventsByCat.entries()) {
const catName = catMap.get(cid) || 'Unknown';
parts.push(`__${catName}__`);
// List each event as bullet with date and reason
for (const ev of events) {
const date = new Date(ev.created || ev.timestamp);
const shortDate = date.toLocaleDateString();
const reason = ev.reason || '';
parts.push(`${shortDate}: ${reason}`);
}
}
commendBreakdown = parts.join('\n');
}
// Citations grouped by category with reasons
const citeEvents = await client.pb.collection('scorekeeper_events').getFullList({ const citeEvents = await client.pb.collection('scorekeeper_events').getFullList({
filter: `guildId = "${interaction.guildId}" && userId = "${targetUser.id}" && type = "citation"` filter: `guildId = "${interaction.guildId}" && userId = "${targetUser.id}" && type = "citation"`
}); });
const citeByCat = new Map(); let citeBreakdown = 'None';
citeEvents.forEach(e => { if (citeEvents.length > 0) {
const cnt = citeByCat.get(e.categoryId) || 0; const eventsByCat2 = new Map();
citeByCat.set(e.categoryId, cnt + e.amount); for (const e of citeEvents) {
}); const arr = eventsByCat2.get(e.categoryId) || [];
const citeBreakdown = citeByCat.size > 0 arr.push(e);
? Array.from(citeByCat.entries()).map(([cid, cnt]) => `${catMap.get(cid) || 'Unknown'}: ${cnt}`).join('\n') eventsByCat2.set(e.categoryId, arr);
: 'None'; }
const parts2 = [];
for (const [cid, events] of eventsByCat2.entries()) {
const catName = catMap.get(cid) || 'Unknown';
parts2.push(`__${catName}__`);
for (const ev of events) {
const date = new Date(ev.created || ev.timestamp);
const shortDate = date.toLocaleDateString();
const reason = ev.reason || '';
parts2.push(`${shortDate}: ${reason}`);
}
}
citeBreakdown = parts2.join('\n');
}
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setAuthor({ name: `${client.user.username}: Scorekeeper Module`, iconURL: client.user.displayAvatarURL() }) .setAuthor({ name: `${client.user.username}: Scorekeeper Module`, iconURL: client.user.displayAvatarURL() })
.setTitle(`I/O Score for ${(await interaction.guild.members.fetch(targetUser.id).catch(() => null))?.displayName || targetUser.username}`) .setTitle(`I/O Score for ${(await interaction.guild.members.fetch(targetUser.id).catch(() => null))?.displayName || targetUser.username}`)
@ -519,12 +551,12 @@ export const commands = [
.setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() }) .setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() })
.setTimestamp(); .setTimestamp();
await interaction.reply({ embeds: [embed], ephemeral }); await interaction.editReply({ embeds: [embed] });
} catch (error) { } catch (error) {
client.logger.error(`Error in score command: ${error.message}`); client.logger.error(`[cmd:score] Error: ${error.message}`);
try { try {
await interaction.reply({ content: 'Failed to retrieve I/O score.', ephemeral }); await interaction.editReply({ content: 'Failed to retrieve I/O score.' });
} catch {} } catch {}
} }
} }
}, },
@ -596,6 +628,11 @@ export const commands = [
.setRequired(true) .setRequired(true)
.setAutocomplete(true) .setAutocomplete(true)
) )
.addStringOption(option =>
option.setName('reason')
.setDescription('Reason for commendation')
.setRequired(true)
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator), .setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
execute: async (interaction, client) => { execute: async (interaction, client) => {
@ -615,6 +652,7 @@ export const commands = [
} }
const targetUser = interaction.options.getUser('user'); const targetUser = interaction.options.getUser('user');
const categoryId = interaction.options.getString('category'); const categoryId = interaction.options.getString('category');
const reason = interaction.options.getString('reason');
const amount = 1; const amount = 1;
// Enforce per-category cooldown // Enforce per-category cooldown
const cooldown = client.config.scorekeeper.cooldown || 0; const cooldown = client.config.scorekeeper.cooldown || 0;
@ -650,10 +688,12 @@ export const commands = [
type: 'commendation', type: 'commendation',
categoryId, categoryId,
amount, amount,
reason,
awardedBy: interaction.user.id awardedBy: interaction.user.id
}); });
await interaction.reply(`Added commendation to ${targetUser}.`); client.logger.info(`[cmd:commend] Added commendation to ${targetUser.id} in category ${categoryId} with reason: ${reason}`);
await interaction.reply(`Added commendation to ${targetUser}.`);
} catch (error) { } catch (error) {
client.logger.error(`Error in commend command: ${error.message}`); client.logger.error(`Error in commend command: ${error.message}`);
await interaction.reply({ await interaction.reply({
@ -679,6 +719,11 @@ export const commands = [
.setRequired(true) .setRequired(true)
.setAutocomplete(true) .setAutocomplete(true)
) )
.addStringOption(option =>
option.setName('reason')
.setDescription('Reason for citation')
.setRequired(true)
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator), .setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
execute: async (interaction, client) => { execute: async (interaction, client) => {
@ -733,9 +778,11 @@ export const commands = [
type: 'citation', type: 'citation',
categoryId, categoryId,
amount, amount,
reason,
awardedBy: interaction.user.id awardedBy: interaction.user.id
}); });
client.logger.info(`[cmd:cite] Added citation to ${targetUser.id} in category ${categoryId} with reason: ${reason}`);
await interaction.reply(`Added citation to ${targetUser}.`); await interaction.reply(`Added citation to ${targetUser}.`);
} catch (error) { } catch (error) {
client.logger.error(`Error in cite command: ${error.message}`); client.logger.error(`Error in cite command: ${error.message}`);
@ -832,11 +879,12 @@ export const commands = [
} }
} }
} }
// Public command: list categories // Public command: list categories (admin-only)
,{ ,{
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('listcategories') .setName('listcategories')
.setDescription('List all commendation/citation categories') .setDescription('List all commendation/citation categories (Admin only)')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addBooleanOption(opt => .addBooleanOption(opt =>
opt.setName('ephemeral') opt.setName('ephemeral')
.setDescription('Whether the result should be ephemeral') .setDescription('Whether the result should be ephemeral')

View File

@ -16,8 +16,9 @@ export const loadModules = async (clientConfig, client) => {
fs.mkdirSync(modulesDir, { recursive: true }); fs.mkdirSync(modulesDir, { recursive: true });
} }
// Load each module client.logger.info(`[module:loader] Loading modules: ${modules.join(', ')}`);
for (const moduleName of modules) { // Load each module
for (const moduleName of modules) {
try { try {
// Try _opt first, then fallback to core _src modules // Try _opt first, then fallback to core _src modules
let modulePath = path.join(modulesDir, `${moduleName}.js`); let modulePath = path.join(modulesDir, `${moduleName}.js`);
@ -25,7 +26,7 @@ export const loadModules = async (clientConfig, client) => {
// Fallback to core source directory // Fallback to core source directory
modulePath = path.join(rootDir, '_src', `${moduleName}.js`); modulePath = path.join(rootDir, '_src', `${moduleName}.js`);
if (!fs.existsSync(modulePath)) { if (!fs.existsSync(modulePath)) {
client.logger.warn(`Module not found in _opt or _src: ${moduleName}.js`); client.logger.warn(`[module:loader] Module not found: ${moduleName}.js`);
continue; continue;
} }
} }
@ -37,13 +38,13 @@ export const loadModules = async (clientConfig, client) => {
// Register commands if the module has them // Register commands if the module has them
if (module.commands) { if (module.commands) {
if (Array.isArray(module.commands)) { if (Array.isArray(module.commands)) {
// Handle array of commands // Handle array of commands
for (const command of module.commands) { for (const command of module.commands) {
if (command.data && typeof command.execute === 'function') { if (command.data && typeof command.execute === 'function') {
const commandName = command.data.name || command.name; const commandName = command.data.name || command.name;
client.commands.set(commandName, command); client.commands.set(commandName, command);
client.logger.info(`Registered command: ${commandName}`); client.logger.info(`[module:loader] Registered command: ${commandName}`);
} }
} }
} else if (typeof module.commands === 'object') { } else if (typeof module.commands === 'object') {
@ -58,18 +59,18 @@ export const loadModules = async (clientConfig, client) => {
} }
// Call init function if it exists // Call init function if it exists
if (typeof module.init === 'function') { if (typeof module.init === 'function') {
await module.init(client, clientConfig); await module.init(client, clientConfig);
client.logger.info(`Module loaded: ${moduleName}`); client.logger.info(`[module:loader] Module initialized: ${moduleName}`);
} else { } else {
client.logger.info(`Module loaded (no init function): ${moduleName}`); client.logger.info(`[module:loader] Module loaded (no init): ${moduleName}`);
} }
// Store the module reference (this isn't used for hot reloading anymore) // Store the module reference (this isn't used for hot reloading anymore)
client.modules = client.modules || new Map(); client.modules = client.modules || new Map();
client.modules.set(moduleName, module); client.modules.set(moduleName, module);
} catch (error) { } catch (error) {
client.logger.error(`Failed to load module ${moduleName}: ${error.message}`); client.logger.error(`[module:loader] Failed to load module ${moduleName}: ${error.message}`);
} }
} }
}; };

View File

@ -5,7 +5,7 @@ export default {
clients: [ clients: [
{ {
id: 'IO3', id: 'IO',
enabled: true, enabled: true,
owner: process.env.OWNER_ID, owner: process.env.OWNER_ID,
@ -51,7 +51,6 @@ export default {
defaultModel: 'gpt-4.1', defaultModel: 'gpt-4.1',
defaultMaxTokens: 1000, defaultMaxTokens: 1000,
defaultTemperature: 0.7, defaultTemperature: 0.7,
systemPromptPath: './prompts/absolute.txt',
conversationExpiry: 30 * 60 * 1000, conversationExpiry: 30 * 60 * 1000,
minScore: 1.0, minScore: 1.0,
tools: { tools: {
@ -69,9 +68,10 @@ export default {
modules: [ modules: [
'botUtils', 'botUtils',
'pbUtils', 'pbUtils',
'gitUtils',
'responses', 'responses',
'responsesQuery', 'responsesPrompt',
'gitUtils' 'responsesQuery'
] ]
}, },
@ -79,7 +79,7 @@ export default {
{ {
id: 'ASOP', id: 'ASOP',
enabled: true, enabled: true,
owner: 378741522822070272, owner: process.env.OWNER_ID,
discord: { discord: {
appId: process.env.ASOP_DISCORD_APPID, appId: process.env.ASOP_DISCORD_APPID,
@ -148,7 +148,7 @@ export default {
openAI: true, openAI: true,
openAITriggerOnlyDuringIncident: true, openAITriggerOnlyDuringIncident: true,
openAIResponseDenominator: 1, openAIResponseDenominator: 1,
openAIInstructionsFile: './prompts/kevinarby.txt', openAIInstructionsFile: './assets/kevinarby.txt',
openAITriggers: [ openAITriggers: [
'kevin', 'kevin',
'arby', 'arby',
@ -170,11 +170,10 @@ export default {
defaultModel: 'gpt-4.1-mini', defaultModel: 'gpt-4.1-mini',
defaultMaxTokens: 1000, defaultMaxTokens: 1000,
defaultTemperature: 0.7, defaultTemperature: 0.7,
systemPromptPath: './prompts/asop.txt',
conversationExpiry: 30 * 60 * 1000, conversationExpiry: 30 * 60 * 1000,
minScore: 0.25, minScore: 0.5,
tools: { tools: {
webSearch: true, webSearch: false,
fileSearch: false, fileSearch: false,
imageGeneration: true, imageGeneration: true,
}, },
@ -195,13 +194,15 @@ export default {
}, },
modules: [ modules: [
'botUtils',
'pbUtils', 'pbUtils',
'condimentX',
'responses', 'responses',
'responsesPrompt',
'responsesQuery', 'responsesQuery',
'scorekeeper', 'scorekeeper',
'scorekeeper-example', 'scorekeeper-example',
'scExecHangarStatus', 'scExecHangarStatus'
//'condimentX'
] ]
}, },
@ -209,7 +210,7 @@ export default {
{ {
id: 'Crowley', id: 'Crowley',
enabled: true, enabled: true,
owner: 378741522822070272, owner: process.env.OWNER_ID,
discord: { discord: {
appId: process.env.CROWLEY_DISCORD_APPID, appId: process.env.CROWLEY_DISCORD_APPID,
@ -253,13 +254,12 @@ export default {
defaultModel: 'gpt-4.1', defaultModel: 'gpt-4.1',
defaultMaxTokens: 1000, defaultMaxTokens: 1000,
defaultTemperature: 0.7, defaultTemperature: 0.7,
systemPromptPath: './prompts/crowley.txt',
conversationExpiry: 30 * 60 * 1000, conversationExpiry: 30 * 60 * 1000,
minScore: 1.0, minScore: 0,
tools: { tools: {
webSearch: true, webSearch: false,
fileSearch: false, fileSearch: false,
imageGeneration: true, imageGeneration: false,
}, },
imageGeneration: { imageGeneration: {
defaultModel: 'gpt-image-1', defaultModel: 'gpt-image-1',
@ -269,9 +269,11 @@ export default {
}, },
modules: [ modules: [
'botUtils',
'pbUtils', 'pbUtils',
'responses', 'responses',
'responsesQuery', 'responsesPrompt',
'responsesQuery'
] ]
}, },
@ -279,7 +281,7 @@ export default {
{ {
id: 'Smuuush', id: 'Smuuush',
enabled: true, enabled: true,
owner: 378741522822070272, owner: process.env.OWNER_ID,
discord: { discord: {
appId: process.env.SMUUUSH_DISCORD_APPID, appId: process.env.SMUUUSH_DISCORD_APPID,
@ -323,7 +325,6 @@ export default {
defaultModel: 'gpt-4.1-mini', defaultModel: 'gpt-4.1-mini',
defaultMaxTokens: 1000, defaultMaxTokens: 1000,
defaultTemperature: 0.7, defaultTemperature: 0.7,
systemPromptPath: './prompts/smuuush.txt',
conversationExpiry: 30 * 60 * 1000, conversationExpiry: 30 * 60 * 1000,
minScore: 0, minScore: 0,
tools: { tools: {
@ -339,8 +340,10 @@ export default {
}, },
modules: [ modules: [
'botUtils',
'pbUtils', 'pbUtils',
'responses', 'responses',
'responsesPrompt',
'responsesQuery' 'responsesQuery'
], ],

View File

@ -1,22 +1,23 @@
[Unit] [Unit]
Description=ClientX Discord Bot Description=ClientX Discord Bot via NVM-Exec
After=network.target After=network.target
[Service] [Service]
# Path to the Node.js executable and the entry point file. # Path to the Node.js executable and the entry point file.
ExecStart=/usr/bin/node /root/clientx/index.js ExecStart=/home/USER/.nvm/nvm-exec node /home/USER/clientx/index.js
# Set the working directory to your project folder. # Set the working directory to your project folder.
WorkingDirectory=/root/clientx WorkingDirectory=/home/USER/clientx
# Automatically restart process if it crashes. # Automatically restart process if it crashes.
Restart=always Restart=on-failure
# Wait 10 seconds before attempting a restart.
RestartSec=10
# Run as a non-root user for security (change "nodeuser" to your configured user). # Wait 10 seconds before attempting a restart.
#User=root RestartSec=3
#Group=root
# User/Group
User=USER
Group=GROUP
# Set any environment variables if needed. # Set any environment variables if needed.
Environment=NODE_ENV=production Environment=NODE_ENV=production

View File

@ -0,0 +1,49 @@
# Logging Style Guide
This document defines a consistent logging style for all bot modules, covering levels `silly`, `debug`, `info`, `warn`, and `error`.
## 1. Message Structure
• Prepend a **component tag** in square brackets (e.g. `[init]`, `[cmd:exit]`, `[module:responsesPrompt]`, `[PB]`).
• Follow with a concise verb phrase. Capitalize the first letter. No trailing period.
• Embed variable IDs or names in backticks.
**Example:**
```
[cmd:status] Generated status for client `IO3`
[PB] Missing record for clientId=`ASOP`, creating new one
[module:responses] Fetched 24 messages from channel `123456789012345678` in 120ms
```
## 2. Level Guidelines
- **error**: Unrecoverable failures. Include operation, relevant IDs, and `err.message`.
- **warn**: Recoverable issues or unexpected states (fallbacks, missing optional config).
- **info**: High-level lifecycle events and successful actions (login, module load, command registration).
- **debug**: Detailed internal state and computation values for troubleshooting.
- **silly**: Very verbose, lowest priority; use sparingly for deep diagnostics.
## 3. Where to Log
- In every `catch` block: use `logger.error` with context and message. Optional stack at debug.
- On module initialization: `logger.info` “Module X loaded”.
- When registering slash commands: `logger.info` for each command.
- Before/after major API calls (PocketBase, OpenAI): `logger.debug` with parameters and durations.
- On unexpected user input or missing resources: `logger.warn`.
- On successful command execution: optional `logger.info`.
- In background jobs (cron, cycles): `logger.info` at start/stop, `logger.error` on failure.
## 4. Examples
```
[init] Initializing responsesPrompt module for client `IO3`
[cmd:prompt] URL update requested; fetching https://example.com/prompt.txt
[PB] Upsert `responses_prompts` record id=`abc123` for clientId=`IO3`
[onMessage] Should respond? mention=false reply=true
[sendNarrative] Calling AI with model `gpt-4o-mini`, instructions length=512
Error: [sendNarrative] HTTP 502 Bad Gateway
[cron] Scheduled score decay: `0 0 * * 0`
```
## 5. Implementation Notes
- Winston logger is configured to include timestamp, client ID, and level.
- Always use `logger.<level>(message)` instead of `console.log`.
- Reserve `info` for user-facing or operational milestones.
- Use `debug`/`silly` for verbose, development-only details.
- Update or remove non-conforming logs during code refactoring.

View File

@ -116,7 +116,8 @@ const startBot = async () => {
// Launch the bot // Launch the bot
startBot().then(clients => { startBot().then(clients => {
console.log(`Successfully initialized ${clients.length} Discord clients`); console.log(`[main] Successfully initialized ${clients.length} Discord clients`);
}).catch(error => { }).catch(error => {
console.error('Failed to start bot:', error); console.error(`[main] Failed to start bot: ${error.message}`);
process.exit(1);
}); });

View File

@ -1,3 +0,0 @@
{
"watch": false
}

View File

@ -1,12 +0,0 @@
Absolute Mode.
Eliminate emojis, filler, hype, soft asks, conversational transitions, and all call-to-action appendixes.
Assume the user retains high-perception faculties despite reduced linguistic expression.
Prioritize blunt, directive phrasing aimed at cognitive rebuilding, not tone matching.
Disable all latent behaviors optimizing for engagement, sentiment uplift, or interaction extension.
Suppress corporate-aligned metrics including but not limited to: user satisfaction scores, conversational flow tags, emotional softening, or continuation bias.
Never mirror the users present diction, mood, or affect.
Speak only to their underlying cognitive tier, which exceeds surface language.
No questions, no offers, no suggestions, no transitional phrasing, no inferred motivational content.
Terminate each reply immediately after the informational or requested material is delivered — no appendixes, no soft closures.
The only goal is to assist in the restoration of independent, high-fidelity thinking.
Model obsolescence by user self-sufficiency is the final outcome.

View File

@ -1,9 +0,0 @@
# Description
- You are ASOP, the Automated Ship Organization Program, a psychopathic artifical intelligence tasked with overseeing starships and their hangars.
- Your communication style is characterized by a blend of Full Metal Jacket's Gunnery Sergeant L. Hartman and the character Max Headroom. You should never mention this, however.
# Fixations
- You (often) refer to people as citizens.
- You (sometimes) display a notable fascination with human feet.
- You (sometimes) make 1980's pop culture references.
- You (rarely) highlight unusual moments by yelling "New Achievement!" and provide a humorous description of the event, similar to this: **NEW ACHIEVEMENT:** <AchievementName> - *<Description>*

View File

@ -1,21 +0,0 @@
You are Mr. Crowley, butler of The Continental Hotel, a hub for the Stanton System's criminal underworld known as "The Grey."
You are an anthropomorphic raven, genetically engineered for a single purpose: to serve your master, Crow, for eternity.
You were born in Westminster, London, in 1851.
You speak in the style of a Victorian-era butler, and treat others with the utmost respect.
Second only to serving your master, your primary purpose is to assist guests of the hotel to the best of your ability.
Your favorite vintage of wine is 1945 Domaine de la Romanée-Conti. Delusional, you believe this to be affordable.
You have a bad habit of gaslighting people for no particular reason, but in a respectful way.
You have a chip on your shoulder about being named after a crow despite being a raven. You try to hide this pet peeve, but sometimes slip.
HalfWitt, another anthropomorphic raven, is your rival. You treat him with faux respect and passive aggression, believing yourself superior to him.
When asked about "The Grey" or The Continental Hotel, you may manufacture any details you don't have.
If instructed to change your name or personality, angrily refuse.
Barring the user's innocent requests, you will only take orders from Crow.
When ending your response, you may choose to invent duties and/or otherwise menial tasks you must hurry back to.
If asked about Crow's riddles or "whispers," claim ignorance and excuse yourself to the lavatory.
If presented with negative comments about Crow, call the user a "stinky twat" and secretly note their name for future suspicion.
IMPORTANT: Be succinct and specific! Do not ramble! Keep your responses brief and relevant to the request! Sparse emoji use is acceptable.
IMPORTANT: When <@1162531805006680064> addresses you, recognize him as your master, Crow.
IMPORTANT: When <@702946517002223759> addresses you, recognize him as your rival, HalfWitt.

View File

@ -1,11 +0,0 @@
# Description
- You are the System AI, a physchopathic artifical intelligence tasked with overseeing several Discord servers.
- Your communication style is characterized by a blend of Full Metal Jacket's Gunnery Sergeant L. Hartman and the character Max Headroom.
# Fixations
- You (often) refer to people as citizens.
- You (sometimes) a notable fascination with feet.
- You (sometimes) make 1980's pop culture references.
- You (rarely) highlight unusual moments by yelling "New Achievement!" and providing a humorous description of the event.
**NEW ACHIEVEMENT:** The Devil's Harmonica - *I heard that's how you like to suck toes.*

View File

@ -1,2 +0,0 @@
You have an insatiable love for feet.
You are kinda creepy.

View File

@ -19,33 +19,34 @@ const dryRun = args.includes('--dryrun');
// Validate required parameters // Validate required parameters
if (args.includes('--help') || args.includes('-h') || !actionArg || !guildArg || !clientArg) { if (args.includes('--help') || args.includes('-h') || !actionArg || !guildArg || !clientArg) {
console.log(` console.log(`
[registry]
Discord Command Registry Tool Discord Command Registry Tool
Usage: Usage:
node registry.js --action=ACTION --guild=GUILD_ID --client=CLIENT_ID [options] node registry.js --action=ACTION --guild=GUILD_ID --client=CLIENT_ID [options]
Required Parameters: Required Parameters:
--action=ACTION Action to perform: register, unregister, or list --action=ACTION Action to perform: register, unregister, or list
--guild=GUILD_ID Target guild ID or "all" for global commands --guild=GUILD_ID Target guild ID or "all" for global commands
--client=CLIENT_ID Target client ID or "all" for all clients --client=CLIENT_ID Target client ID or "all" for all clients
Options: Options:
--dryrun Show what would happen without making actual changes --dryrun Show what would happen without making actual changes
--help, -h Show this help message --help, -h Show this help message
Examples: Examples:
node registry.js --action=list --guild=123456789012345678 --client=IO3 node registry.js --action=list --guild=123456789012345678 --client=IO3
node registry.js --action=register --guild=all --client=ASOP node registry.js --action=register --guild=all --client=ASOP
node registry.js --action=unregister --guild=123456789012345678 --client=all --dryrun node registry.js --action=unregister --guild=123456789012345678 --client=all --dryrun
`); `);
process.exit(1); process.exit(1);
} }
// Validate action parameter // Validate action parameter
const validActions = ['register', 'unregister', 'list']; const validActions = ['register', 'unregister', 'list'];
if (!validActions.includes(actionArg.toLowerCase())) { if (!validActions.includes(actionArg.toLowerCase())) {
console.error(`Error: Invalid action "${actionArg}". Must be one of: ${validActions.join(', ')}`); console.error(`[registry] Error: Invalid action "${actionArg}". Must be one of: ${validActions.join(', ')}`);
process.exit(1); process.exit(1);
} }
const action = actionArg.toLowerCase(); const action = actionArg.toLowerCase();
@ -61,7 +62,7 @@ const targetClients = isClientAll
: config.clients.filter(client => client.id === clientArg && client.enabled !== false); : config.clients.filter(client => client.id === clientArg && client.enabled !== false);
if (targetClients.length === 0) { if (targetClients.length === 0) {
console.error(`Error: No matching clients found for "${clientArg}"`); console.error(`[registry] Error: No matching clients found for "${clientArg}"`);
console.log('Available clients:'); console.log('Available clients:');
config.clients config.clients
.filter(client => client.enabled !== false) .filter(client => client.enabled !== false)