TypeScript SDK
Package @vibex/plugin-sdk, Node >=20. Template ts-worker writes the Worker definition to runtime/main.ts and re-exports it from runtime/main.mjs. Engine field pluginSdk is ^1.0.0. Templates full, file-tab, and host.service use the same npm package with .mjs sources.
Editable file tabs, detail panels, and host.service consume this package's Worker, App, and testing modules.
Modules
| Export | Role |
|---|---|
@vibex/plugin-sdk / /protocol |
VIBEX_PLUGIN_API_VERSION ("1.0"), VIBEX_PLUGIN_PROTOCOL_VERSION ("1.1"), JSON and context types |
/worker |
definePluginWorker, activatePluginWorker, PluginSdkError |
/app |
definePluginApp, VibeXAppBridge |
/stdio |
runStdioPluginWorker |
/testing |
createWorkerHarness, createGenerationHarness, createAppHarness |
The public SDK exports the modules above. Tauri commands, Axum routes, SQLite schema, and absolute Host paths stay in the Host. Full Trust Workers may use the Node standard library. Structured Runtime / Artifact lifecycle uses environment.host.call.
Worker
import { definePluginWorker } from '@vibex/plugin-sdk/worker';
export default definePluginWorker((registrar, environment) => {
registrar.handle('hello', async (input, env) => {
env.log.info('hello', { input });
return { ok: true };
});
registrar.onDispose({
dispose() {
environment.log.info('disposed');
},
});
});registrar.handle(id, handler): id must match the handler regex and appear in the manifest. handler(input, environment) takes JSON and returns JSON or a Promise. Duplicate registration throws handler_duplicate.
registrar.onDispose(disposable) accepts { dispose() } or a function. Unload runs them in reverse.
environment:
| Field | Meaning |
|---|---|
context.pluginId |
Plugin ID |
context.pluginVersion |
Version |
context.generation |
Current activation generation |
context.packageClass |
full-trust or isolated |
context.grantedCapabilities |
Usually ["*"] under Full Trust |
host.call(capability, operation, input?) |
Host RPC |
signal |
AbortSignal; aborted on dispose |
log.debug/info/warn/error(message, fields?) |
Structured log |
activatePluginWorker(definition, environment) is for tests or self-hosting. A apiVersion other than "1.0" throws sdk_incompatible. Invoke after dispose throws worker_disposed. A missing handler throws handler_not_found.
stdio entry
The Host runs node --max-old-space-size=128 dist/worker.mjs. Split definition and entry:
// runtime/worker.ts
export default definePluginWorker((registrar) => {
registrar.handle('hello', async () => ({ ok: true }));
});// runtime/main.mjs
import { runStdioPluginWorker } from '@vibex/plugin-sdk/stdio';
import definition from './worker.ts';
await runStdioPluginWorker(definition);init --template ts-worker writes the definition in runtime/main.ts and export { default } from "./main.ts" in runtime/main.mjs. Add runStdioPluginWorker in main.mjs, or split as above. build emits runtime/main.mjs to dist/worker.mjs.
Tests import the definition module:
import definition from '../runtime/worker.ts';App
import { definePluginApp } from '@vibex/plugin-sdk/app';
export default definePluginApp(({ bridge, root, signal }) => {
const button = document.createElement('button');
button.textContent = 'Refresh';
button.addEventListener('click', () => {
void bridge.invoke('dashboard.refresh', {});
});
root.replaceChildren(button);
bridge.ready();
const dispose = () => root.replaceChildren();
signal.addEventListener('abort', dispose, { once: true });
return dispose;
});bridge: pluginId, generation, invoke(handler, input?), subscribe(channel, listener) (returns unsubscribe), ready(). An artifact.editor mount also has artifact.
bridge.artifact: name is the file name; readText() returns { name, content, revision }; writeText(content, expectedRevision) writes by revision. An external edit yields a recoverable conflict with code artifact_revision_conflict. The Host gives the App the file name, revision, and this bridge. Theme and locale arrive in Host bootstrap.
Editable file tab:
file.openerdeclares extensions andeditorSurface.- **
app.surfaceusesslot**: artifact.editor,appEntrypoint: "app",handler: "surface.createSession". - The Worker registers that handler.
- The App calls
readText(), keeps the revision, and passes it on save.
Testing
import { createWorkerHarness, createGenerationHarness } from '@vibex/plugin-sdk/testing';
const worker = await createWorkerHarness(definition, {
context: { pluginId: 'you.notes', pluginVersion: '0.1.0', generation: 1 },
});
await worker.invoke('hello', {});
// worker.hostCalls records host.call
await worker.dispose();
const gen = await createGenerationHarness(definition, {
requiredHandlers: ['hello'],
});
await gen.activateCandidate(definition);
await gen.dispose();createAppHarness(definition, { root, artifact }) simulates the bridge, subscriptions, revoke, and artifact revision conflicts. A missing required handler makes activateCandidate dispose the candidate and throw required_handler_missing.
Record "@vibex/plugin-sdk": "^1.0.0" in package.json. Until the SDK is on npm, use a file: path to the Host checkout or the Host-family sdk/. When developing VibeX itself, run pnpm --filter @vibex/plugin-sdk build first.

