apple-tools-mcp 1.1.1 → 1.1.3
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/index.js +154 -67
- package/indexer.js +98 -59
- package/lib/indexGate.js +59 -0
- package/lib/shell.js +12 -5
- package/lib/validators.js +64 -2
- package/package.json +1 -1
- package/search.js +50 -21
package/index.js
CHANGED
|
@@ -8,11 +8,20 @@ import {
|
|
|
8
8
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
9
|
import fs from "fs";
|
|
10
10
|
import path from "path";
|
|
11
|
-
import { validateEmailPath, stripHtmlTags } from "./lib/validators.js";
|
|
11
|
+
import { validateEmailPath, stripHtmlTags, unfoldRfc822Headers, validateLimit, validateDaysBack, validateWeekOffset, toUnixMillis } from "./lib/validators.js";
|
|
12
|
+
import { isSearchBlockedByIndexing, cycleEndFlags, indexUnavailableMessage } from "./lib/indexGate.js";
|
|
13
|
+
|
|
14
|
+
const PACKAGE_VERSION = JSON.parse(
|
|
15
|
+
fs.readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
|
16
|
+
).version;
|
|
12
17
|
|
|
13
18
|
// Lock file to prevent duplicate indexing processes
|
|
14
19
|
const LOCK_FILE = path.join(process.env.HOME, ".apple-tools-mcp", "indexer.lock");
|
|
15
20
|
const LOCK_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes - if lock is older, assume hung process
|
|
21
|
+
// True only while this process won the indexer lock. Distinct from
|
|
22
|
+
// sessionIndexComplete: a secondary instance that lost the lock never
|
|
23
|
+
// completes a local cycle and must not stay on "still indexing" forever.
|
|
24
|
+
let ownsIndexLock = false;
|
|
16
25
|
|
|
17
26
|
function acquireLock() {
|
|
18
27
|
try {
|
|
@@ -32,6 +41,7 @@ function acquireLock() {
|
|
|
32
41
|
|
|
33
42
|
// If we already hold the lock, return true
|
|
34
43
|
if (pid === process.pid) {
|
|
44
|
+
ownsIndexLock = true;
|
|
35
45
|
return true;
|
|
36
46
|
}
|
|
37
47
|
|
|
@@ -44,6 +54,7 @@ function acquireLock() {
|
|
|
44
54
|
fs.unlinkSync(LOCK_FILE);
|
|
45
55
|
} else {
|
|
46
56
|
console.error(`Another indexing instance running (PID ${pid}). Skipping indexing.`);
|
|
57
|
+
ownsIndexLock = false;
|
|
47
58
|
return false;
|
|
48
59
|
}
|
|
49
60
|
} catch {
|
|
@@ -57,17 +68,20 @@ function acquireLock() {
|
|
|
57
68
|
// This prevents TOCTOU race condition - will throw EEXIST if file was created between check and write
|
|
58
69
|
try {
|
|
59
70
|
fs.writeFileSync(LOCK_FILE, `${process.pid}:${Date.now()}`, { flag: 'wx' });
|
|
71
|
+
ownsIndexLock = true;
|
|
60
72
|
return true;
|
|
61
73
|
} catch (err) {
|
|
62
74
|
if (err.code === 'EEXIST') {
|
|
63
75
|
// Another process won the race
|
|
64
76
|
console.error("Another process acquired lock during race. Skipping indexing.");
|
|
77
|
+
ownsIndexLock = false;
|
|
65
78
|
return false;
|
|
66
79
|
}
|
|
67
80
|
throw err; // Re-throw unexpected errors
|
|
68
81
|
}
|
|
69
82
|
} catch (e) {
|
|
70
83
|
console.error("Lock file error:", e.message);
|
|
84
|
+
ownsIndexLock = false;
|
|
71
85
|
return false; // On error, fail safe - don't proceed
|
|
72
86
|
}
|
|
73
87
|
}
|
|
@@ -80,6 +94,7 @@ function releaseLock() {
|
|
|
80
94
|
const pid = parseInt(pidStr);
|
|
81
95
|
if (pid === process.pid) {
|
|
82
96
|
fs.unlinkSync(LOCK_FILE);
|
|
97
|
+
ownsIndexLock = false;
|
|
83
98
|
console.error(`Released lock file (PID ${process.pid})`);
|
|
84
99
|
}
|
|
85
100
|
}
|
|
@@ -234,12 +249,12 @@ function runIndexCycle() {
|
|
|
234
249
|
progressCheckTimer = setInterval(() => {
|
|
235
250
|
const timeSinceProgress = Date.now() - lastProgressTime;
|
|
236
251
|
if (timeSinceProgress > MAX_NO_PROGRESS_MS) {
|
|
237
|
-
console.error(`⚠️ No indexing progress for ${Math.round(timeSinceProgress / 60000)} minutes.
|
|
252
|
+
console.error(`⚠️ No indexing progress for ${Math.round(timeSinceProgress / 60000)} minutes. Indexing still running; not starting another cycle.`);
|
|
238
253
|
clearInterval(progressCheckTimer);
|
|
239
254
|
progressCheckTimer = null;
|
|
240
|
-
indexingInProgress
|
|
255
|
+
// Allow searches, but do NOT clear indexingInProgress or release the lock
|
|
256
|
+
// while indexAll() is still running — overlapping writes can corrupt LanceDB.
|
|
241
257
|
sessionIndexComplete = true;
|
|
242
|
-
releaseLock();
|
|
243
258
|
}
|
|
244
259
|
}, PROGRESS_CHECK_INTERVAL_MS);
|
|
245
260
|
|
|
@@ -259,11 +274,8 @@ function runIndexCycle() {
|
|
|
259
274
|
|
|
260
275
|
lastIndexTime = Date.now();
|
|
261
276
|
lastProgressTime = Date.now();
|
|
262
|
-
|
|
263
|
-
sessionIndexComplete = true;
|
|
264
|
-
isFirstEverRun = false; // After successful index, no longer first run
|
|
277
|
+
applyCycleEnd(true);
|
|
265
278
|
console.error("Indexing complete.");
|
|
266
|
-
releaseLock(); // Allow other instances to index
|
|
267
279
|
// Pre-warm tables to eliminate first-query latency
|
|
268
280
|
await prewarmTables();
|
|
269
281
|
}).catch(e => {
|
|
@@ -274,9 +286,7 @@ function runIndexCycle() {
|
|
|
274
286
|
}
|
|
275
287
|
|
|
276
288
|
console.error("Indexing error:", e.message);
|
|
277
|
-
|
|
278
|
-
sessionIndexComplete = true; // Mark complete even on error so queries can proceed
|
|
279
|
-
releaseLock(); // Allow other instances to index
|
|
289
|
+
applyCycleEnd(false);
|
|
280
290
|
});
|
|
281
291
|
}
|
|
282
292
|
|
|
@@ -313,6 +323,28 @@ function stopBackgroundIndexing() {
|
|
|
313
323
|
console.error("Background indexing stopped");
|
|
314
324
|
}
|
|
315
325
|
|
|
326
|
+
// Unblock searches and drop the indexer lock after a cycle ends.
|
|
327
|
+
// Must run on failure as well as success so tools are not stuck forever.
|
|
328
|
+
function applyCycleEnd(success) {
|
|
329
|
+
const flags = cycleEndFlags(success);
|
|
330
|
+
indexingInProgress = flags.indexingInProgress;
|
|
331
|
+
sessionIndexComplete = flags.sessionIndexComplete;
|
|
332
|
+
ownsIndexLock = flags.ownsIndexLock;
|
|
333
|
+
if (flags.isFirstEverRun === false) {
|
|
334
|
+
isFirstEverRun = false;
|
|
335
|
+
}
|
|
336
|
+
releaseLock();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Index-backed tools wait only while THIS process owns the lock and has not
|
|
340
|
+
// finished its cycle. Lost-lock secondaries fall through to isIndexReady().
|
|
341
|
+
function stillIndexingMessage() {
|
|
342
|
+
if (isSearchBlockedByIndexing(sessionIndexComplete, ownsIndexLock)) {
|
|
343
|
+
return getIndexingMessage();
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
316
348
|
// Initialize and start indexing
|
|
317
349
|
async function initializeIndexing() {
|
|
318
350
|
isFirstEverRun = await checkIfFirstRun();
|
|
@@ -321,6 +353,9 @@ async function initializeIndexing() {
|
|
|
321
353
|
// indexing but keep the MCP server running so search still works.
|
|
322
354
|
if (!acquireLock()) {
|
|
323
355
|
console.error("Another apple-tools-mcp instance is indexing. Server will run without background indexing.");
|
|
356
|
+
// Lost lock is not "still indexing": this process will never complete a
|
|
357
|
+
// local cycle. Searches proceed whenever isIndexReady() is true.
|
|
358
|
+
ownsIndexLock = false;
|
|
324
359
|
return;
|
|
325
360
|
}
|
|
326
361
|
|
|
@@ -338,13 +373,14 @@ async function mailSearch(query, options = {}) {
|
|
|
338
373
|
return "Error: query parameter is required for mail_search";
|
|
339
374
|
}
|
|
340
375
|
|
|
341
|
-
|
|
342
|
-
|
|
376
|
+
const indexing = stillIndexingMessage();
|
|
377
|
+
if (indexing) {
|
|
378
|
+
return indexing;
|
|
343
379
|
}
|
|
344
380
|
|
|
345
381
|
const ready = await isIndexReady("emails");
|
|
346
382
|
if (!ready) {
|
|
347
|
-
return "
|
|
383
|
+
return indexUnavailableMessage("emails");
|
|
348
384
|
}
|
|
349
385
|
|
|
350
386
|
const result = await searchEmails(query, options);
|
|
@@ -352,13 +388,14 @@ async function mailSearch(query, options = {}) {
|
|
|
352
388
|
}
|
|
353
389
|
|
|
354
390
|
async function mailRecent(limit = 30, daysBack = 7, unreadOnly = false, includeJunk = false) {
|
|
355
|
-
|
|
356
|
-
|
|
391
|
+
const indexing = stillIndexingMessage();
|
|
392
|
+
if (indexing) {
|
|
393
|
+
return indexing;
|
|
357
394
|
}
|
|
358
395
|
|
|
359
396
|
const ready = await isIndexReady("emails");
|
|
360
397
|
if (!ready) {
|
|
361
|
-
return "
|
|
398
|
+
return indexUnavailableMessage("emails");
|
|
362
399
|
}
|
|
363
400
|
|
|
364
401
|
const result = await getRecentEmailResults(limit, daysBack, unreadOnly, includeJunk);
|
|
@@ -366,13 +403,14 @@ async function mailRecent(limit = 30, daysBack = 7, unreadOnly = false, includeJ
|
|
|
366
403
|
}
|
|
367
404
|
|
|
368
405
|
async function mailDate(date, includeJunk = false) {
|
|
369
|
-
|
|
370
|
-
|
|
406
|
+
const indexing = stillIndexingMessage();
|
|
407
|
+
if (indexing) {
|
|
408
|
+
return indexing;
|
|
371
409
|
}
|
|
372
410
|
|
|
373
411
|
const ready = await isIndexReady("emails");
|
|
374
412
|
if (!ready) {
|
|
375
|
-
return "
|
|
413
|
+
return indexUnavailableMessage("emails");
|
|
376
414
|
}
|
|
377
415
|
|
|
378
416
|
const result = await getEmailDateResults(date, includeJunk);
|
|
@@ -380,13 +418,14 @@ async function mailDate(date, includeJunk = false) {
|
|
|
380
418
|
}
|
|
381
419
|
|
|
382
420
|
async function messagesSearch(query, options = {}) {
|
|
383
|
-
|
|
384
|
-
|
|
421
|
+
const indexing = stillIndexingMessage();
|
|
422
|
+
if (indexing) {
|
|
423
|
+
return indexing;
|
|
385
424
|
}
|
|
386
425
|
|
|
387
426
|
const ready = await isIndexReady("messages");
|
|
388
427
|
if (!ready) {
|
|
389
|
-
return "
|
|
428
|
+
return indexUnavailableMessage("messages");
|
|
390
429
|
}
|
|
391
430
|
|
|
392
431
|
const result = await searchMessages(query, options);
|
|
@@ -394,13 +433,14 @@ async function messagesSearch(query, options = {}) {
|
|
|
394
433
|
}
|
|
395
434
|
|
|
396
435
|
async function messagesRecent(limit = 10, daysBack = 1) {
|
|
397
|
-
|
|
398
|
-
|
|
436
|
+
const indexing = stillIndexingMessage();
|
|
437
|
+
if (indexing) {
|
|
438
|
+
return indexing;
|
|
399
439
|
}
|
|
400
440
|
|
|
401
441
|
const ready = await isIndexReady("messages");
|
|
402
442
|
if (!ready) {
|
|
403
|
-
return "
|
|
443
|
+
return indexUnavailableMessage("messages");
|
|
404
444
|
}
|
|
405
445
|
|
|
406
446
|
const result = await getRecentMessageResults(limit, daysBack);
|
|
@@ -408,13 +448,14 @@ async function messagesRecent(limit = 10, daysBack = 1) {
|
|
|
408
448
|
}
|
|
409
449
|
|
|
410
450
|
async function messagesConversation(contact, limit = 50) {
|
|
411
|
-
|
|
412
|
-
|
|
451
|
+
const indexing = stillIndexingMessage();
|
|
452
|
+
if (indexing) {
|
|
453
|
+
return indexing;
|
|
413
454
|
}
|
|
414
455
|
|
|
415
456
|
const ready = await isIndexReady("messages");
|
|
416
457
|
if (!ready) {
|
|
417
|
-
return "
|
|
458
|
+
return indexUnavailableMessage("messages");
|
|
418
459
|
}
|
|
419
460
|
|
|
420
461
|
const result = await getConversationResults(contact, limit);
|
|
@@ -422,13 +463,14 @@ async function messagesConversation(contact, limit = 50) {
|
|
|
422
463
|
}
|
|
423
464
|
|
|
424
465
|
async function calendarSearch(query, options = {}) {
|
|
425
|
-
|
|
426
|
-
|
|
466
|
+
const indexing = stillIndexingMessage();
|
|
467
|
+
if (indexing) {
|
|
468
|
+
return indexing;
|
|
427
469
|
}
|
|
428
470
|
|
|
429
471
|
const ready = await isIndexReady("calendar");
|
|
430
472
|
if (!ready) {
|
|
431
|
-
return "
|
|
473
|
+
return indexUnavailableMessage("calendar");
|
|
432
474
|
}
|
|
433
475
|
|
|
434
476
|
const result = await searchCalendar(query, options);
|
|
@@ -464,7 +506,7 @@ function readFullEmail(filePath) {
|
|
|
464
506
|
return "Email file not found.";
|
|
465
507
|
}
|
|
466
508
|
|
|
467
|
-
const content = fs.readFileSync(validatedPath, 'utf-8');
|
|
509
|
+
const content = unfoldRfc822Headers(fs.readFileSync(validatedPath, 'utf-8'));
|
|
468
510
|
|
|
469
511
|
// Parse email headers and body
|
|
470
512
|
const fromMatch = content.match(/^From:\s*(.+)$/m);
|
|
@@ -565,7 +607,7 @@ function formatSmartSearchResults(results, synthesizedGroups = null) {
|
|
|
565
607
|
sections.push(` [${r.rank}] Score: ${r.score}`);
|
|
566
608
|
sections.push(` From: ${r.sender}${r.isGroupChat ? ' (Group)' : ''}`);
|
|
567
609
|
sections.push(` Date: ${r.date}`);
|
|
568
|
-
sections.push(` Text: ${r.text.substring(0, 100)}
|
|
610
|
+
sections.push(` Text: ${(r.text || "").substring(0, 100)}${(r.text || "").length > 100 ? "..." : ""}`);
|
|
569
611
|
}
|
|
570
612
|
sections.push("");
|
|
571
613
|
}
|
|
@@ -591,6 +633,11 @@ function formatSmartSearchResults(results, synthesizedGroups = null) {
|
|
|
591
633
|
|
|
592
634
|
// Smart search - routes to appropriate sources and optionally synthesizes results
|
|
593
635
|
async function smartSearch(query, options = {}) {
|
|
636
|
+
const indexing = stillIndexingMessage();
|
|
637
|
+
if (indexing) {
|
|
638
|
+
return indexing;
|
|
639
|
+
}
|
|
640
|
+
|
|
594
641
|
const { limit = 5, synthesize = true } = options;
|
|
595
642
|
|
|
596
643
|
const sources = detectSources(query);
|
|
@@ -634,6 +681,10 @@ async function smartSearch(query, options = {}) {
|
|
|
634
681
|
|
|
635
682
|
await Promise.all(searches);
|
|
636
683
|
|
|
684
|
+
if (Object.keys(results).length === 0) {
|
|
685
|
+
return indexUnavailableMessage();
|
|
686
|
+
}
|
|
687
|
+
|
|
637
688
|
// Synthesize results into timeline if multiple sources returned data
|
|
638
689
|
let synthesizedGroups = null;
|
|
639
690
|
if (synthesize) {
|
|
@@ -671,21 +722,21 @@ function synthesizeResults(mailResults, messageResults, calendarResults) {
|
|
|
671
722
|
|
|
672
723
|
// Add mail results
|
|
673
724
|
for (const r of mailResults) {
|
|
674
|
-
const ts = r.dateTimestamp || new Date(r.date).getTime();
|
|
725
|
+
const ts = toUnixMillis(r.dateTimestamp) || new Date(r.date).getTime();
|
|
675
726
|
const bucket = getBucket(ts);
|
|
676
727
|
if (bucket) bucket.mail.push(r);
|
|
677
728
|
}
|
|
678
729
|
|
|
679
730
|
// Add message results
|
|
680
731
|
for (const r of messageResults) {
|
|
681
|
-
const ts = r.dateTimestamp || new Date(r.date).getTime();
|
|
732
|
+
const ts = toUnixMillis(r.dateTimestamp) || new Date(r.date).getTime();
|
|
682
733
|
const bucket = getBucket(ts);
|
|
683
734
|
if (bucket) bucket.messages.push(r);
|
|
684
735
|
}
|
|
685
736
|
|
|
686
737
|
// Add calendar results
|
|
687
738
|
for (const r of calendarResults) {
|
|
688
|
-
const ts = r.startTimestamp || new Date(r.start).getTime();
|
|
739
|
+
const ts = toUnixMillis(r.startTimestamp) || new Date(r.start).getTime();
|
|
689
740
|
const bucket = getBucket(ts);
|
|
690
741
|
if (bucket) bucket.calendar.push(r);
|
|
691
742
|
}
|
|
@@ -759,6 +810,11 @@ function formatContactLookupResult(contact) {
|
|
|
759
810
|
// ============ PERSON SEARCH (CROSS-SOURCE) ============
|
|
760
811
|
|
|
761
812
|
async function personSearch(name, limit = 10) {
|
|
813
|
+
const indexing = stillIndexingMessage();
|
|
814
|
+
if (indexing) {
|
|
815
|
+
return indexing;
|
|
816
|
+
}
|
|
817
|
+
|
|
762
818
|
// First, try to find the contact to get all their identifiers
|
|
763
819
|
const contacts = searchContacts(name, 5);
|
|
764
820
|
|
|
@@ -887,7 +943,7 @@ function formatPersonSearchResults(results) {
|
|
|
887
943
|
if (results.messages && results.messages.results && results.messages.results.length > 0) {
|
|
888
944
|
sections.push(`💬 MESSAGES (${results.messages.results.length}):`);
|
|
889
945
|
for (const r of results.messages.results.slice(0, 10)) {
|
|
890
|
-
sections.push(` • ${r.text.substring(0, 80)}${r.text.length > 80 ? "..." : ""}`);
|
|
946
|
+
sections.push(` • ${(r.text || "").substring(0, 80)}${(r.text || "").length > 80 ? "..." : ""}`);
|
|
891
947
|
sections.push(` ${r.date}`);
|
|
892
948
|
}
|
|
893
949
|
sections.push("");
|
|
@@ -916,7 +972,7 @@ function formatPersonSearchResults(results) {
|
|
|
916
972
|
// ============ MCP SERVER SETUP ============
|
|
917
973
|
|
|
918
974
|
const server = new Server(
|
|
919
|
-
{ name: "apple-tools-mcp", version:
|
|
975
|
+
{ name: "apple-tools-mcp", version: PACKAGE_VERSION },
|
|
920
976
|
{ capabilities: { tools: {} } }
|
|
921
977
|
);
|
|
922
978
|
|
|
@@ -1244,7 +1300,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1244
1300
|
// Smart search (agentic)
|
|
1245
1301
|
case "smart_search":
|
|
1246
1302
|
result = await smartSearch(args.query, {
|
|
1247
|
-
limit: args?.limit
|
|
1303
|
+
limit: validateLimit(args?.limit, 5, 100),
|
|
1248
1304
|
synthesize: args?.synthesize !== false
|
|
1249
1305
|
});
|
|
1250
1306
|
break;
|
|
@@ -1252,8 +1308,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1252
1308
|
// Email tools
|
|
1253
1309
|
case "mail_search":
|
|
1254
1310
|
result = await mailSearch(args.query, {
|
|
1255
|
-
limit: args?.limit
|
|
1256
|
-
daysBack: args?.days_back
|
|
1311
|
+
limit: validateLimit(args?.limit, 30),
|
|
1312
|
+
daysBack: validateDaysBack(args?.days_back),
|
|
1257
1313
|
sender: args?.sender || null,
|
|
1258
1314
|
recipient: args?.recipient || null,
|
|
1259
1315
|
hasAttachment: args?.has_attachment ?? null,
|
|
@@ -1266,7 +1322,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1266
1322
|
break;
|
|
1267
1323
|
|
|
1268
1324
|
case "mail_recent":
|
|
1269
|
-
result = await mailRecent(
|
|
1325
|
+
result = await mailRecent(
|
|
1326
|
+
validateLimit(args?.limit, 30),
|
|
1327
|
+
validateDaysBack(args?.days_back) || 7,
|
|
1328
|
+
args?.unread_only || false,
|
|
1329
|
+
args?.include_junk || false
|
|
1330
|
+
);
|
|
1270
1331
|
break;
|
|
1271
1332
|
|
|
1272
1333
|
case "mail_date":
|
|
@@ -1280,8 +1341,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1280
1341
|
// Messages tools
|
|
1281
1342
|
case "messages_search":
|
|
1282
1343
|
result = await messagesSearch(args.query, {
|
|
1283
|
-
limit: args?.limit
|
|
1284
|
-
daysBack: args?.days_back
|
|
1344
|
+
limit: validateLimit(args?.limit, 30),
|
|
1345
|
+
daysBack: validateDaysBack(args?.days_back),
|
|
1285
1346
|
contact: args?.contact || null,
|
|
1286
1347
|
groupChatOnly: args?.group_chat_only || false,
|
|
1287
1348
|
groupChatName: args?.group_chat_name || null,
|
|
@@ -1291,19 +1352,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1291
1352
|
break;
|
|
1292
1353
|
|
|
1293
1354
|
case "messages_recent":
|
|
1294
|
-
result = await messagesRecent(
|
|
1355
|
+
result = await messagesRecent(
|
|
1356
|
+
validateLimit(args?.limit, 30),
|
|
1357
|
+
validateDaysBack(args?.days_back) || 1
|
|
1358
|
+
);
|
|
1295
1359
|
break;
|
|
1296
1360
|
|
|
1297
1361
|
case "messages_conversation":
|
|
1298
|
-
result = await messagesConversation(args.contact, args?.limit
|
|
1362
|
+
result = await messagesConversation(args.contact, validateLimit(args?.limit, 50));
|
|
1299
1363
|
break;
|
|
1300
1364
|
|
|
1301
1365
|
// Calendar tools
|
|
1302
1366
|
case "calendar_search":
|
|
1303
1367
|
result = await calendarSearch(args.query, {
|
|
1304
|
-
limit: args?.limit
|
|
1305
|
-
daysBack: args?.days_back
|
|
1306
|
-
daysAhead: args?.days_ahead
|
|
1368
|
+
limit: validateLimit(args?.limit, 30),
|
|
1369
|
+
daysBack: validateDaysBack(args?.days_back),
|
|
1370
|
+
daysAhead: validateDaysBack(args?.days_ahead),
|
|
1307
1371
|
calendarName: args?.calendar_name || null,
|
|
1308
1372
|
allDayOnly: args?.all_day_only || false,
|
|
1309
1373
|
sortBy: args?.sort_by || "relevance"
|
|
@@ -1326,32 +1390,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1326
1390
|
|
|
1327
1391
|
// Mail tools
|
|
1328
1392
|
case "mail_senders":
|
|
1329
|
-
|
|
1393
|
+
{
|
|
1394
|
+
const indexing = stillIndexingMessage();
|
|
1395
|
+
if (indexing) {
|
|
1396
|
+
result = indexing;
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
if (!(await isIndexReady("emails"))) {
|
|
1401
|
+
result = indexUnavailableMessage("emails");
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
result = formatSendersResults(await getFrequentSenders(
|
|
1405
|
+
validateLimit(args?.limit, 30),
|
|
1406
|
+
validateDaysBack(args?.days_back),
|
|
1407
|
+
args?.include_junk || false
|
|
1408
|
+
));
|
|
1330
1409
|
break;
|
|
1331
1410
|
|
|
1332
1411
|
case "rebuild_index":
|
|
1333
1412
|
// Check if indexing is already in progress in this session
|
|
1334
1413
|
if (indexingInProgress) {
|
|
1335
|
-
result = "
|
|
1414
|
+
result = "Indexing is already in progress. Please wait for it to complete before starting a rebuild.";
|
|
1336
1415
|
break;
|
|
1337
1416
|
}
|
|
1338
1417
|
|
|
1339
1418
|
// Acquire lock to prevent parallel rebuilds across multiple MCP instances
|
|
1340
1419
|
if (!acquireLock()) {
|
|
1341
|
-
result = "
|
|
1420
|
+
result = "Indexing is already in progress in a different session. Please wait for it to complete before starting a rebuild.";
|
|
1342
1421
|
break;
|
|
1343
1422
|
}
|
|
1344
1423
|
|
|
1345
1424
|
// Start rebuild in background and return immediately
|
|
1346
1425
|
indexingInProgress = true;
|
|
1426
|
+
sessionIndexComplete = false;
|
|
1347
1427
|
const rebuildSources = args?.sources || ["emails", "messages", "calendar"];
|
|
1348
1428
|
|
|
1349
1429
|
// Fire and forget - don't await
|
|
1350
1430
|
rebuildIndex(rebuildSources).then((rebuildResult) => {
|
|
1351
|
-
|
|
1352
|
-
isFirstEverRun = false;
|
|
1353
|
-
indexingInProgress = false;
|
|
1354
|
-
releaseLock();
|
|
1431
|
+
applyCycleEnd(true);
|
|
1355
1432
|
console.error("Index rebuild completed:", JSON.stringify({
|
|
1356
1433
|
cleared: rebuildResult.cleared,
|
|
1357
1434
|
indexed: Object.fromEntries(
|
|
@@ -1361,8 +1438,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1361
1438
|
}));
|
|
1362
1439
|
}).catch(e => {
|
|
1363
1440
|
console.error("Index rebuild error:", e.message);
|
|
1364
|
-
|
|
1365
|
-
releaseLock();
|
|
1441
|
+
applyCycleEnd(false);
|
|
1366
1442
|
});
|
|
1367
1443
|
|
|
1368
1444
|
result = `🔄 Index rebuild started for: ${rebuildSources.join(", ")}.\n\nThis runs in the background and may take several minutes for large mailboxes. You can continue using other tools - searches will use the new index once complete.`;
|
|
@@ -1381,34 +1457,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1381
1457
|
|
|
1382
1458
|
// Messages tools
|
|
1383
1459
|
case "messages_contacts":
|
|
1384
|
-
result = formatMessageContactsResults(getMessageContacts(args?.limit
|
|
1460
|
+
result = formatMessageContactsResults(getMessageContacts(validateLimit(args?.limit, 50, 500)));
|
|
1385
1461
|
break;
|
|
1386
1462
|
|
|
1387
1463
|
// Calendar tools
|
|
1388
1464
|
case "calendar_upcoming":
|
|
1389
|
-
result = formatUpcomingEventsResults(getUpcomingEvents(args?.limit
|
|
1465
|
+
result = formatUpcomingEventsResults(getUpcomingEvents(validateLimit(args?.limit, 30, 100)));
|
|
1390
1466
|
break;
|
|
1391
1467
|
|
|
1392
1468
|
// ============ NEW TOOLS - PHASE 2 ============
|
|
1393
1469
|
|
|
1394
1470
|
case "calendar_week":
|
|
1395
|
-
result = formatWeekEventsResults(getWeekEvents(args?.week_offset
|
|
1471
|
+
result = formatWeekEventsResults(getWeekEvents(validateWeekOffset(args?.week_offset)));
|
|
1396
1472
|
break;
|
|
1397
1473
|
|
|
1398
1474
|
// ============ NEW TOOLS - PHASE 3 ============
|
|
1399
1475
|
|
|
1400
1476
|
case "mail_thread":
|
|
1401
|
-
|
|
1477
|
+
{
|
|
1478
|
+
const indexing = stillIndexingMessage();
|
|
1479
|
+
if (indexing) {
|
|
1480
|
+
result = indexing;
|
|
1481
|
+
break;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (!(await isIndexReady("emails"))) {
|
|
1485
|
+
result = indexUnavailableMessage("emails");
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
result = formatEmailThreadResults(await getEmailThread(args.file_path, validateLimit(args?.limit, 30)));
|
|
1402
1489
|
break;
|
|
1403
1490
|
|
|
1404
1491
|
case "calendar_recurring":
|
|
1405
|
-
result = formatRecurringEventsResults(getRecurringEvents(args?.limit
|
|
1492
|
+
result = formatRecurringEventsResults(getRecurringEvents(validateLimit(args?.limit, 30, 100)));
|
|
1406
1493
|
break;
|
|
1407
1494
|
|
|
1408
1495
|
// ============ CONTACTS TOOLS ============
|
|
1409
1496
|
|
|
1410
1497
|
case "contacts_search":
|
|
1411
|
-
result = formatContactsSearchResults(searchContacts(args.query, args?.limit
|
|
1498
|
+
result = formatContactsSearchResults(searchContacts(args.query, validateLimit(args?.limit, 30)));
|
|
1412
1499
|
break;
|
|
1413
1500
|
|
|
1414
1501
|
case "contacts_lookup":
|
|
@@ -1416,7 +1503,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1416
1503
|
break;
|
|
1417
1504
|
|
|
1418
1505
|
case "person_search":
|
|
1419
|
-
result = await personSearch(args.name, args?.limit
|
|
1506
|
+
result = await personSearch(args.name, validateLimit(args?.limit, 10));
|
|
1420
1507
|
break;
|
|
1421
1508
|
|
|
1422
1509
|
default:
|
|
@@ -1436,7 +1523,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1436
1523
|
async function main() {
|
|
1437
1524
|
const transport = new StdioServerTransport();
|
|
1438
1525
|
await server.connect(transport);
|
|
1439
|
-
console.error(
|
|
1526
|
+
console.error(`Apple Tools MCP server running (v${PACKAGE_VERSION})`);
|
|
1440
1527
|
// Background indexing runs automatically on startup and every INDEX_INTERVAL
|
|
1441
1528
|
}
|
|
1442
1529
|
|
package/indexer.js
CHANGED
|
@@ -9,9 +9,13 @@ import {
|
|
|
9
9
|
validateLimit,
|
|
10
10
|
validateLanceDBId,
|
|
11
11
|
escapeSQL,
|
|
12
|
-
stripHtmlTags
|
|
12
|
+
stripHtmlTags,
|
|
13
|
+
toUnixMillis,
|
|
14
|
+
unfoldRfc822Headers,
|
|
15
|
+
stripSubjectPrefixes
|
|
13
16
|
} from "./lib/validators.js";
|
|
14
17
|
import { safeSqlite3Json, safeOsascript, safeFind } from "./lib/shell.js";
|
|
18
|
+
import { indexUnavailableMessage } from "./lib/indexGate.js";
|
|
15
19
|
|
|
16
20
|
// Re-export contact functions for use by other modules
|
|
17
21
|
export {
|
|
@@ -252,14 +256,17 @@ function parseEmlx(filePath) {
|
|
|
252
256
|
content = lines.slice(1).join("\n");
|
|
253
257
|
}
|
|
254
258
|
|
|
259
|
+
// Unfold RFC 822 wrapped headers so long From/Subject/To values are complete
|
|
260
|
+
const unfolded = unfoldRfc822Headers(content);
|
|
261
|
+
|
|
255
262
|
// Extract headers
|
|
256
|
-
const fromMatch =
|
|
257
|
-
const subjectMatch =
|
|
258
|
-
const dateMatch =
|
|
259
|
-
const toMatch =
|
|
260
|
-
const ccMatch =
|
|
261
|
-
const messageIdMatch =
|
|
262
|
-
const flaggedMatch =
|
|
263
|
+
const fromMatch = unfolded.match(/^From:\s*(.+)$/m);
|
|
264
|
+
const subjectMatch = unfolded.match(/^Subject:\s*(.+)$/m);
|
|
265
|
+
const dateMatch = unfolded.match(/^Date:\s*(.+)$/m);
|
|
266
|
+
const toMatch = unfolded.match(/^To:\s*(.+)$/m);
|
|
267
|
+
const ccMatch = unfolded.match(/^Cc:\s*(.+)$/m);
|
|
268
|
+
const messageIdMatch = unfolded.match(/^Message-ID:\s*(.+)$/im);
|
|
269
|
+
const flaggedMatch = unfolded.match(/^X-Flagged:\s*(.+)$/im) || unfolded.match(/flags.*flagged/i);
|
|
263
270
|
|
|
264
271
|
// Check for attachments
|
|
265
272
|
const hasAttachment = /Content-Disposition:\s*attachment/i.test(content) ||
|
|
@@ -335,13 +342,10 @@ function parseEmlx(filePath) {
|
|
|
335
342
|
// Full scan - used for first run and rebuild_index
|
|
336
343
|
// Includes both .emlx and .partial.emlx files (partial = not fully downloaded via IMAP)
|
|
337
344
|
async function findAllEmlxFiles() {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
console.error("Error finding emlx files:", e.message);
|
|
343
|
-
return [];
|
|
344
|
-
}
|
|
345
|
+
// "*.emlx" also matches "*.partial.emlx"
|
|
346
|
+
// Let permission/IO errors propagate so indexEmails does not persist a
|
|
347
|
+
// timestamp for an empty scan and skip real mail forever.
|
|
348
|
+
return safeFind(MAIL_DIR, { name: "*.emlx", type: "f" });
|
|
345
349
|
}
|
|
346
350
|
|
|
347
351
|
// Fast incremental scan - uses find with -mtime filter (more reliable than mdfind/Spotlight)
|
|
@@ -579,6 +583,9 @@ function getMessages(sinceTimestamp = null) {
|
|
|
579
583
|
ORDER BY m.date DESC
|
|
580
584
|
`;
|
|
581
585
|
const results = safeSqlite3Json(MESSAGES_DB, query, { timeout: 60000 });
|
|
586
|
+
if (!Array.isArray(results)) {
|
|
587
|
+
throw new Error("Unexpected sqlite3 result for messages");
|
|
588
|
+
}
|
|
582
589
|
|
|
583
590
|
// Post-process: extract text from attributedBody where text is NULL
|
|
584
591
|
let extractedCount = 0;
|
|
@@ -607,7 +614,7 @@ function getMessages(sinceTimestamp = null) {
|
|
|
607
614
|
return results.filter(msg => msg.text && msg.text.trim() !== '');
|
|
608
615
|
} catch (e) {
|
|
609
616
|
console.error("Error reading messages:", e.message);
|
|
610
|
-
|
|
617
|
+
throw e;
|
|
611
618
|
}
|
|
612
619
|
}
|
|
613
620
|
|
|
@@ -645,19 +652,24 @@ function getParticipantStatus(status) {
|
|
|
645
652
|
}
|
|
646
653
|
|
|
647
654
|
function getCalendarEvents() {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
655
|
+
// NOTE: We index ALL calendar events, not filtered by date
|
|
656
|
+
// Calendar events don't have a "file modification time" like emails do,
|
|
657
|
+
// so we can't use the mdfind + DAYS_BACK approach.
|
|
658
|
+
// Calendar indexing is always comprehensive - filtering by date would lose historical context.
|
|
659
|
+
// Must throw on source-read failure: a silent [] would look like "every event
|
|
660
|
+
// was deleted" and the stale-entry pass would wipe the calendar index.
|
|
661
|
+
if (!fs.existsSync(CALENDAR_DB)) {
|
|
662
|
+
throw new Error("Calendar database not found");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const now = Date.now();
|
|
666
|
+
const pastDate = unixMsToMacAbsolute(now - 10 * 365 * 24 * 60 * 60 * 1000); // 10 years back
|
|
667
|
+
const futureDate = unixMsToMacAbsolute(now + 10 * 365 * 24 * 60 * 60 * 1000); // 10 years ahead
|
|
668
|
+
|
|
669
|
+
// Query OccurrenceCache for recurring events and their calculated occurrences
|
|
670
|
+
// This includes both recurring and non-recurring events
|
|
671
|
+
// GROUP BY to avoid duplicate entries for recurring events
|
|
672
|
+
const query = `
|
|
661
673
|
SELECT
|
|
662
674
|
ci.ROWID as id,
|
|
663
675
|
ci.summary,
|
|
@@ -679,10 +691,7 @@ function getCalendarEvents() {
|
|
|
679
691
|
ORDER BY MIN(oc.day) ASC
|
|
680
692
|
`;
|
|
681
693
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
// Get attendees for events that have them (separate query for efficiency)
|
|
685
|
-
const attendeesQuery = `
|
|
694
|
+
const attendeesQuery = `
|
|
686
695
|
SELECT
|
|
687
696
|
p.owner_id,
|
|
688
697
|
COALESCE(i.display_name, p.email, 'Unknown') as name,
|
|
@@ -692,11 +701,13 @@ function getCalendarEvents() {
|
|
|
692
701
|
WHERE p.entity_type = 0
|
|
693
702
|
`;
|
|
694
703
|
|
|
704
|
+
return withCalendarCopy((dbPath) => {
|
|
705
|
+
const rows = safeSqlite3Json(dbPath, query, { timeout: 30000 });
|
|
706
|
+
|
|
695
707
|
let attendeesMap = new Map();
|
|
696
708
|
try {
|
|
697
|
-
const attendeesRows = safeSqlite3Json(
|
|
709
|
+
const attendeesRows = safeSqlite3Json(dbPath, attendeesQuery, { timeout: 10000 });
|
|
698
710
|
|
|
699
|
-
// Group attendees by owner_id (event id)
|
|
700
711
|
for (const att of attendeesRows) {
|
|
701
712
|
if (!attendeesMap.has(att.owner_id)) {
|
|
702
713
|
attendeesMap.set(att.owner_id, []);
|
|
@@ -734,10 +745,7 @@ function getCalendarEvents() {
|
|
|
734
745
|
|
|
735
746
|
console.error(`Calendar: Retrieved ${events.length} events via SQLite (~${Math.round((Date.now() - now))}ms)`);
|
|
736
747
|
return events;
|
|
737
|
-
}
|
|
738
|
-
console.error("Error reading calendar:", e.message);
|
|
739
|
-
return [];
|
|
740
|
-
}
|
|
748
|
+
});
|
|
741
749
|
}
|
|
742
750
|
|
|
743
751
|
// ============ DATABASE FUNCTIONS ============
|
|
@@ -927,7 +935,13 @@ export async function indexEmails(progressCallback = null, forceFullScan = false
|
|
|
927
935
|
// Use fast incremental scan if we have a previous timestamp
|
|
928
936
|
const startTime = Date.now();
|
|
929
937
|
console.error(`Calling findNewEmlxFiles with timestamp: ${lastEmailIndexTime ? new Date(lastEmailIndexTime).toISOString() : 'null (full scan)'}`);
|
|
930
|
-
|
|
938
|
+
let newFiles;
|
|
939
|
+
try {
|
|
940
|
+
newFiles = await findNewEmlxFiles(lastEmailIndexTime);
|
|
941
|
+
} catch (e) {
|
|
942
|
+
console.error(`Email source read failed; skipping this cycle so the lookback timestamp is not advanced: ${e.message}`);
|
|
943
|
+
return { indexed: 0, added: 0, error: e.message };
|
|
944
|
+
}
|
|
931
945
|
console.error(`Found ${newFiles.length} new/modified email files (${Date.now() - startTime}ms)`);
|
|
932
946
|
|
|
933
947
|
const indexedPaths = await getIndexedIdsWithRetry("emails", "filePath");
|
|
@@ -1041,6 +1055,13 @@ export async function indexEmails(progressCallback = null, forceFullScan = false
|
|
|
1041
1055
|
if (uniqueRecords.length > 0) {
|
|
1042
1056
|
if (!tables.emails) {
|
|
1043
1057
|
tables.emails = await db.createTable("emails", uniqueRecords, { mode: "overwrite" });
|
|
1058
|
+
// Track messageIds from the first batch so later batches can
|
|
1059
|
+
// dedupe IMAP copies (INBOX vs Junk) of the same Message-ID.
|
|
1060
|
+
for (const record of uniqueRecords) {
|
|
1061
|
+
if (record.messageId) {
|
|
1062
|
+
indexedMessageIds.add(record.messageId);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1044
1065
|
} else {
|
|
1045
1066
|
// Double-check: verify these IDs truly aren't in the index
|
|
1046
1067
|
const currentIndexed = await getIndexedIdsWithRetry("emails", "filePath");
|
|
@@ -1130,7 +1151,13 @@ export async function indexMessages(forceFullScan = false) {
|
|
|
1130
1151
|
const indexStartTime = Date.now();
|
|
1131
1152
|
|
|
1132
1153
|
// Use incremental scan if we have a previous timestamp
|
|
1133
|
-
|
|
1154
|
+
let messages;
|
|
1155
|
+
try {
|
|
1156
|
+
messages = getMessages(lastMessageIndexTime);
|
|
1157
|
+
} catch (e) {
|
|
1158
|
+
console.error(`Messages source read failed; skipping this cycle so the lookback timestamp is not advanced: ${e.message}`);
|
|
1159
|
+
return { indexed: 0, added: 0, error: e.message };
|
|
1160
|
+
}
|
|
1134
1161
|
console.error(`Found ${messages.length} messages${lastMessageIndexTime ? ' (incremental)' : ' (full scan)'}`);
|
|
1135
1162
|
|
|
1136
1163
|
const indexed = await getIndexedIdsWithRetry("messages", "id");
|
|
@@ -1186,7 +1213,7 @@ export async function indexMessages(forceFullScan = false) {
|
|
|
1186
1213
|
return {
|
|
1187
1214
|
id: String(msg.id),
|
|
1188
1215
|
date: msg.date,
|
|
1189
|
-
dateTimestamp: msg.dateTimestamp
|
|
1216
|
+
dateTimestamp: toUnixMillis(msg.dateTimestamp),
|
|
1190
1217
|
sender: msg.sender,
|
|
1191
1218
|
text: msg.text?.substring(0, 500) || "",
|
|
1192
1219
|
chatId: String(msg.chatId || ""),
|
|
@@ -1270,7 +1297,15 @@ export async function indexMessages(forceFullScan = false) {
|
|
|
1270
1297
|
export async function indexCalendar() {
|
|
1271
1298
|
await initDB();
|
|
1272
1299
|
|
|
1273
|
-
|
|
1300
|
+
let events;
|
|
1301
|
+
try {
|
|
1302
|
+
events = getCalendarEvents();
|
|
1303
|
+
} catch (e) {
|
|
1304
|
+
// Do not treat a source-read failure as "zero events" — that would mark
|
|
1305
|
+
// every indexed row stale and delete the calendar index.
|
|
1306
|
+
console.error(`Calendar source read failed; skipping index update to avoid data loss: ${e.message}`);
|
|
1307
|
+
return { indexed: 0, added: 0, removed: 0, error: e.message };
|
|
1308
|
+
}
|
|
1274
1309
|
console.error(`Found ${events.length} calendar events`);
|
|
1275
1310
|
|
|
1276
1311
|
// Get already indexed event IDs for incremental indexing
|
|
@@ -1493,14 +1528,14 @@ export async function getRecentMessages(limit = 10, daysBack = 1) {
|
|
|
1493
1528
|
if (!tables.messages) return { messages: [], hasMore: false };
|
|
1494
1529
|
|
|
1495
1530
|
try {
|
|
1496
|
-
const cutoff = Date.now()
|
|
1531
|
+
const cutoff = Date.now() - (daysBack * 24 * 60 * 60 * 1000);
|
|
1497
1532
|
const results = await tables.messages.query()
|
|
1498
1533
|
.select(["id", "date", "dateTimestamp", "sender", "text", "chatId", "isGroupChat"])
|
|
1499
1534
|
.toArray();
|
|
1500
1535
|
|
|
1501
1536
|
const filtered = results
|
|
1502
|
-
.filter(r => r.dateTimestamp >= cutoff)
|
|
1503
|
-
.sort((a, b) => b.dateTimestamp - a.dateTimestamp);
|
|
1537
|
+
.filter(r => toUnixMillis(r.dateTimestamp) >= cutoff)
|
|
1538
|
+
.sort((a, b) => toUnixMillis(b.dateTimestamp) - toUnixMillis(a.dateTimestamp));
|
|
1504
1539
|
|
|
1505
1540
|
const hasMore = filtered.length > limit;
|
|
1506
1541
|
const messages = filtered.slice(0, limit);
|
|
@@ -1530,7 +1565,7 @@ export async function getConversation(contact, limit = 50) {
|
|
|
1530
1565
|
|
|
1531
1566
|
// Sort chronologically (oldest first for conversation view)
|
|
1532
1567
|
return filtered
|
|
1533
|
-
.sort((a, b) => a.dateTimestamp - b.dateTimestamp)
|
|
1568
|
+
.sort((a, b) => toUnixMillis(a.dateTimestamp) - toUnixMillis(b.dateTimestamp))
|
|
1534
1569
|
.slice(-limit); // Take last N messages
|
|
1535
1570
|
} catch (e) {
|
|
1536
1571
|
console.error("Error getting conversation:", e.message);
|
|
@@ -1648,7 +1683,7 @@ export function getMessageContacts(limit = 50) {
|
|
|
1648
1683
|
return safeSqlite3Json(MESSAGES_DB, query, { timeout: 30000 });
|
|
1649
1684
|
} catch (e) {
|
|
1650
1685
|
console.error("Error getting message contacts:", e.message);
|
|
1651
|
-
return
|
|
1686
|
+
return { error: e.message };
|
|
1652
1687
|
}
|
|
1653
1688
|
}
|
|
1654
1689
|
|
|
@@ -1678,13 +1713,15 @@ export function getUpcomingEvents(limit = 10) {
|
|
|
1678
1713
|
ORDER BY sort_day ASC
|
|
1679
1714
|
LIMIT ${fetchLimit}
|
|
1680
1715
|
`;
|
|
1681
|
-
const events =
|
|
1716
|
+
const events = withCalendarCopy((dbPath) => {
|
|
1717
|
+
return safeSqlite3Json(dbPath, query, { timeout: 10000 });
|
|
1718
|
+
});
|
|
1682
1719
|
const hasMore = events.length > safeLimit;
|
|
1683
1720
|
const limitedEvents = events.slice(0, safeLimit);
|
|
1684
1721
|
return { events: limitedEvents, showing: limitedEvents.length, hasMore };
|
|
1685
1722
|
} catch (e) {
|
|
1686
1723
|
console.error("Error getting upcoming events:", e.message);
|
|
1687
|
-
return { events: [], showing: 0, hasMore: false };
|
|
1724
|
+
return { events: [], showing: 0, hasMore: false, error: e.message };
|
|
1688
1725
|
}
|
|
1689
1726
|
}
|
|
1690
1727
|
|
|
@@ -1876,7 +1913,9 @@ export function getWeekEvents(weekOffset = 0) {
|
|
|
1876
1913
|
AND ci.summary IS NOT NULL AND ci.summary <> ''
|
|
1877
1914
|
ORDER BY oc.day ASC
|
|
1878
1915
|
`;
|
|
1879
|
-
const events =
|
|
1916
|
+
const events = withCalendarCopy((dbPath) => {
|
|
1917
|
+
return safeSqlite3Json(dbPath, query, { timeout: 15000 });
|
|
1918
|
+
});
|
|
1880
1919
|
|
|
1881
1920
|
const weekStart = monday.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
|
|
1882
1921
|
const weekEnd = sunday.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
|
|
@@ -1898,7 +1937,7 @@ export function getWeekEvents(weekOffset = 0) {
|
|
|
1898
1937
|
// Uses subject-based matching since Message-ID isn't indexed
|
|
1899
1938
|
export async function getEmailThread(filePath, limit = 20) {
|
|
1900
1939
|
await initDB();
|
|
1901
|
-
if (!tables.emails) return { error: "
|
|
1940
|
+
if (!tables.emails) return { error: indexUnavailableMessage("emails"), emails: [] };
|
|
1902
1941
|
|
|
1903
1942
|
try {
|
|
1904
1943
|
// Validate file path to prevent path traversal attacks
|
|
@@ -1915,15 +1954,13 @@ export async function getEmailThread(filePath, limit = 20) {
|
|
|
1915
1954
|
}
|
|
1916
1955
|
|
|
1917
1956
|
// Read the email to get subject
|
|
1918
|
-
const content = fs.readFileSync(validatedPath, "utf-8");
|
|
1957
|
+
const content = unfoldRfc822Headers(fs.readFileSync(validatedPath, "utf-8"));
|
|
1919
1958
|
const subjectMatch = content.match(/^Subject:\s*(.+)$/m);
|
|
1920
1959
|
if (!subjectMatch) {
|
|
1921
1960
|
return { error: "Could not extract subject from email", emails: [] };
|
|
1922
1961
|
}
|
|
1923
1962
|
|
|
1924
|
-
|
|
1925
|
-
let subject = subjectMatch[1].trim();
|
|
1926
|
-
const baseSubject = subject.replace(/^(Re|Fwd|Fw):\s*/gi, "").trim();
|
|
1963
|
+
const baseSubject = stripSubjectPrefixes(subjectMatch[1]);
|
|
1927
1964
|
|
|
1928
1965
|
if (baseSubject.length < 5) {
|
|
1929
1966
|
return { error: "Subject too short to find thread", emails: [] };
|
|
@@ -1937,7 +1974,7 @@ export async function getEmailThread(filePath, limit = 20) {
|
|
|
1937
1974
|
// Filter to emails with matching base subject
|
|
1938
1975
|
const threadEmails = allEmails
|
|
1939
1976
|
.filter(e => {
|
|
1940
|
-
const eBaseSubject = (e.subject || "")
|
|
1977
|
+
const eBaseSubject = stripSubjectPrefixes(e.subject || "");
|
|
1941
1978
|
return eBaseSubject.toLowerCase() === baseSubject.toLowerCase();
|
|
1942
1979
|
})
|
|
1943
1980
|
.sort((a, b) => a.dateTimestamp - b.dateTimestamp)
|
|
@@ -1981,12 +2018,14 @@ export function getRecurringEvents(limit = 20) {
|
|
|
1981
2018
|
ORDER BY occurrenceCount DESC, MIN(oc.day) ASC
|
|
1982
2019
|
LIMIT ${fetchLimit}
|
|
1983
2020
|
`;
|
|
1984
|
-
const events =
|
|
2021
|
+
const events = withCalendarCopy((dbPath) => {
|
|
2022
|
+
return safeSqlite3Json(dbPath, query, { timeout: 30000 });
|
|
2023
|
+
});
|
|
1985
2024
|
const hasMore = events.length > safeLimit;
|
|
1986
2025
|
const limitedEvents = events.slice(0, safeLimit);
|
|
1987
2026
|
return { events: limitedEvents, showing: limitedEvents.length, hasMore };
|
|
1988
2027
|
} catch (e) {
|
|
1989
2028
|
console.error("Error getting recurring events:", e.message);
|
|
1990
|
-
return { events: [], showing: 0, hasMore: false };
|
|
2029
|
+
return { events: [], showing: 0, hasMore: false, error: e.message };
|
|
1991
2030
|
}
|
|
1992
2031
|
}
|
package/lib/indexGate.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index-session search gating.
|
|
3
|
+
*
|
|
4
|
+
* sessionIndexComplete means "this process finished (or abandoned) its own
|
|
5
|
+
* index cycle." A second MCP instance that lost the indexer lock never runs
|
|
6
|
+
* a cycle, so that flag stays false. Lost-lock must not be treated as
|
|
7
|
+
* "still indexing" — callers still check isIndexReady() for a missing index.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether index-backed tools should return the still-indexing message.
|
|
12
|
+
*
|
|
13
|
+
* @param {boolean} sessionIndexComplete
|
|
14
|
+
* @param {boolean} ownsIndexLock true if this process won/holds the indexer lock
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
export function isSearchBlockedByIndexing(sessionIndexComplete, ownsIndexLock) {
|
|
18
|
+
return Boolean(ownsIndexLock) && !sessionIndexComplete;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* In-memory flags after an index or rebuild cycle ends.
|
|
23
|
+
* Searches must be unblocked on both success and failure.
|
|
24
|
+
*
|
|
25
|
+
* @param {boolean} success
|
|
26
|
+
* @returns {{ indexingInProgress: false, sessionIndexComplete: true, ownsIndexLock: false, isFirstEverRun?: false }}
|
|
27
|
+
*/
|
|
28
|
+
export function cycleEndFlags(success) {
|
|
29
|
+
const flags = {
|
|
30
|
+
indexingInProgress: false,
|
|
31
|
+
sessionIndexComplete: true,
|
|
32
|
+
ownsIndexLock: false
|
|
33
|
+
};
|
|
34
|
+
if (success) {
|
|
35
|
+
flags.isFirstEverRun = false;
|
|
36
|
+
}
|
|
37
|
+
return flags;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* User-facing message when a source table is missing.
|
|
42
|
+
* Distinct from still-indexing: retry because the index is not there yet,
|
|
43
|
+
* not because this process is mid-cycle.
|
|
44
|
+
*
|
|
45
|
+
* @param {"emails"|"messages"|"calendar"|undefined} type
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function indexUnavailableMessage(type) {
|
|
49
|
+
if (type === "messages") {
|
|
50
|
+
return "Messages index not available. Please try again shortly.";
|
|
51
|
+
}
|
|
52
|
+
if (type === "calendar") {
|
|
53
|
+
return "Calendar index not available. Please try again shortly.";
|
|
54
|
+
}
|
|
55
|
+
if (type === "emails") {
|
|
56
|
+
return "Email index not available. Please try again shortly.";
|
|
57
|
+
}
|
|
58
|
+
return "Index not available. Please try again shortly.";
|
|
59
|
+
}
|
package/lib/shell.js
CHANGED
|
@@ -93,8 +93,7 @@ export function safeSqlite3Json(dbPath, query, options = {}) {
|
|
|
93
93
|
try {
|
|
94
94
|
return JSON.parse(output);
|
|
95
95
|
} catch (e) {
|
|
96
|
-
|
|
97
|
-
return [];
|
|
96
|
+
throw new Error(`Failed to parse sqlite3 JSON output: ${e.message}`);
|
|
98
97
|
}
|
|
99
98
|
}
|
|
100
99
|
|
|
@@ -267,12 +266,20 @@ export function safeFind(searchPath, options = {}) {
|
|
|
267
266
|
throw result.error;
|
|
268
267
|
}
|
|
269
268
|
|
|
270
|
-
|
|
271
|
-
// We still want to return whatever paths it found
|
|
272
|
-
return result.stdout
|
|
269
|
+
const paths = (result.stdout || '')
|
|
273
270
|
.split('\n')
|
|
274
271
|
.map(line => line.trim())
|
|
275
272
|
.filter(line => line.length > 0);
|
|
273
|
+
|
|
274
|
+
// Partial permission errors can still yield some paths; keep those.
|
|
275
|
+
// A complete failure (no paths + non-zero status) must not be treated as
|
|
276
|
+
// "zero files" — callers persist index timestamps on empty scans.
|
|
277
|
+
if (result.status !== 0 && paths.length === 0) {
|
|
278
|
+
const errorMsg = (result.stderr || '').trim() || `find exited with code ${result.status}`;
|
|
279
|
+
throw new Error(errorMsg);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return paths;
|
|
276
283
|
}
|
|
277
284
|
|
|
278
285
|
// ============ GENERIC SAFE SPAWN ============
|
package/lib/validators.js
CHANGED
|
@@ -118,10 +118,14 @@ export function escapeAppleScript(str) {
|
|
|
118
118
|
if (!str || typeof str !== 'string') {
|
|
119
119
|
return '';
|
|
120
120
|
}
|
|
121
|
-
// Escape backslashes first, then
|
|
121
|
+
// Escape backslashes first, then quotes, then line breaks.
|
|
122
|
+
// Unescaped CR/LF inside an AppleScript double-quoted string can close the
|
|
123
|
+
// string and inject additional statements.
|
|
122
124
|
return str
|
|
123
125
|
.replace(/\\/g, '\\\\')
|
|
124
|
-
.replace(/"/g, '\\"')
|
|
126
|
+
.replace(/"/g, '\\"')
|
|
127
|
+
.replace(/\r/g, '\\r')
|
|
128
|
+
.replace(/\n/g, '\\n');
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
/**
|
|
@@ -407,6 +411,64 @@ export function stripHtmlTags(html, maxLength = 100000) {
|
|
|
407
411
|
*/
|
|
408
412
|
const MAC_ABSOLUTE_EPOCH = 978307200;
|
|
409
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Normalize a Unix timestamp to milliseconds.
|
|
416
|
+
* Messages chat.db values are stored as seconds; email/calendar timestamps
|
|
417
|
+
* are milliseconds. Treating seconds as ms drops every date-filtered message.
|
|
418
|
+
*
|
|
419
|
+
* Threshold 1e11 ms is 1973-03-03 — well before any real mail/message date,
|
|
420
|
+
* and far above Unix seconds for year 2100 (~4e9).
|
|
421
|
+
*
|
|
422
|
+
* @param {number|string|null|undefined} ts
|
|
423
|
+
* @returns {number} Timestamp in milliseconds, or 0 if invalid
|
|
424
|
+
*/
|
|
425
|
+
export function toUnixMillis(ts) {
|
|
426
|
+
if (ts === null || ts === undefined || ts === '') {
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
const n = Number(ts);
|
|
430
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
431
|
+
return 0;
|
|
432
|
+
}
|
|
433
|
+
return n < 1e11 ? n * 1000 : n;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Unfold RFC 822 wrapped header lines (CRLF + WSP continuation) so
|
|
438
|
+
* From/Subject/To matchers see the full header value.
|
|
439
|
+
*
|
|
440
|
+
* @param {string} content - Raw email content (headers + body)
|
|
441
|
+
* @returns {string} Content with folded headers joined into single lines
|
|
442
|
+
*/
|
|
443
|
+
export function unfoldRfc822Headers(content) {
|
|
444
|
+
if (!content || typeof content !== 'string') {
|
|
445
|
+
return '';
|
|
446
|
+
}
|
|
447
|
+
const split = content.search(/\r?\n\r?\n/);
|
|
448
|
+
const headers = split >= 0 ? content.slice(0, split) : content;
|
|
449
|
+
const body = split >= 0 ? content.slice(split) : '';
|
|
450
|
+
return headers.replace(/\r?\n[ \t]+/g, ' ') + body;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Strip repeated Re:/Fwd:/Fw: prefixes from an email subject for thread matching.
|
|
455
|
+
*
|
|
456
|
+
* @param {string} subject
|
|
457
|
+
* @returns {string}
|
|
458
|
+
*/
|
|
459
|
+
export function stripSubjectPrefixes(subject) {
|
|
460
|
+
if (!subject || typeof subject !== 'string') {
|
|
461
|
+
return '';
|
|
462
|
+
}
|
|
463
|
+
let s = subject.trim();
|
|
464
|
+
let prev;
|
|
465
|
+
do {
|
|
466
|
+
prev = s;
|
|
467
|
+
s = s.replace(/^(re|fwd|fw)\s*:\s*/i, '').trim();
|
|
468
|
+
} while (s !== prev);
|
|
469
|
+
return s;
|
|
470
|
+
}
|
|
471
|
+
|
|
410
472
|
/**
|
|
411
473
|
* Convert Mac Absolute Time to Unix timestamp (milliseconds)
|
|
412
474
|
* Handles both seconds and nanoseconds formats
|
package/package.json
CHANGED
package/search.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as lancedb from "@lancedb/lancedb";
|
|
2
2
|
import * as chrono from "chrono-node";
|
|
3
3
|
import { safeOsascript } from "./lib/shell.js";
|
|
4
|
-
import { safeMatch, validateSearchQuery } from "./lib/validators.js";
|
|
4
|
+
import { safeMatch, validateSearchQuery, toUnixMillis } from "./lib/validators.js";
|
|
5
5
|
import { embed, INDEX_DIR, getRecentEmails, getEmailsByDateRange, getRecentMessages, getConversation, getEventsOnDate, resolveEmail, resolvePhone, formatContact } from "./indexer.js";
|
|
6
|
+
import { indexUnavailableMessage } from "./lib/indexGate.js";
|
|
6
7
|
|
|
7
8
|
let db = null;
|
|
8
9
|
let tables = {};
|
|
@@ -102,13 +103,15 @@ function resolvePronouns(query) {
|
|
|
102
103
|
return query;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (pronounPattern.test(query) && queryContext.lastPerson) {
|
|
108
|
-
return query.replace(pronounPattern, queryContext.lastPerson);
|
|
106
|
+
if (!queryContext.lastPerson) {
|
|
107
|
+
return query;
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
|
|
110
|
+
// Never use RegExp#test with a /g regex — lastIndex is stateful and can
|
|
111
|
+
// skip the first (or only) pronoun on this or a later call. A fresh
|
|
112
|
+
// regex plus replace() is lastIndex-safe; do not hoist or test() it.
|
|
113
|
+
const pronounRe = new RegExp('\\b(they|them|their|he|him|his|she|her|hers)\\b', 'gi');
|
|
114
|
+
return query.replace(pronounRe, queryContext.lastPerson);
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
// Extract entities (people, dates) from natural language query and convert to filters
|
|
@@ -525,8 +528,10 @@ export function getDateRange(dateStr) {
|
|
|
525
528
|
const start = parseNaturalDate(dateStr);
|
|
526
529
|
if (!start) return null;
|
|
527
530
|
|
|
528
|
-
|
|
529
|
-
|
|
531
|
+
// Local next midnight — not +24h, which is wrong on DST transition days
|
|
532
|
+
const endDate = new Date(start);
|
|
533
|
+
endDate.setDate(endDate.getDate() + 1);
|
|
534
|
+
return { start, end: endDate.getTime() };
|
|
530
535
|
}
|
|
531
536
|
|
|
532
537
|
// Parse various date formats and return timestamp (for filtering results)
|
|
@@ -559,7 +564,7 @@ function filterByDateRange(results, daysBack, daysAhead, dateField = "date") {
|
|
|
559
564
|
if (daysBack === 0 && daysAhead === 0) return results;
|
|
560
565
|
|
|
561
566
|
return results.filter(r => {
|
|
562
|
-
const ts = r.dateTimestamp || parseDate(r[dateField]);
|
|
567
|
+
const ts = toUnixMillis(r.dateTimestamp) || parseDate(r[dateField]);
|
|
563
568
|
if (!ts) return daysBack === 0 && daysAhead === 0;
|
|
564
569
|
return ts >= cutoffPast && ts <= cutoffFuture;
|
|
565
570
|
});
|
|
@@ -568,8 +573,8 @@ function filterByDateRange(results, daysBack, daysAhead, dateField = "date") {
|
|
|
568
573
|
// Sort results by date (newest first)
|
|
569
574
|
function sortByDate(results, descending = true) {
|
|
570
575
|
return results.sort((a, b) => {
|
|
571
|
-
const tsA = a.dateTimestamp || parseDate(a.date) || parseDate(a.start) || 0;
|
|
572
|
-
const tsB = b.dateTimestamp || parseDate(b.date) || parseDate(b.start) || 0;
|
|
576
|
+
const tsA = toUnixMillis(a.dateTimestamp) || parseDate(a.date) || parseDate(a.start) || 0;
|
|
577
|
+
const tsB = toUnixMillis(b.dateTimestamp) || parseDate(b.date) || parseDate(b.start) || 0;
|
|
573
578
|
return descending ? tsB - tsA : tsA - tsB;
|
|
574
579
|
});
|
|
575
580
|
}
|
|
@@ -582,7 +587,7 @@ export async function searchEmails(query, options = {}) {
|
|
|
582
587
|
try {
|
|
583
588
|
validatedQuery = validateSearchQuery(query);
|
|
584
589
|
} catch (e) {
|
|
585
|
-
return { results: [], error: e.message };
|
|
590
|
+
return { success: false, results: [], error: e.message };
|
|
586
591
|
}
|
|
587
592
|
|
|
588
593
|
// Check result cache first
|
|
@@ -622,7 +627,7 @@ export async function searchEmails(query, options = {}) {
|
|
|
622
627
|
if (!tbl) {
|
|
623
628
|
return {
|
|
624
629
|
success: false,
|
|
625
|
-
error: "
|
|
630
|
+
error: indexUnavailableMessage("emails")
|
|
626
631
|
};
|
|
627
632
|
}
|
|
628
633
|
|
|
@@ -714,6 +719,7 @@ export async function searchEmails(query, options = {}) {
|
|
|
714
719
|
to: row.to || "Unknown",
|
|
715
720
|
subject: row.subject || "No subject",
|
|
716
721
|
date: formatLocalDate(row.date) || "Unknown",
|
|
722
|
+
dateTimestamp: toUnixMillis(row.dateTimestamp) || null,
|
|
717
723
|
mailbox: row.mailbox || "Unknown",
|
|
718
724
|
hasAttachment: row.hasAttachment || false,
|
|
719
725
|
isFlagged: row.isFlagged || false,
|
|
@@ -950,7 +956,7 @@ export async function getEmailDateResults(dateStr, includeJunk = false) {
|
|
|
950
956
|
}
|
|
951
957
|
|
|
952
958
|
export function formatEmailResults(searchResult) {
|
|
953
|
-
if (!searchResult.success) return searchResult.error;
|
|
959
|
+
if (!searchResult.success) return searchResult.error || "Email search failed";
|
|
954
960
|
if (searchResult.results.length === 0) return searchResult.message;
|
|
955
961
|
|
|
956
962
|
const results = searchResult.results.map(r => {
|
|
@@ -965,6 +971,9 @@ export function formatEmailResults(searchResult) {
|
|
|
965
971
|
return result + "\n---";
|
|
966
972
|
}).join("\n");
|
|
967
973
|
|
|
974
|
+
if (searchResult.hasMore) {
|
|
975
|
+
return results + `\n\nShowing ${searchResult.showing} results. More matches exist; increase limit to see them.`;
|
|
976
|
+
}
|
|
968
977
|
return results;
|
|
969
978
|
}
|
|
970
979
|
|
|
@@ -976,7 +985,7 @@ export async function searchMessages(query, options = {}) {
|
|
|
976
985
|
try {
|
|
977
986
|
validatedQuery = validateSearchQuery(query);
|
|
978
987
|
} catch (e) {
|
|
979
|
-
return { results: [], error: e.message };
|
|
988
|
+
return { success: false, results: [], error: e.message };
|
|
980
989
|
}
|
|
981
990
|
|
|
982
991
|
// Check result cache first
|
|
@@ -1013,7 +1022,7 @@ export async function searchMessages(query, options = {}) {
|
|
|
1013
1022
|
if (!tbl) {
|
|
1014
1023
|
return {
|
|
1015
1024
|
success: false,
|
|
1016
|
-
error: "
|
|
1025
|
+
error: indexUnavailableMessage("messages")
|
|
1017
1026
|
};
|
|
1018
1027
|
}
|
|
1019
1028
|
|
|
@@ -1084,6 +1093,7 @@ export async function searchMessages(query, options = {}) {
|
|
|
1084
1093
|
rank: idx + 1,
|
|
1085
1094
|
score: row._hybridScore ? row._hybridScore.toFixed(3) : (row._distance ? (1 - row._distance).toFixed(3) : "N/A"),
|
|
1086
1095
|
date: formatLocalDate(row.date) || "Unknown",
|
|
1096
|
+
dateTimestamp: toUnixMillis(row.dateTimestamp) || null,
|
|
1087
1097
|
sender: sender,
|
|
1088
1098
|
senderContact: senderContact, // Resolved contact name (if found)
|
|
1089
1099
|
text: row.text || "",
|
|
@@ -1126,6 +1136,7 @@ export async function getRecentMessageResults(limit = 10, daysBack = 1) {
|
|
|
1126
1136
|
return {
|
|
1127
1137
|
rank: idx + 1,
|
|
1128
1138
|
date: formatLocalDate(row.date) || "Unknown",
|
|
1139
|
+
dateTimestamp: toUnixMillis(row.dateTimestamp) || null,
|
|
1129
1140
|
sender: sender,
|
|
1130
1141
|
senderContact: senderContact,
|
|
1131
1142
|
text: row.text || "",
|
|
@@ -1166,7 +1177,7 @@ export async function getConversationResults(contact, limit = 50) {
|
|
|
1166
1177
|
}
|
|
1167
1178
|
|
|
1168
1179
|
export function formatMessageResults(searchResult) {
|
|
1169
|
-
if (!searchResult.success) return searchResult.error;
|
|
1180
|
+
if (!searchResult.success) return searchResult.error || "Message search failed";
|
|
1170
1181
|
if (searchResult.results.length === 0) return searchResult.message;
|
|
1171
1182
|
|
|
1172
1183
|
const results = searchResult.results.map(r => {
|
|
@@ -1180,6 +1191,9 @@ export function formatMessageResults(searchResult) {
|
|
|
1180
1191
|
return result + "\n---";
|
|
1181
1192
|
}).join("\n");
|
|
1182
1193
|
|
|
1194
|
+
if (searchResult.hasMore) {
|
|
1195
|
+
return results + `\n\nShowing ${searchResult.showing} results. More matches exist; increase limit to see them.`;
|
|
1196
|
+
}
|
|
1183
1197
|
return results;
|
|
1184
1198
|
}
|
|
1185
1199
|
|
|
@@ -1202,7 +1216,7 @@ export async function searchCalendar(query, options = {}) {
|
|
|
1202
1216
|
try {
|
|
1203
1217
|
validatedQuery = validateSearchQuery(query);
|
|
1204
1218
|
} catch (e) {
|
|
1205
|
-
return { results: [], error: e.message };
|
|
1219
|
+
return { success: false, results: [], error: e.message };
|
|
1206
1220
|
}
|
|
1207
1221
|
|
|
1208
1222
|
// Check result cache first
|
|
@@ -1238,7 +1252,7 @@ export async function searchCalendar(query, options = {}) {
|
|
|
1238
1252
|
if (!tbl) {
|
|
1239
1253
|
return {
|
|
1240
1254
|
success: false,
|
|
1241
|
-
error: "
|
|
1255
|
+
error: indexUnavailableMessage("calendar")
|
|
1242
1256
|
};
|
|
1243
1257
|
}
|
|
1244
1258
|
|
|
@@ -1498,7 +1512,7 @@ function formatMinutes(minutes) {
|
|
|
1498
1512
|
}
|
|
1499
1513
|
|
|
1500
1514
|
export function formatCalendarResults(searchResult) {
|
|
1501
|
-
if (!searchResult.success) return searchResult.error;
|
|
1515
|
+
if (!searchResult.success) return searchResult.error || "Calendar search failed";
|
|
1502
1516
|
if (searchResult.results.length === 0) return searchResult.message;
|
|
1503
1517
|
|
|
1504
1518
|
let header = "";
|
|
@@ -1521,6 +1535,9 @@ export function formatCalendarResults(searchResult) {
|
|
|
1521
1535
|
return result + "\n---";
|
|
1522
1536
|
}).join("\n");
|
|
1523
1537
|
|
|
1538
|
+
if (searchResult.hasMore) {
|
|
1539
|
+
return header + results + `\n\nShowing ${searchResult.showing} results. More matches exist; increase limit to see them.`;
|
|
1540
|
+
}
|
|
1524
1541
|
return header + results;
|
|
1525
1542
|
}
|
|
1526
1543
|
|
|
@@ -1567,6 +1584,9 @@ export function formatSendersResults(senders) {
|
|
|
1567
1584
|
|
|
1568
1585
|
// Format messages_contacts results
|
|
1569
1586
|
export function formatMessageContactsResults(contacts) {
|
|
1587
|
+
if (contacts?.error) {
|
|
1588
|
+
return `Error getting message contacts: ${contacts.error}`;
|
|
1589
|
+
}
|
|
1570
1590
|
if (!contacts || contacts.length === 0) {
|
|
1571
1591
|
return "No message contacts found.";
|
|
1572
1592
|
}
|
|
@@ -1581,6 +1601,9 @@ export function formatMessageContactsResults(contacts) {
|
|
|
1581
1601
|
|
|
1582
1602
|
// Format calendar_upcoming results
|
|
1583
1603
|
export function formatUpcomingEventsResults(result) {
|
|
1604
|
+
if (result?.error) {
|
|
1605
|
+
return `Error getting upcoming events: ${result.error}`;
|
|
1606
|
+
}
|
|
1584
1607
|
const events = result.events || result; // Handle both new {events, showing, hasMore} and old array format
|
|
1585
1608
|
if (!events || events.length === 0) {
|
|
1586
1609
|
return "No upcoming events found.";
|
|
@@ -1650,6 +1673,9 @@ export function formatWeekEventsResults(result) {
|
|
|
1650
1673
|
// Format mail_thread results
|
|
1651
1674
|
export function formatEmailThreadResults(result) {
|
|
1652
1675
|
if (result.error) {
|
|
1676
|
+
if (result.error === indexUnavailableMessage("emails")) {
|
|
1677
|
+
return result.error;
|
|
1678
|
+
}
|
|
1653
1679
|
return `Error: ${result.error}`;
|
|
1654
1680
|
}
|
|
1655
1681
|
|
|
@@ -1673,6 +1699,9 @@ export function formatEmailThreadResults(result) {
|
|
|
1673
1699
|
|
|
1674
1700
|
// Format calendar_recurring results
|
|
1675
1701
|
export function formatRecurringEventsResults(result) {
|
|
1702
|
+
if (result?.error) {
|
|
1703
|
+
return `Error getting recurring events: ${result.error}`;
|
|
1704
|
+
}
|
|
1676
1705
|
const events = result.events || result; // Handle both new {events, showing, hasMore} and old array format
|
|
1677
1706
|
if (!events || events.length === 0) {
|
|
1678
1707
|
return "No recurring events found.";
|
|
@@ -1689,4 +1718,4 @@ export function formatRecurringEventsResults(result) {
|
|
|
1689
1718
|
}
|
|
1690
1719
|
|
|
1691
1720
|
// Export internal functions for testing
|
|
1692
|
-
export { expandQuery, parseNegation, extractKeywords };
|
|
1721
|
+
export { expandQuery, parseNegation, extractKeywords, resolvePronouns, updateContext };
|