Skip to main content
Import everything from freestyle-voice.

Plugin

The object your factory returns. The default export is (options?: PluginOptions) => Plugin | Plugin[] | false | null | undefined. Return an array to ship a preset of plugins, or a falsy value to disable based on options.

Hooks

Each hook is optional. Mutating hooks receive three arguments: a read-only input, an output you change in place, and a HookApi (api) for cancellation/suppression control and — on server hooks — the host’s LLM. They chain across plugins in resolved order (enforce: "pre" → none → "post", then load order). Hook inputs:
  • beforeTranscribe{ providerId, modelId, audioDurationMs, appContext? }
  • afterTranscribe{ providerId, modelId, appContext? }
  • beforeCleanup{ text, appContext?, destination }
  • afterCleanup and beforeOutput{ appContext? }
beforeCleanup’s destination is a CleanupToneDestination"overall" | "personal" | "work" | "email" — the tone bucket the host inferred; set output.destination to override it.

Consuming an utterance

A plugin can handle an utterance entirely instead of dictating it — for example, a voice command that ran an action. Call api.control.consume() from a server hook (typically afterTranscribe): the host skips every remaining stage and delivers nothing to the focused app.
api.control.consume() replaces the old output.consumed flag. It is unambiguous (a genuinely empty transcript is different from a consumed one) and preserves the raw text for logging.

HookApi

The third argument to every mutating hook. Built once per dictation and threaded through every stage, so control and llm are consistent across the whole run.
  • stopPropagation() — stop running the remaining plugins for the current hook only; later hooks still run.
  • consume(reason?) — the utterance is handled (e.g. a voice command). Every remaining stage is skipped and no output is delivered.
  • abort(reason?) — unrecoverable failure: nothing is delivered and the host emits a pipelineError event. Aborting also fires control.signal, so pass api.signal to cancellable work (e.g. api.llm.generateText({ signal: api.signal })).
The host checks control.state between stages. llm is present only on server hooks (beforeTranscribe, afterTranscribe, beforeCleanup, afterCleanup) and only when an LLM is configured — never on beforeOutput. Always guard with if (api.llm).

PluginContext

Passed to setup.
The LLM capability moved off PluginContext. It is now on the per-hook HookApi as api.llm, so it can reflect the model configured for each dictation.
Settings are read-only. Pass runtime config through factory options and read it back with getOwn. For state your plugin needs to write, use storage.

PluginLlm

Access to the host’s configured language model, so server-side plugins can run their own LLM calls (classification, tool-calling agents, and so on) reusing the user’s configured cleanup model and stored keys — no separate provider or key configuration required. Reached through api.llm on any server hook.
api.llm is present only on server hooks and only when a model is configured, so always guard with if (api.llm). Read it fresh in each hook — don’t capture it in setup (it’s built per-dictation):
For a simple one-shot call you can skip the AI SDK entirely and use the wrapper:
getModel() is typed unknown in the SDK to avoid a hard dependency on the ai package — cast the result to LanguageModel (from ai) at the call site. Bundle ai in your plugin’s devDependencies; installed plugins don’t get a transitive npm install.
Freestyle Cloud. Signed-in Freestyle Cloud users get api.llm too — it routes to Freestyle Cloud’s managed LLM endpoint, so plugins work without the user configuring their own provider or key. Plugins never see credentials either way: the host resolves the provider, model, and key and hands back only the capability.

PluginStorage

Per-plugin persistent key-value storage, scoped by plugin name so plugins never collide. Values are JSON-serialized into the host database (and sync across machines when the database is synced). Think of it as localStorage for plugins.
Capture it in setup and use it from your hooks or middleware:

Events

PipelineStage is "capture" | "transcribe" | "cleanup" | "transform" | "output".

AppContext

Best-effort info about the focused app, on several hook inputs.

OutputMode

How a transcript is delivered. Set output.mode in beforeOutput.

transform

Wraps a plain text function into an afterCleanup hook.

The UI bridge

Freestyle injects window.freestyle into plugin pages as the one privileged surface: a helper to call the local server API and to trigger a small set of host actions. See a worked example in the first-plugin guide.
api(path, init?) resolves path against serverUrl and returns a native Response. Plugin UI is now served same-origin with the server, so this is a thin wrapper over fetch (no proxying, no manual token) — call res.json() / res.text() as usual.
Plugin pages may only reach their own /api/plugins/<slug>/… namespace plus /api/health — the host confines them by the request Referer. They cannot read settings, keys, or history.
invoke(channel, payload) asks the host to do something:
window.freestyle is only present inside a hosted plugin page, so guard for it (if (!window.freestyle) ...) before use.

Manifest

The freestyle field in package.json.
icon is a lucide name.