Headless Commands
Background commands without UI — triggered, scheduled, or event-driven.
Headless commands run logic without a UI: they're triggered manually, on a schedule, or by an event, do their work, optionally show a toast, and exit.
| Mode | Runtime | Node.js | Use case |
|---|---|---|---|
worker-headless | Web Worker | No | Simple background work, no filesystem |
node-headless | Node.js / Deno | Yes | System operations, filesystem, external APIs |
The lifecycle entry
Register your command with startKunkunHeadlessPlugin. The host drives init → onTrigger → destroy:
// src/index.ts
import { startKunkunHeadlessPlugin, type TriggerContext } from "@kunkunsh/sdk/runtime";
import { showToast, Toast, LocalStorage } from "@kunkunsh/sdk";
startKunkunHeadlessPlugin({
async init() {
// one-time setup: load preferences, open connections
},
async onTrigger(context: TriggerContext) {
const raw = await LocalStorage.getItem("count");
const count = (parseInt(raw ?? "0", 10) || 0) + 1;
await LocalStorage.setItem("count", String(count));
await showToast({ title: "Ran", message: `#${count}`, style: Toast.Style.Success });
},
async destroy() {
// cleanup: flush, close connections
},
});context describes why the command fired (manual launch, schedule tick, or a subscribed event).
Build
// build.ts — worker-headless
import { kunkunCommandPlugin } from "@kunkunsh/sdk/build";
import type { BunPlugin } from "bun";
await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
target: "browser", format: "esm", minify: true,
plugins: [kunkunCommandPlugin({ mode: "no-view" }) as BunPlugin],
});For node-headless, use target: "node" and outdir: "./dist/node".
Tarball installs don't hydrate node_modules — bundle JS deps into the output. Native .node addons: package the loader + platform files, or lazy-load them.
Manifest
{
"name": "sync",
"title": "Sync Now",
"mode": "node-headless",
"main": "dist/node/index.js",
"permissions": [{ "permission": "fs-read", "allow": ["$HOME/**"] }, "notifications"]
}Scheduling & triggers
Headless commands can run without a user launching them:
{ "name": "poll", "mode": "node-headless", "main": "dist/node/index.js", "interval": "5m" }{ "name": "nightly", "mode": "node-headless", "main": "dist/node/index.js", "cron": "0 3 * * *" }interval—"30s" | "5m" | "1h" | "1d"or seconds as a number.cron— standard cron expression.
You can also subscribe to system events (e.g. clipboard:change) inside a headless worker via the events API; the sample-headless-worker extension demonstrates this.
Node access
In node-headless you have fs, shell, path, dialog, and network:
import { fs, shell, path } from "@kunkunsh/sdk";
const home = await path.homeDir();
const entries = await fs.readDir(home);
const { stdout } = await shell.execute("echo", ["hi"]);Runtime selection
node-headless runs on Deno → Node v20+ → utilityProcess (auto). Force it:
{ "name": "deno-only", "mode": "node-headless", "main": "dist/node/index.js", "runtime": "deno" }Best practices
- Keep runs fast and idempotent.
- Give feedback with
showToastwhen it makes sense. - Catch and report errors — don't let a scheduled task fail silently.
- Persist counters/caches with
LocalStorageor record storage.
Real examples
sample-headless-node— cron-style Node tasksample-headless-worker—clipboard:change-triggered worker