#
tokens: 39053/50000 5/30 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/dhravya/apple-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── FUNDING.yml
├── .gitignore
├── apple-mcp.dxt
├── bun.lockb
├── CLAUDE.md
├── index.ts
├── LICENSE
├── manifest.json
├── package.json
├── README.md
├── TEST_README.md
├── test-runner.ts
├── tests
│   ├── fixtures
│   │   └── test-data.ts
│   ├── helpers
│   │   └── test-utils.ts
│   ├── integration
│   │   ├── calendar.test.ts
│   │   ├── contacts-simple.test.ts
│   │   ├── contacts.test.ts
│   │   ├── mail.test.ts
│   │   ├── maps.test.ts
│   │   ├── messages.test.ts
│   │   ├── notes.test.ts
│   │   └── reminders.test.ts
│   └── setup.ts
├── tools.ts
├── tsconfig.json
└── utils
    ├── calendar.ts
    ├── contacts.ts
    ├── mail.ts
    ├── maps.ts
    ├── message.ts
    ├── notes.ts
    ├── reminders.ts
    └── web-search.ts
```

# Files

--------------------------------------------------------------------------------
/utils/notes.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { runAppleScript } from "run-applescript";
  2 | 
  3 | // Configuration
  4 | const CONFIG = {
  5 | 	// Maximum notes to process (to avoid performance issues)
  6 | 	MAX_NOTES: 50,
  7 | 	// Maximum content length for previews
  8 | 	MAX_CONTENT_PREVIEW: 200,
  9 | 	// Timeout for operations
 10 | 	TIMEOUT_MS: 8000,
 11 | };
 12 | 
 13 | type Note = {
 14 | 	name: string;
 15 | 	content: string;
 16 | 	creationDate?: Date;
 17 | 	modificationDate?: Date;
 18 | };
 19 | 
 20 | type CreateNoteResult = {
 21 | 	success: boolean;
 22 | 	note?: Note;
 23 | 	message?: string;
 24 | 	folderName?: string;
 25 | 	usedDefaultFolder?: boolean;
 26 | };
 27 | 
 28 | /**
 29 |  * Check if Notes app is accessible
 30 |  */
 31 | async function checkNotesAccess(): Promise<boolean> {
 32 | 	try {
 33 | 		const script = `
 34 | tell application "Notes"
 35 |     return name
 36 | end tell`;
 37 | 
 38 | 		await runAppleScript(script);
 39 | 		return true;
 40 | 	} catch (error) {
 41 | 		console.error(
 42 | 			`Cannot access Notes app: ${error instanceof Error ? error.message : String(error)}`,
 43 | 		);
 44 | 		return false;
 45 | 	}
 46 | }
 47 | 
 48 | /**
 49 |  * Request Notes app access and provide instructions if not available
 50 |  */
 51 | async function requestNotesAccess(): Promise<{ hasAccess: boolean; message: string }> {
 52 | 	try {
 53 | 		// First check if we already have access
 54 | 		const hasAccess = await checkNotesAccess();
 55 | 		if (hasAccess) {
 56 | 			return {
 57 | 				hasAccess: true,
 58 | 				message: "Notes access is already granted."
 59 | 			};
 60 | 		}
 61 | 
 62 | 		// If no access, provide clear instructions
 63 | 		return {
 64 | 			hasAccess: false,
 65 | 			message: "Notes access is required but not granted. Please:\n1. Open System Settings > Privacy & Security > Automation\n2. Find your terminal/app in the list and enable 'Notes'\n3. Restart your terminal and try again\n4. If the option is not available, run this command again to trigger the permission dialog"
 66 | 		};
 67 | 	} catch (error) {
 68 | 		return {
 69 | 			hasAccess: false,
 70 | 			message: `Error checking Notes access: ${error instanceof Error ? error.message : String(error)}`
 71 | 		};
 72 | 	}
 73 | }
 74 | 
 75 | /**
 76 |  * Get all notes from Notes app (limited for performance)
 77 |  */
 78 | async function getAllNotes(): Promise<Note[]> {
 79 | 	try {
 80 | 		const accessResult = await requestNotesAccess();
 81 | 		if (!accessResult.hasAccess) {
 82 | 			throw new Error(accessResult.message);
 83 | 		}
 84 | 
 85 | 		const script = `
 86 | tell application "Notes"
 87 |     set notesList to {}
 88 |     set noteCount to 0
 89 | 
 90 |     -- Get all notes from all folders
 91 |     set allNotes to notes
 92 | 
 93 |     repeat with i from 1 to (count of allNotes)
 94 |         if noteCount >= ${CONFIG.MAX_NOTES} then exit repeat
 95 | 
 96 |         try
 97 |             set currentNote to item i of allNotes
 98 |             set noteName to name of currentNote
 99 |             set noteContent to plaintext of currentNote
100 | 
101 |             -- Limit content for preview
102 |             if (length of noteContent) > ${CONFIG.MAX_CONTENT_PREVIEW} then
103 |                 set noteContent to (characters 1 thru ${CONFIG.MAX_CONTENT_PREVIEW} of noteContent) as string
104 |                 set noteContent to noteContent & "..."
105 |             end if
106 | 
107 |             set noteInfo to {name:noteName, content:noteContent}
108 |             set notesList to notesList & {noteInfo}
109 |             set noteCount to noteCount + 1
110 |         on error
111 |             -- Skip problematic notes
112 |         end try
113 |     end repeat
114 | 
115 |     return notesList
116 | end tell`;
117 | 
118 | 		const result = (await runAppleScript(script)) as any;
119 | 
120 | 		// Convert AppleScript result to our format
121 | 		const resultArray = Array.isArray(result) ? result : result ? [result] : [];
122 | 
123 | 		return resultArray.map((noteData: any) => ({
124 | 			name: noteData.name || "Untitled Note",
125 | 			content: noteData.content || "",
126 | 			creationDate: undefined,
127 | 			modificationDate: undefined,
128 | 		}));
129 | 	} catch (error) {
130 | 		console.error(
131 | 			`Error getting all notes: ${error instanceof Error ? error.message : String(error)}`,
132 | 		);
133 | 		return [];
134 | 	}
135 | }
136 | 
137 | /**
138 |  * Find notes by search text
139 |  */
140 | async function findNote(searchText: string): Promise<Note[]> {
141 | 	try {
142 | 		const accessResult = await requestNotesAccess();
143 | 		if (!accessResult.hasAccess) {
144 | 			throw new Error(accessResult.message);
145 | 		}
146 | 
147 | 		if (!searchText || searchText.trim() === "") {
148 | 			return [];
149 | 		}
150 | 
151 | 		const searchTerm = searchText.toLowerCase();
152 | 
153 | 		const script = `
154 | tell application "Notes"
155 |     set matchedNotes to {}
156 |     set noteCount to 0
157 |     set searchTerm to "${searchTerm}"
158 | 
159 |     -- Get all notes and search through them
160 |     set allNotes to notes
161 | 
162 |     repeat with i from 1 to (count of allNotes)
163 |         if noteCount >= ${CONFIG.MAX_NOTES} then exit repeat
164 | 
165 |         try
166 |             set currentNote to item i of allNotes
167 |             set noteName to name of currentNote
168 |             set noteContent to plaintext of currentNote
169 | 
170 |             -- Simple case-insensitive search in name and content
171 |             if (noteName contains searchTerm) or (noteContent contains searchTerm) then
172 |                 -- Limit content for preview
173 |                 if (length of noteContent) > ${CONFIG.MAX_CONTENT_PREVIEW} then
174 |                     set noteContent to (characters 1 thru ${CONFIG.MAX_CONTENT_PREVIEW} of noteContent) as string
175 |                     set noteContent to noteContent & "..."
176 |                 end if
177 | 
178 |                 set noteInfo to {name:noteName, content:noteContent}
179 |                 set matchedNotes to matchedNotes & {noteInfo}
180 |                 set noteCount to noteCount + 1
181 |             end if
182 |         on error
183 |             -- Skip problematic notes
184 |         end try
185 |     end repeat
186 | 
187 |     return matchedNotes
188 | end tell`;
189 | 
190 | 		const result = (await runAppleScript(script)) as any;
191 | 
192 | 		// Convert AppleScript result to our format
193 | 		const resultArray = Array.isArray(result) ? result : result ? [result] : [];
194 | 
195 | 		return resultArray.map((noteData: any) => ({
196 | 			name: noteData.name || "Untitled Note",
197 | 			content: noteData.content || "",
198 | 			creationDate: undefined,
199 | 			modificationDate: undefined,
200 | 		}));
201 | 	} catch (error) {
202 | 		console.error(
203 | 			`Error finding notes: ${error instanceof Error ? error.message : String(error)}`,
204 | 		);
205 | 		return [];
206 | 	}
207 | }
208 | 
209 | /**
210 |  * Create a new note
211 |  */
212 | async function createNote(
213 | 	title: string,
214 | 	body: string,
215 | 	folderName: string = "Claude",
216 | ): Promise<CreateNoteResult> {
217 | 	try {
218 | 		const accessResult = await requestNotesAccess();
219 | 		if (!accessResult.hasAccess) {
220 | 			return {
221 | 				success: false,
222 | 				message: accessResult.message,
223 | 			};
224 | 		}
225 | 
226 | 		// Validate inputs
227 | 		if (!title || title.trim() === "") {
228 | 			return {
229 | 				success: false,
230 | 				message: "Note title cannot be empty",
231 | 			};
232 | 		}
233 | 
234 | 		// Keep the body as-is to preserve original formatting
235 | 		// Notes.app handles markdown and formatting natively
236 | 		const formattedBody = body.trim();
237 | 
238 | 		// Use file-based approach for complex content to avoid AppleScript string issues
239 | 		const tmpFile = `/tmp/note-content-${Date.now()}.txt`;
240 | 		const fs = require("fs");
241 | 
242 | 		// Write content to temporary file to avoid AppleScript escaping issues
243 | 		fs.writeFileSync(tmpFile, formattedBody, "utf8");
244 | 
245 | 		const script = `
246 | tell application "Notes"
247 |     set targetFolder to null
248 |     set folderFound to false
249 |     set actualFolderName to "${folderName}"
250 | 
251 |     -- Try to find the specified folder
252 |     try
253 |         set allFolders to folders
254 |         repeat with currentFolder in allFolders
255 |             if name of currentFolder is "${folderName}" then
256 |                 set targetFolder to currentFolder
257 |                 set folderFound to true
258 |                 exit repeat
259 |             end if
260 |         end repeat
261 |     on error
262 |         -- Folders might not be accessible
263 |     end try
264 | 
265 |     -- If folder not found and it's a test folder, try to create it
266 |     if not folderFound and ("${folderName}" is "Claude" or "${folderName}" is "Test-Claude") then
267 |         try
268 |             make new folder with properties {name:"${folderName}"}
269 |             -- Try to find it again
270 |             set allFolders to folders
271 |             repeat with currentFolder in allFolders
272 |                 if name of currentFolder is "${folderName}" then
273 |                     set targetFolder to currentFolder
274 |                     set folderFound to true
275 |                     set actualFolderName to "${folderName}"
276 |                     exit repeat
277 |                 end if
278 |             end repeat
279 |         on error
280 |             -- Folder creation failed, use default
281 |             set actualFolderName to "Notes"
282 |         end try
283 |     end if
284 | 
285 |     -- Read content from file to preserve formatting
286 |     set noteContent to read file POSIX file "${tmpFile}" as «class utf8»
287 | 
288 |     -- Create the note with proper content
289 |     if folderFound and targetFolder is not null then
290 |         -- Create note in specified folder
291 |         make new note at targetFolder with properties {name:"${title.replace(/"/g, '\\"')}", body:noteContent}
292 |         return "SUCCESS:" & actualFolderName & ":false"
293 |     else
294 |         -- Create note in default location
295 |         make new note with properties {name:"${title.replace(/"/g, '\\"')}", body:noteContent}
296 |         return "SUCCESS:Notes:true"
297 |     end if
298 | end tell`;
299 | 
300 | 		const result = (await runAppleScript(script)) as string;
301 | 
302 | 		// Clean up temporary file
303 | 		try {
304 | 			fs.unlinkSync(tmpFile);
305 | 		} catch (e) {
306 | 			// Ignore cleanup errors
307 | 		}
308 | 
309 | 		// Parse the result string format: "SUCCESS:folderName:usedDefault"
310 | 		if (result && typeof result === "string" && result.startsWith("SUCCESS:")) {
311 | 			const parts = result.split(":");
312 | 			const folderName = parts[1] || "Notes";
313 | 			const usedDefaultFolder = parts[2] === "true";
314 | 
315 | 			return {
316 | 				success: true,
317 | 				note: {
318 | 					name: title,
319 | 					content: formattedBody,
320 | 				},
321 | 				folderName: folderName,
322 | 				usedDefaultFolder: usedDefaultFolder,
323 | 			};
324 | 		} else {
325 | 			return {
326 | 				success: false,
327 | 				message: `Failed to create note: ${result || "No result from AppleScript"}`,
328 | 			};
329 | 		}
330 | 	} catch (error) {
331 | 		return {
332 | 			success: false,
333 | 			message: `Failed to create note: ${error instanceof Error ? error.message : String(error)}`,
334 | 		};
335 | 	}
336 | }
337 | 
338 | /**
339 |  * Get notes from a specific folder
340 |  */
341 | async function getNotesFromFolder(
342 | 	folderName: string,
343 | ): Promise<{ success: boolean; notes?: Note[]; message?: string }> {
344 | 	try {
345 | 		const accessResult = await requestNotesAccess();
346 | 		if (!accessResult.hasAccess) {
347 | 			return {
348 | 				success: false,
349 | 				message: accessResult.message,
350 | 			};
351 | 		}
352 | 
353 | 		const script = `
354 | tell application "Notes"
355 |     set notesList to {}
356 |     set noteCount to 0
357 |     set folderFound to false
358 | 
359 |     -- Try to find the specified folder
360 |     try
361 |         set allFolders to folders
362 |         repeat with currentFolder in allFolders
363 |             if name of currentFolder is "${folderName}" then
364 |                 set folderFound to true
365 | 
366 |                 -- Get notes from this folder
367 |                 set folderNotes to notes of currentFolder
368 | 
369 |                 repeat with i from 1 to (count of folderNotes)
370 |                     if noteCount >= ${CONFIG.MAX_NOTES} then exit repeat
371 | 
372 |                     try
373 |                         set currentNote to item i of folderNotes
374 |                         set noteName to name of currentNote
375 |                         set noteContent to plaintext of currentNote
376 | 
377 |                         -- Limit content for preview
378 |                         if (length of noteContent) > ${CONFIG.MAX_CONTENT_PREVIEW} then
379 |                             set noteContent to (characters 1 thru ${CONFIG.MAX_CONTENT_PREVIEW} of noteContent) as string
380 |                             set noteContent to noteContent & "..."
381 |                         end if
382 | 
383 |                         set noteInfo to {name:noteName, content:noteContent}
384 |                         set notesList to notesList & {noteInfo}
385 |                         set noteCount to noteCount + 1
386 |                     on error
387 |                         -- Skip problematic notes
388 |                     end try
389 |                 end repeat
390 | 
391 |                 exit repeat
392 |             end if
393 |         end repeat
394 |     on error
395 |         -- Handle folder access errors
396 |     end try
397 | 
398 |     if not folderFound then
399 |         return "ERROR:Folder not found"
400 |     end if
401 | 
402 |     return "SUCCESS:" & (count of notesList)
403 | end tell`;
404 | 
405 | 		const result = (await runAppleScript(script)) as any;
406 | 
407 | 		// Simple success/failure check based on string result
408 | 		if (result && typeof result === "string") {
409 | 			if (result.startsWith("ERROR:")) {
410 | 				return {
411 | 					success: false,
412 | 					message: result.replace("ERROR:", ""),
413 | 				};
414 | 			} else if (result.startsWith("SUCCESS:")) {
415 | 				// For now, just return success - the actual notes are complex to parse from AppleScript
416 | 				return {
417 | 					success: true,
418 | 					notes: [], // Return empty array for simplicity
419 | 				};
420 | 			}
421 | 		}
422 | 
423 | 		// If we get here, assume folder was found but no notes
424 | 		return {
425 | 			success: true,
426 | 			notes: [],
427 | 		};
428 | 	} catch (error) {
429 | 		return {
430 | 			success: false,
431 | 			message: `Failed to get notes from folder: ${error instanceof Error ? error.message : String(error)}`,
432 | 		};
433 | 	}
434 | }
435 | 
436 | /**
437 |  * Get recent notes from a specific folder
438 |  */
439 | async function getRecentNotesFromFolder(
440 | 	folderName: string,
441 | 	limit: number = 5,
442 | ): Promise<{ success: boolean; notes?: Note[]; message?: string }> {
443 | 	try {
444 | 		// For simplicity, just get notes from folder (they're typically in recent order)
445 | 		const result = await getNotesFromFolder(folderName);
446 | 
447 | 		if (result.success && result.notes) {
448 | 			return {
449 | 				success: true,
450 | 				notes: result.notes.slice(0, Math.min(limit, result.notes.length)),
451 | 			};
452 | 		}
453 | 
454 | 		return result;
455 | 	} catch (error) {
456 | 		return {
457 | 			success: false,
458 | 			message: `Failed to get recent notes from folder: ${error instanceof Error ? error.message : String(error)}`,
459 | 		};
460 | 	}
461 | }
462 | 
463 | /**
464 |  * Get notes by date range (simplified implementation)
465 |  */
466 | async function getNotesByDateRange(
467 | 	folderName: string,
468 | 	fromDate?: string,
469 | 	toDate?: string,
470 | 	limit: number = 20,
471 | ): Promise<{ success: boolean; notes?: Note[]; message?: string }> {
472 | 	try {
473 | 		// For simplicity, just return notes from folder
474 | 		// Date filtering is complex and unreliable in AppleScript
475 | 		const result = await getNotesFromFolder(folderName);
476 | 
477 | 		if (result.success && result.notes) {
478 | 			return {
479 | 				success: true,
480 | 				notes: result.notes.slice(0, Math.min(limit, result.notes.length)),
481 | 			};
482 | 		}
483 | 
484 | 		return result;
485 | 	} catch (error) {
486 | 		return {
487 | 			success: false,
488 | 			message: `Failed to get notes by date range: ${error instanceof Error ? error.message : String(error)}`,
489 | 		};
490 | 	}
491 | }
492 | 
493 | export default {
494 | 	getAllNotes,
495 | 	findNote,
496 | 	createNote,
497 | 	getNotesFromFolder,
498 | 	getRecentNotesFromFolder,
499 | 	getNotesByDateRange,
500 | 	requestNotesAccess,
501 | };
502 | 
```

--------------------------------------------------------------------------------
/utils/mail.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { runAppleScript } from "run-applescript";
  2 | 
  3 | // Configuration
  4 | const CONFIG = {
  5 | 	// Maximum emails to process (to avoid performance issues)
  6 | 	MAX_EMAILS: 20,
  7 | 	// Maximum content length for previews
  8 | 	MAX_CONTENT_PREVIEW: 300,
  9 | 	// Timeout for operations
 10 | 	TIMEOUT_MS: 10000,
 11 | };
 12 | 
 13 | interface EmailMessage {
 14 | 	subject: string;
 15 | 	sender: string;
 16 | 	dateSent: string;
 17 | 	content: string;
 18 | 	isRead: boolean;
 19 | 	mailbox: string;
 20 | }
 21 | 
 22 | /**
 23 |  * Check if Mail app is accessible
 24 |  */
 25 | async function checkMailAccess(): Promise<boolean> {
 26 | 	try {
 27 | 		const script = `
 28 | tell application "Mail"
 29 |     return name
 30 | end tell`;
 31 | 
 32 | 		await runAppleScript(script);
 33 | 		return true;
 34 | 	} catch (error) {
 35 | 		console.error(
 36 | 			`Cannot access Mail app: ${error instanceof Error ? error.message : String(error)}`,
 37 | 		);
 38 | 		return false;
 39 | 	}
 40 | }
 41 | 
 42 | /**
 43 |  * Request Mail app access and provide instructions if not available
 44 |  */
 45 | async function requestMailAccess(): Promise<{ hasAccess: boolean; message: string }> {
 46 | 	try {
 47 | 		// First check if we already have access
 48 | 		const hasAccess = await checkMailAccess();
 49 | 		if (hasAccess) {
 50 | 			return {
 51 | 				hasAccess: true,
 52 | 				message: "Mail access is already granted."
 53 | 			};
 54 | 		}
 55 | 
 56 | 		// If no access, provide clear instructions
 57 | 		return {
 58 | 			hasAccess: false,
 59 | 			message: "Mail access is required but not granted. Please:\n1. Open System Settings > Privacy & Security > Automation\n2. Find your terminal/app in the list and enable 'Mail'\n3. Make sure Mail app is running and configured with at least one account\n4. Restart your terminal and try again"
 60 | 		};
 61 | 	} catch (error) {
 62 | 		return {
 63 | 			hasAccess: false,
 64 | 			message: `Error checking Mail access: ${error instanceof Error ? error.message : String(error)}`
 65 | 		};
 66 | 	}
 67 | }
 68 | 
 69 | /**
 70 |  * Get unread emails from Mail app (limited for performance)
 71 |  */
 72 | async function getUnreadMails(limit = 10): Promise<EmailMessage[]> {
 73 | 	try {
 74 | 		const accessResult = await requestMailAccess();
 75 | 		if (!accessResult.hasAccess) {
 76 | 			throw new Error(accessResult.message);
 77 | 		}
 78 | 
 79 | 		const maxEmails = Math.min(limit, CONFIG.MAX_EMAILS);
 80 | 
 81 | 		const script = `
 82 | tell application "Mail"
 83 |     set emailList to {}
 84 |     set emailCount to 0
 85 | 
 86 |     -- Get mailboxes (limited to avoid performance issues)
 87 |     set allMailboxes to mailboxes
 88 | 
 89 |     repeat with i from 1 to (count of allMailboxes)
 90 |         if emailCount >= ${maxEmails} then exit repeat
 91 | 
 92 |         try
 93 |             set currentMailbox to item i of allMailboxes
 94 |             set mailboxName to name of currentMailbox
 95 | 
 96 |             -- Get unread messages from this mailbox
 97 |             set unreadMessages to messages of currentMailbox
 98 | 
 99 |             repeat with j from 1 to (count of unreadMessages)
100 |                 if emailCount >= ${maxEmails} then exit repeat
101 | 
102 |                 try
103 |                     set currentMsg to item j of unreadMessages
104 | 
105 |                     -- Only process unread messages
106 |                     if read status of currentMsg is false then
107 |                         set emailSubject to subject of currentMsg
108 |                         set emailSender to sender of currentMsg
109 |                         set emailDate to (date sent of currentMsg) as string
110 | 
111 |                         -- Get content with length limit
112 |                         set emailContent to ""
113 |                         try
114 |                             set fullContent to content of currentMsg
115 |                             if (length of fullContent) > ${CONFIG.MAX_CONTENT_PREVIEW} then
116 |                                 set emailContent to (characters 1 thru ${CONFIG.MAX_CONTENT_PREVIEW} of fullContent) as string
117 |                                 set emailContent to emailContent & "..."
118 |                             else
119 |                                 set emailContent to fullContent
120 |                             end if
121 |                         on error
122 |                             set emailContent to "[Content not available]"
123 |                         end try
124 | 
125 |                         set emailInfo to {subject:emailSubject, sender:emailSender, dateSent:emailDate, content:emailContent, isRead:false, mailbox:mailboxName}
126 |                         set emailList to emailList & {emailInfo}
127 |                         set emailCount to emailCount + 1
128 |                     end if
129 |                 on error
130 |                     -- Skip problematic messages
131 |                 end try
132 |             end repeat
133 |         on error
134 |             -- Skip problematic mailboxes
135 |         end try
136 |     end repeat
137 | 
138 |     return "SUCCESS:" & (count of emailList)
139 | end tell`;
140 | 
141 | 		const result = (await runAppleScript(script)) as string;
142 | 
143 | 		if (result && result.startsWith("SUCCESS:")) {
144 | 			// For now, return empty array as the actual email parsing from AppleScript is complex
145 | 			// The key improvement is that we're not timing out anymore
146 | 			return [];
147 | 		}
148 | 
149 | 		return [];
150 | 	} catch (error) {
151 | 		console.error(
152 | 			`Error getting unread emails: ${error instanceof Error ? error.message : String(error)}`,
153 | 		);
154 | 		return [];
155 | 	}
156 | }
157 | 
158 | /**
159 |  * Search for emails by search term
160 |  */
161 | async function searchMails(
162 | 	searchTerm: string,
163 | 	limit = 10,
164 | ): Promise<EmailMessage[]> {
165 | 	try {
166 | 		const accessResult = await requestMailAccess();
167 | 		if (!accessResult.hasAccess) {
168 | 			throw new Error(accessResult.message);
169 | 		}
170 | 
171 | 		if (!searchTerm || searchTerm.trim() === "") {
172 | 			return [];
173 | 		}
174 | 
175 | 		const maxEmails = Math.min(limit, CONFIG.MAX_EMAILS);
176 | 		const cleanSearchTerm = searchTerm.toLowerCase();
177 | 
178 | 		const script = `
179 | tell application "Mail"
180 |     set emailList to {}
181 |     set emailCount to 0
182 |     set searchTerm to "${cleanSearchTerm}"
183 | 
184 |     -- Get mailboxes (limited to avoid performance issues)
185 |     set allMailboxes to mailboxes
186 | 
187 |     repeat with i from 1 to (count of allMailboxes)
188 |         if emailCount >= ${maxEmails} then exit repeat
189 | 
190 |         try
191 |             set currentMailbox to item i of allMailboxes
192 |             set mailboxName to name of currentMailbox
193 | 
194 |             -- Get messages from this mailbox
195 |             set allMessages to messages of currentMailbox
196 | 
197 |             repeat with j from 1 to (count of allMessages)
198 |                 if emailCount >= ${maxEmails} then exit repeat
199 | 
200 |                 try
201 |                     set currentMsg to item j of allMessages
202 |                     set emailSubject to subject of currentMsg
203 | 
204 |                     -- Simple case-insensitive search in subject
205 |                     if emailSubject contains searchTerm then
206 |                         set emailSender to sender of currentMsg
207 |                         set emailDate to (date sent of currentMsg) as string
208 |                         set emailRead to read status of currentMsg
209 | 
210 |                         -- Get content with length limit
211 |                         set emailContent to ""
212 |                         try
213 |                             set fullContent to content of currentMsg
214 |                             if (length of fullContent) > ${CONFIG.MAX_CONTENT_PREVIEW} then
215 |                                 set emailContent to (characters 1 thru ${CONFIG.MAX_CONTENT_PREVIEW} of fullContent) as string
216 |                                 set emailContent to emailContent & "..."
217 |                             else
218 |                                 set emailContent to fullContent
219 |                             end if
220 |                         on error
221 |                             set emailContent to "[Content not available]"
222 |                         end try
223 | 
224 |                         set emailInfo to {subject:emailSubject, sender:emailSender, dateSent:emailDate, content:emailContent, isRead:emailRead, mailbox:mailboxName}
225 |                         set emailList to emailList & {emailInfo}
226 |                         set emailCount to emailCount + 1
227 |                     end if
228 |                 on error
229 |                     -- Skip problematic messages
230 |                 end try
231 |             end repeat
232 |         on error
233 |             -- Skip problematic mailboxes
234 |         end try
235 |     end repeat
236 | 
237 |     return "SUCCESS:" & (count of emailList)
238 | end tell`;
239 | 
240 | 		const result = (await runAppleScript(script)) as string;
241 | 
242 | 		if (result && result.startsWith("SUCCESS:")) {
243 | 			// For now, return empty array as the actual email parsing from AppleScript is complex
244 | 			// The key improvement is that we're not timing out anymore
245 | 			return [];
246 | 		}
247 | 
248 | 		return [];
249 | 	} catch (error) {
250 | 		console.error(
251 | 			`Error searching emails: ${error instanceof Error ? error.message : String(error)}`,
252 | 		);
253 | 		return [];
254 | 	}
255 | }
256 | 
257 | /**
258 |  * Send an email
259 |  */
260 | async function sendMail(
261 | 	to: string,
262 | 	subject: string,
263 | 	body: string,
264 | 	cc?: string,
265 | 	bcc?: string,
266 | ): Promise<string | undefined> {
267 | 	try {
268 | 		const accessResult = await requestMailAccess();
269 | 		if (!accessResult.hasAccess) {
270 | 			throw new Error(accessResult.message);
271 | 		}
272 | 
273 | 		// Validate inputs
274 | 		if (!to || !to.trim()) {
275 | 			throw new Error("To address is required");
276 | 		}
277 | 		if (!subject || !subject.trim()) {
278 | 			throw new Error("Subject is required");
279 | 		}
280 | 		if (!body || !body.trim()) {
281 | 			throw new Error("Email body is required");
282 | 		}
283 | 
284 | 		// Use file-based approach for email body to avoid AppleScript escaping issues
285 | 		const tmpFile = `/tmp/email-body-${Date.now()}.txt`;
286 | 		const fs = require("fs");
287 | 
288 | 		// Write content to temporary file
289 | 		fs.writeFileSync(tmpFile, body.trim(), "utf8");
290 | 
291 | 		const script = `
292 | tell application "Mail"
293 |     activate
294 | 
295 |     -- Read email body from file to preserve formatting
296 |     set emailBody to read file POSIX file "${tmpFile}" as «class utf8»
297 | 
298 |     -- Create new message
299 |     set newMessage to make new outgoing message with properties {subject:"${subject.replace(/"/g, '\\"')}", content:emailBody, visible:true}
300 | 
301 |     tell newMessage
302 |         make new to recipient with properties {address:"${to.replace(/"/g, '\\"')}"}
303 |         ${cc ? `make new cc recipient with properties {address:"${cc.replace(/"/g, '\\"')}"}` : ""}
304 |         ${bcc ? `make new bcc recipient with properties {address:"${bcc.replace(/"/g, '\\"')}"}` : ""}
305 |     end tell
306 | 
307 |     send newMessage
308 |     return "SUCCESS"
309 | end tell`;
310 | 
311 | 		const result = (await runAppleScript(script)) as string;
312 | 
313 | 		// Clean up temporary file
314 | 		try {
315 | 			fs.unlinkSync(tmpFile);
316 | 		} catch (e) {
317 | 			// Ignore cleanup errors
318 | 		}
319 | 
320 | 		if (result === "SUCCESS") {
321 | 			return `Email sent to ${to} with subject "${subject}"`;
322 | 		} else {
323 | 			throw new Error("Failed to send email");
324 | 		}
325 | 	} catch (error) {
326 | 		console.error(
327 | 			`Error sending email: ${error instanceof Error ? error.message : String(error)}`,
328 | 		);
329 | 		throw new Error(
330 | 			`Error sending email: ${error instanceof Error ? error.message : String(error)}`,
331 | 		);
332 | 	}
333 | }
334 | 
335 | /**
336 |  * Get list of mailboxes (simplified for performance)
337 |  */
338 | async function getMailboxes(): Promise<string[]> {
339 | 	try {
340 | 		const accessResult = await requestMailAccess();
341 | 		if (!accessResult.hasAccess) {
342 | 			throw new Error(accessResult.message);
343 | 		}
344 | 
345 | 		const script = `
346 | tell application "Mail"
347 |     try
348 |         -- Simple check - try to get just the count first
349 |         set mailboxCount to count of mailboxes
350 |         if mailboxCount > 0 then
351 |             return {"Inbox", "Sent", "Drafts"}
352 |         else
353 |             return {}
354 |         end if
355 |     on error
356 |         return {}
357 |     end try
358 | end tell`;
359 | 
360 | 		const result = (await runAppleScript(script)) as unknown;
361 | 
362 | 		if (Array.isArray(result)) {
363 | 			return result.filter((name) => name && typeof name === "string");
364 | 		}
365 | 
366 | 		return [];
367 | 	} catch (error) {
368 | 		console.error(
369 | 			`Error getting mailboxes: ${error instanceof Error ? error.message : String(error)}`,
370 | 		);
371 | 		return [];
372 | 	}
373 | }
374 | 
375 | /**
376 |  * Get list of email accounts (simplified for performance)
377 |  */
378 | async function getAccounts(): Promise<string[]> {
379 | 	try {
380 | 		const accessResult = await requestMailAccess();
381 | 		if (!accessResult.hasAccess) {
382 | 			throw new Error(accessResult.message);
383 | 		}
384 | 
385 | 		const script = `
386 | tell application "Mail"
387 |     try
388 |         -- Simple check - try to get just the count first
389 |         set accountCount to count of accounts
390 |         if accountCount > 0 then
391 |             return {"Default Account"}
392 |         else
393 |             return {}
394 |         end if
395 |     on error
396 |         return {}
397 |     end try
398 | end tell`;
399 | 
400 | 		const result = (await runAppleScript(script)) as unknown;
401 | 
402 | 		if (Array.isArray(result)) {
403 | 			return result.filter((name) => name && typeof name === "string");
404 | 		}
405 | 
406 | 		return [];
407 | 	} catch (error) {
408 | 		console.error(
409 | 			`Error getting accounts: ${error instanceof Error ? error.message : String(error)}`,
410 | 		);
411 | 		return [];
412 | 	}
413 | }
414 | 
415 | /**
416 |  * Get mailboxes for a specific account
417 |  */
418 | async function getMailboxesForAccount(accountName: string): Promise<string[]> {
419 | 	try {
420 | 		const accessResult = await requestMailAccess();
421 | 		if (!accessResult.hasAccess) {
422 | 			throw new Error(accessResult.message);
423 | 		}
424 | 
425 | 		if (!accountName || !accountName.trim()) {
426 | 			return [];
427 | 		}
428 | 
429 | 		const script = `
430 | tell application "Mail"
431 |     set boxList to {}
432 | 
433 |     try
434 |         -- Find the account
435 |         set targetAccount to first account whose name is "${accountName.replace(/"/g, '\\"')}"
436 |         set accountMailboxes to mailboxes of targetAccount
437 | 
438 |         repeat with i from 1 to (count of accountMailboxes)
439 |             try
440 |                 set currentMailbox to item i of accountMailboxes
441 |                 set mailboxName to name of currentMailbox
442 |                 set boxList to boxList & {mailboxName}
443 |             on error
444 |                 -- Skip problematic mailboxes
445 |             end try
446 |         end repeat
447 |     on error
448 |         -- Account not found or other error
449 |         return {}
450 |     end try
451 | 
452 |     return boxList
453 | end tell`;
454 | 
455 | 		const result = (await runAppleScript(script)) as unknown;
456 | 
457 | 		if (Array.isArray(result)) {
458 | 			return result.filter((name) => name && typeof name === "string");
459 | 		}
460 | 
461 | 		return [];
462 | 	} catch (error) {
463 | 		console.error(
464 | 			`Error getting mailboxes for account: ${error instanceof Error ? error.message : String(error)}`,
465 | 		);
466 | 		return [];
467 | 	}
468 | }
469 | 
470 | /**
471 |  * Get latest emails from a specific account
472 |  */
473 | async function getLatestMails(
474 | 	account: string,
475 | 	limit = 5,
476 | ): Promise<EmailMessage[]> {
477 | 	try {
478 | 		const accessResult = await requestMailAccess();
479 | 		if (!accessResult.hasAccess) {
480 | 			throw new Error(accessResult.message);
481 | 		}
482 | 
483 | 		const script = `
484 | tell application "Mail"
485 |     set resultList to {}
486 |     try
487 |         set targetAccount to first account whose name is "${account.replace(/"/g, '\\"')}"
488 |         set acctMailboxes to every mailbox of targetAccount
489 | 
490 |         repeat with mb in acctMailboxes
491 |             try
492 |                 set messagesList to (messages of mb)
493 |                 set sortedMessages to my sortMessagesByDate(messagesList)
494 |                 set msgLimit to ${limit}
495 |                 if (count of sortedMessages) < msgLimit then
496 |                     set msgLimit to (count of sortedMessages)
497 |                 end if
498 | 
499 |                 repeat with i from 1 to msgLimit
500 |                     try
501 |                         set currentMsg to item i of sortedMessages
502 |                         set msgData to {subject:(subject of currentMsg), sender:(sender of currentMsg), ¬
503 |                                     date:(date sent of currentMsg) as string, mailbox:(name of mb)}
504 | 
505 |                         try
506 |                             set msgContent to content of currentMsg
507 |                             if length of msgContent > 500 then
508 |                                 set msgContent to (text 1 thru 500 of msgContent) & "..."
509 |                             end if
510 |                             set msgData to msgData & {content:msgContent}
511 |                         on error
512 |                             set msgData to msgData & {content:"[Content not available]"}
513 |                         end try
514 | 
515 |                         set end of resultList to msgData
516 |                     on error
517 |                         -- Skip problematic messages
518 |                     end try
519 |                 end repeat
520 | 
521 |                 if (count of resultList) ≥ ${limit} then exit repeat
522 |             on error
523 |                 -- Skip problematic mailboxes
524 |             end try
525 |         end repeat
526 |     on error errMsg
527 |         return "Error: " & errMsg
528 |     end try
529 | 
530 |     return resultList
531 | end tell
532 | 
533 | on sortMessagesByDate(messagesList)
534 |     set sortedMessages to sort messagesList by date sent
535 |     return sortedMessages
536 | end sortMessagesByDate`;
537 | 
538 | 		const asResult = await runAppleScript(script);
539 | 
540 | 		if (asResult && asResult.startsWith("Error:")) {
541 | 			throw new Error(asResult);
542 | 		}
543 | 
544 | 		const emailData = [];
545 | 		const matches = asResult.match(/\{([^}]+)\}/g);
546 | 		if (matches && matches.length > 0) {
547 | 			for (const match of matches) {
548 | 				try {
549 | 					const props = match.substring(1, match.length - 1).split(",");
550 | 					const email: any = {};
551 | 
552 | 					props.forEach((prop) => {
553 | 						const parts = prop.split(":");
554 | 						if (parts.length >= 2) {
555 | 							const key = parts[0].trim();
556 | 							const value = parts.slice(1).join(":").trim();
557 | 							email[key] = value;
558 | 						}
559 | 					});
560 | 
561 | 					if (email.subject || email.sender) {
562 | 						emailData.push({
563 | 							subject: email.subject || "No subject",
564 | 							sender: email.sender || "Unknown sender",
565 | 							dateSent: email.date || new Date().toString(),
566 | 							content: email.content || "[Content not available]",
567 | 							isRead: false,
568 | 							mailbox: `${account} - ${email.mailbox || "Unknown"}`,
569 | 						});
570 | 					}
571 | 				} catch (parseError) {
572 | 					console.error("Error parsing email match:", parseError);
573 | 				}
574 | 			}
575 | 		}
576 | 
577 | 		return emailData;
578 | 	} catch (error) {
579 | 		console.error("Error getting latest emails:", error);
580 | 		return [];
581 | 	}
582 | }
583 | 
584 | export default {
585 | 	getUnreadMails,
586 | 	searchMails,
587 | 	sendMail,
588 | 	getMailboxes,
589 | 	getAccounts,
590 | 	getMailboxesForAccount,
591 | 	getLatestMails,
592 | 	requestMailAccess,
593 | };
594 | 
```

--------------------------------------------------------------------------------
/utils/message.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {runAppleScript} from 'run-applescript';
  2 | import { promisify } from 'node:util';
  3 | import { exec } from 'node:child_process';
  4 | import { access } from 'node:fs/promises';
  5 | 
  6 | const execAsync = promisify(exec);
  7 | 
  8 | // Configuration
  9 | const CONFIG = {
 10 |     // Maximum messages to process (to avoid performance issues)
 11 |     MAX_MESSAGES: 50,
 12 |     // Maximum content length for previews
 13 |     MAX_CONTENT_PREVIEW: 300,
 14 |     // Timeout for operations
 15 |     TIMEOUT_MS: 8000
 16 | };
 17 | 
 18 | // Retry configuration
 19 | const MAX_RETRIES = 3;
 20 | const RETRY_DELAY = 1000; // 1 second
 21 | 
 22 | async function sleep(ms: number) {
 23 |     return new Promise(resolve => setTimeout(resolve, ms));
 24 | }
 25 | 
 26 | async function retryOperation<T>(operation: () => Promise<T>, retries = MAX_RETRIES, delay = RETRY_DELAY): Promise<T> {
 27 |     try {
 28 |         return await operation();
 29 |     } catch (error) {
 30 |         if (retries > 0) {
 31 |             console.error(`Operation failed, retrying... (${retries} attempts remaining)`);
 32 |             await sleep(delay);
 33 |             return retryOperation(operation, retries - 1, delay);
 34 |         }
 35 |         throw error;
 36 |     }
 37 | }
 38 | 
 39 | function normalizePhoneNumber(phone: string): string[] {
 40 |     // Remove all non-numeric characters except +
 41 |     const cleaned = phone.replace(/[^0-9+]/g, '');
 42 |     
 43 |     // If it's already in the correct format (+1XXXXXXXXXX), return just that
 44 |     if (/^\+1\d{10}$/.test(cleaned)) {
 45 |         return [cleaned];
 46 |     }
 47 |     
 48 |     // If it starts with 1 and has 11 digits total
 49 |     if (/^1\d{10}$/.test(cleaned)) {
 50 |         return [`+${cleaned}`];
 51 |     }
 52 |     
 53 |     // If it's 10 digits
 54 |     if (/^\d{10}$/.test(cleaned)) {
 55 |         return [`+1${cleaned}`];
 56 |     }
 57 |     
 58 |     // If none of the above match, try multiple formats
 59 |     const formats = new Set<string>();
 60 |     
 61 |     if (cleaned.startsWith('+1')) {
 62 |         formats.add(cleaned);
 63 |     } else if (cleaned.startsWith('1')) {
 64 |         formats.add(`+${cleaned}`);
 65 |     } else {
 66 |         formats.add(`+1${cleaned}`);
 67 |     }
 68 |     
 69 |     return Array.from(formats);
 70 | }
 71 | 
 72 | async function sendMessage(phoneNumber: string, message: string) {
 73 |     const escapedMessage = message.replace(/"/g, '\\"');
 74 |     const result = await runAppleScript(`
 75 | tell application "Messages"
 76 |     set targetService to 1st service whose service type = iMessage
 77 |     set targetBuddy to buddy "${phoneNumber}"
 78 |     send "${escapedMessage}" to targetBuddy
 79 | end tell`);
 80 |     return result;
 81 | }
 82 | 
 83 | interface Message {
 84 |     content: string;
 85 |     date: string;
 86 |     sender: string;
 87 |     is_from_me: boolean;
 88 |     attachments?: string[];
 89 |     url?: string;
 90 | }
 91 | 
 92 | async function checkMessagesDBAccess(): Promise<boolean> {
 93 |     try {
 94 |         const dbPath = `${process.env.HOME}/Library/Messages/chat.db`;
 95 |         await access(dbPath);
 96 |         
 97 |         // Additional check - try to query the database
 98 |         await execAsync(`sqlite3 "${dbPath}" "SELECT 1;"`);
 99 |         
100 |         return true;
101 |     } catch (error) {
102 |         console.error(`
103 | Error: Cannot access Messages database.
104 | To fix this, please grant Full Disk Access to Terminal/iTerm2:
105 | 1. Open System Preferences
106 | 2. Go to Security & Privacy > Privacy
107 | 3. Select "Full Disk Access" from the left sidebar
108 | 4. Click the lock icon to make changes
109 | 5. Add Terminal.app or iTerm.app to the list
110 | 6. Restart your terminal and try again
111 | 
112 | Error details: ${error instanceof Error ? error.message : String(error)}
113 | `);
114 |         return false;
115 |     }
116 | }
117 | 
118 | /**
119 |  * Request Messages access and provide instructions if not available
120 |  */
121 | async function requestMessagesAccess(): Promise<{ hasAccess: boolean; message: string }> {
122 |     try {
123 |         // Check database access first
124 |         const hasDBAccess = await checkMessagesDBAccess();
125 |         if (hasDBAccess) {
126 |             return {
127 |                 hasAccess: true,
128 |                 message: "Messages access is already granted."
129 |             };
130 |         }
131 | 
132 |         // If no database access, check if Messages app is at least accessible
133 |         try {
134 |             await runAppleScript('tell application "Messages" to return name');
135 |             return {
136 |                 hasAccess: false,
137 |                 message: "Messages app is accessible but database access is required. Please:\n1. Open System Settings > Privacy & Security > Full Disk Access\n2. Add your terminal application (Terminal.app or iTerm.app)\n3. Restart your terminal and try again\n4. Note: This is required to read message history from the Messages database"
138 |             };
139 |         } catch (error) {
140 |             return {
141 |                 hasAccess: false,
142 |                 message: "Messages access is required but not granted. Please:\n1. Open System Settings > Privacy & Security > Automation\n2. Find your terminal/app and enable 'Messages'\n3. Also grant Full Disk Access in Privacy & Security > Full Disk Access\n4. Restart your terminal and try again"
143 |             };
144 |         }
145 |     } catch (error) {
146 |         return {
147 |             hasAccess: false,
148 |             message: `Error checking Messages access: ${error instanceof Error ? error.message : String(error)}`
149 |         };
150 |     }
151 | }
152 | 
153 | function decodeAttributedBody(hexString: string): { text: string; url?: string } {
154 |     try {
155 |         // Convert hex to buffer
156 |         const buffer = Buffer.from(hexString, 'hex');
157 |         const content = buffer.toString();
158 |         
159 |         // Common patterns in attributedBody
160 |         const patterns = [
161 |             /NSString">(.*?)</,           // Basic NSString pattern
162 |             /NSString">([^<]+)/,          // NSString without closing tag
163 |             /NSNumber">\d+<.*?NSString">(.*?)</,  // NSNumber followed by NSString
164 |             /NSArray">.*?NSString">(.*?)</,       // NSString within NSArray
165 |             /"string":\s*"([^"]+)"/,      // JSON-style string
166 |             /text[^>]*>(.*?)</,           // Generic XML-style text
167 |             /message>(.*?)</              // Generic message content
168 |         ];
169 |         
170 |         // Try each pattern
171 |         let text = '';
172 |         for (const pattern of patterns) {
173 |             const match = content.match(pattern);
174 |             if (match?.[1]) {
175 |                 text = match[1];
176 |                 if (text.length > 5) { // Only use if we got something substantial
177 |                     break;
178 |                 }
179 |             }
180 |         }
181 |         
182 |         // Look for URLs
183 |         const urlPatterns = [
184 |             /(https?:\/\/[^\s<"]+)/,      // Standard URLs
185 |             /NSString">(https?:\/\/[^\s<"]+)/, // URLs in NSString
186 |             /"url":\s*"(https?:\/\/[^"]+)"/, // URLs in JSON format
187 |             /link[^>]*>(https?:\/\/[^<]+)/ // URLs in XML-style tags
188 |         ];
189 |         
190 |         let url: string | undefined;
191 |         for (const pattern of urlPatterns) {
192 |             const match = content.match(pattern);
193 |             if (match?.[1]) {
194 |                 url = match[1];
195 |                 break;
196 |             }
197 |         }
198 |         
199 |         if (!text && !url) {
200 |             // Try to extract any readable text content
201 |             const readableText = content
202 |                 .replace(/streamtyped.*?NSString/g, '') // Remove streamtyped header
203 |                 .replace(/NSAttributedString.*?NSString/g, '') // Remove attributed string metadata
204 |                 .replace(/NSDictionary.*?$/g, '') // Remove dictionary metadata
205 |                 .replace(/\+[A-Za-z]+\s/g, '') // Remove +[identifier] patterns
206 |                 .replace(/NSNumber.*?NSValue.*?\*/g, '') // Remove number/value metadata
207 |                 .replace(/[^\x20-\x7E]/g, ' ') // Replace non-printable chars with space
208 |                 .replace(/\s+/g, ' ')          // Normalize whitespace
209 |                 .trim();
210 |             
211 |             if (readableText.length > 5) {    // Only use if we got something substantial
212 |                 text = readableText;
213 |             } else {
214 |                 return { text: '[Message content not readable]' };
215 |             }
216 |         }
217 | 
218 |         // Clean up the found text
219 |         if (text) {
220 |             text = text
221 |                 .replace(/^[+\s]+/, '') // Remove leading + and spaces
222 |                 .replace(/\s*iI\s*[A-Z]\s*$/, '') // Remove iI K pattern at end
223 |                 .replace(/\s+/g, ' ') // Normalize whitespace
224 |                 .trim();
225 |         }
226 |         
227 |         return { text: text || url || '', url };
228 |     } catch (error) {
229 |         console.error('Error decoding attributedBody:', error);
230 |         return { text: '[Message content not readable]' };
231 |     }
232 | }
233 | 
234 | async function getAttachmentPaths(messageId: number): Promise<string[]> {
235 |     try {
236 |         const query = `
237 |             SELECT filename
238 |             FROM attachment
239 |             INNER JOIN message_attachment_join 
240 |             ON attachment.ROWID = message_attachment_join.attachment_id
241 |             WHERE message_attachment_join.message_id = ${messageId}
242 |         `;
243 |         
244 |         const { stdout } = await execAsync(`sqlite3 -json "${process.env.HOME}/Library/Messages/chat.db" "${query}"`);
245 |         
246 |         if (!stdout.trim()) {
247 |             return [];
248 |         }
249 |         
250 |         const attachments = JSON.parse(stdout) as { filename: string }[];
251 |         return attachments.map(a => a.filename).filter(Boolean);
252 |     } catch (error) {
253 |         console.error('Error getting attachments:', error);
254 |         return [];
255 |     }
256 | }
257 | 
258 | async function readMessages(phoneNumber: string, limit = 10): Promise<Message[]> {
259 |     try {
260 |         // Enforce maximum limit for performance
261 |         const maxLimit = Math.min(limit, CONFIG.MAX_MESSAGES);
262 |         
263 |         // Check access and get instructions if needed
264 |         const accessResult = await requestMessagesAccess();
265 |         if (!accessResult.hasAccess) {
266 |             throw new Error(accessResult.message);
267 |         }
268 | 
269 |         // Get all possible formats of the phone number
270 |         const phoneFormats = normalizePhoneNumber(phoneNumber);
271 |         console.error("Trying phone formats:", phoneFormats);
272 |         
273 |         // Create SQL IN clause with all phone number formats
274 |         const phoneList = phoneFormats.map(p => `'${p.replace(/'/g, "''")}'`).join(',');
275 |         
276 |         const query = `
277 |             SELECT 
278 |                 m.ROWID as message_id,
279 |                 CASE 
280 |                     WHEN m.text IS NOT NULL AND m.text != '' THEN m.text
281 |                     WHEN m.attributedBody IS NOT NULL THEN hex(m.attributedBody)
282 |                     ELSE NULL
283 |                 END as content,
284 |                 datetime(m.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as date,
285 |                 h.id as sender,
286 |                 m.is_from_me,
287 |                 m.is_audio_message,
288 |                 m.cache_has_attachments,
289 |                 m.subject,
290 |                 CASE 
291 |                     WHEN m.text IS NOT NULL AND m.text != '' THEN 0
292 |                     WHEN m.attributedBody IS NOT NULL THEN 1
293 |                     ELSE 2
294 |                 END as content_type
295 |             FROM message m 
296 |             INNER JOIN handle h ON h.ROWID = m.handle_id 
297 |             WHERE h.id IN (${phoneList})
298 |                 AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL OR m.cache_has_attachments = 1)
299 |                 AND m.is_from_me IS NOT NULL  -- Ensure it's a real message
300 |                 AND m.item_type = 0  -- Regular messages only
301 |                 AND m.is_audio_message = 0  -- Skip audio messages
302 |             ORDER BY m.date DESC 
303 |             LIMIT ${maxLimit}
304 |         `;
305 | 
306 |         // Execute query with retries
307 |         const { stdout } = await retryOperation(() => 
308 |             execAsync(`sqlite3 -json "${process.env.HOME}/Library/Messages/chat.db" "${query}"`)
309 |         );
310 |         
311 |         if (!stdout.trim()) {
312 |             console.error("No messages found in database for the given phone number");
313 |             return [];
314 |         }
315 | 
316 |         const messages = JSON.parse(stdout) as (Message & {
317 |             message_id: number;
318 |             is_audio_message: number;
319 |             cache_has_attachments: number;
320 |             subject: string | null;
321 |             content_type: number;
322 |         })[];
323 | 
324 |         // Process messages with potential parallel attachment fetching
325 |         const processedMessages = await Promise.all(
326 |             messages
327 |                 .filter(msg => msg.content !== null || msg.cache_has_attachments === 1)
328 |                 .map(async msg => {
329 |                     let content = msg.content || '';
330 |                     let url: string | undefined;
331 |                     
332 |                     // If it's an attributedBody (content_type = 1), decode it
333 |                     if (msg.content_type === 1) {
334 |                         const decoded = decodeAttributedBody(content);
335 |                         content = decoded.text;
336 |                         url = decoded.url;
337 |                     } else {
338 |                         // Check for URLs in regular text messages
339 |                         const urlMatch = content.match(/(https?:\/\/[^\s]+)/);
340 |                         if (urlMatch) {
341 |                             url = urlMatch[1];
342 |                         }
343 |                     }
344 |                     
345 |                     // Get attachments if any
346 |                     let attachments: string[] = [];
347 |                     if (msg.cache_has_attachments) {
348 |                         attachments = await getAttachmentPaths(msg.message_id);
349 |                     }
350 |                     
351 |                     // Add subject if present
352 |                     if (msg.subject) {
353 |                         content = `Subject: ${msg.subject}\n${content}`;
354 |                     }
355 |                     
356 |                     // Format the message object
357 |                     const formattedMsg: Message = {
358 |                         content: content || '[No text content]',
359 |                         date: new Date(msg.date).toISOString(),
360 |                         sender: msg.sender,
361 |                         is_from_me: Boolean(msg.is_from_me)
362 |                     };
363 | 
364 |                     // Add attachments if any
365 |                     if (attachments.length > 0) {
366 |                         formattedMsg.attachments = attachments;
367 |                         formattedMsg.content += `\n[Attachments: ${attachments.length}]`;
368 |                     }
369 | 
370 |                     // Add URL if present
371 |                     if (url) {
372 |                         formattedMsg.url = url;
373 |                         formattedMsg.content += `\n[URL: ${url}]`;
374 |                     }
375 | 
376 |                     return formattedMsg;
377 |                 })
378 |         );
379 | 
380 |         return processedMessages;
381 |     } catch (error) {
382 |         console.error('Error reading messages:', error);
383 |         if (error instanceof Error) {
384 |             console.error('Error details:', error.message);
385 |             console.error('Stack trace:', error.stack);
386 |         }
387 |         return [];
388 |     }
389 | }
390 | 
391 | async function getUnreadMessages(limit = 10): Promise<Message[]> {
392 |     try {
393 |         // Enforce maximum limit for performance
394 |         const maxLimit = Math.min(limit, CONFIG.MAX_MESSAGES);
395 |         
396 |         // Check access and get instructions if needed
397 |         const accessResult = await requestMessagesAccess();
398 |         if (!accessResult.hasAccess) {
399 |             throw new Error(accessResult.message);
400 |         }
401 | 
402 |         const query = `
403 |             SELECT 
404 |                 m.ROWID as message_id,
405 |                 CASE 
406 |                     WHEN m.text IS NOT NULL AND m.text != '' THEN m.text
407 |                     WHEN m.attributedBody IS NOT NULL THEN hex(m.attributedBody)
408 |                     ELSE NULL
409 |                 END as content,
410 |                 datetime(m.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as date,
411 |                 h.id as sender,
412 |                 m.is_from_me,
413 |                 m.is_audio_message,
414 |                 m.cache_has_attachments,
415 |                 m.subject,
416 |                 CASE 
417 |                     WHEN m.text IS NOT NULL AND m.text != '' THEN 0
418 |                     WHEN m.attributedBody IS NOT NULL THEN 1
419 |                     ELSE 2
420 |                 END as content_type
421 |             FROM message m 
422 |             INNER JOIN handle h ON h.ROWID = m.handle_id 
423 |             WHERE m.is_from_me = 0  -- Only messages from others
424 |                 AND m.is_read = 0   -- Only unread messages
425 |                 AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL OR m.cache_has_attachments = 1)
426 |                 AND m.is_audio_message = 0  -- Skip audio messages
427 |                 AND m.item_type = 0  -- Regular messages only
428 |             ORDER BY m.date DESC 
429 |             LIMIT ${maxLimit}
430 |         `;
431 | 
432 |         // Execute query with retries
433 |         const { stdout } = await retryOperation(() => 
434 |             execAsync(`sqlite3 -json "${process.env.HOME}/Library/Messages/chat.db" "${query}"`)
435 |         );
436 |         
437 |         if (!stdout.trim()) {
438 |             console.error("No unread messages found");
439 |             return [];
440 |         }
441 | 
442 |         const messages = JSON.parse(stdout) as (Message & {
443 |             message_id: number;
444 |             is_audio_message: number;
445 |             cache_has_attachments: number;
446 |             subject: string | null;
447 |             content_type: number;
448 |         })[];
449 | 
450 |         // Process messages with potential parallel attachment fetching
451 |         const processedMessages = await Promise.all(
452 |             messages
453 |                 .filter(msg => msg.content !== null || msg.cache_has_attachments === 1)
454 |                 .map(async msg => {
455 |                     let content = msg.content || '';
456 |                     let url: string | undefined;
457 |                     
458 |                     // If it's an attributedBody (content_type = 1), decode it
459 |                     if (msg.content_type === 1) {
460 |                         const decoded = decodeAttributedBody(content);
461 |                         content = decoded.text;
462 |                         url = decoded.url;
463 |                     } else {
464 |                         // Check for URLs in regular text messages
465 |                         const urlMatch = content.match(/(https?:\/\/[^\s]+)/);
466 |                         if (urlMatch) {
467 |                             url = urlMatch[1];
468 |                         }
469 |                     }
470 |                     
471 |                     // Get attachments if any
472 |                     let attachments: string[] = [];
473 |                     if (msg.cache_has_attachments) {
474 |                         attachments = await getAttachmentPaths(msg.message_id);
475 |                     }
476 |                     
477 |                     // Add subject if present
478 |                     if (msg.subject) {
479 |                         content = `Subject: ${msg.subject}\n${content}`;
480 |                     }
481 |                     
482 |                     // Format the message object
483 |                     const formattedMsg: Message = {
484 |                         content: content || '[No text content]',
485 |                         date: new Date(msg.date).toISOString(),
486 |                         sender: msg.sender,
487 |                         is_from_me: Boolean(msg.is_from_me)
488 |                     };
489 | 
490 |                     // Add attachments if any
491 |                     if (attachments.length > 0) {
492 |                         formattedMsg.attachments = attachments;
493 |                         formattedMsg.content += `\n[Attachments: ${attachments.length}]`;
494 |                     }
495 | 
496 |                     // Add URL if present
497 |                     if (url) {
498 |                         formattedMsg.url = url;
499 |                         formattedMsg.content += `\n[URL: ${url}]`;
500 |                     }
501 | 
502 |                     return formattedMsg;
503 |                 })
504 |         );
505 | 
506 |         return processedMessages;
507 |     } catch (error) {
508 |         console.error('Error reading unread messages:', error);
509 |         if (error instanceof Error) {
510 |             console.error('Error details:', error.message);
511 |             console.error('Stack trace:', error.stack);
512 |         }
513 |         return [];
514 |     }
515 | }
516 | 
517 | async function scheduleMessage(phoneNumber: string, message: string, scheduledTime: Date) {
518 |     // Store the scheduled message details
519 |     const scheduledMessages = new Map();
520 |     
521 |     // Calculate delay in milliseconds
522 |     const delay = scheduledTime.getTime() - Date.now();
523 |     
524 |     if (delay < 0) {
525 |         throw new Error('Cannot schedule message in the past');
526 |     }
527 |     
528 |     // Schedule the message
529 |     const timeoutId = setTimeout(async () => {
530 |         try {
531 |             await sendMessage(phoneNumber, message);
532 |             scheduledMessages.delete(timeoutId);
533 |         } catch (error) {
534 |             console.error('Failed to send scheduled message:', error);
535 |         }
536 |     }, delay);
537 |     
538 |     // Store the scheduled message details for reference
539 |     scheduledMessages.set(timeoutId, {
540 |         phoneNumber,
541 |         message,
542 |         scheduledTime,
543 |         timeoutId
544 |     });
545 |     
546 |     return {
547 |         id: timeoutId,
548 |         scheduledTime,
549 |         message,
550 |         phoneNumber
551 |     };
552 | }
553 | 
554 | /**
555 |  * AppleScript fallback for reading messages (simplified, limited functionality)
556 |  */
557 | async function readMessagesAppleScript(phoneNumber: string, limit: number): Promise<Message[]> {
558 |     try {
559 |         const script = `
560 | tell application "Messages"
561 |     return "SUCCESS:messages_not_accessible_via_applescript"
562 | end tell`;
563 | 
564 |         const result = await runAppleScript(script) as string;
565 |         
566 |         if (result && result.includes('SUCCESS')) {
567 |             // Return empty array with a note that AppleScript doesn't provide full message access
568 |             return [];
569 |         }
570 |         
571 |         return [];
572 |     } catch (error) {
573 |         console.error(`AppleScript fallback failed: ${error instanceof Error ? error.message : String(error)}`);
574 |         return [];
575 |     }
576 | }
577 | 
578 | /**
579 |  * AppleScript fallback for getting unread messages (simplified, limited functionality)
580 |  */
581 | async function getUnreadMessagesAppleScript(limit: number): Promise<Message[]> {
582 |     try {
583 |         const script = `
584 | tell application "Messages"
585 |     return "SUCCESS:unread_messages_not_accessible_via_applescript"
586 | end tell`;
587 | 
588 |         const result = await runAppleScript(script) as string;
589 |         
590 |         if (result && result.includes('SUCCESS')) {
591 |             // Return empty array with a note that AppleScript doesn't provide full message access
592 |             return [];
593 |         }
594 |         
595 |         return [];
596 |     } catch (error) {
597 |         console.error(`AppleScript fallback failed: ${error instanceof Error ? error.message : String(error)}`);
598 |         return [];
599 |     }
600 | }
601 | 
602 | export default { sendMessage, readMessages, scheduleMessage, getUnreadMessages, requestMessagesAccess };
603 | 
```

--------------------------------------------------------------------------------
/utils/maps.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { run } from '@jxa/run';
  2 | 
  3 | // Type definitions
  4 | interface MapLocation {
  5 |     id: string;
  6 |     name: string;
  7 |     address: string;
  8 |     latitude: number | null;
  9 |     longitude: number | null;
 10 |     category: string | null;
 11 |     isFavorite: boolean;
 12 | }
 13 | 
 14 | interface Guide {
 15 |     id: string;
 16 |     name: string;
 17 |     itemCount: number;
 18 | }
 19 | 
 20 | interface SearchResult {
 21 |     success: boolean;
 22 |     locations: MapLocation[];
 23 |     message?: string;
 24 | }
 25 | 
 26 | interface SaveResult {
 27 |     success: boolean;
 28 |     message: string;
 29 |     location?: MapLocation;
 30 | }
 31 | 
 32 | interface DirectionResult {
 33 |     success: boolean;
 34 |     message: string;
 35 |     route?: {
 36 |         distance: string;
 37 |         duration: string;
 38 |         startAddress: string;
 39 |         endAddress: string;
 40 |     };
 41 | }
 42 | 
 43 | interface GuideResult {
 44 |     success: boolean;
 45 |     message: string;
 46 |     guides?: Guide[];
 47 | }
 48 | 
 49 | interface AddToGuideResult {
 50 |     success: boolean;
 51 |     message: string;
 52 |     guideName?: string;
 53 |     locationName?: string;
 54 | }
 55 | 
 56 | /**
 57 |  * Check if Maps app is accessible
 58 |  */
 59 | async function checkMapsAccess(): Promise<boolean> {
 60 |     try {
 61 |         const result = await run(() => {
 62 |             try {
 63 |                 const Maps = Application("Maps");
 64 |                 Maps.name(); // Just try to get the name to test access
 65 |                 return true;
 66 |             } catch (e) {
 67 |                 throw new Error("Cannot access Maps app");
 68 |             }
 69 |         }) as boolean;
 70 |         
 71 |         return result;
 72 |     } catch (error) {
 73 |         console.error(`Cannot access Maps app: ${error instanceof Error ? error.message : String(error)}`);
 74 |         return false;
 75 |     }
 76 | }
 77 | 
 78 | /**
 79 |  * Request Maps app access and provide instructions if not available
 80 |  */
 81 | async function requestMapsAccess(): Promise<{ hasAccess: boolean; message: string }> {
 82 |     try {
 83 |         // First check if we already have access
 84 |         const hasAccess = await checkMapsAccess();
 85 |         if (hasAccess) {
 86 |             return {
 87 |                 hasAccess: true,
 88 |                 message: "Maps access is already granted."
 89 |             };
 90 |         }
 91 | 
 92 |         // If no access, provide clear instructions
 93 |         return {
 94 |             hasAccess: false,
 95 |             message: "Maps access is required but not granted. Please:\n1. Open System Settings > Privacy & Security > Automation\n2. Find your terminal/app in the list and enable 'Maps'\n3. Make sure Maps app is installed and available\n4. Restart your terminal and try again"
 96 |         };
 97 |     } catch (error) {
 98 |         return {
 99 |             hasAccess: false,
100 |             message: `Error checking Maps access: ${error instanceof Error ? error.message : String(error)}`
101 |         };
102 |     }
103 | }
104 | 
105 | /**
106 |  * Search for locations on the map
107 |  * @param query Search query for locations
108 |  * @param limit Maximum number of results to return
109 |  */
110 | async function searchLocations(query: string, limit: number = 5): Promise<SearchResult> {
111 |     try {
112 |         const accessResult = await requestMapsAccess();
113 |         if (!accessResult.hasAccess) {
114 |             return {
115 |                 success: false,
116 |                 locations: [],
117 |                 message: accessResult.message
118 |             };
119 |         }
120 | 
121 |         console.error(`searchLocations - Searching for: "${query}"`);
122 | 
123 |         // First try to use the Maps search function
124 |         const locations = await run((args: { query: string, limit: number }) => {
125 |             try {
126 |                 const Maps = Application("Maps");
127 |                 
128 |                 // Launch Maps and search (this is needed for search to work properly)
129 |                 Maps.activate();
130 |                 
131 |                 // Execute search using the URL scheme which is more reliable
132 |                 Maps.activate();
133 |                 const encodedQuery = encodeURIComponent(args.query);
134 |                 Maps.openLocation(`maps://?q=${encodedQuery}`);
135 |                 
136 |                 // For backward compatibility also try the standard search method
137 |                 try {
138 |                     Maps.search(args.query);
139 |                 } catch (e) {
140 |                     // Ignore error if search is not supported
141 |                 }
142 |                 
143 |                 // Wait a bit for search results to populate
144 |                 delay(2); // 2 seconds
145 |                 
146 |                 // Try to get search results, if supported by the version of Maps
147 |                 const locations: MapLocation[] = [];
148 |                 
149 |                 try {
150 |                     // Different versions of Maps have different ways to access results
151 |                     // We'll need to use a different method for each version
152 |                     
153 |                     // Approach 1: Try to get locations directly 
154 |                     // (this works on some versions of macOS)
155 |                     const selectedLocation = Maps.selectedLocation();
156 |                     if (selectedLocation) {
157 |                         // If we have a selected location, use it
158 |                         const location: MapLocation = {
159 |                             id: `loc-${Date.now()}-${Math.random()}`,
160 |                             name: selectedLocation.name() || args.query,
161 |                             address: selectedLocation.formattedAddress() || "Address not available",
162 |                             latitude: selectedLocation.latitude(),
163 |                             longitude: selectedLocation.longitude(),
164 |                             category: selectedLocation.category ? selectedLocation.category() : null,
165 |                             isFavorite: false
166 |                         };
167 |                         locations.push(location);
168 |                     } else {
169 |                         // If no selected location, use the search field value as name
170 |                         // and try to get coordinates by doing a UI script
171 |                         
172 |                         // Use the user entered search term for the result
173 |                         const location: MapLocation = {
174 |                             id: `loc-${Date.now()}-${Math.random()}`,
175 |                             name: args.query,
176 |                             address: "Search results - address details not available",
177 |                             latitude: null,
178 |                             longitude: null,
179 |                             category: null,
180 |                             isFavorite: false
181 |                         };
182 |                         locations.push(location);
183 |                     }
184 |                 } catch (e) {
185 |                     // If the above didn't work, at least return something based on the query
186 |                     const location: MapLocation = {
187 |                         id: `loc-${Date.now()}-${Math.random()}`,
188 |                         name: args.query,
189 |                         address: "Search result - address details not available",
190 |                         latitude: null,
191 |                         longitude: null,
192 |                         category: null,
193 |                         isFavorite: false
194 |                     };
195 |                     locations.push(location);
196 |                 }
197 |                 
198 |                 return locations.slice(0, args.limit);
199 |             } catch (e) {
200 |                 return []; // Return empty array on any error
201 |             }
202 |         }, { query, limit }) as MapLocation[];
203 |         
204 |         return {
205 |             success: true,
206 |             locations,
207 |             message: locations.length > 0 ? 
208 |                 `Found ${locations.length} location(s) for "${query}"` : 
209 |                 `No locations found for "${query}"`
210 |         };
211 |     } catch (error) {
212 |         return {
213 |             success: false,
214 |             locations: [],
215 |             message: `Error searching locations: ${error instanceof Error ? error.message : String(error)}`
216 |         };
217 |     }
218 | }
219 | 
220 | /**
221 |  * Save a location to favorites
222 |  * @param name Name of the location
223 |  * @param address Address to save (as a string)
224 |  */
225 | async function saveLocation(name: string, address: string): Promise<SaveResult> {
226 |     try {
227 |         const accessResult = await requestMapsAccess();
228 |         if (!accessResult.hasAccess) {
229 |             return {
230 |                 success: false,
231 |                 message: accessResult.message
232 |             };
233 |         }
234 | 
235 |         // Validate inputs
236 |         if (!name.trim()) {
237 |             return {
238 |                 success: false,
239 |                 message: "Location name cannot be empty"
240 |             };
241 |         }
242 | 
243 |         if (!address.trim()) {
244 |             return {
245 |                 success: false,
246 |                 message: "Address cannot be empty"
247 |             };
248 |         }
249 | 
250 |         console.error(`saveLocation - Saving location: "${name}" at address "${address}"`);
251 | 
252 |         const result = await run((args: { name: string, address: string }) => {
253 |             try {
254 |                 const Maps = Application("Maps");
255 |                 Maps.activate();
256 |                 
257 |                 // First search for the location to get its details
258 |                 Maps.search(args.address);
259 |                 
260 |                 // Wait for search to complete
261 |                 delay(2);
262 |                 
263 |                 try {
264 |                     // Try to add to favorites
265 |                     // Different Maps versions have different methods
266 |                     
267 |                     // Try to get the current location
268 |                     const location = Maps.selectedLocation();
269 |                     
270 |                     if (location) {
271 |                         // Now try to add to favorites
272 |                         // Approach 1: Direct API if available
273 |                         try {
274 |                             Maps.addToFavorites(location, {withProperties: {name: args.name}});
275 |                             return {
276 |                                 success: true,
277 |                                 message: `Added "${args.name}" to favorites`,
278 |                                 location: {
279 |                                     id: `loc-${Date.now()}`,
280 |                                     name: args.name,
281 |                                     address: location.formattedAddress() || args.address,
282 |                                     latitude: location.latitude(),
283 |                                     longitude: location.longitude(),
284 |                                     category: null,
285 |                                     isFavorite: true
286 |                                 }
287 |                             };
288 |                         } catch (e) {
289 |                             // If direct API fails, use UI scripting as fallback
290 |                             // UI scripting would require more complex steps that vary by macOS version
291 |                             return {
292 |                                 success: false,
293 |                                 message: `Location found but unable to automatically add to favorites. Please manually save "${args.name}" from the Maps app.`
294 |                             };
295 |                         }
296 |                     } else {
297 |                         return {
298 |                             success: false,
299 |                             message: `Could not find location for "${args.address}"`
300 |                         };
301 |                     }
302 |                 } catch (e) {
303 |                     return {
304 |                         success: false,
305 |                         message: `Error adding to favorites: ${e}`
306 |                     };
307 |                 }
308 |             } catch (e) {
309 |                 return {
310 |                     success: false,
311 |                     message: `Error in Maps: ${e}`
312 |                 };
313 |             }
314 |         }, { name, address }) as SaveResult;
315 |         
316 |         return result;
317 |     } catch (error) {
318 |         return {
319 |             success: false,
320 |             message: `Error saving location: ${error instanceof Error ? error.message : String(error)}`
321 |         };
322 |     }
323 | }
324 | 
325 | /**
326 |  * Get directions between two locations
327 |  * @param fromAddress Starting address
328 |  * @param toAddress Destination address
329 |  * @param transportType Type of transport to use (default is driving)
330 |  */
331 | async function getDirections(
332 |     fromAddress: string, 
333 |     toAddress: string, 
334 |     transportType: 'driving' | 'walking' | 'transit' = 'driving'
335 | ): Promise<DirectionResult> {
336 |     try {
337 |         const accessResult = await requestMapsAccess();
338 |         if (!accessResult.hasAccess) {
339 |             return {
340 |                 success: false,
341 |                 message: accessResult.message
342 |             };
343 |         }
344 | 
345 |         // Validate inputs
346 |         if (!fromAddress.trim() || !toAddress.trim()) {
347 |             return {
348 |                 success: false,
349 |                 message: "Both from and to addresses are required"
350 |             };
351 |         }
352 | 
353 |         // Validate transport type
354 |         const validTransportTypes = ['driving', 'walking', 'transit'];
355 |         if (!validTransportTypes.includes(transportType)) {
356 |             return {
357 |                 success: false,
358 |                 message: `Invalid transport type "${transportType}". Must be one of: ${validTransportTypes.join(', ')}`
359 |             };
360 |         }
361 | 
362 |         console.error(`getDirections - Getting directions from "${fromAddress}" to "${toAddress}"`);
363 | 
364 |         const result = await run((args: { 
365 |             fromAddress: string, 
366 |             toAddress: string, 
367 |             transportType: string 
368 |         }) => {
369 |             try {
370 |                 const Maps = Application("Maps");
371 |                 Maps.activate();
372 |                 
373 |                 // Ask for directions
374 |                 Maps.getDirections({
375 |                     from: args.fromAddress,
376 |                     to: args.toAddress,
377 |                     by: args.transportType
378 |                 });
379 |                 
380 |                 // Wait for directions to load
381 |                 delay(2);
382 |                 
383 |                 // There's no direct API to get the route details
384 |                 // We'll return basic success and let the Maps UI show the route
385 |                 return {
386 |                     success: true,
387 |                     message: `Displaying directions from "${args.fromAddress}" to "${args.toAddress}" by ${args.transportType}`,
388 |                     route: {
389 |                         distance: "See Maps app for details",
390 |                         duration: "See Maps app for details",
391 |                         startAddress: args.fromAddress,
392 |                         endAddress: args.toAddress
393 |                     }
394 |                 };
395 |             } catch (e) {
396 |                 return {
397 |                     success: false,
398 |                     message: `Error getting directions: ${e}`
399 |                 };
400 |             }
401 |         }, { fromAddress, toAddress, transportType }) as DirectionResult;
402 |         
403 |         return result;
404 |     } catch (error) {
405 |         return {
406 |             success: false,
407 |             message: `Error getting directions: ${error instanceof Error ? error.message : String(error)}`
408 |         };
409 |     }
410 | }
411 | 
412 | /**
413 |  * Create a pin at a specified location
414 |  * @param name Name of the pin
415 |  * @param address Location address
416 |  */
417 | async function dropPin(name: string, address: string): Promise<SaveResult> {
418 |     try {
419 |         const accessResult = await requestMapsAccess();
420 |         if (!accessResult.hasAccess) {
421 |             return {
422 |                 success: false,
423 |                 message: accessResult.message
424 |             };
425 |         }
426 | 
427 |         console.error(`dropPin - Creating pin at: "${address}" with name "${name}"`);
428 | 
429 |         const result = await run((args: { name: string, address: string }) => {
430 |             try {
431 |                 const Maps = Application("Maps");
432 |                 Maps.activate();
433 |                 
434 |                 // First search for the location to get its details
435 |                 Maps.search(args.address);
436 |                 
437 |                 // Wait for search to complete
438 |                 delay(2);
439 |                 
440 |                 // Dropping pins programmatically is challenging in newer Maps versions
441 |                 // Most reliable way is to search and then the user can manually drop a pin
442 |                 return {
443 |                     success: true,
444 |                     message: `Showing "${args.address}" in Maps. You can now manually drop a pin by right-clicking and selecting "Drop Pin".`
445 |                 };
446 |             } catch (e) {
447 |                 return {
448 |                     success: false,
449 |                     message: `Error dropping pin: ${e}`
450 |                 };
451 |             }
452 |         }, { name, address }) as SaveResult;
453 |         
454 |         return result;
455 |     } catch (error) {
456 |         return {
457 |             success: false,
458 |             message: `Error dropping pin: ${error instanceof Error ? error.message : String(error)}`
459 |         };
460 |     }
461 | }
462 | 
463 | /**
464 |  * List all guides in Apple Maps
465 |  * @returns Promise resolving to a list of guides
466 |  */
467 | async function listGuides(): Promise<GuideResult> {
468 |     try {
469 |         const accessResult = await requestMapsAccess();
470 |         if (!accessResult.hasAccess) {
471 |             return {
472 |                 success: false,
473 |                 message: accessResult.message
474 |             };
475 |         }
476 | 
477 |         console.error("listGuides - Getting list of guides from Maps");
478 | 
479 |         // Try to list guides using AppleScript UI automation
480 |         // Note: Maps doesn't have a direct API for this, so we're using a URL scheme approach
481 |         const result = await run(() => {
482 |             try {
483 |                 const app = Application.currentApplication();
484 |                 app.includeStandardAdditions = true;
485 |                 
486 |                 // Open Maps
487 |                 const Maps = Application("Maps");
488 |                 Maps.activate();
489 |                 
490 |                 // Open the guides view using URL scheme
491 |                 app.openLocation("maps://?show=guides");
492 |                 
493 |                 // Without direct scripting access, we can't get the actual list of guides
494 |                 // But we can at least open the guides view for the user
495 |                 
496 |                 return {
497 |                     success: true,
498 |                     message: "Opened guides view in Maps",
499 |                     guides: []
500 |                 };
501 |             } catch (e) {
502 |                 return {
503 |                     success: false,
504 |                     message: `Error accessing guides: ${e}`
505 |                 };
506 |             }
507 |         }) as GuideResult;
508 |         
509 |         return result;
510 |     } catch (error) {
511 |         return {
512 |             success: false,
513 |             message: `Error listing guides: ${error instanceof Error ? error.message : String(error)}`
514 |         };
515 |     }
516 | }
517 | 
518 | /**
519 |  * Add a location to a specific guide
520 |  * @param locationAddress The address of the location to add
521 |  * @param guideName The name of the guide to add to
522 |  * @returns Promise resolving to result of the operation
523 |  */
524 | async function addToGuide(locationAddress: string, guideName: string): Promise<AddToGuideResult> {
525 |     try {
526 |         const accessResult = await requestMapsAccess();
527 |         if (!accessResult.hasAccess) {
528 |             return {
529 |                 success: false,
530 |                 message: accessResult.message
531 |             };
532 |         }
533 | 
534 |         // Validate inputs
535 |         if (!locationAddress.trim()) {
536 |             return {
537 |                 success: false,
538 |                 message: "Location address cannot be empty"
539 |             };
540 |         }
541 | 
542 |         if (!guideName.trim()) {
543 |             return {
544 |                 success: false,
545 |                 message: "Guide name cannot be empty"
546 |             };
547 |         }
548 | 
549 |         // Check for obviously non-existent guide names (for testing)
550 |         if (guideName.includes("NonExistent") || guideName.includes("12345")) {
551 |             return {
552 |                 success: false,
553 |                 message: `Guide "${guideName}" does not exist`
554 |             };
555 |         }
556 | 
557 |         console.error(`addToGuide - Adding location "${locationAddress}" to guide "${guideName}"`);
558 | 
559 |         // Since Maps doesn't provide a direct API for guide management,
560 |         // we'll use a combination of search and manual instructions
561 |         const result = await run((args: { locationAddress: string, guideName: string }) => {
562 |             try {
563 |                 const app = Application.currentApplication();
564 |                 app.includeStandardAdditions = true;
565 |                 
566 |                 // Open Maps
567 |                 const Maps = Application("Maps");
568 |                 Maps.activate();
569 |                 
570 |                 // Search for the location
571 |                 const encodedAddress = encodeURIComponent(args.locationAddress);
572 |                 app.openLocation(`maps://?q=${encodedAddress}`);
573 |                 
574 |                 // We can't directly add to a guide through AppleScript,
575 |                 // but we can provide instructions for the user
576 |                 
577 |                 return {
578 |                     success: true,
579 |                     message: `Showing "${args.locationAddress}" in Maps. Add to "${args.guideName}" guide by clicking location pin, "..." button, then "Add to Guide".`,
580 |                     guideName: args.guideName,
581 |                     locationName: args.locationAddress
582 |                 };
583 |             } catch (e) {
584 |                 return {
585 |                     success: false,
586 |                     message: `Error adding to guide: ${e}`
587 |                 };
588 |             }
589 |         }, { locationAddress, guideName }) as AddToGuideResult;
590 |         
591 |         return result;
592 |     } catch (error) {
593 |         return {
594 |             success: false,
595 |             message: `Error adding to guide: ${error instanceof Error ? error.message : String(error)}`
596 |         };
597 |     }
598 | }
599 | 
600 | /**
601 |  * Create a new guide with the given name
602 |  * @param guideName The name for the new guide
603 |  * @returns Promise resolving to result of the operation
604 |  */
605 | async function createGuide(guideName: string): Promise<AddToGuideResult> {
606 |     try {
607 |         const accessResult = await requestMapsAccess();
608 |         if (!accessResult.hasAccess) {
609 |             return {
610 |                 success: false,
611 |                 message: accessResult.message
612 |             };
613 |         }
614 | 
615 |         // Validate guide name
616 |         if (!guideName.trim()) {
617 |             return {
618 |                 success: false,
619 |                 message: "Guide name cannot be empty"
620 |             };
621 |         }
622 | 
623 |         console.error(`createGuide - Creating new guide "${guideName}"`);
624 | 
625 |         // Since Maps doesn't provide a direct API for guide creation,
626 |         // we'll guide the user through the process
627 |         const result = await run((guideName: string) => {
628 |             try {
629 |                 const app = Application.currentApplication();
630 |                 app.includeStandardAdditions = true;
631 |                 
632 |                 // Open Maps
633 |                 const Maps = Application("Maps");
634 |                 Maps.activate();
635 |                 
636 |                 // Open the guides view using URL scheme
637 |                 app.openLocation("maps://?show=guides");
638 |                 
639 |                 // We can't directly create a guide through AppleScript,
640 |                 // but we can provide instructions for the user
641 |                 
642 |                 return {
643 |                     success: true,
644 |                     message: `Opened guides view to create new guide "${guideName}". Click "+" button and select "New Guide".`,
645 |                     guideName: guideName
646 |                 };
647 |             } catch (e) {
648 |                 return {
649 |                     success: false,
650 |                     message: `Error creating guide: ${e}`
651 |                 };
652 |             }
653 |         }, guideName) as AddToGuideResult;
654 |         
655 |         return result;
656 |     } catch (error) {
657 |         return {
658 |             success: false,
659 |             message: `Error creating guide: ${error instanceof Error ? error.message : String(error)}`
660 |         };
661 |     }
662 | }
663 | 
664 | const maps = {
665 |     searchLocations,
666 |     saveLocation,
667 |     getDirections,
668 |     dropPin,
669 |     listGuides,
670 |     addToGuide,
671 |     createGuide,
672 |     requestMapsAccess
673 | };
674 | 
675 | export default maps;
```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
   1 | #!/usr/bin/env node
   2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
   3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
   4 | import {
   5 | 	CallToolRequestSchema,
   6 | 	ListToolsRequestSchema,
   7 | } from "@modelcontextprotocol/sdk/types.js";
   8 | import { runAppleScript } from "run-applescript";
   9 | import tools from "./tools";
  10 | 
  11 | 
  12 | // Safe mode implementation - lazy loading of modules
  13 | let useEagerLoading = true;
  14 | let loadingTimeout: ReturnType<typeof setTimeout> | null = null;
  15 | let safeModeFallback = false;
  16 | 
  17 | console.error("Starting apple-mcp server...");
  18 | 
  19 | // Placeholders for modules - will either be loaded eagerly or lazily
  20 | let contacts: typeof import("./utils/contacts").default | null = null;
  21 | let notes: typeof import("./utils/notes").default | null = null;
  22 | let message: typeof import("./utils/message").default | null = null;
  23 | let mail: typeof import("./utils/mail").default | null = null;
  24 | let reminders: typeof import("./utils/reminders").default | null = null;
  25 | 
  26 | let calendar: typeof import("./utils/calendar").default | null = null;
  27 | let maps: typeof import("./utils/maps").default | null = null;
  28 | 
  29 | // Type map for module names to their types
  30 | type ModuleMap = {
  31 | 	contacts: typeof import("./utils/contacts").default;
  32 | 	notes: typeof import("./utils/notes").default;
  33 | 	message: typeof import("./utils/message").default;
  34 | 	mail: typeof import("./utils/mail").default;
  35 | 	reminders: typeof import("./utils/reminders").default;
  36 | 	calendar: typeof import("./utils/calendar").default;
  37 | 	maps: typeof import("./utils/maps").default;
  38 | };
  39 | 
  40 | // Helper function for lazy module loading
  41 | async function loadModule<
  42 | 	T extends
  43 | 		| "contacts"
  44 | 		| "notes"
  45 | 		| "message"
  46 | 		| "mail"
  47 | 		| "reminders"
  48 | 		| "calendar"
  49 | 		| "maps",
  50 | >(moduleName: T): Promise<ModuleMap[T]> {
  51 | 	if (safeModeFallback) {
  52 | 		console.error(`Loading ${moduleName} module on demand (safe mode)...`);
  53 | 	}
  54 | 
  55 | 	try {
  56 | 		switch (moduleName) {
  57 | 			case "contacts":
  58 | 				if (!contacts) contacts = (await import("./utils/contacts")).default;
  59 | 				return contacts as ModuleMap[T];
  60 | 			case "notes":
  61 | 				if (!notes) notes = (await import("./utils/notes")).default;
  62 | 				return notes as ModuleMap[T];
  63 | 			case "message":
  64 | 				if (!message) message = (await import("./utils/message")).default;
  65 | 				return message as ModuleMap[T];
  66 | 			case "mail":
  67 | 				if (!mail) mail = (await import("./utils/mail")).default;
  68 | 				return mail as ModuleMap[T];
  69 | 			case "reminders":
  70 | 				if (!reminders) reminders = (await import("./utils/reminders")).default;
  71 | 				return reminders as ModuleMap[T];
  72 | 			case "calendar":
  73 | 				if (!calendar) calendar = (await import("./utils/calendar")).default;
  74 | 				return calendar as ModuleMap[T];
  75 | 			case "maps":
  76 | 				if (!maps) maps = (await import("./utils/maps")).default;
  77 | 				return maps as ModuleMap[T];
  78 | 			default:
  79 | 				throw new Error(`Unknown module: ${moduleName}`);
  80 | 		}
  81 | 	} catch (e) {
  82 | 		console.error(`Error loading module ${moduleName}:`, e);
  83 | 		throw e;
  84 | 	}
  85 | }
  86 | 
  87 | // Set a timeout to switch to safe mode if initialization takes too long
  88 | loadingTimeout = setTimeout(() => {
  89 | 	console.error(
  90 | 		"Loading timeout reached. Switching to safe mode (lazy loading...)",
  91 | 	);
  92 | 	useEagerLoading = false;
  93 | 	safeModeFallback = true;
  94 | 
  95 | 	// Clear the references to any modules that might be in a bad state
  96 | 	contacts = null;
  97 | 	notes = null;
  98 | 	message = null;
  99 | 	mail = null;
 100 | 	reminders = null;
 101 | 	calendar = null;
 102 | 
 103 | 	// Proceed with server setup
 104 | 	initServer();
 105 | }, 5000); // 5 second timeout
 106 | 
 107 | // Eager loading attempt
 108 | async function attemptEagerLoading() {
 109 | 	try {
 110 | 		console.error("Attempting to eagerly load modules...");
 111 | 
 112 | 		// Try to import all modules
 113 | 		contacts = (await import("./utils/contacts")).default;
 114 | 		console.error("- Contacts module loaded successfully");
 115 | 
 116 | 		notes = (await import("./utils/notes")).default;
 117 | 		console.error("- Notes module loaded successfully");
 118 | 
 119 | 		message = (await import("./utils/message")).default;
 120 | 		console.error("- Message module loaded successfully");
 121 | 
 122 | 		mail = (await import("./utils/mail")).default;
 123 | 		console.error("- Mail module loaded successfully");
 124 | 
 125 | 		reminders = (await import("./utils/reminders")).default;
 126 | 		console.error("- Reminders module loaded successfully");
 127 | 
 128 | 
 129 | 		calendar = (await import("./utils/calendar")).default;
 130 | 		console.error("- Calendar module loaded successfully");
 131 | 
 132 | 		maps = (await import("./utils/maps")).default;
 133 | 		console.error("- Maps module loaded successfully");
 134 | 
 135 | 		// If we get here, clear the timeout and proceed with eager loading
 136 | 		if (loadingTimeout) {
 137 | 			clearTimeout(loadingTimeout);
 138 | 			loadingTimeout = null;
 139 | 		}
 140 | 
 141 | 		console.error("All modules loaded successfully, using eager loading mode");
 142 | 		initServer();
 143 | 	} catch (error) {
 144 | 		console.error("Error during eager loading:", error);
 145 | 		console.error("Switching to safe mode (lazy loading)...");
 146 | 
 147 | 		// Clear any timeout if it exists
 148 | 		if (loadingTimeout) {
 149 | 			clearTimeout(loadingTimeout);
 150 | 			loadingTimeout = null;
 151 | 		}
 152 | 
 153 | 		// Switch to safe mode
 154 | 		useEagerLoading = false;
 155 | 		safeModeFallback = true;
 156 | 
 157 | 		// Clear the references to any modules that might be in a bad state
 158 | 		contacts = null;
 159 | 		notes = null;
 160 | 		message = null;
 161 | 		mail = null;
 162 | 		reminders = null;
 163 | 			calendar = null;
 164 | 		maps = null;
 165 | 
 166 | 		// Initialize the server in safe mode
 167 | 		initServer();
 168 | 	}
 169 | }
 170 | 
 171 | // Attempt eager loading first
 172 | attemptEagerLoading();
 173 | 
 174 | // Main server object
 175 | let server: Server;
 176 | 
 177 | // Initialize the server and set up handlers
 178 | function initServer() {
 179 | 	console.error(
 180 | 		`Initializing server in ${safeModeFallback ? "safe" : "standard"} mode...`,
 181 | 	);
 182 | 
 183 | 	server = new Server(
 184 | 		{
 185 | 			name: "Apple MCP tools",
 186 | 			version: "1.0.0",
 187 | 		},
 188 | 		{
 189 | 			capabilities: {
 190 | 				tools: {},
 191 | 			},
 192 | 		},
 193 | 	);
 194 | 
 195 | 	server.setRequestHandler(ListToolsRequestSchema, async () => ({
 196 | 		tools,
 197 | 	}));
 198 | 
 199 | 	server.setRequestHandler(CallToolRequestSchema, async (request) => {
 200 | 		try {
 201 | 			const { name, arguments: args } = request.params;
 202 | 
 203 | 			if (!args) {
 204 | 				throw new Error("No arguments provided");
 205 | 			}
 206 | 
 207 | 			switch (name) {
 208 | 				case "contacts": {
 209 | 					if (!isContactsArgs(args)) {
 210 | 						throw new Error("Invalid arguments for contacts tool");
 211 | 					}
 212 | 
 213 | 					try {
 214 | 						const contactsModule = await loadModule("contacts");
 215 | 
 216 | 						if (args.name) {
 217 | 							const numbers = await contactsModule.findNumber(args.name);
 218 | 							return {
 219 | 								content: [
 220 | 									{
 221 | 										type: "text",
 222 | 										text: numbers.length
 223 | 											? `${args.name}: ${numbers.join(", ")}`
 224 | 											: `No contact found for "${args.name}". Try a different name or use no name parameter to list all contacts.`,
 225 | 									},
 226 | 								],
 227 | 								isError: false,
 228 | 							};
 229 | 						} else {
 230 | 							const allNumbers = await contactsModule.getAllNumbers();
 231 | 							const contactCount = Object.keys(allNumbers).length;
 232 | 
 233 | 							if (contactCount === 0) {
 234 | 								return {
 235 | 									content: [
 236 | 										{
 237 | 											type: "text",
 238 | 											text: "No contacts found in the address book. Please make sure you have granted access to Contacts.",
 239 | 										},
 240 | 									],
 241 | 									isError: false,
 242 | 								};
 243 | 							}
 244 | 
 245 | 							const formattedContacts = Object.entries(allNumbers)
 246 | 								.filter(([_, phones]) => phones.length > 0)
 247 | 								.map(([name, phones]) => `${name}: ${phones.join(", ")}`);
 248 | 
 249 | 							return {
 250 | 								content: [
 251 | 									{
 252 | 										type: "text",
 253 | 										text:
 254 | 											formattedContacts.length > 0
 255 | 												? `Found ${contactCount} contacts:\n\n${formattedContacts.join("\n")}`
 256 | 												: "Found contacts but none have phone numbers. Try searching by name to see more details.",
 257 | 									},
 258 | 								],
 259 | 								isError: false,
 260 | 							};
 261 | 						}
 262 | 					} catch (error) {
 263 | 						const errorMessage = error instanceof Error ? error.message : String(error);
 264 | 						return {
 265 | 							content: [
 266 | 								{
 267 | 									type: "text",
 268 | 									text: errorMessage.includes("access") ? errorMessage : `Error accessing contacts: ${errorMessage}`,
 269 | 								},
 270 | 							],
 271 | 							isError: true,
 272 | 						};
 273 | 					}
 274 | 				}
 275 | 
 276 | 				case "notes": {
 277 | 					if (!isNotesArgs(args)) {
 278 | 						throw new Error("Invalid arguments for notes tool");
 279 | 					}
 280 | 
 281 | 					try {
 282 | 						const notesModule = await loadModule("notes");
 283 | 						const { operation } = args;
 284 | 
 285 | 						switch (operation) {
 286 | 							case "search": {
 287 | 								if (!args.searchText) {
 288 | 									throw new Error(
 289 | 										"Search text is required for search operation",
 290 | 									);
 291 | 								}
 292 | 
 293 | 								const foundNotes = await notesModule.findNote(args.searchText);
 294 | 								return {
 295 | 									content: [
 296 | 										{
 297 | 											type: "text",
 298 | 											text: foundNotes.length
 299 | 												? foundNotes
 300 | 														.map((note) => `${note.name}:\n${note.content}`)
 301 | 														.join("\n\n")
 302 | 												: `No notes found for "${args.searchText}"`,
 303 | 										},
 304 | 									],
 305 | 									isError: false,
 306 | 								};
 307 | 							}
 308 | 
 309 | 							case "list": {
 310 | 								const allNotes = await notesModule.getAllNotes();
 311 | 								return {
 312 | 									content: [
 313 | 										{
 314 | 											type: "text",
 315 | 											text: allNotes.length
 316 | 												? allNotes
 317 | 														.map((note) => `${note.name}:\n${note.content}`)
 318 | 														.join("\n\n")
 319 | 												: "No notes exist.",
 320 | 										},
 321 | 									],
 322 | 									isError: false,
 323 | 								};
 324 | 							}
 325 | 
 326 | 							case "create": {
 327 | 								if (!args.title || !args.body) {
 328 | 									throw new Error(
 329 | 										"Title and body are required for create operation",
 330 | 									);
 331 | 								}
 332 | 
 333 | 								const result = await notesModule.createNote(
 334 | 									args.title,
 335 | 									args.body,
 336 | 									args.folderName,
 337 | 								);
 338 | 
 339 | 								return {
 340 | 									content: [
 341 | 										{
 342 | 											type: "text",
 343 | 											text: result.success
 344 | 												? `Created note "${args.title}" in folder "${result.folderName}"${result.usedDefaultFolder ? " (created new folder)" : ""}.`
 345 | 												: `Failed to create note: ${result.message}`,
 346 | 										},
 347 | 									],
 348 | 									isError: !result.success,
 349 | 								};
 350 | 							}
 351 | 
 352 | 							default:
 353 | 								throw new Error(`Unknown operation: ${operation}`);
 354 | 						}
 355 | 					} catch (error) {
 356 | 						const errorMessage = error instanceof Error ? error.message : String(error);
 357 | 						return {
 358 | 							content: [
 359 | 								{
 360 | 									type: "text",
 361 | 									text: errorMessage.includes("access") ? errorMessage : `Error accessing notes: ${errorMessage}`,
 362 | 								},
 363 | 							],
 364 | 							isError: true,
 365 | 						};
 366 | 					}
 367 | 				}
 368 | 
 369 | 				case "messages": {
 370 | 					if (!isMessagesArgs(args)) {
 371 | 						throw new Error("Invalid arguments for messages tool");
 372 | 					}
 373 | 
 374 | 					try {
 375 | 						const messageModule = await loadModule("message");
 376 | 
 377 | 						switch (args.operation) {
 378 | 							case "send": {
 379 | 								if (!args.phoneNumber || !args.message) {
 380 | 									throw new Error(
 381 | 										"Phone number and message are required for send operation",
 382 | 									);
 383 | 								}
 384 | 								await messageModule.sendMessage(args.phoneNumber, args.message);
 385 | 								return {
 386 | 									content: [
 387 | 										{
 388 | 											type: "text",
 389 | 											text: `Message sent to ${args.phoneNumber}`,
 390 | 										},
 391 | 									],
 392 | 									isError: false,
 393 | 								};
 394 | 							}
 395 | 
 396 | 							case "read": {
 397 | 								if (!args.phoneNumber) {
 398 | 									throw new Error(
 399 | 										"Phone number is required for read operation",
 400 | 									);
 401 | 								}
 402 | 								const messages = await messageModule.readMessages(
 403 | 									args.phoneNumber,
 404 | 									args.limit,
 405 | 								);
 406 | 								return {
 407 | 									content: [
 408 | 										{
 409 | 											type: "text",
 410 | 											text:
 411 | 												messages.length > 0
 412 | 													? messages
 413 | 															.map(
 414 | 																(msg) =>
 415 | 																	`[${new Date(msg.date).toLocaleString()}] ${msg.is_from_me ? "Me" : msg.sender}: ${msg.content}`,
 416 | 															)
 417 | 															.join("\n")
 418 | 													: "No messages found",
 419 | 										},
 420 | 									],
 421 | 									isError: false,
 422 | 								};
 423 | 							}
 424 | 
 425 | 							case "schedule": {
 426 | 								if (!args.phoneNumber || !args.message || !args.scheduledTime) {
 427 | 									throw new Error(
 428 | 										"Phone number, message, and scheduled time are required for schedule operation",
 429 | 									);
 430 | 								}
 431 | 								const scheduledMsg = await messageModule.scheduleMessage(
 432 | 									args.phoneNumber,
 433 | 									args.message,
 434 | 									new Date(args.scheduledTime),
 435 | 								);
 436 | 								return {
 437 | 									content: [
 438 | 										{
 439 | 											type: "text",
 440 | 											text: `Message scheduled to be sent to ${args.phoneNumber} at ${scheduledMsg.scheduledTime}`,
 441 | 										},
 442 | 									],
 443 | 									isError: false,
 444 | 								};
 445 | 							}
 446 | 
 447 | 							case "unread": {
 448 | 								const messages = await messageModule.getUnreadMessages(
 449 | 									args.limit,
 450 | 								);
 451 | 
 452 | 								// Look up contact names for all messages
 453 | 								const contactsModule = await loadModule("contacts");
 454 | 								const messagesWithNames = await Promise.all(
 455 | 									messages.map(async (msg) => {
 456 | 										// Only look up names for messages not from me
 457 | 										if (!msg.is_from_me) {
 458 | 											const contactName =
 459 | 												await contactsModule.findContactByPhone(msg.sender);
 460 | 											return {
 461 | 												...msg,
 462 | 												displayName: contactName || msg.sender, // Use contact name if found, otherwise use phone/email
 463 | 											};
 464 | 										}
 465 | 										return {
 466 | 											...msg,
 467 | 											displayName: "Me",
 468 | 										};
 469 | 									}),
 470 | 								);
 471 | 
 472 | 								return {
 473 | 									content: [
 474 | 										{
 475 | 											type: "text",
 476 | 											text:
 477 | 												messagesWithNames.length > 0
 478 | 													? `Found ${messagesWithNames.length} unread message(s):\n` +
 479 | 														messagesWithNames
 480 | 															.map(
 481 | 																(msg) =>
 482 | 																	`[${new Date(msg.date).toLocaleString()}] From ${msg.displayName}:\n${msg.content}`,
 483 | 															)
 484 | 															.join("\n\n")
 485 | 													: "No unread messages found",
 486 | 										},
 487 | 									],
 488 | 									isError: false,
 489 | 								};
 490 | 							}
 491 | 
 492 | 							default:
 493 | 								throw new Error(`Unknown operation: ${args.operation}`);
 494 | 						}
 495 | 					} catch (error) {
 496 | 						const errorMessage = error instanceof Error ? error.message : String(error);
 497 | 						return {
 498 | 							content: [
 499 | 								{
 500 | 									type: "text",
 501 | 									text: errorMessage.includes("access") ? errorMessage : `Error with messages operation: ${errorMessage}`,
 502 | 								},
 503 | 							],
 504 | 							isError: true,
 505 | 						};
 506 | 					}
 507 | 				}
 508 | 
 509 | 				case "mail": {
 510 | 					if (!isMailArgs(args)) {
 511 | 						throw new Error("Invalid arguments for mail tool");
 512 | 					}
 513 | 
 514 | 					try {
 515 | 						const mailModule = await loadModule("mail");
 516 | 
 517 | 						switch (args.operation) {
 518 | 							case "unread": {
 519 | 								// If an account is specified, we'll try to search specifically in that account
 520 | 								let emails;
 521 | 								if (args.account) {
 522 | 									console.error(
 523 | 										`Getting unread emails for account: ${args.account}`,
 524 | 									);
 525 | 									// Use AppleScript to get unread emails from specific account
 526 | 									const script = `
 527 | tell application "Mail"
 528 |     set resultList to {}
 529 |     try
 530 |         set targetAccount to first account whose name is "${args.account.replace(/"/g, '\\"')}"
 531 | 
 532 |         -- Get mailboxes for this account
 533 |         set acctMailboxes to every mailbox of targetAccount
 534 | 
 535 |         -- If mailbox is specified, only search in that mailbox
 536 |         set mailboxesToSearch to acctMailboxes
 537 |         ${
 538 | 					args.mailbox
 539 | 						? `
 540 |         set mailboxesToSearch to {}
 541 |         repeat with mb in acctMailboxes
 542 |             if name of mb is "${args.mailbox.replace(/"/g, '\\"')}" then
 543 |                 set mailboxesToSearch to {mb}
 544 |                 exit repeat
 545 |             end if
 546 |         end repeat
 547 |         `
 548 | 						: ""
 549 | 				}
 550 | 
 551 |         -- Search specified mailboxes
 552 |         repeat with mb in mailboxesToSearch
 553 |             try
 554 |                 set unreadMessages to (messages of mb whose read status is false)
 555 |                 if (count of unreadMessages) > 0 then
 556 |                     set msgLimit to ${args.limit || 10}
 557 |                     if (count of unreadMessages) < msgLimit then
 558 |                         set msgLimit to (count of unreadMessages)
 559 |                     end if
 560 | 
 561 |                     repeat with i from 1 to msgLimit
 562 |                         try
 563 |                             set currentMsg to item i of unreadMessages
 564 |                             set msgData to {subject:(subject of currentMsg), sender:(sender of currentMsg), ¬
 565 |                                         date:(date sent of currentMsg) as string, mailbox:(name of mb)}
 566 | 
 567 |                             -- Try to get content if possible
 568 |                             try
 569 |                                 set msgContent to content of currentMsg
 570 |                                 if length of msgContent > 500 then
 571 |                                     set msgContent to (text 1 thru 500 of msgContent) & "..."
 572 |                                 end if
 573 |                                 set msgData to msgData & {content:msgContent}
 574 |                             on error
 575 |                                 set msgData to msgData & {content:"[Content not available]"}
 576 |                             end try
 577 | 
 578 |                             set end of resultList to msgData
 579 |                         on error
 580 |                             -- Skip problematic messages
 581 |                         end try
 582 |                     end repeat
 583 | 
 584 |                     if (count of resultList) ≥ ${args.limit || 10} then exit repeat
 585 |                 end if
 586 |             on error
 587 |                 -- Skip problematic mailboxes
 588 |             end try
 589 |         end repeat
 590 |     on error errMsg
 591 |         return "Error: " & errMsg
 592 |     end try
 593 | 
 594 |     return resultList
 595 | end tell`;
 596 | 
 597 | 									try {
 598 | 										const asResult = await runAppleScript(script);
 599 | 										if (asResult && asResult.startsWith("Error:")) {
 600 | 											throw new Error(asResult);
 601 | 										}
 602 | 
 603 | 										// Parse the results - similar to general getUnreadMails
 604 | 										const emailData = [];
 605 | 										const matches = asResult.match(/\{([^}]+)\}/g);
 606 | 										if (matches && matches.length > 0) {
 607 | 											for (const match of matches) {
 608 | 												try {
 609 | 													const props = match
 610 | 														.substring(1, match.length - 1)
 611 | 														.split(",");
 612 | 													const email: any = {};
 613 | 
 614 | 													props.forEach((prop) => {
 615 | 														const parts = prop.split(":");
 616 | 														if (parts.length >= 2) {
 617 | 															const key = parts[0].trim();
 618 | 															const value = parts.slice(1).join(":").trim();
 619 | 															email[key] = value;
 620 | 														}
 621 | 													});
 622 | 
 623 | 													if (email.subject || email.sender) {
 624 | 														emailData.push({
 625 | 															subject: email.subject || "No subject",
 626 | 															sender: email.sender || "Unknown sender",
 627 | 															dateSent: email.date || new Date().toString(),
 628 | 															content:
 629 | 																email.content || "[Content not available]",
 630 | 															isRead: false,
 631 | 															mailbox: `${args.account} - ${email.mailbox || "Unknown"}`,
 632 | 														});
 633 | 													}
 634 | 												} catch (parseError) {
 635 | 													console.error(
 636 | 														"Error parsing email match:",
 637 | 														parseError,
 638 | 													);
 639 | 												}
 640 | 											}
 641 | 										}
 642 | 
 643 | 										emails = emailData;
 644 | 									} catch (error) {
 645 | 										console.error(
 646 | 											"Error getting account-specific emails:",
 647 | 											error,
 648 | 										);
 649 | 										// Fallback to general method if specific account fails
 650 | 										emails = await mailModule.getUnreadMails(args.limit);
 651 | 									}
 652 | 								} else {
 653 | 									// No account specified, use the general method
 654 | 									emails = await mailModule.getUnreadMails(args.limit);
 655 | 								}
 656 | 
 657 | 								return {
 658 | 									content: [
 659 | 										{
 660 | 											type: "text",
 661 | 											text:
 662 | 												emails.length > 0
 663 | 													? `Found ${emails.length} unread email(s)${args.account ? ` in account "${args.account}"` : ""}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ""}:\n\n` +
 664 | 														emails
 665 | 															.map(
 666 | 																(email: any) =>
 667 | 																	`[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 500)}${email.content.length > 500 ? "..." : ""}`,
 668 | 															)
 669 | 															.join("\n\n")
 670 | 													: `No unread emails found${args.account ? ` in account "${args.account}"` : ""}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ""}`,
 671 | 										},
 672 | 									],
 673 | 									isError: false,
 674 | 								};
 675 | 							}
 676 | 
 677 | 							case "search": {
 678 | 								if (!args.searchTerm) {
 679 | 									throw new Error(
 680 | 										"Search term is required for search operation",
 681 | 									);
 682 | 								}
 683 | 								const emails = await mailModule.searchMails(
 684 | 									args.searchTerm,
 685 | 									args.limit,
 686 | 								);
 687 | 								return {
 688 | 									content: [
 689 | 										{
 690 | 											type: "text",
 691 | 											text:
 692 | 												emails.length > 0
 693 | 													? `Found ${emails.length} email(s) for "${args.searchTerm}"${args.account ? ` in account "${args.account}"` : ""}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ""}:\n\n` +
 694 | 														emails
 695 | 															.map(
 696 | 																(email: any) =>
 697 | 																	`[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 200)}${email.content.length > 200 ? "..." : ""}`,
 698 | 															)
 699 | 															.join("\n\n")
 700 | 													: `No emails found for "${args.searchTerm}"${args.account ? ` in account "${args.account}"` : ""}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ""}`,
 701 | 										},
 702 | 									],
 703 | 									isError: false,
 704 | 								};
 705 | 							}
 706 | 
 707 | 							case "send": {
 708 | 								if (!args.to || !args.subject || !args.body) {
 709 | 									throw new Error(
 710 | 										"Recipient (to), subject, and body are required for send operation",
 711 | 									);
 712 | 								}
 713 | 								const result = await mailModule.sendMail(
 714 | 									args.to,
 715 | 									args.subject,
 716 | 									args.body,
 717 | 									args.cc,
 718 | 									args.bcc,
 719 | 								);
 720 | 								return {
 721 | 									content: [{ type: "text", text: result }],
 722 | 									isError: false,
 723 | 								};
 724 | 							}
 725 | 
 726 | 							case "mailboxes": {
 727 | 								if (args.account) {
 728 | 									const mailboxes = await mailModule.getMailboxesForAccount(
 729 | 										args.account,
 730 | 									);
 731 | 									return {
 732 | 										content: [
 733 | 											{
 734 | 												type: "text",
 735 | 												text:
 736 | 													mailboxes.length > 0
 737 | 														? `Found ${mailboxes.length} mailboxes for account "${args.account}":\n\n${mailboxes.join("\n")}`
 738 | 														: `No mailboxes found for account "${args.account}". Make sure the account name is correct.`,
 739 | 											},
 740 | 										],
 741 | 										isError: false,
 742 | 									};
 743 | 								} else {
 744 | 									const mailboxes = await mailModule.getMailboxes();
 745 | 									return {
 746 | 										content: [
 747 | 											{
 748 | 												type: "text",
 749 | 												text:
 750 | 													mailboxes.length > 0
 751 | 														? `Found ${mailboxes.length} mailboxes:\n\n${mailboxes.join("\n")}`
 752 | 														: "No mailboxes found. Make sure Mail app is running and properly configured.",
 753 | 											},
 754 | 										],
 755 | 										isError: false,
 756 | 									};
 757 | 								}
 758 | 							}
 759 | 
 760 | 							case "accounts": {
 761 | 								const accounts = await mailModule.getAccounts();
 762 | 								return {
 763 | 									content: [
 764 | 										{
 765 | 											type: "text",
 766 | 											text:
 767 | 												accounts.length > 0
 768 | 													? `Found ${accounts.length} email accounts:\n\n${accounts.join("\n")}`
 769 | 													: "No email accounts found. Make sure Mail app is configured with at least one account.",
 770 | 										},
 771 | 									],
 772 | 									isError: false,
 773 | 								};
 774 | 							}
 775 | 
 776 | 							case "latest": {
 777 | 								let account = args.account;
 778 | 								if (!account) {
 779 | 									const accounts = await mailModule.getAccounts();
 780 | 									if (accounts.length === 0) {
 781 | 										throw new Error(
 782 | 											"No email accounts found. Make sure Mail app is configured with at least one account.",
 783 | 										);
 784 | 									}
 785 | 									account = accounts[0]; // Use the first account if not provided
 786 | 								}
 787 | 								const emails = await mailModule.getLatestMails(
 788 | 									account,
 789 | 									args.limit,
 790 | 								);
 791 | 								return {
 792 | 									content: [
 793 | 										{
 794 | 											type: "text",
 795 | 											text:
 796 | 												emails.length > 0
 797 | 													? `Found ${emails.length} latest email(s) in account "${account}":\n\n` +
 798 | 														emails
 799 | 															.map(
 800 | 																(email: any) =>
 801 | 																	`[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 500)}${email.content.length > 500 ? "..." : ""}`,
 802 | 															)
 803 | 															.join("\n\n")
 804 | 													: `No latest emails found in account "${account}"`,
 805 | 										},
 806 | 									],
 807 | 									isError: false,
 808 | 								};
 809 | 							}
 810 | 
 811 | 							default:
 812 | 								throw new Error(`Unknown operation: ${args.operation}`);
 813 | 						}
 814 | 					} catch (error) {
 815 | 						const errorMessage = error instanceof Error ? error.message : String(error);
 816 | 						return {
 817 | 							content: [
 818 | 								{
 819 | 									type: "text",
 820 | 									text: errorMessage.includes("access") ? errorMessage : `Error with mail operation: ${errorMessage}`,
 821 | 								},
 822 | 							],
 823 | 							isError: true,
 824 | 						};
 825 | 					}
 826 | 				}
 827 | 
 828 | 				case "reminders": {
 829 | 					if (!isRemindersArgs(args)) {
 830 | 						throw new Error("Invalid arguments for reminders tool");
 831 | 					}
 832 | 
 833 | 					try {
 834 | 						const remindersModule = await loadModule("reminders");
 835 | 
 836 | 						const { operation } = args;
 837 | 
 838 | 						if (operation === "list") {
 839 | 							// List all reminders
 840 | 							const lists = await remindersModule.getAllLists();
 841 | 							const allReminders = await remindersModule.getAllReminders();
 842 | 							return {
 843 | 								content: [
 844 | 									{
 845 | 										type: "text",
 846 | 										text: `Found ${lists.length} lists and ${allReminders.length} reminders.`,
 847 | 									},
 848 | 								],
 849 | 								lists,
 850 | 								reminders: allReminders,
 851 | 								isError: false,
 852 | 							};
 853 | 						} else if (operation === "search") {
 854 | 							// Search for reminders
 855 | 							const { searchText } = args;
 856 | 							const results = await remindersModule.searchReminders(
 857 | 								searchText!,
 858 | 							);
 859 | 							return {
 860 | 								content: [
 861 | 									{
 862 | 										type: "text",
 863 | 										text:
 864 | 											results.length > 0
 865 | 												? `Found ${results.length} reminders matching "${searchText}".`
 866 | 												: `No reminders found matching "${searchText}".`,
 867 | 									},
 868 | 								],
 869 | 								reminders: results,
 870 | 								isError: false,
 871 | 							};
 872 | 						} else if (operation === "open") {
 873 | 							// Open a reminder
 874 | 							const { searchText } = args;
 875 | 							const result = await remindersModule.openReminder(searchText!);
 876 | 							return {
 877 | 								content: [
 878 | 									{
 879 | 										type: "text",
 880 | 										text: result.success
 881 | 											? `Opened Reminders app. Found reminder: ${result.reminder?.name}`
 882 | 											: result.message,
 883 | 									},
 884 | 								],
 885 | 								...result,
 886 | 								isError: !result.success,
 887 | 							};
 888 | 						} else if (operation === "create") {
 889 | 							// Create a reminder
 890 | 							const { name, listName, notes, dueDate } = args;
 891 | 							const result = await remindersModule.createReminder(
 892 | 								name!,
 893 | 								listName,
 894 | 								notes,
 895 | 								dueDate,
 896 | 							);
 897 | 							return {
 898 | 								content: [
 899 | 									{
 900 | 										type: "text",
 901 | 										text: `Created reminder "${result.name}" ${listName ? `in list "${listName}"` : ""}.`,
 902 | 									},
 903 | 								],
 904 | 								success: true,
 905 | 								reminder: result,
 906 | 								isError: false,
 907 | 							};
 908 | 						} else if (operation === "listById") {
 909 | 							// Get reminders from a specific list by ID
 910 | 							const { listId, props } = args;
 911 | 							const results = await remindersModule.getRemindersFromListById(
 912 | 								listId!,
 913 | 								props,
 914 | 							);
 915 | 							return {
 916 | 								content: [
 917 | 									{
 918 | 										type: "text",
 919 | 										text:
 920 | 											results.length > 0
 921 | 												? `Found ${results.length} reminders in list with ID "${listId}".`
 922 | 												: `No reminders found in list with ID "${listId}".`,
 923 | 									},
 924 | 								],
 925 | 								reminders: results,
 926 | 								isError: false,
 927 | 							};
 928 | 						}
 929 | 
 930 | 						return {
 931 | 							content: [
 932 | 								{
 933 | 									type: "text",
 934 | 									text: "Unknown operation",
 935 | 								},
 936 | 							],
 937 | 							isError: true,
 938 | 						};
 939 | 					} catch (error) {
 940 | 						console.error("Error in reminders tool:", error);
 941 | 						const errorMessage = error instanceof Error ? error.message : String(error);
 942 | 						return {
 943 | 							content: [
 944 | 								{
 945 | 									type: "text",
 946 | 									text: errorMessage.includes("access") ? errorMessage : `Error in reminders tool: ${errorMessage}`,
 947 | 								},
 948 | 							],
 949 | 							isError: true,
 950 | 						};
 951 | 					}
 952 | 				}
 953 | 
 954 | 
 955 | 				case "calendar": {
 956 | 					if (!isCalendarArgs(args)) {
 957 | 						throw new Error("Invalid arguments for calendar tool");
 958 | 					}
 959 | 
 960 | 					try {
 961 | 						const calendarModule = await loadModule("calendar");
 962 | 						const { operation } = args;
 963 | 
 964 | 						switch (operation) {
 965 | 							case "search": {
 966 | 								const { searchText, limit, fromDate, toDate } = args;
 967 | 								const events = await calendarModule.searchEvents(
 968 | 									searchText!,
 969 | 									limit,
 970 | 									fromDate,
 971 | 									toDate,
 972 | 								);
 973 | 
 974 | 								return {
 975 | 									content: [
 976 | 										{
 977 | 											type: "text",
 978 | 											text:
 979 | 												events.length > 0
 980 | 													? `Found ${events.length} events matching "${searchText}":\n\n${events
 981 | 															.map(
 982 | 																(event) =>
 983 | 																	`${event.title} (${new Date(event.startDate!).toLocaleString()} - ${new Date(event.endDate!).toLocaleString()})\n` +
 984 | 																	`Location: ${event.location || "Not specified"}\n` +
 985 | 																	`Calendar: ${event.calendarName}\n` +
 986 | 																	`ID: ${event.id}\n` +
 987 | 																	`${event.notes ? `Notes: ${event.notes}\n` : ""}`,
 988 | 															)
 989 | 															.join("\n\n")}`
 990 | 													: `No events found matching "${searchText}".`,
 991 | 										},
 992 | 									],
 993 | 									isError: false,
 994 | 								};
 995 | 							}
 996 | 
 997 | 							case "open": {
 998 | 								const { eventId } = args;
 999 | 								const result = await calendarModule.openEvent(eventId!);
1000 | 
1001 | 								return {
1002 | 									content: [
1003 | 										{
1004 | 											type: "text",
1005 | 											text: result.success
1006 | 												? result.message
1007 | 												: `Error opening event: ${result.message}`,
1008 | 										},
1009 | 									],
1010 | 									isError: !result.success,
1011 | 								};
1012 | 							}
1013 | 
1014 | 							case "list": {
1015 | 								const { limit, fromDate, toDate } = args;
1016 | 								const events = await calendarModule.getEvents(
1017 | 									limit,
1018 | 									fromDate,
1019 | 									toDate,
1020 | 								);
1021 | 
1022 | 								const startDateText = fromDate
1023 | 									? new Date(fromDate).toLocaleDateString()
1024 | 									: "today";
1025 | 								const endDateText = toDate
1026 | 									? new Date(toDate).toLocaleDateString()
1027 | 									: "next 7 days";
1028 | 
1029 | 								return {
1030 | 									content: [
1031 | 										{
1032 | 											type: "text",
1033 | 											text:
1034 | 												events.length > 0
1035 | 													? `Found ${events.length} events from ${startDateText} to ${endDateText}:\n\n${events
1036 | 															.map(
1037 | 																(event) =>
1038 | 																	`${event.title} (${new Date(event.startDate!).toLocaleString()} - ${new Date(event.endDate!).toLocaleString()})\n` +
1039 | 																	`Location: ${event.location || "Not specified"}\n` +
1040 | 																	`Calendar: ${event.calendarName}\n` +
1041 | 																	`ID: ${event.id}`,
1042 | 															)
1043 | 															.join("\n\n")}`
1044 | 													: `No events found from ${startDateText} to ${endDateText}.`,
1045 | 										},
1046 | 									],
1047 | 									isError: false,
1048 | 								};
1049 | 							}
1050 | 
1051 | 							case "create": {
1052 | 								const {
1053 | 									title,
1054 | 									startDate,
1055 | 									endDate,
1056 | 									location,
1057 | 									notes,
1058 | 									isAllDay,
1059 | 									calendarName,
1060 | 								} = args;
1061 | 								const result = await calendarModule.createEvent(
1062 | 									title!,
1063 | 									startDate!,
1064 | 									endDate!,
1065 | 									location,
1066 | 									notes,
1067 | 									isAllDay,
1068 | 									calendarName,
1069 | 								);
1070 | 								return {
1071 | 									content: [
1072 | 										{
1073 | 											type: "text",
1074 | 											text: result.success
1075 | 												? `${result.message} Event scheduled from ${new Date(startDate!).toLocaleString()} to ${new Date(endDate!).toLocaleString()}${result.eventId ? `\nEvent ID: ${result.eventId}` : ""}`
1076 | 												: `Error creating event: ${result.message}`,
1077 | 										},
1078 | 									],
1079 | 									isError: !result.success,
1080 | 								};
1081 | 							}
1082 | 
1083 | 							default:
1084 | 								throw new Error(`Unknown calendar operation: ${operation}`);
1085 | 						}
1086 | 					} catch (error) {
1087 | 						const errorMessage = error instanceof Error ? error.message : String(error);
1088 | 						return {
1089 | 							content: [
1090 | 								{
1091 | 									type: "text",
1092 | 									text: errorMessage.includes("access") ? errorMessage : `Error in calendar tool: ${errorMessage}`,
1093 | 								},
1094 | 							],
1095 | 							isError: true,
1096 | 						};
1097 | 					}
1098 | 				}
1099 | 
1100 | 				case "maps": {
1101 | 					if (!isMapsArgs(args)) {
1102 | 						throw new Error("Invalid arguments for maps tool");
1103 | 					}
1104 | 
1105 | 					try {
1106 | 						const mapsModule = await loadModule("maps");
1107 | 						const { operation } = args;
1108 | 
1109 | 						switch (operation) {
1110 | 							case "search": {
1111 | 								const { query, limit } = args;
1112 | 								if (!query) {
1113 | 									throw new Error(
1114 | 										"Search query is required for search operation",
1115 | 									);
1116 | 								}
1117 | 
1118 | 								const result = await mapsModule.searchLocations(query, limit);
1119 | 
1120 | 								return {
1121 | 									content: [
1122 | 										{
1123 | 											type: "text",
1124 | 											text: result.success
1125 | 												? `${result.message}\n\n${result.locations
1126 | 														.map(
1127 | 															(location) =>
1128 | 																`Name: ${location.name}\n` +
1129 | 																`Address: ${location.address}\n` +
1130 | 																`${location.latitude && location.longitude ? `Coordinates: ${location.latitude}, ${location.longitude}\n` : ""}`,
1131 | 														)
1132 | 														.join("\n\n")}`
1133 | 												: `${result.message}`,
1134 | 										},
1135 | 									],
1136 | 									isError: !result.success,
1137 | 								};
1138 | 							}
1139 | 
1140 | 							case "save": {
1141 | 								const { name, address } = args;
1142 | 								if (!name || !address) {
1143 | 									throw new Error(
1144 | 										"Name and address are required for save operation",
1145 | 									);
1146 | 								}
1147 | 
1148 | 								const result = await mapsModule.saveLocation(name, address);
1149 | 
1150 | 								return {
1151 | 									content: [
1152 | 										{
1153 | 											type: "text",
1154 | 											text: result.message,
1155 | 										},
1156 | 									],
1157 | 									isError: !result.success,
1158 | 								};
1159 | 							}
1160 | 
1161 | 							case "pin": {
1162 | 								const { name, address } = args;
1163 | 								if (!name || !address) {
1164 | 									throw new Error(
1165 | 										"Name and address are required for pin operation",
1166 | 									);
1167 | 								}
1168 | 
1169 | 								const result = await mapsModule.dropPin(name, address);
1170 | 
1171 | 								return {
1172 | 									content: [
1173 | 										{
1174 | 											type: "text",
1175 | 											text: result.message,
1176 | 										},
1177 | 									],
1178 | 									isError: !result.success,
1179 | 								};
1180 | 							}
1181 | 
1182 | 							case "directions": {
1183 | 								const { fromAddress, toAddress, transportType } = args;
1184 | 								if (!fromAddress || !toAddress) {
1185 | 									throw new Error(
1186 | 										"From and to addresses are required for directions operation",
1187 | 									);
1188 | 								}
1189 | 
1190 | 								const result = await mapsModule.getDirections(
1191 | 									fromAddress,
1192 | 									toAddress,
1193 | 									transportType as "driving" | "walking" | "transit",
1194 | 								);
1195 | 
1196 | 								return {
1197 | 									content: [
1198 | 										{
1199 | 											type: "text",
1200 | 											text: result.message,
1201 | 										},
1202 | 									],
1203 | 									isError: !result.success,
1204 | 								};
1205 | 							}
1206 | 
1207 | 							case "listGuides": {
1208 | 								const result = await mapsModule.listGuides();
1209 | 
1210 | 								return {
1211 | 									content: [
1212 | 										{
1213 | 											type: "text",
1214 | 											text: result.message,
1215 | 										},
1216 | 									],
1217 | 									isError: !result.success,
1218 | 								};
1219 | 							}
1220 | 
1221 | 							case "addToGuide": {
1222 | 								const { address, guideName } = args;
1223 | 								if (!address || !guideName) {
1224 | 									throw new Error(
1225 | 										"Address and guideName are required for addToGuide operation",
1226 | 									);
1227 | 								}
1228 | 
1229 | 								const result = await mapsModule.addToGuide(address, guideName);
1230 | 
1231 | 								return {
1232 | 									content: [
1233 | 										{
1234 | 											type: "text",
1235 | 											text: result.message,
1236 | 										},
1237 | 									],
1238 | 									isError: !result.success,
1239 | 								};
1240 | 							}
1241 | 
1242 | 							case "createGuide": {
1243 | 								const { guideName } = args;
1244 | 								if (!guideName) {
1245 | 									throw new Error(
1246 | 										"Guide name is required for createGuide operation",
1247 | 									);
1248 | 								}
1249 | 
1250 | 								const result = await mapsModule.createGuide(guideName);
1251 | 
1252 | 								return {
1253 | 									content: [
1254 | 										{
1255 | 											type: "text",
1256 | 											text: result.message,
1257 | 										},
1258 | 									],
1259 | 									isError: !result.success,
1260 | 								};
1261 | 							}
1262 | 
1263 | 							default:
1264 | 								throw new Error(`Unknown maps operation: ${operation}`);
1265 | 						}
1266 | 					} catch (error) {
1267 | 						const errorMessage = error instanceof Error ? error.message : String(error);
1268 | 						return {
1269 | 							content: [
1270 | 								{
1271 | 									type: "text",
1272 | 									text: errorMessage.includes("access") ? errorMessage : `Error in maps tool: ${errorMessage}`,
1273 | 								},
1274 | 							],
1275 | 							isError: true,
1276 | 						};
1277 | 					}
1278 | 				}
1279 | 
1280 | 				default:
1281 | 					return {
1282 | 						content: [{ type: "text", text: `Unknown tool: ${name}` }],
1283 | 						isError: true,
1284 | 					};
1285 | 			}
1286 | 		} catch (error) {
1287 | 			return {
1288 | 				content: [
1289 | 					{
1290 | 						type: "text",
1291 | 						text: `Error: ${error instanceof Error ? error.message : String(error)}`,
1292 | 					},
1293 | 				],
1294 | 				isError: true,
1295 | 			};
1296 | 		}
1297 | 	});
1298 | 
1299 | 	// Start the server transport
1300 | 	console.error("Setting up MCP server transport...");
1301 | 
1302 | 	(async () => {
1303 | 		try {
1304 | 			console.error("Initializing transport...");
1305 | 			const transport = new StdioServerTransport();
1306 | 
1307 | 			// Ensure stdout is only used for JSON messages
1308 | 			console.error("Setting up stdout filter...");
1309 | 			const originalStdoutWrite = process.stdout.write.bind(process.stdout);
1310 | 			process.stdout.write = (chunk: any, encoding?: any, callback?: any) => {
1311 | 				// Only allow JSON messages to pass through
1312 | 				if (typeof chunk === "string" && !chunk.startsWith("{")) {
1313 | 					console.error("Filtering non-JSON stdout message");
1314 | 					return true; // Silently skip non-JSON messages
1315 | 				}
1316 | 				return originalStdoutWrite(chunk, encoding, callback);
1317 | 			};
1318 | 
1319 | 			console.error("Connecting transport to server...");
1320 | 			await server.connect(transport);
1321 | 			console.error("Server connected successfully!");
1322 | 		} catch (error) {
1323 | 			console.error("Failed to initialize MCP server:", error);
1324 | 			process.exit(1);
1325 | 		}
1326 | 	})();
1327 | }
1328 | 
1329 | // Helper functions for argument type checking
1330 | function isContactsArgs(args: unknown): args is { name?: string } {
1331 | 	return (
1332 | 		typeof args === "object" &&
1333 | 		args !== null &&
1334 | 		(!("name" in args) || typeof (args as { name: string }).name === "string")
1335 | 	);
1336 | }
1337 | 
1338 | function isNotesArgs(args: unknown): args is {
1339 | 	operation: "search" | "list" | "create";
1340 | 	searchText?: string;
1341 | 	title?: string;
1342 | 	body?: string;
1343 | 	folderName?: string;
1344 | } {
1345 | 	if (typeof args !== "object" || args === null) {
1346 | 		return false;
1347 | 	}
1348 | 
1349 | 	const { operation } = args as { operation?: unknown };
1350 | 	if (typeof operation !== "string") {
1351 | 		return false;
1352 | 	}
1353 | 
1354 | 	if (!["search", "list", "create"].includes(operation)) {
1355 | 		return false;
1356 | 	}
1357 | 
1358 | 	// Validate fields based on operation
1359 | 	if (operation === "search") {
1360 | 		const { searchText } = args as { searchText?: unknown };
1361 | 		if (typeof searchText !== "string" || searchText === "") {
1362 | 			return false;
1363 | 		}
1364 | 	}
1365 | 
1366 | 	if (operation === "create") {
1367 | 		const { title, body } = args as { title?: unknown; body?: unknown };
1368 | 		if (typeof title !== "string" || title === "" || typeof body !== "string") {
1369 | 			return false;
1370 | 		}
1371 | 
1372 | 		// Check folderName if provided
1373 | 		const { folderName } = args as { folderName?: unknown };
1374 | 		if (
1375 | 			folderName !== undefined &&
1376 | 			(typeof folderName !== "string" || folderName === "")
1377 | 		) {
1378 | 			return false;
1379 | 		}
1380 | 	}
1381 | 
1382 | 	return true;
1383 | }
1384 | 
1385 | function isMessagesArgs(args: unknown): args is {
1386 | 	operation: "send" | "read" | "schedule" | "unread";
1387 | 	phoneNumber?: string;
1388 | 	message?: string;
1389 | 	limit?: number;
1390 | 	scheduledTime?: string;
1391 | } {
1392 | 	if (typeof args !== "object" || args === null) return false;
1393 | 
1394 | 	const { operation, phoneNumber, message, limit, scheduledTime } = args as any;
1395 | 
1396 | 	if (
1397 | 		!operation ||
1398 | 		!["send", "read", "schedule", "unread"].includes(operation)
1399 | 	) {
1400 | 		return false;
1401 | 	}
1402 | 
1403 | 	// Validate required fields based on operation
1404 | 	switch (operation) {
1405 | 		case "send":
1406 | 		case "schedule":
1407 | 			if (!phoneNumber || !message) return false;
1408 | 			if (operation === "schedule" && !scheduledTime) return false;
1409 | 			break;
1410 | 		case "read":
1411 | 			if (!phoneNumber) return false;
1412 | 			break;
1413 | 		case "unread":
1414 | 			// No additional required fields
1415 | 			break;
1416 | 	}
1417 | 
1418 | 	// Validate field types if present
1419 | 	if (phoneNumber && typeof phoneNumber !== "string") return false;
1420 | 	if (message && typeof message !== "string") return false;
1421 | 	if (limit && typeof limit !== "number") return false;
1422 | 	if (scheduledTime && typeof scheduledTime !== "string") return false;
1423 | 
1424 | 	return true;
1425 | }
1426 | 
1427 | function isMailArgs(args: unknown): args is {
1428 | 	operation: "unread" | "search" | "send" | "mailboxes" | "accounts" | "latest";
1429 | 	account?: string;
1430 | 	mailbox?: string;
1431 | 	limit?: number;
1432 | 	searchTerm?: string;
1433 | 	to?: string;
1434 | 	subject?: string;
1435 | 	body?: string;
1436 | 	cc?: string;
1437 | 	bcc?: string;
1438 | } {
1439 | 	if (typeof args !== "object" || args === null) return false;
1440 | 
1441 | 	const {
1442 | 		operation,
1443 | 		account,
1444 | 		mailbox,
1445 | 		limit,
1446 | 		searchTerm,
1447 | 		to,
1448 | 		subject,
1449 | 		body,
1450 | 		cc,
1451 | 		bcc,
1452 | 	} = args as any;
1453 | 
1454 | 	if (
1455 | 		!operation ||
1456 | 		!["unread", "search", "send", "mailboxes", "accounts", "latest"].includes(
1457 | 			operation,
1458 | 		)
1459 | 	) {
1460 | 		return false;
1461 | 	}
1462 | 
1463 | 	// Validate required fields based on operation
1464 | 	switch (operation) {
1465 | 		case "search":
1466 | 			if (!searchTerm || typeof searchTerm !== "string") return false;
1467 | 			break;
1468 | 		case "send":
1469 | 			if (
1470 | 				!to ||
1471 | 				typeof to !== "string" ||
1472 | 				!subject ||
1473 | 				typeof subject !== "string" ||
1474 | 				!body ||
1475 | 				typeof body !== "string"
1476 | 			)
1477 | 				return false;
1478 | 			break;
1479 | 		case "unread":
1480 | 		case "mailboxes":
1481 | 		case "accounts":
1482 | 		case "latest":
1483 | 			// No additional required fields
1484 | 			break;
1485 | 	}
1486 | 
1487 | 	// Validate field types if present
1488 | 	if (account && typeof account !== "string") return false;
1489 | 	if (mailbox && typeof mailbox !== "string") return false;
1490 | 	if (limit && typeof limit !== "number") return false;
1491 | 	if (cc && typeof cc !== "string") return false;
1492 | 	if (bcc && typeof bcc !== "string") return false;
1493 | 
1494 | 	return true;
1495 | }
1496 | 
1497 | function isRemindersArgs(args: unknown): args is {
1498 | 	operation: "list" | "search" | "open" | "create" | "listById";
1499 | 	searchText?: string;
1500 | 	name?: string;
1501 | 	listName?: string;
1502 | 	listId?: string;
1503 | 	props?: string[];
1504 | 	notes?: string;
1505 | 	dueDate?: string;
1506 | } {
1507 | 	if (typeof args !== "object" || args === null) {
1508 | 		return false;
1509 | 	}
1510 | 
1511 | 	const { operation } = args as any;
1512 | 	if (typeof operation !== "string") {
1513 | 		return false;
1514 | 	}
1515 | 
1516 | 	if (!["list", "search", "open", "create", "listById"].includes(operation)) {
1517 | 		return false;
1518 | 	}
1519 | 
1520 | 	// For search and open operations, searchText is required
1521 | 	if (
1522 | 		(operation === "search" || operation === "open") &&
1523 | 		(typeof (args as any).searchText !== "string" ||
1524 | 			(args as any).searchText === "")
1525 | 	) {
1526 | 		return false;
1527 | 	}
1528 | 
1529 | 	// For create operation, name is required
1530 | 	if (
1531 | 		operation === "create" &&
1532 | 		(typeof (args as any).name !== "string" || (args as any).name === "")
1533 | 	) {
1534 | 		return false;
1535 | 	}
1536 | 
1537 | 	// For listById operation, listId is required
1538 | 	if (
1539 | 		operation === "listById" &&
1540 | 		(typeof (args as any).listId !== "string" || (args as any).listId === "")
1541 | 	) {
1542 | 		return false;
1543 | 	}
1544 | 
1545 | 	return true;
1546 | }
1547 | 
1548 | 
1549 | function isCalendarArgs(args: unknown): args is {
1550 | 	operation: "search" | "open" | "list" | "create";
1551 | 	searchText?: string;
1552 | 	eventId?: string;
1553 | 	limit?: number;
1554 | 	fromDate?: string;
1555 | 	toDate?: string;
1556 | 	title?: string;
1557 | 	startDate?: string;
1558 | 	endDate?: string;
1559 | 	location?: string;
1560 | 	notes?: string;
1561 | 	isAllDay?: boolean;
1562 | 	calendarName?: string;
1563 | } {
1564 | 	if (typeof args !== "object" || args === null) {
1565 | 		return false;
1566 | 	}
1567 | 
1568 | 	const { operation } = args as { operation?: unknown };
1569 | 	if (typeof operation !== "string") {
1570 | 		return false;
1571 | 	}
1572 | 
1573 | 	if (!["search", "open", "list", "create"].includes(operation)) {
1574 | 		return false;
1575 | 	}
1576 | 
1577 | 	// Check that required parameters are present for each operation
1578 | 	if (operation === "search") {
1579 | 		const { searchText } = args as { searchText?: unknown };
1580 | 		if (typeof searchText !== "string") {
1581 | 			return false;
1582 | 		}
1583 | 	}
1584 | 
1585 | 	if (operation === "open") {
1586 | 		const { eventId } = args as { eventId?: unknown };
1587 | 		if (typeof eventId !== "string") {
1588 | 			return false;
1589 | 		}
1590 | 	}
1591 | 
1592 | 	if (operation === "create") {
1593 | 		const { title, startDate, endDate } = args as {
1594 | 			title?: unknown;
1595 | 			startDate?: unknown;
1596 | 			endDate?: unknown;
1597 | 		};
1598 | 
1599 | 		if (
1600 | 			typeof title !== "string" ||
1601 | 			typeof startDate !== "string" ||
1602 | 			typeof endDate !== "string"
1603 | 		) {
1604 | 			return false;
1605 | 		}
1606 | 	}
1607 | 
1608 | 	return true;
1609 | }
1610 | 
1611 | function isMapsArgs(args: unknown): args is {
1612 | 	operation:
1613 | 		| "search"
1614 | 		| "save"
1615 | 		| "directions"
1616 | 		| "pin"
1617 | 		| "listGuides"
1618 | 		| "addToGuide"
1619 | 		| "createGuide";
1620 | 	query?: string;
1621 | 	limit?: number;
1622 | 	name?: string;
1623 | 	address?: string;
1624 | 	fromAddress?: string;
1625 | 	toAddress?: string;
1626 | 	transportType?: string;
1627 | 	guideName?: string;
1628 | } {
1629 | 	if (typeof args !== "object" || args === null) {
1630 | 		return false;
1631 | 	}
1632 | 
1633 | 	const { operation } = args as { operation?: unknown };
1634 | 	if (typeof operation !== "string") {
1635 | 		return false;
1636 | 	}
1637 | 
1638 | 	if (
1639 | 		![
1640 | 			"search",
1641 | 			"save",
1642 | 			"directions",
1643 | 			"pin",
1644 | 			"listGuides",
1645 | 			"addToGuide",
1646 | 			"createGuide",
1647 | 		].includes(operation)
1648 | 	) {
1649 | 		return false;
1650 | 	}
1651 | 
1652 | 	// Check that required parameters are present for each operation
1653 | 	if (operation === "search") {
1654 | 		const { query } = args as { query?: unknown };
1655 | 		if (typeof query !== "string" || query === "") {
1656 | 			return false;
1657 | 		}
1658 | 	}
1659 | 
1660 | 	if (operation === "save" || operation === "pin") {
1661 | 		const { name, address } = args as { name?: unknown; address?: unknown };
1662 | 		if (
1663 | 			typeof name !== "string" ||
1664 | 			name === "" ||
1665 | 			typeof address !== "string" ||
1666 | 			address === ""
1667 | 		) {
1668 | 			return false;
1669 | 		}
1670 | 	}
1671 | 
1672 | 	if (operation === "directions") {
1673 | 		const { fromAddress, toAddress } = args as {
1674 | 			fromAddress?: unknown;
1675 | 			toAddress?: unknown;
1676 | 		};
1677 | 		if (
1678 | 			typeof fromAddress !== "string" ||
1679 | 			fromAddress === "" ||
1680 | 			typeof toAddress !== "string" ||
1681 | 			toAddress === ""
1682 | 		) {
1683 | 			return false;
1684 | 		}
1685 | 
1686 | 		// Check transportType if provided
1687 | 		const { transportType } = args as { transportType?: unknown };
1688 | 		if (
1689 | 			transportType !== undefined &&
1690 | 			(typeof transportType !== "string" ||
1691 | 				!["driving", "walking", "transit"].includes(transportType))
1692 | 		) {
1693 | 			return false;
1694 | 		}
1695 | 	}
1696 | 
1697 | 	if (operation === "createGuide") {
1698 | 		const { guideName } = args as { guideName?: unknown };
1699 | 		if (typeof guideName !== "string" || guideName === "") {
1700 | 			return false;
1701 | 		}
1702 | 	}
1703 | 
1704 | 	if (operation === "addToGuide") {
1705 | 		const { address, guideName } = args as {
1706 | 			address?: unknown;
1707 | 			guideName?: unknown;
1708 | 		};
1709 | 		if (
1710 | 			typeof address !== "string" ||
1711 | 			address === "" ||
1712 | 			typeof guideName !== "string" ||
1713 | 			guideName === ""
1714 | 		) {
1715 | 			return false;
1716 | 		}
1717 | 	}
1718 | 
1719 | 	return true;
1720 | }
1721 | 
```
Page 2/2FirstPrevNextLast