Skip to content

Commit 73078d6

Browse files
Pirate Mode Activated (RooCodeInc#2890)
* add fetching global cline rules files * add toggle functionality to clinerules * selectively filter out OS generated files from read directory * remove .file filtering * remove duplicate imports * pass path to global rules directory in system prompt * empty commit to trigger tests
1 parent 89cbbe9 commit 73078d6

File tree

6 files changed

+138
-23
lines changed

6 files changed

+138
-23
lines changed

src/core/context/instructions/user-instructions/cline-rules.ts

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,25 @@ import { fileExistsAtPath, isDirectory, readDirectory } from "../../../../utils/
44
import { formatResponse } from "../../../prompts/responses"
55
import fs from "fs/promises"
66

7-
export const getGlobalClineRules = async (globalClineRulesFilePath: string) => {
7+
export type ClineRulesToggles = Record<string, boolean> // filepath -> enabled/disabled
8+
9+
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
810
if (await fileExistsAtPath(globalClineRulesFilePath)) {
911
if (await isDirectory(globalClineRulesFilePath)) {
1012
try {
1113
const rulesFilePaths = await readDirectory(globalClineRulesFilePath)
12-
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, globalClineRulesFilePath)
13-
const clineRulesFileInstructions = formatResponse.clineRulesGlobalDirectoryInstructions(rulesFilesTotalContent)
14-
return clineRulesFileInstructions
14+
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(
15+
rulesFilePaths,
16+
globalClineRulesFilePath,
17+
toggles,
18+
)
19+
if (rulesFilesTotalContent) {
20+
const clineRulesFileInstructions = formatResponse.clineRulesGlobalDirectoryInstructions(
21+
globalClineRulesFilePath,
22+
rulesFilesTotalContent,
23+
)
24+
return clineRulesFileInstructions
25+
}
1526
} catch {
1627
console.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
1728
}
@@ -24,25 +35,29 @@ export const getGlobalClineRules = async (globalClineRulesFilePath: string) => {
2435
return undefined
2536
}
2637

27-
export const getLocalClineRules = async (cwd: string) => {
38+
export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles) => {
2839
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
2940

3041
let clineRulesFileInstructions: string | undefined
3142

3243
if (await fileExistsAtPath(clineRulesFilePath)) {
3344
if (await isDirectory(clineRulesFilePath)) {
3445
try {
35-
const rulesFilePaths = await readDirectory(path.join(cwd, GlobalFileNames.clineRules))
36-
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, cwd)
37-
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
46+
const rulesFilePaths = await readDirectory(clineRulesFilePath)
47+
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, cwd, toggles)
48+
if (rulesFilesTotalContent) {
49+
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
50+
}
3851
} catch {
3952
console.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
4053
}
4154
} else {
4255
try {
43-
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
44-
if (ruleFileContent) {
45-
clineRulesFileInstructions = formatResponse.clineRulesLocalFileInstructions(cwd, ruleFileContent)
56+
if (clineRulesFilePath in toggles && toggles[clineRulesFilePath] !== false) {
57+
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
58+
if (ruleFileContent) {
59+
clineRulesFileInstructions = formatResponse.clineRulesLocalFileInstructions(cwd, ruleFileContent)
60+
}
4661
}
4762
} catch {
4863
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
@@ -53,13 +68,82 @@ export const getLocalClineRules = async (cwd: string) => {
5368
return clineRulesFileInstructions
5469
}
5570

56-
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], basePath: string) => {
71+
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
5772
const ruleFilesTotalContent = await Promise.all(
5873
rulesFilePaths.map(async (filePath) => {
5974
const ruleFilePath = path.resolve(basePath, filePath)
6075
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
76+
77+
if (ruleFilePath in toggles && toggles[ruleFilePath] === false) {
78+
return null
79+
}
80+
6181
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
6282
}),
63-
).then((contents) => contents.join("\n\n"))
83+
).then((contents) => contents.filter(Boolean).join("\n\n"))
6484
return ruleFilesTotalContent
6585
}
86+
87+
export async function synchronizeRuleToggles(
88+
rulesDirectoryPath: string,
89+
currentToggles: ClineRulesToggles,
90+
): Promise<ClineRulesToggles> {
91+
// Create a copy of toggles to modify
92+
const updatedToggles = { ...currentToggles }
93+
94+
try {
95+
const pathExists = await fileExistsAtPath(rulesDirectoryPath)
96+
97+
if (pathExists) {
98+
const isDir = await isDirectory(rulesDirectoryPath)
99+
100+
if (isDir) {
101+
// DIRECTORY CASE
102+
const filePaths = await readDirectory(rulesDirectoryPath)
103+
const existingRulePaths = new Set<string>()
104+
105+
for (const filePath of filePaths) {
106+
const ruleFilePath = path.resolve(rulesDirectoryPath, filePath)
107+
existingRulePaths.add(ruleFilePath)
108+
109+
const pathHasToggle = ruleFilePath in updatedToggles
110+
if (!pathHasToggle) {
111+
updatedToggles[ruleFilePath] = true
112+
}
113+
}
114+
115+
// Clean up toggles for non-existent files
116+
for (const togglePath in updatedToggles) {
117+
const pathExists = existingRulePaths.has(togglePath)
118+
if (!pathExists) {
119+
delete updatedToggles[togglePath]
120+
}
121+
}
122+
} else {
123+
// FILE CASE
124+
// Add toggle for this file
125+
const pathHasToggle = rulesDirectoryPath in updatedToggles
126+
if (!pathHasToggle) {
127+
updatedToggles[rulesDirectoryPath] = true
128+
}
129+
130+
// Remove toggles for any other paths
131+
for (const togglePath in updatedToggles) {
132+
if (togglePath !== rulesDirectoryPath) {
133+
delete updatedToggles[togglePath]
134+
}
135+
}
136+
}
137+
} else {
138+
// PATH DOESN'T EXIST CASE
139+
// Clear all toggles since the path doesn't exist
140+
for (const togglePath in updatedToggles) {
141+
delete updatedToggles[togglePath]
142+
}
143+
}
144+
} catch (error) {
145+
console.error(`Failed to synchronize rule toggles for path: ${rulesDirectoryPath}`, error)
146+
}
147+
148+
return updatedToggles
149+
}

src/core/prompts/responses.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ Otherwise, if you have not completed the task and do not need additional informa
204204
clineIgnoreInstructions: (content: string) =>
205205
`# .clineignore\n\n(The following is provided by a root-level .clineignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${content}\n.clineignore`,
206206

207-
clineRulesGlobalDirectoryInstructions: (content: string) =>
208-
`# .clinerules/\n\nThe following is provided by a global .clinerules/ directory where the user has specified instructions:\n\n${content}`,
207+
clineRulesGlobalDirectoryInstructions: (globalClineRulesFilePath: string, content: string) =>
208+
`# .clinerules/\n\nThe following is provided by a global .clinerules/ directory, located at ${globalClineRulesFilePath.toPosix()}, where the user has specified instructions for all working directories:\n\n${content}`,
209209

210210
clineRulesLocalDirectoryInstructions: (cwd: string, content: string) =>
211211
`# .clinerules/\n\nThe following is provided by a root-level .clinerules/ directory where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,

src/core/storage/state-keys.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type SecretKey =
1919
| "asksageApiKey"
2020
| "xaiApiKey"
2121
| "sambanovaApiKey"
22+
2223
export type GlobalStateKey =
2324
| "apiProvider"
2425
| "apiModelId"
@@ -47,6 +48,7 @@ export type GlobalStateKey =
4748
| "openRouterModelInfo"
4849
| "openRouterProviderSorting"
4950
| "autoApprovalSettings"
51+
| "globalClineRulesToggles"
5052
| "browserSettings"
5153
| "chatSettings"
5254
| "vsCodeLmModelSelector"
@@ -71,3 +73,5 @@ export type GlobalStateKey =
7173
| "reasoningEffort"
7274
| "planActSeparateModelsSetting"
7375
| "favoritedModelIds"
76+
77+
export type LocalStateKey = "localClineRulesToggles"

src/core/storage/state.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { BrowserSettings } from "../../shared/BrowserSettings"
1010
import { ChatSettings } from "../../shared/ChatSettings"
1111
import { TelemetrySetting } from "../../shared/TelemetrySetting"
1212
import { UserInfo } from "../../shared/UserInfo"
13+
import { ClineRulesToggles } from "../context/instructions/user-instructions/cline-rules"
1314
/*
1415
Storage
1516
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
@@ -97,6 +98,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
9798
customInstructions,
9899
taskHistory,
99100
autoApprovalSettings,
101+
globalClineRulesToggles,
100102
browserSettings,
101103
chatSettings,
102104
vsCodeLmModelSelector,
@@ -167,6 +169,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
167169
getGlobalState(context, "customInstructions") as Promise<string | undefined>,
168170
getGlobalState(context, "taskHistory") as Promise<HistoryItem[] | undefined>,
169171
getGlobalState(context, "autoApprovalSettings") as Promise<AutoApprovalSettings | undefined>,
172+
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
170173
getGlobalState(context, "browserSettings") as Promise<BrowserSettings | undefined>,
171174
getGlobalState(context, "chatSettings") as Promise<ChatSettings | undefined>,
172175
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
@@ -291,6 +294,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
291294
customInstructions,
292295
taskHistory,
293296
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
297+
clineRulesToggles: globalClineRulesToggles || {},
294298
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
295299
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
296300
userInfo,

src/core/task/index.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,20 +70,26 @@ import {
7070
checkIsAnthropicContextWindowError,
7171
checkIsOpenRouterContextWindowError,
7272
} from "../context/context-management/context-error-handling"
73-
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
74-
import { McpHub } from "../../services/mcp/McpHub"
7573
import { ContextManager } from "../context/context-management/ContextManager"
7674
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
7775
import {
7876
ensureRulesDirectoryExists,
7977
ensureTaskDirectoryExists,
8078
getSavedApiConversationHistory,
8179
getSavedClineMessages,
80+
GlobalFileNames,
8281
saveApiConversationHistory,
8382
saveClineMessages,
8483
} from "../storage/disk"
85-
import { getGlobalClineRules, getLocalClineRules } from "../context/instructions/user-instructions/cline-rules"
86-
import { getGlobalState } from "../storage/state"
84+
import { McpHub } from "../../services/mcp/McpHub"
85+
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
86+
import {
87+
ClineRulesToggles,
88+
getGlobalClineRules,
89+
getLocalClineRules,
90+
synchronizeRuleToggles,
91+
} from "../context/instructions/user-instructions/cline-rules"
92+
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "../storage/state"
8793

8894
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
8995

@@ -1280,10 +1286,19 @@ export class Task {
12801286
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
12811287
: ""
12821288

1283-
const localClineRulesFileInstructions = await getLocalClineRules(cwd)
1284-
1289+
const globalClineRulesToggles =
1290+
((await getGlobalState(this.getContext(), "globalClineRulesToggles")) as ClineRulesToggles) || {}
12851291
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
1286-
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath)
1292+
const updatedGlobalToggles = await synchronizeRuleToggles(globalClineRulesFilePath, globalClineRulesToggles)
1293+
await updateGlobalState(this.getContext(), "globalClineRulesToggles", updatedGlobalToggles)
1294+
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath, updatedGlobalToggles)
1295+
1296+
const localClineRulesToggles =
1297+
((await getWorkspaceState(this.getContext(), "localClineRulesToggles")) as ClineRulesToggles) || {}
1298+
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
1299+
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
1300+
await updateWorkspaceState(this.getContext(), "localClineRulesToggles", updatedLocalToggles)
1301+
const localClineRulesFileInstructions = await getLocalClineRules(cwd, updatedLocalToggles)
12871302

12881303
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
12891304
let clineIgnoreInstructions: string | undefined

src/utils/fs.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ export async function getFileSizeInKB(filePath: string): Promise<number> {
7575
}
7676
}
7777

78+
// Common OS-generated files that would appear in an otherwise clean directory
79+
const OS_GENERATED_FILES = [
80+
".DS_Store", // macOS Finder
81+
"Thumbs.db", // Windows Explorer thumbnails
82+
"desktop.ini", // Windows folder settings
83+
]
84+
7885
/**
7986
* Recursively reads a directory and returns an array of absolute file paths.
8087
*
@@ -86,7 +93,8 @@ export const readDirectory = async (directoryPath: string) => {
8693
try {
8794
const filePaths = await fs
8895
.readdir(directoryPath, { withFileTypes: true, recursive: true })
89-
.then((files) => files.filter((file) => file.isFile()))
96+
.then((entries) => entries.filter((entry) => !OS_GENERATED_FILES.includes(entry.name)))
97+
.then((entries) => entries.filter((entry) => entry.isFile()))
9098
.then((files) => files.map((file) => path.resolve(file.parentPath, file.name)))
9199
return filePaths
92100
} catch {

0 commit comments

Comments
 (0)