Compare commits

...
7 changed files with 123 additions and 75 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add the ability to fetch from global cline rules files
@@ -4,7 +4,27 @@ import { fileExistsAtPath, isDirectory, readDirectory } from "../../../../utils/
import { formatResponse } from "../../../prompts/responses"
import fs from "fs/promises"
export const getClineRules = async (cwd: string) => {
export const getGlobalClineRules = async (globalClineRulesFilePath: string) => {
if (await fileExistsAtPath(globalClineRulesFilePath)) {
if (await isDirectory(globalClineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(globalClineRulesFilePath)
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, globalClineRulesFilePath)
const clineRulesFileInstructions = formatResponse.clineRulesGlobalDirectoryInstructions(rulesFilesTotalContent)
return clineRulesFileInstructions
} catch {
console.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
}
} else {
console.error(`${globalClineRulesFilePath} is not a directory`)
return undefined
}
}
return undefined
}
export const getLocalClineRules = async (cwd: string) => {
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
let clineRulesFileInstructions: string | undefined
@@ -14,7 +34,7 @@ export const getClineRules = async (cwd: string) => {
try {
const rulesFilePaths = await readDirectory(path.join(cwd, GlobalFileNames.clineRules))
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, cwd)
clineRulesFileInstructions = formatResponse.clineRulesDirectoryInstructions(cwd, rulesFilesTotalContent)
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
} catch {
console.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
}
@@ -22,7 +42,7 @@ export const getClineRules = async (cwd: string) => {
try {
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
clineRulesFileInstructions = formatResponse.clineRulesFileInstructions(cwd, ruleFileContent)
clineRulesFileInstructions = formatResponse.clineRulesLocalFileInstructions(cwd, ruleFileContent)
}
} catch {
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
@@ -33,11 +53,11 @@ export const getClineRules = async (cwd: string) => {
return clineRulesFileInstructions
}
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], cwd: string) => {
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], basePath: string) => {
const ruleFilesTotalContent = await Promise.all(
rulesFilePaths.map(async (filePath) => {
const ruleFilePath = path.resolve(cwd, filePath)
const ruleFilePathRelative = path.relative(cwd, ruleFilePath)
const ruleFilePath = path.resolve(basePath, filePath)
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
}),
).then((contents) => contents.join("\n\n"))
+3 -60
View File
@@ -2,10 +2,8 @@ import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import type { AxiosRequestConfig } from "axios"
import crypto from "crypto"
import { execa } from "execa"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import os from "os"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
@@ -36,7 +34,7 @@ import { searchCommits } from "../../utils/git"
import { getWorkspacePath } from "../../utils/path"
import { getTotalTasksSize } from "../../utils/storage"
import { openMention } from "../mentions"
import { GlobalFileNames } from "../storage/disk"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import {
getAllExtensionState,
getGlobalState,
@@ -74,8 +72,8 @@ export class Controller {
this.workspaceTracker = new WorkspaceTracker((msg) => this.postMessageToWebview(msg))
this.mcpHub = new McpHub(
() => this.ensureMcpServersDirectoryExists(),
() => this.ensureSettingsDirectoryExists(),
() => ensureMcpServersDirectoryExists(),
() => ensureSettingsDirectoryExists(this.context),
(msg) => this.postMessageToWebview(msg),
this.context.extension?.packageJSON?.version ?? "1.0.0",
)
@@ -1106,61 +1104,6 @@ export class Controller {
}
}
// MCP
async getDocumentsPath(): Promise<string> {
if (process.platform === "win32") {
try {
const { stdout: docsPath } = await execa("powershell", [
"-NoProfile", // Ignore user's PowerShell profile(s)
"-Command",
"[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments)",
])
const trimmedPath = docsPath.trim()
if (trimmedPath) {
return trimmedPath
}
} catch (err) {
console.error("Failed to retrieve Windows Documents path. Falling back to homedir/Documents.")
}
} else if (process.platform === "linux") {
try {
// First check if xdg-user-dir exists
await execa("which", ["xdg-user-dir"])
// If it exists, try to get XDG documents path
const { stdout } = await execa("xdg-user-dir", ["DOCUMENTS"])
const trimmedPath = stdout.trim()
if (trimmedPath) {
return trimmedPath
}
} catch {
// Log error but continue to fallback
console.error("Failed to retrieve XDG Documents path. Falling back to homedir/Documents.")
}
}
// Default fallback for all platforms
return path.join(os.homedir(), "Documents")
}
async ensureMcpServersDirectoryExists(): Promise<string> {
const userDocumentsPath = await this.getDocumentsPath()
const mcpServersDir = path.join(userDocumentsPath, "Cline", "MCP")
try {
await fs.mkdir(mcpServersDir, { recursive: true })
} catch (error) {
return "~/Documents/Cline/MCP" // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine since this path is only ever used in the system prompt
}
return mcpServersDir
}
async ensureSettingsDirectoryExists(): Promise<string> {
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
// VSCode LM API
private async getVsCodeLmModels() {
+5 -2
View File
@@ -204,10 +204,13 @@ Otherwise, if you have not completed the task and do not need additional informa
clineIgnoreInstructions: (content: string) =>
`# .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`,
clineRulesDirectoryInstructions: (cwd: string, content: string) =>
clineRulesGlobalDirectoryInstructions: (content: string) =>
`# .clinerules/\n\nThe following is provided by a global .clinerules/ directory where the user has specified instructions:\n\n${content}`,
clineRulesLocalDirectoryInstructions: (cwd: string, content: string) =>
`# .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}`,
clineRulesFileInstructions: (cwd: string, content: string) =>
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}`,
}
+7 -3
View File
@@ -619,7 +619,8 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
export function addUserInstructions(
settingsCustomInstructions?: string,
clineRulesFileInstructions?: string,
globalClineRulesFileInstructions?: string,
localClineRulesFileInstructions?: string,
clineIgnoreInstructions?: string,
preferredLanguageInstructions?: string,
) {
@@ -630,8 +631,11 @@ export function addUserInstructions(
if (settingsCustomInstructions) {
customInstructions += settingsCustomInstructions + "\n\n"
}
if (clineRulesFileInstructions) {
customInstructions += clineRulesFileInstructions + "\n\n"
if (globalClineRulesFileInstructions) {
customInstructions += globalClineRulesFileInstructions + "\n\n"
}
if (localClineRulesFileInstructions) {
customInstructions += localClineRulesFileInstructions + "\n\n"
}
if (clineIgnoreInstructions) {
customInstructions += clineIgnoreInstructions
+67
View File
@@ -5,6 +5,9 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { fileExistsAtPath } from "../../utils/fs"
import { ClineMessage } from "../../shared/ExtensionMessage"
import { TaskMetadata } from "../context/context-tracking/ContextTrackerTypes"
import os from "os"
import { execa } from "execa"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
contextHistory: "context_history.json",
@@ -15,6 +18,42 @@ export const GlobalFileNames = {
taskMetadata: "task_metadata.json",
}
export async function getDocumentsPath(): Promise<string> {
if (process.platform === "win32") {
try {
const { stdout: docsPath } = await execa("powershell", [
"-NoProfile", // Ignore user's PowerShell profile(s)
"-Command",
"[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments)",
])
const trimmedPath = docsPath.trim()
if (trimmedPath) {
return trimmedPath
}
} catch (err) {
console.error("Failed to retrieve Windows Documents path. Falling back to homedir/Documents.")
}
} else if (process.platform === "linux") {
try {
// First check if xdg-user-dir exists
await execa("which", ["xdg-user-dir"])
// If it exists, try to get XDG documents path
const { stdout } = await execa("xdg-user-dir", ["DOCUMENTS"])
const trimmedPath = stdout.trim()
if (trimmedPath) {
return trimmedPath
}
} catch {
// Log error but continue to fallback
console.error("Failed to retrieve XDG Documents path. Falling back to homedir/Documents.")
}
}
// Default fallback for all platforms
return path.join(os.homedir(), "Documents")
}
export async function ensureTaskDirectoryExists(context: vscode.ExtensionContext, taskId: string): Promise<string> {
const globalStoragePath = context.globalStorageUri.fsPath
const taskDir = path.join(globalStoragePath, "tasks", taskId)
@@ -22,6 +61,34 @@ export async function ensureTaskDirectoryExists(context: vscode.ExtensionContext
return taskDir
}
export async function ensureRulesDirectoryExists(): Promise<string> {
const userDocumentsPath = await getDocumentsPath()
const clineRulesDir = path.join(userDocumentsPath, "Cline", "Rules")
try {
await fs.mkdir(clineRulesDir, { recursive: true })
} catch (error) {
return path.join(os.homedir(), "Documents", "Cline", "Rules") // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine because we will fail gracefully with a path that does not exist
}
return clineRulesDir
}
export async function ensureMcpServersDirectoryExists(): Promise<string> {
const userDocumentsPath = await getDocumentsPath()
const mcpServersDir = path.join(userDocumentsPath, "Cline", "MCP")
try {
await fs.mkdir(mcpServersDir, { recursive: true })
} catch (error) {
return "~/Documents/Cline/MCP" // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine since this path is only ever used in the system prompt
}
return mcpServersDir
}
export async function ensureSettingsDirectoryExists(context: vscode.ExtensionContext): Promise<string> {
const settingsDir = path.join(context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
export async function getSavedApiConversationHistory(
context: vscode.ExtensionContext,
taskId: string,
+10 -4
View File
@@ -73,15 +73,16 @@ import {
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { ContextManager } from "../context/context-management/ContextManager"
import { getClineRules } from "../context/instructions/user-instructions/cline-rules"
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
import {
ensureRulesDirectoryExists,
ensureTaskDirectoryExists,
getSavedApiConversationHistory,
getSavedClineMessages,
saveApiConversationHistory,
saveClineMessages,
} from "../storage/disk"
import { getGlobalClineRules, getLocalClineRules } from "../context/instructions/user-instructions/cline-rules"
import { getGlobalState } from "../storage/state"
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
@@ -1279,7 +1280,10 @@ export class Task {
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
: ""
const clineRulesFileInstructions = await getClineRules(cwd)
const localClineRulesFileInstructions = await getLocalClineRules(cwd)
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath)
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
let clineIgnoreInstructions: string | undefined
@@ -1289,14 +1293,16 @@ export class Task {
if (
settingsCustomInstructions ||
clineRulesFileInstructions ||
globalClineRulesFileInstructions ||
localClineRulesFileInstructions ||
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(
settingsCustomInstructions,
clineRulesFileInstructions,
globalClineRulesFileInstructions,
localClineRulesFileInstructions,
clineIgnoreInstructions,
preferredLanguageInstructions,
)