npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@clipbus/plugin-sdk

v0.8.7

Published

Typed SDK for authoring Clipbus plugins — runtime (Node.js) and UI (WebView) helpers generated from the Clipbus plugin wire contract.

Readme

@clipbus/plugin-sdk

Standalone, publishable SDK for Clipbus plugins. Provides typed helpers for the runtime (Node.js) and UI (WebView) sides of a plugin.

Installation

Install from npm:

npm install @clipbus/plugin-sdk
{
  "dependencies": {
    "@clipbus/plugin-sdk": "^0.1.0"
  }
}

Then import the runtime, UI, or DOM entry point:

const { definePlugin } = require('@clipbus/plugin-sdk/runtime');
import { clipbus } from '@clipbus/plugin-sdk/ui';
import { autoFit } from '@clipbus/plugin-sdk/dom';

Publishing prerequisite: the clipbus npm org/scope must exist before the first npm publish (a one-time npmjs.com operation under a logged-in account). Fallback unscoped name if @clipbus is unavailable: clipbus-plugin-sdk.

Runtime entry (@clipbus/plugin-sdk/runtime)

Used in Node.js plugin code to define capabilities and return results.

definePlugin(definition)

Validates and returns a plugin definition object. Throws if setup is not a function.

const { definePlugin } = require('@clipbus/plugin-sdk/runtime');

module.exports = definePlugin({
  setup(init) {
    return {
      detectors: { 'my-detector': { detect(input, ctx) { /* ... */ } } },
      attachmentRenderers: { 'my-renderer': { resolveAttachment(input, ctx) { /* ... */ } } },
      actions: { 'my-action': { resolveSession(input, ctx) { /* ... */ } } }
    };
  }
});

actionResult.text(value, options?)

Returns a locked { result: { resultKind: 'text', text }, userMessage } shape.

| Param | Type | Description | |---|---|---| | value | unknown | Coerced to string. null/undefined"". | | options.userMessage | string? | Optional message shown to the user. |

actionResult.none(options?)

Returns { result: { resultKind: 'none', text: null }, userMessage }.

actionResult.image(value, options?)

Returns a locked { result: { resultKind: 'image', imageTempPath, imageFormatHint }, userMessage } shape. Use this when an action produces an image result written to a temp path allocated via ctx.host.action.allocateImageTempPath().

| Param | Type | Description | |---|---|---| | value.imageTempPath | string | Path returned by allocateImageTempPath. | | value.imageFormatHint | string? | Optional format hint (e.g. "png", "jpeg"). | | options.userMessage | string? | Optional message shown to the user. |


messageHandlers — handle UI → Runtime RPC calls

Register handlers that respond to calls made from UI via clipbus.runtime.invoke. Each key is a string; the value is an async function receiving (request, ctx).

const { definePlugin } = require('@clipbus/plugin-sdk/runtime');

module.exports = definePlugin({
  messageHandlers: {
    'generate-image': async (req, ctx) => {
      const { path } = await ctx.host.action.allocateImageTempPath({ formatHint: 'png' });
      // ... write bytes to path ...
      return { imageTempPath: path };
    },
  },
});

defineMessage(key) — shared type contract for UI ↔ Runtime RPC

Creates a typed contract object. Import from @clipbus/plugin-sdk/runtime on the runtime side (gives .handle()) or from @clipbus/plugin-sdk/ui on the UI side (gives .invoke()).

// shared/contracts.ts (imported by both runtime and UI)
import { defineMessage } from '@clipbus/plugin-sdk/runtime';

export const GenerateImage = defineMessage<
  { prompt: string },
  { imageTempPath: string }
>('generate-image');

// runtime/index.ts
definePlugin({
  messageHandlers: Object.fromEntries([
    GenerateImage.handle(async (req, ctx) => {
      const { path } = await ctx.host.action.allocateImageTempPath({ formatHint: 'png' });
      return { imageTempPath: path };
    }),
  ]),
});

Host verbs (ctx.host.*) — runtime context only

These verbs are available on the ctx.host object passed to every runtime handler. All calls go through real Node IPC and return typed responses.

ctx.host.item

| Method | Returns | Description | |---|---|---| | setTags({tags}) | Promise<{tags: string[]}> | Replace item tags (requires setTags permission) | | addTags({tags}) | Promise<{tags: string[]}> | Add tags to item | | removeTags({tags}) | Promise<{tags: string[]}> | Remove tags from item | | setPinned({pinned}) | Promise<{pinned: boolean}> | Pin or unpin the item | | setAttachments(payload) | Promise<{}> | Replace attachments (requires setAttachment permission) | | setSearchExtension(payload) | Promise<{}> | Replace search extension entries | | materializeImagePath() | Promise<{path: string}> | Copies the item's image to a stable temp file. Idempotent across invocation. Only valid for image-type items. | | readAttachment({attachmentType, attachmentKey}) | Promise<{payloadJson?: string}> | Returns the payloadJson string for the named attachment, or null if absent. |

ctx.host.action

| Method | Returns | Description | |---|---|---| | allocateImageTempPath({formatHint?}) | Promise<{path: string}> | Allocates a unique writable path for an image result. Consumed by actionResult.image({imageTempPath}). Cleaned up when invocation scope closes. |

ctx.host.asset

| Method | Returns | Description | |---|---|---| | registerImage({path}) | Promise<{url: string}> | Registers a Node-produced / local image file with the current session and returns an opaque clipbus-asset:// URL the WebView can render in <img>. The host validates the file, copies it into the session owner dir, then mints a session-scoped token. The real path never reaches JS. |

ctx.host.clipboard, ctx.host.navigation, ctx.host.settings

| Method | Returns | Description | |---|---|---| | clipboard.copyText({text}) | Promise<void> | Copy text to system clipboard | | navigation.openUrl({url}) | Promise<void> | Open a URL | | navigation.revealInFinder({path}) | Promise<void> | Show file path in Finder | | navigation.openFilePath({path}) | Promise<void> | Open a file by path with default app | | settings.get({key}) | Promise<{value: string \| null}> | Read a single plugin setting | | settings.getAll() | Promise<{settings: Record<string, string>}> | Read all plugin settings |

ctx.host.locale

| Method | Returns | Description | |---|---|---| | locale.get() | Promise<{locale: string}> | Read the host's current interface locale. Values: "en", "ja", "zh-Hans". Wrap in try/catch when targeting older hosts that may not support this capability. |


DOM utilities (@clipbus/plugin-sdk/dom)

Optional sub-package for WebView plugins that need DOM manipulation utilities.

import { patchConsole, patchTextInputState, autoFit } from '@clipbus/plugin-sdk/dom';

// Patch console and text input state (call once at module load)
patchConsole();
patchTextInputState();

// Auto-fit WebView height with ResizeObserver
const dispose = await autoFit({ min: 120, max: 480, target: containerEl });
dispose(); // stop monitoring

UI entry (@clipbus/plugin-sdk/ui)

Used in WebView plugin code (ES modules, Vite build). Exposes a single clipbus object.

import { clipbus } from '@clipbus/plugin-sdk/ui';

// No async ready() call needed — subscribers can be registered immediately.
const item = clipbus.item.current(); // may be undefined if bootstrap hasn't arrived yet
clipbus.item.on(next => { /* always fires once host pushes bootstrap */ });

No clipbus.ready()

The SDK does not export clipbus.ready(). Observers (clipbus.<domain>.on(fn)) are context-neutral: register them at module load and the host's bootstrap push will trigger them. Use .current() for a synchronous snapshot; defend against undefined with ?. / ?? / early-return.


clipbus.item — Topic<PluginClipboardItem>

| Member | Shape | Description | |---|---|---| | clipbus.item.current() | () → PluginClipboardItem \| undefined | Read current item snapshot (may be undefined before bootstrap arrives) | | clipbus.item.on(fn) | (fn) → Unsubscribe | Subscribe to item changes | | clipbus.item.readAttachment({attachmentType, attachmentKey}) | Verb → {payloadJson?: string} | Read an attachment payload for the current item |

clipbus.item.attachment — OptionalTopic<PluginAttachmentPayload> (data layer; attachment context)

| Member | Shape | Description | |---|---|---| | clipbus.item.attachment.current() | () → PluginAttachmentPayload \| undefined | Current attachment payload | | clipbus.item.attachment.on(fn) | (fn) → Unsubscribe | Subscribe to payload changes |

clipbus.attachmentRenderer — renderer operations (attachment context)

| Member | Shape | Description | |---|---|---| | clipbus.attachmentRenderer.setButtons({buttons}) | Verb | Replace native renderer button list (whole-list replace; first call overrides resolveAttachment seed). Strict wire shape — pass {buttons: [...]}. | | clipbus.attachmentRenderer.onHostInvoke(fn) | Stream | Subscribe to host-dispatched button clicks; callback receives { buttonID } |

clipbus.theme — Topic<PluginThemeTokenSnapshot>

| Member | Shape | Description | |---|---|---| | clipbus.theme.current() | () → PluginThemeTokenSnapshot \| undefined | Read current theme tokens. The host injects __CLIPBUS_PLUGIN_THEME__ at WebView startup, so this is synchronously available in practice. | | clipbus.theme.on(fn) | (fn) → Unsubscribe | Subscribe to theme updates via the clipbus-plugin-theme host event |

There is no clipbus.theme.refresh() or clipbus.theme.getThemeSnapshot() — the unified topic replaces both.


clipbus.locale — Topic<PluginLocalePayload>

| Member | Shape | Description | |---|---|---| | clipbus.locale.current() | () → PluginLocalePayload \| undefined | Read current locale. The host injects __CLIPBUS_PLUGIN_LOCALE__ at WebView startup, so this is synchronously available in practice. | | clipbus.locale.on(fn) | (fn) → Unsubscribe | Subscribe to the clipbus-plugin-locale host event. The host locale is fixed per app session (changing it requires a restart), so this rarely fires after the initial value — it exists for parity with other topics. |

Locale values: "en", "ja", "zh-Hans" (the host's current interface language).

current() may return undefined when the host has not yet injected the value. Defend with a fallback at the call site: clipbus.locale.current()?.locale ?? 'en'.


clipbus.asset — local image URLs for <img> (any context)

Returns opaque clipbus-asset:// URLs so a WebView can render local images in <img> without ever seeing a real file path (the WebView sandbox blocks file://). The host resolves the token internally; tokens are session-scoped and invalidated when the owner is released.

| Member | Shape | Description | |---|---|---| | clipbus.asset.currentItemImageUrl() | Verb → {url?: string} | URL of the current item's own image. url is absent when the item is not an image. | | clipbus.asset.pathReferenceImageUrl({index}) | Verb → {url?: string} | URL of the current item's path_reference entry at index. url is absent when out of range or the entry is not an image. |

Arbitrary / generated images are minted only on the runtime side via ctx.host.asset.registerImage({path}) (see above), so a WebView XSS cannot turn an arbitrary file into a renderable URL.

import { clipbus } from '@clipbus/plugin-sdk/ui';
const { url } = await clipbus.asset.currentItemImageUrl();
if (url) imgEl.src = url; // url is undefined for non-image items

clipbus.runtime

| Member | Shape | Description | |---|---|---| | clipbus.runtime.invoke<TResp>({key, payload, timeoutMs?}) | Verb → TResp | Call a handler registered in the plugin's own Node runtime via messageHandlers. Strict wire shape — single object parameter. Default timeout: 30 s. Returns the handler's value directly (the SDK auto-unwraps the IPC envelope). Throws on handler error; err.name and err.data are preserved across the process boundary. |

import { clipbus, defineMessage } from '@clipbus/plugin-sdk/ui';

// Recommended: type-safe via defineMessage (shared with the runtime side)
const GenerateImage = defineMessage<{ prompt: string }, { imageTempPath: string }>('generate-image');
const { imageTempPath } = await GenerateImage.invoke({ prompt: 'a cat' }, { timeoutMs: 60_000 });

// Or bare interface — returns TResp directly (no { response } wrapper)
const { imageTempPath: path2 } = await clipbus.runtime.invoke<{ imageTempPath: string }>({
  key: 'generate-image',
  payload: { prompt: 'a cat' },
  timeoutMs: 60_000,
});

clipbus.action (action context only)

Namespace of verbs and sub-topics scoped to the action WebView. clipbus.action itself is not a Topic — it's a grouping. The mutation verbs below require the WebView to be in action context (clipbus.pluginContext.current()?.mode === 'action') and reject otherwise with PluginContextError.

| Member | Shape | Description | |---|---|---| | clipbus.action.setButtons({buttons}) | Verb | Replace native action button list (whole-list replace; first call overrides resolveSession seed). Strict wire shape — pass {buttons: [...]}. | | clipbus.action.complete({result, userMessage?}) | Verb | Submit draft action result to host; triggers phase transition to .success | | clipbus.action.onHostInvoke(fn) | Stream | Subscribe to host-dispatched button clicks; callback receives { buttonID } |

clipbus.action.draft — OptionalTopic<Record<string, unknown>>

Draft is read-only from the UI. The host pushes initialDraft (returned by resolveSession) as the topic value; the UI keeps form state locally and submits the final result via clipbus.action.complete(...).

| Member | Shape | Description | |---|---|---| | clipbus.action.draft.current() | () → Record<string, unknown> \| undefined | Current draft state pushed by host. Cast to your draft type at the call site. | | clipbus.action.draft.on(fn) | (fn) → Unsubscribe | Subscribe to draft updates (rare — initialDraft is usually all the UI consumes). |

There is no clipbus.action.draft.update(...) or clipbus.action.draft.getDraftSnapshot(). To run runtime-side logic from the UI mid-edit (allocate image temp path, fetch external data), use clipbus.runtime.invoke(...) against your own messageHandlers.

PluginContextError

Context-bound verbs (e.g. clipbus.action.complete, clipbus.action.setButtons, clipbus.attachmentRenderer.setButtons) reject with PluginContextError when called from the wrong WebView pane:

import { clipbus, PluginContextError } from '@clipbus/plugin-sdk/ui';

try {
  await clipbus.action.complete({ result: { resultKind: 'none' } });
} catch (e) {
  if ((e as Error).name === 'PluginContextError') {
    // current WebView is not an action context
  }
}

clipbus.window

| Member | Shape | Description | |---|---|---| | clipbus.window.setHeight({height}) | Verb | Report current WebView height (integer pixels). Strict wire shape — pass {height: number}, not a bare number. | | clipbus.window.autoFit() | Verb | Tell the host to enter auto-fit mode for this WebView. Bounds come from manifest.attachmentRenderers[].height ("auto" or {min, max}). No options. |

For convenience, @clipbus/plugin-sdk/dom also exports an autoFit({min, max, target}) helper that wires up a ResizeObserver and calls clipbus.window.setHeight(...) on every layout change — see the DOM utilities section above.


clipbus.infoPanel — open the native info panel (any context)

Opens Clipbus's native detail panel from a plugin, rendered from a structured document (rich text / code / QR / key-value fields / tags / actions). Not permission-gated. Usable from both attachmentRenderer and action WebViews.

| Member | Shape | Description | |---|---|---| | clipbus.infoPanel.open({document}) | Verb → {panelID: string} | Open a panel; returns a panelID that keys the streams below. | | clipbus.infoPanel.close({panelID}) | Verb | Close a panel this plugin opened. No-op for an unknown / not-owned panelID. | | clipbus.infoPanel.onAction.on(fn) | Stream | Subscribe to button clicks; callback receives { panelID, buttonID }. | | clipbus.infoPanel.onClose.on(fn) | Stream | Fires once when a panel closes (user-dismissed or close()); receives { panelID }. |

document shape{ icon?, title, subtitle?, badges?: string[], blocks: Block[], actions: Action[] }. Block is a discriminated union on type:

| type | Fields | |---|---| | label | { text } | | text | { text, style: 'body' \| 'mono' \| 'muted' } | | code | { text, language: 'plain' \| 'json' } | | qrCode | { value } | | fields | { fields: { key, value, isMono? }[] } | | tags | { tags: string[] } | | divider | {} |

Action is { id, label, isPrimary? }.

Buttons are pure-notification. The host runs no built-in effect for a plugin button — every click is delivered to infoPanel.onAction, and the plugin performs its own side effect (e.g. call clipbus.clipboard.copyText(...) / clipbus.navigation.openUrl(...) in the handler). There is no host-side copy / openURL effect and no link text style.

const { panelID } = await clipbus.infoPanel.open({
  document: {
    title: "Details",
    blocks: [
      { type: "text", text: "Tap a button below.", style: "body" },
      { type: "tags", tags: ["demo"] },
    ],
    actions: [{ id: "copy", label: "Copy", isPrimary: true }],
  },
});
clipbus.infoPanel.onAction.on(({ buttonID }) => {
  if (buttonID === "copy") clipbus.clipboard.copyText({ text: "…" });
});
clipbus.infoPanel.onClose.on(({ panelID }) => { /* clean up */ });

Shared-resource semantics. Info panels are a global resource — the host caps the total number of open panels (currently 12, shared across the app and all plugins) and reclaims the oldest non-focused window when the cap is exceeded; a reclaimed panel still fires onClose. Events and close() are scoped to the opening plugin session: onAction / onClose are delivered only to the plugin that opened the panel, and close() only affects that plugin's own panels. When a plugin's WebView is torn down its onAction / onClose subscriptions stop firing; any panel it left open stays on screen until the user dismisses it or the cap reclaims it.


clipbus.clipboard

| Member | Shape | Description | |---|---|---| | clipbus.clipboard.copyText({text}) | Verb | Copy text to system clipboard |


clipbus.navigation

| Member | Shape | Description | |---|---|---| | clipbus.navigation.openUrl({url}) | Verb | Open a URL | | clipbus.navigation.revealInFinder({path}) | Verb | Reveal file path in Finder | | clipbus.navigation.openFilePath({path}) | Verb | Open a file by path |


clipbus.settings

| Member | Shape | Description | |---|---|---| | clipbus.settings.get({key}) | Verb → {value: unknown \| null} | Read a plugin setting by key | | clipbus.settings.getAll() | Verb → {settings: Record<string, unknown>} | Read all plugin settings |


clipbus.console / clipbus.textInput / clipbus.pluginContext

| Member | Shape | Description | |---|---|---| | clipbus.console.log({level, message}) | Verb | Write a log line to the host log (level: 'debug' \| 'info' \| 'warn' \| 'error') | | clipbus.textInput.stateChanged({isFocused, isComposing}) | Verb | Notify host that a text input's focus / IME state changed | | clipbus.pluginContext.current() | Topic | { mode: 'attachmentRenderer' \| 'action', pluginID } — read which kind of WebView this code is running in | | clipbus.pluginContext.on(fn) | Topic | Subscribe (rare; context doesn't normally change after bootstrap) |

Strict wire shape: every verb takes a single object parameter ({text}, {url}, etc.), never a bare value. The single source of truth for every payload / response shape is API.md, generated from protocol/plugin/src/catalog.ts.


Types

All public types live in the generated data.generated.ts / capabilityClients.generated.ts and carry the Plugin prefix. The pre-plugin-api-shrink aliases without the prefix have been removed.

// Data types — re-exported from both /runtime and /ui
import type {
  PluginClipboardItem,
  PluginAttachmentPayload,
  PluginAttachmentEntry,
  PluginAttachmentRef,
  PluginAttachmentMutationEntry,
  PluginSearchExtensionEntry,
  PluginContentEnvelope,           // discriminated union: text | image | path_reference
  PluginPathEntry,                 // { kind: 'file' | 'folder', path, displayName }
  PluginDetectorArtifact,
  PluginDetectorInput,
  PluginResolveAttachmentInput,
  PluginAttachmentResolveResult,
  PluginResolveActionSessionInput,
  PluginActionResolveResult,
  PluginAutoRunActionInput,
  PluginActionOperationResult,
  PluginActionResult,              // discriminated union: text | image | none
  PluginActionButton,
  PluginThemeTokens,
  PluginThemeTokenSnapshot,
  PluginConsoleLogLevel,
} from '@clipbus/plugin-sdk/runtime';

// Handler interfaces — runtime side only
import type {
  PluginDetectorHandler,
  PluginAttachmentRendererHandler,
  PluginAutoRunActionHandler,      // covers both auto-run and draft lifecycles
} from '@clipbus/plugin-sdk/runtime';

// definePlugin types — runtime side only
import type {
  PluginRegistry,
  MessageHandler,
  MessageHandlerContext,
} from '@clipbus/plugin-sdk/runtime';

For the exhaustive type list, run cd protocol/plugin && npm run codegen and read the synced data.generated.ts next to this file.

Manifest validation (@clipbus/plugin-sdk/validate)

The ./validate entry point provides a manifest validator that stays aligned with the host's manifest validation rules. Use it in CI or npm run verify to catch errors at development time — the same errors the host reports at install time.

const { validateManifest, MANIFEST_VALIDATION_CODES } = require('@clipbus/plugin-sdk/validate');

const manifest = JSON.parse(require('fs').readFileSync('manifest.json', 'utf8'));
const issues = validateManifest(manifest, { phase: 'manifestOnly' });
// issues: Array<{ code: string; message: string }>

validateManifest(manifest, options?)

| Parameter | Type | Description | |---|---|---| | manifest | unknown | Parsed manifest.json content (JSON.parse output). | | options.phase | 'manifestOnly' \| 'runtimeLoadable' | 'manifestOnly' (default): pure JSON rules only. 'runtimeLoadable': also checks file existence and path containment; requires rootDir. | | options.rootDir | string | Absolute path to the plugin root. Required for 'runtimeLoadable'. |

Returns ManifestValidationIssue[]. An empty array means the manifest is valid.

Unlike the host validator (which stops at the first error), this function collects all issues in a single pass — better DX for development.

MANIFEST_VALIDATION_CODES

readonly string[] — all 26 error code strings, each a verbatim camelCase name that matches the corresponding host error. Use for exhaustive handling or parity checks.

CLI: clipbus-validate-manifest

Installed automatically when @clipbus/plugin-sdk is a dependency:

npx clipbus-validate-manifest [pluginDir] [--runtime]
  • pluginDir — path to the plugin directory containing manifest.json (default: cwd).
  • --runtime — use runtimeLoadable phase (checks file existence in addition to JSON rules).

Exits 0 on success, 1 if any issues are found.


Preview harness (@clipbus/plugin-sdk/preview)

Framework-neutral development-time workbench for iterating on plugin UI without a real macOS host process. Provides wire injection (host → plugin bootstrap), a bidirectional fake host (plugin → host calls go to a call log panel), and viewport height tracking via clipbus.window.setHeight.

import { createPreviewWorkbench } from '@clipbus/plugin-sdk/preview';

createPreviewWorkbench(document.getElementById('app')!, {
  scenarios,
  mount(slotEl, { scenario }) {
    const app = createApp(MyPlugin);
    app.mount(slotEl);
    return () => app.unmount();
  },
});

See docs/preview.md for PreviewScenario / PreviewViewport field reference and full harness API.


See also

  • API.md — autoritative API reference, regenerated from protocol/plugin/src/catalog.ts
  • SPECIFICATION.md — API shape rules, mirror table, naming conventions, PR checklist