Skip to content

zidane/plugins

A strict loader for Agent Plugins packages that bundle skills and MCP server config.

import { … } from 'zidane/plugins'src/plugins/index.ts

Use loadAgentPlugin to read a plugin directory with a plugin.json manifest, a skills/ folder and an optional mcp.json. It validates the manifest against the closed 1.0.0 schema and returns the parsed skills and MCP servers with diagnostics for anything it rejected. The lenient skill and MCP readers stay as they are, and this loader wraps them.

TypeScript
import { loadAgentPlugin } from 'zidane/plugins'

const plugin = await loadAgentPlugin('./my-plugin')
if (plugin.ok)
  console.log(plugin.manifest?.name, plugin.skills.length, plugin.mcpServers.length)
else
  console.error(plugin.diagnostics)

Functions#

agentPluginsSchemaIdFunction#

Canonical schema identifier for a given document kind + version. Built rather than hardcoded so the two call sites (manifest, mcp.json) can't drift, and so the version cross-check the spec requires between plugin.json and mcp.json is a plain string comparison.

TypeScript
function agentPluginsSchemaId(kind: PluginSchemaKind, version: string): string
Parameters
NameTypeDescription
kindPluginSchemaKind

Document kind, plugin or mcp.

versionstring

Agent Plugins spec version.

string

src/plugins/types.ts:62

defaultPluginDataRootFunction#

Default parent of every plugin's ${PLUGIN_DATA} directory.

Deliberately OUTSIDE any plugin root: the spec requires the data directory to persist across plugin updates, and an update replaces the package tree. Keyed by plugin name so two copies of the same plugin (project + user) share state, which is what an author storing a cache or a token expects.

Routed through resolveStorageDirs rather than hardcoding ~/.zidane so it honours ZIDANE_STORAGE_DIR and the XDG dirs like every other persisted location — otherwise an XDG-mode install would scatter plugin data into a ~/.zidane nothing else in the app touches.

NB: resolveStorageDirs does NOT read ZIDANE_PREFIX; it defaults prefix to .zidane. Callers with a configured prefix must pass it, or plugin data lands beside a prefix the rest of the install doesn't use.

TypeScript
function defaultPluginDataRoot(opts?: ResolveStorageDirsOptions): string
Parameters
NameTypeDefaultDescription
opts?ResolveStorageDirsOptions{}

Options forwarded to resolveStorageDirs, for example a custom storage prefix.

string

src/plugins/load.ts:44

expandPluginVarsFunction#

Substitute the two reserved placeholders in one string.

Single-pass by construction (String.replace never re-scans its own output), so a ${PLUGIN_ROOT} whose value somehow contained ${PLUGIN_DATA} cannot expand twice. Unknown ${...} text is left verbatim: the spec defines exactly two placeholders, and silently blanking anything else would corrupt legitimate literals.

TypeScript
function expandPluginVars(value: string, vars: Record<string, string>): string
Parameters
NameTypeDescription
valuestring

String that may contain ${PLUGIN_ROOT} or ${PLUGIN_DATA}.

varsRecord<string, string>

Placeholder values keyed by name, without ${}.

string

src/plugins/mcp.ts:450

isSupportedAgentPluginsVersionFunction#

True when this loader implements the given Agent Plugins version.

TypeScript
function isSupportedAgentPluginsVersion(version: string): version is '1.0.0'
Parameters
NameTypeDescription
versionstring

Version string to check.

version is '1.0.0'

src/plugins/types.ts:94

loadAgentPluginFunction#

Load an Agent Plugin from dir.

TypeScript
function loadAgentPlugin(
  dir: string,
  opts?: LoadAgentPluginOptions,
): Promise<LoadedPlugin>
Parameters
NameTypeDefaultDescription
dirstring

Path of the plugin root directory.

opts?LoadAgentPluginOptions{}

Loader options. See LoadAgentPluginOptions.

Promise<LoadedPlugin>

A LoadedPlugin. Check ok before trusting manifest; check components to tell "the plugin ships no skills" (absent) from "its skills were rejected" (invalid).

Example
import { loadAgentPlugin } from 'zidane/plugins'

const plugin = await loadAgentPlugin('./plugins/my-plugin', { namePrefix: 'my' })
if (!plugin.ok)
  throw new Error(plugin.diagnostics.map(d => d.message).join('\n'))
console.log(plugin.manifest?.name, plugin.skills.length, plugin.mcpServers.length)

src/plugins/load.ts:66

parsePluginManifestFunction#

Parse manifest text, then validate it.

A JSON syntax error is fatal — unlike mcps.json (where parseJsonTolerant repairs copy-paste whitespace artifacts), a plugin manifest is a package file produced by a build or an author's editor, not pasted into a settings box. Repairing it would mean guessing at the contents of a third-party package.

TypeScript
function parsePluginManifest(text: string): ManifestValidation
Parameters
NameTypeDescription
textstring

Raw plugin.json text.

src/plugins/manifest.ts:78

parseSchemaIdFunction#

Split a canonical $schema identifier into its document kind + version. Returns null for anything that isn't a canonical identifier; the caller decides whether that's fatal (manifest) or component-scoped (mcp.json).

Purely syntactic: it never fetches. The spec forbids retrieving a schema during load, so version support is a lookup against SUPPORTED_AGENT_PLUGINS_VERSIONS, not a download.

TypeScript
function parseSchemaId(
  value: string,
): { kind: PluginSchemaKind; version: string } | null
Parameters
NameTypeDescription
valuestring

Candidate $schema string.

{ kind: PluginSchemaKind; version: string } | null

src/plugins/types.ts:83

validatePluginManifestFunction#

Validate an already-parsed manifest value against the closed 1.0.0 schema.

Fatal violations short-circuit: $schema and name are checked first because without them there is no contract to validate against and no identity to report problems under.

TypeScript
function validatePluginManifest(raw: unknown): ManifestValidation
Parameters
NameTypeDescription
rawunknown

Parsed JSON value of plugin.json.

src/plugins/manifest.ts:104

Constants#

PLUGIN_MANIFEST_FILEConstant#

Fixed location of the plugin manifest.

TypeScript
const PLUGIN_MANIFEST_FILE = 'plugin.json'

src/plugins/load.ts:24

PLUGIN_MCP_FILEConstant#

Fixed location for the MCP component type.

TypeScript
const PLUGIN_MCP_FILE = 'mcp.json'

src/plugins/mcp.ts:41

PLUGIN_SKILLS_DIRConstant#

Fixed location for the skills component type.

TypeScript
const PLUGIN_SKILLS_DIR = 'skills'

src/plugins/skills.ts:27

SUPPORTED_AGENT_PLUGINS_VERSIONSConstant#

Agent Plugins spec versions this loader implements, newest first.

The spec requires clients to select validation rules from the manifest's $schema WITHOUT retrieving it, and to let plugins keep targeting older versions under a locally supported compatibility policy. Our policy: a version is supported iff it appears in this list. Adding 1.1.0 later means adding it here plus any rule deltas in ./manifest — never reinterpreting an already-published identifier, which the spec forbids.

TypeScript
const SUPPORTED_AGENT_PLUGINS_VERSIONS: readonly ['1.0.0']

src/plugins/types.ts:46

Interfaces#

LoadAgentPluginOptionsInterface#

Options for loadAgentPlugin.

TypeScript
interface LoadAgentPluginOptions
LoadAgentPluginOptions properties
NameTypeDescription
dataDir?string

Exact directory exposed to MCP servers as ${PLUGIN_DATA}.

Must live outside the plugin root: the spec requires this directory to persist across plugin updates, and an update replaces the package tree. Prefer LoadAgentPluginOptions.dataRoot unless you need to pin one specific plugin's directory.

dataRoot?string

Parent directory under which ${PLUGIN_DATA} is created as <dataRoot>/<plugin-name>/.

Exists because the plugin name comes from the manifest, which the caller hasn't read yet — a host that wants its own namespace (the extensions seam puts plugin data inside the loading extension's storage root) can supply the parent and let the loader append the name.

Defaults to <dataDir>/plugin-data from resolveStorageDirs. Ignored when LoadAgentPluginOptions.dataDir is set.

mcp?boolean

Discover mcp.json. Default true.

namePrefix?string

Prefix applied to discovered skill names as <prefix>-<name>.

Agent Plugins leaves naming to the client, and zidane's skill catalog is a single flat namespace (discoverSkills dedupes by name), so two plugins shipping a deploy skill would silently shadow one another. Opt in to prefixing when loading untrusted or multiple plugins.

skills?boolean

Discover skills/. Default true.

source?SkillSource

Source tag stamped onto every discovered skill, driving collision precedence and the trustProjectSkills gate downstream. Defaults to 'project' — the conservative choice, since a plugin sitting in a repo is exactly the untrusted-input case that gate exists for.

src/plugins/types.ts:254

LoadedPluginInterface#

Result of loadAgentPlugin. Never thrown — a rejected plugin comes back as ok: false with the reason in diagnostics, because one bad plugin must not take down a host that's loading several.

TypeScript
interface LoadedPlugin
LoadedPlugin properties
NameTypeDescription
components{ mcp: PluginComponentStatus; skills: PluginComponentStatus }

Per-component-type outcome — see PluginComponentStatus.

diagnosticsPluginDiagnostic[]

Everything worth reporting: rejections, disabled components, skipped skills.

dirstring

Plugin root as requested, resolved to an absolute path.

manifest?PluginManifest

Validated manifest. Absent iff ok === false.

mcpServersMcpServerConfig[]

MCP servers declared by mcp.json.

okboolean

false when a fatal manifest or boundary violation rejected the whole plugin. Components are empty in that case and were never discovered — the spec requires rejection to happen BEFORE component discovery.

rootstring

Symlink-resolved plugin root. Every package path a caller touches must stay under this — it's the boundary the spec defines. Falls back to dir when the root itself could not be resolved.

skillsSkillConfig[]

Skills discovered under skills/.

src/plugins/types.ts:219

PluginAuthorInterface#

Optional author block. Closed: only these three keys, all strings.

TypeScript
interface PluginAuthor
PluginAuthor properties
NameTypeDescription
email?string

Author email address.

name?string

Author name.

url?string

Author URL.

src/plugins/types.ts:121

PluginDiagnosticInterface#

A load-time finding. Mirrors SkillDiagnostic's shape (severity + stable code + message) so host UIs already rendering skill diagnostics can render these with no new plumbing.

severity: 'error' on a returned plugin means a narrow boundary fired (a component type disabled, a skill skipped) — NOT that the whole plugin failed. Plugin-level rejection is signalled by LoadedPlugin.ok === false.

TypeScript
interface PluginDiagnostic
PluginDiagnostic properties
NameTypeDescription
codestring

Stable machine-readable code (e.g. manifest-unknown-field).

field?string

Manifest field the finding relates to, when applicable.

messagestring

Human-readable description of the finding.

path?string

Filesystem path the finding relates to, when applicable.

severity'error' | 'warning'

error disables a component or skips an item. warning is informational.

src/plugins/types.ts:179

PluginManifestInterface#

A validated plugin.json. Only spec fields survive validation — unknown top-level keys are reported and dropped, so consumers can never accidentally depend on a non-portable field.

version here is the PLUGIN's own version string; the spec version selected by $schema lands on PluginManifest.specVersion.

TypeScript
interface PluginManifest
PluginManifest properties
NameTypeDescription
author?PluginAuthor

Plugin author.

description?string

Short description of the plugin.

extensions?Record<string, unknown>

Client-owned data keyed by reverse-domain namespace. Values are deliberately unknown: the spec says to ignore namespaces the client does not implement WITHOUT validating their contents, so this loader never inspects a member it doesn't own.

homepage?string

URL of the plugin homepage.

keywords?string[]

Search keywords.

license?string

License identifier or name.

namestring

Plugin name + package identifier (1-64 chars, see ./manifest).

repository?string

URL of the source repository.

specVersion'1.0.0'

Agent Plugins version selected by the manifest's $schema.

version?string

Version of the plugin itself.

src/plugins/types.ts:138

Types#

AgentPluginsVersionType#

An Agent Plugins spec version this loader supports.

TypeScript
type AgentPluginsVersion = typeof SUPPORTED_AGENT_PLUGINS_VERSIONS[number]

src/plugins/types.ts:49

ManifestValidationType#

Result of manifest validation: the manifest when ok, plus diagnostics either way.

TypeScript
type ManifestValidation =
  | { diagnostics: PluginDiagnostic[]; manifest: PluginManifest; ok: true }
  | { diagnostics: PluginDiagnostic[]; ok: false }

src/plugins/manifest.ts:64

PluginComponentStatusType#

Per-component-type outcome. The spec insists a missing fixed location is NOT an error, and that an invalid one disables only its own component type — so these four states are load-bearing, not cosmetic:

  • absent fixed location not present. Valid.
  • loaded discovered (possibly zero entries, e.g. an empty skills/).
  • invalid wrong filesystem kind, or the location escaped the root.
  • unsupported never discovered — not implemented by this client, disabled by the caller, or the plugin was rejected before component discovery ran.
TypeScript
type PluginComponentStatus = 'absent' | 'loaded' | 'invalid' | 'unsupported'

src/plugins/types.ts:208

PluginSchemaKindType#

Which canonical schema a $schema identifier selects.

TypeScript
type PluginSchemaKind = 'plugin' | 'mcp'

src/plugins/types.ts:52