mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:50:43 +00:00
fix(plugins): catalog externalized npm installs
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Plugins/externalization: add official npm-first catalogs for externalized channel, provider, and generic plugins, keep unpublished ACPX/Google Chat/LINE bundled, and make missing-plugin repair honor npm-first metadata while ClawHub pack files roll out. Thanks @vincentkoc.
|
||||
- Agents/sessions: preserve terminal lifecycle state when final run metadata persists from a stale in-memory snapshot, preventing `main` sessions from staying stuck as running after completed or timed-out turns.
|
||||
- Gateway/CLI: make `openclaw gateway start` repair stale managed service definitions that point at old OpenClaw versions, missing binaries, or temporary installer paths before starting.
|
||||
- Status: show the `openai-codex` OAuth profile for `openai/gpt-*` sessions running through the native Codex runtime instead of reporting auth as unknown. (#76197) Thanks @mbelinky.
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"!dist/.runtime-postbuildstamp",
|
||||
"!dist/**/*.map",
|
||||
"!dist/plugin-sdk/.tsbuildinfo",
|
||||
"!dist/extensions/acpx/**",
|
||||
"!dist/extensions/node_modules/**",
|
||||
"!dist/extensions/*/node_modules/**",
|
||||
"!dist/extensions/bluebubbles/**",
|
||||
@@ -43,8 +42,6 @@
|
||||
"!dist/extensions/discord/**",
|
||||
"!dist/extensions/feishu/**",
|
||||
"!dist/extensions/google-meet/**",
|
||||
"!dist/extensions/googlechat/**",
|
||||
"!dist/extensions/line/**",
|
||||
"!dist/extensions/lobster/**",
|
||||
"!dist/extensions/matrix/**",
|
||||
"!dist/extensions/mattermost/**",
|
||||
@@ -82,6 +79,9 @@
|
||||
"skills/",
|
||||
"scripts/npm-runner.mjs",
|
||||
"scripts/preinstall-package-manager-warning.mjs",
|
||||
"scripts/lib/official-external-channel-catalog.json",
|
||||
"scripts/lib/official-external-plugin-catalog.json",
|
||||
"scripts/lib/official-external-provider-catalog.json",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"scripts/windows-cmd-helpers.mjs"
|
||||
|
||||
@@ -47,6 +47,357 @@
|
||||
"expectedIntegrity": "sha512-lYmBrU71ox3v7dzRqaltvzTXPcMjjgYrNqpBj5HIBkXgEFkXRRG8wplXg9Fub41/FjsSPn3WAbYpdTc+k+jsHg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/bluebubbles",
|
||||
"description": "OpenClaw BlueBubbles channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "bluebubbles",
|
||||
"label": "BlueBubbles",
|
||||
"selectionLabel": "BlueBubbles (macOS app)",
|
||||
"detailLabel": "BlueBubbles",
|
||||
"docsPath": "/channels/bluebubbles",
|
||||
"docsLabel": "bluebubbles",
|
||||
"blurb": "iMessage via the BlueBubbles mac app + REST API.",
|
||||
"aliases": ["bb"],
|
||||
"preferOver": ["imessage"],
|
||||
"systemImage": "bubble.left.and.text.bubble.right",
|
||||
"order": 75
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/bluebubbles",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/discord",
|
||||
"description": "OpenClaw Discord channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "discord",
|
||||
"label": "Discord",
|
||||
"selectionLabel": "Discord (Bot API)",
|
||||
"detailLabel": "Discord Bot",
|
||||
"docsPath": "/channels/discord",
|
||||
"docsLabel": "discord",
|
||||
"blurb": "very well supported right now.",
|
||||
"systemImage": "bubble.left.and.bubble.right",
|
||||
"markdownCapable": true,
|
||||
"preferSessionLookupForAnnounceTarget": true
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/discord",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/feishu",
|
||||
"description": "OpenClaw Feishu/Lark channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "feishu",
|
||||
"label": "Feishu",
|
||||
"selectionLabel": "Feishu/Lark (飞书)",
|
||||
"docsPath": "/channels/feishu",
|
||||
"docsLabel": "feishu",
|
||||
"blurb": "飞书/Lark enterprise messaging with doc/wiki/drive tools.",
|
||||
"aliases": ["lark"],
|
||||
"order": 35,
|
||||
"quickstartAllowFrom": true
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/feishu",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/matrix",
|
||||
"description": "OpenClaw Matrix channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "matrix",
|
||||
"label": "Matrix",
|
||||
"selectionLabel": "Matrix (plugin)",
|
||||
"docsPath": "/channels/matrix",
|
||||
"docsLabel": "matrix",
|
||||
"blurb": "open protocol; install the plugin to enable.",
|
||||
"order": 70,
|
||||
"quickstartAllowFrom": true
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/matrix",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10",
|
||||
"allowInvalidConfigRecovery": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/mattermost",
|
||||
"description": "OpenClaw Mattermost channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "mattermost",
|
||||
"label": "Mattermost",
|
||||
"selectionLabel": "Mattermost (plugin)",
|
||||
"docsPath": "/channels/mattermost",
|
||||
"docsLabel": "mattermost",
|
||||
"blurb": "self-hosted Slack-style chat; install the plugin to enable.",
|
||||
"order": 65
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/mattermost",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/msteams",
|
||||
"description": "OpenClaw Microsoft Teams channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "msteams",
|
||||
"label": "Microsoft Teams",
|
||||
"selectionLabel": "Microsoft Teams (Teams SDK)",
|
||||
"docsPath": "/channels/msteams",
|
||||
"docsLabel": "msteams",
|
||||
"blurb": "Teams SDK; enterprise support.",
|
||||
"aliases": ["teams"],
|
||||
"order": 60
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/msteams",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/nextcloud-talk",
|
||||
"description": "OpenClaw Nextcloud Talk channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "nextcloud-talk",
|
||||
"label": "Nextcloud Talk",
|
||||
"selectionLabel": "Nextcloud Talk (self-hosted)",
|
||||
"docsPath": "/channels/nextcloud-talk",
|
||||
"docsLabel": "nextcloud-talk",
|
||||
"blurb": "Self-hosted chat via Nextcloud Talk webhook bots.",
|
||||
"aliases": ["nc-talk", "nc"],
|
||||
"order": 65,
|
||||
"quickstartAllowFrom": true
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/nextcloud-talk",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/nostr",
|
||||
"description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "nostr",
|
||||
"label": "Nostr",
|
||||
"selectionLabel": "Nostr (NIP-04 DMs)",
|
||||
"docsPath": "/channels/nostr",
|
||||
"docsLabel": "nostr",
|
||||
"blurb": "Decentralized protocol; encrypted DMs via NIP-04.",
|
||||
"order": 55,
|
||||
"quickstartAllowFrom": true
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/nostr",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/qqbot",
|
||||
"description": "OpenClaw QQ Bot channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "qqbot",
|
||||
"label": "QQ Bot",
|
||||
"selectionLabel": "QQ Bot (Official API)",
|
||||
"detailLabel": "QQ Bot",
|
||||
"docsPath": "/channels/qqbot",
|
||||
"docsLabel": "qqbot",
|
||||
"blurb": "connect to QQ via official QQ Bot API with group chat and direct message support.",
|
||||
"systemImage": "bubble.left.and.bubble.right"
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/qqbot",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/synology-chat",
|
||||
"description": "Synology Chat channel plugin for OpenClaw",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "synology-chat",
|
||||
"label": "Synology Chat",
|
||||
"selectionLabel": "Synology Chat (Webhook)",
|
||||
"docsPath": "/channels/synology-chat",
|
||||
"docsLabel": "synology-chat",
|
||||
"blurb": "Connect your Synology NAS Chat to OpenClaw with full agent capabilities.",
|
||||
"order": 90
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/synology-chat",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/tlon",
|
||||
"description": "OpenClaw Tlon/Urbit channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "tlon",
|
||||
"label": "Tlon",
|
||||
"selectionLabel": "Tlon (Urbit)",
|
||||
"docsPath": "/channels/tlon",
|
||||
"docsLabel": "tlon",
|
||||
"blurb": "decentralized messaging on Urbit; install the plugin to enable.",
|
||||
"order": 90,
|
||||
"quickstartAllowFrom": true
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/tlon",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/twitch",
|
||||
"description": "OpenClaw Twitch channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "twitch",
|
||||
"label": "Twitch",
|
||||
"selectionLabel": "Twitch (Chat)",
|
||||
"docsPath": "/channels/twitch",
|
||||
"blurb": "Twitch chat integration",
|
||||
"aliases": ["twitch-chat"]
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/twitch",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/whatsapp",
|
||||
"description": "OpenClaw WhatsApp channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "whatsapp",
|
||||
"label": "WhatsApp",
|
||||
"selectionLabel": "WhatsApp (QR link)",
|
||||
"detailLabel": "WhatsApp Web",
|
||||
"docsPath": "/channels/whatsapp",
|
||||
"docsLabel": "whatsapp",
|
||||
"blurb": "works with your own number; recommend a separate phone + eSIM.",
|
||||
"systemImage": "message"
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/whatsapp",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/zalo",
|
||||
"description": "OpenClaw Zalo channel plugin",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "zalo",
|
||||
"label": "Zalo",
|
||||
"selectionLabel": "Zalo (Bot API)",
|
||||
"docsPath": "/channels/zalo",
|
||||
"docsLabel": "zalo",
|
||||
"blurb": "Vietnam-focused messaging platform with Bot API.",
|
||||
"aliases": ["zl"],
|
||||
"order": 80,
|
||||
"quickstartAllowFrom": true
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/zalo",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/zalouser",
|
||||
"description": "OpenClaw Zalo Personal Account plugin via native zca-js integration",
|
||||
"source": "official",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "zalouser",
|
||||
"label": "Zalo Personal",
|
||||
"selectionLabel": "Zalo (Personal Account)",
|
||||
"docsPath": "/channels/zalouser",
|
||||
"docsLabel": "zalouser",
|
||||
"blurb": "Zalo personal account via QR code login.",
|
||||
"aliases": ["zlu"],
|
||||
"order": 85,
|
||||
"quickstartAllowFrom": false
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/zalouser",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
142
scripts/lib/official-external-plugin-catalog.json
Normal file
142
scripts/lib/official-external-plugin-catalog.json
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"description": "OpenClaw Brave plugin",
|
||||
"source": "official",
|
||||
"kind": "plugin",
|
||||
"openclaw": {
|
||||
"plugin": {
|
||||
"id": "brave",
|
||||
"label": "Brave"
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/brave-plugin",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/diagnostics-otel",
|
||||
"description": "OpenClaw diagnostics OpenTelemetry exporter",
|
||||
"source": "official",
|
||||
"kind": "plugin",
|
||||
"openclaw": {
|
||||
"plugin": {
|
||||
"id": "diagnostics-otel",
|
||||
"label": "Diagnostics OpenTelemetry"
|
||||
},
|
||||
"install": {
|
||||
"clawhubSpec": "clawhub:@openclaw/diagnostics-otel",
|
||||
"npmSpec": "@openclaw/diagnostics-otel",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/diagnostics-prometheus",
|
||||
"description": "OpenClaw diagnostics Prometheus exporter",
|
||||
"source": "official",
|
||||
"kind": "plugin",
|
||||
"openclaw": {
|
||||
"plugin": {
|
||||
"id": "diagnostics-prometheus",
|
||||
"label": "Diagnostics Prometheus"
|
||||
},
|
||||
"install": {
|
||||
"clawhubSpec": "clawhub:@openclaw/diagnostics-prometheus",
|
||||
"npmSpec": "@openclaw/diagnostics-prometheus",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/diffs",
|
||||
"description": "OpenClaw diff viewer plugin",
|
||||
"source": "official",
|
||||
"kind": "plugin",
|
||||
"openclaw": {
|
||||
"plugin": {
|
||||
"id": "diffs",
|
||||
"label": "Diffs"
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/diffs",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.30"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/google-meet",
|
||||
"description": "OpenClaw Google Meet participant plugin",
|
||||
"source": "official",
|
||||
"kind": "plugin",
|
||||
"openclaw": {
|
||||
"plugin": {
|
||||
"id": "google-meet",
|
||||
"label": "Google Meet"
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/google-meet",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.20"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/lobster",
|
||||
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
|
||||
"source": "official",
|
||||
"kind": "plugin",
|
||||
"openclaw": {
|
||||
"plugin": {
|
||||
"id": "lobster",
|
||||
"label": "Lobster"
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/lobster",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/memory-lancedb",
|
||||
"description": "OpenClaw LanceDB-backed long-term memory plugin with auto-recall/capture",
|
||||
"source": "official",
|
||||
"kind": "plugin",
|
||||
"openclaw": {
|
||||
"plugin": {
|
||||
"id": "memory-lancedb",
|
||||
"label": "Memory LanceDB"
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/memory-lancedb",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/voice-call",
|
||||
"description": "OpenClaw voice-call plugin",
|
||||
"source": "official",
|
||||
"kind": "plugin",
|
||||
"openclaw": {
|
||||
"plugin": {
|
||||
"id": "voice-call",
|
||||
"label": "Voice Call"
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/voice-call",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
30
scripts/lib/official-external-provider-catalog.json
Normal file
30
scripts/lib/official-external-provider-catalog.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"name": "@openclaw/codex",
|
||||
"description": "OpenClaw Codex harness and model provider plugin",
|
||||
"source": "official",
|
||||
"kind": "provider",
|
||||
"openclaw": {
|
||||
"plugin": {
|
||||
"id": "codex",
|
||||
"label": "Codex"
|
||||
},
|
||||
"providers": [
|
||||
{
|
||||
"id": "codex",
|
||||
"name": "Codex",
|
||||
"docs": "/providers/models",
|
||||
"categories": ["cloud", "llm"],
|
||||
"authChoices": []
|
||||
}
|
||||
],
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/codex",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.5.1-beta.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -26,6 +26,14 @@ const CORE_PROD_REQUIRED_PATHS = [
|
||||
path: "scripts/lib/official-external-channel-catalog.json",
|
||||
whenPresent: "src/channels/plugins/catalog.ts",
|
||||
},
|
||||
{
|
||||
path: "scripts/lib/official-external-plugin-catalog.json",
|
||||
whenPresent: "src/plugins/official-external-plugin-catalog.ts",
|
||||
},
|
||||
{
|
||||
path: "scripts/lib/official-external-provider-catalog.json",
|
||||
whenPresent: "src/plugins/official-external-plugin-catalog.ts",
|
||||
},
|
||||
{
|
||||
path: "scripts/lib/plugin-sdk-entrypoints.json",
|
||||
whenPresent: "src/plugin-sdk/entrypoints.ts",
|
||||
|
||||
@@ -55,6 +55,9 @@ const requiredPathGroups = [
|
||||
...WORKSPACE_TEMPLATE_PACK_PATHS,
|
||||
"scripts/npm-runner.mjs",
|
||||
"scripts/preinstall-package-manager-warning.mjs",
|
||||
"scripts/lib/official-external-channel-catalog.json",
|
||||
"scripts/lib/official-external-plugin-catalog.json",
|
||||
"scripts/lib/official-external-provider-catalog.json",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/plugin-sdk/compat.js",
|
||||
|
||||
@@ -53,6 +53,10 @@ function buildCatalogEntry(packageJson) {
|
||||
};
|
||||
}
|
||||
|
||||
function getCatalogChannelId(entry) {
|
||||
return trimString(entry?.openclaw?.channel?.id) || trimString(entry?.name);
|
||||
}
|
||||
|
||||
export function buildOfficialChannelCatalog(params = {}) {
|
||||
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
|
||||
const extensionsRoot = path.join(repoRoot, "extensions");
|
||||
@@ -74,7 +78,11 @@ export function buildOfficialChannelCatalog(params = {}) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
const entry = buildCatalogEntry(packageJson);
|
||||
if (entry) {
|
||||
const channelId = entry ? getCatalogChannelId(entry) : "";
|
||||
const alreadyPresent = channelId
|
||||
? entries.some((existing) => getCatalogChannelId(existing) === channelId)
|
||||
: false;
|
||||
if (entry && !alreadyPresent) {
|
||||
entries.push(entry);
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import officialExternalChannelCatalog from "../../../scripts/lib/official-external-channel-catalog.json" with { type: "json" };
|
||||
import { MANIFEST_KEY } from "../../compat/legacy-names.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
|
||||
import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
} from "../../plugins/install-source-info.js";
|
||||
import type { OpenClawPackageManifest } from "../../plugins/manifest.js";
|
||||
import type { PluginPackageChannel, PluginPackageInstall } from "../../plugins/manifest.js";
|
||||
import { listOfficialExternalChannelCatalogEntries } from "../../plugins/official-external-plugin-catalog.js";
|
||||
import type { PluginOrigin } from "../../plugins/plugin-origin.types.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { isRecord, resolveConfigDir, resolveUserPath } from "../../utils.js";
|
||||
@@ -196,7 +196,7 @@ function resolveOfficialCatalogPaths(options: CatalogOptions): string[] {
|
||||
}
|
||||
|
||||
function loadOfficialCatalogEntries(options: CatalogOptions): ChannelPluginCatalogEntry[] {
|
||||
const builtInEntries = parseCatalogEntries(officialExternalChannelCatalog);
|
||||
const builtInEntries = listOfficialExternalChannelCatalogEntries();
|
||||
const officialPaths = resolveOfficialCatalogPaths(options);
|
||||
const fileEntries =
|
||||
options.officialCatalogPaths && options.officialCatalogPaths.length > 0
|
||||
|
||||
@@ -117,12 +117,13 @@ describe("listPersistedBundledPluginLocationBridges", () => {
|
||||
pluginId: "diagnostics-otel",
|
||||
preferredSource: "npm",
|
||||
npmSpec: "@openclaw/diagnostics-otel",
|
||||
clawhubSpec: "clawhub:@openclaw/diagnostics-otel",
|
||||
channelIds: ["diagnostics-otel"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not create a relocation bridge without npm metadata", async () => {
|
||||
it("uses official external catalog metadata when the persisted bundled row lacks npm metadata", async () => {
|
||||
readPersistedInstalledPluginIndexMock.mockResolvedValue(
|
||||
makeIndex({
|
||||
pluginId: "diagnostics-otel",
|
||||
@@ -149,6 +150,37 @@ describe("listPersistedBundledPluginLocationBridges", () => {
|
||||
makeRegistry("diagnostics-otel"),
|
||||
);
|
||||
|
||||
await expect(listPersistedBundledPluginLocationBridges({})).resolves.toEqual([
|
||||
{
|
||||
bundledPluginId: "diagnostics-otel",
|
||||
pluginId: "diagnostics-otel",
|
||||
preferredSource: "npm",
|
||||
npmSpec: "@openclaw/diagnostics-otel",
|
||||
clawhubSpec: "clawhub:@openclaw/diagnostics-otel",
|
||||
channelIds: ["diagnostics-otel"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not create a relocation bridge without persisted or official install metadata", async () => {
|
||||
readPersistedInstalledPluginIndexMock.mockResolvedValue(
|
||||
makeIndex({
|
||||
pluginId: "local-only",
|
||||
manifestPath: "/app/dist/extensions/local-only/openclaw.plugin.json",
|
||||
manifestHash: "hash",
|
||||
source: "/app/dist/extensions/local-only/index.js",
|
||||
rootDir: "/app/dist/extensions/local-only",
|
||||
origin: "bundled",
|
||||
enabled: true,
|
||||
startup: startupInfo,
|
||||
compat: [],
|
||||
packageInstall: {
|
||||
warnings: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
loadPluginManifestRegistryForInstalledIndexMock.mockReturnValue(makeRegistry("local-only"));
|
||||
|
||||
await expect(listPersistedBundledPluginLocationBridges({})).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,11 @@ import { readPersistedInstalledPluginIndex } from "../plugins/installed-plugin-i
|
||||
import type { InstalledPluginIndexRecord } from "../plugins/installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "../plugins/manifest-registry-installed.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import {
|
||||
getOfficialExternalPluginCatalogEntry,
|
||||
getOfficialExternalPluginCatalogManifest,
|
||||
resolveOfficialExternalPluginInstall,
|
||||
} from "../plugins/official-external-plugin-catalog.js";
|
||||
|
||||
function buildBridgeFromPersistedBundledRecord(
|
||||
record: InstalledPluginIndexRecord,
|
||||
@@ -14,17 +19,32 @@ function buildBridgeFromPersistedBundledRecord(
|
||||
if (record.origin !== "bundled" || !record.enabled) {
|
||||
return null;
|
||||
}
|
||||
const npmSpec = record.packageInstall?.npm?.spec;
|
||||
if (!npmSpec) {
|
||||
const officialEntry = getOfficialExternalPluginCatalogEntry(record.pluginId);
|
||||
const officialInstall = officialEntry
|
||||
? resolveOfficialExternalPluginInstall(officialEntry)
|
||||
: null;
|
||||
const npmSpec = officialInstall?.npmSpec?.trim() ?? record.packageInstall?.npm?.spec;
|
||||
const clawhubSpec = officialInstall?.clawhubSpec?.trim();
|
||||
if (!npmSpec && !clawhubSpec) {
|
||||
return null;
|
||||
}
|
||||
const officialChannelId = officialEntry
|
||||
? getOfficialExternalPluginCatalogManifest(officialEntry)?.channel?.id?.trim()
|
||||
: undefined;
|
||||
const channelIds = manifest?.channels.length
|
||||
? manifest.channels
|
||||
: officialChannelId
|
||||
? [officialChannelId]
|
||||
: [];
|
||||
return {
|
||||
bundledPluginId: record.pluginId,
|
||||
pluginId: record.pluginId,
|
||||
preferredSource: "npm",
|
||||
npmSpec,
|
||||
preferredSource:
|
||||
officialInstall?.defaultChoice === "clawhub" && clawhubSpec ? "clawhub" : "npm",
|
||||
...(npmSpec ? { npmSpec } : {}),
|
||||
...(clawhubSpec ? { clawhubSpec } : {}),
|
||||
...(record.enabledByDefault ? { enabledByDefault: true } : {}),
|
||||
...(manifest?.channels.length ? { channelIds: manifest.channels } : {}),
|
||||
...(channelIds.length ? { channelIds } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,16 @@ const mocks = vi.hoisted(() => ({
|
||||
installPluginFromClawHub: vi.fn(),
|
||||
installPluginFromNpmSpec: vi.fn(),
|
||||
listChannelPluginCatalogEntries: vi.fn(),
|
||||
listOfficialExternalPluginCatalogEntries: vi.fn(),
|
||||
loadInstalledPluginIndexInstallRecords: vi.fn(),
|
||||
loadPluginMetadataSnapshot: vi.fn(),
|
||||
resolveOfficialExternalPluginId: vi.fn((entry: { id?: string }) => entry.id),
|
||||
resolveOfficialExternalPluginInstall: vi.fn(
|
||||
(entry: { install?: unknown }) => entry.install ?? null,
|
||||
),
|
||||
resolveOfficialExternalPluginLabel: vi.fn(
|
||||
(entry: { label?: string; id?: string }) => entry.label ?? entry.id ?? "plugin",
|
||||
),
|
||||
resolveDefaultPluginExtensionsDir: vi.fn(() => "/tmp/openclaw-plugins"),
|
||||
resolveProviderInstallCatalogEntries: vi.fn(),
|
||||
updateNpmInstalledPlugins: vi.fn(),
|
||||
@@ -42,6 +50,13 @@ vi.mock("../../../plugins/plugin-metadata-snapshot.js", () => ({
|
||||
loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot,
|
||||
}));
|
||||
|
||||
vi.mock("../../../plugins/official-external-plugin-catalog.js", () => ({
|
||||
listOfficialExternalPluginCatalogEntries: mocks.listOfficialExternalPluginCatalogEntries,
|
||||
resolveOfficialExternalPluginId: mocks.resolveOfficialExternalPluginId,
|
||||
resolveOfficialExternalPluginInstall: mocks.resolveOfficialExternalPluginInstall,
|
||||
resolveOfficialExternalPluginLabel: mocks.resolveOfficialExternalPluginLabel,
|
||||
}));
|
||||
|
||||
vi.mock("../../../plugins/provider-install-catalog.js", () => ({
|
||||
resolveProviderInstallCatalogEntries: mocks.resolveProviderInstallCatalogEntries,
|
||||
}));
|
||||
@@ -59,6 +74,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
});
|
||||
mocks.loadInstalledPluginIndexInstallRecords.mockResolvedValue({});
|
||||
mocks.listChannelPluginCatalogEntries.mockReturnValue([]);
|
||||
mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([]);
|
||||
mocks.resolveProviderInstallCatalogEntries.mockReturnValue([]);
|
||||
mocks.installPluginFromClawHub.mockResolvedValue({
|
||||
ok: true,
|
||||
@@ -221,6 +237,104 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("honors npm-first catalog metadata for missing OpenClaw channel plugins", async () => {
|
||||
mocks.installPluginFromNpmSpec.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
pluginId: "twitch",
|
||||
targetDir: "/tmp/openclaw-plugins/twitch",
|
||||
version: "2026.5.2",
|
||||
npmResolution: {
|
||||
name: "@openclaw/twitch",
|
||||
version: "2026.5.2",
|
||||
resolvedSpec: "@openclaw/twitch@2026.5.2",
|
||||
integrity: "sha512-twitch",
|
||||
resolvedAt: "2026-05-01T00:00:00.000Z",
|
||||
},
|
||||
});
|
||||
mocks.listChannelPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "twitch",
|
||||
pluginId: "twitch",
|
||||
meta: { label: "Twitch" },
|
||||
install: {
|
||||
npmSpec: "@openclaw/twitch",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { repairMissingPluginInstallsForIds } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
const result = await repairMissingPluginInstallsForIds({
|
||||
cfg: {},
|
||||
pluginIds: [],
|
||||
channelIds: ["twitch"],
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/twitch",
|
||||
expectedPluginId: "twitch",
|
||||
}),
|
||||
);
|
||||
expect(result.changes).toEqual([
|
||||
'Installed missing configured plugin "twitch" from @openclaw/twitch.',
|
||||
]);
|
||||
});
|
||||
|
||||
it("installs missing configured non-channel plugins from the official external catalog", async () => {
|
||||
mocks.installPluginFromNpmSpec.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
pluginId: "diagnostics-otel",
|
||||
targetDir: "/tmp/openclaw-plugins/diagnostics-otel",
|
||||
version: "2026.5.2",
|
||||
npmResolution: {
|
||||
name: "@openclaw/diagnostics-otel",
|
||||
version: "2026.5.2",
|
||||
resolvedSpec: "@openclaw/diagnostics-otel@2026.5.2",
|
||||
integrity: "sha512-otel",
|
||||
resolvedAt: "2026-05-01T00:00:00.000Z",
|
||||
},
|
||||
});
|
||||
mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "diagnostics-otel",
|
||||
label: "Diagnostics OpenTelemetry",
|
||||
install: {
|
||||
clawhubSpec: "clawhub:@openclaw/diagnostics-otel",
|
||||
npmSpec: "@openclaw/diagnostics-otel",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { repairMissingConfiguredPluginInstalls } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"diagnostics-otel": { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/diagnostics-otel",
|
||||
expectedPluginId: "diagnostics-otel",
|
||||
}),
|
||||
);
|
||||
expect(result.changes).toEqual([
|
||||
'Installed missing configured plugin "diagnostics-otel" from @openclaw/diagnostics-otel.',
|
||||
]);
|
||||
});
|
||||
|
||||
it("installs a missing third-party downloadable plugin from npm only", async () => {
|
||||
mocks.installPluginFromNpmSpec.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
|
||||
@@ -10,6 +10,13 @@ import { loadInstalledPluginIndexInstallRecords } from "../../../plugins/install
|
||||
import { writePersistedInstalledPluginIndexInstallRecords } from "../../../plugins/installed-plugin-index-records.js";
|
||||
import { buildNpmResolutionInstallFields } from "../../../plugins/installs.js";
|
||||
import { loadManifestMetadataSnapshot } from "../../../plugins/manifest-contract-eligibility.js";
|
||||
import type { PluginPackageInstall } from "../../../plugins/manifest.js";
|
||||
import {
|
||||
listOfficialExternalPluginCatalogEntries,
|
||||
resolveOfficialExternalPluginId,
|
||||
resolveOfficialExternalPluginInstall,
|
||||
resolveOfficialExternalPluginLabel,
|
||||
} from "../../../plugins/official-external-plugin-catalog.js";
|
||||
import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js";
|
||||
import { updateNpmInstalledPlugins } from "../../../plugins/update.js";
|
||||
import { asObjectRecord } from "./object.js";
|
||||
@@ -20,6 +27,7 @@ type DownloadableInstallCandidate = {
|
||||
npmSpec?: string;
|
||||
clawhubSpec?: string;
|
||||
expectedIntegrity?: string;
|
||||
defaultChoice?: PluginPackageInstall["defaultChoice"];
|
||||
};
|
||||
|
||||
function buildOpenClawClawHubSpec(npmSpec: string): string | undefined {
|
||||
@@ -37,6 +45,24 @@ function shouldFallbackClawHubToNpm(result: { ok: false; code?: string }): boole
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeInstallDefaultChoice(
|
||||
value: PluginPackageInstall["defaultChoice"] | undefined,
|
||||
): PluginPackageInstall["defaultChoice"] | undefined {
|
||||
return value === "clawhub" || value === "npm" || value === "local" ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveCandidateClawHubSpec(install: PluginPackageInstall): string | undefined {
|
||||
const explicit = install.clawhubSpec?.trim();
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
const npmSpec = install.npmSpec?.trim();
|
||||
if (!npmSpec || normalizeInstallDefaultChoice(install.defaultChoice) === "npm") {
|
||||
return undefined;
|
||||
}
|
||||
return buildOpenClawClawHubSpec(npmSpec);
|
||||
}
|
||||
|
||||
function collectConfiguredPluginIds(cfg: OpenClawConfig): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
const plugins = asObjectRecord(cfg.plugins);
|
||||
@@ -95,9 +121,7 @@ function collectDownloadableInstallCandidates(params: {
|
||||
continue;
|
||||
}
|
||||
const npmSpec = entry.install.npmSpec?.trim();
|
||||
const clawhubSpec =
|
||||
entry.install.clawhubSpec?.trim() ??
|
||||
(npmSpec ? buildOpenClawClawHubSpec(npmSpec) : undefined);
|
||||
const clawhubSpec = resolveCandidateClawHubSpec(entry.install);
|
||||
if (!npmSpec && !clawhubSpec) {
|
||||
continue;
|
||||
}
|
||||
@@ -109,6 +133,7 @@ function collectDownloadableInstallCandidates(params: {
|
||||
...(entry.install.expectedIntegrity
|
||||
? { expectedIntegrity: entry.install.expectedIntegrity }
|
||||
: {}),
|
||||
...(entry.install.defaultChoice ? { defaultChoice: entry.install.defaultChoice } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -124,9 +149,7 @@ function collectDownloadableInstallCandidates(params: {
|
||||
continue;
|
||||
}
|
||||
const npmSpec = entry.install.npmSpec?.trim();
|
||||
const clawhubSpec =
|
||||
entry.install.clawhubSpec?.trim() ??
|
||||
(npmSpec ? buildOpenClawClawHubSpec(npmSpec) : undefined);
|
||||
const clawhubSpec = resolveCandidateClawHubSpec(entry.install);
|
||||
if (!npmSpec && !clawhubSpec) {
|
||||
continue;
|
||||
}
|
||||
@@ -138,6 +161,34 @@ function collectDownloadableInstallCandidates(params: {
|
||||
...(entry.install.expectedIntegrity
|
||||
? { expectedIntegrity: entry.install.expectedIntegrity }
|
||||
: {}),
|
||||
...(entry.install.defaultChoice ? { defaultChoice: entry.install.defaultChoice } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
for (const entry of listOfficialExternalPluginCatalogEntries()) {
|
||||
const pluginId = resolveOfficialExternalPluginId(entry);
|
||||
if (!pluginId || candidates.has(pluginId) || params.blockedPluginIds?.has(pluginId)) {
|
||||
continue;
|
||||
}
|
||||
if (!configuredPluginIds.has(pluginId) && !params.missingPluginIds.has(pluginId)) {
|
||||
continue;
|
||||
}
|
||||
const install = resolveOfficialExternalPluginInstall(entry);
|
||||
if (!install) {
|
||||
continue;
|
||||
}
|
||||
const npmSpec = install.npmSpec?.trim();
|
||||
const clawhubSpec = resolveCandidateClawHubSpec(install);
|
||||
if (!npmSpec && !clawhubSpec) {
|
||||
continue;
|
||||
}
|
||||
candidates.set(pluginId, {
|
||||
pluginId,
|
||||
label: resolveOfficialExternalPluginLabel(entry),
|
||||
...(npmSpec ? { npmSpec } : {}),
|
||||
...(clawhubSpec ? { clawhubSpec } : {}),
|
||||
...(install.expectedIntegrity ? { expectedIntegrity: install.expectedIntegrity } : {}),
|
||||
...(install.defaultChoice ? { defaultChoice: install.defaultChoice } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,7 +208,7 @@ async function installCandidate(params: {
|
||||
const { candidate } = params;
|
||||
const extensionsDir = resolveDefaultPluginExtensionsDir();
|
||||
const changes: string[] = [];
|
||||
if (candidate.clawhubSpec) {
|
||||
if (candidate.clawhubSpec && candidate.defaultChoice !== "npm") {
|
||||
const clawhubResult = await installPluginFromClawHub({
|
||||
spec: candidate.clawhubSpec,
|
||||
extensionsDir,
|
||||
|
||||
173
src/plugins/official-external-plugin-catalog.ts
Normal file
173
src/plugins/official-external-plugin-catalog.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import officialExternalChannelCatalog from "../../scripts/lib/official-external-channel-catalog.json" with { type: "json" };
|
||||
import officialExternalPluginCatalog from "../../scripts/lib/official-external-plugin-catalog.json" with { type: "json" };
|
||||
import officialExternalProviderCatalog from "../../scripts/lib/official-external-provider-catalog.json" with { type: "json" };
|
||||
import { MANIFEST_KEY } from "../compat/legacy-names.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import type { PluginPackageInstall } from "./manifest.js";
|
||||
|
||||
type ManifestKey = typeof MANIFEST_KEY;
|
||||
|
||||
export type OfficialExternalProviderAuthChoice = {
|
||||
method?: string;
|
||||
choiceId?: string;
|
||||
choiceLabel?: string;
|
||||
choiceHint?: string;
|
||||
assistantPriority?: number;
|
||||
assistantVisibility?: "visible" | "manual-only";
|
||||
groupId?: string;
|
||||
groupLabel?: string;
|
||||
groupHint?: string;
|
||||
optionKey?: string;
|
||||
cliFlag?: string;
|
||||
cliOption?: string;
|
||||
cliDescription?: string;
|
||||
onboardingScopes?: readonly ("text-inference" | "image-generation")[];
|
||||
};
|
||||
|
||||
export type OfficialExternalProviderCatalogProvider = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
docs?: string;
|
||||
categories?: readonly string[];
|
||||
authChoices?: readonly OfficialExternalProviderAuthChoice[];
|
||||
};
|
||||
|
||||
export type OfficialExternalPluginCatalogManifest = {
|
||||
plugin?: {
|
||||
id?: string;
|
||||
label?: string;
|
||||
};
|
||||
channel?: {
|
||||
id?: string;
|
||||
label?: string;
|
||||
};
|
||||
providers?: readonly OfficialExternalProviderCatalogProvider[];
|
||||
install?: PluginPackageInstall;
|
||||
};
|
||||
|
||||
export type OfficialExternalPluginCatalogEntry = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
source?: string;
|
||||
kind?: string;
|
||||
} & Partial<Record<ManifestKey, OfficialExternalPluginCatalogManifest>>;
|
||||
|
||||
const OFFICIAL_CATALOG_SOURCES = [
|
||||
officialExternalChannelCatalog,
|
||||
officialExternalProviderCatalog,
|
||||
officialExternalPluginCatalog,
|
||||
] as const;
|
||||
|
||||
function parseCatalogEntries(raw: unknown): OfficialExternalPluginCatalogEntry[] {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw.filter((entry): entry is OfficialExternalPluginCatalogEntry => isRecord(entry));
|
||||
}
|
||||
if (!isRecord(raw)) {
|
||||
return [];
|
||||
}
|
||||
const list = raw.entries ?? raw.packages ?? raw.plugins;
|
||||
if (!Array.isArray(list)) {
|
||||
return [];
|
||||
}
|
||||
return list.filter((entry): entry is OfficialExternalPluginCatalogEntry => isRecord(entry));
|
||||
}
|
||||
|
||||
function normalizeDefaultChoice(value: unknown): PluginPackageInstall["defaultChoice"] | undefined {
|
||||
return value === "clawhub" || value === "npm" || value === "local" ? value : undefined;
|
||||
}
|
||||
|
||||
export function getOfficialExternalPluginCatalogManifest(
|
||||
entry: OfficialExternalPluginCatalogEntry,
|
||||
): OfficialExternalPluginCatalogManifest | undefined {
|
||||
const manifest = entry[MANIFEST_KEY];
|
||||
return isRecord(manifest) ? (manifest as OfficialExternalPluginCatalogManifest) : undefined;
|
||||
}
|
||||
|
||||
export function resolveOfficialExternalPluginId(
|
||||
entry: OfficialExternalPluginCatalogEntry,
|
||||
): string | undefined {
|
||||
const manifest = getOfficialExternalPluginCatalogManifest(entry);
|
||||
return (
|
||||
normalizeOptionalString(manifest?.plugin?.id) ??
|
||||
normalizeOptionalString(manifest?.channel?.id) ??
|
||||
normalizeOptionalString(manifest?.providers?.[0]?.id)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveOfficialExternalPluginLabel(
|
||||
entry: OfficialExternalPluginCatalogEntry,
|
||||
): string {
|
||||
const manifest = getOfficialExternalPluginCatalogManifest(entry);
|
||||
return (
|
||||
normalizeOptionalString(manifest?.plugin?.label) ??
|
||||
normalizeOptionalString(manifest?.channel?.label) ??
|
||||
normalizeOptionalString(manifest?.providers?.[0]?.name) ??
|
||||
normalizeOptionalString(entry.name) ??
|
||||
resolveOfficialExternalPluginId(entry) ??
|
||||
"plugin"
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveOfficialExternalPluginInstall(
|
||||
entry: OfficialExternalPluginCatalogEntry,
|
||||
): PluginPackageInstall | null {
|
||||
const manifest = getOfficialExternalPluginCatalogManifest(entry);
|
||||
const install = manifest?.install;
|
||||
const clawhubSpec = normalizeOptionalString(install?.clawhubSpec);
|
||||
const npmSpec = normalizeOptionalString(install?.npmSpec) ?? normalizeOptionalString(entry.name);
|
||||
const localPath = normalizeOptionalString(install?.localPath);
|
||||
if (!clawhubSpec && !npmSpec && !localPath) {
|
||||
return null;
|
||||
}
|
||||
const defaultChoice =
|
||||
normalizeDefaultChoice(install?.defaultChoice) ??
|
||||
(npmSpec ? "npm" : clawhubSpec ? "clawhub" : localPath ? "local" : undefined);
|
||||
return {
|
||||
...(clawhubSpec ? { clawhubSpec } : {}),
|
||||
...(npmSpec ? { npmSpec } : {}),
|
||||
...(localPath ? { localPath } : {}),
|
||||
...(defaultChoice ? { defaultChoice } : {}),
|
||||
...(install?.minHostVersion ? { minHostVersion: install.minHostVersion } : {}),
|
||||
...(install?.expectedIntegrity ? { expectedIntegrity: install.expectedIntegrity } : {}),
|
||||
...(install?.allowInvalidConfigRecovery === true ? { allowInvalidConfigRecovery: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function listOfficialExternalPluginCatalogEntries(): OfficialExternalPluginCatalogEntry[] {
|
||||
const entries = OFFICIAL_CATALOG_SOURCES.flatMap((source) => parseCatalogEntries(source));
|
||||
const resolved = new Map<string, OfficialExternalPluginCatalogEntry>();
|
||||
for (const entry of entries) {
|
||||
const pluginId = resolveOfficialExternalPluginId(entry);
|
||||
const key = pluginId ? `${entry.kind ?? "plugin"}:${pluginId}` : `${entry.name ?? ""}`;
|
||||
if (key && !resolved.has(key)) {
|
||||
resolved.set(key, entry);
|
||||
}
|
||||
}
|
||||
return [...resolved.values()];
|
||||
}
|
||||
|
||||
export function listOfficialExternalChannelCatalogEntries(): OfficialExternalPluginCatalogEntry[] {
|
||||
return listOfficialExternalPluginCatalogEntries().filter((entry) =>
|
||||
Boolean(getOfficialExternalPluginCatalogManifest(entry)?.channel),
|
||||
);
|
||||
}
|
||||
|
||||
export function listOfficialExternalProviderCatalogEntries(): OfficialExternalPluginCatalogEntry[] {
|
||||
return listOfficialExternalPluginCatalogEntries().filter(
|
||||
(entry) => (getOfficialExternalPluginCatalogManifest(entry)?.providers?.length ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
export function getOfficialExternalPluginCatalogEntry(
|
||||
pluginId: string,
|
||||
): OfficialExternalPluginCatalogEntry | undefined {
|
||||
const normalized = pluginId.trim();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return listOfficialExternalPluginCatalogEntries().find(
|
||||
(entry) => resolveOfficialExternalPluginId(entry) === normalized,
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,12 @@ import {
|
||||
} from "./install-source-info.js";
|
||||
import type { InstalledPluginInstallRecordInfo } from "./installed-plugin-index.js";
|
||||
import type { PluginPackageInstall } from "./manifest.js";
|
||||
import {
|
||||
getOfficialExternalPluginCatalogManifest,
|
||||
listOfficialExternalProviderCatalogEntries,
|
||||
resolveOfficialExternalPluginInstall,
|
||||
type OfficialExternalProviderAuthChoice,
|
||||
} from "./official-external-plugin-catalog.js";
|
||||
import type { PluginOrigin } from "./plugin-origin.types.js";
|
||||
import { loadPluginRegistrySnapshot, type PluginRegistryRecord } from "./plugin-registry.js";
|
||||
import {
|
||||
@@ -248,6 +254,84 @@ function resolveProviderIndexInstallCatalogEntries(params: {
|
||||
return entries;
|
||||
}
|
||||
|
||||
function isProviderFlowScope(value: unknown): value is "text-inference" | "image-generation" {
|
||||
return value === "text-inference" || value === "image-generation";
|
||||
}
|
||||
|
||||
function normalizeProviderAuthChoiceScopes(
|
||||
scopes: OfficialExternalProviderAuthChoice["onboardingScopes"],
|
||||
): ("text-inference" | "image-generation")[] | undefined {
|
||||
if (!Array.isArray(scopes)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = scopes.filter(isProviderFlowScope);
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function resolveOfficialExternalProviderInstallCatalogEntries(params: {
|
||||
installedPluginIds: ReadonlySet<string>;
|
||||
seenChoiceIds: ReadonlySet<string>;
|
||||
}): ProviderInstallCatalogEntry[] {
|
||||
const entries: ProviderInstallCatalogEntry[] = [];
|
||||
for (const entry of listOfficialExternalProviderCatalogEntries()) {
|
||||
const manifest = getOfficialExternalPluginCatalogManifest(entry);
|
||||
const pluginId = manifest?.plugin?.id?.trim();
|
||||
if (!manifest || !pluginId || params.installedPluginIds.has(pluginId)) {
|
||||
continue;
|
||||
}
|
||||
const install = resolveOfficialExternalPluginInstall(entry);
|
||||
if (!install) {
|
||||
continue;
|
||||
}
|
||||
for (const provider of manifest?.providers ?? []) {
|
||||
const providerId = provider.id?.trim();
|
||||
const label = provider.name?.trim() || manifest.plugin?.label?.trim() || entry.name?.trim();
|
||||
if (!providerId || !label) {
|
||||
continue;
|
||||
}
|
||||
for (const choice of provider.authChoices ?? []) {
|
||||
const methodId = choice.method?.trim();
|
||||
const choiceId = choice.choiceId?.trim();
|
||||
const choiceLabel = choice.choiceLabel?.trim();
|
||||
if (!methodId || !choiceId || !choiceLabel || params.seenChoiceIds.has(choiceId)) {
|
||||
continue;
|
||||
}
|
||||
entries.push({
|
||||
pluginId,
|
||||
providerId,
|
||||
methodId,
|
||||
choiceId,
|
||||
choiceLabel,
|
||||
...(choice.choiceHint ? { choiceHint: choice.choiceHint } : {}),
|
||||
...(choice.assistantPriority !== undefined
|
||||
? { assistantPriority: choice.assistantPriority }
|
||||
: {}),
|
||||
...(choice.assistantVisibility
|
||||
? { assistantVisibility: choice.assistantVisibility }
|
||||
: {}),
|
||||
...(choice.groupId ? { groupId: choice.groupId } : {}),
|
||||
...(choice.groupLabel ? { groupLabel: choice.groupLabel } : {}),
|
||||
...(choice.groupHint ? { groupHint: choice.groupHint } : {}),
|
||||
...(choice.optionKey ? { optionKey: choice.optionKey } : {}),
|
||||
...(choice.cliFlag ? { cliFlag: choice.cliFlag } : {}),
|
||||
...(choice.cliOption ? { cliOption: choice.cliOption } : {}),
|
||||
...(choice.cliDescription ? { cliDescription: choice.cliDescription } : {}),
|
||||
...(normalizeProviderAuthChoiceScopes(choice.onboardingScopes)
|
||||
? { onboardingScopes: normalizeProviderAuthChoiceScopes(choice.onboardingScopes) }
|
||||
: {}),
|
||||
label,
|
||||
origin: "bundled",
|
||||
install,
|
||||
installSource: describePluginInstallSource(install, {
|
||||
expectedPackageName: entry.name,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function resolveProviderInstallCatalogEntries(
|
||||
params?: ProviderInstallCatalogParams,
|
||||
): ProviderInstallCatalogEntry[] {
|
||||
@@ -274,11 +358,18 @@ export function resolveProviderInstallCatalogEntries(
|
||||
})
|
||||
.toSorted((left, right) => left.choiceLabel.localeCompare(right.choiceLabel));
|
||||
const seenChoiceIds = new Set(manifestEntries.map((entry) => entry.choiceId));
|
||||
const officialEntries = resolveOfficialExternalProviderInstallCatalogEntries({
|
||||
installedPluginIds,
|
||||
seenChoiceIds,
|
||||
});
|
||||
for (const entry of officialEntries) {
|
||||
seenChoiceIds.add(entry.choiceId);
|
||||
}
|
||||
const indexEntries = resolveProviderIndexInstallCatalogEntries({
|
||||
installedPluginIds,
|
||||
seenChoiceIds,
|
||||
});
|
||||
return [...manifestEntries, ...indexEntries].toSorted((left, right) =>
|
||||
return [...manifestEntries, ...officialEntries, ...indexEntries].toSorted((left, right) =>
|
||||
left.choiceLabel.localeCompare(right.choiceLabel),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,33 +101,32 @@ describe("buildOfficialChannelCatalog", () => {
|
||||
},
|
||||
}),
|
||||
}),
|
||||
{
|
||||
expect.objectContaining({
|
||||
name: "@openclaw/whatsapp",
|
||||
version: "2026.3.23",
|
||||
description: "OpenClaw WhatsApp channel plugin",
|
||||
openclaw: {
|
||||
channel: {
|
||||
source: "official",
|
||||
openclaw: expect.objectContaining({
|
||||
channel: expect.objectContaining({
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp (QR link)",
|
||||
detailLabel: "WhatsApp Web",
|
||||
docsPath: "/channels/whatsapp",
|
||||
blurb: "works with your own number; recommend a separate phone + eSIM.",
|
||||
},
|
||||
install: {
|
||||
}),
|
||||
install: expect.objectContaining({
|
||||
npmSpec: "@openclaw/whatsapp",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps official external catalog npm sources exactly pinned", () => {
|
||||
it("keeps third-party official external catalog npm sources exactly pinned", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-official-channel-catalog-policy-");
|
||||
const entries = buildOfficialChannelCatalog({ repoRoot }).entries.filter(
|
||||
(entry) => entry.source === "external",
|
||||
(entry) => entry.source === "external" && !entry.name?.startsWith("@openclaw/"),
|
||||
);
|
||||
|
||||
expect(entries.length).toBeGreaterThan(0);
|
||||
@@ -138,6 +137,29 @@ describe("buildOfficialChannelCatalog", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("allows official OpenClaw channel npm specs without integrity during launch", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-official-channel-catalog-openclaw-policy-");
|
||||
const twitch = buildOfficialChannelCatalog({ repoRoot }).entries.find(
|
||||
(entry) => entry.openclaw?.channel?.id === "twitch",
|
||||
);
|
||||
|
||||
expect(twitch).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "@openclaw/twitch",
|
||||
openclaw: expect.objectContaining({
|
||||
install: {
|
||||
npmSpec: "@openclaw/twitch",
|
||||
defaultChoice: "npm",
|
||||
minHostVersion: ">=2026.4.10",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const installSource = describePluginInstallSource(twitch?.openclaw?.install ?? {});
|
||||
expect(installSource.npm?.pinState).toBe("floating-without-integrity");
|
||||
expect(installSource.warnings).toEqual(["npm-spec-floating", "npm-spec-missing-integrity"]);
|
||||
});
|
||||
|
||||
it("writes the official catalog under dist", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-official-channel-catalog-write-");
|
||||
writeJson(path.join(repoRoot, "extensions", "whatsapp", "package.json"), {
|
||||
@@ -171,22 +193,28 @@ describe("buildOfficialChannelCatalog", () => {
|
||||
expect.objectContaining({
|
||||
name: "openclaw-plugin-yuanbao",
|
||||
}),
|
||||
{
|
||||
expect.objectContaining({
|
||||
name: "@openclaw/whatsapp",
|
||||
openclaw: {
|
||||
channel: {
|
||||
source: "official",
|
||||
openclaw: expect.objectContaining({
|
||||
channel: expect.objectContaining({
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp",
|
||||
selectionLabel: "WhatsApp (QR link)",
|
||||
docsPath: "/channels/whatsapp",
|
||||
blurb: "wa",
|
||||
},
|
||||
install: {
|
||||
}),
|
||||
install: expect.objectContaining({
|
||||
npmSpec: "@openclaw/whatsapp",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultChoice: "npm",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
const whatsappEntries = JSON.parse(fs.readFileSync(outputPath, "utf8")).entries.filter(
|
||||
(entry: { openclaw?: { channel?: { id?: string } } }) =>
|
||||
entry.openclaw?.channel?.id === "whatsapp",
|
||||
);
|
||||
expect(whatsappEntries).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -485,6 +485,9 @@ describe("collectMissingPackPaths", () => {
|
||||
"dist/control-ui/index.html",
|
||||
"scripts/npm-runner.mjs",
|
||||
"scripts/preinstall-package-manager-warning.mjs",
|
||||
"scripts/lib/official-external-channel-catalog.json",
|
||||
"scripts/lib/official-external-plugin-catalog.json",
|
||||
"scripts/lib/official-external-provider-catalog.json",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/task-registry-control.runtime.js",
|
||||
@@ -517,6 +520,9 @@ describe("collectMissingPackPaths", () => {
|
||||
...WORKSPACE_TEMPLATE_PACK_PATHS,
|
||||
"scripts/npm-runner.mjs",
|
||||
"scripts/preinstall-package-manager-warning.mjs",
|
||||
"scripts/lib/official-external-channel-catalog.json",
|
||||
"scripts/lib/official-external-plugin-catalog.json",
|
||||
"scripts/lib/official-external-provider-catalog.json",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/plugin-sdk/root-alias.cjs",
|
||||
|
||||
Reference in New Issue
Block a user