Technical
Project and model lists
On the Agent tab of SpeakWith from Gumroad, Destination has three menus: Agent, Project, and Model. SpeakWith does not go looking for folders or models on its own. A program on your Mac tells it what to list, over one local address, and this page shows what that program sends. It is written for people who write such a program. If you only use the Agent tab, the Send to agent guide is the page to read.
Before you start
The Engine that ships with SpeakWith from Gumroad keeps a small catalog service at http://127.0.0.1:9896. It starts with the Engine and stays up when you close the SpeakWith window, so a list pushed while the window is closed is there when it opens again. The service listens on this Mac only. Nothing on this page leaves the machine.
Check that it is listening before you write anything:
curl http://127.0.0.1:9896/healthThe answer is {"ok":true,"role":"agent_catalog"}. Anything else means the Engine is not running, and every call on this page needs it. Start the Engine and try again.
Projects
The Project menu is filled by one call. It replaces the list you sent last time with the one you send now, so send everything you want listed, every time.
Send the folders you know, whole, under a name of your own as the publisher:
curl -X PUT http://127.0.0.1:9896/catalog/menus \
-H 'Content-Type: application/json' \
-d '{
"v": 1,
"publisher": "my-session-manager",
"projects": [
{
"id": "lighthouse-book",
"path": "/Users/you/Writing/lighthouse-book",
"label": "lighthouse-book",
"last": 1790720000,
"section": "recent"
},
{
"id": "field-notes",
"path": "/Users/you/Research/field-notes",
"label": "field-notes",
"section": "other"
}
]
}'Each row has an id, a path, and a label. The label is the text in the menu. Two fields are optional: last is the Unix time, in seconds, the folder was last worked in, and section is recent or other.
What you see on the Agent tab: rows marked recent, with a last time, appear under Recent, each with how long ago that was, such as 2 h ago. Every other row appears under All projects. When nothing was picked before, the first row is picked for you. A pick that is still in the new list stays picked.
A folder someone adds from the app, through Add folder… under Project, is not yours to replace.
SpeakWith sends that folder as one call of its own and marks the row manual, so your next menus push leaves it in the list. You can make the same call yourself. The folder has to exist on this Mac.
curl -X POST http://127.0.0.1:9896/catalog/projects \
-H 'Content-Type: application/json' \
-d '{ "path": "/Users/you/Writing/lighthouse-book", "label": "lighthouse-book" }'The menus push replaces your projects and your own model rows. If you send destinations, it replaces those as a whole. It does not touch model rows another agent advertised, and it never changes a pick that is still a row.
Models
The Model menu reads Agent default until some list arrives. There are two ways to hand one over. Use the first when your program is the one filling the Project menu anyway. Use the second when your program is the agent itself, connected over ACP.
On the same menus push.
Add a models list to the menus push above. Each row has an id, the value your program understands when the pick is handed back, and a label, the text in the menu. A description is optional.
"models": [
{ "id": "claude-opus-5-5", "label": "Opus 5.5" },
{ "id": "claude-sonnet-5-5", "label": "Sonnet 5.5", "description": "Faster, for a second pass" }
]Rows sent this way are filed under your publisher. Your next push replaces them and leaves the rows other agents advertised alone.
On session/new.
An agent connected over ACP answers session/new. Put the models in that answer as a configOptions entry whose category is model. SpeakWith reads it and stores the list under that agent with PUT /catalog/models/{agent}. Your program never calls the catalog for models at all.
{
"sessionId": "s-1",
"configOptions": [
{
"id": "model",
"category": "model",
"currentValue": "claude-opus-5-5",
"options": [
{ "value": "claude-opus-5-5", "name": "Opus 5.5", "description": "Most capable" },
{ "value": "claude-sonnet-5-5", "name": "Sonnet 5.5" }
]
}
]
}Each value becomes the model id and each name the label; leave name out and the value is shown. currentValue names the row the agent is on right now. SpeakWith also reads the older models.availableModels shape, but when an answer carries both, the configOptions entry wins.
What you see on the Agent tab: with the first way, the Model menu fills as soon as your push lands, before any agent has been contacted. With the second, it fills once that agent has answered session/new, which happens when a session is opened with it. Either way the status line ends in default model until someone picks a row, and a pick that is still in the list stays picked across pushes.
Reading it back
Every write answers with the whole catalog as it now stands, so you never need a second call to see what your change settled to. You can also read it at any time, and the answer includes the current pick as selection, with agent, projectPath, and modelId:
curl http://127.0.0.1:9896/catalogTo be told when something changes, open http://127.0.0.1:9896/events. It is a server-sent event stream with one catalog event per change. The event carries no detail; read the catalog again when it arrives.