Clipbus plugins

Plugins let Clipbus act on what you copy — detect text and images, show them as rich cards, and add quick actions. They're built with the open @clipbus/plugin-sdk, and each card is a web page (the official template and examples are built with Vue 3).

Overview

A Clipbus plugin is a small module that hooks into the clipboard. It works with text and images and can provide three kinds of capability — you implement only the ones you need.

detectorDetectInspect each copied text or image and write structured attachments onto the item.
rendererRenderShow an attachment as a card — Clipbus draws the title, tint and buttons, your web page draws the body.
actionActTurn the current text or image into a new value on demand; the result can feed the next Action.

How it works

Every time you copy text or an image — including copying the same content again — Clipbus runs the registered detectors; they run again after you edit an item or ask for re-detection. A detector that recognizes the content writes one or more attachments — typed JSON payloads — onto the clipboard item through ctx.item. When an item carries an attachment whose type a plugin declares a renderer for, that renderer's page draws a card in the attachment panel under the item.

Copydetectorattachmentrenderercard

Each card gets a chip in the attachment panel. Clipbus draws the card frame — title, tint and buttons — straight from data, so the card shows the right look on first display while your page loads into the body.

Runtime & card pages

A plugin has two separate parts, and both talk only to Clipbus. The runtime is one long-lived Node process per plugin; your detectors, actions and messages are registered through one definePlugin({ detectors, actions, messages }) call. Each card is a web page that Clipbus loads in a WebView from a renderer's page in the manifest. The two never share memory: a page reaches its runtime with a typed message, and Clipbus relays it.

Node runtimeCard page
Entryruntime.nodeEntryrenderers[].page HTML
You writedefinePlugin({ … })HTML + ctx calls
Import from@clipbus/plugin-sdk/runtime@clipbus/plugin-sdk/view
Host APIctx — the handler's second argumentctx — module-level

Both sides get a ctx with the same domains — ctx.item, ctx.system, ctx.env, ctx.settings, ctx.log — and a method available on both sides has the same signature on each. ctx.item is always the item the call is about: the detected item, the Action Flow's starting item, or the card's item.

Not every capability needs both parts — that is the distinction that matters:

CapabilityRuns inRuntime handlerCard page
detectorNode runtime(input, ctx) => void—
rendererCard page—renderers[].page
actionNode runtime(input, ctx) => result.*—
messageNode runtime, called from a card pagecontract.handle(fn)contract.call(data)

A plugin with only cards can leave out the runtime; Clipbus then starts no Node process for it. Messages are the typed channel between the two parts: define each one once with defineMessage<Req, Resp>(name) in a module both sides import, handle it in the runtime and call it from the page.

features/decode/messages.ts · illustrative
import { defineMessage } from "@clipbus/plugin-sdk";

export const verify = defineMessage<{ token: string }, { valid: boolean }>("verify");

// runtime — definePlugin({ messages: [ … ] })
verify.handle(async ({ token }) => ({ valid: await checkSignature(token) }));

// card page
const { valid } = await verify.call({ token: attachment.payload.token });

Detector

A detector turns copied content into attachments. It is keyed by its manifest detectors[].id, receives { item, content, trigger } and returns nothing — it writes through ctx.item.*. content is the full text, or an image's size and format (read the pixels from ctx.input.imagePath()); trigger says why it ran: copy, recopy, edit or manual.

features/decode · illustrative
// payload.ts — one contract, shared by the runtime and the card page
export const jwtAttachment = defineAttachment<JwtPreview>("plugin.example.decode.jwt");

// detector.ts — write through ctx.item, return nothing
export async function detectJwt({ content }, ctx) {
  if (content.kind !== "text") return;
  const jwt = tryDecodeJwt(content.text); // { header, claims, token } | undefined
  if (!jwt) return; // nothing written — this detector's last output is cleared
  await jwtAttachment.set(ctx, [
    { key: "main", payload: jwt, card: { title: `JWT · ${jwt.header.alg}` } },
  ]);
}

// plugin.ts — definePlugin({ detectors: { jwt: detectJwt } })

Writes are staged and committed together when the handler returns; a throw or a timeout discards them and keeps the previous output. Each run replaces that detector's previous attachments and search entries as a whole — what a run does not write is cleared — and writes equal to the current value are skipped. An attachment type starts with your plugin id, its payload is a JSON object, and defineAttachment<T>(type) types it for both sides. A detector can also add searchable text with ctx.item.setSearchExtension and tag or pin the item.

Renderer

A renderer shows one attachment type as a card. It lives only in the manifest — there is no runtime handler. Its entry sets the card's default title, tint and buttons, and its page is the HTML that draws the card body — the official template uses Vue 3.

features/decode/main.ts · illustrative
import { ctx } from "@clipbus/plugin-sdk/view";
import { autoFit } from "@clipbus/plugin-sdk/dom";

// startup snapshot, typed by the contract
const { attachment } = ctx.renderer.context(jwtAttachment);
render(attachment.payload);
autoFit(); // card height follows the content

if (isExpired(attachment.payload.claims)) {
  void ctx.renderer.update({ title: "JWT · expired", tint: "#DC2626" });
}

ctx.renderer.onButton("copy", () =>
  ctx.system.copyText({ text: JSON.stringify(attachment.payload.claims, null, 2) }));

Card appearance is data. Clipbus composes the frame from the manifest defaults, then the attachment's card (written with the attachment, or saved back by the page), then ctx.renderer.update({ title, tint, buttons, height }) from the running page. An omitted field stays and null returns to the manifest default; title, tint and buttons are saved back to the attachment, while height applies to the current display only. Button presses — including their number shortcuts — arrive through ctx.renderer.onButton.

Each renderer sets height in its manifest entry as { min, max } in points (1–800): a fixed height uses the same value for both, and omitting it gives the default 80–400. To let the card follow its content, call autoFit() from @clipbus/plugin-sdk/dom; it observes the content size and sends renderer.update({ height }) within that range.

Action

An Action turns the current value into a new one on demand. It is keyed by its manifest actions[].id and offered only when the current value's kind is in its inputKinds. The handler receives { item, content }: item is the Action Flow's starting item and content the current value — text or image — so results can feed the next Action. It returns result.text(text), result.image(path) or result.none(), each with an optional message for the user.

features/decode/action.ts · illustrative
import { result } from "@clipbus/plugin-sdk/runtime";

export async function decodeBase64({ content }) {
  if (content.kind !== "text") return result.none("Not text");
  const text = tryDecodeBase64(content.text);
  return text === undefined ? result.none("Not Base64") : result.text(text);
}

An image result is a file your plugin wrote, for example under cache/; Clipbus checks it and takes its own copy. In the Action panel the result is previewed and the user explicitly advances it to the next Action; none cannot advance, and results never auto-advance.

The plugin collection

The open-source collection scubers/awesome-clipbus-plugins holds the official plugins and the plugin template. Each plugin lives in its own top-level directory with its own build. To install one without cloning, open Settings → Plugins → Add Plugin → Git Repository, enter the repository URL and set Subdirectory to the plugin's directory name, e.g. clipbus-decoder-plugin.

It currently ships ten plugins — decoder, formatter, converter, extractor, preview, text, image, generator, cron and vibe; the collection README lists what each one does. template-plugin/ is the official scaffold and full-capability reference: copy it to start a new plugin rather than installing it.

Plugin anatomy & manifest

A plugin is split by feature — each capability in its own self-contained folder. plugin.ts is the definePlugin entry that registers the detectors, actions and messages. manifest.json declares the plugin's identity, its runtime entry, and the detectors, renderers and actions it contributes.

manifest.json
{
  "schemaVersion": 6,
  "plugin": {
    "id": "plugin.example.decode",
    "title": "Decoder",
    "version": "1.0.0"
  },
  "install": { "entry": "scripts/install.mjs" },
  "runtime": { "nodeEntry": "dist/plugin.cjs" },
  "detectors": [
    { "id": "jwt", "title": "JWT detector", "inputKinds": ["text"] }
  ],
  "renderers": [{
    "id": "jwt-card",
    "title": "JWT",
    "attachmentType": "plugin.example.decode.jwt",
    "page": "dist/ui/jwt-card/index.html",
    "height": { "min": 120, "max": 480 },
    "tint": "#0F766E",
    "buttons": [{ "id": "copy", "title": "Copy claims" }]
  }],
  "actions": [
    { "id": "base64", "title": "Decode Base64", "inputKinds": ["text"] }
  ]
}

Paths such as runtime.nodeEntry, install.entry and renderers[].page are relative to the plugin root. Every attachmentType starts with the plugin id, and messages are not declared in the manifest. The optional install hook is a Node script that runs when the plugin is installed; Clipbus asks for confirmation before running it, and hooks that need npm get it through CLIPBUS_PLUGIN_NPM_PATH. Dependency packages' own install scripts do not run (Clipbus sets npm_config_ignore_scripts); commit package-lock.json and install with npm ci.

The runtime and card pages have no network access, and the runtime cannot start other programs; it reads its package and reads and writes its own data and cache. To read or write a folder of the user's, declare one slot per purpose under permissions.folders in the manifest (read or readWrite); the user picks the folder when enabling the plugin, and the runtime gets its path from ctx.plugin.folder(id), which is null until one is picked.

project structure
your-plugin/
├── manifest.json            # identity, runtime entry, contributions
├── package.json             # depends on @clipbus/plugin-sdk
├── scripts/                 # build, verify, install hook
├── src/
│   ├── features/<name>/
│   │   ├── payload.ts       # defineAttachment contract
│   │   ├── messages.ts      # defineMessage contracts
│   │   ├── detector.ts      # writes through ctx.item
│   │   ├── action.ts        # returns result.*
│   │   ├── app.vue          # card page
│   │   └── main.ts          # page entry (+ index.html)
│   ├── shared/
│   ├── preview/
│   └── plugin.ts            # definePlugin — detectors, actions, messages
└── tests/

Getting started

Start from the plugin collection: clone github.com/scubers/awesome-clipbus-plugins and copy template-plugin/ — the official scaffold. It covers every extension point (detectors, renderers from compact to expanded, actions and typed messages), so the copy is a production-ready skeleton.

Recommended — let an AI agent build it. The collection ships two skills under .agents/skills/: clipbus-plugin-generator surveys the existing plugins, then extends the best match or scaffolds a new one from the template and runs the full verification gate; clipbus-template-sync pulls template and SDK updates into existing plugins. The template itself carries a bilingual README / GUIDE and an AGENTS.md for AI assistants, and depends on @clipbus/plugin-sdk, whose API.md is the source of truth.

Local development requires Node.js 22 or later.

terminal
npm install        # installs @clipbus/plugin-sdk
npm run dev        # Vite preview workbench
#   → preview cards with a simulated Clipbus
npm run build      # typecheck + lint + build → dist/
npm test           # runtime tests against the build
npm run verify     # manifest check + build + tests (PR gate)

To debug inside the app, open Settings → Plugins → Add Plugin → Local Development Plugin and choose the directory containing your manifest.json. It appears under Developer Plugins, and its runtime restarts when the source files change. If the manifest declares an install hook, Clipbus asks for confirmation before running it (cwd = that directory); logs go to <AppData>/development-plugins/<id>/install-logs/ and stay out of your git status. From the plugin's Actions menu, use Reload to re-read the manifest, Run Install Script… to re-run the install hook, and Installation Logs if install fails. Write your own logs with ctx.log.info(…); forwardConsole() also sends a card page's console output to the plugin log.

API reference (SDK)

Everything an author can use is generated into the SDK package, always matching your installed version, so it never drifts from this page. You don't need to read the SDK's internal source.

  • Docs map — node_modules/@clipbus/plugin-sdk/docs/README.md
  • Source of truth — node_modules/@clipbus/plugin-sdk/API.md (generated; wins on any conflict)
  • Upgrading from SDK 0.x — docs/migration-v1-v2.md; plugins on manifest v4 show as needing an update until they are migrated
  • SDK surface rules — SPECIFICATION.md
  • npm — @clipbus/plugin-sdk · refresh with npm update @clipbus/plugin-sdk

Plugin limits

The free tier includes up to 3 active plugins per type (detector / renderer / action). Clipbus Pro lifts these limits to unlimited plugins (and adds Cloud Sync) as a one-time purchase. See pricing for details.