Plugin Development
a16n uses a plugin architecture to support different AI coding tools. Each plugin handles:
- Discovery - Finding agent customization files in a project
- Emission - Writing converted files to the target format
Plugin Interface
Plugins implement the A16nPlugin interface from @a16njs/models; see the Models API Reference for complete interface documentation.
The key methods are:
discover(rootOrWorkspace)- Scan a directory and return found customizationsemit(models, rootOrWorkspace, options)- Write customizations to disk in the plugin's format
Both methods accept string | Workspace — a plain directory path string or a Workspace instance. When a string is passed, plugins should wrap it using toWorkspace() from @a16njs/models.
import type { A16nPlugin, Workspace } from '@a16njs/models';
import { CustomizationType, toWorkspace } from '@a16njs/models';
const myPlugin: A16nPlugin = {
id: 'my-agent',
name: 'My Agent',
supports: [CustomizationType.GlobalPrompt, CustomizationType.FileRule],
async discover(rootOrWorkspace: string | Workspace) {
const ws = toWorkspace(rootOrWorkspace, 'my-agent-discover');
// Return { items: [...], warnings: [...] }
},
async emit(models, rootOrWorkspace, options) {
const ws = toWorkspace(rootOrWorkspace, 'my-agent-emit');
// Return { written: [...], warnings: [...], unsupported: [...] }
}
};
export default myPlugin;
Key Concepts
Customization Types
Plugins declare which types they support via the supports array. Entries are instances of CustomizationType from @a16njs/models.
Workspace Interface
The Workspace interface abstracts filesystem operations so plugins can work with real directories or in-memory workspaces (useful for testing). All paths passed to workspace methods are relative to the workspace root.
import type { Workspace } from '@a16njs/models';
// Available methods:
await ws.exists('path/to/file'); // boolean
await ws.read('path/to/file'); // string (UTF-8)
await ws.write('path/to/file', content); // void
await ws.readdir('path/to/dir'); // WorkspaceEntry[]
await ws.mkdir('path/to/dir'); // void (always recursive)
ws.resolve('path/to/file'); // absolute path string
WorkspaceEntry items returned by readdir() have boolean properties isFile and isDirectory (not methods).
Use toWorkspace() to normalize the string | Workspace parameter at the top of your functions:
import { toWorkspace } from '@a16njs/models';
const ws = toWorkspace(rootOrWorkspace, 'my-plugin-discover');
// ws is always a Workspace — strings are auto-wrapped in LocalWorkspace
Discovery
Discovery scans a project directory and returns customizations in the intermediate representation:
async discover(rootOrWorkspace: string | Workspace): Promise<DiscoveryResult> {
const ws = toWorkspace(rootOrWorkspace, 'my-plugin-discover');
const items: AgentCustomization[] = [];
const warnings: Warning[] = [];
// Use ws.readdir(), ws.read(), ws.exists() to scan for config files
// Parse and convert to AgentCustomization items
return { items, warnings };
}
Emission
Emission converts intermediate representation back to the plugin's native format:
async emit(
items: AgentCustomization[],
rootOrWorkspace: string | Workspace,
options?: EmitOptions
): Promise<EmitResult> {
const ws = toWorkspace(rootOrWorkspace, 'my-plugin-emit');
const written: WrittenFile[] = [];
const warnings: Warning[] = [];
const unsupported: AgentCustomization[] = [];
for (const item of items) {
if (this.supports.includes(item.type)) {
// Use ws.mkdir(), ws.write() to write files
// Use ws.resolve(relPath) for written[].path
} else {
unsupported.push(item);
}
}
return { written, warnings, unsupported };
}
Project Structure
Recommended plugin structure:
a16n-plugin-example/
├── src/
│ ├── index.ts # Plugin entry point & default export
│ ├── discover.ts # Discovery logic
│ └── emit.ts # Emission logic
├── test/
│ ├── fixtures/ # Test fixtures
│ ├── discover.test.ts
│ └── emit.test.ts
├── package.json
├── tsconfig.json
└── README.md
package.json Requirements
Your package.json must:
- Use the
a16n-plugin-prefix in the package name (required for auto-discovery) - Set
mainto point to your built entry file - Declare
@a16njs/modelsas apeerDependency
{
"name": "a16n-plugin-example",
"type": "module",
"main": "./dist/index.js",
"peerDependencies": {
"@a16njs/models": "^0.9.0"
},
"devDependencies": {
"@a16njs/models": "^0.9.0"
}
}
Learning from Existing Plugins
The best way to understand plugin development is to study the existing implementations:
The Cursor plugin demonstrates:
- MDC file parsing (YAML frontmatter + markdown body)
- Multiple file types (rules, commands, ignore files)
- Frontmatter-based type classification
The Claude plugin demonstrates:
- Settings JSON handling
The cursorrules plugin demonstrates:
- Community plugin naming format (
a16n-plugin-*) - Discovery-only support (no emission)
Publishing
Community plugins should be published to npm with the a16n-plugin- prefix:
a16n-plugin-<name>
a16n automatically discovers installed packages matching the a16n-plugin-* naming convention by scanning node_modules directories. No registration or configuration is needed - just npm install the plugin alongside a16n.
The @a16njs/plugin-* scoped packages are reserved for bundled plugins maintained in the a16n monorepo. Community plugins should use the unscoped a16n-plugin-* convention.
See Also
- Models API Reference - Plugin interface documentation
- Plugins:
- Plugin: Cursor - Cursor implementation details
- Plugin: Claude - Claude implementation details
- Plugin: a16n - a16n implementation details
- Community plugins following the
a16n-plugin-*naming convention can be built using this guide
- Understanding Conversions - Translation can be hard!
- GitHub Repository - Source code