apple-mcp
Version:
Apple MCP tools for contacts, notes, messages, and mail integration
796 lines (709 loc) • 189 kB
JavaScript
#!/usr/bin/env node
import{createRequire as q6}from"node:module";var z6=Object.create;var{getPrototypeOf:X6,defineProperty:e1,getOwnPropertyNames:B6}=Object;var G6=Object.prototype.hasOwnProperty;var w6=($,Q,J)=>{J=$!=null?z6(X6($)):{};let Y=Q||!$||!$.__esModule?e1(J,"default",{value:$,enumerable:!0}):J;for(let W of B6($))if(!G6.call(Y,W))e1(Y,W,{get:()=>$[W],enumerable:!0});return Y};var $8=($,Q)=>()=>(Q||$((Q={exports:{}}).exports,Q),Q.exports);var V0=($,Q)=>{for(var J in Q)e1($,J,{get:Q[J],enumerable:!0,configurable:!0,set:(Y)=>Q[J]=()=>Y})};var H0=($,Q)=>()=>($&&(Q=$($=0)),Q);var k1=q6(import.meta.url);import j$ from"node:process";import{promisify as K$}from"node:util";import{execFile as V$,execFileSync as a5}from"node:child_process";async function A($,{humanReadableOutput:Q=!0}={}){if(j$.platform!=="darwin")throw new Error("macOS only");let J=Q?[]:["-ss"],{stdout:Y}=await U$("osascript",["-e",$,J]);return Y.trim()}var U$;var w0=H0(()=>{U$=K$(V$)});var O8={};V0(O8,{default:()=>C$});async function A$(){try{return await A(`
tell application "Contacts"
return name
end tell`),!0}catch($){return console.error(`Cannot access Contacts app: ${$ instanceof Error?$.message:String($)}`),!1}}async function g1(){try{if(await A$())return{hasAccess:!0,message:"Contacts access is already granted."};return{hasAccess:!1,message:`Contacts access is required but not granted. Please:
1. Open System Settings > Privacy & Security > Automation
2. Find your terminal/app in the list and enable 'Contacts'
3. Alternatively, open System Settings > Privacy & Security > Contacts
4. Add your terminal/app to the allowed applications
5. Restart your terminal and try again`}}catch($){return{hasAccess:!1,message:`Error checking Contacts access: ${$ instanceof Error?$.message:String($)}`}}}async function Z1(){try{let $=await g1();if(!$.hasAccess)throw new Error($.message);let Q=`
tell application "Contacts"
set contactList to {}
set contactCount to 0
-- Get a limited number of people to avoid performance issues
set allPeople to people
repeat with i from 1 to (count of allPeople)
if contactCount >= ${F8.MAX_CONTACTS} then exit repeat
try
set currentPerson to item i of allPeople
set personName to name of currentPerson
set personPhones to {}
try
set phonesList to phones of currentPerson
repeat with phoneItem in phonesList
try
set phoneValue to value of phoneItem
if phoneValue is not "" then
set personPhones to personPhones & {phoneValue}
end if
on error
-- Skip problematic phone entries
end try
end repeat
on error
-- Skip if no phones or phones can't be accessed
end try
-- Only add contact if they have phones
if (count of personPhones) > 0 then
set contactInfo to {name:personName, phones:personPhones}
set contactList to contactList & {contactInfo}
set contactCount to contactCount + 1
end if
on error
-- Skip problematic contacts
end try
end repeat
return contactList
end tell`,J=await A(Q),Y=Array.isArray(J)?J:J?[J]:[],W={};for(let H of Y)if(H&&H.name&&H.phones)W[H.name]=Array.isArray(H.phones)?H.phones:[H.phones];return W}catch($){return console.error(`Error getting all contacts: ${$ instanceof Error?$.message:String($)}`),{}}}async function f$($){try{let Q=await g1();if(!Q.hasAccess)throw new Error(Q.message);if(!$||$.trim()==="")return[];let J=$.toLowerCase().trim(),Y=`
tell application "Contacts"
set matchedPhones to {}
set searchText to "${J}"
-- Get a limited number of people to search through
set allPeople to people
set foundExact to false
set partialMatches to {}
repeat with i from 1 to (count of allPeople)
if i > ${F8.MAX_CONTACTS} then exit repeat
try
set currentPerson to item i of allPeople
set personName to name of currentPerson
set lowerPersonName to (do shell script "echo " & quoted form of personName & " | tr '[:upper:]' '[:lower:]'")
-- Check for exact match first (highest priority)
if lowerPersonName is searchText then
try
set phonesList to phones of currentPerson
repeat with phoneItem in phonesList
try
set phoneValue to value of phoneItem
if phoneValue is not "" then
set matchedPhones to matchedPhones & {phoneValue}
set foundExact to true
end if
on error
-- Skip problematic phone entries
end try
end repeat
if foundExact then exit repeat
on error
-- Skip if no phones
end try
-- Check if search term is contained in name (partial match)
else if lowerPersonName contains searchText or searchText contains lowerPersonName then
try
set phonesList to phones of currentPerson
repeat with phoneItem in phonesList
try
set phoneValue to value of phoneItem
if phoneValue is not "" then
set partialMatches to partialMatches & {phoneValue}
end if
on error
-- Skip problematic phone entries
end try
end repeat
on error
-- Skip if no phones
end try
end if
on error
-- Skip problematic contacts
end try
end repeat
-- Return exact matches if found, otherwise partial matches
if foundExact then
return matchedPhones
else
return partialMatches
end if
end tell`,W=await A(Y),H=Array.isArray(W)?W:W?[W]:[];if(H.length===0){console.error(`No AppleScript matches for "${$}", trying comprehensive search...`);let z=await Z1(),B=(w)=>{return w.toLowerCase().replace(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu,"").replace(/[♥️❤️💙💚💛💜🧡🖤🤍🤎]/g,"").replace(/\s+/g," ").trim()},G=[(w)=>B(w)===J,(w)=>{let q=B(w),V=B($);return q===V},(w)=>B(w).startsWith(J),(w)=>B(w).includes(J),(w)=>J.includes(B(w)),(w)=>{let V=B(w).split(" ")[0];return V===J||V.startsWith(J)||J.startsWith(V)||V.replace(/(.)\1+/g,"$1")===J||J.replace(/(.)\1+/g,"$1")===V},(w)=>{let V=B(w).split(" "),F=V[V.length-1];return F===J||F.startsWith(J)},(w)=>{return B(w).split(" ").some((F)=>F.includes(J)||J.includes(F)||F.replace(/(.)\1+/g,"$1")===J)}];for(let w of G){let q=Object.keys(z).filter(w);if(q.length>0)return console.error(`Found ${q.length} matches using fuzzy strategy for "${$}": ${q.join(", ")}`),z[q[0]]||[]}}return H.filter((z)=>z&&z.trim()!=="")}catch(Q){console.error(`Error finding contact: ${Q instanceof Error?Q.message:String(Q)}`);try{let J=await Z1(),Y=$.toLowerCase().trim(),W=Object.keys(J).find((H)=>H.toLowerCase().includes(Y)||Y.includes(H.toLowerCase()));if(W)return console.error(`Fallback found match for "${$}": ${W}`),J[W]}catch(J){console.error(`Fallback search also failed: ${J}`)}return[]}}async function b$($){try{let Q=await g1();if(!Q.hasAccess)throw new Error(Q.message);if(!$||$.trim()==="")return null;let J=$.replace(/[^0-9+]/g,""),Y=`
tell application "Contacts"
set foundName to ""
set searchPhone to "${J}"
-- Get a limited number of people to search through
set allPeople to people
repeat with i from 1 to (count of allPeople)
if i > ${F8.MAX_CONTACTS} then exit repeat
if foundName is not "" then exit repeat
try
set currentPerson to item i of allPeople
try
set phonesList to phones of currentPerson
repeat with phoneItem in phonesList
try
set phoneValue to value of phoneItem
-- Normalize phone value for comparison
set normalizedPhone to phoneValue
-- Simple phone matching
if normalizedPhone contains searchPhone or searchPhone contains normalizedPhone then
set foundName to name of currentPerson
exit repeat
end if
on error
-- Skip problematic phone entries
end try
end repeat
on error
-- Skip if no phones
end try
on error
-- Skip problematic contacts
end try
end repeat
return foundName
end tell`,W=await A(Y);if(W&&W.trim()!=="")return W;let H=await Z1();for(let[z,B]of Object.entries(H))if(B.map((w)=>w.replace(/[^0-9+]/g,"")).some((w)=>w===J||w===`+${J}`||w===`+1${J}`||`+1${w}`===J||J.includes(w)||w.includes(J)))return z;return null}catch(Q){return console.error(`Error finding contact by phone: ${Q instanceof Error?Q.message:String(Q)}`),null}}var F8,C$;var L8=H0(()=>{w0();F8={MAX_CONTACTS:1000,TIMEOUT_MS:1e4};C$={getAllNumbers:Z1,findNumber:f$,findContactByPhone:b$,requestContactsAccess:g1}});var D8={};V0(D8,{default:()=>h$});async function P$(){try{return await A(`
tell application "Notes"
return name
end tell`),!0}catch($){return console.error(`Cannot access Notes app: ${$ instanceof Error?$.message:String($)}`),!1}}async function K1(){try{if(await P$())return{hasAccess:!0,message:"Notes access is already granted."};return{hasAccess:!1,message:`Notes access is required but not granted. Please:
1. Open System Settings > Privacy & Security > Automation
2. Find your terminal/app in the list and enable 'Notes'
3. Restart your terminal and try again
4. If the option is not available, run this command again to trigger the permission dialog`}}catch($){return{hasAccess:!1,message:`Error checking Notes access: ${$ instanceof Error?$.message:String($)}`}}}async function E$(){try{let $=await K1();if(!$.hasAccess)throw new Error($.message);let Q=`
tell application "Notes"
set notesList to {}
set noteCount to 0
-- Get all notes from all folders
set allNotes to notes
repeat with i from 1 to (count of allNotes)
if noteCount >= ${J0.MAX_NOTES} then exit repeat
try
set currentNote to item i of allNotes
set noteName to name of currentNote
set noteContent to plaintext of currentNote
-- Limit content for preview
if (length of noteContent) > ${J0.MAX_CONTENT_PREVIEW} then
set noteContent to (characters 1 thru ${J0.MAX_CONTENT_PREVIEW} of noteContent) as string
set noteContent to noteContent & "..."
end if
set noteInfo to {name:noteName, content:noteContent}
set notesList to notesList & {noteInfo}
set noteCount to noteCount + 1
on error
-- Skip problematic notes
end try
end repeat
return notesList
end tell`,J=await A(Q);return(Array.isArray(J)?J:J?[J]:[]).map((W)=>({name:W.name||"Untitled Note",content:W.content||"",creationDate:void 0,modificationDate:void 0}))}catch($){return console.error(`Error getting all notes: ${$ instanceof Error?$.message:String($)}`),[]}}async function N$($){try{let Q=await K1();if(!Q.hasAccess)throw new Error(Q.message);if(!$||$.trim()==="")return[];let Y=`
tell application "Notes"
set matchedNotes to {}
set noteCount to 0
set searchTerm to "${$.toLowerCase()}"
-- Get all notes and search through them
set allNotes to notes
repeat with i from 1 to (count of allNotes)
if noteCount >= ${J0.MAX_NOTES} then exit repeat
try
set currentNote to item i of allNotes
set noteName to name of currentNote
set noteContent to plaintext of currentNote
-- Simple case-insensitive search in name and content
if (noteName contains searchTerm) or (noteContent contains searchTerm) then
-- Limit content for preview
if (length of noteContent) > ${J0.MAX_CONTENT_PREVIEW} then
set noteContent to (characters 1 thru ${J0.MAX_CONTENT_PREVIEW} of noteContent) as string
set noteContent to noteContent & "..."
end if
set noteInfo to {name:noteName, content:noteContent}
set matchedNotes to matchedNotes & {noteInfo}
set noteCount to noteCount + 1
end if
on error
-- Skip problematic notes
end try
end repeat
return matchedNotes
end tell`,W=await A(Y);return(Array.isArray(W)?W:W?[W]:[]).map((z)=>({name:z.name||"Untitled Note",content:z.content||"",creationDate:void 0,modificationDate:void 0}))}catch(Q){return console.error(`Error finding notes: ${Q instanceof Error?Q.message:String(Q)}`),[]}}async function R$($,Q,J="Claude"){try{let Y=await K1();if(!Y.hasAccess)return{success:!1,message:Y.message};if(!$||$.trim()==="")return{success:!1,message:"Note title cannot be empty"};let W=Q.trim(),H=`/tmp/note-content-${Date.now()}.txt`,z=k1("fs");z.writeFileSync(H,W,"utf8");let B=`
tell application "Notes"
set targetFolder to null
set folderFound to false
set actualFolderName to "${J}"
-- Try to find the specified folder
try
set allFolders to folders
repeat with currentFolder in allFolders
if name of currentFolder is "${J}" then
set targetFolder to currentFolder
set folderFound to true
exit repeat
end if
end repeat
on error
-- Folders might not be accessible
end try
-- If folder not found and it's a test folder, try to create it
if not folderFound and ("${J}" is "Claude" or "${J}" is "Test-Claude") then
try
make new folder with properties {name:"${J}"}
-- Try to find it again
set allFolders to folders
repeat with currentFolder in allFolders
if name of currentFolder is "${J}" then
set targetFolder to currentFolder
set folderFound to true
set actualFolderName to "${J}"
exit repeat
end if
end repeat
on error
-- Folder creation failed, use default
set actualFolderName to "Notes"
end try
end if
-- Read content from file to preserve formatting
set noteContent to read file POSIX file "${H}" as «class utf8»
-- Create the note with proper content
if folderFound and targetFolder is not null then
-- Create note in specified folder
make new note at targetFolder with properties {name:"${$.replace(/"/g,"\\\"")}", body:noteContent}
return "SUCCESS:" & actualFolderName & ":false"
else
-- Create note in default location
make new note with properties {name:"${$.replace(/"/g,"\\\"")}", body:noteContent}
return "SUCCESS:Notes:true"
end if
end tell`,G=await A(B);try{z.unlinkSync(H)}catch(w){}if(G&&typeof G==="string"&&G.startsWith("SUCCESS:")){let w=G.split(":"),q=w[1]||"Notes",V=w[2]==="true";return{success:!0,note:{name:$,content:W},folderName:q,usedDefaultFolder:V}}else return{success:!1,message:`Failed to create note: ${G||"No result from AppleScript"}`}}catch(Y){return{success:!1,message:`Failed to create note: ${Y instanceof Error?Y.message:String(Y)}`}}}async function S8($){try{let Q=await K1();if(!Q.hasAccess)return{success:!1,message:Q.message};let J=`
tell application "Notes"
set notesList to {}
set noteCount to 0
set folderFound to false
-- Try to find the specified folder
try
set allFolders to folders
repeat with currentFolder in allFolders
if name of currentFolder is "${$}" then
set folderFound to true
-- Get notes from this folder
set folderNotes to notes of currentFolder
repeat with i from 1 to (count of folderNotes)
if noteCount >= ${J0.MAX_NOTES} then exit repeat
try
set currentNote to item i of folderNotes
set noteName to name of currentNote
set noteContent to plaintext of currentNote
-- Limit content for preview
if (length of noteContent) > ${J0.MAX_CONTENT_PREVIEW} then
set noteContent to (characters 1 thru ${J0.MAX_CONTENT_PREVIEW} of noteContent) as string
set noteContent to noteContent & "..."
end if
set noteInfo to {name:noteName, content:noteContent}
set notesList to notesList & {noteInfo}
set noteCount to noteCount + 1
on error
-- Skip problematic notes
end try
end repeat
exit repeat
end if
end repeat
on error
-- Handle folder access errors
end try
if not folderFound then
return "ERROR:Folder not found"
end if
return "SUCCESS:" & (count of notesList)
end tell`,Y=await A(J);if(Y&&typeof Y==="string"){if(Y.startsWith("ERROR:"))return{success:!1,message:Y.replace("ERROR:","")};else if(Y.startsWith("SUCCESS:"))return{success:!0,notes:[]}}return{success:!0,notes:[]}}catch(Q){return{success:!1,message:`Failed to get notes from folder: ${Q instanceof Error?Q.message:String(Q)}`}}}async function v$($,Q=5){try{let J=await S8($);if(J.success&&J.notes)return{success:!0,notes:J.notes.slice(0,Math.min(Q,J.notes.length))};return J}catch(J){return{success:!1,message:`Failed to get recent notes from folder: ${J instanceof Error?J.message:String(J)}`}}}async function I$($,Q,J,Y=20){try{let W=await S8($);if(W.success&&W.notes)return{success:!0,notes:W.notes.slice(0,Math.min(Y,W.notes.length))};return W}catch(W){return{success:!1,message:`Failed to get notes by date range: ${W instanceof Error?W.message:String(W)}`}}}var J0,h$;var k8=H0(()=>{w0();J0={MAX_NOTES:50,MAX_CONTENT_PREVIEW:200,TIMEOUT_MS:8000};h$={getAllNotes:E$,findNote:N$,createNote:R$,getNotesFromFolder:S8,getRecentNotesFromFolder:v$,getNotesByDateRange:I$,requestNotesAccess:K1}});var f8={};V0(f8,{default:()=>n$});import{promisify as T$}from"node:util";import{exec as x$}from"node:child_process";import{access as y$}from"node:fs/promises";async function l$($){return new Promise((Q)=>setTimeout(Q,$))}async function M8($,Q=Z$,J=g$){try{return await $()}catch(Y){if(Q>0)return console.error(`Operation failed, retrying... (${Q} attempts remaining)`),await l$(J),M8($,Q-1,J);throw Y}}function m$($){let Q=$.replace(/[^0-9+]/g,"");if(/^\+1\d{10}$/.test(Q))return[Q];if(/^1\d{10}$/.test(Q))return[`+${Q}`];if(/^\d{10}$/.test(Q))return[`+1${Q}`];let J=new Set;if(Q.startsWith("+1"))J.add(Q);else if(Q.startsWith("1"))J.add(`+${Q}`);else J.add(`+1${Q}`);return Array.from(J)}async function R4($,Q){let J=Q.replace(/"/g,"\\\"");return await A(`
tell application "Messages"
set targetService to 1st service whose service type = iMessage
set targetBuddy to buddy "${$}"
send "${J}" to targetBuddy
end tell`)}async function u$(){try{let $=`${process.env.HOME}/Library/Messages/chat.db`;return await y$($),await l1(`sqlite3 "${$}" "SELECT 1;"`),!0}catch($){return console.error(`
Error: Cannot access Messages database.
To fix this, please grant Full Disk Access to Terminal/iTerm2:
1. Open System Preferences
2. Go to Security & Privacy > Privacy
3. Select "Full Disk Access" from the left sidebar
4. Click the lock icon to make changes
5. Add Terminal.app or iTerm.app to the list
6. Restart your terminal and try again
Error details: ${$ instanceof Error?$.message:String($)}
`),!1}}async function A8(){try{if(await u$())return{hasAccess:!0,message:"Messages access is already granted."};try{return await A('tell application "Messages" to return name'),{hasAccess:!1,message:`Messages app is accessible but database access is required. Please:
1. Open System Settings > Privacy & Security > Full Disk Access
2. Add your terminal application (Terminal.app or iTerm.app)
3. Restart your terminal and try again
4. Note: This is required to read message history from the Messages database`}}catch(Q){return{hasAccess:!1,message:`Messages access is required but not granted. Please:
1. Open System Settings > Privacy & Security > Automation
2. Find your terminal/app and enable 'Messages'
3. Also grant Full Disk Access in Privacy & Security > Full Disk Access
4. Restart your terminal and try again`}}}catch($){return{hasAccess:!1,message:`Error checking Messages access: ${$ instanceof Error?$.message:String($)}`}}}function v4($){try{let J=Buffer.from($,"hex").toString(),Y=[/NSString">(.*?)</,/NSString">([^<]+)/,/NSNumber">\d+<.*?NSString">(.*?)</,/NSArray">.*?NSString">(.*?)</,/"string":\s*"([^"]+)"/,/text[^>]*>(.*?)</,/message>(.*?)</],W="";for(let B of Y){let G=J.match(B);if(G?.[1]){if(W=G[1],W.length>5)break}}let H=[/(https?:\/\/[^\s<"]+)/,/NSString">(https?:\/\/[^\s<"]+)/,/"url":\s*"(https?:\/\/[^"]+)"/,/link[^>]*>(https?:\/\/[^<]+)/],z;for(let B of H){let G=J.match(B);if(G?.[1]){z=G[1];break}}if(!W&&!z){let B=J.replace(/streamtyped.*?NSString/g,"").replace(/NSAttributedString.*?NSString/g,"").replace(/NSDictionary.*?$/g,"").replace(/\+[A-Za-z]+\s/g,"").replace(/NSNumber.*?NSValue.*?\*/g,"").replace(/[^\x20-\x7E]/g," ").replace(/\s+/g," ").trim();if(B.length>5)W=B;else return{text:"[Message content not readable]"}}if(W)W=W.replace(/^[+\s]+/,"").replace(/\s*iI\s*[A-Z]\s*$/,"").replace(/\s+/g," ").trim();return{text:W||z||"",url:z}}catch(Q){return console.error("Error decoding attributedBody:",Q),{text:"[Message content not readable]"}}}async function I4($){try{let Q=`
SELECT filename
FROM attachment
INNER JOIN message_attachment_join
ON attachment.ROWID = message_attachment_join.attachment_id
WHERE message_attachment_join.message_id = ${$}
`,{stdout:J}=await l1(`sqlite3 -json "${process.env.HOME}/Library/Messages/chat.db" "${Q}"`);if(!J.trim())return[];return JSON.parse(J).map((W)=>W.filename).filter(Boolean)}catch(Q){return console.error("Error getting attachments:",Q),[]}}async function p$($,Q=10){try{let J=Math.min(Q,N4.MAX_MESSAGES),Y=await A8();if(!Y.hasAccess)throw new Error(Y.message);let W=m$($);console.error("Trying phone formats:",W);let z=`
SELECT
m.ROWID as message_id,
CASE
WHEN m.text IS NOT NULL AND m.text != '' THEN m.text
WHEN m.attributedBody IS NOT NULL THEN hex(m.attributedBody)
ELSE NULL
END as content,
datetime(m.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as date,
h.id as sender,
m.is_from_me,
m.is_audio_message,
m.cache_has_attachments,
m.subject,
CASE
WHEN m.text IS NOT NULL AND m.text != '' THEN 0
WHEN m.attributedBody IS NOT NULL THEN 1
ELSE 2
END as content_type
FROM message m
INNER JOIN handle h ON h.ROWID = m.handle_id
WHERE h.id IN (${W.map((q)=>`'${q.replace(/'/g,"''")}'`).join(",")})
AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL OR m.cache_has_attachments = 1)
AND m.is_from_me IS NOT NULL -- Ensure it's a real message
AND m.item_type = 0 -- Regular messages only
AND m.is_audio_message = 0 -- Skip audio messages
ORDER BY m.date DESC
LIMIT ${J}
`,{stdout:B}=await M8(()=>l1(`sqlite3 -json "${process.env.HOME}/Library/Messages/chat.db" "${z}"`));if(!B.trim())return console.error("No messages found in database for the given phone number"),[];let G=JSON.parse(B);return await Promise.all(G.filter((q)=>q.content!==null||q.cache_has_attachments===1).map(async(q)=>{let V=q.content||"",F;if(q.content_type===1){let T=v4(V);V=T.text,F=T.url}else{let T=V.match(/(https?:\/\/[^\s]+)/);if(T)F=T[1]}let C=[];if(q.cache_has_attachments)C=await I4(q.message_id);if(q.subject)V=`Subject: ${q.subject}
${V}`;let I={content:V||"[No text content]",date:new Date(q.date).toISOString(),sender:q.sender,is_from_me:Boolean(q.is_from_me)};if(C.length>0)I.attachments=C,I.content+=`
[Attachments: ${C.length}]`;if(F)I.url=F,I.content+=`
[URL: ${F}]`;return I}))}catch(J){if(console.error("Error reading messages:",J),J instanceof Error)console.error("Error details:",J.message),console.error("Stack trace:",J.stack);return[]}}async function c$($=10){try{let Q=Math.min($,N4.MAX_MESSAGES),J=await A8();if(!J.hasAccess)throw new Error(J.message);let Y=`
SELECT
m.ROWID as message_id,
CASE
WHEN m.text IS NOT NULL AND m.text != '' THEN m.text
WHEN m.attributedBody IS NOT NULL THEN hex(m.attributedBody)
ELSE NULL
END as content,
datetime(m.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as date,
h.id as sender,
m.is_from_me,
m.is_audio_message,
m.cache_has_attachments,
m.subject,
CASE
WHEN m.text IS NOT NULL AND m.text != '' THEN 0
WHEN m.attributedBody IS NOT NULL THEN 1
ELSE 2
END as content_type
FROM message m
INNER JOIN handle h ON h.ROWID = m.handle_id
WHERE m.is_from_me = 0 -- Only messages from others
AND m.is_read = 0 -- Only unread messages
AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL OR m.cache_has_attachments = 1)
AND m.is_audio_message = 0 -- Skip audio messages
AND m.item_type = 0 -- Regular messages only
ORDER BY m.date DESC
LIMIT ${Q}
`,{stdout:W}=await M8(()=>l1(`sqlite3 -json "${process.env.HOME}/Library/Messages/chat.db" "${Y}"`));if(!W.trim())return console.error("No unread messages found"),[];let H=JSON.parse(W);return await Promise.all(H.filter((B)=>B.content!==null||B.cache_has_attachments===1).map(async(B)=>{let G=B.content||"",w;if(B.content_type===1){let F=v4(G);G=F.text,w=F.url}else{let F=G.match(/(https?:\/\/[^\s]+)/);if(F)w=F[1]}let q=[];if(B.cache_has_attachments)q=await I4(B.message_id);if(B.subject)G=`Subject: ${B.subject}
${G}`;let V={content:G||"[No text content]",date:new Date(B.date).toISOString(),sender:B.sender,is_from_me:Boolean(B.is_from_me)};if(q.length>0)V.attachments=q,V.content+=`
[Attachments: ${q.length}]`;if(w)V.url=w,V.content+=`
[URL: ${w}]`;return V}))}catch(Q){if(console.error("Error reading unread messages:",Q),Q instanceof Error)console.error("Error details:",Q.message),console.error("Stack trace:",Q.stack);return[]}}async function d$($,Q,J){let Y=new Map,W=J.getTime()-Date.now();if(W<0)throw new Error("Cannot schedule message in the past");let H=setTimeout(async()=>{try{await R4($,Q),Y.delete(H)}catch(z){console.error("Failed to send scheduled message:",z)}},W);return Y.set(H,{phoneNumber:$,message:Q,scheduledTime:J,timeoutId:H}),{id:H,scheduledTime:J,message:Q,phoneNumber:$}}var l1,N4,Z$=3,g$=1000,n$;var b8=H0(()=>{w0();l1=T$(x$),N4={MAX_MESSAGES:50,MAX_CONTENT_PREVIEW:300,TIMEOUT_MS:8000};n$={sendMessage:R4,readMessages:p$,scheduleMessage:d$,getUnreadMessages:c$,requestMessagesAccess:A8}});var C8={};V0(C8,{default:()=>QQ});async function i$(){try{return await A(`
tell application "Mail"
return name
end tell`),!0}catch($){return console.error(`Cannot access Mail app: ${$ instanceof Error?$.message:String($)}`),!1}}async function q0(){try{if(await i$())return{hasAccess:!0,message:"Mail access is already granted."};return{hasAccess:!1,message:`Mail access is required but not granted. Please:
1. Open System Settings > Privacy & Security > Automation
2. Find your terminal/app in the list and enable 'Mail'
3. Make sure Mail app is running and configured with at least one account
4. Restart your terminal and try again`}}catch($){return{hasAccess:!1,message:`Error checking Mail access: ${$ instanceof Error?$.message:String($)}`}}}async function o$($=10){try{let Q=await q0();if(!Q.hasAccess)throw new Error(Q.message);let J=Math.min($,m0.MAX_EMAILS),Y=`
tell application "Mail"
set emailList to {}
set emailCount to 0
-- Get mailboxes (limited to avoid performance issues)
set allMailboxes to mailboxes
repeat with i from 1 to (count of allMailboxes)
if emailCount >= ${J} then exit repeat
try
set currentMailbox to item i of allMailboxes
set mailboxName to name of currentMailbox
-- Get unread messages from this mailbox
set unreadMessages to messages of currentMailbox
repeat with j from 1 to (count of unreadMessages)
if emailCount >= ${J} then exit repeat
try
set currentMsg to item j of unreadMessages
-- Only process unread messages
if read status of currentMsg is false then
set emailSubject to subject of currentMsg
set emailSender to sender of currentMsg
set emailDate to (date sent of currentMsg) as string
-- Get content with length limit
set emailContent to ""
try
set fullContent to content of currentMsg
if (length of fullContent) > ${m0.MAX_CONTENT_PREVIEW} then
set emailContent to (characters 1 thru ${m0.MAX_CONTENT_PREVIEW} of fullContent) as string
set emailContent to emailContent & "..."
else
set emailContent to fullContent
end if
on error
set emailContent to "[Content not available]"
end try
set emailInfo to {subject:emailSubject, sender:emailSender, dateSent:emailDate, content:emailContent, isRead:false, mailbox:mailboxName}
set emailList to emailList & {emailInfo}
set emailCount to emailCount + 1
end if
on error
-- Skip problematic messages
end try
end repeat
on error
-- Skip problematic mailboxes
end try
end repeat
return "SUCCESS:" & (count of emailList)
end tell`,W=await A(Y);if(W&&W.startsWith("SUCCESS:"))return[];return[]}catch(Q){return console.error(`Error getting unread emails: ${Q instanceof Error?Q.message:String(Q)}`),[]}}async function a$($,Q=10){try{let J=await q0();if(!J.hasAccess)throw new Error(J.message);if(!$||$.trim()==="")return[];let Y=Math.min(Q,m0.MAX_EMAILS),H=`
tell application "Mail"
set emailList to {}
set emailCount to 0
set searchTerm to "${$.toLowerCase()}"
-- Get mailboxes (limited to avoid performance issues)
set allMailboxes to mailboxes
repeat with i from 1 to (count of allMailboxes)
if emailCount >= ${Y} then exit repeat
try
set currentMailbox to item i of allMailboxes
set mailboxName to name of currentMailbox
-- Get messages from this mailbox
set allMessages to messages of currentMailbox
repeat with j from 1 to (count of allMessages)
if emailCount >= ${Y} then exit repeat
try
set currentMsg to item j of allMessages
set emailSubject to subject of currentMsg
-- Simple case-insensitive search in subject
if emailSubject contains searchTerm then
set emailSender to sender of currentMsg
set emailDate to (date sent of currentMsg) as string
set emailRead to read status of currentMsg
-- Get content with length limit
set emailContent to ""
try
set fullContent to content of currentMsg
if (length of fullContent) > ${m0.MAX_CONTENT_PREVIEW} then
set emailContent to (characters 1 thru ${m0.MAX_CONTENT_PREVIEW} of fullContent) as string
set emailContent to emailContent & "..."
else
set emailContent to fullContent
end if
on error
set emailContent to "[Content not available]"
end try
set emailInfo to {subject:emailSubject, sender:emailSender, dateSent:emailDate, content:emailContent, isRead:emailRead, mailbox:mailboxName}
set emailList to emailList & {emailInfo}
set emailCount to emailCount + 1
end if
on error
-- Skip problematic messages
end try
end repeat
on error
-- Skip problematic mailboxes
end try
end repeat
return "SUCCESS:" & (count of emailList)
end tell`,z=await A(H);if(z&&z.startsWith("SUCCESS:"))return[];return[]}catch(J){return console.error(`Error searching emails: ${J instanceof Error?J.message:String(J)}`),[]}}async function r$($,Q,J,Y,W){try{let H=await q0();if(!H.hasAccess)throw new Error(H.message);if(!$||!$.trim())throw new Error("To address is required");if(!Q||!Q.trim())throw new Error("Subject is required");if(!J||!J.trim())throw new Error("Email body is required");let z=`/tmp/email-body-${Date.now()}.txt`,B=k1("fs");B.writeFileSync(z,J.trim(),"utf8");let G=`
tell application "Mail"
activate
-- Read email body from file to preserve formatting
set emailBody to read file POSIX file "${z}" as «class utf8»
-- Create new message
set newMessage to make new outgoing message with properties {subject:"${Q.replace(/"/g,"\\\"")}", content:emailBody, visible:true}
tell newMessage
make new to recipient with properties {address:"${$.replace(/"/g,"\\\"")}"}
${Y?`make new cc recipient with properties {address:"${Y.replace(/"/g,"\\\"")}"}`:""}
${W?`make new bcc recipient with properties {address:"${W.replace(/"/g,"\\\"")}"}`:""}
end tell
send newMessage
return "SUCCESS"
end tell`,w=await A(G);try{B.unlinkSync(z)}catch(q){}if(w==="SUCCESS")return`Email sent to ${$} with subject "${Q}"`;else throw new Error("Failed to send email")}catch(H){throw console.error(`Error sending email: ${H instanceof Error?H.message:String(H)}`),new Error(`Error sending email: ${H instanceof Error?H.message:String(H)}`)}}async function t$(){try{let $=await q0();if(!$.hasAccess)throw new Error($.message);let J=await A(`
tell application "Mail"
try
-- Simple check - try to get just the count first
set mailboxCount to count of mailboxes
if mailboxCount > 0 then
return {"Inbox", "Sent", "Drafts"}
else
return {}
end if
on error
return {}
end try
end tell`);if(Array.isArray(J))return J.filter((Y)=>Y&&typeof Y==="string");return[]}catch($){return console.error(`Error getting mailboxes: ${$ instanceof Error?$.message:String($)}`),[]}}async function s$(){try{let $=await q0();if(!$.hasAccess)throw new Error($.message);let J=await A(`
tell application "Mail"
try
-- Simple check - try to get just the count first
set accountCount to count of accounts
if accountCount > 0 then
return {"Default Account"}
else
return {}
end if
on error
return {}
end try
end tell`);if(Array.isArray(J))return J.filter((Y)=>Y&&typeof Y==="string");return[]}catch($){return console.error(`Error getting accounts: ${$ instanceof Error?$.message:String($)}`),[]}}async function e$($){try{let Q=await q0();if(!Q.hasAccess)throw new Error(Q.message);if(!$||!$.trim())return[];let J=`
tell application "Mail"
set boxList to {}
try
-- Find the account
set targetAccount to first account whose name is "${$.replace(/"/g,"\\\"")}"
set accountMailboxes to mailboxes of targetAccount
repeat with i from 1 to (count of accountMailboxes)
try
set currentMailbox to item i of accountMailboxes
set mailboxName to name of currentMailbox
set boxList to boxList & {mailboxName}
on error
-- Skip problematic mailboxes
end try
end repeat
on error
-- Account not found or other error
return {}
end try
return boxList
end tell`,Y=await A(J);if(Array.isArray(Y))return Y.filter((W)=>W&&typeof W==="string");return[]}catch(Q){return console.error(`Error getting mailboxes for account: ${Q instanceof Error?Q.message:String(Q)}`),[]}}async function $Q($,Q=5){try{let J=await q0();if(!J.hasAccess)throw new Error(J.message);let Y=`
tell application "Mail"
set resultList to {}
try
set targetAccount to first account whose name is "${$.replace(/"/g,"\\\"")}"
set acctMailboxes to every mailbox of targetAccount
repeat with mb in acctMailboxes
try
set messagesList to (messages of mb)
set sortedMessages to my sortMessagesByDate(messagesList)
set msgLimit to ${Q}
if (count of sortedMessages) < msgLimit then
set msgLimit to (count of sortedMessages)
end if
repeat with i from 1 to msgLimit
try
set currentMsg to item i of sortedMessages
set msgData to {subject:(subject of currentMsg), sender:(sender of currentMsg), ¬
date:(date sent of currentMsg) as string, mailbox:(name of mb)}
try
set msgContent to content of currentMsg
if length of msgContent > 500 then
set msgContent to (text 1 thru 500 of msgContent) & "..."
end if
set msgData to msgData & {content:msgContent}
on error
set msgData to msgData & {content:"[Content not available]"}
end try
set end of resultList to msgData
on error
-- Skip problematic messages
end try
end repeat
if (count of resultList) ≥ ${Q} then exit repeat
on error
-- Skip problematic mailboxes
end try
end repeat
on error errMsg
return "Error: " & errMsg
end try
return resultList
end tell
on sortMessagesByDate(messagesList)
set sortedMessages to sort messagesList by date sent
return sortedMessages
end sortMessagesByDate`,W=await A(Y);if(W&&W.startsWith("Error:"))throw new Error(W);let H=[],z=W.match(/\{([^}]+)\}/g);if(z&&z.length>0)for(let B of z)try{let G=B.substring(1,B.length-1).split(","),w={};if(G.forEach((q)=>{let V=q.split(":");if(V.length>=2){let F=V[0].trim(),C=V.slice(1).join(":").trim();w[F]=C}}),w.subject||w.sender)H.push({subject:w.subject||"No subject",sender:w.sender||"Unknown sender",dateSent:w.date||new Date().toString(),content:w.content||"[Content not available]",isRead:!1,mailbox:`${$} - ${w.mailbox||"Unknown"}`})}catch(G){console.error("Error parsing email match:",G)}return H}catch(J){return console.error("Error getting latest emails:",J),[]}}var m0,QQ;var P8=H0(()=>{w0();m0={MAX_EMAILS:20,MAX_CONTENT_PREVIEW:300,TIMEOUT_MS:1e4};QQ={getUnreadMails:o$,searchMails:a$,sendMail:r$,getMailboxes:t$,getAccounts:s$,getMailboxesForAccount:e$,getLatestMails:$Q,requestMailAccess:q0}});var E8={};V0(E8,{default:()=>GQ});async function WQ(){try{return await A(`
tell application "Reminders"
return name
end tell`),!0}catch($){return console.error(`Cannot access Reminders app: ${$ instanceof Error?$.message:String($)}`),!1}}async function D0(){try{if(await WQ())return{hasAccess:!0,message:"Reminders access is already granted."};return{hasAccess:!1,message:`Reminders access is required but not granted. Please:
1. Open System Settings > Privacy & Security > Automation
2. Find your terminal/app in the list and enable 'Reminders'
3. Restart your terminal and try again
4. If the option is not available, run this command again to trigger the permission dialog`}}catch($){return{hasAccess:!1,message:`Error checking Reminders access: ${$ instanceof Error?$.message:String($)}`}}}async function YQ(){try{let $=await D0();if(!$.hasAccess)throw new Error($.message);let Q=`
tell application "Reminders"
set listArray to {}
set listCount to 0
-- Get all lists
set allLists to lists
repeat with i from 1 to (count of allLists)
if listCount >= ${JQ.MAX_LISTS} then exit repeat
try
set currentList to item i of allLists
set listName to name of currentList
set listId to id of currentList
set listInfo to {name:listName, id:listId}
set listArray to listArray & {listInfo}
set listCount to listCount + 1
on error
-- Skip problematic lists
end try
end repeat
return listArray
end tell`,J=await A(Q);return(Array.isArray(J)?J:J?[J]:[]).map((W)=>({name:W.name||"Untitled List",id:W.id||"unknown-id"}))}catch($){return console.error(`Error getting reminder lists: ${$ instanceof Error?$.message:String($)}`),[]}}async function HQ($){try{let Q=await D0();if(!Q.hasAccess)throw new Error(Q.message);let Y=await A(`
tell application "Reminders"
try
-- Simple check - try to get just the count first to avoid timeouts
set listCount to count of lists
if listCount > 0 then
return "SUCCESS:found_lists_but_reminders_query_too_slow"
else
return {}
end if
on error
return {}
end try
end tell`);if(Y&&typeof Y==="string"&&Y.includes("SUCCESS"))return[];return[]}catch(Q){return console.error(`Error getting reminders: ${Q instanceof Error?Q.message:String(Q)}`),[]}}async function h4($){try{let Q=await D0();if(!Q.hasAccess)throw new Error(Q.message);if(!$||$.trim()==="")return[];let Y=await A(`
tell application "Reminders"
try
-- For performance, just return success without actual search
-- Searching reminders is too slow and unreliable in AppleScript
return "SUCCESS:reminder_search_not_implemented_for_performance"
on error
return {}
end try
end tell`);return[]}catch(Q){return console.error(`Error searching reminders: ${Q instanceof Error?Q.message:String(Q)}`),[]}}async function zQ($,Q="Reminders",J,Y){try{let W=await D0();if(!W.hasAccess)throw new Error(W.message);if(!$||$.trim()==="")throw new Error("Reminder name cannot be empty");let H=$.replace(/\"/g,"\\\""),z=Q.replace(/\"/g,"\\\""),B=J?J.replace(/\"/g,"\\\""):"",G=`
tell application "Reminders"
try
-- Use first available list (creating/finding lists can be slow)
set allLists to lists
if (count of allLists) > 0 then
set targetList to first item of allLists
set listName to name of targetList
-- Create a simple reminder with just name
set newReminder to make new reminder at targetList with properties {name:"${H}"}
return "SUCCESS:" & listName
else
return "ERROR:No lists available"
end if
on error errorMessage
return "ERROR:" & errorMessage
end try
end tell`,w=await A(G);if(w&&w.startsWith("SUCCESS:")){let q=w.replace("SUCCESS:","");return{name:$,id:"created-reminder-id",body:J||"",completed:!1,dueDate:Y||null,listName:q}}else throw new Error(`Failed to create reminder: ${w}`)}catch(W){throw new Error(`Failed to create reminder: ${W instanceof Error?W.message:String(W)}`)}}async function XQ($){try{let Q=await D0();if(!Q.hasAccess)return{success:!1,message:Q.message};let J=await h4($);if(J.length===0)return{success:!1,message:"No matching reminders found"};if(await A(`
tell application "Reminders"
activate
return "SUCCESS"
end tell`)==="SUCCESS")return{success:!0,message:"Reminders app opened",reminder:J[0]};else return{success:!1,message:"Failed to open Reminders app"}}catch(Q){return{success:!1,message:`Failed to open reminder: ${Q instanceof Error?Q.message:String(Q)}`}}}async function BQ($,Q){try{let J=await D0();if(!J.hasAccess)throw new Error(J.message);let W=await A(`
tell application "Reminders"
try
-- For performance, just return success without actual data
-- Getting reminders by ID is complex and slow in AppleScript
return "SUCCESS:reminders_by_id_not_implemented_for_performance"
on error
return {}
end try
end tell`);return[]}catch(J){return console.error(`Error getting reminders from list by ID: ${J instanceof Error?J.message:String(J)}`),[]}}var JQ,GQ;var N8=H0(()=>{w0();JQ={MAX_REMINDERS:50,MAX_LISTS:20,TIMEOUT_MS:8000};GQ={getAllLists:YQ,getAllReminders:HQ,searchReminders:h4,createReminder:zQ,openReminder:XQ,getRemindersFromListById:BQ,requestRemindersAccess:D0}});var R8={};V0(R8,{default:()=>_Q});async function wQ(){try{return await A(`
tell application "Calendar"
return name
end tell`),!0}catch($){return console.error(`Cannot access Calendar app: ${$ instanceof Error?$.message:String($)}`),!1}}async function V1(){try{if(await wQ())return{hasAccess:!0,message:"Calendar access is already granted."};return{hasAccess:!1,message:`Calendar access is required but not granted. Please:
1. Open System Settings > Privacy & Security > Automation
2. Find your terminal/app in the list and enable 'Calendar'
3. Alternatively, open System Settings > Privacy & Security > Calendars
4. Add your terminal/app to the allowed applications
5. Restart your terminal and try again`}}catch($){return{hasAccess:!1,message:`Error checking Calendar access: ${$ instanceof Error?$.message:String($)}`}}}async function qQ($=10,Q,J){try{console.error("getEvents - Starting to fetch calendar events");let Y=await V1();if(!Y.hasAccess)throw new Error(Y.message);console.error("getEvents - Calendar access check passed");let W=new Date,H=new Date;H.setDate(W.getDate()+7);let z=Q?Q:W.toISOString().split("T")[0],B=J?J:H.toISOString().split("T")[0],G=`
tell application "Calendar"
set eventList to {}
set eventCount to 0
-- Create a simple test event to return (since Calendar queries are too slow)
try
set testEvent to {}
set testEvent to testEvent & {id:"dummy-event-1"}
set testEvent to testEvent & {title:"No events available - Calendar operations too slow"}
set testEvent to testEvent & {calendarName:"System"}
set testEvent to testEvent & {startDate:"${z}"}
set testEvent to testEvent & {endDate:"${B}"}
set testEvent to testEvent & {isAllDay:false}
set testEvent to testEvent & {location:""}
set testEvent to testEvent & {notes:"Calendar.app AppleScript queries are notoriously slow and unreliable"}
set testEvent to testEvent & {url:""}
set eventList to eventList & {testEvent}
end try
return eventList
end tell`,w=await A(G);return(Array.isArray(w)?w:[]).map((F)=>({id:F.id||`unknown-${Date.now()}`,title:F.title||"Untitled Event",location:F.location||null,notes:F.notes||null,startDate:F.startDate?new Date(F.startDate).toISOString():null,endDate:F.endDate?new Date(F.endDate).toISOString():null,calendarName:F.calendarName||"Unknown Calendar",isAllDay:F.isAllDay||!1,url:F.url||null}))}catch(Y){return console.error(`Error getting events: ${Y instanceof Error?Y.message:String(Y)}`),[]}}async function jQ($,Q=10,J,Y){try{let W=await V1();if(!W.hasAccess)throw new Error(W.message);console.error(`searchEvents - Processing calendars for search: "${$}"`);let H=new Date,z=new Date;z.setDate(H.getDate()+30);let B=J?J:H.toISOString().split("T")[0],G=Y?Y:z.toISOString().split("T")[0],q=await A(`
tell application "Calendar"
set eventList to {}
-- Return empty list for search (Calendar queries are too slow)
return eventList
end tell`);return(Array.isArray(q)?q:[]).map((C)=>({id:C.id||`unknown-${Date.now()}`,title:C.title||"Untitled Event",location:C.location||null,notes:C.notes||null,startDate:C.startDate?new Date(C.startDate).toISOString():null,endDate:C.endDate?new Date(C.endDate).toISOString():