> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clideck.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugins

> Extend CliDeck with custom plugins

Plugins extend CliDeck without modifying the core. A plugin can observe terminal output, transform input, react to status changes, create and control sessions, add toolbar buttons, and communicate between backend and frontend.

The plugin API is the same one the bundled plugins use — Autopilot, for example, is built entirely on it.

## Bundled Plugins

CliDeck ships with three plugins out of the box. They're automatically installed into `~/.clideck/plugins/` on first run.

### Trim Clip

A clipboard utility that cleans up copied text. Click the scissors icon in the terminal toolbar to trim trailing whitespace from every line in your clipboard. Useful when copying code from terminal output that has extra spaces.

| Setting                     | Description                                            | Default |
| --------------------------- | ------------------------------------------------------ | ------- |
| **Enabled**                 | Show the toolbar button                                | On      |
| **Trim leading whitespace** | Also remove leading spaces (off preserves indentation) | Off     |

### Autopilot

A project-level workflow router that automatically forwards output between role-assigned agent sessions. See the dedicated [Autopilot](/features/autopilot) page for full documentation.

| Setting       | Description                                    | Default          |
| ------------- | ---------------------------------------------- | ---------------- |
| **Enabled**   | Enable the Autopilot button on project headers | On               |
| **Provider**  | LLM provider for routing decisions             | Anthropic        |
| **Model**     | Model for routing decisions                    | claude-haiku-4-5 |
| **API Key**   | Provider API key (falls back to env var)       | —                |
| **Debugging** | Save router prompts/responses to disk          | Off              |

### Voice Input

Speak to your agents instead of typing. Press a hotkey (default: **F4**) to start recording, press again to stop. Your speech is transcribed and typed into the active terminal session.

Two transcription backends are available:

| Backend              | Description                          | Requirements                       |
| -------------------- | ------------------------------------ | ---------------------------------- |
| **OpenAI** (default) | Uses OpenAI's Whisper API (remote)   | OpenAI API key                     |
| **Local**            | Runs Whisper via MLX on your machine | macOS with Apple Silicon, Python 3 |

| Setting               | Description                                          | Default |
| --------------------- | ---------------------------------------------------- | ------- |
| **Enabled**           | Enable voice input                                   | Off     |
| **ASR Backend**       | OpenAI (remote) or Local (MLX)                       | OpenAI  |
| **OpenAI API Key**    | Required for OpenAI backend                          | —       |
| **Language**          | Transcription language (or auto-detect)              | Auto    |
| **Record Key**        | Hotkey to start/stop recording                       | F4      |
| **Replacements File** | Path to a text replacements file for post-processing | —       |

The replacements file uses a simple format — one rule per line:

```
wrong phrase => correct phrase
another mistake => fix | all
```

The `| all` flag makes the replacement case-insensitive.

<Note>
  Voice Input requires microphone access. Your browser will ask for permission the first time you record.
</Note>

## Installing Custom Plugins

Each plugin is a folder inside `~/.clideck/plugins/`. There are two kinds:

**Plugin without dependencies** — loaded immediately on startup:

```
~/.clideck/plugins/my-plugin/
  clideck-plugin.json   # manifest (optional, recommended)
  index.js             # backend code (required)
  client.js            # frontend code (optional)
  public/              # static assets (optional)
```

**Plugin with npm dependencies** — requires a one-time install:

```
~/.clideck/plugins/my-plugin/
  clideck-plugin.json   # manifest with "install": "npm"
  package.json          # npm dependencies
  index.js             # backend code (required)
  client.js            # frontend code (optional)
```

Plugins without dependencies are loaded on restart. Plugins with dependencies appear in the Plugins panel with an **Install** button — click it to run `npm install` in the plugin directory, then the plugin loads.

<Note>
  The `install` field currently only supports `"npm"`. If your plugin needs non-Node dependencies (Python, Rust, etc.), use `scripts.postinstall` in your `package.json` — npm runs it automatically during install.
</Note>

<Warning>
  Plugins run with full Node.js access — same trust model as npm packages. Only install plugins you trust or have reviewed.
</Warning>

## Managing Plugins

Plugins appear in the **Plugins panel** (circuit icon in the sidebar rail). Each plugin shows its icon, name, and settings. If a plugin has settings, they show up as controls (toggles, dropdowns, text fields, number inputs) that you can change on the fly.

Install state is tracked in CliDeck's config. If a plugin's `node_modules` is deleted manually, CliDeck detects this on next startup and resets the install state — you'll see the Install button again. When a bundled plugin is updated to a new version, its install state is also cleared so new dependencies get picked up.

To remove a custom plugin, click **Delete** in the plugin's settings panel, or delete its folder from `~/.clideck/plugins/` and restart CliDeck.

## Creating a Plugin

### The Manifest

A `clideck-plugin.json` manifest lets you define your plugin's identity and settings. If omitted, the folder name is used as the ID and name.

```json theme={null}
{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "icon": "<svg class=\"w-4 h-4\" ...>...</svg>",
  "install": "npm",
  "settings": [
    { "key": "enabled", "label": "Enable feature", "type": "toggle", "default": true },
    { "key": "mode", "label": "Mode", "type": "select", "options": ["fast", "slow"], "default": "fast" },
    { "key": "limit", "label": "Max items", "type": "number", "min": 1, "max": 100, "default": 10 },
    { "key": "apiKey", "label": "API Key", "type": "text", "default": "" }
  ]
}
```

| Field      | Required | Description                                                                  |
| ---------- | -------- | ---------------------------------------------------------------------------- |
| `id`       | No       | Unique plugin identifier (defaults to folder name)                           |
| `name`     | No       | Display name in the UI (defaults to folder name)                             |
| `version`  | No       | Version string (defaults to `0.0.0`)                                         |
| `icon`     | No       | Inline SVG string for the Plugins panel. If omitted, a default icon is shown |
| `install`  | No       | Set to `"npm"` if the plugin has a `package.json` with dependencies          |
| `settings` | No       | Array of user-configurable settings                                          |

### Setting Types

| Type             | Control                       | Extra Options                                             |
| ---------------- | ----------------------------- | --------------------------------------------------------- |
| `toggle`         | On/off switch                 | —                                                         |
| `select`         | Dropdown                      | `options`: array of strings or `{ value, label }` objects |
| `dynamic-select` | Dropdown with runtime options | Options set via `api.setSettingOptions(key, options)`     |
| `number`         | Number input                  | `min`, `max` (optional)                                   |
| `text`           | Text field                    | —                                                         |

All setting types support an optional `description` field for hint text below the control.

Settings are validated against the manifest — type, allowed options, and min/max are enforced automatically.

### Backend API (index.js)

The backend entry point exports an `init` function that receives the plugin API:

```javascript theme={null}
module.exports = {
  init(api) {
    // Your plugin code here
  }
};
```

#### Session Hooks

```javascript theme={null}
// Transform input before it reaches the terminal
// Return a string to replace the input, or nothing to leave it unchanged
api.onSessionInput((sessionId, data) => {
  return data;
});

// Observe terminal output (read-only)
api.onSessionOutput((sessionId, data) => {
  // data is the raw terminal output
});

// React to working/idle transitions
api.onStatusChange((sessionId, working, source) => {
  // working: true = agent started working, false = agent went idle
  // source: 'telemetry', 'client', 'esc', 'menu', etc.
});

// React to interactive menu detection (choice menus in agent output)
api.onMenuDetected((sessionId, choices) => {
  // choices: [{ value, label, selected }]
});

// Receive clean, complete transcript entries (user inputs and agent outputs)
api.onTranscriptEntry((sessionId, role, text) => {
  // role: 'user' (complete input line) or 'agent' (processed output block)
  // text: clean text with ANSI codes stripped, ready to use
});
```

<Note>
  Input hooks run as a chain — each hook's return value becomes the input for the next hook. This lets multiple plugins transform input in sequence.
</Note>

<Note>
  `onSessionInput` and `onSessionOutput` receive raw terminal data (keystrokes, escape sequences, ANSI codes). If you need clean, human-readable text, use `onTranscriptEntry` instead — it delivers complete lines with all terminal noise stripped out.
</Note>

#### Session Info

```javascript theme={null}
api.getSessions();    // [{ id, name, cwd, commandId, presetId, themeId, projectId, roleName, working }]
api.getSession(id);   // single session object, or null
```

#### Frontend Messaging

Backend and frontend communicate through namespaced messages:

```javascript theme={null}
// Send a message to the frontend
api.sendToFrontend('my-event', { key: 'value' });

// Receive a message from the frontend
api.onFrontendMessage('my-action', (msg) => {
  // handle message
});
```

Messages are automatically namespaced as `plugin.<id>.<event>` — no collision between plugins.

#### Session Control

```javascript theme={null}
// Write data to a session's terminal (as if the user typed it)
api.inputToSession(id, data);

// Create a new session programmatically
const sessionId = api.createSession({
  presetId: 'claude-code',   // or commandId
  name: 'My Session',
  cwd: '/path/to/project',
  projectId: 'uuid',         // optional
  roleName: 'Reviewer',      // optional
  ephemeral: true,            // optional — won't persist for resume
});

// Close a session
api.closeSession(id);

// Enable/disable auto-approval of interactive menus for a session
api.setAutoApproveMenu(id, true);
```

#### Config Access

```javascript theme={null}
api.getRoles();       // all defined roles (deep copy)
api.getProjects();    // all defined projects (deep copy)
```

#### Transcript and Screen

```javascript theme={null}
// Get the last N transcript turns (user/agent alternating entries)
api.getTranscript(id, 20);

// Get structured screen turns parsed from the terminal buffer
api.getScreenTurns(id, 'claude-code', { raw: false });

// Get the raw clean screen content
api.getScreen(id);

// Detect interactive menus from raw screen lines
api.detectMenu(lines, 'claude-code');
```

#### Toolbar and Project Actions

```javascript theme={null}
api.addToolbarAction({
  id: 'do-thing',
  title: 'Do Thing',
  icon: '<svg>...</svg>',  // inline SVG, 16x16
});

// Add a button to project headers in the sidebar
api.addProjectAction({
  id: 'run-workflow',
  title: 'Run Workflow',
  icon: '<svg>...</svg>',
});
```

#### Session Pills

Session pills are sidebar entries that represent background tasks (like Autopilot). They appear under their project, similar to sessions.

```javascript theme={null}
// Create a pill
api.addSessionPill({ id: 'my-task', title: 'My Task', projectId: 'uuid', icon: '' });

// Update pill state
api.updateSessionPill('my-task', {
  working: true,
  statusText: 'Processing...',
  title: 'Updated Title',      // optional
  projectId: 'new-uuid',       // optional
});

// Append a timestamped log entry (viewable by clicking the pill)
api.appendPillLog('my-task', 'Step 1 complete');

// Remove the pill from the sidebar
api.removeSessionPill('my-task');
```

#### Settings

```javascript theme={null}
api.getSetting('mode');      // current value for a single key
api.getSettings();           // all settings as { key: value }

// Update a setting programmatically (validated against manifest)
api.setSetting('mode', 'fast');

// Set dropdown options at runtime for dynamic-select settings
api.setSettingOptions('model', [
  { value: 'gpt-4', label: 'GPT-4 ($30/M input)' },
  { value: 'gpt-3.5', label: 'GPT-3.5 ($0.50/M input)' },
]);

api.onSettingsChange((key, value) => {
  // called when the user changes a setting in the UI
});
```

#### Utilities

```javascript theme={null}
api.log('message');          // log with [plugin:my-plugin] prefix
api.pluginId;                // this plugin's ID
api.pluginDir;               // absolute path to this plugin's folder
api.version;                 // API version (currently 1)

api.onShutdown(() => {
  // cleanup when CliDeck shuts down
});

// Resolve a package — checks plugin-local node_modules first, then app-level
const piAi = api.resolve('@mariozechner/pi-ai');
```

### Frontend API (client.js)

The optional frontend entry point is an ES module:

```javascript theme={null}
export function init(api) {
  // Send a message to the backend
  api.send('my-action', { key: 'value' });

  // Receive a message from the backend
  api.onMessage('my-event', (msg) => {
    // msg is the full message object including type
  });

  // Add a button to the terminal toolbar
  api.addToolbarButton({
    title: 'Do Thing',
    icon: '<svg>...</svg>',
    onClick: () => { /* handle click */ }
  });

  // Session helpers
  api.getActiveSessionId();       // current active session ID
  api.writeToSession(id, text);   // write text to a session's terminal

  // Toast notifications
  api.toast('Something happened', { type: 'success' });
}
```

#### Toast Notifications

Show toast notifications from your plugin. Returns an object with a `dismiss()` method.

```javascript theme={null}
api.toast(message, opts)
```

| Option     | Type    | Default  | Description                                                        |
| ---------- | ------- | -------- | ------------------------------------------------------------------ |
| `type`     | string  | `'info'` | `'info'`, `'success'`, `'warn'`, or `'error'`                      |
| `duration` | number  | `3000`   | Auto-dismiss in ms. `0` = persistent (manual dismiss only)         |
| `id`       | string  | —        | Dedupe key. A new toast with the same id replaces the previous one |
| `html`     | boolean | `false`  | If true, message is rendered as HTML                               |

```javascript theme={null}
// Transient success
api.toast('Model loaded', { type: 'success' });

// Persistent progress (dismiss when done)
const t = api.toast('Installing dependencies...', { type: 'info', duration: 0, id: 'setup' });
// later...
t.dismiss();

// Replace previous toast with same id
api.toast('Dependencies installed', { type: 'success', id: 'setup' });
```

Multiple toasts from different plugins stack automatically without overlapping.

### Static Assets

Files in the `public/` subdirectory are served at `/plugins/<id>/<filename>`. Use this for images, CSS, or additional scripts your plugin needs.

## Example: Simple Logger Plugin

A minimal plugin that logs when agents start and stop working:

```
~/.clideck/plugins/work-logger/
  clideck-plugin.json
  index.js
```

**clideck-plugin.json:**

```json theme={null}
{
  "id": "work-logger",
  "name": "Work Logger",
  "version": "1.0.0"
}
```

**index.js:**

```javascript theme={null}
module.exports = {
  init(api) {
    api.onStatusChange((sessionId, working) => {
      const session = api.getSession(sessionId);
      const name = session?.name || sessionId;
      api.log(`${name} is now ${working ? 'working' : 'idle'}`);
    });

    api.onShutdown(() => {
      api.log('shutting down');
    });
  }
};
```

## Error Handling

Plugin errors are caught and logged — one plugin can't crash CliDeck or affect other plugins. If your plugin throws during `init`, it's unloaded and its hooks are removed.
