A local-first CLI and Agent Skill for studying private messaging history and drafting messages that sound like you.
Message Like Me turns private local messaging history into deterministic conversation metrics, bounded study packets, and reusable style profiles. It reads native iMessage history and strict local source bundles, including multi-account Beeper exports produced through Wrench. Its Agent Skill teaches Codex, Claude, and other coding agents how to interpret those local artifacts and draft unsent replies in your voice.
The CLI does not call an AI service, authenticate with a product account, send messages, or operate Messages. The agent already running the skill supplies the semantic analysis and drafting judgment.
This is an evidence layer for relationship-aware drafting, not a digital clone. It does not train a model, represent your identity, infer your beliefs, or claim that a draft is what you would have written. Your current meaning, facts, and intent outrank historical style.
Message Like Me requires Bun 1.3.14 or newer. Install the immutable public release from GitHub, then install the Agent Skill:
bun add --global github:hraness/message-like-me#v0.3.0
messagelikeme skill installStart a new agent session after installing the skill. The default target is Codex at user scope. Other supported targets and project-local installation are available explicitly:
messagelikeme skill install --target claude
messagelikeme skill install --target agents --scope project
messagelikeme skill pathMessage Like Me is distributed directly through GitHub and is not published to npm.
Initialize the private data store and inspect its location:
messagelikeme init
messagelikeme doctor --jsonOn macOS, the default store is:
~/Library/Application Support/Message Like Me/
The directory is private to the current user. It contains a local SQLite
database, stored profiles, and a private installation key used to derive
stable pseudonymous IDs. Study packets are written only to the explicit path
you choose. You can put the store elsewhere by placing
--data-dir /absolute/private/path before the command.
Import the current user's iMessage database:
messagelikeme ingest imessage --jsonThe default source is the current user's Messages chat.db. Use --database
only to name another caller-owned physical database:
messagelikeme ingest imessage --database /absolute/path/to/chat.db --jsonIngestion validates the source schema and ownership, makes a stable private
copy of the database and its transactional sidecars, and opens only that copy
with SQLite. It does not change Messages, chat.db, or its sidecars. macOS may
require permission for the terminal or agent host to read Messages data.
To study accounts connected through Beeper, install or update to Wrench 0.13.0 or newer, then ask it to create a new private Message Like Me bundle:
wrench beeper export-message-like-me \
--auth <beeper-auth-id> \
--output /absolute/private/path/beeper-bundle \
--jsonThe optional --limit-chats, --limit-messages, and --max-participants
flags lower the export bounds. The output path must be a normalized absolute
path to a directory that does not already exist. Wrench calls the pinned
official Beeper CLI directly. It enumerates
the connected account realm, invokes export --no-attachments once per
account in deterministic order, and reports the account ordinal, elapsed-time
heartbeats, and cumulative validated chat and message counts on stderr. It
retains each private raw shard until it can atomically publish the complete
mode-0700 seven-file bundle with mode-0600 files.
The export does not use the separate Beeper Desktop API MCP project. The CLI path supplies the bounded account snapshots and local files needed for hash validation, deterministic conversion, crash recovery, and atomic publication. Provider URLs and credentials are excluded. Message Like Me does not receive the Beeper credential and does not call Beeper or Wrench itself.
Ingest the finished directory, then inspect its redacted source health:
messagelikeme ingest bundle --input /absolute/private/path/beeper-bundle --json
messagelikeme sources list --json
messagelikeme sources show <source-id> --jsonThe importer verifies the fixed version-one inventory, canonical UTF-8 NDJSON, record and byte bounds, owner-only permissions, artifact digests, and manifest digest before changing the store. One bundle may contain several connected accounts and networks; each becomes a separate source namespace. Native iMessage and prior bundle sources remain alongside it.
The complete interchange, integrity, identity, and reimport laws are in the version-one local message bundle contract.
Beeper exports describe bounded local observations. A later bounded export
that omits an older record does not delete retained history. Explicit deletion,
removal, replacement, and tombstone records suppress their target, and a later
reappearance restores it. Older snapshots cannot overwrite newer state. Use
sources show <source-id> --private --json only when you deliberately need the
private provider account and source metadata.
Optionally enrich and join direct conversations with private identities from macOS Contacts:
messagelikeme ingest contacts --jsonThe default source is the current user's AddressBook directory. An explicit
absolute AddressBook root, Sources directory, store directory, or
AddressBook-vN.abcddb file can be selected with --addressbook:
messagelikeme ingest contacts \
--addressbook /absolute/path/to/AddressBook \
--jsonContacts ingest may run before or after any message source. It reads only
bounded name, email, and phone fields from a stable private copy. Exact
normalized email or E.164 phone handles can join several one-to-one threads
for the same AddressBook person into one analysis scope. A bundle conversation
is eligible only when the producer positively marks its direct participant
roster complete. Existing conversation IDs remain aliases for that person
scope. Shared handles remain ambiguous, local phone numbers never gain a
guessed country code, unmatched threads stay separate, and groups are never
collapsed to one person. Contact labels have their own revision, so a rename
does not stale a messaging-style profile. messagelikeme doctor reports local
aggregate state without asking for an account or credential.
Contact listings and aggregate views omit private labels, handles, and message bodies by default:
messagelikeme contacts list --min-outgoing 20 --json
messagelikeme contacts show <contact-id> --json
messagelikeme inspect tempo <contact-id> --session-gap 28800 --burst-gap 300 --json
messagelikeme inspect sessions <contact-id> --limit 20 --jsonThe metrics cover conversation start and end, message counts, incoming and
outgoing turns, within-session response latency, single-message versus
multi-message replies, surface prose features, multi-point response contexts,
reactions, and explicit reply use. Incoming messages establish what you were
responding to; they are never counted as examples of your writing style.
Sessions, bursts, and response episodes never cross a source conversation
boundary. Person scopes spanning several apps expose a sorted services
breakdown instead of hiding the mixed-channel evidence behind a null service.
Reactions with no provider timestamp still contribute to reaction counts and
direction, but never to temporal metrics. Raw provider reaction values remain
private; ordinary metrics and drafting context expose only fixed-size counts,
direction, datedness, and the outgoing reaction ratio. Session and burst gaps
are configurable seconds and are recorded with each result. They are
segmentation choices, not universal facts about conversation.
Pass --private to contacts list or contacts show only when you need to
resolve a pseudonymous contact to its local private label or participants.
When you already know the complete Contacts label, resolve only that exact private name instead of listing every label:
messagelikeme contacts resolve "Exact Contact Name" --private --jsonResolution is normalized for case and Unicode representation, but it does not perform prefix, substring, phonetic, or fuzzy matching. It returns only direct person scopes and labels, never handles or message bodies.
Aggregate metrics cannot explain why a short burst works in one context or why a longer single message appears in another. For that semantic work, prepare a small, diverse study packet at an explicit private path:
messagelikeme study prepare <contact-id> \
--output /absolute/private/path/study.json \
--before 2026-08-01T00:00:00.000Z \
--limit 24 \
--jsonstudy prepare and evaluate prepare are the only commands that write bounded
message bodies outside the private database. Their outputs are mode 0600.
A study packet contains incoming context and outgoing responses selected across
different response shapes; it is not a full transcript export. By default,
each body is capped at 4 KiB, each example keeps at most 12 text messages per
direction, and the entire packet keeps at most 256 KiB of body text. Packet
coverage fields report every truncation or omission explicitly.
Keep the JSON receipt with the analysis. Its packetSha256 binds the finished
profile to these exact packet bytes; the packet does not contain its own digest.
--after is inclusive and --before is exclusive. Temporal bounds let you
reserve later conversations for evaluation. Invoke $message-like-me in your
agent and ask it to analyze that contact. The skill separates measured facts
from inferred patterns, covers prose and tempo, studies how several inbound
points are handled, and treats reply links and tapbacks separately from written
text.
The agent writes a schema-version-two profile and asks the CLI to validate and store it:
messagelikeme profile apply /absolute/private/path/profile.json --json
messagelikeme profile show <contact-id> --jsonA version-two profile records the global corpus revision for provenance, a person-and-window-specific evidence revision for validity, the exact study-packet SHA-256, and the packet's non-body evidence manifest. Measured and inferred claims cite valid packet example IDs and record counterexamples, support counts, confidence, and drafting consequences. Messages for someone else or outside the studied time window do not stale it; changes inside its actual evidence do.
Export a profile only when you need an explicit private copy:
messagelikeme profile export <contact-id> --output /absolute/private/path/profile.jsonVersion-one profiles remain readable for migration, but new analyses should use
schema/style-profile-v2.schema.json.
Prepare a separate prompt and reference set from conversations after the study cutoff:
messagelikeme evaluate prepare <contact-id> \
--after 2026-08-01T00:00:00.000Z \
--prompt-output /absolute/private/path/evaluation-prompts.json \
--reference-output /absolute/private/path/evaluation-references.json \
--jsonGive the agent only the prompt file and fix one candidate bubble sequence per case before opening the reference file. Then compare intent coverage, factual meaning, prose, bubble shape, explicit replies, privacy leakage, and calibration. The files support a blind workflow but do not enforce one, and the historical response is one observation rather than a unique correct answer. The CLI deliberately does not collapse these dimensions into a universal fidelity score. See the methodology.
Ask an agent with the installed $message-like-me skill to draft for a
pseudonymous contact. The compact deterministic context is available through:
messagelikeme context <contact-id> --jsonThe skill preserves your intended meaning, selects the applicable profile, and can express the result as one message or a realistic sequence of separate bubbles. It uses explicit replies only when your evidence and the current context support them.
Drafting ends with text in the agent task. Message Like Me has no send, react, schedule, or messaging-application command.
Run messagelikeme --help for the checked grammar. The public surfaces are:
messagelikeme init [--json]
messagelikeme ingest imessage [--database PATH] [--json]
messagelikeme ingest contacts [--addressbook PATH] [--json]
messagelikeme ingest bundle --input ABS_PATH [--json]
messagelikeme sources list [--private] [--json]
messagelikeme sources show SOURCE_ID [--private] [--json]
messagelikeme contacts list [--min-outgoing N] [--limit N] [--private] [--json]
messagelikeme contacts show CONTACT_ID [--private] [--json]
messagelikeme contacts resolve QUERY --private [--limit N] [--json]
messagelikeme inspect tempo CONTACT_ID [--session-gap N] [--burst-gap N] [--json]
messagelikeme inspect sessions CONTACT_ID [--limit N] [--session-gap N] [--burst-gap N] [--json]
messagelikeme study prepare CONTACT_ID --output FILE [--limit N]
[--after ISO_TIMESTAMP] [--before ISO_TIMESTAMP]
[--session-gap N] [--burst-gap N] [--json]
messagelikeme evaluate prepare CONTACT_ID --after ISO_TIMESTAMP
--prompt-output FILE --reference-output FILE [--before ISO_TIMESTAMP]
[--limit N] [--session-gap N] [--burst-gap N] [--json]
messagelikeme profile apply FILE [--json]
messagelikeme profile show CONTACT_ID [--json]
messagelikeme profile export CONTACT_ID --output FILE [--json]
messagelikeme context CONTACT_ID [--json]
messagelikeme skill path [--json]
messagelikeme skill install [--target codex|claude|agents]
[--scope user|project] [--project PATH] [--force] [--json]
messagelikeme doctor [--json]
Place global --data-dir PATH before the command.
- The original
chat.dband AddressBook databases remain authoritative. SQLite opens only stable private copies, never the source files or sidecars. - Source bundles remain private caller-owned inputs. Import verifies their fixed inventory, canonical bytes, digests, bounds, and owner-only modes.
- The normalized corpus, profiles, and installation key stay in a private local store with owner-only permissions.
- Stable source, contact, participant, conversation, message, and reaction IDs are derived with a private per-install HMAC key. Pseudonymous IDs are not encryption.
- Aggregate commands omit bodies and private labels. Study and evaluation packets are bounded, explicit body-bearing exports.
- Message text never goes to a Message Like Me server. There is no service, account, auth flow, analytics client, or network-backed model call.
- Opening a study packet makes its bounded excerpts visible to the agent environment already running the skill. Use an agent environment whose data handling you accept; the CLI cannot make a hosted agent local.
- Public fixtures are synthetic. Private corpora, profiles, packets, and drafts do not belong in Git, issues, logs, packages, or examples.
- A draft is never sent.
Read SECURITY.md before integrating the library into another tool or handling a private packet outside the CLI. The methodology defines every unit and evidence boundary; the research review documents papers, neighboring OSS, and the claims this project does not make.
The package exports the versioned corpus, metrics, study-packet, and profile types plus deterministic canonical JSON and SHA-256 helpers:
import type { ContactMetrics, StyleProfileV2 } from "@hraness/message-like-me"
import { canonicalJson, sha256 } from "@hraness/message-like-me"The library does not start the CLI, inspect Messages or Contacts, connect to a network, or send a draft merely because it is imported.
bun install --frozen-lockfile --ignore-scripts
bun run checkTests use synthetic Messages and AddressBook databases plus synthetic source bundles and conversations. Never add a real message, handle, group title, attachment, contact record, private path, or derived profile to a fixture.
The canonical repository is
hraness/message-like-me.
The informational project page is
messagelikeme.com. The CLI does not connect to
the site, and the site never receives message or contact data.
MIT.