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

dunbar-app

v0.1.0

Published

Local-only macOS CLI that exports per-person iMessage frequency to a CSV for import into the Dunbar relationship app, with an optional single-use LAN QR handoff to auto-import on iPhone.

Downloads

40

Readme

dunbar-app

A small, local-only macOS command-line tool that reads your message history off-device and exports a CSV of per-person message frequency for import into the Dunbar relationship app — plus an optional single-use LAN QR handoff that sends the CSV straight to the Dunbar iPhone app over Wi-Fi.

iOS can't expose iMessage frequency to third-party apps, so Dunbar reads it off the Mac instead: this helper aggregates how often you talk to each person from the local Messages database and writes a CSV. You then either scan a QR to import it directly on your phone, or move the CSV across manually.

Run it with no install:

npx dunbar-app

What it does

  • Reads the macOS Messages database (~/Library/Messages/chat.db) read-only.
  • For each person you have a 1:1 conversation with, computes:
    • total message count (both directions) over a lookback window (default 5 years), and
    • the last-contacted date (most recent message).
  • Skips group chats — group activity is a weak closeness signal.
  • Resolves phone numbers / emails to contact names via your local macOS Contacts (AddressBook), best-effort, and merges multiple handles (e.g. a person's phone + iCloud email) into a single row when Contacts links them.
  • Writes a CSV with one row per person.
  • By default, serves that CSV once over your LAN and prints a QR code so the Dunbar app can pull it directly (see LAN QR handoff). Use --no-serve to skip.

Privacy

Everything stays on your machine. The helper:

  • opens the Messages database and Contacts read-only (better-sqlite3 in readonly mode with query_only = ON), so it can never modify your databases;
  • makes no outbound/internet calls of any kind — there is no telemetry, no upload, nothing leaves your computer;
  • the optional QR handoff is LAN-only, guarded by a single-use random token, and serves exactly one file once before shutting down;
  • produces exactly one artifact: the CSV file you choose (default ~/Desktop/dunbar-messages.csv).

Requirements

  • macOS with Node.js ≥ 18.
  • Full Disk Access for the program that runs the helper (Terminal, iTerm, or your IDE), because ~/Library/Messages/ is protected by macOS.

The only native dependency, better-sqlite3, ships prebuilt binaries, so npx dunbar-app installs and runs without a C/C++ compiler.

Grant Full Disk Access

  1. Open System Settings → Privacy & Security → Full Disk Access.
  2. Click + and add the app you'll run the helper from (e.g. Terminal or iTerm).
    • To resolve contact names too, this same Full Disk Access also covers AddressBook.
  3. Toggle it on.
  4. Fully quit and reopen that app (the permission only takes effect on relaunch).
  5. Run the helper again.

If access isn't granted, the helper exits with a clear message and a CSV is not written.

Usage

# Default: 5-year lookback, output to ~/Desktop/dunbar-messages.csv, then serve + QR.
npx dunbar-app

Options

dunbar-app [output] [--years N] [--chat-db PATH] [--no-contacts] [--no-serve]

  output          CSV output path (default: ~/Desktop/dunbar-messages.csv)
  --years N       Lookback window in years (default: 5)
  --chat-db PATH  Path to a chat.db (default: ~/Library/Messages/chat.db; handy for testing)
  --no-contacts   Skip Contacts name resolution; use raw handles (no cross-handle merging)
  --no-serve      Skip the LAN QR handoff; only write the CSV
  -h, --help      Show this help and exit

Examples:

npx dunbar-app ~/Desktop/my-export.csv      # custom output path
npx dunbar-app --years 2                     # only the last 2 years
npx dunbar-app --no-serve                    # just write the CSV, no QR

CSV schema

The Dunbar importer depends on this exact header and order:

name,phone,email,source,message_count,last_contacted_at

| Column | Meaning | | ------------------- | ----------------------------------------------------------------------- | | name | Contact display name; falls back to the raw handle if not in Contacts. | | phone | Normalized phone (digits, leading + kept). Blank if the handle is an email. | | email | Email / Apple ID. Blank if the handle is a phone number. | | source | imessage (future: whatsapp). | | message_count | Total messages exchanged (both directions) in 1:1 chats. | | last_contacted_at | Most recent message, ISO 8601 UTC, e.g. 2025-03-14T00:00:00Z. |

Rows use CRLF line endings and minimal quoting (byte-compatible with the original Python tool's csv output).

Sample output:

name,phone,email,source,message_count,last_contacted_at
Alice Anderson,+15551112222,[email protected],imessage,42,2026-06-29T04:59:41Z
Bob Brown,+15553334444,,imessage,8,2026-06-20T04:59:41Z
+15557778888,+15557778888,,imessage,5,2026-03-22T04:59:41Z

(Alice Anderson shows the merge: her phone-number chat and her iCloud-email chat were combined into one person, with both phone and email populated and message counts summed.)

LAN QR handoff

After writing the CSV, the helper (unless --no-serve is passed) serves the file once over your local network and prints a QR code. Open the Dunbar app on a phone on the same Wi-Fi, scan it, and the CSV imports directly — no AirDrop or Files round-trip.

What you'll see in the terminal:

  • the CSV path,
  • a QR code encoding the deep link,
  • the raw deep link as text, and
  • a "scan in the Dunbar app, same Wi-Fi" hint.

Shared contract (Dunbar app ↔ helper)

  • Deep link in the QR: dunbar://import?host=<LAN_IPv4>&port=<PORT>&token=<TOKEN>
  • The app fetches: http://<host>:<port>/messages.csv?token=<TOKEN>
  • Server behavior:
    • GET /messages.csv with a matching token (via ?token= query or an X-Dunbar-Token header) → 200, Content-Type: text/csv; charset=utf-8, body = the CSV.
    • Token mismatch/missing → 403. Any other path → 404.
    • Single-use: after the first successful 200 for /messages.csv, or after a 5-minute idle timeout, the server shuts down and the process exits cleanly — printing Sent ✓ on success or Timed out otherwise.
    • LAN-only, bound to 0.0.0.0 on a random free port; no outbound network calls.

The token is a random URL-safe value with ≥16 bytes of entropy, minted fresh on every run.

Tip: macOS may pop up a firewall prompt the first time Node accepts an incoming connection. Allow it (or temporarily allow incoming connections) so the phone can reach the handoff server on your LAN.

Manual fallback (no QR)

If you used --no-serve or QR import isn't convenient:

  1. Run the helper to produce the CSV (e.g. ~/Desktop/dunbar-messages.csv).
  2. AirDrop the CSV from your Mac to your iPhone (right-click → Share → AirDrop), or save it to Files.
  3. In Dunbar, run the in-app file import.

How it works (timestamps & 1:1 detection)

  • Timestamps: modern chat.db stores message.date as nanoseconds since 2001-01-01 UTC (Mac absolute time); older databases use seconds. The helper detects and converts both defensively (see src/utils.js).
  • 1:1 detection: a chat is 1:1 when its participant set in chat_handle_join is exactly one other handle. Group chats join to multiple handles and are excluded.

Architecture (multi-source)

The code is structured so additional message sources drop in behind one interface and merge into the same CSV.

bin/
  dunbar-app.js     executable entry point (the npm `bin`)
src/
  sources/
    base.js         MessageSource interface: isAvailable() + fetchContacts(since)
    imessage.js     iMessage / SMS reader (implemented)
    whatsapp.js     WhatsApp reader (STUB + full implementation notes)
  contacts.js       handle -> name resolution via AddressBook (best-effort)
  pipeline.js       merge handles/sources into one Person per real contact
  csvWriter.js      exact-schema CSV output
  serve.js          single-use LAN QR handoff server
  sqlite.js         read-only SQLite access (better-sqlite3)
  cli.js            argument parsing + orchestration
  utils.js          timestamp conversion + phone/email normalization
  models.js         shared handle-type constants

Each source emits SourceContact records; pipeline.mergeContacts collapses them into one row per person; csvWriter writes the schema. The pipeline never needs to know which source the data came from.

WhatsApp extension point (not implemented yet)

src/sources/whatsapp.js is a ready-to-fill stub. Its isAvailable() returns false, so it's skipped today. The file documents the full plan: WhatsApp's native macOS app stores a Core Data SQLite at

~/Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqlite

with tables ZWACHATSESSION (sessions; 1:1 JIDs end in @s.whatsapp.net, groups in @g.us) and ZWAMESSAGE (messages). Implementing fetchContacts there — open read-only, filter to 1:1 sessions, COUNT/MAX(date), convert Core Data timestamps with the existing utils.macAbsoluteToDate — and registering it in cli.buildSources makes WhatsApp a drop-in that emits rows with source="whatsapp" into the same CSV.

Dependencies

  • better-sqlite3 — read-only access to chat.db and the AddressBook. Chosen because it ships prebuilt binaries (no compiler needed for npx), has a simple synchronous API that fits this batch tool, and opens databases read-only so the live Messages store is never modified.
  • qrcode-terminal — tiny, pure-JS QR renderer for the LAN handoff; no native code.

Tests / verification

A synthetic fixture mimics the real chat.db schema (handle, chat, message, chat_message_join, chat_handle_join) plus an AddressBook, so the full query → aggregation → CSV path can be verified without Full Disk Access:

npm install        # once, to fetch better-sqlite3 + qrcode-terminal
npm test           # runs node --test over test/*.test.js

To exercise the CLI by hand against the fixture:

# build the fixture
node test/makeFixture.js /tmp/dunbar_fixture

# run the helper against it (CSV only)
DUNBAR_ADDRESSBOOK_GLOB="/tmp/dunbar_fixture/AddressBook/Sources/*/AddressBook-v22.abcddb" \
  node bin/dunbar-app.js /tmp/out.csv --chat-db /tmp/dunbar_fixture/chat.db --no-serve

The fixture exercises: group-chat exclusion, lookback filtering, both nanosecond and legacy-second timestamps, contact-driven handle merging, raw-handle fallback, exact CSV schema/format, and the LAN handoff server's token/path/single-use contract.

Publishing

This package is intended to be published to the public npm registry so anyone can run npx dunbar-app. Publishing requires npm credentials, so a human must run these steps:

# 1. Log in to npm (one-time per machine / session)
npm login

# 2. From the package root, publish publicly
cd /Users/ryan.holmes/Workspace/Dunbar/dunbar-helper
npm publish --access public

Notes:

  • The package name dunbar-app was confirmed available on the public npm registry at the time of writing.
  • If your default npm registry is a private proxy, publish to public npm explicitly with npm publish --access public --registry https://registry.npmjs.org/.
  • Bump version in package.json (it starts at 0.1.0) before each subsequent publish.