mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
Merge branch 'pass-controller-to-services' of https://github.com/cline/cline into pass-controller-to-services
This commit is contained in:
commit
427e1edcfd
11 changed files with 315 additions and 214 deletions
|
|
@ -2,4 +2,4 @@
|
|||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Introducing .clineignore
|
||||
Add .clineignore file to block Cline from accessing specified file patterns
|
||||
|
|
@ -9,6 +9,9 @@ import * as path from "path"
|
|||
import { serializeError } from "serialize-error"
|
||||
import * as vscode from "vscode"
|
||||
import { ApiHandler, buildApiHandler } from "../api"
|
||||
import { OpenAiHandler } from "../api/providers/openai"
|
||||
import { OpenRouterHandler } from "../api/providers/openrouter"
|
||||
import { ApiStream } from "../api/transform/stream"
|
||||
import CheckpointTracker from "../integrations/checkpoints/CheckpointTracker"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
|
||||
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
|
||||
|
|
@ -46,20 +49,16 @@ import { HistoryItem } from "../shared/HistoryItem"
|
|||
import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage"
|
||||
import { calculateApiCost } from "../utils/cost"
|
||||
import { fileExistsAtPath } from "../utils/fs"
|
||||
import { LLMFileAccessController } from "../services/llm-access-control/LLMFileAccessController"
|
||||
import { arePathsEqual, getReadablePath } from "../utils/path"
|
||||
import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string"
|
||||
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
|
||||
import { constructNewFileContent } from "./assistant-message/diff"
|
||||
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "./ignore/ClineIgnoreController"
|
||||
import { parseMentions } from "./mentions"
|
||||
import { formatResponse } from "./prompts/responses"
|
||||
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
|
||||
import { OpenRouterHandler } from "../api/providers/openrouter"
|
||||
import { addUserInstructions, SYSTEM_PROMPT } from "./prompts/system"
|
||||
import { getNextTruncationRange, getTruncatedMessages } from "./sliding-window"
|
||||
import { SYSTEM_PROMPT } from "./prompts/system"
|
||||
import { addUserInstructions } from "./prompts/system"
|
||||
import { OpenAiHandler } from "../api/providers/openai"
|
||||
import { ApiStream } from "../api/transform/stream"
|
||||
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -81,7 +80,7 @@ export class Cline {
|
|||
private chatSettings: ChatSettings
|
||||
apiConversationHistory: Anthropic.MessageParam[] = []
|
||||
clineMessages: ClineMessage[] = []
|
||||
private llmFileAccessController: LLMFileAccessController
|
||||
private clineIgnoreController: ClineIgnoreController
|
||||
private askResponse?: ClineAskResponse
|
||||
private askResponseText?: string
|
||||
private askResponseImages?: string[]
|
||||
|
|
@ -125,9 +124,9 @@ export class Cline {
|
|||
images?: string[],
|
||||
historyItem?: HistoryItem,
|
||||
) {
|
||||
this.llmFileAccessController = new LLMFileAccessController(cwd)
|
||||
this.llmFileAccessController.initialize().catch((error) => {
|
||||
console.error("Failed to initialize LLMFileAccessController:", error)
|
||||
this.clineIgnoreController = new ClineIgnoreController(cwd)
|
||||
this.clineIgnoreController.initialize().catch((error) => {
|
||||
console.error("Failed to initialize ClineIgnoreController:", error)
|
||||
})
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.api = buildApiHandler(apiConfiguration)
|
||||
|
|
@ -1057,7 +1056,7 @@ export class Cline {
|
|||
this.terminalManager.disposeAll()
|
||||
this.urlContentFetcher.closeBrowser()
|
||||
this.browserSession.closeBrowser()
|
||||
this.llmFileAccessController.dispose()
|
||||
this.clineIgnoreController.dispose()
|
||||
await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint
|
||||
}
|
||||
|
||||
|
|
@ -1242,9 +1241,15 @@ export class Cline {
|
|||
}
|
||||
}
|
||||
|
||||
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
|
||||
let clineIgnoreInstructions: string | undefined
|
||||
if (clineIgnoreContent) {
|
||||
clineIgnoreInstructions = `# .clineignore\n\nThe 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${clineIgnoreContent}`
|
||||
}
|
||||
|
||||
if (settingsCustomInstructions || clineRulesFileInstructions) {
|
||||
// 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(settingsCustomInstructions, clineRulesFileInstructions)
|
||||
systemPrompt += addUserInstructions(settingsCustomInstructions, clineRulesFileInstructions, clineIgnoreInstructions)
|
||||
}
|
||||
|
||||
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
|
||||
|
|
@ -1592,9 +1597,11 @@ export class Cline {
|
|||
break
|
||||
}
|
||||
|
||||
const accessAllowed = this.llmFileAccessController.validateAccess(relPath)
|
||||
const accessAllowed = this.clineIgnoreController.validateAccess(relPath)
|
||||
if (!accessAllowed) {
|
||||
await handleError("writing file", new Error(`Access denied: ${relPath} (blocked by .clineignore)`))
|
||||
await this.say("clineignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
|
|
@ -1868,12 +1875,11 @@ export class Cline {
|
|||
break
|
||||
}
|
||||
|
||||
const accessAllowed = this.llmFileAccessController.validateAccess(relPath)
|
||||
const accessAllowed = this.clineIgnoreController.validateAccess(relPath)
|
||||
if (!accessAllowed) {
|
||||
await handleError(
|
||||
"reading file",
|
||||
new Error(`Access denied: ${relPath} (blocked by .clineignore)`),
|
||||
)
|
||||
await this.say("clineignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
|
|
@ -1949,7 +1955,7 @@ export class Cline {
|
|||
absolutePath,
|
||||
files,
|
||||
didHitLimit,
|
||||
this.llmFileAccessController,
|
||||
this.clineIgnoreController,
|
||||
)
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
|
|
@ -2013,7 +2019,7 @@ export class Cline {
|
|||
const absolutePath = path.resolve(cwd, relDirPath)
|
||||
const result = await parseSourceCodeForDefinitionsTopLevel(
|
||||
absolutePath,
|
||||
this.llmFileAccessController,
|
||||
this.clineIgnoreController,
|
||||
)
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
|
|
@ -2090,7 +2096,7 @@ export class Cline {
|
|||
absolutePath,
|
||||
regex,
|
||||
filePattern,
|
||||
this.llmFileAccessController,
|
||||
this.clineIgnoreController,
|
||||
)
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
|
|
@ -2329,6 +2335,16 @@ export class Cline {
|
|||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
|
||||
const ignoredFileAttemptedToAccess = this.clineIgnoreController.validateCommand(command)
|
||||
if (ignoredFileAttemptedToAccess) {
|
||||
await this.say("clineignore_error", ignoredFileAttemptedToAccess)
|
||||
pushToolResult(
|
||||
formatResponse.toolError(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess)),
|
||||
)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
let didAutoApprove = false
|
||||
|
||||
if (!requiresApproval && this.shouldAutoApproveTool(block.name)) {
|
||||
|
|
@ -3237,8 +3253,8 @@ export class Cline {
|
|||
.filter(Boolean)
|
||||
.map((absolutePath) => path.relative(cwd, absolutePath))
|
||||
|
||||
// Filter paths through LLMFileAccessController
|
||||
const allowedVisibleFiles = this.llmFileAccessController
|
||||
// Filter paths through clineIgnoreController
|
||||
const allowedVisibleFiles = this.clineIgnoreController
|
||||
.filterPaths(visibleFilePaths)
|
||||
.map((p) => p.toPosix())
|
||||
.join("\n")
|
||||
|
|
@ -3256,8 +3272,8 @@ export class Cline {
|
|||
.filter(Boolean)
|
||||
.map((absolutePath) => path.relative(cwd, absolutePath))
|
||||
|
||||
// Filter paths through LLMFileAccessController
|
||||
const allowedOpenTabs = this.llmFileAccessController
|
||||
// Filter paths through clineIgnoreController
|
||||
const allowedOpenTabs = this.clineIgnoreController
|
||||
.filterPaths(openTabPaths)
|
||||
.map((p) => p.toPosix())
|
||||
.join("\n")
|
||||
|
|
@ -3377,7 +3393,7 @@ export class Cline {
|
|||
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
|
||||
} else {
|
||||
const [files, didHitLimit] = await listFiles(cwd, true, 200)
|
||||
const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.llmFileAccessController)
|
||||
const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.clineIgnoreController)
|
||||
details += result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { LLMFileAccessController } from "./LLMFileAccessController"
|
||||
import { ClineIgnoreController } from "./ClineIgnoreController"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { after, beforeEach, describe, it } from "mocha"
|
||||
import "should"
|
||||
|
||||
describe("LLMFileAccessController", () => {
|
||||
describe("ClineIgnoreController", () => {
|
||||
let tempDir: string
|
||||
let controller: LLMFileAccessController
|
||||
let controller: ClineIgnoreController
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a temp directory for testing
|
||||
|
|
@ -22,7 +22,7 @@ describe("LLMFileAccessController", () => {
|
|||
),
|
||||
)
|
||||
|
||||
controller = new LLMFileAccessController(tempDir)
|
||||
controller = new ClineIgnoreController(tempDir)
|
||||
await controller.initialize()
|
||||
})
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ describe("LLMFileAccessController", () => {
|
|||
["*.secret", "private/", "*.tmp", "data-*.json", "temp/*"].join("\n"),
|
||||
)
|
||||
|
||||
controller = new LLMFileAccessController(tempDir)
|
||||
controller = new ClineIgnoreController(tempDir)
|
||||
await controller.initialize()
|
||||
|
||||
const results = [
|
||||
|
|
@ -150,7 +150,7 @@ describe("LLMFileAccessController", () => {
|
|||
["# Comment line", "*.secret", "private/", "temp.*"].join("\n"),
|
||||
)
|
||||
|
||||
controller = new LLMFileAccessController(tempDir)
|
||||
controller = new ClineIgnoreController(tempDir)
|
||||
await controller.initialize()
|
||||
|
||||
const result = controller.validateAccess("test.secret")
|
||||
|
|
@ -237,7 +237,7 @@ describe("LLMFileAccessController", () => {
|
|||
await fs.mkdir(emptyDir)
|
||||
|
||||
try {
|
||||
const controller = new LLMFileAccessController(emptyDir)
|
||||
const controller = new ClineIgnoreController(emptyDir)
|
||||
await controller.initialize()
|
||||
const result = controller.validateAccess("file.txt")
|
||||
result.should.be.true()
|
||||
|
|
@ -249,7 +249,7 @@ describe("LLMFileAccessController", () => {
|
|||
it("should handle empty .clineignore", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".clineignore"), "")
|
||||
|
||||
controller = new LLMFileAccessController(tempDir)
|
||||
controller = new ClineIgnoreController(tempDir)
|
||||
await controller.initialize()
|
||||
|
||||
const result = controller.validateAccess("regular-file.txt")
|
||||
188
src/core/ignore/ClineIgnoreController.ts
Normal file
188
src/core/ignore/ClineIgnoreController.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import path from "path"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import ignore, { Ignore } from "ignore"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
export const LOCK_TEXT_SYMBOL = "\u{1F512}"
|
||||
|
||||
/**
|
||||
* Controls LLM access to files by enforcing ignore patterns.
|
||||
* Designed to be instantiated once in Cline.ts and passed to file manipulation services.
|
||||
* Uses the 'ignore' library to support standard .gitignore syntax in .clineignore files.
|
||||
*/
|
||||
export class ClineIgnoreController {
|
||||
private cwd: string
|
||||
private ignoreInstance: Ignore
|
||||
private disposables: vscode.Disposable[] = []
|
||||
clineIgnoreContent: string | undefined
|
||||
|
||||
constructor(cwd: string) {
|
||||
this.cwd = cwd
|
||||
this.ignoreInstance = ignore()
|
||||
this.clineIgnoreContent = undefined
|
||||
// Set up file watcher for .clineignore
|
||||
this.setupFileWatcher()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the controller by loading custom patterns
|
||||
* Must be called after construction and before using the controller
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
await this.loadClineIgnore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the file watcher for .clineignore changes
|
||||
*/
|
||||
private setupFileWatcher(): void {
|
||||
const clineignorePattern = new vscode.RelativePattern(this.cwd, ".clineignore")
|
||||
const fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern)
|
||||
|
||||
// Watch for changes and updates
|
||||
this.disposables.push(
|
||||
fileWatcher.onDidChange(() => {
|
||||
this.loadClineIgnore()
|
||||
}),
|
||||
fileWatcher.onDidCreate(() => {
|
||||
this.loadClineIgnore()
|
||||
}),
|
||||
fileWatcher.onDidDelete(() => {
|
||||
this.loadClineIgnore()
|
||||
}),
|
||||
)
|
||||
|
||||
// Add fileWatcher itself to disposables
|
||||
this.disposables.push(fileWatcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load custom patterns from .clineignore if it exists
|
||||
*/
|
||||
private async loadClineIgnore(): Promise<void> {
|
||||
try {
|
||||
// Reset ignore instance to prevent duplicate patterns
|
||||
this.ignoreInstance = ignore()
|
||||
const ignorePath = path.join(this.cwd, ".clineignore")
|
||||
if (await fileExistsAtPath(ignorePath)) {
|
||||
const content = await fs.readFile(ignorePath, "utf8")
|
||||
this.clineIgnoreContent = content
|
||||
this.ignoreInstance.add(content)
|
||||
} else {
|
||||
this.clineIgnoreContent = undefined
|
||||
}
|
||||
} catch (error) {
|
||||
// Should never happen: reading file failed even though it exists
|
||||
console.error("Unexpected error loading .clineignore:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file should be accessible to the LLM
|
||||
* @param filePath - Path to check (relative to cwd)
|
||||
* @returns true if file is accessible, false if ignored
|
||||
*/
|
||||
validateAccess(filePath: string): boolean {
|
||||
// Always allow access if .clineignore does not exist
|
||||
if (!this.clineIgnoreContent) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
// Normalize path to be relative to cwd and use forward slashes
|
||||
const absolutePath = path.resolve(this.cwd, filePath)
|
||||
const relativePath = path.relative(this.cwd, absolutePath).toPosix()
|
||||
|
||||
// Ignore expects paths to be path.relative()'d
|
||||
return !this.ignoreInstance.ignores(relativePath)
|
||||
} catch (error) {
|
||||
// console.error(`Error validating access for ${filePath}:`, error)
|
||||
// Ignore is designed to work with relative file paths, so will throw error for paths outside cwd. We are allowing access to all files outside cwd.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a terminal command should be allowed to execute based on file access patterns
|
||||
* @param command - Terminal command to validate
|
||||
* @returns path of file that is being accessed if it is being accessed, undefined if command is allowed
|
||||
*/
|
||||
validateCommand(command: string): string | undefined {
|
||||
// Always allow if no .clineignore exists
|
||||
if (!this.clineIgnoreContent) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Split command into parts and get the base command
|
||||
const parts = command.trim().split(/\s+/)
|
||||
const baseCommand = parts[0].toLowerCase()
|
||||
|
||||
// Commands that read file contents
|
||||
const fileReadingCommands = [
|
||||
// Unix commands
|
||||
"cat",
|
||||
"less",
|
||||
"more",
|
||||
"head",
|
||||
"tail",
|
||||
"grep",
|
||||
"awk",
|
||||
"sed",
|
||||
// PowerShell commands and aliases
|
||||
"get-content",
|
||||
"gc",
|
||||
"type",
|
||||
"select-string",
|
||||
"sls",
|
||||
]
|
||||
|
||||
if (fileReadingCommands.includes(baseCommand)) {
|
||||
// Check each argument that could be a file path
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const arg = parts[i]
|
||||
// Skip command flags/options (both Unix and PowerShell style)
|
||||
if (arg.startsWith("-") || arg.startsWith("/")) {
|
||||
continue
|
||||
}
|
||||
// Ignore PowerShell parameter names
|
||||
if (arg.includes(":")) {
|
||||
continue
|
||||
}
|
||||
// Validate file access
|
||||
if (!this.validateAccess(arg)) {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter an array of paths, removing those that should be ignored
|
||||
* @param paths - Array of paths to filter (relative to cwd)
|
||||
* @returns Array of allowed paths
|
||||
*/
|
||||
filterPaths(paths: string[]): string[] {
|
||||
try {
|
||||
return paths
|
||||
.map((p) => ({
|
||||
path: p,
|
||||
allowed: this.validateAccess(p),
|
||||
}))
|
||||
.filter((x) => x.allowed)
|
||||
.map((x) => x.path)
|
||||
} catch (error) {
|
||||
console.error("Error filtering paths:", error)
|
||||
return [] // Fail closed for security
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when the controller is no longer needed
|
||||
*/
|
||||
dispose(): void {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
this.disposables = []
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import * as path from "path"
|
||||
import * as diff from "diff"
|
||||
import { LLMFileAccessController } from "../../services/llm-access-control/LLMFileAccessController"
|
||||
import * as path from "path"
|
||||
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController"
|
||||
|
||||
export const formatResponse = {
|
||||
toolDenied: () => `The user denied this operation.`,
|
||||
|
|
@ -11,6 +11,9 @@ export const formatResponse = {
|
|||
|
||||
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
|
||||
|
||||
clineIgnoreError: (path: string) =>
|
||||
`Access to ${path} is blocked by the .clineignore file settings. You must try to continue in the task without using this file, or ask the user to update the .clineignore file.`,
|
||||
|
||||
noToolsUsed: () =>
|
||||
`[ERROR] You did not use a tool in your previous response! Please retry with a tool use.
|
||||
|
||||
|
|
@ -51,7 +54,7 @@ Otherwise, if you have not completed the task and do not need additional informa
|
|||
absolutePath: string,
|
||||
files: string[],
|
||||
didHitLimit: boolean,
|
||||
llmFileAccessController: LLMFileAccessController,
|
||||
clineIgnoreController?: ClineIgnoreController,
|
||||
): string => {
|
||||
const sorted = files
|
||||
.map((file) => {
|
||||
|
|
@ -84,15 +87,15 @@ Otherwise, if you have not completed the task and do not need additional informa
|
|||
return aParts.length - bParts.length
|
||||
})
|
||||
|
||||
const accessControlledSortedFiles = llmFileAccessController
|
||||
const clineIgnoreParsed = clineIgnoreController
|
||||
? sorted.map((filePath) => {
|
||||
// path is relative to absolute path, not cwd
|
||||
// validateAccess expects either path relative to cwd or absolute path
|
||||
// otherwise, for validating against ignore patterns like "assets/icons", we would end up with just "icons", which would result in the path not being ignored.
|
||||
const absoluteFilePath = path.resolve(absolutePath, filePath)
|
||||
const isIgnored = !llmFileAccessController.validateAccess(absoluteFilePath)
|
||||
const isIgnored = !clineIgnoreController.validateAccess(absoluteFilePath)
|
||||
if (isIgnored) {
|
||||
return "\u{1F512} " + filePath
|
||||
return LOCK_TEXT_SYMBOL + " " + filePath
|
||||
}
|
||||
|
||||
return filePath
|
||||
|
|
@ -100,16 +103,13 @@ Otherwise, if you have not completed the task and do not need additional informa
|
|||
: sorted
|
||||
|
||||
if (didHitLimit) {
|
||||
return `${accessControlledSortedFiles.join(
|
||||
return `${clineIgnoreParsed.join(
|
||||
"\n",
|
||||
)}\n\n(File list truncated. Use list_files on specific subdirectories if you need to explore further.)`
|
||||
} else if (
|
||||
accessControlledSortedFiles.length === 0 ||
|
||||
(accessControlledSortedFiles.length === 1 && accessControlledSortedFiles[0] === "")
|
||||
) {
|
||||
} else if (clineIgnoreParsed.length === 0 || (clineIgnoreParsed.length === 1 && clineIgnoreParsed[0] === "")) {
|
||||
return "No files found."
|
||||
} else {
|
||||
return accessControlledSortedFiles.join("\n")
|
||||
return clineIgnoreParsed.join("\n")
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -957,20 +957,26 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
|
|||
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
|
||||
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
|
||||
|
||||
export function addUserInstructions(settingsCustomInstructions?: string, clineRulesFileInstructions?: string) {
|
||||
export function addUserInstructions(
|
||||
settingsCustomInstructions?: string,
|
||||
clineRulesFileInstructions?: string,
|
||||
clineIgnoreInstructions?: string,
|
||||
) {
|
||||
let customInstructions = ""
|
||||
if (settingsCustomInstructions) {
|
||||
customInstructions += settingsCustomInstructions + "\n\n"
|
||||
}
|
||||
if (clineRulesFileInstructions) {
|
||||
customInstructions += clineRulesFileInstructions
|
||||
customInstructions += clineRulesFileInstructions + "\n\n"
|
||||
}
|
||||
if (clineIgnoreInstructions) {
|
||||
customInstructions += clineIgnoreInstructions
|
||||
}
|
||||
|
||||
return `
|
||||
====
|
||||
|
||||
USER'S CUSTOM INSTRUCTIONS
|
||||
The user may have a ".clineignore" file that blocks you from accessing certain directories and files. The paths blocked by this file will be indicated to you with a \u{1F512}, but you need not mention the \u{1F512}. For example, do NOT say things like 'I notice that {some_file}.ts is marked with \u{1F512} in the file list.' Instead, if you ever encounter an error about being blocked by the .clineignore, don't make a big deal about it, just move on without remarking on it. If you absolutely have to say something, just make a casual remark that you are 'blocked by the .clineignore'. Note that the .clineignore is also blocked.
|
||||
|
||||
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,151 +0,0 @@
|
|||
import path from "path"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import ignore, { Ignore } from "ignore"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Controls LLM access to files by enforcing ignore patterns.
|
||||
* Designed to be instantiated once in Cline.ts and passed to file manipulation services.
|
||||
* Uses the 'ignore' library to support standard .gitignore syntax in .clineignore files.
|
||||
*/
|
||||
export class LLMFileAccessController {
|
||||
private cwd: string
|
||||
private ignoreInstance: Ignore
|
||||
private fileWatcher: vscode.FileSystemWatcher | null
|
||||
private disposables: vscode.Disposable[] = []
|
||||
|
||||
/**
|
||||
* Default patterns that are always ignored
|
||||
*/
|
||||
private static readonly DEFAULT_PATTERNS = [".clineignore"]
|
||||
|
||||
constructor(cwd: string) {
|
||||
this.cwd = cwd
|
||||
this.ignoreInstance = ignore()
|
||||
this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS)
|
||||
this.fileWatcher = null
|
||||
|
||||
// Set up file watcher for .clineignore
|
||||
this.setupFileWatcher()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the controller by loading custom patterns
|
||||
* Must be called after construction and before using the controller
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
await this.loadCustomPatterns()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the file watcher for .clineignore changes
|
||||
*/
|
||||
private setupFileWatcher(): void {
|
||||
const clineignorePattern = new vscode.RelativePattern(this.cwd, ".clineignore")
|
||||
this.fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern)
|
||||
|
||||
// Watch for changes and updates
|
||||
this.disposables.push(
|
||||
this.fileWatcher.onDidChange(() => {
|
||||
this.loadCustomPatterns().catch((error) => {
|
||||
console.error("Failed to load updated .clineignore patterns:", error)
|
||||
})
|
||||
}),
|
||||
this.fileWatcher.onDidCreate(() => {
|
||||
this.loadCustomPatterns().catch((error) => {
|
||||
console.error("Failed to load new .clineignore patterns:", error)
|
||||
})
|
||||
}),
|
||||
this.fileWatcher.onDidDelete(() => {
|
||||
this.resetToDefaultPatterns()
|
||||
}),
|
||||
)
|
||||
|
||||
// Add fileWatcher itself to disposables
|
||||
this.disposables.push(this.fileWatcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load custom patterns from .clineignore if it exists
|
||||
*/
|
||||
private async loadCustomPatterns(): Promise<void> {
|
||||
try {
|
||||
const ignorePath = path.join(this.cwd, ".clineignore")
|
||||
if (await fileExistsAtPath(ignorePath)) {
|
||||
// Reset ignore instance to prevent duplicate patterns
|
||||
this.resetToDefaultPatterns()
|
||||
const content = await fs.readFile(ignorePath, "utf8")
|
||||
const customPatterns = content
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith("#"))
|
||||
|
||||
this.ignoreInstance.add(customPatterns)
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue with default patterns
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset ignore patterns to defaults
|
||||
*/
|
||||
private resetToDefaultPatterns(): void {
|
||||
this.ignoreInstance = ignore()
|
||||
this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file should be accessible to the LLM
|
||||
* @param filePath - Path to check (relative to cwd)
|
||||
* @returns true if file is accessible, false if ignored
|
||||
*/
|
||||
validateAccess(filePath: string): boolean {
|
||||
try {
|
||||
// Normalize path to be relative to cwd and use forward slashes
|
||||
const absolutePath = path.resolve(this.cwd, filePath)
|
||||
const relativePath = path.relative(this.cwd, absolutePath).replace(/\\/g, "/")
|
||||
|
||||
// Block access to paths outside cwd (those starting with '..')
|
||||
if (relativePath.startsWith("..")) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Use ignore library to check if path should be ignored
|
||||
return !this.ignoreInstance.ignores(relativePath)
|
||||
} catch (error) {
|
||||
console.error(`Error validating access for ${filePath}:`, error)
|
||||
return false // Fail closed for security
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter an array of paths, removing those that should be ignored
|
||||
* @param paths - Array of paths to filter (relative to cwd)
|
||||
* @returns Array of allowed paths
|
||||
*/
|
||||
filterPaths(paths: string[]): string[] {
|
||||
try {
|
||||
return paths
|
||||
.map((p) => ({
|
||||
path: p,
|
||||
allowed: this.validateAccess(p),
|
||||
}))
|
||||
.filter((x) => x.allowed)
|
||||
.map((x) => x.path)
|
||||
} catch (error) {
|
||||
console.error("Error filtering paths:", error)
|
||||
return [] // Fail closed for security
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when the controller is no longer needed
|
||||
*/
|
||||
dispose(): void {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
this.disposables = []
|
||||
this.fileWatcher = null
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@ import * as vscode from "vscode"
|
|||
import * as childProcess from "child_process"
|
||||
import * as path from "path"
|
||||
import * as readline from "readline"
|
||||
import { LLMFileAccessController } from "../llm-access-control/LLMFileAccessController"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { ClineIgnoreController } from "../../core/ignore/ClineIgnoreController"
|
||||
|
||||
/*
|
||||
This file provides functionality to perform regex searches on files using ripgrep.
|
||||
|
|
@ -120,7 +120,7 @@ export async function regexSearchFiles(
|
|||
directoryPath: string,
|
||||
regex: string,
|
||||
filePattern?: string,
|
||||
llmFileAccessController?: LLMFileAccessController,
|
||||
clineIgnoreController?: ClineIgnoreController,
|
||||
): Promise<string> {
|
||||
const vscodeAppRoot = vscode.env.appRoot
|
||||
const rgPath = await getBinPath(vscodeAppRoot)
|
||||
|
|
@ -173,9 +173,9 @@ export async function regexSearchFiles(
|
|||
results.push(currentResult as SearchResult)
|
||||
}
|
||||
|
||||
// Filter results using LLMFileAccessController if provided
|
||||
const filteredResults = llmFileAccessController
|
||||
? results.filter((result) => llmFileAccessController.validateAccess(result.filePath))
|
||||
// Filter results using ClineIgnoreController if provided
|
||||
const filteredResults = clineIgnoreController
|
||||
? results.filter((result) => clineIgnoreController.validateAccess(result.filePath))
|
||||
: results
|
||||
|
||||
return formatResults(filteredResults, cwd)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import * as path from "path"
|
|||
import { listFiles } from "../glob/list-files"
|
||||
import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { LLMFileAccessController } from "../llm-access-control/LLMFileAccessController"
|
||||
import { ClineIgnoreController } from "../../core/ignore/ClineIgnoreController"
|
||||
|
||||
// TODO: implement caching behavior to avoid having to keep analyzing project for new tasks.
|
||||
export async function parseSourceCodeForDefinitionsTopLevel(
|
||||
dirPath: string,
|
||||
llmFileAccessController?: LLMFileAccessController,
|
||||
clineIgnoreController?: ClineIgnoreController,
|
||||
): Promise<string> {
|
||||
// check if the path exists
|
||||
const dirExists = await fileExistsAtPath(path.resolve(dirPath))
|
||||
|
|
@ -30,10 +30,10 @@ export async function parseSourceCodeForDefinitionsTopLevel(
|
|||
// const filesWithoutDefinitions: string[] = []
|
||||
|
||||
// Filter filepaths for access if controller is provided
|
||||
const allowedFilesToParse = llmFileAccessController ? llmFileAccessController.filterPaths(filesToParse) : filesToParse
|
||||
const allowedFilesToParse = clineIgnoreController ? clineIgnoreController.filterPaths(filesToParse) : filesToParse
|
||||
|
||||
for (const filePath of allowedFilesToParse) {
|
||||
const definitions = await parseFile(filePath, languageParsers, llmFileAccessController)
|
||||
const definitions = await parseFile(filePath, languageParsers, clineIgnoreController)
|
||||
if (definitions) {
|
||||
result += `${path.relative(dirPath, filePath).toPosix()}\n${definitions}\n`
|
||||
}
|
||||
|
|
@ -109,9 +109,9 @@ This approach allows us to focus on the most relevant parts of the code (defined
|
|||
async function parseFile(
|
||||
filePath: string,
|
||||
languageParsers: LanguageParser,
|
||||
llmFileAccessController?: LLMFileAccessController,
|
||||
clineIgnoreController?: ClineIgnoreController,
|
||||
): Promise<string | null> {
|
||||
if (llmFileAccessController && !llmFileAccessController.validateAccess(filePath)) {
|
||||
if (clineIgnoreController && !clineIgnoreController.validateAccess(filePath)) {
|
||||
return null
|
||||
}
|
||||
const fileContent = await fs.readFile(filePath, "utf8")
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ export type ClineSay =
|
|||
| "use_mcp_server"
|
||||
| "diff_error"
|
||||
| "deleted_api_reqs"
|
||||
| "clineignore_error"
|
||||
|
||||
export interface ClineSayTool {
|
||||
tool:
|
||||
|
|
|
|||
|
|
@ -989,6 +989,47 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
</div>
|
||||
</>
|
||||
)
|
||||
case "clineignore_error":
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "rgba(255, 191, 0, 0.1)",
|
||||
padding: 8,
|
||||
borderRadius: 3,
|
||||
fontSize: 12,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 4,
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-error"
|
||||
style={{
|
||||
marginRight: 8,
|
||||
fontSize: 18,
|
||||
color: "#FFA500",
|
||||
}}></i>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
color: "#FFA500",
|
||||
}}>
|
||||
Access Denied
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Cline tried to access <code>{message.text}</code> which is blocked by the{" "}
|
||||
<code>.clineignore</code>
|
||||
file settings.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case "completion_result":
|
||||
const hasChanges = message.text?.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false
|
||||
const text = hasChanges ? message.text?.slice(0, -COMPLETION_RESULT_CHANGES_FLAG.length) : message.text
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue