Skip to content
Back to site

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).

Area0.10.2
Import pathfrom 'ai'from '@hivekeep/sdk'
tool() schema fieldparametersinputSchema
PluginContextloose config: anyPluginContext<Config> generic
Vault accessimport { getSecretValue } from '@/server/services/vault'ctx.vault.getSecret(key)
Hook contextloose HookContext (with [key: string]: unknown)HookPayloadMap[H] typed per hook
Plugin providers{ definition, displayName, capabilities, noApiKey?, apiKeyUrl? } legacy ProviderDefinitionNative LLMProvider / EmbeddingProvider / ImageProvider (same shape as built-ins)
PluginExports.providersRecord<string, PluginProviderRegistration>PluginProvider[] (flat list of native providers)
Card primitivesRecord<string, unknown>Strict PluginCardPrimitive discriminated union + card.* builders
manifest.permissions http:*broken, silently rejected every callworks as a catch-all

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}` }),
})

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.

Plugin providers used to ship a ProviderDefinition wrapper:

// 0.1 — DELETED
providers: {
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.2
import 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.

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.

Two small ergonomic improvements:

  1. JSON Schema: add the $schema line to your plugin.json and 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",
  2. hivekeep range: declare which Hivekeep host versions your plugin targets so users don’t try to load incompatible builds. >=0.40.0 is the floor for the 0.2 SDK.

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 use inputSchema (not parameters).
  • Vault access goes through ctx.vault.getSecret(...).
  • Hook handlers don’t cast away the payload type.
  • Plugin providers implement LLMProvider / EmbeddingProvider / ImageProvider directly; PluginExports.providers is an array.
  • plugin.json has a $schema line and an explicit hivekeep range.
  • bun typecheck passes.

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.