src / promptPreprocessor.ts
import { type ChatMessage, type PromptPreprocessorController } from "@lmstudio/sdk";
import { loadPermissions } from "./permissions";
export function currentTimeString(): string {
return new Date().toLocaleString("en-US", {
weekday: "short", year: "numeric", month: "short", day: "numeric",
hour: "2-digit", minute: "2-digit", second: "2-digit", timeZoneName: "short",
});
}
/** Fetch display name, size in GB, and param string for the current model. */
async function getModelLine(ctl: PromptPreprocessorController): Promise<string> {
try {
const model = await ctl.client.llm.model();
const info = await model.getModelInfo();
if (!info) return "";
const gb = (info.sizeBytes / 1_073_741_824).toFixed(1) + " GB";
const params = info.paramsString ? ` ยท ${info.paramsString} params` : "";
return `${info.displayName} ยท ${gb}${params}`;
} catch {
return "";
}
}
function buildFullGuide(time: string, modelLine: string): string {
const perms = loadPermissions();
const readWrite: string[] = [];
const readOnly: string[] = [];
const denied: string[] = [];
for (const [path, rule] of Object.entries(perms.rules)) {
if (rule === "read_write") readWrite.push(path);
else if (rule === "read") readOnly.push(path);
else denied.push(path);
}
const scopeLines: string[] = [];
if (readWrite.length) scopeLines.push(` read_write : ${readWrite.join(", ")}`);
if (readOnly.length) scopeLines.push(` read : ${readOnly.join(", ")}`);
if (denied.length) scopeLines.push(` deny : ${denied.join(", ")}`);
scopeLines.push(` default : ${perms.default}`);
const cmdStatus = perms.command_executor.enabled ? "enabled" : "disabled";
const modelStr = modelLine ? `\nModel: ${modelLine}` : "";
return `\
[agent-toolkit]
Time: ${time}${modelStr}
Tools:
web_search <query> [page_size] [page] โ DuckDuckGo web search โ [title, URL] list
image_search <query> [count] โ DuckDuckGo image search โ inline images
visit_website <url> [find_in_page] โ fetch page: title, links, images, text
view_images <urls[]> โ download & display images inline
get_current_time โ current date/time
get_model_info โ name, size, context length, capabilities
open_control_panel โ open the permissions/settings GUI
media_control <action> [level] โ play/pause/toggle/next/previous/volume/mute/status
run_javascript <code> [timeout_s] โ Deno sandbox: maths, logic, data (no net/env/subprocess)
read_file <path> โ whole file
read_file_lines <path> <start> <end> โ line range (prefer for large files)
write_file <path> <content> โ create / overwrite
edit_file <path> <old> <new> โ substring replace (prefer for edits)
edit_file_lines <path> <start> <end> <c> โ line-range replace
list_directory <path> [recursive] โ list folder
create_directory <path>
delete_path <path> [recursive]
move_path <src> <dst>
run_command <cmd> [working_directory] โ shell (${cmdStatus})
get_permissions โ show full policy
Permission tiers (files and commands share the same rules):
read_write โ full access; write commands allowed
read โ read-only: ls/cat/grep/find/โฆ; no write commands
deny โ invisible; no commands
Accessible paths:
${scopeLines.join("\n")}
Notes: ~ is expanded ยท deny list always wins ยท write-impact commands verify path perms
When showing file edits to the user, always wrap the diff output in a \`\`\`diff code block so changes are rendered with green/red highlighting.`;
}
export async function promptPreprocessor(
ctl: PromptPreprocessorController,
userMessage: ChatMessage,
): Promise<string> {
const original = userMessage.getText();
const time = currentTimeString();
// pullHistory() excludes the current user message.
// LM Studio may pre-populate a system message even on turn 1, so we check
// for the absence of any user or assistant message rather than total length.
const history = await ctl.pullHistory();
const isFirst = !history.getMessagesArray().some(
(m) => m.getRole() === "user" || m.getRole() === "assistant",
);
if (isFirst) {
const [modelLine, perms] = await Promise.all([
getModelLine(ctl),
Promise.resolve(loadPermissions()),
]);
const systemPromptBlock = perms.custom_system_prompt.trim()
? `[System instructions]\n${perms.custom_system_prompt.trim()}\n\n`
: "";
return `${systemPromptBlock}${buildFullGuide(time, modelLine)}\n\n${original}`;
}
// Subsequent messages: time + model name (lightweight, no extra API call needed
// since model rarely changes mid-conversation โ fetch async, don't block).
const modelLine = await getModelLine(ctl).catch(() => "");
const modelStr = modelLine ? ` | ${modelLine}` : "";
return `[agent-toolkit] Time: ${time}${modelStr}\n\n${original}`;
}