Forked from promptpirate/chat-auto-exporter
src / promptPreprocessor.ts
import {
type PromptPreprocessorController,
type ChatMessage,
} from "@lmstudio/sdk";
import { globalConfigSchematics } from "./config";
import { writeFile, mkdir, readdir } from "fs/promises";
import { join } from "path";
import { createHash } from "crypto";
/**
* This function runs every time you press "Send" in a chat.
* It peeks at the conversation, counts messages, and exports
* to .md when the threshold is hit.
*
* IMPORTANT: We never modify the user's message β we just
* observe and export, then pass it through unchanged.
*/
export async function preprocess(
ctl: PromptPreprocessorController,
userMessage: ChatMessage,
): Promise<ChatMessage> {
// --- 1. Pull the chat history (does NOT include the current message) ---
const history = await ctl.pullHistory();
// --- 2. Count only user + assistant messages (skip system/tool) ---
const allMessages = history.getMessagesArray();
const chatMessages = allMessages.filter(
(msg) => msg.getRole() === "user" || msg.getRole() === "assistant",
);
const messageCount = chatMessages.length;
ctl.debug(`[chat-auto-exporter] History has ${messageCount} messages (not counting this one yet)`);
// --- 3. Read the user's settings ---
const globalConfig = ctl.getGlobalPluginConfig(globalConfigSchematics);
const exportFolder = globalConfig.get("exportFolder");
const threshold = globalConfig.get("messageThreshold");
// --- 4. Check: is the count a multiple of the threshold? ---
if (threshold <= 0 || messageCount === 0 || messageCount % threshold !== 0) {
// Not time to export yet β pass the message through unchanged
return userMessage;
}
// --- 5. Make sure the user set an export folder ---
if (!exportFolder || exportFolder.trim() === "") {
ctl.debug("[chat-auto-exporter] Export folder not set! Skipping export.");
return userMessage;
}
// --- 6. Build a sortable, RAG-friendly filename ---
// Fingerprint = hash of first user message (same chat = same hash)
const firstUserMsg = chatMessages.find((msg) => msg.getRole() === "user");
const fingerprint = firstUserMsg
? createHash("md5").update(firstUserMsg.getText()).digest("hex").slice(0, 8)
: "unknown";
// Check if a file for this chat already exists (any timestamp prefix)
// If yes β overwrite that same file (keeps the original date in the name)
// If no β create a new file with today's date+time prefix
await mkdir(exportFolder, { recursive: true });
const existingFiles = await readdir(exportFolder);
const existingFile = existingFiles.find((f) => f.includes(`chat-${fingerprint}.md`));
let filename: string;
if (existingFile) {
filename = existingFile; // reuse the old name (preserves original date)
} else {
const now = new Date();
const datePart = now.toISOString().slice(0, 10);
const timePart = now.toTimeString().slice(0, 5).replace(":", "");
filename = `${datePart}_${timePart}_chat-${fingerprint}.md`;
}
// --- 7. Format the conversation as Markdown ---
// Include the current user message in the export too
const exportMessages = [...chatMessages, userMessage];
const exportTime = new Date();
const timestamp = exportTime.toISOString().replace("T", " ").split(".")[0];
let markdown = `# Chat Export\n\n`;
markdown += `*Exported on: ${timestamp}* \n`;
markdown += `*Messages: ${exportMessages.length}*\n\n`;
markdown += `---\n\n`;
for (const msg of exportMessages) {
const role = msg.getRole();
const label = role === "user" ? "π§ User" : "π€ Assistant";
const text = msg.getText();
markdown += `### ${label}\n\n${text}\n\n---\n\n`;
}
// --- 8. Write the .md file to disk ---
try {
const filePath = join(exportFolder, filename);
await writeFile(filePath, markdown, "utf-8");
ctl.debug(`[chat-auto-exporter] Exported ${exportMessages.length} messages to: ${filePath}`);
} catch (err) {
ctl.debug(`[chat-auto-exporter] Export failed: ${err}`);
}
// --- 9. Return the message unchanged (we're just observing!) ---
return userMessage;
}