VibecapeDocs

Build an extension

Create, validate, install, and publish a Vibecape extension with the current SDK and CLI.

Vibecape extensions can add document actions, dialogs, full main views, settings panels, custom document types, and privileged main-process actions. Treat the installed @vibecape/extension-kit and @vibecape/cli as the source of truth.

Prerequisites

  • Node.js 18 or later
  • pnpm
  • Vibecape installed locally for installation testing

Before using an old example, inspect the current contracts:

pnpm exec vibecape extension --help

Also check manifest.schema.json and the exported declarations from @vibecape/extension-kit. Do not import private modules from the Vibecape App.

Initialize the project

pnpm dlx @vibecape/cli extension init ./my-extension \
  --id com.example.my-extension \
  --name "My Extension"
cd my-extension
pnpm install

The generated package follows this structure:

my-extension/
  manifest.json
  package.json
  README.md
  tsconfig.json
  assets/
    icon.svg
  main/
    index.ts
  renderer/
    index.tsx

Do not hand-write dist files. The CLI owns the main and renderer bundles, serialized UI description, release archive, and vibecape-extension.json.

Define the manifest

Start with the smallest manifest that describes the extension:

{
  "$schema": "./node_modules/@vibecape/extension-kit/manifest.schema.json",
  "id": "com.example.my-extension",
  "name": "My Extension",
  "description": "One concrete sentence describing the user outcome.",
  "icon": "./assets/icon.svg",
  "main": "./dist/main/index.js",
  "renderer": "./dist/renderer/index.js",
  "activationEvents": ["onStartup"],
  "extensionApiVersion": 1
}

package.json.version is the only release version. Keep all paths inside the package root, add engines.vibecape only after testing the real minimum host version, and declare contributes.documentTypes only for a real custom format.

Register main actions

Privileged work belongs in an ExtensionMain action. Validate every input because calls cross a process boundary.

import { ExtensionMain } from "@vibecape/extension-kit";

export default class MyExtension extends ExtensionMain {
  readonly id = "com.example.my-extension";

  activate(): void {
    this.extension.action({
      id: "example.readDocument",
      title: "Read current document",
      run: async (input) => {
        const record = input && typeof input === "object" ? input : {};
        const directId = "docId" in record ? record.docId : undefined;
        const doc =
          "doc" in record && record.doc && typeof record.doc === "object"
            ? record.doc
            : {};
        const contextId = "id" in doc ? doc.id : undefined;
        const docId =
          typeof directId === "string"
            ? directId
            : typeof contextId === "string"
              ? contextId
              : "";

        if (!docId) throw new Error("docId is required");

        const file = await this.app.files.read({ id: docId });
        this.logger.info("Read document", { docId });
        return { id: file.id, name: file.name, content: file.content };
      },
    });
  }
}

Keep action IDs stable and namespaced. Inputs and results must be JSON-serializable. Secrets, authentication, network access, filesystem work, and expensive processing stay in the main runtime.

The structured bridge includes workspace, file, asset, external URL, Finder, and clipboard operations. Use the installed type declarations for the exact signatures.

Contribute renderer UI

Default-export defineRenderer and export every component referenced by a contribution:

import { defineRenderer } from "@vibecape/extension-kit/react";

export function HomePage() {
  return <div>Extension main view</div>;
}

export function ExportDialog() {
  return <div>Export workflow</div>;
}

export function ConfigPanel() {
  return <div>Extension settings</div>;
}

export default defineRenderer(({ ui }) => {
  ui.extension.mainview({
    id: "home",
    title: "My Extension",
    component: HomePage,
  });

  ui.customize.mainview({
    id: "home-entry",
    title: "My Extension",
    page: "home",
  });

  ui.doc.menu.root({ id: "open", title: "Open", order: 10 }).mainview("home");
  ui.doc.menu.export({ id: "export", title: "Export", order: 20 }).dialog(ExportDialog);
  ui.doc.menu.root({ id: "read", title: "Read", order: 30 }).run("example.readDocument");
  ui.config.panel({ id: "settings", title: "My Extension", component: ConfigPanel });
});

Every .run(actionId) needs a matching main action. Every .mainview(pageId) must reference a declared extension main view. A document action receives the current document context; a Customize entry can open without a document, so handle an absent doc.

Validate and install-test

Run the scripts provided by the generated project:

pnpm run validate
pnpm run typecheck
pnpm run build
pnpm run test

Before considering the extension complete, verify:

  1. The manifest passes the current CLI validator.
  2. Main and renderer bundles exist when declared.
  3. Every serialized component is exported.
  4. Every serialized action has a main registration.
  5. The release descriptor matches the archive version, size, and SHA-256.
  6. The extension installs from Extensions → More → Install from folder.
  7. Each entry point works with and without a current document where applicable.
  8. Stopping and restarting removes and restores contributions cleanly.

Publish with GitHub Releases

Third-party extensions are distributed from a public GitHub repository:

  1. Bump package.json.version using semantic versioning.
  2. Re-run validation, tests, and build.
  3. Create a stable tag such as v1.2.0.
  4. Upload the generated archive and vibecape-extension.json without renaming them.

There is no third-party vibecape extension publish command. Vibecape resolves the latest stable GitHub Release from the repository URL.

On this page