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
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-motions/<key>/motions.json + the clip files it names (.vrma / .glb / .fbx)
companion-models/<key>/model.json + the avatar (.vrm / .glb) and an optional thumbnail
companion-characters/<key>/character.json + an optional thumbnail
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. 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.
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. 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. |
version |
<major>.<minor> only, for the official-plugin pipeline and the local upload script. See Publishing. When you publish through the console or the API you give the full version at publish time instead, and 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. The official Discord readme is a good model: numbered setup steps, every setting explained, and what each error means.
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.
Tools: the downloadable types don't declare the
toolsregistry yet. If you register tools from TypeScript, add the declaration shown in Tools until the typings include it.
Commands, skills and companion 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-motions/<key>/ |
the folder name | motions.json |
Companion packs |
companion-models/<key>/ |
the folder name | model.json |
Companion packs |
companion-characters/<key>/ |
the folder name | character.json |
Companion packs |
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-motions/fox/ and companion-models/fox/. Give them different keys
(fox-moves, fox-model).
Limits
| What | Limit |
|---|---|
Source files (.ts/.js) |
50 files, 1 MB each |
| Bundled code after merging | 2 MB |
| Any other text file (readme, command, skill file, companion JSON) | 256 KB each |
| Files per skill folder | 100 |
| Zip entries | 4,000 |
| Whole package, uncompressed | 256 MB |
| Companion avatar model | 100 MB |
| Companion animation clip | 32 MB |
| Thumbnail image (PNG, JPEG or WebP) | 2 MB |
Text files must be UTF-8. Binary files are accepted only inside the three companion-* folders, and every one of
them must be referenced by a motions.json, model.json or character.json — an unreferenced file refuses the
publish, naming it.