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:

  1. Collect all plugins
  2. Check capability compatibility
  3. Warn on missing dependencies
  4. Warn on version conflicts
  5. 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

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

Migration: Existing Features

Todos → Plugin

  1. Extract to plugins/todos/
  2. Create manifest.json
  3. Define capabilities: todos
  4. Add UI specs for forms/previews

Tasks/Jobs → Plugin

  1. Extract to plugins/jobs/
  2. Create manifest.json
  3. Define capabilities: jobs, job-ai
  4. Add AI-specific command handlers

Commands → Plugin

Each command family becomes a plugin:

Plugin Registry

Local (Development)

{
  "plugins": {
    "todos": { "enabled": true, "path": "./plugins/todos" },
    "jobs": { "enabled": true, "path": "./plugins/jobs" }
  }
}

Remote (Future)

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

  1. Plugin distribution: Local only, or Nostr-based registry?
  2. Hot reload: Support during development?
  3. Plugin dependencies: How to handle circular deps?
  4. Migration: How to handle user data when plugin updates schema?
  5. 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.

  1. Register the local plugin in plugins.json. Set the package name and local appweaver.icon SVG path in the plugin's package.json.
  2. Export a data-only landingPage from plugins/<alias>/landing.ts, typed with PluginLandingDefinition from apps/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.
  3. Keep screenshots and GIFs in the plugin's landing/assets/ directory. Refer to them with plugin-root-relative paths. Set installScreenshot: null when unavailable. assetAliases can preserve existing /screenshots/... URLs referenced by published articles while the source image stays plugin-owned.
  4. 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.
  5. Run bun run --cwd apps/landing build. It generates explicit content imports, copies plugin icons/media into public/plugin-assets/, generates static docs, builds the site, and writes dist/apps/<slug>/index.html with 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.