UNPKG

agent-rules

Version:

Rules and instructions for agentic coding tools like Cursor, Claude CLI, Gemini CLI, Qodo, Cline and more

891 lines (878 loc) 35.9 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/main.ts import path6 from "path"; import fs6 from "fs/promises"; import { fileURLToPath } from "url"; import { debuglog as debuglog6 } from "util"; // src/adapters/base-adapter.ts import path from "path"; import fs from "fs/promises"; import { debuglog } from "util"; var debug = 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 = path.join(resolvedMcpTemplateDirectory, "mcp.json"); const targetMcpFile = path.resolve(process.cwd(), mcpConfig.filePath); const mergeKey = mcpConfig.mergeKey || "mcpServers"; debug(`Processing MCP configuration from ${templateMcpFile} to ${targetMcpFile}`); try { const templateContent = await fs.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 fs.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 fs.mkdir(path.dirname(targetMcpFile), { recursive: true }); await fs.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 = path.resolve(process.cwd(), commandsConfig.targetDirectory); debug(`Processing commands from ${resolvedCommandsTemplateDirectory} to ${targetDirectory}`); try { await fs.mkdir(targetDirectory, { recursive: true }); const files = await fs.readdir(resolvedCommandsTemplateDirectory); const commandFiles = files.filter((file) => file.endsWith(".command.md")); for (const commandFile of commandFiles) { const sourceFilePath = path.join(resolvedCommandsTemplateDirectory, commandFile); const stat = await fs.stat(sourceFilePath); if (stat.isFile()) { const targetFileName = commandsConfig.fileNameTransform ? commandsConfig.fileNameTransform(commandFile) : commandFile; const targetFilePath = path.join(targetDirectory, targetFileName); const content = await fs.readFile(sourceFilePath, "utf-8"); await fs.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 import path2 from "path"; import fs2 from "fs/promises"; import { debuglog as debuglog2 } from "util"; var debug2 = debuglog2("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 fs2.readdir(resolvedTemplateDirectory); for (const templateFile of templateFiles) { const templateFilePath = path2.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 = path2.normalize(decodedPath); const sanitizedTemplateFile = path2.basename(normalizedPath); const fullTemplatePath = path2.join(path2.dirname(templateFilePath), sanitizedTemplateFile); debug2("Processing template file:", sanitizedTemplateFile); try { const stat = await fs2.stat(fullTemplatePath); if (stat.isFile()) { const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix); const targetPath = path2.join(targetFilePath, targetFileName); const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory); debug2("Writing template file to target path:", resolvedTargetFilePath); const templateContent = await fs2.readFile(fullTemplatePath, "utf-8"); await fs2.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 = path2.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 = path2.normalize(decodedPath); const resolvedTargetFilePath = path2.resolve(normalizedPath); if (!resolvedTargetFilePath.startsWith(baseDirectory)) { throw new Error(`Invalid target path: ${targetFilePath}`); } return resolvedTargetFilePath; } }; // src/adapters/cursor-adapter.ts import path3 from "path"; import fs3 from "fs/promises"; import { debuglog as debuglog3 } from "util"; import { fromMarkdown } from "mdast-util-from-markdown"; import { toMarkdown } from "mdast-util-to-markdown"; import { frontmatter } from "micromark-extension-frontmatter"; import { frontmatterFromMarkdown, frontmatterToMarkdown } from "mdast-util-frontmatter"; import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; var debug3 = debuglog3("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 fs3.readdir(resolvedTemplateDirectory); for (const templateFile of templateFiles) { const templateFilePath = path3.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 = path3.normalize(decodedPath); const sanitizedTemplateFile = path3.basename(normalizedPath); const fullTemplatePath = path3.join(path3.dirname(templateFilePath), sanitizedTemplateFile); debug3("Processing template file:", sanitizedTemplateFile); try { const stat = await fs3.stat(fullTemplatePath); if (stat.isFile()) { const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix); const targetPath = path3.join(targetFilePath, targetFileName); const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory); debug3("Writing template file to target path:", resolvedTargetFilePath); const templateContent = await fs3.readFile(fullTemplatePath, "utf-8"); const processedContent = this.processFrontmatter(templateContent); await fs3.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 = path3.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 = fromMarkdown(content, { extensions: [frontmatter(["yaml"])], mdastExtensions: [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 toMarkdown(ast, { extensions: [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 = parseYaml(frontmatterValue); if (frontmatterData && typeof frontmatterData === "object" && "applyTo" in frontmatterData) { const transformedData = { ...frontmatterData }; transformedData.globs = frontmatterData.applyTo; delete transformedData.applyTo; return stringifyYaml(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 = path3.normalize(decodedPath); const resolvedTargetFilePath = path3.resolve(normalizedPath); if (!resolvedTargetFilePath.startsWith(baseDirectory)) { throw new Error(`Invalid target path: ${targetFilePath}`); } return resolvedTargetFilePath; } }; // src/adapters/claude-code-adapter.ts import path4 from "path"; import fs4 from "fs/promises"; import { debuglog as debuglog4 } from "util"; import { fromMarkdown as fromMarkdown2 } from "mdast-util-from-markdown"; var debug4 = debuglog4("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 fs4.readdir(resolvedTemplateDirectory); for (const templateFile of templateFiles) { if (!templateFile.endsWith(".md")) { debug4("Skipping non-markdown file:", templateFile); continue; } const templateFilePath = path4.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 = path4.normalize(decodedPath); const sanitizedTemplateFile = path4.basename(normalizedPath); const fullTemplatePath = path4.join(path4.dirname(templateFilePath), sanitizedTemplateFile); debug4("Processing template file:", sanitizedTemplateFile); try { const stat = await fs4.stat(fullTemplatePath); if (stat.isFile()) { const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix); const targetPath = path4.join(targetFilePath, targetFileName); const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory); debug4("Writing template file to target path:", resolvedTargetFilePath); const templateContent = await fs4.readFile(fullTemplatePath, "utf-8"); await fs4.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 = path4.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 = path4.dirname(path4.dirname(resolvedTargetDirectory)); const claudeMainFilePath = path4.join(projectRoot, _ClaudeCodeAdapter.CLAUDE_MAIN_FILE); debug4("Claude main file path:", claudeMainFilePath); let claudeContent = ""; try { claudeContent = await fs4.readFile(claudeMainFilePath, "utf-8"); } catch (error) { debug4("CLAUDE.md not found, creating new file"); claudeContent = ""; } const templateFiles = await fs4.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 fs4.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 { fromMarkdown2(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 = path4.normalize(decodedPath); const resolvedTargetFilePath = path4.resolve(normalizedPath); if (!resolvedTargetFilePath.startsWith(baseDirectory)) { throw new Error(`Invalid target path: ${targetFilePath}`); } return resolvedTargetFilePath; } }; // src/adapters/gemini-adapter.ts import path5 from "path"; import fs5 from "fs/promises"; import { debuglog as debuglog5 } from "util"; import { fromMarkdown as fromMarkdown3 } from "mdast-util-from-markdown"; var debug5 = debuglog5("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 fs5.readdir(resolvedTemplateDirectory); for (const templateFile of templateFiles) { if (!templateFile.endsWith(".md")) { debug5("Skipping non-markdown file:", templateFile); continue; } const templateFilePath = path5.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 = path5.normalize(decodedPath); const sanitizedTemplateFile = path5.basename(normalizedPath); const fullTemplatePath = path5.join(path5.dirname(templateFilePath), sanitizedTemplateFile); debug5("Processing template file:", sanitizedTemplateFile); try { const stat = await fs5.stat(fullTemplatePath); if (stat.isFile()) { const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix); const targetPath = path5.join(targetFilePath, targetFileName); const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory); debug5("Writing template file to target path:", resolvedTargetFilePath); const templateContent = await fs5.readFile(fullTemplatePath, "utf-8"); const processedContent = this.stripFrontmatter(templateContent); await fs5.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 = path5.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 = path5.dirname(path5.dirname(resolvedTargetDirectory)); const geminiMainFilePath = path5.join(projectRoot, _GeminiAdapter.GEMINI_MAIN_FILE); debug5("Gemini main file path:", geminiMainFilePath); let geminiContent = ""; try { geminiContent = await fs5.readFile(geminiMainFilePath, "utf-8"); } catch (error) { debug5("GEMINI.md not found, creating new file"); geminiContent = ""; } const templateFiles = await fs5.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 fs5.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 { fromMarkdown3(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 = path5.normalize(decodedPath); const resolvedTargetFilePath = path5.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 debug6 = debuglog6("agent-rules"); var templateRoot = "__template__"; function resolvePackageRootDirectoryForTemplates() { let guessedDirName = ""; try { if (typeof import.meta !== "undefined" && import.meta.url) { const __filename = fileURLToPath(import.meta.url); guessedDirName = path6.dirname(__filename); } else { guessedDirName = __dirname; } } catch (error) { guessedDirName = __dirname; } if (guessedDirName.endsWith("src")) { return path6.resolve(guessedDirName, ".."); } else if (guessedDirName.endsWith("dist/bin") || guessedDirName.endsWith("dist\\bin")) { return path6.resolve(guessedDirName, ".."); } else { return guessedDirName; } } __name(resolvePackageRootDirectoryForTemplates, "resolvePackageRootDirectoryForTemplates"); function getAiAppDirectory(aiApp) { const adapter = AdapterRegistry.getAdapter(aiApp); return adapter.getConfig(); } __name(getAiAppDirectory, "getAiAppDirectory"); async function resolveTemplateDirectory(scaffoldInstructions) { const { codeLanguage, codeTopic } = scaffoldInstructions; const currentFileDirectory = resolvePackageRootDirectoryForTemplates(); const templateDirectory = path6.join(currentFileDirectory, templateRoot, codeLanguage, codeTopic); const resolvedTemplateDirectory = path6.resolve(templateDirectory); try { const templateStats = await fs6.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 = path6.join(currentFileDirectory, templateRoot, codeLanguage, "_mcp"); const resolvedMcpTemplateDirectory = path6.resolve(mcpTemplateDirectory); try { const templateStats = await fs6.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 = path6.join(currentFileDirectory, templateRoot, codeLanguage, "_commands"); const resolvedCommandsTemplateDirectory = path6.resolve(commandsTemplateDirectory); try { const templateStats = await fs6.stat(resolvedCommandsTemplateDirectory); if (!templateStats.isDirectory()) { return null; } return resolvedCommandsTemplateDirectory; } catch (error) { return null; } } __name(resolveCommandsTemplateDirectory, "resolveCommandsTemplateDirectory"); async function createTargetDirectory(directory) { const resolvedTargetDirectory = path6.resolve(directory); await fs6.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"); export { getAiAppDirectory, resolveCommandsTemplateDirectory, resolveMcpTemplateDirectory, resolveTemplateDirectory, scaffoldAiAppInstructions };