apple-tools-mcp 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/index.js +1 -19
- package/indexer.js +112 -0
- package/package.json +1 -1
- package/search.js +60 -20
package/README.md
CHANGED
|
@@ -137,10 +137,10 @@ Once configured, Claude can use these tools:
|
|
|
137
137
|
| Tool | Description |
|
|
138
138
|
|------|-------------|
|
|
139
139
|
| `calendar_search` | Semantic search for events with filters |
|
|
140
|
-
| `calendar_date` | Get events on a specific date |
|
|
140
|
+
| `calendar_date` | Get events on a specific date (live Calendar.app, not the search index) |
|
|
141
141
|
| `calendar_upcoming` | Get next N upcoming events |
|
|
142
142
|
| `calendar_week` | Get all events for current or future week |
|
|
143
|
-
| `calendar_free_time` | Find available time slots on a date |
|
|
143
|
+
| `calendar_free_time` | Find available time slots on a date (live Calendar.app, not the search index) |
|
|
144
144
|
| `calendar_recurring` | List recurring events |
|
|
145
145
|
|
|
146
146
|
### Contacts Tools
|
package/index.js
CHANGED
|
@@ -436,29 +436,11 @@ async function calendarSearch(query, options = {}) {
|
|
|
436
436
|
}
|
|
437
437
|
|
|
438
438
|
async function calendarDate(date) {
|
|
439
|
-
if (!sessionIndexComplete) {
|
|
440
|
-
return getIndexingMessage();
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const ready = await isIndexReady("calendar");
|
|
444
|
-
if (!ready) {
|
|
445
|
-
return "Calendar index not available. Please try again shortly.";
|
|
446
|
-
}
|
|
447
|
-
|
|
448
439
|
const result = await getCalendarDateResults(date);
|
|
449
440
|
return formatCalendarResults(result);
|
|
450
441
|
}
|
|
451
442
|
|
|
452
443
|
async function calendarFreeTime(date, options = {}) {
|
|
453
|
-
if (!sessionIndexComplete) {
|
|
454
|
-
return getIndexingMessage();
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const ready = await isIndexReady("calendar");
|
|
458
|
-
if (!ready) {
|
|
459
|
-
return "Calendar index not available. Please try again shortly.";
|
|
460
|
-
}
|
|
461
|
-
|
|
462
444
|
const result = await calculateFreeTime(date, options);
|
|
463
445
|
return formatFreeTimeResults(result);
|
|
464
446
|
}
|
|
@@ -1454,7 +1436,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1454
1436
|
async function main() {
|
|
1455
1437
|
const transport = new StdioServerTransport();
|
|
1456
1438
|
await server.connect(transport);
|
|
1457
|
-
console.error("Apple Tools MCP server running (v1.1.
|
|
1439
|
+
console.error("Apple Tools MCP server running (v1.1.1)");
|
|
1458
1440
|
// Background indexing runs automatically on startup and every INDEX_INTERVAL
|
|
1459
1441
|
}
|
|
1460
1442
|
|
package/indexer.js
CHANGED
|
@@ -63,6 +63,7 @@ const BATCH_DELAY_MS = 0; // No delay needed - benchmarks showed no thermal thro
|
|
|
63
63
|
|
|
64
64
|
// Mac Absolute Time epoch: Jan 1, 2001 00:00:00 UTC
|
|
65
65
|
const MAC_ABSOLUTE_EPOCH = 978307200;
|
|
66
|
+
export const CALENDAR_TMP_PREFIX = "apple-tools-cal-";
|
|
66
67
|
|
|
67
68
|
let embeddingPipeline = null;
|
|
68
69
|
let db = null;
|
|
@@ -1687,6 +1688,117 @@ export function getUpcomingEvents(limit = 10) {
|
|
|
1687
1688
|
}
|
|
1688
1689
|
}
|
|
1689
1690
|
|
|
1691
|
+
// Convert local-day unix ms bounds to Apple/Core Data seconds (unix - 978307200)
|
|
1692
|
+
export function localDayToMacBounds(startMs, endMs) {
|
|
1693
|
+
const startMac = Math.floor(Number(startMs) / 1000) - MAC_ABSOLUTE_EPOCH;
|
|
1694
|
+
const endMac = Math.floor(Number(endMs) / 1000) - MAC_ABSOLUTE_EPOCH;
|
|
1695
|
+
if (!Number.isFinite(startMac) || !Number.isFinite(endMac)) {
|
|
1696
|
+
throw new Error("Invalid date bounds");
|
|
1697
|
+
}
|
|
1698
|
+
return { startMac, endMac };
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// Date-bounded OccurrenceCache query: one row per occurrence, no GROUP BY ci.ROWID
|
|
1702
|
+
export function buildEventsOnDateQuery(startMac, endMac) {
|
|
1703
|
+
const startBound = Math.floor(Number(startMac));
|
|
1704
|
+
const endBound = Math.floor(Number(endMac));
|
|
1705
|
+
if (!Number.isFinite(startBound) || !Number.isFinite(endBound)) {
|
|
1706
|
+
throw new Error("Invalid Mac Absolute Time bounds");
|
|
1707
|
+
}
|
|
1708
|
+
const timedStart = "COALESCE(oc.occurrence_end_date - (ci.end_date - ci.start_date), ci.start_date)";
|
|
1709
|
+
const occEnd = "COALESCE(oc.occurrence_end_date, ci.end_date)";
|
|
1710
|
+
// First local day = occurrence-end local date minus (durationDays - 1). Day math, not seconds (DST-safe).
|
|
1711
|
+
const allDayStartDate = `date(${occEnd} + 978307200, 'unixepoch', 'localtime', '-' || (CAST(round((ci.end_date - ci.start_date) / 86400.0) AS INTEGER) - 1) || ' days')`;
|
|
1712
|
+
const allDayStartMac = `CAST(strftime('%s', ${allDayStartDate} || ' 00:00:00', 'utc') AS INTEGER) - 978307200`;
|
|
1713
|
+
return `
|
|
1714
|
+
SELECT DISTINCT
|
|
1715
|
+
ci.ROWID as itemId,
|
|
1716
|
+
ci.summary as title,
|
|
1717
|
+
datetime(CASE WHEN ci.all_day THEN ${allDayStartMac} ELSE ${timedStart} END + 978307200, 'unixepoch', 'localtime') as start,
|
|
1718
|
+
datetime(${occEnd} + 978307200, 'unixepoch', 'localtime') as end,
|
|
1719
|
+
CASE WHEN ci.all_day THEN ${allDayStartMac} ELSE ${timedStart} END as startMac,
|
|
1720
|
+
${occEnd} as endMac,
|
|
1721
|
+
ci.all_day as isAllDay,
|
|
1722
|
+
c.title as calendar,
|
|
1723
|
+
l.title as location
|
|
1724
|
+
FROM OccurrenceCache oc
|
|
1725
|
+
INNER JOIN CalendarItem ci ON oc.event_id = ci.ROWID
|
|
1726
|
+
LEFT JOIN Calendar c ON ci.calendar_id = c.ROWID
|
|
1727
|
+
LEFT JOIN Location l ON ci.location_id = l.ROWID
|
|
1728
|
+
WHERE ci.summary IS NOT NULL AND ci.summary <> ''
|
|
1729
|
+
AND (c.title IS NULL OR c.title NOT IN ('Found in Mail', 'Found in Natural Language'))
|
|
1730
|
+
AND (
|
|
1731
|
+
(oc.day >= ${startBound} AND oc.day < ${endBound})
|
|
1732
|
+
OR (
|
|
1733
|
+
ci.all_day = 0
|
|
1734
|
+
AND ${timedStart} < ${startBound}
|
|
1735
|
+
AND ${occEnd} > ${startBound}
|
|
1736
|
+
)
|
|
1737
|
+
)
|
|
1738
|
+
ORDER BY startMac ASC, title ASC
|
|
1739
|
+
`;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// Copy Calendar.sqlitedb (+ wal/shm) to /tmp for one query, then delete the copies.
|
|
1743
|
+
// Calendar.app holds a lock on the live db; sqlite can read a snapshot copy.
|
|
1744
|
+
function withCalendarCopy(fn) {
|
|
1745
|
+
const id = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1746
|
+
const tmpDb = path.join("/tmp", `${CALENDAR_TMP_PREFIX}${id}.sqlitedb`);
|
|
1747
|
+
const tmpWal = `${tmpDb}-wal`;
|
|
1748
|
+
const tmpShm = `${tmpDb}-shm`;
|
|
1749
|
+
const srcWal = `${CALENDAR_DB}-wal`;
|
|
1750
|
+
const srcShm = `${CALENDAR_DB}-shm`;
|
|
1751
|
+
|
|
1752
|
+
try {
|
|
1753
|
+
if (!fs.existsSync(CALENDAR_DB)) {
|
|
1754
|
+
throw new Error("Calendar database not found");
|
|
1755
|
+
}
|
|
1756
|
+
fs.copyFileSync(CALENDAR_DB, tmpDb);
|
|
1757
|
+
if (fs.existsSync(srcWal)) {
|
|
1758
|
+
fs.copyFileSync(srcWal, tmpWal);
|
|
1759
|
+
}
|
|
1760
|
+
if (fs.existsSync(srcShm)) {
|
|
1761
|
+
fs.copyFileSync(srcShm, tmpShm);
|
|
1762
|
+
}
|
|
1763
|
+
return fn(tmpDb);
|
|
1764
|
+
} finally {
|
|
1765
|
+
for (const file of [tmpDb, tmpWal, tmpShm]) {
|
|
1766
|
+
try {
|
|
1767
|
+
fs.unlinkSync(file);
|
|
1768
|
+
} catch {
|
|
1769
|
+
// already gone or never created
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// calendar_date: live OccurrenceCache for every occurrence on a local day
|
|
1776
|
+
export function getEventsOnDate(startMs, endMs) {
|
|
1777
|
+
try {
|
|
1778
|
+
if (!fs.existsSync(CALENDAR_DB)) {
|
|
1779
|
+
return { events: [], error: "Calendar database not found" };
|
|
1780
|
+
}
|
|
1781
|
+
const { startMac, endMac } = localDayToMacBounds(startMs, endMs);
|
|
1782
|
+
const query = buildEventsOnDateQuery(startMac, endMac);
|
|
1783
|
+
let lastError;
|
|
1784
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1785
|
+
try {
|
|
1786
|
+
const events = withCalendarCopy((dbPath) => {
|
|
1787
|
+
return safeSqlite3Json(dbPath, query, { timeout: 15000 });
|
|
1788
|
+
});
|
|
1789
|
+
return { events, error: null };
|
|
1790
|
+
} catch (e) {
|
|
1791
|
+
lastError = e;
|
|
1792
|
+
console.error(`Error getting events on date (attempt ${attempt + 1}):`, e.message);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return { events: [], error: lastError.message };
|
|
1796
|
+
} catch (e) {
|
|
1797
|
+
console.error("Error getting events on date:", e.message);
|
|
1798
|
+
return { events: [], error: e.message };
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1690
1802
|
// ============ NEW TOOLS - PHASE 2 ============
|
|
1691
1803
|
|
|
1692
1804
|
// mail_unread_count: Get count of unread emails via AppleScript
|
package/package.json
CHANGED
package/search.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as lancedb from "@lancedb/lancedb";
|
|
|
2
2
|
import * as chrono from "chrono-node";
|
|
3
3
|
import { safeOsascript } from "./lib/shell.js";
|
|
4
4
|
import { safeMatch, validateSearchQuery } from "./lib/validators.js";
|
|
5
|
-
import { embed, INDEX_DIR, getRecentEmails, getEmailsByDateRange, getRecentMessages, getConversation,
|
|
5
|
+
import { embed, INDEX_DIR, getRecentEmails, getEmailsByDateRange, getRecentMessages, getConversation, getEventsOnDate, resolveEmail, resolvePhone, formatContact } from "./indexer.js";
|
|
6
6
|
|
|
7
7
|
let db = null;
|
|
8
8
|
let tables = {};
|
|
@@ -1319,28 +1319,40 @@ export async function searchCalendar(query, options = {}) {
|
|
|
1319
1319
|
}
|
|
1320
1320
|
}
|
|
1321
1321
|
|
|
1322
|
-
//
|
|
1322
|
+
// Local midnight of parsed date through next local midnight (handles DST; not +24h)
|
|
1323
|
+
export function getLocalDayBounds(dateStr) {
|
|
1324
|
+
const start = parseNaturalDate(dateStr);
|
|
1325
|
+
if (start == null) return null;
|
|
1326
|
+
const endDate = new Date(start);
|
|
1327
|
+
endDate.setDate(endDate.getDate() + 1);
|
|
1328
|
+
return { start, end: endDate.getTime() };
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Get events on a specific date from live Calendar.sqlitedb (not the vector index)
|
|
1323
1332
|
export async function getCalendarDateResults(dateStr) {
|
|
1324
1333
|
try {
|
|
1325
|
-
const range =
|
|
1334
|
+
const range = getLocalDayBounds(dateStr);
|
|
1326
1335
|
if (!range) {
|
|
1327
1336
|
return { success: false, error: `Could not parse date: ${dateStr}` };
|
|
1328
1337
|
}
|
|
1329
1338
|
|
|
1330
1339
|
console.error(`[Calendar Date] Query for "${dateStr}"`);
|
|
1331
|
-
console.error(`[Calendar Date] Range: ${new Date(range.start).
|
|
1340
|
+
console.error(`[Calendar Date] Range: ${new Date(range.start).toString()} to ${new Date(range.end).toString()}`);
|
|
1332
1341
|
|
|
1333
|
-
const
|
|
1342
|
+
const { events, error } = getEventsOnDate(range.start, range.end);
|
|
1343
|
+
if (error) {
|
|
1344
|
+
return { success: false, error: `Error getting calendar events: ${error}` };
|
|
1345
|
+
}
|
|
1334
1346
|
|
|
1335
|
-
const formattedResults =
|
|
1347
|
+
const formattedResults = events.map((row, idx) => ({
|
|
1336
1348
|
index: idx + 1,
|
|
1337
1349
|
title: row.title || "No title",
|
|
1338
|
-
start: formatLocalDate(row.start) || "Unknown",
|
|
1339
|
-
startTimestamp: row.
|
|
1340
|
-
end: formatLocalDate(row.end) || "Unknown",
|
|
1350
|
+
start: formatLocalDate(row.start) || row.start || "Unknown",
|
|
1351
|
+
startTimestamp: row.startMac != null ? (Number(row.startMac) + 978307200) * 1000 : null,
|
|
1352
|
+
end: formatLocalDate(row.end) || row.end || "Unknown",
|
|
1341
1353
|
calendar: row.calendar || "Unknown",
|
|
1342
1354
|
location: row.location || "",
|
|
1343
|
-
isAllDay: row.isAllDay
|
|
1355
|
+
isAllDay: !!row.isAllDay
|
|
1344
1356
|
}));
|
|
1345
1357
|
|
|
1346
1358
|
const dateLabel = new Date(range.start).toLocaleDateString("en-US", {
|
|
@@ -1360,6 +1372,24 @@ export async function getCalendarDateResults(dateStr) {
|
|
|
1360
1372
|
}
|
|
1361
1373
|
}
|
|
1362
1374
|
|
|
1375
|
+
// Clamp a timed event to [dayStartMs, dayEndMs) and return minutes from local midnight.
|
|
1376
|
+
// Overnight events (23:00–01:00) contribute only the slice that falls on this day.
|
|
1377
|
+
export function clampBusyToLocalDay(evtStartMs, evtEndMs, dayStartMs, dayEndMs) {
|
|
1378
|
+
const clampedStart = Math.max(evtStartMs, dayStartMs);
|
|
1379
|
+
const clampedEnd = Math.min(evtEndMs, dayEndMs);
|
|
1380
|
+
if (!(clampedEnd > clampedStart)) return null;
|
|
1381
|
+
|
|
1382
|
+
const startMinutes = clampedStart <= dayStartMs
|
|
1383
|
+
? 0
|
|
1384
|
+
: new Date(clampedStart).getHours() * 60 + new Date(clampedStart).getMinutes();
|
|
1385
|
+
const endMinutes = clampedEnd >= dayEndMs
|
|
1386
|
+
? 24 * 60
|
|
1387
|
+
: new Date(clampedEnd).getHours() * 60 + new Date(clampedEnd).getMinutes();
|
|
1388
|
+
|
|
1389
|
+
if (!(endMinutes > startMinutes)) return null;
|
|
1390
|
+
return { startMinutes, endMinutes };
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1363
1393
|
// Calculate free time slots on a specific date
|
|
1364
1394
|
export async function calculateFreeTime(dateStr, options = {}) {
|
|
1365
1395
|
const {
|
|
@@ -1369,12 +1399,17 @@ export async function calculateFreeTime(dateStr, options = {}) {
|
|
|
1369
1399
|
} = options;
|
|
1370
1400
|
|
|
1371
1401
|
try {
|
|
1372
|
-
const range =
|
|
1402
|
+
const range = getLocalDayBounds(dateStr);
|
|
1373
1403
|
if (!range) {
|
|
1374
1404
|
return { success: false, error: `Could not parse date: ${dateStr}` };
|
|
1375
1405
|
}
|
|
1376
1406
|
|
|
1377
|
-
|
|
1407
|
+
const { events: liveEvents, error } = getEventsOnDate(range.start, range.end);
|
|
1408
|
+
if (error) {
|
|
1409
|
+
return { success: false, error: `Error calculating free time: ${error}` };
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
let events = liveEvents;
|
|
1378
1413
|
|
|
1379
1414
|
// Filter by calendar if specified
|
|
1380
1415
|
if (calendarName) {
|
|
@@ -1390,16 +1425,21 @@ export async function calculateFreeTime(dateStr, options = {}) {
|
|
|
1390
1425
|
continue;
|
|
1391
1426
|
}
|
|
1392
1427
|
|
|
1393
|
-
const evtStart =
|
|
1394
|
-
|
|
1428
|
+
const evtStart = evt.startMac != null
|
|
1429
|
+
? (Number(evt.startMac) + 978307200) * 1000
|
|
1430
|
+
: (evt.startTimestamp || parseDate(evt.start));
|
|
1431
|
+
const evtEnd = evt.endMac != null
|
|
1432
|
+
? (Number(evt.endMac) + 978307200) * 1000
|
|
1433
|
+
: (evt.end ? parseDate(evt.end) : evtStart + (60 * 60 * 1000));
|
|
1395
1434
|
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1435
|
+
const clamped = clampBusyToLocalDay(evtStart, evtEnd, range.start, range.end);
|
|
1436
|
+
if (!clamped) continue;
|
|
1398
1437
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1438
|
+
const start = Math.max(clamped.startMinutes, startHour * 60);
|
|
1439
|
+
const end = Math.min(clamped.endMinutes, endHour * 60);
|
|
1440
|
+
if (end > start) {
|
|
1441
|
+
busyPeriods.push({ start, end });
|
|
1442
|
+
}
|
|
1403
1443
|
}
|
|
1404
1444
|
|
|
1405
1445
|
// Sort busy periods
|