mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2489249fc6 | |||
| 8d112900f3 | |||
| e0d8236a89 | |||
| bfe8038682 | |||
| 56b5f718b7 |
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user