dist / visionPrimer.js
dist / visionPrimer.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PLUGIN_ID = exports.PRIMER_MODEL = exports.PRIMER_ID = void 0;
exports.primerConversation = primerConversation;
exports.conversationPath = conversationPath;
exports.primeActiveConversations = primeActiveConversations;
exports.ensureVisionModel = ensureVisionModel;
exports.startVisionPrimer = startVisionPrimer;
const sdk_1 = require("@lmstudio/sdk");
const node_os_1 = require("node:os");
const node_path_1 = require("node:path");
const promises_1 = require("node:fs/promises");
exports.PRIMER_ID = "strata-vision-primer";
exports.PRIMER_MODEL = "ggml-org/SmolVLM-256M-Instruct-GGUF/SmolVLM-256M-Instruct-Q8_0.gguf";
exports.PLUGIN_ID = "yuanjumay/strata-endpoint";
// Existing local-install conversations remain eligible during migration.
const LEGACY_PLUGIN_ID = "local/strata-endpoint";
function primerConversation(raw, model = exports.PRIMER_MODEL, identifier = exports.PRIMER_ID) {
const chat = JSON.parse(raw);
if (!Array.isArray(chat.plugins) || !chat.plugins.some((id) => id === exports.PLUGIN_ID || id === LEGACY_PLUGIN_ID))
return;
// Never replace another selected local model. This workaround only fills an empty slot.
if (chat.lastUsedModel && !(chat.lastUsedModel.indexedModelIdentifier === model && chat.lastUsedModel.identifier === exports.PRIMER_ID && identifier !== exports.PRIMER_ID))
return;
chat.lastUsedModel = {
indexedModelIdentifier: model,
identifier,
instanceLoadTimeConfig: { fields: [] },
instanceOperationTimeConfig: { fields: [] },
};
return JSON.stringify(chat, null, 2);
}
function conversationPath(home, identifier) {
const root = (0, node_path_1.resolve)(home, "conversations");
const target = (0, node_path_1.resolve)(root, identifier);
const rel = (0, node_path_1.relative)(root, target);
if (!rel || (0, node_path_1.isAbsolute)(rel) || rel.startsWith("..") || !target.endsWith(".conversation.json"))
throw new Error("Invalid active conversation path.");
return target;
}
async function primeActiveConversations(home, model = exports.PRIMER_MODEL, identifier = exports.PRIMER_ID) {
const stateRoot = (0, node_path_1.join)(home, ".internal", "ui-state");
const windows = (await (0, promises_1.readdir)(stateRoot)).filter(name => /^window-\d+\.json$/.test(name));
const targets = new Set();
for (const window of windows) {
const ui = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(stateRoot, window), "utf8"));
const id = ui.chat?.activeConversationIdentifier;
if (typeof id === "string")
targets.add(conversationPath(home, id));
}
let changed = 0;
for (const path of targets) {
const before = await (0, promises_1.readFile)(path, "utf8");
const after = primerConversation(before, model, identifier);
if (!after)
continue;
// Back up before touching the active chat; do not use a newest-file heuristic.
try {
await (0, promises_1.writeFile)(`${path}.strata-vision-primer.bak`, before, { flag: "wx" });
}
catch (error) {
if (error.code !== "EEXIST")
throw error;
}
if (await (0, promises_1.readFile)(path, "utf8") !== before)
throw new Error("Conversation changed during priming; retry after editing stops.");
const temp = `${path}.strata-primer-${process.pid}-${Date.now()}.tmp`;
try {
await (0, promises_1.writeFile)(temp, after, { flag: "wx" });
// Recheck immediately before replacement to avoid overwriting an in-flight edit.
if (await (0, promises_1.readFile)(path, "utf8") !== before)
throw new Error("Conversation changed before priming; retry.");
await (0, promises_1.rename)(temp, path);
}
finally {
await (0, promises_1.unlink)(temp).catch(() => { });
}
changed++;
}
return changed;
}
async function ensureVisionModel(client) {
const loaded = await Promise.all((await client.llm.listLoaded()).map(model => model.getModelInfo()));
const own = loaded.find(model => model.identifier === exports.PRIMER_ID);
if (own && own.path !== exports.PRIMER_MODEL)
throw new Error("Primer identifier is already used by another model.");
// Also reuse a copy the user loaded manually; do not create a second copy.
const existing = own ?? loaded.find(model => model.path === exports.PRIMER_MODEL && model.vision);
if (existing)
return existing.identifier;
const installed = await client.system.listDownloadedModels("llm");
if (!installed.some(model => model.path === exports.PRIMER_MODEL && model.vision))
throw new Error("Install SmolVLM-256M-Instruct Q8_0 with its vision adapter first. No model is downloaded automatically.");
const model = await client.llm.load(exports.PRIMER_MODEL, {
identifier: exports.PRIMER_ID,
config: { gpu: { ratio: "off" }, offloadKVCacheToGpu: false, contextLength: 512 },
verbose: false,
signal: AbortSignal.timeout(30_000),
// No idle TTL: the helper receives no predictions, so a TTL would evict it
// even while the user is actively chatting with Strata.
});
return (await model.getModelInfo()).identifier;
}
async function startVisionPrimer(home = (0, node_path_1.join)((0, node_os_1.homedir)(), ".lmstudio"), followActiveChat = false, suppliedClient) {
const client = suppliedClient ?? new sdk_1.LMStudioClient({
baseUrl: process.env.LMS_PLUGIN_BASE_URL,
clientIdentifier: process.env.LMS_PLUGIN_CLIENT_IDENTIFIER,
clientPasskey: process.env.LMS_PLUGIN_CLIENT_PASSKEY,
});
let busy = false;
let identifier;
let nextCheck = 0;
const tick = async () => {
if (busy)
return;
busy = true;
try {
if (!identifier && Date.now() < nextCheck)
return;
if (Date.now() >= nextCheck) {
identifier = undefined;
identifier = await ensureVisionModel(client);
nextCheck = Date.now() + 30_000;
}
if (!identifier)
return;
const changed = await primeActiveConversations(home, exports.PRIMER_MODEL, identifier);
if (changed)
console.info(`[Strata] Vision primer ready (${identifier}); updated ${changed} active Strata conversation(s).`);
}
finally {
busy = false;
}
};
// Install retries before the initial attempt so a transient startup failure
// does not permanently disable automatic loading for this plugin process.
if (followActiveChat) {
const timer = setInterval(() => {
void tick().catch(error => { nextCheck = Date.now() + 5000; console.debug("[Strata] Vision primer retry:", error.message); });
}, 2000);
timer.unref();
}
await tick();
console.info(`[Strata] Vision primer ready (${identifier}).`);
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PLUGIN_ID = exports.PRIMER_MODEL = exports.PRIMER_ID = void 0;
exports.primerConversation = primerConversation;
exports.conversationPath = conversationPath;
exports.primeActiveConversations = primeActiveConversations;
exports.ensureVisionModel = ensureVisionModel;
exports.startVisionPrimer = startVisionPrimer;
const sdk_1 = require("@lmstudio/sdk");
const node_os_1 = require("node:os");
const node_path_1 = require("node:path");
const promises_1 = require("node:fs/promises");
exports.PRIMER_ID = "strata-vision-primer";
exports.PRIMER_MODEL = "ggml-org/SmolVLM-256M-Instruct-GGUF/SmolVLM-256M-Instruct-Q8_0.gguf";
exports.PLUGIN_ID = "yuanjumay/strata-endpoint";
// Existing local-install conversations remain eligible during migration.
const LEGACY_PLUGIN_ID = "local/strata-endpoint";
function primerConversation(raw, model = exports.PRIMER_MODEL, identifier = exports.PRIMER_ID) {
const chat = JSON.parse(raw);
if (!Array.isArray(chat.plugins) || !chat.plugins.some((id) => id === exports.PLUGIN_ID || id === LEGACY_PLUGIN_ID))
return;
// Never replace another selected local model. This workaround only fills an empty slot.
if (chat.lastUsedModel && !(chat.lastUsedModel.indexedModelIdentifier === model && chat.lastUsedModel.identifier === exports.PRIMER_ID && identifier !== exports.PRIMER_ID))
return;
chat.lastUsedModel = {
indexedModelIdentifier: model,
identifier,
instanceLoadTimeConfig: { fields: [] },
instanceOperationTimeConfig: { fields: [] },
};
return JSON.stringify(chat, null, 2);
}
function conversationPath(home, identifier) {
const root = (0, node_path_1.resolve)(home, "conversations");
const target = (0, node_path_1.resolve)(root, identifier);
const rel = (0, node_path_1.relative)(root, target);
if (!rel || (0, node_path_1.isAbsolute)(rel) || rel.startsWith("..") || !target.endsWith(".conversation.json"))
throw new Error("Invalid active conversation path.");
return target;
}
async function primeActiveConversations(home, model = exports.PRIMER_MODEL, identifier = exports.PRIMER_ID) {
const stateRoot = (0, node_path_1.join)(home, ".internal", "ui-state");
const windows = (await (0, promises_1.readdir)(stateRoot)).filter(name => /^window-\d+\.json$/.test(name));
const targets = new Set();
for (const window of windows) {
const ui = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(stateRoot, window), "utf8"));
const id = ui.chat?.activeConversationIdentifier;
if (typeof id === "string")
targets.add(conversationPath(home, id));
}
let changed = 0;
for (const path of targets) {
const before = await (0, promises_1.readFile)(path, "utf8");
const after = primerConversation(before, model, identifier);
if (!after)
continue;
// Back up before touching the active chat; do not use a newest-file heuristic.
try {
await (0, promises_1.writeFile)(`${path}.strata-vision-primer.bak`, before, { flag: "wx" });
}
catch (error) {
if (error.code !== "EEXIST")
throw error;
}
if (await (0, promises_1.readFile)(path, "utf8") !== before)
throw new Error("Conversation changed during priming; retry after editing stops.");
const temp = `${path}.strata-primer-${process.pid}-${Date.now()}.tmp`;
try {
await (0, promises_1.writeFile)(temp, after, { flag: "wx" });
// Recheck immediately before replacement to avoid overwriting an in-flight edit.
if (await (0, promises_1.readFile)(path, "utf8") !== before)
throw new Error("Conversation changed before priming; retry.");
await (0, promises_1.rename)(temp, path);
}
finally {
await (0, promises_1.unlink)(temp).catch(() => { });
}
changed++;
}
return changed;
}
async function ensureVisionModel(client) {
const loaded = await Promise.all((await client.llm.listLoaded()).map(model => model.getModelInfo()));
const own = loaded.find(model => model.identifier === exports.PRIMER_ID);
if (own && own.path !== exports.PRIMER_MODEL)
throw new Error("Primer identifier is already used by another model.");
// Also reuse a copy the user loaded manually; do not create a second copy.
const existing = own ?? loaded.find(model => model.path === exports.PRIMER_MODEL && model.vision);
if (existing)
return existing.identifier;
const installed = await client.system.listDownloadedModels("llm");
if (!installed.some(model => model.path === exports.PRIMER_MODEL && model.vision))
throw new Error("Install SmolVLM-256M-Instruct Q8_0 with its vision adapter first. No model is downloaded automatically.");
const model = await client.llm.load(exports.PRIMER_MODEL, {
identifier: exports.PRIMER_ID,
config: { gpu: { ratio: "off" }, offloadKVCacheToGpu: false, contextLength: 512 },
verbose: false,
signal: AbortSignal.timeout(30_000),
// No idle TTL: the helper receives no predictions, so a TTL would evict it
// even while the user is actively chatting with Strata.
});
return (await model.getModelInfo()).identifier;
}
async function startVisionPrimer(home = (0, node_path_1.join)((0, node_os_1.homedir)(), ".lmstudio"), followActiveChat = false, suppliedClient) {
const client = suppliedClient ?? new sdk_1.LMStudioClient({
baseUrl: process.env.LMS_PLUGIN_BASE_URL,
clientIdentifier: process.env.LMS_PLUGIN_CLIENT_IDENTIFIER,
clientPasskey: process.env.LMS_PLUGIN_CLIENT_PASSKEY,
});
let busy = false;
let identifier;
let nextCheck = 0;
const tick = async () => {
if (busy)
return;
busy = true;
try {
if (!identifier && Date.now() < nextCheck)
return;
if (Date.now() >= nextCheck) {
identifier = undefined;
identifier = await ensureVisionModel(client);
nextCheck = Date.now() + 30_000;
}
if (!identifier)
return;
const changed = await primeActiveConversations(home, exports.PRIMER_MODEL, identifier);
if (changed)
console.info(`[Strata] Vision primer ready (${identifier}); updated ${changed} active Strata conversation(s).`);
}
finally {
busy = false;
}
};
// Install retries before the initial attempt so a transient startup failure
// does not permanently disable automatic loading for this plugin process.
if (followActiveChat) {
const timer = setInterval(() => {
void tick().catch(error => { nextCheck = Date.now() + 5000; console.debug("[Strata] Vision primer retry:", error.message); });
}, 2000);
timer.unref();
}
await tick();
console.info(`[Strata] Vision primer ready (${identifier}).`);
}