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

@duobox/extension-kit

v0.3.1

Published

Public SDK for building Duobox extensions.

Readme

@duobox/extension-kit

Public SDK for building Duobox extensions.

Use this package inside an extension project to describe extension metadata, implement main-process capabilities, and register renderer UI into Duobox-provided slots.

pnpm add -D @duobox/extension-kit @duobox/cli

Mental model

A Duobox extension owns its own UI and behavior. The Duobox app should stay generic: it exposes runtime APIs and UI slots, while the extension declares how it wants to appear.

Renderer development is intentionally direct:

  • where: the app UI slot, such as doc.menu.export or extension.mainview
  • what: the component or operation the extension contributes
  • how: the presentation, such as dialog, run, or mainview

The renderer should make that shape obvious at the top level:

import { defineRenderer } from "@duobox/extension-kit/react";
import { FeishuMainview } from "./FeishuMainview";
import { PublishDialog } from "./PublishDialog";

export default defineRenderer(function render({ ui }) {
  ui.doc.menu
    .export({
      title: "Publish to Feishu",
      icon: "send",
    })
    .dialog(PublishDialog);

  ui.extension.mainview({
    id: "main",
    title: "Feishu",
    component: FeishuMainview,
  });
});

Do not move extension-specific dialogs, panels, menus, or business logic into the Duobox app. If a Feishu extension needs a publish dialog, that dialog belongs to the Feishu extension package.

Installation and runtime activation are separate. Publishing or installing an extension only makes it available on disk; the user must start it before Duobox loads its main process, renderer UI, actions, or optional agent plugin. Stopping an extension keeps it installed but removes its UI slots, actions, and agent plugin from the host.

Package entrypoints

import { ExtensionMain } from "@duobox/extension-kit";
import type { ExtensionManifest } from "@duobox/extension-kit";

import { ExtensionMain as MainBase } from "@duobox/extension-kit/main";

import { defineRenderer } from "@duobox/extension-kit/react";
import type { ExtensionRendererContext } from "@duobox/extension-kit/react";

The root entrypoint re-exports the main runtime types and renderer types. Subpath exports are available when you want clearer import boundaries. Duobox extensions are trusted local code, so the manifest does not declare a permissions model.

0.2.0 replaces the former string-based this.app.invoke(...) API with the structured AppBridge API. Extensions targeting earlier SDK versions must be migrated before upgrading.

Suggested extension layout

my-extension/
  manifest.json
  package.json
  main/
    index.ts
  renderer/
    index.tsx
    PublishDialog.tsx
    FeishuMainview.tsx
  assets/
    icon.svg

The Duobox CLI builds this layout into dist/, extracts renderer UI contributions into dist/renderer/ui.json, and packs the extension into a zip archive.

manifest.json

An extension requires at least id, name, and main.

{
  "$schema": "./node_modules/@duobox/extension-kit/manifest.schema.json",
  "id": "duobox.feishu",
  "name": "Feishu",
  "description": "Publish Duobox documents to Feishu.",
  "icon": "./assets/icon.svg",
  "main": "./dist/main/index.js",
  "renderer": "./dist/renderer/index.js",
  "activationEvents": ["onStartup"],
  "engines": {
    "duobox": ">=1.1.0"
  },
  "extensionApiVersion": 1
}

During development, keep source files in main/ and renderer/. The CLI rewrites release entries to the built dist/ files when packing.

The CLI and Duobox App use the same validator exported from @duobox/extension-kit/manifest. Removed fields such as permissions and agentPlugin are rejected consistently during validation, build, and install. The published JSON Schema is available as @duobox/extension-kit/manifest.schema.json for editor completion and Coding Agent inspection. The runtime validator remains authoritative.

Main runtime

The main runtime is for extension capabilities that need host APIs, long-running work, network calls, or document operations.

import { ExtensionMain } from "@duobox/extension-kit";

export default class FeishuMain extends ExtensionMain {
  async activate() {
    this.logger.info("Feishu extension activated");

    this.extension.action({
      id: "feishu.publish",
      title: "Publish to Feishu",
      kind: "sync",
      async run(input) {
        return { ok: true, input };
      },
    });
  }

  deactivate() {
    this.logger.info("Feishu extension deactivated");
  }
}

AppBridge

this.app is a structured AppBridge for typed access to app capabilities:

const doc = await this.app.files.read({
  id: "document-id",
});

await this.app.system.clipboard.writeText({
  text: doc.content,
});

await this.app.system.clipboard.writeHtml({
  html: "<strong>Rich text</strong>",
  text: "Rich text",
});

await this.app.system.openExternal({
  url: "https://example.com",
});

Current AppBridge capability groups include:

  • workspace.get and workspace.tree
  • files.read and files.write
  • assets.resolveLocalPath
  • system.openExternal and system.showItemInFolder
  • system.clipboard.writeText and system.clipboard.writeHtml
  • log.debug / info / warn / error

Renderer runtime

The renderer entry must export a default render function created with defineRenderer.

import { defineRenderer } from "@duobox/extension-kit/react";

export function PublishDialog() {
  return <div>Publish UI</div>;
}

export function FeishuMainview() {
  return <div>Feishu UI</div>;
}

export default defineRenderer(function render({ ui }) {
  ui.doc.menu.export({ title: "Publish" }).dialog(PublishDialog);
  ui.extension.mainview({
    id: "main",
    title: "Feishu",
    component: FeishuMainview,
  });
  ui.command.palette({ title: "Publish" }).run("feishu.publish");
});

Components passed to slots must be either:

  • a string component reference supported by the host, or
  • an exported function component from the renderer entry module

The CLI validates this during duobox extension build.

Supported UI slots

ui.doc.menu.root/export/share(meta).dialog(Component)

Adds an extension item to the current document menu. root, export, and share describe the action semantics and ordering group. Duobox currently shows all three groups inside the document menu's top-level Extensions submenu. Dialog components receive the current doc context.

ui.doc.menu
  .export({
    id: "publish-feishu",
    title: "Publish to Feishu",
    icon: "send",
    group: "publish",
    order: 20,
  })
  .dialog(PublishDialog);

ui.doc.menu.*(meta).run(actionId)

Runs a main-process action and injects the current document as { doc }.

ui.doc.menu
  .share({ id: "copy-link", title: "Copy share link" })
  .run("share.copyLink");

The injected document context is JSON-serializable:

type ExtensionDocumentContext = {
  id: string;
  title: string;
  format: string;
  metadata: Record<string, unknown>;
};

Use a dialog or mainview component when the workflow needs additional dynamic input, then call extension.invoke(actionId, { docId: doc.id, ...params }) explicitly.

ui.extension.mainview(input)

Declares the extension-owned page. Every renderer must declare exactly one mainview. Duobox automatically adds it to the Customize list, and document menu items can open the same page by id and pass the current document context.

ui.extension.mainview({
  id: "main",
  title: "Feishu",
  component: FeishuMainview,
});
ui.doc.menu.root({ title: "Feishu" }).mainview("main");

Mainview components should use the host-provided semantic components instead of recreating page padding, surfaces, controls, and responsive layout:

import type { ExtensionMainviewProps } from "@duobox/extension-kit/react";

export function FeishuMainview({ ui }: ExtensionMainviewProps) {
  const { Button, Group, Page, Row, Toolbar } = ui.components;

  return (
    <Page>
      <Toolbar title="Publishing" />
      <Group>
        <Row
          label="Destination"
          description="Choose where documents are published"
          trailing={<Button>Connect</Button>}
        />
      </Group>
    </Page>
  );
}

The current component contract includes:

  • layout: Page, Section, Group, Row, Stack, Inline, SplitPane;
  • navigation and actions: Toolbar, Tabs, Button, IconButton;
  • collections: List, ListItem;
  • content and state: Text, Badge, Status, Callout, EmptyState, LoadingState, Code, CodeBlock;
  • controls: Input.

The host owns the mainview outer padding, scrolling, theme, and Overview. Extensions own their business hierarchy and compose it from ui.components. Do not import private App or Settings components, set page-level padding, or reimplement shared controls with a mainview-wide <style> block.

ui.command.palette(meta).run(actionId)

Adds an extension command to the global command palette. The action id must be registered by the extension main runtime with this.extension.action(...).

ui.command
  .palette({
    id: "publish-feishu-command",
    title: "Publish to Feishu",
    group: "publish",
    keywords: ["publish", "feishu"],
  })
  .run("feishu.publish");

Type reference

Commonly used types:

  • ExtensionMain
  • ExtensionManifest
  • ExtensionActivationEvent
  • ExtensionAction
  • ExtensionDocumentContext
  • AppBridge
  • ExtensionRendererContext
  • ExtensionRendererUi
  • ExtensionMainviewProps
  • ExtensionRendererUI
  • ExtensionUIComponents
  • ExtensionRendererContribution
  • ExtensionUiWhere

Build with the CLI

Initialize a new extension directly:

pnpm dlx @duobox/cli extension init my-extension

Inside an initialized extension, run:

pnpm install
pnpm run validate
pnpm run typecheck
pnpm run build

See @duobox/cli for command details.