AoAH Day 8: Building a contacts CLI manager with Sortal / Dec 2025

I've been accumulating a lot of contacts that I use to write cross references on my website. This works by using Cmarkit to parse my custom Markdown, and spot entries like [@sadiqj] and convert those into a full reference like Sadiq Jaffer.

Today, I want to build a full CLI application that stores all my contacts as Yaml files in my home directory using XDG conventions, and give me a simple search interface so I can quickly autocomplete these posts from my editor. I call this little application "Sortal".

Approach

The data model I want behind sortal is that I can have a flat set of Yaml files somewhere in my XDG path, and that the CLI and library will build those up into data structures for querying and printing. I have around 2500 contacts or so, so this is very manageable without a fancy database.

Sortal uses many of the previous libraries I've been building up so far. I prompted the agent to generate a standalone OCaml project, and to analyse my existing (extremely hacked together) website code to determine a reasonable semantic for Sortal's schema, and then to design a CLI that uses yamlt and xdge along with jsont/cmdliner to plan a user interface with subcommands. I also blended in Fmt and Logs to get nice colours in my terminal. I'm using a lot of libraries from Daniel Bünzli, which is no coincidence as the model needs to have far less context if it only has to use a few, well-designed and modular dependencies.

Architecturally, I used Claude's planning mode for this with the best Opus 4.5 model, along with instructions to maintain a separation between the core jsont schemas, the library logic, and the cmdliner terms. A useful tip is to ensure that you prompt Claude to "ask for any clarifications" after working, and it'll drop you into a custom terminal user interface that structures followup questions. This is a very convenient way of batching answers to the code model.

I did have to do some prompting to refine how the agent designed the xdge and cmdliner integration, specifically by selecting only the XDG dirs that Sortal actually uses. It does work beautifully with the CLI once fixed, as the man page shows:

I also prompted the model to use the man pages to guide a better CLI design, and it came up with a reasonable set of subcommands:

Tests

I'm actually opting not to do any fancy testing for this library, since it's basically a bunch of data munging at this stage and I'm going to using it myself for my own data.

The one thing I did do was to prompt the agent to have an automated import script from my existing Yaml formats, so I could run both in parallel. But aside from that, just keeping an eye the model's inferred success criteria was helpful:

- dune build @check succeeds
- All tests pass (dune runtest)
- Documentation builds without warnings (dune build @doc)
- No __ references in generated HTML docs
- CLI executable works correctly
- sortal.schema builds without eio/xdge dependencies
- Existing API unchanged (backward compatible)

This is all the sort of thing I would have done myself by hand, but the model has now enough previous libraries and memory and the ocaml-metadata skill to figure out my preferred style of libraries.

Results

The proof is in the pudding, so after knocking up a quick import script for my existing contacts, here's it showing Sadiq Jaffer.

The associated Yaml is very straightforward, out of my ~/.local/share/sortal directory:

version: 1
kind: person
handle: sadiqj
names:
  - Sadiq Jaffer
emails:
  - address: sj514@cam.ac.uk
    type: personal
  - address: sadiq@toao.com
organizations:
urls:
  - url: https://toao.com
services:
  - url: https://github.com/sadiqj
    kind: github
    handle: sadiqj
    primary: false
orcid: 0009-0006-4120-3244
feeds:
  - type: atom
    url: https://toao.com/feeds/posts.atom.xml

As a giant quality of life improvement, I also coded up a Git_store module that automated the commit and push of the XDG directory to a private repo on every CLI change. This gives me a super quick way of synching my contacts.

I also left the version field in there to allow for future schema evolution. I guess this would need a jsont codec that only reads the version field and preserves unknown object members and passes them to a versioning module. Something for the future!

Reflections

Well, it works! I'm using this now for this website, and I'm going to add more quality-of-life things as I need it, such as support for thumbnails.

It's very encouraging to see an emerging workflow in OCaml: this language is brilliant at sketching out a big complicated service, and then refactoring to break it up into composable libraries. Having said that, I don't think the agentic coding exhibits particularly good taste in library design, and is nowhere near capable of building libraries of the quality of Daniel Bünzli, but they are very good at using those libraries.

Also of note is that I have zero PPX in this CLI, which I would have used if I were building by hand. Instead, building up boilerplate combinator functions (like jsont codecs) is done pretty well by the coding agent, and results in semantically richer code. I very much appreciate Patrick Ferris maintaining ppxlib, but I do not miss having a dependency on it!


Loading recent items...