Migrating from 0.1
If you wrote a plugin against @hivekeep/sdk@0.1 (or against Hivekeep before that, when plugins reached into @/server/... paths), here’s what changed and how to bring it forward to 0.2.
Most of these are mechanical edits — the runtime behaviour is identical or strictly improved. The only “you have to think about it” item is providers (see § Providers).
| Area | 0.1 | 0.2 |
|---|---|---|
| Import path | from 'ai' | from '@hivekeep/sdk' |
tool() schema field | parameters | inputSchema |
PluginContext | loose config: any | PluginContext<Config> generic |
| Vault access | import { getSecretValue } from '@/server/services/vault' | ctx.vault.getSecret(key) |
| Hook context | loose HookContext (with [key: string]: unknown) | HookPayloadMap[H] typed per hook |
| Plugin providers | { definition, displayName, capabilities, noApiKey?, apiKeyUrl? } legacy ProviderDefinition | Native LLMProvider / EmbeddingProvider / ImageProvider (same shape as built-ins) |
PluginExports.providers | Record<string, PluginProviderRegistration> | PluginProvider[] (flat list of native providers) |
| Card primitives | Record<string, unknown> | Strict PluginCardPrimitive discriminated union + card.* builders |
manifest.permissions http:* | broken — silently rejected every call | works as a catch-all |
Imports
Section titled “Imports”Replace every from 'ai' import with from '@hivekeep/sdk':
import { tool } from 'ai'import { z } from 'zod'import { tool, z } from '@hivekeep/sdk'zod is now re-exported from the SDK so you don’t carry your own dep.
If you reached into Hivekeep internals (from '@/server/channels/adapter', from '@/server/services/vault', etc.), switch those to the SDK as well:
import type { ChannelAdapter, IncomingMessage } from '@/server/channels/adapter'import type { ChannelAdapter, IncomingMessage } from '@hivekeep/sdk'@/server/... paths are Hivekeep-internal — only the host can resolve them. Third-party plugins published on npm will fail to load if they import from there.
The zod schema field on tool() is now inputSchema, not parameters:
tool({ description: 'Greet someone', parameters: z.object({ name: z.string() }), inputSchema: z.object({ name: z.string() }), execute: async ({ name }) => ({ reply: `Hi ${name}` }),})PluginContext<Config>
Section titled “PluginContext<Config>”ctx.config is now generic. Declare your config shape and ctx.config.<field> is typed:
interface MyConfig { apiKey: string; region?: 'eu' | 'us' }
export default function (ctx) {export default function (ctx: PluginContext<MyConfig>) { const apiKey = ctx.config.apiKey as string const apiKey = ctx.config.apiKey // typed: string}The runtime never validates against the generic — Hivekeep already validated against your manifest’s config schema. The generic is purely for autocomplete.
If you read secrets via the internal vault module, switch to ctx.vault:
import { getSecretValue } from '@/server/services/vault'const token = await getSecretValue(cfg.authTokenVaultKey)const token = await ctx.vault.getSecret(cfg.authTokenVaultKey)ctx.vault.getSecret(key) is permissive — you read any vault key you know about (typically a key reference Hivekeep persisted from a password-type config field).
ctx.vault.setSecret(key, value) / deleteSecret(key) / listKeys() are scoped to a plugin:<your-name>: namespace. You cannot touch another plugin’s secrets, even by guessing the prefix.
HookContext was a loose { agentId, userId?, taskId?, [key: string]: unknown }. It’s now a discriminated union keyed by hook name:
import type { HookHandler } from '@/server/hooks/types'import type { HookHandler } from '@hivekeep/sdk'
hooks: { afterChat: (ctx) => { const response = ctx.response as string // had to cast }, afterChat: (ctx) => { // ctx.message, ctx.response, ctx.agentId, ctx.userId — all typed }, afterToolCall: (ctx) => { // ctx.toolName, ctx.toolArgs, ctx.toolResult — all typed },}The four hook names that were declared but never fired in 0.1 (beforeCompacting, afterCompacting, onTaskSpawn, onCronTrigger) have been removed from the type. They never actually ran — handlers registered against them were silently dead. If you had such a handler, delete it.
Providers
Section titled “Providers”Plugin providers used to ship a ProviderDefinition wrapper:
// 0.1 — DELETEDproviders: { mistral: { definition: { type: 'mistral', testConnection: async (config) => ({ valid: true }), listModels: async (config) => [{ id: 'mistral-medium', name: 'Mistral Medium', capability: 'llm' }], }, displayName: 'Mistral', capabilities: ['llm'], apiKeyUrl: 'https://console.mistral.ai/api-keys', },}That shape is gone. In 0.2 you implement the same native interfaces as the built-in providers:
// 0.2import type { LLMProvider, ChatRequest, ChatChunk } from '@hivekeep/sdk'
class MistralProvider implements LLMProvider { readonly type = 'mistral' readonly displayName = 'Mistral' readonly apiKeyUrl = 'https://console.mistral.ai/api-keys' readonly configSchema = [ { key: 'apiKey', type: 'secret', label: 'API Key', required: true }, ] as const
async authenticate(config) { return { valid: true } } async listModels(config) { return [{ id: 'mistral-medium', name: 'Mistral Medium', contextWindow: 32768 }] }
async *chat(model, request: ChatRequest, config): AsyncIterable<ChatChunk> { // stream text-delta / tool-use / thinking-delta / thinking-signature, // finish with one finish chunk carrying { reason, usage } }}
export default function (ctx) { return { providers: [new MistralProvider()] }}PluginExports.providers is now a flat array (PluginProvider[]) instead of a Record. The loader detects each provider’s family by inspecting which method it implements (chat → LLM, embed → embedding, generate → image).
Why bother: a native LLMProvider does streaming, prompt caching, thinking, tool calls — the same surface the built-in Anthropic / OpenAI providers use. The legacy { testConnection, listModels } shape only supported basic chat-completions, with no way to declare advanced features.
If you emitted cards with Record<string, unknown>[] layouts, you can keep doing that — the new PluginCardPrimitive union accepts any object that matches one of the known type discriminants. But you’ll get autocomplete and compile-time validation by switching to the card.* builders:
import { card } from '@hivekeep/sdk'
ctx.cards.emit({ agentId, cardType: 'progress', layout: [ { type: 'header', title: 'Working...' }, { type: 'progress', indeterminate: true }, card.header({ title: 'Working...' }), card.progress({ indeterminate: true }), ], initialState: {},})Hand-written literals still work — the builders are sugar, not gatekeepers.
Permissions
Section titled “Permissions”manifest.permissions: ["http:*"] was silently broken in 0.1 — the matcher only recognized exact hosts and *.domain.tld subdomain patterns. In 0.2 the catch-all works:
{ "permissions": [ "http:*", // any host (use sparingly) "http:api.example.com", // exact "http:*.example.com" // subdomains + apex ]}ctx.http.fetch() enforces these. A blocked call throws PluginPermissionError (code: 'PLUGIN_PERMISSION_DENIED') with a message that suggests the right declaration. Raw globalThis.fetch() from inside your plugin is not blocked — ctx.http.fetch is opt-in hardening, not a sandbox.
Manifest
Section titled “Manifest”Two small ergonomic improvements:
-
JSON Schema — add the
$schemaline to yourplugin.jsonand any editor with JSON-Schema support (VSCode, JetBrains) autocompletes the manifest fields and surfaces typos inline.{"$schema": "https://unpkg.com/@hivekeep/sdk/schemas/plugin-manifest.schema.json","name": "my-plugin","version": "0.1.0", -
hivekeeprange — declare which Hivekeep host versions your plugin targets so users don’t try to load incompatible builds.>=0.40.0is the floor for the 0.2 SDK.
Checklist
Section titled “Checklist”Done with your migration when:
- No
from 'ai'left in your plugin’s imports. - No
from '@/server/...'left in your plugin’s imports. -
tool()calls useinputSchema(notparameters). - Vault access goes through
ctx.vault.getSecret(...). - Hook handlers don’t cast away the payload type.
- Plugin providers implement
LLMProvider/EmbeddingProvider/ImageProviderdirectly;PluginExports.providersis an array. -
plugin.jsonhas a$schemaline and an explicithivekeeprange. -
bun typecheckpasses.
Stuck on something not covered here? Open an issue at https://github.com/MarlBurroW/hivekeep/issues with the plugin’s source — the SDK’s tests run against the hello-agent reference example, so anything matching that shape is guaranteed to load.