MojoPad

The Local API

MojoPad has a local API, and anything running as you on this Mac can use it. A Shortcut that files a note into today’s journal. A shell script that pulls every page of a kind into a report. A Keyboard Maestro macro that searches your wiki by meaning from any app. Another program you wrote, reading a page by name. None of it needs an AI, an account, or the internet.

Not to be confused with the mojo API, which is a different thing with a similar name. That one runs inside a page, in scriptlets and plugins, and is described under Scripting. This one is spoken to from outside MojoPad, over a connection on your own machine. If you are writing something that is not a MojoPad page, this is the one you want.

The same door an assistant uses is open to anything else you run. When Let agents reach the wiki that’s open right now is on, MojoPad answers a small set of requests on your own machine — so a shell script, a Shortcut, Keyboard Maestro, or any app you write can read from and add to the wiki you have open. Nothing on the internet can: the door is on 127.0.0.1, it needs a key, and it refuses anything that looks like it came from a web page.

Finding it. Each time MojoPad starts it writes ~/.mojopad/bridge.json — the port it is listening on and a fresh key, readable only by you. A script reads that file and puts the key in an Authorization: Bearer header. The key changes every launch, so nothing can hold one from last week.

PORT=$(python3 -c "import json;print(json.load(open('$HOME/.mojopad/bridge.json'))['port'])")
TOK=$(python3 -c "import json;print(json.load(open('$HOME/.mojopad/bridge.json'))['token'])")

curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:$PORT/info

What you can ask for

Everything below is POST with a JSON body, except /info. ref means a page’s name or its id.

AskBodyAnswers with
/infothe app version, the contract version, and which wiki is open
/pages{"limit": 200, "offset": 0}every page’s name, id, type and when it changed
/page{"ref": "Lend Lease"}that page’s words, tags and other names
/props{"ref": "Lend Lease"}its typed properties, each with where its value came from
/search{"query": "…", "k": 6}pages ranked by meaning
/ask{"question": "…"}an answer from your local model, with its sources
/add{"title": "…", "content": "…"}makes a page
/append{"ref": "…", "text": "…"}adds to the end of a page
/today{"text": "…"}adds to today’s note
/move{"ref": "…", "folder": "Reading/Papers"}moves a page that already exists into a folder
/folders{}the folders in the open wiki — their names, their nesting, and the text to file into one by. Never which pages are in them.
/identify{"ref": "…", "value": "…"}records a paper’s number on a page as a draft, asked about every time

The five marked ✎ change your wiki, and they are refused unless Allow agents to modify wikis you already have is also on. Reading is never affected by that switch.

Moving is the narrowest of them. It changes which folder holds a page and not one word of the page — it does not read what is on it, and it cannot make a page or remove one. A name that answers to nothing is refused rather than turned into a new page, so an assistant that misremembers a title gets told so. The question you are asked names the page by the name your wiki spells it — an assistant often holds a paper’s number rather than its title — and the folder the page will really land in.

And it hands back nothing about where the page was. Nothing an assistant can ask over this connection returns which folder holds a page, and a move is not a way around that: it answers with where the page is now and no more. Which papers you keep under which folder is the shape of your thinking, and it is not something a run of moves should be able to read off its own replies.

The way back covers the whole run. An assistant sorting a shelf moves in a loop, so the moves that arrive together are collected and one offer to put all of them back is raised when they stop — every page to the position it held, along with any folder the run invented to hold them and nothing has been put in since. It carries no timer, because you were not the one at the keyboard.

The rules it keeps, so you don’t have to

  • A locked page stays locked. Reading one tells you it exists and that it is locked; it does not hand over the words. Nothing can be added to one either.
  • Text that arrives this way cannot run. A script may put words on your page; it may not make your page do things. Text that would add a scriptlet is refused outright — not cleaned up and accepted. That distinction is the whole of it: tidying the text away can be worked around by sending it in two halves, or in a shape that rebuilds itself once the middle is removed, whereas a refusal cannot. MojoPad works out what the page would become and asks the part of itself that actually runs scriptlets how many it can see; if the answer went up, the whole addition is turned down.
  • A page that is a program is not a page to add a line to. Nothing can be added to an Event: or Plugin: page from outside — their words are the code, and an Event: Open page runs every time the document is opened.
  • A board is a layout, not text. Storyboards and mind maps refuse an addition rather than have a line of prose written into the shape that holds their cards.
  • A page lives in one folder. Moving one takes it out of the folder it is in rather than leaving a copy behind — the same rule the sidebar keeps when you drag a page yourself. A page already in the folder it was asked for is left alone and reported as such.
  • It answers for the wiki you have open. There is no way to name a different one. For a document that isn’t open, the file-based tools an assistant uses are the right door — see the rest of this chapter.

The contract has its own version. /info reports api alongside the app version. The app version moves every release; the api number moves only when the shape of a request or an answer changes, so a script can tell the difference between MojoPad being newer and MojoPad being different.