ReadAware

Plugin API reference

A plugin is a folder holding a manifest.json and one JavaScript module. This page is the authoring contract; the same contract ships as a TypeScript declaration file (types/plugin-api.d.ts) in the marketplace repository, so editors autocomplete everything below.

Anatomy

my-plugin/
  manifest.json
  main.js        # one self-contained ES module

main.js default-exports a lifecycle object. Everything a plugin can reach comes through the context handed to activate; every register* and on call returns a disposable that the app reclaims when the plugin is disabled or uninstalled, so deactivate only needs to release the plugin's own external resources.

export default {
  activate(ctx) {
    // register contributions via ctx
  },
  deactivate() {
    // optional: close sockets, flush queues
  },
};

Enabling and disabling take effect immediately — no app restart. Write in TypeScript if you like (recommended; see Publishing) — what the app loads is always the built main.js.

manifest.json

{
  "id": "anki-sync",
  "name": "Anki Sync",
  "version": "0.1.0",
  "minAppVersion": "0.3.0",
  "description": "Send looked-up words to Anki.",
  "author": "you",
  "permissions": ["service:network", "annotations:read"],
  "main": "main.js"
}
FieldMeaning
idLowercase letters, digits, hyphens (max 64). Must equal the folder name; namespaces the plugin's storage and tools.
name, versionShown in Settings → Plugins and the marketplace.
minAppVersionLowest app version the plugin supports. This contract requires 0.3.0 or newer.
permissionsWhat the plugin uses (table below). Shown to the user before installation.
mainEntry module relative to the folder; defaults to main.js.
settingsOptional declarative settings form (same field shapes as form views). The app renders it in the plugin panel and persists the values as one object under the storage key settings.

The domain model

The data surface is derived from the app's domain model rather than authored beside it. Each domain — books, collections, annotations, reading, conversations — is a namespace on ctx exposing three things:

  • reads — the domain's read models (what the app's own surfaces render);
  • writes — commands under .write that mirror exactly the domain's event verbs and go through the app's own event-sourced write path, stamped plugin:<id> in the event log so every plugin write is attributable;
  • subscriptions.on(event, handler) over the domain's events under their canonical names (book.starred, highlight.created, …) — the same vocabulary the app itself records.

Permissions follow the same shape: <domain>:read / <domain>:write, and within a domain write implies read. Device-local state (view preferences, reader appearance, sync internals) and free-form rendering are deliberately not plugin surface — UI goes through the declarative views below.

Permissions

Capability groups on ctx are simply absent unless their permission is declared — API-level gating against accidental overreach. Namespaced storage, UI contributions, session events, and reader navigation are not permissions; every plugin has them.

PermissionGrants
books:readctx.books — the shelf's books, a book's table of contents and chapter text.
books:writectx.books.write — import files, edit metadata, star, remove; content providers and virtual books.
collections:read / collections:writectx.collections — the shelf's user-defined groups: list and membership; create, rename, remove, assign books.
annotations:read / annotations:writectx.annotations — highlights, notes, and asked questions; create, recolor, edit, and remove highlights and notes (asks are agent-written, read-only).
reading:readctx.reading — positions, statuses, and reading time. Read-only by design: its events are recorded facts of reader activity, not user commands.
conversations:readctx.conversations — per-book AI threads and global threads (read-only).
agent:toolsctx.agent.registerTool — tools for the reading assistant.
service:networkctx.network.fetch — outbound HTTP.
service:llmctx.llm.ask — one-shot model calls on the user's configured account. No thread, no memory, no tools.
service:dictionaryctx.dictionary.lookUp — the app's dictionary (shares its cache and target-language preference with the reader; uses the user's AI). Pass language for an explicit target, or use getLanguage / setLanguage for the shared preference.
service:clipboardctx.clipboard.writeText.

Contributions

Selection actions

Entries in the reader's selection and annotation menus. The handler receives the selected text, its CFI range, the chapter, and the book. When available, context contains the surrounding passage. Inside the reader an action either runs silently (return a toast) or opens a dialog (return a view) — those are the only two outcomes. Declare presentation: "dialog" when the handler is async: the host opens its loading shell immediately and fills the same request when run resolves. A dictionary-style action may declare role: "lookup"; the host then routes its existing Look up keyboard command to that plugin action instead of maintaining a second built-in lookup path.

ctx.ui.registerSelectionAction({
  id: "save-quote",
  title: "Save quote",
  icon: "quotes",
  presentation: "dialog",
  run: (input) => {
    // input: { text, context?, cfiRange, chapterHref, book, source }
    return { toast: "Quote saved." };
  },
});

Header actions

An icon button on a top bar. On the reader surface the view opens as an anchored popover; on the shelf it opens as a popover or a full page, per presentation. The reader never allows full-page interruptions.

ctx.ui.registerHeaderAction({
  id: "reading-report",
  title: "Reading report",
  icon: "chart-line-up",
  surface: "shelf",
  presentation: "page",
  view: async () => ({
    kind: "markdown",
    title: "This week",
    markdown: "You read **4h 12m** across 3 books.",
  }),
});

Commands

A command-palette entry. All plugin actions also appear in the palette automatically; explicit commands are for actions with no button.

ctx.ui.registerCommand({
  id: "sync-now",
  title: "Anki Sync: sync now",
  run: async () => ({ toast: "Synced." }),
});

Agent tools

Tools the reading assistant may call during chat (requires agent:tools). parameters is plain JSON Schema for the arguments object; omit it for a no-argument tool. Tools are namespaced plugin_<pluginId>_<name> before they reach the model, and calls are visible to the user as tool steps in the chat.

ctx.agent?.registerTool({
  name: "search_deck",
  label: "Searching your Anki deck",
  description: "Search the user's Anki collection for a term.",
  parameters: {
    type: "object",
    properties: { query: { type: "string" } },
    required: ["query"],
  },
  execute: async ({ query }) => {
    const res = await ctx.network.fetch("http://127.0.0.1:8765", {
      method: "POST",
      body: JSON.stringify({ action: "findNotes", query }),
    });
    return res.json();
  },
});

Views

Plugins declare a tree of host components; the app renders every visual primitive and control. Plugins never provide JSX, HTML, CSS, or classes.

  • markdown — a markdown string, typeset by the app.
  • list — searchable host lists with fixed debounce, keywords, accessories, and empty states. timeline adds Today / This week / This month / All filters and local-date groups; an item can use presentation: "dialog" to show its returned view over the list instead of pushing a child page. List-level actions are host-rendered icon buttons; timelines place them at the far right of the tab row.
  • form — text, textarea, number, select, choice, checkbox, and toggle controls from the ReadAware component library, plus onSubmit.
  • detail — Raycast-style primary content, metadata, and host-rendered controls and actions. Semantic select controls stay by the content heading; dialogs keep provenance, dates, and tags in a quiet line beneath it, while actions sit beside the host Close button in a fixed footer.
  • blocks — host typography, markdown, dictionary content, metadata, quotes, actions, metrics, progress, tags, alerts, sections, groups, and responsive columns. Columns expose only bounded weight, spacing, minimum-width presets, and semantic alignment. Exact CSS and wrapping stay inside the design system; declarations are runtime-validated and nesting is capped.

Handlers (run, onSelect, onSubmit) all return the same result shape:

  • nothing — the surface stays as it is;
  • { toast: "…" } — a transient notice;
  • { view } — open, or push onto, the surface;
  • { view, navigation: "replace" | "reset" } — replace the current view, or return to a new root view;
  • { close: true } — dismiss the surface (composable with toast);
  • { fieldErrors } — from a form submit: stay on the form and show errors under the fields.

Async work is a non-event: return a promise and the app shows the loading state. Icons are chosen by name from the app's curated Phosphor set — no custom SVG.

Domain data

Each granted domain namespace offers reads, canonical event subscriptions, and (with the write permission) commands. In brief:

  • ctx.bookslist(), get(id), getToc(id), getChapterText(id, index); write: import, editMetadata, setStarred, remove, plus content providers (below).
  • ctx.collectionslist(), booksIn(id); write: create, rename, remove, assignBooks(bookIds, collectionId | null).
  • ctx.annotations list({ bookId?, kind?, query? }) returns a discriminated union of highlights, notes, and asks; write: createHighlight, recolorHighlight, removeHighlight, createNote, updateNote, removeNote.
  • ctx.readinggetState(bookId), listStates(), getTime(bookId).
  • ctx.conversationsgetBookThread(bookId), listThreads(), getThread(id); subscribe via on (aiConversation.started, aiMessage.appended, aiMessage.removed, aiConversation.cleared).

Events

Two classes, deliberately separate. Domain events are the facts the app records; subscribe per domain, under canonical names, with the domain's read permission. Each delivery is { type, payload, createdAt, origin } — origin says which software actor produced the fact (user, agent, system, or plugin:<id>).

ctx.annotations?.on("highlight.created", ({ payload, origin }) => {
  // payload: { highlightId, bookId, text, color?, … }
});
ctx.books?.on("book.removed", ({ payload }) => { /* { bookId } */ });

Session facts describe what is on screen right now. They never enter the event log and need no permission: ctx.session.on(event, handler).

Session eventPayload
book-opened{ book: { id, title, author? } }
book-closed{ bookId }
chapter-changed{ bookId, chapterHref }
reading-progress{ bookId, fraction } — fires on page turns, fraction 0..1

Content providers and virtual books

With books:write, a plugin can put real books on the shelf. import takes a file's bytes. Content providers skip the file entirely: register a provider, add virtual books bound to it, and serve HTML sections when the book is opened. The reader paginates, annotates, and tracks progress on them like any book — an RSS feed as a book is exactly this.

ctx.books?.write?.registerContentProvider({
  id: "rss",
  async load(key) {
    const feed = await fetchFeed(key); // your code, via ctx.network.fetch
    return {
      title: feed.title,
      sections: feed.items.map((item) => ({
        title: item.title,
        html: item.contentHtml,
      })),
    };
  },
});

await ctx.books?.write?.addVirtualBook({
  providerId: "rss",
  key: "https://example.com/feed.xml",
  title: "Example Weekly",
});

Storage and settings

ctx.storage is a namespaced key-value store persisted with the app's local data — get, set, remove. If the manifest declares settings fields, the app renders the form and the values arrive at ctx.storage.get("settings") as one object.

For structured data, ctx.storage.collection(name) opens a named document collection — put / get / delete / list over per-document records, with optional bookId / anchor provenance you can filter by. Provenance is an index, not ownership: documents survive the referenced book's deletion, and the collection's lifecycle belongs to the plugin (uninstall clears it). The built-in Dictionary plugin and its saved-word timeline are built entirely on this tier.

Ambient context

Always available, no permission needed:

  • ctx.manifest, ctx.appVersion;
  • ctx.ui.showToast(message);
  • ctx.ui.exportFile({ filename, content, mimeType? }) opens the host save flow for generated CSV, JSON, Markdown, or other UTF-8 text;
  • ctx.session.on(…) — the session facts above;
  • ctx.reader.openBook(bookId) and ctx.reader.goTo({ bookId?, cfi?, href? }) — navigate the reader (user-visible control, no data exposure).

Stability

This is contract v2, shipped in app 0.3.0 — a deliberate breaking rebuild that derived the whole surface from the domain model (v1 manifests fail installation with a readable error). From here the API grows additively: new domains, new event names, new block kinds. Breaking changes to what is documented here are treated as bugs. Declare minAppVersion for anything that depends on a recent addition.