How to Add Support for a New AI Provider to the Impeccable Transformer Architecture
To add a new AI provider to Impeccable, create a transformer module in scripts/lib/transformers/, export it in scripts/lib/transformers/index.js, and register the provider in the providerMappings array within scripts/build.js.
Impeccable (available at pbakaus/impeccable) converts universal skill markdown files in source/skills/ into provider-specific bundles for AI coding assistants like Cursor, Claude Code, Gemini, and Codex. The transformer architecture allows you to extend this support to any new AI provider by implementing a standardized transformation interface that handles frontmatter conversion, placeholder substitution, and file output.
Create the Transformer File
Start by adding a new JavaScript file at scripts/lib/transformers/<new-provider>.js. This module must export a transform<NewProvider> function that follows the signature used by existing transformers such as transformCursor in scripts/lib/transformers/cursor.js or transformClaudeCode in scripts/lib/transformers/claude-code.js.
// scripts/lib/transformers/<new-provider>.js
import path from 'path';
import {
cleanDir,
ensureDir,
writeFile,
generateYamlFrontmatter,
replacePlaceholders,
prefixSkillReferences,
} from '../utils.js';
/**
* <NewProvider> Transformer (Skills Only)
*
* – All skills output to .<new-provider>/skills/{name}/SKILL.md
* – Frontmatter fields can be customised per provider
* – Handles user‑invokable skills, optional prefixing, and reference files
*
* @param {Array} skills – Array of skill objects from readSourceFiles()
* @param {string} distDir – Root distribution directory (e.g. “dist”)
* @param {Object} patterns – Design‑pattern data (unused, kept for interface consistency)
* @param {Object} options – { prefix: string, outputSuffix: string }
*/
export function transform<NewProvider>(skills, distDir, patterns = null, options = {}) {
const { prefix = '', outputSuffix = '' } = options;
const providerDir = path.join(distDir, `<new-provider>${outputSuffix}`);
const skillsDir = path.join(providerDir, `.<new-provider>/skills`);
// Fresh start
cleanDir(providerDir);
ensureDir(skillsDir);
const allSkillNames = skills.map(s => s.name);
const commandNames = skills.filter(s => s.userInvokable).map(s => `${prefix}${s.name}`);
let refCount = 0;
for (const skill of skills) {
const skillName = `${prefix}${skill.name}`;
const skillDir = path.join(skillsDir, skillName);
// ---------- Frontmatter ----------
const frontmatterObj = {
name: skillName,
description: skill.description,
// Add any provider‑specific keys here, e.g. version, tags, etc.
};
if (skill.license) frontmatterObj.license = skill.license;
if (skill.allowedTools) frontmatterObj['allowed-tools'] = skill.allowedTools;
const frontmatter = generateYamlFrontmatter(frontmatterObj);
// ---------- Body ----------
let skillBody = replacePlaceholders(skill.body, '<new-provider>', commandNames);
if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames);
// Provider‑specific body tweaks go here (e.g. placeholder conversion)
const content = `${frontmatter}\n\n${skillBody}`;
const outPath = path.join(skillDir, 'SKILL.md');
writeFile(outPath, content);
// ---------- Reference files ----------
if (skill.references && skill.references.length > 0) {
const refDir = path.join(skillDir, 'reference');
ensureDir(refDir);
for (const ref of skill.references) {
const refOutPath = path.join(refDir, `${ref.name}.md`);
const refContent = replacePlaceholders(ref.content, '<new-provider>');
writeFile(refOutPath, refContent);
refCount++;
}
}
}
const userInvokableCount = skills.filter(s => s.userInvokable).length;
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
console.log(`✓ <NewProvider>${prefixInfo}: ${skills.length} skills (${userInvokableCount} user‑invokable)${refInfo}`);
}
Key Implementation Details
- Output paths: Use the dot-folder convention
.<new-provider>/skillsto match the structure used by Cursor (.cursor), Claude Code (.claude), and other providers. - Frontmatter customization: Adapt the
frontmatterObjto include provider-specific metadata fields. ThegenerateYamlFrontmatterutility inscripts/lib/utils.jshandles YAML serialization. - Placeholder handling: Call
replacePlaceholdersto convert generic{{arg}}syntax into the provider's expected format (for example,$ARGfor Codex or{{args}}for Gemini). - Reference copying: The
referencesarray contains additional markdown files that must travel with the skill; write these to areference/subdirectory within each skill folder.
Export the New Transformer
Make the transformer available to the build system by adding a named export to scripts/lib/transformers/index.js:
// scripts/lib/transformers/index.js
export { transformCursor } from './cursor.js';
export { transformClaudeCode } from './claude-code.js';
export { transformGemini } from './gemini.js';
export { transformCodex } from './codex.js';
export { transformAgents } from './agents.js';
export { transformKiro } from './kiro.js';
export { transform<NewProvider> } from './<new-provider>.js';
This central export hub allows scripts/build.js to import your transformer by name using dynamic or static imports.
Wire the Provider into the Build Pipeline
Register the new provider in the universal bundle assembly by editing scripts/build.js. Locate the assembleUniversal() function (around line 42) and add your provider to the providerMappings array:
// scripts/build.js – inside assembleUniversal()
const providerMappings = [
{ provider: 'cursor', configDir: '.cursor' },
{ provider: 'claude-code', configDir: '.claude' },
{ provider: 'gemini', configDir: '.gemini' },
{ provider: 'codex', configDir: '.codex' },
{ provider: 'agents', configDir: '.agents' },
{ provider: 'kiro', configDir: '.kiro' },
// 👇 new entry
{ provider: '<new-provider>', configDir: '.<new-provider>' },
];
The build orchestrator (scripts/build.js) performs the following steps automatically:
- Read source files:
readSourceFiles()parses eachsource/skills/**/SKILL.md. - Transform per provider: Each
transform<Provider>()writes a provider-specific directory underdist/. - Assemble universal bundle:
assembleUniversal()copies every provider's.provider/folder intodist/universal/. - Create ZIPs:
createAllZips()inscripts/lib/zip.jspackages each provider bundle for distribution.
(Optional) Add Tests
Verify your implementation by creating a Jest test suite under tests/lib/transformers/<new-provider>.test.js. Use existing tests such as cursor.test.js as a template:
// tests/lib/transformers/<new-provider>.test.js
import { transform<NewProvider> } from '../../../scripts/lib/transformers/<new-provider>.js';
import { readSourceFiles } from '../../../scripts/lib/utils.js';
import path from 'path';
import fs from 'fs';
test('<NewProvider> transformer produces expected files', async () => {
const root = path.resolve(__dirname, '../../../'); // project root
const { skills } = await readSourceFiles(root);
const dist = path.join(root, 'dist-test');
transform<NewProvider>(skills, dist);
// Example assertion – ensure a known skill got written
const skillPath = path.join(dist, '<new-provider>', '.<new-provider>', 'skills', 'teach-impeccable', 'SKILL.md');
expect(fs.existsSync(skillPath)).toBe(true);
});
Run bun test to execute the test suite and confirm that your transformer correctly processes skills, handles references, and writes files to the expected directory structure.
Summary
- Create a transformer module at
scripts/lib/transformers/<new-provider>.jsusing thetransform<NewProvider>function signature and utility helpers fromscripts/lib/utils.js. - Export the transformer function in
scripts/lib/transformers/index.jsto make it available to the build system. - Register the provider in the
providerMappingsarray withinscripts/build.jsto include it in universal bundles. - Test your implementation by copying an existing test suite in
tests/lib/transformers/and adjusting the assertions for your provider's output format.
Frequently Asked Questions
What is the minimum code required to add a new AI provider to Impeccable?
You need three changes: a transformer file in scripts/lib/transformers/<new-provider>.js that exports a transform<NewProvider> function, an export statement in scripts/lib/transformers/index.js, and an entry in the providerMappings array in scripts/build.js. The transformer can reuse utilities like cleanDir, writeFile, and replacePlaceholders from scripts/lib/utils.js to minimize boilerplate.
How does Impeccable handle placeholder substitution for different providers?
The replacePlaceholders utility in scripts/lib/utils.js swaps generic {{arg}} syntax with provider-specific formats. For example, the Codex transformer converts placeholders to $ARG variables, while the Gemini transformer uses {{args}}. Your transformer should call replacePlaceholders(skill.body, '<new-provider>', commandNames) with the appropriate target syntax for your AI provider.
Where does the build output go for a new provider?
The transform<NewProvider> function receives a distDir parameter (typically dist). It should create a subdirectory at dist/<new-provider>/<new-provider>/skills/ containing the transformed SKILL.md files and any reference materials. The assembleUniversal() function in scripts/build.js then copies these files into dist/universal/<new-provider>/skills/ for the combined bundle.
Can I add custom frontmatter fields for my AI provider?
Yes. Before calling generateYamlFrontmatter, modify the frontmatterObj to include provider-specific keys. The Claude Code transformer demonstrates extensive frontmatter customization, adding fields like allowed-tools and license information. Any properties you add to this object will be serialized into YAML frontmatter in the output SKILL.md files.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →