API Reference
The Omniscio Plugin SDK exposes 24 APIs to plugins. Some are available on both the frontend (via the AgentMC bridge) and the backend (via the PluginContext); others are backend-only or frontend-only.
API Summary
| API | Description | Availability | Permission |
|---|---|---|---|
| Storage | Key-value storage | Both | storage |
| Secrets | Keychain-encrypted credentials | Both | secrets |
| Database | Collection-based CRUD | Both | storage |
| Sessions | Spawn and manage Claude sessions | Both | sessions |
| AI | Generate text with AI models | Both | ai |
| Filesystem | Read and write files | Backend only | storage |
| HTTP | Fetch external URLs | Backend only | network |
| Cron | Schedule recurring tasks | Backend only | cron |
| CLI Endpoints | Register REST endpoints | Backend only | cli |
| Sidebar | Badge count and item list | Both | None |
| Toast | In-app and OS notifications | Both | notifications (for notify) |
| Settings | Read plugin settings | Both | None |
| Events | Messaging between your UI and backend | Both | None |
| Logging | Structured log output | Backend only | None |
| Inbox | Surface items in Omniscio's inbox | Both | inbox |
| Auth | Signed-in identity and scoped account tokens | Both | auth (auth.session for tokens) |
| Recording | Screen recording, host-mediated | Backend only | recording |
| Text to Speech | Speak text with the user's configured voice | Webview only | tts |
| Session History | Read past sessions the user has granted | Webview only | sessions.readHistory |
| Firebase | The user's Firebase accounts and projects | Webview only | firebase |
| Spend | Read-only AI cost and usage totals | Backend only | spend |
| Workspace | The user's real project checkouts and worktrees | Backend only | workspace.read / .write / .exec |
| Backend Calls | Call your backend's handlers from your panel | Both | None |
| Host Capabilities | Feature-detect what this Omniscio supports | Both | None |
Access Patterns
Backend -- the PluginContext object is passed to your plugin's activate() function:
// src/backend/index.ts
import type { PluginContext } from '@agent-mc/plugin-sdk'
export function activate(ctx: PluginContext) {
ctx.storage.set('initialized', true)
ctx.log.info('Plugin activated')
}Frontend -- the global AgentMC bridge is available in your UI code:
// src/ui/plugin.ts
const value = await AgentMC.storage.get('initialized')
AgentMC.toast.show({ type: 'info', message: 'Hello from the UI' })Backend Calls
Your backend registers a handler with ctx.rpc, and your panel calls it with AgentMC.backend.invoke. The call resolves to whatever the handler returns.
// backend
ctx.rpc?.handle('notes:count', async () => (await ctx.db.query('notes')).length)
// panel
const count = await AgentMC.backend?.invoke('notes:count')| Member | Description |
|---|---|
ctx.rpc.handle(method, handler) | Register (or replace) the handler for method |
ctx.rpc.removeHandler(method) | Remove it |
AgentMC.backend.invoke(method, ...args) | Call it from your panel |
- Needs Omniscio v0.1.92. Both members are
undefinedon older hosts, so check before you call. - Arguments and the result travel as JSON, and a thrown error reaches the panel as its message only.
- The host gives up on a call after 240 seconds, and refuses more than 100 calls in flight per plugin.
- For anything that changes data, use the command channel instead: it checks input, runs changes one at a time and reports honest outcomes on top of these calls.
Host Capabilities
AgentMC.host.getCapabilities() resolves to the capability tokens this Omniscio supports, such as 'ai-engine-choice'. A missing token means no. It needs no permission and exists from Omniscio v0.1.89.
const tokens = (await AgentMC.host?.getCapabilities()) ?? []
const canPickEngine = tokens.includes('ai-engine-choice')The backend twin, ctx.host.getCapabilities(), is typed ahead of the Omniscio release that adds it, and is undefined until then.
Bridge-Only APIs
The following APIs are available only on the frontend through the AgentMC bridge. They do not have backend equivalents and do not require permissions.
Theme
Read and react to Omniscio's current theme.
// Get the current theme — this is a PROMISE
const theme = await AgentMC.theme.get()
// theme: { mode: string; accent: string; surfaces: Record<string, unknown> }
// NOTE: static placeholder host-side — mode is always 'dark' today.
// Listen for theme changes
const unsubscribe = AgentMC.theme.onChange((theme) => {
document.body.className = theme.mode
})| Method | Returns | Description |
|---|---|---|
get() | Promise<{ mode: string; accent: string; surfaces: Record<string, unknown> }> | Current theme. visualTheme never existed; get() is async |
onChange(callback) | () => void | Subscribe to theme changes. Returns an unsubscribe function |
Export
Save files, generate PDFs, and interact with the user's filesystem through system dialogs.
// Save a text file (shows Save As dialog)
await AgentMC.export.saveFile({
filename: 'report.md',
content: markdownContent,
type: 'text/markdown',
})
// Generate and save a PDF — note the casing, and it takes HTML
const saved = await AgentMC.export.savePdf({
filename: 'report.pdf',
html: renderedHtml,
})
if (!saved.saved) console.log('user cancelled')
// Let the user pick a folder
const folder = await AgentMC.export.pickFolder({
title: 'Choose output directory',
})
// Write multiple files to a directory
await AgentMC.export.writeFiles({
directory: folder,
files: [
{ name: 'index.ts', content: tsCode },
{ name: 'styles.css', content: cssCode },
],
})
// Verify files exist
await AgentMC.export.verifyFiles({
directory: folder,
files: ['index.ts', 'styles.css'],
})
// Open a folder in the system file manager
await AgentMC.export.openFolder({ path: folder })| Method | Description |
|---|---|
saveFile(opts) | Show a Save As dialog and write a file |
savePdf(opts) | Render HTML to PDF and save. (savePDF with that casing does not exist) |
pickFolder(opts?) | Show a folder picker dialog, returns the selected path |
writeFiles(opts) | Write multiple files to a directory |
verifyFiles(opts) | Check that files exist in a directory |
openFolder(opts) | Open a folder in the OS file manager |
Project
Query and create Omniscio projects.
// List all projects
const projects = await AgentMC.project.listAll()
// Find a project by folder path
const project = await AgentMC.project.findByFolder('/path/to/repo')
// Create a new project
const newProject = await AgentMC.project.create({
name: 'My App',
folderPath: '/path/to/my-app',
})
// Open Omniscio's Add Project dialog with a folder pre-selected
await AgentMC.project.openAddDialog({
preselectedFolder: '/path/to/repo',
})| Method | Description |
|---|---|
listAll() | List all projects in Omniscio |
findByFolder(folderPath) | Find a project by its folder path |
create(opts) | Create a new project |
openAddDialog(opts) | Open the native Add Project dialog |
Assets
Read files bundled with your plugin (from the assets/ directory in your plugin package).
// Read a bundled file
const template = await AgentMC.assets.readFile('templates/default.md')
// List files in a directory
const files = await AgentMC.assets.listFiles('templates')| Method | Returns | Description |
|---|---|---|
readFile(path) | Promise<string> | Read a bundled asset file as text |
listFiles(path) | Promise<string[]> | List files in a bundled asset directory |
Documents
Read and append to files the user picked from the OS file picker. Webview-only — there is no ctx.documents — and it needs no permission, because the picker itself is the consent.
// Ask for a file. Resolves [] if the user cancels.
const [doc] = await AgentMC.documents.open({
mode: 'readwrite',
title: 'Choose a PDF',
filters: [{ name: 'PDF', extensions: ['pdf'] }]
})
if (!doc) return
// Read the bytes via fetch. `url` is opaque — never parse it.
const bytes = await (await fetch(doc.url)).arrayBuffer()
// Append. Always stat() first — expectedLength is only loosely checked.
const base64Delta = btoa('appended text')
const { length } = await AgentMC.documents.stat(doc.id)
await AgentMC.documents.append(doc.id, base64Delta, { expectedLength: length })
// Recover Handles after a webview reload, and release when done.
console.log(`${(await AgentMC.documents.list()).length} still open`)
await AgentMC.documents.close(doc.id)| Method | Returns | Description |
|---|---|---|
open(options) | Promise<DocumentHandle[]> | Show the file picker and mint a Handle per chosen file; [] if cancelled |
list() | Promise<DocumentHandle[]> | The Handles this plugin still holds, with freshly re-stat'd sizes |
stat(handleId) | Promise<{ length: number }> | The Document's current length — the value to pass as expectedLength |
append(handleId, base64, options) | Promise<{ length: number }> | The namespace's only write; resolves the new length |
close(handleId) | Promise<void> | Drop the Handle; the file itself is untouched |
A DocumentHandle is { id, name, size, mode, url } and carries no path. Two of those fields mislead if you skim them:
sizeis live when the Handle is serialized, not when the file was picked — but it freezes the moment the call resolves. Never pass asizeyou are holding asexpectedLength; callstat()each time. A stale length is not reliably an error: the host refuses only when the region you would discard is larger than the payload you write, so a slightly stale one silently overwrites your own recent bytes.urlis opaque, and only works throughfetch. Never parse, log, or persist it. Documents are served asapplication/octet-streamwithnosniffandContent-Disposition: attachment, so it is not usable as an<img src>or<iframe src>. Its path shape is not a contract, and it is on its way to carrying a capability token that grants read access to the file.
append takes its arguments as (handleId, base64, options). Both leading arguments are strings, so transposing them still typechecks — the host rejects the id at runtime instead.
A read Handle refuses append; the mode is fixed when the Handle is minted. And if the file is replaced on disk — which an ordinary Save in Preview or Word does — append refuses not-the-picked-document and that Handle is finished: call open() again for a fresh gesture.
No offline mock yet
The SDK ships no mock for documents. Both its mocks — createTestContext() and the dev shell's context — implement PluginContext, the backend type, and documents is deliberately absent from it, so neither can host this namespace without typing a capability a plugin backend cannot call. Faking it would repeat the ctx.events failure, where an in-memory fake kept plugin tests green for months while the production path was dead in both directions. Stub it yourself until a webview mock surface exists.
Gating is in flux
documents is live and ungated on Omniscio master, but an in-flight host change puts it behind an in-development plugin-documents-io flag that defaults off. On a build with that flag, every call rejects with This capability is not available. — a passing typecheck here is not evidence the host will answer.