# zidane/plugins

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

Import: `import { … } from 'zidane/plugins'`. Source: [src/plugins/index.ts](https://github.com/Tahul/zidane/blob/main/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.

```ts
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

### `agentPluginsSchemaId`

Function.

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.

```ts
function agentPluginsSchemaId(kind: PluginSchemaKind, version: string): string
```

**Parameters**

- `kind` (`PluginSchemaKind`): Document kind, `plugin` or `mcp`.
- `version` (`string`): Agent Plugins spec version.

**Returns** `string`

Source: [src/plugins/types.ts:62](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L62)

### `defaultPluginDataRoot`

Function.

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.

```ts
function defaultPluginDataRoot(opts?: ResolveStorageDirsOptions): string
```

**Parameters**

- `opts?` (`ResolveStorageDirsOptions`, default `{}`): Options forwarded to `resolveStorageDirs`, for example a custom storage `prefix`.

**Returns** `string`

Source: [src/plugins/load.ts:44](https://github.com/Tahul/zidane/blob/main/src/plugins/load.ts#L44)

### `expandPluginVars`

Function.

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.

```ts
function expandPluginVars(value: string, vars: Record<string, string>): string
```

**Parameters**

- `value` (`string`): String that may contain `${PLUGIN_ROOT}` or `${PLUGIN_DATA}`.
- `vars` (`Record<string, string>`): Placeholder values keyed by name, without `${}`.

**Returns** `string`

Source: [src/plugins/mcp.ts:450](https://github.com/Tahul/zidane/blob/main/src/plugins/mcp.ts#L450)

### `isSupportedAgentPluginsVersion`

Function.

True when this loader implements the given Agent Plugins version.

```ts
function isSupportedAgentPluginsVersion(version: string): version is '1.0.0'
```

**Parameters**

- `version` (`string`): Version string to check.

**Returns** `version is '1.0.0'`

Source: [src/plugins/types.ts:94](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L94)

### `loadAgentPlugin`

Function.

Load an Agent Plugin from `dir`.

```ts
function loadAgentPlugin(
  dir: string,
  opts?: LoadAgentPluginOptions,
): Promise<LoadedPlugin>
```

**Parameters**

- `dir` (`string`): Path of the plugin root directory.
- `opts?` (`LoadAgentPluginOptions`, default `{}`): Loader options. See [`LoadAgentPluginOptions`](https://zidane.si/docs/reference/plugins.md#LoadAgentPluginOptions).

**Returns** `Promise<LoadedPlugin>`: A [`LoadedPlugin`](https://zidane.si/docs/reference/plugins.md#LoadedPlugin). Check `ok` before trusting `manifest`;
check `components` to tell "the plugin ships no skills" (`absent`) from
"its skills were rejected" (`invalid`).

**Example**

```ts
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)
```

Source: [src/plugins/load.ts:66](https://github.com/Tahul/zidane/blob/main/src/plugins/load.ts#L66)

### `parsePluginManifest`

Function.

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.

```ts
function parsePluginManifest(text: string): ManifestValidation
```

**Parameters**

- `text` (`string`): Raw `plugin.json` text.

**Returns** `ManifestValidation`

Source: [src/plugins/manifest.ts:78](https://github.com/Tahul/zidane/blob/main/src/plugins/manifest.ts#L78)

### `parseSchemaId`

Function.

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`](https://zidane.si/docs/reference/plugins.md#SUPPORTED_AGENT_PLUGINS_VERSIONS), not a download.

```ts
function parseSchemaId(
  value: string,
): { kind: PluginSchemaKind; version: string } | null
```

**Parameters**

- `value` (`string`): Candidate `$schema` string.

**Returns** `{ kind: PluginSchemaKind; version: string } | null`

Source: [src/plugins/types.ts:83](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L83)

### `validatePluginManifest`

Function.

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.

```ts
function validatePluginManifest(raw: unknown): ManifestValidation
```

**Parameters**

- `raw` (`unknown`): Parsed JSON value of `plugin.json`.

**Returns** `ManifestValidation`

Source: [src/plugins/manifest.ts:104](https://github.com/Tahul/zidane/blob/main/src/plugins/manifest.ts#L104)

## Constants

### `PLUGIN_MANIFEST_FILE`

Constant.

Fixed location of the plugin manifest.

```ts
const PLUGIN_MANIFEST_FILE = 'plugin.json'
```

Source: [src/plugins/load.ts:24](https://github.com/Tahul/zidane/blob/main/src/plugins/load.ts#L24)

### `PLUGIN_MCP_FILE`

Constant.

Fixed location for the MCP component type.

```ts
const PLUGIN_MCP_FILE = 'mcp.json'
```

Source: [src/plugins/mcp.ts:41](https://github.com/Tahul/zidane/blob/main/src/plugins/mcp.ts#L41)

### `PLUGIN_SKILLS_DIR`

Constant.

Fixed location for the skills component type.

```ts
const PLUGIN_SKILLS_DIR = 'skills'
```

Source: [src/plugins/skills.ts:27](https://github.com/Tahul/zidane/blob/main/src/plugins/skills.ts#L27)

### `SUPPORTED_AGENT_PLUGINS_VERSIONS`

Constant.

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.

```ts
const SUPPORTED_AGENT_PLUGINS_VERSIONS: readonly ['1.0.0']
```

Source: [src/plugins/types.ts:46](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L46)

## Interfaces

### `LoadAgentPluginOptions`

Interface.

Options for [`loadAgentPlugin`](https://zidane.si/docs/reference/plugins.md#loadAgentPlugin).

```ts
interface LoadAgentPluginOptions
```

**Properties**

- `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.

Source: [src/plugins/types.ts:254](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L254)

### `LoadedPlugin`

Interface.

Result of [`loadAgentPlugin`](https://zidane.si/docs/reference/plugins.md#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.

```ts
interface LoadedPlugin
```

**Properties**

- `components` (`{ mcp: PluginComponentStatus; skills: PluginComponentStatus }`): Per-component-type outcome — see [`PluginComponentStatus`](https://zidane.si/docs/reference/plugins.md#PluginComponentStatus).
- `diagnostics` (`PluginDiagnostic[]`): Everything worth reporting: rejections, disabled components, skipped skills.
- `dir` (`string`): Plugin root as requested, resolved to an absolute path.
- `manifest?` (`PluginManifest`): Validated manifest. Absent iff `ok === false`.
- `mcpServers` (`McpServerConfig[]`): MCP servers declared by `mcp.json`.
- `ok` (`boolean`): `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.
- `root` (`string`): 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.
- `skills` (`SkillConfig[]`): Skills discovered under `skills/`.

Source: [src/plugins/types.ts:219](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L219)

### `PluginAuthor`

Interface.

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

```ts
interface PluginAuthor
```

**Properties**

- `email?` (`string`): Author email address.
- `name?` (`string`): Author name.
- `url?` (`string`): Author URL.

Source: [src/plugins/types.ts:121](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L121)

### `PluginDiagnostic`

Interface.

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`.

```ts
interface PluginDiagnostic
```

**Properties**

- `code` (`string`): Stable machine-readable code (e.g. `manifest-unknown-field`).
- `field?` (`string`): Manifest field the finding relates to, when applicable.
- `message` (`string`): 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.

Source: [src/plugins/types.ts:179](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L179)

### `PluginManifest`

Interface.

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`.

```ts
interface PluginManifest
```

**Properties**

- `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.
- `name` (`string`): 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.

Source: [src/plugins/types.ts:138](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L138)

## Types

### `AgentPluginsVersion`

Type.

An Agent Plugins spec version this loader supports.

```ts
type AgentPluginsVersion = typeof SUPPORTED_AGENT_PLUGINS_VERSIONS[number]
```

Source: [src/plugins/types.ts:49](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L49)

### `ManifestValidation`

Type.

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

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

Source: [src/plugins/manifest.ts:64](https://github.com/Tahul/zidane/blob/main/src/plugins/manifest.ts#L64)

### `PluginComponentStatus`

Type.

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.

```ts
type PluginComponentStatus = 'absent' | 'loaded' | 'invalid' | 'unsupported'
```

Source: [src/plugins/types.ts:208](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L208)

### `PluginSchemaKind`

Type.

Which canonical schema a `$schema` identifier selects.

```ts
type PluginSchemaKind = 'plugin' | 'mcp'
```

Source: [src/plugins/types.ts:52](https://github.com/Tahul/zidane/blob/main/src/plugins/types.ts#L52)
