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 --helpworks). - 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:
mkdir -p packages/hello-plugin/src/cli/commands
cd packages/hello-plugin
pnpm initReplace the generated package.json with:
{
"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:
{
"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:
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:
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 returnundefined. The handler degrades gracefully: no LLM means the canned greeting.- Return a
CommandResult.exitCode: 0means success;resultis the structured payload the CLI will render.
See SDK → Commands for the full defineCommand API.
Step 4 — The manifest
src/manifest.ts:
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.idmust be in@scope/nameformat.handlerpath is relative to the package root and points at.js(the built output, not the source).permissionsusescombinePermissions()withminimalPreset(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:
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
pnpm install
pnpm buildYou 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.
Step 7 — Link the plugin
From your KB Labs workspace root:
pnpm kb marketplace link /absolute/path/to/packages/hello-plugin
pnpm kb marketplace clear-cacheThe clear-cache step is mandatory. The CLI caches the plugin registry aggressively — without clearing, newly linked plugins won't appear.
Step 8 — Run it
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:
pnpm kb hello:greet --helpYou 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 buildand 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 totsup.config.ts. - Added a REST route? Add
rest: { ... }to the manifest and write a handler usingdefineRouteinstead ofdefineCommand. - Need to debug? Set
KB_LOG_LEVEL=debugor pass--debugto see the full log pipeline.
What to read next
- SDK → Quickstart — the same example in a shorter, reference-style format.
- Plugins → Overview — the full plugin lifecycle and package conventions.
- Plugins → CLI Commands — the full authoring guide for CLI commands.
- Plugins → REST Routes — add HTTP endpoints to your plugin.
- Plugins → Studio Pages — add UI pages to Studio.
- Plugins → Permissions — tighten permissions for production.
- Plugins → Publishing — publish your plugin to npm and the marketplace.