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>/skills to match the structure used by Cursor (.cursor), Claude Code (.claude), and other providers.
  • Frontmatter customization: Adapt the frontmatterObj to include provider-specific metadata fields. The generateYamlFrontmatter utility in scripts/lib/utils.js handles YAML serialization.
  • Placeholder handling: Call replacePlaceholders to convert generic {{arg}} syntax into the provider's expected format (for example, $ARG for Codex or {{args}} for Gemini).
  • Reference copying: The references array contains additional markdown files that must travel with the skill; write these to a reference/ 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:

  1. Read source files: readSourceFiles() parses each source/skills/**/SKILL.md.
  2. Transform per provider: Each transform<Provider>() writes a provider-specific directory under dist/.
  3. Assemble universal bundle: assembleUniversal() copies every provider's .provider/ folder into dist/universal/.
  4. Create ZIPs: createAllZips() in scripts/lib/zip.js packages 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>.js using the transform<NewProvider> function signature and utility helpers from scripts/lib/utils.js.
  • Export the transformer function in scripts/lib/transformers/index.js to make it available to the build system.
  • Register the provider in the providerMappings array within scripts/build.js to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →