apple-tools-mcp 1.2.0 → 2.0.0
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 +261 -7
- package/contacts.js +71 -19
- package/index.js +133 -87
- package/indexer.js +33 -23
- package/lib/appleScript.js +292 -0
- package/lib/audit.js +22 -14
- package/lib/calendarWrite.js +594 -0
- package/lib/contactsWrite.js +300 -0
- package/lib/indexGate.js +49 -0
- package/lib/lancedbTables.js +307 -0
- package/lib/mailWrite.js +464 -0
- package/lib/messagesWrite.js +281 -0
- package/lib/writeBridge.js +214 -0
- package/lib/writeGuards.js +250 -0
- package/lib/writeRouting.js +69 -0
- package/lib/writeTools.js +396 -0
- package/package.json +4 -2
- package/scripts/smoke-writes.js +313 -0
- package/search.js +7 -18
package/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import fs from "fs";
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { validateEmailPath, stripHtmlTags, unfoldRfc822Headers, validateLimit, validateDaysBack, validateWeekOffset, toUnixMillis } from "./lib/validators.js";
|
|
12
|
-
import {
|
|
12
|
+
import { cycleEndFlags, indexUnavailableMessage, indexQueryGate } from "./lib/indexGate.js";
|
|
13
13
|
import { isIndexerMode } from "./lib/processMode.js";
|
|
14
14
|
import { loadResolvedIndexInterval, logResolvedInterval } from "./lib/config.js";
|
|
15
15
|
import { createIndexerLock, DEFAULT_LOCK_HEARTBEAT_MS } from "./lib/indexerLock.js";
|
|
@@ -22,6 +22,13 @@ import {
|
|
|
22
22
|
waitForIndexerLock,
|
|
23
23
|
beginOwnedIndexing
|
|
24
24
|
} from "./lib/indexerRuntime.js";
|
|
25
|
+
import {
|
|
26
|
+
WRITE_TOOL_DEFINITIONS,
|
|
27
|
+
isWriteTool,
|
|
28
|
+
dispatchWriteTool,
|
|
29
|
+
executeWriteToolLocally
|
|
30
|
+
} from "./lib/writeTools.js";
|
|
31
|
+
import { startWriteBridgeServer, defaultSocketPath } from "./lib/writeBridge.js";
|
|
25
32
|
|
|
26
33
|
const PACKAGE_VERSION = JSON.parse(
|
|
27
34
|
fs.readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
|
@@ -70,9 +77,37 @@ function stopLockHeartbeat() {
|
|
|
70
77
|
indexerLock.stopHeartbeat();
|
|
71
78
|
}
|
|
72
79
|
|
|
80
|
+
// Daemon-only: serves writes for stdio clients whose host app cannot be
|
|
81
|
+
// granted Contacts/Calendar automation (see lib/writeBridge.js).
|
|
82
|
+
const WRITE_SOCKET_PATH = defaultSocketPath();
|
|
83
|
+
let writeBridge = null;
|
|
84
|
+
|
|
85
|
+
function stopWriteBridge() {
|
|
86
|
+
if (!writeBridge) return;
|
|
87
|
+
try {
|
|
88
|
+
writeBridge.close();
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error("Error closing write bridge:", e.message);
|
|
91
|
+
}
|
|
92
|
+
writeBridge = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function startWriteBridge() {
|
|
96
|
+
try {
|
|
97
|
+
writeBridge = await startWriteBridgeServer({
|
|
98
|
+
socketPath: WRITE_SOCKET_PATH,
|
|
99
|
+
handler: (tool, args) => executeWriteToolLocally(tool, args),
|
|
100
|
+
log: (msg) => console.error(msg)
|
|
101
|
+
});
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.error(`Write bridge unavailable: ${e.message}. Writes will run in each MCP process.`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
73
107
|
function shutdownIndexing(exitCode) {
|
|
74
108
|
stopBackgroundIndexing();
|
|
75
109
|
stopLockHeartbeat();
|
|
110
|
+
stopWriteBridge();
|
|
76
111
|
releaseLock();
|
|
77
112
|
if (exitCode !== undefined) {
|
|
78
113
|
process.exit(exitCode);
|
|
@@ -83,6 +118,7 @@ function shutdownIndexing(exitCode) {
|
|
|
83
118
|
process.on("exit", () => {
|
|
84
119
|
stopBackgroundIndexing();
|
|
85
120
|
stopLockHeartbeat();
|
|
121
|
+
stopWriteBridge();
|
|
86
122
|
releaseLock();
|
|
87
123
|
});
|
|
88
124
|
process.on("SIGINT", () => {
|
|
@@ -180,14 +216,6 @@ async function checkIfFirstRun() {
|
|
|
180
216
|
return !(emailsReady || messagesReady || calendarReady);
|
|
181
217
|
}
|
|
182
218
|
|
|
183
|
-
// Get appropriate status message based on indexing state
|
|
184
|
-
function getIndexingMessage() {
|
|
185
|
-
if (isFirstEverRun) {
|
|
186
|
-
return "Building initial index. This may take several minutes on first run. Please try again shortly.";
|
|
187
|
-
}
|
|
188
|
-
return "Indexing new data. Please try again in a moment.";
|
|
189
|
-
}
|
|
190
|
-
|
|
191
219
|
// Run a single indexing cycle (called by background timer)
|
|
192
220
|
function runIndexCycle() {
|
|
193
221
|
const cycle = beginIndexCycle(indexingInProgress);
|
|
@@ -318,10 +346,30 @@ function applyCycleEnd(success) {
|
|
|
318
346
|
// Index-backed tools wait only while THIS process owns the lock and has not
|
|
319
347
|
// finished its cycle. Lost-lock secondaries fall through to isIndexReady().
|
|
320
348
|
function stillIndexingMessage() {
|
|
321
|
-
|
|
322
|
-
|
|
349
|
+
const gate = indexQueryGate({
|
|
350
|
+
sessionIndexComplete,
|
|
351
|
+
ownsIndexLock,
|
|
352
|
+
indexReady: true,
|
|
353
|
+
isFirstEverRun
|
|
354
|
+
});
|
|
355
|
+
return gate.ok ? null : gate.message;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Shared preflight for index-backed query tools (on-disk tables, not local cycle). */
|
|
359
|
+
async function requireIndex(type) {
|
|
360
|
+
const indexing = stillIndexingMessage();
|
|
361
|
+
if (indexing) {
|
|
362
|
+
return indexing;
|
|
323
363
|
}
|
|
324
|
-
|
|
364
|
+
const ready = await isIndexReady(type);
|
|
365
|
+
const gate = indexQueryGate({
|
|
366
|
+
sessionIndexComplete,
|
|
367
|
+
ownsIndexLock,
|
|
368
|
+
indexReady: ready,
|
|
369
|
+
type,
|
|
370
|
+
isFirstEverRun
|
|
371
|
+
});
|
|
372
|
+
return gate.ok ? null : gate.message;
|
|
325
373
|
}
|
|
326
374
|
|
|
327
375
|
function waitForLockAndStartDaemon() {
|
|
@@ -338,12 +386,27 @@ function waitForLockAndStartDaemon() {
|
|
|
338
386
|
|
|
339
387
|
// Initialize and start indexing
|
|
340
388
|
async function initializeIndexing() {
|
|
341
|
-
isFirstEverRun = await checkIfFirstRun();
|
|
342
|
-
|
|
343
389
|
if (INDEXER_MODE) {
|
|
344
390
|
console.error(`Apple Tools MCP indexer running (v${PACKAGE_VERSION})`);
|
|
345
391
|
logResolvedInterval(resolvedIndexInterval);
|
|
346
392
|
loggedIndexInterval = true;
|
|
393
|
+
// launchd started this process, so node owns its TCC prompts. Offer the
|
|
394
|
+
// write bridge to stdio clients whose host app cannot get those grants.
|
|
395
|
+
// This happens before any index work: writes must stay available even if
|
|
396
|
+
// the vector index is missing, locked, or unreadable.
|
|
397
|
+
await startWriteBridge();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
isFirstEverRun = await checkIfFirstRun();
|
|
402
|
+
} catch (e) {
|
|
403
|
+
// A failed readiness probe must not take the daemon (or its write
|
|
404
|
+
// bridge) down; assume a first run and let the cycle report the details.
|
|
405
|
+
console.error(`Could not determine index state: ${e.message}`);
|
|
406
|
+
isFirstEverRun = true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (INDEXER_MODE) {
|
|
347
410
|
waitForLockAndStartDaemon();
|
|
348
411
|
return;
|
|
349
412
|
}
|
|
@@ -355,7 +418,8 @@ async function initializeIndexing() {
|
|
|
355
418
|
if (!startup.startBackground) {
|
|
356
419
|
console.error("Another apple-tools-mcp instance is indexing. Server will run without background indexing.");
|
|
357
420
|
// Lost lock is not "still indexing": this process will never complete a
|
|
358
|
-
// local cycle. Searches proceed whenever isIndexReady() is true
|
|
421
|
+
// local cycle. Searches proceed whenever isIndexReady() is true (initDB
|
|
422
|
+
// re-lists shared on-disk tables; it must not cache an empty first connect).
|
|
359
423
|
ownsIndexLock = false;
|
|
360
424
|
return;
|
|
361
425
|
}
|
|
@@ -369,8 +433,12 @@ async function initializeIndexing() {
|
|
|
369
433
|
});
|
|
370
434
|
}
|
|
371
435
|
|
|
372
|
-
// Start indexing immediately on server startup
|
|
373
|
-
|
|
436
|
+
// Start indexing immediately on server startup. A startup failure is logged
|
|
437
|
+
// rather than rejected: an unhandled rejection would tear down the daemon,
|
|
438
|
+
// taking the write bridge with it.
|
|
439
|
+
initializeIndexing().catch((e) => {
|
|
440
|
+
console.error(`Indexing startup failed: ${e.message}`);
|
|
441
|
+
});
|
|
374
442
|
|
|
375
443
|
// ============ SEMANTIC SEARCH FUNCTIONS ============
|
|
376
444
|
|
|
@@ -379,14 +447,9 @@ async function mailSearch(query, options = {}) {
|
|
|
379
447
|
return "Error: query parameter is required for mail_search";
|
|
380
448
|
}
|
|
381
449
|
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
384
|
-
return
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const ready = await isIndexReady("emails");
|
|
388
|
-
if (!ready) {
|
|
389
|
-
return indexUnavailableMessage("emails");
|
|
450
|
+
const blocked = await requireIndex("emails");
|
|
451
|
+
if (blocked) {
|
|
452
|
+
return blocked;
|
|
390
453
|
}
|
|
391
454
|
|
|
392
455
|
const result = await searchEmails(query, options);
|
|
@@ -394,14 +457,9 @@ async function mailSearch(query, options = {}) {
|
|
|
394
457
|
}
|
|
395
458
|
|
|
396
459
|
async function mailRecent(limit = 30, daysBack = 7, unreadOnly = false, includeJunk = false) {
|
|
397
|
-
const
|
|
398
|
-
if (
|
|
399
|
-
return
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const ready = await isIndexReady("emails");
|
|
403
|
-
if (!ready) {
|
|
404
|
-
return indexUnavailableMessage("emails");
|
|
460
|
+
const blocked = await requireIndex("emails");
|
|
461
|
+
if (blocked) {
|
|
462
|
+
return blocked;
|
|
405
463
|
}
|
|
406
464
|
|
|
407
465
|
const result = await getRecentEmailResults(limit, daysBack, unreadOnly, includeJunk);
|
|
@@ -409,14 +467,9 @@ async function mailRecent(limit = 30, daysBack = 7, unreadOnly = false, includeJ
|
|
|
409
467
|
}
|
|
410
468
|
|
|
411
469
|
async function mailDate(date, includeJunk = false) {
|
|
412
|
-
const
|
|
413
|
-
if (
|
|
414
|
-
return
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const ready = await isIndexReady("emails");
|
|
418
|
-
if (!ready) {
|
|
419
|
-
return indexUnavailableMessage("emails");
|
|
470
|
+
const blocked = await requireIndex("emails");
|
|
471
|
+
if (blocked) {
|
|
472
|
+
return blocked;
|
|
420
473
|
}
|
|
421
474
|
|
|
422
475
|
const result = await getEmailDateResults(date, includeJunk);
|
|
@@ -424,14 +477,9 @@ async function mailDate(date, includeJunk = false) {
|
|
|
424
477
|
}
|
|
425
478
|
|
|
426
479
|
async function messagesSearch(query, options = {}) {
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
429
|
-
return
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const ready = await isIndexReady("messages");
|
|
433
|
-
if (!ready) {
|
|
434
|
-
return indexUnavailableMessage("messages");
|
|
480
|
+
const blocked = await requireIndex("messages");
|
|
481
|
+
if (blocked) {
|
|
482
|
+
return blocked;
|
|
435
483
|
}
|
|
436
484
|
|
|
437
485
|
const result = await searchMessages(query, options);
|
|
@@ -439,14 +487,9 @@ async function messagesSearch(query, options = {}) {
|
|
|
439
487
|
}
|
|
440
488
|
|
|
441
489
|
async function messagesRecent(limit = 10, daysBack = 1) {
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
return
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const ready = await isIndexReady("messages");
|
|
448
|
-
if (!ready) {
|
|
449
|
-
return indexUnavailableMessage("messages");
|
|
490
|
+
const blocked = await requireIndex("messages");
|
|
491
|
+
if (blocked) {
|
|
492
|
+
return blocked;
|
|
450
493
|
}
|
|
451
494
|
|
|
452
495
|
const result = await getRecentMessageResults(limit, daysBack);
|
|
@@ -454,14 +497,9 @@ async function messagesRecent(limit = 10, daysBack = 1) {
|
|
|
454
497
|
}
|
|
455
498
|
|
|
456
499
|
async function messagesConversation(contact, limit = 50) {
|
|
457
|
-
const
|
|
458
|
-
if (
|
|
459
|
-
return
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const ready = await isIndexReady("messages");
|
|
463
|
-
if (!ready) {
|
|
464
|
-
return indexUnavailableMessage("messages");
|
|
500
|
+
const blocked = await requireIndex("messages");
|
|
501
|
+
if (blocked) {
|
|
502
|
+
return blocked;
|
|
465
503
|
}
|
|
466
504
|
|
|
467
505
|
const result = await getConversationResults(contact, limit);
|
|
@@ -469,14 +507,9 @@ async function messagesConversation(contact, limit = 50) {
|
|
|
469
507
|
}
|
|
470
508
|
|
|
471
509
|
async function calendarSearch(query, options = {}) {
|
|
472
|
-
const
|
|
473
|
-
if (
|
|
474
|
-
return
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const ready = await isIndexReady("calendar");
|
|
478
|
-
if (!ready) {
|
|
479
|
-
return indexUnavailableMessage("calendar");
|
|
510
|
+
const blocked = await requireIndex("calendar");
|
|
511
|
+
if (blocked) {
|
|
512
|
+
return blocked;
|
|
480
513
|
}
|
|
481
514
|
|
|
482
515
|
const result = await searchCalendar(query, options);
|
|
@@ -772,6 +805,7 @@ function formatContactsSearchResults(contacts) {
|
|
|
772
805
|
output += `• ${c.displayName}`;
|
|
773
806
|
if (c.organization) output += ` (${c.organization})`;
|
|
774
807
|
output += "\n";
|
|
808
|
+
if (c.uniqueId) output += ` Contact ID: ${c.uniqueId}\n`;
|
|
775
809
|
if (c.emails.length > 0) {
|
|
776
810
|
output += ` Emails: ${c.emails.map(e => e.email).join(", ")}\n`;
|
|
777
811
|
}
|
|
@@ -791,6 +825,7 @@ function formatContactLookupResult(contact) {
|
|
|
791
825
|
let output = `Contact: ${contact.displayName}\n`;
|
|
792
826
|
output += "─".repeat(40) + "\n";
|
|
793
827
|
|
|
828
|
+
if (contact.uniqueId) output += `Contact ID: ${contact.uniqueId}\n`;
|
|
794
829
|
if (contact.organization) output += `Organization: ${contact.organization}\n`;
|
|
795
830
|
if (contact.department) output += `Department: ${contact.department}\n`;
|
|
796
831
|
if (contact.jobTitle) output += `Job Title: ${contact.jobTitle}\n`;
|
|
@@ -1292,6 +1327,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1292
1327
|
required: ["name"],
|
|
1293
1328
|
},
|
|
1294
1329
|
},
|
|
1330
|
+
|
|
1331
|
+
// ============ WRITE TOOLS (2.0.0) ============
|
|
1332
|
+
// Mail / Messages / Calendar / Contacts writes with dry_run + confirm.
|
|
1333
|
+
...WRITE_TOOL_DEFINITIONS,
|
|
1295
1334
|
],
|
|
1296
1335
|
}));
|
|
1297
1336
|
|
|
@@ -1302,6 +1341,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1302
1341
|
try {
|
|
1303
1342
|
let result;
|
|
1304
1343
|
|
|
1344
|
+
// Writes never wait on the vector index: they talk to Mail / Messages /
|
|
1345
|
+
// Calendar / Contacts directly and must keep working while the indexer
|
|
1346
|
+
// daemon holds the lock.
|
|
1347
|
+
if (isWriteTool(name)) {
|
|
1348
|
+
const writeResult = await dispatchWriteTool(name, args || {}, {
|
|
1349
|
+
indexerMode: INDEXER_MODE,
|
|
1350
|
+
socketPath: WRITE_SOCKET_PATH,
|
|
1351
|
+
log: (msg) => console.error(msg)
|
|
1352
|
+
});
|
|
1353
|
+
return {
|
|
1354
|
+
content: [{ type: "text", text: writeResult.message }],
|
|
1355
|
+
...(writeResult.ok === false ? { isError: true } : {})
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1305
1359
|
switch (name) {
|
|
1306
1360
|
// Smart search (agentic)
|
|
1307
1361
|
case "smart_search":
|
|
@@ -1397,16 +1451,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1397
1451
|
// Mail tools
|
|
1398
1452
|
case "mail_senders":
|
|
1399
1453
|
{
|
|
1400
|
-
const
|
|
1401
|
-
if (
|
|
1402
|
-
result =
|
|
1454
|
+
const blocked = await requireIndex("emails");
|
|
1455
|
+
if (blocked) {
|
|
1456
|
+
result = blocked;
|
|
1403
1457
|
break;
|
|
1404
1458
|
}
|
|
1405
1459
|
}
|
|
1406
|
-
if (!(await isIndexReady("emails"))) {
|
|
1407
|
-
result = indexUnavailableMessage("emails");
|
|
1408
|
-
break;
|
|
1409
|
-
}
|
|
1410
1460
|
result = formatSendersResults(await getFrequentSenders(
|
|
1411
1461
|
validateLimit(args?.limit, 30),
|
|
1412
1462
|
validateDaysBack(args?.days_back),
|
|
@@ -1481,16 +1531,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1481
1531
|
|
|
1482
1532
|
case "mail_thread":
|
|
1483
1533
|
{
|
|
1484
|
-
const
|
|
1485
|
-
if (
|
|
1486
|
-
result =
|
|
1534
|
+
const blocked = await requireIndex("emails");
|
|
1535
|
+
if (blocked) {
|
|
1536
|
+
result = blocked;
|
|
1487
1537
|
break;
|
|
1488
1538
|
}
|
|
1489
1539
|
}
|
|
1490
|
-
if (!(await isIndexReady("emails"))) {
|
|
1491
|
-
result = indexUnavailableMessage("emails");
|
|
1492
|
-
break;
|
|
1493
|
-
}
|
|
1494
1540
|
result = formatEmailThreadResults(await getEmailThread(args.file_path, validateLimit(args?.limit, 30)));
|
|
1495
1541
|
break;
|
|
1496
1542
|
|
package/indexer.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./lib/validators.js";
|
|
17
17
|
import { safeSqlite3Json, safeOsascript, safeFind } from "./lib/shell.js";
|
|
18
18
|
import { indexUnavailableMessage } from "./lib/indexGate.js";
|
|
19
|
+
import { createLanceTableCache, LANCE_CONNECT_OPTIONS } from "./lib/lancedbTables.js";
|
|
19
20
|
|
|
20
21
|
// Re-export contact functions for use by other modules
|
|
21
22
|
export {
|
|
@@ -70,8 +71,17 @@ const MAC_ABSOLUTE_EPOCH = 978307200;
|
|
|
70
71
|
export const CALENDAR_TMP_PREFIX = "apple-tools-cal-";
|
|
71
72
|
|
|
72
73
|
let embeddingPipeline = null;
|
|
74
|
+
|
|
75
|
+
// One connection per process. Do not early-return a first connect that saw an
|
|
76
|
+
// empty catalog — MCP stdio must pick up tables the indexer daemon already wrote.
|
|
77
|
+
const lanceCache = createLanceTableCache({
|
|
78
|
+
connect: (uri, options) => lancedb.connect(uri, options),
|
|
79
|
+
indexDir: INDEX_DIR,
|
|
80
|
+
mkdirSync: (p, o) => fs.mkdirSync(p, o),
|
|
81
|
+
connectOptions: LANCE_CONNECT_OPTIONS
|
|
82
|
+
});
|
|
83
|
+
const tables = lanceCache.tables;
|
|
73
84
|
let db = null;
|
|
74
|
-
let tables = {};
|
|
75
85
|
|
|
76
86
|
async function getEmbedder() {
|
|
77
87
|
if (!embeddingPipeline) {
|
|
@@ -751,23 +761,19 @@ function getCalendarEvents() {
|
|
|
751
761
|
// ============ DATABASE FUNCTIONS ============
|
|
752
762
|
|
|
753
763
|
export async function initDB() {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
764
|
+
const result = await lanceCache.initDB();
|
|
765
|
+
db = result.db;
|
|
766
|
+
return { db, tables };
|
|
767
|
+
}
|
|
758
768
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
if (tableNames.includes("messages")) {
|
|
764
|
-
tables.messages = await db.openTable("messages");
|
|
765
|
-
}
|
|
766
|
-
if (tableNames.includes("calendar")) {
|
|
767
|
-
tables.calendar = await db.openTable("calendar");
|
|
768
|
-
}
|
|
769
|
+
export async function getOpenTable(type) {
|
|
770
|
+
await initDB();
|
|
771
|
+
return tables[type] || null;
|
|
772
|
+
}
|
|
769
773
|
|
|
770
|
-
|
|
774
|
+
function resetLanceConnection() {
|
|
775
|
+
lanceCache.reset();
|
|
776
|
+
db = null;
|
|
771
777
|
}
|
|
772
778
|
|
|
773
779
|
export async function clearEmailsTable() {
|
|
@@ -828,10 +834,9 @@ export async function rebuildIndex(sources = ["emails", "messages", "calendar"],
|
|
|
828
834
|
}
|
|
829
835
|
}
|
|
830
836
|
|
|
831
|
-
// Reset
|
|
832
|
-
//
|
|
833
|
-
|
|
834
|
-
tables = {};
|
|
837
|
+
// Reset connection cache after dropping tables so initDB() re-opens
|
|
838
|
+
// (or recreates) them instead of holding stale/empty handles.
|
|
839
|
+
resetLanceConnection();
|
|
835
840
|
|
|
836
841
|
// Re-index requested sources
|
|
837
842
|
for (const source of sources) {
|
|
@@ -1451,7 +1456,7 @@ export async function indexAll(progressCallback = null) {
|
|
|
1451
1456
|
|
|
1452
1457
|
export async function isIndexReady(type = "emails") {
|
|
1453
1458
|
await initDB();
|
|
1454
|
-
return tables[type]
|
|
1459
|
+
return tables[type] != null;
|
|
1455
1460
|
}
|
|
1456
1461
|
|
|
1457
1462
|
// ============ DIRECT QUERIES (for recent items) ============
|
|
@@ -1736,7 +1741,9 @@ export function localDayToMacBounds(startMs, endMs) {
|
|
|
1736
1741
|
}
|
|
1737
1742
|
|
|
1738
1743
|
// Date-bounded OccurrenceCache query: one row per occurrence, no GROUP BY ci.ROWID
|
|
1739
|
-
|
|
1744
|
+
// includeUid adds the iCal UID that the calendar write tools address events by.
|
|
1745
|
+
// Older Calendar schemas may lack the column, so callers can retry without it.
|
|
1746
|
+
export function buildEventsOnDateQuery(startMac, endMac, { includeUid = true } = {}) {
|
|
1740
1747
|
const startBound = Math.floor(Number(startMac));
|
|
1741
1748
|
const endBound = Math.floor(Number(endMac));
|
|
1742
1749
|
if (!Number.isFinite(startBound) || !Number.isFinite(endBound)) {
|
|
@@ -1750,6 +1757,7 @@ export function buildEventsOnDateQuery(startMac, endMac) {
|
|
|
1750
1757
|
return `
|
|
1751
1758
|
SELECT DISTINCT
|
|
1752
1759
|
ci.ROWID as itemId,
|
|
1760
|
+
${includeUid ? "ci.unique_identifier as uid," : "'' as uid,"}
|
|
1753
1761
|
ci.summary as title,
|
|
1754
1762
|
datetime(CASE WHEN ci.all_day THEN ${allDayStartMac} ELSE ${timedStart} END + 978307200, 'unixepoch', 'localtime') as start,
|
|
1755
1763
|
datetime(${occEnd} + 978307200, 'unixepoch', 'localtime') as end,
|
|
@@ -1816,9 +1824,11 @@ export function getEventsOnDate(startMs, endMs) {
|
|
|
1816
1824
|
return { events: [], error: "Calendar database not found" };
|
|
1817
1825
|
}
|
|
1818
1826
|
const { startMac, endMac } = localDayToMacBounds(startMs, endMs);
|
|
1819
|
-
const query = buildEventsOnDateQuery(startMac, endMac);
|
|
1820
1827
|
let lastError;
|
|
1821
1828
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1829
|
+
// Drop the UID column after a failed first attempt: a schema without
|
|
1830
|
+
// unique_identifier must still return events for calendar_date.
|
|
1831
|
+
const query = buildEventsOnDateQuery(startMac, endMac, { includeUid: attempt === 0 });
|
|
1822
1832
|
try {
|
|
1823
1833
|
const events = withCalendarCopy((dbPath) => {
|
|
1824
1834
|
return safeSqlite3Json(dbPath, query, { timeout: 15000 });
|