apple-tools-mcp 1.1.1 → 1.1.2
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 +59 -30
- package/indexer.js +96 -58
- package/lib/shell.js +12 -5
- package/lib/validators.js +64 -2
- package/package.json +1 -1
- package/search.js +44 -18
package/index.js
CHANGED
|
@@ -8,7 +8,7 @@ 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
12
|
|
|
13
13
|
// Lock file to prevent duplicate indexing processes
|
|
14
14
|
const LOCK_FILE = path.join(process.env.HOME, ".apple-tools-mcp", "indexer.lock");
|
|
@@ -234,12 +234,12 @@ function runIndexCycle() {
|
|
|
234
234
|
progressCheckTimer = setInterval(() => {
|
|
235
235
|
const timeSinceProgress = Date.now() - lastProgressTime;
|
|
236
236
|
if (timeSinceProgress > MAX_NO_PROGRESS_MS) {
|
|
237
|
-
console.error(`⚠️ No indexing progress for ${Math.round(timeSinceProgress / 60000)} minutes.
|
|
237
|
+
console.error(`⚠️ No indexing progress for ${Math.round(timeSinceProgress / 60000)} minutes. Indexing still running; not starting another cycle.`);
|
|
238
238
|
clearInterval(progressCheckTimer);
|
|
239
239
|
progressCheckTimer = null;
|
|
240
|
-
indexingInProgress
|
|
240
|
+
// Allow searches, but do NOT clear indexingInProgress or release the lock
|
|
241
|
+
// while indexAll() is still running — overlapping writes can corrupt LanceDB.
|
|
241
242
|
sessionIndexComplete = true;
|
|
242
|
-
releaseLock();
|
|
243
243
|
}
|
|
244
244
|
}, PROGRESS_CHECK_INTERVAL_MS);
|
|
245
245
|
|
|
@@ -464,7 +464,7 @@ function readFullEmail(filePath) {
|
|
|
464
464
|
return "Email file not found.";
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
-
const content = fs.readFileSync(validatedPath, 'utf-8');
|
|
467
|
+
const content = unfoldRfc822Headers(fs.readFileSync(validatedPath, 'utf-8'));
|
|
468
468
|
|
|
469
469
|
// Parse email headers and body
|
|
470
470
|
const fromMatch = content.match(/^From:\s*(.+)$/m);
|
|
@@ -565,7 +565,7 @@ function formatSmartSearchResults(results, synthesizedGroups = null) {
|
|
|
565
565
|
sections.push(` [${r.rank}] Score: ${r.score}`);
|
|
566
566
|
sections.push(` From: ${r.sender}${r.isGroupChat ? ' (Group)' : ''}`);
|
|
567
567
|
sections.push(` Date: ${r.date}`);
|
|
568
|
-
sections.push(` Text: ${r.text.substring(0, 100)}
|
|
568
|
+
sections.push(` Text: ${(r.text || "").substring(0, 100)}${(r.text || "").length > 100 ? "..." : ""}`);
|
|
569
569
|
}
|
|
570
570
|
sections.push("");
|
|
571
571
|
}
|
|
@@ -591,6 +591,10 @@ function formatSmartSearchResults(results, synthesizedGroups = null) {
|
|
|
591
591
|
|
|
592
592
|
// Smart search - routes to appropriate sources and optionally synthesizes results
|
|
593
593
|
async function smartSearch(query, options = {}) {
|
|
594
|
+
if (!sessionIndexComplete) {
|
|
595
|
+
return getIndexingMessage();
|
|
596
|
+
}
|
|
597
|
+
|
|
594
598
|
const { limit = 5, synthesize = true } = options;
|
|
595
599
|
|
|
596
600
|
const sources = detectSources(query);
|
|
@@ -671,21 +675,21 @@ function synthesizeResults(mailResults, messageResults, calendarResults) {
|
|
|
671
675
|
|
|
672
676
|
// Add mail results
|
|
673
677
|
for (const r of mailResults) {
|
|
674
|
-
const ts = r.dateTimestamp || new Date(r.date).getTime();
|
|
678
|
+
const ts = toUnixMillis(r.dateTimestamp) || new Date(r.date).getTime();
|
|
675
679
|
const bucket = getBucket(ts);
|
|
676
680
|
if (bucket) bucket.mail.push(r);
|
|
677
681
|
}
|
|
678
682
|
|
|
679
683
|
// Add message results
|
|
680
684
|
for (const r of messageResults) {
|
|
681
|
-
const ts = r.dateTimestamp || new Date(r.date).getTime();
|
|
685
|
+
const ts = toUnixMillis(r.dateTimestamp) || new Date(r.date).getTime();
|
|
682
686
|
const bucket = getBucket(ts);
|
|
683
687
|
if (bucket) bucket.messages.push(r);
|
|
684
688
|
}
|
|
685
689
|
|
|
686
690
|
// Add calendar results
|
|
687
691
|
for (const r of calendarResults) {
|
|
688
|
-
const ts = r.startTimestamp || new Date(r.start).getTime();
|
|
692
|
+
const ts = toUnixMillis(r.startTimestamp) || new Date(r.start).getTime();
|
|
689
693
|
const bucket = getBucket(ts);
|
|
690
694
|
if (bucket) bucket.calendar.push(r);
|
|
691
695
|
}
|
|
@@ -759,6 +763,10 @@ function formatContactLookupResult(contact) {
|
|
|
759
763
|
// ============ PERSON SEARCH (CROSS-SOURCE) ============
|
|
760
764
|
|
|
761
765
|
async function personSearch(name, limit = 10) {
|
|
766
|
+
if (!sessionIndexComplete) {
|
|
767
|
+
return getIndexingMessage();
|
|
768
|
+
}
|
|
769
|
+
|
|
762
770
|
// First, try to find the contact to get all their identifiers
|
|
763
771
|
const contacts = searchContacts(name, 5);
|
|
764
772
|
|
|
@@ -887,7 +895,7 @@ function formatPersonSearchResults(results) {
|
|
|
887
895
|
if (results.messages && results.messages.results && results.messages.results.length > 0) {
|
|
888
896
|
sections.push(`💬 MESSAGES (${results.messages.results.length}):`);
|
|
889
897
|
for (const r of results.messages.results.slice(0, 10)) {
|
|
890
|
-
sections.push(` • ${r.text.substring(0, 80)}${r.text.length > 80 ? "..." : ""}`);
|
|
898
|
+
sections.push(` • ${(r.text || "").substring(0, 80)}${(r.text || "").length > 80 ? "..." : ""}`);
|
|
891
899
|
sections.push(` ${r.date}`);
|
|
892
900
|
}
|
|
893
901
|
sections.push("");
|
|
@@ -1244,7 +1252,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1244
1252
|
// Smart search (agentic)
|
|
1245
1253
|
case "smart_search":
|
|
1246
1254
|
result = await smartSearch(args.query, {
|
|
1247
|
-
limit: args?.limit
|
|
1255
|
+
limit: validateLimit(args?.limit, 5, 100),
|
|
1248
1256
|
synthesize: args?.synthesize !== false
|
|
1249
1257
|
});
|
|
1250
1258
|
break;
|
|
@@ -1252,8 +1260,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1252
1260
|
// Email tools
|
|
1253
1261
|
case "mail_search":
|
|
1254
1262
|
result = await mailSearch(args.query, {
|
|
1255
|
-
limit: args?.limit
|
|
1256
|
-
daysBack: args?.days_back
|
|
1263
|
+
limit: validateLimit(args?.limit, 30),
|
|
1264
|
+
daysBack: validateDaysBack(args?.days_back),
|
|
1257
1265
|
sender: args?.sender || null,
|
|
1258
1266
|
recipient: args?.recipient || null,
|
|
1259
1267
|
hasAttachment: args?.has_attachment ?? null,
|
|
@@ -1266,7 +1274,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1266
1274
|
break;
|
|
1267
1275
|
|
|
1268
1276
|
case "mail_recent":
|
|
1269
|
-
result = await mailRecent(
|
|
1277
|
+
result = await mailRecent(
|
|
1278
|
+
validateLimit(args?.limit, 30),
|
|
1279
|
+
validateDaysBack(args?.days_back) || 7,
|
|
1280
|
+
args?.unread_only || false,
|
|
1281
|
+
args?.include_junk || false
|
|
1282
|
+
);
|
|
1270
1283
|
break;
|
|
1271
1284
|
|
|
1272
1285
|
case "mail_date":
|
|
@@ -1280,8 +1293,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1280
1293
|
// Messages tools
|
|
1281
1294
|
case "messages_search":
|
|
1282
1295
|
result = await messagesSearch(args.query, {
|
|
1283
|
-
limit: args?.limit
|
|
1284
|
-
daysBack: args?.days_back
|
|
1296
|
+
limit: validateLimit(args?.limit, 30),
|
|
1297
|
+
daysBack: validateDaysBack(args?.days_back),
|
|
1285
1298
|
contact: args?.contact || null,
|
|
1286
1299
|
groupChatOnly: args?.group_chat_only || false,
|
|
1287
1300
|
groupChatName: args?.group_chat_name || null,
|
|
@@ -1291,19 +1304,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1291
1304
|
break;
|
|
1292
1305
|
|
|
1293
1306
|
case "messages_recent":
|
|
1294
|
-
result = await messagesRecent(
|
|
1307
|
+
result = await messagesRecent(
|
|
1308
|
+
validateLimit(args?.limit, 30),
|
|
1309
|
+
validateDaysBack(args?.days_back) || 1
|
|
1310
|
+
);
|
|
1295
1311
|
break;
|
|
1296
1312
|
|
|
1297
1313
|
case "messages_conversation":
|
|
1298
|
-
result = await messagesConversation(args.contact, args?.limit
|
|
1314
|
+
result = await messagesConversation(args.contact, validateLimit(args?.limit, 50));
|
|
1299
1315
|
break;
|
|
1300
1316
|
|
|
1301
1317
|
// Calendar tools
|
|
1302
1318
|
case "calendar_search":
|
|
1303
1319
|
result = await calendarSearch(args.query, {
|
|
1304
|
-
limit: args?.limit
|
|
1305
|
-
daysBack: args?.days_back
|
|
1306
|
-
daysAhead: args?.days_ahead
|
|
1320
|
+
limit: validateLimit(args?.limit, 30),
|
|
1321
|
+
daysBack: validateDaysBack(args?.days_back),
|
|
1322
|
+
daysAhead: validateDaysBack(args?.days_ahead),
|
|
1307
1323
|
calendarName: args?.calendar_name || null,
|
|
1308
1324
|
allDayOnly: args?.all_day_only || false,
|
|
1309
1325
|
sortBy: args?.sort_by || "relevance"
|
|
@@ -1326,7 +1342,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1326
1342
|
|
|
1327
1343
|
// Mail tools
|
|
1328
1344
|
case "mail_senders":
|
|
1329
|
-
|
|
1345
|
+
if (!sessionIndexComplete) {
|
|
1346
|
+
result = getIndexingMessage();
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
result = formatSendersResults(await getFrequentSenders(
|
|
1350
|
+
validateLimit(args?.limit, 30),
|
|
1351
|
+
validateDaysBack(args?.days_back),
|
|
1352
|
+
args?.include_junk || false
|
|
1353
|
+
));
|
|
1330
1354
|
break;
|
|
1331
1355
|
|
|
1332
1356
|
case "rebuild_index":
|
|
@@ -1344,6 +1368,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1344
1368
|
|
|
1345
1369
|
// Start rebuild in background and return immediately
|
|
1346
1370
|
indexingInProgress = true;
|
|
1371
|
+
sessionIndexComplete = false;
|
|
1347
1372
|
const rebuildSources = args?.sources || ["emails", "messages", "calendar"];
|
|
1348
1373
|
|
|
1349
1374
|
// Fire and forget - don't await
|
|
@@ -1381,34 +1406,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1381
1406
|
|
|
1382
1407
|
// Messages tools
|
|
1383
1408
|
case "messages_contacts":
|
|
1384
|
-
result = formatMessageContactsResults(getMessageContacts(args?.limit
|
|
1409
|
+
result = formatMessageContactsResults(getMessageContacts(validateLimit(args?.limit, 50, 500)));
|
|
1385
1410
|
break;
|
|
1386
1411
|
|
|
1387
1412
|
// Calendar tools
|
|
1388
1413
|
case "calendar_upcoming":
|
|
1389
|
-
result = formatUpcomingEventsResults(getUpcomingEvents(args?.limit
|
|
1414
|
+
result = formatUpcomingEventsResults(getUpcomingEvents(validateLimit(args?.limit, 30, 100)));
|
|
1390
1415
|
break;
|
|
1391
1416
|
|
|
1392
1417
|
// ============ NEW TOOLS - PHASE 2 ============
|
|
1393
1418
|
|
|
1394
1419
|
case "calendar_week":
|
|
1395
|
-
result = formatWeekEventsResults(getWeekEvents(args?.week_offset
|
|
1420
|
+
result = formatWeekEventsResults(getWeekEvents(validateWeekOffset(args?.week_offset)));
|
|
1396
1421
|
break;
|
|
1397
1422
|
|
|
1398
1423
|
// ============ NEW TOOLS - PHASE 3 ============
|
|
1399
1424
|
|
|
1400
1425
|
case "mail_thread":
|
|
1401
|
-
|
|
1426
|
+
if (!sessionIndexComplete) {
|
|
1427
|
+
result = getIndexingMessage();
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
result = formatEmailThreadResults(await getEmailThread(args.file_path, validateLimit(args?.limit, 30)));
|
|
1402
1431
|
break;
|
|
1403
1432
|
|
|
1404
1433
|
case "calendar_recurring":
|
|
1405
|
-
result = formatRecurringEventsResults(getRecurringEvents(args?.limit
|
|
1434
|
+
result = formatRecurringEventsResults(getRecurringEvents(validateLimit(args?.limit, 30, 100)));
|
|
1406
1435
|
break;
|
|
1407
1436
|
|
|
1408
1437
|
// ============ CONTACTS TOOLS ============
|
|
1409
1438
|
|
|
1410
1439
|
case "contacts_search":
|
|
1411
|
-
result = formatContactsSearchResults(searchContacts(args.query, args?.limit
|
|
1440
|
+
result = formatContactsSearchResults(searchContacts(args.query, validateLimit(args?.limit, 30)));
|
|
1412
1441
|
break;
|
|
1413
1442
|
|
|
1414
1443
|
case "contacts_lookup":
|
|
@@ -1416,7 +1445,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1416
1445
|
break;
|
|
1417
1446
|
|
|
1418
1447
|
case "person_search":
|
|
1419
|
-
result = await personSearch(args.name, args?.limit
|
|
1448
|
+
result = await personSearch(args.name, validateLimit(args?.limit, 10));
|
|
1420
1449
|
break;
|
|
1421
1450
|
|
|
1422
1451
|
default:
|
|
@@ -1436,7 +1465,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1436
1465
|
async function main() {
|
|
1437
1466
|
const transport = new StdioServerTransport();
|
|
1438
1467
|
await server.connect(transport);
|
|
1439
|
-
console.error("Apple Tools MCP server running (v1.1.
|
|
1468
|
+
console.error("Apple Tools MCP server running (v1.1.2)");
|
|
1440
1469
|
// Background indexing runs automatically on startup and every INDEX_INTERVAL
|
|
1441
1470
|
}
|
|
1442
1471
|
|
package/indexer.js
CHANGED
|
@@ -9,7 +9,10 @@ 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";
|
|
15
18
|
|
|
@@ -252,14 +255,17 @@ function parseEmlx(filePath) {
|
|
|
252
255
|
content = lines.slice(1).join("\n");
|
|
253
256
|
}
|
|
254
257
|
|
|
258
|
+
// Unfold RFC 822 wrapped headers so long From/Subject/To values are complete
|
|
259
|
+
const unfolded = unfoldRfc822Headers(content);
|
|
260
|
+
|
|
255
261
|
// Extract headers
|
|
256
|
-
const fromMatch =
|
|
257
|
-
const subjectMatch =
|
|
258
|
-
const dateMatch =
|
|
259
|
-
const toMatch =
|
|
260
|
-
const ccMatch =
|
|
261
|
-
const messageIdMatch =
|
|
262
|
-
const flaggedMatch =
|
|
262
|
+
const fromMatch = unfolded.match(/^From:\s*(.+)$/m);
|
|
263
|
+
const subjectMatch = unfolded.match(/^Subject:\s*(.+)$/m);
|
|
264
|
+
const dateMatch = unfolded.match(/^Date:\s*(.+)$/m);
|
|
265
|
+
const toMatch = unfolded.match(/^To:\s*(.+)$/m);
|
|
266
|
+
const ccMatch = unfolded.match(/^Cc:\s*(.+)$/m);
|
|
267
|
+
const messageIdMatch = unfolded.match(/^Message-ID:\s*(.+)$/im);
|
|
268
|
+
const flaggedMatch = unfolded.match(/^X-Flagged:\s*(.+)$/im) || unfolded.match(/flags.*flagged/i);
|
|
263
269
|
|
|
264
270
|
// Check for attachments
|
|
265
271
|
const hasAttachment = /Content-Disposition:\s*attachment/i.test(content) ||
|
|
@@ -335,13 +341,10 @@ function parseEmlx(filePath) {
|
|
|
335
341
|
// Full scan - used for first run and rebuild_index
|
|
336
342
|
// Includes both .emlx and .partial.emlx files (partial = not fully downloaded via IMAP)
|
|
337
343
|
async function findAllEmlxFiles() {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
console.error("Error finding emlx files:", e.message);
|
|
343
|
-
return [];
|
|
344
|
-
}
|
|
344
|
+
// "*.emlx" also matches "*.partial.emlx"
|
|
345
|
+
// Let permission/IO errors propagate so indexEmails does not persist a
|
|
346
|
+
// timestamp for an empty scan and skip real mail forever.
|
|
347
|
+
return safeFind(MAIL_DIR, { name: "*.emlx", type: "f" });
|
|
345
348
|
}
|
|
346
349
|
|
|
347
350
|
// Fast incremental scan - uses find with -mtime filter (more reliable than mdfind/Spotlight)
|
|
@@ -579,6 +582,9 @@ function getMessages(sinceTimestamp = null) {
|
|
|
579
582
|
ORDER BY m.date DESC
|
|
580
583
|
`;
|
|
581
584
|
const results = safeSqlite3Json(MESSAGES_DB, query, { timeout: 60000 });
|
|
585
|
+
if (!Array.isArray(results)) {
|
|
586
|
+
throw new Error("Unexpected sqlite3 result for messages");
|
|
587
|
+
}
|
|
582
588
|
|
|
583
589
|
// Post-process: extract text from attributedBody where text is NULL
|
|
584
590
|
let extractedCount = 0;
|
|
@@ -607,7 +613,7 @@ function getMessages(sinceTimestamp = null) {
|
|
|
607
613
|
return results.filter(msg => msg.text && msg.text.trim() !== '');
|
|
608
614
|
} catch (e) {
|
|
609
615
|
console.error("Error reading messages:", e.message);
|
|
610
|
-
|
|
616
|
+
throw e;
|
|
611
617
|
}
|
|
612
618
|
}
|
|
613
619
|
|
|
@@ -645,19 +651,24 @@ function getParticipantStatus(status) {
|
|
|
645
651
|
}
|
|
646
652
|
|
|
647
653
|
function getCalendarEvents() {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
654
|
+
// NOTE: We index ALL calendar events, not filtered by date
|
|
655
|
+
// Calendar events don't have a "file modification time" like emails do,
|
|
656
|
+
// so we can't use the mdfind + DAYS_BACK approach.
|
|
657
|
+
// Calendar indexing is always comprehensive - filtering by date would lose historical context.
|
|
658
|
+
// Must throw on source-read failure: a silent [] would look like "every event
|
|
659
|
+
// was deleted" and the stale-entry pass would wipe the calendar index.
|
|
660
|
+
if (!fs.existsSync(CALENDAR_DB)) {
|
|
661
|
+
throw new Error("Calendar database not found");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const now = Date.now();
|
|
665
|
+
const pastDate = unixMsToMacAbsolute(now - 10 * 365 * 24 * 60 * 60 * 1000); // 10 years back
|
|
666
|
+
const futureDate = unixMsToMacAbsolute(now + 10 * 365 * 24 * 60 * 60 * 1000); // 10 years ahead
|
|
667
|
+
|
|
668
|
+
// Query OccurrenceCache for recurring events and their calculated occurrences
|
|
669
|
+
// This includes both recurring and non-recurring events
|
|
670
|
+
// GROUP BY to avoid duplicate entries for recurring events
|
|
671
|
+
const query = `
|
|
661
672
|
SELECT
|
|
662
673
|
ci.ROWID as id,
|
|
663
674
|
ci.summary,
|
|
@@ -679,10 +690,7 @@ function getCalendarEvents() {
|
|
|
679
690
|
ORDER BY MIN(oc.day) ASC
|
|
680
691
|
`;
|
|
681
692
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
// Get attendees for events that have them (separate query for efficiency)
|
|
685
|
-
const attendeesQuery = `
|
|
693
|
+
const attendeesQuery = `
|
|
686
694
|
SELECT
|
|
687
695
|
p.owner_id,
|
|
688
696
|
COALESCE(i.display_name, p.email, 'Unknown') as name,
|
|
@@ -692,11 +700,13 @@ function getCalendarEvents() {
|
|
|
692
700
|
WHERE p.entity_type = 0
|
|
693
701
|
`;
|
|
694
702
|
|
|
703
|
+
return withCalendarCopy((dbPath) => {
|
|
704
|
+
const rows = safeSqlite3Json(dbPath, query, { timeout: 30000 });
|
|
705
|
+
|
|
695
706
|
let attendeesMap = new Map();
|
|
696
707
|
try {
|
|
697
|
-
const attendeesRows = safeSqlite3Json(
|
|
708
|
+
const attendeesRows = safeSqlite3Json(dbPath, attendeesQuery, { timeout: 10000 });
|
|
698
709
|
|
|
699
|
-
// Group attendees by owner_id (event id)
|
|
700
710
|
for (const att of attendeesRows) {
|
|
701
711
|
if (!attendeesMap.has(att.owner_id)) {
|
|
702
712
|
attendeesMap.set(att.owner_id, []);
|
|
@@ -734,10 +744,7 @@ function getCalendarEvents() {
|
|
|
734
744
|
|
|
735
745
|
console.error(`Calendar: Retrieved ${events.length} events via SQLite (~${Math.round((Date.now() - now))}ms)`);
|
|
736
746
|
return events;
|
|
737
|
-
}
|
|
738
|
-
console.error("Error reading calendar:", e.message);
|
|
739
|
-
return [];
|
|
740
|
-
}
|
|
747
|
+
});
|
|
741
748
|
}
|
|
742
749
|
|
|
743
750
|
// ============ DATABASE FUNCTIONS ============
|
|
@@ -927,7 +934,13 @@ export async function indexEmails(progressCallback = null, forceFullScan = false
|
|
|
927
934
|
// Use fast incremental scan if we have a previous timestamp
|
|
928
935
|
const startTime = Date.now();
|
|
929
936
|
console.error(`Calling findNewEmlxFiles with timestamp: ${lastEmailIndexTime ? new Date(lastEmailIndexTime).toISOString() : 'null (full scan)'}`);
|
|
930
|
-
|
|
937
|
+
let newFiles;
|
|
938
|
+
try {
|
|
939
|
+
newFiles = await findNewEmlxFiles(lastEmailIndexTime);
|
|
940
|
+
} catch (e) {
|
|
941
|
+
console.error(`Email source read failed; skipping this cycle so the lookback timestamp is not advanced: ${e.message}`);
|
|
942
|
+
return { indexed: 0, added: 0, error: e.message };
|
|
943
|
+
}
|
|
931
944
|
console.error(`Found ${newFiles.length} new/modified email files (${Date.now() - startTime}ms)`);
|
|
932
945
|
|
|
933
946
|
const indexedPaths = await getIndexedIdsWithRetry("emails", "filePath");
|
|
@@ -1041,6 +1054,13 @@ export async function indexEmails(progressCallback = null, forceFullScan = false
|
|
|
1041
1054
|
if (uniqueRecords.length > 0) {
|
|
1042
1055
|
if (!tables.emails) {
|
|
1043
1056
|
tables.emails = await db.createTable("emails", uniqueRecords, { mode: "overwrite" });
|
|
1057
|
+
// Track messageIds from the first batch so later batches can
|
|
1058
|
+
// dedupe IMAP copies (INBOX vs Junk) of the same Message-ID.
|
|
1059
|
+
for (const record of uniqueRecords) {
|
|
1060
|
+
if (record.messageId) {
|
|
1061
|
+
indexedMessageIds.add(record.messageId);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1044
1064
|
} else {
|
|
1045
1065
|
// Double-check: verify these IDs truly aren't in the index
|
|
1046
1066
|
const currentIndexed = await getIndexedIdsWithRetry("emails", "filePath");
|
|
@@ -1130,7 +1150,13 @@ export async function indexMessages(forceFullScan = false) {
|
|
|
1130
1150
|
const indexStartTime = Date.now();
|
|
1131
1151
|
|
|
1132
1152
|
// Use incremental scan if we have a previous timestamp
|
|
1133
|
-
|
|
1153
|
+
let messages;
|
|
1154
|
+
try {
|
|
1155
|
+
messages = getMessages(lastMessageIndexTime);
|
|
1156
|
+
} catch (e) {
|
|
1157
|
+
console.error(`Messages source read failed; skipping this cycle so the lookback timestamp is not advanced: ${e.message}`);
|
|
1158
|
+
return { indexed: 0, added: 0, error: e.message };
|
|
1159
|
+
}
|
|
1134
1160
|
console.error(`Found ${messages.length} messages${lastMessageIndexTime ? ' (incremental)' : ' (full scan)'}`);
|
|
1135
1161
|
|
|
1136
1162
|
const indexed = await getIndexedIdsWithRetry("messages", "id");
|
|
@@ -1186,7 +1212,7 @@ export async function indexMessages(forceFullScan = false) {
|
|
|
1186
1212
|
return {
|
|
1187
1213
|
id: String(msg.id),
|
|
1188
1214
|
date: msg.date,
|
|
1189
|
-
dateTimestamp: msg.dateTimestamp
|
|
1215
|
+
dateTimestamp: toUnixMillis(msg.dateTimestamp),
|
|
1190
1216
|
sender: msg.sender,
|
|
1191
1217
|
text: msg.text?.substring(0, 500) || "",
|
|
1192
1218
|
chatId: String(msg.chatId || ""),
|
|
@@ -1270,7 +1296,15 @@ export async function indexMessages(forceFullScan = false) {
|
|
|
1270
1296
|
export async function indexCalendar() {
|
|
1271
1297
|
await initDB();
|
|
1272
1298
|
|
|
1273
|
-
|
|
1299
|
+
let events;
|
|
1300
|
+
try {
|
|
1301
|
+
events = getCalendarEvents();
|
|
1302
|
+
} catch (e) {
|
|
1303
|
+
// Do not treat a source-read failure as "zero events" — that would mark
|
|
1304
|
+
// every indexed row stale and delete the calendar index.
|
|
1305
|
+
console.error(`Calendar source read failed; skipping index update to avoid data loss: ${e.message}`);
|
|
1306
|
+
return { indexed: 0, added: 0, removed: 0, error: e.message };
|
|
1307
|
+
}
|
|
1274
1308
|
console.error(`Found ${events.length} calendar events`);
|
|
1275
1309
|
|
|
1276
1310
|
// Get already indexed event IDs for incremental indexing
|
|
@@ -1493,14 +1527,14 @@ export async function getRecentMessages(limit = 10, daysBack = 1) {
|
|
|
1493
1527
|
if (!tables.messages) return { messages: [], hasMore: false };
|
|
1494
1528
|
|
|
1495
1529
|
try {
|
|
1496
|
-
const cutoff = Date.now()
|
|
1530
|
+
const cutoff = Date.now() - (daysBack * 24 * 60 * 60 * 1000);
|
|
1497
1531
|
const results = await tables.messages.query()
|
|
1498
1532
|
.select(["id", "date", "dateTimestamp", "sender", "text", "chatId", "isGroupChat"])
|
|
1499
1533
|
.toArray();
|
|
1500
1534
|
|
|
1501
1535
|
const filtered = results
|
|
1502
|
-
.filter(r => r.dateTimestamp >= cutoff)
|
|
1503
|
-
.sort((a, b) => b.dateTimestamp - a.dateTimestamp);
|
|
1536
|
+
.filter(r => toUnixMillis(r.dateTimestamp) >= cutoff)
|
|
1537
|
+
.sort((a, b) => toUnixMillis(b.dateTimestamp) - toUnixMillis(a.dateTimestamp));
|
|
1504
1538
|
|
|
1505
1539
|
const hasMore = filtered.length > limit;
|
|
1506
1540
|
const messages = filtered.slice(0, limit);
|
|
@@ -1530,7 +1564,7 @@ export async function getConversation(contact, limit = 50) {
|
|
|
1530
1564
|
|
|
1531
1565
|
// Sort chronologically (oldest first for conversation view)
|
|
1532
1566
|
return filtered
|
|
1533
|
-
.sort((a, b) => a.dateTimestamp - b.dateTimestamp)
|
|
1567
|
+
.sort((a, b) => toUnixMillis(a.dateTimestamp) - toUnixMillis(b.dateTimestamp))
|
|
1534
1568
|
.slice(-limit); // Take last N messages
|
|
1535
1569
|
} catch (e) {
|
|
1536
1570
|
console.error("Error getting conversation:", e.message);
|
|
@@ -1648,7 +1682,7 @@ export function getMessageContacts(limit = 50) {
|
|
|
1648
1682
|
return safeSqlite3Json(MESSAGES_DB, query, { timeout: 30000 });
|
|
1649
1683
|
} catch (e) {
|
|
1650
1684
|
console.error("Error getting message contacts:", e.message);
|
|
1651
|
-
return
|
|
1685
|
+
return { error: e.message };
|
|
1652
1686
|
}
|
|
1653
1687
|
}
|
|
1654
1688
|
|
|
@@ -1678,13 +1712,15 @@ export function getUpcomingEvents(limit = 10) {
|
|
|
1678
1712
|
ORDER BY sort_day ASC
|
|
1679
1713
|
LIMIT ${fetchLimit}
|
|
1680
1714
|
`;
|
|
1681
|
-
const events =
|
|
1715
|
+
const events = withCalendarCopy((dbPath) => {
|
|
1716
|
+
return safeSqlite3Json(dbPath, query, { timeout: 10000 });
|
|
1717
|
+
});
|
|
1682
1718
|
const hasMore = events.length > safeLimit;
|
|
1683
1719
|
const limitedEvents = events.slice(0, safeLimit);
|
|
1684
1720
|
return { events: limitedEvents, showing: limitedEvents.length, hasMore };
|
|
1685
1721
|
} catch (e) {
|
|
1686
1722
|
console.error("Error getting upcoming events:", e.message);
|
|
1687
|
-
return { events: [], showing: 0, hasMore: false };
|
|
1723
|
+
return { events: [], showing: 0, hasMore: false, error: e.message };
|
|
1688
1724
|
}
|
|
1689
1725
|
}
|
|
1690
1726
|
|
|
@@ -1876,7 +1912,9 @@ export function getWeekEvents(weekOffset = 0) {
|
|
|
1876
1912
|
AND ci.summary IS NOT NULL AND ci.summary <> ''
|
|
1877
1913
|
ORDER BY oc.day ASC
|
|
1878
1914
|
`;
|
|
1879
|
-
const events =
|
|
1915
|
+
const events = withCalendarCopy((dbPath) => {
|
|
1916
|
+
return safeSqlite3Json(dbPath, query, { timeout: 15000 });
|
|
1917
|
+
});
|
|
1880
1918
|
|
|
1881
1919
|
const weekStart = monday.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
|
|
1882
1920
|
const weekEnd = sunday.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
|
|
@@ -1915,15 +1953,13 @@ export async function getEmailThread(filePath, limit = 20) {
|
|
|
1915
1953
|
}
|
|
1916
1954
|
|
|
1917
1955
|
// Read the email to get subject
|
|
1918
|
-
const content = fs.readFileSync(validatedPath, "utf-8");
|
|
1956
|
+
const content = unfoldRfc822Headers(fs.readFileSync(validatedPath, "utf-8"));
|
|
1919
1957
|
const subjectMatch = content.match(/^Subject:\s*(.+)$/m);
|
|
1920
1958
|
if (!subjectMatch) {
|
|
1921
1959
|
return { error: "Could not extract subject from email", emails: [] };
|
|
1922
1960
|
}
|
|
1923
1961
|
|
|
1924
|
-
|
|
1925
|
-
let subject = subjectMatch[1].trim();
|
|
1926
|
-
const baseSubject = subject.replace(/^(Re|Fwd|Fw):\s*/gi, "").trim();
|
|
1962
|
+
const baseSubject = stripSubjectPrefixes(subjectMatch[1]);
|
|
1927
1963
|
|
|
1928
1964
|
if (baseSubject.length < 5) {
|
|
1929
1965
|
return { error: "Subject too short to find thread", emails: [] };
|
|
@@ -1937,7 +1973,7 @@ export async function getEmailThread(filePath, limit = 20) {
|
|
|
1937
1973
|
// Filter to emails with matching base subject
|
|
1938
1974
|
const threadEmails = allEmails
|
|
1939
1975
|
.filter(e => {
|
|
1940
|
-
const eBaseSubject = (e.subject || "")
|
|
1976
|
+
const eBaseSubject = stripSubjectPrefixes(e.subject || "");
|
|
1941
1977
|
return eBaseSubject.toLowerCase() === baseSubject.toLowerCase();
|
|
1942
1978
|
})
|
|
1943
1979
|
.sort((a, b) => a.dateTimestamp - b.dateTimestamp)
|
|
@@ -1981,12 +2017,14 @@ export function getRecurringEvents(limit = 20) {
|
|
|
1981
2017
|
ORDER BY occurrenceCount DESC, MIN(oc.day) ASC
|
|
1982
2018
|
LIMIT ${fetchLimit}
|
|
1983
2019
|
`;
|
|
1984
|
-
const events =
|
|
2020
|
+
const events = withCalendarCopy((dbPath) => {
|
|
2021
|
+
return safeSqlite3Json(dbPath, query, { timeout: 30000 });
|
|
2022
|
+
});
|
|
1985
2023
|
const hasMore = events.length > safeLimit;
|
|
1986
2024
|
const limitedEvents = events.slice(0, safeLimit);
|
|
1987
2025
|
return { events: limitedEvents, showing: limitedEvents.length, hasMore };
|
|
1988
2026
|
} catch (e) {
|
|
1989
2027
|
console.error("Error getting recurring events:", e.message);
|
|
1990
|
-
return { events: [], showing: 0, hasMore: false };
|
|
2028
|
+
return { events: [], showing: 0, hasMore: false, error: e.message };
|
|
1991
2029
|
}
|
|
1992
2030
|
}
|
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,7 +1,7 @@
|
|
|
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
6
|
|
|
7
7
|
let db = null;
|
|
@@ -102,13 +102,16 @@ function resolvePronouns(query) {
|
|
|
102
102
|
return query;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (pronounPattern.test(query) && queryContext.lastPerson) {
|
|
108
|
-
return query.replace(pronounPattern, queryContext.lastPerson);
|
|
105
|
+
if (!queryContext.lastPerson) {
|
|
106
|
+
return query;
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
|
|
109
|
+
// Do not use RegExp#test with /g — lastIndex is stateful and can skip the
|
|
110
|
+
// first (or only) pronoun. replace() with a fresh regex is sufficient.
|
|
111
|
+
return query.replace(
|
|
112
|
+
/\b(they|them|their|he|him|his|she|her|hers)\b/gi,
|
|
113
|
+
queryContext.lastPerson
|
|
114
|
+
);
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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.";
|
|
@@ -1673,6 +1696,9 @@ export function formatEmailThreadResults(result) {
|
|
|
1673
1696
|
|
|
1674
1697
|
// Format calendar_recurring results
|
|
1675
1698
|
export function formatRecurringEventsResults(result) {
|
|
1699
|
+
if (result?.error) {
|
|
1700
|
+
return `Error getting recurring events: ${result.error}`;
|
|
1701
|
+
}
|
|
1676
1702
|
const events = result.events || result; // Handle both new {events, showing, hasMore} and old array format
|
|
1677
1703
|
if (!events || events.length === 0) {
|
|
1678
1704
|
return "No recurring events found.";
|
|
@@ -1689,4 +1715,4 @@ export function formatRecurringEventsResults(result) {
|
|
|
1689
1715
|
}
|
|
1690
1716
|
|
|
1691
1717
|
// Export internal functions for testing
|
|
1692
|
-
export { expandQuery, parseNegation, extractKeywords };
|
|
1718
|
+
export { expandQuery, parseNegation, extractKeywords, resolvePronouns, updateContext };
|