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

@lovision/plugin-sdk

v1.1.0

Published

Worker-side SDK for Instinct plugins.

Readme

@lovision/plugin-sdk

Worker-side SDK for Instinct plugins.

Published from the main branch through npm Trusted Publishing.

This package is the intentional public contract for plugin code that runs inside the worker sandbox. It does not include local dev/build/scaffold tooling, editor UI, install confirmation, or diagnostics panels.

Companion guides

Step 14 also ships task-first docs for external plugin authors under:

Public surface

@lovision/plugin-sdk exports one runtime value and a small set of typing helpers:

  • definePlugin(...)
  • PluginContext
  • DefinePluginOptions
  • CommandInvocation
  • conflict / event / transactional-snapshot types that plugin code may want to reference explicitly

Internal runtime classes (SdkRuntime, createContext, facade classes, test fixtures) are intentionally not part of the package root.

Supported worker API

PluginContext exposes:

  • ctx.call<T>(method, params, opts?) — raw host capability invoke
  • ctx.handle(method, handler) — worker-side responder registration
  • ctx.selection.{get,set,clear,count}
  • ctx.nodes.{update,create,remove,move,resize,rename}
  • ctx.document.{snapshot,snapshotDiff,transactionalSnapshot}
  • ctx.events.on("documentchange" | "selectionchange", handler)
  • ctx.viewport.{get,scrollToSelection,focusNode}
  • ctx.storage.local
  • ctx.storage.document
  • ctx.storage.node(nodeId)
  • ctx.notify.send(message, options?)
  • ctx.closePlugin(message?)

All typed wrappers still sit on top of host-owned permissions and runtime checks. Declaring a manifest permission makes the call legal; omitting it causes a PluginError(code: "PERMISSION_DENIED").

Entry shapes

definePlugin accepts two callbacks; either or both can be provided.

command

When PluginManager.invoke(pluginId, commandId, params) runs, the host sends an internal __runCommand RPC. The SDK auto-registers a handler that dispatches to your command callback:

import { definePlugin } from "@lovision/plugin-sdk";

definePlugin({
  command: async (ctx, { id, params }) => {
    const root = await ctx.call("document.snapshot", undefined);
    return { processed: id, params };
  },
});

run

run fires once when the worker boots. Use it to register ctx.handle responders that should exist before the host starts dispatching commands:

definePlugin({
  run: (ctx) => {
    ctx.handle("ping", () => ({ pong: true }));
  },
});

If you provide run but not command, the SDK still wires __runCommand and falls back to invoking run(ctx) again — handy for single-command plugins where setup is the command body.

Typed wrappers

PluginContext exposes typed wrappers over the host facade:

definePlugin({
  command: async (ctx) => {
    const ids = await ctx.selection.get();        // string[]
    if (ids.length === 0) {
      await ctx.notify.send("Nothing selected", { kind: "warning" });
      await ctx.closePlugin();
      return;
    }
    const result = await ctx.nodes.update(
      ids.map((id) => ({ id, changes: { opacity: 0.5 } })),
    );
    await ctx.storage.document.set("clean-selection:last-run", {
      count: ids.length,
      at: 1_713_568_000_000,
    });
    await ctx.notify.send(`Updated ${ids.length} node(s) (v${result.newVersion})`);
    await ctx.closePlugin("done");
  },
});

| API | Permission | Notes | | --- | --- | --- | | ctx.selection.get() | selection:read | returns the host editor's current selection | | ctx.selection.set(ids) / clear() / count() | selection:write or selection:read | selection wrappers | | ctx.nodes.update(updates, opts?) | document:write.style | supports expectedVersion and Step 6 conflict handling | | ctx.nodes.create/remove/move/resize/rename | matching document:write.* scope | structural and text/layout mutations | | ctx.document.snapshot() | document:read | frozen SceneSnapshot | | ctx.document.snapshotDiff(fromVersion) | document:read | snapshot-native diff (upsert / remove) | | ctx.document.transactionalSnapshot() | document:read plus matching write scope when flushed | SDK-local writable proxy over document.snapshot() | | ctx.events.on(name, handler) | document:read or selection:read | documentchange / selectionchange | | ctx.viewport.get() / scrollToSelection() / focusNode(nodeId) | viewport:read / viewport:write | best-effort editor navigation | | ctx.storage.local | storage:local | plugin-private local KV | | ctx.storage.document | storage:document | document-scoped plugin KV | | ctx.storage.node(nodeId) | storage:node | node-scoped plugin KV | | ctx.notify.send(msg, options?) | notify | host notification | | ctx.closePlugin(message?) | none | closes the current invocation |

Calls without the requisite manifest permission reject with PluginError(code: "PERMISSION_DENIED", data: { method, required, declared, missing }). WRITE_CONFLICT rejects carry data: { currentVersion, expectedVersion, conflictingNodes?, missing? }. Storage values are limited to JSONValue (null | boolean | number | string | JSONValue[] | { [k]: JSONValue }).

Transactional writes and events

snapshotDiff and events.on(...) are meant to work together:

const snap = await ctx.document.snapshot();
const off = await ctx.events.on("documentchange", async (event) => {
  const diff = await ctx.document.snapshotDiff(event.fromVersion);
  console.log(diff.changedNodeIds);
});
  • ctx.events.on("documentchange", handler) requires document:read
  • ctx.events.on("selectionchange", handler) requires selection:read
  • the SDK auto-unsubscribes any remaining listeners when the current command invocation ends

ctx.document.transactionalSnapshot() is the read-then-write path for Figma-like plugins:

const tx = await ctx.document.transactionalSnapshot();
tx.root.children[0].opacity = 1;
tx.root.children[0].name = "Hero";
await tx.flush({ onConflict: "retry" });
  • reads see pending writes before flush
  • pendingPatch() exposes the unflushed NodeUpdate[]
  • cancel() discards only the current pending patch
  • unflushed transactions auto-flush on normal command return and before ctx.closePlugin()
  • uncaught command errors do not auto-flush

ctx.nodes.update(...) also accepts onConflict: "abort" | "retry" | handler for direct patch-based flows.

Diagnostics and Phase 1c runtime semantics

Step 13 added host-owned diagnostics semantics that plugin authors need to know even though the UI does not live in this package:

  • network:fetch is still manifest-gated and allowlist-gated. Declaring the permission alone is not enough; the host also enforces the allowed domains.
  • network denied row summaries show the rejected host plus a short trace ID.
  • the editor Logs surface keeps the full trace, URL, and payload.
  • diagnostics are lane-owned by the host:
    • formal-install issues appear on Installed/MainMenu-facing rows
    • development overlay issues stay on the Development row
    • one lane clearing does not clear the other

ESLint subpath

The package ships a minimal plugin-facing ESLint flat config at @lovision/plugin-sdk/eslint-config.

import pluginSdkLint from "@lovision/plugin-sdk/eslint-config";

export default [...pluginSdkLint];

Today that config intentionally only enables one high-value warning:

  • no-await-in-loop

The goal is to catch the most common worker-side anti-pattern early. If the warning fires, restructure the loop into staged concurrency (Promise.all, batched requests, or a serial loop that no longer awaits inside the iterator body).

Explicit non-goals

  • no local HTTPS dev server
  • no bundle builder or manifest validator
  • no create-instinct-plugin scaffold
  • no editor menu / dev panel / install confirmation UI
  • no public fixture modules

Those surfaces are intentionally outside @lovision/plugin-sdk so the worker contract can stay small and stable.