Project Files
src / sources / adapters / lmStudioConversationSourceAdapter.ts
import fs from "fs";
import path from "path";
import type { SourceAdapter, SourceAdapterContext, SourceDocument } from "../types.js";
import { convertLmStudioConversationToMarkdown, defaultLmStudioHome } from "../lmStudioConversationMarkdown.js";
interface ConversationTarget {
conversationsDir: string;
chatId?: string;
lmStudioHome: string;
}
interface ConversationRenderOptions {
includeThinking?: boolean;
includeToolCalls?: boolean;
}
export class LmStudioConversationSourceAdapter implements SourceAdapter {
canHandle(source: string): boolean {
return parseConversationTarget(source) !== null;
}
async load(source: string, context: SourceAdapterContext): Promise<SourceDocument[]> {
const target = parseConversationTarget(source);
if (!target) return [];
const files = target.chatId
? [path.join(target.conversationsDir, `${target.chatId}.conversation.json`)]
: enumerateConversationFiles(target.conversationsDir, context.maxPages);
const docs: SourceDocument[] = [];
for (const filePath of files.slice(0, context.maxPages)) {
try {
docs.push(await loadConversationFile(filePath, target.lmStudioHome, {
includeThinking: false,
includeToolCalls: false,
}));
} catch (err) {
console.warn(`[sources/conversation] failed to load ${filePath}:`, String(err));
}
}
return docs;
}
}
export function isLmStudioConversationSource(source: string): boolean {
return parseConversationTarget(source) !== null;
}
export async function loadConversationFile(
filePath: string,
lmStudioHome: string,
renderOptions: ConversationRenderOptions = {}
): Promise<SourceDocument> {
const chatId = path.basename(filePath).replace(/\.conversation\.json$/i, "");
if (!/^\d{13}$/.test(chatId)) throw new Error(`invalid LM Studio chat id: ${chatId}`);
const raw = await fs.promises.readFile(filePath, "utf8");
const payload = JSON.parse(raw);
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
const workingDirectory = path.join(lmStudioHome, "working-directories", chatId);
const markdown = convertLmStudioConversationToMarkdown(messages, {
includeThinking: renderOptions.includeThinking ?? false,
includeToolCalls: renderOptions.includeToolCalls ?? false,
lmStudioHome,
workingDirectory,
});
const stat = await fs.promises.stat(filePath);
return {
sourceId: `lmstudio-conversation://${chatId}`,
sourceKind: "conversation",
canonicalUrl: filePath,
title: conversationTitle(payload, chatId),
rawContent: markdown,
rawContentType: "markdown",
baseUrl: workingDirectory + path.sep,
fetchedAt: new Date().toISOString(),
version: `${stat.mtimeMs}:${stat.size}`,
metadata: {
chatId,
conversationPath: filePath,
workingDirectory,
lmStudioHome,
messageCount: messages.length,
},
};
}
function enumerateConversationFiles(conversationsDir: string, maxPages: number): string[] {
if (!fs.existsSync(conversationsDir)) return [];
const entries = fs.readdirSync(conversationsDir, { withFileTypes: true });
return entries
.filter(entry => entry.isFile() && /^\d{13}\.conversation\.json$/.test(entry.name))
.map(entry => path.join(conversationsDir, entry.name))
.sort((a, b) => safeMtimeMs(b) - safeMtimeMs(a))
.slice(0, Math.max(1, maxPages));
}
function safeMtimeMs(filePath: string): number {
try {
return fs.statSync(filePath).mtimeMs;
} catch {
return 0;
}
}
function parseConversationTarget(source: string): ConversationTarget | null {
const value = source.trim();
if (!value) return null;
const scheme = /^lmstudio-conversations?:\/\/(?:([^/]+))?$/i.exec(value);
if (scheme) {
const lmStudioHome = defaultLmStudioHome();
return {
conversationsDir: path.join(lmStudioHome, "conversations"),
chatId: scheme[1],
lmStudioHome,
};
}
const fileMatch = /^(.*?)(?:\/)?(\d{13})\.conversation\.json$/i.exec(value);
if (fileMatch && path.isAbsolute(value)) {
const conversationsDir = path.dirname(value);
return {
conversationsDir,
chatId: fileMatch[2],
lmStudioHome: inferLmStudioHomeFromConversationsDir(conversationsDir),
};
}
if (path.isAbsolute(value) && path.basename(value) === "conversations") {
return {
conversationsDir: value,
lmStudioHome: inferLmStudioHomeFromConversationsDir(value),
};
}
return null;
}
function inferLmStudioHomeFromConversationsDir(conversationsDir: string): string {
return path.basename(conversationsDir) === "conversations" ? path.dirname(conversationsDir) : defaultLmStudioHome();
}
function conversationTitle(payload: any, chatId: string): string {
const candidates = [
payload?.title,
payload?.name,
payload?.conversationName,
payload?.metadata?.title,
payload?.meta?.title,
];
for (const candidate of candidates) {
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
}
return `LM Studio Conversation ${chatId}`;
}