Bee Box documentation · directory: https://beebox.run/docs/concepts/ · index: https://beebox.run/docs/concepts/index.md · root: https://beebox.run/llms.txt # Plugins A plugin is a typed library shipped inside the `beebox` package and reached through the public subpath `beebox/plugins/`. A box uses a plugin by listing it in `_config/box.json` and writing small stubs that import and extend it. Nothing new runs at runtime: a stub is an ordinary box schema, view or trick, loaded by the mechanisms that load every other one. The design and its decisions: plugins plan, design notes. The standing position ("knowledge, not plugins": no registry service, no marketplace, no lifecycle framework) is in [extensibility](https://beebox.run/docs/design/extensibility.md) and still holds. ## Three tiers Before writing a plugin, pick the tier. | Tier | Use it when | Artifact | |---|---|---| | Instruction | The agent can call a service with what it already has (omdb, tmdb) | A documentation entry; the service is consumed, not integrated | | Trick | A script that integrates with nothing | A well-structured trick the docs point at; the agent copies it into `src/tricks/scripts/` | | Plugin | The engine must know about it: a card type it validates, a view, a lint over cards, a health check | `src/plugins//` | A plugin exists only for the third case. ## Vocabulary - **Installed**: present in the engine package. Every in-repo plugin, always. - **Active**: named in `_config/box.json` under `plugins`. The only mechanical state. Active means: the plugin's skill is mirrored into `.claude/skills//`, its lint and health hooks run, and `bbx plugins list` shows it as active. New boxes activate nothing. - **Base**: a `CardSchemaConfig` the plugin exports without a type name, keyed by its default type name in `schemas`. - **Stub**: the box file that completes a base, re-exports a view, or calls a script. The box owns it; the engine never writes or overwrites it. - **Effective schemas**: built-in types plus the box's loaded stubs. Health is computed from these, never from the active list alone. ## Layout ``` src/plugins// plugin.ts default-exports definePlugin({...}); Node-safe view.tsx optional; browser-safe; the stub re-exports it README.md What it is · Setup · Conventions · Migration · Uninstall skill.ts exports the skill body as a string; the engine writes the frontmatter src/plugins.ts the registry; add the member here ``` `plugin.ts` and `view.tsx` are separate entries because `plugin.ts` may import `node:fs` for a health check while `view.tsx` must bundle for the browser. The build emits `dist/plugins//plugin.js` and `view.js`; the export map names both. ## The contract ```ts import { definePlugin } from "../../cards/plugin-definition.js"; export default definePlugin({ name: "courseware", // equals the directory name description: "Courses, lesson plans, learner progress", // passes the brief lint docs: "src/plugins/courseware/README.md", skill: SKILL_BODY, schemas: { course: courseBase, "lesson-plan": lessonPlanBase }, // key = default type name views: [{ name: "concept-map", rendersCardTypes: ["concept-map"] }], healthChecks: async (boxRoot) => [...], // read-only lintCards: async (input, ctx) => [...], // box-aware; uses ctx, never engine imports }); ``` The declarative fields are read without running anything. The two hooks are the whole hook surface; add a new hook kind only with a real consumer and a plan. ## What stays core The connectors (Gmail, Google Calendar, Google Drive) stay core. They are the box's intake for mail, calendar and drive, and intake is medium; they share Google auth, the admin connect flow, the scheduler's auth alerts and the default schedules with the engine, and import 44 engine modules the boundary above forbids. The sizing and the decision are in `docs/implemented-plans/plugins-slice-3.md`. A source a box is *about* (a feed reader, say) is content and would be a plugin; it would be the real consumer for a `connectors` hook, which does not exist until then. ## The import boundary A file under `src/plugins//` may import only `beebox/{cards,schema,view-widgets}` through `src/exports/*`, `src/cards/plugin-definition.ts`, its own directory, and external packages. `view.tsx` may not import `node:*`. The `plugin-imports` layout rule and an ESLint block enforce this. The rule is what keeps an in-repo plugin from being core by another name: if a plugin needs an engine internal, the engine is missing a public piece, and that is a separate change. ## Stubs Schema stub, `/src/schemas/progress.ts`: ```ts import { cardSchema, extendSchema } from "beebox/cards"; import { z } from "beebox/schema"; import courseware from "beebox/plugins/courseware"; export default cardSchema("progress", extendSchema(courseware.schemas.progress, { fields: { mood: z.string().optional() }, })); ``` `extendSchema` merges fields (a key present in both is a type error), runs both `validate` and both `superRefine` hooks, chains `summarize`, and concatenates `instructions`. Scalar options are delta-wins. View stub, `/src/views/concept-map.tsx`: ```ts export { default } from "beebox/plugins/courseware/view"; export const rendersCardTypes = ["concept-map"]; ``` Stubs use static imports; the health checks find plugin references by scanning import specifiers. ## Activation is agent-run The README's Setup section lists the exact stubs. The agent adds the name to `plugins` in `_config/box.json`, writes the stubs, and commits. No plugin code runs at activation. `bbx plugins list` shows every installed plugin, active or not, with its description and README path; the agent guide carries one standing line pointing at it. ## Health Core owns these checks; they derive from effective schemas and the active list together: - `plugin-config`: an entry in the box's `plugins` list names no installed plugin, or the field is not an array of names. Error. Same wording as `bbx status` and `bbx plugins list`. - `plugin-type-unprovided`: cards of a type with no effective schema that some plugin declares. Error. Covers both "inactive" and "active but no stub". - `plugin-declared-missing`: an active plugin declares a type with no effective schema (no stub file, or a stub file that defines no schema of that type) or a view with no stub file. Error. `bbx plugins list` shows the same: `present`, `missing`, or `present, defines no schema`. - `plugin-stub-inactive`: a stub imports a plugin that is not active. Warning. - `plugin-stub-missing`: a stub imports a plugin that does not exist, or the stub failed to load. Error. - `skill-name-conflict`: a managed skill's name (a plugin's or a static one) collides with an unmarked box skill; the engine did not overwrite it. Warning. - `legacy-exposition-rules`: an exposition-plan card still carries a non-empty `rules` field (courseware README, Migration). Warning. Core owns it so it fires while the plugin is inactive. - `/...`: the plugin's own `healthChecks`, prefixed. `bbx upgrade` validates the whole box under the new engine after migrations, then runs `bbx health --json` there and prints the failing rows above under `Plugin health after upgrade:`. Validation errors and warnings and failing health rows are reported and the upgrade finishes; they never block it. ## Deactivation, removal, rot Remove the name from `box.json`. The stubs stay as box code and keep compiling; the skill the engine wrote is retired (marked files only). A plugin removed from the engine breaks its stubs' imports: the loader reports it, `plugin-stub-missing` names it, and `bbx upgrade` fails typecheck before committing. The project never removes a plugin in the release that adds its replacement, and the README's Migration section names the script to run. ## Conventions `test/plugins.test.ts` loops every registered plugin and checks the rules marked (checked). The rest are convention only. - **name**: equals the directory under `src/plugins/`, kebab-case (checked). - **docs**: names the README, which has the five sections What it is, Setup, Conventions, Migration, Uninstall (checked). - **stubs**: every base key has a stub `src/schemas/.ts` in the README; every view has a stub `src/views/.tsx` and a `view.tsx` entry (checked). - **stubs validate**: the README stubs written into an empty temp box compile, load, and pass `bbx validate` with exit 0 and no error lines (checked). - **scoped tags**: keys are kebab-case and not in the core table (checked; `RedeclaredTagError` also fires at load). - **render names**: a plugin's tag render names are unique against the widget's component map (convention). Core names, from `buildRenderConfig` in `src/frontend/src/components/Markdown/body.tsx`: Fragment, Para, Blockquote, Link, Img, QuoteInline, QuoteBlock, SourceInline, SourceBlock, Purpose, Correction, Task, CaptureImage, Silence, RedactedInline, RedactedBlock, TodoInline, TodoBlock, SeeAlso, UnknownTagInline, UnknownTagBlock. - **CSS**: classes are prefixed `bbx--` (convention). - **tricks**: a trick a README asks the agent to write is named `-` (convention). - **tags on a type**: a box-local type or plugin base declares tags on the schema with `markdocTags`. They validate only in that type's body and render as components only through a view passing `tags` to `Markdown`; elsewhere they show as authored text. - **setup test**: each plugin's setup test writes the README stubs into a temp box (`readmeStubs(name)` in `test/helpers/plugin-readme.ts`) and runs `bbx validate`, so the docs cannot drift from what compiles. - **migration**: manual and agent-run. A plugin may ship a mechanical update script; the README tells the agent to run it. - **skill**: the body lives in `skill.ts`, exported as a string; the engine writes the frontmatter. - **types**: default type names are what cross-card checks rely on. A box may rename a type; the README says which checks that disables. - **secrets and hooks**: a plugin ships no secrets; a script's `secrets.json` is the box's. Health checks are read-only. Lint hooks receive a `LintContext` and import nothing from the engine.