# Plugin Development Guide

## Overview

Agents Orchestrator plugins are self-contained bundles that run in a sandboxed Web Worker. A plugin consists of a manifest file (`plugin.json`) and an entry point script (`index.js`). Plugins interact with the host application through a message-passing API gated by declared permissions.

## Plugin Anatomy

A minimal plugin directory:

```
~/.config/bterminal/plugins/my-plugin/
  plugin.json     -- Manifest (required)
  index.js        -- Entry point (required)
```

### Manifest: plugin.json

```json
{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "A brief description of what this plugin does.",
  "author": "Your Name",
  "entry": "index.js",
  "permissions": [
    "notifications",
    "tasks"
  ]
}
```

**Required fields:**

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique identifier (lowercase, hyphens allowed) |
| `name` | `string` | Human-readable display name |
| `version` | `string` | Semver version string |
| `description` | `string` | Short description (under 200 characters) |
| `entry` | `string` | Relative path to entry point script |
| `permissions` | `string[]` | List of API permissions the plugin requires |

**Optional fields:**

| Field | Type | Description |
|-------|------|-------------|
| `author` | `string` | Plugin author name |
| `homepage` | `string` | URL to plugin documentation |
| `minVersion` | `string` | Minimum Agents Orchestrator version required |

### Entry Point: index.js

The entry point is loaded as a Web Worker module. The `agor` global object provides the plugin API.

```javascript
// index.js
const { meta, notifications } = agor;

console.log(`${meta.id} v${meta.version} loaded`);

notifications.send({
  title: meta.name,
  body: 'Plugin activated.',
  type: 'info',
});
```

## Web Worker Sandbox

Plugins run in an isolated Web Worker context. The sandbox enforces strict boundaries:

**Not available:**

- Filesystem access (no `fs`, no `Deno.readFile`, no `fetch` to `file://`)
- Network access (no `fetch`, no `XMLHttpRequest`, no WebSocket)
- DOM access (no `document`, no `window`)
- Tauri IPC (no `invoke`, no event listeners)
- Dynamic imports (no `import()`, no `importScripts()`)

**Available:**

- Standard JavaScript built-ins (`JSON`, `Map`, `Set`, `Promise`, `Date`, etc.)
- `console.log/warn/error` (routed to host debug output)
- `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`
- The `agor` API object (permission-gated)

## Plugin API

All API methods are asynchronous and return Promises. Each API namespace requires a corresponding permission declared in `plugin.json`.

### agor.meta

Always available (no permission required).

```typescript
interface PluginMeta {
  id: string;        // Plugin ID from manifest
  version: string;   // Plugin version from manifest
  name: string;      // Plugin display name
}
```

### agor.palette

**Permission:** `"palette"`

Register commands in the application command palette.

```typescript
// Register a command
agor.palette.register({
  id: 'my-plugin.greet',
  label: 'My Plugin: Say Hello',
  callback: () => {
    agor.notifications.send({
      title: 'Hello',
      body: 'Greetings from the plugin.',
      type: 'info',
    });
  },
});

// Unregister a command
agor.palette.unregister('my-plugin.greet');
```

### agor.messages

**Permission:** `"messages"` -- Read agent messages for the active session (read-only).

- `agor.messages.list(sessionId)` -- returns `Array<{ role, content, timestamp }>`.
- `agor.messages.count(sessionId)` -- returns message count.

### agor.tasks

**Permission:** `"tasks"` -- Read/write task board entries (see Tasks-as-KV below).

- `agor.tasks.create({ title, description, status })` -- returns task ID.
- `agor.tasks.list({ status?, limit? })` -- returns task array.
- `agor.tasks.updateStatus(taskId, status)` -- updates status.
- `agor.tasks.delete(taskId)` -- removes task.

### agor.events

**Permission:** `"events"` -- Subscribe to application events.

- `agor.events.on(eventName, callback)` -- returns unsubscribe function.
- Events: `agent:status`, `agent:message`, `agent:complete`, `agent:error`.
- Each event payload includes `sessionId` plus event-specific fields.

### agor.notifications

**Permission:** `"notifications"` -- Send toast notifications.

- `agor.notifications.send({ title, body, type })` -- type: info | success | warning | error.

## Permission Model

Each API namespace is gated by a permission string. If a plugin calls an API it has not declared in its `permissions` array, the call is rejected with a `PermissionDenied` error.

| Permission | API Namespace | Access Level |
|------------|---------------|-------------|
| `palette` | `agor.palette` | Register/unregister commands |
| `messages` | `agor.messages` | Read-only agent messages |
| `tasks` | `agor.tasks` | Read/write tasks |
| `events` | `agor.events` | Subscribe to events |
| `notifications` | `agor.notifications` | Send toast notifications |

Permissions are displayed to the user during plugin installation. Users must accept the permission list to proceed.

## Tasks-as-KV Pattern

Plugins that need persistent key-value storage use the task system with prefixed keys. This avoids adding a separate storage layer.

### Convention

Task titles use the format `plugin:{plugin-id}:{key}`. The task description field holds the value (string, or JSON-serialized for structured data).

```javascript
// Store a value
await agor.tasks.create({
  title: `plugin:${agor.meta.id}:last-run`,
  description: JSON.stringify({ timestamp: Date.now(), count: 42 }),
  status: 'done',
});

// Retrieve a value
const tasks = await agor.tasks.list({ limit: 1 });
const entry = tasks.find(t => t.title === `plugin:${agor.meta.id}:last-run`);
const data = JSON.parse(entry.description);
```

### LRU Cap

Plugins should limit their KV entries to avoid unbounded growth. Recommended cap: 100 entries per plugin. When creating a new entry that would exceed the cap, delete the oldest entry first.

### Purge

On plugin uninstall, all tasks with the `plugin:{plugin-id}:` prefix are automatically purged.

## Shared Utilities

Two helper functions are injected into the Web Worker global scope for common patterns:

### safeGet(obj, path, defaultValue)

Safe property access for nested objects. Avoids `TypeError` on undefined intermediate properties.

```javascript
const value = safeGet(event, 'data.session.cost', 0);
```

### safeMsg(template, ...args)

String interpolation with type coercion and truncation (max 500 characters per argument).

```javascript
const msg = safeMsg('Session {} completed with {} turns', sessionId, turnCount);
```

## Example Plugin: hello-world

```json
{
  "id": "hello-world",
  "name": "Hello World",
  "version": "1.0.0",
  "description": "Minimal example plugin that greets the user.",
  "entry": "index.js",
  "permissions": ["palette", "notifications"]
}
```

```javascript
// index.js
const { meta, palette, notifications } = agor;

palette.register({
  id: 'hello-world.greet',
  label: 'Hello World: Greet',
  callback: () => {
    notifications.send({
      title: 'Hello',
      body: `Greetings from ${meta.name} v${meta.version}.`,
      type: 'info',
    });
  },
});

```

## Publishing

To publish a plugin to the marketplace:

1. Create a directory in the `agents-orchestrator/agor-plugins` repository under `plugins/{plugin-id}/`.
2. Add the plugin files (`plugin.json`, `index.js`, and any supporting files).
3. Create a `.tar.gz` archive of the plugin directory.
4. Compute the SHA-256 checksum: `sha256sum my-plugin.tar.gz`.
5. Add an entry to `catalog.json` with the download URL, checksum, and metadata.
6. Submit a pull request to the `agor-plugins` repository.

The catalog maintainers review the plugin for security (no obfuscated code, reasonable permissions) and functionality before merging.

## Scaffolding

Use the scaffolding script to generate a new plugin skeleton:

```bash
./scripts/plugin-init.sh my-plugin "My Plugin" "A description of my plugin"
```

This creates:

```
~/.config/bterminal/plugins/my-plugin/
  plugin.json     -- Pre-filled manifest
  index.js        -- Minimal entry point with palette command stub
```

The script prompts for permissions to declare. Generated files include comments explaining each section.
