MojoPad

Scripting: Plugins

Any page named Plugin: Something is a JavaScript program. It appears in the Plugin menu and in the Scripts palette, and runs with full access to the document through the mojo API. The Scripts palette also has a script runner (⌘⏎ to run) for one-off experiments.

Nothing runs until you say so

Code in a document is gated per document, and MojoPad asks once. Scriptlets, plugins and Event pages all wait behind the same question: do you trust this document to run code? Until you answer yes, none of them run — which is what stops a wiki somebody sends you from executing anything the moment you open it.

So if a plugin you just wrote appears to do nothing, this is the first thing to check. The answer is remembered per document, and only pages you wrote yourself are eligible in the first place — a page that arrived from a followed folder, an import, an agent or a link never runs, however it is named. See Only pages you wrote will run under Scripting: Event Pages.

The mojo API

CallDoes
mojo.pages()id, name, type, tags, aliases of every page
mojo.read(ref)plain text of a page (by name or id)
mojo.readHtml(ref)stored HTML of a page
mojo.write(ref, text)replace a page's content
mojo.append(ref, text, opts?)append to a page. {format: 'html'} adds markup — a table, a list — rather than the characters that would write one. Both arguments are required
mojo.currentPage()the page you are looking at, in the same shape pages() returns — or null if none is open
mojo.insertAtCursor(text, opts?)insert where you are typing, rather than at the end of the page
await mojo.create(name, text?, opts?)create a page (no-op if it exists); returns its id. {folder: 'Reading'} files it
mojo.move(ref, folder)move a page that is already here into a folder — the other half of filing. It never makes a page
mojo.exists(ref)does a page answer to this name?
mojo.navigate(ref)open a page
await mojo.alert(msg) / await mojo.prompt(label)talk to the user
mojo.getMeta(ref, key) / mojo.setMeta(ref, key, value)read/write page meta (the Meta palette). Both arguments are required on getMeta
mojo.today()today's date, formatted per your settings
await mojo.fetchData(url)the bytes of a file on the web — a paper, an image — asked for like any other fetch
await mojo.addFile(name, bytes, { aliases, folder })keep a file in this wiki — a paper it fetched, a chart it built. {folder: 'PDF Library'} says where it lands instead of Unfiled
mojo.collections()the folder tree, as the sidebar shows it — each folder’s id, its title, its nesting, and the page each leaf points at. The id is the one thing that survives a rename, and it tells two folders apart when their names differ only in case
mojo.log(…)record a line in the run log — it does not interrupt
await mojo.mcp.servers()the tools you have connected
await mojo.mcp.tools(name)what one of them can do
await mojo.mcp.call(name, tool, args)use it — you are asked the first time each tool is used in a wiki

The three mcp calls name a tool the way you named it in Settings ▸ AI Agents ▸ Tools you connect, never an address or a command. There is a worked example in the AI chapters.

When a call is wrong, it says so

Two of these used to fail without a word, and both now throw with the form they wanted. mojo.append(text) with one argument resolved your CONTENT as a page name, found no page called that, and returned false — a plugin that appended nothing looked exactly like a plugin that worked. mojo.getMeta(key) with one argument failed somewhere inside instead of telling you it wanted two.

If a call throws, the message names the shape it expects. For the page in front of you: mojo.append(mojo.currentPage().id, text).

Working on the page you are looking at

You do not have to ask which page it is. mojo.currentPage() hands back the open page, so a plugin that adds to "this page" needs no prompt — which also avoids guessing between pages whose names repeat.

const here = mojo.currentPage()
if (here) mojo.append(here.id, '<table>…</table>', { format: 'html' })

And mojo.insertAtCursor(text) puts the result where you are typing rather than at the end — the right move for a plugin that answers something you just asked.

A complete plugin

Create a page named Plugin: Stale Pages containing:

const cutoff = Date.now() - 90*24*3600e3
for (const p of mojo.pages()) {
  if (!mojo.read(p.name)) continue
  mojo.log(p.name)
}

…then run it from the Plugin menu. Scripts are plain modern JavaScript (await works at the top level). Locked encrypted pages are invisible to scripts, like everywhere else.

Built-in plugins

The Plugin menu also ships with Word Count, Alphabetize, Strike Out and Move to Bottom (⌃⌘⌫), Apply Default Font to Page, Record Audio… (embeds a voice memo right in the page), and Send Page by Email.