src / toolsProvider.ts
/**
* Alerts Plugin — toolsProvider
*
* Tools:
* alert(action) — set | list | dismiss | snooze | delete | check
* remind — quick one-liner: "remind me to X in 30 minutes"
*/
import { text, tool, type Tool, type ToolCallContext, type ToolsProvider } from "@lmstudio/sdk";
import { z } from "zod";
import { pluginConfigSchematics } from "./config";
import {
loadAlerts, addAlert, updateAlert, deleteAlert, getDueAlerts, parseRelativeTime,
type AlertPriority,
} from "./store";
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);
}
};
}
export const toolsProvider: ToolsProvider = async (ctl) => {
const cfg = ctl.getPluginConfig(pluginConfigSchematics);
return [
tool({
name: "alert",
description: text`
Manage time-based alerts and reminders.
action: "set" — Create a new alert. Requires: message, due_at (ISO or natural language like "in 2 hours", "tomorrow", "in 3 days")
action: "list" — List all alerts. Optional: status filter (pending | snoozed | dismissed | all)
action: "check" — Return only alerts that are currently due/overdue
action: "dismiss" — Mark an alert as dismissed. Requires: id
action: "snooze" — Push alert forward by configured snooze duration. Requires: id
action: "delete" — Permanently delete an alert. Requires: id
priority: low | normal | high (default: normal)
tags: comma-separated tags for filtering (e.g. "work,urgent")
`,
parameters: {
action: z.enum(["set", "list", "check", "dismiss", "snooze", "delete"]),
message: z.string().default(""),
due_at: z.string().default("").describe("ISO datetime or natural language: 'in 2 hours', 'tomorrow', 'in 3 days'"),
id: z.string().default(""),
status: z.enum(["pending", "snoozed", "dismissed", "all"]).default("pending"),
priority: z.enum(["low", "normal", "high"]).default("normal"),
tags: z.string().default(""),
},
implementation: safe_impl("alert", async ({ action, message, due_at, id, status, priority, tags }, ctx) => {
const dp = cfg.get("dataPath");
const snoozeMins = cfg.get("snoozeDurationMinutes");
if (action === "set") {
if (!message) throw new Error("message required to set an alert");
if (!due_at) throw new Error("due_at required — e.g. 'in 2 hours', 'tomorrow', '2026-06-01T09:00:00'");
const dueDate = parseRelativeTime(due_at);
const tagList = tags ? tags.split(",").map(t => t.trim()).filter(Boolean) : [];
const alert = await addAlert(dp, message, dueDate, priority as AlertPriority, tagList);
return json({ created: true, alert: { id: alert.id, message: alert.message, due_at: alert.dueAt, priority: alert.priority } });
}
if (action === "list") {
ctx.status("Loading alerts");
const all = await loadAlerts(dp);
const filtered = status === "all" ? all : all.filter(a => a.status === status);
const sorted = [...filtered].sort((a, b) => new Date(a.dueAt).getTime() - new Date(b.dueAt).getTime());
return json({ count: sorted.length, status_filter: status, alerts: sorted });
}
if (action === "check") {
const all = await loadAlerts(dp);
const due = getDueAlerts(all);
return json({ due_count: due.length, alerts: due });
}
if (!id) throw new Error("id required for " + action);
if (action === "dismiss") {
const updated = await updateAlert(dp, id, { status: "dismissed" });
if (!updated) throw new Error(`Alert ${id} not found`);
return json({ dismissed: true, id });
}
if (action === "snooze") {
const snoozeUntil = new Date(Date.now() + snoozeMins * 60_000);
const updated = await updateAlert(dp, id, { status: "snoozed", snoozedUntil: snoozeUntil.toISOString() });
if (!updated) throw new Error(`Alert ${id} not found`);
return json({ snoozed: true, id, snoozed_until: snoozeUntil.toISOString(), minutes: snoozeMins });
}
// delete
const removed = await deleteAlert(dp, id);
if (!removed) throw new Error(`Alert ${id} not found`);
return json({ deleted: true, id });
}),
}),
tool({
name: "remind",
description: text`
Quick shorthand to set a reminder. Equivalent to alert(action="set").
Examples:
remind(message="Follow up with recruiter", in_time="in 2 hours")
remind(message="Submit invoice", in_time="tomorrow")
remind(message="Review PR", in_time="in 30 minutes", priority="high")
`,
parameters: {
message: z.string().describe("What to remind about"),
in_time: z.string().describe("When — natural language or ISO: 'in 30 minutes', 'tomorrow', '2026-06-01T09:00:00'"),
priority: z.enum(["low", "normal", "high"]).default("normal"),
tags: z.string().default(""),
},
implementation: safe_impl("remind", async ({ message, in_time, priority, tags }, ctx) => {
const dp = cfg.get("dataPath");
if (!message) throw new Error("message required");
if (!in_time) throw new Error("in_time required — e.g. 'in 2 hours', 'tomorrow'");
ctx.status(`Setting reminder: "${message}" for ${in_time}`);
const dueDate = parseRelativeTime(in_time);
const tagList = tags ? tags.split(",").map(t => t.trim()).filter(Boolean) : [];
const alert = await addAlert(dp, message, dueDate, priority as AlertPriority, tagList);
const timeStr = dueDate.toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
return `Reminder set: "${message}" — due ${timeStr} (id: ${alert.id})`;
}),
}),
];
};