Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Contents

- `src/` – the deterministic local iMessage and Contacts readers, normalized
corpus and metrics, private SQLite store, profile parser, Agent Skill
installer, and `messagelikeme` CLI.
- `src/` – the deterministic local iMessage, Contacts, and private source-bundle
readers, normalized corpus and metrics, private SQLite store, profile parser,
Agent Skill installer, and `messagelikeme` CLI.
- `schema/` – public versioned JSON Schemas for deterministic artifacts and
agent-authored profiles.
- `docs/` – public methodology, evidence limits, research review, and prior-art
Expand All @@ -25,14 +25,20 @@
- Use Bun 1.3.14 and run `bun run check` before handing off a change. Do not add
another package manager or lockfile.
- Keep the public description exact: “A local-first CLI and Agent Skill for
studying your private iMessage history and drafting messages that sound like
studying private messaging history and drafting messages that sound like
you.”
- Keep the public repository independently buildable. Do not reference another
source repository, private packages, sibling paths, private fixtures, or
publication mechanics.
- Keep `chat.db` authoritative and ingestion read-only, query-only,
ownership-checked, schema-validated, and bounded. Never modify Messages,
contacts, attachments, or SQLite sidecars.
- Treat a `message-like-me.local-message-bundle` as an untrusted, private,
versioned directory boundary. Require its fixed inventory, canonical UTF-8,
owner-only modes, bounded records, artifact digests, and manifest digest.
Never let bundle absence erase retained history unless a future contract
explicitly declares authoritative coverage; apply explicit deletions and
tombstones separately.
- Treat AddressBook databases as optional label-enrichment sources. Isolate
every database plus WAL or journal before SQLite opens it, validate contact
entities and property owners dynamically, read only names and exact
Expand All @@ -54,7 +60,8 @@
- Keep the command name `messagelikeme`, the repository and package name
`message-like-me`, and the Agent Skill name `message-like-me`. Treat
`messagelikeme.com` as an informational project page, never as a data plane.
- Keep CLI commands namespaced as `ingest imessage|contacts`,
- Keep CLI commands namespaced as `ingest imessage|contacts|bundle`,
`sources list|show`,
`contacts list|show|resolve`,
`inspect tempo|sessions`, `study prepare`, `profile apply|show|export`, plus
`init`, `context`, `skill`, and `doctor`. Machine-readable commands support
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## 0.3.0

- Add strict private version-one source-bundle ingestion for local Beeper
exports produced through Wrench, including account and network provenance,
replies, edits, deletions, attachments, reactions, and tombstones.
- Namespace corpus ownership by source so native iMessage and multiple provider
accounts coexist. Bounded snapshot absence retains prior history, explicit
terminal state suppresses evidence, and later reappearance restores it.
- Add `sources list` and `sources show` with active message, conversation,
reaction, undated-reaction, completeness, and warning health.
- Require Wrench 0.13.0 or newer for its direct official Beeper CLI path,
sequential per-account progress, retained-shard validation, and atomic
seven-file publication.
- Partition sessions, bursts, and response episodes by conversation, preserve
truncated text bubbles as tempo evidence, and count undated reactions without
inventing timestamps or exposing raw provider reaction values in aggregate
output.
- Upgrade existing version-two stores in place while retaining conversations,
profiles, study packets, and evidence provenance.

## 0.2.0

- Aggregate conservatively matched direct threads into one AddressBook person
Expand Down
118 changes: 93 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Message Like Me

**A local-first CLI and Agent Skill for studying your private iMessage history
and drafting messages that sound like you.**
**A local-first CLI and Agent Skill for studying private messaging history and
drafting messages that sound like you.**

Message Like Me turns a local Messages database into deterministic conversation
metrics, bounded study packets, and reusable style profiles. Its Agent Skill
teaches Codex, Claude, and other coding agents how to interpret those local
artifacts and draft unsent replies in your voice.
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
Expand All @@ -23,7 +25,7 @@ Message Like Me requires Bun 1.3.14 or newer. Install the immutable public
release from GitHub, then install the Agent Skill:

```sh
bun add --global github:hraness/message-like-me#v0.2.0
bun add --global github:hraness/message-like-me#v0.3.0
messagelikeme skill install
```

Expand All @@ -40,7 +42,7 @@ messagelikeme skill path
Message Like Me is distributed directly through GitHub and is not published to
npm.

## Start with your local history
## Start with private local history

Initialize the private data store and inspect its location:

Expand Down Expand Up @@ -79,6 +81,58 @@ 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](https://github.com/hraness/wrench/releases), then ask
it to create a new private Message Like Me bundle:

```sh
wrench beeper export-message-like-me \
--auth <beeper-auth-id> \
--output /absolute/private/path/beeper-bundle \
--json
```

The 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](https://github.com/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](https://github.com/beeper/desktop-api-mcp).
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:

```sh
messagelikeme ingest bundle --input /absolute/private/path/beeper-bundle --json
messagelikeme sources list --json
messagelikeme sources show <source-id> --json
```

The 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](docs/local-message-bundle-v1.md).

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:

Expand All @@ -96,16 +150,17 @@ messagelikeme ingest contacts \
--json
```

Contacts ingest may run before or after iMessage ingest. It reads only bounded
name, email, and phone fields from a stable private copy. Exact normalized
email or phone handles can join several one-to-one iMessage, SMS, and email
threads for the same AddressBook person into one analysis scope. 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.
Contacts 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.

## Inspect behavior without exposing prose

Expand All @@ -124,8 +179,15 @@ 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.
Session and burst gaps are configurable seconds and are recorded with each
result. They are segmentation choices, not universal facts about conversation.
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.
Expand Down Expand Up @@ -244,6 +306,9 @@ 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]
Expand Down Expand Up @@ -271,10 +336,13 @@ Place global `--data-dir PATH` before the command.

- The original `chat.db` and 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 contact, conversation, and message IDs are derived with a private
per-install HMAC key. Pseudonymous IDs are not encryption.
- 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,
Expand Down Expand Up @@ -312,9 +380,9 @@ bun install --frozen-lockfile --ignore-scripts
bun run check
```

Tests use synthetic Messages and AddressBook databases plus synthetic
conversations. Never add a real message, handle, group title, attachment,
contact record, private path, or derived profile to a fixture.
Tests 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`](https://github.com/hraness/message-like-me).
Expand Down
44 changes: 40 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ boundary, observed result, and a reproduction built from synthetic data.

## Private-data boundary

Message Like Me reads private iMessage history to derive local analysis. The
Message Like Me reads private messaging history to derive local analysis. The
following values are sensitive even when they do not contain an obvious name:

- the source Messages and AddressBook databases and their SQLite sidecars;
- local message bundles, manifests, connected-account metadata, and provider
provenance;
- contact names, email addresses, and phone numbers;
- message bodies, timestamps, reply links, tapbacks, and attachment metadata;
- contact, participant, conversation, and group metadata;
Expand Down Expand Up @@ -54,6 +56,36 @@ provenance. Missing or unsupported text remains unavailable rather than being
guessed. Reply targets and tapbacks remain separate from prose so they cannot
silently become authored style evidence.

## Local message bundle ingestion

`messagelikeme ingest bundle` accepts only a normalized absolute path to a
current-user-owned physical mode-`0700` directory. The version-one directory
contains exactly `manifest.json` and six mode-`0600` canonical UTF-8 NDJSON
artifacts. Files must be regular, singly linked, owner-controlled, stable while
read, and free of symbolic-link traversal.

The importer validates the manifest before allocating for its artifacts. It
caps one line at 2 MiB, the complete bundle at 500,000 records and 512 MiB, and
connected accounts at 128. It streams each artifact, rejects invalid UTF-8,
requires canonical JSON plus final newlines, and verifies exact record counts,
bytes, SHA-256 artifact digests, and the canonical manifest projection digest.
These checks detect malformed or changed local input. They do not establish
that the provider data is truthful or complete.

The accepted privacy declaration permits attachment metadata only and requires
provider URLs and credentials to be excluded. The bundle may still contain
message bodies, names, handles, timestamps, account identifiers, and graph
coordinates. Keep it under the same controls as the normalized store, and do
not place it in Git, logs, issues, packages, or ordinary agent context.

Each connected account is stored in its own per-install HMAC namespace.
Bounded, truncated, and unknown source absence never deletes retained history.
Explicit tombstones and terminal message or reaction state suppress their
validated targets. A later matching record can clear suppression, while an
older or conflicting equal-time snapshot is rejected. `sources list` is
redacted. `sources show --private` deliberately reveals provider account and
source metadata.

## Contacts enrichment

Contacts enrichment is optional. The reader discovers populated
Expand Down Expand Up @@ -83,7 +115,8 @@ a prose profile.

## Local identifiers

Contact, conversation, and message identifiers are derived with an HMAC key
Source, contact, participant, conversation, message, and reaction identifiers
are derived with an HMAC key
created for one local installation. They reduce accidental disclosure and keep
stable local references without storing handles in ordinary views. They are
not anonymization against an attacker who can read the local corpus or key.
Expand All @@ -95,8 +128,11 @@ it.
## Inspection and study packets

Aggregate contact, session, tempo, and surface-style views omit message bodies
and private labels by default. `--private` deliberately reveals local private
identity fields. Use it only when the current task needs that mapping.
and private labels by default. Raw provider reaction values also remain private;
aggregate and drafting-context views expose only fixed-size reaction counts,
direction, datedness, and the outgoing reaction ratio. `--private` deliberately
reveals local private identity fields. Use it only when the current task needs
that mapping.

`contacts resolve QUERY --private` performs bounded exact matching against
private labels. It does not do prefix, substring, phonetic, or fuzzy matching,
Expand Down
5 changes: 3 additions & 2 deletions dist/cli-xby0v0et.js → dist/cli-mxxakdqk.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ function prettyJson(value) {

// src/types.ts
var CORPUS_SCHEMA_VERSION = 1;
var METRICS_SCHEMA_VERSION = 1;
var METRICS_SCHEMA_VERSION = 2;
var PROFILE_SCHEMA_VERSION = 2;
var LEGACY_PROFILE_SCHEMA_VERSION = 1;
var STUDY_PACKET_SCHEMA_VERSION = 2;
var EVALUATION_PACKET_SCHEMA_VERSION = 1;
var CONTACTS_SCHEMA_VERSION = 1;
var MESSAGE_BUNDLE_SCHEMA_VERSION = 1;

export { canonicalJson, sha256, prettyJson, CORPUS_SCHEMA_VERSION, METRICS_SCHEMA_VERSION, PROFILE_SCHEMA_VERSION, LEGACY_PROFILE_SCHEMA_VERSION, STUDY_PACKET_SCHEMA_VERSION, EVALUATION_PACKET_SCHEMA_VERSION, CONTACTS_SCHEMA_VERSION };
export { canonicalJson, sha256, prettyJson, CORPUS_SCHEMA_VERSION, METRICS_SCHEMA_VERSION, PROFILE_SCHEMA_VERSION, LEGACY_PROFILE_SCHEMA_VERSION, STUDY_PACKET_SCHEMA_VERSION, EVALUATION_PACKET_SCHEMA_VERSION, CONTACTS_SCHEMA_VERSION, MESSAGE_BUNDLE_SCHEMA_VERSION };
Loading