name: sero-plugin
description: |
Create Sero plugins — self-contained packages with a Pi extension (agent
tools/commands), an optional React UI loaded via Module Federation, and an
optional background runtime. Use when the user asks to create a Sero
plugin/app/extension, add a sidebar panel, dashboard widget, or
agent-integrated feature, or convert a Pi extension into a Sero plugin.
Trigger on phrases like "create a plugin", "new Sero app", "add a tool
with UI", "build an extension", "convert this Pi extension", or any
request that touches plugins/sero-*-plugin/.
Building Sero Plugins
A Sero plugin can expose up to three coordinated surfaces, all backed by a single JSON state file:
- Pi extension (
extension/) — standard Pi. Pi-CLI-safe, no Sero imports. Registers tools and commands. - Web UI (
ui/) — optional React UI loaded via Module Federation. Sero-only. - Background runtime (
runtime/) — optional. Sero-only. Long-lived workspace orchestration.
state.json (source of truth)
┌──────────────┼──────────────┐
Pi extension Web UI Background runtime
(Pi-CLI safe) (Sero only) (Sero only)
Sero auto-discovers plugins/sero-*-plugin/ directories with a sero.app
manifest in package.json. No manual registry edits.
When to read reference files
| Reference | When to read |
|---|---|
example/sero-notes-plugin/ |
Start here. Canonical, minimal plugin that exercises every surface (extension + CLI bridge, background runtime, UI hooks, static + dynamic widgets, MF config). Copy and rename it as your starting point. See example/README.md for the file map. |
references/templates.md |
Creating any file (package.json, extension, runtime, UI, Vite, styles). Also contains the Quick do/don't guide and mini examples. |
references/api-and-widgets.md |
Using app-runtime hooks, background runtimes, dashboard widgets, manifest fields |
references/conversion-guide.md |
Converting an existing Pi extension into a plugin |
Core rules
UI → plugin behavior
If the UI must trigger plugin-owned logic (sign in, refresh, mutate state, call an API), register a normal plugin tool and invoke it from React:
const { run } = useAppTools();
await run('myapp_auth', { action: 'login' });
Never add a plugin-specific host bridge (window.sero.myPlugin.*, custom
preload/IPC channels). Declare the host seams you actually use in
sero.plugin.requiredHostCapabilities:
| Capability | Declare when |
|---|---|
appAgent.invokeTool |
UI uses useAppTools() or window.sero.appAgent.invokeTool(...) |
tool.cli |
Plugin uses bridgeTools, custom cli metadata, or cli.overrideBuiltin |
appRuntime.background |
Plugin declares sero.app.runtime |
Tool bridging (AD-020)
App tools are bridged into the single sero-cli tool — always use pi.registerTool().
- Plugins bridge all tools to
sero <tool> ...by default sero.plugin.bridgeTools: omit/true= bridge all,false= none,string[]= selected tool names- Custom CLI help / summary / raw-args parsing goes on the tool's
clifield cli.overrideBuiltin: trueonly when intentionally replacing a builtin command- Never register app tools as
customToolsincreateAgentSession() - Bridged tools run against the current session's extension — don't capture registration-scoped
pistate in tool logic - If a tool exposes CLI subcommands, keep them self-explanatory and consistent with any structured tool actions; support aliases when needed and return explicit errors for unknown subcommands rather than silently falling back to another action
Prompt templates, skills, and slash-command naming
- Plugin-owned prompt templates and skills are manifest-driven. Declaring
prompts/orskills/folders is not enough by itself. - Add prompt templates to
pi.promptsinpackage.json(for example"./prompts/myapp.md"). - Add plugin skills to
pi.skillsinpackage.json(for example"./skills"). - If a bridged tool should also have a same-name slash shortcut (for example
/mcpfor the bridgedmcptool), prefer a prompt template declared inpi.prompts. - Keep
pi.registerCommand(...)names distinct from bridged tool names unless you intentionally want to shadow/replace that CLI entry point. - After adding or changing
pi.prompts/pi.skills, reload resources (/reload) or restart the session before testing the slash menu.
Background runtime
Use runtime/ only for long-lived workspace-scoped behavior: startup
recovery/reconcile, watchers beyond simple UI sync, subagent orchestration,
worktree/PR/cleanup flows, managed dev-server coordination.
Do not use it for simple CRUD, one-shot UI actions, or Pi-CLI-safe logic.
Boundaries:
extension/stays Pi-saferuntime/is Sero-only; type against@sero-ai/common, never desktop internals- Plugin-specific orchestration stays in the plugin, not the host
Host ownership
- Plugin-specific business logic lives in the plugin, not desktop preload/IPC
- External plugins consume
packages/*via published package names — do not import../../packages/*source paths - Keep plugin-specific types in
shared/; only promote neutral cross-plugin contracts to@sero-ai/common - Never copy desktop host aliases (
@electron,@plugins,@packages,@/) into plugin TS config
State
- Never use
localStorage— all UI state throughuseAppState(file-backed) - Keep state JSON-serialisable (no Date/Map/Set/functions)
- Always use atomic writes (temp file →
fs.rename) - Resolve global paths from
SERO_HOME/PI_CODING_AGENT_DIR; fall back to~/.pionly when env vars are unset (Pi CLI mode)
Step-by-step workflow
1. Plan
Decide up front:
- Plugin name (e.g.
myapp) — directory, app ID, state path - Display name, Lucide icon, scope (
workspacedefault, orglobal) - State shape shared across surfaces
- Tools to register
- Runtime? Only for long-lived orchestration
- Dev port — unique 5174+; check existing:
grep -r '"devPort"' plugins/sero-*-plugin/package.json
2. Create the directory
plugins/sero-<name>-plugin/
├── package.json
├── vite.config.ts # UI only, at package root
├── prompts/ # optional prompt templates (declare in pi.prompts)
├── skills/ # optional plugin skills (declare in pi.skills)
├── shared/types.ts
├── extension/
│ ├── index.ts
│ └── tsconfig.json
├── runtime/ # optional
│ ├── index.ts
│ └── tsconfig.json
└── ui/ # optional
├── <Name>App.tsx
├── styles.css
├── tsconfig.json
├── vite-env.d.ts
└── index.html
Extension-only plugins omit vite.config.ts, ui/, and the
sero.app.ui/component/devPort fields.
3. Write package.json
Triple-duty: Pi manifest + Sero app manifest + Sero plugin manifest.
See references/templates.md for the full template.
Critical:
"keywords": ["pi-package"]- Pi SDK packages in
peerDependencies @sero-ai/app-runtimeand@sero-ai/uiindevDependenciesstateFileis required even for global apps (Pi-CLI fallback)ui,component,devPortonly when the plugin ships a UIruntimeonly when the plugin ships a runtime; addruntime/tsconfig.jsonto thetypecheckscript- Declare prompt templates in
pi.promptsand plugin skills inpi.skills; folders alone are not auto-loaded - If you need a same-name slash shortcut for a bridged tool, use a prompt template in
pi.promptsrather thanpi.registerCommandwith the same name - Declare
requiredHostCapabilitiesonly for seams you use - For extension-only plugins, drop the Vite
dev/buildscripts - For built-in plugins with non-trivial extension deps, set
sero.plugin.bundleExtensions: true; list native, large, or runtime-loaded extension packages insero.plugin.extensionExternals
4. Define shared/types.ts
Single source of truth imported by extension, UI, and runtime.
- JSON-serialisable only
- Export a
DEFAULT_STATEconstant - Auto-incrementing IDs for lists
5. Mount the plugin into your workspace (dev only)
Expose the plugin source inside Sero as a multi-root workspace:
sero workspace mount-plugin /absolute/path/to/plugins/sero-<name>-plugin
6. Build the Pi extension
extension/index.ts is standard Pi. Template in references/templates.md.
Key patterns:
- Use
StringEnumfrom@earendil-works/pi-aifor action enums (notType.Union) - Resolve
statePathfromctx.cwdinsideexecutehandlers; usesession_startonly as a warm fallback - Atomic writes only (temp → rename)
- Keep tool output concise
- Do not depend on
session_switchunless your target SDK guarantees it
7. Optional: background runtime
runtime/index.ts must export createAppRuntime(ctx) and default-export { createAppRuntime }.
Required wiring:
sero.app.runtime: "./runtime/index.ts"inpackage.jsonruntime/tsconfig.jsonincluded in thetypecheckscriptrequiredHostCapabilities: ["appRuntime.background"]- If the runtime imports native/non-bundle-safe packages, list them in
sero.app.runtimeExternals - If the extension imports native/non-bundle-safe packages and uses bundled release packaging, list them in
sero.plugin.extensionExternals
Context provides: appId, workspaceId, workspacePath, stateFilePath,
and host.{appState, subagents, workspace, verification, git, devServers}.
8. Optional: build the UI
React component in ui/<Name>App.tsx. Template in references/templates.md.
Required:
- Both exports:
export function MyApp()ANDexport default MyApp - Import UI components from
@sero-ai/ui - Import
./styles.cssfrom every exposed MF entry (main app, widgets, etc.) - Tailwind semantic colors (
bg-background,text-foreground, etc.) ui/styles.cssshould import@sero-ai/ui/styles/plugin.cssand scan plugin-local files with@source "./**/*.{ts,tsx}"- Do not alias
@sero-ai/app-runtime - Scope keyboard listeners to the container (
tabIndex={0}), neverwindow - Use
ResizeObserverfor dynamic sizing
Also create: vite.config.ts (at package root), ui/styles.css,
ui/tsconfig.json, ui/vite-env.d.ts, ui/index.html.
9. Verify
pnpm install
pnpm --filter @sero-ai/plugin-<name> build # UI only
pnpm --filter @sero-ai/plugin-<name> typecheck
bash scripts/build-plugin.sh plugins/sero-<name>-plugin # installable bundle / release packaging
All must pass before the plugin is ready.
10. Test end-to-end
cd apps/desktop
SERO_DEV_PLUGINS=<name> bash scripts/dev.sh
Verify:
- Plugin appears in the sidebar
- UI mutations →
state.json→ UI re-renders - Agent tool call →
state.json→ file watcher → UI updates - If the plugin ships
pi.prompts/pi.skills, reload resources (/reload) and confirm the slash menu shows them - If tools are CLI-bridged, confirm no command name shadows the bridged entry point (
help <tool>/sero <tool> ...still hit the real tool) - If
runtime/: starts on workspace open, reacts to state, cleans up on close
Module Federation
react/react-domare shared singletons@sero-ai/app-runtimeis not shared via MF — usesglobalThissingleton; add tooptimizeDeps.exclude- Production
vite.config.tsmust usebase: './' - MF remote name convention:
sero_<id>(valid JS identifier) - Exposed module:
./<Component> - Every exposed entry must import the plugin stylesheet so external remotes ship their own CSS
Development workflow
- UI changes (
ui/) are handled by the remote Vite dev server. Sero also sends a lightweight cache-busted UI refresh for active local dev sessions; this should remount only the plugin surface, not refresh shell resources or restart agent/runtime state. - Changes to
extension/,runtime/,shared/types.ts,prompts/,skills/, orpackage.jsonrefresh the local plugin development session and may reload plugin resources devPortinpackage.jsonmust matchserver.portinvite.config.ts- Logs:
/tmp/sero-vite.log,/tmp/sero-remote-<name>.log,/tmp/sero-electron.log - If Local Plugin Development fails before Vite starts with an
esbuildhost/binary version mismatch or a missing Rollup native package, suspect stale platform-specific optional dependencies in the plugin's ownnode_modules. Sero attempts a one-time host-sidepnpm install --forcerepair for these failures. If repair still fails, deletenode_modulesand reinstall from inside that plugin directory on the macOS host before changing source code. Do not run the reinstall from a Sero workspace/container terminal, because that can install Linux native packages into a path that the macOS host dev server later executes.
Conventions
- Plugin directory:
plugins/sero-<name>-plugin/ - npm package:
@sero-ai/plugin-<name> - State file:
.sero/apps/<id>/state.json(workspace) or underSERO_HOME(global) - No source file over 500 LOC — split into sub-components, utils, or types files
- Always top-level imports; no dynamic
await import(...)for source modules
Reference implementations
| Plugin | Best for |
|---|---|
example/sero-notes-plugin/ (in this skill) |
Canonical minimal plugin. Every surface (tool + CLI bridge, command, runtime, UI hooks, static + dynamic widgets) in the smallest possible footprint. |
plugins/sero-git-plugin/ |
Focused plugin with single tool + substantial UI |
plugins/sero-admin-plugin/ |
Multiple panels, settings, dashboard surfaces |
plugins/sero-web-plugin/ |
Converting an existing Pi extension |
plugins/sero-cron-plugin/ |
Background jobs, command-oriented plugins |
../plugins/sero-kanban-plugin/ |
External plugin with background runtime + tool-driven UI + widgets |
Related docs
docs/plugins/guide.md— packaging and distributing installable pluginsdocs/plugins/technical.md— plugin system internals, federation, IPC, securitydocs/plugins/host-compatibility.md—requiredHostCapabilities, CLI bridging, hot-reloaddocs/architecture.md— desktop shell layout and host state flow
Troubleshooting
| Problem | Fix |
|---|---|
Build fails: Could not resolve entry module "node_modules/__mf__virtual/...hostAutoInit..." |
Keep Vite root at the package root; use build.rollupOptions.input: 'ui/index.html' instead of root: 'ui' |
Local Plugin Development fails: Cannot start service: Host version "..." does not match binary version "..." from esbuild |
The plugin has stale/mismatched native optional dependencies. Sero should try one host-side auto-repair. If it still fails, from the plugin directory on the macOS host (not inside the Sero container), run rm -rf node_modules && pnpm install --force, then retry pnpm exec vite --host 127.0.0.1 or Local Plugin Development. Do not patch Vite config/package versions first. |
Local Plugin Development fails: Cannot find module @rollup/rollup-darwin-arm64 or another @rollup/rollup-* native package |
Same root cause: platform-specific optional dependencies were not installed for the current machine. Sero should try one host-side auto-repair. If it still fails, from the plugin directory on the macOS host, run rm -rf node_modules && pnpm install --force. |
| App not in sidebar | Check sero.app.id/name in package.json, run pnpm install |
| Agent missing tool | Restart dev server, check pi.extensions field |
| UI changes not showing | Check remote Vite running; extension changes need full restart |
| External plugin missing styles | Import ./styles.css from every exposed MF entry; verify @source paths in ui/styles.css |
| Packaged plugin missing an extension dependency | If it must stay in node_modules, add the package name to sero.plugin.extensionExternals; otherwise keep it in dependencies and let bundleExtensions include it in the JS entrypoint |
Cannot find module './styles.css' |
Add ui/vite-env.d.ts with /// <reference types="vite/client" /> |
| Runtime never starts | Declare sero.app.runtime + requiredHostCapabilities: ["appRuntime.background"]; check /tmp/sero-electron.log |
| Build fails because no HTML entry exists | Add ui/index.html |
| "No UI module registered" | Set sero.app.component and devPort |
| "No workspace selected" | Pick one, or set scope: "global" |
| State not syncing | Verify same stateFile path, use atomic writes |
| Keyboard stealing input | Scope listeners to container, not window |
lazy: Expected dynamic import |
Add export default MyApp to component file |
| MF errors | devPort in package.json must match server.port in vite.config.ts |