From 2a768697e58076ed072a4d8f31e97598286bb248 Mon Sep 17 00:00:00 2001 From: Artur Do Lago Date: Sun, 11 Jan 2026 13:56:25 +0100 Subject: [PATCH] feat(plugin): add plugin marketplace with CLI commands Add plugin discovery and management system: - Registry index (plugins/index.json) with curated plugins - Registry client with caching and fallback - Plugin manager for install/remove operations - CLI commands: plugin search, list, install, remove, info Registry hosted as static JSON, uses existing npm/Bun infrastructure. Co-Authored-By: Claude Opus 4.5 --- .../agent-core/src/cli/cmd/plugin/index.ts | 20 +++ .../agent-core/src/cli/cmd/plugin/info.ts | 82 +++++++++ .../agent-core/src/cli/cmd/plugin/install.ts | 53 ++++++ .../agent-core/src/cli/cmd/plugin/list.ts | 52 ++++++ .../agent-core/src/cli/cmd/plugin/remove.ts | 42 +++++ .../agent-core/src/cli/cmd/plugin/search.ts | 70 ++++++++ packages/agent-core/src/index.ts | 2 + packages/agent-core/src/plugin/manager.ts | 163 ++++++++++++++++++ packages/agent-core/src/plugin/registry.ts | 133 ++++++++++++++ plugins/index.json | 42 +++++ 10 files changed, 659 insertions(+) create mode 100644 packages/agent-core/src/cli/cmd/plugin/index.ts create mode 100644 packages/agent-core/src/cli/cmd/plugin/info.ts create mode 100644 packages/agent-core/src/cli/cmd/plugin/install.ts create mode 100644 packages/agent-core/src/cli/cmd/plugin/list.ts create mode 100644 packages/agent-core/src/cli/cmd/plugin/remove.ts create mode 100644 packages/agent-core/src/cli/cmd/plugin/search.ts create mode 100644 packages/agent-core/src/plugin/manager.ts create mode 100644 packages/agent-core/src/plugin/registry.ts create mode 100644 plugins/index.json diff --git a/packages/agent-core/src/cli/cmd/plugin/index.ts b/packages/agent-core/src/cli/cmd/plugin/index.ts new file mode 100644 index 0000000000..36d0ab4bbe --- /dev/null +++ b/packages/agent-core/src/cli/cmd/plugin/index.ts @@ -0,0 +1,20 @@ +import { cmd } from "../cmd" +import { SearchCommand } from "./search" +import { ListCommand } from "./list" +import { InstallCommand } from "./install" +import { RemoveCommand } from "./remove" +import { InfoCommand } from "./info" + +export const PluginCommand = cmd({ + command: "plugin", + describe: "manage plugins (search, install, remove)", + builder: (yargs) => + yargs + .command(SearchCommand) + .command(ListCommand) + .command(InstallCommand) + .command(RemoveCommand) + .command(InfoCommand) + .demandCommand(1, "Please specify a subcommand"), + async handler() {}, +}) diff --git a/packages/agent-core/src/cli/cmd/plugin/info.ts b/packages/agent-core/src/cli/cmd/plugin/info.ts new file mode 100644 index 0000000000..7a3f7c06ba --- /dev/null +++ b/packages/agent-core/src/cli/cmd/plugin/info.ts @@ -0,0 +1,82 @@ +import { cmd } from "../cmd" +import { fetchRegistry, getPlugin } from "../../../plugin/registry" +import { getInstalled } from "../../../plugin/manager" +import { UI } from "../../ui" + +export const InfoCommand = cmd({ + command: "info ", + describe: "show detailed information about a plugin", + builder: (yargs) => + yargs + .positional("name", { + describe: "plugin name", + type: "string", + demandOption: true, + }) + .option("json", { + describe: "output as JSON", + type: "boolean", + default: false, + }), + async handler(args) { + const { name, json } = args + + try { + const registry = await fetchRegistry() + const plugin = getPlugin(registry, name) + const installed = await getInstalled(name) + + if (!plugin && !installed) { + UI.error(`Plugin "${name}" not found`) + UI.println(UI.Style.TEXT_DIM + "Search for plugins with: agent-core plugin search" + UI.Style.TEXT_NORMAL) + process.exit(1) + } + + const info = { + ...plugin, + installed: !!installed, + installedVersion: installed?.version, + } + + if (json) { + console.log(JSON.stringify(info, null, 2)) + return + } + + // Display info + if (plugin) { + UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + plugin.displayName + UI.Style.TEXT_NORMAL) + UI.println(UI.Style.TEXT_DIM + plugin.name + UI.Style.TEXT_NORMAL) + UI.empty() + UI.println(plugin.description) + UI.empty() + UI.println(UI.Style.TEXT_NORMAL_BOLD + "Package: " + UI.Style.TEXT_NORMAL + `${plugin.npm}@${plugin.version}`) + UI.println(UI.Style.TEXT_NORMAL_BOLD + "Category: " + UI.Style.TEXT_NORMAL + plugin.category) + UI.println(UI.Style.TEXT_NORMAL_BOLD + "Tags: " + UI.Style.TEXT_NORMAL + plugin.tags.join(", ")) + UI.println(UI.Style.TEXT_NORMAL_BOLD + "Capabilities:" + UI.Style.TEXT_NORMAL + " " + plugin.capabilities.join(", ")) + UI.println(UI.Style.TEXT_NORMAL_BOLD + "Author: " + UI.Style.TEXT_NORMAL + plugin.author) + if (plugin.homepage) { + UI.println(UI.Style.TEXT_NORMAL_BOLD + "Homepage: " + UI.Style.TEXT_INFO + plugin.homepage + UI.Style.TEXT_NORMAL) + } + UI.empty() + + if (installed) { + UI.println(UI.Style.TEXT_SUCCESS + "✓ Installed" + UI.Style.TEXT_NORMAL + UI.Style.TEXT_DIM + ` (${installed.spec})` + UI.Style.TEXT_NORMAL) + } else { + UI.println(UI.Style.TEXT_DIM + "Not installed" + UI.Style.TEXT_NORMAL) + UI.println(UI.Style.TEXT_DIM + `Install with: agent-core plugin install ${plugin.name}` + UI.Style.TEXT_NORMAL) + } + } else if (installed) { + // Plugin not in registry but is installed + UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + installed.name + UI.Style.TEXT_NORMAL) + UI.empty() + UI.println(UI.Style.TEXT_NORMAL_BOLD + "Package:" + UI.Style.TEXT_NORMAL + ` ${installed.spec}`) + UI.empty() + UI.println(UI.Style.TEXT_WARNING + "⚠" + UI.Style.TEXT_NORMAL + UI.Style.TEXT_DIM + " This plugin is not in the official registry" + UI.Style.TEXT_NORMAL) + } + } catch (error) { + UI.error(`Failed to get plugin info: ${error instanceof Error ? error.message : error}`) + process.exit(1) + } + }, +}) diff --git a/packages/agent-core/src/cli/cmd/plugin/install.ts b/packages/agent-core/src/cli/cmd/plugin/install.ts new file mode 100644 index 0000000000..1999aba601 --- /dev/null +++ b/packages/agent-core/src/cli/cmd/plugin/install.ts @@ -0,0 +1,53 @@ +import { cmd } from "../cmd" +import { installPlugin, isInstalled } from "../../../plugin/manager" +import { UI } from "../../ui" + +export const InstallCommand = cmd({ + command: "install ", + aliases: ["add", "i"], + describe: "install a plugin from the registry", + builder: (yargs) => + yargs + .positional("name", { + describe: "plugin name", + type: "string", + demandOption: true, + }) + .option("force", { + alias: "f", + describe: "force reinstall even if already installed", + type: "boolean", + default: false, + }), + async handler(args) { + const { name, force } = args + + try { + // Check if already installed + if (!force && (await isInstalled(name))) { + UI.println(UI.Style.TEXT_WARNING + `Plugin "${name}" is already installed` + UI.Style.TEXT_NORMAL) + UI.println(UI.Style.TEXT_DIM + "Use --force to reinstall" + UI.Style.TEXT_NORMAL) + return + } + + UI.println(UI.Style.TEXT_DIM + `Installing ${name}...` + UI.Style.TEXT_NORMAL) + + const result = await installPlugin(name) + + if (result.success) { + UI.println(UI.Style.TEXT_SUCCESS + "✓" + UI.Style.TEXT_NORMAL + " " + result.message) + if (result.plugin) { + UI.empty() + UI.println(UI.Style.TEXT_DIM + `Capabilities: ${result.plugin.capabilities.join(", ")}` + UI.Style.TEXT_NORMAL) + UI.println(UI.Style.TEXT_DIM + "Restart agent-core for changes to take effect" + UI.Style.TEXT_NORMAL) + } + } else { + UI.println(UI.Style.TEXT_DANGER + "✗" + UI.Style.TEXT_NORMAL + " " + result.message) + process.exit(1) + } + } catch (error) { + UI.error(`Install failed: ${error instanceof Error ? error.message : error}`) + process.exit(1) + } + }, +}) diff --git a/packages/agent-core/src/cli/cmd/plugin/list.ts b/packages/agent-core/src/cli/cmd/plugin/list.ts new file mode 100644 index 0000000000..caca49a242 --- /dev/null +++ b/packages/agent-core/src/cli/cmd/plugin/list.ts @@ -0,0 +1,52 @@ +import { cmd } from "../cmd" +import { listInstalled } from "../../../plugin/manager" +import { UI } from "../../ui" + +export const ListCommand = cmd({ + command: "list", + aliases: ["ls"], + describe: "list installed plugins", + builder: (yargs) => + yargs.option("json", { + describe: "output as JSON", + type: "boolean", + default: false, + }), + async handler(args) { + try { + const installed = await listInstalled() + + if (args.json) { + console.log(JSON.stringify(installed, null, 2)) + return + } + + if (installed.length === 0) { + UI.println(UI.Style.TEXT_WARNING + "No plugins installed" + UI.Style.TEXT_NORMAL) + UI.println(UI.Style.TEXT_DIM + "Search for plugins with: agent-core plugin search" + UI.Style.TEXT_NORMAL) + return + } + + UI.println(UI.Style.TEXT_NORMAL_BOLD + `Installed plugins (${installed.length}):` + UI.Style.TEXT_NORMAL) + UI.empty() + + for (const plugin of installed) { + const name = plugin.registryInfo?.displayName ?? plugin.name + const badge = plugin.fromRegistry + ? UI.Style.TEXT_SUCCESS + "✓" + UI.Style.TEXT_NORMAL + : UI.Style.TEXT_WARNING + "?" + UI.Style.TEXT_NORMAL + + UI.println(`${badge} ${UI.Style.TEXT_HIGHLIGHT_BOLD}${name}${UI.Style.TEXT_NORMAL}`) + UI.println(UI.Style.TEXT_DIM + ` ${plugin.spec}` + UI.Style.TEXT_NORMAL) + + if (plugin.registryInfo) { + UI.println(UI.Style.TEXT_DIM + ` ${plugin.registryInfo.description}` + UI.Style.TEXT_NORMAL) + } + UI.empty() + } + } catch (error) { + UI.error(`Failed to list plugins: ${error instanceof Error ? error.message : error}`) + process.exit(1) + } + }, +}) diff --git a/packages/agent-core/src/cli/cmd/plugin/remove.ts b/packages/agent-core/src/cli/cmd/plugin/remove.ts new file mode 100644 index 0000000000..0f113ef5ee --- /dev/null +++ b/packages/agent-core/src/cli/cmd/plugin/remove.ts @@ -0,0 +1,42 @@ +import { cmd } from "../cmd" +import { removePlugin, isInstalled } from "../../../plugin/manager" +import { UI } from "../../ui" + +export const RemoveCommand = cmd({ + command: "remove ", + aliases: ["rm", "uninstall"], + describe: "remove an installed plugin", + builder: (yargs) => + yargs.positional("name", { + describe: "plugin name", + type: "string", + demandOption: true, + }), + async handler(args) { + const { name } = args + + try { + // Check if installed + if (!(await isInstalled(name))) { + UI.println(UI.Style.TEXT_WARNING + `Plugin "${name}" is not installed` + UI.Style.TEXT_NORMAL) + UI.println(UI.Style.TEXT_DIM + "Use 'agent-core plugin list' to see installed plugins" + UI.Style.TEXT_NORMAL) + return + } + + UI.println(UI.Style.TEXT_DIM + `Removing ${name}...` + UI.Style.TEXT_NORMAL) + + const result = await removePlugin(name) + + if (result.success) { + UI.println(UI.Style.TEXT_SUCCESS + "✓" + UI.Style.TEXT_NORMAL + " " + result.message) + UI.println(UI.Style.TEXT_DIM + "Restart agent-core for changes to take effect" + UI.Style.TEXT_NORMAL) + } else { + UI.println(UI.Style.TEXT_DANGER + "✗" + UI.Style.TEXT_NORMAL + " " + result.message) + process.exit(1) + } + } catch (error) { + UI.error(`Remove failed: ${error instanceof Error ? error.message : error}`) + process.exit(1) + } + }, +}) diff --git a/packages/agent-core/src/cli/cmd/plugin/search.ts b/packages/agent-core/src/cli/cmd/plugin/search.ts new file mode 100644 index 0000000000..26967070f1 --- /dev/null +++ b/packages/agent-core/src/cli/cmd/plugin/search.ts @@ -0,0 +1,70 @@ +import { cmd } from "../cmd" +import { fetchRegistry, searchPlugins } from "../../../plugin/registry" +import { UI } from "../../ui" + +export const SearchCommand = cmd({ + command: "search [query]", + describe: "search for plugins in the registry", + builder: (yargs) => + yargs + .positional("query", { + describe: "search query (matches name, description, tags)", + type: "string", + default: "", + }) + .option("category", { + alias: "c", + describe: "filter by category", + type: "string", + }) + .option("json", { + describe: "output as JSON", + type: "boolean", + default: false, + }), + async handler(args) { + try { + const registry = await fetchRegistry() + let plugins = args.query ? searchPlugins(registry, args.query) : registry.plugins + + if (args.category) { + plugins = plugins.filter((p) => p.category === args.category) + } + + if (args.json) { + console.log(JSON.stringify(plugins, null, 2)) + return + } + + if (plugins.length === 0) { + UI.println(UI.Style.TEXT_WARNING + "No plugins found" + UI.Style.TEXT_NORMAL) + if (args.query) { + UI.println(UI.Style.TEXT_DIM + "Try a different search term or browse all with: agent-core plugin search" + UI.Style.TEXT_NORMAL) + } + return + } + + UI.println(UI.Style.TEXT_NORMAL_BOLD + `Found ${plugins.length} plugin(s):` + UI.Style.TEXT_NORMAL) + UI.empty() + + for (const plugin of plugins) { + UI.println( + UI.Style.TEXT_HIGHLIGHT_BOLD + plugin.displayName + UI.Style.TEXT_NORMAL + + UI.Style.TEXT_DIM + ` (${plugin.name})` + UI.Style.TEXT_NORMAL + ) + UI.println(" " + plugin.description) + UI.println( + UI.Style.TEXT_DIM + + ` ${plugin.npm}@${plugin.version} · ${plugin.category} · ${plugin.tags.join(", ")}` + + UI.Style.TEXT_NORMAL + ) + UI.empty() + } + + UI.println(UI.Style.TEXT_DIM + "Install with: agent-core plugin install " + UI.Style.TEXT_NORMAL) + } catch (error) { + UI.error(`Failed to fetch registry: ${error instanceof Error ? error.message : error}`) + process.exit(1) + } + }, +}) diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts index 5da9b9c214..393653e484 100644 --- a/packages/agent-core/src/index.ts +++ b/packages/agent-core/src/index.ts @@ -28,6 +28,7 @@ import { WebCommand } from "./cli/cmd/web" import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" import { DaemonCommand, DaemonStatusCommand, DaemonStopCommand } from "./cli/cmd/daemon" +import { PluginCommand } from "./cli/cmd/plugin" process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { @@ -104,6 +105,7 @@ const cli = yargs(hideBin(process.argv)) .command(DaemonCommand) .command(DaemonStatusCommand) .command(DaemonStopCommand) + .command(PluginCommand) .fail((msg, err) => { if ( msg?.startsWith("Unknown argument") || diff --git a/packages/agent-core/src/plugin/manager.ts b/packages/agent-core/src/plugin/manager.ts new file mode 100644 index 0000000000..1d2b3066b0 --- /dev/null +++ b/packages/agent-core/src/plugin/manager.ts @@ -0,0 +1,163 @@ +import { BunProc } from "../bun" +import { Config } from "../config/config" +import { Log } from "../util/log" +import { fetchRegistry, getPlugin, type RegistryPlugin, type Registry } from "./registry" + +const log = Log.create({ service: "plugin-manager" }) + +export interface InstallResult { + success: boolean + message: string + plugin?: RegistryPlugin +} + +export interface RemoveResult { + success: boolean + message: string +} + +export interface InstalledPlugin { + name: string + npm: string + version: string + spec: string + fromRegistry: boolean + registryInfo?: RegistryPlugin +} + +/** + * Install a plugin from the registry + */ +export async function installPlugin(name: string): Promise { + const registry = await fetchRegistry() + const plugin = getPlugin(registry, name) + + if (!plugin) { + return { + success: false, + message: `Plugin "${name}" not found in registry. Use 'agent-core plugin search' to find available plugins.`, + } + } + + const pkgSpec = `${plugin.npm}@${plugin.version}` + + log.info("Installing plugin", { name: plugin.name, spec: pkgSpec }) + + try { + // Install via Bun + await BunProc.install(plugin.npm, plugin.version) + + // Add to config if not already present + const config = await Config.get() + const plugins = [...(config.plugin ?? [])] + + // Check if already installed (any version) + const existingIdx = plugins.findIndex( + (p) => typeof p === "string" && (p === plugin.npm || p.startsWith(`${plugin.npm}@`)) + ) + + if (existingIdx >= 0) { + // Update to new version + plugins[existingIdx] = pkgSpec + } else { + plugins.push(pkgSpec) + } + + await Config.update({ plugin: plugins }) + + log.info("Plugin installed", { name: plugin.name, spec: pkgSpec }) + return { + success: true, + message: `Installed ${plugin.displayName} (${pkgSpec})`, + plugin, + } + } catch (error) { + const msg = error instanceof Error ? error.message : String(error) + log.error("Failed to install plugin", { name, error: msg }) + return { + success: false, + message: `Failed to install ${name}: ${msg}`, + } + } +} + +/** + * Remove a plugin from config + */ +export async function removePlugin(name: string): Promise { + const config = await Config.get() + const plugins = [...(config.plugin ?? [])] + + // Find plugin in config (could be name or name@version) + const idx = plugins.findIndex( + (p) => typeof p === "string" && (p === name || p.startsWith(`${name}@`)) + ) + + if (idx === -1) { + return { + success: false, + message: `Plugin "${name}" not found in config. Use 'agent-core plugin list' to see installed plugins.`, + } + } + + const removed = plugins[idx] + plugins.splice(idx, 1) + + await Config.update({ plugin: plugins }) + + log.info("Plugin removed", { name, spec: removed }) + return { + success: true, + message: `Removed ${removed}`, + } +} + +/** + * List installed plugins with registry info + */ +export async function listInstalled(): Promise { + const config = await Config.get() + const configPlugins = (config.plugin ?? []).filter((p): p is string => typeof p === "string") + + let registry: Registry | null = null + try { + registry = await fetchRegistry() + } catch { + // Continue without registry info + } + + return configPlugins.map((spec) => { + // Parse spec (e.g., "opencode-copilot-auth@0.0.11" or "opencode-copilot-auth") + const lastAtIdx = spec.lastIndexOf("@") + const hasVersion = lastAtIdx > 0 + const npm = hasVersion ? spec.substring(0, lastAtIdx) : spec + const version = hasVersion ? spec.substring(lastAtIdx + 1) : "latest" + + const registryInfo = registry ? getPlugin(registry, npm) : undefined + + return { + name: registryInfo?.name ?? npm, + npm, + version, + spec, + fromRegistry: !!registryInfo, + registryInfo, + } + }) +} + +/** + * Check if a plugin is installed + */ +export async function isInstalled(name: string): Promise { + const installed = await listInstalled() + return installed.some((p) => p.name === name || p.npm === name) +} + +/** + * Get details about an installed plugin + */ +export async function getInstalled(name: string): Promise { + const installed = await listInstalled() + return installed.find((p) => p.name === name || p.npm === name) +} diff --git a/packages/agent-core/src/plugin/registry.ts b/packages/agent-core/src/plugin/registry.ts new file mode 100644 index 0000000000..9492eed783 --- /dev/null +++ b/packages/agent-core/src/plugin/registry.ts @@ -0,0 +1,133 @@ +import { z } from "zod" +import { Log } from "../util/log" +import { Global } from "../global" +import * as fs from "node:fs/promises" +import * as path from "node:path" + +const log = Log.create({ service: "plugin-registry" }) + +const RegistryPluginSchema = z.object({ + name: z.string(), + displayName: z.string(), + description: z.string(), + npm: z.string(), + version: z.string(), + category: z.string(), + tags: z.array(z.string()), + capabilities: z.array(z.string()), + author: z.string(), + homepage: z.string().optional(), +}) + +const RegistrySchema = z.object({ + version: z.number(), + plugins: z.array(RegistryPluginSchema), + categories: z.array(z.string()), +}) + +export type RegistryPlugin = z.infer +export type Registry = z.infer + +const REGISTRY_URL = "https://raw.githubusercontent.com/adolago/agent-core/main/plugins/index.json" +const CACHE_TTL = 3600000 // 1 hour +const CACHE_FILE = "plugin-registry.json" + +interface CachedRegistry { + data: Registry + timestamp: number +} + +let memoryCache: CachedRegistry | null = null + +async function getCachePath(): Promise { + return path.join(Global.Path.cache, CACHE_FILE) +} + +async function readCacheFile(): Promise { + try { + const cachePath = await getCachePath() + const content = await fs.readFile(cachePath, "utf-8") + return JSON.parse(content) + } catch { + return null + } +} + +async function writeCacheFile(cached: CachedRegistry): Promise { + try { + const cachePath = await getCachePath() + await fs.mkdir(path.dirname(cachePath), { recursive: true }) + await fs.writeFile(cachePath, JSON.stringify(cached, null, 2)) + } catch (error) { + log.warn("Failed to write registry cache", { error }) + } +} + +export async function fetchRegistry(forceRefresh = false): Promise { + // Check memory cache first + if (!forceRefresh && memoryCache && Date.now() - memoryCache.timestamp < CACHE_TTL) { + return memoryCache.data + } + + // Check file cache + if (!forceRefresh) { + const fileCache = await readCacheFile() + if (fileCache && Date.now() - fileCache.timestamp < CACHE_TTL) { + memoryCache = fileCache + return fileCache.data + } + } + + // Fetch from remote + log.info("Fetching plugin registry", { url: REGISTRY_URL }) + try { + const response = await fetch(REGISTRY_URL) + if (!response.ok) { + throw new Error(`Failed to fetch registry: ${response.status} ${response.statusText}`) + } + + const data = await response.json() + const registry = RegistrySchema.parse(data) + + const cached: CachedRegistry = { data: registry, timestamp: Date.now() } + memoryCache = cached + await writeCacheFile(cached) + + log.info("Registry fetched", { plugins: registry.plugins.length }) + return registry + } catch (error) { + // If fetch fails, try to use stale cache + const staleCache = memoryCache || (await readCacheFile()) + if (staleCache) { + log.warn("Using stale registry cache", { error }) + return staleCache.data + } + throw error + } +} + +export function searchPlugins(registry: Registry, query: string): RegistryPlugin[] { + const q = query.toLowerCase().trim() + if (!q) return registry.plugins + + return registry.plugins.filter( + (p) => + p.name.toLowerCase().includes(q) || + p.displayName.toLowerCase().includes(q) || + p.description.toLowerCase().includes(q) || + p.tags.some((t) => t.toLowerCase().includes(q)) || + p.category.toLowerCase().includes(q) + ) +} + +export function getPlugin(registry: Registry, name: string): RegistryPlugin | undefined { + return registry.plugins.find((p) => p.name === name || p.npm === name) +} + +export function filterByCategory(registry: Registry, category: string): RegistryPlugin[] { + return registry.plugins.filter((p) => p.category === category) +} + +export function filterByCapability(registry: Registry, capability: string): RegistryPlugin[] { + return registry.plugins.filter((p) => p.capabilities.includes(capability)) +} diff --git a/plugins/index.json b/plugins/index.json new file mode 100644 index 0000000000..390e1436b5 --- /dev/null +++ b/plugins/index.json @@ -0,0 +1,42 @@ +{ + "version": 1, + "plugins": [ + { + "name": "opencode-copilot-auth", + "displayName": "GitHub Copilot Auth", + "description": "Authenticate with GitHub Copilot for free model access", + "npm": "opencode-copilot-auth", + "version": "0.0.11", + "category": "auth", + "tags": ["auth", "github", "copilot", "free"], + "capabilities": ["auth"], + "author": "opencode-ai", + "homepage": "https://github.com/opencode-ai/opencode" + }, + { + "name": "opencode-anthropic-auth", + "displayName": "Anthropic OAuth", + "description": "OAuth authentication for Anthropic Claude models", + "npm": "opencode-anthropic-auth", + "version": "0.0.8", + "category": "auth", + "tags": ["auth", "anthropic", "oauth", "claude"], + "capabilities": ["auth"], + "author": "opencode-ai", + "homepage": "https://github.com/opencode-ai/opencode" + }, + { + "name": "opencode-google-auth", + "displayName": "Google Antigravity", + "description": "OAuth authentication for Google Gemini models via Antigravity proxy", + "npm": "opencode-google-auth", + "version": "0.0.5", + "category": "auth", + "tags": ["auth", "google", "gemini", "antigravity", "free"], + "capabilities": ["auth"], + "author": "opencode-ai", + "homepage": "https://github.com/opencode-ai/opencode" + } + ], + "categories": ["auth", "tools", "personas", "integrations"] +}