Plugin development guide
Eidos Lite plugins add document formatters, command-palette actions, and custom interfaces to a Space. Plugins run in a local, isolated sandbox without requiring network access or external CDNs.
Extension capabilities
Section titled “Extension capabilities”A plugin can combine any of these extension points in a single plugin.json manifest:
| Your feature | Extension point | User entry point |
|---|---|---|
| Format Markdown or text | Formatter provider | Format Document or Format Document With… |
| Count words or run a document operation | Action | Cmd+K, optionally a custom shortcut |
| Edit CSV, Markdown, or custom files | Document view | File context menu → Open with |
| Open an independent plugin page | Page view | Space plugin navigation entry |
| Show table records on a map or chart | Table view | Table view menu → New view |
Quickstart: Create and run your first plugin
Section titled “Quickstart: Create and run your first plugin”1. Install prerequisites
Section titled “1. Install prerequisites”Ensure you have Node.js 22.12 or newer and a package manager (npm, pnpm, or yarn) installed. Also install Eidos Lite for running and testing your plugins locally.
2. Scaffold a project
Section titled “2. Scaffold a project”Choose either method to create a new plugin project:
Method 1: Using npm / npx (Recommended, zero extra CLI setup)
npx @eidos.space/plugin-tools create my-plugincd my-pluginnpm install # or pnpm installMethod 2: Using the Eidos CLI
eidos plugin create my-plugincd my-pluginpnpm installThe generated project has the following layout:
my-plugin/├── plugin.json # Manifest declaring capabilities and entry points├── package.json # Dependencies and npm scripts├── tsconfig.json # Strict TypeScript configuration└── src/ ├── main.ts # View mount entry point ├── csv.ts # Business logic └── style.css # StylesThe scaffold pre-configures official dependencies and scripts in package.json:
@eidos.space/plugin-sdk: TypeScript types and interfaces for the Plugin 1.0 specification.@eidos.space/plugin-tools: CLI providingeidos-plugin check(validation) andeidos-plugin pack(self-contained bundle packaging).npm run check: Runseidos-plugin check .for static verification.npm run pack:plugin: Runseidos-plugin pack .to produce an offline.eidos-pluginbundle.
3. Load and test in Eidos Lite
Section titled “3. Load and test in Eidos Lite”You don’t need a manual build step to test your plugin. Eidos Lite compiles source files on the fly:
- Launch Eidos Lite and open any Space.
- In the top toolbar, click the Plugins icon (between Search and the Space menu).
- Click Load development source… and select your project’s
plugin.json(or a standalone.tsfile). - Review the declared capabilities and click Enable for this Space.
- Create or select a
.csvfile in your Space, right-click it, and choose Open with → CSV Table.
When you edit code in src/, Eidos Lite automatically recompiles and hot-reloads the view!
Core concepts: Manifest and lifecycle
Section titled “Core concepts: Manifest and lifecycle”The manifest (plugin.json)
Section titled “The manifest (plugin.json)”The manifest declares your plugin’s identity, permissions, and extension points:
{ "apiVersion": 1, "id": "example.my-plugin", "name": "My Plugin", "version": "0.1.0", "extension": "./src/extension.ts", "views": [], "actions": [], "formatters": [], "placements": []}apiVersion: Currently1, specifying the Plugin 1.0 host contract.id: Globally unique identifier (e.g.vendor.feature).extension: Points to background controller code that runs on activation.views: List of custom UI views.placements: Tells the host where to display views and actions in the UI.
Type-only SDK
Section titled “Type-only SDK”The core SDK provides TypeScript types only:
import type { ExtensionContext, ViewContext } from "@eidos.space/plugin-sdk"All runtime APIs are injected by the host into the ctx context parameter. There is no SDK bundle overhead and no connect() call.
- Background lifecycle: For actions and formatters,
extensionexports a defaultactivate(ctx: ExtensionContext)function. - View lifecycle: Each view’s entry exports a default
mount(ctx: ViewContext, root: HTMLElement)function. - Disposal: Clean up event listeners and timers by returning
{ dispose() { ... } }or callingctx.subscriptions.add(...).
Where installed plugins live
Section titled “Where installed plugins live”Plugins install once per device and are enabled per Space:
- Eidos Lite and
eidos servestore plugins in~/.eidos/plugins(%USERPROFILE%\\.eidos\\pluginson Windows). - Set
EIDOS_HOMEto relocate the store directory. - The store contains immutable package archives (
packages/<sha256>.eidos-plugin), device configuration (config.json), and per-plugin offline data underdata/<plugin-id>/.
Branding and functional icons (Icons & Branding)
Section titled “Branding and functional icons (Icons & Branding)”The plugin architecture cleanly separates plugin brand logos from functional view/action icons:
- Plugin Logo (
manifest.icon): Represents the overall plugin brand. Displayed in the Marketplace listing, plugin details pane, and Space plugin settings. - View/Action Icons (
view.icon/action.icon): Represents a specific capability or view (e.g. mind map, grid, formatter). Displayed in the file context menu (Open with), editor view tabs, and Command Palette. Ifview.iconis omitted, the host automatically falls back tomanifest.icon; if neither is provided, an elegant theme-aware initial badge is generated.
Icons support three declaration formats:
{ "icon": { "paths": ["M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"] }}- Inline monochrome SVG paths (recommended):
{ "paths": ["..."] }, rendered using the host’s design system text color (currentColor) with pixel-perfect alignment and zero overhead. - Local relative file paths: e.g.
"./assets/logo.svg"or"./assets/view.png". Duringeidos-plugin pack, the bundler automatically reads and inlines the asset as a Base64 Data URL to preserve offline self-containment. - Data URL string: e.g.
"data:image/svg+xml;base64,...".
File associations and global default editors
Section titled “File associations and global default editors”By declaring location: "file/open" in placements, a view registers as an editor for specific file extensions:
"placements": [ { "location": "file/open", "view": "table", "extensions": [".csv"] }]- Open with menu: Right-click a
.csvfile in the Space file tree and hover over Open with to see the plugin editor with its dedicated functional icon. - Global default editor settings: In Eidos Lite preferences (Settings → Files), all installed plugins and their associated file extensions are listed alongside Markdown and HTML. Users can choose their preferred default editor for
.csvor custom file types globally, opening files directly on double-click.
Extension points by example
Section titled “Extension points by example”1. Document formatters
Section titled “1. Document formatters”A formatter processes document text and returns updated text. The host manages undo/redo, selections, and saving.
In plugin.json:
{ "apiVersion": 1, "id": "example.typography", "name": "Typography", "version": "0.1.0", "extension": "./src/extension.ts", "formatters": [ { "id": "trim", "title": "Trim trailing whitespace", "extensions": [".md", ".txt"] } ]}In src/extension.ts:
import type { ExtensionContext } from "@eidos.space/plugin-sdk"
export default function activate(ctx: ExtensionContext) { ctx.formatters.register("trim", { format({ text, signal }) { signal.throwIfAborted() return { text: text.replace(/[ \t]+(?=\r\n|\r|\n|$)/g, "") } }, })}Users run formatting via:
- Cmd+K → Format Document
- Default shortcut: Shift+Option+F (macOS) / Shift+Alt+F (Windows)
2. Commands and shortcuts (Actions)
Section titled “2. Commands and shortcuts (Actions)”Actions execute custom logic from the Command Palette (Cmd+K) or keyboard shortcuts.
In plugin.json:
{ "apiVersion": 1, "id": "example.text-tools", "name": "Text Tools", "version": "0.1.0", "extension": "./src/extension.ts", "actions": [ { "id": "count", "title": "Count characters", "context": "document", "access": "read", "extensions": [".md", ".txt"] } ], "placements": [ { "location": "command-palette", "action": "count" }, { "location": "keybinding", "action": "count", "key": "Mod+Alt+C" } ]}In src/extension.ts:
import type { ExtensionContext } from "@eidos.space/plugin-sdk"
export default function activate(ctx: ExtensionContext) { ctx.actions.register("count", async ({ binding, ui }) => { if (binding.kind !== "document") return const snapshot = await binding.document.read() await ui.notify(`Characters: ${Array.from(snapshot.text).length}`) })}3. Custom document views
Section titled “3. Custom document views”Document views replace the default editor for specific file extensions when selected in Open with.
In plugin.json:
{ "apiVersion": 1, "id": "example.markdown-viewer", "name": "Markdown Preview", "version": "0.1.0", "views": [ { "id": "preview", "title": "Rich Preview", "entry": "./src/preview.ts", "context": "document", "access": "write" } ], "placements": [ { "location": "file/open", "view": "preview", "extensions": [".md"] } ]}In src/preview.ts:
import type { ViewContext } from "@eidos.space/plugin-sdk"
export default async function mount(ctx: ViewContext, root: HTMLElement) { if (ctx.binding.kind !== "document") return const file = ctx.binding.document
// Subscribe to changes from the host or other editors const observation = await file.observe((snapshot) => { root.textContent = snapshot.text }) ctx.subscriptions.add(observation.subscription)
// Initial render root.textContent = observation.snapshot.text
return { dispose() { root.replaceChildren() }, }}To edit the document:
const before = await file.read()const result = await file.edit({ text: nextText, expectedVersion: before.version, label: "Edit cell",})if (result.status === "stale") { // Conflict reconciliation}4. Independent page views
Section titled “4. Independent page views”Independent pages add navigation entries to the Space sidebar:
In plugin.json:
{ "views": [ { "id": "home", "title": "Dashboard", "context": "page", "entry": "./src/page.ts" } ], "placements": [{ "location": "navigation", "view": "home" }]}In src/page.ts:
import type { ViewContext } from "@eidos.space/plugin-sdk"
export default function mount(ctx: ViewContext, root: HTMLElement) { if (ctx.binding.kind !== "page") return
root.innerHTML = ` <div style="padding: 24px;"> <h1>Plugin Dashboard</h1> <p>Current route: ${ctx.binding.route || "/"}</p> </div> `
return { dispose() { root.replaceChildren() }, }}5. Table views
Section titled “5. Table views”Table views render .eidos table records in novel layouts like charts, maps, or galleries.
In plugin.json:
{ "views": [ { "id": "map", "title": "Map View", "entry": "./src/map.ts", "context": "table", "access": "read" } ], "placements": [{ "location": "table/view", "view": "map" }]}In src/map.ts:
import type { ViewContext } from "@eidos.space/plugin-sdk"
export default async function mount(ctx: ViewContext, root: HTMLElement) { if (ctx.binding.kind !== "table") return const table = ctx.binding.table
// Read view metadata and column definitions const meta = await table.read()
// Fetch paginated rows with the host's current filters and sorting const page = await table.getPage({ offset: 0, limit: 100 })
// Accelerated SQLite server-side aggregation const aggregates = await table.aggregate({ metric: { op: "count" }, groupBy: { fieldId: "category" }, })
// Listen for table mutations const unsubscribe = await table.observe(() => { // Re-render when records change }) ctx.subscriptions.add(unsubscribe)
return { dispose() { root.replaceChildren() }, }}Using React and UI components
Section titled “Using React and UI components”You can use React, Vue, Svelte, or vanilla DOM. To use React:
pnpm add react react-dompnpm add -D @types/react @types/react-domIn src/main.tsx:
import { createRoot } from "react-dom/client"import type { ViewContext } from "@eidos.space/plugin-sdk"import { App } from "./app"import "./style.css"
export default function mount(ctx: ViewContext, element: HTMLElement) { const root = createRoot(element) root.render(<App context={ctx} />) return { dispose() { root.unmount() }, }}Theme adaptation
Section titled “Theme adaptation”Use the host’s semantic CSS variables to automatically match light and dark modes:
.card { background-color: var(--e-color-bg-subtle); color: var(--e-color-text-primary); border: 1px solid var(--e-color-border-subtle); border-radius: var(--e-radius-md);}Offline sandbox & third-party library best practices
Section titled “Offline sandbox & third-party library best practices”Eidos plugins execute inside an isolated, network-gapped iframe sandbox. When integrating third-party UI libraries (such as @glideapps/glide-data-grid, chart renderers, or rich text engines), keep these practices in mind:
- Always explicitly import library CSS:
Many modern libraries (using Linaria, Vanilla Extract, or standard CSS) store critical structural layout and positioning rules in separate
.cssfiles.- Always
@importthe library’s CSS at the top ofmain.tsxorstyle.css. - Missing core CSS can cause the host container to compute an effective height of
0px. Internal resize observers (e.g.,ResizeObserver/useResizeDetector) will measure zero dimensions and skip mounting<canvas>or DOM trees entirely, resulting in “toolbar visible but content completely blank”.
- Always
- Clean ESM dependencies and no dynamic require:
- The host bundler compiles code and dependencies into self-contained browser ESM modules.
- Avoid CommonJS runtime
require()calls in client code; use standard staticimportsyntax.
- Handle document concurrency and stale edit conflicts:
- Read the current snapshot and its version:
const snapshot = await ctx.binding.document.read(). - Apply edits with the expected version:
await ctx.binding.document.edit({ text, expectedVersion: snapshot.version, label: "Edit cell" }). - If
{ status: "stale" }is returned, the document was modified by another editor or external process. Re-read the latest snapshot to reconcile or prompt the user, avoiding accidental overwrites.
- Read the current snapshot and its version:
Storage and network permissions
Section titled “Storage and network permissions”Offline storage
Section titled “Offline storage”Plugins have access to isolated, persistent key-value binary storage that survives restarts:
In plugin.json:
{ "storage": { "maxBytes": 536870912 }}In your code:
// Write dataawait ctx.storage.write( "config", new TextEncoder().encode(JSON.stringify({ dark: true })))
// Read dataconst bytes = await ctx.storage.read("config")Network requests
Section titled “Network requests”All external network requests must be explicitly declared in browser.networkOrigins. Direct eval, Node.js APIs, and CDN scripts are blocked by the sandbox CSP.
{ "browser": { "networkOrigins": ["https://api.example.com"] }}Packaging and marketplace publishing
Section titled “Packaging and marketplace publishing”1. Validate and package
Section titled “1. Validate and package”You can run scripts directly from your plugin directory or use the CLI:
# Validate manifest metadata, TypeScript types, and dependenciesnpm run check# Or run directly:# npx eidos-plugin check .# eidos plugin check .
# Bundle into an offline, self-contained .eidos-plugin archivenpm run pack:plugin# Or specify a custom output path:# npx eidos-plugin pack . --out ./dist/my-plugin-0.1.0.eidos-pluginPackaging creates a <id>-<version>.eidos-plugin archive. This is a standardized gzip-compressed JSON envelope containing format: 1, full manifest metadata, and bundled module sources. Users can install it directly in Eidos Lite (Plugins → Install plugin…) without Node.js or an internet connection.
2. Publish to the Marketplace
Section titled “2. Publish to the Marketplace”- Publish
<id>-<version>.eidos-pluginas a GitHub Release asset in your repository. - Submit a PR to the official registry updating
plugins.registry.jsonwith your plugin ID, version, release download URL, and SHA-256 checksum. - Once merged, your plugin appears in the Eidos Lite Marketplace for all users.
Next steps
Section titled “Next steps”- Explore the complete Plugin API reference for all context methods, interfaces, and options.
- Read the normative Plugin 1.0 specifications for underlying host contracts.