npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

macos-ts

v0.14.1

Published

TypeScript package for reading and searching Apple Notes, iMessages, Contacts, and more on macOS via direct SQLite access. Includes markdown conversion, attachment support, and offers a local MCP server!

Readme

macos-ts

TypeScript package for accessing macOS data via direct SQLite access — reads use no AppleScript and make no network calls. Currently supports Apple Notes, Apple Messages (iMessage/SMS), Apple Contacts, and Apple Photos. Sending an iMessage/SMS is the one write operation, and it is the only feature that uses Apple Events (osascript); see Messages.

Requirements

  • macOS (reads local macOS databases)
  • Bun runtime
  • Full Disk Access for the process reading data (System Settings → Privacy & Security → Full Disk Access)

Install

bun add macos-ts

Usage

Notes

import { Notes } from "macos-ts";

const db = new Notes();

// List accounts and folders
const accounts = db.accounts();
const folders = db.folders();

// List notes (filter, sort, search, limit)
const allNotes = db.notes();
const workNotes = db.notes({ folder: "Work" });
const recent = db.notes({ search: "meeting", sortBy: "modifiedAt", order: "desc", limit: 10 });

// Incremental reads: notes changed since the last sync. Over-inclusive — matches
// if the modified OR created date is >= the cutoff (the OR also catches iCloud
// notes stamped as modified on another device), so nothing changed is missed.
const changedNotes = db.notes({ modifiedAfter: lastSyncTime });

// Search by title or snippet
const results = db.search("meeting notes");
const filtered = db.search("meeting notes", { folder: "Work", limit: 10 });

// Read a note as markdown
const note = db.read(noteId);
console.log(note.markdown);

// Paginate large notes
const page = db.read(noteId, { offset: 0, limit: 100 });
console.log(page.markdown);      // first 100 lines
console.log(page.hasMore);       // true if more lines follow
console.log(page.totalLines);    // total line count

// Attachments — file-backed only by default (URL chips, hashtags, mentions,
// tables, and galleries live inline in the note body and have no file on disk)
const attachments = db.listAttachments(noteId);
const allAttachments = db.listAttachments(noteId, { includeInlineAttachments: true });
const url = db.getAttachmentUrl("attachment-uuid"); // file:// URL or null

// Or check a content type directly
import { isFileBackedAttachment } from "macos-ts";
isFileBackedAttachment("public.jpeg"); // true
isFileBackedAttachment("public.url");  // false

// Cleanup
db.close();

Messages

import { Messages } from "macos-ts";

const db = new Messages();

// List contacts and conversations
const handles = db.handles();
const chats = db.chats();
const recentChats = db.chats({ search: "John", sortBy: "lastMessageDate", order: "desc", limit: 10 });

// Get a specific chat
const chat = db.getChat(chatId);

// List messages in a chat (filter by date, sender, limit)
const msgs = db.messages(chatId);
const recent = db.messages(chatId, { limit: 20, order: "desc" });
const filtered = db.messages(chatId, { afterDate: new Date("2024-01-01"), fromMe: true });

// Get a single message
const msg = db.getMessage(messageId);

// Search across all conversations
const results = db.search("dinner tonight");
const inChat = db.search("dinner tonight", { chatId: 1, limit: 10 });

// Attachments
const attachments = db.attachments(messageId);

// Send a new message (the only write operation in this package).
// Target a raw handle (phone/email)...
db.send({ handle: "+15551234567", text: "On my way!" });
db.send({ handle: "[email protected]", text: "Hi", service: "SMS" });
// ...or an existing conversation by chatId (service taken from the chat):
db.send({ chatId: 1, text: "Sounds good" });

// Cleanup
db.close();

Sending requires Apple Events. chat.db is only a passive log, so sending a message means driving Messages.app via osascript (AppleScript). Consequences:

  • macOS Automation permission is required — the first send() triggers a one-time TCC prompt to allow your terminal/host app to control "Messages".
  • No delivery confirmation. A successful return means the request was dispatched, not that it was delivered. Failures throw MessageSendError.
  • Irreversible. A sent message cannot be unsent.
  • Messages.app must be open and signed in.

Contacts

import { Contacts } from "macos-ts";

const db = new Contacts();

// List all contacts (filter, sort, search, limit)
const allContacts = db.contacts();
const sorted = db.contacts({ sortBy: "modifiedAt", order: "desc", limit: 10 });
const inGroup = db.contacts({ groupId: 1 });

// Search by name, organization, phone number, or email.
// Results include phones and emails inline (primary first).
const results = db.search("John");
console.log(results[0].phones); // [{ number, label, isPrimary }]
console.log(results[0].emails); // [{ address, label, isPrimary }]
const byPhone = db.search("555-1234");
const byEmail = db.search("[email protected]");

// Get full contact details (addresses, social profiles, notes, etc.)
const details = db.getContact(contactId);
console.log(details.emails);     // [{ address, label, isPrimary }]
console.log(details.phones);     // [{ number, label, isPrimary }]
console.log(details.addresses);  // [{ street, city, state, zipCode, country, label }]

// Groups
const groups = db.groups();
const members = db.groupMembers(groupId);

// Cleanup
db.close();

Photos

import { Photos } from "macos-ts";

const db = new Photos();

// List photos (filter by media type, favorites, date range, album).
// Each result includes modifiedAt + fileSize, so callers can diff against a
// manifest in one query without per-photo getPhoto() / fs.stat() round-trips.
const allPhotos = db.photos();
const favorites = db.photos({ favorite: true });
const videos = db.photos({ mediaType: "video" });
const recent = db.photos({ afterDate: new Date("2024-01-01"), limit: 20 });
const inAlbum = db.photos({ albumId: 1 });

// Incremental reads: everything that CHANGED since the last sync. Use this
// (not afterDate) for backup/sync — see the callout below.
const changed = db.photos({ modifiedAfter: lastSyncTime });

// Get full photo details (dimensions, GPS, file size, iCloud status)
const details = db.getPhoto(photoId);
console.log(details.title);             // "Sunset at the Beach"
console.log(details.locallyAvailable);  // false = iCloud only or purged

// Get file URL for a photo
const { url, locallyAvailable } = db.getPhotoUrl(photoId);
// url: "file:///Users/.../Photos Library.photoslibrary/originals/0/IMG_001.JPG"
// locallyAvailable is filesystem-verified: true means the Photos.sqlite flag
// AND the file actually exists at `url`. Apple's "Optimize Mac Storage" can
// purge files without flipping the flag, so callers can trust this signal
// without an extra stat() check.

// List albums (user and smart albums)
const albums = db.albums();

afterDate/beforeDate vs modifiedAfter

These filter on different dates for different jobs — don't confuse them:

| Filter | Date checked | Use it for | | --- | --- | --- | | afterDate / beforeDate | Capture date (ZDATECREATED) — when the photo was taken | Browsing by when events happened ("my June trip"). A 2020 photo imported today still has a 2020 capture date. | | modifiedAfter | Modification date OR library-added date (ZMODIFICATIONDATE / ZADDEDDATE) | Incremental sync ("what changed since T"). Deliberately over-inclusive — matches if either date is >= T, so a re-imported old photo is still returned and nothing changed is ever missed. |

const vacation = db.albums({ search: "vacation" });

// Get album contents (photo IDs)
const album = db.getAlbum(albumId);
console.log(album.photoIds);  // [1, 5, 42, ...]

// Search by filename or title
const results = db.search("sunset");

// Cleanup
db.close();

MCP Server

macos-ts includes a stdio MCP server so AI agents can interact with your macOS data.

Configure

Add to your MCP client config (e.g., Claude Desktop, Claude Code):

{
  "mcpServers": {
    "macos": {
      "command": "bunx",
      "args": ["macos-ts"]
    }
  }
}

Available Tools

Discovery

  • get_capabilities — Discover available data sources, their tools, and recommended starting points

Notes

  • list_accounts — List all Apple Notes accounts on this Mac
  • list_folders — List folders, optionally filtered by account
  • list_notes — List notes with optional filtering (folder, account, text search, modifiedAfter for incremental reads), sorting (title, createdAt, modifiedAt), and limit
  • search_notes — Search notes by title and content
  • read_note — Read a note as markdown (supports pagination)
  • list_attachments — List file-backed attachments for a note (set includeInlineAttachments=true to also return inline rows like URL chips, hashtags, mentions, tables, and galleries)
  • get_attachment_url — Get the file URL for an attachment

Messages

  • list_handles — List all known contact handles (phone numbers, email addresses)
  • list_chats — List iMessage/SMS conversations with optional search, sorting, and limiting
  • get_chat — Get details for a specific conversation by ID
  • list_messages — List messages in a conversation with date filtering and pagination
  • get_message — Get a single message by ID
  • search_messages — Search message text across all conversations or within a specific chat
  • list_message_attachments — List attachments for a specific message
  • send_message — Send a new iMessage/SMS to a handle or chatId (the only write tool; requires macOS Automation permission, gives no delivery confirmation, and cannot be undone)

Contacts

  • list_contacts — List contacts with optional search, sorting (displayName, createdAt, modifiedAt), group filtering, and limit
  • get_contact — Get full contact details (emails, phones, addresses, URLs, social profiles, related names, dates)
  • search_contacts — Search contacts by name, organization, phone number, or email address (results include phones and emails inline)
  • list_groups — List all contact groups with member counts
  • list_group_members — List contacts in a specific group

Photos

  • list_photos — List photos/videos with filtering by media type, favorites, capture-date range (afterDate/beforeDate), modifiedAfter (change/added date, for incremental reads), album, and sorting
  • get_photo — Get full photo metadata (dimensions, GPS, file size, iCloud availability)
  • get_photo_url — Get the local file:// URL for a photo's original file
  • list_albums — List user-created and smart albums with photo counts
  • get_album — Get album details and list of photo IDs
  • search_photos — Search photos by filename or title

Tool Response Format

All tools return responses in a structured envelope:

{
  "data": [ ... ],
  "totalResults": 42,
  "_next": [
    { "tool": "read_note", "description": "Read a note's full markdown content" }
  ]
}
  • data — The actual result (array or object)
  • totalResults — Count of items (for array results)
  • _next — Suggested follow-up tools to call next

Error responses include structured recovery guidance:

{
  "error": "NoteNotFoundError",
  "message": "Note not found: 999",
  "category": "not_found",
  "retryable": false,
  "recovery": "Use list_notes or search_notes to find valid note IDs."
}

API

Notes: Pass dbPath or containerPath to new Notes() to override auto-detection. Note content is returned as markdown — see docs/markdown-conversion.md for the full formatting map.

Errors: DatabaseNotFoundError (missing DB or no Full Disk Access), NoteNotFoundError, PasswordProtectedError (locked notes can't be decrypted).

Messages: Pass dbPath to new Messages() to override auto-detection (defaults to ~/Library/Messages/chat.db).

Errors: DatabaseNotFoundError, ChatNotFoundError, MessageNotFoundError, MessageSendError (sending failed; category is access_denied when macOS Automation permission is missing, otherwise internal).

Contacts: Pass dbPath to new Contacts() to override auto-detection (defaults to ~/Library/Application Support/AddressBook/AddressBook-v22.abcddb). Labels are automatically cleaned from Apple's internal _$!<Label>!$_ format to plain strings (e.g., "Home", "Work", "Mobile").

Errors: DatabaseNotFoundError, ContactNotFoundError, GroupNotFoundError.

Photos: Pass dbPath to new Photos() to override auto-detection (defaults to ~/Pictures/Photos Library.photoslibrary/database/Photos.sqlite). The getPhotoUrl method resolves the original file path and indicates whether the photo is locally available or iCloud-only. locallyAvailable is verified against the filesystem — true only when the Photos.sqlite flag is set and the file is present at the returned URL.

Errors: DatabaseNotFoundError, PhotoNotFoundError, AlbumNotFoundError.

Development

bun test              # Run test suite
bun run bench         # Hand-time key read paths against the fixtures
bun run lint          # TypeScript type checking + Biome lint
bun run mcp           # Start the MCP stdio server
bun tui               # Interactive TUI for browsing and reading notes
bun run create-fixture # Regenerate the Notes test fixture database
bun run tests/fixtures/create-messages-db.ts   # Regenerate the Messages test fixture database
bun run tests/fixtures/create-contacts-db.ts   # Regenerate the Contacts test fixture database
bun run tests/fixtures/create-photos-db.ts     # Regenerate the Photos test fixture database

Tests run against a checked-in fixture database — no Full Disk Access needed.

Limitations

  • Read-only — writing to SQLite databases directly risks iCloud sync corruption
  • Password-protected notes — cannot be decrypted; throws PasswordProtectedError

License

MIT