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.
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.
| Call | Does |
|---|---|
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.
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).
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.
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.
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.