Skip to content

Building Plugins

A plugin is an npm package that exports a descriptor — the same object built-in targets use — telling AgentsMesh how to generate, import, lint, and detect config files for an AI tool. Plugins get full parity: generation, import, linting, global mode, conversions, and everything else.

The pipeline is straightforward:

.agentsmesh/rules/*.md descriptor.generateRules()
.agentsmesh/commands/*.md → descriptor.generateCommands() → .foo-ide/rules/*.md
.agentsmesh/mcp.json descriptor.generateMcp() .foo-ide/mcp.json
(canonical source) (your plugin) (generated output)

Your plugin sits in the middle — it receives parsed canonical files and returns { path, content } pairs that AgentsMesh writes to disk. The reverse direction (import) reads tool-native files and writes canonical files back to .agentsmesh/.

Your First Plugin

  1. Create the package

    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"
    }
    }
  2. Write the descriptor

    Create index.js with a single named export:

    index.js
    export const descriptor = {
    // ---- Identity ----
    id: 'foo-ide',
    // ---- User-facing metadata (required) ----
    // Drives display names in `agentsmesh matrix`, the support-matrix doc,
    // and any UI rendering that lists targets. Plugins missing `metadata`
    // are rejected at load time.
    metadata: {
    displayName: 'Foo IDE',
    category: 'ide', // 'cli' | 'ide' | 'agent-platform'
    officialUrl: 'https://example.test/foo-ide',
    shortDescription: 'Foo IDE — a fictional editor with native rule support.',
    },
    // ---- Generation ----
    generators: {
    name: 'foo-ide',
    generateRules(canonical) {
    return canonical.rules.map((rule) => ({
    path: `.foo-ide/rules/${rule.slug}.md`,
    content: rule.body,
    }));
    },
    async importFrom() { return []; },
    },
    // ---- Capabilities: what this tool supports ----
    capabilities: {
    rules: 'native',
    additionalRules: 'none', commands: 'none', agents: 'none',
    skills: 'none', mcp: 'none', hooks: 'none',
    ignore: 'none', permissions: 'none',
    },
    // ---- Layout: where outputs live on disk ----
    project: {
    managedOutputs: {
    dirs: ['.foo-ide/rules'],
    files: [],
    },
    paths: {
    rulePath: (slug) => `.foo-ide/rules/${slug}.md`,
    commandPath: () => null,
    agentPath: () => null,
    },
    },
    // ---- Plumbing ----
    emptyImportMessage: 'No Foo IDE config found.',
    lintRules: null,
    buildImportPaths: async () => {},
    detectionPaths: ['.foo-ide'],
    };

    Every non-none capability must have a matching generator. The metadata block must be present (all four subfields are required). AgentsMesh validates this at load time with a Zod schema and warns (or hard-errors under AGENTSMESH_STRICT_PLUGINS=1) if something is missing.

  3. Test locally

    Terminal window
    agentsmesh plugin add ./path/to/agentsmesh-target-foo-ide
    agentsmesh generate --dry-run # preview without writing
    agentsmesh generate # generate for real
    agentsmesh matrix # confirm it appears
  4. Publish to npm

    Terminal window
    npm publish

    Users install with:

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

You can also export a descriptors array (for multi-target packages) or a default export — AgentsMesh checks all three.

Adding More Features

The minimal plugin above only generates rules. Most AI tools support commands, agents, MCP servers, and more. Each feature follows the same pattern: declare a capability, add a generator, and register the output path.

Commands and Agents

Add generateCommands to generators, change capabilities.commands to 'native', and register the output path:

generators: {
// ...existing rules generator...
generateCommands(canonical) {
return canonical.commands.map((cmd) => ({
path: `.foo-ide/commands/${cmd.name}.md`,
content: `# ${cmd.name}\n\n${cmd.description}\n\n${cmd.body}`,
}));
},
},
capabilities: {
// ...
commands: 'native', // was 'none'
},
project: {
managedOutputs: {
dirs: ['.foo-ide/rules', '.foo-ide/commands'], // add the new dir
files: [],
},
paths: {
// ...
commandPath: (name) => `.foo-ide/commands/${name}.md`, // was () => null
},
},

Agents follow the same pattern with generateAgents, capabilities.agents, and agentPath.

Skills

Skills are directory-based — each skill is a folder with a SKILL.md file and optional supporting files:

generators: {
generateSkills(canonical) {
return canonical.skills.map((skill) => ({
path: `.foo-ide/skills/${skill.name}/SKILL.md`,
content: `# ${skill.name}\n\n${skill.body}`,
}));
},
},
capabilities: {
skills: 'native',
},
project: {
skillDir: '.foo-ide/skills',
managedOutputs: {
dirs: ['.foo-ide/skills'], // add skills dir
files: [],
},
},

MCP, Permissions, Hooks, and Ignore

These four features are “settings-backed” — they can be satisfied either by a dedicated generator or by emitScopedSettings if the tool uses a single settings file.

With a dedicated generator:

generators: {
generateMcp(canonical) {
if (!canonical.mcp) return [];
return [{
path: '.foo-ide/mcp.json',
content: JSON.stringify(canonical.mcp, null, 2),
}];
},
generateIgnore(canonical) {
if (canonical.ignore.length === 0) return [];
return [{
path: '.fooignore',
content: canonical.ignore.join('\n'),
}];
},
},
capabilities: {
mcp: 'native',
ignore: 'native',
},

If your tool reads all settings from a single file, see Scoped Settings in the Advanced section.

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 an optional flavor field:

capabilities: {
agents: { level: 'embedded', flavor: 'markdown' },
},

Adding Import Support

Import is the reverse pipeline — agentsmesh import --from foo-ide reads the tool’s native files and writes canonical files to .agentsmesh/.

Add an importer block to the descriptor. The shared runner walks each feature in canonical order, resolves scope-specific source paths, and dispatches to the right helper:

export const descriptor = {
// ...other fields...
importer: {
rules: {
feature: 'rules',
mode: 'directory',
source: { project: ['.foo-ide/rules'], global: ['.foo-ide/rules'] },
canonicalDir: '.agentsmesh/rules',
extensions: ['.md'],
preset: 'rule',
},
mcp: {
feature: 'mcp',
mode: 'mcpJson',
source: { project: ['.foo-ide/mcp.json'] },
canonicalDir: '.agentsmesh',
canonicalFilename: '.agentsmesh/mcp.json',
},
ignore: {
feature: 'ignore',
mode: 'flatFile',
source: { project: ['.fooignore'] },
canonicalDir: '.agentsmesh',
canonicalFilename: '.agentsmesh/ignore',
},
},
generators: {
// ...
async importFrom(projectRoot, options) {
const { runDescriptorImport } = await import('agentsmesh/targets');
return runDescriptorImport(descriptor, projectRoot, options?.scope ?? 'project');
},
},
};

Features whose source omits the active scope are automatically skipped — no if (scope === 'global') branches in your code.

Importer modes:

ModeUse caseRequired fields
singleFileRoot rule with fallback chaincanonicalRootFilename, optional markAsRoot
directoryRecursive scan of a directoryextensions, preset (rule/command/agent) or custom map
flatFileVerbatim copy (e.g. ignore file)canonicalFilename
mcpJsonParse mcpServers JSONcanonicalFilename

Project Layout

You have already seen project.paths and project.managedOutputs in the minimal plugin. Here are the other layout fields:

FieldPurpose
managedOutputsDirectories and files that AgentsMesh fully manages — stale artifacts are deleted on generate
rootInstructionPathPath to the primary root instruction file (e.g. .foo-ide/ROOT.md)
skillDirTarget-native skills directory for skill generation and import
outputFamiliesDeclared output path families for reference rewriting between targets

Output families enable AgentsMesh to rewrite internal file references from canonical paths to target-relative paths:

project: {
outputFamilies: [
{ id: 'rules', kind: 'primary', pathPrefix: '.foo-ide/rules/' },
{ id: 'commands', kind: 'additional', pathPrefix: '.foo-ide/commands/' },
],
},

Testing and 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-]*$
  • metadata.displayName, metadata.category, metadata.officialUrl, and metadata.shortDescription are all required; category must be one of 'cli' | 'ide' | 'agent-platform'
  • generators.generateRules and generators.importFrom are required
  • Non-none capabilities must have their matching generator (or emitScopedSettings for settings-backed features)

For CI release jobs that must not silently skip a broken plugin, set strict: true on the plugin entry or run with AGENTSMESH_STRICT_PLUGINS=1. See the plugin CLI reference for the full contract.

Terminal window
agentsmesh generate --dry-run # preview generated output
agentsmesh matrix # verify capabilities appear correctly
agentsmesh lint # run lint hooks across all targets

Publishing

  1. Ensure "type": "module" and "peerDependencies" are set in package.json
  2. Export descriptor (named), descriptors (array), or default from your entry point
  3. Name your package agentsmesh-target-<id> by convention
  4. Publish:
Terminal window
npm publish

Users install and generate:

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

Advanced Features

The features below are used by targets with complex requirements. Most plugins will not need them. Each section is self-contained.

Global Mode

To support agentsmesh generate --global, add globalSupport — a single object bundling global capabilities, detection paths, and layout:

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;
},
},
},

Feature Conversions

If your target lacks native support for commands or agents but can represent them as skills, declare supportsConversion:

supportsConversion: { commands: true, agents: true },
capabilities: {
commands: 'embedded',
agents: 'embedded',
},

When declared, conversion defaults to enabled. Users can override in agentsmesh.yaml:

conversions:
commands_to_skills:
foo-ide: true
agents_to_skills:
foo-ide:
project: true
global: false

See the Conversions reference for full syntax.

Per-Feature Lint Hooks

Add target-specific validation for individual features:

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;
},
},

The options parameter includes { scope: 'project' | 'global' }. Available hooks: commands, mcp, permissions, hooks, ignore.

Scoped Settings

If your tool reads all config from a native settings file, use emitScopedSettings instead of individual generators for mcp, hooks, ignore, and permissions:

emitScopedSettings(canonical, scope) {
const settings = {
version: 1,
featureCount: canonical.rules.length,
};
return [{ path: '.foo-ide/settings.json', content: JSON.stringify(settings, null, 2) }];
},

Hook Post-Processing

Generate additional files alongside hooks (like wrapper scripts) with postProcessHookOutputs:

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:

globalSupport: {
// ...capabilities, detectionPaths, layout...
async scopeExtras(canonical, projectRoot, scope, enabledFeatures) {
if (scope === 'global' && enabledFeatures.has('rules')) {
return [{ path: '.foo-ide/scope-info.txt', content: `scope=${scope}` }];
}
return [];
},
},

Shared Artifacts

If your target generates files into a path shared with other targets, declare ownership:

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 can be the owner of a given prefix — AgentsMesh fails fast at startup if two targets claim the same one.

Content Merging

If your target needs to merge generated content into an existing file rather than overwriting, use mergeGeneratedOutputContent:

mergeGeneratedOutputContent(existingContent, pendingContent, newContent, path) {
if (path.endsWith('settings.json')) {
const existing = JSON.parse(existingContent || '{}');
const update = JSON.parse(newContent);
return JSON.stringify({ ...existing, ...update }, null, 2);
}
return newContent;
},

Descriptor Reference

Required Fields

FieldTypePurpose
idstringUnique target ID (^[a-z][a-z0-9-]*$)
metadataTargetMetadataUser-facing metadata (displayName, category: 'cli' | 'ide' | 'agent-platform', officialUrl, shortDescription). Drives matrix display and target lists
generatorsTargetGeneratorsFeature generators (generateRules, importFrom required; others per capability)
capabilitiesTargetCapabilitiesSupport levels for all 9 features
emptyImportMessagestringShown when import --from finds nothing
lintRulesfunction | nullRule linter callback, or null to skip
projectTargetLayoutProject-scope layout (paths, managed outputs)
buildImportPathsfunctionPopulates import reference map
detectionPathsstring[]Paths for target detection during init

Optional Fields

FieldTypePurpose
globalSupportGlobalTargetSupportGlobal-scope support (capabilities, detection, layout, scopeExtras)
supportsConversion{ commands?, agents? }Enables user-configurable feature projection
conversionDefaults{ commandsToSkills?, agentsToSkills? }Built-in defaults for commands_to_skills / agents_to_skills projection. undefined falls back to caller-supplied default
excludeFromStarterInitbooleanWhen true, agentsmesh init skips this target in the default bulk starter scaffold; explicit --target <id> still works
preservesManualActivationbooleanWhen true, the target preserves manual-only activation semantics (e.g. Cursor’s alwaysApply: false); other targets get a lint warning on trigger: 'manual' rules
importerTargetImporterDescriptorDeclarative importer for scan + map orchestration
lintTargetLintHooksPer-feature lint hooks
sharedArtifactsRecord<string, 'owner' | 'consumer'>Shared output path ownership
emitScopedSettingsfunctionEmit native settings sidecar files
mergeGeneratedOutputContentfunctionCustom merge for generated outputs
postProcessHookOutputsfunctionAsync post-pass for hook outputs

What’s Next