Build and share plugins

A plugin is an npm package with a raea key: components your compositions import, and tools, panels and item types in the raea app, each sandboxed with only the permissions and hosts the person approved. Build one in a folder, test it with hot reload, and share it on npm or Git.

View as Markdown

A plugin adds to raea what it doesn't have yet. It's an ordinary npm package whose package.json has a "raea" key, and it can add to either side of raea, or to both:

  • The engine. Components, parts and systems your compositions import by the package's name, like any other package. raea looks for them in your plugins after the project's own packages, so a component installed once works in every project.
  • The app. A tool on feed items, a panel beside the feed, or a new kind of feed item that agents post. Each one is a React .tsx file that the app runs in a sandboxed frame of its own.

Plugins are installed per person. The app keeps them in ~/.raea/plugins (or RAEA_PLUGINS), and they're there in every project you open.

The manifest

JSON
{
  "name": "@you/raea-palette",
  "version": "0.1.0",
  "description": "Pull a palette from any still and pin it to the item.",
  "raea": {
    "title": "Palette",
    "raea": ">=0.1.0",
    "engine": { "imports": ["@you/raea-palette"] },
    "app": {
      "contributes": [
        { "point": "tool", "id": "palette", "title": "Palette", "entry": "./app/Palette.tsx", "on": ["look", "watch"] },
        { "point": "panel", "id": "swatches", "title": "Swatches", "entry": "./app/Swatches.tsx" },
        { "point": "item", "id": "board", "title": "Palette board", "entry": "./app/Board.tsx" }
      ],
      "permissions": ["items:read", "comments:write", "notify"],
      "network": ["api.example.com"]
    }
  }
}
keywhat it says
titlethe name the person sees on the Plugins page
raeathe raea versions it works with, as an npm range
engine.importswhat compositions import from it (the package's own exports are what actually resolve)
app.contributeseach part it adds to the app: point (tool, panel or item), an id of lowercase letters, digits and dashes, a title, and an entry, a .tsx inside the package
onfor a tool: the item types it shows on (watch, look, listen, play, artifact, plugin); every type when left out
app.permissionswhat it may ask of the app beyond reading what it's shown on
app.networkthe hosts its code may reach, exactly (api.example.com) or a subdomain wildcard (*.example.com); none when empty

A package without a "raea" key isn't a plugin, and the app refuses it at install, naming every problem it found.

Where a contribution shows

  • tool: a button in an item's detail view. Pressing it opens your component in a pane beside the item, and it gets the item it's on.
  • panel: a toggle on the feed that opens your component beside it, for the project as a whole.
  • item: a kind of feed item. An agent posts one with feed_post, type plugin, naming your plugin and the view, as "@you/raea-palette#board", and a JSON file that becomes the item's data. The feed shows the item with your component. If someone opens the item without your plugin, it says which plugin it needs.

Talking to the app: @raea/app

Your component is the .tsx file's default export. It imports react, @raea/app and its own files (.ts, .tsx, .js, .json), plus any packages it depends on. Tailwind classes work and use the app's own colours, so text-ink and bg-raised look right in both themes.

TSX
import { useState } from "react"
import { app, usePlugin, useItem } from "@raea/app"

export default function Palette() {
  const ctx = usePlugin()   // where it's shown: point, project, itemId, and an item's data
  const item = useItem()    // the item it's on: title, the agent's ask, its media with URLs (null in a panel)
  const [said, setSaid] = useState("")
  const pin = async () => {
    await app.comment("Palette: #1d2a3a #c58b4e #efe4d2")
    await app.notify("Pinned the palette")
    setSaid("pinned")
  }
  return (
    <main className="p-4 text-ink">
      <h1 className="text-sm font-medium">{item?.title}</h1>
      <button className="mt-3 rounded-md border border-line px-3 py-1.5 text-xs" onClick={pin}>Pin the palette</button>
      <p className="mt-2 text-xs text-ink-3">{said}</p>
    </main>
  )
}
callneedswhat it does
app.context() / usePlugin()nothingthe plugin, the contribution, its point, the project, the item it's on, a plugin item's data
app.item() / useItem()nothingthe item it's shown on, when there is one
app.items()items:readthe project's feed items and their notes
app.comment(text, pin?, itemId?)comments:writea note on the item, optionally pinned to a place on it (0 to 1 across and down), or on another item by id
app.notify(text)notifya short notice for the person

A call without its permission is refused with an error that names the missing permission, for example "items needs the items:read permission, which this plugin wasn't given". A call that doesn't exist is refused and the error lists the calls that do.

What a plugin can't do

The frame is a sandbox. It has no access to the app's own code, your keys, the app's host or another plugin's frame. @raea/app is its only way to reach the app, and only fetch, XMLHttpRequest, WebSocket, EventSource and sendBeacon calls to the hosts in network get through. Anything else fails with an error that names the hosts the plugin may reach. Imports of Node's built-in modules (node:fs and the like) are refused, and so are files outside the plugin's folder.

Build and test it

  1. Make a folder with the package.json above and your .tsx files.
  2. In the raea app, open Plugins. Under Installed, type the folder's path (~/code/raea-palette) and press Install.
  3. The plugin installs off. Its card shows what it adds, what it may do and which hosts it reaches. Press Approve and turn on.
  4. Open an item and press your tool, or toggle your panel on the feed.
  5. Edit the .tsx and save. A folder plugin is linked, not copied, so the app rebuilds it and the frame reloads in place. A compile error shows in the frame with its file and line.

To try an item view, ask your agent to post one: "post a palette board for this still with @you/raea-palette#board". It writes the JSON and posts it with feed_post.

Components for compositions

The same package can export components for the engine. Once the plugin is installed, any composition imports them by the package's name:

TSX
import { Grain } from "@you/raea-grain"

raea resolves an import from the project's own node_modules first, then from your plugins. A project that installs its own copy of a package keeps that copy. Components follow the rules every composition follows: every value comes from the frame, so a render matches a play. The SDK lists what they can import.

Share it

A plugin is shared the way any npm package is. Nobody uploads anything to raea.

  • npm: npm publish. People install it by typing its name (@you/raea-palette) on the Plugins page.
  • Git: push the folder to a repository. People install it with the Git URL (github:you/raea-palette or git+https://github.com/you/raea-palette.git).

Installing runs none of the package's code. npm's install scripts are off, and nothing in the plugin runs until the person has read its card and approved it. Turning a plugin off stops it everywhere without uninstalling it, and Remove uninstalls it. Neither touches a project's files.

Not built yet

  • raea add and raea app dev verbs for plugins.
  • Checking a plugin's raea range when it loads.
  • Rail tabs, commands, agent tools and settings pages as contribution points.
  • A browsable index of plugins with verified sources.

A native Bevy plugin (a Rust crate) still has to be in raea's catalog or built into raea from source. The Bevy plugin guide in the raea skill covers those.