"""Contact/handle search tools for iMessage."""
from ..constants import DEFAULT_PAGINATION_LIMIT
from ..db import get_connection, normalize_handle
from ..db.contacts import get_contact_cache
async def search_contacts(
query: str,
limit: int = DEFAULT_PAGINATION_LIMIT,
) -> dict:
"""Search for contacts/handles in the iMessage database.
Searches by phone number or email address. Returns matching handles
with their service type and conversation count.
Args:
query: Search query (phone number or email, partial matches supported)
limit: Maximum number of results to return (default: 20)
Returns:
Dictionary containing:
- results: List of matching handles with id, service, conversation_count
- total: Total number of matches
- query: The normalized search query
"""
normalized = normalize_handle(query)
with get_connection() as conn:
# Search for handles matching the query
# Use LIKE for partial matching
cursor = conn.execute(
"""
SELECT
h.ROWID as rowid,
h.id,
h.service,
h.country,
COUNT(DISTINCT chj.chat_id) as conversation_count
FROM handle h
LEFT JOIN chat_handle_join chj ON h.ROWID = chj.handle_id
WHERE h.id LIKE ?
GROUP BY h.ROWID
ORDER BY conversation_count DESC
LIMIT ?
""",
(f"%{normalized}%", limit),
)
results = []
for row in cursor:
results.append(
{
"id": row["id"],
"service": row["service"],
"country": row["country"],
"conversation_count": row["conversation_count"],
}
)
# Get total count
count_cursor = conn.execute(
"SELECT COUNT(*) FROM handle WHERE id LIKE ?",
(f"%{normalized}%",),
)
total = count_cursor.fetchone()[0]
return {
"results": results,
"total": total,
"query": normalized,
}
async def lookup_contact(phone_or_email: str) -> dict | None:
"""Look up a contact by phone number or email address.
Returns contact information if found in your Contacts, or None if not found.
This tool explicitly resolves contacts from the macOS Contacts app.
Args:
phone_or_email: The phone number or email address to look up.
Returns:
Dictionary containing:
- name: Contact's display name if found
- matched_handle: The input phone/email that was matched
- error: Error message if Contacts access is not available
Or None if the contact is not found.
"""
cache = get_contact_cache()
if not cache.is_available:
return {
"error": "Contacts access not available. Grant Contacts permission "
"to Terminal.app or Claude Desktop.app in System Settings > "
"Privacy & Security > Contacts."
}
name = await cache.resolve_name(phone_or_email)
if name:
return {"name": name, "matched_handle": phone_or_email}
return None