
Security News
PolinRider: North Korea-Linked Supply Chain Campaign Expands Across Open Source Ecosystems
PolinRider expands across npm, Packagist, Go modules, and Chrome extensions, using hidden loaders to target developer environments.
@marswave/cola-plugin-sdk
Advanced tools
SDK for building Cola channel plugins that connect external messaging platforms to Cola.
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:
Plugins never import Cola desktop, server, or shared internals; the SDK is the stable boundary.
pnpm add @marswave/cola-plugin-sdk
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.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.
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.
}
}
})
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.
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].
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.
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().
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:
cola channel allow <pluginId> <senderId>.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.
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.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? }.
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).
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.
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
SDK for building Cola channel plugins that connect external messaging platforms to Cola.
We found that @marswave/cola-plugin-sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 6 open source maintainers collaborating on the project.
Did you know?

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.

Security News
PolinRider expands across npm, Packagist, Go modules, and Chrome extensions, using hidden loaders to target developer environments.

Security News
Open source attacks are accelerating as AI coding agents pull in dependencies faster, with less human review.

Research
/Security News
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.