Logging changes, module updates.
This commit is contained in:
parent
f4996cafd2
commit
0aa1180dc8
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,6 +3,7 @@
|
||||
node_modules
|
||||
|
||||
.env
|
||||
.nvmrc
|
||||
images/*
|
||||
logs/*
|
||||
pocketbase/*
|
||||
|
||||
@ -9,7 +9,7 @@ export const commands = [
|
||||
{
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('exit')
|
||||
.setDescription('Gracefully shutdown the bot (owner only)')
|
||||
.setDescription('Gracefully shutdown the bot (Owner only)')
|
||||
// Restrict to server administrators by default
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.setDMPermission(false)
|
||||
@ -39,13 +39,13 @@ export const commands = [
|
||||
// Acknowledge before shutting down
|
||||
await interaction.reply({ content: `Shutting down with exit code ${exitCode}...`, ephemeral: true });
|
||||
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
|
||||
try {
|
||||
await client.destroy();
|
||||
} catch (err) {
|
||||
client.logger.error(`Error during client.destroy(): ${err}`);
|
||||
client.logger.error(`[cmd:exit] Error during client.destroy(): ${err}`);
|
||||
}
|
||||
process.exit(exitCode);
|
||||
}
|
||||
@ -99,6 +99,7 @@ export const commands = [
|
||||
`Modules : ${loadedModules.length > 0 ? loadedModules.join(', ') : 'None'}`
|
||||
];
|
||||
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
|
||||
*/
|
||||
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
|
||||
botUtilsClients.push({ client, clientConfig });
|
||||
}
|
||||
@ -362,11 +362,11 @@ export const init = async (client, config) => {
|
||||
|
||||
// Deferred setup on ready
|
||||
const readyHandler = async () => {
|
||||
client.logger.info('Initializing CondimentX module');
|
||||
client.logger.info('[module:condimentX] Initializing module');
|
||||
if (openAI === true) {
|
||||
openai = new OpenAI({ apiKey: openAIToken });
|
||||
openai = new OpenAI({ apiKey: openAIToken }); // credentials loaded
|
||||
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;
|
||||
});
|
||||
if (openAIWebhook) openAIWebhookClient = new WebhookClient({ id: openAIWebhookID, token: openAIWebhookToken });
|
||||
@ -374,7 +374,7 @@ export const init = async (client, config) => {
|
||||
try {
|
||||
guild = client.guilds.cache.get(guildID);
|
||||
if (!guild) {
|
||||
client.logger.error(`CondimentX error: Guild ${guildID} not found`);
|
||||
client.logger.error(`[module:condimentX] Guild ${guildID} not found`);
|
||||
return;
|
||||
}
|
||||
indexRole = await guild.roles.fetch(indexRoleID);
|
||||
|
||||
@ -45,7 +45,7 @@ export const commands = [
|
||||
{
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('git')
|
||||
.setDescription('Run an arbitrary git command (owner only)')
|
||||
.setDescription('Run an arbitrary git command (Owner only)')
|
||||
.addStringOption(opt =>
|
||||
opt.setName('args')
|
||||
.setDescription('Arguments to pass to git')
|
||||
@ -71,7 +71,7 @@ export const commands = [
|
||||
try {
|
||||
// Log the exact git command being executed
|
||||
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);
|
||||
// Prepend the git command as a header; keep it intact when chunking
|
||||
const header = `git ${cmdStr}\n`;
|
||||
@ -98,5 +98,5 @@ export const commands = [
|
||||
|
||||
// No special init logic
|
||||
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');
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { onMessageQueueEvent } from './pbUtils.js';
|
||||
* Example module that listens for 'test' messages in the message_queue collection.
|
||||
*/
|
||||
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) => {
|
||||
// Only process newly created records
|
||||
if (action !== 'create') return;
|
||||
@ -15,14 +15,14 @@ export const init = async (client, config) => {
|
||||
if (record.dataType !== 'test') return;
|
||||
|
||||
// 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
|
||||
try {
|
||||
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) {
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -19,7 +19,7 @@ if (typeof global.EventSource === 'undefined') {
|
||||
export const init = async (client, config) => {
|
||||
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
|
||||
extendPocketBase(client, pb, logger);
|
||||
@ -33,9 +33,9 @@ export const init = async (client, config) => {
|
||||
client.emit('message_queue_event', e.action, 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) {
|
||||
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()
|
||||
@ -54,7 +54,7 @@ export function onMessageQueueEvent(client, handler) {
|
||||
try {
|
||||
handler(action, record);
|
||||
} catch (err) {
|
||||
client.logger.error(`Error in message_queue handler: ${err.message}`);
|
||||
client.logger.error(`[module:pbUtils] Error in message_queue handler: ${err.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -3,8 +3,7 @@
|
||||
* Listens to message events, sends chat queries to the OpenAI Responses API,
|
||||
* and handles text or image (function_call) outputs.
|
||||
*/
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
// Removed local file fallback; prompt now comes exclusively from PocketBase via responsesPrompt module
|
||||
import { OpenAI } from 'openai';
|
||||
import axios from 'axios';
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -256,6 +239,7 @@ async function handleImage(client, message, resp, cfg) {
|
||||
async function onMessage(client, cfg, message) {
|
||||
const logger = client.logger;
|
||||
const botId = client.user?.id;
|
||||
client.logger.debug(`[onMessage] Received message ${message.id} from ${message.author.id}`);
|
||||
if (!(await shouldRespond(message, botId, logger))) return;
|
||||
await message.channel.sendTyping();
|
||||
|
||||
@ -290,7 +274,7 @@ async function onMessage(client, cfg, message) {
|
||||
const speakerMention = `<@${message.author.id}>`;
|
||||
const body = {
|
||||
model: cfg.defaultModel,
|
||||
instructions: client.responsesSystemPrompt,
|
||||
instructions: client.responsesPrompt,
|
||||
input: `${speakerMention} said to you: ${message.content}`,
|
||||
previous_response_id: prev,
|
||||
max_output_tokens: cfg.defaultMaxTokens,
|
||||
@ -392,7 +376,7 @@ async function onMessage(client, cfg, message) {
|
||||
}
|
||||
};
|
||||
// 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);
|
||||
// Queue enqueued; handler will send response when its turn arrives
|
||||
return;
|
||||
@ -442,7 +426,7 @@ export async function sendNarrative(client, cfg, channelId, text) {
|
||||
const logger = client.logger;
|
||||
try {
|
||||
// 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 = {
|
||||
model: cfg.defaultModel,
|
||||
instructions,
|
||||
@ -450,13 +434,13 @@ export async function sendNarrative(client, cfg, channelId, text) {
|
||||
max_output_tokens: cfg.defaultMaxTokens,
|
||||
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);
|
||||
logger.info(`sendNarrative AI response id=${resp.id}`);
|
||||
logger.info(`[sendNarrative] Received AI response id=${resp.id}`);
|
||||
// Fetch the target channel or thread
|
||||
const channel = await client.channels.fetch(channelId);
|
||||
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;
|
||||
}
|
||||
// Split the output and send
|
||||
@ -468,7 +452,7 @@ export async function sendNarrative(client, cfg, channelId, text) {
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
const cfg = clientConfig.responses;
|
||||
client.logger.info('Initializing Responses module');
|
||||
client.responsesSystemPrompt = await loadSystemPrompt(cfg.systemPromptPath, client.logger);
|
||||
client.logger.info('[module:responses] Initializing Responses module');
|
||||
// Initialize prompt from responsesPrompt module (must be loaded before this)
|
||||
client.responsesPrompt = client.responsesPrompt ?? '';
|
||||
client.openai = new OpenAI({ apiKey: cfg.apiKey });
|
||||
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
159
_opt/responsesPrompt.js
Normal 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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -181,12 +181,13 @@ export const commands = [
|
||||
});
|
||||
}
|
||||
} 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 });
|
||||
}
|
||||
}
|
||||
const prompt = interaction.options.getString('prompt');
|
||||
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;
|
||||
await interaction.deferReply({ ephemeral });
|
||||
|
||||
@ -203,7 +204,7 @@ export const commands = [
|
||||
// Build request body
|
||||
const body = {
|
||||
model: cfg.defaultModel,
|
||||
instructions: client.responsesSystemPrompt,
|
||||
instructions: client.responsesPrompt,
|
||||
input: prompt,
|
||||
previous_response_id: previous,
|
||||
max_output_tokens: cfg.defaultMaxTokens,
|
||||
|
||||
@ -39,7 +39,7 @@ export const commands = [
|
||||
}
|
||||
}
|
||||
} 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({
|
||||
content: 'Failed to parse timestamp. Please use Unix time in seconds or a valid ISO 8601 string.',
|
||||
ephemeral: true
|
||||
@ -51,7 +51,7 @@ export const commands = [
|
||||
|
||||
// Check PocketBase connection status
|
||||
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
|
||||
if (typeof client.pb.ensureConnection === 'function') {
|
||||
@ -107,7 +107,7 @@ export const commands = [
|
||||
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 {
|
||||
// Create new record
|
||||
if (typeof client.pb.createOne === 'function') {
|
||||
@ -123,12 +123,12 @@ export const commands = [
|
||||
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)}>`);
|
||||
} catch (error) {
|
||||
client.logger.error(`Error in hangarsync command: ${error.message}`);
|
||||
client.logger.error(`[cmd:hangarsync] Error: ${error.message}`);
|
||||
await interaction.reply({
|
||||
content: `Error syncing hangar status. Please try again later.`,
|
||||
ephemeral: true
|
||||
@ -150,7 +150,7 @@ export const commands = [
|
||||
|
||||
// Check PocketBase connection status
|
||||
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
|
||||
if (typeof client.pb.ensureConnection === 'function') {
|
||||
@ -190,14 +190,14 @@ export const commands = [
|
||||
}
|
||||
|
||||
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({
|
||||
content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
} 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({
|
||||
content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.',
|
||||
ephemeral: true
|
||||
|
||||
@ -11,7 +11,7 @@ const moduleState = {
|
||||
* Initialize the scorekeeper module
|
||||
*/
|
||||
export const init = async (client, config) => {
|
||||
client.logger.info('Initializing Scorekeeper module');
|
||||
client.logger.info('[module:scorekeeper] Initializing Scorekeeper module');
|
||||
|
||||
// Check if configuration exists
|
||||
if (!config.scorekeeper) {
|
||||
@ -57,10 +57,10 @@ async function checkCollection(client) {
|
||||
try {
|
||||
// Check if collection exists by trying to list records
|
||||
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) {
|
||||
// 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('- guildId (text, required)');
|
||||
client.logger.warn('- userId (text, required)');
|
||||
@ -79,9 +79,9 @@ async function checkCollection(client) {
|
||||
async function checkCategoriesCollection(client) {
|
||||
try {
|
||||
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) {
|
||||
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('- guildId (text, required)');
|
||||
client.logger.warn('- name (text, required, unique per guild)');
|
||||
@ -98,9 +98,9 @@ async function checkCategoriesCollection(client) {
|
||||
async function checkEventsCollection(client) {
|
||||
try {
|
||||
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) {
|
||||
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('- guildId (text, required)');
|
||||
client.logger.warn('- userId (text, required)');
|
||||
@ -460,6 +460,9 @@ export const commands = [
|
||||
execute: async (interaction, client) => {
|
||||
const targetUser = interaction.options.getUser('user') || interaction.user;
|
||||
const ephemeral = interaction.options.getBoolean('ephemeral') ?? true;
|
||||
// Acknowledge early to avoid interaction timeout
|
||||
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 {
|
||||
|
||||
@ -474,30 +477,59 @@ export const commands = [
|
||||
filter: `guildId = "${interaction.guildId}"`
|
||||
});
|
||||
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({
|
||||
filter: `guildId = "${interaction.guildId}" && userId = "${targetUser.id}" && type = "commendation"`
|
||||
});
|
||||
const commendByCat = new Map();
|
||||
commendEvents.forEach(e => {
|
||||
const cnt = commendByCat.get(e.categoryId) || 0;
|
||||
commendByCat.set(e.categoryId, cnt + e.amount);
|
||||
});
|
||||
const commendBreakdown = commendByCat.size > 0
|
||||
? Array.from(commendByCat.entries()).map(([cid, cnt]) => `${catMap.get(cid) || 'Unknown'}: ${cnt}`).join('\n')
|
||||
: 'None';
|
||||
// Citations per category
|
||||
let commendBreakdown = 'None';
|
||||
if (commendEvents.length > 0) {
|
||||
// Group events by category
|
||||
const eventsByCat = new Map();
|
||||
for (const e of commendEvents) {
|
||||
const arr = eventsByCat.get(e.categoryId) || [];
|
||||
arr.push(e);
|
||||
eventsByCat.set(e.categoryId, arr);
|
||||
}
|
||||
// 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({
|
||||
filter: `guildId = "${interaction.guildId}" && userId = "${targetUser.id}" && type = "citation"`
|
||||
});
|
||||
const citeByCat = new Map();
|
||||
citeEvents.forEach(e => {
|
||||
const cnt = citeByCat.get(e.categoryId) || 0;
|
||||
citeByCat.set(e.categoryId, cnt + e.amount);
|
||||
});
|
||||
const citeBreakdown = citeByCat.size > 0
|
||||
? Array.from(citeByCat.entries()).map(([cid, cnt]) => `${catMap.get(cid) || 'Unknown'}: ${cnt}`).join('\n')
|
||||
: 'None';
|
||||
let citeBreakdown = 'None';
|
||||
if (citeEvents.length > 0) {
|
||||
const eventsByCat2 = new Map();
|
||||
for (const e of citeEvents) {
|
||||
const arr = eventsByCat2.get(e.categoryId) || [];
|
||||
arr.push(e);
|
||||
eventsByCat2.set(e.categoryId, arr);
|
||||
}
|
||||
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()
|
||||
.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}`)
|
||||
@ -519,11 +551,11 @@ export const commands = [
|
||||
.setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], ephemeral });
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
client.logger.error(`Error in score command: ${error.message}`);
|
||||
client.logger.error(`[cmd:score] Error: ${error.message}`);
|
||||
try {
|
||||
await interaction.reply({ content: 'Failed to retrieve I/O score.', ephemeral });
|
||||
await interaction.editReply({ content: 'Failed to retrieve I/O score.' });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
@ -596,6 +628,11 @@ export const commands = [
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option.setName('reason')
|
||||
.setDescription('Reason for commendation')
|
||||
.setRequired(true)
|
||||
)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||
|
||||
execute: async (interaction, client) => {
|
||||
@ -615,6 +652,7 @@ export const commands = [
|
||||
}
|
||||
const targetUser = interaction.options.getUser('user');
|
||||
const categoryId = interaction.options.getString('category');
|
||||
const reason = interaction.options.getString('reason');
|
||||
const amount = 1;
|
||||
// Enforce per-category cooldown
|
||||
const cooldown = client.config.scorekeeper.cooldown || 0;
|
||||
@ -650,9 +688,11 @@ export const commands = [
|
||||
type: 'commendation',
|
||||
categoryId,
|
||||
amount,
|
||||
reason,
|
||||
awardedBy: interaction.user.id
|
||||
});
|
||||
|
||||
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) {
|
||||
client.logger.error(`Error in commend command: ${error.message}`);
|
||||
@ -679,6 +719,11 @@ export const commands = [
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option.setName('reason')
|
||||
.setDescription('Reason for citation')
|
||||
.setRequired(true)
|
||||
)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||
|
||||
execute: async (interaction, client) => {
|
||||
@ -733,9 +778,11 @@ export const commands = [
|
||||
type: 'citation',
|
||||
categoryId,
|
||||
amount,
|
||||
reason,
|
||||
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}.`);
|
||||
} catch (error) {
|
||||
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()
|
||||
.setName('listcategories')
|
||||
.setDescription('List all commendation/citation categories')
|
||||
.setDescription('List all commendation/citation categories (Admin only)')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.addBooleanOption(opt =>
|
||||
opt.setName('ephemeral')
|
||||
.setDescription('Whether the result should be ephemeral')
|
||||
|
||||
@ -16,6 +16,7 @@ export const loadModules = async (clientConfig, client) => {
|
||||
fs.mkdirSync(modulesDir, { recursive: true });
|
||||
}
|
||||
|
||||
client.logger.info(`[module:loader] Loading modules: ${modules.join(', ')}`);
|
||||
// Load each module
|
||||
for (const moduleName of modules) {
|
||||
try {
|
||||
@ -25,7 +26,7 @@ export const loadModules = async (clientConfig, client) => {
|
||||
// Fallback to core source directory
|
||||
modulePath = path.join(rootDir, '_src', `${moduleName}.js`);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -43,7 +44,7 @@ export const loadModules = async (clientConfig, client) => {
|
||||
if (command.data && typeof command.execute === 'function') {
|
||||
const commandName = command.data.name || command.name;
|
||||
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') {
|
||||
@ -60,16 +61,16 @@ export const loadModules = async (clientConfig, client) => {
|
||||
// Call init function if it exists
|
||||
if (typeof module.init === 'function') {
|
||||
await module.init(client, clientConfig);
|
||||
client.logger.info(`Module loaded: ${moduleName}`);
|
||||
client.logger.info(`[module:loader] Module initialized: ${moduleName}`);
|
||||
} 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)
|
||||
client.modules = client.modules || new Map();
|
||||
client.modules.set(moduleName, module);
|
||||
} catch (error) {
|
||||
client.logger.error(`Failed to load module ${moduleName}: ${error.message}`);
|
||||
client.logger.error(`[module:loader] Failed to load module ${moduleName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
41
config.js
41
config.js
@ -5,7 +5,7 @@ export default {
|
||||
|
||||
clients: [
|
||||
{
|
||||
id: 'IO3',
|
||||
id: 'IO',
|
||||
enabled: true,
|
||||
owner: process.env.OWNER_ID,
|
||||
|
||||
@ -51,7 +51,6 @@ export default {
|
||||
defaultModel: 'gpt-4.1',
|
||||
defaultMaxTokens: 1000,
|
||||
defaultTemperature: 0.7,
|
||||
systemPromptPath: './prompts/absolute.txt',
|
||||
conversationExpiry: 30 * 60 * 1000,
|
||||
minScore: 1.0,
|
||||
tools: {
|
||||
@ -69,9 +68,10 @@ export default {
|
||||
modules: [
|
||||
'botUtils',
|
||||
'pbUtils',
|
||||
'gitUtils',
|
||||
'responses',
|
||||
'responsesQuery',
|
||||
'gitUtils'
|
||||
'responsesPrompt',
|
||||
'responsesQuery'
|
||||
]
|
||||
|
||||
},
|
||||
@ -79,7 +79,7 @@ export default {
|
||||
{
|
||||
id: 'ASOP',
|
||||
enabled: true,
|
||||
owner: 378741522822070272,
|
||||
owner: process.env.OWNER_ID,
|
||||
|
||||
discord: {
|
||||
appId: process.env.ASOP_DISCORD_APPID,
|
||||
@ -148,7 +148,7 @@ export default {
|
||||
openAI: true,
|
||||
openAITriggerOnlyDuringIncident: true,
|
||||
openAIResponseDenominator: 1,
|
||||
openAIInstructionsFile: './prompts/kevinarby.txt',
|
||||
openAIInstructionsFile: './assets/kevinarby.txt',
|
||||
openAITriggers: [
|
||||
'kevin',
|
||||
'arby',
|
||||
@ -170,11 +170,10 @@ export default {
|
||||
defaultModel: 'gpt-4.1-mini',
|
||||
defaultMaxTokens: 1000,
|
||||
defaultTemperature: 0.7,
|
||||
systemPromptPath: './prompts/asop.txt',
|
||||
conversationExpiry: 30 * 60 * 1000,
|
||||
minScore: 0.25,
|
||||
minScore: 0.5,
|
||||
tools: {
|
||||
webSearch: true,
|
||||
webSearch: false,
|
||||
fileSearch: false,
|
||||
imageGeneration: true,
|
||||
},
|
||||
@ -195,13 +194,15 @@ export default {
|
||||
},
|
||||
|
||||
modules: [
|
||||
'botUtils',
|
||||
'pbUtils',
|
||||
'condimentX',
|
||||
'responses',
|
||||
'responsesPrompt',
|
||||
'responsesQuery',
|
||||
'scorekeeper',
|
||||
'scorekeeper-example',
|
||||
'scExecHangarStatus',
|
||||
//'condimentX'
|
||||
'scExecHangarStatus'
|
||||
]
|
||||
|
||||
},
|
||||
@ -209,7 +210,7 @@ export default {
|
||||
{
|
||||
id: 'Crowley',
|
||||
enabled: true,
|
||||
owner: 378741522822070272,
|
||||
owner: process.env.OWNER_ID,
|
||||
|
||||
discord: {
|
||||
appId: process.env.CROWLEY_DISCORD_APPID,
|
||||
@ -253,13 +254,12 @@ export default {
|
||||
defaultModel: 'gpt-4.1',
|
||||
defaultMaxTokens: 1000,
|
||||
defaultTemperature: 0.7,
|
||||
systemPromptPath: './prompts/crowley.txt',
|
||||
conversationExpiry: 30 * 60 * 1000,
|
||||
minScore: 1.0,
|
||||
minScore: 0,
|
||||
tools: {
|
||||
webSearch: true,
|
||||
webSearch: false,
|
||||
fileSearch: false,
|
||||
imageGeneration: true,
|
||||
imageGeneration: false,
|
||||
},
|
||||
imageGeneration: {
|
||||
defaultModel: 'gpt-image-1',
|
||||
@ -269,9 +269,11 @@ export default {
|
||||
},
|
||||
|
||||
modules: [
|
||||
'botUtils',
|
||||
'pbUtils',
|
||||
'responses',
|
||||
'responsesQuery',
|
||||
'responsesPrompt',
|
||||
'responsesQuery'
|
||||
]
|
||||
|
||||
},
|
||||
@ -279,7 +281,7 @@ export default {
|
||||
{
|
||||
id: 'Smuuush',
|
||||
enabled: true,
|
||||
owner: 378741522822070272,
|
||||
owner: process.env.OWNER_ID,
|
||||
|
||||
discord: {
|
||||
appId: process.env.SMUUUSH_DISCORD_APPID,
|
||||
@ -323,7 +325,6 @@ export default {
|
||||
defaultModel: 'gpt-4.1-mini',
|
||||
defaultMaxTokens: 1000,
|
||||
defaultTemperature: 0.7,
|
||||
systemPromptPath: './prompts/smuuush.txt',
|
||||
conversationExpiry: 30 * 60 * 1000,
|
||||
minScore: 0,
|
||||
tools: {
|
||||
@ -339,8 +340,10 @@ export default {
|
||||
},
|
||||
|
||||
modules: [
|
||||
'botUtils',
|
||||
'pbUtils',
|
||||
'responses',
|
||||
'responsesPrompt',
|
||||
'responsesQuery'
|
||||
],
|
||||
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
[Unit]
|
||||
Description=ClientX Discord Bot
|
||||
Description=ClientX Discord Bot via NVM-Exec
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
# 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.
|
||||
WorkingDirectory=/root/clientx
|
||||
WorkingDirectory=/home/USER/clientx
|
||||
|
||||
# Automatically restart process if it crashes.
|
||||
Restart=always
|
||||
# Wait 10 seconds before attempting a restart.
|
||||
RestartSec=10
|
||||
Restart=on-failure
|
||||
|
||||
# Run as a non-root user for security (change "nodeuser" to your configured user).
|
||||
#User=root
|
||||
#Group=root
|
||||
# Wait 10 seconds before attempting a restart.
|
||||
RestartSec=3
|
||||
|
||||
# User/Group
|
||||
User=USER
|
||||
Group=GROUP
|
||||
|
||||
# Set any environment variables if needed.
|
||||
Environment=NODE_ENV=production
|
||||
49
docs/logging-guidelines.md
Normal file
49
docs/logging-guidelines.md
Normal 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.
|
||||
5
index.js
5
index.js
@ -116,7 +116,8 @@ const startBot = async () => {
|
||||
|
||||
// Launch the bot
|
||||
startBot().then(clients => {
|
||||
console.log(`Successfully initialized ${clients.length} Discord clients`);
|
||||
console.log(`[main] Successfully initialized ${clients.length} Discord clients`);
|
||||
}).catch(error => {
|
||||
console.error('Failed to start bot:', error);
|
||||
console.error(`[main] Failed to start bot: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
{
|
||||
"watch": false
|
||||
}
|
||||
@ -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 user’s 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.
|
||||
@ -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>*
|
||||
@ -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.
|
||||
@ -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.*
|
||||
@ -1,2 +0,0 @@
|
||||
You have an insatiable love for feet.
|
||||
You are kinda creepy.
|
||||
@ -20,6 +20,7 @@ const dryRun = args.includes('--dryrun');
|
||||
// Validate required parameters
|
||||
if (args.includes('--help') || args.includes('-h') || !actionArg || !guildArg || !clientArg) {
|
||||
console.log(`
|
||||
[registry]
|
||||
Discord Command Registry Tool
|
||||
|
||||
Usage:
|
||||
@ -38,14 +39,14 @@ Examples:
|
||||
node registry.js --action=list --guild=123456789012345678 --client=IO3
|
||||
node registry.js --action=register --guild=all --client=ASOP
|
||||
node registry.js --action=unregister --guild=123456789012345678 --client=all --dryrun
|
||||
`);
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate action parameter
|
||||
const validActions = ['register', 'unregister', 'list'];
|
||||
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);
|
||||
}
|
||||
const action = actionArg.toLowerCase();
|
||||
@ -61,7 +62,7 @@ const targetClients = isClientAll
|
||||
: config.clients.filter(client => client.id === clientArg && client.enabled !== false);
|
||||
|
||||
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:');
|
||||
config.clients
|
||||
.filter(client => client.enabled !== false)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user