KB LabsDocs

Your First Plugin

Last updated April 7, 2026


Build and install a minimal KB Labs plugin from scratch in 15 minutes.

This guide walks through building a minimal plugin end-to-end: package layout, manifest, handler, build, link, run. By the end you'll have a working CLI command registered with your KB Labs workspace.

If you want a shorter version focused on just the code, see SDK → Quickstart. This guide is the full flow including the tooling setup.

Prerequisites

  • A running KB Labs workspace (pnpm kb --help works).
  • Node 20+ and pnpm installed.
  • 15 minutes.

What we're building

A plugin called @example/hello-plugin with one CLI command: pnpm kb hello:greet. It takes a --name flag and prints a greeting. If the user passes --ai, it uses the configured LLM to generate a more interesting greeting instead.

It's intentionally tiny — big enough to cover every piece of the plugin lifecycle, small enough to fit in one guide.

Step 1 — Create the package

Start from scratch in a new directory:

Bash
mkdir -p packages/hello-plugin/src/cli/commands
cd packages/hello-plugin
pnpm init

Replace the generated package.json with:

JSON
{
  "name": "@example/hello-plugin",
  "version": "0.1.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch",
    "type-check": "tsc --noEmit"
  },
  "dependencies": {
    "@kb-labs/sdk": "^1.5.0"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.5.0"
  }
}

The only runtime dependency is @kb-labs/sdk. Plugins import only from the SDK — never from @kb-labs/core-platform, @kb-labs/plugin-contracts, or any other internal package. That's the golden rule.

Step 2 — TypeScript and build config

tsconfig.json:

JSON
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

tsup.config.ts:

TypeScript
import { defineConfig } from 'tsup';
 
export default defineConfig({
  entry: {
    index: 'src/index.ts',
    'cli/commands/hello': 'src/cli/commands/hello.ts',
  },
  format: ['esm'],
  dts: true,
  clean: true,
});

The two entries matter: src/index.ts builds the package root (where the manifest lives), and src/cli/commands/hello.ts builds the handler as a separate file so the manifest can reference it by path.

Step 3 — The handler

src/cli/commands/hello.ts:

TypeScript
import { defineCommand, useLLM, useLogger, type CLIInput } from '@kb-labs/sdk';
 
interface HelloFlags {
  name?: string;
  ai?: boolean;
}
 
interface HelloResult {
  greeting: string;
  source: 'deterministic' | 'llm';
}
 
export default defineCommand<unknown, CLIInput<HelloFlags>, HelloResult>({
  id: 'hello:greet',
  description: 'Say hi to someone',
  handler: {
    async execute(ctx, input) {
      const logger = useLogger();
      const name = input.flags.name ?? 'world';
 
      logger.info('hello:greet invoked', { name, ai: input.flags.ai });
 
      if (input.flags.ai) {
        const llm = useLLM();
        if (llm) {
          const response = await llm.complete(
            `Write a one-sentence friendly greeting for ${name}.`,
          );
          return {
            exitCode: 0,
            result: { greeting: response.content.trim(), source: 'llm' },
          };
        }
      }
 
      return {
        exitCode: 0,
        result: { greeting: `Hello, ${name}!`, source: 'deterministic' },
      };
    },
  },
});

Three things to notice:

  • Everything is imported from @kb-labs/sdk. defineCommand, useLLM, useLogger, CLIInput — all one import.
  • useLLM() may return undefined. The handler degrades gracefully: no LLM means the canned greeting.
  • Return a CommandResult. exitCode: 0 means success; result is the structured payload the CLI will render.

See SDK → Commands for the full defineCommand API.

Step 4 — The manifest

src/manifest.ts:

TypeScript
import {
  defineFlags,
  combinePermissions,
  minimalPreset,
} from '@kb-labs/sdk';
 
const helloFlags = defineFlags({
  name: {
    type: 'string',
    description: 'Name to greet',
  },
  ai: {
    type: 'boolean',
    description: 'Use LLM to generate the greeting',
    default: false,
  },
});
 
export const manifest = {
  schema: 'kb.plugin/3',
  id: '@example/hello-plugin',
  version: '0.1.0',
  display: {
    name: 'Hello Plugin',
    description: 'A minimal example plugin',
    tags: ['example', 'tutorial'],
  },
  permissions: combinePermissions()
    .with(minimalPreset)
    .withPlatform({ llm: true })
    .build(),
  cli: {
    commands: [
      {
        id: 'hello:greet',
        group: 'hello',
        describe: 'Say hi to someone',
        handler: './cli/commands/hello.js#default',
        flags: helloFlags,
        examples: [
          'pnpm kb hello:greet',
          'pnpm kb hello:greet --name=Alice',
          'pnpm kb hello:greet --name=Alice --ai',
        ],
      },
    ],
  },
} as const;
 
export default manifest;
  • schema: 'kb.plugin/3' is mandatory — a literal string.
  • id must be in @scope/name format.
  • handler path is relative to the package root and points at .js (the built output, not the source).
  • permissions uses combinePermissions() with minimalPreset (baseline Node env) plus .withPlatform({ llm: true }) to grant LLM access.

See Plugins → Manifest Reference for every field.

Step 5 — The package entry

src/index.ts:

TypeScript
export { default as manifest } from './manifest.js';

The package's root entry just re-exports the manifest. The handler files are loaded separately at runtime via the paths the manifest points at.

Step 6 — Build

Bash
pnpm install
pnpm build

You should see dist/index.js, dist/manifest.js, and dist/cli/commands/hello.js. If tsup complains about missing entries or invalid configs, check the tsup.config.ts entry map matches your source file paths.

From your KB Labs workspace root:

Bash
pnpm kb marketplace link /absolute/path/to/packages/hello-plugin
pnpm kb marketplace clear-cache

The clear-cache step is mandatory. The CLI caches the plugin registry aggressively — without clearing, newly linked plugins won't appear.

Step 8 — Run it

Bash
pnpm kb hello:greet
# → { greeting: 'Hello, world!', source: 'deterministic' }
 
pnpm kb hello:greet --name=Alice
# → { greeting: 'Hello, Alice!', source: 'deterministic' }
 
pnpm kb hello:greet --name=Alice --ai
# → { greeting: 'Hi Alice, hope you're having a fantastic day!', source: 'llm' }
# (exact text depends on the configured LLM)

Check the help output:

Bash
pnpm kb hello:greet --help

You should see the description, flag list, and examples — all built from the manifest.

What just happened

Every file in the plugin has a purpose:

  • package.json — declares the package and its SDK dependency.
  • tsconfig.json — TypeScript config with ESM + strict mode.
  • tsup.config.ts — build config that produces one file per handler.
  • src/manifest.ts — describes the plugin to the platform.
  • src/cli/commands/hello.ts — the handler itself.
  • src/index.ts — re-exports the manifest.

The runtime loaded the manifest, saw the hello:greet command, resolved the handler path to ./dist/cli/commands/hello.js, imported it, found the default export, and called handler.execute(ctx, { flags: { name, ai } }) when you typed pnpm kb hello:greet. The result was rendered by the CLI's presenter layer.

Iterating

  • Made a code change? pnpm build and re-run. No need to re-link — local links refresh automatically.
  • Added a new command? Update the manifest's cli.commands[] array and add a new entry to tsup.config.ts.
  • Added a REST route? Add rest: { ... } to the manifest and write a handler using defineRoute instead of defineCommand.
  • Need to debug? Set KB_LOG_LEVEL=debug or pass --debug to see the full log pipeline.
Your First Plugin — KB Labs Docs