Contacts Unify
Turn a messy, multi-source pile of contacts into one deduplicated, trustworthy address
book — with a review UI simple enough for a non-technical person to drive. Optionally, mine
iMessage history to find the people worth a deeper OSINT dossier.
Why this matters: duplicates and inconsistency make people stop maintaining their contacts at
all. The win is a single source of truth they'll actually keep feeding.
Scope of local_only: Phase A (dedupe) is fully local — no network egress. Phase B is opt-in
and its dossier step (research-person) does reach the public web (LinkedIn, gov/org sites). Treat
Phase B network access as a deliberate, separately-approved step, never an automatic follow-on.
Consent & authorization (read before running): only operate on contacts/messages belonging to
the user, on a machine they own or have explicit permission to access (including any SSH host). Phase
B researches other people from the user's private data — only build a dossier on someone the user
names explicitly. Never infer a research shortlist from message volume and run it unattended, and
don't dossier minors or obviously sensitive relationships.
When to use
- "Fix / clean up / merge my contacts", duplicate cards, contacts scattered across accounts.
- Consolidating iCloud + several Google accounts + Zoho + stray VCF/CSV exports.
- "Who do I talk to most?" → rank message contacts, then research the important ones.
Two phases (run either or both)
Phase A — Unify & dedupe (the core)
Full plan: references/unify-plan.md (read it before building). Summary of the workflow:
- Confirm the target + sources. Whose contacts? On the local Mac, or over SSH to another box
(e.g.
ssh <host> → sudo -u <user> -i)? Inventory which sources exist:
- macOS Contacts / iCloud — sqlite
~/Library/Application Support/AddressBook/Sources/*/AddressBook-v22.abcddb
(read mode=ro for rich fields + ZCREATIONDATE/ZMODIFICATIONDATE); osascript for the live
list and for writes.
- Google accounts (often several) — Google Contacts export / People API / Takeout
.vcf/CSV.
- Zoho, legacy Gmail, and any loose
.vcf/.csv on Desktop/Downloads.
- Tag every contact with its source account as provenance.
- Back up first, always. Whole AddressBook DB →
~/Desktop/Contacts-backup-<ts>/; every source
→ timestamped snapshot in the working dir. This is non-negotiable.
- Group & classify. Group by normalized name; cross-link groups sharing a normalized phone
(last 10 digits) or email even under different names. Classify each:
identical (auto-merge),
combine (safe union), review (disjoint → maybe different people → default keep-separate).
Name-only cards (businesses, placeholders with no phone/email) can't be matched on identifiers —
group them by normalized name only and always route them to review, never silent auto-merge.
- Review UI. Launch the self-contained web page (stock macOS python,
http.server, no pip).
Each duplicate group shows every competing field value with its label (home/work/cell),
source account, and age ("edited yesterday", "added 3mo ago"). The user picks the winning
value per field; losers become secondary labelled fields or are dropped if marked outdated.
Search/filter for large books. Save records decisions; Apply performs merges/deletes.
Without dates + labels, "Keep/Delete" is meaningless — dates and labels are mandatory.
- Apply — dry-run by default. Nothing mutates without
--live. Before any delete, export the
removed cards to deleted-contacts-<ts>.vcf (drag-to-restore undo). Auto-merge only truly
identical cards (same normalized phone/email set — no info lost).
- Never sync back to any cloud account until the user names an explicit target and approves.
Starter code: scripts/contacts_cleanup.py is the proven single-source (macOS) merger with the
full web review UI, backup, undo, and classification already working. Read it, then extend it to
multi-source ingest + per-field winner selection. Do not rebuild from scratch.
Phase B — iMessage mining → dossiers (optional deep dive)
Full plan: references/imessage-dossier-plan.md.
- Ask the user who's worth a deep dive. Before mining, offer to focus: high-value people,
specific groups, or traits (e.g. "Utah legislators", "arts-org board members", "clients",
"family"). Let them steer the shortlist — don't research everyone. The ranking informs the
conversation; it does not authorize research. A dossier runs only for a person the user names.
- Mine the message graph (read-only). Run
scripts/imessage_signal.py — it copies chat.db
to a temp file, opens it read-only, and ranks handles by volume and recency (plus a 1:1-vs-group
signal). It reads counts and timestamps only; message content never leaves the machine.
Reciprocity is an unreliable column — chat.db nulls the sender on the user's own messages, so
rank on volume + recency, not reciprocity. Output: ranked people-signal.csv/json + gap lists
("high-frequency but not in contacts" / "in contacts but never messaged"). Full Disk Access
required for the running user.
- Resolve identities against the unified address book from Phase A (collapse phone+email splits).
- Fan out dossiers. For the approved shortlist, invoke the
research-person skill to build
confidence-marked Obsidian People/ notes, enriched from public sources appropriate to the
person's world — LinkedIn, and (Utah-centric example) the state legislature site (le.utah.gov),
arts-group/nonprofit sites, and the lobbyist registry (disclosures.utah.gov). Private message
content stays internal context, never published.
Safety rules (both phases)
- Dry-run by default;
--live only after the user sees what would change.
- Back up before any mutation; every delete is reversible via exported
.vcf.
- Read-only on
chat.db (work on a copy); never write the live Messages or Contacts DB directly
except through the reviewed, --live-gated apply step.
- No outbound sync and no message content in any shared vault without explicit approval.
Deliverables
- Working dir (e.g.
~/dev/agent-notes/me/contacts-merge/<person>/) with source snapshots,
contacts-data.json, contacts-decisions.(csv|json), and the review HTML.
Unify My Contacts.command launcher + a friend-readable README.md.
- (Phase B)
people-signal.(csv|json) + research-person dossiers for the approved shortlist.
1---2name: contacts-unify3description: Merge and dedupe macOS, iCloud, Google, Zoho, or VCF contacts with reviewable provenance.4---56# Contacts Unify78Turn a messy, multi-source pile of contacts into **one deduplicated, trustworthy address9book** — with a review UI simple enough for a non-technical person to drive. Optionally, mine10iMessage history to find the people worth a deeper OSINT dossier.1112**Why this matters:** duplicates and inconsistency make people stop maintaining their contacts at13all. The win is a single source of truth they'll actually keep feeding.1415**Scope of `local_only`:** Phase A (dedupe) is fully local — no network egress. Phase B is opt-in16and its dossier step (`research-person`) *does* reach the public web (LinkedIn, gov/org sites). Treat17Phase B network access as a deliberate, separately-approved step, never an automatic follow-on.1819**Consent & authorization (read before running):** only operate on contacts/messages belonging to20the user, on a machine they own or have explicit permission to access (including any SSH host). Phase21B researches *other people* from the user's private data — only build a dossier on someone the user22**names explicitly**. Never infer a research shortlist from message volume and run it unattended, and23don't dossier minors or obviously sensitive relationships.2425## When to use2627- "Fix / clean up / merge my contacts", duplicate cards, contacts scattered across accounts.28- Consolidating iCloud + several Google accounts + Zoho + stray VCF/CSV exports.29- "Who do I talk to most?" → rank message contacts, then research the important ones.3031## Two phases (run either or both)3233### Phase A — Unify & dedupe (the core)3435Full plan: **`references/unify-plan.md`** (read it before building). Summary of the workflow:36371. **Confirm the target + sources.** Whose contacts? On the local Mac, or over SSH to another box38 (e.g. `ssh <host>` → `sudo -u <user> -i`)? Inventory which sources exist:39 - macOS Contacts / iCloud — sqlite `~/Library/Application Support/AddressBook/Sources/*/AddressBook-v22.abcddb`40 (read `mode=ro` for rich fields + `ZCREATIONDATE`/`ZMODIFICATIONDATE`); `osascript` for the live41 list and for writes.42 - Google accounts (often several) — Google Contacts export / People API / Takeout `.vcf`/CSV.43 - Zoho, legacy Gmail, and any loose `.vcf`/`.csv` on Desktop/Downloads.44 - **Tag every contact with its source account** as provenance.452. **Back up first, always.** Whole AddressBook DB → `~/Desktop/Contacts-backup-<ts>/`; every source46 → timestamped snapshot in the working dir. This is non-negotiable.473. **Group & classify.** Group by normalized name; cross-link groups sharing a normalized phone48 (last 10 digits) or email even under different names. Classify each: `identical` (auto-merge),49 `combine` (safe union), `review` (disjoint → maybe different people → default keep-separate).50 Name-only cards (businesses, placeholders with no phone/email) can't be matched on identifiers —51 group them by normalized name only and always route them to `review`, never silent auto-merge.524. **Review UI.** Launch the self-contained web page (stock macOS python, `http.server`, no pip).53 Each duplicate group shows every **competing field value** with its **label** (home/work/cell),54 **source account**, and **age** ("edited yesterday", "added 3mo ago"). The user picks the winning55 value per field; losers become secondary labelled fields or are dropped if marked outdated.56 Search/filter for large books. **Save** records decisions; **Apply** performs merges/deletes.57 *Without dates + labels, "Keep/Delete" is meaningless — dates and labels are mandatory.*585. **Apply — dry-run by default.** Nothing mutates without `--live`. Before any delete, export the59 removed cards to `deleted-contacts-<ts>.vcf` (drag-to-restore undo). Auto-merge only truly60 identical cards (same normalized phone/email set — no info lost).616. **Never sync back** to any cloud account until the user names an explicit target and approves.6263**Starter code:** `scripts/contacts_cleanup.py` is the proven single-source (macOS) merger with the64full web review UI, backup, undo, and classification already working. Read it, then extend it to65multi-source ingest + per-field winner selection. Do not rebuild from scratch.6667### Phase B — iMessage mining → dossiers (optional deep dive)6869Full plan: **`references/imessage-dossier-plan.md`**.70711. **Ask the user who's worth a deep dive.** Before mining, offer to focus: high-value people,72 specific groups, or traits (e.g. "Utah legislators", "arts-org board members", "clients",73 "family"). Let them steer the shortlist — don't research everyone. The ranking *informs* the74 conversation; it does not authorize research. A dossier runs only for a person the user names.752. **Mine the message graph (read-only).** Run `scripts/imessage_signal.py` — it copies `chat.db`76 to a temp file, opens it read-only, and ranks handles by volume and recency (plus a 1:1-vs-group77 signal). It reads counts and timestamps only; **message content never leaves the machine**.78 *Reciprocity is an unreliable column* — chat.db nulls the sender on the user's own messages, so79 rank on volume + recency, not reciprocity. Output: ranked `people-signal.csv/json` + gap lists80 ("high-frequency but not in contacts" / "in contacts but never messaged"). Full Disk Access81 required for the running user.823. **Resolve identities** against the unified address book from Phase A (collapse phone+email splits).834. **Fan out dossiers.** For the approved shortlist, invoke the **`research-person`** skill to build84 confidence-marked Obsidian `People/` notes, enriched from public sources appropriate to the85 person's world — LinkedIn, and (Utah-centric example) the state legislature site (le.utah.gov),86 arts-group/nonprofit sites, and the lobbyist registry (disclosures.utah.gov). Private message87 content stays internal context, never published.8889## Safety rules (both phases)9091- Dry-run by default; `--live` only after the user sees what would change.92- Back up before any mutation; every delete is reversible via exported `.vcf`.93- Read-only on `chat.db` (work on a copy); never write the live Messages or Contacts DB directly94 except through the reviewed, `--live`-gated apply step.95- No outbound sync and no message content in any shared vault without explicit approval.9697## Deliverables9899- Working dir (e.g. `~/dev/agent-notes/me/contacts-merge/<person>/`) with source snapshots,100 `contacts-data.json`, `contacts-decisions.(csv|json)`, and the review HTML.101- `Unify My Contacts.command` launcher + a friend-readable `README.md`.102- (Phase B) `people-signal.(csv|json)` + `research-person` dossiers for the approved shortlist.