Skip to content
113 changes: 104 additions & 9 deletions src/core/prompts/sections/custom-instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import path from "path"

import { LANGUAGES, isLanguage } from "../../../shared/language"

/**
* Safely read a file and return its trimmed content
*/
async function safeReadFile(filePath: string): Promise<string> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is identical to the existing implementation. Consider importing and reusing the existing function instead.

try {
const content = await fs.readFile(filePath, "utf-8")
Expand All @@ -16,7 +19,81 @@ async function safeReadFile(filePath: string): Promise<string> {
}
}

/**
* Check if a directory exists
*/
async function directoryExists(dirPath: string): Promise<boolean> {
try {
const stats = await fs.stat(dirPath)
return stats.isDirectory()
} catch (err) {
return false
}
}

/**
* Read all text files from a directory in alphabetical order
*/
async function readTextFilesFromDirectory(dirPath: string): Promise<Array<{ filename: string; content: string }>> {
try {
const files = await fs
.readdir(dirPath, { withFileTypes: true, recursive: true })
.then((files) => files.filter((file) => file.isFile()))
.then((files) => files.map((file) => path.resolve(dirPath, file.name)))

const fileContents = await Promise.all(
files.map(async (file) => {
const filePath = path.join(dirPath, file)
try {
// Check if it's a file (not a directory)
const stats = await fs.stat(filePath)
if (stats.isFile()) {
const content = await safeReadFile(filePath)
return { filename: file, content }
}
return null
} catch (err) {
return null
}
}),
)

// Filter out null values (directories or failed reads)
return fileContents.filter((item): item is { filename: string; content: string } => item !== null)
} catch (err) {
return []
}
}

/**
* Format content from multiple files with filenames as headers
*/
function formatDirectoryContent(dirPath: string, files: Array<{ filename: string; content: string }>): string {
if (files.length === 0) return ""

const formattedContent = files
.map((file) => {
return `# Filename: ${file.filename}\n${file.content}`
})
.join("\n\n")

return `\n# Rules from ${path.relative(process.cwd(), dirPath)}:\n${formattedContent}\n`
}

/**
* Load rule files from the specified directory
*/
export async function loadRuleFiles(cwd: string): Promise<string> {
// Check for .roo/rules/ directory
const rooRulesDir = path.join(cwd, ".roo", "rules")
if (await directoryExists(rooRulesDir)) {
const files = await readTextFilesFromDirectory(rooRulesDir)
if (files.length > 0) {
return formatDirectoryContent(rooRulesDir, files)
}
}

// Fall back to existing behavior
const ruleFiles = [".roorules", ".clinerules"]

for (const file of ruleFiles) {
Expand All @@ -41,16 +118,30 @@ export async function addCustomInstructions(
// Load mode-specific rules if mode is provided
let modeRuleContent = ""
let usedRuleFile = ""

if (mode) {
const rooModeRuleFile = `.roorules-${mode}`
modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile))
if (modeRuleContent) {
usedRuleFile = rooModeRuleFile
} else {
const clineModeRuleFile = `.clinerules-${mode}`
modeRuleContent = await safeReadFile(path.join(cwd, clineModeRuleFile))
// Check for .roo/rules-${mode}/ directory
const modeRulesDir = path.join(cwd, ".roo", `rules-${mode}`)
if (await directoryExists(modeRulesDir)) {
const files = await readTextFilesFromDirectory(modeRulesDir)
if (files.length > 0) {
modeRuleContent = formatDirectoryContent(modeRulesDir, files)
usedRuleFile = modeRulesDir
}
}

// If no directory exists, fall back to existing behavior
if (!modeRuleContent) {
const rooModeRuleFile = `.roorules-${mode}`
modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile))
if (modeRuleContent) {
usedRuleFile = clineModeRuleFile
usedRuleFile = rooModeRuleFile
} else {
const clineModeRuleFile = `.clinerules-${mode}`
modeRuleContent = await safeReadFile(path.join(cwd, clineModeRuleFile))
if (modeRuleContent) {
usedRuleFile = clineModeRuleFile
}
}
}
}
Expand Down Expand Up @@ -78,7 +169,11 @@ export async function addCustomInstructions(

// Add mode-specific rules first if they exist
if (modeRuleContent && modeRuleContent.trim()) {
rules.push(`# Rules from ${usedRuleFile}:\n${modeRuleContent}`)
if (usedRuleFile.includes(path.join(".roo", `rules-${mode}`))) {
rules.push(modeRuleContent.trim())
} else {
rules.push(`# Rules from ${usedRuleFile}:\n${modeRuleContent}`)
}
}

if (options.rooIgnoreInstructions) {
Expand Down