src / toolsProvider.ts
/**
* Context Plugin — toolsProvider
*
* Tools:
* context(action) — clipboard | active_window | running_apps | system
*/
import { text, tool, type ToolCallContext, type ToolsProvider } from "@lmstudio/sdk";
import { z } from "zod";
import { execSync } from "child_process";
import { hostname, platform, release, arch, cpus, totalmem, freemem, userInfo } from "os";
import { pluginConfigSchematics } from "./config";
function json(obj: unknown): string {
return JSON.stringify(obj, null, 2);
}
function safe_impl<T extends Record<string, unknown>>(
name: string,
fn: (params: T, ctx: ToolCallContext) => Promise<string>
): (params: T, ctx: ToolCallContext) => Promise<string> {
return async (params: T, ctx: ToolCallContext) => {
if (ctx.signal.aborted) return JSON.stringify({ tool_error: true, tool: name, error: "cancelled" });
try {
return await fn(params, ctx);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return JSON.stringify({ tool_error: true, tool: name, error: msg }, null, 2);
}
};
}
function requireMac(): void {
if (process.platform !== "darwin") {
throw new Error("This action is macOS only.");
}
}
function runCmd(cmd: string, timeoutMs = 5000): string {
return execSync(cmd, { timeout: timeoutMs, encoding: "utf-8" }).trim();
}
function getClipboard(maxChars: number): string {
requireMac();
const content = runCmd("pbpaste");
if (content.length > maxChars) {
return content.slice(0, maxChars) + `\n[truncated — ${content.length} total chars]`;
}
return content;
}
function getActiveWindow(): Record<string, string> {
requireMac();
const script = `
tell application "System Events"
set frontApp to first application process whose frontmost is true
set appName to name of frontApp
set windowTitle to ""
try
set windowTitle to name of front window of frontApp
end try
return appName & "|||" & windowTitle
end tell
`.trim();
const raw = runCmd(`osascript -e '${script.replace(/'/g, "'\\''")}'`);
const [app, title] = raw.split("|||");
return { active_app: app?.trim() ?? "", window_title: title?.trim() ?? "" };
}
function getRunningApps(): string[] {
requireMac();
const script = `tell application "System Events" to return name of every application process whose background only is false`;
const raw = runCmd(`osascript -e '${script}'`);
return raw.split(", ").map(s => s.trim()).filter(Boolean).sort();
}
export const toolsProvider: ToolsProvider = async (ctl) => {
const cfg = ctl.getPluginConfig(pluginConfigSchematics);
return [
tool({
name: "context",
description: text`
Read the user's current system context.
action: "clipboard" — Get current clipboard text content
action: "active_window" — Get the name of the active app and focused window title
action: "running_apps" — List all visible running applications
action: "system" — OS info: platform, CPU, RAM, hostname, username
macOS only for clipboard/active_window/running_apps.
system info works on all platforms.
`,
parameters: {
action: z.enum(["clipboard", "active_window", "running_apps", "system"]),
},
implementation: safe_impl("context", async ({ action }, ctx) => {
const maxClipboardChars = cfg.get("maxClipboardChars");
if (action === "clipboard") {
ctx.status("Reading clipboard");
const content = getClipboard(maxClipboardChars);
return json({ chars: content.length, content });
}
if (action === "active_window") {
ctx.status("Getting active window");
return json(getActiveWindow());
}
if (action === "running_apps") {
ctx.status("Listing running apps");
const apps = getRunningApps();
return json({ count: apps.length, apps });
}
// system
ctx.status("Collecting system info");
const totalRam = totalmem();
const freeRam = freemem();
const cpuModel = cpus()[0]?.model ?? "unknown";
const cpuCount = cpus().length;
return json({
platform: platform(),
os_release: release(),
arch: arch(),
hostname: hostname(),
username: userInfo().username,
cpu: `${cpuCount}× ${cpuModel}`,
ram_total_gb: (totalRam / 1024 ** 3).toFixed(1),
ram_free_gb: (freeRam / 1024 ** 3).toFixed(1),
ram_used_pct: Math.round((1 - freeRam / totalRam) * 100),
});
}),
}),
];
};