Project Files
dist / tools / common.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateChatId = validateChatId;
exports.ensureDir = ensureDir;
exports.safeFileSize = safeFileSize;
exports.readJson = readJson;
exports.convertMessagesToMarkdown = convertMessagesToMarkdown;
const promises_1 = require("fs/promises");
const path_1 = require("path");
const os_1 = require("os");
const fs_1 = require("fs");
function validateChatId(id) {
if (!/^[0-9]{13}$/.test(id)) {
throw new Error("Invalid chat_id. Must be exactly 13 digits (0-9).");
}
}
async function ensureDir(dir) {
try {
await (0, promises_1.mkdir)(dir, { recursive: true });
}
catch {
}
}
async function safeFileSize(path) {
try {
const s = await (0, promises_1.stat)(path);
return s.size;
}
catch {
return 0;
}
}
async function readJson(path) {
const buf = await (0, promises_1.readFile)(path);
return JSON.parse(buf.toString("utf-8"));
}
function renderRole(heading, content) {
const texts = toTextArray(content);
if (texts.length === 0)
return "";
return `### ${heading}\n${texts.join("\n")}\n\n`;
}
function convertMessagesToMarkdown(messages, opts) {
let md = "";
const allSteps = [];
for (const msg of messages) {
const ver = selectVersion(msg?.versions);
if (ver && Array.isArray(ver.steps)) {
allSteps.push(...ver.steps);
}
}
for (const msg of messages) {
const version = selectVersion(msg?.versions);
if (!version || !version.role)
continue;
if (version.role === "assistant") {
const heading = getAssistantHeading(version, "Assistant");
md += renderAssistant(version, { includeThinking: opts.includeThinking, includeToolCalls: opts.includeToolCalls }, heading, allSteps);
}
else if (version.role === "user") {
md += renderUser(version, opts.embedImages);
}
else if (version.role === "system") {
md += renderRole("System", version.content);
}
else if (version.role === "tool") {
md += renderRole("Tool", version.content);
}
else {
md += renderRole(String(version.role), version.content);
}
}
return md;
}
function selectVersion(versions) {
if (!Array.isArray(versions) || versions.length === 0)
return null;
return versions[versions.length - 1];
}
function toTextArray(value) {
if (value == null)
return [];
if (Array.isArray(value))
return value.map(v => v?.text ?? JSON.stringify(v)).filter(Boolean);
if (typeof value === "string")
return [value];
if (typeof value === "object" && "text" in value)
return [value.text];
return [JSON.stringify(value)];
}
function stripAssistantMarkers(s) {
return s
.replace(/<\|start\|>assistant<\|channel\|>final<\|message\|>/g, "")
.replace(/<\|channel\|>final<\|message\|>/g, "");
}
function getAssistantHeading(version, fallback = "Assistant") {
const candidate = version?.senderInfo?.senderName ??
version?.modelId ??
version?.model?.id ??
version?.model ??
version?.provider ??
version?.meta?.modelId ??
null;
return typeof candidate === "string" && candidate.trim() ? candidate : fallback;
}
function parseToolCallsFromSteps(steps, extraSteps) {
const byId = {};
const order = [];
const ensure = (id) => {
if (!id)
return undefined;
if (!byId[id]) {
byId[id] = { callId: id };
order.push(id);
}
return byId[id];
};
const asId = (v) => (v != null ? String(v) : undefined);
const combined = [
...(Array.isArray(steps) ? steps : []),
...(Array.isArray(extraSteps) ? extraSteps : []),
];
for (const step of combined) {
if (step?.type === "toolCallRequest") {
const id = asId(step.callId) ?? asId(step.toolCallRequestId);
const entry = ensure(id) ?? {};
entry.callId = entry.callId ?? id;
entry.name = step.name ?? entry.name;
entry.args = step.parameters ?? entry.args;
entry.plugin = step.pluginIdentifier ?? entry.plugin;
entry.status = entry.status ?? "requested";
continue;
}
if (step?.type === "contentBlock" && Array.isArray(step?.content)) {
for (const c of step.content) {
if (c?.type === "toolCallRequest") {
const id = asId(c.callId) ??
asId(step.callId) ??
asId(c.toolCallRequestId) ??
asId(step.toolCallRequestId) ??
undefined;
const entry = ensure(id) ?? {};
entry.callId = entry.callId ?? id;
entry.name = c.name ?? entry.name;
entry.args = c.parameters ?? entry.args;
entry.plugin = step.pluginIdentifier ?? c.pluginIdentifier ?? entry.plugin;
entry.status = entry.status ?? "requested";
}
if (typeof c?.type === "string" && /tool.*(result|response|output)/i.test(c.type)) {
const id = asId(c.callId) ?? asId(step.callId) ?? asId(c.toolCallRequestId);
const entry = ensure(id);
if (entry) {
entry.result = c.result ?? c.output ?? c.data ?? c.content ?? c.text ?? c;
entry.status = entry.status ?? "succeeded";
}
}
}
}
if (typeof step?.type === "string" && /tool.*(result|response|output|call)/i.test(step.type)) {
const id = asId(step.callId) ?? asId(step.toolCallRequestId);
const entry = ensure(id);
if (entry) {
entry.result = step.result ?? step.output ?? step.data ?? step.content ?? step.text ?? step;
entry.status = entry.status ?? "succeeded";
}
}
if (step?.type === "toolStatus") {
const id = asId(step.callId);
const entry = ensure(id);
if (!entry)
continue;
const st = step?.statusState?.status;
const t = st?.type;
if (t === "toolCallFailed") {
entry.status = "failed";
entry.error = st?.error ?? st;
}
else if (t === "toolCallSucceeded") {
entry.status = "succeeded";
if (st?.result !== undefined) {
entry.result = st.result;
}
}
else if (typeof t === "string") {
entry.status = t;
}
}
}
return order.map(id => byId[id]);
}
function extractCallIdsFromStep(step) {
const ids = [];
const pushId = (v) => {
if (v != null)
ids.push(String(v));
};
if (!step)
return ids;
if (step.callId)
pushId(step.callId);
if (step.toolCallRequestId)
pushId(step.toolCallRequestId);
if (Array.isArray(step.content)) {
for (const c of step.content) {
if (!c)
continue;
if (c.callId)
pushId(c.callId);
if (c.toolCallRequestId)
pushId(c.toolCallRequestId);
}
}
if (step.type === "toolCallRequest") {
if (step.callId)
pushId(step.callId);
if (step.toolCallRequestId)
pushId(step.toolCallRequestId);
}
return ids;
}
function gatherCallIdsFromSteps(steps) {
const set = new Set();
if (!Array.isArray(steps))
return set;
for (const s of steps) {
for (const id of extractCallIdsFromStep(s))
set.add(id);
}
return set;
}
function buildToolCallBlock(c) {
const title = c.status === "failed"
? `Tool call failed for ${c.name ?? "Unknown tool"}`
: `${c.name ?? "Tool call"}`;
const argsMd = c.args !== undefined
? "```json\n" + JSON.stringify(c.args, null, 2) + "\n```"
: "_no arguments_";
let inner = `- Arguments:\n\n${argsMd}\n`;
if (c.error !== undefined) {
const errMd = typeof c.error === "string" ? c.error : JSON.stringify(c.error, null, 2);
inner += `\n- Errors:\n\n\`\`\`\n${errMd}\n\`\`\`\n`;
}
else if (c.result !== undefined) {
let resContent = c.result;
if (resContent && typeof resContent === "object" && typeof resContent.content === "string") {
try {
const parsed = JSON.parse(resContent.content);
resContent = parsed;
}
catch {
resContent = resContent.content;
}
}
const resMd = typeof resContent === "string" ? resContent : JSON.stringify(resContent, null, 2);
if (typeof resContent === "string" && resContent.trim().startsWith("{")) {
inner += `\n- Result:\n\n\`\`\`\n${resContent}\n\`\`\`\n`;
}
else {
inner += `\n- Result:\n\n\`\`\`json\n${resMd}\n\`\`\`\n`;
}
}
return `<details>\n<summary>${title}</summary>\n\n${inner}</details>`;
}
function renderAssistant(version, opts, headingLabel = "Assistant", globalSteps) {
let contentBody = "";
let thinkingBlock = "";
if (Array.isArray(version?.steps)) {
let callsById;
if (opts.includeToolCalls) {
const localCallIds = gatherCallIdsFromSteps(version.steps);
let extraFiltered = undefined;
if (localCallIds.size > 0 && Array.isArray(globalSteps)) {
extraFiltered = globalSteps.filter((s) => {
const ids = extractCallIdsFromStep(s);
for (const id of ids) {
if (localCallIds.has(id))
return true;
}
return false;
});
if (extraFiltered.length === 0)
extraFiltered = undefined;
}
const calls = parseToolCallsFromSteps(version.steps, extraFiltered);
callsById = new Map(calls.filter(c => c.callId != null).map(c => [String(c.callId), c]));
}
for (const step of version.steps) {
if (step?.type === "contentBlock") {
if (step?.style?.type === "thinking" && opts.includeThinking) {
const title = step?.style?.title ?? "Thinking";
const think = Array.isArray(step?.content)
? step.content[0]?.text ?? ""
: step?.content?.text ?? "";
thinkingBlock += `<details>\n<summary>${title}</summary>\n\n${think}\n\n</details>\n\n`;
}
else {
const texts = Array.isArray(step?.content)
? step.content.map((c) => c?.text).filter(Boolean)
: toTextArray(step?.content);
if (texts.length > 0) {
let joined = stripAssistantMarkers(texts.join("\n"));
joined = joined.replace(/(!\[.*?\]\([^)]*\))\n(?!\n)/g, "$1\n\n");
contentBody += joined;
if (!contentBody.endsWith("\n"))
contentBody += "\n";
}
if (opts.includeToolCalls && callsById != null && Array.isArray(step?.content)) {
for (const c of step.content) {
if (c?.type !== "toolCallRequest")
continue;
const id = c.callId != null ? String(c.callId) :
c.toolCallRequestId != null ? String(c.toolCallRequestId) :
undefined;
const call = id != null ? callsById.get(id) : undefined;
if (call != null && id != null) {
contentBody += "\n" + buildToolCallBlock(call) + "\n\n";
callsById.delete(id);
}
}
}
}
}
else if (step?.content?.text) {
contentBody += step.content.text;
if (!contentBody.endsWith("\n"))
contentBody += "\n";
}
}
if (opts.includeToolCalls && callsById != null && callsById.size > 0) {
for (const call of callsById.values()) {
contentBody += "\n" + buildToolCallBlock(call) + "\n\n";
}
}
}
else if (Array.isArray(version?.content)) {
contentBody += toTextArray(version.content).join("\n");
if (contentBody && !contentBody.endsWith("\n"))
contentBody += "\n";
}
if (opts.includeThinking && version?.thinking && typeof version.thinking === "string") {
thinkingBlock += `<details>\n<summary>Thinking</summary>\n\n${version.thinking}\n\n</details>\n\n`;
}
let body = "";
if (thinkingBlock)
body += thinkingBlock;
if (contentBody) {
body += contentBody.endsWith("\n\n") ? contentBody : contentBody.trimEnd() + "\n\n";
}
return body ? `### ${headingLabel}\n${body}` : "";
}
function renderUser(version, embedImages) {
const { texts, images } = extractUserContent(version, embedImages);
let body = "";
if (texts.length > 0) {
body += texts.join("\n") + "\n\n";
}
if (images.length > 0) {
for (let i = 0; i < images.length; i++) {
const img = images[i];
const alt = (img.alt && String(img.alt).trim()) || `User image ${i + 1}`;
body += `\n\n`;
}
}
return body ? `### User\n${body}` : "";
}
function extractUserContent(version, embedImages) {
const texts = [];
const images = [];
const textSet = new Set();
const imageSet = new Set();
const pushText = (t) => {
let s;
if (typeof t === "string")
s = t;
else if (t && typeof t === "object" && typeof t.text === "string")
s = t.text;
if (typeof s === "string") {
const v = s.trim();
if (v && !textSet.has(v)) {
textSet.add(v);
texts.push(v);
}
}
};
const normalizeUrlForMd = (url) => {
if (/^(data:|https?:)/i.test(url))
return url;
if (/^(\/|[A-Za-z]:[\\/])/.test(url))
return url;
return url;
};
const pushImage = (url, alt) => {
const u = normalizeUrlForMd(url);
if (!imageSet.has(u)) {
imageSet.add(u);
images.push({ url: u, alt });
}
};
const candidateFilenameFromFile = (obj) => {
const v = obj.identifier ??
obj.fileIdentifier ??
obj.name ??
obj.filename ??
obj.url ??
obj.path ??
obj.filePath;
return typeof v === "string" && v.trim() ? v : undefined;
};
const pushImageFromFileItem = (obj) => {
if (!obj || typeof obj !== "object")
return;
const t = typeof obj.type === "string" ? obj.type.toLowerCase() : "";
const ft = typeof obj.fileType === "string" ? obj.fileType.toLowerCase() : "";
if (t === "file" || ft) {
if (ft === "image" || t === "file") {
const filename = candidateFilenameFromFile(obj);
if (filename) {
const embedded = embedImages ? buildDataUrlFromMetadata(filename) : undefined;
const url = embedded ?? (0, path_1.join)((0, os_1.homedir)(), ".lmstudio", "user-files", filename);
const alt = obj.alt ?? obj.name ?? obj.filename ?? obj.identifier ?? obj.fileIdentifier;
pushImage(url, alt);
}
}
}
};
const pushImageFrom = (obj) => {
if (!obj || typeof obj !== "object")
return;
const directUrl = obj.url ??
obj.image_url ??
obj.imageURL ??
obj.source?.url ??
obj.image?.url ??
(typeof obj.image_url === "object" ? obj.image_url?.url : undefined);
if (typeof directUrl === "string" && directUrl.trim()) {
pushImage(directUrl, obj.alt ?? obj.image?.alt ?? obj.name ?? obj.filename);
return;
}
const base64 = obj.data ??
obj.b64 ??
obj.base64 ??
obj.image_base64 ??
obj.image?.data ??
obj.source?.data;
if (typeof base64 === "string" && base64.trim()) {
const mime = obj.mimeType ??
obj.mimetype ??
obj.contentType ??
obj.image?.mimeType ??
obj.source?.mimeType ??
"image/png";
const dataUrl = base64.startsWith("data:") ? base64 : `data:${mime};base64,${base64}`;
pushImage(dataUrl, obj.alt ?? obj.image?.alt ?? obj.name ?? obj.filename);
return;
}
if (obj.type === "file" || obj.fileType) {
pushImageFromFileItem(obj);
}
};
const scanContent = (content) => {
if (!content)
return;
if (typeof content === "string") {
pushText(content);
return;
}
if (Array.isArray(content)) {
for (const part of content) {
if (!part)
continue;
const t = typeof part.type === "string" ? part.type.toLowerCase() : "";
if (t.includes("text") || "text" in part) {
pushText(part.text ?? part.value ?? part.content ?? part);
continue;
}
if (t === "file" || (typeof part.fileType === "string" && part.fileType.toLowerCase() === "image")) {
pushImageFromFileItem(part);
continue;
}
if (t.includes("image") || "image_url" in part || "url" in part) {
pushImageFrom(part);
continue;
}
if (typeof part === "object") {
if (typeof part.text === "string")
pushText(part.text);
else
pushImageFrom(part);
}
}
return;
}
if (typeof content === "object") {
const t = typeof content.type === "string" ? content.type.toLowerCase() : "";
if (t.includes("text") || typeof content.text === "string") {
pushText(content.text ?? content.content ?? content);
}
else if (t === "file" || (typeof content.fileType === "string" && content.fileType.toLowerCase() === "image")) {
pushImageFromFileItem(content);
}
else if (t.includes("image") || "image_url" in content || "url" in content) {
pushImageFrom(content);
}
else if (Array.isArray(content.parts)) {
scanContent(content.parts);
}
}
};
const root = version?.preprocessed?.content ?? version?.content;
scanContent(root);
if (Array.isArray(version?.attachments)) {
for (const att of version.attachments) {
const t = typeof att?.type === "string" ? att.type.toLowerCase() : "";
if (t === "file" || (typeof att?.fileType === "string" && att.fileType.toLowerCase() === "image")) {
pushImageFromFileItem(att);
}
else if (t.includes("image") || "url" in (att ?? {}) || "image_url" in (att ?? {})) {
pushImageFrom(att);
}
}
}
if (Array.isArray(version?.steps)) {
for (const step of version.steps) {
if (step?.type === "contentBlock") {
scanContent(step?.content);
}
}
}
return { texts, images };
}
function buildDataUrlFromMetadata(filename) {
try {
const root = (0, path_1.join)((0, os_1.homedir)(), ".lmstudio", "user-files");
const metaPath = (0, path_1.join)(root, `${filename}.metadata.json`);
if (!(0, fs_1.existsSync)(metaPath))
return undefined;
const raw = (0, fs_1.readFileSync)(metaPath, "utf-8");
const meta = JSON.parse(raw);
const previewData = typeof meta?.preview?.data === "string" ? meta.preview.data.trim() : undefined;
if (previewData) {
if (previewData.startsWith("data:"))
return previewData;
const mimePreview = meta?.preview?.mimeType ??
meta?.mimeType ??
meta?.mimetype ??
meta?.contentType ??
"image/png";
return `data:${mimePreview};base64,${previewData}`;
}
const mime = meta?.mimeType ??
meta?.mimetype ??
meta?.contentType ??
meta?.type ??
meta?.image?.mimeType ??
meta?.image?.type ??
"image/png";
if (typeof meta?.content === "string") {
const s = meta.content.trim();
if (s.startsWith("data:"))
return s;
}
let b64 = meta?.base64 ??
meta?.b64 ??
meta?.data ??
meta?.image?.base64 ??
meta?.image?.b64 ??
meta?.image?.data ??
meta?.content?.base64 ??
meta?.content?.data ??
meta?.payload;
if (typeof b64 === "string" && b64.trim()) {
const s = b64.trim();
if (s.startsWith("data:"))
return s;
return `data:${mime};base64,${s}`;
}
return undefined;
}
catch {
return undefined;
}
}
function escapeMarkdownAlt(s) {
return s.replace(/\]/g, "\\]");
}