Plugin System Specification
Overview
Extensible plugin architecture for AppWeaver. Core features (todos, jobs, tasks) are first-class plugins. Third-party plugins can be installed separately.
Core Concepts
Plugin
A self-contained module that provides capabilities and handles commands.
Capability
A named feature a plugin provides (e.g., todos, jobs, git). Versioned.
Command
A user-invokable action, exposed by plugins.
Plugin Structure
my-plugin/
├── manifest.json # Metadata, dependencies, capabilities
├── src/
│ ├── index.ts # Plugin entry point
│ ├── commands/ # Command handlers
│ └── capabilities/ # Capability implementations
└── ui/ # Optional UI specs (declarative)
manifest.json
{
"name": "jobs",
"version": "1.2.0",
"description": "Job tracking and management",
"author": "bot-core",
"apiVersion": "1.0.0",
"provides": [
{ "name": "jobs", "version": "1.0.0" }
],
"requires": [
{ "name": "storage", "version": ">=2.0.0" },
{ "name": "ai", "version": "^1.0.0" }
],
"commands": [
{
"name": "job list",
"inputMode": "direct",
"description": "List all jobs"
},
{
"name": "job create",
"inputMode": "form",
"ui": { "fields": [...] }
}
],
"capabilities": {
"uiComponents": ["actionButton", "expandable", "list"]
}
}
Versioning Strategy
API Version (semver)
Bot declares supported plugin API version. Plugins declare required version.
apiVersion: "1.0.0" // Plugin requires
botSupports: "^1.0.0" // Bot declares
Compatibility: Plugin's required version must fall within Bot's supported range.
Plugin Version
Each plugin versions independently. Follows semver (major.minor.patch).
Capability Version
Each capability has version. Allows incremental capability changes.
{
name: 'todos',
version: '1.2.0', // Capability version, separate from plugin version
}
Data Schema Version
Storage layer versions data structures. Migrations handled by storage plugin.
Capability System
Declaration
Plugins declare what they provide and what they need.
// Plugin A
provides: [{ name: 'storage', version: '2.0.0' }];
requires: [];
// Plugin B
provides: [{ name: 'todos', version: '1.0.0' }];
requires: [{ name: 'storage', version: '>=2.0.0' }];
Resolution
At startup, Bot resolves dependency graph:
- Collect all plugins
- Check capability compatibility
- Warn on missing dependencies
- Warn on version conflicts
- Load in dependency order
Built-in Capabilities
| Capability | Description |
|---|---|
storage |
Key-value persistence with versioning |
ai |
AI inference interface |
nostr |
Nostr message send/receive |
commands |
Command registration |
ui |
UI component rendering |
Command Interface
Handler Signature
interface CommandHandler {
(ctx: CommandContext): Promise<CommandResult>;
}
interface CommandContext {
userId: string;
botId: string;
args: string[];
raw: string;
capabilities: Map<string, any>;
}
interface CommandResult {
type: 'text' | 'form' | 'preview' | 'error';
content: string;
ui?: UiSpec;
}
Form UI Spec
interface FormSpec {
fields: FieldSpec[];
submitLabel?: string;
}
interface FieldSpec {
name: string;
type: 'text' | 'textarea' | 'url' | 'number' | 'select' | 'checkbox';
label: string;
required?: boolean;
placeholder?: string;
options?: string[]; // For select
default?: any;
}
Preview UI Spec
interface PreviewSpec {
content: string;
actions: ActionSpec[];
}
interface ActionSpec {
label: string;
command: string; // Supports {var} interpolation
}
UI Components (Declarative)
Plugins return UI specs, NOT code. Client interprets and renders.
Supported Components
actionButton: Tap triggers commandexpandable: Collapsed/expanded sectionsstatusCard: Summary + actionslist: Tappable rowscodeBlock: Syntax-highlighted code
Example: Job Card (from jobs plugin)
{
"type": "preview",
"content": "Senior Engineer @ Acme\n$150k/yr",
"actions": [
{ "label": "Apply", "command": "/job apply {id}" },
{ "label": "Edit", "command": "/job edit {id}" }
]
}
Client renders as rich card on mobile, text + buttons in terminal.
Plugin Lifecycle
Loading → Resolving Dependencies → Initializing → Running
↓
Unloading (on disable/uninstall)
Initialization
interface Plugin {
init(ctx: PluginContext): Promise<void>;
shutdown(): Promise<void>;
}
interface PluginContext {
getCapability(name: string): any;
registerCommand(cmd: CommandSpec): void;
storage: StorageCapability;
}
Security
- No code execution: UI specs are declarative data
- Capability sandboxing: Plugins only access declared capabilities
- Command validation: All commands validated before execution
- Storage isolation: Plugins access only their own data namespace
Migration: Existing Features
Todos → Plugin
- Extract to
plugins/todos/ - Create manifest.json
- Define capabilities:
todos - Add UI specs for forms/previews
Tasks/Jobs → Plugin
- Extract to
plugins/jobs/ - Create manifest.json
- Define capabilities:
jobs,job-ai - Add AI-specific command handlers
Commands → Plugin
Each command family becomes a plugin:
plugins/todos-/todo *plugins/jobs-/job *plugins/git-/git *
Plugin Registry
Local (Development)
{
"plugins": {
"todos": { "enabled": true, "path": "./plugins/todos" },
"jobs": { "enabled": true, "path": "./plugins/jobs" }
}
}
Remote (Future)
- Nostr-based plugin discovery
- Install from npub
- Verify signed manifests
File Structure
.opencode/
├── plugins/
│ ├── todos/
│ │ ├── manifest.json
│ │ └── src/
│ │ ├── index.ts
│ │ ├── commands/
│ │ └── storage/
│ └── jobs/
│ ├── manifest.json
│ └── src/
│ ├── index.ts
│ ├── commands/
│ └── ai/
├── plugin.json # Registry config
└── capabilities/ # Built-in capability interfaces
Open Questions
- Plugin distribution: Local only, or Nostr-based registry?
- Hot reload: Support during development?
- Plugin dependencies: How to handle circular deps?
- Migration: How to handle user data when plugin updates schema?
- Discovery: How do users find available plugins?
Add a plugin landing page
Plugins own their landing content; the landing app owns generic rendering and
static generation. For example, PayPerQ's landing.ts declares /apps/ppq.
- Register the local plugin in
plugins.json. Set the package name and localappweaver.iconSVG path in the plugin'spackage.json. - Export a data-only
landingPagefromplugins/<alias>/landing.ts, typed withPluginLandingDefinitionfromapps/landing/content-types.ts. Define its route, names, description, features, demo metadata, presentation, and roadmap target there. No central route/icon/copy mappings are required. - Keep screenshots and GIFs in the plugin's
landing/assets/directory. Refer to them with plugin-root-relative paths. SetinstallScreenshot: nullwhen unavailable.assetAliasescan preserve existing/screenshots/...URLs referenced by published articles while the source image stays plugin-owned. - Keep plugin documents in the plugin's
docs/. Use ordinary relative Markdown links to core documents, e.g.[Web renderer](../../../docs/WEB_RENDERER.md)from a plugin document, so both local readers and the generator can follow it. - Run
bun run --cwd apps/landing build. It generates explicit content imports, copies plugin icons/media intopublic/plugin-assets/, generates static docs, builds the site, and writesdist/apps/<slug>/index.htmlwith app-specific SEO and fallback content. All steps are local; deployment uses the maintainer's existing Vercel CLI workflow.
For interactive demos, retain the existing root demo:generate and
web:demo:build workflow, then run landing's demo:copy. The page uses generated
demo command metadata to select the embedded widget. The reusable demo engine
remains core-owned; plugin fixtures and story definitions remain plugin-owned.
See the landing author guide for generator details.