This document describes the new extensible plugin-based architecture for OnlyRules, which provides a unified and organized approach to supporting multiple AI IDE rule formats.
The new architecture implements a plugin-based system where each AI IDE has its own dedicated formatter class that implements a common interface. This design makes it easy to add new AI assistants without modifying core logic.
Defines the specification for each rule format:
interface RuleFormatSpec {
id: string; // Unique identifier (e.g., 'cursor', 'copilot')
name: string; // Human-readable name
category: RuleFormatCategory; // Format category
extension: string; // File extension
supportsMultipleRules: boolean;
requiresMetadata: boolean;
defaultPath: string; // Default output path
}Categories organize formats by their structure:
- DIRECTORY_BASED: Formats using directories (
.cursor/rules,.clinerules) - ROOT_FILE: Single root files (
CLAUDE.md,GEMINI.md) - MEMORY_BASED: Memory/project-specific files (
.claude/memories)
Abstract base class that all formatters extend:
abstract class BaseRuleFormatter {
abstract readonly spec: RuleFormatSpec;
abstract generateRule(rule: ParsedRule, context: RuleGenerationContext): Promise<RuleGenerationResult>;
abstract isRuleCompatible(rule: ParsedRule): boolean;
abstract getOutputPath(rule: ParsedRule, context: RuleGenerationContext): string;
protected abstract transformContent(rule: ParsedRule): string;
}The DefaultRuleParser handles parsing .mdc files and extracting rules with metadata:
- Supports both single rules (
.md) and multi-rule files (.mdc) - Extracts YAML frontmatter as metadata
- Determines if rules are root/global rules
The DefaultRuleFormatterFactory manages all available formatters:
- Auto-registers all built-in formatters
- Provides methods to get formatters by ID or category
- Supports runtime registration of custom formatters
The DefaultRuleGenerationPipeline orchestrates the complete generation process:
- Parses input from files, URLs, or direct content
- Handles IDE-style rule organization
- Executes formatters for compatible rules
- Provides comprehensive error handling and logging
These formats create directories with individual rule files:
- Cursor:
.cursor/rules/{name}.mdcwith YAML frontmatter - GitHub Copilot:
.github/instructions/{name}.instructions.mdwith frontmatter - Cline:
.clinerules/{name}.mdwith plain markdown - Roo:
.roo/rules/{name}.mdwith description headers
These formats create single files in the project root:
- Claude:
CLAUDE.mdfor global rules - Gemini:
GEMINI.mdfor global rules
These formats create memory/project-specific files:
- Claude Memories:
.claude/memories/{name}.md - Gemini Memories:
.gemini/memories/{name}.md
To add support for a new AI assistant, create a new formatter class:
// src/formatters/my-new-ai.ts
import { join } from 'node:path';
import {
BaseRuleFormatter,
RuleFormatSpec,
RuleFormatCategory,
ParsedRule,
RuleGenerationContext,
RuleGenerationResult
} from '../core/interfaces';
export class MyNewAIFormatter extends BaseRuleFormatter {
readonly spec: RuleFormatSpec = {
id: 'my-new-ai',
name: 'My New AI Assistant',
category: RuleFormatCategory.DIRECTORY_BASED,
extension: '.md',
supportsMultipleRules: true,
requiresMetadata: false,
defaultPath: '.mynewai/rules'
};
async generateRule(
rule: ParsedRule,
context: RuleGenerationContext
): Promise<RuleGenerationResult> {
try {
const filePath = this.getOutputPath(rule, context);
await this.checkFileExists(filePath, context.force);
await this.ensureDirectory(filePath);
const content = this.transformContent(rule);
await this.writeFile(filePath, content);
return {
format: this.spec.id,
success: true,
filePath,
ruleName: rule.name
};
} catch (error) {
return {
format: this.spec.id,
success: false,
error: (error as Error).message,
ruleName: rule.name
};
}
}
isRuleCompatible(rule: ParsedRule): boolean {
// Define compatibility logic
return true;
}
getOutputPath(rule: ParsedRule, context: RuleGenerationContext): string {
const filename = `${rule.name || 'default'}${this.spec.extension}`;
return join(context.outputDir, this.spec.defaultPath, filename);
}
protected transformContent(rule: ParsedRule): string {
// Transform content for your AI's specific format
// Remove frontmatter, add headers, etc.
return rule.content.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
}
}Add the import and registration in src/core/factory.ts:
// Add import
import { MyNewAIFormatter } from '../formatters/my-new-ai';
// Add registration in registerBuiltInFormatters()
this.registerFormatter(new MyNewAIFormatter());If you need backward compatibility, add mapping in src/core/generator-v2.ts:
const formatMapping: Record<string, string> = {
// ... existing mappings
'.mynewai/rules': 'my-new-ai'
};import { generateRules } from 'onlyrules';
await generateRules({
file: './my-rules.mdc',
output: './output',
force: true,
verbose: true
});import { DefaultRuleGenerationPipeline } from 'onlyrules';
const pipeline = new DefaultRuleGenerationPipeline();
const results = await pipeline.execute({
input: './my-rules.mdc',
outputDir: './output',
formats: ['cursor', 'copilot', 'claude-root'],
force: true,
verbose: true
});import { DefaultRuleGenerationPipeline } from 'onlyrules';
import { MyCustomFormatter } from './my-custom-formatter';
const pipeline = new DefaultRuleGenerationPipeline();
pipeline.registerFormatter(new MyCustomFormatter());
const results = await pipeline.execute({
input: './my-rules.mdc',
outputDir: './output',
formats: ['my-custom-format'],
force: true
});import { getAvailableFormats, getFormatsByCategory } from 'onlyrules';
// Get all format IDs
const allFormats = getAvailableFormats();
console.log(allFormats); // ['cursor', 'copilot', 'cline', ...]
// Get formats by category
const byCategory = getFormatsByCategory();
console.log(byCategory);
// {
// directory: ['cursor', 'copilot', 'cline', 'roo'],
// root: ['claude-root', 'gemini-root'],
// memory: ['claude-memories', 'gemini-memories']
// }The new system is backward compatible. Existing code will continue to work without changes. The system automatically uses the new architecture unless ONLYRULES_USE_LEGACY=true is set.
If you have custom extensions to the old system:
- Custom Writers: Convert to formatter classes implementing
BaseRuleFormatter - Custom Parsers: Extend
DefaultRuleParseror implementRuleParserinterface - Custom Logic: Use the pipeline system for better separation of concerns
- Extensible: Add new AI assistants without touching core logic
- Organized: Clear separation by categories and responsibilities
- Type Safe: Full TypeScript interfaces and type checking
- Consistent: Standardized patterns for all formatters
- Testable: Each formatter can be tested independently
- Maintainable: Clear structure makes maintenance easier
The system includes formatters for various AI assistants:
- cursor: Cursor IDE (
.cursor/rules/{name}.mdc) - copilot: GitHub Copilot (
.github/copilot-instructions.md) - cline: Cline (
.clinerules/project.md) - roo: Roo (
.roo/rules/{name}.md) - kiro: Kiro (
.kiro/steering) - codebuddy: Tencent Cloud CodeBuddy (
.codebuddy/rules/{name}.md)
- claude-root: Claude (
CLAUDE.md) - gemini-root: Gemini (
GEMINI.md)
- claude-memories: Claude Memories (
claude_memories/{category}/{name}.md) - gemini-memories: Gemini Memories (
gemini_memories/{category}/{name}.md)
- agents: OpenAI Codex (
AGENTS.md) - junie: Junie (
.junie/guidelines.md) - windsurf: Windsurf (
.windsurfrules) - trae: Trae (
.trae/rules.md) - augment: Augment (
.augment/rules/manual/{name}.md) - augment-always: Augment Always (
.augment/rules/always/{name}.md) - lingma-project: Lingma (
.lingma/rules)
Each formatter should be tested independently:
import { MyNewAIFormatter } from '../src/formatters/my-new-ai';
test('MyNewAI formatter generates correct files', async () => {
const formatter = new MyNewAIFormatter();
const rule = { name: 'test', content: '# Test Rule', isRoot: false };
const context = { outputDir: './test-output', force: true, verbose: false };
const result = await formatter.generateRule(rule, context);
expect(result.success).toBe(true);
expect(result.format).toBe('my-new-ai');
// Add more assertions...
});ONLYRULES_USE_LEGACY=true: Force use of legacy implementation- Set to test fallback behavior or when debugging
- Plugin Discovery: Automatic discovery of third-party formatter plugins
- Configuration: Per-format configuration options
- Validation: Schema validation for different AI rule formats
- Templates: Format-specific rule templates
- Async Loading: Lazy loading of formatters for better startup performance