# Package format

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.

```markdown
---
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](publishing.md#versions). 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.svg` or `icon.png` at 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`/`.jpeg` and `.webp` pictures up to 5 MB each,
  and `.mp4` or `.webm` videos up to 50 MB each. A listed `gallery` file that isn't in the folder fails the publish.

Binary files only travel in a zip: publishing from the editor (a JSON file list) gives that version no gallery, and
its icon is the one set with **Add icon** in the editor (`PUT /plugins/{id}/draft/icon`).

## 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.

```ts
// 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`:

```ts
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](sandbox-model.md).

## 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):

```json
{
  "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.

3. 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](plugin-commands-and-skills.md) |
| `skills/<key>/` | the folder name | exactly one `skill.md` (any letter case) with frontmatter `description` | [Commands and skills](plugin-commands-and-skills.md) |
| `companion-models/<key>/` | the folder name | `model.json` | [Companion packs](plugin-companion-packs.md) |
| `companion-characters/<key>/` | the folder name | `character.json` | [Companion packs](plugin-companion-packs.md) |
| `companion-items/<key>/` | the folder name | `item.json` | [Companion packs](plugin-companion-packs.md#item--itemjson) |
| `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.
