extract marketplace catalog to JSON with refresh

This commit is contained in:
Dhruv Sharma 2026-03-31 04:02:25 +05:30
parent 39396d4534
commit bd60fb7b52
2 changed files with 358 additions and 0 deletions

View File

@ -0,0 +1,59 @@
[
{
"id": "agent-codex",
"package": "@composio/ao-plugin-agent-codex",
"slot": "agent",
"description": "Agent plugin: OpenAI Codex CLI",
"source": "registry",
"latestVersion": "0.2.0"
},
{
"id": "agent-aider",
"package": "@composio/ao-plugin-agent-aider",
"slot": "agent",
"description": "Agent plugin: Aider CLI",
"source": "registry",
"latestVersion": "0.2.0"
},
{
"id": "agent-opencode",
"package": "@composio/ao-plugin-agent-opencode",
"slot": "agent",
"description": "Agent plugin: OpenCode CLI",
"source": "registry",
"latestVersion": "0.2.0"
},
{
"id": "notifier-openclaw",
"package": "@composio/ao-plugin-notifier-openclaw",
"slot": "notifier",
"description": "Notifier plugin: OpenClaw webhook notifications",
"source": "registry",
"setupAction": "openclaw-setup",
"latestVersion": "0.1.1"
},
{
"id": "notifier-discord",
"package": "@composio/ao-plugin-notifier-discord",
"slot": "notifier",
"description": "Notifier plugin: Discord notifications",
"source": "registry",
"latestVersion": "0.1.0"
},
{
"id": "tracker-gitlab",
"package": "@composio/ao-plugin-tracker-gitlab",
"slot": "tracker",
"description": "Tracker plugin: GitLab issues",
"source": "registry",
"latestVersion": "0.1.1"
},
{
"id": "scm-gitlab",
"package": "@composio/ao-plugin-scm-gitlab",
"slot": "scm",
"description": "SCM plugin: GitLab pull/merge requests",
"source": "registry",
"latestVersion": "0.1.1"
}
]

View File

@ -0,0 +1,299 @@
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import type { InstalledPluginConfig, PluginModule, PluginSlot } from "@composio/ao-core";
import registryData from "../assets/plugin-registry.json";
export interface MarketplacePluginEntry {
id: string;
package: string;
slot: PluginSlot;
description: string;
source: "registry";
setupAction?: "openclaw-setup";
latestVersion?: string;
}
export const BUNDLED_MARKETPLACE_PLUGIN_CATALOG = registryData as MarketplacePluginEntry[];
export const MARKETPLACE_PLUGIN_CATALOG = BUNDLED_MARKETPLACE_PLUGIN_CATALOG;
export const DEFAULT_REMOTE_MARKETPLACE_REGISTRY_URL =
"https://raw.githubusercontent.com/ComposioHQ/agent-orchestrator/main/packages/cli/src/assets/plugin-registry.json";
const LOCAL_PLUGIN_ENTRY_CANDIDATES = ["dist/index.js", "index.js"] as const;
const MARKETPLACE_CACHE_FILE = "plugin-registry.json";
function isPluginSlot(value: unknown): value is PluginSlot {
return (
value === "runtime" ||
value === "agent" ||
value === "workspace" ||
value === "tracker" ||
value === "scm" ||
value === "notifier" ||
value === "terminal"
);
}
function isMarketplacePluginEntry(value: unknown): value is MarketplacePluginEntry {
if (!value || typeof value !== "object") return false;
const candidate = value as Partial<MarketplacePluginEntry>;
return (
typeof candidate.id === "string" &&
typeof candidate.package === "string" &&
isPluginSlot(candidate.slot) &&
typeof candidate.description === "string" &&
candidate.source === "registry"
);
}
function mergeMarketplaceCatalogs(
primary: MarketplacePluginEntry[],
fallback: MarketplacePluginEntry[],
): MarketplacePluginEntry[] {
const merged = new Map<string, MarketplacePluginEntry>();
for (const entry of fallback) {
merged.set(entry.id, entry);
}
for (const entry of primary) {
merged.set(entry.id, entry);
}
return [...merged.values()];
}
export function getMarketplaceRegistryCachePath(): string {
const override = process.env["AO_PLUGIN_REGISTRY_CACHE_PATH"];
if (override && override.trim().length > 0) {
return override;
}
return join(homedir(), ".agent-orchestrator", MARKETPLACE_CACHE_FILE);
}
function validateMarketplaceCatalog(payload: unknown, sourceLabel: string): MarketplacePluginEntry[] {
if (!Array.isArray(payload)) {
throw new Error(`${sourceLabel} did not return a registry array.`);
}
const entries = payload.filter(isMarketplacePluginEntry);
if (entries.length !== payload.length) {
throw new Error(`${sourceLabel} returned invalid marketplace registry entries.`);
}
return entries;
}
function readMarketplaceCatalogFile(filePath: string): MarketplacePluginEntry[] | null {
if (!existsSync(filePath)) return null;
try {
const parsed = JSON.parse(readFileSync(filePath, "utf-8")) as unknown;
return validateMarketplaceCatalog(parsed, filePath);
} catch {
return null;
}
}
function writeMarketplaceCatalogCache(entries: MarketplacePluginEntry[]): void {
const cachePath = getMarketplaceRegistryCachePath();
mkdirSync(dirname(cachePath), { recursive: true });
const tempPath = `${cachePath}.tmp.${process.pid}.${Date.now()}`;
writeFileSync(tempPath, `${JSON.stringify(entries, null, 2)}\n`, "utf-8");
renameSync(tempPath, cachePath);
}
export function loadMarketplaceCatalog(): MarketplacePluginEntry[] {
const cached = readMarketplaceCatalogFile(getMarketplaceRegistryCachePath());
if (!cached) {
return [...BUNDLED_MARKETPLACE_PLUGIN_CATALOG];
}
return mergeMarketplaceCatalogs(cached, BUNDLED_MARKETPLACE_PLUGIN_CATALOG);
}
export async function refreshMarketplaceCatalog(
url = process.env["AO_PLUGIN_REGISTRY_URL"] ?? DEFAULT_REMOTE_MARKETPLACE_REGISTRY_URL,
): Promise<MarketplacePluginEntry[]> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch marketplace registry from ${url} (HTTP ${response.status}).`);
}
const parsed = (await response.json()) as unknown;
const remoteEntries = validateMarketplaceCatalog(parsed, url);
const mergedEntries = mergeMarketplaceCatalogs(remoteEntries, BUNDLED_MARKETPLACE_PLUGIN_CATALOG);
writeMarketplaceCatalogCache(mergedEntries);
return mergedEntries;
}
function isPluginModule(value: unknown): value is PluginModule {
if (!value || typeof value !== "object") return false;
const candidate = value as Partial<PluginModule>;
return Boolean(candidate.manifest && typeof candidate.create === "function");
}
export function normalizeImportedPluginModule(value: unknown): PluginModule | null {
if (isPluginModule(value)) return value;
if (value && typeof value === "object" && "default" in value) {
const defaultExport = (value as { default?: unknown }).default;
if (isPluginModule(defaultExport)) return defaultExport;
}
return null;
}
function resolvePackageExportsEntry(exportsField: unknown): string | null {
if (typeof exportsField === "string") return exportsField;
if (!exportsField || typeof exportsField !== "object") return null;
const exportsRecord = exportsField as Record<string, unknown>;
const dotEntry = exportsRecord["."];
if (typeof dotEntry === "string") return dotEntry;
if (dotEntry && typeof dotEntry === "object") {
const importEntry = (dotEntry as Record<string, unknown>)["import"];
if (typeof importEntry === "string") return importEntry;
const defaultEntry = (dotEntry as Record<string, unknown>)["default"];
if (typeof defaultEntry === "string") return defaultEntry;
}
const importEntry = exportsRecord["import"];
if (typeof importEntry === "string") return importEntry;
return null;
}
function resolveLocalPluginEntrypoint(pluginPath: string): string | null {
if (!existsSync(pluginPath)) return null;
const stat = statSync(pluginPath);
if (stat.isFile()) return pluginPath;
if (!stat.isDirectory()) return null;
const packageJsonPath = join(pluginPath, "package.json");
if (existsSync(packageJsonPath)) {
try {
const raw = readFileSync(packageJsonPath, "utf-8");
const packageJson = JSON.parse(raw) as { exports?: unknown; main?: unknown; module?: unknown };
const exportsEntry = resolvePackageExportsEntry(packageJson.exports);
if (exportsEntry) {
const resolvedExportsEntry = resolve(pluginPath, exportsEntry);
if (existsSync(resolvedExportsEntry)) return resolvedExportsEntry;
}
if (typeof packageJson.module === "string") {
const moduleEntry = resolve(pluginPath, packageJson.module);
if (existsSync(moduleEntry)) return moduleEntry;
}
if (typeof packageJson.main === "string") {
const mainEntry = resolve(pluginPath, packageJson.main);
if (existsSync(mainEntry)) return mainEntry;
}
} catch {
// Fall through to common entrypoints below.
}
}
for (const candidate of LOCAL_PLUGIN_ENTRY_CANDIDATES) {
const entry = join(pluginPath, candidate);
if (existsSync(entry)) return entry;
}
return null;
}
export function isLocalPluginReference(reference: string): boolean {
return (
reference.startsWith("./") ||
reference.startsWith("../") ||
reference.startsWith("/") ||
reference.startsWith("~/")
);
}
export function findMarketplacePlugin(reference: string): MarketplacePluginEntry | undefined {
return loadMarketplaceCatalog().find(
(plugin) => plugin.id === reference || plugin.package === reference,
);
}
function expandHomePath(value: string): string {
return value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
}
function parseNpmPackageReference(reference: string): { packageName: string; version?: string } {
if (reference.startsWith("@")) {
const slashIndex = reference.indexOf("/");
const versionSeparator = slashIndex >= 0 ? reference.indexOf("@", slashIndex + 1) : -1;
if (versionSeparator > 0) {
return {
packageName: reference.slice(0, versionSeparator),
version: reference.slice(versionSeparator + 1),
};
}
return { packageName: reference };
}
const versionSeparator = reference.lastIndexOf("@");
if (versionSeparator > 0) {
return {
packageName: reference.slice(0, versionSeparator),
version: reference.slice(versionSeparator + 1),
};
}
return { packageName: reference };
}
export function buildPluginDescriptor(
reference: string,
configPath: string,
): {
descriptor: InstalledPluginConfig;
specifier: string;
setupAction?: MarketplacePluginEntry["setupAction"];
} {
const marketplacePlugin = findMarketplacePlugin(reference);
if (marketplacePlugin) {
return {
descriptor: {
name: marketplacePlugin.id,
source: marketplacePlugin.source,
package: marketplacePlugin.package,
version: marketplacePlugin.latestVersion,
enabled: true,
},
specifier: marketplacePlugin.package,
setupAction: marketplacePlugin.setupAction,
};
}
if (isLocalPluginReference(reference)) {
const expandedReference = expandHomePath(reference);
const absolutePath = isAbsolute(expandedReference)
? expandedReference
: resolve(dirname(configPath), expandedReference);
const entrypoint = resolveLocalPluginEntrypoint(absolutePath);
if (!entrypoint) {
throw new Error(`Could not resolve a plugin entrypoint from ${reference}`);
}
return {
descriptor: {
name: reference,
source: "local",
path: reference,
enabled: true,
},
specifier: pathToFileURL(entrypoint).href,
};
}
const { packageName, version } = parseNpmPackageReference(reference);
return {
descriptor: {
name: reference,
source: "npm",
package: packageName,
version,
enabled: true,
},
specifier: packageName,
};
}