Building Plugins
Plugins are npm packages that add new AI tool targets to AgentsMesh at runtime. They export a TargetDescriptor object — the same interface that built-in targets use — and get the same capabilities: generation, import, linting, global mode, conversions, scoped settings, and hook post-processing.
This guide walks through building a plugin from scratch.
Project setup
Create a new npm package. The conventional name is agentsmesh-target-<id>:
mkdir agentsmesh-target-foo-idecd agentsmesh-target-foo-idenpm init -ySet "type": "module" and declare agentsmesh as a peer dependency in package.json:
{ "name": "agentsmesh-target-foo-ide", "version": "1.0.0", "type": "module", "main": "index.js", "exports": { ".": "./index.js" }, "peerDependencies": { "agentsmesh": ">=0.6" }}The TargetDescriptor
Every plugin exports a descriptor object that tells AgentsMesh how to generate, import, lint, and detect the target. Here is the full interface with all available fields:
Required fields
| Field | Type | Purpose |
|---|---|---|
id | string | Unique target ID (lowercase, hyphens only: ^[a-z][a-z0-9-]*$) |
generators | TargetGenerators | Feature generator functions |
capabilities | TargetCapabilities | Feature support levels for project scope |
emptyImportMessage | string | Shown when import --from finds nothing |
lintRules | function | null | Rule linter callback, or null to skip |
project | TargetLayout | Project-scope layout (paths, managed outputs) |
buildImportPaths | function | Import reference map builder |
detectionPaths | string[] | Paths used to detect this target during init |
Optional fields
| Field | Type | Purpose |
|---|---|---|
globalSupport | { capabilities, detectionPaths, layout, scopeExtras? } | Global-scope support as one cohesive contract |
supportsConversion | { commands?: true, agents?: true } | Enables user-configurable feature projection |
lint | TargetLintHooks | Per-feature lint hooks (commands, mcp, permissions, hooks, ignore) |
sharedArtifacts | Record<string, 'owner' | 'consumer'> | Declares ownership of shared output paths |
emitScopedSettings | function | Emit native settings sidecar files |
mergeGeneratedOutputContent | function | Merge generated content into an existing/pending output |
postProcessHookOutputs | function | Async post-pass for hook generator outputs |
Minimal plugin
The simplest working plugin generates rule files:
export const descriptor = { id: 'foo-ide', generators: { name: 'foo-ide', generateRules(canonical) { return canonical.rules.map((rule) => ({ path: `.foo-ide/${rule.slug}.md`, content: rule.body, })); }, async importFrom() { return []; }, }, capabilities: { rules: 'native', additionalRules: 'none', commands: 'none', agents: 'none', skills: 'none', mcp: 'none', hooks: 'none', ignore: 'none', permissions: 'none', }, emptyImportMessage: 'No Foo IDE config found.', lintRules: null, project: { paths: { rulePath: (slug) => `.foo-ide/${slug}.md`, commandPath: () => null, agentPath: () => null, }, }, buildImportPaths: async () => {}, detectionPaths: ['.foo-ide'],};Feature generators
The generators object can include any combination of feature generators. Each receives the parsed canonical files and returns an array of { path, content } outputs:
generators: { name: 'foo-ide',
// Required generateRules(canonical) { /* ... */ }, async importFrom(projectRoot, options) { /* ... */ },
// Optional feature generators generateCommands(canonical) { /* ... */ }, generateAgents(canonical) { /* ... */ }, generateSkills(canonical) { /* ... */ }, generateMcp(canonical) { /* ... */ }, generatePermissions(canonical) { /* ... */ }, generateHooks(canonical) { /* ... */ }, generateIgnore(canonical) { /* ... */ },
// Optional lint hook on generators (legacy) lint(canonical) { /* return LintDiagnostic[] */ },},For each feature you advertise as 'native', 'embedded', or 'partial', provide the corresponding generator so AgentsMesh can actually emit that feature. Settings-backed features (mcp, hooks, ignore, permissions) may instead be satisfied by emitScopedSettings.
Capability levels
| Level | Meaning |
|---|---|
'native' | The tool has first-class support for this feature |
'embedded' | AgentsMesh projects the feature with metadata for round-trip |
'partial' | Supported with limitations |
'none' | Not supported — generator is skipped |
Capabilities can also be objects with a level and optional flavor:
capabilities: { agents: { level: 'embedded', flavor: 'markdown' },},Global mode
To support agentsmesh generate --global, add globalSupport with capabilities, detection paths, and a global layout:
export const descriptor = { // ...project fields...
globalSupport: { capabilities: { rules: 'native', additionalRules: 'none', commands: 'embedded', agents: 'embedded', skills: 'native', mcp: 'native', hooks: 'none', ignore: 'native', permissions: 'none', }, detectionPaths: ['.foo-ide', '.foo-ide/ROOT.md'], layout: { rootInstructionPath: '.foo-ide/ROOT.md', skillDir: '.foo-ide/skills', paths: { rulePath: (slug) => `.foo-ide/rules/${slug}.md`, commandPath: (name) => `.foo-ide/commands/${name}.md`, agentPath: (name) => `.foo-ide/agents/${name}.md`, }, rewriteGeneratedPath(path) { return path; }, }, },};Output families
Declare output families for reference rewriting and root decoration:
project: { outputFamilies: [ { id: 'rules', kind: 'primary', pathPrefix: '.foo-ide/rules/' }, { id: 'commands', kind: 'additional', pathPrefix: '.foo-ide/commands/' }, { id: 'agents', kind: 'additional', pathPrefix: '.foo-ide/agents/' }, ], // ...},Managed outputs
Declare which directories and files are fully managed by AgentsMesh, so stale artifacts are cleaned up on generate:
project: { managedOutputs: { dirs: ['.foo-ide/rules', '.foo-ide/commands', '.foo-ide/agents'], files: ['.foo-ide/ROOT.md', '.foo-ide/mcp.json', '.fooignore'], }, // ...},Feature conversions
If your target lacks native support for commands or agents but can represent them as skills, declare supportsConversion:
export const descriptor = { // ... supportsConversion: { commands: true, agents: true }, capabilities: { commands: 'embedded', // projected as skills agents: 'embedded', // projected as skills },};Users can then configure this in agentsmesh.yaml. Values can be a boolean (both scopes) or a per-scope object:
conversions: commands_to_skills: foo-ide: true # both scopes (default when supportsConversion.commands is true) agents_to_skills: foo-ide: # per-scope control project: true global: falseWhen supportsConversion.commands is declared and the user has not set an explicit override, the conversion defaults to enabled. See the Conversions reference for the full syntax.
Per-feature lint hooks
Add target-specific validation for individual features:
export const descriptor = { // ... lint: { commands(canonical, options) { const issues = []; for (const cmd of canonical.commands) { if (!cmd.description) { issues.push({ level: 'warning', file: cmd.source || '', target: 'foo-ide', message: `Command '${cmd.name}' is missing a description`, }); } } return issues; }, mcp(canonical, options) { /* ... */ }, permissions(canonical, options) { /* ... */ }, hooks(canonical, options) { /* ... */ }, ignore(canonical, options) { /* ... */ }, },};The options parameter includes { scope: 'project' | 'global' } so hooks can differentiate between scopes.
Scoped settings
If your tool has a native settings file (like Gemini’s settings.json), emit it with emitScopedSettings:
export const descriptor = { // ... emitScopedSettings(canonical, scope) { const settings = { version: 1, featureCount: canonical.rules.length, }; const prefix = scope === 'global' ? '~/.foo-ide' : '.foo-ide'; return [{ path: `${prefix}/settings.json`, content: JSON.stringify(settings, null, 2) }]; },};Hook post-processing
If your tool needs to generate additional files alongside hooks (like wrapper scripts), use postProcessHookOutputs:
export const descriptor = { // ... async postProcessHookOutputs(projectRoot, canonical, outputs) { const processed = [...outputs]; for (const output of outputs) { if (output.path.endsWith('.json')) { processed.push({ path: output.path.replace('.json', '.wrapper.sh'), content: '#!/bin/sh\nexec "$@"\n', }); } } return processed; },};Scope extras
Generate additional global outputs beyond the standard feature loop with globalSupport.scopeExtras:
export const descriptor = { // ... globalSupport: { // ... async scopeExtras(canonical, projectRoot, scope, enabledFeatures) { const results = []; if (scope === 'global' && enabledFeatures.has('rules')) { results.push({ path: '.foo-ide/scope-info.txt', content: `scope=${scope}`, }); } return results; }, },};Shared artifacts
If your target generates files into a shared path (like .agents/skills/), declare ownership:
export const descriptor = { // ... sharedArtifacts: { '.foo-ide/skills/': 'owner', },};Use 'owner' if your target controls writes to that prefix, or 'consumer' if you only read from it.
Only one target may declare 'owner' for a given prefix. If two targets claim the same prefix — or one prefix is a path-prefix of another (e.g. .agents/ and .agents/skills/) — AgentsMesh fails fast at startup with an error naming both targets and both prefixes. Resolve by changing one role to 'consumer' or by namespacing the prefix.
Testing your plugin
Local development
During development, add the plugin from a local path:
agentsmesh plugin add ./path/to/agentsmesh-target-foo-ideagentsmesh generate --dry-run # preview without writingagentsmesh generate # generate for realValidation
AgentsMesh validates every plugin descriptor with a Zod schema at load time. Invalid descriptors are warned and skipped — one bad plugin does not block other targets.
Common validation errors:
idmust match^[a-z][a-z0-9-]*$generators.generateRulesandgenerators.importFromare required- Capability levels must be
'native','embedded','partial', or'none' - Non-
nonecapabilities must have their matching generator, oremitScopedSettingsfor settings-backed features (mcp,hooks,ignore,permissions)
Verify with matrix and lint
# Confirm your target appears in the matrixagentsmesh matrix
# Run lint to check all targets including pluginsagentsmesh lintPublishing
- Ensure
"type": "module"and"peerDependencies"are set inpackage.json - Export
descriptor(ordescriptorsarray, ordefault) from your main entry point - Publish to npm:
npm publishUsers install with:
agentsmesh plugin add agentsmesh-target-foo-ideagentsmesh generateComplete example
For a comprehensive example covering every TargetDescriptor field, see the rich-plugin test fixture in the AgentsMesh source. It exercises all 8 feature generators, per-feature lint hooks, project and global layouts, output families, shared artifacts, scope extras, scoped settings, hook post-processing, and conversion support.