Files
openclaw/scripts/generate-plugin-inventory-doc.mjs
Peter Steinberger f91de52f0d refactor: move runtime state to SQLite
* refactor: remove stale file-backed shims

* fix: harden sqlite state ci boundaries

* refactor: store matrix idb snapshots in sqlite

* fix: satisfy rebased CI guardrails

* refactor: store current conversation bindings in sqlite table

* refactor: store tui last sessions in sqlite table

* refactor: reset sqlite schema history

* refactor: drop unshipped sqlite table migration

* refactor: remove plugin index file rollback

* refactor: drop unshipped sqlite sidecar migrations

* refactor: remove runtime commitments kv migration

* refactor: preserve kysely sync result types

* refactor: drop unshipped sqlite schema migration table

* test: keep session usage coverage sqlite-backed

* refactor: keep sqlite migration doctor-only

* refactor: isolate device legacy imports

* refactor: isolate push voicewake legacy imports

* refactor: isolate remaining runtime legacy imports

* refactor: tighten sqlite migration guardrails

* test: cover sqlite persisted enum parsing

* refactor: isolate legacy update and tui imports

* refactor: tighten sqlite state ownership

* refactor: move legacy imports behind doctor

* refactor: remove legacy session row lookup

* refactor: canonicalize memory transcript locators

* refactor: drop transcript path scope fallbacks

* refactor: drop runtime legacy session delivery pruning

* refactor: store tts prefs only in sqlite

* refactor: remove cron store path runtime

* refactor: use cron sqlite store keys

* refactor: rename telegram message cache scope

* refactor: read memory dreaming status from sqlite

* refactor: rename cron status store key

* refactor: stop remembering transcript file paths

* test: use sqlite locators in agent fixtures

* refactor: remove file-shaped commitments and cron store surfaces

* refactor: keep compaction transcript handles out of session rows

* refactor: derive transcript handles from session identity

* refactor: derive runtime transcript handles

* refactor: remove gateway session locator reads

* refactor: remove transcript locator from session rows

* refactor: store raw stream diagnostics in sqlite

* refactor: remove file-shaped transcript rotation

* refactor: hide legacy trajectory paths from runtime

* refactor: remove runtime transcript file bridges

* refactor: repair database-first rebase fallout

* refactor: align tests with database-first state

* refactor: remove transcript file handoffs

* refactor: sync post-compaction memory by transcript scope

* refactor: run codex app-server sessions by id

* refactor: bind codex runtime state by session id

* refactor: pass memory transcripts by sqlite scope

* refactor: remove transcript locator cleanup leftovers

* test: remove stale transcript file fixtures

* refactor: remove transcript locator test helper

* test: make cron sqlite keys explicit

* test: remove cron runtime store paths

* test: remove stale session file fixtures

* test: use sqlite cron keys in diagnostics

* refactor: remove runtime delivery queue backfill

* test: drop fake export session file mocks

* refactor: rename acp session read failure flag

* refactor: rename acp row session key

* refactor: remove session store test seams

* refactor: move legacy session parser tests to doctor

* refactor: reindex managed memory in place

* refactor: drop stale session store wording

* refactor: rename session row helpers

* refactor: rename sqlite session entry modules

* refactor: remove transcript locator leftovers

* refactor: trim file-era audit wording

* refactor: clean managed media through sqlite

* fix: prefer explicit agent for exports

* fix: use prepared agent for session resets

* fix: canonicalize legacy codex binding import

* test: rename state cleanup helper

* docs: align backup docs with sqlite state

* refactor: drop legacy Pi usage auth fallback

* refactor: move legacy auth profile imports to doctor

* refactor: keep Pi model discovery auth in memory

* refactor: remove MSTeams legacy learning key fallback

* refactor: store model catalog config in sqlite

* refactor: use sqlite model catalog at runtime

* refactor: remove model json compatibility aliases

* refactor: store auth profiles in sqlite

* refactor: seed copied auth profiles in sqlite

* refactor: make auth profile runtime sqlite-addressed

* refactor: migrate hermes secrets into sqlite auth store

* refactor: move plugin install config migration to doctor

* refactor: rename plugin index audit checks

* test: drop auth file assumptions

* test: remove legacy transcript file assertions

* refactor: drop legacy cli session aliases

* refactor: store skill uploads in sqlite

* refactor: keep subagent attachments in sqlite vfs

* refactor: drop subagent attachment cleanup state

* refactor: move legacy session aliases to doctor

* refactor: require node 24 for sqlite state runtime

* refactor: move provider caches into sqlite state

* fix: harden virtual agent filesystem

* refactor: enforce database-first runtime state

* refactor: rename compaction transcript rotation setting

* test: clean sqlite refactor test types

* refactor: consolidate sqlite runtime state

* refactor: model session conversations in sqlite

* refactor: stop deriving cron delivery from session keys

* refactor: stop classifying sessions from key shape

* refactor: hydrate announce targets from typed delivery

* refactor: route heartbeat delivery from typed sqlite context

* refactor: tighten typed sqlite session routing

* refactor: remove session origin routing shadow

* refactor: drop session origin shadow fixtures

* perf: query sqlite vfs paths by prefix

* refactor: use typed conversation metadata for sessions

* refactor: prefer typed session routing metadata

* refactor: require typed session routing metadata

* refactor: resolve group tool policy from typed sessions

* refactor: delete dead session thread info bridge

* Show Codex subscription reset times in channel errors (#80456)

* feat(plugin-sdk): consolidate session workflow APIs

* fix(agents): allow read-only agent mount reads

* [codex] refresh plugin regression fixtures

* fix(agents): restore compaction gateway logs

* test: tighten gateway startup assertions

* Redact persisted secret-shaped payloads [AI] (#79006)

* test: tighten device pair notify assertions

* test: tighten hermes secret assertions

* test: assert matrix client error shapes

* test: assert config compat warnings

* fix(heartbeat): remap cron-run exec events to session keys (#80214)

* fix(codex): route btw through native side threads

* fix(auth): accept friendly OpenAI order for Codex profiles

* fix(codex): rotate auth profiles inside harness

* fix: keep browser status page probe within timeout

* test: assert agents add outputs

* test: pin cron read status

* fix(agents): avoid Pi resource discovery stalls

Co-authored-by: dataCenter430 <titan032000@gmail.com>

* fix: retire timed-out codex app-server clients

* test: tighten qa lab runtime assertions

* test: check security fix outputs

* test: verify extension runtime messages

* feat(wake): expose typed sessionKey on wake protocol + system event CLI

* fix(gateway): await session_end during shutdown drain and track channel + compaction lifecycle paths (#57790)

* test: guard talk consult call helper

* fix(codex): scale context engine projection (#80761)

* fix(codex): scale context engine projection

* fix: document Codex context projection scaling

* fix: document Codex context projection scaling

* fix: document Codex context projection scaling

* fix: document Codex context projection scaling

* chore: align Codex projection changelog

* chore: realign Codex projection changelog

* fix: isolate Codex projection patch

---------

Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org>
Co-authored-by: Josh Lehman <josh@martian.engineering>

* refactor: move agent runtime state toward piless

* refactor: remove cron session reaper

* refactor: move session management to sqlite

* refactor: finish database-first state migration

* chore: refresh generated sqlite db types

* refactor: remove stale file-backed shims

* test: harden kysely type coverage

# Conflicts:
#	.agents/skills/kysely-database-access/SKILL.md
#	src/infra/kysely-sync.types.test.ts
#	src/proxy-capture/store.sqlite.test.ts
#	src/state/openclaw-agent-db.test.ts
#	src/state/openclaw-state-db.test.ts

* refactor: remove cron store path runtime

* refactor: keep compaction transcript handles out of session rows

* refactor: derive embedded transcripts from sqlite identity

* refactor: remove embedded transcript locator handoff

* refactor: remove runtime transcript file bridges

* refactor: remove transcript file handoffs

* refactor: remove MSTeams legacy learning key fallback

* refactor: store model catalog config in sqlite

* refactor: use sqlite model catalog at runtime

# Conflicts:
#	docs/cli/secrets.md
#	docs/gateway/authentication.md
#	docs/gateway/secrets.md

* fix: keep oauth sibling sync sqlite-local

# Conflicts:
#	src/commands/onboard-auth.test.ts

* refactor: remove task session store maintenance

# Conflicts:
#	src/commands/tasks.ts

* refactor: keep diagnostics in state sqlite

* refactor: enforce database-first runtime state

* refactor: consolidate sqlite runtime state

* Show Codex subscription reset times in channel errors (#80456)

* fix(codex): refresh subscription limit resets

* fix(codex): format reset times for channels

* Update CHANGELOG with latest changes and fixes

Updated CHANGELOG with recent fixes and improvements.

* fix(codex): keep command load failures on codex surface

* fix(codex): format account rate limits as rows

* fix(codex): summarize account limits as usage status

* fix(codex): simplify account limit status

* test: tighten subagent announce queue assertion

* test: tighten session delete lifecycle assertions

* test: tighten cron ops assertions

* fix: track cron execution milestones

* test: tighten hermes secret assertions

* test: assert matrix sync store payloads

* test: assert config compat warnings

* fix(codex): align btw side thread semantics

* fix(codex): honor codex fallback blocking

* fix(agents): avoid Pi resource discovery stalls

* test: tighten codex event assertions

* test: tighten cron assertions

* Fix Codex app-server OAuth harness auth

* refactor: move agent runtime state toward piless

* refactor: move device and push state to sqlite

* refactor: move runtime json state imports to doctor

* refactor: finish database-first state migration

* chore: refresh generated sqlite db types

* refactor: clarify cron sqlite store keys

* refactor: remove stale file-backed shims

* refactor: bind codex runtime state by session id

* test: expect sqlite trajectory branch export

* refactor: rename session row helpers

* fix: keep legacy device identity import in doctor

* refactor: enforce database-first runtime state

* refactor: consolidate sqlite runtime state

* build: align pi contract wrappers

* chore: repair database-first rebase

* refactor: remove session file test contracts

* test: update gateway session expectations

* refactor: stop routing from session compatibility shadows

* refactor: stop persisting session route shadows

* refactor: use typed delivery context in clients

* refactor: stop echoing session route shadows

* refactor: repair embedded runner rebase imports

# Conflicts:
#	src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts

* refactor: align pi contract imports

* refactor: satisfy kysely sync helper guard

* refactor: remove file transcript bridge remnants

* refactor: remove session locator compatibility

* refactor: remove session file test contracts

* refactor: keep rebase database-first clean

* refactor: remove session file assumptions from e2e

* docs: clarify database-first goal state

* test: remove legacy store markers from sqlite runtime tests

* refactor: remove legacy store assumptions from runtime seams

* refactor: align sqlite runtime helper seams

* test: update memory recall sqlite audit mock

* refactor: align database-first runtime type seams

* test: clarify doctor cron legacy store names

* fix: preserve sqlite session route projections

* test: fix copilot token cache test syntax

* docs: update database-first proof status

* test: align database-first test fixtures

* docs: update database-first proof status

* refactor: clean extension database-first drift

* test: align agent session route proof

* test: clarify doctor legacy path fixtures

* chore: clean database-first changed checks

* chore: repair database-first rebase markers

* build: allow baileys git subdependency

* chore: repair exp-vfs rebase drift

* chore: finish exp-vfs rebase cleanup

* chore: satisfy rebase lint drift

* chore: fix qqbot rebase type seam

* chore: fix rebase drift leftovers

* fix: keep auth profile oauth secrets out of sqlite

* fix: repair rebase drift tests

* test: stabilize pairing request ordering

* test: use source manifests in plugin contract checks

* fix: restore gateway session metadata after rebase

* fix: repair database-first rebase drift

* fix: clean up database-first rebase fallout

* test: stabilize line quick reply receipt time

* fix: repair extension rebase drift

* test: keep transcript redaction tests sqlite-backed

* fix: carry injected transcript redaction through sqlite

* chore: clean database branch rebase residue

* fix: repair database branch CI drift

* fix: repair database branch CI guard drift

* fix: stabilize oauth tls preflight test

* test: align database branch fast guards

* test: repair build artifact boundary guards

* chore: clean changelog rebase markers

---------

Co-authored-by: pashpashpash <nik@vault77.ai>
Co-authored-by: Eva <eva@100yen.org>
Co-authored-by: stainlu <stainlu@newtype-ai.org>
Co-authored-by: Jason Zhou <jason.zhou.design@gmail.com>
Co-authored-by: Ruben Cuevas <hi@rubencu.com>
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
Co-authored-by: Kaspre <36520309+Kaspre@users.noreply.github.com>
Co-authored-by: dataCenter430 <titan032000@gmail.com>
Co-authored-by: Kaspre <kaspre@gmail.com>
Co-authored-by: pandadev66 <nova.full.stack@outlook.com>
Co-authored-by: Eva <admin@100yen.org>
Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org>
Co-authored-by: Josh Lehman <josh@martian.engineering>
Co-authored-by: jeffjhunter <support@aipersonamethod.com>
2026-05-13 13:15:12 +01:00

617 lines
19 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
const DOC_PATH = "docs/plugins/plugin-inventory.md";
const REFERENCE_INDEX_PATH = "docs/plugins/reference.md";
const REFERENCE_DIR = "docs/plugins/reference";
const ROOT = process.cwd();
const EXTENSIONS_DIR = path.join(ROOT, "extensions");
const PROVIDER_DOC_ALIASES = new Map([
["amazon-bedrock", "/providers/bedrock"],
["amazon-bedrock-mantle", "/providers/bedrock-mantle"],
["kimi", "/providers/moonshot"],
["perplexity", "/providers/perplexity-provider"],
]);
const PLUGIN_DOC_ALIASES = new Map([
["acpx", "/tools/acp-agents-setup"],
["brave", "/tools/brave-search"],
["browser", "/tools/browser"],
["codex", "/plugins/codex-harness"],
["document-extract", "/tools/pdf"],
["duckduckgo", "/tools/duckduckgo-search"],
["exa", "/tools/exa-search"],
["firecrawl", "/tools/firecrawl"],
["perplexity", "/tools/perplexity-search"],
["tavily", "/tools/tavily"],
["tokenjuice", "/tools/tokenjuice"],
]);
/** @type {Map<string, string>} */
const PLUGIN_REFERENCE_EXTRA_SECTIONS = new Map();
function readJson(relativePath) {
return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), "utf8"));
}
function readJsonPath(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function fileExists(relativePath) {
return fs.existsSync(path.join(ROOT, relativePath));
}
function collectExcludedPackagedExtensionDirs(rootPackageJson) {
const excluded = new Set();
for (const entry of rootPackageJson.files ?? []) {
if (typeof entry !== "string") {
continue;
}
const match = /^!dist\/extensions\/([^/]+)\/\*\*$/u.exec(entry);
if (match?.[1]) {
excluded.add(match[1]);
}
}
return excluded;
}
function normalizeDocPath(value) {
if (typeof value !== "string" || !value.startsWith("/")) {
return null;
}
return value.replace(/\.mdx?$/u, "");
}
function docLink({ label, href }) {
return `[${label}](${href})`;
}
function pluginReferencePath(id) {
return `/plugins/reference/${id}`;
}
function humanizeId(value) {
const names = new Map([
["acpx", "ACPx"],
["ai", "AI"],
["api", "API"],
["aws", "AWS"],
["azure", "Azure"],
["byteplus", "BytePlus"],
["codex", "Codex"],
["cli", "CLI"],
["comfy", "ComfyUI"],
["dashscope", "DashScope"],
["deepgram", "Deepgram"],
["deepinfra", "DeepInfra"],
["deepseek", "DeepSeek"],
["duckduckgo", "DuckDuckGo"],
["exa", "Exa"],
["fal", "fal"],
["feishu", "Feishu"],
["github", "GitHub"],
["googlechat", "Google Chat"],
["gpt", "GPT"],
["groq", "Groq"],
["huggingface", "Hugging Face"],
["imessage", "iMessage"],
["irc", "IRC"],
["kimi", "Kimi"],
["line", "LINE"],
["litellm", "LiteLLM"],
["llm", "LLM"],
["lmstudio", "LM Studio"],
["mdns", "mDNS"],
["minimax", "MiniMax"],
["modelstudio", "Model Studio"],
["msteams", "Microsoft Teams"],
["nextcloud", "Nextcloud"],
["nvidia", "NVIDIA"],
["openai", "OpenAI"],
["opencode", "OpenCode"],
["openrouter", "OpenRouter"],
["otel", "OpenTelemetry"],
["qa", "QA"],
["qqbot", "QQ Bot"],
["qwen", "Qwen"],
["qwencloud", "Qwen Cloud"],
["searxng", "SearXNG"],
["sglang", "SGLang"],
["stepfun", "StepFun"],
["tokenhub", "TokenHub"],
["tts", "TTS"],
["twitch", "Twitch"],
["ui", "UI"],
["vllm", "vLLM"],
["whatsapp", "WhatsApp"],
["xai", "xAI"],
["zai", "Z.AI"],
["zalouser", "Zalo Personal"],
]);
return value
.split("-")
.map((part) => names.get(part) ?? part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
}
function displayList(values) {
return values
.filter((value) => typeof value === "string" && value.length > 0)
.map(humanizeId)
.join(", ");
}
function normalizePackageDescription(value) {
if (typeof value !== "string") {
return null;
}
return value.trim().replace(/\s+/gu, " ").replace(/\.$/u, "");
}
function resolveDescription({ manifest, packageJson }) {
const manifestDescription = normalizePackageDescription(manifest.description);
if (manifestDescription) {
return `${manifestDescription}.`;
}
const channels = Array.isArray(manifest.channels) ? manifest.channels : [];
if (channels.length > 0) {
const channelLabel = displayList(channels);
const channelNoun = channelLabel.toLowerCase().includes("channel") ? "" : " channel";
return `Adds the ${channelLabel}${channelNoun} surface for sending and receiving OpenClaw messages.`;
}
const providers = Array.isArray(manifest.providers) ? manifest.providers : [];
if (providers.length > 0) {
return `Adds ${displayList(providers)} model provider support to OpenClaw.`;
}
const contracts = Object.keys(manifest.contracts ?? {}).toSorted((left, right) =>
left.localeCompare(right),
);
const contractDescriptions = {
agentToolResultMiddleware: "Adds agent tool-result middleware.",
documentExtractors: "Adds document extraction for local attachments.",
imageGenerationProviders: "Adds image generation provider support.",
mediaUnderstandingProviders: "Adds media understanding provider support.",
memoryEmbeddingProviders: "Adds memory embedding provider support.",
migrationProviders: "Adds migration import support.",
musicGenerationProviders: "Adds music generation provider support.",
realtimeTranscriptionProviders: "Adds realtime transcription provider support.",
realtimeVoiceProviders: "Adds realtime voice provider support.",
speechProviders: "Adds text-to-speech provider support.",
tools: "Adds agent-callable tools.",
videoGenerationProviders: "Adds video generation provider support.",
webContentExtractors: "Adds readable web content extraction.",
webFetchProviders: "Adds web fetch provider support.",
webSearchProviders: "Adds web search provider support.",
};
const describedContracts = contracts
.map((contract) => contractDescriptions[contract])
.filter((value) => typeof value === "string");
if (describedContracts.length > 0) {
return describedContracts.join(" ");
}
const packageDescription = normalizePackageDescription(packageJson.description);
return packageDescription ? `${packageDescription}.` : "Provides an OpenClaw plugin.";
}
function pushUniqueDocLink(values, value) {
if (
value &&
!values.some((existing) => existing.label === value.label && existing.href === value.href)
) {
values.push(value);
}
}
function resolveDocs({ dirName, manifest, packageJson }) {
const links = [];
const pluginAlias = PLUGIN_DOC_ALIASES.get(manifest.id) ?? PLUGIN_DOC_ALIASES.get(dirName);
if (pluginAlias) {
pushUniqueDocLink(links, { href: pluginAlias, label: manifest.id ?? dirName });
}
const channelDoc = normalizeDocPath(packageJson.openclaw?.channel?.docsPath);
if (channelDoc) {
pushUniqueDocLink(links, {
href: channelDoc,
label: channelDoc.replace(/^\/channels\//u, ""),
});
}
for (const channel of manifest.channels ?? []) {
if (typeof channel !== "string") {
continue;
}
const relativePath = `docs/channels/${channel}.md`;
if (fileExists(relativePath)) {
pushUniqueDocLink(links, { href: `/channels/${channel}`, label: channel });
}
}
for (const provider of manifest.providers ?? []) {
if (typeof provider !== "string") {
continue;
}
const alias = PROVIDER_DOC_ALIASES.get(provider);
if (alias) {
pushUniqueDocLink(links, { href: alias, label: provider });
continue;
}
const relativePath = `docs/providers/${provider}.md`;
if (fileExists(relativePath)) {
pushUniqueDocLink(links, { href: `/providers/${provider}`, label: provider });
}
}
for (const candidate of [manifest.id, dirName]) {
if (typeof candidate !== "string") {
continue;
}
if (fileExists(`docs/channels/${candidate}.md`)) {
pushUniqueDocLink(links, { href: `/channels/${candidate}`, label: candidate });
}
if (fileExists(`docs/providers/${candidate}.md`)) {
pushUniqueDocLink(links, { href: `/providers/${candidate}`, label: candidate });
}
if (fileExists(`docs/plugins/${candidate}.md`)) {
pushUniqueDocLink(links, { href: `/plugins/${candidate}`, label: candidate });
}
}
return links;
}
function resolveSurface(manifest) {
const parts = [];
if (Array.isArray(manifest.channels) && manifest.channels.length > 0) {
parts.push(`channels: ${manifest.channels.join(", ")}`);
}
if (Array.isArray(manifest.providers) && manifest.providers.length > 0) {
parts.push(`providers: ${manifest.providers.join(", ")}`);
}
const contracts = Object.keys(manifest.contracts ?? {}).toSorted((left, right) =>
left.localeCompare(right),
);
if (contracts.length > 0) {
parts.push(`contracts: ${contracts.join(", ")}`);
}
if (Array.isArray(manifest.skills) && manifest.skills.length > 0) {
parts.push("skills");
}
if (parts.length === 0) {
return "plugin";
}
return parts.join("; ");
}
function resolveInstallRoute(packageJson, status) {
if (status === "source") {
return "source checkout only";
}
if (status === "core") {
return "included in OpenClaw";
}
const install = packageJson.openclaw?.install;
const release = packageJson.openclaw?.release;
const clawhubSpec =
typeof install?.clawhubSpec === "string" ? `: \`${install.clawhubSpec}\`` : "";
const npmSpec =
typeof install?.npmSpec === "string" && install.npmSpec !== packageJson.name
? `: \`${install.npmSpec}\``
: "";
if (release?.publishToClawHub === true && release?.publishToNpm === true) {
if (install?.defaultChoice === "clawhub") {
return clawhubSpec ? `ClawHub${clawhubSpec}; npm${npmSpec}` : `ClawHub + npm${npmSpec}`;
}
return clawhubSpec ? `npm${npmSpec}; ClawHub${clawhubSpec}` : `npm${npmSpec}; ClawHub`;
}
if (release?.publishToClawHub === true) {
return `ClawHub${clawhubSpec || npmSpec}`;
}
if (release?.publishToNpm === true || typeof install?.npmSpec === "string") {
return `npm${npmSpec}`;
}
return "installable plugin";
}
function resolveStatus({ dirName, packageJson, excludedDirs }) {
const release = packageJson.openclaw?.release;
const hasInstallSpec =
typeof packageJson.openclaw?.install?.clawhubSpec === "string" ||
typeof packageJson.openclaw?.install?.npmSpec === "string";
if (!excludedDirs.has(dirName)) {
return "core";
}
if (release?.publishToClawHub === true || release?.publishToNpm === true || hasInstallSpec) {
return "external";
}
return "source";
}
function escapeCell(value) {
return String(value).replaceAll("\n", " ").replaceAll("|", "\\|");
}
function renderTable(records) {
const rows = [
["Plugin", "Description", "Distribution", "Surface"],
...records.map((record) => [
docLink({ href: pluginReferencePath(record.id), label: escapeCell(record.id) }),
escapeCell(record.description),
`\`${escapeCell(record.packageName)}\`<br />${escapeCell(record.installRoute)}`,
escapeCell(record.surface),
]),
];
const widths = rows[0].map((_, index) => Math.max(...rows.map((row) => row[index].length), 3));
const lines = [];
lines.push(formatTableRow(rows[0], widths));
lines.push(
formatTableRow(
widths.map((width) => "-".repeat(width)),
widths,
),
);
for (const row of rows.slice(1)) {
lines.push(formatTableRow(row, widths));
}
return lines.join("\n");
}
function formatTableRow(row, widths) {
return `| ${row.map((cell, index) => cell.padEnd(widths[index])).join(" | ")} |`;
}
function renderRelatedDocs(record) {
if (record.docs.length === 0) {
return "";
}
return `## Related docs
${record.docs.map((link) => `- ${docLink(link)}`).join("\n")}`;
}
function renderReferencePage(record) {
const relatedDocs = renderRelatedDocs(record);
const extraSectionsValue = PLUGIN_REFERENCE_EXTRA_SECTIONS.get(record.id);
const extraSections = typeof extraSectionsValue === "string" ? extraSectionsValue : "";
return `---
summary: "${record.description.replaceAll('"', '\\"')}"
read_when:
- You are installing, configuring, or auditing the ${record.id} plugin
title: "${record.name} plugin"
---
# ${record.name} plugin
${record.description}
## Distribution
- Package: \`${record.packageName}\`
- Install route: ${record.installRoute}
## Surface
${record.surface}${extraSections ? `\n\n${extraSections}` : ""}${relatedDocs ? `\n\n${relatedDocs}` : ""}
`;
}
function renderReferenceIndex(records) {
return `---
summary: "Generated index of OpenClaw plugin reference pages"
read_when:
- You need a reference page for a specific OpenClaw plugin
- You are auditing plugin docs coverage
title: "Plugin reference"
---
# Plugin reference
This page is generated from \`extensions/*/package.json\` and
\`openclaw.plugin.json\`. Regenerate it with:
\`\`\`bash
pnpm plugins:inventory:gen
\`\`\`
${renderTable(records)}
`;
}
function collectPluginSourceEntries() {
const entries = [];
for (const dirName of fs
.readdirSync(EXTENSIONS_DIR)
.toSorted((left, right) => left.localeCompare(right))) {
const packagePath = path.join(EXTENSIONS_DIR, dirName, "package.json");
const manifestPath = path.join(EXTENSIONS_DIR, dirName, "openclaw.plugin.json");
if (!fs.existsSync(packagePath) || !fs.existsSync(manifestPath)) {
continue;
}
const packageJson = readJsonPath(packagePath);
const manifest = readJsonPath(manifestPath);
const id = typeof manifest.id === "string" && manifest.id ? manifest.id : dirName;
entries.push({ dirName, id, manifest, packageJson });
}
return entries;
}
function validatePluginCoverage(records, sourceEntries) {
const expectedIds = sourceEntries
.map((entry) => entry.id)
.toSorted((left, right) => left.localeCompare(right));
const actualIds = records
.map((record) => record.id)
.toSorted((left, right) => left.localeCompare(right));
const missing = expectedIds.filter((id) => !actualIds.includes(id));
const extra = actualIds.filter((id) => !expectedIds.includes(id));
const duplicateIds = actualIds.filter((id, index) => actualIds.indexOf(id) !== index);
if (missing.length > 0 || extra.length > 0 || duplicateIds.length > 0) {
throw new Error(
[
"plugin inventory coverage mismatch",
missing.length > 0 ? `missing: ${missing.join(", ")}` : null,
extra.length > 0 ? `extra: ${extra.join(", ")}` : null,
duplicateIds.length > 0 ? `duplicates: ${duplicateIds.join(", ")}` : null,
]
.filter(Boolean)
.join("; "),
);
}
}
function collectPluginRecords() {
const rootPackageJson = readJson("package.json");
const excludedDirs = collectExcludedPackagedExtensionDirs(rootPackageJson);
const sourceEntries = collectPluginSourceEntries();
const records = [];
for (const { dirName, id, manifest, packageJson } of sourceEntries) {
const status = resolveStatus({ dirName, packageJson, excludedDirs });
records.push({
description: resolveDescription({ manifest, packageJson }),
docs: resolveDocs({ dirName, manifest, packageJson }),
id,
installRoute: resolveInstallRoute(packageJson, status),
name: humanizeId(id),
packageName: packageJson.name ?? "-",
status,
surface: resolveSurface(manifest),
});
}
validatePluginCoverage(records, sourceEntries);
return records.toSorted((left, right) => left.id.localeCompare(right.id));
}
function writeGeneratedDocs(records) {
fs.mkdirSync(path.join(ROOT, REFERENCE_DIR), { recursive: true });
for (const record of records) {
fs.writeFileSync(
path.join(ROOT, REFERENCE_DIR, `${record.id}.md`),
renderReferencePage(record),
"utf8",
);
}
fs.writeFileSync(path.join(ROOT, REFERENCE_INDEX_PATH), renderReferenceIndex(records), "utf8");
}
function readGeneratedDocs(records) {
return [
[REFERENCE_INDEX_PATH, renderReferenceIndex(records)],
...records.map((record) => [
path.join(REFERENCE_DIR, `${record.id}.md`),
renderReferencePage(record),
]),
];
}
function renderDocument() {
const records = collectPluginRecords();
const groups = {
core: records.filter((record) => record.status === "core"),
external: records.filter((record) => record.status === "external"),
source: records.filter((record) => record.status === "source"),
};
return `---
summary: "Generated inventory of OpenClaw plugins shipped in core, published externally, or kept source-only"
read_when:
- You are deciding whether a plugin ships in the core npm package or installs separately
- You are updating bundled plugin package metadata or release automation
- You need the canonical internal vs external plugin list
title: "Plugin inventory"
---
# Plugin inventory
This page is generated from \`extensions/*/package.json\`, \`openclaw.plugin.json\`,
and the root npm package \`files\` exclusions. Regenerate it with:
\`\`\`bash
pnpm plugins:inventory:gen
\`\`\`
## Definitions
- **Core npm package:** built into the \`openclaw\` npm package and available without a separate plugin install.
- **Official external package:** OpenClaw-maintained plugin omitted from the core npm package, kept in this official inventory, and installed on demand through ClawHub and/or npm.
- **Source checkout only:** repo-local plugin omitted from published npm artifacts and not advertised as an installable package.
Source checkouts are different from npm installs: after \`pnpm install\`, bundled
plugins load from \`extensions/<id>\` so local edits and package-local workspace
dependencies are available.
## Install a plugin
Use the **Distribution** column to decide whether install is needed. Plugins that
say \`included in OpenClaw\` are already present in the core package. Official
external packages need one install, then a Gateway restart.
For example, Discord is an official external package:
\`\`\`bash
openclaw plugins install @openclaw/discord
openclaw gateway restart
openclaw plugins inspect discord --runtime --json
\`\`\`
Bare package specs try ClawHub first, then npm fallback. To force a source, use
\`clawhub:@openclaw/discord\` or \`npm:@openclaw/discord\`. After install, follow
the plugin's setup doc, such as [Discord](/channels/discord), to add credentials
and channel config. See [Manage plugins](/plugins/manage-plugins) for update,
uninstall, and publishing commands.
## Core npm package
${renderTable(groups.core)}
## Official external packages
${renderTable(groups.external)}
## Source checkout only
${renderTable(groups.source)}
`;
}
function main(argv = process.argv.slice(2)) {
const write = argv.includes("--write");
const check = argv.includes("--check");
if (write === check) {
console.error("usage: node scripts/generate-plugin-inventory-doc.mjs --write|--check");
process.exit(2);
}
const records = collectPluginRecords();
const next = renderDocument();
const docPath = path.join(ROOT, DOC_PATH);
if (write) {
fs.writeFileSync(docPath, next, "utf8");
writeGeneratedDocs(records);
return;
}
const current = fs.existsSync(docPath) ? fs.readFileSync(docPath, "utf8") : "";
if (current !== next) {
console.error(`${DOC_PATH} is stale. Run \`pnpm plugins:inventory:gen\`.`);
process.exit(1);
}
for (const [relativePath, expected] of readGeneratedDocs(records)) {
const fullPath = path.join(ROOT, relativePath);
const actual = fs.existsSync(fullPath) ? fs.readFileSync(fullPath, "utf8") : "";
if (actual !== expected) {
console.error(`${relativePath} is stale. Run \`pnpm plugins:inventory:gen\`.`);
process.exit(1);
}
}
}
main();