TypeScript SDK Guide
pm-cli ships a fully typed TypeScript SDK at @unbrained/pm-cli/sdk. This guide covers every capability type with working examples.
Installation
npm install @unbrained/pm-cli
The SDK is included with the CLI โ no separate package needed. Import from the sub-path:
import { defineExtension, type ExtensionApi } from "@unbrained/pm-cli/sdk";
Extension anatomy
Every extension is a module that exports a default value from defineExtension:
// index.ts
import { defineExtension, type ExtensionApi } from "@unbrained/pm-cli/sdk";
export default defineExtension({
name: "my-ext",
version: "0.1.0",
activate(api: ExtensionApi) {
// register capabilities here
},
});
Paired with a manifest.json:
{
"name": "my-ext",
"version": "0.1.0",
"entry": "index.js",
"capabilities": ["commands"]
}
Note:
entrypoints to the compiled JS output. Compile withtscoresbuildbefore installing.
Capability 1: Custom commands
api.registerCommand({
name: "greet",
description: "Print a greeting.",
intent: "verify extension activation",
examples: ["pm greet", "pm greet --name Alice"],
arguments: [
{ name: "subject", description: "Who to greet", optional: true },
],
flags: [
{ long: "--name", value_name: "str", description: "Override greeting name" },
],
run: async (ctx) => {
const name = (ctx.options.name as string) ?? ctx.args[0] ?? "world";
return { ok: true, message: `Hello, ${name}!` };
},
});
The run function receives a CommandHandlerContext:
interface CommandHandlerContext {
command: string; // "greet"
args: string[]; // positional args
options: Record<string, unknown>; // parsed flags
global: GlobalOptions; // --json, --quiet, --path, etc.
pm_root: string; // absolute path to .agents/pm/
}
Capability 2: Schema โ custom item types
api.registerItemTypes([
{
name: "Incident",
folder: "incidents",
aliases: ["incident", "inc"],
required_create_fields: ["title", "description", "severity"],
options: [
{ key: "severity", values: ["critical", "major", "minor"], required: true },
{ key: "service", values: ["api", "web", "worker"] },
],
},
]);
api.registerItemFields([
{ name: "severity", type: "string" },
{ name: "service", type: "string", optional: true },
]);
After registering:
pm create --type incident --title "DB down" --severity critical --service api
Capability 3: Schema โ custom flags
Inject additional flags into any built-in command:
api.registerFlags("create", [
{
long: "--severity",
value_name: "level",
description: "Incident severity: critical | major | minor",
},
]);
Capability 4: Importers
api.registerImporter("json-fixtures", {
description: "Import Incident fixtures from a JSON file.",
run: async (config: Record<string, unknown>) => {
const file = config.file as string;
const fixtures = JSON.parse(await fs.readFile(file, "utf-8"));
return fixtures.map((f: any) => ({
type: "Incident",
title: f.title,
body: f.description ?? "",
status: f.status === "closed" ? "closed" : "open",
metadata: { severity: f.severity, service: f.service },
}));
},
});
Usage:
pm extension import json-fixtures --config file=./incidents.json
Capability 5: Lifecycle hooks
api.hooks.beforeCommand((ctx) => {
if (ctx.command === "close" && process.env.PM_AUDIT) {
console.error(`[audit] close attempt by ${ctx.global.author ?? "unknown"}`);
}
});
api.hooks.afterCommand((ctx, result) => {
if (ctx.command === "create") {
console.error(`[audit] created: ${(result as any)?.id ?? "?"}`);
}
});
api.hooks.onWrite((item) => {
// Called after every item write โ good for webhooks
});
Capability 6: Renderers
Override TOON or JSON output for a specific format:
api.registerRenderer("toon", {
render: (data: unknown) => {
if (data && typeof data === "object" && "id" in data) {
const item = data as Record<string, unknown>;
return `id: ${item.id}\nstatus: ${item.status}\n`;
}
return JSON.stringify(data, null, 2);
},
});
Capability 7: Preflight
Gate destructive operations:
api.registerPreflight({
command: "close",
run: async (ctx) => {
const id = ctx.args[0];
// Read the item to check its type and severity
const item = ctx.item as Record<string, unknown> | undefined;
if (item?.type === "Incident" && !item?.severity) {
return {
allow: false,
reason: "Incidents must have a severity set before closing.",
};
}
return { allow: true };
},
});
Capability 8: Services
Override built-in services for formatting, error handling, etc.:
api.registerService("output_format", {
override: (format: string) => {
if (format === "toon" && process.env.PM_FORCE_JSON) return "json";
return format;
},
});
Capability 9: Search providers
Plug in a custom embedding / retrieval backend:
api.registerSearchProvider({
name: "my-openai",
embed: async (ctx) => {
// Return a float32 embedding vector for ctx.text
const resp = await fetch("https://api.openai.com/v1/embeddings", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: "text-embedding-3-small", input: ctx.text }),
});
const json = await resp.json() as { data: { embedding: number[] }[] };
return json.data[0].embedding;
},
query: async (ctx) => {
// Return scored hits: [{ id, score }]
return { hits: [] };
},
});
Complete reference extension
See the ts-sdk-starter example โ a single TypeScript file demonstrating all 9 capabilities, ready to install:
pm install github.com/unbraind/pm-cli-companion/examples/ts-sdk-starter --project
Extension manifest
All capability strings must be declared in manifest.json:
| Capability | Registration method |
|---|---|
commands |
registerCommand |
schema |
registerFlags, registerItemTypes, registerItemFields, registerMigration |
importers |
registerImporter / registerExporter |
hooks |
api.hooks.* |
renderers |
registerRenderer |
preflight |
registerPreflight |
services |
registerService |
search |
registerSearchProvider, registerVectorStoreAdapter |
parser |
registerParser |
Debugging
# Check extension health
pm extension doctor --detail deep --trace
# Activate with verbose output
PM_EXT_DEBUG=1 pm <any-command>
# Validate manifest
pm extension validate ./my-ext