src / generator.ts
// src/generator.ts
import { type Chat, type GeneratorController } from "@lmstudio/sdk";
import { copyFile, writeFile } from "fs/promises";
import { existsSync } from "fs";
import { basename, join } from "path";
import { configSchematics } from "./config";
import { ensureDir, readJson, safeFileSize, validateChatId, convertMessagesToMarkdown } from "./tools/common";
import { getWorkingDirectoryInfo } from "./workingDirectoryInfo";
import { embedLocalImagesInMarkdown } from "./embedLocalImages";
export async function generate(ctl: GeneratorController, _chat: Chat) {
// Read per-chat config (acts as defaults)
const cfg = ctl.getPluginConfig(configSchematics);
const shouldOverwrite = cfg.get("overwrite");
const includeThinking = cfg.get("includeThinking");
const includeToolCalls = cfg.get("includeToolCalls");
const emitMessagesJson = cfg.get("emitMessagesJson");
const embedImages = cfg.get("embedImages");
const workingDirectory = ctl.getWorkingDirectory();
const wdInfo = getWorkingDirectoryInfo(workingDirectory);
const chatId = basename(workingDirectory);
validateChatId(chatId);
if (!wdInfo.looksLikeLMStudioWorkingDir) {
ctl.fragmentGenerated(
`Error: unexpected working directory layout.\n` +
`Working directory (raw): ${wdInfo.workingDirectoryRaw}\n` +
`Working directory (trimmed): ${wdInfo.workingDirectoryTrimmed}\n` +
`parentDirName: ${wdInfo.parentDirName}\n` +
`chatId: ${wdInfo.chatId} (valid13Digits=${wdInfo.chatIdValid13Digits})`
);
return;
}
const lmHome = wdInfo.lmHomeFromWorkingDir;
if (!lmHome) {
ctl.fragmentGenerated(
`Error: unable to derive LM Studio home from working directory.\n` +
`Working directory (raw): ${wdInfo.workingDirectoryRaw}\n` +
`Working directory (trimmed): ${wdInfo.workingDirectoryTrimmed}\n` +
`parentDirName: ${wdInfo.parentDirName}\n` +
`chatId: ${wdInfo.chatId} (valid13Digits=${wdInfo.chatIdValid13Digits})`
);
return;
}
const globalSource = join(lmHome, "conversations", `${chatId}.conversation.json`);
const localJson = join(workingDirectory, `${chatId}.conversation.json`);
const destMd = join(workingDirectory, `${chatId}.conversation.md`);
const destMessagesJson = join(workingDirectory, `${chatId}.conversation.messages.json`);
// Minimal status in the chat UI
ctl.fragmentGenerated(`Exporting chat ${chatId}… [embedImages=${embedImages}, emitMessagesJson=${emitMessagesJson}]`);
// 1) Always refresh JSON
if (!existsSync(globalSource)) {
ctl.fragmentGenerated(
`Error: conversation file not found: ${globalSource}\n` +
`Working directory (raw): ${wdInfo.workingDirectoryRaw}\n` +
`Working directory (trimmed): ${wdInfo.workingDirectoryTrimmed}\n` +
`isAbsolute: ${wdInfo.isAbsolute}\n` +
`resolvedPath: ${wdInfo.resolvedPath}`
);
return;
}
await ensureDir(workingDirectory);
await copyFile(globalSource, localJson);
const jsonBytes = await safeFileSize(localJson);
// 2) Respect overwrite for rendered outputs
if (!shouldOverwrite && (existsSync(destMd) || (emitMessagesJson && existsSync(destMessagesJson)))) {
ctl.fragmentGenerated(
`Skipped conversion (overwrite=false). JSON refreshed (${jsonBytes} bytes).\n` +
`Markdown: ${destMd}\n` +
(emitMessagesJson ? `Messages JSON: ${destMessagesJson}\n` : "")
);
return;
}
// 3) Convert to Markdown (and optional messages.json)
const data = await readJson(localJson);
const messages = Array.isArray((data as any)?.messages) ? (data as any).messages : [];
if (emitMessagesJson) {
const payload = JSON.stringify({ messages }, null, 2);
await writeFile(destMessagesJson, payload, "utf-8");
}
let markdown = convertMessagesToMarkdown(messages, {
includeThinking,
includeToolCalls,
embedImages,
});
if (embedImages) {
markdown = await embedLocalImagesInMarkdown(markdown, workingDirectory);
}
await writeFile(destMd, markdown.endsWith("\n") ? markdown : markdown + "\n", "utf-8");
const mdBytes = await safeFileSize(destMd);
ctl.fragmentGenerated(
`Export complete. Files:\n` +
`- ${destMd} (${mdBytes} bytes)\n` +
(emitMessagesJson ? `- ${destMessagesJson}\n` : "")
);
}