A starter template for building a kandev native-UI plugin — its own git repo, packaged into a versioned tarball and installed against a running kandev instance. Click “Use this template” on GitHub (or copy this repo) to bootstrap your own plugin.
It is a small, complete, working example of everything a kandev plugin can do, wired together so you can delete what you don't need rather than assemble it from scratch:
- Native nav item + route —
ui/bundle.jsadds a sidebar entry that opens/template, a page rendered natively inside the kandev SPA (not an iframe) using the host's own React instance. - Chat toolbar action — a component registered into the
chat-input-actionsslot renders an icon button in the chat composer toolbar, with the current{ sessionId, taskId, taskTitle }asslotProps. - Live WS-driven counter — a
registerWsHandler("task.created", ...)handler updates module state that the page re-renders from, live, with no reload. - Backend event handling with Host state —
OnEventcountstask.createddeliveries in a persistent counter via theHost.GetState/SetStateround trip, so restarts don't reset it. - Backend webhook —
HandleWebhookanswers thepingwebhook kandev proxies to the plugin, building its reply from the operator settings. - Operator settings (
config_schema) — agreetingstring and a secretapi_token, rendered as a form at Settings > Plugins > Template Plugin and read by the plugin process viahost.GetConfig(ctx). Secret fields are vault-stored and masked everywhere outside the plugin process.
The plugin id appears in four places that must stay in sync. Rename all of
them from kandev-plugin-template to your own id (e.g. kandev-plugin-acme):
manifest.yaml—id, plusdisplay_name/description/author.go.mod— themoduleline.Makefile—BINandPKG_OUT(andVERSIONto match the manifest).ui/bundle.js— the id passed towindow.registerKandevPlugin(...).
Then trim the scaffolding: drop the webhook / event / config blocks in
manifest.yaml you don't use, delete the matching handlers in
server/plugin.go, and keep only the registry.register* calls in
ui/bundle.js your plugin actually contributes. Update server/plugin_test.go
to cover what remains.
kandev spawns the platform-matching binary from runtime.executables in
manifest.yaml as a subprocess and talks to it over a private gRPC connection
(hashicorp/go-plugin) — there is no
HTTP listen address, no shared secret, and no manual wiring: pluginsdk.Serve
in server/main.go owns the entire transport. You implement three RPCs and
get a Host handle back:
type Plugin interface {
OnEvent(ctx context.Context, e *Event) error
HandleWebhook(ctx context.Context, req *WebhookRequest) (*WebhookResponse, error)
}server/plugin.go's templatePlugin embeds pluginsdk.UnimplementedPlugin
(a no-op default for both RPCs, plus Host()/SetHost() accessors) and
overrides what it needs. server/main.go is just
pluginsdk.Serve(&templatePlugin{}).
pkg/pluginsdk is not published as its own module yet, so go.mod here uses a
local replace:
replace github.com/kandev/kandev => ../kandev/apps/backend
This assumes your plugin repo is checked out as a sibling of the kandev
monorepo:
some-dir/
├── kandev/ # https://github.com/kdlbs/kandev, Go module at apps/backend/
└── kandev-plugin-template/ # this repo
Note the module root is kandev/apps/backend, not the repo root — kandev is
a monorepo and the Go backend (including pkg/pluginsdk) lives one level down.
Adjust the replace path if your layout differs. Once pkg/pluginsdk ships as
a standalone, versioned module, this repo will drop the replace and pin a
real version instead.
manifest.yaml # plugin manifest — id, capabilities, runtime.executables, ui.bundle, config_schema
server/
main.go # pluginsdk.Serve wiring — no flags, no HTTP, no secrets
plugin.go # templatePlugin: OnEvent / InvokeTool / HandleWebhook
plugin_test.go # tests against a fake Host, no subprocess spawn needed
ui/
bundle.js # hand-written, no-build ES module — the plugin's frontend half
ui/bundle.js is hand-written, dependency-free ES module JavaScript. There is
no build step: it ships byte-for-byte inside the package tar.gz, and kandev
serves it directly. Edit the file and repackage — nothing else to run.
make build # go build -o bin/... ./server/...
make test # go test ./server/...
make vet # go vet ./server/...Note: bare
go build ./server/...(no-o) fails withbuild output "server" already exists and is a directory— Go's default output name for a lone main package is the last path element ("server"), which collides with theserver/source directory. Always pass-o, rungo build .from insideserver/, or usemake build.go vet/go testare unaffected.
make package # cross-compiles linux/darwin (amd64+arm64) + windows/amd64,
# then packs manifest + ui/ + binaries into a versioned .tar.gz
make package-host # host platform only — faster local iterationBoth stage manifest.yaml + ui/ alongside the freshly built
server/plugin-<goos>-<goarch>[.exe] binaries, then pack the tree with
github.com/kandev/kandev/cmd/plugin-pack (resolved through this repo's
replace directive), which computes checksums.txt and writes the tarball.
Either through the UI (Settings > Plugins > Install plugin, URL or file upload), or directly:
curl -F package=@kandev-plugin-template-0.1.0.tar.gz \
http://localhost:<kandev-port>/api/plugins/installkandev verifies checksums.txt, validates the manifest, extracts the package,
spawns the host-matching binary, and — once the go-plugin handshake completes —
marks the plugin active. Sideloaded plugins register disabled/unverified;
enable yours in Settings > Plugins (the plugins feature flag must be on).
Reinstalling the same version returns 409 — bump version in manifest.yaml.
.github/workflows/release.yml builds and publishes automatically. Push a tag
that matches your manifest version and it cross-compiles all platforms, packs
the tarball, and creates a GitHub Release with the two assets the kandev
marketplace
install pipeline expects:
<id>-<version>.tar.gz— the plugin package (with its own internalchecksums.txtverified on install), andchecksums.txt— the sha256 of the tarball (advisory provenance; the catalog reserves apackage_sha256field for it but the index builder does not populate it yet, so it is optional today).
# bump VERSION in Makefile + version in manifest.yaml first, then:
git tag v0.1.0
git push origin v0.1.0The workflow checks out the kandev monorepo as a sibling so the replace
directive resolves (see "Developing against the SDK"); pin a kandev ref in the
workflow if you need reproducible SDK versions.
MIT — see LICENSE. This template is meant to be copied and made your own; your resulting plugin can carry whatever license you choose.