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:
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/sdkbun create vite my-extension --template vue-ts
cd my-extension
pnpm add @kunkunsh/sdkbun create vite my-extension --template svelte-ts
cd my-extension
pnpm add @kunkunsh/sdkbun create vite my-extension --template vanilla-ts
cd my-extension
pnpm add @kunkunsh/sdknpm 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:
{
"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/bunComponent — 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 buildLoad it into Kunkun
- Open Kunkun → Settings → Developer
- Load Extension → select your extension directory
- For custom views, run your dev server (
pnpm dev) and Kunkun will loaddevMainwith 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 commandsample-headless-node/sample-headless-worker— cron and event-triggered background tasksffmpeg+video-processing— a service provider and its consumerkkterminal/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.