src / toolsProvider.ts
import { tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
async function toolsProvider(ctl: ToolsProviderController) {
const do_tools: any[] = [];
// -1
const getDateTimeTool = tool({
name: "get_date_time",
description: "Returns current date and time. Use only on explicit time request.",
parameters: {},
implementation: async () => {
return new Date().toISOString();
}
});
do_tools.push(getDateTimeTool);
//--2
const CWTool = tool({
name: "count_letters",
description: "Counts occurrences of a character in a string.",
parameters: {
word: z.string(),
letr: z.string().length(1)
},
implementation: async ({ word, letr }) => {
return [...word].filter(c => c === letr).length;
}
});
do_tools.push(CWTool);
//--3
const getLengthTool = tool({
name: "get_word_length",
description: "Returns the length of the input string.",
parameters: {
word: z.string()
},
implementation: async ({ word }) => {
return { length: word.length };
}
});
do_tools.push(getLengthTool);
//--4
const getMyIPTool = tool({
name: "get_myip",
description: "Returns user's public IP address. Test internet connection.",
parameters: {},
implementation: async () => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
try {
const response = await fetch("https://ifconfig.me/ip", {
signal: controller.signal
});
clearTimeout(timeoutId);
const ip = (await response.text()).trim();
return { ip };
} catch (error) {
clearTimeout(timeoutId);
const controller2 = new AbortController();
const timeoutId2 = setTimeout(() => controller2.abort(), 2000);
try {
const response = await fetch("https://api.country.is/", {
signal: controller2.signal
});
clearTimeout(timeoutId2);
const ip = (await response.text()).trim();
return ip;
} catch (error) {
clearTimeout(timeoutId2);
return { ip: null, error: "No response from 2 servers" };
}
}
}
});
do_tools.push(getMyIPTool);
//--5
const getAnagramTool = tool({
name: "get_anagram",
description: "Returns anagram of the input string/word.",
parameters: {
word: z.string().describe("Input word")
},
implementation: async (params) => {
const w = params.word;
const c = [...w];
const crypto = await import('crypto');
//const { randomInt } = await import('crypto'); |\_ _ _ _ _just_option____
//const j = randomInt(0, i + 1); |/
console.log(`Input W: "${w}", length W: ${w.length}`);
console.log(`Input C: "${c}", length C: ${c.length}`);
console.log(`Bytes: ${c.map(x => x.codePointAt(0))}`);
for (let i = c.length - 1; i > 0; i--) {
const j = crypto.randomInt(0, i +1); //
[c[i], c[j]] = [c[j], c[i]]; //xchg letters - (Fisher-Yates shuffle)
console.log(`${i}:${j} C: "${c}", length: "${c.length}"`);
}
console.log(`result C: ${c.join("")}`);
const result = c.join('');
// Validity of result
const inputSorted = [...w].sort().join('');
const outputSorted = [...result].sort().join('');
if (inputSorted !== outputSorted) {
throw new Error(`Anagram validation failed: ${inputSorted} !== ${outputSorted}`);
}
return { anagram: result };
}
});
do_tools.push(getAnagramTool);
//--6
// Unix timestamp -> date time
const getTSD = tool({
name: "ts_to_date",
description: "Returns full date-time of input Unix ts/timestamp.",
parameters: {
ts: z.number().describe("Numeric value required for conversion")
},
implementation: async (params) => {
// Date = Unix timestamp
const pts = params.ts;
const date = new Date(pts);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0'); //+1: month' from zero
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
// day of week & month
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const months = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
const dayName = days[date.getDay()];
const monthName = months[date.getMonth()];
// Full date: "Friday, 13 June 2025, 14:30:45"
const fullDate = `${dayName}, ${day} ${monthName} ${year}, ${hours}:${minutes}:${seconds}`;
return { fullDate: fullDate };
}
});
do_tools.push(getTSD); //push tool 6 to provider
//--7 : tnx Qwen code
// scan_plugins(): Scan LM Studio plugins directory to table
const scanPlugins = tool({
name: "list_plugins",
description: "Scans plugins directory and returns list of plugins with their Author, revision, installation date.",
parameters: {},
implementation: async () => {
try {
// Get the directory of the currently running plugin
// In LM Studio plugin environment, we need to determine the plugin directory
const currentDir = process.cwd();
// Navigate up 3 levels (../../../) to reach the plugins root
const pluginsRoot = path.resolve(currentDir, "..", "..", "..");
const plugins: Array<{author: string, name: string, revision: string | null, installDate: string}> = [];
// Recursive function to find manifest.json files
const findManifests = (dir: string, depth: number = 0, author: string | null = null) => {
if (!fs.existsSync(dir)) {
return;
}
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
// Skip node_* and mcp directories
if (entry.isDirectory() && (entry.name.startsWith("node_") || entry.name === "mcp")) {
continue;
}
if (entry.isFile() && entry.name === "manifest.json") {
// Found a manifest file
try {
const manifestContent = fs.readFileSync(fullPath, "utf-8");
const manifest = JSON.parse(manifestContent);
const stats = fs.statSync(fullPath);
const installDate = stats.mtime.toISOString().slice(0,10); //date:yyyy-mm-dd
// Get plugin name from parent directory name
const pluginDir = path.basename(path.dirname(fullPath));
// Author should have been passed from depth=1
const pluginAuthor = author || "unknown";
plugins.push({
author: pluginAuthor,
name: pluginDir,
revision: manifest.revision || null,
installDate: installDate
});
} catch (err) {
console.error(`Error reading manifest: ${fullPath}`, err);
}
} else if (entry.isDirectory()) {
// depth=0: pluginsRoot itself
// depth=1: author folders (e.g., "alex")
// depth=2: plugin folders (e.g., "myplugin")
// At depth=1, entry.name is the author
// At depth>1, keep the existing author
const newAuthor = depth === 1 ? entry.name : author;
findManifests(fullPath, depth + 1, newAuthor);
}
}
};
// Start scanning from plugins root
findManifests(pluginsRoot);
// Save list to list.json in plugin directory (one level up from current)
const listFilePath = path.resolve(currentDir, "..", "list.json");
fs.writeFileSync(listFilePath, JSON.stringify({ plugins, count: plugins.length }, null, 2), "utf-8");
return { plugins, count: plugins.length };
} catch (error) {
return {
error: "Failed to scan plugins",
details: error instanceof Error ? error.message : String(error)
};
}
}
});
do_tools.push(scanPlugins); //push tool 7 to provider
//--8
// updated_plugins() - Check for plugin updates
const updatedPlugins = tool({
name: "updated_plugins",
description: "Checks installed plugins for updates by comparing local revision with remote manifest.json. Reports plugins with available updates.",
parameters: {},
implementation: async () => {
try {
// Read list.json from plugin directory (one level up)
const currentDir = process.cwd();
const listFilePath = path.resolve(currentDir, "..", "list.json");
if (!fs.existsSync(listFilePath)) {
return { error: "File list.json not found. Run list_plugins() first." };
}
const listData = JSON.parse(fs.readFileSync(listFilePath, "utf-8"));
const plugins = listData.plugins || [];
const updates: Array<{author: string, name: string, currentRevision: string, latestRevision: string}> = [];
let checked = 0;
let errors: Array<{author: string, name: string, error: string}> = [];
// Check each plugin one by one with delay
for (const plugin of plugins) {
const manifestUrl = `https://lmstudio.ai/${plugin.author}/${plugin.name}/files/manifest.json`;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(manifestUrl, {
signal: controller.signal,
headers: { "User-Agent": "LMStudio-Plugin/1.0" }
});
clearTimeout(timeoutId);
if (!response.ok) {
errors.push({ author: plugin.author, name: plugin.name, error: `HTTP ${response.status}` });
continue;
}
// The URL returns HTML with embedded JSON data
// Parse revision from JSON like: "revisionNumber":8
const html = await response.text();
// Extract revision value from HTML
// Pattern: "revision":<number>
const revisionRegex = /"revision"\s*:\s*(\d+)/;
const clrHtml = html.replace(/<[^>]*>/g, ''); //clear all tags
const match = clrHtml.match(revisionRegex);
if (!match) {
errors.push({ author: plugin.author, name: plugin.name, error: "Could not parse revision from HTML" });
continue;
}
const remoteRevision = match[1]; //rev.N
if (remoteRevision && plugin.revision && parseInt(remoteRevision) > parseInt(plugin.revision)) {
updates.push({
author: plugin.author,
name: plugin.name,
currentRevision: plugin.revision,
latestRevision: String(remoteRevision)
});
}
checked++;
} catch (err) {
errors.push({
author: plugin.author,
name: plugin.name,
error: err instanceof Error ? err.message : String(err)
});
}
// Pause between requests (1 second)
if (checked < plugins.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// Update list.json with check results
const updatedListData = {
plugins: plugins.map((p: any) => {
const update = updates.find(u => u.author === p.author && u.name === p.name);
const error = errors.find(e => e.author === p.author && e.name === p.name);
return {
...p,
hasUpdate: !!update,
latestRevision: update?.latestRevision || null,
checkError: error?.error || null
};
}),
count: plugins.length,
lastChecked: new Date().toISOString(),
updatesAvailable: updates.length
};
fs.writeFileSync(listFilePath, JSON.stringify(updatedListData, null, 2), "utf-8");
// Return summary
const summary: any = {
checked,
updatesAvailable: updates.length,
errors: errors.length
};
if (updates.length > 0) {
summary.updates = updates.map(u => `${u.author}/${u.name}: ${u.currentRevision} -> ${u.latestRevision}`);
}
if (errors.length > 0) {
summary.errors = errors.map(e => `${e.author}/${e.name}: ${e.error}`);
}
if (updates.length === 0 && errors.length === 0) {
summary.message = "All plugins are up to date";
} else if (updates.length === 0) {
summary.message = "No updates found, but some checks failed";
} else {
summary.message = `${updates.length} update(s) available`;
}
return summary;
} catch (error) {
return {
error: "Failed to check updates",
details: error instanceof Error ? error.message : String(error)
};
}
}
});
do_tools.push(updatedPlugins); //push tool 8 to provider
//--9
const readList = tool({
name: "read_list",
description: "Just returns list.json with their Author, revision, installation Date, updated Flag, ...etc",
parameters: {},
implementation: async () => {
try {
// Get the directory of the currently running plugin
const currentDir = process.cwd();
// Save list to list.json in plugin directory (one level up from current)
const FilePath = path.resolve(currentDir, "..", "list.json");
if (!fs.existsSync(FilePath)) {
return { error: "File list.json not found. Run list_plugins() first to create it. Only on explicit request." };
}
const Data = fs.readFileSync(FilePath, "utf-8");
return Data;
} catch (error) {
return { error: "File list.json", details: error instanceof Error ? error.message : String(error)
};
}
}
});
do_tools.push(readList); //push tool 9 to provider
//--
return do_tools;
}
export { toolsProvider };
//end.