Kunkun

Getting Started

Build, build-output, and load your first Kunkun extension.

This guide gets a minimal extension running. If you're deciding which type to build, read Extension Types first.

Prerequisites

  • Node.js 18+ or Bun (Bun is used for worker/headless builds)
  • TypeScript basics; React for worker/node views, or any static-SPA framework for custom views
  • Kunkun installed (desktop app)

Anatomy of an extension

Every extension is an npm package whose package.json carries a kunkun manifest field:

{
  "name": "my-extension",
  "version": "0.1.0",
  "license": "MIT",
  "$schema": "https://schema.kunkun.sh/",
  "kunkun": {
    "identifier": "com.you.my-extension",
    "name": "My Extension",
    "permissions": ["clipboard-read"],
    "commands": [ /* one or more commands, each with a `mode` */ ]
  }
}

The identifier is your stable, globally-unique id. Each command declares a mode (custom-view, worker-view, node-headless, …) and an entry main. See the Manifest Reference for every field — there's an interactive validator there too.

Extensions install as built artifacts

Kunkun installs an extension tarball as-is — it does not run npm install inside the installed folder. Your build must be self-contained: bundle your JS dependencies into the command/service outputs. Native .node addons can't be inlined; package their loader + platform files alongside the build, or lazy-load them behind an optional feature.

A typical extension project looks like this:

package.json
build.ts

The exact layout varies by extension type — see each building guide for the specifics. To scaffold a custom-view project, reach for Vite — bun create vite my-extension --template react-ts (or vue-ts / svelte-ts / vanilla-ts; any framework that builds to a static frontend works).

Quick start: a Custom-View extension

A custom view is a static SPA loaded into an isolated window over the kunkun-ext:// protocol. Use any framework that builds to a static frontend (SPA or SSG). We use React below because it's the most widely known — the other tabs show the same steps for Vue, Svelte, and vanilla.

Create the project

bun create vite my-extension --template react-ts
cd my-extension
pnpm add @kunkunsh/sdk
bun create vite my-extension --template vue-ts
cd my-extension
pnpm add @kunkunsh/sdk
bun create vite my-extension --template svelte-ts
cd my-extension
pnpm add @kunkunsh/sdk
bun create vite my-extension --template vanilla-ts
cd my-extension
pnpm add @kunkunsh/sdk

npm create vite@latest / pnpm create vite work the same way if you prefer.

Configure a static build

Kunkun serves your build from the extension's own origin, so assets must be referenced relative. In vite.config.ts, set base: './':

import { defineConfig } from 'vite'
// ...framework plugin
export default defineConfig({
  base: './',            // required: load assets relative to kunkun-ext://
  // plugins: [react()]  // or vue() / svelte()
})

Using SvelteKit or another meta-framework instead of plain Vite? Build a static SPA with @sveltejs/adapter-static (ssr = false, hash routing). See the Custom UI Extension guide.

Add the manifest

Vite already generated a package.json. Add the $schema and kunkun fields to it — the manifest is the kunkun field, not a separate file:

package.json
{
  "name": "my-extension",
  "version": "0.1.0",
  // ...the scripts/dependencies Vite generated
  "$schema": "https://schema.kunkun.sh/",
  "kunkun": {
    "identifier": "com.you.my-extension",
    "name": "My Extension",
    "icon": { "type": "iconify", "value": "mdi:puzzle", "invert": true },
    "shortDescription": "My first Kunkun extension",
    "permissions": ["clipboard-read", "clipboard-write", "storage"],
    "commands": [
      {
        "name": "main",
        "title": "My Command",
        "mode": "custom-view",
        "main": "/",
        "dist": "dist",
        "devMain": "http://localhost:5173"
      }
    ]
  }
}

Use the SDK

First wire the host RPC by importing @kunkunsh/sdk/ui/custom once in your entry file, before the app mounts:

// src/main.tsx (React) / src/main.ts (Vue, Svelte, vanilla)
import "@kunkunsh/sdk/ui/custom"; // must come first — wires the extension ↔ host channel
// ...your normal app bootstrap (createRoot(...).render(...), mount(App), etc.)

Then call the friendly APIs from anywhere in your components:

import { useState } from "react";
import { Clipboard, showToast, Toast } from "@kunkunsh/sdk";

export default function App() {
  const [text, setText] = useState("");
  return (
    <>
      <button onClick={async () => {
        setText(await Clipboard.readText());
        await showToast({ title: "Read!", style: Toast.Style.Success });
      }}>Read clipboard</button>
      <p>{text}</p>
    </>
  );
}
<script setup lang="ts">
import { ref } from "vue";
import { Clipboard, showToast, Toast } from "@kunkunsh/sdk";

const text = ref("");
async function read() {
  text.value = await Clipboard.readText();
  await showToast({ title: "Read!", style: Toast.Style.Success });
}
</script>

<template>
  <button @click="read">Read clipboard</button>
  <p>{{ text }}</p>
</template>
<script lang="ts">
  import { Clipboard, showToast, Toast } from '@kunkunsh/sdk'
  let text = $state('')
  async function read() {
    text = await Clipboard.readText()
    await showToast({ title: 'Read!', style: Toast.Style.Success })
  }
</script>

<button onclick={read}>Read clipboard</button>
<p>{text}</p>

Build

pnpm build   # outputs the static SPA into `dist/`

Quick start: a Worker-View extension

A worker view runs a React component in a Web Worker; the host renders the UI from a component tree, giving consistent styling and strong isolation.

Scaffold

mkdir my-worker-ext && cd my-worker-ext
pnpm init && pnpm add @kunkunsh/sdk react valibot
pnpm add -D @types/react @types/bun

Component — src/App.tsx

import { useState } from "react";
import { Button, Div, H2, P } from "@kunkunsh/sdk/ui";
import { Clipboard, showToast, Toast } from "@kunkunsh/sdk";

export default function App() {
  const [text, setText] = useState("");
  return (
    <Div className="p-6">
      <H2>My Worker Extension</H2>
      <Button title="Read Clipboard" variant="primary" onClick={async () => {
        setText(await Clipboard.readText());
        await showToast({ title: "Read!", style: Toast.Style.Success });
      }} />
      <P>Content: {text}</P>
    </Div>
  );
}

Build script — build.ts

import { dedupeReact, kunkunCommandPlugin } from "@kunkunsh/sdk/build";
import type { BunPlugin } from "bun";

await Bun.build({
  entrypoints: ["./src/App.tsx"],
  outdir: "./dist",
  target: "browser",
  format: "esm",
  minify: true,
  plugins: [kunkunCommandPlugin({ mode: "view" }) as BunPlugin, dedupeReact(import.meta.dir)],
});

Manifest + build

{
  "kunkun": {
    "identifier": "com.you.my-worker-ext",
    "name": "My Worker Extension",
    "permissions": ["clipboard-read", "storage"],
    "commands": [{ "name": "main", "title": "My Command", "mode": "worker-view", "main": "dist/App.js" }]
  },
  "scripts": { "build": "bun build.ts" }
}
bun run build

Load it into Kunkun

  1. Open Kunkun → Settings → Developer
  2. Load Extension → select your extension directory
  3. For custom views, run your dev server (pnpm dev) and Kunkun will load devMain with HMR.

Publish to npm (or jsr) and users install it from the built-in extension store.

Explore real examples

The extensions/ folder in the repo has working examples ranging from trivial to advanced:

  • uuid-generator — the simplest possible no-view command
  • sample-headless-node / sample-headless-worker — cron and event-triggered background tasks
  • ffmpeg + video-processing — a service provider and its consumer
  • kkterminal / space-lens — full CLI + plugin case studies

Next steps

New to the architecture? Read Concepts → Architecture to understand how your code actually runs, then How Extensions Run for the host↔plugin RPC model.

On this page