Skip to content

Addons

Tokori supports addons in the same shape Anki does: drop a folder into your addons/ directory, restart, and your custom study mode (or translate engine, or vocab importer) shows up alongside the built-ins.

Status: preview. Discovery, manifest validation, and the enable/disable UI are live in the current build. Actually executing an addon's JS in a sandbox is gated on a future release. Enabling an addon today persists your choice so it activates the moment Stage 2 ships — no migration needed.

Where addons live

OSFolder
macOS~/Library/Application Support/ai.tokori.desktop/addons/
Linux~/.config/ai.tokori.desktop/addons/
Windows%APPDATA%\ai.tokori.desktop\addons\

Easiest way to open it: Settings → Addons → Open addons folder.

Each addon is one subfolder. The folder name is yours to choose — the canonical identifier is the id field inside manifest.json.

Manifest

Every addon needs a manifest.json at the folder root:

json
{
  "id": "hsk-cloze-quiz",
  "name": "HSK cloze quiz",
  "version": "1.0.0",
  "description": "Cloze-style review for HSK vocab.",
  "kind": "study",
  "entry": "index.js",
  "author": "you@example.com",
  "homepage": "https://github.com/you/hsk-cloze-quiz",
  "license": "MIT",
  "minAppVersion": "0.1.0"
}

Required fields

FieldRule
idLowercase kebab-case, 3-64 chars, starts with a letter. This is the stable identifier — persisted in user settings, used as the registry key. Don't rename it once published.
nameDisplay name shown in pickers.
versionSemver (e.g. 1.0.0, 2.1.3-beta.1).
descriptionOne-line pitch.
kindOne of study, translate, vocab-import. Picks which built-in registry the addon plugs into.
entryRelative path to the JS entry point inside the folder (e.g. index.js, dist/main.js). Must stay inside the addon folder — no .., no absolute paths.

Optional fields

FieldRule
authorFree-form.
homepageURL — surfaced as a "More info" link.
licenseSPDX-style string (MIT, Apache-2.0, …).
minAppVersionTokori semver this addon was tested against. Mismatches show a warning but don't block load.

Authoring an addon

The contract you implement depends on kind:

  • kind: "study" → default-export a StudyPlugin from your entry point. See Plugin SDK for the full shape and a worked example.
  • kind: "translate" → default-export a TranslateEngine (src/lib/translate/api.ts).
  • kind: "vocab-import" → default-export a VocabImportPlugin (src/lib/vocab-import/api.ts).

In other words, an addon is exactly the same code as an in-tree plugin — there's no separate addon API to learn. The only differences are (a) where the file lives and (b) the manifest.

Minimal study addon, expanded:

text
addons/
└── hsk-cloze-quiz/
    ├── manifest.json
    └── index.js          ← bundled, ESM, default-exports a StudyPlugin

index.js should be pre-bundled — Tokori doesn't run a TypeScript / JSX compiler on addon load. Use the same toolchain you'd use for an npm package: tsc or esbuild, target ES2022, output ESM, externalise react / @tokori/*.

What addons can and can't do

Stage-1 contract (what we'll commit to forwards):

  • Can: register a study mode, translate engine, or vocab importer; persist per-addon settings via the standard usePluginSetting helper; read the user's vocab snapshot through the ctx capability object.
  • Can't: make arbitrary network calls (use the provided callAi shim instead); read other addons' state; reach into db.ts directly; install Rust commands; modify other plugins' behaviour.

The capability surface is the same ctx that in-tree plugins receive — see Plugin SDK. That's the only allowed coupling to the host app, and the only API stability promise we make for addon authors.

Removing an addon

Delete the folder. Tokori re-scans on next launch or when you press Rescan in Settings → Addons. The per-addon enabled flag is left behind in user settings (cheap) so reinstalling the same id restores your prior toggle.

Security model

Addons are local-only — Tokori has no online catalogue, no auto-update, no script that pulls JS from a third party. You install an addon by deliberately copying a folder. That's intentional: the trust boundary is the user putting files on their own disk, the same boundary every desktop app uses.

When Stage 2 lands, the addon entry point will execute inside a sandbox (Web Worker + Blob URL, or an isolated iframe) so it can't reach the DOM, the filesystem, or the network outside of the capability surface in ctx. Today, with execution off, an addon's code never runs at all.

How discovery works internally

For contributors curious about the plumbing:

  • src-tauri/src/commands.rs::list_addons enumerates <app-data>/addons/*/manifest.json and ships each manifest.json text back to the frontend (no Rust-side validation — the rule lives in one place).
  • src/lib/addons/manifest.ts parses + validates each manifest.
  • src/lib/addons/registry.ts caches the result, exposes useAddons(), and persists enabled flags via db.getSetting/setSetting.
  • src/components/settings/addons-section.tsx is the UI.

Stage 2 will add a loader that pulls each enabled addon's entry JS through a sandbox and merges the exported plugin into the matching built-in registry (STUDY_PLUGINS, TRANSLATE_ENGINES, …). At that point the public surface — the manifest, the per-kind plugin contracts — won't change.

MIT licensed.