Compare commits

...

5 Commits

Author SHA1 Message Date
0xtoshii 2489249fc6 wrap recursive dir 2025-05-02 12:06:32 -07:00
0xtoshii 8d112900f3 changeset 2025-05-01 23:51:04 -07:00
0xtoshii e0d8236a89 base 2 2025-05-01 23:44:08 -07:00
0xtoshii bfe8038682 task call 2025-05-01 22:57:41 -07:00
0xtoshii 56b5f718b7 base 2025-05-01 17:56:43 -07:00
8 changed files with 292 additions and 87 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
allow cursorrules and windsurfrules
@@ -6,6 +6,7 @@ import fs from "fs/promises"
import { ClineRulesToggles } from "@shared/cline-rules"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
import * as vscode from "vscode"
import { synchronizeRuleToggles, getRuleFilesTotalContent } from "@core/context/instructions/user-instructions/rule-helpers"
/**
* Converts .clinerules file to directory and places old .clinerule file inside directory, renaming it
@@ -51,11 +52,7 @@ export const getGlobalClineRules = async (globalClineRulesFilePath: string, togg
if (await isDirectory(globalClineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(globalClineRulesFilePath)
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(
rulesFilePaths,
globalClineRulesFilePath,
toggles,
)
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, globalClineRulesFilePath, toggles)
if (rulesFilesTotalContent) {
const clineRulesFileInstructions = formatResponse.clineRulesGlobalDirectoryInstructions(
globalClineRulesFilePath,
@@ -84,7 +81,7 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
if (await isDirectory(clineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(clineRulesFilePath)
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, cwd, toggles)
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
}
@@ -108,86 +105,6 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
return clineRulesFileInstructions
}
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
const ruleFilesTotalContent = await Promise.all(
rulesFilePaths.map(async (filePath) => {
const ruleFilePath = path.resolve(basePath, filePath)
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
if (ruleFilePath in toggles && toggles[ruleFilePath] === false) {
return null
}
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
}),
).then((contents) => contents.filter(Boolean).join("\n\n"))
return ruleFilesTotalContent
}
export async function synchronizeRuleToggles(
rulesDirectoryPath: string,
currentToggles: ClineRulesToggles,
): Promise<ClineRulesToggles> {
// Create a copy of toggles to modify
const updatedToggles = { ...currentToggles }
try {
const pathExists = await fileExistsAtPath(rulesDirectoryPath)
if (pathExists) {
const isDir = await isDirectory(rulesDirectoryPath)
if (isDir) {
// DIRECTORY CASE
const filePaths = await readDirectory(rulesDirectoryPath)
const existingRulePaths = new Set<string>()
for (const filePath of filePaths) {
const ruleFilePath = path.resolve(rulesDirectoryPath, filePath)
existingRulePaths.add(ruleFilePath)
const pathHasToggle = ruleFilePath in updatedToggles
if (!pathHasToggle) {
updatedToggles[ruleFilePath] = true
}
}
// Clean up toggles for non-existent files
for (const togglePath in updatedToggles) {
const pathExists = existingRulePaths.has(togglePath)
if (!pathExists) {
delete updatedToggles[togglePath]
}
}
} else {
// FILE CASE
// Add toggle for this file
const pathHasToggle = rulesDirectoryPath in updatedToggles
if (!pathHasToggle) {
updatedToggles[rulesDirectoryPath] = true
}
// Remove toggles for any other paths
for (const togglePath in updatedToggles) {
if (togglePath !== rulesDirectoryPath) {
delete updatedToggles[togglePath]
}
}
}
} else {
// PATH DOESN'T EXIST CASE
// Clear all toggles since the path doesn't exist
for (const togglePath in updatedToggles) {
delete updatedToggles[togglePath]
}
}
} catch (error) {
console.error(`Failed to synchronize rule toggles for path: ${rulesDirectoryPath}`, error)
}
return updatedToggles
}
export async function refreshClineRulesToggles(
context: vscode.ExtensionContext,
workingDirectory: string,
@@ -0,0 +1,120 @@
import path from "path"
import fs from "fs/promises"
import { GlobalFileNames } from "@core/storage/disk"
import { fileExistsAtPath, isDirectory } from "@utils/fs"
import { formatResponse } from "@core/prompts/responses"
import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
import {
synchronizeRuleToggles,
combineRuleToggles,
getRuleFilesTotalContent,
readDirectoryRecursive,
} from "@core/context/instructions/user-instructions/rule-helpers"
import { ClineRulesToggles } from "@shared/cline-rules"
import * as vscode from "vscode"
/**
* Refreshes the toggles for windsurf and cursor rules
*/
export async function refreshExternalRulesToggles(
context: vscode.ExtensionContext,
workingDirectory: string,
): Promise<{
windsurfLocalToggles: ClineRulesToggles
cursorLocalToggles: ClineRulesToggles
}> {
// local windsurf toggles
const localWindsurfRulesToggles = ((await getWorkspaceState(context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
const localWindsurfRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.windsurfRules)
const updatedLocalWindsurfToggles = await synchronizeRuleToggles(localWindsurfRulesFilePath, localWindsurfRulesToggles)
await updateWorkspaceState(context, "localWindsurfRulesToggles", updatedLocalWindsurfToggles)
// local cursor toggles
const localCursorRulesToggles = ((await getWorkspaceState(context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
// cursor has two valid locations for rules files, so we need to check both and combine
// synchronizeRuleToggles will drop whichever rules files are not in each given path, but combining the results will result in no data loss
let localCursorRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.cursorRulesDir)
const updatedLocalCursorToggles1 = await synchronizeRuleToggles(localCursorRulesFilePath, localCursorRulesToggles, ".mdc")
localCursorRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.cursorRulesFile)
const updatedLocalCursorToggles2 = await synchronizeRuleToggles(localCursorRulesFilePath, localCursorRulesToggles)
const updatedLocalCursorToggles = combineRuleToggles(updatedLocalCursorToggles1, updatedLocalCursorToggles2)
await updateWorkspaceState(context, "localCursorRulesToggles", updatedLocalCursorToggles)
return {
windsurfLocalToggles: updatedLocalWindsurfToggles,
cursorLocalToggles: updatedLocalCursorToggles,
}
}
/**
* Gather formatted windsurf rules
*/
export const getLocalWindsurfRules = async (cwd: string, toggles: ClineRulesToggles) => {
const windsurfRulesFilePath = path.resolve(cwd, GlobalFileNames.windsurfRules)
let windsurfRulesFileInstructions: string | undefined
if (await fileExistsAtPath(windsurfRulesFilePath)) {
if (!(await isDirectory(windsurfRulesFilePath))) {
try {
if (windsurfRulesFilePath in toggles && toggles[windsurfRulesFilePath] !== false) {
const ruleFileContent = (await fs.readFile(windsurfRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
windsurfRulesFileInstructions = formatResponse.windsurfRulesLocalFileInstructions(cwd, ruleFileContent)
}
}
} catch {
console.error(`Failed to read .windsurfrules file at ${windsurfRulesFilePath}`)
}
}
}
return windsurfRulesFileInstructions
}
/**
* Gather formatted cursor rules, which can come from two sources
*/
export const getLocalCursorRules = async (cwd: string, toggles: ClineRulesToggles) => {
// we first check for the .cursorrules file
const cursorRulesFilePath = path.resolve(cwd, GlobalFileNames.cursorRulesFile)
let cursorRulesFileInstructions: string | undefined
if (await fileExistsAtPath(cursorRulesFilePath)) {
if (!(await isDirectory(cursorRulesFilePath))) {
try {
if (cursorRulesFilePath in toggles && toggles[cursorRulesFilePath] !== false) {
const ruleFileContent = (await fs.readFile(cursorRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
cursorRulesFileInstructions = formatResponse.cursorRulesLocalFileInstructions(cwd, ruleFileContent)
}
}
} catch {
console.error(`Failed to read .cursorrules file at ${cursorRulesFilePath}`)
}
}
}
// we then check for the .cursor/rules dir
const cursorRulesDirPath = path.resolve(cwd, GlobalFileNames.cursorRulesDir)
let cursorRulesDirInstructions: string | undefined
if (await fileExistsAtPath(cursorRulesDirPath)) {
if (await isDirectory(cursorRulesDirPath)) {
try {
const rulesFilePaths = await readDirectoryRecursive(cursorRulesDirPath, ".mdc")
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
cursorRulesDirInstructions = formatResponse.cursorRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
}
} catch {
console.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}`)
}
}
}
return [cursorRulesFileInstructions, cursorRulesDirInstructions]
}
@@ -0,0 +1,121 @@
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
import * as path from "path"
import fs from "fs/promises"
import { ClineRulesToggles } from "@shared/cline-rules"
/**
* Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
*/
export async function readDirectoryRecursive(directoryPath: string, allowedFileExtension: string): Promise<string[]> {
try {
const entries = await readDirectory(directoryPath)
let results: string[] = []
for (const entry of entries) {
if (allowedFileExtension !== "") {
const fileExtension = path.extname(entry)
if (fileExtension !== allowedFileExtension) {
continue
}
}
results.push(entry)
}
return results
} catch (error) {
console.error(`Error reading directory ${directoryPath}: ${error}`)
return []
}
}
/**
* Gets the up to date toggles
*/
export async function synchronizeRuleToggles(
rulesDirectoryPath: string,
currentToggles: ClineRulesToggles,
allowedFileExtension: string = "",
): Promise<ClineRulesToggles> {
// Create a copy of toggles to modify
const updatedToggles = { ...currentToggles }
try {
const pathExists = await fileExistsAtPath(rulesDirectoryPath)
if (pathExists) {
const isDir = await isDirectory(rulesDirectoryPath)
if (isDir) {
// DIRECTORY CASE
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension)
const existingRulePaths = new Set<string>()
for (const filePath of filePaths) {
const ruleFilePath = path.resolve(rulesDirectoryPath, filePath)
existingRulePaths.add(ruleFilePath)
const pathHasToggle = ruleFilePath in updatedToggles
if (!pathHasToggle) {
updatedToggles[ruleFilePath] = true
}
}
// Clean up toggles for non-existent files
for (const togglePath in updatedToggles) {
const pathExists = existingRulePaths.has(togglePath)
if (!pathExists) {
delete updatedToggles[togglePath]
}
}
} else {
// FILE CASE
// Add toggle for this file
const pathHasToggle = rulesDirectoryPath in updatedToggles
if (!pathHasToggle) {
updatedToggles[rulesDirectoryPath] = true
}
// Remove toggles for any other paths
for (const togglePath in updatedToggles) {
if (togglePath !== rulesDirectoryPath) {
delete updatedToggles[togglePath]
}
}
}
} else {
// PATH DOESN'T EXIST CASE
// Clear all toggles since the path doesn't exist
for (const togglePath in updatedToggles) {
delete updatedToggles[togglePath]
}
}
} catch (error) {
console.error(`Failed to synchronize rule toggles for path: ${rulesDirectoryPath}`, error)
}
return updatedToggles
}
/**
* Certain project rules have more than a single location where rules are allowed to be stored
*/
export function combineRuleToggles(toggles1: ClineRulesToggles, toggles2: ClineRulesToggles): ClineRulesToggles {
return { ...toggles1, ...toggles2 }
}
/**
* Read the content of rules files
*/
export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
const ruleFilesTotalContent = await Promise.all(
rulesFilePaths.map(async (filePath) => {
const ruleFilePath = path.resolve(basePath, filePath)
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
if (ruleFilePath in toggles && toggles[ruleFilePath] === false) {
return null
}
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
}),
).then((contents) => contents.filter(Boolean).join("\n\n"))
return ruleFilesTotalContent
}
+9
View File
@@ -215,6 +215,15 @@ Otherwise, if you have not completed the task and do not need additional informa
clineRulesLocalFileInstructions: (cwd: string, content: string) =>
`# .clinerules\n\nThe following is provided by a root-level .clinerules file where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
windsurfRulesLocalFileInstructions: (cwd: string, content: string) =>
`# .windsurfrules\n\nThe following is provided by a root-level .windsurfrules file where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
cursorRulesLocalFileInstructions: (cwd: string, content: string) =>
`# .cursorrules\n\nThe following is provided by a root-level .cursorrules file where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
cursorRulesLocalDirectoryInstructions: (cwd: string, content: string) =>
`# .cursor/rules\n\nThe following is provided by a root-level .cursor/rules directory where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
}
// to avoid circular dependency
+12
View File
@@ -635,6 +635,9 @@ export function addUserInstructions(
settingsCustomInstructions?: string,
globalClineRulesFileInstructions?: string,
localClineRulesFileInstructions?: string,
localCursorRulesFileInstructions?: string,
localCursorRulesDirInstructions?: string,
localWindsurfRulesFileInstructions?: string,
clineIgnoreInstructions?: string,
preferredLanguageInstructions?: string,
) {
@@ -651,6 +654,15 @@ export function addUserInstructions(
if (localClineRulesFileInstructions) {
customInstructions += localClineRulesFileInstructions + "\n\n"
}
if (localCursorRulesFileInstructions) {
customInstructions += localCursorRulesFileInstructions + "\n\n"
}
if (localCursorRulesDirInstructions) {
customInstructions += localCursorRulesDirInstructions + "\n\n"
}
if (localWindsurfRulesFileInstructions) {
customInstructions += localWindsurfRulesFileInstructions + "\n\n"
}
if (clineIgnoreInstructions) {
customInstructions += clineIgnoreInstructions
}
+3
View File
@@ -15,6 +15,9 @@ export const GlobalFileNames = {
openRouterModels: "openrouter_models.json",
mcpSettings: "cline_mcp_settings.json",
clineRules: ".clinerules",
cursorRulesDir: ".cursor/rules",
cursorRulesFile: ".cursorrules",
windsurfRules: ".windsurfrules",
taskMetadata: "task_metadata.json",
}
+19 -1
View File
@@ -88,6 +88,11 @@ import {
refreshClineRulesToggles,
ensureLocalClinerulesDirExists,
} from "@core/context/instructions/user-instructions/cline-rules"
import {
refreshExternalRulesToggles,
getLocalWindsurfRules,
getLocalCursorRules,
} from "@core/context/instructions/user-instructions/external-rules"
import { getGlobalState } from "@core/storage/state"
import { parseSlashCommands } from "@core/slash-commands"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
@@ -1444,11 +1449,17 @@ export class Task {
: ""
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.getContext(), cwd)
const { windsurfLocalToggles, cursorLocalToggles } = await refreshExternalRulesToggles(this.getContext(), cwd)
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath, globalToggles)
const localClineRulesFileInstructions = await getLocalClineRules(cwd, localToggles)
const [localCursorRulesFileInstructions, localCursorRulesDirInstructions] = await getLocalCursorRules(
cwd,
cursorLocalToggles,
)
const localWindsurfRulesFileInstructions = await getLocalWindsurfRules(cwd, windsurfLocalToggles)
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
let clineIgnoreInstructions: string | undefined
@@ -1460,17 +1471,24 @@ export class Task {
settingsCustomInstructions ||
globalClineRulesFileInstructions ||
localClineRulesFileInstructions ||
localCursorRulesFileInstructions ||
localCursorRulesDirInstructions ||
localWindsurfRulesFileInstructions ||
clineIgnoreInstructions ||
preferredLanguageInstructions
) {
// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
systemPrompt += addUserInstructions(
const userInstructions = addUserInstructions(
settingsCustomInstructions,
globalClineRulesFileInstructions,
localClineRulesFileInstructions,
localCursorRulesFileInstructions,
localCursorRulesDirInstructions,
localWindsurfRulesFileInstructions,
clineIgnoreInstructions,
preferredLanguageInstructions,
)
systemPrompt += userInstructions
}
const contextManagementMetadata = await this.contextManager.getNewContextMessagesAndMetadata(
this.apiConversationHistory,