diff --git a/src/channels/plugins/catalog.ts b/src/channels/plugins/catalog.ts index 0e8225481..66496aca7 100644 --- a/src/channels/plugins/catalog.ts +++ b/src/channels/plugins/catalog.ts @@ -2,7 +2,7 @@ import path from "node:path"; import { discoverClawdbotPlugins } from "../../plugins/discovery.js"; import type { PluginOrigin } from "../../plugins/types.js"; -import type { ClawdbotManifest } from "../../plugins/manifest.js"; +import type { ClawdbotPackageManifest } from "../../plugins/manifest.js"; import type { ChannelMeta } from "./types.js"; export type ChannelPluginCatalogEntry = { @@ -27,7 +27,7 @@ const ORIGIN_PRIORITY: Record = { }; function toChannelMeta(params: { - channel: NonNullable; + channel: NonNullable; id: string; }): ChannelMeta | null { const label = params.channel.label?.trim(); @@ -70,7 +70,7 @@ function toChannelMeta(params: { } function resolveInstallInfo(params: { - manifest: ClawdbotManifest; + manifest: ClawdbotPackageManifest; packageName?: string; packageDir?: string; workspaceDir?: string; @@ -93,7 +93,7 @@ function buildCatalogEntry(candidate: { packageName?: string; packageDir?: string; workspaceDir?: string; - packageClawdbot?: ClawdbotManifest; + packageClawdbot?: ClawdbotPackageManifest; }): ChannelPluginCatalogEntry | null { const manifest = candidate.packageClawdbot; if (!manifest?.channel) return null; diff --git a/src/plugins/discovery.ts b/src/plugins/discovery.ts index a71b1caa0..05cdbdf61 100644 --- a/src/plugins/discovery.ts +++ b/src/plugins/discovery.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { resolveConfigDir, resolveUserPath } from "../utils.js"; import { resolveBundledPluginsDir } from "./bundled-dir.js"; -import type { ClawdbotManifest, PackageManifest } from "./manifest.js"; +import type { ClawdbotPackageManifest, PackageManifest } from "./manifest.js"; import type { PluginDiagnostic, PluginOrigin } from "./types.js"; const EXTENSION_EXTS = new Set([".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"]); @@ -18,7 +18,7 @@ export type PluginCandidate = { packageVersion?: string; packageDescription?: string; packageDir?: string; - packageClawdbot?: ClawdbotManifest; + packageClawdbot?: ClawdbotPackageManifest; }; export type PluginDiscoveryResult = { diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index d4db46f6c..82bc719de 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -1,4 +1,97 @@ -export type PluginManifestChannel = { +import fs from "node:fs"; +import path from "node:path"; + +import type { PluginConfigUiHint, PluginKind } from "./types.js"; + +export const PLUGIN_MANIFEST_FILENAME = "clawdbot.plugin.json"; + +export type PluginManifest = { + id: string; + configSchema: Record; + kind?: PluginKind; + channels?: string[]; + providers?: string[]; + name?: string; + description?: string; + version?: string; + uiHints?: Record; +}; + +export type PluginManifestLoadResult = + | { ok: true; manifest: PluginManifest; manifestPath: string } + | { ok: false; error: string; manifestPath: string }; + +function normalizeStringList(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean); +} + +function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === "object" && !Array.isArray(value)); +} + +export function resolvePluginManifestPath(rootDir: string): string { + return path.join(rootDir, PLUGIN_MANIFEST_FILENAME); +} + +export function loadPluginManifest(rootDir: string): PluginManifestLoadResult { + const manifestPath = resolvePluginManifestPath(rootDir); + if (!fs.existsSync(manifestPath)) { + return { ok: false, error: `plugin manifest not found: ${manifestPath}`, manifestPath }; + } + let raw: unknown; + try { + raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as unknown; + } catch (err) { + return { + ok: false, + error: `failed to parse plugin manifest: ${String(err)}`, + manifestPath, + }; + } + if (!isRecord(raw)) { + return { ok: false, error: "plugin manifest must be an object", manifestPath }; + } + const id = typeof raw.id === "string" ? raw.id.trim() : ""; + if (!id) { + return { ok: false, error: "plugin manifest requires id", manifestPath }; + } + const configSchema = isRecord(raw.configSchema) ? raw.configSchema : null; + if (!configSchema) { + return { ok: false, error: "plugin manifest requires configSchema", manifestPath }; + } + + const kind = typeof raw.kind === "string" ? (raw.kind as PluginKind) : undefined; + const name = typeof raw.name === "string" ? raw.name.trim() : undefined; + const description = typeof raw.description === "string" ? raw.description.trim() : undefined; + const version = typeof raw.version === "string" ? raw.version.trim() : undefined; + const channels = normalizeStringList(raw.channels); + const providers = normalizeStringList(raw.providers); + + let uiHints: Record | undefined; + if (isRecord(raw.uiHints)) { + uiHints = raw.uiHints as Record; + } + + return { + ok: true, + manifest: { + id, + configSchema, + kind, + channels, + providers, + name, + description, + version, + uiHints, + }, + manifestPath, + }; +} + +// package.json "clawdbot" metadata (used for onboarding/catalog) +export type PluginPackageChannel = { id?: string; label?: string; selectionLabel?: string; @@ -16,21 +109,21 @@ export type PluginManifestChannel = { preferSessionLookupForAnnounceTarget?: boolean; }; -export type PluginManifestInstall = { +export type PluginPackageInstall = { npmSpec?: string; localPath?: string; defaultChoice?: "npm" | "local"; }; -export type ClawdbotManifest = { +export type ClawdbotPackageManifest = { extensions?: string[]; - channel?: PluginManifestChannel; - install?: PluginManifestInstall; + channel?: PluginPackageChannel; + install?: PluginPackageInstall; }; export type PackageManifest = { name?: string; version?: string; description?: string; - clawdbot?: ClawdbotManifest; + clawdbot?: ClawdbotPackageManifest; };