Add text features: trusted triggers, context cache, response policy, MCP config tools

This commit is contained in:
Fisto
2026-09-11 15:31:23 +02:00
parent 23a1a8e593
commit 82f78bb2b4
20 changed files with 841 additions and 88 deletions
+90
View File
@@ -6,6 +6,16 @@
* `whitelist` commands and normalizes @mentions.
*/
export interface CachedMessage {
id: string;
authorId: string;
authorName: string;
content: string;
timestamp: string;
/** Attachment file names only. Bodies stay on Discord. */
attachments?: string[];
}
export interface PuppetMessage {
id: string;
channelId: string;
@@ -18,6 +28,8 @@ export interface PuppetMessage {
trusted: boolean;
/** Parsed command text when the message is one. */
command?: string;
/** Prior messages for context. Present on mention triggers. */
context?: CachedMessage[];
}
export const TRUSTED_LOG_LIMIT = 100;
@@ -81,3 +93,81 @@ export function isTrusted(
export function randomActionDelay(): number {
return 500 + Math.floor(Math.random() * 2301);
}
/* Context assembly */
export interface ContextOptions {
maxMessages?: number;
maxChars?: number;
/** Skip this message ID. Use it to keep the trigger out of its own context. */
excludeId?: string;
}
export interface AssembledContext {
messages: CachedMessage[];
chars: number;
truncated: boolean;
}
export function formatContextLine(message: CachedMessage): string {
const parts: string[] = [];
if (message.content) parts.push(message.content);
for (const name of message.attachments ?? []) parts.push(`[file: ${name}]`);
return `${message.authorName}: ${parts.join(" ")}`.trim();
}
/** Assemble the newest messages inside a message count limit and a character limit. */
export function assembleContext(messages: readonly CachedMessage[], options: ContextOptions = {}): AssembledContext {
const maxMessages = options.maxMessages ?? 15;
const maxChars = options.maxChars ?? 4000;
const selected: CachedMessage[] = [];
let chars = 0;
let truncated = false;
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (options.excludeId && message.id === options.excludeId) continue;
if (selected.length >= maxMessages) {
truncated = true;
break;
}
const line = formatContextLine(message);
if (selected.length > 0 && chars + line.length > maxChars) {
truncated = true;
break;
}
selected.push(message);
chars += line.length;
}
selected.reverse();
return { messages: selected, chars, truncated };
}
/** Compare two snowflake IDs. Falls back to string compare for invalid input. */
export function compareSnowflakes(a: string, b: string): number {
try {
const left = BigInt(a);
const right = BigInt(b);
return left < right ? -1 : left > right ? 1 : 0;
} catch {
return a < b ? -1 : a > b ? 1 : 0;
}
}
/** Convert a core `message.fetch` or `message.read` message into a cached message. */
export function toCachedMessage(raw: any): CachedMessage {
return {
id: String(raw.id),
authorId: String(raw.author?.id ?? ""),
authorName: String(raw.author?.username ?? raw.author?.id ?? "unknown"),
content: String(raw.content ?? ""),
timestamp: String(raw.timestamp ?? ""),
attachments: Array.isArray(raw.attachments)
? raw.attachments.map((attachment: any) => String(attachment?.filename ?? attachment)).filter(Boolean)
: undefined
};
}