toolsProvider.js
"use strict";
/**
* Calendar Plugin — toolsProvider
*
* Tools:
* calendar(action) — today | upcoming | search | free_slots
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.toolsProvider = void 0;
const sdk_1 = require("@lmstudio/sdk");
const zod_1 = require("zod");
const config_1 = require("./config");
const ical_1 = require("./ical");
const macos_1 = require("./macos");
function json(obj) {
return JSON.stringify(obj, null, 2);
}
function safe_impl(name, fn) {
return async (params, ctx) => {
if (ctx.signal.aborted)
return JSON.stringify({ tool_error: true, tool: name, error: "cancelled" });
try {
return await fn(params, ctx);
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return JSON.stringify({ tool_error: true, tool: name, error: msg }, null, 2);
}
};
}
async function getEvents(source, icsPath, from, to) {
if (source === "ics") {
if (!icsPath)
throw new Error("ICS file path not configured. Set it in plugin settings.");
const all = await (0, ical_1.parseIcsFile)(icsPath);
return (0, ical_1.filterByRange)(all, from, to);
}
return (0, macos_1.fetchMacOSEvents)(from, to);
}
const toolsProvider = async (ctl) => {
const cfg = ctl.getPluginConfig(config_1.pluginConfigSchematics);
return [
(0, sdk_1.tool)({
name: "calendar",
description: (0, sdk_1.text) `
Query your local calendar (macOS Calendar.app or an ICS file).
action: "today" — All events for today
action: "upcoming" — Events in the next N days (days_ahead, default from settings)
action: "search" — Find events matching a keyword in title/location
action: "free_slots" — Find gaps ≥ N minutes between events on a given date
date: ISO date string "YYYY-MM-DD" (default: today)
keyword: search term (for search action)
days_ahead: number of days to look forward (for upcoming)
min_gap_minutes: minimum free block size in minutes (for free_slots, default 30)
`,
parameters: {
action: zod_1.z.enum(["today", "upcoming", "search", "free_slots"]),
date: zod_1.z.string().default("").describe("YYYY-MM-DD, default today"),
days_ahead: zod_1.z.coerce.number().int().min(1).max(90).optional(),
keyword: zod_1.z.string().default(""),
min_gap_minutes: zod_1.z.coerce.number().int().min(5).max(480).default(30),
},
implementation: safe_impl("calendar", async ({ action, date, days_ahead, keyword, min_gap_minutes }, ctx) => {
const source = cfg.get("calendarSource");
const icsPath = cfg.get("icsFilePath");
const defaultDaysAhead = cfg.get("daysAhead");
const today = new Date();
today.setHours(0, 0, 0, 0);
const targetDate = date ? new Date(date + "T00:00:00") : new Date(today);
if (action === "today") {
ctx.status("Fetching today's events");
const endOfDay = new Date(targetDate);
endOfDay.setHours(23, 59, 59, 999);
const events = await getEvents(source, icsPath, targetDate, endOfDay);
return json({
date: targetDate.toDateString(),
count: events.length,
events: events.map(ical_1.formatEvent),
});
}
if (action === "upcoming") {
const ahead = days_ahead ?? defaultDaysAhead;
ctx.status(`Fetching events for next ${ahead} days`);
const to = new Date(today);
to.setDate(to.getDate() + ahead);
to.setHours(23, 59, 59, 999);
const events = await getEvents(source, icsPath, today, to);
const grouped = {};
for (const e of events) {
const key = e.start.toDateString();
grouped[key] = grouped[key] ?? [];
grouped[key].push((0, ical_1.formatEvent)(e));
}
return json({ days_ahead: ahead, total_events: events.length, schedule: grouped });
}
if (action === "search") {
if (!keyword)
throw new Error("keyword required for search action");
ctx.status(`Searching for "${keyword}"`);
const to = new Date(today);
to.setDate(to.getDate() + 90);
const all = await getEvents(source, icsPath, today, to);
const kw = keyword.toLowerCase();
const matches = all.filter(e => e.summary.toLowerCase().includes(kw) ||
(e.location ?? "").toLowerCase().includes(kw));
return json({ keyword, matches_found: matches.length, events: matches.map(ical_1.formatEvent) });
}
// free_slots
ctx.status(`Finding free slots ≥ ${min_gap_minutes} min on ${targetDate.toDateString()}`);
const endOfDay = new Date(targetDate);
endOfDay.setHours(23, 59, 59, 999);
const events = await getEvents(source, icsPath, targetDate, endOfDay);
const workStart = new Date(targetDate);
workStart.setHours(9, 0, 0, 0);
const workEnd = new Date(targetDate);
workEnd.setHours(18, 0, 0, 0);
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
const freeSlots = [];
let cursor = workStart;
for (const e of sorted) {
if (e.start > cursor) {
const gapMin = Math.round((e.start.getTime() - cursor.getTime()) / 60000);
if (gapMin >= min_gap_minutes) {
freeSlots.push({
from: cursor.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
to: e.start.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
minutes: gapMin,
});
}
}
if (e.end > cursor)
cursor = e.end;
}
if (cursor < workEnd) {
const gapMin = Math.round((workEnd.getTime() - cursor.getTime()) / 60000);
if (gapMin >= min_gap_minutes) {
freeSlots.push({
from: cursor.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
to: workEnd.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
minutes: gapMin,
});
}
}
return json({ date: targetDate.toDateString(), min_gap_minutes, free_slots: freeSlots });
}),
}),
];
};
exports.toolsProvider = toolsProvider;