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

# Plugins

# Plugins (Extensions)

## Quick start (new to plugins?)

A plugin is just a **small code module** that extends crocbot with extra
features (commands, tools, and Gateway RPC).

Most of the time, you’ll use plugins when you want a feature that’s not built
into core crocbot yet (or you want to keep optional features out of your main
install).

Fast path:

1. See what’s already loaded:

```bash theme={null}
crocbot plugins list
```

2. Install a plugin from npm:

```bash theme={null}
crocbot plugins install <npm-spec>
```

3. Restart the Gateway, then configure under `plugins.entries.<id>.config`.

## Available plugins

Discover plugins on CrocHub and other registries, then install them via
`crocbot plugins install <npm-spec>`.

crocbot plugins are **TypeScript modules** loaded at runtime via jiti. **Config
validation does not execute plugin code**; it uses the plugin manifest and JSON
Schema instead. See [Plugin manifest](/plugins/manifest).

Plugins can register:

* Gateway RPC methods
* Gateway HTTP handlers
* Agent tools
* CLI commands
* Background services
* Optional config validation
* **Skills** (by listing `skills` directories in the plugin manifest)
* **Auto-reply commands** (execute without invoking the AI agent)

Plugins run **in‑process** with the Gateway, so treat them as trusted code.
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).

## Runtime helpers

Plugins can access selected core helpers via `api.runtime` (config, logging, media utilities).
See the plugin SDK types for the current surface area.

## Discovery & precedence

crocbot scans, in order:

1. Config paths

* `plugins.load.paths` (file or directory)

2. Workspace extensions

* `<workspace>/.crocbot/extensions/*.ts`
* `<workspace>/.crocbot/extensions/*/index.ts`

3. Global extensions

* `~/.crocbot/extensions/*.ts`
* `~/.crocbot/extensions/*/index.ts`

4. Bundled extensions (currently only `telegram`)

* `<crocbot>/extensions/telegram`

The bundled Telegram plugin is enabled by default. Any other plugin must be
installed via workspace/global paths or `plugins.load.paths`, then enabled or
disabled via `plugins.entries.<id>.enabled` (or `crocbot plugins enable <id>`).

Each plugin must include a `crocbot.plugin.json` file in its root. If a path
points at a file, the plugin root is the file's directory and must contain the
manifest.

If multiple plugins resolve to the same id, the first match in the order above
wins and lower-precedence copies are ignored.

### Package packs

A plugin directory may include a `package.json` with `crocbot.extensions`:

```json theme={null}
{
  "name": "my-pack",
  "crocbot": {
    "extensions": ["./src/safety.ts", "./src/tools.ts"]
  }
}
```

Each entry becomes a plugin. If the pack lists multiple extensions, the plugin id
becomes `name/<fileBase>`.

If your plugin imports npm deps, install them in that directory so
`node_modules` is available (`npm install` / `pnpm install`).

### Channel catalog metadata

Channel plugins can advertise onboarding metadata via `crocbot.channel` and
install hints via `crocbot.install`. This keeps the core catalog data-free.

Example:

```json theme={null}
{
  "name": "@crocbot/nextcloud-talk",
  "crocbot": {
    "extensions": ["./index.ts"],
    "channel": {
      "id": "nextcloud-talk",
      "label": "Nextcloud Talk",
      "selectionLabel": "Nextcloud Talk (self-hosted)",
      "docsPath": "/channels/nextcloud-talk",
      "docsLabel": "nextcloud-talk",
      "blurb": "Self-hosted chat via Nextcloud Talk webhook bots.",
      "order": 65,
      "aliases": ["nc-talk", "nc"]
    },
    "install": {
      "npmSpec": "@crocbot/nextcloud-talk",
      "localPath": "extensions/nextcloud-talk",
      "defaultChoice": "npm"
    }
  }
}
```

crocbot can also merge **external channel catalogs** (for example, an MPM
registry export). Drop a JSON file at one of:

* `~/.crocbot/mpm/plugins.json`
* `~/.crocbot/mpm/catalog.json`
* `~/.crocbot/plugins/catalog.json`

Or point `CROCBOT_PLUGIN_CATALOG_PATHS` (or `CROCBOT_MPM_CATALOG_PATHS`) at
one or more JSON files (comma/semicolon/`PATH`-delimited). Each file should
contain `{ "entries": [ { "name": "@scope/pkg", "crocbot": { "channel": {...}, "install": {...} } } ] }`.

## Plugin IDs

Default plugin ids:

* Package packs: `package.json` `name`
* Standalone file: file base name (`~/.../example-plugin.ts` → `example-plugin`)

If a plugin exports `id`, crocbot uses it but warns when it doesn’t match the
configured id.

## Config

```json5 theme={null}
{
  plugins: {
    enabled: true,
    allow: ["example-plugin"],
    deny: ["untrusted-plugin"],
    load: { paths: ["~/Projects/oss/example-plugin"] },
    entries: {
      "example-plugin": { enabled: true, config: { provider: "demo" } }
    }
  }
}
```

Fields:

* `enabled`: master toggle (default: true)
* `allow`: allowlist (optional)
* `deny`: denylist (optional; deny wins)
* `load.paths`: extra plugin files/dirs
* `entries.<id>`: per‑plugin toggles + config

Config changes **require a gateway restart**.

Validation rules (strict):

* Unknown plugin ids in `entries`, `allow`, `deny`, or `slots` are **errors**.
* Unknown `channels.<id>` keys are **errors** unless a plugin manifest declares
  the channel id.
* Plugin config is validated using the JSON Schema embedded in
  `crocbot.plugin.json` (`configSchema`).
* If a plugin is disabled, its config is preserved and a **warning** is emitted.

## Plugin slots (exclusive categories)

Some plugin categories are **exclusive** (only one active at a time). Use
`plugins.slots` to select which plugin owns the slot:

```json5 theme={null}
{
  plugins: {
    slots: {
      memory: "memory-core" // or "none" to disable memory plugins
    }
  }
}
```

If multiple plugins declare `kind: "memory"`, only the selected one loads. Others
are disabled with diagnostics.

## Control UI (schema + labels)

The Control UI uses `config.schema` (JSON Schema + `uiHints`) to render better forms.

crocbot augments `uiHints` at runtime based on discovered plugins:

* Adds per-plugin labels for `plugins.entries.<id>` / `.enabled` / `.config`
* Merges optional plugin-provided config field hints under:
  `plugins.entries.<id>.config.<field>`

If you want your plugin config fields to show good labels/placeholders (and mark secrets as sensitive),
provide `uiHints` alongside your JSON Schema in the plugin manifest.

Example:

```json theme={null}
{
  "id": "my-plugin",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "apiKey": { "type": "string" },
      "region": { "type": "string" }
    }
  },
  "uiHints": {
    "apiKey": { "label": "API Key", "sensitive": true },
    "region": { "label": "Region", "placeholder": "us-east-1" }
  }
}
```

## CLI

```bash theme={null}
crocbot plugins list
crocbot plugins info <id>
crocbot plugins install <path>                 # copy a local file/dir into ~/.crocbot/extensions/<id>
crocbot plugins install ./extensions/example-plugin # relative path ok
crocbot plugins install ./plugin.tgz           # install from a local tarball
crocbot plugins install ./plugin.zip           # install from a local zip
crocbot plugins install -l ./extensions/example-plugin # link (no copy) for dev
crocbot plugins install @crocbot/example-plugin # install from npm
crocbot plugins update <id>
crocbot plugins update --all
crocbot plugins enable <id>
crocbot plugins disable <id>
crocbot plugins doctor
```

`plugins update` only works for npm installs tracked under `plugins.installs`.

Plugins may also register their own top‑level commands.

## Plugin API (overview)

Plugins export either:

* A function: `(api) => { ... }`
* An object: `{ id, name, configSchema, register(api) { ... } }`

## Plugin hooks

Plugins can ship hooks and register them at runtime. This lets a plugin bundle
event-driven automation without a separate hook pack install.

### Example

```
import { registerPluginHooksFromDir } from "crocbot/plugin-sdk";

export default function register(api) {
  registerPluginHooksFromDir(api, "./hooks");
}
```

Notes:

* Hook directories follow the normal hook structure (`HOOK.md` + `handler.ts`).
* Hook eligibility rules still apply (OS/bins/env/config requirements).
* Plugin-managed hooks show up in `crocbot hooks list` with `plugin:<id>`.
* You cannot enable/disable plugin-managed hooks via `crocbot hooks`; enable/disable the plugin instead.

## Provider plugins (model auth)

Plugins can register **model provider auth** flows so users can run OAuth or
API-key setup inside crocbot (no external scripts needed).

Register a provider via `api.registerProvider(...)`. Each provider exposes one
or more auth methods (OAuth, API key, device code, etc.). These methods power:

* `crocbot models auth login --provider <id> [--method <id>]`

Example:

```ts theme={null}
api.registerProvider({
  id: "acme",
  label: "AcmeAI",
  auth: [
    {
      id: "oauth",
      label: "OAuth",
      kind: "oauth",
      run: async (ctx) => {
        // Run OAuth flow and return auth profiles.
        return {
          profiles: [
            {
              profileId: "acme:default",
              credential: {
                type: "oauth",
                provider: "acme",
                access: "...",
                refresh: "...",
                expires: Date.now() + 3600 * 1000,
              },
            },
          ],
          defaultModel: "acme/opus-1",
        };
      },
    },
  ],
});
```

Notes:

* `run` receives a `ProviderAuthContext` with `prompter`, `runtime`,
  `openUrl`, and `oauth.createVpsAwareHandlers` helpers.
* Return `configPatch` when you need to add default models or provider config.
* Return `defaultModel` so `--set-default` can update agent defaults.

### Register a messaging channel

Plugins can register **channel plugins** that behave like the built-in Telegram channel.
Channel config lives under `channels.<id>` and is validated by your channel plugin code.

```ts theme={null}
const myChannel = {
  id: "acmechat",
  meta: {
    id: "acmechat",
    label: "AcmeChat",
    selectionLabel: "AcmeChat (API)",
    docsPath: "/channels/acmechat",
    blurb: "demo channel plugin.",
    aliases: ["acme"],
  },
  capabilities: { chatTypes: ["direct"] },
  config: {
    listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
    resolveAccount: (cfg, accountId) =>
      (cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? { accountId }),
  },
  outbound: {
    deliveryMode: "direct",
    sendText: async () => ({ ok: true }),
  },
};

export default function (api) {
  api.registerChannel({ plugin: myChannel });
}
```

Notes:

* Put config under `channels.<id>` (not `plugins.entries`).
* `meta.label` is used for labels in CLI/UI lists.
* `meta.aliases` adds alternate ids for normalization and CLI inputs.
* `meta.preferOver` lists channel ids to skip auto-enable when both are configured.
* `meta.detailLabel` and `meta.systemImage` let UIs show richer channel labels/icons.

### Write a new messaging channel (step‑by‑step)

Use this when you want a **new chat surface** (a “messaging channel”), not a model provider.
Model provider docs live under `/providers/*`.

1. Pick an id + config shape

* All channel config lives under `channels.<id>`.
* Prefer `channels.<id>.accounts.<accountId>` for multi‑account setups.

2. Define the channel metadata

* `meta.label`, `meta.selectionLabel`, `meta.docsPath`, `meta.blurb` control CLI/UI lists.
* `meta.docsPath` should point at a docs page like `/channels/<id>`.
* `meta.preferOver` lets a plugin replace another channel (auto-enable prefers it).
* `meta.detailLabel` and `meta.systemImage` are used by UIs for detail text/icons.

3. Implement the required adapters

* `config.listAccountIds` + `config.resolveAccount`
* `capabilities` (chat types, media, threads, etc.)
* `outbound.deliveryMode` + `outbound.sendText` (for basic send)

4. Add optional adapters as needed

* `setup` (wizard), `security` (DM policy), `status` (health/diagnostics)
* `gateway` (start/stop/login), `mentions`, `threading`, `streaming`
* `actions` (message actions), `commands` (native command behavior)

5. Register the channel in your plugin

* `api.registerChannel({ plugin })`

Minimal config example:

```json5 theme={null}
{
  channels: {
    acmechat: {
      accounts: {
        default: { token: "ACME_TOKEN", enabled: true }
      }
    }
  }
}
```

Minimal channel plugin (outbound‑only):

```ts theme={null}
const plugin = {
  id: "acmechat",
  meta: {
    id: "acmechat",
    label: "AcmeChat",
    selectionLabel: "AcmeChat (API)",
    docsPath: "/channels/acmechat",
    blurb: "AcmeChat messaging channel.",
    aliases: ["acme"],
  },
  capabilities: { chatTypes: ["direct"] },
  config: {
    listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
    resolveAccount: (cfg, accountId) =>
      (cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? { accountId }),
  },
  outbound: {
    deliveryMode: "direct",
    sendText: async ({ text }) => {
      // deliver `text` to your channel here
      return { ok: true };
    },
  },
};

export default function (api) {
  api.registerChannel({ plugin });
}
```

Load the plugin (extensions dir or `plugins.load.paths`), restart the gateway,
then configure `channels.<id>` in your config.

### Agent tools

See the dedicated guide: [Plugin agent tools](/plugins/agent-tools).

### Register a gateway RPC method

```ts theme={null}
export default function (api) {
  api.registerGatewayMethod("myplugin.status", ({ respond }) => {
    respond(true, { ok: true });
  });
}
```

### Register CLI commands

```ts theme={null}
export default function (api) {
  api.registerCli(({ program }) => {
    program.command("mycmd").action(() => {
      console.log("Hello");
    });
  }, { commands: ["mycmd"] });
}
```

### Register auto-reply commands

Plugins can register custom slash commands that execute **without invoking the
AI agent**. This is useful for toggle commands, status checks, or quick actions
that don't need LLM processing.

```ts theme={null}
export default function (api) {
  api.registerCommand({
    name: "mystatus",
    description: "Show plugin status",
    handler: (ctx) => ({
      text: `Plugin is running! Channel: ${ctx.channel}`,
    }),
  });
}
```

Command handler context:

* `senderId`: The sender's ID (if available)
* `channel`: The channel where the command was sent
* `isAuthorizedSender`: Whether the sender is an authorized user
* `args`: Arguments passed after the command (if `acceptsArgs: true`)
* `commandBody`: The full command text
* `config`: The current crocbot config

Command options:

* `name`: Command name (without the leading `/`)
* `description`: Help text shown in command lists
* `acceptsArgs`: Whether the command accepts arguments (default: false). If false and arguments are provided, the command won't match and the message falls through to other handlers
* `requireAuth`: Whether to require authorized sender (default: true)
* `handler`: Function that returns `{ text: string }` (can be async)

Example with authorization and arguments:

```ts theme={null}
api.registerCommand({
  name: "setmode",
  description: "Set plugin mode",
  acceptsArgs: true,
  requireAuth: true,
  handler: async (ctx) => {
    const mode = ctx.args?.trim() || "default";
    await saveMode(mode);
    return { text: `Mode set to: ${mode}` };
  },
});
```

Notes:

* Plugin commands are processed **before** built-in commands and the AI agent
* Commands are registered globally and work across all channels
* Command names are case-insensitive (`/MyStatus` matches `/mystatus`)
* Command names must start with a letter and contain only letters, numbers, hyphens, and underscores
* Reserved command names (like `help`, `status`, `reset`, etc.) cannot be overridden by plugins
* Duplicate command registration across plugins will fail with a diagnostic error

### Register background services

```ts theme={null}
export default function (api) {
  api.registerService({
    id: "my-service",
    start: () => api.logger.info("ready"),
    stop: () => api.logger.info("bye"),
  });
}
```

## Naming conventions

* Gateway methods: `pluginId.action` (example: `myplugin.status`)
* Tools: `snake_case` (example: `my_tool`)
* CLI commands: kebab or camel, but avoid clashing with core commands

## Skills

Plugins can ship a skill in the repo (`skills/<name>/SKILL.md`).
Enable it with `plugins.entries.<id>.enabled` (or other config gates) and ensure
it’s present in your workspace/managed skills locations.

## Distribution (npm)

Recommended packaging:

* Main package: `crocbot` (this repo)
* Plugins: separate npm packages under `@crocbot/*` (example: `@crocbot/example-plugin`)

Publishing contract:

* Plugin `package.json` must include `crocbot.extensions` with one or more entry files.
* Entry files can be `.js` or `.ts` (jiti loads TS at runtime).
* `crocbot plugins install <npm-spec>` uses `npm pack`, extracts into `~/.crocbot/extensions/<id>/`, and enables it in config.
* Config key stability: scoped packages are normalized to the **unscoped** id for `plugins.entries.*`.

## Safety notes

Plugins run in-process with the Gateway. Treat them as trusted code:

* Only install plugins you trust.
* Prefer `plugins.allow` allowlists.
* Restart the Gateway after changes.

## Testing plugins

Plugins can (and should) ship tests:

* In-repo plugins can keep Vitest tests under `src/**` (example: `src/plugins/example.plugin.test.ts`).
* Separately published plugins should run their own CI (lint/build/test) and validate `crocbot.extensions` points at the built entrypoint (`dist/index.js`).
