Kunkun
Building Extensions

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.

ModeRuntimeNode.jsUse case
worker-headlessWeb WorkerNoSimple background work, no filesystem
node-headlessNode.js / DenoYesSystem 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

  1. Keep runs fast and idempotent.
  2. Give feedback with showToast when it makes sense.
  3. Catch and report errors — don't let a scheduled task fail silently.
  4. Persist counters/caches with LocalStorage or record storage.

Real examples

  • sample-headless-node — cron-style Node task
  • sample-headless-workerclipboard:change-triggered worker

On this page