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 (.ts or .js): it is the entry point, whatever its name.
  • More than one: a root index.ts or index.js is required and is the entry point (index.ts wins 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.userMessageInbound inside events.pluginInstalled. Nothing runs that handler at publish time, so the capability never exists.
  • Don't compute registrations from host.* — host.settings.get() returns undefined at publish time, and a host.fetch at the top level fails the publish.
  • Keep the manifest and tool metadata plain JSON. Functions, class instances or cyclic objects in globalThis.manifest or in a tool's description/parameters fail the publish.

Type-checking with TypeScript

  1. 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.
  2. 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.

  1. Run npx tsc -p . before every publish.

Tools: the downloadable types don't declare the tools registry 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.