sha-toy-ant: having a shifting inner light, like a gem turned over in the hand.
ChatGPT's Apple Messages plugin searches your texts, catches you up, drafts and sends replies, and routes every one of them through OpenAI's servers. Chatoyant does the same job on your Mac with an open-weight model: nothing is uploaded, no account, and it works with Wi-Fi off.
curl -fsSL https://raw.githubusercontent.com/psamme/chatoyant/main/install.sh | sh
cd ~/chatoyant && python3 chatoyant.py chat "what did I miss since yesterday"Not ready to point it at your real inbox? python3 chatoyant.py demo-chat
runs the same assistant on a generated fake one: a week of texts from
made-up friends, a landlord, a dentist, two group chats and a photo. The
GIF above is that inbox, recorded with vhs docs/demo.tape and
docs/retime.py; there is an mp4 of the same run at docs/demo.mp4.
Manual install: clone the repo, brew install ollama, ollama pull qwen3:8b,
ollama pull gemma3:4b, then python3 chatoyant.py doctor to check Full Disk Access,
Ollama and models in one go.
Things you say to it, with the tool it reaches for:
- "what did I miss since yesterday"
catch_up - "what's going on in tahoe crew"
read_conversation - "when did Maya mention the dentist"
search_messages,semantic_search - "tell him count me in" (a draft in your own texting voice)
my_voice,send_message - "text Maya, Theo and Luca that tahoe is on"
send_message, one 1:1 text each, never a new group - "summarize my unread messages and tell me if any need a response"
catch_up, every unread thread of any age - "what's in the photo Luca sent"
find_photos,view_image - "find a free slot Thursday and put it on the calendar"
calendar_events,add_calendar_event - "what's my texting ratio with Maya"
texting_stats - "what was everyone texting me about a year ago today"
on_this_day - "who reacted to my last message in family"
read_conversation - "what's Jordan's number"
find_contact
Sending is gated. CHATOYANT_SEND=ask (the default) opens a native macOS
dialog showing the exact recipient and text, and nothing goes out until
you click Send. off returns drafts only. on sends without asking, for
automations you fully trust. The model cannot set this flag itself.
Inside the chat: /model switches models, /send ask|off|on changes the
gate, /clear starts over, /help lists the rest. Ctrl-D quits.
Every message it quotes gets a small number. /goto 2 opens Messages.app
on that exact message (highlighted, scrolled into view); /find bring a sweater does the same for any words you remember; /open maya opens a
thread. Messages has no link to a single message, so /goto drives its
search field for you, which needs Accessibility permission for your
terminal (System Settings > Privacy & Security > Accessibility) and was
built on macOS 26. If that step cannot run, it still lands you on the
thread and says why. In terminals that support links (iTerm2, Ghostty,
Kitty, WezTerm, VS Code) each quote is also cmd-clickable and opens its
conversation.
chat.dbis opened read-only (?mode=ro). Nothing is ever written to it.- The only network call is to Ollama at
127.0.0.1:11434, or whateverOLLAMA_HOSTyou set. The host is printed at startup so you can see it. - stdout and logs carry counts and errors, never message content.
- Sending goes through a native confirmation dialog with the exact text.
- No dependencies beyond Python 3.10 and Ollama (numpy only if you build the semantic index). Nothing to audit but this repo.
- Photo thumbnails and descriptions are cached in
.cache/, gitignored, deletable any time. Everything generated from your archive is gitignored.
Requirements: macOS, Full Disk Access for the terminal or app launching it (System Settings > Privacy & Security), and a one-time Messages automation prompt on first send. Calendar uses a small EventKit helper compiled on first use; macOS asks once.
Chatoyant is also an MCP server, so the same tools work inside any app that speaks MCP.
python3 chatoyant.py mcp-config # prints the JSON to paste into the appIt talks to its client over stdio only. The privacy story then depends on the model the client uses: attach a cloud model (Claude, GPT, Gemini) and your texts go to that cloud, tool call by tool call. Attach a local one (LM Studio, Jan, Ollama in Cursor) and they don't.
chatoyant chat uses gpt-oss:20b if you have it pulled (reasoning
capped at low), otherwise qwen3:8b. Photos go through gemma3:4b.
Override with CHATOYANT_MODEL and CHATOYANT_VISION. The model is loaded
while you type and stays warm between questions.
An M4 with Low Power Mode off does about 26 tok/s on gpt-oss:20b and a full
catch-up in about 30 s. With Low Power Mode on, everything runs 3-4x
slower; chatoyant chat warns you when it is on. Turn it off in System
Settings > Battery.
ollama pull nomic-embed-text
python3 chatoyant.py index # embed your archive once (~30 min)
python3 chatoyant.py update # add new texts only (seconds)
python3 chatoyant.py watch --install # run update whenever Messages writesEvery other tool reads chat.db live; the index only matters for
semantic_search and the archive app below.
Chatoyant also builds a local app over your iMessage, Instagram and Google Chat history: a decade of texting in charts, yearbook superlatives judged by a local model, a memories page for what you and your people said on this day every year, close reads of the people you text most, and an ask-your-history chat with sources. This half also lives on its own at psamme/message-stats.
python3 chatoyant.py setup # point it at your Instagram / Takeout exports (optional)
python3 chatoyant.py demo # tour with fake data
python3 chatoyant.py build # your real archive -> app/index.html
python3 chatoyant.py serve # ask-your-history UI at http://127.0.0.1:8765Screenshots show generated fake data.
| ChatGPT Apple Messages plugin | Chatoyant | |
|---|---|---|
| Search, catch up, draft, send with approval | yes | yes |
| Photos and calendar | yes | yes |
| Where your texts are processed | OpenAI's servers | your Mac |
| Account or subscription | ChatGPT plan | none |
| Works offline | no | yes |
| Writing quality of drafts | frontier model | open 8B-20B model, plainer |
| Native window | yes | terminal, or your MCP client |
| Texting stats, on-this-day, semantic search over your whole history | no | yes |
| Source you can read | no | one Python file per piece, MIT |
python3 chatoyant.py doctorChecks Full Disk Access, that chat.db opens read-only, that Ollama is
reachable at the printed host, which models are pulled, and whether Low
Power Mode is on. Run it first if anything feels off.
Does it send my texts anywhere? No. The only outbound connection is to
Ollama on your own machine. Three ways to check, pick your favorite:
run lsof -i -P | grep -i python while it answers a question and you will
see one connection to 127.0.0.1:11434; put Little Snitch or LuLu in
front of it; or turn Wi-Fi off and keep asking questions.
Can it read messages I've deleted? Only what is still in chat.db.
It reads the same database Messages.app does, nothing more.
Does it work with SMS and group chats? Yes. Anything in Messages.app is fair game, including reactions and attachments.
Will it send something by accident? Not with the default ask setting:
every send is a native dialog you have to click. demo-chat sets sending
to off.
The tool surface is one list in assistant.py: add a method and a schema
entry and it appears in both the CLI and the MCP server. Run
python3 -m unittest discover tests before opening a PR. MIT.

