Building
Package format
The folder layout, readme.md, the entry file, TypeScript, and the size limits a package must meet.
A plugin package is a folder of files — uploaded as a zip, or composed for you by the console editor. There is no manifest file to maintain: every file's location says what it is, and the code declares the rest.
Layout
readme.md required
setup.md optional install instructions shown on the plugin's page
icon.svg (or icon.png) optional plugin icon, one of the two
gallery/… optional pictures and videos shown above the readme
index.ts (or index.js, or one single .ts/.js file)
src/… more .ts/.js files, imported from index.ts
commands/<name>.md one slash command per file
skills/<key>/skill.md one skill per folder
skills/<key>/… files that skill can read on demand
companion-models/<key>/model.json + the avatar (.vrm / .glb, or a Live2D .zip), its clip files (.vrma / .glb / .fbx) and a thumbnail
companion-characters/<key>/character.json + an optional thumbnail
companion-items/<key>/item.json + the item's .glb model and an optional thumbnail
portraits/<key>/portrait.json + one portrait.png, portrait.jpg or portrait.webp
Every file must fit one of these slots. A file anywhere else — a package.json, a tsconfig.json, a stray
notes.txt — refuses the publish and the error names the file. Nothing is silently dropped. (A zip whose entries
all sit inside one wrapper folder is fine, and OS junk such as .DS_Store is ignored.)
Only readme.md is always required, but a package that adds nothing is refused: it needs at least one command,
skill, code capability or companion/portrait folder. The rest is whatever your plugin needs: a companion pack has no
code at all, and a prompt-only plugin has no commands or skills.
readme.md
Required for every publish from the console or the Portal API. It is what people read before they install.
---
name: Brave Search
description: A bring-your-own-key web search provider — calls the Brave Search API with your own key.
categories: research, developer
version: 1.0
---
# Brave Search
What it does, how to set it up, what each setting means, and what it can reach.
| Frontmatter key | Used for |
|---|---|
name |
The display name on the Browse card and the plugin page. Required for a zip upload; otherwise it falls back to the name you gave the plugin when you created it. |
description |
The one-line summary on the Browse card. Falls back to the plugin's existing description. |
slug |
Only for a zip upload (POST /plugins/upload): the plugin's slug. Default: the name lowercased with other characters turned into -. |
categories |
Up to three, comma-separated, from productivity, developer, communication, research, data, home, writing, support, finance, fun. Anything else fails the publish. |
gallery |
The order and captions of the files in gallery/, one - file: name.png line per item with an optional indented caption: (at most 200 characters). Files you don't list still show, after the listed ones. |
version |
<major>.<minor> only (default 1.0), for zip uploads and the official-plugin pipeline: the platform picks the patch. See Publishing. When you give the full version yourself (the editor's Publish…, or POST /plugins/{id}/versions), this key is ignored. |
The body (everything after the frontmatter) is shown verbatim as the plugin's readme in the console. It is your
user documentation, so write it like one. A setup.md is shown on the Browse page as a collapsible Install
instructions card, so keep setup steps there and leave the readme for what the plugin does. The official Discord readme is a good model: numbered setup steps,
every setting explained, and what each error means.
Icon and gallery
icon.svgoricon.pngat the package root (one of them, not both), at most 512 KB. An SVG is a text file; a PNG is binary, so it needs a zip upload.gallery/holds at most 12 files and no subfolders:.png,.jpg/.jpegand.webppictures up to 5 MB each, and.mp4or.webmvideos up to 50 MB each. A listedgalleryfile that isn't in the folder fails the publish.
Pictures and videos are binary, so they don't travel in the editor's JSON file list. The editor keeps them beside the plugin instead, and the next Save or Publish… carries them:
- the icon is the one set with Add icon (
PUT /plugins/{id}/draft/icon); - the gallery is managed in the Gallery card on the editor's Store page tab: add files, remove them, reorder
them, and write a caption for each. It saves as you go (
/plugins/{id}/draft/gallery, see the API reference) and goes live with your next publish. The limits are the ones above.
A zip with a gallery/ folder replaces the editor's gallery, and the Store page then shows what that zip published.
A zip with no gallery/ folder keeps the gallery the plugin already has, the same way a zip with no icon keeps its
icon. To get rid of the gallery, remove its items in the editor and publish.
Code: the entry file
- One source file (
.tsor.js): it is the entry point, whatever its name. - More than one: a root
index.tsorindex.jsis required and is the entry point (index.tswins if both exist). The other files are pulled in through imports.
// infrastructure/plugins/oh-my-openagents/index.ts
import { reconcileCrew } from "./src/reconcile";
import { ORCHESTRATION_DOCTRINE } from "./src/doctrine";
import { CREW } from "./src/crew";
Imports resolve only inside your package. A relative import (./src/crew) is looked up against your own files,
trying the exact path, then .ts, then .js, then /index.ts, then /index.js. A bare import (lodash), a
node: import or a URL fails the publish with "plugins bundle only their own files". There is no npm and no
node_modules; if you need a small helper, copy it into your package.
At publish time esbuild merges your files into one readable (not minified) ES2022 script. TypeScript types are stripped — not checked. Type-check locally (below).
Globals, not imports
The SDK is ambient: host, middleware, events, tools, compaction, web, modelProvider, channel,
connection and manifest are globals. You never import them. Assign to them through globalThis:
globalThis.middleware.systemPrompt = async (context) => context.systemPrompt + GUIDANCE;
globalThis.web.search = async (input) => { /* … */ };
globalThis.channel = { receive: async (request, tools) => { /* … */ }, send: async (request) => { /* … */ } };
Replacing a whole registry object (as the last line does) works the same as assigning its members one by one.
What the sandbox does not have
Plugin code runs in a bare JavaScript engine: no fetch, require, process, fs, Buffer, atob/btoa or
TextEncoder. Use host.fetch for HTTP and host.crypto for hashing, HMACs, signatures and base64. See
Sandbox model.
Register at the top level
At publish time the platform evaluates your bundle once, in a throwaway sandbox where every host.* call either
does nothing (host.log, host.settings.get) or throws "unavailable at publish". It then reads what your code
assigned to the registries and to globalThis.manifest. So:
- Do assign handlers and the manifest at the top level, from constants or simple logic.
- Don't register inside a handler — e.g. assigning
middleware.userMessageInboundinsideevents.pluginInstalled. Nothing runs that handler at publish time, so the capability never exists. - Don't compute registrations from
host.*—host.settings.get()returnsundefinedat publish time, and ahost.fetchat the top level fails the publish. - Keep the manifest and tool metadata plain JSON. Functions, class instances or cyclic objects in
globalThis.manifestor in a tool'sdescription/parametersfail the publish.
Type-checking with TypeScript
- On your plugin's page in the console, press Download SDK types. You get
agentparley-plugin-sdk.d.ts— the full typed contract, with documentation on every type. - Put it next to your code and add a
tsconfig.json(for your editor only — never in the zip):
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"types": [],
"skipLibCheck": true,
"noEmit": true
},
"include": ["index.ts", "src/**/*.ts", "agentparley-plugin-sdk.d.ts"]
}
"lib": ["ES2022"] with no DOM and "types": [] with no Node matches the sandbox: if you reach for fetch or
Buffer, the compiler tells you.
- Run
npx tsc -p .before every publish.
Commands, skills, companion and portrait folders
| Folder | Key | Required inside | Details |
|---|---|---|---|
commands/<name>.md |
the file name (deploy.md → /deploy) |
frontmatter description, a non-empty body |
Commands and skills |
skills/<key>/ |
the folder name | exactly one skill.md (any letter case) with frontmatter description |
Commands and skills |
companion-models/<key>/ |
the folder name | model.json |
Companion packs |
companion-characters/<key>/ |
the folder name | character.json |
Companion packs |
companion-items/<key>/ |
the folder name | item.json |
Companion packs |
portraits/<key>/ |
the folder name | portrait.json ({ "name": "…" }, 1–60 characters, no other member) and exactly one portrait.png, .jpg or .webp of at most 512 KB |
agent pictures; at most 100 per package. Installers pick them under an agent's Picture |
Keys match ^[a-z0-9][a-z0-9_-]{0,63}$.
Keys are unique across the whole package, not per folder. A command research and a skill research
collide, and so do companion-characters/fox/ and companion-models/fox/. Give them different keys
(fox, fox-model).
Limits
| What | Limit |
|---|---|
Source files (.ts/.js) |
50 files, 1 MB each |
| Bundled code after merging | 2 MB |
Skill files and setup.md |
256 KB each |
| Any other text file (readme, command, companion JSON) | 1 MB each in a zip |
| Files per skill folder | 100 |
| Zip entries | 4,000 |
| Whole package, uncompressed | 256 MB |
| Companion avatar model | 100 MB |
| Companion animation clip | 32 MB |
Companion item model (.glb) |
3 MB |
| Thumbnail image (PNG, JPEG or WebP) | 2 MB |
| Plugin icon | 512 KB |
| Gallery picture / video | 5 MB / 50 MB (12 files) |
| Portrait image | 512 KB (100 per package) |
Text files must be UTF-8. Binary files are accepted only inside the companion-* folders, and every one of
them must be referenced by a model.json, character.json or item.json — an unreferenced file refuses the
publish, naming it.