Skip to content

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>:

Terminal window
mkdir agentsmesh-target-foo-ide
cd agentsmesh-target-foo-ide
npm init -y

Set "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

FieldTypePurpose
idstringUnique target ID (lowercase, hyphens only: ^[a-z][a-z0-9-]*$)
generatorsTargetGeneratorsFeature generator functions
capabilitiesTargetCapabilitiesFeature support levels for project scope
emptyImportMessagestringShown when import --from finds nothing
lintRulesfunction | nullRule linter callback, or null to skip
projectTargetLayoutProject-scope layout (paths, managed outputs)
buildImportPathsfunctionImport reference map builder
detectionPathsstring[]Paths used to detect this target during init

Optional fields

FieldTypePurpose
globalSupport{ capabilities, detectionPaths, layout, scopeExtras? }Global-scope support as one cohesive contract
supportsConversion{ commands?: true, agents?: true }Enables user-configurable feature projection
lintTargetLintHooksPer-feature lint hooks (commands, mcp, permissions, hooks, ignore)
sharedArtifactsRecord<string, 'owner' | 'consumer'>Declares ownership of shared output paths
emitScopedSettingsfunctionEmit native settings sidecar files
mergeGeneratedOutputContentfunctionMerge generated content into an existing/pending output
postProcessHookOutputsfunctionAsync post-pass for hook generator outputs

Minimal plugin

The simplest working plugin generates rule files:

index.js
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

LevelMeaning
'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: false

When 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:

Terminal window
agentsmesh plugin add ./path/to/agentsmesh-target-foo-ide
agentsmesh generate --dry-run # preview without writing
agentsmesh generate # generate for real

Validation

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:

  • id must match ^[a-z][a-z0-9-]*$
  • generators.generateRules and generators.importFrom are required
  • Capability levels must be 'native', 'embedded', 'partial', or 'none'
  • Non-none capabilities must have their matching generator, or emitScopedSettings for settings-backed features (mcp, hooks, ignore, permissions)

Verify with matrix and lint

Terminal window
# Confirm your target appears in the matrix
agentsmesh matrix
# Run lint to check all targets including plugins
agentsmesh lint

Publishing

  1. Ensure "type": "module" and "peerDependencies" are set in package.json
  2. Export descriptor (or descriptors array, or default) from your main entry point
  3. Publish to npm:
Terminal window
npm publish

Users install with:

Terminal window
agentsmesh plugin add agentsmesh-target-foo-ide
agentsmesh generate

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