HTTP/1.1 200 OK
- X-Thesiscurl is all you need
- X-Shapeprose in, headers out
- X-StoresHTTP messages, verbatim
- X-Installpip install curldb
- Content-Typetext/plain
Every message an AI sends is already an HTTP message. curldb keeps them: one SQLite file per session, queried by the envelope, zero daemon, zero dependencies.
HTTP is the best control plane
A message has three places. A status line for what happened. Headers for what the message is. A body for the payload. Thirty years of software already reads those three places: proxies, caches, logs, rate limiters, your terminal.
AI APIs put the method in one JSON field, the role in another, the content in a third, and send the whole thing over HTTP. HTTP inside JSON inside HTTP. The moment a header hides inside prose, every tool on the wire goes blind.
Ask a model to answer as an HTTP response and it does, with headers nobody taught it: X-Verdict, X-Confidence, X-Scope. It learned the shape from the web. The envelope was always there. The API swallowed it; curldb keeps it.
Prose in, structure out. Not JSON.
You ask in plain words. The answer comes back with a status line and headers. That is structure without a schema: no fields you had to define first, no parser to keep in sync. A header like X-Confidence: 0.8 already means something to every reader that knows HTTP, models included. Store the message and the headers are the index; nothing had to be extracted.
HTTP already has a place for the verb, the resource, and the outcome. Putting them inside the payload again gives you the oldest joke on the wire:
HTTP/1.1 200 OK
Content-Type: application/json
{"status": 404, "error": "not found"}
Every proxy, cache, and log on the way saw a success. Only the parser at the far end knows it failed. The status line was right there, unused.
tee
The conversation stays on your screen. tee sends a copy to curldb. Close the terminal; the copy is still there.
$ claude 'how do I design a REST API for a todo app?' | tee /dev/tty | curldb add
HTTP/1.1 200 OK
X-Scope: design
X-Confidence: 0.8
Nouns, not verbs. /todos and /todos/42. POST creates, PATCH edits, DELETE removes. Let status codes carry the outcome.
#1 200 +2 headers
$ claude 'latest news on Rust async?' | tee /dev/tty | curldb add
HTTP/1.1 200 OK
X-Scope: news
X-Confidence: 0.6
Async closures stabilised. The runtime story is unchanged: pick one, stay on it.
#2 200 +2 headers
$ curldb query 'header:X-Scope=design'
1 09-11 11:13 200 Nouns, not verbs. /todos and /todos/42. POST creates
$ curldb tags
X-Scope: design(1) news(1)
X-Confidence: 0.8(1) 0.6(1)
Each reply prints as usual and lands in curldb with a number. Nothing was parsed on the way in; the headers the model wrote are the index. Query by status, any header, or full text of the body.
A post office
A post office takes anything with an address on it, keeps the envelope with the letter, does not open the letter, and stamps a number on the way in. That is the whole of curldb. What sits in the file is not a record about a request; it is the request itself, envelope and all. Same for a response.
Because the stored thing is the message itself, it can be sent again. A stored request goes back out on the wire as a request:
$ curl -s localhost:200/1 | nc example.com 80
And the other direction: a message that already is a message goes in as itself. Content-Type: message/http tells the post office that the body is the letter, envelope included, so a response is filed as a response:
$ printf 'HTTP/1.1 409 Conflict
X-Verdict: shaky
that contradicts the earlier decision' | curl -X POST localhost:200/ -H 'Content-Type: message/http' --data-binary @-
HTTP/1.1 201 Created
Location: /3
A stored response can be handed to a client as a response. Replaying, mocking, forwarding through nginx, re-annotating with a different model: none of it needs a converter, because nothing was converted going in.
curldb keeps. Whoever sends, sends.
Four rules
- One session, one file
- The file name is the session. Backup is
cp. Searching across sessions is querying each file in turn. - Headers are tags
- No schema.
X-Verdict: shakyis unreadable to a program and obvious to a model, so an unknown header is fine.curldb tagslists what exists; the vocabulary grows out of the data. - The body is never touched
- Whoever adds headers, you, the model, an annotating model, a hook, adds headers only. Annotators sign with
Via:. - The server is a cabinet
- Every stored message gets a number; a POST answers with it in
Location.GET /2returns message 2 exactly as it was stored;Content-Typesays what it is:message/httpfor a request or response,text/markdownfor a note. A stored reply is data; it is never replayed as the server's own answer. Localhost, no token, one CLI.
Where the messages come from
Nobody types HTTP. People speak in prose; machines add the envelope.
The model answers in HTTP because its system prompt asks it to. Your own words get wrapped on the way in: a small model on a tee branch reads what you typed and writes the headers, X-Topic, X-Intent, whatever fits, without touching a word of the body. Tool calls need no model at all; a hook translates them mechanically, POST /tool/Read with JSON args, a response with the result. Markdown notes with YAML front matter are header plus body already.
One turn of a conversation, one file, one index over all of it: what was said, what was decided, which tool failed, which conclusion did not hold.
pip install curldb
One file, standard library only, Python 3.10 or newer. MIT.