agent-rules
Version:
Rules and instructions for agentic coding tools like Cursor, Claude CLI, Gemini CLI, Qodo, Cline and more
1,138 lines (1,120 loc) • 47.6 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// src/bin/cli.ts
var import_prompts = require("@clack/prompts");
var import_node_util7 = require("util");
var import_promises7 = require("fs/promises");
var import_node_path7 = require("path");
var import_node_url2 = require("url");
// src/main.ts
var import_node_path6 = __toESM(require("path"), 1);
var import_promises6 = __toESM(require("fs/promises"), 1);
var import_node_url = require("url");
var import_node_util6 = require("util");
// src/adapters/base-adapter.ts
var import_node_path = __toESM(require("path"), 1);
var import_promises = __toESM(require("fs/promises"), 1);
var import_node_util = require("util");
var debug = (0, import_node_util.debuglog)("agent-rules");
var BaseAdapter = class {
static {
__name(this, "BaseAdapter");
}
config;
constructor(config) {
this.config = config;
}
/**
* Get the configuration for this AI app
*/
getConfig() {
return this.config;
}
/**
* Process MCP configuration (optional override)
* Default implementation handles JSON merging
* @param scaffoldInstructions - The instructions containing template choices
* @param resolvedMcpTemplateDirectory - The resolved path to the MCP template directory
* @param resolvedTargetDirectory - The resolved path to the target directory
*/
async processMcpConfiguration(scaffoldInstructions, resolvedMcpTemplateDirectory, resolvedTargetDirectory) {
const mcpConfig = this.getMcpConfig();
if (!mcpConfig) return;
const templateMcpFile = import_node_path.default.join(resolvedMcpTemplateDirectory, "mcp.json");
const targetMcpFile = import_node_path.default.resolve(process.cwd(), mcpConfig.filePath);
const mergeKey = mcpConfig.mergeKey || "mcpServers";
debug(`Processing MCP configuration from ${templateMcpFile} to ${targetMcpFile}`);
try {
const templateContent = await import_promises.default.readFile(templateMcpFile, "utf-8");
const templateMcpConfig = JSON.parse(templateContent);
let templateServers = {};
const keysToTry = mergeKey === "mcpServers" ? ["mcpServers"] : [mergeKey, "mcpServers"];
for (const key of keysToTry) {
if (Object.hasOwn(templateMcpConfig, key)) {
const templateValue = Reflect.get(templateMcpConfig, key);
if (templateValue && typeof templateValue === "object" && !Array.isArray(templateValue)) {
templateServers = templateValue;
break;
}
}
}
let existingConfig = {};
try {
const existingContent = await import_promises.default.readFile(targetMcpFile, "utf-8");
existingConfig = JSON.parse(existingContent);
} catch (error) {
debug(`Target MCP file does not exist, creating new one: ${targetMcpFile}`);
}
let existingServers = {};
if (Object.hasOwn(existingConfig, mergeKey)) {
const existingValue = Reflect.get(existingConfig, mergeKey);
if (existingValue && typeof existingValue === "object" && !Array.isArray(existingValue)) {
existingServers = existingValue;
}
}
const mergedConfig = {
...existingConfig,
[mergeKey]: {
...existingServers,
...templateServers
}
};
await import_promises.default.mkdir(import_node_path.default.dirname(targetMcpFile), { recursive: true });
await import_promises.default.writeFile(targetMcpFile, JSON.stringify(mergedConfig, null, 2), "utf-8");
debug(`MCP configuration merged into: ${targetMcpFile}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.warn(`Warning: Failed to process MCP configuration: ${errorMessage}`);
}
}
/**
* Process commands configuration (optional override)
* Default implementation handles copying command files with optional filename transformation
* @param scaffoldInstructions - The instructions containing template choices
* @param resolvedCommandsTemplateDirectory - The resolved path to the commands template directory
* @param resolvedTargetDirectory - The resolved path to the target directory (project root)
*/
async processCommandsConfiguration(scaffoldInstructions, resolvedCommandsTemplateDirectory, resolvedTargetDirectory) {
const commandsConfig = this.getCommandsConfig();
if (!commandsConfig) return;
const targetDirectory = import_node_path.default.resolve(process.cwd(), commandsConfig.targetDirectory);
debug(`Processing commands from ${resolvedCommandsTemplateDirectory} to ${targetDirectory}`);
try {
await import_promises.default.mkdir(targetDirectory, { recursive: true });
const files = await import_promises.default.readdir(resolvedCommandsTemplateDirectory);
const commandFiles = files.filter((file) => file.endsWith(".command.md"));
for (const commandFile of commandFiles) {
const sourceFilePath = import_node_path.default.join(resolvedCommandsTemplateDirectory, commandFile);
const stat = await import_promises.default.stat(sourceFilePath);
if (stat.isFile()) {
const targetFileName = commandsConfig.fileNameTransform ? commandsConfig.fileNameTransform(commandFile) : commandFile;
const targetFilePath = import_node_path.default.join(targetDirectory, targetFileName);
const content = await import_promises.default.readFile(sourceFilePath, "utf-8");
await import_promises.default.writeFile(targetFilePath, content, "utf-8");
debug(`Copied command file: ${commandFile} -> ${targetFileName}`);
}
}
debug(`Commands configuration completed in: ${targetDirectory}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.warn(`Warning: Failed to process commands configuration: ${errorMessage}`);
}
}
};
// src/adapters/github-copilot-adapter.ts
var import_node_path2 = __toESM(require("path"), 1);
var import_promises2 = __toESM(require("fs/promises"), 1);
var import_node_util2 = require("util");
var debug2 = (0, import_node_util2.debuglog)("agent-rules");
var GitHubCopilotAdapter = class extends BaseAdapter {
static {
__name(this, "GitHubCopilotAdapter");
}
constructor() {
const config = {
directory: ".github/instructions",
filesSuffix: ".instructions.md"
};
super(config);
}
/**
* Get MCP configuration for GitHub Copilot
*/
getMcpConfig() {
return {
filePath: ".vscode/mcp.json",
mergeKey: "servers"
};
}
/**
* Get commands configuration for GitHub Copilot
*/
getCommandsConfig() {
return {
targetDirectory: ".github/prompts",
fileNameTransform: /* @__PURE__ */ __name((filename) => filename.replace(".command.md", ".prompt.md"), "fileNameTransform")
};
}
/**
* Process instructions by copying template files to the target directory
*/
async processInstructions(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) {
await this.copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, this.config.filesSuffix);
}
/**
* Copy all template files from source to target directory
*/
async copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, filesSuffix) {
const templateFiles = await import_promises2.default.readdir(resolvedTemplateDirectory);
for (const templateFile of templateFiles) {
const templateFilePath = import_node_path2.default.join(resolvedTemplateDirectory, templateFile);
await this.copyTemplateFile(templateFilePath, resolvedTargetDirectory, resolvedTargetDirectory, filesSuffix);
}
}
/**
* Copy a single template file to the target directory
*/
async copyTemplateFile(templateFilePath, targetFilePath, resolvedTargetDirectory, filesSuffix) {
const baseDirectory = resolvedTargetDirectory;
const decodedPath = decodeURIComponent(templateFilePath);
const normalizedPath = import_node_path2.default.normalize(decodedPath);
const sanitizedTemplateFile = import_node_path2.default.basename(normalizedPath);
const fullTemplatePath = import_node_path2.default.join(import_node_path2.default.dirname(templateFilePath), sanitizedTemplateFile);
debug2("Processing template file:", sanitizedTemplateFile);
try {
const stat = await import_promises2.default.stat(fullTemplatePath);
if (stat.isFile()) {
const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix);
const targetPath = import_node_path2.default.join(targetFilePath, targetFileName);
const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory);
debug2("Writing template file to target path:", resolvedTargetFilePath);
const templateContent = await import_promises2.default.readFile(fullTemplatePath, "utf-8");
await import_promises2.default.writeFile(resolvedTargetFilePath, templateContent, "utf-8");
}
} catch (error) {
console.warn(`Skipping file ${sanitizedTemplateFile}: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Generate the target filename by applying the file suffix
*/
generateTargetFileName(templateFileName, filesSuffix) {
const parsedFile = import_node_path2.default.parse(templateFileName);
let baseName = parsedFile.name;
if (baseName.endsWith(".instructions")) {
baseName = baseName.replace(/\.instructions$/, "");
}
return `${baseName}${filesSuffix}`;
}
/**
* Validate that the target path doesn't escape the base directory
*/
validateTargetPath(targetFilePath, baseDirectory) {
const decodedPath = decodeURIComponent(targetFilePath);
const normalizedPath = import_node_path2.default.normalize(decodedPath);
const resolvedTargetFilePath = import_node_path2.default.resolve(normalizedPath);
if (!resolvedTargetFilePath.startsWith(baseDirectory)) {
throw new Error(`Invalid target path: ${targetFilePath}`);
}
return resolvedTargetFilePath;
}
};
// src/adapters/cursor-adapter.ts
var import_node_path3 = __toESM(require("path"), 1);
var import_promises3 = __toESM(require("fs/promises"), 1);
var import_node_util3 = require("util");
var import_mdast_util_from_markdown = require("mdast-util-from-markdown");
var import_mdast_util_to_markdown = require("mdast-util-to-markdown");
var import_micromark_extension_frontmatter = require("micromark-extension-frontmatter");
var import_mdast_util_frontmatter = require("mdast-util-frontmatter");
var import_yaml = require("yaml");
var debug3 = (0, import_node_util3.debuglog)("agent-rules");
var CursorAdapter = class extends BaseAdapter {
static {
__name(this, "CursorAdapter");
}
constructor() {
const config = {
directory: ".cursor/rules",
filesSuffix: ".mdc"
};
super(config);
}
/**
* Get MCP configuration for Cursor (not supported yet)
*/
getMcpConfig() {
return null;
}
/**
* Get commands configuration for Cursor (not supported yet)
*/
getCommandsConfig() {
return null;
}
/**
* Process instructions by copying template files to the target directory
*/
async processInstructions(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) {
await this.copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, this.config.filesSuffix);
}
/**
* Copy all template files from source to target directory
*/
async copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, filesSuffix) {
const templateFiles = await import_promises3.default.readdir(resolvedTemplateDirectory);
for (const templateFile of templateFiles) {
const templateFilePath = import_node_path3.default.join(resolvedTemplateDirectory, templateFile);
await this.copyTemplateFile(templateFilePath, resolvedTargetDirectory, resolvedTargetDirectory, filesSuffix);
}
}
/**
* Copy a single template file to the target directory
*/
async copyTemplateFile(templateFilePath, targetFilePath, resolvedTargetDirectory, filesSuffix) {
const baseDirectory = resolvedTargetDirectory;
const decodedPath = decodeURIComponent(templateFilePath);
const normalizedPath = import_node_path3.default.normalize(decodedPath);
const sanitizedTemplateFile = import_node_path3.default.basename(normalizedPath);
const fullTemplatePath = import_node_path3.default.join(import_node_path3.default.dirname(templateFilePath), sanitizedTemplateFile);
debug3("Processing template file:", sanitizedTemplateFile);
try {
const stat = await import_promises3.default.stat(fullTemplatePath);
if (stat.isFile()) {
const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix);
const targetPath = import_node_path3.default.join(targetFilePath, targetFileName);
const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory);
debug3("Writing template file to target path:", resolvedTargetFilePath);
const templateContent = await import_promises3.default.readFile(fullTemplatePath, "utf-8");
const processedContent = this.processFrontmatter(templateContent);
await import_promises3.default.writeFile(resolvedTargetFilePath, processedContent, "utf-8");
}
} catch (error) {
console.warn(`Skipping file ${sanitizedTemplateFile}: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Generate the target filename by applying the file suffix
*/
generateTargetFileName(templateFileName, filesSuffix) {
const parsedFile = import_node_path3.default.parse(templateFileName);
let baseName = parsedFile.name;
if (baseName.endsWith(".instructions")) {
baseName = baseName.replace(/\.instructions$/, "");
}
return `${baseName}${filesSuffix}`;
}
/**
* Process markdown content to transform frontmatter from template format to Cursor format
*/
processFrontmatter(content) {
try {
const ast = (0, import_mdast_util_from_markdown.fromMarkdown)(content, {
extensions: [(0, import_micromark_extension_frontmatter.frontmatter)(["yaml"])],
mdastExtensions: [(0, import_mdast_util_frontmatter.frontmatterFromMarkdown)(["yaml"])]
});
let hasTransformations = false;
for (const node of ast.children) {
if (node.type === "yaml") {
const transformedValue = this.transformFrontmatterFields(node.value);
if (transformedValue !== node.value) {
node.value = transformedValue;
hasTransformations = true;
}
}
}
if (hasTransformations) {
return (0, import_mdast_util_to_markdown.toMarkdown)(ast, {
extensions: [(0, import_mdast_util_frontmatter.frontmatterToMarkdown)(["yaml"])]
});
}
return content;
} catch (error) {
debug3("Error processing frontmatter with AST:", error);
return content;
}
}
/**
* Transform frontmatter fields from template format to Cursor format using structured YAML parsing
*/
transformFrontmatterFields(frontmatterValue) {
try {
const frontmatterData = (0, import_yaml.parse)(frontmatterValue);
if (frontmatterData && typeof frontmatterData === "object" && "applyTo" in frontmatterData) {
const transformedData = { ...frontmatterData };
transformedData.globs = frontmatterData.applyTo;
delete transformedData.applyTo;
return (0, import_yaml.stringify)(transformedData, { lineWidth: -1 }).trim();
}
return frontmatterValue;
} catch (error) {
debug3("Error parsing YAML frontmatter:", error);
return frontmatterValue.replace(/^applyTo:\s*(.+)$/gm, "globs: $1");
}
}
/**
* Validate that the target path doesn't escape the base directory
*/
validateTargetPath(targetFilePath, baseDirectory) {
const decodedPath = decodeURIComponent(targetFilePath);
const normalizedPath = import_node_path3.default.normalize(decodedPath);
const resolvedTargetFilePath = import_node_path3.default.resolve(normalizedPath);
if (!resolvedTargetFilePath.startsWith(baseDirectory)) {
throw new Error(`Invalid target path: ${targetFilePath}`);
}
return resolvedTargetFilePath;
}
};
// src/adapters/claude-code-adapter.ts
var import_node_path4 = __toESM(require("path"), 1);
var import_promises4 = __toESM(require("fs/promises"), 1);
var import_node_util4 = require("util");
var import_mdast_util_from_markdown2 = require("mdast-util-from-markdown");
var debug4 = (0, import_node_util4.debuglog)("agent-rules");
var ClaudeCodeAdapter = class _ClaudeCodeAdapter extends BaseAdapter {
static {
__name(this, "ClaudeCodeAdapter");
}
static CLAUDE_MAIN_FILE = "CLAUDE.md";
// Map topic values to their display labels from CLI
static TOPIC_LABELS = {
"secure-code": "Secure Coding",
"security-vulnerabilities": "Security Vulnerabilities",
testing: "Testing"
};
constructor() {
const config = {
directory: ".claude/rules",
filesSuffix: ".md"
};
super(config);
}
/**
* Get MCP configuration for Claude Code (not supported yet)
*/
getMcpConfig() {
return null;
}
/**
* Get commands configuration for Claude Code (not supported yet)
*/
getCommandsConfig() {
return null;
}
/**
* Process instructions by copying template files and updating CLAUDE.md with imports
*/
async processInstructions(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) {
await this.copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, this.config.filesSuffix);
await this.updateClaudeMainFile(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory);
}
/**
* Copy all template files from source to target directory
*/
async copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, filesSuffix) {
const templateFiles = await import_promises4.default.readdir(resolvedTemplateDirectory);
for (const templateFile of templateFiles) {
if (!templateFile.endsWith(".md")) {
debug4("Skipping non-markdown file:", templateFile);
continue;
}
const templateFilePath = import_node_path4.default.join(resolvedTemplateDirectory, templateFile);
await this.copyTemplateFile(templateFilePath, resolvedTargetDirectory, resolvedTargetDirectory, filesSuffix);
}
}
/**
* Copy a single template file to the target directory
*/
async copyTemplateFile(templateFilePath, targetFilePath, resolvedTargetDirectory, filesSuffix) {
const baseDirectory = resolvedTargetDirectory;
const decodedPath = decodeURIComponent(templateFilePath);
const normalizedPath = import_node_path4.default.normalize(decodedPath);
const sanitizedTemplateFile = import_node_path4.default.basename(normalizedPath);
const fullTemplatePath = import_node_path4.default.join(import_node_path4.default.dirname(templateFilePath), sanitizedTemplateFile);
debug4("Processing template file:", sanitizedTemplateFile);
try {
const stat = await import_promises4.default.stat(fullTemplatePath);
if (stat.isFile()) {
const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix);
const targetPath = import_node_path4.default.join(targetFilePath, targetFileName);
const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory);
debug4("Writing template file to target path:", resolvedTargetFilePath);
const templateContent = await import_promises4.default.readFile(fullTemplatePath, "utf-8");
await import_promises4.default.writeFile(resolvedTargetFilePath, templateContent, "utf-8");
}
} catch (error) {
console.warn(`Skipping file ${sanitizedTemplateFile}: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Generate the target filename by applying the file suffix
*/
generateTargetFileName(templateFileName, filesSuffix) {
const parsedFile = import_node_path4.default.parse(templateFileName);
let baseName = parsedFile.name;
if (baseName.endsWith(".instructions")) {
baseName = baseName.replace(/\.instructions$/, "");
}
return `${baseName}${filesSuffix}`;
}
/**
* Update the main CLAUDE.md file with imports for the processed templates
*/
async updateClaudeMainFile(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) {
const projectRoot = import_node_path4.default.dirname(import_node_path4.default.dirname(resolvedTargetDirectory));
const claudeMainFilePath = import_node_path4.default.join(projectRoot, _ClaudeCodeAdapter.CLAUDE_MAIN_FILE);
debug4("Claude main file path:", claudeMainFilePath);
let claudeContent = "";
try {
claudeContent = await import_promises4.default.readFile(claudeMainFilePath, "utf-8");
} catch (error) {
debug4("CLAUDE.md not found, creating new file");
claudeContent = "";
}
const templateFiles = await import_promises4.default.readdir(resolvedTemplateDirectory);
const imports = templateFiles.filter((file) => file.endsWith(".md")).map((file) => {
const targetFileName = this.generateTargetFileName(file, this.config.filesSuffix);
return `- @./.claude/rules/${targetFileName}`;
});
if (imports.length === 0) {
debug4("No template files to import");
return;
}
const topicLabel = _ClaudeCodeAdapter.TOPIC_LABELS[scaffoldInstructions.codeTopic] || scaffoldInstructions.codeTopic;
const updatedContent = this.addImportsToClaudeContent(claudeContent, topicLabel, imports);
await import_promises4.default.writeFile(claudeMainFilePath, updatedContent, "utf-8");
debug4("Updated CLAUDE.md with imports for topic:", topicLabel);
}
/**
* Add imports to Claude content, avoiding duplicates and organizing by categories
*/
addImportsToClaudeContent(content, categoryLabel, imports) {
try {
(0, import_mdast_util_from_markdown2.fromMarkdown)(content);
const existingContent = content.toLowerCase();
const categoryHeaderLower = `# ${categoryLabel}`.toLowerCase();
const categoryExists = existingContent.includes(categoryHeaderLower);
if (categoryExists) {
const hasExistingImports = imports.some(
(importLine) => existingContent.includes(importLine.toLowerCase())
);
if (hasExistingImports) {
debug4("Imports already exist in CLAUDE.md, skipping update");
return content;
}
}
let updatedContent = content.trim();
if (updatedContent.length > 0) {
updatedContent += "\n\n";
}
if (!categoryExists) {
updatedContent += `# ${categoryLabel}
`;
}
updatedContent += imports.join("\n") + "\n";
return updatedContent;
} catch (error) {
debug4("Error processing Claude content with AST:", error);
let updatedContent = content.trim();
if (updatedContent.length > 0) {
updatedContent += "\n\n";
}
updatedContent += `# ${categoryLabel}
`;
updatedContent += imports.join("\n") + "\n";
return updatedContent;
}
}
/**
* Validate that the target path doesn't escape the base directory
*/
validateTargetPath(targetFilePath, baseDirectory) {
const decodedPath = decodeURIComponent(targetFilePath);
const normalizedPath = import_node_path4.default.normalize(decodedPath);
const resolvedTargetFilePath = import_node_path4.default.resolve(normalizedPath);
if (!resolvedTargetFilePath.startsWith(baseDirectory)) {
throw new Error(`Invalid target path: ${targetFilePath}`);
}
return resolvedTargetFilePath;
}
};
// src/adapters/gemini-adapter.ts
var import_node_path5 = __toESM(require("path"), 1);
var import_promises5 = __toESM(require("fs/promises"), 1);
var import_node_util5 = require("util");
var import_mdast_util_from_markdown3 = require("mdast-util-from-markdown");
var debug5 = (0, import_node_util5.debuglog)("agent-rules");
var GeminiAdapter = class _GeminiAdapter extends BaseAdapter {
static {
__name(this, "GeminiAdapter");
}
static GEMINI_MAIN_FILE = "GEMINI.md";
// Map topic values to their display labels from CLI
static TOPIC_LABELS = {
"secure-code": "Secure Coding",
"security-vulnerabilities": "Security Vulnerabilities",
testing: "Testing"
};
constructor() {
const config = {
directory: ".gemini/rules",
filesSuffix: ".md"
};
super(config);
}
/**
* Get MCP configuration for Gemini
*/
getMcpConfig() {
return {
filePath: ".gemini/settings.json",
mergeKey: "mcpServers"
};
}
/**
* Get commands configuration for Gemini (not supported yet)
*/
getCommandsConfig() {
return null;
}
/**
* Process instructions by copying template files and updating GEMINI.md with imports
*/
async processInstructions(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) {
await this.copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, this.config.filesSuffix);
await this.updateGeminiMainFile(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory);
}
/**
* Copy all template files from source to target directory
*/
async copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, filesSuffix) {
const templateFiles = await import_promises5.default.readdir(resolvedTemplateDirectory);
for (const templateFile of templateFiles) {
if (!templateFile.endsWith(".md")) {
debug5("Skipping non-markdown file:", templateFile);
continue;
}
const templateFilePath = import_node_path5.default.join(resolvedTemplateDirectory, templateFile);
await this.copyTemplateFile(templateFilePath, resolvedTargetDirectory, resolvedTargetDirectory, filesSuffix);
}
}
/**
* Copy a single template file to the target directory
*/
async copyTemplateFile(templateFilePath, targetFilePath, resolvedTargetDirectory, filesSuffix) {
const baseDirectory = resolvedTargetDirectory;
const decodedPath = decodeURIComponent(templateFilePath);
const normalizedPath = import_node_path5.default.normalize(decodedPath);
const sanitizedTemplateFile = import_node_path5.default.basename(normalizedPath);
const fullTemplatePath = import_node_path5.default.join(import_node_path5.default.dirname(templateFilePath), sanitizedTemplateFile);
debug5("Processing template file:", sanitizedTemplateFile);
try {
const stat = await import_promises5.default.stat(fullTemplatePath);
if (stat.isFile()) {
const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix);
const targetPath = import_node_path5.default.join(targetFilePath, targetFileName);
const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory);
debug5("Writing template file to target path:", resolvedTargetFilePath);
const templateContent = await import_promises5.default.readFile(fullTemplatePath, "utf-8");
const processedContent = this.stripFrontmatter(templateContent);
await import_promises5.default.writeFile(resolvedTargetFilePath, processedContent, "utf-8");
}
} catch (error) {
console.warn(`Skipping file ${sanitizedTemplateFile}: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Strip YAML frontmatter from markdown content since Gemini doesn't support it
*/
stripFrontmatter(content) {
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (match && match[2] !== void 0) {
return match[2];
}
return content;
}
/**
* Generate the target filename by applying the file suffix
*/
generateTargetFileName(templateFileName, filesSuffix) {
const parsedFile = import_node_path5.default.parse(templateFileName);
let baseName = parsedFile.name;
if (baseName.endsWith(".instructions")) {
baseName = baseName.replace(/\.instructions$/, "");
}
return `${baseName}${filesSuffix}`;
}
/**
* Update the main GEMINI.md file with imports for the processed templates
*/
async updateGeminiMainFile(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) {
const projectRoot = import_node_path5.default.dirname(import_node_path5.default.dirname(resolvedTargetDirectory));
const geminiMainFilePath = import_node_path5.default.join(projectRoot, _GeminiAdapter.GEMINI_MAIN_FILE);
debug5("Gemini main file path:", geminiMainFilePath);
let geminiContent = "";
try {
geminiContent = await import_promises5.default.readFile(geminiMainFilePath, "utf-8");
} catch (error) {
debug5("GEMINI.md not found, creating new file");
geminiContent = "";
}
const templateFiles = await import_promises5.default.readdir(resolvedTemplateDirectory);
const imports = templateFiles.filter((file) => file.endsWith(".md")).map((file) => {
const targetFileName = this.generateTargetFileName(file, this.config.filesSuffix);
return `@./.gemini/rules/${targetFileName}`;
});
if (imports.length === 0) {
debug5("No template files to import");
return;
}
const topicLabel = _GeminiAdapter.TOPIC_LABELS[scaffoldInstructions.codeTopic] || scaffoldInstructions.codeTopic;
const updatedContent = this.addImportsToGeminiContent(geminiContent, topicLabel, imports);
await import_promises5.default.writeFile(geminiMainFilePath, updatedContent, "utf-8");
debug5("Updated GEMINI.md with imports for topic:", topicLabel);
}
/**
* Add imports to Gemini content, avoiding duplicates and organizing by categories
*/
addImportsToGeminiContent(content, categoryLabel, imports) {
try {
(0, import_mdast_util_from_markdown3.fromMarkdown)(content);
const existingContent = content.toLowerCase();
const categoryHeaderLower = `# ${categoryLabel}`.toLowerCase();
const categoryExists = existingContent.includes(categoryHeaderLower);
if (categoryExists) {
const hasExistingImports = imports.some(
(importLine) => existingContent.includes(importLine.toLowerCase())
);
if (hasExistingImports) {
debug5("Imports already exist in GEMINI.md, skipping update");
return content;
}
}
let updatedContent = content.trim();
if (updatedContent.length > 0) {
updatedContent += "\n\n";
}
if (!categoryExists) {
updatedContent += `# ${categoryLabel}
`;
}
updatedContent += imports.join("\n") + "\n";
return updatedContent;
} catch (error) {
debug5("Error processing Gemini content with AST:", error);
let updatedContent = content.trim();
if (updatedContent.length > 0) {
updatedContent += "\n\n";
}
updatedContent += `# ${categoryLabel}
`;
updatedContent += imports.join("\n") + "\n";
return updatedContent;
}
}
/**
* Validate that the target path doesn't escape the base directory
*/
validateTargetPath(targetFilePath, baseDirectory) {
const decodedPath = decodeURIComponent(targetFilePath);
const normalizedPath = import_node_path5.default.normalize(decodedPath);
const resolvedTargetFilePath = import_node_path5.default.resolve(normalizedPath);
if (!resolvedTargetFilePath.startsWith(baseDirectory)) {
throw new Error(`Invalid target path: ${targetFilePath}`);
}
return resolvedTargetFilePath;
}
};
// src/adapters/adapter-registry.ts
var AdapterRegistry = class {
static {
__name(this, "AdapterRegistry");
}
static adapters = /* @__PURE__ */ new Map([
["github-copilot", () => new GitHubCopilotAdapter()],
["cursor", () => new CursorAdapter()],
["claude-code", () => new ClaudeCodeAdapter()],
["gemini", () => new GeminiAdapter()]
]);
/**
* Get an adapter instance for the specified AI app
* @param aiApp - The AI app identifier
* @returns The adapter instance
* @throws Error if the AI app is not supported
*/
static getAdapter(aiApp) {
const adapterFactory = this.adapters.get(aiApp);
if (!adapterFactory) {
throw new Error(`AI App "${aiApp}" is not supported.`);
}
return adapterFactory();
}
/**
* Get the list of supported AI apps
* @returns Array of supported AI app identifiers
*/
static getSupportedAiApps() {
return Array.from(this.adapters.keys());
}
};
// src/main.ts
var import_meta = {};
var debug6 = (0, import_node_util6.debuglog)("agent-rules");
var templateRoot = "__template__";
function resolvePackageRootDirectoryForTemplates() {
let guessedDirName = "";
try {
if (typeof import_meta !== "undefined" && import_meta.url) {
const __filename = (0, import_node_url.fileURLToPath)(import_meta.url);
guessedDirName = import_node_path6.default.dirname(__filename);
} else {
guessedDirName = __dirname;
}
} catch (error) {
guessedDirName = __dirname;
}
if (guessedDirName.endsWith("src")) {
return import_node_path6.default.resolve(guessedDirName, "..");
} else if (guessedDirName.endsWith("dist/bin") || guessedDirName.endsWith("dist\\bin")) {
return import_node_path6.default.resolve(guessedDirName, "..");
} else {
return guessedDirName;
}
}
__name(resolvePackageRootDirectoryForTemplates, "resolvePackageRootDirectoryForTemplates");
async function resolveTemplateDirectory(scaffoldInstructions) {
const { codeLanguage, codeTopic } = scaffoldInstructions;
const currentFileDirectory = resolvePackageRootDirectoryForTemplates();
const templateDirectory = import_node_path6.default.join(currentFileDirectory, templateRoot, codeLanguage, codeTopic);
const resolvedTemplateDirectory = import_node_path6.default.resolve(templateDirectory);
try {
const templateStats = await import_promises6.default.stat(resolvedTemplateDirectory);
if (!templateStats.isDirectory()) {
throw new Error(`Template directory is not a directory: ${resolvedTemplateDirectory}`);
}
} catch (error) {
throw new Error(`Template directory not found: ${resolvedTemplateDirectory}`);
}
return resolvedTemplateDirectory;
}
__name(resolveTemplateDirectory, "resolveTemplateDirectory");
async function resolveMcpTemplateDirectory(scaffoldInstructions) {
const { codeLanguage } = scaffoldInstructions;
const currentFileDirectory = resolvePackageRootDirectoryForTemplates();
const mcpTemplateDirectory = import_node_path6.default.join(currentFileDirectory, templateRoot, codeLanguage, "_mcp");
const resolvedMcpTemplateDirectory = import_node_path6.default.resolve(mcpTemplateDirectory);
try {
const templateStats = await import_promises6.default.stat(resolvedMcpTemplateDirectory);
if (!templateStats.isDirectory()) {
throw new Error(`MCP template directory is not a directory: ${resolvedMcpTemplateDirectory}`);
}
} catch (error) {
throw new Error(`MCP template directory not found: ${resolvedMcpTemplateDirectory}`);
}
return resolvedMcpTemplateDirectory;
}
__name(resolveMcpTemplateDirectory, "resolveMcpTemplateDirectory");
async function resolveCommandsTemplateDirectory(scaffoldInstructions) {
const { codeLanguage } = scaffoldInstructions;
const currentFileDirectory = resolvePackageRootDirectoryForTemplates();
const commandsTemplateDirectory = import_node_path6.default.join(currentFileDirectory, templateRoot, codeLanguage, "_commands");
const resolvedCommandsTemplateDirectory = import_node_path6.default.resolve(commandsTemplateDirectory);
try {
const templateStats = await import_promises6.default.stat(resolvedCommandsTemplateDirectory);
if (!templateStats.isDirectory()) {
return null;
}
return resolvedCommandsTemplateDirectory;
} catch (error) {
return null;
}
}
__name(resolveCommandsTemplateDirectory, "resolveCommandsTemplateDirectory");
async function createTargetDirectory(directory) {
const resolvedTargetDirectory = import_node_path6.default.resolve(directory);
await import_promises6.default.mkdir(resolvedTargetDirectory, { recursive: true });
return resolvedTargetDirectory;
}
__name(createTargetDirectory, "createTargetDirectory");
async function scaffoldAiAppInstructions(scaffoldInstructions) {
const { aiApp, codeLanguage, codeTopic, includeMcp } = scaffoldInstructions;
if (!aiApp || !codeLanguage || !codeTopic) {
throw new Error("Scaffold instructions must include aiApp and all other template choices.");
}
const adapter = AdapterRegistry.getAdapter(aiApp);
const aiAppConfig = adapter.getConfig();
const { directory } = aiAppConfig;
debug6(`Scaffolding AI App instructions in directory: ${directory} with adapter: ${aiApp}`);
const resolvedTemplateDirectory = await resolveTemplateDirectory(scaffoldInstructions);
const resolvedTargetDirectory = await createTargetDirectory(directory);
await adapter.processInstructions(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory);
if (includeMcp) {
const mcpConfig = adapter.getMcpConfig();
if (mcpConfig) {
debug6(`Processing MCP configuration for ${aiApp}`);
const resolvedMcpTemplateDirectory = await resolveMcpTemplateDirectory(scaffoldInstructions);
await adapter.processMcpConfiguration(scaffoldInstructions, resolvedMcpTemplateDirectory, resolvedTargetDirectory);
} else {
console.warn(`MCP configuration not supported for ${aiApp}`);
}
}
if (scaffoldInstructions.includeCommands) {
const commandsConfig = adapter.getCommandsConfig();
if (commandsConfig) {
debug6(`Processing commands configuration for ${aiApp}`);
const resolvedCommandsTemplateDirectory = await resolveCommandsTemplateDirectory(scaffoldInstructions);
if (resolvedCommandsTemplateDirectory) {
await adapter.processCommandsConfiguration(scaffoldInstructions, resolvedCommandsTemplateDirectory, resolvedTargetDirectory);
}
}
}
}
__name(scaffoldAiAppInstructions, "scaffoldAiAppInstructions");
// src/bin/cli.ts
var import_meta2 = {};
var debug7 = (0, import_node_util7.debuglog)("agent-rules");
var AVAILABLE_TOPICS = ["secure-code", "security-vulnerabilities", "testing", "nodejs-dev"];
var AVAILABLE_APPS = AdapterRegistry.getSupportedAiApps();
function parseCommandLineArgs() {
try {
const { values } = (0, import_node_util7.parseArgs)({
args: process.argv.slice(2),
options: {
app: {
type: "string",
short: "a"
},
topics: {
type: "string",
multiple: true,
short: "t"
},
mcp: {
type: "boolean",
short: "m"
},
commands: {
type: "boolean",
short: "c"
},
help: {
type: "boolean",
short: "h"
},
version: {
type: "boolean",
short: "v"
}
},
allowPositionals: false
});
return {
app: values.app,
topics: values.topics,
mcp: values.mcp,
commands: values.commands,
help: values.help,
version: values.version
};
} catch (error) {
console.error("Error parsing command line arguments:", error.message);
showHelp();
process.exit(1);
}
}
__name(parseCommandLineArgs, "parseCommandLineArgs");
function showHelp() {
console.log(`
Usage: agent-rules [options]
Options:
-a, --app <app> AI app to generate rules for (${AVAILABLE_APPS.join(", ")})
-t, --topics <topics> Topics to generate rules for (${AVAILABLE_TOPICS.join(", ")})
Can be specified multiple times: --topics secure-code --topics testing
-m, --mcp Include MCP (Model Context Protocol) server configuration
-c, --commands Include custom commands
-h, --help Show this help message
-v, --version Show version number
Examples:
agent-rules # Interactive mode
agent-rules --app cursor --topics secure-code # Generate secure coding rules for Cursor
agent-rules -a github-copilot -t testing -t secure-code # Multiple topics
agent-rules --app gemini --topics testing --mcp # Include MCP configuration
agent-rules --app github-copilot --topics secure-code --commands # Include commands
Available AI Apps: ${AVAILABLE_APPS.join(", ")}
Available Topics: ${AVAILABLE_TOPICS.join(", ")}
`);
}
__name(showHelp, "showHelp");
async function showVersion() {
try {
const __filename = (0, import_node_url2.fileURLToPath)(import_meta2.url);
const __dirname2 = (0, import_node_path7.dirname)(__filename);
const packageJsonPath = (0, import_node_path7.resolve)(__dirname2, "../../package.json");
const packageJson = JSON.parse(await (0, import_promises7.readFile)(packageJsonPath, "utf-8"));
console.log(packageJson.version);
} catch (error) {
console.error("Error reading version:", error);
process.exit(1);
}
}
__name(showVersion, "showVersion");
function validateCliArgs(args) {
if (args.app && !AVAILABLE_APPS.includes(args.app)) {
console.error(`Error: Invalid app "${args.app}". Available apps: ${AVAILABLE_APPS.join(", ")}`);
process.exit(1);
}
if (args.topics) {
const invalidTopics = args.topics.filter((topic) => !AVAILABLE_TOPICS.includes(topic));
if (invalidTopics.length > 0) {
console.error(`Error: Invalid topics "${invalidTopics.join(", ")}". Available topics: ${AVAILABLE_TOPICS.join(", ")}`);
process.exit(1);
}
}
if (args.app && !args.topics || !args.app && args.topics) {
console.error("Error: When using command line flags, both --app and --topics must be specified");
showHelp();
process.exit(1);
}
}
__name(validateCliArgs, "validateCliArgs");
async function initInteractive() {
(0, import_prompts.intro)((0, import_node_util7.styleText)(["bgMagentaBright", "black"], " Agent, rules!"));
const codeLanguage = "nodejs";
debug7("Selected code language:", codeLanguage);
const aiApp = await (0, import_prompts.select)({
message: "Which AI App would you like to generate agentic rules for?",
options: [
{ value: "claude-code", label: "Claude Code" },
{ value: "github-copilot", label: "GitHub Copilot" },
{ value: "cursor", label: "Cursor" }
],
initialValue: "github-copilot"
});
if (typeof aiApp === "symbol") {
throw new Error("Operation cancelled by user");
}
debug7("Selected AI App:", aiApp);
const topicChoices = await (0, import_prompts.multiselect)({
message: "Which topic do you want to generate agentic rules for?",
options: [
{ value: "nodejs-dev", label: "Node.js Development", hint: "General best practices for Node.js application development" },
{ value: "secure-code", label: "Secure Coding", hint: "Apply security best practices for defensive coding in Node.js" },
{ value: "security-vulnerabilities", label: "Security Vulnerabilities", hint: "Scan and fix security vulnerabilities in Node.js application code and 3rd-party dependencies" },
{ value: "testing", label: "Testing", hint: "Establish mature testing strategy and test code guidelines in Node.js applications" }
],
required: true
});
if (typeof topicChoices === "symbol") {
throw new Error("Operation cancelled by user");
}
debug7("Selected code topic: ", topicChoices.join(", "));
const includeMcp = await (0, import_prompts.select)({
message: "Include MCP server configuration?",
options: [
{ value: true, label: "Yes", hint: "Add recommended MCP servers for improved agentic coding" },
{ value: false, label: "No", hint: "Skip MCP configuration" }
],
initialValue: false
});
if (typeof includeMcp === "symbol") {
throw new Error("Operation cancelled by user");
}
debug7("Include MCP configuration:", includeMcp);
const includeCommands = await (0, import_prompts.select)({
message: "Include custom commands?",
options: [
{ value: true, label: "Yes", hint: "Add custom commands for this AI app" },
{ value: false, label: "No", hint: "Skip commands configuration" }
],
initialValue: false
});
if (typeof includeCommands === "symbol") {
throw new Error("Operation cancelled by user");
}
debug7("Include commands configuration:", includeCommands);
for (const codeTopic of topicChoices) {
const templateChoices = {
aiApp,
codeLanguage,
codeTopic,
includeMcp,
includeCommands
};
await scaffoldAiAppInstructions(templateChoices);
}
(0, import_prompts.outro)("Aye Captain, godspeed with yar vibe coding \u{1FAE1}");
}
__name(initInteractive, "initInteractive");
async function initWithCliArgs(args) {
const codeLanguage = "nodejs";
debug7("CLI mode - Selected AI App:", args.app);
debug7("CLI mode - Selected code topics:", args.topics?.join(", "));
debug7("CLI mode - Include MCP configuration:", args.mcp);
debug7("CLI mode - Include commands configuration:", args.commands);
for (const codeTopic of args.topics) {
const templateChoices = {
aiApp: args.app,
codeLanguage,
codeTopic,
includeMcp: args.mcp || false,
includeCommands: args.commands || false
};
await scaffoldAiAppInstructions(templateChoices);
}
console.log("\u2705 Agent rules generated successfully!");
}
__name(initWithCliArgs, "initWithCliArgs");
async function init() {
const args = parseCommandLineArgs();
if (args.help) {
showHelp();
return;
}
if (args.version) {
await showVersion();
return;
}
if (args.app || args.topics) {
validateCliArgs(args);
await initWithCliArgs(args);
} else {
await initInteractive();
}
}
__name(init, "init");
async function main() {
try {
await init();
} catch (error) {
debug7("Full error details:", error);
console.error("\nerror: %s", error.message || error);
console.error("\n\n\u{1F635} Shiver me timbers!\n");
process.exit(1);
}
}
__name(main, "main");
main();