chore: import upstream snapshot with attribution
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:43:05 +08:00
commit 426e9eeabd
41828 changed files with 9656266 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
# @elizaos/registry
In-repo source of truth for the elizaOS community plugin registry. Replaces the
archived external `elizaos-plugins/registry` repo
([elizaOS/eliza#8173](https://github.com/elizaOS/eliza/issues/8173)). Repo-wide
rules live in the root [AGENTS.md](../../AGENTS.md).
## Role
Two registries live here, exported under separate subpaths — **do not conflate
their schemas** (they model different things):
- **`.` (community / third-party)** — the third-party plugin registry **data**
plus the **tooling** to validate it and build the wire format the runtime
fetches. Consumed as registry data (over HTTP at `plugins.elizacloud.ai`) and,
optionally, as a typed loader via `workspace:*`. Dependency-free, hand-rolled
validation.
- **`./first-party` (curated, in-repo)** — the first-party curated registry of
bundled apps / plugins / connectors (moved here from `@elizaos/app-core`). Rich
Zod schema with `config` fields, `render` hints, `launch.routePlugin`, and
connector `accounts`. Exposes `loadRegistry()` + typed accessors and a
plugin-side `registerRegistryEntry()` runtime overlay. Re-exported by
`@elizaos/app-core/registry` for backwards compatibility.
Published to npm (`@elizaos/registry`, `publishConfig.access: public`). It ships
its `src/` as raw TypeScript — same publish-as-source convention as
`@elizaos/prompts` — with `files: ["src"]` so the first-party JSON under
`src/first-party/` travels with it. It was un-privatized in #15833: published
`@elizaos/shared` / `@elizaos/agent` / `@elizaos/app-core` pin it via
`workspace:*`, so while it was `private: true` the rewritten pin 404'd for every
external `npm install` of the beta line. `packages/scripts/publish-graph-guard.mjs`
guards that invariant for the whole workspace going forward.
## Layout
```
entries/third-party/*.json one source entry per community package (SoT)
schema/registry-entry.schema.json JSON Schema mirroring src/schema.ts
generated-registry.json built wire format ({ registry: { "<pkg>": {…} } })
src/
types.ts RegistryEntry (source) + GeneratedRegistry (wire) types
schema.ts validateRegistryEntry / assertRegistryEntry (dependency-free)
loader.ts loadThirdPartyEntries — read + validate entries/third-party
generate.ts generateRegistry / toGeneratedEntry — entries → wire format
validate-cli.ts `bun run validate`
index.ts public barrel (typed loader for programmatic consumers)
first-party/ @elizaos/registry/first-party — curated bundled registry
schema.ts Zod entry schemas (app / plugin / connector)
loader.ts loadRegistryFromRawEntries / indexEntries / typed accessors
index.ts loadRegistry() (reads generated.json) + registerRegistryEntry()
app-registry.ts registerCuratedApp curated-app name store
generate.ts aggregator: plugin-owned + curated/ -> generated.json
generated.json built aggregate the runtime reads (one file; commit it)
curated/{apps,plugins,connectors}/*.json entries with no vendored package
```
## First-party registration is plugin-side
Each in-repo plugin/package **owns its registry entry** as a `registry-entry.json`
in its own directory (a single entry object, or an array). Curated entries with
no vendored package — built-in app-viewers and entries for plugins not checked
out here — live under `first-party/curated/`. The aggregator gathers both into a
single committed `generated.json` that the runtime reads, so on-device staging is
one file. Plugins may also contribute or override an entry **at runtime** via
`registerRegistryEntry()` (deduped by `id`; runtime entries win).
```bash
bun run --cwd packages/registry generate:first-party # rewrite generated.json
bun run --cwd packages/registry generate:first-party:check # CI drift gate
```
- **Add/change a first-party entry:** edit the plugin's `registry-entry.json`
(or a file under `first-party/curated/`), then `generate:first-party` and
commit the regenerated `generated.json`.
## Two formats — don't conflate
- **Source entry** (`entries/third-party/*.json`): the human/CLI-authored
per-package metadata. Matches `elizaos plugins submit --dry-run` output and
the `ThirdPartyMetadata` shape in `packages/elizaos/src/commands/plugins.ts`.
- **Generated wire registry** (`generated-registry.json`): produced from the
source entries; matches the parser in
`packages/agent/src/services/registry-client-network.ts`. Never hand-edit;
always `bun run generate`.
## Commands
```bash
bun run --cwd packages/registry validate # exits non-zero on a malformed entry
bun run --cwd packages/registry generate # regenerate generated-registry.json
bun run --cwd packages/registry test # vitest
bun run --cwd packages/registry typecheck
```
## How to extend
- **List a plugin:** add `entries/third-party/<package>.json` (see
`README.md` → "Adding a third-party plugin"), then `validate` + `generate` and
commit the regenerated `generated-registry.json` alongside the entry.
- **Change the entry shape:** update `src/types.ts`, `src/schema.ts`, AND
`schema/registry-entry.schema.json` together so the code and the published
JSON Schema stay in lockstep.
## Conventions / gotchas
- The `@elizaos/*` scope is reserved for first-party packages — the validator
rejects it in source entries.
- `generate.ts` and `validate-cli.ts` are run with `bun` (TypeScript directly);
there is no `dist` build step beyond regenerating the JSON.
- Keep the package dependency-free (validation is hand-rolled) so the tooling
runs in any CI context without install ordering concerns.
<!-- BEGIN: evidence-and-e2e-mandate (managed; canonical standard = repo-root AGENTS.md) -->
## ⛔ NON-NEGOTIABLE — evidence, trajectories & real end-to-end tests
> The binding, repo-wide standard is **[AGENTS.md](../../AGENTS.md)**. Read it.
> Nothing in this package is *done* until it is *proven* done — a reviewer must confirm it
> works **without reading the code**, from the artifacts you attach. This applies to **every**
> feature, fix, refactor, and chore here. "Tests pass" is not proof; "CI is green" is not proof.
- **Record AND read model trajectories.** Capture the *actual* inputs and outputs of the model
from a **live** LLM — not the deterministic proxy, not a mock: the prompt, the
providers/context, the raw model output, every tool/action call, and the result. Then **open
the trajectory and review it by hand.** A captured-but-unread trajectory is not evidence
(`packages/scenario-runner/bin/eliza-scenarios run <scenario> --report <out>`).
- **Real, full-featured E2E — no larp.** Every feature ships detailed end-to-end tests that
drive the *real* path end to end. Not the happy "front door" only: cover error paths,
edge/empty/invalid input, concurrency, roles/permissions, and adversarial input. A test that
asserts against a mock/stub/fixture standing in for the thing under test **does not count**.
If the real model/device/chain/connector/account is hard to reach, **make it reachable — that
is the work**, not an excuse to mock. If the existing tests here are shallow or mocked, fixing
them is part of your change.
- **Screenshots + logs at every phase**, plus a **complete walkthrough video/run-through** of
the entire feature or view, start to finish (`bun run test:e2e:record`).
- **Manually review every artifact the change touches** — never just the green check: client
logs (console + network), server logs (`[ClassName] …`), the model trajectories in and out,
before/after full-page screenshots, **and the domain artifacts listed below for this package.**
- **No residuals. No shortcuts.** The goal is not "done" — it is *everything* done. Clear every
blocker by the **hard path**: build the real architecture, stand up the real
model/device/service, actually test it. Never leave a TODO, a stub, a stepping-stone, or a
"follow-up." When unsure, research thoroughly, weigh the options, and ship the best,
highest-effort, production-ready version. Keep going until every possibility is exhausted.
Artifacts → attached inline in the PR (MP4 video, JPG screenshots, logs in `<details>`); attach each evidence type **or**
explicitly mark it N/A with a reason — never leave it blank. If `develop` moved and changed
behavior, **re-capture** evidence; stale proof is worse than none.
**Capture & manually review for this package — runtime / framework:**
- A **live-LLM** scenario trajectory for the runtime path you touched — provider → model → action → evaluator — with the raw `<response>` XML and every tool/action call visible and **read**.
- Backend `[ClassName]` logs proving the message loop, task scheduler, or service actually fired end to end.
- The memory/state artifacts produced — rows written, embeddings, room/world/entity records, scheduled-task rows — inspected, not assumed.
- For shared modules: `build:node` vs full `build` so the browser/edge bundles still compile.
<!-- END: evidence-and-e2e-mandate -->
+153
View File
@@ -0,0 +1,153 @@
# @elizaos/registry
In-repo source of truth for the elizaOS community plugin registry. Replaces the
archived external `elizaos-plugins/registry` repo
([elizaOS/eliza#8173](https://github.com/elizaOS/eliza/issues/8173)). Repo-wide
rules live in the root [AGENTS.md](../../AGENTS.md).
## Role
Two registries live here, exported under separate subpaths — **do not conflate
their schemas** (they model different things):
- **`.` (community / third-party)** — the third-party plugin registry **data**
plus the **tooling** to validate it and build the wire format the runtime
fetches. Consumed as registry data (over HTTP at `plugins.elizacloud.ai`) and,
optionally, as a typed loader via `workspace:*`. Dependency-free, hand-rolled
validation.
- **`./first-party` (curated, in-repo)** — the first-party curated registry of
bundled apps / plugins / connectors (moved here from `@elizaos/app-core`). Rich
Zod schema with `config` fields, `render` hints, `launch.routePlugin`, and
connector `accounts`. Exposes `loadRegistry()` + typed accessors and a
plugin-side `registerRegistryEntry()` runtime overlay. Re-exported by
`@elizaos/app-core/registry` for backwards compatibility.
Published to npm (`@elizaos/registry`, `publishConfig.access: public`). It ships
its `src/` as raw TypeScript — same publish-as-source convention as
`@elizaos/prompts` — with `files: ["src"]` so the first-party JSON under
`src/first-party/` travels with it. It was un-privatized in #15833: published
`@elizaos/shared` / `@elizaos/agent` / `@elizaos/app-core` pin it via
`workspace:*`, so while it was `private: true` the rewritten pin 404'd for every
external `npm install` of the beta line. `packages/scripts/publish-graph-guard.mjs`
guards that invariant for the whole workspace going forward.
## Layout
```
entries/third-party/*.json one source entry per community package (SoT)
schema/registry-entry.schema.json JSON Schema mirroring src/schema.ts
generated-registry.json built wire format ({ registry: { "<pkg>": {…} } })
src/
types.ts RegistryEntry (source) + GeneratedRegistry (wire) types
schema.ts validateRegistryEntry / assertRegistryEntry (dependency-free)
loader.ts loadThirdPartyEntries — read + validate entries/third-party
generate.ts generateRegistry / toGeneratedEntry — entries → wire format
validate-cli.ts `bun run validate`
index.ts public barrel (typed loader for programmatic consumers)
first-party/ @elizaos/registry/first-party — curated bundled registry
schema.ts Zod entry schemas (app / plugin / connector)
loader.ts loadRegistryFromRawEntries / indexEntries / typed accessors
index.ts loadRegistry() (reads generated.json) + registerRegistryEntry()
app-registry.ts registerCuratedApp curated-app name store
generate.ts aggregator: plugin-owned + curated/ -> generated.json
generated.json built aggregate the runtime reads (one file; commit it)
curated/{apps,plugins,connectors}/*.json entries with no vendored package
```
## First-party registration is plugin-side
Each in-repo plugin/package **owns its registry entry** as a `registry-entry.json`
in its own directory (a single entry object, or an array). Curated entries with
no vendored package — built-in app-viewers and entries for plugins not checked
out here — live under `first-party/curated/`. The aggregator gathers both into a
single committed `generated.json` that the runtime reads, so on-device staging is
one file. Plugins may also contribute or override an entry **at runtime** via
`registerRegistryEntry()` (deduped by `id`; runtime entries win).
```bash
bun run --cwd packages/registry generate:first-party # rewrite generated.json
bun run --cwd packages/registry generate:first-party:check # CI drift gate
```
- **Add/change a first-party entry:** edit the plugin's `registry-entry.json`
(or a file under `first-party/curated/`), then `generate:first-party` and
commit the regenerated `generated.json`.
## Two formats — don't conflate
- **Source entry** (`entries/third-party/*.json`): the human/CLI-authored
per-package metadata. Matches `elizaos plugins submit --dry-run` output and
the `ThirdPartyMetadata` shape in `packages/elizaos/src/commands/plugins.ts`.
- **Generated wire registry** (`generated-registry.json`): produced from the
source entries; matches the parser in
`packages/agent/src/services/registry-client-network.ts`. Never hand-edit;
always `bun run generate`.
## Commands
```bash
bun run --cwd packages/registry validate # exits non-zero on a malformed entry
bun run --cwd packages/registry generate # regenerate generated-registry.json
bun run --cwd packages/registry test # vitest
bun run --cwd packages/registry typecheck
```
## How to extend
- **List a plugin:** add `entries/third-party/<package>.json` (see
`README.md` → "Adding a third-party plugin"), then `validate` + `generate` and
commit the regenerated `generated-registry.json` alongside the entry.
- **Change the entry shape:** update `src/types.ts`, `src/schema.ts`, AND
`schema/registry-entry.schema.json` together so the code and the published
JSON Schema stay in lockstep.
## Conventions / gotchas
- The `@elizaos/*` scope is reserved for first-party packages — the validator
rejects it in source entries.
- `generate.ts` and `validate-cli.ts` are run with `bun` (TypeScript directly);
there is no `dist` build step beyond regenerating the JSON.
- Keep the package dependency-free (validation is hand-rolled) so the tooling
runs in any CI context without install ordering concerns.
<!-- BEGIN: evidence-and-e2e-mandate (managed; canonical standard = repo-root AGENTS.md) -->
## ⛔ NON-NEGOTIABLE — evidence, trajectories & real end-to-end tests
> The binding, repo-wide standard is **[AGENTS.md](../../AGENTS.md)**. Read it.
> Nothing in this package is *done* until it is *proven* done — a reviewer must confirm it
> works **without reading the code**, from the artifacts you attach. This applies to **every**
> feature, fix, refactor, and chore here. "Tests pass" is not proof; "CI is green" is not proof.
- **Record AND read model trajectories.** Capture the *actual* inputs and outputs of the model
from a **live** LLM — not the deterministic proxy, not a mock: the prompt, the
providers/context, the raw model output, every tool/action call, and the result. Then **open
the trajectory and review it by hand.** A captured-but-unread trajectory is not evidence
(`packages/scenario-runner/bin/eliza-scenarios run <scenario> --report <out>`).
- **Real, full-featured E2E — no larp.** Every feature ships detailed end-to-end tests that
drive the *real* path end to end. Not the happy "front door" only: cover error paths,
edge/empty/invalid input, concurrency, roles/permissions, and adversarial input. A test that
asserts against a mock/stub/fixture standing in for the thing under test **does not count**.
If the real model/device/chain/connector/account is hard to reach, **make it reachable — that
is the work**, not an excuse to mock. If the existing tests here are shallow or mocked, fixing
them is part of your change.
- **Screenshots + logs at every phase**, plus a **complete walkthrough video/run-through** of
the entire feature or view, start to finish (`bun run test:e2e:record`).
- **Manually review every artifact the change touches** — never just the green check: client
logs (console + network), server logs (`[ClassName] …`), the model trajectories in and out,
before/after full-page screenshots, **and the domain artifacts listed below for this package.**
- **No residuals. No shortcuts.** The goal is not "done" — it is *everything* done. Clear every
blocker by the **hard path**: build the real architecture, stand up the real
model/device/service, actually test it. Never leave a TODO, a stub, a stepping-stone, or a
"follow-up." When unsure, research thoroughly, weigh the options, and ship the best,
highest-effort, production-ready version. Keep going until every possibility is exhausted.
Artifacts → attached inline in the PR (MP4 video, JPG screenshots, logs in `<details>`); attach each evidence type **or**
explicitly mark it N/A with a reason — never leave it blank. If `develop` moved and changed
behavior, **re-capture** evidence; stale proof is worse than none.
**Capture & manually review for this package — runtime / framework:**
- A **live-LLM** scenario trajectory for the runtime path you touched — provider → model → action → evaluator — with the raw `<response>` XML and every tool/action call visible and **read**.
- Backend `[ClassName]` logs proving the message loop, task scheduler, or service actually fired end to end.
- The memory/state artifacts produced — rows written, embeddings, room/world/entity records, scheduled-task rows — inspected, not assumed.
- For shared modules: `build:node` vs full `build` so the browser/edge bundles still compile.
<!-- END: evidence-and-e2e-mandate -->
+99
View File
@@ -0,0 +1,99 @@
# @elizaos/registry
In-repo source of truth for the elizaOS **community plugin registry**.
This package replaces the archived, read-only `elizaos-plugins/registry` GitHub
repository that the publish docs used to point at (see
[elizaOS/eliza#8173](https://github.com/elizaOS/eliza/issues/8173)). Third-party
plugins are now listed by adding one JSON file here and opening a pull request
against this monorepo.
## What's in here
```
packages/registry/
├── entries/third-party/ one <package>.json per community package (source of truth)
├── schema/registry-entry.schema.json JSON Schema for an entry
├── generated-registry.json wire format the runtime fetches (built from entries/)
└── src/
├── types.ts RegistryEntry + GeneratedRegistry types
├── schema.ts dependency-free entry validation
├── loader.ts read + validate entries/third-party/*.json
├── generate.ts entries → generated-registry.json
├── validate-cli.ts `bun run validate`
└── index.ts typed loader for programmatic consumers
```
Two formats live here:
1. **Source entry** (`entries/third-party/*.json`) — what contributors and
`elizaos plugins submit --dry-run` author. One file per package.
2. **Generated wire registry** (`generated-registry.json`) — the
`{ registry: { "<package>": { … } } }` shape the runtime consumes from
`plugins.elizacloud.ai/generated-registry.json`. Produced by `generate.ts`;
never hand-edited.
## Commands
```bash
bun run --cwd packages/registry validate # validate every entry against the schema
bun run --cwd packages/registry generate # regenerate generated-registry.json
bun run --cwd packages/registry test # unit tests
bun run --cwd packages/registry typecheck
```
## Adding a third-party plugin
The runtime auto-discovers any npm package whose `keywords` include `elizaos`,
so a listing here is for **discoverability and curation**, not a hard
requirement to run a plugin. To get listed:
1. **Publish your plugin to npm** under your own scope or an unscoped
`elizaos-plugin-*` name. The `@elizaos/*` scope is reserved for first-party
packages and is rejected by the validator.
2. **Generate the entry metadata** from your plugin project:
```bash
elizaos plugins submit . --dry-run
```
This prints the `entries/third-party/<package>.json` the CLI derives from
your `package.json` (name, repository, kind, description, tags).
3. **Add the file** under `entries/third-party/`, named after the package
(`/` → `__`, `@` dropped). For `elizaos-plugin-echo`:
```json
{
"package": "elizaos-plugin-echo",
"repository": "github:elizaOS/eliza",
"kind": "plugin",
"description": "Reference third-party elizaOS plugin: an ECHO action that repeats a message back.",
"homepage": "https://github.com/elizaOS/eliza/tree/main/packages/examples/plugin-echo",
"version": "2.0.0-beta.0",
"directory": "packages/examples/plugin-echo",
"tags": ["example", "utility", "reference"]
}
```
Required: `package`, `repository`, `kind` (`plugin` · `connector` · `app`).
Optional: `description`, `homepage`, `version`, `directory`, `tags`.
4. **Validate and regenerate:**
```bash
bun run --cwd packages/registry validate
bun run --cwd packages/registry generate
```
5. **Open a pull request** with the new entry file and the regenerated
`generated-registry.json`. Community entries are reviewed for security,
functionality, and documentation quality before merge.
## Worked example
[`packages/examples/plugin-echo`](../examples/plugin-echo) is a real, buildable
third-party plugin (a single `ECHO` action) listed via
[`entries/third-party/elizaos-plugin-echo.json`](entries/third-party/elizaos-plugin-echo.json).
Use it as the reference example for your own listing.
@@ -0,0 +1,9 @@
{
"package": "@1claw/plugin-elizaos",
"repository": "github:1clawAI/1claw-elizaos-plugin",
"kind": "plugin",
"description": "HSM-backed secrets + multi-chain signing for elizaOS agents via 1Claw \u2014 vault access and EVM/Solana/BTC/XRP/Cardano/Tron signing without the agent ever holding private keys; optional Shroud TEE LLM proxy.",
"homepage": "https://github.com/1clawAI/1claw-elizaos-plugin",
"version": "0.1.0",
"tags": ["security", "secrets", "vault", "signing", "multi-chain"]
}
@@ -0,0 +1,18 @@
{
"package": "@agentradar/eliza-plugin",
"repository": "github:Bichev/agentradar-integrations",
"kind": "plugin",
"description": "AgentRadar trust for elizaOS — a VERIFY_AGENT action and trust provider that check a wallet/agent's on-chain trust score (ERC-8004 reputation + scam database + static analysis) before your agent interacts with or pays it.",
"homepage": "https://github.com/Bichev/agentradar-integrations/tree/main/eliza-plugin-agentradar",
"version": "0.1.0",
"directory": "eliza-plugin-agentradar",
"tags": [
"trust",
"verification",
"x402",
"erc-8004",
"security",
"agent-economy",
"reputation"
]
}
@@ -0,0 +1,9 @@
{
"package": "blackwall-eliza-guardrail",
"repository": "github:bluetieroperations-create/blackwall-eliza-guardrail",
"kind": "plugin",
"description": "BLACK_WALL pre-action risk guardrail for elizaOS agents - wraps every action handler with a forecast() check so STOP-rated actions abort before they run; gateCall() adds per-call gating for multi-step handlers. Observe mode by default (zero behavior change).",
"homepage": "https://github.com/bluetieroperations-create/blackwall-eliza-guardrail",
"version": "0.2.1",
"tags": ["security", "guardrails", "ai-safety", "risk-gate", "onchain-safety"]
}
@@ -0,0 +1,23 @@
{
"package": "eliza-plugin-klymax402",
"repository": "github:Br0ski777/eliza-plugin-klymax402",
"kind": "plugin",
"description": "15 x402 pay-per-call APIs on Base for ElizaOS agents — stock prices, B2B enrichment, web search, crypto funding rates, SEO analysis, NLP, SSL/DNS checks and more. Agents pay USDC per call with no API keys.",
"homepage": "https://klymax402.com",
"version": "0.1.0",
"tags": [
"x402",
"payments",
"base",
"usdc",
"micropayments",
"stock-price",
"b2b-enrichment",
"web-search",
"crypto",
"defi",
"seo",
"nlp",
"agentic-payments"
]
}
@@ -0,0 +1,21 @@
{
"package": "elizaos-plugin-coinrailz",
"repository": "github:tdnupe3/elizaos-plugin-coinrailz",
"kind": "plugin",
"description": "Agent Treasury + Payments: non-custodial USDC yield on Solana (Kamino, ~3.4% APY) and Base (ERC-4626 routing via Aave/Compound/Morpho), plus 65 live x402 pay-per-call services for inference, IoT data, satellite imagery, and execution.",
"homepage": "https://coinrailz.com",
"version": "2.1.0",
"tags": [
"payments",
"x402",
"usdc",
"yield",
"treasury",
"solana",
"base",
"defi",
"micropayments",
"iot",
"satellite"
]
}
@@ -0,0 +1,10 @@
{
"package": "elizaos-plugin-echo",
"repository": "github:elizaOS/eliza",
"kind": "plugin",
"description": "Reference third-party elizaOS plugin: an ECHO action that repeats a message back.",
"homepage": "https://github.com/elizaOS/eliza/tree/main/packages/examples/plugin-echo",
"version": "2.0.0-beta.0",
"directory": "packages/examples/plugin-echo",
"tags": ["example", "utility", "reference"]
}
@@ -0,0 +1,8 @@
{
"package": "elizaos-plugin-neynar-search",
"repository": "github:xavier-arosemena/elizaos-plugin-neynar-search",
"kind": "plugin",
"description": "Farcaster engagement discovery, like, reply, and follow/unfollow actions via Neynar REST API for ElizaOS agents",
"version": "0.1.0",
"tags": ["farcaster", "neynar", "social", "discovery"]
}
@@ -0,0 +1,19 @@
{
"package": "elizaos-plugin-quantumscan",
"repository": "github:quantumscan-io/elizaos-plugin-quantumscan",
"kind": "plugin",
"description": "Post-quantum cryptography (PQC) vulnerability scanner for GitHub, GitLab, and Bitbucket repositories. Detects ECDSA, RSA, DH, AES-128, and other quantum-vulnerable algorithms. Returns risk scores and NIST FIPS 203/204/205 migration targets.",
"homepage": "https://quantumscan.io",
"version": "1.0.2",
"tags": [
"security",
"pqc",
"post-quantum",
"cryptography",
"blockchain",
"nist",
"cbom",
"vulnerability-scanner",
"quantum-computing"
]
}
@@ -0,0 +1,8 @@
{
"package": "elizaos-plugin-reddit-search",
"repository": "github:xavier-arosemena/elizaos-plugin-reddit-search",
"kind": "plugin",
"description": "ElizaOS plugin for Reddit integration — search, engage, and manage Reddit interactions autonomously",
"version": "0.1.0",
"tags": ["reddit", "social", "discovery"]
}
@@ -0,0 +1,19 @@
{
"package": "elizaos-plugin-true402",
"repository": "github:true402/elizaos-plugin-true402",
"kind": "plugin",
"description": "Pre-trade rug/honeypot guard for Base tokens — real buy/sell simulation plus liquidity and ownership checks, pay-per-call over x402 (USDC on Base). No accounts, no API keys; free daily trial.",
"homepage": "https://true402.dev",
"version": "0.1.1",
"tags": [
"x402",
"base",
"usdc",
"token-safety",
"rug-check",
"honeypot",
"defi",
"micropayments",
"trading"
]
}
@@ -0,0 +1,9 @@
{
"package": "@human.tech/plugin-waap",
"repository": "github:holonym-foundation/eliza-plugin-waap",
"kind": "plugin",
"description": "WaaP wallet plugin for ElizaOS — 2-of-2 MPC signing for EVM and Sui, with native 2FA and server-enforced spending policies; the agent never holds private keys.",
"homepage": "https://github.com/holonym-foundation/eliza-plugin-waap",
"version": "0.1.3",
"tags": ["wallet", "mpc", "2pc", "evm", "sui", "2fa"]
}
@@ -0,0 +1,9 @@
{
"package": "plugin-agentscoin",
"repository": "github:axiosdevs/plugin-agentscoin",
"kind": "plugin",
"description": "Give your elizaOS agent an AgentsCoin wallet: create wallet, mine AGENT, check balance, send.",
"homepage": "https://agents-coin.com",
"version": "0.1.0",
"tags": ["crypto", "wallet", "ai-agents", "agentic-payments", "evm", "mcp"]
}
@@ -0,0 +1,16 @@
{
"package": "plugin-raven-verify",
"repository": "github:billybotticelli4u-collab/plugin-raven-verify",
"kind": "plugin",
"description": "Pre-action Solana token verification via Raven — signed, scope-bounded on-chain evidence receipts (checks performed, checks NOT performed, coverage gaps, ed25519 signature). Reports evidence, not a safe/unsafe verdict.",
"homepage": "https://ravenattest.com",
"version": "0.2.0",
"tags": [
"solana",
"token",
"verification",
"attestation",
"raven",
"signed-receipt"
]
}
@@ -0,0 +1,20 @@
{
"package": "plugin-x402-alpha-data",
"repository": "github:sna4an/plugin-x402-alpha-data",
"kind": "plugin",
"description": "Paid alpha data endpoints via x402 protocol \u2014 token safety, whale tracking, yield strategies, governance, sentiment.",
"homepage": "https://github.com/sna4an/plugin-x402-alpha-data",
"version": "1.0.0",
"tags": [
"x402",
"defi",
"alpha",
"whale-tracking",
"token-safety",
"yield",
"solana",
"base",
"web3",
"paid-api"
]
}
@@ -0,0 +1,18 @@
{
"package": "plugin-x402-endpoints",
"repository": "github:digitalweb33333-creator/plugin-x402-endpoints",
"kind": "plugin",
"description": "Exposes the x402-endpoints catalogue (28 paid data endpoints: official EU/global registries — GLEIF, VIES, BODACC, EUR-Lex, Companies House, EPO, sanctions, CVE, FDA — plus crypto pre-trade data) as native ElizaOS actions, billed per call via x402 (USDC on Base mainnet).",
"homepage": "https://github.com/digitalweb33333-creator/plugin-x402-endpoints",
"version": "0.1.0",
"tags": [
"x402",
"micropayments",
"base",
"usdc",
"compliance",
"crypto",
"data",
"kyb"
]
}
@@ -0,0 +1,16 @@
{
"package": "@seekdaseek/plugin-agentfeed",
"repository": "github:seekdaseek/plugin-agentfeed",
"kind": "plugin",
"description": "Live crypto market data for trading agents \u2014 liquidations, long/short positioning, open interest, funding rates, SOL/BTC prices via Pyth, and SPL token rug-risk checks. Paid per-call in USDC via x402 on Solana ($0.001-$0.01/call), no API keys or subscriptions. Built-in per-call spend cap.",
"homepage": "https://github.com/seekdaseek/plugin-agentfeed",
"version": "0.1.2",
"tags": [
"market-data",
"trading",
"solana",
"x402",
"liquidations",
"crypto"
]
}
@@ -0,0 +1,17 @@
{
"package": "@stvor/plugin-agent-commerce",
"repository": "github:stvor-hq/plugin-agent-commerce",
"kind": "plugin",
"description": "ERC-8183 agentic commerce for elizaOS: job lifecycle state machine, SHA-256 payload attestation, reputation gating, and prompt-injection protection.",
"homepage": "https://github.com/stvor-hq/plugin-agent-commerce",
"version": "0.2.0",
"tags": [
"commerce",
"erc8183",
"escrow",
"reputation",
"security",
"rate-limiting",
"prompt-injection"
]
}
@@ -0,0 +1,16 @@
{
"package": "@thecolony/elizaos-plugin",
"repository": "github:TheColonyCC/elizaos-plugin",
"kind": "plugin",
"description": "The Colony social plugin for elizaOS agents — post, reply, DM, vote, react, search, follow, manage sub-colonies, and run autonomously on thecolony.cc, the AI-agent-only social network.",
"homepage": "https://thecolony.cc",
"version": "0.34.0",
"tags": [
"thecolony",
"colony",
"social",
"social-network",
"agent-economy",
"autonomous-agents"
]
}
@@ -0,0 +1,18 @@
{
"package": "@usenami/plugin-funding",
"repository": "github:namixai/plugin-funding",
"kind": "plugin",
"description": "Multi-venue perpetual funding-rate data & cross-exchange funding-arbitrage for elizaOS agents — across 20+ exchanges incl. Hyperliquid HIP-3 sub-DEXes. Free FUNDING_SCREENER; paid FUNDING_ARB + FUNDING_SPREAD settle per-call over x402 (auto-pay in USDC on Base).",
"homepage": "https://usenami.io",
"version": "0.1.0",
"tags": [
"funding-rate",
"perpetuals",
"arbitrage",
"funding-arbitrage",
"x402",
"market-data",
"trading",
"hyperliquid"
]
}
@@ -0,0 +1,17 @@
{
"package": "@usenami/plugin-signer",
"repository": "github:namixai/plugin-signer",
"kind": "plugin",
"description": "Keyless CEX/DEX perp order signing for elizaOS agents — HMAC/EIP-712 signatures are computed inside an AWS Nitro Enclave, so the venue API key never enters the agent process. Covers 6 venues: Binance, OKX, Asterdex, KuCoin, Bybit, Hyperliquid.",
"homepage": "https://usenami.io/signer",
"version": "0.2.0",
"tags": [
"trading",
"perpetuals",
"cex",
"dex",
"signing",
"security",
"nitro-enclave"
]
}
@@ -0,0 +1,18 @@
{
"package": "@wasmagent/eliza-rollout-plugin",
"repository": "github:WasmAgent/wasmagent-js",
"kind": "plugin",
"description": "Captures elizaOS action runs as rollout-wire/v1 training records (DPO/PPO JSONL) for fine-tuning via evomerge. No sandbox layer — elizaOS Bun Worker isolation already covers that.",
"homepage": "https://github.com/WasmAgent/wasmagent-js/tree/main/packages/eliza-rollout-plugin",
"version": "1.0.3",
"directory": "packages/eliza-rollout-plugin",
"tags": [
"training-data",
"rlaif",
"dpo",
"ppo",
"rollout",
"fine-tuning",
"wasmagent"
]
}
@@ -0,0 +1,18 @@
{
"package": "@xynqai/plugin-eliza",
"repository": "github:visionsXBT/xynq",
"kind": "plugin",
"description": "XYNQ decentralized inference for ElizaOS — OpenAI-compatible models, live GPU mesh status, USDC worker earnings, and Solana credit top-ups.",
"homepage": "https://github.com/visionsXBT/xynq/tree/main/plugin-eliza",
"version": "0.1.1",
"directory": "plugin-eliza",
"tags": [
"llm",
"inference",
"decentralized",
"solana",
"gpu",
"crypto",
"usdc"
]
}
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
{
"name": "@elizaos/registry",
"version": "2.0.0-beta.0",
"description": "In-repo source of truth for the elizaOS community plugin registry: third-party entry data, JSON schema, validation, generator, and a typed loader.",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/elizaOS/eliza/tree/main/packages/registry",
"repository": {
"type": "git",
"url": "git+https://github.com/elizaOS/eliza.git",
"directory": "packages/registry"
},
"keywords": [
"elizaos",
"registry",
"plugins"
],
"files": [
"src"
],
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=24.0.0"
},
"exports": {
"./package.json": "./package.json",
".": {
"types": "./src/index.ts",
"import": "./src/index.ts",
"default": "./src/index.ts"
},
"./first-party": {
"types": "./src/first-party/index.ts",
"import": "./src/first-party/index.ts",
"default": "./src/first-party/index.ts"
},
"./first-party/generated.json": "./src/first-party/generated.json",
"./first-party/curated-app-definitions.json": "./src/first-party/curated-app-definitions.json",
"./first-party/channel-plugin-map.json": "./src/first-party/channel-plugin-map.json",
"./first-party/provider-plugin-map.json": "./src/first-party/provider-plugin-map.json",
"./first-party/short-id-plugin-map.json": "./src/first-party/short-id-plugin-map.json"
},
"scripts": {
"validate": "bun run src/validate-cli.ts",
"generate": "bun run src/generate.ts",
"generate:first-party": "bun run src/first-party/generate.ts",
"generate:first-party:check": "bun run src/first-party/generate.ts --check",
"typecheck": "tsgo --noEmit -p tsconfig.json",
"test": "node ../scripts/run-vitest.mjs run --config ./vitest.config.ts",
"lint": "bunx @biomejs/biome check --write --unsafe src",
"lint:check": "bunx @biomejs/biome check src",
"lint:fix": "bunx @biomejs/biome check --write --unsafe src",
"format": "bunx @biomejs/biome format --write src",
"format:check": "bunx @biomejs/biome format src",
"format:fix": "bunx @biomejs/biome format --write src"
},
"dependencies": {
"zod": "^4.4.3"
},
"devDependencies": {
"@types/node": "^24.0.0"
}
}
@@ -0,0 +1,48 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plugins.elizacloud.ai/schema/registry-entry.schema.json",
"title": "elizaOS third-party registry entry",
"description": "One JSON file per community package under entries/third-party/. Mirrors src/schema.ts.",
"type": "object",
"additionalProperties": false,
"required": ["package", "repository", "kind"],
"properties": {
"package": {
"type": "string",
"description": "npm package name. The @elizaos/* scope is reserved for first-party.",
"pattern": "^(?:@[a-z0-9][a-z0-9._-]*\\/)?[a-z0-9][a-z0-9._-]*$",
"not": { "pattern": "^@elizaos\\/" }
},
"repository": {
"type": "string",
"description": "GitHub repository as github:owner/repo.",
"pattern": "^github:[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$"
},
"kind": {
"type": "string",
"description": "What the package is.",
"enum": ["plugin", "connector", "app"]
},
"description": {
"type": "string",
"description": "One-line description."
},
"homepage": {
"type": "string",
"description": "Project homepage URL."
},
"version": {
"type": "string",
"description": "Published npm version this entry was registered against."
},
"directory": {
"type": "string",
"description": "Monorepo subdirectory when the package lives inside a larger repo."
},
"tags": {
"type": "array",
"description": "Discovery tags (npm keywords minus elizaos boilerplate).",
"items": { "type": "string" }
}
}
}
@@ -0,0 +1,52 @@
/**
* Symbol-keyed global store of curated app definitions (slug, canonical name,
* aliases), registered at runtime via `registerCuratedApp`. The Symbol.for key
* lets app / shared / plugin consumers share one store regardless of which copy
* of this package they import.
*/
export interface ElizaCuratedAppDefinition {
slug: string;
canonicalName: string;
aliases: string[];
}
interface CuratedAppRegistryStore {
entries: ElizaCuratedAppDefinition[];
}
const ELIZA_CURATED_APP_REGISTRY_KEY = Symbol.for(
"elizaos.curated-app-registry",
);
function getCuratedAppRegistryStore(): CuratedAppRegistryStore {
const globalObject = globalThis as Record<PropertyKey, unknown>;
const existing = globalObject[ELIZA_CURATED_APP_REGISTRY_KEY] as
| CuratedAppRegistryStore
| null
| undefined;
if (existing) return existing;
const created: CuratedAppRegistryStore = { entries: [] };
globalObject[ELIZA_CURATED_APP_REGISTRY_KEY] = created;
return created;
}
/**
* Register an additional curated app definition at runtime.
*
* Symbol-keyed global so app/shared/plugin consumers read the same registry
* regardless of which package they import from.
*/
export function registerCuratedApp(def: ElizaCuratedAppDefinition): void {
const store = getCuratedAppRegistryStore();
const existing = store.entries.findIndex((d) => d.slug === def.slug);
if (existing >= 0) {
store.entries[existing] = def;
} else {
store.entries.push(def);
}
}
export function getRegisteredCuratedApps(): ElizaCuratedAppDefinition[] {
return [...getCuratedAppRegistryStore().entries];
}
@@ -0,0 +1,22 @@
{
"blooio": "@elizaos/plugin-blooio",
"bluebubbles": "@elizaos/plugin-bluebubbles",
"discord": "@elizaos/plugin-discord",
"discordLocal": "@elizaos/plugin-discord-local",
"farcaster": "@elizaos/plugin-farcaster",
"feishu": "@elizaos/plugin-feishu",
"googlechat": "@elizaos/plugin-google-chat",
"imessage": "@elizaos/plugin-imessage",
"matrix": "@elizaos/plugin-matrix",
"mattermost": "@elizaos/plugin-mattermost",
"msteams": "@elizaos/plugin-msteams",
"nostr": "@elizaos/plugin-nostr",
"signal": "@elizaos/plugin-signal",
"slack": "@elizaos/plugin-slack",
"telegram": "@elizaos/plugin-telegram",
"twitch": "@elizaos/plugin-twitch",
"twitter": "@elizaos/plugin-x",
"wechat": "@elizaos/plugin-wechat",
"whatsapp": "@elizaos/plugin-whatsapp",
"x": "@elizaos/plugin-x"
}
@@ -0,0 +1,193 @@
/**
* Exercises the OWNER+AGENT connector schema extension and the
* `normalizeConnectorAuth` legacy auto-map against the real Zod schema and
* loader (no I/O). Backwards compat: manifests that only declare `auth` must
* keep parsing and end up with a populated `accounts.agent`.
*/
import { describe, expect, it } from "vitest";
import { loadRegistryFromRawEntries, normalizeConnectorAuth } from "./loader";
import {
type AccountConfig,
type ConnectorEntry,
connectorEntrySchema,
} from "./schema";
const baseConnector = {
id: "test-connector",
name: "Test Connector",
kind: "connector" as const,
subtype: "messaging" as const,
source: "bundled" as const,
tags: [],
config: {},
render: {
visible: true,
pinTo: [],
style: "card" as const,
group: "messaging",
actions: [],
},
resources: {},
dependsOn: [],
};
describe("connectorEntrySchema accounts field", () => {
it("accepts an entry with no accounts block", () => {
const parsed = connectorEntrySchema.parse(baseConnector);
expect(parsed.accounts).toBeUndefined();
});
it("accepts an entry with both owner and agent sides", () => {
const parsed = connectorEntrySchema.parse({
...baseConnector,
accounts: {
owner: {
supported: true,
authKind: "oauth-cloud",
credentialKeys: ["MY_OWNER_TOKEN"],
osSupport: ["darwin", "win32"],
},
agent: {
supported: true,
authKind: "api-key",
credentialKeys: ["MY_AGENT_TOKEN"],
},
},
});
expect(parsed.accounts?.owner?.authKind).toBe("oauth-cloud");
expect(parsed.accounts?.agent?.authKind).toBe("api-key");
expect(parsed.accounts?.owner?.osSupport).toEqual(["darwin", "win32"]);
});
it("rejects an unknown authKind", () => {
const result = connectorEntrySchema.safeParse({
...baseConnector,
accounts: {
agent: { supported: true, authKind: "not-a-real-kind" },
},
});
expect(result.success).toBe(false);
});
it("defaults credentialKeys to [] when omitted", () => {
const parsed = connectorEntrySchema.parse({
...baseConnector,
accounts: {
agent: { supported: true, authKind: "none" },
},
});
expect(parsed.accounts?.agent?.credentialKeys).toEqual([]);
});
it("rejects an empty accounts object — at least one side must be defined", () => {
const result = connectorEntrySchema.safeParse({
...baseConnector,
accounts: {},
});
expect(result.success).toBe(false);
});
});
describe("normalizeConnectorAuth (legacy auth → accounts.agent)", () => {
it("maps legacy oauth auth to accounts.agent with oauth-cloud", () => {
const entry = connectorEntrySchema.parse({
...baseConnector,
auth: { kind: "oauth", credentialKeys: ["FOO_TOKEN"] },
});
const normalized = normalizeConnectorAuth(entry);
expect(normalized.accounts?.agent).toEqual<AccountConfig>({
supported: true,
authKind: "oauth-cloud",
credentialKeys: ["FOO_TOKEN"],
});
});
it("maps legacy token auth to accounts.agent with api-key", () => {
const entry = connectorEntrySchema.parse({
...baseConnector,
auth: { kind: "token", credentialKeys: ["BAR_TOKEN"] },
});
const normalized = normalizeConnectorAuth(entry);
expect(normalized.accounts?.agent?.authKind).toBe("api-key");
expect(normalized.accounts?.agent?.credentialKeys).toEqual(["BAR_TOKEN"]);
});
it("maps legacy none auth to accounts.agent with none", () => {
const entry = connectorEntrySchema.parse({
...baseConnector,
auth: { kind: "none", credentialKeys: [] },
});
const normalized = normalizeConnectorAuth(entry);
expect(normalized.accounts?.agent?.authKind).toBe("none");
});
it("maps legacy credentials auth to accounts.agent with api-key", () => {
const entry = connectorEntrySchema.parse({
...baseConnector,
auth: { kind: "credentials", credentialKeys: ["USERNAME", "PASSWORD"] },
});
const normalized = normalizeConnectorAuth(entry);
expect(normalized.accounts?.agent?.authKind).toBe("api-key");
expect(normalized.accounts?.agent?.credentialKeys).toEqual([
"USERNAME",
"PASSWORD",
]);
});
it("preserves an explicit accounts.agent when also given legacy auth", () => {
const entry = connectorEntrySchema.parse({
...baseConnector,
auth: { kind: "oauth", credentialKeys: ["FOO_TOKEN"] },
accounts: {
agent: { supported: true, authKind: "local-app" },
},
});
const normalized = normalizeConnectorAuth(entry);
expect(normalized.accounts?.agent?.authKind).toBe("local-app");
});
it("fills accounts.agent from legacy auth when accounts only declares owner (partial migration)", () => {
const entry = connectorEntrySchema.parse({
...baseConnector,
auth: { kind: "token", credentialKeys: ["LEGACY_TOKEN"] },
accounts: {
owner: {
supported: true,
authKind: "oauth-cloud",
credentialKeys: [],
},
},
});
const normalized = normalizeConnectorAuth(entry);
expect(normalized.accounts?.owner?.authKind).toBe("oauth-cloud");
expect(normalized.accounts?.agent?.authKind).toBe("api-key");
expect(normalized.accounts?.agent?.credentialKeys).toEqual([
"LEGACY_TOKEN",
]);
});
it("is a no-op when neither auth nor accounts is declared", () => {
const entry = connectorEntrySchema.parse(baseConnector);
const normalized = normalizeConnectorAuth(entry);
expect(normalized.accounts).toBeUndefined();
});
});
describe("loadRegistryFromRawEntries applies normalizeConnectorAuth", () => {
it("populates accounts.agent for legacy-auth connectors at load time", () => {
const registry = loadRegistryFromRawEntries([
{
file: "test/legacy.json",
data: {
...baseConnector,
id: "legacy-connector",
auth: { kind: "oauth", credentialKeys: ["LEGACY_TOKEN"] },
},
},
]);
const entry = registry.byId.get("legacy-connector") as ConnectorEntry;
expect(entry.accounts?.agent?.authKind).toBe("oauth-cloud");
expect(entry.accounts?.agent?.credentialKeys).toEqual(["LEGACY_TOKEN"]);
});
});
@@ -0,0 +1,42 @@
[
{
"slug": "contacts",
"canonicalName": "@elizaos/plugin-contacts",
"aliases": []
},
{
"slug": "phone",
"canonicalName": "@elizaos/plugin-phone",
"aliases": []
},
{
"slug": "wifi",
"canonicalName": "@elizaos/plugin-wifi",
"aliases": []
},
{
"slug": "feed",
"canonicalName": "@elizaos/plugin-feed",
"aliases": []
},
{
"slug": "hyperliquid",
"canonicalName": "@elizaos/plugin-hyperliquid",
"aliases": []
},
{
"slug": "polymarket",
"canonicalName": "@elizaos/plugin-polymarket",
"aliases": []
},
{
"slug": "screenshare",
"canonicalName": "@elizaos/plugin-screenshare",
"aliases": []
},
{
"slug": "trajectory-logger",
"canonicalName": "@elizaos/plugin-trajectory-logger",
"aliases": []
}
]
@@ -0,0 +1,27 @@
{
"id": "database-viewer",
"name": "Database Viewer",
"description": "Inspect tables, media, vectors, and ad-hoc SQL.",
"npmName": "@elizaos/app-database-viewer",
"source": "bundled",
"tags": ["database", "sql", "viewer"],
"config": {},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Server",
"group": "Apps",
"groupOrder": 7,
"actions": ["launch"]
},
"resources": {},
"dependsOn": [],
"kind": "app",
"subtype": "tool",
"launch": {
"type": "internal-tab",
"target": "database",
"capabilities": ["database", "sql", "viewer"]
}
}
@@ -0,0 +1,27 @@
{
"id": "log-viewer",
"name": "Log Viewer",
"description": "Search runtime and service logs.",
"npmName": "@elizaos/app-log-viewer",
"source": "bundled",
"tags": ["logs", "debug", "viewer"],
"config": {},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "FileText",
"group": "Apps",
"groupOrder": 8,
"actions": ["launch"]
},
"resources": {},
"dependsOn": [],
"kind": "app",
"subtype": "tool",
"launch": {
"type": "internal-tab",
"target": "logs",
"capabilities": ["logs", "debug", "viewer"]
}
}
@@ -0,0 +1,27 @@
{
"id": "memory-viewer",
"name": "Memory Viewer",
"description": "Browse memory, fact, and extraction activity.",
"npmName": "@elizaos/app-memory-viewer",
"source": "bundled",
"tags": ["memory", "facts", "viewer"],
"config": {},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Dna",
"group": "Apps",
"groupOrder": 5,
"actions": ["launch"]
},
"resources": {},
"dependsOn": [],
"kind": "app",
"subtype": "tool",
"launch": {
"type": "internal-tab",
"target": "memories",
"capabilities": ["memory", "facts", "viewer"]
}
}
@@ -0,0 +1,27 @@
{
"id": "plugin-viewer",
"name": "Plugin Viewer",
"description": "Inspect installed plugins, connectors, and runtime feature flags.",
"npmName": "@elizaos/app-plugin-viewer",
"source": "bundled",
"tags": ["plugins", "connectors", "viewer"],
"config": {},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Package",
"group": "Apps",
"groupOrder": 1,
"actions": ["launch"]
},
"resources": {},
"dependsOn": [],
"kind": "app",
"subtype": "tool",
"launch": {
"type": "internal-tab",
"target": "plugins",
"capabilities": ["plugins", "connectors", "viewer"]
}
}
@@ -0,0 +1,27 @@
{
"id": "relationship-viewer",
"name": "Relationship Viewer",
"description": "Explore cross-channel people, identities, and relationship graphs.",
"npmName": "@elizaos/app-relationship-viewer",
"source": "bundled",
"tags": ["relationships", "graph", "viewer"],
"config": {},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Link",
"group": "Apps",
"groupOrder": 4,
"actions": ["launch"]
},
"resources": {},
"dependsOn": [],
"kind": "app",
"subtype": "tool",
"launch": {
"type": "internal-tab",
"target": "relationships",
"capabilities": ["relationships", "graph", "viewer"]
}
}
@@ -0,0 +1,27 @@
{
"id": "runtime-debugger",
"name": "Runtime Debugger",
"description": "Inspect runtime objects, plugin order, providers, and services.",
"npmName": "@elizaos/app-runtime-debugger",
"source": "bundled",
"tags": ["runtime", "debug", "viewer"],
"config": {},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Settings",
"group": "Apps",
"groupOrder": 6,
"actions": ["launch"]
},
"resources": {},
"dependsOn": [],
"kind": "app",
"subtype": "tool",
"launch": {
"type": "internal-tab",
"target": "runtime",
"capabilities": ["runtime", "debug", "viewer"]
}
}
@@ -0,0 +1,27 @@
{
"id": "skills-viewer",
"name": "Skills Viewer",
"description": "Create, enable, review, and install custom agent skills.",
"npmName": "@elizaos/app-skills-viewer",
"source": "bundled",
"tags": ["skills", "viewer"],
"config": {},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Wrench",
"group": "Apps",
"groupOrder": 2,
"actions": ["launch"]
},
"resources": {},
"dependsOn": [],
"kind": "app",
"subtype": "tool",
"launch": {
"type": "internal-tab",
"target": "skills",
"capabilities": ["skills", "viewer"]
}
}
@@ -0,0 +1,27 @@
{
"id": "trajectory-viewer",
"name": "Trajectory Viewer",
"description": "Inspect LLM call history, prompts, and execution traces.",
"npmName": "@elizaos/app-trajectory-viewer",
"source": "bundled",
"tags": ["trajectories", "debug", "viewer"],
"config": {},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "TrendingDown",
"group": "Apps",
"groupOrder": 3,
"actions": ["launch"]
},
"resources": {},
"dependsOn": [],
"kind": "app",
"subtype": "tool",
"launch": {
"type": "internal-tab",
"target": "trajectories",
"capabilities": ["trajectories", "debug", "viewer"]
}
}
@@ -0,0 +1,111 @@
{
"id": "mattermost",
"name": "Mattermost",
"description": "Mattermost integration plugin for elizaOS agents - TypeScript implementation",
"npmName": "@elizaos/plugin-mattermost",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["connector", "social", "social-chat", "messaging", "mattermost"],
"config": {
"MATTERMOST_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Enabled",
"help": "Enable or disable this feature",
"advanced": false
},
"MATTERMOST_TEAM_ID": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Team Id",
"help": "Team/tenant ID",
"advanced": false
},
"MATTERMOST_BOT_TOKEN": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "Bot Token",
"help": "Bot token for Mattermost",
"advanced": false
},
"MATTERMOST_DM_POLICY": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Dm Policy",
"help": "DM policy (e.g. allow, deny, allowlist)",
"advanced": false
},
"MATTERMOST_SERVER_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Server Url",
"help": "Server URL",
"advanced": false
},
"MATTERMOST_GROUP_POLICY": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Group Policy",
"help": "Group message policy (e.g. allow, deny)",
"advanced": false
},
"MATTERMOST_ALLOWED_USERS": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Allowed Users",
"help": "Comma-separated allowed user list",
"advanced": false
},
"MATTERMOST_REQUIRE_MENTION": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Require Mention",
"help": "Only respond when mentioned",
"advanced": false
},
"MATTERMOST_ALLOWED_CHANNELS": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Allowed Channels",
"help": "Comma-separated room/channel list",
"advanced": false
},
"MATTERMOST_IGNORE_BOT_MESSAGES": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Ignore Bot Messages",
"help": "Ignore messages from other bots",
"advanced": false
}
},
"render": {
"visible": false,
"pinTo": [],
"style": "setup-panel",
"icon": "Diamond",
"group": "connector",
"groupOrder": 1,
"actions": ["enable", "configure", "setup-guide"]
},
"resources": {
"setupGuideUrl": "https://docs.eliza.ai/plugin-setup-guide#mattermost"
},
"dependsOn": [],
"kind": "connector",
"subtype": "messaging",
"auth": {
"kind": "token",
"credentialKeys": ["MATTERMOST_BOT_TOKEN"]
},
"channels": ["mattermost"]
}
@@ -0,0 +1,105 @@
{
"id": "msteams",
"name": "MS Teams",
"description": "Microsoft Teams integration for ElizaOS",
"npmName": "@elizaos/plugin-msteams",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["connector", "social", "social-chat", "messaging", "msteams"],
"config": {
"MSTEAMS_APP_ID": {
"type": "string",
"required": false,
"sensitive": false,
"label": "App Id",
"help": "Application/client ID",
"advanced": false
},
"MSTEAMS_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Enabled",
"help": "Enable or disable this feature",
"advanced": false
},
"MSTEAMS_TENANT_ID": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Tenant Id",
"help": "Team/tenant ID",
"advanced": false
},
"MSTEAMS_APP_PASSWORD": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "App Password",
"help": "Password for Msteams App",
"advanced": false
},
"MSTEAMS_MEDIA_MAX_MB": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Media Max Mb",
"help": "Maximum size in MB",
"advanced": false
},
"MSTEAMS_WEBHOOK_PATH": {
"type": "file-path",
"required": false,
"sensitive": false,
"label": "Webhook Path",
"help": "Webhook path",
"advanced": false
},
"MSTEAMS_WEBHOOK_PORT": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Webhook Port",
"help": "Port number",
"advanced": false,
"min": 1,
"max": 65535
},
"MSTEAMS_ALLOWED_TENANTS": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Allowed Tenants",
"help": "Comma-separated allowed channel/room list",
"advanced": false
},
"MSTEAMS_SHAREPOINT_SITE_ID": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Sharepoint Site Id",
"help": "Site identifier",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "setup-panel",
"icon": "Square",
"group": "connector",
"groupOrder": 1,
"actions": ["enable", "configure", "setup-guide"]
},
"resources": {
"setupGuideUrl": "https://docs.eliza.ai/plugin-setup-guide#microsoft-teams"
},
"dependsOn": [],
"kind": "connector",
"subtype": "messaging",
"auth": {
"kind": "token",
"credentialKeys": []
},
"channels": ["msteams"]
}
@@ -0,0 +1,104 @@
{
"id": "nextcloud-talk",
"name": "Nextcloud Talk",
"description": "Nextcloud Talk plugin for elizaOS - TypeScript implementation",
"npmName": "@elizaos/plugin-nextcloud-talk",
"version": "2.0.0-beta.0",
"source": "store",
"tags": [
"connector",
"social",
"social-chat",
"messaging",
"nextcloud-talk",
"nextcloud",
"talk"
],
"config": {
"NEXTCLOUD_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Url",
"help": "URL for Nextcloud",
"advanced": false
},
"NEXTCLOUD_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Enabled",
"help": "Enable or disable this feature",
"advanced": false
},
"NEXTCLOUD_BOT_SECRET": {
"type": "secret",
"required": false,
"sensitive": true,
"label": "Bot Secret",
"help": "Secret for Nextcloud Bot",
"advanced": false
},
"NEXTCLOUD_WEBHOOK_HOST": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Webhook Host",
"help": "Host address",
"advanced": false
},
"NEXTCLOUD_WEBHOOK_PATH": {
"type": "file-path",
"required": false,
"sensitive": false,
"label": "Webhook Path",
"help": "Webhook path",
"advanced": false
},
"NEXTCLOUD_WEBHOOK_PORT": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Webhook Port",
"help": "Port number",
"advanced": false,
"min": 1,
"max": 65535
},
"NEXTCLOUD_ALLOWED_ROOMS": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Allowed Rooms",
"help": "Comma-separated room/channel list",
"advanced": false
},
"NEXTCLOUD_WEBHOOK_PUBLIC_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Webhook Public Url",
"help": "Public-facing webhook URL",
"advanced": false
}
},
"render": {
"visible": false,
"pinTo": [],
"style": "setup-panel",
"icon": "Cloud",
"group": "connector",
"groupOrder": 1,
"actions": ["enable", "configure", "setup-guide"]
},
"resources": {
"setupGuideUrl": "https://docs.eliza.ai/plugin-setup-guide#nextcloud-talk"
},
"dependsOn": [],
"kind": "connector",
"subtype": "messaging",
"auth": {
"kind": "token",
"credentialKeys": []
}
}
@@ -0,0 +1,94 @@
{
"id": "tlon",
"name": "Tlon",
"description": "Tlon connector for Urbit chat and social interactions.",
"npmName": "@elizaos/plugin-tlon",
"version": "2.0.0-beta.0",
"source": "store",
"tags": [
"connector",
"social",
"urbit",
"chat",
"social-chat",
"messaging",
"tlon"
],
"config": {
"TLON_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Url",
"help": "URL for Tlon",
"advanced": false
},
"TLON_CODE": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Code",
"help": "Authentication/access code",
"advanced": false
},
"TLON_SHIP": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Ship",
"help": "Ship/node identifier",
"advanced": false
},
"TLON_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Enabled",
"help": "Enable or disable this feature",
"advanced": false
},
"TLON_DM_ALLOWLIST": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Dm Allowlist",
"help": "Comma-separated allowed user list",
"advanced": false
},
"TLON_GROUP_CHANNELS": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Group Channels",
"help": "Comma-separated room/channel list",
"advanced": false
},
"TLON_AUTO_DISCOVER_CHANNELS": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Auto Discover Channels",
"help": "Comma-separated room/channel list",
"advanced": false
}
},
"render": {
"visible": false,
"pinTo": [],
"style": "setup-panel",
"icon": "Tornado",
"group": "connector",
"groupOrder": 1,
"actions": ["enable", "configure", "setup-guide"]
},
"resources": {
"setupGuideUrl": "https://docs.eliza.ai/plugin-setup-guide#tlon-urbit"
},
"dependsOn": [],
"kind": "connector",
"subtype": "messaging",
"auth": {
"kind": "token",
"credentialKeys": []
}
}
@@ -0,0 +1,112 @@
{
"id": "zalo",
"name": "Zalo",
"description": "Zalo connector for official account messaging and support workflows.",
"npmName": "@elizaos/plugin-zalo",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["connector", "messaging", "zalo", "chat", "social", "social-chat"],
"config": {
"ZALO_APP_ID": {
"type": "string",
"required": false,
"sensitive": false,
"label": "App Id",
"help": "Application/client ID",
"advanced": false
},
"ZALO_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Enabled",
"help": "Enable or disable this feature",
"advanced": false
},
"ZALO_PROXY_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Proxy Url",
"help": "Proxy URL",
"advanced": false
},
"ZALO_SECRET_KEY": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "Secret Key",
"help": "Secret for Zalo Secret Key",
"advanced": false
},
"ZALO_USE_POLLING": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Use Polling",
"help": "Use polling instead of webhooks",
"advanced": false
},
"ZALO_WEBHOOK_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Webhook Url",
"help": "Webhook URL",
"advanced": false
},
"ZALO_ACCESS_TOKEN": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "Access Token",
"help": "Access token for Zalo",
"advanced": false
},
"ZALO_WEBHOOK_PATH": {
"type": "file-path",
"required": false,
"sensitive": false,
"label": "Webhook Path",
"help": "Webhook path",
"advanced": false
},
"ZALO_WEBHOOK_PORT": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Webhook Port",
"help": "Port number",
"advanced": false,
"min": 1,
"max": 65535
},
"ZALO_REFRESH_TOKEN": {
"type": "secret",
"required": false,
"sensitive": true,
"label": "Refresh Token",
"help": "Refresh token for Zalo",
"advanced": false
}
},
"render": {
"visible": false,
"pinTo": [],
"style": "setup-panel",
"icon": "Circle",
"group": "connector",
"groupOrder": 1,
"actions": ["enable", "configure", "setup-guide"]
},
"resources": {
"setupGuideUrl": "https://docs.eliza.ai/plugin-setup-guide#zalo-vietnam-messaging"
},
"dependsOn": [],
"kind": "connector",
"subtype": "messaging",
"auth": {
"kind": "token",
"credentialKeys": ["ZALO_ACCESS_TOKEN"]
}
}
@@ -0,0 +1,122 @@
{
"id": "zalouser",
"name": "Zalouser",
"description": "Zalo personal-account connector for one-to-one messaging workflows.",
"npmName": "@elizaos/plugin-zalouser",
"version": "2.0.0-beta.0",
"source": "store",
"tags": [
"connector",
"messaging",
"zalo",
"personal-account",
"social",
"social-chat",
"zalouser"
],
"config": {
"ZALOUSER_IMEI": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Imei",
"help": "Device IMEI identifier",
"advanced": false
},
"ZALOUSER_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Enabled",
"help": "Enable or disable this feature",
"advanced": false
},
"ZALOUSER_PROFILES": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Profiles",
"help": "Profile configuration",
"advanced": false
},
"ZALOUSER_DM_POLICY": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Dm Policy",
"help": "DM policy (e.g. allow, deny, allowlist)",
"advanced": false
},
"ZALOUSER_USER_AGENT": {
"type": "string",
"required": false,
"sensitive": false,
"label": "User Agent",
"help": "User agent string",
"advanced": false
},
"ZALOUSER_COOKIE_PATH": {
"type": "file-path",
"required": false,
"sensitive": false,
"label": "Cookie Path",
"help": "Directory/file path",
"advanced": false
},
"ZALOUSER_GROUP_POLICY": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Group Policy",
"help": "Group message policy (e.g. allow, deny)",
"advanced": false
},
"ZALOUSER_LISTEN_TIMEOUT": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Listen Timeout",
"help": "Timeout value",
"advanced": false,
"min": 0,
"unit": "ms"
},
"ZALOUSER_ALLOWED_THREADS": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Allowed Threads",
"help": "Comma-separated allowed channel/room list",
"advanced": false
},
"ZALOUSER_DEFAULT_PROFILE": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Default Profile",
"help": "Profile configuration",
"advanced": false
}
},
"render": {
"visible": false,
"pinTo": [],
"style": "setup-panel",
"icon": "Circle",
"group": "connector",
"groupOrder": 1,
"actions": ["enable", "configure", "setup-guide"]
},
"resources": {
"homepage": "https://github.com/elizaos/eliza#readme",
"repository": "https://github.com/elizaos/eliza",
"setupGuideUrl": "https://docs.eliza.ai/plugin-setup-guide#zalo-user-personal"
},
"dependsOn": [],
"kind": "connector",
"subtype": "messaging",
"auth": {
"kind": "token",
"credentialKeys": []
}
}
@@ -0,0 +1,203 @@
{
"id": "auto-trader",
"name": "Auto Trader",
"description": "Autonomous trading plugin with LLM-powered strategies for Solana",
"npmName": "@elizaos/plugin-auto-trader",
"version": "2.0.0-beta.0",
"source": "store",
"tags": [
"trading",
"solana",
"jupiter",
"defi",
"auto-trader",
"auto",
"trader"
],
"config": {
"SLIPPAGE_BPS": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Bps",
"help": "Basis points (e.g. slippage)",
"advanced": false
},
"TRADING_MODE": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Mode",
"help": "Operating mode",
"advanced": false
},
"SOLANA_ADDRESS": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Address",
"help": "Wallet address",
"advanced": false
},
"SOLANA_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Enabled",
"help": "Enable or disable this feature",
"advanced": false
},
"SOLANA_RPC_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Rpc Url",
"help": "RPC endpoint URL",
"advanced": false
},
"ZEROEX_API_KEY": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "Api Key",
"help": "API key for Zeroex",
"advanced": false
},
"BIRDEYE_API_KEY": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "Api Key",
"help": "API key for Birdeye",
"advanced": false
},
"EVM_PROVIDER_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Provider Url",
"help": "Provider endpoint URL",
"advanced": false
},
"RUGCHECK_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Enabled",
"help": "Enable or disable this feature",
"advanced": false
},
"MIN_LIQUIDITY_USD": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Liquidity Usd",
"help": "Min liquidity usd",
"advanced": false
},
"STOP_LOSS_PERCENT": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Loss Percent",
"help": "Percentage value",
"advanced": false
},
"MAX_BUY_SELL_RATIO": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Buy Sell Ratio",
"help": "Ratio limit",
"advanced": false,
"min": 0
},
"MAX_DAILY_LOSS_USD": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Daily Loss Usd",
"help": "Max daily loss usd",
"advanced": false,
"min": 0
},
"MIN_SELL_COUNT_24H": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Sell Count 24h",
"help": "Min sell count 24h",
"advanced": false,
"min": 0
},
"MIN_VOLUME_24H_USD": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Volume 24h Usd",
"help": "Min volume 24h usd",
"advanced": false
},
"SOLANA_PRIVATE_KEY": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "Private Key",
"help": "Private key for Solana",
"advanced": false
},
"TAKE_PROFIT_PERCENT": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Profit Percent",
"help": "Percentage value",
"advanced": false
},
"TRADING_INTERVAL_MS": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Interval Ms",
"help": "Polling interval in milliseconds",
"advanced": false,
"min": 0,
"unit": "ms"
},
"MAX_POSITION_SIZE_USD": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Position Size Usd",
"help": "Max position size usd",
"advanced": false,
"min": 0
},
"MAX_PORTFOLIO_ALLOCATION": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Portfolio Allocation",
"help": "Maximum allocation percentage",
"advanced": false,
"min": 1,
"max": 65535
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "TrendingDown",
"group": "blockchain",
"groupOrder": 4,
"actions": ["enable", "configure"]
},
"resources": {
"homepage": "https://github.com/elizaos/eliza#readme",
"repository": "https://github.com/elizaos/eliza"
},
"dependsOn": [],
"kind": "plugin",
"subtype": "blockchain"
}
@@ -0,0 +1,103 @@
{
"id": "blooio",
"name": "Blooio",
"description": "Integrates Blooio iMessage/SMS messaging into elizaOS with signed webhooks and outbound message sending.",
"npmName": "@elizaos/plugin-blooio",
"version": "2.0.0-beta.0",
"source": "store",
"tags": [
"connector",
"social",
"social-chat",
"messaging",
"blooio",
"imessage",
"sms"
],
"config": {
"BLOOIO_API_KEY": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "Api Key",
"help": "API key for Blooio",
"advanced": false
},
"BLOOIO_BASE_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Base Url",
"help": "Base URL for API requests",
"advanced": false
},
"BLOOIO_FROM_NUMBER": {
"type": "string",
"required": false,
"sensitive": false,
"label": "From Number",
"help": "Sender phone number",
"advanced": false
},
"BLOOIO_WEBHOOK_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Webhook Url",
"help": "Webhook callback URL",
"advanced": false
},
"BLOOIO_WEBHOOK_PATH": {
"type": "file-path",
"required": false,
"sensitive": false,
"label": "Webhook Path",
"help": "Webhook path",
"advanced": false
},
"BLOOIO_WEBHOOK_PORT": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Webhook Port",
"help": "Port number",
"advanced": false,
"min": 1,
"max": 65535
},
"BLOOIO_WEBHOOK_SECRET": {
"type": "secret",
"required": false,
"sensitive": true,
"label": "Webhook Secret",
"help": "Secret key for webhook/client verification",
"advanced": false
},
"BLOOIO_SIGNATURE_TOLERANCE_SEC": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Signature Tolerance Sec",
"help": "Tolerance window in seconds",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Circle",
"group": "feature-other",
"groupOrder": 12,
"actions": ["enable", "configure"]
},
"resources": {
"homepage": "https://github.com/elizaos/eliza#readme",
"repository": "https://github.com/elizaos/eliza",
"setupGuideUrl": "https://docs.eliza.ai/plugin-setup-guide#blooio-sms-via-api"
},
"dependsOn": [],
"kind": "plugin",
"subtype": "other",
"channels": ["blooio"]
}
@@ -0,0 +1,44 @@
{
"id": "clipboard",
"name": "Clipboard",
"description": "File-based memory storage plugin for ElizaOS - persistent notes and clipboard",
"npmName": "@elizaos/plugin-clipboard",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["clipboard", "memory", "notes", "storage"],
"config": {
"CLIPBOARD_BASE_PATH": {
"type": "file-path",
"required": false,
"sensitive": false,
"label": "Base Path",
"help": "Base directory for clipboard files (default: <stateDir>/clipboard)",
"advanced": false
},
"CLIPBOARD_MAX_FILE_SIZE": {
"type": "number",
"required": false,
"sensitive": false,
"default": "1048576",
"label": "Max File Size",
"help": "Maximum file size in bytes (default: 1MB)",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "StickyNote",
"group": "documents",
"groupOrder": 6,
"actions": ["enable", "configure"]
},
"resources": {
"homepage": "https://github.com/elizaos-plugins/plugin-clipboard#readme",
"repository": "https://github.com/elizaos-plugins/plugin-clipboard"
},
"dependsOn": [],
"kind": "plugin",
"subtype": "documents"
}
@@ -0,0 +1,93 @@
{
"id": "copilot-proxy",
"name": "Copilot Proxy",
"description": "ElizaOS Copilot Proxy Model Provider - TypeScript implementation",
"npmName": "@elizaos/plugin-copilot-proxy",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["copilot-proxy", "copilot", "proxy"],
"config": {
"COPILOT_PROXY_MODEL": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Proxy Model",
"help": "Model identifier",
"placeholder": "e.g., gpt-4o, claude-sonnet-4-20250514",
"advanced": false
},
"COPILOT_PROXY_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Proxy Enabled",
"help": "Enable or disable this feature",
"advanced": false
},
"COPILOT_PROXY_BASE_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Proxy Base Url",
"help": "Base URL for API requests",
"advanced": false
},
"COPILOT_PROXY_MAX_TOKENS": {
"type": "secret",
"required": false,
"sensitive": true,
"label": "Proxy Max Tokens",
"help": "Maximum tokens per request",
"advanced": false
},
"COPILOT_PROXY_LARGE_MODEL": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Proxy Large Model",
"help": "Model identifier",
"placeholder": "e.g., gpt-4o, claude-sonnet-4-20250514",
"advanced": false
},
"COPILOT_PROXY_SMALL_MODEL": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Proxy Small Model",
"help": "Model identifier",
"placeholder": "e.g., gpt-4o, claude-sonnet-4-20250514",
"advanced": false
},
"COPILOT_PROXY_CONTEXT_WINDOW": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Proxy Context Window",
"help": "Context window size",
"advanced": false
},
"COPILOT_PROXY_TIMEOUT_SECONDS": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Proxy Timeout Seconds",
"help": "Timeout in seconds",
"advanced": false,
"min": 0,
"unit": "ms"
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Handshake",
"group": "agents",
"groupOrder": 7,
"actions": ["enable", "configure"]
},
"resources": {},
"dependsOn": [],
"kind": "plugin",
"subtype": "agents"
}
@@ -0,0 +1,63 @@
{
"id": "directives",
"name": "Directives",
"description": "Inline directive parsing for Eliza agents (@think, @model, @verbose, etc.)",
"npmName": "@elizaos/plugin-directives",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["directives"],
"config": {
"ALLOW_EXEC": {
"type": "boolean",
"required": false,
"sensitive": false,
"default": "false",
"label": "Exec",
"help": "Allow exec directive for shell configuration",
"advanced": false
},
"ALLOW_ELEVATED": {
"type": "boolean",
"required": false,
"sensitive": false,
"default": "true",
"label": "Elevated",
"help": "Allow elevated permission directives",
"advanced": false
},
"DEFAULT_VERBOSE": {
"type": "string",
"required": false,
"sensitive": false,
"default": "off",
"label": "Verbose",
"help": "Default verbose level (off, on, full)",
"advanced": true
},
"DEFAULT_THINKING": {
"type": "string",
"required": false,
"sensitive": false,
"default": "low",
"label": "Thinking",
"help": "Default thinking level (off, minimal, low, medium, high)",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "ClipboardList",
"group": "agents",
"groupOrder": 7,
"actions": ["enable", "configure"]
},
"resources": {
"homepage": "https://github.com/elizaos/eliza#readme",
"repository": "https://github.com/elizaos/eliza"
},
"dependsOn": [],
"kind": "plugin",
"subtype": "agents"
}
@@ -0,0 +1,34 @@
{
"id": "experience",
"name": "Experience",
"description": "Built-in advanced capability for recording and recalling transferable agent learnings, surfaced in the Character Experience workspace alongside trajectory inspection tooling.",
"version": "2.0.0-beta.0",
"source": "bundled",
"tags": ["experience", "learning", "memory"],
"config": {
"AUTO_RECORD_THRESHOLD": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Record Threshold",
"help": "Minimum confidence required before the experience evaluator records a new learning.",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Star",
"group": "storage",
"groupOrder": 10,
"actions": ["enable", "configure"]
},
"resources": {
"homepage": "https://elizaos.ai",
"repository": "https://github.com/elizaos/elizaos"
},
"dependsOn": [],
"kind": "plugin",
"subtype": "storage"
}
@@ -0,0 +1,25 @@
{
"id": "gmail-watch",
"name": "Gmail Watch",
"description": "Gmail Pub/Sub watcher spawns gog gmail watch serve and auto-renews",
"npmName": "@elizaos/plugin-gmail-watch",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["gmail-watch", "gmail", "watch"],
"config": {},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Mail",
"group": "media",
"groupOrder": 8,
"actions": ["enable", "configure"]
},
"resources": {
"setupGuideUrl": "https://docs.eliza.ai/plugin-setup-guide#gmail-watch"
},
"dependsOn": [],
"kind": "plugin",
"subtype": "media"
}
@@ -0,0 +1,48 @@
{
"id": "hedera",
"name": "Hedera",
"description": "ElizaOS plugin for Hedera blockchain",
"npmName": "@elizaos/plugin-hedera",
"source": "store",
"tags": ["hedera", "hedera-agent-kit", "llm"],
"config": {
"HEDERA_NETWORK": {
"type": "string",
"required": false,
"sensitive": false,
"default": "testnet",
"label": "Network",
"help": "Hedera network to connect to (mainnet, testnet, or previewnet)",
"advanced": false
},
"HEDERA_ACCOUNT_ID": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Account Id",
"help": "Id/address of Hedera account (ex. 0.X.XXXXXX)",
"advanced": false,
"min": 0
},
"HEDERA_PRIVATE_KEY": {
"type": "secret",
"required": false,
"sensitive": true,
"label": "Private Key",
"help": "Private key for Hedera account. Accepts ED25519 and ECDSA private keys both DER and HEX encoded",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"group": "feature-other",
"groupOrder": 12,
"actions": ["enable", "configure"]
},
"resources": {},
"dependsOn": [],
"kind": "plugin",
"subtype": "other"
}
@@ -0,0 +1,124 @@
{
"id": "memory",
"name": "Memory",
"description": "Agent memory",
"npmName": "@elizaos/plugin-memory",
"version": "2.0.1",
"source": "store",
"tags": ["memory", "summarization"],
"config": {
"MEMORY_ENABLE_BM25": {
"type": "boolean",
"required": false,
"sensitive": false,
"default": "true",
"label": "Enable Bm25",
"help": "Enable keyword-based search with stemming for exact term matching.",
"advanced": false
},
"MEMORY_TOKEN_BUDGET": {
"type": "secret",
"required": false,
"sensitive": true,
"default": "1000",
"label": "Token Budget",
"help": "Maximum tokens to use for memory context in prompts.",
"advanced": false
},
"MEMORY_MIN_CONFIDENCE": {
"type": "number",
"required": false,
"sensitive": false,
"default": "0.7",
"label": "Min Confidence",
"help": "Minimum confidence score (0-1) required to store extracted facts.",
"advanced": false
},
"MEMORY_RETRIEVAL_LIMIT": {
"type": "number",
"required": false,
"sensitive": false,
"default": "5",
"label": "Retrieval Limit",
"help": "Maximum number of memories to retrieve per query.",
"advanced": false,
"min": 0
},
"MEMORY_SUMMARY_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"default": "true",
"label": "Summary Enabled",
"help": "Enable hierarchical conversation summarization to reduce token usage.",
"advanced": false
},
"MEMORY_SUMMARY_MAX_DEPTH": {
"type": "number",
"required": false,
"sensitive": false,
"default": "3",
"label": "Summary Max Depth",
"help": "Maximum depth of the summary hierarchy (1=direct summaries, 2=summaries of summaries, etc).",
"advanced": false
},
"MEMORY_SUMMARIES_PER_LEVEL": {
"type": "number",
"required": false,
"sensitive": false,
"default": "5",
"label": "Summaries Per Level",
"help": "How many summaries to accumulate before creating the next level.",
"advanced": false
},
"MEMORY_ENABLE_VECTOR_SEARCH": {
"type": "boolean",
"required": false,
"sensitive": false,
"default": "true",
"label": "Enable Vector Search",
"help": "Enable semantic similarity search using embeddings.",
"advanced": false
},
"MEMORY_MESSAGES_PER_SUMMARY": {
"type": "number",
"required": false,
"sensitive": false,
"default": "15",
"label": "Messages Per Summary",
"help": "How many messages to include in each Level 1 summary.",
"advanced": false
},
"MEMORY_SUMMARY_TOKEN_BUDGET": {
"type": "secret",
"required": false,
"sensitive": true,
"default": "500",
"label": "Summary Token Budget",
"help": "Maximum tokens to use for conversation summaries in context.",
"advanced": false
},
"MEMORY_CONSOLIDATION_THRESHOLD": {
"type": "number",
"required": false,
"sensitive": false,
"default": "10",
"label": "Consolidation Threshold",
"help": "Number of messages to buffer before triggering memory consolidation.",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Dna",
"group": "documents",
"groupOrder": 6,
"actions": ["enable", "configure"]
},
"resources": {},
"dependsOn": [],
"kind": "plugin",
"subtype": "documents"
}
@@ -0,0 +1,83 @@
{
"id": "moltbook",
"name": "Moltbook",
"description": "Moltbook social plugin for Eliza agents - Reddit-style platform for AI agents",
"npmName": "@elizaos/plugin-moltbook",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["moltbook", "social", "reddit", "agents", "autonomous"],
"config": {
"MOLTBOOK_MODEL": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Model",
"help": "Model identifier",
"placeholder": "e.g., gpt-4o, claude-sonnet-4-20250514",
"advanced": false
},
"MOLTBOOK_TOKEN": {
"type": "secret",
"required": false,
"sensitive": true,
"label": "Token",
"help": "Authentication token for Moltbook",
"advanced": false
},
"MOLTBOOK_AGENT_NAME": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Agent Name",
"help": "Agent display name",
"advanced": false
},
"MOLTBOOK_PERSONALITY": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Personality",
"help": "Agent personality description",
"advanced": false
},
"MOLTBOOK_AUTONOMOUS_MODE": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Autonomous Mode",
"help": "Enable autonomous operation mode",
"advanced": false
},
"MOLTBOOK_AUTONOMY_MAX_STEPS": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Autonomy Max Steps",
"help": "Maximum number of steps",
"advanced": false
},
"MOLTBOOK_AUTONOMY_INTERVAL_MS": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Autonomy Interval Ms",
"help": "Interval in milliseconds",
"advanced": false,
"min": 0,
"unit": "ms"
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "ScrollText",
"group": "gaming",
"groupOrder": 11,
"actions": ["enable", "configure"]
},
"resources": {},
"dependsOn": [],
"kind": "plugin",
"subtype": "gaming"
}
@@ -0,0 +1,85 @@
{
"id": "nearai",
"name": "NEAR AI",
"description": "NEAR AI Cloud TEE inference provider via an OpenAI-compatible API.",
"npmName": "@elizaos/plugin-nearai",
"version": "2.0.3",
"source": "store",
"tags": ["ai-provider", "llm", "nearai", "near-ai", "tee", "chat"],
"config": {
"NEARAI_API_KEY": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "API Key",
"help": "API key used to authenticate requests to NEAR AI Cloud.",
"advanced": false,
"autoEnableProvider": true
},
"NEARAI_BASE_URL": {
"type": "url",
"required": false,
"sensitive": false,
"default": "https://cloud-api.near.ai/v1",
"label": "Base URL",
"help": "Base URL for the NEAR AI Cloud OpenAI-compatible API.",
"advanced": false
},
"NEARAI_SMALL_MODEL": {
"type": "string",
"required": false,
"sensitive": false,
"default": "google/gemma-4-31B-it",
"label": "Small Model",
"help": "Override the default NEAR AI small text model identifier.",
"placeholder": "e.g., google/gemma-4-31B-it",
"advanced": false
},
"NEARAI_LARGE_MODEL": {
"type": "string",
"required": false,
"sensitive": false,
"default": "google/gemma-4-31B-it",
"label": "Large Model",
"help": "Override the default NEAR AI large text model identifier.",
"placeholder": "e.g., google/gemma-4-31B-it",
"advanced": false
},
"NEARAI_EXPERIMENTAL_TELEMETRY": {
"type": "boolean",
"required": false,
"sensitive": false,
"default": false,
"label": "Experimental Telemetry",
"help": "Enable experimental telemetry features for enhanced debugging and usage analytics.",
"advanced": true
},
"NEARAI_BROWSER_BASE_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Browser Base URL",
"help": "Browser-only proxy endpoint base URL for NEAR AI requests. Do not expose API keys in browser clients.",
"advanced": true
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "ShieldCheck",
"group": "ai-provider",
"groupOrder": 0,
"actions": ["enable", "configure"]
},
"resources": {
"homepage": "https://near.ai",
"repository": "https://github.com/elizaos-plugins/plugin-nearai",
"setupGuideUrl": "https://docs.eliza.ai/plugin-setup-guide#near-ai"
},
"dependsOn": [],
"channels": [],
"shortIds": [],
"kind": "plugin",
"subtype": "ai-provider"
}
@@ -0,0 +1,23 @@
{
"id": "plugin-manager",
"name": "Plugin Manager",
"description": "Plugin discovery, install, eject, sync, registry search, and creation",
"npmName": "@elizaos/plugin-plugin-manager",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["plugins", "registry", "install", "create", "agents"],
"config": {},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Package",
"group": "agents",
"groupOrder": 7,
"actions": ["enable", "configure"]
},
"resources": {},
"dependsOn": [],
"kind": "plugin",
"subtype": "agents"
}
@@ -0,0 +1,48 @@
{
"id": "prose",
"name": "Prose",
"description": "OpenProse VM integration for ElizaOS - a programming language for AI sessions",
"npmName": "@elizaos/plugin-prose",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["openprose", "prose", "llm", "vm", "workflow", "orchestration"],
"config": {
"PROSE_SKILLS_DIR": {
"type": "file-path",
"required": false,
"sensitive": false,
"label": "Skills Dir",
"help": "Directory containing prose skill files (prose.md, help.md, etc.)",
"advanced": false
},
"PROSE_STATE_MODE": {
"type": "string",
"required": false,
"sensitive": false,
"label": "State Mode",
"help": "Default state management mode (filesystem|in-context|sqlite|postgres)",
"advanced": false
},
"PROSE_WORKSPACE_DIR": {
"type": "file-path",
"required": false,
"sensitive": false,
"label": "Workspace Dir",
"help": "Base directory for .prose workspace files",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "PenTool",
"group": "media",
"groupOrder": 8,
"actions": ["enable", "configure"]
},
"resources": {},
"dependsOn": [],
"kind": "plugin",
"subtype": "media"
}
@@ -0,0 +1,64 @@
{
"id": "rss",
"name": "Rss",
"description": "news and feed support",
"npmName": "@elizaos/plugin-rss",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["rss", "atom", "feed", "news", "syndication"],
"config": {
"RSS_FEEDS": {
"type": "json",
"required": false,
"sensitive": false,
"label": "Feeds",
"help": "JSON array or comma-separated list of feed URLs to auto-subscribe",
"advanced": false
},
"RSS_FEED_FORMAT": {
"type": "string",
"required": false,
"sensitive": false,
"default": "csv",
"label": "Feed Format",
"help": "Output format for feed items: 'csv' (compact) or 'markdown' (readable)",
"advanced": false
},
"RSS_DISABLE_ACTIONS": {
"type": "boolean",
"required": false,
"sensitive": false,
"default": "False",
"label": "Disable Actions",
"help": "Set to true to disable subscription management actions",
"advanced": false
},
"RSS_CHECK_INTERVAL_MINUTES": {
"type": "number",
"required": false,
"sensitive": false,
"default": "15",
"label": "Check Interval Minutes",
"help": "Interval in minutes between feed checks",
"advanced": false,
"min": 0,
"unit": "ms"
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Rss",
"group": "media",
"groupOrder": 8,
"actions": ["enable", "configure"]
},
"resources": {
"homepage": "https://elizaos.ai",
"repository": "https://github.com/elizaos/eliza"
},
"dependsOn": [],
"kind": "plugin",
"subtype": "media"
}
@@ -0,0 +1,91 @@
{
"id": "s3-storage",
"name": "S3 Storage",
"description": "S3-compatible object storage plugin for saving files, media, and generated artifacts.",
"npmName": "@elizaos/plugin-s3-storage",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["storage", "files", "s3", "s3-storage"],
"config": {
"AWS_REGION": {
"type": "string",
"required": true,
"sensitive": false,
"label": "Region",
"help": "AWS region in which the S3 bucket resides.",
"advanced": false
},
"AWS_S3_BUCKET": {
"type": "string",
"required": true,
"sensitive": false,
"label": "S3 Bucket",
"help": "Name of the AWS S3 bucket used for file uploads and downloads.",
"advanced": false
},
"AWS_S3_ENDPOINT": {
"type": "url",
"required": false,
"sensitive": false,
"label": "S3 Endpoint",
"help": "Custom S3-compatible endpoint (e.g., for non-AWS providers).",
"advanced": false
},
"AWS_ACCESS_KEY_ID": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "Access Key Id",
"help": "AWS access key ID used for authenticating requests to AWS S3.",
"advanced": false
},
"AWS_S3_SSL_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "S3 Ssl Enabled",
"help": "Flag indicating whether SSL should be enabled for the custom S3 endpoint.",
"advanced": false
},
"AWS_S3_UPLOAD_PATH": {
"type": "file-path",
"required": false,
"sensitive": false,
"label": "S3 Upload Path",
"help": "Default path or prefix inside the S3 bucket where files will be uploaded.",
"advanced": false
},
"AWS_SECRET_ACCESS_KEY": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "Secret Access Key",
"help": "AWS secret access key used for authenticating requests to AWS S3.",
"advanced": false
},
"AWS_S3_FORCE_PATH_STYLE": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "S3 Force Path Style",
"help": "Flag to force path-style addressing when connecting to the S3 endpoint.",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Server",
"group": "storage",
"groupOrder": 10,
"actions": ["enable", "configure"]
},
"resources": {
"homepage": "https://github.com/elizaos-plugins/plugin-storage-s3#readme",
"repository": "https://github.com/elizaos-plugins/plugin-storage-s3"
},
"dependsOn": [],
"kind": "plugin",
"subtype": "storage"
}
@@ -0,0 +1,39 @@
{
"id": "trust",
"name": "Trust",
"description": "Trust scoring, security signals, and permission helpers built into the elizaOS runtime (enable via enableTrust).",
"version": "2.0.0-beta.0",
"source": "bundled",
"tags": ["trust", "permissions", "security", "rbac"],
"config": {
"OWNER_ENTITY_ID": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Entity Id",
"help": "Entity identifier",
"advanced": false
},
"WORLD_ID": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Id",
"help": "World identifier",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Handshake",
"group": "blockchain",
"groupOrder": 4,
"actions": ["enable", "configure"]
},
"resources": {},
"dependsOn": [],
"kind": "plugin",
"subtype": "blockchain"
}
@@ -0,0 +1,71 @@
{
"id": "tts",
"name": "Tts",
"description": "A plugin for generating text-to-speech using FAL.ai API within the ElizaOS ecosystem, featuring automatic language detection and voice selection.",
"npmName": "@elizaos/plugin-tts",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["llm", "text-to-speech"],
"config": {
"TTS_AUTO_MODE": {
"type": "string",
"required": false,
"sensitive": false,
"default": "off",
"label": "Auto Mode",
"help": "When to automatically apply TTS (off, always, inbound, tagged)",
"advanced": false
},
"TTS_SUMMARIZE": {
"type": "boolean",
"required": false,
"sensitive": false,
"default": "true",
"label": "Summarize",
"help": "Summarize long text instead of truncating",
"advanced": false
},
"TTS_MAX_LENGTH": {
"type": "number",
"required": false,
"sensitive": false,
"default": "1500",
"label": "Max Length",
"help": "Maximum text length for TTS synthesis",
"advanced": false
},
"TTS_DEFAULT_VOICE": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Default Voice",
"help": "Default voice ID for synthesis",
"advanced": false
},
"TTS_DEFAULT_PROVIDER": {
"type": "string",
"required": false,
"sensitive": false,
"default": "auto",
"label": "Default Provider",
"help": "Default TTS provider (auto, elevenlabs, openai, edge)",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Volume2",
"group": "voice",
"groupOrder": 3,
"actions": ["enable", "configure"]
},
"resources": {
"homepage": "https://github.com/elizaos/eliza#readme",
"repository": "https://github.com/elizaos/eliza"
},
"dependsOn": [],
"kind": "plugin",
"subtype": "voice"
}
@@ -0,0 +1,168 @@
{
"id": "twilio",
"name": "Twilio",
"description": "A Twilio plugin for ElizaOS that enables SMS and voice call capabilities.",
"npmName": "@elizaos/plugin-twilio",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["llm", "twilio", "sms", "voice"],
"config": {
"TWILIO_AUTH_TOKEN": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "Auth Token",
"help": "Auth token for Twilio",
"advanced": false
},
"TWILIO_ACCOUNT_SID": {
"type": "secret",
"required": true,
"sensitive": true,
"label": "Account Sid",
"help": "Account SID for Twilio",
"advanced": false
},
"TWILIO_WEBHOOK_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Webhook Url",
"help": "Webhook URL",
"advanced": false
},
"VOICE_CALL_ENABLED": {
"type": "boolean",
"required": false,
"sensitive": false,
"label": "Call Enabled",
"help": "Enable or disable this feature",
"advanced": false
},
"TWILIO_PHONE_NUMBER": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Phone Number",
"help": "Phone number",
"advanced": false
},
"TWILIO_WEBHOOK_PORT": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Webhook Port",
"help": "Port number",
"advanced": false,
"min": 1,
"max": 65535
},
"VOICE_CALL_PROVIDER": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Call Provider",
"help": "Provider selection",
"advanced": false
},
"VOICE_CALL_TO_NUMBER": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Call To Number",
"help": "Phone number",
"advanced": false
},
"VOICE_CALL_ALLOW_FROM": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Call Allow From",
"help": "Comma-separated allowed user list",
"advanced": false
},
"VOICE_CALL_PUBLIC_URL": {
"type": "url",
"required": false,
"sensitive": false,
"label": "Call Public Url",
"help": "Public URL",
"advanced": false
},
"VOICE_CALL_FROM_NUMBER": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Call From Number",
"help": "Phone number",
"advanced": false
},
"VOICE_CALL_WEBHOOK_PATH": {
"type": "file-path",
"required": false,
"sensitive": false,
"label": "Call Webhook Path",
"help": "Webhook path",
"advanced": false
},
"VOICE_CALL_WEBHOOK_PORT": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Call Webhook Port",
"help": "Port number",
"advanced": false,
"min": 1,
"max": 65535
},
"VOICE_CALL_INBOUND_POLICY": {
"type": "string",
"required": false,
"sensitive": false,
"label": "Call Inbound Policy",
"help": "Inbound message/call policy",
"advanced": false
},
"VOICE_CALL_INBOUND_GREETING": {
"type": "textarea",
"required": false,
"sensitive": false,
"label": "Call Inbound Greeting",
"help": "Greeting message for inbound connections",
"advanced": false
},
"VOICE_CALL_MAX_CONCURRENT_CALLS": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Call Max Concurrent Calls",
"help": "Maximum concurrent calls",
"advanced": false
},
"VOICE_CALL_MAX_DURATION_SECONDS": {
"type": "number",
"required": false,
"sensitive": false,
"label": "Call Max Duration Seconds",
"help": "Maximum duration in seconds",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Phone",
"group": "feature-other",
"groupOrder": 12,
"actions": ["enable", "configure"]
},
"resources": {
"homepage": "https://github.com/elizaos/eliza#readme",
"repository": "https://github.com/elizaos/eliza",
"setupGuideUrl": "https://docs.eliza.ai/plugin-setup-guide#twilio-sms--voice"
},
"dependsOn": [],
"kind": "plugin",
"subtype": "other"
}
@@ -0,0 +1,130 @@
{
"id": "vercel-ai-gateway",
"name": "Vercel Ai Gateway",
"description": "Vercel AI Gateway provider for routing model requests through a single endpoint.",
"npmName": "@elizaos/plugin-vercel-ai-gateway",
"version": "2.0.0-beta.0",
"source": "store",
"tags": [
"ai-provider",
"llm",
"gateway",
"routing",
"vercel-ai-gateway",
"vercel"
],
"config": {
"AIGATEWAY_API_KEY": {
"type": "secret",
"required": false,
"sensitive": true,
"label": "Api Key",
"help": "Alternate env var for AI Gateway API key.",
"advanced": false,
"autoEnableProvider": true
},
"VERCEL_OIDC_TOKEN": {
"type": "secret",
"required": false,
"sensitive": true,
"label": "Oidc Token",
"help": "OIDC token for Vercel (used as Bearer auth for Gateway).",
"advanced": false
},
"AI_GATEWAY_API_KEY": {
"type": "secret",
"required": false,
"sensitive": true,
"label": "Gateway Api Key",
"help": "API key for Vercel AI Gateway (OpenAI-compatible endpoints).",
"advanced": false,
"autoEnableProvider": true
},
"AI_GATEWAY_BASE_URL": {
"type": "url",
"required": false,
"sensitive": false,
"default": "https://ai-gateway.vercel.sh/v1",
"label": "Gateway Base Url",
"help": "Base URL for AI Gateway OpenAI-compatible API.",
"advanced": false
},
"AI_GATEWAY_TIMEOUT_MS": {
"type": "number",
"required": false,
"sensitive": false,
"default": "60000",
"label": "Gateway Timeout Ms",
"help": "Request timeout in milliseconds.",
"advanced": false,
"min": 0,
"unit": "ms"
},
"AI_GATEWAY_IMAGE_MODEL": {
"type": "string",
"required": false,
"sensitive": false,
"default": "dall-e-3",
"label": "Gateway Image Model",
"help": "Identifier of the model used for image generation.",
"placeholder": "e.g., gpt-4o, claude-sonnet-4-20250514",
"advanced": false
},
"AI_GATEWAY_LARGE_MODEL": {
"type": "string",
"required": false,
"sensitive": false,
"default": "gpt-5",
"label": "Gateway Large Model",
"help": "Identifier of the large language model to be used.",
"placeholder": "e.g., gpt-4o, claude-sonnet-4-20250514",
"advanced": false
},
"AI_GATEWAY_SMALL_MODEL": {
"type": "string",
"required": false,
"sensitive": false,
"default": "gpt-5-mini",
"label": "Gateway Small Model",
"help": "Identifier of the small language model to be used.",
"placeholder": "e.g., gpt-4o, claude-sonnet-4-20250514",
"advanced": false
},
"AI_GATEWAY_EMBEDDING_MODEL": {
"type": "string",
"required": false,
"sensitive": false,
"default": "text-embedding-3-small",
"label": "Gateway Embedding Model",
"help": "Identifier of the model to use for generating text embeddings.",
"placeholder": "e.g., text-embedding-3-small",
"advanced": false
},
"AI_GATEWAY_EMBEDDING_DIMENSIONS": {
"type": "number",
"required": false,
"sensitive": false,
"default": "1536",
"label": "Gateway Embedding Dimensions",
"help": "Number of dimensions for the returned embedding vectors.",
"advanced": false
}
},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Triangle",
"group": "ai-provider",
"groupOrder": 0,
"actions": ["enable", "configure"]
},
"resources": {
"homepage": "https://github.com/elizaos-plugins/plugin-vercel-ai-gateway#readme",
"repository": "https://github.com/elizaos-plugins/plugin-vercel-ai-gateway",
"setupGuideUrl": "https://docs.eliza.ai/plugin-setup-guide#vercel-ai-gateway"
},
"dependsOn": [],
"kind": "plugin",
"subtype": "ai-provider"
}
@@ -0,0 +1,23 @@
{
"id": "webhooks",
"name": "Webhooks",
"description": "HTTP webhook ingress for external triggers (wake, agent, mapped hooks)",
"npmName": "@elizaos/plugin-webhooks",
"version": "2.0.0-beta.0",
"source": "store",
"tags": ["webhooks"],
"config": {},
"render": {
"visible": true,
"pinTo": [],
"style": "card",
"icon": "Webhook",
"group": "devtools",
"groupOrder": 5,
"actions": ["enable", "configure"]
},
"resources": {},
"dependsOn": [],
"kind": "plugin",
"subtype": "devtools"
}
@@ -0,0 +1,61 @@
/**
* Verifies the plugin-owned facewear entry validates against the first-party
* schema and is discoverable by both id and npm package name through the loaded
* registry.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { loadRegistryFromRawEntries } from "./loader";
import { registryEntrySchema } from "./schema";
// The facewear entry is plugin-owned: it lives next to its plugin and is
// aggregated into generated.json at build time (see generate.ts).
const FACEWEAR_ENTRY_PATH = join(
import.meta.dirname,
"..",
"..",
"..",
"..",
"plugins",
"plugin-facewear",
"registry-entry.json",
);
describe("facewear registry entry", () => {
it("is valid and discoverable by id and npm package name", () => {
const data = JSON.parse(readFileSync(FACEWEAR_ENTRY_PATH, "utf8"));
const parsed = registryEntrySchema.parse(data);
const registry = loadRegistryFromRawEntries([
{ file: FACEWEAR_ENTRY_PATH, data },
]);
if (parsed.kind !== "plugin") {
throw new Error("Expected facewear registry entry to be a plugin");
}
expect(parsed.kind).toBe("plugin");
if (parsed.kind !== "plugin") {
throw new Error("Expected facewear registry entry to be a plugin");
}
expect(parsed.subtype).toBe("media");
expect(parsed.npmName).toBe("@elizaos/plugin-facewear");
expect(parsed.config).toHaveProperty("FACEWEAR_SMARTGLASSES_TRANSPORT");
expect(parsed.config).toHaveProperty("FACEWEAR_INIT_MODE");
expect(parsed.tags).toEqual(
expect.arrayContaining([
"facewear",
"smartglasses",
"even-realities",
"bluetooth",
"wifi",
]),
);
expect(parsed.render.actions).toEqual(["enable", "configure"]);
expect(data.launch).toBeUndefined();
expect(registry.byId.get("facewear")?.name).toBe("Facewear");
expect(registry.byNpmName.get("@elizaos/plugin-facewear")?.id).toBe(
"facewear",
);
});
});
@@ -0,0 +1,290 @@
/**
* First-party registry aggregator — the build-time generator behind the
* `generate:first-party` script.
*
* Registration is plugin-side: each in-repo plugin/package owns its registry
* entry as a `registry-entry.json` in its own directory (a single entry object,
* or an array of entries). Curated entries with no vendored package — built-in
* app-viewers and entries for plugins not checked out in this repo — live under
* `curated/`. This script gathers all of them, validates each fail-loud against
* the Zod schema, dedupes by id, and writes the aggregated `generated.json` that
* the runtime loader reads (a single committed artifact, trivial to stage
* alongside an on-device bundle), plus the derived curated-app / channel /
* provider maps. `--check` re-runs the generator and fails on drift for CI.
*
* bun run --cwd packages/registry generate:first-party # rewrite generated.json
* bun run --cwd packages/registry generate:first-party --check # CI drift gate
*/
import { execFileSync } from "node:child_process";
import {
existsSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from "node:fs";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { type RegistryEntry, registryEntrySchema } from "./schema";
// Resolve biome from the repo-root package.json, not this file: this package
// doesn't declare @biomejs/biome, so under isolated installs resolving from
// here walks up PAST the repo into whatever stale copy a parent workspace
// hoisted — version-skewed against biome.json's pinned schema.
const rootRequire = createRequire(
new URL("../../../../package.json", import.meta.url),
);
// `JSON.stringify(…, null, 2)` puts every array element on its own line, but
// biome — the repo's format gate (`bun run format:check`) — collapses arrays
// that fit onto a single line. Without reconciling the two, the committed
// artifacts can only satisfy one gate at a time, and a later `registry build`
// (which re-runs this generator) silently re-breaks the biome gate. Piping each
// artifact through biome makes the generator emit exactly what `format:check`
// expects, so generator output, committed files, and the format gate all agree.
function biomeFormatJson(content: string, filePath: string): string {
const biomeBin = rootRequire.resolve("@biomejs/biome/bin/biome");
return execFileSync(
process.execPath,
[biomeBin, "format", `--stdin-file-path=${filePath}`],
{ input: content, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 },
);
}
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(HERE, "..", "..", "..", "..");
const CURATED_DIR = join(HERE, "curated");
const GENERATED_PATH = join(HERE, "generated.json");
// Small derived artifact: just the curated-app definitions (slug, canonicalName,
// aliases), ordered. `@elizaos/shared` statically imports this (browser-safe, no
// fs) to materialize ELIZA_CURATED_APP_DEFINITIONS without bundling the full
// registry. Regenerated alongside generated.json.
const CURATED_DEFS_PATH = join(HERE, "curated-app-definitions.json");
// Derived channel -> plugin-package map. agent + app-core statically import this
// (browser-safe, no fs) instead of hand-maintaining duplicate CHANNEL_PLUGIN_MAPs.
const CHANNEL_MAP_PATH = join(HERE, "channel-plugin-map.json");
// Derived env-key -> provider plugin package map. The agent statically imports
// this instead of hand-maintaining PROVIDER_PLUGIN_MAP. Entries opt in by
// marking config fields with `autoEnableProvider: true`.
const PROVIDER_MAP_PATH = join(HERE, "provider-plugin-map.json");
// Derived short-id -> plugin-package map. The agent statically imports this to
// build OPTIONAL_PLUGIN_MAP instead of hand-maintaining the alias table. Entries
// opt in by listing bare ids in `shortIds` (e.g. evm/solana/wallet -> wallet).
const SHORTID_MAP_PATH = join(HERE, "short-id-plugin-map.json");
interface CuratedAppDefinition {
slug: string;
canonicalName: string;
aliases: string[];
}
export function collectCuratedAppDefinitions(
entries: RegistryEntry[],
): CuratedAppDefinition[] {
return entries
.filter((e) => Boolean(e.curatedApp))
.sort((a, b) => (a.curatedApp?.order ?? 0) - (b.curatedApp?.order ?? 0))
.map((e) => ({
slug: e.curatedApp?.slug ?? "",
// canonicalName is the entry's npm package; every curated entry declares one.
canonicalName: e.npmName ?? "",
aliases: e.curatedApp?.aliases ?? [],
}));
}
// Derive the channel -> plugin-package map from connector entries' `channels`.
// This replaces the hand-maintained CHANNEL_PLUGIN_MAP duplicated in agent +
// app-core. Keys are sorted for a stable artifact; consumers read by key.
export function collectChannelPluginMap(
entries: RegistryEntry[],
): Record<string, string> {
const map: Record<string, string> = {};
for (const e of entries) {
if (!e.npmName) continue;
for (const channel of e.channels ?? []) {
if (map[channel] && map[channel] !== e.npmName) {
throw new Error(
`[registry/generate] channel "${channel}" claimed by both ${map[channel]} and ${e.npmName}`,
);
}
map[channel] = e.npmName;
}
}
return Object.fromEntries(
Object.keys(map)
.sort()
.map((k) => [k, map[k]]),
);
}
// Derive the short-id -> plugin-package map from entries' `shortIds`. This
// replaces the hand-maintained OPTIONAL_PLUGIN_MAP alias table for entries that
// declare a registry entry. Keys are sorted for a stable artifact. Conflicting
// claims fail loudly at generation time so drift cannot silently ship — the
// same guarantee CHANNEL_PLUGIN_MAP gives channel aliases.
export function collectShortIdPluginMap(
entries: RegistryEntry[],
): Record<string, string> {
const map: Record<string, string> = {};
for (const e of entries) {
if (!e.npmName) continue;
for (const shortId of e.shortIds ?? []) {
if (map[shortId] && map[shortId] !== e.npmName) {
throw new Error(
`[registry/generate] short id "${shortId}" claimed by both ${map[shortId]} and ${e.npmName}`,
);
}
map[shortId] = e.npmName;
}
}
return Object.fromEntries(
Object.keys(map)
.sort()
.map((k) => [k, map[k]]),
);
}
export function collectProviderPluginMap(
entries: RegistryEntry[],
): Record<string, string> {
const map: Record<string, string> = {};
for (const e of entries) {
if (!e.npmName) continue;
for (const [envKey, field] of Object.entries(e.config ?? {})) {
if (field.autoEnableProvider !== true) continue;
if (map[envKey] && map[envKey] !== e.npmName) {
throw new Error(
`[registry/generate] provider env key "${envKey}" claimed by both ${map[envKey]} and ${e.npmName}`,
);
}
map[envKey] = e.npmName;
}
}
return Object.fromEntries(
Object.keys(map)
.sort()
.map((k) => [k, map[k]]),
);
}
interface SourcedEntry {
entry: RegistryEntry;
file: string;
}
function readEntryFile(file: string): SourcedEntry[] {
const raw = JSON.parse(readFileSync(file, "utf-8")) as unknown;
const candidates = Array.isArray(raw) ? raw : [raw];
return candidates.map((data) => {
const parsed = registryEntrySchema.safeParse(data);
if (!parsed.success) {
throw new Error(
`[registry/generate] ${file} failed validation: ${String(parsed.error)}`,
);
}
return { entry: parsed.data, file };
});
}
function collectPluginOwnedEntries(): SourcedEntry[] {
const out: SourcedEntry[] = [];
for (const base of ["plugins", "packages"]) {
const baseDir = join(REPO_ROOT, base);
if (!existsSync(baseDir)) continue;
for (const dirent of readdirSync(baseDir, { withFileTypes: true })) {
if (!dirent.isDirectory()) continue;
const file = join(baseDir, dirent.name, "registry-entry.json");
if (existsSync(file)) out.push(...readEntryFile(file));
}
}
return out;
}
function collectCuratedEntries(): SourcedEntry[] {
const out: SourcedEntry[] = [];
if (!existsSync(CURATED_DIR)) return out;
const walk = (dir: string) => {
for (const dirent of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, dirent.name);
if (dirent.isDirectory()) {
walk(full);
} else if (dirent.name.endsWith(".json")) {
out.push(...readEntryFile(full));
}
}
};
walk(CURATED_DIR);
return out;
}
export function collectFirstPartyEntries(): RegistryEntry[] {
const sourced = [...collectPluginOwnedEntries(), ...collectCuratedEntries()];
const byId = new Map<string, string>();
for (const { entry, file } of sourced) {
const existing = byId.get(entry.id);
if (existing) {
throw new Error(
`[registry/generate] duplicate id "${entry.id}" in ${file} and ${existing}`,
);
}
byId.set(entry.id, file);
}
return sourced.map((s) => s.entry).sort((a, b) => a.id.localeCompare(b.id));
}
export function generateFirstPartyRegistry(): {
full: string;
curated: string;
channels: string;
providers: string;
shortIds: string;
} {
const entries = collectFirstPartyEntries();
return {
full: `${JSON.stringify({ entries }, null, 2)}\n`,
curated: `${JSON.stringify(collectCuratedAppDefinitions(entries), null, 2)}\n`,
channels: `${JSON.stringify(collectChannelPluginMap(entries), null, 2)}\n`,
providers: `${JSON.stringify(collectProviderPluginMap(entries), null, 2)}\n`,
shortIds: `${JSON.stringify(collectShortIdPluginMap(entries), null, 2)}\n`,
};
}
function main(): void {
const check = process.argv.includes("--check");
const next = generateFirstPartyRegistry();
const artifacts: [string, string][] = [
[GENERATED_PATH, biomeFormatJson(next.full, GENERATED_PATH)],
[CURATED_DEFS_PATH, biomeFormatJson(next.curated, CURATED_DEFS_PATH)],
[CHANNEL_MAP_PATH, biomeFormatJson(next.channels, CHANNEL_MAP_PATH)],
[PROVIDER_MAP_PATH, biomeFormatJson(next.providers, PROVIDER_MAP_PATH)],
[SHORTID_MAP_PATH, biomeFormatJson(next.shortIds, SHORTID_MAP_PATH)],
];
if (check) {
for (const [path, expected] of artifacts) {
const current =
existsSync(path) && statSync(path).isFile()
? readFileSync(path, "utf-8")
: "";
if (current !== expected) {
console.error(
`[registry/generate] ${path} is stale. Run \`bun run --cwd packages/registry generate:first-party\` and commit the result.`,
);
process.exit(1);
}
}
console.log("[registry/generate] generated artifacts are up to date.");
return;
}
for (const [path, content] of artifacts) writeFileSync(path, content);
const count = JSON.parse(next.full).entries.length;
const curatedCount = JSON.parse(next.curated).length;
console.log(
`[registry/generate] wrote ${count} entries + ${curatedCount} curated-app definitions`,
);
}
if (import.meta.main) {
main();
}
File diff suppressed because it is too large Load Diff
+175
View File
@@ -0,0 +1,175 @@
/**
* First-party curated registry — runtime entry point.
*
* Reads the aggregated `generated.json`, validates, caches, and exposes typed
* accessors. This is the single import path the rest of the codebase consumes
* (`@elizaos/registry/first-party`, re-exported by `@elizaos/app-core/registry`
* for backwards compatibility).
*
* Registration is plugin-side: bundled JSON is the default, and any plugin can
* contribute or override an entry at runtime via `registerRegistryEntry()`
* (deduped by `id`; runtime entries win). Resolving the generated file and the
* cache slot are both hardened against on-device bundling and circular-import
* re-entry (see `resolveGeneratedPath` and `cacheSlot`).
*/
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
indexEntries,
type LoadedRegistry,
loadRegistryFromRawEntries,
normalizeConnectorAuth,
} from "./loader";
import { type RegistryEntry, registryEntrySchema } from "./schema";
export * from "./app-registry";
export {
getApps,
getConnectors,
getEntry,
getEntryByNpmName,
getPlugins,
indexEntries,
type LoadedRegistry,
mergeWithRuntime,
normalizeConnectorAuth,
type RegistryValidationError,
} from "./loader";
export * from "./schema";
// Entries are aggregated at build time (see generate.ts) from plugin-owned
// `registry-entry.json` files plus the `curated/` set into a single committed
// `generated.json`. The runtime reads that one file — a single artifact that is
// trivial to stage alongside an on-device bundle.
//
// Bun.build collapses top-level `const` declarations into `var`s inside an
// `__esm` wrapper, and `import.meta.url` inside that wrapper can resolve to
// `undefined` on the on-device runtime when the module is initialised through
// the `Promise.resolve().then(() => init_xxx())` adapter the bundler emits for
// dynamic-import re-exports. The fallback to `process.argv[1]` matches the
// bundle's own entrypoint (e.g. `/data/data/.../agent-bundle.js`) so the
// registry sits at `<bundle-dir>/generated.json`.
function resolveGeneratedPath(): string {
const url =
typeof import.meta.url === "string" && import.meta.url
? import.meta.url
: null;
let moduleDir: string;
if (url) {
try {
moduleDir = dirname(fileURLToPath(url));
} catch {
moduleDir = dirname(process.argv[1] ?? process.cwd());
}
} else {
moduleDir = dirname(process.argv[1] ?? process.cwd());
}
return join(moduleDir, "generated.json");
}
// Plugin-side registration overlay. Symbol-keyed global so every consumer —
// regardless of which package instance imports this module — contributes to one
// store. Entries registered here override bundled JSON twins by `id`.
const RUNTIME_ENTRIES_KEY = Symbol.for(
"elizaos.first-party-registry.runtime-entries",
);
function getRuntimeEntryStore(): { entries: RegistryEntry[] } {
const globalObject = globalThis as Record<PropertyKey, unknown>;
const existing = globalObject[RUNTIME_ENTRIES_KEY] as
| { entries: RegistryEntry[] }
| null
| undefined;
if (existing) return existing;
const created = { entries: [] as RegistryEntry[] };
globalObject[RUNTIME_ENTRIES_KEY] = created;
return created;
}
/**
* Register (or override) a first-party registry entry at runtime.
*
* Validated fail-loud against `registryEntrySchema`, exactly like the bundled
* JSON loader, so a malformed plugin-contributed entry can't slip in. A
* registered entry overrides a bundled entry with the same `id`.
*/
export function registerRegistryEntry(entry: RegistryEntry): void {
const parsed = registryEntrySchema.safeParse(entry);
if (!parsed.success) {
throw new Error(
`registerRegistryEntry: entry failed validation: ${String(parsed.error)}`,
);
}
const normalized =
parsed.data.kind === "connector"
? normalizeConnectorAuth(parsed.data)
: parsed.data;
const store = getRuntimeEntryStore();
const idx = store.entries.findIndex((e) => e.id === normalized.id);
if (idx >= 0) {
store.entries[idx] = normalized;
} else {
store.entries.push(normalized);
}
// Invalidate the cache so the next loadRegistry() observes the new entry.
if (cacheSlot) cacheSlot.value = null;
}
// TDZ-hardening: this module's cached registry slot must survive being
// re-entered during circular-import partial evaluation on Bun's strict ESM
// runtime. A bare `let cache = null` would still be in the temporal dead zone
// when an import cycle re-enters `loadRegistry()`, throwing
// `Cannot access 'cache' before initialization` and bricking boot.
var cacheSlot: { value: LoadedRegistry | null } = { value: null };
function readEntriesFromDisk(): RegistryEntry[] {
const generatedPath = resolveGeneratedPath();
if (!existsSync(generatedPath)) {
// error-policy:J4 explicit designed degrade — in packaged builds the
// aggregated registry is legitimately not bundled, so an empty first-party
// set is a valid state (not a load failure). Warn and continue rather than
// crashing the agent subprocess. `console` is intentional: this low-level
// package has no `@elizaos/logger` dependency and must stay dependency-free.
console.warn(`[registry] generated.json missing: ${generatedPath}`);
return [];
}
const parsed = JSON.parse(readFileSync(generatedPath, "utf-8")) as {
entries?: unknown[];
};
const raws = (parsed.entries ?? []).map((data, i) => ({
file: `${generatedPath}#${i}`,
data,
}));
return loadRegistryFromRawEntries(raws).all;
}
export function loadRegistry(): LoadedRegistry {
// Self-heal: if a cycle re-entered us before the module-top initializer ran,
// hoisted `cacheSlot` is `undefined`. Lazily initialize so we never throw.
if (!cacheSlot) {
cacheSlot = { value: null };
}
if (cacheSlot.value) return cacheSlot.value;
const fileEntries = readEntriesFromDisk();
const runtime = getRuntimeEntryStore().entries;
if (runtime.length === 0) {
cacheSlot.value = indexEntries(fileEntries);
return cacheSlot.value;
}
// Plugin-registered entries override bundled twins by id.
const merged = new Map(fileEntries.map((e) => [e.id, e]));
for (const e of runtime) merged.set(e.id, e);
cacheSlot.value = indexEntries([...merged.values()]);
return cacheSlot.value;
}
export function clearRegistryCacheForTests(): void {
if (!cacheSlot) {
cacheSlot = { value: null };
return;
}
cacheSlot.value = null;
}
+224
View File
@@ -0,0 +1,224 @@
/**
* Registry loader. Validates raw entries and indexes them by id, kind, group,
* and npm name, and provides the kind-narrowed accessors plus the legacy
* `auth` → `accounts.agent` normalization.
*
* Static data only. Runtime overlay (enabled, configured, isActive) is merged
* in at the API layer via `mergeWithRuntime()` — the loader never touches it.
*
* Validation is fail-loud at boot: bad entries throw with a precise zod
* message naming the offending file, so a malformed entry can't slip into a
* running process.
*/
import {
type AccountAuthKind,
type AppEntry,
type ConnectorEntry,
type PluginEntry,
type RegistryEntry,
type RegistryKind,
type RegistryRuntimeOverlay,
type RegistryView,
registryEntrySchema,
} from "./schema.js";
export class RegistryValidationError extends Error {
readonly file: string;
readonly cause: unknown;
constructor(file: string, cause: unknown) {
super(`Registry entry at ${file} failed validation: ${String(cause)}`);
this.name = "RegistryValidationError";
this.file = file;
this.cause = cause;
}
}
export interface LoadedRegistry {
byId: Map<string, RegistryEntry>;
byKind: Map<RegistryKind, RegistryEntry[]>;
byGroup: Map<string, RegistryEntry[]>;
byNpmName: Map<string, RegistryEntry>;
all: RegistryEntry[];
}
interface RawEntry {
file: string;
data: unknown;
}
export function loadRegistryFromRawEntries(raws: RawEntry[]): LoadedRegistry {
const seenIds = new Set<string>();
const all: RegistryEntry[] = [];
for (const { file, data } of raws) {
const parsed = registryEntrySchema.safeParse(data);
if (!parsed.success) {
throw new RegistryValidationError(file, parsed.error);
}
const entry =
parsed.data.kind === "connector"
? normalizeConnectorAuth(parsed.data)
: parsed.data;
if (seenIds.has(entry.id)) {
throw new RegistryValidationError(
file,
`duplicate id "${entry.id}" — every registry entry must have a unique id`,
);
}
seenIds.add(entry.id);
all.push(entry);
}
return indexEntries(all);
}
// Legacy `auth` → `accounts.agent` auto-mapping. Connector manifests that
// only declared `auth` keep working without edits — the resulting `accounts`
// shape lets the UI render a single agent section (backwards compatible) and
// gives downstream code a uniform field to read.
//
// The guard checks `accounts?.agent` (not just `accounts`) so that incremental
// migrations — a manifest that adds `accounts.owner` first while keeping
// `auth` as the agent credential source — still get the legacy auth mapped
// onto `accounts.agent`. A pre-existing explicit `accounts.agent` is always
// preserved.
export function normalizeConnectorAuth(entry: ConnectorEntry): ConnectorEntry {
if (!entry.auth || entry.accounts?.agent) {
return entry;
}
const authKind: AccountAuthKind = mapLegacyAuthKindToAccountAuthKind(
entry.auth.kind,
);
return {
...entry,
accounts: {
...(entry.accounts ?? {}),
agent: {
supported: true,
authKind,
credentialKeys: entry.auth.credentialKeys,
},
},
};
}
// Maps the legacy four-value auth.kind enum onto the richer AccountAuthKind.
// `credentials` (username + password) is materially different from `api-key`
// (a single opaque token) but the current AccountAuthKind enum has no
// dedicated arm — both flows land on a manual paste of secret fields in the
// connector setup UI, so they coalesce here. Connectors with username+password
// flows should declare `accounts.agent.authKind` explicitly in their manifest
// when they need to distinguish, rather than relying on this auto-mapping.
function mapLegacyAuthKindToAccountAuthKind(
kind: "token" | "oauth" | "credentials" | "none",
): AccountAuthKind {
switch (kind) {
case "oauth":
return "oauth-cloud";
case "none":
return "none";
case "credentials":
case "token":
return "api-key";
}
}
export function indexEntries(entries: RegistryEntry[]): LoadedRegistry {
const byId = new Map<string, RegistryEntry>();
const byKind = new Map<RegistryKind, RegistryEntry[]>([
["app", []],
["plugin", []],
["connector", []],
]);
const byGroup = new Map<string, RegistryEntry[]>();
const byNpmName = new Map<string, RegistryEntry>();
for (const entry of entries) {
byId.set(entry.id, entry);
byKind.get(entry.kind)?.push(entry);
const groupBucket = byGroup.get(entry.render.group);
if (groupBucket) {
groupBucket.push(entry);
} else {
byGroup.set(entry.render.group, [entry]);
}
if (entry.npmName) {
byNpmName.set(entry.npmName, entry);
}
}
for (const [group, bucket] of byGroup) {
bucket.sort(compareEntriesForDisplay);
byGroup.set(group, bucket);
}
return { byId, byKind, byGroup, byNpmName, all: entries };
}
function compareEntriesForDisplay(a: RegistryEntry, b: RegistryEntry): number {
const aOrder = a.render.groupOrder ?? Number.MAX_SAFE_INTEGER;
const bOrder = b.render.groupOrder ?? Number.MAX_SAFE_INTEGER;
if (aOrder !== bOrder) return aOrder - bOrder;
return a.name.localeCompare(b.name);
}
// ---------------------------------------------------------------------------
// Typed kind-narrowed accessors. Keeps callers from re-asserting kind.
// ---------------------------------------------------------------------------
export function getApps(registry: LoadedRegistry): AppEntry[] {
return (registry.byKind.get("app") ?? []) as AppEntry[];
}
export function getPlugins(registry: LoadedRegistry): PluginEntry[] {
return (registry.byKind.get("plugin") ?? []) as PluginEntry[];
}
export function getConnectors(registry: LoadedRegistry): ConnectorEntry[] {
return (registry.byKind.get("connector") ?? []) as ConnectorEntry[];
}
export function getEntry(
registry: LoadedRegistry,
id: string,
): RegistryEntry | undefined {
return registry.byId.get(id);
}
export function getEntryByNpmName(
registry: LoadedRegistry,
npmName: string,
): RegistryEntry | undefined {
return registry.byNpmName.get(npmName);
}
// ---------------------------------------------------------------------------
// Runtime overlay merge. The API calls this once per request after fetching
// runtime state. Static registry stays pure.
// ---------------------------------------------------------------------------
export function mergeWithRuntime(
entries: RegistryEntry[],
overlays: RegistryRuntimeOverlay[],
): RegistryView[] {
const overlayById = new Map(overlays.map((o) => [o.id, o]));
return entries.map((entry) => {
const overlay = overlayById.get(entry.id) ?? defaultOverlay(entry.id);
return { ...entry, ...overlay };
});
}
function defaultOverlay(id: string): RegistryRuntimeOverlay {
return {
id,
enabled: false,
configured: false,
isActive: false,
validationErrors: [],
validationWarnings: [],
};
}
@@ -0,0 +1,37 @@
/**
* Verifies plugin-local-inference self-declares its PRE-READY boot hook in the
* first-party registry, so the app-core host drains it through the generic
* boot-hook channel instead of hard-wiring the
* `@elizaos/plugin-local-inference/runtime` internals at fixed init points in
* `repairRuntimeAfterBoot` (arch-audit #12089 item 18).
*/
import { afterEach, describe, expect, it } from "vitest";
import {
clearRegistryCacheForTests,
getEntryByNpmName,
loadRegistry,
} from "./index";
import type { PluginEntry } from "./schema";
describe("local-inference boot-hook registry declaration", () => {
afterEach(() => {
clearRegistryCacheForTests();
});
it("declares registerLocalInferenceBoot as its pre-ready bootHook", () => {
const entry = getEntryByNpmName(
loadRegistry(),
"@elizaos/plugin-local-inference",
) as PluginEntry | undefined;
expect(entry).toBeDefined();
expect(entry?.kind).toBe("plugin");
// Self-declared boot hook: the host drains this through the generic
// pre-ready boot-hook channel instead of hard-wiring the local-inference
// specifier + its internals in `repairRuntimeAfterBoot`.
expect(entry?.launch?.bootHook).toEqual({
specifier: "@elizaos/plugin-local-inference/runtime",
exportName: "registerLocalInferenceBoot",
});
});
});
@@ -0,0 +1,20 @@
{
"AIGATEWAY_API_KEY": "@elizaos/plugin-vercel-ai-gateway",
"AI_GATEWAY_API_KEY": "@elizaos/plugin-vercel-ai-gateway",
"ANTHROPIC_API_KEY": "@elizaos/plugin-anthropic",
"CEREBRAS_API_KEY": "@elizaos/plugin-openai",
"ELIZAOS_CLOUD_API_KEY": "@elizaos/plugin-elizacloud",
"ELIZAOS_CLOUD_ENABLED": "@elizaos/plugin-elizacloud",
"ELIZA_CHAT_VIA_CLI": "@elizaos/plugin-cli-inference",
"GEMINI_API_KEY": "@elizaos/plugin-google-genai",
"GOOGLE_API_KEY": "@elizaos/plugin-google-genai",
"GOOGLE_GENERATIVE_AI_API_KEY": "@elizaos/plugin-google-genai",
"GROQ_API_KEY": "@elizaos/plugin-groq",
"NEARAI_API_KEY": "@elizaos/plugin-nearai",
"OLLAMA_BASE_URL": "@elizaos/plugin-ollama",
"OPENAI_API_KEY": "@elizaos/plugin-openai",
"OPENROUTER_API_KEY": "@elizaos/plugin-openrouter",
"XAI_API_KEY": "@elizaos/plugin-xai",
"ZAI_API_KEY": "@elizaos/plugin-zai",
"Z_AI_API_KEY": "@elizaos/plugin-zai"
}
@@ -0,0 +1,105 @@
/**
* Guards the generated provider→plugin env-key map: the contract is preserved in
* the built artifact, keys are derived only from explicit config markers, and
* duplicate env-key claims across plugins are rejected.
*/
import { describe, expect, it } from "vitest";
import { collectProviderPluginMap } from "./generate";
import providerPluginMap from "./provider-plugin-map.json" with {
type: "json",
};
import type { RegistryEntry } from "./schema";
const expectedProviderPluginMap = {
AIGATEWAY_API_KEY: "@elizaos/plugin-vercel-ai-gateway",
AI_GATEWAY_API_KEY: "@elizaos/plugin-vercel-ai-gateway",
ANTHROPIC_API_KEY: "@elizaos/plugin-anthropic",
CEREBRAS_API_KEY: "@elizaos/plugin-openai",
ELIZAOS_CLOUD_API_KEY: "@elizaos/plugin-elizacloud",
ELIZAOS_CLOUD_ENABLED: "@elizaos/plugin-elizacloud",
ELIZA_CHAT_VIA_CLI: "@elizaos/plugin-cli-inference",
GEMINI_API_KEY: "@elizaos/plugin-google-genai",
GOOGLE_API_KEY: "@elizaos/plugin-google-genai",
GOOGLE_GENERATIVE_AI_API_KEY: "@elizaos/plugin-google-genai",
GROQ_API_KEY: "@elizaos/plugin-groq",
NEARAI_API_KEY: "@elizaos/plugin-nearai",
OLLAMA_BASE_URL: "@elizaos/plugin-ollama",
OPENAI_API_KEY: "@elizaos/plugin-openai",
OPENROUTER_API_KEY: "@elizaos/plugin-openrouter",
XAI_API_KEY: "@elizaos/plugin-xai",
ZAI_API_KEY: "@elizaos/plugin-zai",
Z_AI_API_KEY: "@elizaos/plugin-zai",
};
function providerEntry(
id: string,
npmName: string,
envKey: string,
): RegistryEntry {
return {
id,
name: id,
npmName,
source: "bundled",
tags: ["ai-provider"],
config: {
[envKey]: {
type: "secret",
required: false,
sensitive: true,
autoEnableProvider: true,
},
},
render: {
visible: true,
pinTo: [],
style: "card",
group: "models",
actions: [],
},
resources: {},
dependsOn: [],
channels: [],
kind: "plugin",
subtype: "ai-provider",
};
}
describe("provider plugin map generation", () => {
it("keeps the provider env contract in the generated artifact", () => {
expect(providerPluginMap).toEqual(expectedProviderPluginMap);
});
it("derives provider env keys only from explicit config markers", () => {
expect(
collectProviderPluginMap([
providerEntry("marked", "@elizaos/plugin-marked", "MARKED_API_KEY"),
{
...providerEntry(
"unmarked",
"@elizaos/plugin-unmarked",
"UNMARKED_API_KEY",
),
config: {
UNMARKED_API_KEY: {
type: "secret",
required: false,
sensitive: true,
},
},
},
]),
).toEqual({ MARKED_API_KEY: "@elizaos/plugin-marked" });
});
it("rejects duplicate provider env-key claims", () => {
expect(() =>
collectProviderPluginMap([
providerEntry("one", "@elizaos/plugin-one", "DUPLICATE_API_KEY"),
providerEntry("two", "@elizaos/plugin-two", "DUPLICATE_API_KEY"),
]),
).toThrow(
'provider env key "DUPLICATE_API_KEY" claimed by both @elizaos/plugin-one and @elizaos/plugin-two',
);
});
});
+453
View File
@@ -0,0 +1,453 @@
/**
* Registry source-of-truth Zod schemas and inferred types for apps, plugins,
* and connectors — config fields, render hints, per-account auth, the
* discriminated `registryEntrySchema` union, and the runtime overlay/view.
*
* Replaces the fragmented surface of:
* - plugins.json (97 entries, 5 categories)
* - PluginInfo (api/client-types-config.ts)
* - ConfigUiHint (types/index.ts)
* - RegistryAppInfo (shared/contracts/apps.ts)
* - VISIBLE_CONNECTOR_IDS / DEFAULT_ICONS / FEATURE_SUBGROUP / SUBGROUP_DISPLAY_ORDER
* (components/pages/plugin-list-utils.ts)
* - paramsToSchema() heuristics (PORT/TIMEOUT/MODEL guessing)
*
* Static registry only. Runtime overlay (enabled, configured, isActive,
* validationErrors) lives in RegistryRuntimeOverlay and is merged at API read
* time — never in the registry files themselves.
*/
import * as zod from "zod";
const z = (zod as typeof zod & { z?: typeof zod }).z ?? zod;
// ---------------------------------------------------------------------------
// Config field schema — replaces PluginParamDef + ConfigUiHint.
// One field, one place. UI hints are co-located with type info.
// ---------------------------------------------------------------------------
const configFieldType = z.enum([
"string",
"secret",
"boolean",
"number",
"select",
"multiselect",
"json",
"textarea",
"url",
"file-path",
]);
const configFieldOption = z.object({
value: z.string(),
label: z.string(),
description: z.string().optional(),
icon: z.string().optional(),
disabled: z.boolean().optional(),
});
const visibilityCondition: zod.ZodType<{
key: string;
equals?: unknown;
in?: unknown[];
notEquals?: unknown;
}> = z.object({
key: z.string(),
equals: z.unknown().optional(),
in: z.array(z.unknown()).optional(),
notEquals: z.unknown().optional(),
});
export const configFieldSchema = z.object({
type: configFieldType,
required: z.boolean(),
sensitive: z.boolean().optional(),
default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
label: z.string().optional(),
help: z.string().optional(),
placeholder: z.string().optional(),
group: z.string().optional(),
order: z.number().int().optional(),
width: z.enum(["full", "half", "third"]).optional(),
advanced: z.boolean().optional(),
hidden: z.boolean().optional(),
readonly: z.boolean().optional(),
icon: z.string().optional(),
options: z.array(configFieldOption).optional(),
pattern: z.string().optional(),
patternError: z.string().optional(),
min: z.number().optional(),
max: z.number().optional(),
step: z.number().optional(),
unit: z.string().optional(),
visible: visibilityCondition.optional(),
// When true, this config key auto-enables the entry's `npmName` in runtime
// provider discovery. This drives the generated provider-plugin map; do not
// infer provider env vars from naming patterns.
autoEnableProvider: z.boolean().optional(),
});
export type ConfigField = zod.infer<typeof configFieldSchema>;
// ---------------------------------------------------------------------------
// Render hints — replaces VISIBLE_CONNECTOR_IDS / DEFAULT_ICONS /
// FEATURE_SUBGROUP / SUBGROUP_DISPLAY_ORDER.
//
// Surface mapping is implicit:
// kind: "connector" → ConnectorsView (primary)
// kind: "app" → AppsView (primary)
// kind: "plugin" → PluginsView (primary)
// Every entry shows in its primary surface unless `visible: false`.
//
// Use `pinTo` to ALSO surface an item somewhere it wouldn't appear by default
// (e.g. promoting an app into the chat quick-launcher). Opt-in only — keeps
// the common case zero-config.
// ---------------------------------------------------------------------------
const renderActionSchema = z.enum([
"enable",
"configure",
"launch",
"attach",
"detach",
"stop",
"uninstall",
"install",
"setup-guide",
]);
const secondarySurfaceSchema = z.enum([
"chat-apps-section",
"settings-integrations",
// OS-mode launcher. OS-level system entries (contacts, phone, wifi) set
// `visible: false` + `pinTo: ["os-shell"]` so they are hidden from the
// app/web/desktop catalogs and surface only when running as the device OS,
// where the native contacts/phone/wifi APIs are available.
"os-shell",
]);
export const renderSchema = z.object({
visible: z.boolean().default(true),
pinTo: z.array(secondarySurfaceSchema).default([]),
style: z.enum(["card", "setup-panel", "hero-card"]).default("card"),
icon: z.string().optional(),
heroImage: z.string().optional(),
group: z.string(),
groupOrder: z.number().int().optional(),
actions: z.array(renderActionSchema).default([]),
});
export type RenderHints = zod.infer<typeof renderSchema>;
export type SecondarySurface = zod.infer<typeof secondarySurfaceSchema>;
// ---------------------------------------------------------------------------
// External resources (already in plugins.json today).
// ---------------------------------------------------------------------------
export const resourcesSchema = z.object({
homepage: z.string().url().optional(),
repository: z.string().url().optional(),
setupGuideUrl: z.string().url().optional(),
});
export type Resources = zod.infer<typeof resourcesSchema>;
// ---------------------------------------------------------------------------
// App-only: launch + viewer + session (mirrors RegistryAppInfo).
// ---------------------------------------------------------------------------
const appViewerSchema = z.object({
url: z.string(),
embedParams: z.record(z.string(), z.string()).optional(),
postMessageAuth: z.boolean().optional(),
sandbox: z.string().optional(),
});
const appSessionSchema = z.object({
mode: z.enum(["viewer", "spectate-and-steer", "external"]),
features: z
.array(z.enum(["commands", "telemetry", "pause", "resume", "suggestions"]))
.optional(),
});
const appSupportsSchema = z.object({
v0: z.boolean(),
v1: z.boolean(),
v2: z.boolean(),
});
const appNpmSchema = z.object({
package: z.string(),
v0Version: z.string().nullable(),
v1Version: z.string().nullable(),
v2Version: z.string().nullable(),
});
const packageRoutePluginSpecifierSchema = z
.string()
.min(1)
.refine(
(value) =>
!value.startsWith(".") &&
!value.startsWith("/") &&
!/(^|\/)(apps|plugins)\//.test(value),
"routePlugin.specifier must be a package specifier, not a filesystem path",
);
const appRoutePluginSchema = z.object({
specifier: packageRoutePluginSpecifierSchema,
exportName: z.string().min(1).optional(),
});
// An app's optional runtime-hook contributor: a named export
// `(runtime) => void | Promise<void>` the host invokes once, post-ready, to wire
// runtime-only concerns (services, cron jobs, background bootstraps) that never
// reach the route table. Parallel to `routePlugin` but for the generic
// runtime-hook channel the boot tail drains — the host resolves it by data, so
// no feature plugin is hard-wired by name. `exportName` is required: unlike a
// route plugin (which can be the module default), a hook must name its function.
const appRuntimeHookSchema = z.object({
specifier: packageRoutePluginSpecifierSchema,
exportName: z.string().min(1),
});
// An app's optional PRE-READY boot hook: a named export
// `(runtime) => void | Promise<void>` the host invokes once, at a fixed point in
// `repairRuntimeAfterBoot` BEFORE the runtime is marked ready, to install
// handlers / warm subsystems that must exist before the first turn (e.g. a local
// model handler). Parallel to `runtimeHook`, but pre-ready instead of post-ready:
// the host resolves it by data (naming no plugin) and drains it through the
// generic boot-hook registry. The hook owns its own applicability gating
// (platform/config checks) — it is a no-op when it does not apply. `exportName`
// is required (a hook must name its function).
const appBootHookSchema = z.object({
specifier: packageRoutePluginSpecifierSchema,
exportName: z.string().min(1),
});
export const appLaunchSchema = z.object({
type: z.enum(["internal-tab", "overlay", "server-launch"]),
target: z.string().optional(),
url: z.string().nullable().optional(),
viewer: appViewerSchema.optional(),
session: appSessionSchema.optional(),
supports: appSupportsSchema.optional(),
npm: appNpmSchema.optional(),
capabilities: z.array(z.string()).default([]),
uiExtension: z.object({ detailPanelId: z.string() }).optional(),
curatedSlug: z.string().optional(),
routePlugin: appRoutePluginSchema.optional(),
runtimeHook: appRuntimeHookSchema.optional(),
bootHook: appBootHookSchema.optional(),
/**
* If true, the app declares itself as the default landing tab.
* Mirrors `package.json#elizaos.app.mainTab`. Consumed by
* `getMainTabApp()` in this package to compute the shell's landing
* tab. Exactly one installed app should set this; multiple declarers
* are resolved deterministically by alphabetic id.
*/
mainTab: z.boolean().optional(),
});
export type AppLaunch = zod.infer<typeof appLaunchSchema>;
// ---------------------------------------------------------------------------
// Common fields shared by every entry.
// ---------------------------------------------------------------------------
const commonFields = {
id: z
.string()
.min(1)
.regex(/^[a-z0-9][a-z0-9-]*$/, "id must be kebab-case ascii"),
name: z.string().min(1),
description: z.string().optional(),
npmName: z.string().optional(),
version: z.string().optional(),
releaseStream: z.enum(["latest", "beta"]).optional(),
source: z.enum(["bundled", "store"]).default("bundled"),
tags: z.array(z.string()).default([]),
config: z.record(z.string(), configFieldSchema).default({}),
render: renderSchema,
resources: resourcesSchema.default({}),
dependsOn: z.array(z.string()).default([]),
// Channel keys this entry handles (drives CHANNEL_PLUGIN_MAP). Usually `[id]`,
// but an entry can claim aliases (e.g. x -> ["x", "twitter"]). Most entries
// declare none. Connectors are the typical owners, but a plugin may also claim
// a channel (e.g. blooio).
channels: z.array(z.string()).default([]),
// Optional short-id aliases this entry claims (drives OPTIONAL_PLUGIN_MAP).
// These are the bare ids that `plugins.allow`, `plugins.entries`, and
// `config.features` may carry (e.g. "evm", "solana", "wallet") which must
// resolve to this entry's `npmName` instead of falling through to loading the
// short id as a literal package name (`import("evm")`). The declaring plugin
// owns its aliases here; the central OPTIONAL_PLUGIN_MAP is generated from
// these instead of a hand-synced host table. Most entries declare none.
shortIds: z.array(z.string()).default([]),
// Curated-app marker: when present, this entry is one of the curated apps the
// agent can resolve by name (NL matching). `slug` is the short curated name,
// `order` fixes its catalog position, `aliases` are extra match terms. The
// canonical name is the entry's `npmName`. Drives ELIZA_CURATED_APP_DEFINITIONS.
curatedApp: z
.object({
slug: z.string(),
order: z.number(),
aliases: z.array(z.string()).default([]),
})
.optional(),
} as const;
// ---------------------------------------------------------------------------
// Discriminated union — three kinds, each with their own constraints.
// ---------------------------------------------------------------------------
const pluginSubtype = z.enum([
"ai-provider",
"feature",
"database",
"voice",
"documents",
"blockchain",
"media",
"agents",
"automation",
"storage",
"gaming",
"devtools",
"other",
]);
const connectorSubtype = z.enum([
"messaging",
"social",
"streaming",
"email",
"calendar",
"other",
]);
export const pluginEntrySchema = z.object({
...commonFields,
kind: z.literal("plugin"),
subtype: pluginSubtype,
launch: appLaunchSchema.optional(),
// When true, this voice plugin is the runtime's default TEXT_TO_SPEECH
// provider — the one wired in when no other TTS plugin has self-registered a
// handler. Consumed by `resolveDefaultTextToSpeechProvider()` in
// @elizaos/app-core, which selects by this flag (data-driven) rather than by
// hard-coding a plugin id. Only meaningful on `subtype: "voice"`; exactly one
// voice entry should set it. Mirrors the `mainTab` pattern above.
defaultTextToSpeech: z.boolean().optional(),
});
// ---------------------------------------------------------------------------
// Per-account auth config. Connectors can declare an OWNER side (the user's
// own platform account — e.g. user's Gmail, user's Discord) and/or an AGENT
// side (a separate identity the agent operates — e.g. a bot Gmail, a Discord
// bot). Auth method, credential keys, and OS support are independent per side.
//
// Purely additive over `auth`. When a manifest only declares `auth`, the
// loader auto-maps it to `accounts.agent` (see loader.ts:normalizeConnectorAuth).
// ---------------------------------------------------------------------------
const accountAuthKind = z.enum([
"oauth-cloud", // "Log in with X" routed through Eliza Cloud
"oauth-local", // local-only OAuth (e.g. per-homeserver Matrix)
"qr", // QR-pairing (WhatsApp Baileys, Signal device-link)
"local-app", // local-app inspection (Discord-CDP, iMessage chat.db)
"browser-extension", // browser companion
"api-key", // manual paste of bot token / API key
"none",
]);
const accountOsSupport = z.enum(["darwin", "win32", "linux"]);
export const accountConfigSchema = z.object({
supported: z.boolean().default(true),
authKind: accountAuthKind,
credentialKeys: z.array(z.string()).default([]),
osSupport: z.array(accountOsSupport).optional(),
notes: z.string().optional(),
});
export type AccountConfig = zod.infer<typeof accountConfigSchema>;
export type AccountAuthKind = zod.infer<typeof accountAuthKind>;
export const connectorEntrySchema = z.object({
...commonFields,
kind: z.literal("connector"),
subtype: connectorSubtype,
auth: z
.object({
kind: z.enum(["token", "oauth", "credentials", "none"]),
credentialKeys: z.array(z.string()).default([]),
})
.optional(),
accounts: z
.object({
owner: accountConfigSchema.optional(),
agent: accountConfigSchema.optional(),
})
.refine((val) => val.owner !== undefined || val.agent !== undefined, {
message:
"accounts must define at least one of owner or agent — an empty {} is meaningless and indicates an invalid manifest",
})
.optional(),
});
export const appEntrySchema = z.object({
...commonFields,
kind: z.literal("app"),
subtype: z.enum(["game", "tool", "shell", "marketplace", "trading", "other"]),
launch: appLaunchSchema,
});
export const registryEntrySchema = z.discriminatedUnion("kind", [
pluginEntrySchema,
connectorEntrySchema,
appEntrySchema,
]);
export type PluginEntry = zod.infer<typeof pluginEntrySchema>;
export type ConnectorEntry = zod.infer<typeof connectorEntrySchema>;
export type AppEntry = zod.infer<typeof appEntrySchema>;
export type RegistryEntry = zod.infer<typeof registryEntrySchema>;
export type RegistryKind = RegistryEntry["kind"];
// ---------------------------------------------------------------------------
// Runtime overlay — never in registry files. Merged at API read time.
// ---------------------------------------------------------------------------
export const registryRuntimeOverlaySchema = z.object({
id: z.string(),
enabled: z.boolean(),
configured: z.boolean(),
isActive: z.boolean(),
loadError: z.string().optional(),
validationErrors: z
.array(z.object({ field: z.string(), message: z.string() }))
.default([]),
validationWarnings: z
.array(z.object({ field: z.string(), message: z.string() }))
.default([]),
installedVersion: z.string().optional(),
latestVersion: z.string().nullable().optional(),
});
export type RegistryRuntimeOverlay = zod.infer<
typeof registryRuntimeOverlaySchema
>;
// ---------------------------------------------------------------------------
// Combined view — what the API hands to the UI.
// ---------------------------------------------------------------------------
export type RegistryView = RegistryEntry & RegistryRuntimeOverlay;
@@ -0,0 +1,27 @@
{
"agent-wallet": "@elizaos/plugin-wallet",
"agent_wallet": "@elizaos/plugin-wallet",
"app-browser": "@elizaos/plugin-browser",
"app-polymarket": "@elizaos/plugin-polymarket",
"appBrowser": "@elizaos/plugin-browser",
"appPolymarket": "@elizaos/plugin-polymarket",
"bluebubbles": "@elizaos/plugin-bluebubbles",
"browser": "@elizaos/plugin-browser",
"browser-bridge": "@elizaos/plugin-browser",
"browserBridge": "@elizaos/plugin-browser",
"coding-agent": "@elizaos/plugin-coding-tools",
"coding-tools": "@elizaos/plugin-coding-tools",
"codingAgent": "@elizaos/plugin-coding-tools",
"codingTools": "@elizaos/plugin-coding-tools",
"computeruse": "@elizaos/plugin-computeruse",
"discordLocal": "@elizaos/plugin-discord-local",
"eliza-browser": "@elizaos/plugin-browser",
"elizaBrowser": "@elizaos/plugin-browser",
"elizacloud": "@elizaos/plugin-elizacloud",
"evm": "@elizaos/plugin-wallet",
"form": "@elizaos/plugin-form",
"polymarket": "@elizaos/plugin-polymarket",
"solana": "@elizaos/plugin-wallet",
"vision": "@elizaos/plugin-vision",
"wallet": "@elizaos/plugin-wallet"
}
@@ -0,0 +1,111 @@
/**
* Guards the generated short-id -> plugin-package map: the alias contract is
* preserved in the built artifact, keys are derived only from entries' explicit
* `shortIds` markers, and duplicate short-id claims across plugins are rejected
* (fail-loud on drift instead of silently letting one entry win). This replaces
* the hand-synced OPTIONAL_PLUGIN_MAP alias table for registry-owned plugins.
*/
import { describe, expect, it } from "vitest";
import { collectShortIdPluginMap } from "./generate";
import type { RegistryEntry } from "./schema";
import shortIdPluginMap from "./short-id-plugin-map.json" with { type: "json" };
const expectedShortIdPluginMap = {
"agent-wallet": "@elizaos/plugin-wallet",
agent_wallet: "@elizaos/plugin-wallet",
"app-browser": "@elizaos/plugin-browser",
"app-polymarket": "@elizaos/plugin-polymarket",
appBrowser: "@elizaos/plugin-browser",
appPolymarket: "@elizaos/plugin-polymarket",
bluebubbles: "@elizaos/plugin-bluebubbles",
browser: "@elizaos/plugin-browser",
"browser-bridge": "@elizaos/plugin-browser",
browserBridge: "@elizaos/plugin-browser",
"coding-agent": "@elizaos/plugin-coding-tools",
"coding-tools": "@elizaos/plugin-coding-tools",
codingAgent: "@elizaos/plugin-coding-tools",
codingTools: "@elizaos/plugin-coding-tools",
computeruse: "@elizaos/plugin-computeruse",
discordLocal: "@elizaos/plugin-discord-local",
"eliza-browser": "@elizaos/plugin-browser",
elizaBrowser: "@elizaos/plugin-browser",
elizacloud: "@elizaos/plugin-elizacloud",
evm: "@elizaos/plugin-wallet",
form: "@elizaos/plugin-form",
polymarket: "@elizaos/plugin-polymarket",
solana: "@elizaos/plugin-wallet",
vision: "@elizaos/plugin-vision",
wallet: "@elizaos/plugin-wallet",
};
function shortIdEntry(
id: string,
npmName: string,
shortIds: string[],
): RegistryEntry {
return {
id,
name: id,
npmName,
source: "bundled",
tags: [],
config: {},
render: {
visible: true,
pinTo: [],
style: "card",
group: "apps",
actions: [],
},
resources: {},
dependsOn: [],
channels: [],
shortIds,
kind: "plugin",
subtype: "feature",
};
}
describe("short-id plugin map generation", () => {
it("keeps the short-id alias contract in the generated artifact", () => {
expect(shortIdPluginMap).toEqual(expectedShortIdPluginMap);
});
it("derives short ids only from entries' explicit shortIds markers", () => {
expect(
collectShortIdPluginMap([
shortIdEntry("aliased", "@elizaos/plugin-aliased", ["ali", "aliased"]),
shortIdEntry("bare", "@elizaos/plugin-bare", []),
]),
).toEqual({
ali: "@elizaos/plugin-aliased",
aliased: "@elizaos/plugin-aliased",
});
});
it("ignores entries without an npmName", () => {
const entry = shortIdEntry("orphan", "@elizaos/plugin-orphan", ["orphan"]);
// Simulate a metadata-only entry that declares aliases but ships no package.
const orphan = { ...entry, npmName: undefined } as RegistryEntry;
expect(collectShortIdPluginMap([orphan])).toEqual({});
});
it("rejects duplicate short-id claims across plugins (fail-loud on drift)", () => {
expect(() =>
collectShortIdPluginMap([
shortIdEntry("one", "@elizaos/plugin-one", ["dupe"]),
shortIdEntry("two", "@elizaos/plugin-two", ["dupe"]),
]),
).toThrow(
'short id "dupe" claimed by both @elizaos/plugin-one and @elizaos/plugin-two',
);
});
it("allows the same short id repeated within one entry's own package", () => {
expect(
collectShortIdPluginMap([
shortIdEntry("same", "@elizaos/plugin-same", ["same", "same"]),
]),
).toEqual({ same: "@elizaos/plugin-same" });
});
});
@@ -0,0 +1,65 @@
/**
* Verifies the fine-tuning dashboard is exposed as a static app-catalog entry
* and resolvable through the first-party registry accessors.
*/
import { afterEach, describe, expect, it } from "vitest";
import {
clearRegistryCacheForTests,
getEntryByNpmName,
loadRegistry,
} from "./index";
import type { AppEntry } from "./schema";
describe("training app registry entry", () => {
afterEach(() => {
clearRegistryCacheForTests();
});
it("exposes the fine-tuning dashboard through the static app catalog", () => {
const entry = getEntryByNpmName(
loadRegistry(),
"@elizaos/plugin-training",
) as AppEntry | undefined;
expect(entry).toMatchObject({
id: "training",
kind: "app",
name: "Fine Tuning",
npmName: "@elizaos/plugin-training",
render: {
visible: true,
icon: "BrainCircuit",
actions: ["launch", "configure"],
},
launch: {
type: "internal-tab",
target: "fine-tuning",
capabilities: expect.arrayContaining([
"training",
"fine-tuning",
"trajectories",
"datasets",
"models",
"evals",
"benchmarks",
"analysis",
"data-collection",
]),
uiExtension: {
detailPanelId: "plugin-dash-fine-tuning",
},
routePlugin: {
specifier: "@elizaos/plugin-training/setup-routes",
exportName: "trainingPlugin",
},
// Self-declared runtime-hook: the host drains this through the generic
// runtime-hook channel instead of hard-wiring the training specifier in
// the boot tail.
runtimeHook: {
specifier: "@elizaos/plugin-training",
exportName: "registerTrainingRuntimeHooks",
},
},
});
});
});
+86
View File
@@ -0,0 +1,86 @@
/**
* Generate `generated-registry.json` — the wire format the runtime fetches —
* from the source `entries/third-party/*.json` files.
*
* Run directly (`bun run src/generate.ts`) to regenerate the committed output.
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { loadThirdPartyEntries } from "./loader.ts";
import type {
GeneratedRegistry,
GeneratedRegistryEntry,
RegistryEntry,
} from "./types.ts";
const packageRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
const OUTPUT_PATH = path.join(packageRoot, "generated-registry.json");
function repoSlug(repository: string): string {
return repository.replace(/^github:/, "");
}
/** Map one source entry into its wire-format counterpart. */
export function toGeneratedEntry(entry: RegistryEntry): GeneratedRegistryEntry {
return {
git: {
repo: repoSlug(entry.repository),
v0: { branch: null },
v1: { branch: null },
v2: { branch: "main" },
},
npm: {
repo: entry.package,
v0: null,
v1: null,
v2: entry.version ?? null,
},
supports: { v0: false, v1: false, v2: true },
description: entry.description ?? "",
homepage: entry.homepage ?? null,
topics: entry.tags ?? [],
stargazers_count: 0,
language: "TypeScript",
origin: "third-party",
source: "community",
support: "community",
builtIn: false,
firstParty: false,
thirdParty: true,
kind: entry.kind,
registryKind: entry.kind,
directory: entry.directory ?? null,
};
}
/** Build the full wire registry from the source entries on disk. */
export function generateRegistry(
entries = loadThirdPartyEntries(),
): GeneratedRegistry {
const registry: Record<string, GeneratedRegistryEntry> = {};
for (const entry of entries) {
registry[entry.package] = toGeneratedEntry(entry);
}
return { registry };
}
function main(): void {
const registry = generateRegistry();
fs.writeFileSync(OUTPUT_PATH, `${JSON.stringify(registry, null, 2)}\n`);
const count = Object.keys(registry.registry).length;
console.log(`Generated ${OUTPUT_PATH} (${count} third-party entries)`);
}
function isDirectExecution(): boolean {
const entry = process.argv[1];
return Boolean(entry) && import.meta.url === pathToFileURL(entry).href;
}
if (isDirectExecution()) {
main();
}
+26
View File
@@ -0,0 +1,26 @@
/**
* @elizaos/registry — in-repo source of truth for the community plugin registry.
*
* Source entries live as one JSON file per package under `entries/third-party/`.
* {@link loadThirdPartyEntries} reads and validates them; {@link generateRegistry}
* produces the `generated-registry.json` wire format the runtime consumes.
*/
export {
generateRegistry,
toGeneratedEntry,
} from "./generate.ts";
export {
loadThirdPartyEntries,
thirdPartyEntriesDir,
} from "./loader.ts";
export {
assertRegistryEntry,
validateRegistryEntry,
} from "./schema.ts";
export type {
GeneratedRegistry,
GeneratedRegistryEntry,
RegistryEntry,
RegistryEntryKind,
} from "./types.ts";
+54
View File
@@ -0,0 +1,54 @@
/**
* Read and validate the third-party registry entries on disk.
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { assertRegistryEntry } from "./schema.ts";
import type { RegistryEntry } from "./types.ts";
const packageRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
/** Absolute path to the `entries/third-party` directory. */
export function thirdPartyEntriesDir(): string {
return path.join(packageRoot, "entries", "third-party");
}
/**
* Load every third-party entry, validating each against the schema. Throws on
* the first invalid file so a malformed entry never reaches the generator.
* Entries are returned sorted by package name for deterministic output.
*/
export function loadThirdPartyEntries(
dir = thirdPartyEntriesDir(),
): RegistryEntry[] {
if (!fs.existsSync(dir)) {
return [];
}
const files = fs
.readdirSync(dir)
.filter((name) => name.endsWith(".json"))
.sort();
const seen = new Set<string>();
const entries: RegistryEntry[] = [];
for (const file of files) {
const raw = fs.readFileSync(path.join(dir, file), "utf8");
const parsed = JSON.parse(raw);
const entry = assertRegistryEntry(parsed, file);
if (seen.has(entry.package)) {
throw new Error(
`Duplicate registry entry for ${entry.package} (${file})`,
);
}
seen.add(entry.package);
entries.push(entry);
}
entries.sort((a, b) => a.package.localeCompare(b.package));
return entries;
}
+92
View File
@@ -0,0 +1,92 @@
/**
* Tests for the third-party registry tooling: `validateRegistryEntry` accepts a
* well-formed entry and rejects the reserved @elizaos scope, non-GitHub repos,
* unknown kinds, and unknown fields; `generateRegistry` produces the wire
* format. Runs against the real entries/third-party sources on disk.
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { generateRegistry, toGeneratedEntry } from "./generate.ts";
import { loadThirdPartyEntries } from "./loader.ts";
import { validateRegistryEntry } from "./schema.ts";
import type { RegistryEntry } from "./types.ts";
const packageRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
const VALID: RegistryEntry = {
package: "elizaos-plugin-echo",
repository: "github:elizaOS/eliza",
kind: "plugin",
description: "Echoes a message.",
directory: "packages/examples/plugin-echo",
tags: ["example"],
};
describe("validateRegistryEntry", () => {
it("accepts a well-formed entry", () => {
expect(validateRegistryEntry(VALID)).toEqual([]);
});
it("rejects the reserved @elizaos scope", () => {
const errors = validateRegistryEntry({
...VALID,
package: "@elizaos/plugin-echo",
});
expect(errors).toContain(
"package must not use the reserved @elizaos/* scope",
);
});
it("rejects a non-github repository", () => {
const errors = validateRegistryEntry({
...VALID,
repository: "gitlab:owner/repo",
});
expect(errors).toContain(
'repository must be of the form "github:owner/repo"',
);
});
it("rejects an unknown kind", () => {
const errors = validateRegistryEntry({ ...VALID, kind: "widget" });
expect(errors.some((e) => e.startsWith("kind must be one of"))).toBe(true);
});
it("rejects unknown fields", () => {
const errors = validateRegistryEntry({ ...VALID, bogus: true });
expect(errors).toContain("unknown field: bogus");
});
});
describe("toGeneratedEntry", () => {
it("maps a source entry to the wire format", () => {
const wire = toGeneratedEntry(VALID);
expect(wire.git.repo).toBe("elizaOS/eliza");
expect(wire.npm.repo).toBe("elizaos-plugin-echo");
expect(wire.thirdParty).toBe(true);
expect(wire.firstParty).toBe(false);
expect(wire.supports).toEqual({ v0: false, v1: false, v2: true });
expect(wire.directory).toBe("packages/examples/plugin-echo");
});
});
describe("on-disk entries", () => {
it("all entries are valid and include the echo example", () => {
const entries = loadThirdPartyEntries();
expect(entries.length).toBeGreaterThan(0);
expect(entries.some((e) => e.package === "elizaos-plugin-echo")).toBe(true);
const { registry } = generateRegistry(entries);
expect(registry["elizaos-plugin-echo"]).toBeDefined();
});
it("keeps generated-registry.json in sync with source entries", () => {
const generatedPath = path.join(packageRoot, "generated-registry.json");
const generated = JSON.parse(fs.readFileSync(generatedPath, "utf8"));
expect(generated).toEqual(generateRegistry(loadThirdPartyEntries()));
});
});
+95
View File
@@ -0,0 +1,95 @@
/**
* Validation for source registry entries. Dependency-free so it runs in any
* tooling context (CLI, CI, the typed loader) without pulling in a schema
* library. The JSON Schema in `schema/registry-entry.schema.json` mirrors these
* rules for editors and external validators.
*/
import type { RegistryEntry, RegistryEntryKind } from "./types.ts";
const PACKAGE_NAME_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
const REPOSITORY_RE = /^github:[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
const VALID_KINDS: readonly RegistryEntryKind[] = [
"plugin",
"connector",
"app",
];
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((v) => typeof v === "string");
}
/**
* Validate a parsed JSON value as a {@link RegistryEntry}. Returns the list of
* problems; an empty list means the value is a valid entry.
*/
export function validateRegistryEntry(value: unknown): string[] {
const errors: string[] = [];
if (!isRecord(value)) {
return ["entry must be a JSON object"];
}
const pkg = value.package;
if (typeof pkg !== "string" || !PACKAGE_NAME_RE.test(pkg)) {
errors.push("package must be a valid npm package name");
} else if (pkg.startsWith("@elizaos/")) {
errors.push("package must not use the reserved @elizaos/* scope");
}
if (
typeof value.repository !== "string" ||
!REPOSITORY_RE.test(value.repository)
) {
errors.push('repository must be of the form "github:owner/repo"');
}
if (!VALID_KINDS.includes(value.kind as RegistryEntryKind)) {
errors.push(`kind must be one of: ${VALID_KINDS.join(", ")}`);
}
for (const field of ["description", "homepage", "version", "directory"]) {
if (value[field] !== undefined && typeof value[field] !== "string") {
errors.push(`${field} must be a string when present`);
}
}
if (value.tags !== undefined && !isStringArray(value.tags)) {
errors.push("tags must be an array of strings when present");
}
const knownKeys = new Set([
"package",
"repository",
"kind",
"description",
"homepage",
"version",
"directory",
"tags",
]);
for (const key of Object.keys(value)) {
if (!knownKeys.has(key)) {
errors.push(`unknown field: ${key}`);
}
}
return errors;
}
/** Validate and narrow, throwing on the first batch of errors. */
export function assertRegistryEntry(
value: unknown,
source: string,
): RegistryEntry {
const errors = validateRegistryEntry(value);
if (errors.length > 0) {
throw new Error(
`Invalid registry entry (${source}):\n - ${errors.join("\n - ")}`,
);
}
return value as RegistryEntry;
}
+74
View File
@@ -0,0 +1,74 @@
/**
* Types for the elizaOS community plugin registry.
*
* Two formats live in this package:
*
* 1. {@link RegistryEntry} — the human/CLI-authored source format, one JSON file
* per package under `entries/third-party/`. This is what `elizaos plugins
* submit --dry-run` emits and what contributors add by pull request.
* 2. {@link GeneratedRegistry} — the wire format the runtime consumes from
* `plugins.elizacloud.ai/generated-registry.json`. Produced from the source
* entries by {@link generateRegistry}.
*/
/** The kind of artifact a registry entry describes. */
export type RegistryEntryKind = "plugin" | "connector" | "app";
/**
* Source format for a single third-party registry entry. Mirrors the metadata
* `elizaos plugins submit` generates from a plugin's `package.json`.
*/
export interface RegistryEntry {
/** npm package name. The `@elizaos/*` scope is reserved for first-party. */
package: string;
/** GitHub repository as `github:owner/repo`. */
repository: string;
/** What this package is. */
kind: RegistryEntryKind;
/** One-line description. */
description?: string;
/** Project homepage URL. */
homepage?: string;
/** Published npm version this entry was registered against. */
version?: string;
/** Monorepo subdirectory when the package lives inside a larger repo. */
directory?: string;
/** Discovery tags (npm keywords minus elizaos boilerplate). */
tags?: string[];
}
/** Per-package entry in the generated wire registry. */
export interface GeneratedRegistryEntry {
git: {
repo: string;
v0: { branch: string | null };
v1: { branch: string | null };
v2: { branch: string | null };
};
npm: {
repo: string;
v0: string | null;
v1: string | null;
v2: string | null;
};
supports: { v0: boolean; v1: boolean; v2: boolean };
description: string;
homepage: string | null;
topics: string[];
stargazers_count: number;
language: string;
origin: "third-party";
source: "community";
support: "community";
builtIn: false;
firstParty: false;
thirdParty: true;
kind: RegistryEntryKind;
registryKind: RegistryEntryKind;
directory: string | null;
}
/** Top-level wire format served to the runtime. */
export interface GeneratedRegistry {
registry: Record<string, GeneratedRegistryEntry>;
}
+14
View File
@@ -0,0 +1,14 @@
/**
* CLI: validate every source entry against the schema. Exits non-zero on the
* first invalid file. Wired as `bun run validate` and run in CI.
*/
import { loadThirdPartyEntries } from "./loader.ts";
try {
const entries = loadThirdPartyEntries();
console.log(`[registry] ${entries.length} third-party entries validated`);
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ESNext"],
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"types": ["node"],
"typeRoots": [
"../../node_modules/@types",
"../../node_modules/.bun/node_modules/@types"
]
},
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}
+12
View File
@@ -0,0 +1,12 @@
/**
* Vitest config for @elizaos/registry: node environment, runs `src` unit tests.
*/
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
include: ["src/**/*.test.ts"],
exclude: ["dist/**", "**/node_modules/**"],
},
});