Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions .clinerules
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
# Code Quality Rules

1. Test Coverage:
- Before attempting completion, always make sure that any code changes have test coverage
- Ensure all tests pass before submitting changes

- Before attempting completion, always make sure that any code changes have test coverage
- Ensure all tests pass before submitting changes

2. Lint Rules:
- Never disable any lint rules without explicit user approval
- If a lint rule needs to be disabled, ask the user first and explain why
- Prefer fixing the underlying issue over disabling the lint rule
- Document any approved lint rule disabling with a comment explaining the reason

3. Logging Guidelines:
- Always instrument code changes using the logger exported from `src\utils\logging\index.ts`.
- This will facilitate efficient debugging without impacting production (as the logger no-ops outside of a test environment.)
- Logs can be found in `logs\app.log`
- Logfile is overwritten on each run to keep it to a manageable volume.
- Never disable any lint rules without explicit user approval
- If a lint rule needs to be disabled, ask the user first and explain why
- Prefer fixing the underlying issue over disabling the lint rule
- Document any approved lint rule disabling with a comment explaining the reason

3. Logging Guidelines:
- Always instrument code changes using the logger exported from `src\utils\logging\index.ts`.
- This will facilitate efficient debugging without impacting production (as the logger no-ops outside of a test environment.)
- Logs can be found in `logs\app.log`
- Logfile is overwritten on each run to keep it to a manageable volume.

# Adding a New Setting

To add a new setting that persists its state, follow the steps in cline_docs/settings.md

The `.clineignore` file allows users to specify files and directories that Cline should not access. When implementing new features, respect the `.clineignore` rules and ensure that your code does not attempt to read or modify ignored files.
70 changes: 68 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@
"@types/jest": "^29.5.14",
"@types/mocha": "^10.0.7",
"@types/node": "20.x",
"@types/should": "^13.0.0",
"@types/string-similarity": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
Expand All @@ -382,6 +383,7 @@
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"rimraf": "^6.0.1",
"should": "^13.2.3",
"svgtofont": "^6.2.0",
"ts-jest": "^29.2.5",
"typescript": "^5.4.5"
Expand Down
102 changes: 91 additions & 11 deletions src/core/Cline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { fileExistsAtPath } from "../utils/fs"
import { arePathsEqual, getReadablePath } from "../utils/path"
import { parseMentions } from "./mentions"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "./ignore/ClineIgnoreController"
import { formatResponse } from "./prompts/responses"
import { SYSTEM_PROMPT } from "./prompts/system"
import { modes, defaultModeSlug, getModeBySlug } from "../shared/modes"
Expand Down Expand Up @@ -86,6 +87,7 @@ export class Cline {

apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = []
clineMessages: ClineMessage[] = []
private clineIgnoreController: ClineIgnoreController
private askResponse?: ClineAskResponse
private askResponseText?: string
private askResponseImages?: string[]
Expand Down Expand Up @@ -129,6 +131,11 @@ export class Cline {
historyItem?: HistoryItem | undefined,
experiments?: Record<string, boolean>,
) {
this.clineIgnoreController = new ClineIgnoreController(cwd)
this.clineIgnoreController.initialize().catch((error) => {
console.error("Failed to initialize ClineIgnoreController:", error)
})

if (!task && !images && !historyItem) {
throw new Error("Either historyItem or task/images must be provided")
}
Expand Down Expand Up @@ -732,6 +739,7 @@ export class Cline {
this.browserSession.closeBrowser()
// Need to await for when we want to make sure directories/files are
// reverted before re-starting the task from a checkpoint.
this.clineIgnoreController.dispose()
await this.diffViewProvider.revertChanges()
}

Expand Down Expand Up @@ -864,6 +872,14 @@ export class Cline {
})
}

const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
let clineIgnoreInstructions: string | undefined
if (clineIgnoreContent) {
clineIgnoreInstructions = `# .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${clineIgnoreContent}\n.clineignore`
// 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>
this.customInstructions += clineIgnoreInstructions
}

const {
browserViewportSize,
mode,
Expand Down Expand Up @@ -944,6 +960,7 @@ export class Cline {
}
return { role, content }
})

const stream = this.api.createMessage(systemPrompt, cleanConversationHistory)
const iterator = stream[Symbol.asyncIterator]()

Expand Down Expand Up @@ -1257,6 +1274,16 @@ export class Cline {
// wait so we can determine if it's a new file or editing an existing file
break
}

// check if file is allowed to be accessed
const accessAllowed = this.clineIgnoreController.validateAccess(relPath)
if (!accessAllowed) {
await this.say("clineignore_error", relPath)
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))

break
}

// Check if file exists using cached map or fs.access
let fileExists: boolean
if (this.diffViewProvider.editType !== undefined) {
Expand Down Expand Up @@ -1331,6 +1358,15 @@ export class Cline {
await this.diffViewProvider.reset()
break
}

const accessAllowed = this.clineIgnoreController.validateAccess(relPath)
if (!accessAllowed) {
await this.say("clineignore_error", relPath)
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))

break
}

this.consecutiveMistakeCount = 0

// if isEditingFile false, that means we have the full contents of the file already.
Expand Down Expand Up @@ -1899,6 +1935,12 @@ export class Cline {
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
break
}
const accessAllowed = this.clineIgnoreController.validateAccess(relPath)
if (!accessAllowed) {
await this.say("clineignore_error", relPath)
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
break
}
this.consecutiveMistakeCount = 0
const absolutePath = path.resolve(cwd, relPath)
const completeMessage = JSON.stringify({
Expand Down Expand Up @@ -1944,7 +1986,12 @@ export class Cline {
this.consecutiveMistakeCount = 0
const absolutePath = path.resolve(cwd, relDirPath)
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
const result = formatResponse.formatFilesList(absolutePath, files, didHitLimit)
const result = formatResponse.formatFilesList(
absolutePath,
files,
didHitLimit,
this.clineIgnoreController,
)
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: result,
Expand Down Expand Up @@ -1985,7 +2032,10 @@ export class Cline {
}
this.consecutiveMistakeCount = 0
const absolutePath = path.resolve(cwd, relDirPath)
const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath)
const result = await parseSourceCodeForDefinitionsTopLevel(
absolutePath,
this.clineIgnoreController,
)
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: result,
Expand Down Expand Up @@ -2033,7 +2083,13 @@ export class Cline {
}
this.consecutiveMistakeCount = 0
const absolutePath = path.resolve(cwd, relDirPath)
const results = await regexSearchFiles(cwd, absolutePath, regex, filePattern)
const results = await regexSearchFiles(
cwd,
absolutePath,
regex,
filePattern,
this.clineIgnoreController,
)
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: results,
Expand Down Expand Up @@ -2214,6 +2270,18 @@ 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),
),
)

break
}

const didApprove = await askApproval("command", command)
if (!didApprove) {
break
Expand Down Expand Up @@ -3067,26 +3135,38 @@ export class Cline {

// It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
details += "\n\n# VSCode Visible Files"
const visibleFiles = vscode.window.visibleTextEditors
const visibleFilePaths = vscode.window.visibleTextEditors
?.map((editor) => editor.document?.uri?.fsPath)
.filter(Boolean)
.map((absolutePath) => path.relative(cwd, absolutePath).toPosix())

// Filter paths through clineIgnoreController
const allowedVisibleFiles = this.clineIgnoreController
.filterPaths(visibleFilePaths)
.map((p) => p.toPosix())
.join("\n")
if (visibleFiles) {
details += `\n${visibleFiles}`

if (allowedVisibleFiles) {
details += `\n${allowedVisibleFiles}`
} else {
details += "\n(No visible files)"
}

details += "\n\n# VSCode Open Tabs"
const openTabs = vscode.window.tabGroups.all
const openTabPaths = vscode.window.tabGroups.all
.flatMap((group) => group.tabs)
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
.filter(Boolean)
.map((absolutePath) => path.relative(cwd, absolutePath).toPosix())

// Filter paths through clineIgnoreController
const allowedOpenTabs = this.clineIgnoreController
.filterPaths(openTabPaths)
.map((p) => p.toPosix())
.join("\n")
if (openTabs) {
details += `\n${openTabs}`

if (allowedOpenTabs) {
details += `\n${allowedOpenTabs}`
} else {
details += "\n(No open tabs)"
}
Expand Down Expand Up @@ -3225,11 +3305,11 @@ 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)
const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.clineIgnoreController)
details += result
}
}

console.log("Environment details:", details)
return `<environment_details>\n${details.trim()}\n</environment_details>`
}

Expand Down
Loading
Loading