Contacts TUI screenshot

I haven’t really liked social networking as a development. Sometimes I’d even say I mourn its existence. I used to have people with whom I shared a great email correspondence who abandoned that in favor of the Eternal Holiday Newsletter mode of Facebook, or the “oh, I tweeted about it last week if you want to look it up” of the assorted microblogging services.

The kind of conversation we can have at a BBQ you invited your high school friends to is different from the one we can have over coffee. The kinds of things I’d say to you on my front porch might even be different from what I’d say at the cafe.

So the net effect has been to have a higher volume of very lossy connections at the cost of some very meaningful and higher-fidelity connections.

As I get older, that has gone from something I don’t like to something I feel very concerned about. Alison is constantly on me to stretch before and after our morning runs because she’s right: I do not have the body that once came off a six-week layoff from a parachuting injury to run in the Ft. Bragg 10-Miler the first day I was allowed to run again. Likewise, as much as I have a few habits of thought I try to cultivate to keep from turning completely inward sometimes, there are some parts of the world that don’t become more comfortable to deal with as we age. Some days, my reaction to whatever is going on out there is to wish people would just shut the fuck up about it. I think that’s a fair, reasonable, understandable reaction, but I also think that’s not a great place to go live.

As much as I’m introverted, politely contrarian, and completely over some of the fucking takes I’m reading, I have to stretch. As much as there are some people I’d love to hear more and different from, at some point you just have to acknowledge that for whatever reason, those vibrant email threads petered out and are not coming back, so you have to replenish the supply of human connection.

During my unplanned sabbatical a couple of years ago, I realized I was going to need to settle in for a few months of being at loose ends while I figured out what was next and started to drum up opportunities. I knew I needed to network, and I also knew I needed to just interact with people. Left to my own devices, and without a plan, I knew I’d just go full introvert and I didn’t want to go full introvert.

So I set about leveraging org-contacts to build what I called the plaintext CRM: A bunch of functions that made it easier to keep in touch with people. The core ideas were:

  • Different kinds of relationships need different kinds of attention
  • It’s easy to lose track of a thread, so it helps to have some workflow management when you reach out to someone
  • It’d be nice to have a log of contacts so you could remember where you left off
  • It’d be nice to integrate this with org-mode so ticklers to ping people or follow up, or just mark a contact “timed out” would show up in my agenda each day.

I used it for plain old social stuff during my layoff, and also to track recruiter pings. It helped me feel (and behave) a lot less transactionally about reaching out to people, because while I did have a small pitchfork at my back to get out and network, it reminded me to follow up on social stuff as well.

It was a good system for where I was at, because I was living in Emacs, it was sort of a fun and stunty layoff project to implement something like that in Emacs, and I was able to stick to it because it was right there in my todo system.

Usage fell off once I was back at work and got busy again, which has bothered me on and off for a while now.

Several weeks ago, I fed the elisp to Claude as a proof of concept for an MCP. The repo for that is private because I did put it into production for a period and the security studies on MCP are a little dire. They’re just modulations on common security issues other protocols can present, but I’m not gonna lay bare my vibe-coded contact management infra.

The MCP itself worked as well as a chat-based interface can, and did what a lot of AI stuff does these days: Points to something that is both pretty cool and simply not there yet.

The pretty cool part was making Claude aware of my contacts database and the state of contact with everyone in it. During day-planning, I had it set up to check in on the state of contacts and drop a few pings, followups, or calls into my calendar for the day. That was a small proof of concept for MCPs as a kind of executive assistant.

The parts that were not there yet:

  • It’s a slow interface. An MCP is just API calls, which are fast enough over a local network and hitting a sqlite db, but there’s some overhead to get an LLM to invoke the tool, interpret the results, and do something with them.
  • It’s a lossy interface. I spent a few iterations tuning instructions to get Claude to do the right thing with certain language, but there was still an element of the one-armed bandit effect: Make a comment the wrong way, and Claude would add to a contact’s note instead of recording an interaction, for instance.
  • It’s an opaque interface. Some of the better MCPs include some kind of CLI component so you can directly manage their state. I was stuck with telling Claude to change this or that thing once I realized it had done something I didn’t like – a whole lost several minutes to “show me what you did; change what you did; no not that way – this way.”
  • It’s a bad idea to let an LLM near this kind of information.

On that last point:

You see people advertising their MCPs as “running locally and completely in your control.” Well, okay, but what the LLM is doing with that data is not completely in your control. What the LLM’s owner is doing with that data is not completely in your control. And for a homebrew sort of hobby implementation of a new protocol in a fast-moving technology, you’re assuming extra risk.

Second, I’m just curious, not completely obtuse. I want to learn about these things – work actually requires me to learn about these things – but there are people with heartfelt, sincere, thought-through objections to the AI industry whom I know would not like that even innocuous data about them is being fed into an LLM maw. I’m sorry I picked a project that involved that. I stopped at “actual need and well-understood problem” and didn’t think past that.

So I took the MCP down, which left me a little frustrated: I really did not want to go back to managing contacts in Emacs, and I really do not like any of the alternatives I’ve found for contact management: Finding the sweet spot of actually managing contact information and also managing the workflow of interacting with contacts has been a little hard.

So I teed up one last session with Claude to use the existing MCP workflows, data structures, and actions and turned them into a Go TUI that just repurposed my existing sqlite-hosted data into a database on my local disk. It does pretty much what my original elisp implementation does, but it’s written in Go so it’s a compact binary (and Claude is sort of a Go prodigy compared to experiments in Typsecript, Ruby, and bash).

I’m pretty happy with what I got:

  • It’s fast.
  • It does all the contact lifecycle stuff I used to do in Emacs.
  • It adds the ability to define a custom cadence and style for some individuals, because there are folks you don’t need to reach out to much but if you see they had a nice life event or career development, it’s good to drop in a reminder to ping them with congratulations (or condolences).
  • It has hot keys for most interactions, so there’s not a lot of cursoring around the interface.
  • It has some nice state-setting and filtering hot keys: Quick to find overdue contacts, filter down to family, friends, or professional network.
  • It has archiving, so for that one recruiter from a few years ago at the place you walked away from, you can get them out of the contact management cadence but keep the context for later, just in case.
  • It’s completely local and un-networked. Not only is everyone’s data not going into the LLM maw, it’s not getting sucked in by anyone else trying to build their graph on your friends’ data.

This version is publicly available and pretty easy to build and install on a Mac, at least:

pdxmph/contacts-tui

There are a few features mentioned in the docs for creating a fixtures db you can kick the tires with, or just onboard with a blank db and start using it. There is no contact import tooling. Given the simplicity of the sqlite backend, it’d be pretty easy to lash up a CSV importer, and iirc it is not terribly hard to write some AppleScript to do that from the Contacts app or from a format like VCF.

I’m not sure it solves anything for anyone else the way they’d like, but it hits an intersection that’s useful to me by creating structure to be better at something I often neglect and wish I did better.