Skip to main content

Authoring a plugin

A marketplace plugin is two artifacts, generated from one source of truth:

  1. manifest.json — the published contract: id, version, permissions, marketplace presentation, and every contribution.
  2. plugin.js — a self-contained ES-module bundle, required only when the manifest declares agent tools.

The corte-plugins-marketplace repo is the working reference: author the manifest in TypeScript (defineManifest), keep tool handlers in plugin.ts, and let its shared build emit both artifacts — it fails on any drift (a declared tool without a handler, a handler without a declaration, mismatched versions, or a contribution id outside your namespace).

The manifest

import { defineManifest } from '@corte/plugin-types'

export const manifest = defineManifest({
version: 1,
id: 'com.you.thing', // reverse-DNS, ≥3 segments, yours forever
name: 'Your Thing',
pluginVersion: '1.0.0', // semver; every publish is a new immutable version
description: 'Marketplace card copy (≤600 chars).',
about: 'Detail-page copy.\n\nBlank lines separate paragraphs.',
iconUrl: 'https://…/icon.png', // https; card + detail identity
screenshots: ['https://…/1.png'], // https, ≤6; detail-page showcase
tags: ['color', 'social'], // ≤8; discovery + search
permissions: ['read:timeline'], // only what you use — users see this
contributions: { /* templates, library, models, effects, tools */ },
})

Every contribution id (template id, effect type, tool name, stock id) must start with <your-plugin-id>. — enforced at publish and at activation.

Hosting assets: any https URL works. The pattern we use ourselves: commit assets next to the plugin (plugins/<name>/assets/) in a public repo and reference them via raw.githubusercontent.com — content updates then need no republish, only URL changes do.

Permissions

PermissionGrants
read:timelinegetTimeline() (incl. text-clip content), getSelectedClipIds()
read:mediagetBlob(), getAudioSamples()
write:clipsaddText(), setClipText(), moveClip(), splitClip()
write:effectsapplyEffect() (built-in or your own effect types)
notifyshowToast()
net:fetchKeeps fetch/XMLHttpRequest/WebSocket alive in your worker; without it they throw

Request the minimum — the permission list is the install consent screen, and permission changes in an update force users to re-review.

Shader effects (pure data)

Single-pass GLSL ES 300 fragment shaders. The host provides the vUV varying (exact capitalization) and binds the source frame to uTex; parameters bind declaratively — no imperative GL:

{
type: 'com.you.thing.vignette',
displayName: 'Vignette',
category: 'stylize',
params: [{ key: 'amount', label: 'Amount', min: 0, max: 1, default: 0.5 }],
fragmentShader: `#version 300 es
precision highp float;
in vec2 vUV;
out vec4 fragColor;
uniform sampler2D uTex;
uniform float uAmount;
void main() {
vec4 c = texture(uTex, vUV);
float d = distance(vUV, vec2(0.5));
fragColor = vec4(c.rgb * mix(1.0, smoothstep(0.9, 0.3, d), uAmount), c.a);
}`,
uniforms: { uAmount: 'amount' }, // uniform name → param key, resolved per frame
}

Users get every param as a keyframable slider automatically. A shader that fails to compile is skipped gracefully — it never breaks a user's render.

Agent tools (the code bundle)

Tools are descriptors in the manifest (name, description, JSON-Schema args, readOnly) plus handlers in the bundle. Only manifest-declared tools are ever advertised to the model; names stay dotted in your code and are shown to the model with dots as underscores.

The bundle runs in a sandboxed worker with an async context:

import { defineSandboxPlugin, type SandboxPluginContext, type ToolResult } from '@corte/plugin-types'
import { manifest } from './manifest.ts'

const handlers = (ctx: SandboxPluginContext) => ({
'com.you.thing.report': async (args: Record<string, unknown>): Promise<ToolResult> => {
const t = await ctx.editor.getTimeline()
return { json: { tracks: t.tracks.length, duration: t.duration } }
},
})

export default defineSandboxPlugin({
meta: { id: manifest.id, name: manifest.name, version: manifest.pluginVersion, permissions: manifest.permissions },
activate(ctx) {
const byName = handlers(ctx)
for (const tool of manifest.contributions.tools ?? []) {
const handler = byName[tool.name]
if (handler) ctx.tools.register({ name: tool.name, handler })
}
},
})

The sandbox API

editor.getTimeline()Tracks, clips, durations, text content
editor.getSelectedClipIds()The user's current selection
editor.getMediaAssets()The project's media list
editor.applyEffect(clipIds, effect)Apply any effect with params
editor.addText(spec) / setClipText(id, content)Create / rewrite text clips
editor.moveClip(id, to) / splitClip(id, atFrame)Retiming — e.g. beat-snapping
editor.showToast(message, kind)Notify the user
media.getBlob(assetId)Raw asset bytes
media.getAudioSamples(assetId, { targetHz })Host-decoded mono PCM for audio analysis

Everything is async (calls cross the worker boundary), everything is permission-checked, and tool invocations time out after 30 s. Keep real logic in separate pure modules and unit-test them — every plugin in the reference repo does.

Next: publishing and operating your plugin.