🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@marswave/cola-plugin-sdk

Package Overview
Dependencies
Maintainers
6
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@marswave/cola-plugin-sdk

SDK for building Cola channel plugins that connect external messaging platforms to Cola.

latest
Source
npmnpm
Version
0.0.4
Version published
Maintainers
6
Created
Source

Cola Plugin SDK

Documentation

TypeScript SDK for building Cola plugins.

A Cola plugin is a local Node.js package that extends Cola from outside its codebase. One plugin can play any combination of three roles:

  • Channel. Connect an external message platform — Slack, WeChat, a device, a queue — and route messages in and out of Cola.
  • Tools. Register LLM-callable tools (Notion search, SQL queries, anything the model should be able to invoke). Tools default to global scope, so a tool-only plugin needs no channel.
  • Observer. Subscribe to Cola's session event stream across every session — desktop, CLI, every channel — for analytics, audit mirroring, or cross-channel automation.

Plugins never import Cola desktop, server, or shared internals; the SDK is the stable boundary.

Package

pnpm add @marswave/cola-plugin-sdk

Public API

  • definePlugin() is the general entry point. It accepts a top-level tools[] slot (each tool picks scope: 'global' | 'own-channel'), optional start / stop lifecycle hooks, and an optional channel.
  • defineChannel() is a thin wrapper for channel-only plugins that don't declare top-level tools or lifecycle hooks.
  • Public types for plugin metadata, channel capabilities, gateway/outbound/auth adapters, config schemas, commands, host runtime APIs, STT/TTS, top-level tools, and session lifecycle events (including the origin tag and scope: 'self' | 'all' subscription option).
  • createPollLoop() and withBackoff() helper utilities for polling channels.

The SDK is the compatibility boundary for distributed plugins. Do not import files from Cola's desktop app, server, or shared internal modules in a plugin that will be installed by users.

Minimal Channel Plugin

import { defineChannel } from '@marswave/cola-plugin-sdk'
import type { GatewayContext, OutboundContext } from '@marswave/cola-plugin-sdk'

type GatewayState = {
  startedAt?: number
}

export default defineChannel<GatewayState>({
  id: 'example',
  meta: {
    label: 'Example',
    description: 'Example message channel',
    markdownCapable: true
  },
  capabilities: {
    receive: { text: true },
    send: { text: true, markdown: true }
  },
  gateway: {
    async start(ctx: GatewayContext<GatewayState>) {
      ctx.state.startedAt = Date.now()

      await ctx.deliver({
        sessionId: ['dm', 'owner'],
        sender: { id: 'owner', name: 'Owner' },
        deliveryContext: { to: 'owner' },
        message: 'Hello from the example plugin'
      })
    },
    async stop(ctx) {
      ctx.logger.info('Example channel stopped')
    },
    getStatus() {
      return { connected: true, configured: true }
    }
  },
  outbound: {
    async sendText(ctx: OutboundContext) {
      // Send ctx.text back to the platform represented by ctx.deliveryContext.
    }
  }
})

Plugin Metadata

Cola discovers installed plugins from ~/.cola/plugins/<id>/package.json. The package must include a cola.plugin manifest. Channel plugins should also add cola.channel display metadata.

{
  "name": "cola-plugin-example",
  "version": "0.1.0",
  "type": "module",
  "dependencies": {
    "@marswave/cola-plugin-sdk": "^0.0.3"
  },
  "cola": {
    "plugin": {
      "id": "example",
      "entry": "./dist/index.js",
      "minColaVersion": "0.9.10"
    },
    "channel": {
      "label": "Example",
      "description": "Example message channel",
      "aliases": ["ex"],
      "docsPath": "/channels/example"
    }
  }
}

Build or bundle the plugin entry before installing it. cola.plugin.entry must point to JavaScript that Cola can import at runtime.

Set cola.plugin.minColaVersion, for example "0.9.10", only when the plugin uses runtime features that require a specific Cola app release. The plugin's own package version and @marswave/cola-plugin-sdk dependency version do not force users to update Cola by themselves.

Core Concepts

Gateway

The gateway adapter owns the platform receive loop. It can run long polling, connect to a WebSocket, subscribe to a queue, or use any platform-specific mechanism. For each accepted inbound message, call ctx.deliver() with a stable sessionId, the sender, optional routing data, normalized text, and optional attachments.

Cola owns the plugin:<pluginId>: scope prefix. Plugins only choose the session suffix, such as ['dm', userId] or ['room', roomId].

Outbound

The outbound adapter sends Cola's reply back to the platform represented by deliveryContext. sendText() is required when the channel can send text. Optional methods handle media, typing indicators, and reactions.

For media delivery, implement sendMedia() and declare mediaCapabilities. supportedKinds narrows uploads to 'image', 'video', 'audio', and/or 'file'; omit it to accept every kind already enabled in capabilities.send. Use maxBytesPerFile, maxBytesTotal, and maxCount to publish the platform limits Cola should enforce before calling your adapter.

Auth And Identity

The auth adapter runs login and disconnect flows. QR-code and status updates can be surfaced with ctx.onQrCode() and ctx.onStatus(). After a platform account is authorized, call ctx.runtime.identity.bind(senderId) so Cola accepts messages from that sender. Disconnect flows should call unbind().

Group Chats And Access

By default a delivered message is treated as a direct (DM) message. To support group chats, set conversation on the deliver payload and report whether the bot was @mentioned:

await ctx.deliver({
  sessionId: ['group', groupId],
  sender: { id: senderId, name: senderName },
  conversation: { kind: 'group', id: groupId }, // 'group' | 'channel' | 'direct'
  mentionedBot, // detected from the inbound message; required for group/channel
  message: text
})

A group message without mentionedBot: true is silently ignored, so the agent only joins when explicitly addressed. Omitting conversation entirely is treated as a direct message (backward compatible).

Access is authorized on two axes:

  • DMs are authorized per sender: cola channel allow <pluginId> <senderId>.
  • Groups are authorized as a whole: cola channel allow-group <pluginId> <groupId>, so every member is allowed without listing each one.

Unauthorized DMs, and groups that @mention the bot but are not authorized, receive a guidance hint naming the command to run. Override the text with channel.unauthorizedHint(target). Manage authorization with cola channel allow / revoke / allow-group / revoke-group / allowlist.

Host Runtime

ctx.runtime exposes host services that are safe for plugins:

  • identity for channel user binding.
  • config.get() for readonly plugin config, and config.patch(partial) to shallow-merge into this plugin's OWN persisted config. It hot-updates the running config without a restart, ignores the reserved pluginDir key, and can only ever write the calling plugin's config. Use it to persist credentials obtained at runtime, such as after a scan-to-register flow.
  • events.on() for session events — session lifecycle, compaction, agent loop, turn boundaries, assistant message stream, tool:call / tool:result, and runtime tool:execution_*. Defaults to this plugin's own scope; pass { scope: 'all' } to observe every session in Cola. See the event reference for the full catalogue, the origin tag, and the non-forwarding contract.
  • stt and tts for host-provided local speech services when available.
  • llm for one-shot model calls using the user's configured primary model: llm.generateText(prompt, opts?) resolves with plain text, llm.generateObject(prompt, schema, opts?) forces and validates a JSON result against a JSON Schema, and llm.streamText(prompt, opts?) yields incremental text deltas as an AsyncIterable<string>. All accept { systemPrompt?, timeoutMs? } (the timeout covers the entire call, including the full stream) and throw when no provider key is configured or the call fails.
  • logger for local logging and explicit error reporting.
  • reloadGateway() for restarting receive loops after config or auth changes.

Plugin tools

A plugin can register LLM-callable tools via the top-level tools array. Each tool declares a scope:

  • 'global' (default) — visible in every session: desktop, CLI, and every plugin channel. Use this for capability-style tools (Notion search, SQL query, etc.).
  • 'own-channel' — visible only when a session under this plugin's own channel scope is active. Requires the plugin to also declare channel.

Tool names are exposed to the LLM as plugin__<pluginId>__<name> so the same short name from different plugins does not collide. tool:call and tool:result events report the un-prefixed name.

export default definePlugin({
  id: 'notion',
  meta: { label: 'Notion', description: 'Notion integration' },
  tools: [
    {
      name: 'search',
      description: 'Search the Notion workspace',
      parameters: { type: 'object', properties: { q: { type: 'string' } } },
      async execute({ q }, ctx) {
        // ...
        return { content: [{ type: 'text', text: 'result' }] }
      }
    }
  ]
})

parameters is a JSON Schema object (Typebox schemas are JSON Schema at runtime). execute() receives the parsed input and a PluginToolContext with sessionId (undefined for desktop/CLI scopes), scopeKey, toolCallId, an AbortSignal, the plugin logger, and the readonly config. Return { content, details?, isError? }.

Lifecycle and Cross-Channel Observation

Plugin.start(ctx) runs once when the plugin loads, and Plugin.stop() once when it unloads. Use them to register listeners, open background workers, etc. Each runs under a 5 s timeout so a slow plugin can't stall the host.

runtime.events.on(type, handler, options?) accepts options.scope:

  • 'self' (default) — only sessions under this plugin's own scope.
  • 'all' — every session in Cola, including desktop, CLI, and other plugins. Use this for cross-channel automation and global observability.

Every event payload carries an origin discriminator ({ kind: 'plugin', pluginId } | { kind: 'desktop' } | { kind: 'cli', user } | { kind: 'other', scopeKey }) so cross-scope handlers can tell where the event came from.

export default definePlugin({
  id: 'observer',
  meta: { label: 'Observer', description: 'Mirrors activity to a webhook' },
  async start({ runtime }) {
    runtime.events.on(
      'turn:end',
      (e) => {
        runtime.logger.info(`turn ${e.turnIndex} ended on ${e.origin.kind}`)
      },
      { scope: 'all' }
    )
  }
})

Listeners are read-only; the return value is ignored, and each handler runs under a 2 s timeout so a slow plugin can't block the agent. The toolName on tool:* events is always the original, unprefixed name (e.g. lookup_user, not plugin__example__lookup_user).

Compatibility

The SDK follows semver after publication. Removing or changing a public export is a breaking change. Types and adapters marked @internal may change before they become stable.

License

MIT. See LICENSE.

MIT is intentionally permissive: plugin authors can use the SDK in open-source, commercial, or private plugins as long as they preserve the copyright notice and license text.

FAQs

Package last updated on 12 Jun 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts