Your plugin's activate function receives one argument: a PluginAPI object. This page documents every method on it. The full TypeScript types live in the @projelli/plugin-api npm package, so your editor will show signatures, parameter names, and JSDoc as you type.
Methods that touch host data return Promises (the call round-trips across postMessage). Methods that just register UI return void.
readonly string. Your plugin's id, copied from manifest.json. Useful for namespacing log output and storage keys.
readonly string. Your plugin's version, copied from manifest.json.
Register a command handler. The id should be namespaced to your plugin (e.g. my-plugin.do-thing). The handler can be sync or async; whatever it returns is the resolved value of commands.invoke.
api.commands.register('my-plugin.hello', () => {
api.notify.info('Hello!');
return 'done';
});
Invoke any registered command (yours or another plugin's, if you know its id). Returns whatever the handler returned.
const result = await api.commands.invoke('my-plugin.hello');
Add a button to the editor toolbar. Spec fields: id, icon (a Lucide icon name), tooltip, command (the command id to invoke on click).
api.toolbar.addButton({
id: 'my-button',
icon: 'sparkles',
tooltip: 'Run my plugin',
command: 'my-plugin.hello',
});
Icon names: anything in Lucide's icon set. Use lowercase, hyphenated names (chevron-down, not ChevronDown).
Remove a button you previously added. Does nothing if the id isn't found.
Add (or replace) a panel in the right sidebar. Spec fields: id, title, html (inline HTML rendered inside a sandboxed iframe). Calling addPanel with an existing id replaces the panel's content.
api.sidebar.addPanel({
id: 'my-panel',
title: 'My Panel',
html: '<div style="padding: 16px;">Hello sidebar</div>',
});
The iframe is sandboxed: scripts inside the panel cannot reach the worker or the main thread. To make a panel interactive, use a toolbar button or a registered command (the user clicks, the worker reacts, the panel re-renders). v2.1 may add panel-to-worker messaging.
Remove a panel. Does nothing if the id isn't found.
Get the current selection in the active editor. Returns null if there's no active editor or no selection.
const sel = await api.editor.getSelection();
if (sel) console.log(sel.text, sel.range);
The returned object has text: string and range: { startLine, startColumn, endLine, endColumn }. Lines and columns are 1-based.
Get the full text of the active document. Throws if there's no active editor.
const content = await api.editor.getContent();
Replace the current selection with new text. If there's no selection, behaves like insertAtCursor.
await api.editor.replaceSelection('new text');
Insert text at the cursor position.
List files and folders at the given path (defaults to workspace root). Returns an array of FileNode objects.
const files = await api.workspace.listFiles('notes/');
Read a file's text content. Path is relative to the workspace root.
const text = await api.workspace.readFile('notes/today.md');
Write a file. Creates parent folders as needed. Overwrites any existing file at that path.
await api.workspace.writeFile('notes/draft.md', '# Draft\n');
Send a prompt to the user's configured AI provider. Returns the model's text response.
Options: prompt: string (required), system?: string (optional system prompt).
const reply = await api.ai.invoke({
system: 'You are a precise translator.',
prompt: 'Translate "hello" into French.',
});
The plugin never sees the user's API key. Calls are billed against the user's provider account.
Per-plugin key/value storage. Keys are scoped to your plugin id; you can't see other plugins' data.
Read a stored value. Returns undefined if not set.
Write a value. Must be JSON-serializable.
await api.storage.set('lastRun', Date.now());
const last = await api.storage.get('lastRun');
Delete a stored value.
Make an HTTP request from the worker. Accepts the same options as the standard fetch (method, headers, body). The body is always materialized; no streaming in v2.0.
const res = await api.network.fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ q: 'hello' }),
});
if (res.ok) console.log(res.body);
Response shape: { status, statusText, headers, body, bodyEncoding, url, ok }. bodyEncoding is 'utf-8' for text responses or 'base64' for binary.
Register a settings page in the Plugins panel. Spec: id, title, schema (a record of field id to field spec). Each field has type ('string' | 'number' | 'boolean' | 'select'), default, label, and choices (required when type is 'select').
api.settings.addPage({
id: 'my-plugin-settings',
title: 'My Plugin',
schema: {
targetLanguage: {
type: 'select',
default: 'Spanish',
label: 'Target language',
choices: ['Spanish', 'French', 'German'],
},
},
});
Read a setting value. Falls back to the schema default if the user hasn't changed it.
Write a setting value. Useful for plugins that update their own settings programmatically.
Show a toast notification.
Blue toast.
Yellow toast.
Red toast.
api.notify.info('Done!');
api.notify.warn('Selection is empty.');
api.notify.error('Network request failed.');
Your src/index.ts must default-export an object that matches the PluginModule interface:
export default {
async activate(api: PluginAPI) {
// setup
},
async deactivate() {
// optional cleanup, called before disable / uninstall / update
},
};
activate runs once per worker lifetime. deactivate is optional. The bundle must be an ES module (the scaffolder configures this for you).