diff --git a/bin/roo-cline-1.0.1.vsix b/bin/roo-cline-1.0.1.vsix deleted file mode 100644 index 4d9ba52..0000000 Binary files a/bin/roo-cline-1.0.1.vsix and /dev/null differ diff --git a/bin/roo-cline-1.0.2.vsix b/bin/roo-cline-1.0.2.vsix new file mode 100644 index 0000000..6145744 Binary files /dev/null and b/bin/roo-cline-1.0.2.vsix differ diff --git a/package-lock.json b/package-lock.json index 0e8aea7..856b36e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "1.0.0", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "1.0.0", + "version": "1.0.2", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 0255f09..9582105 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "roo-cline", "displayName": "Roo Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", - "version": "1.0.1", + "version": "1.0.2", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", diff --git a/src/core/Cline.ts b/src/core/Cline.ts index bf2324c..64ad0ec 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -757,11 +757,7 @@ export class Cline { } async *attemptApiRequest(previousApiReqIndex: number): ApiStream { - let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false) - if (this.customInstructions && this.customInstructions.trim()) { - // 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 - systemPrompt += addCustomInstructions(this.customInstructions) - } + const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false) + await addCustomInstructions(this.customInstructions ?? '', cwd) // 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 if (previousApiReqIndex >= 0) { diff --git a/src/core/prompts/__tests__/system.test.ts b/src/core/prompts/__tests__/system.test.ts new file mode 100644 index 0000000..602c0c9 --- /dev/null +++ b/src/core/prompts/__tests__/system.test.ts @@ -0,0 +1,112 @@ +import fs from 'fs/promises' +import path from 'path' +import os from 'os' +import { addCustomInstructions } from '../system' + +// Mock external dependencies +jest.mock('os-name', () => () => 'macOS') +jest.mock('default-shell', () => '/bin/zsh') +jest.mock('os', () => ({ + homedir: () => '/Users/test', + ...jest.requireActual('os') +})) + +describe('system.ts', () => { + let tempDir: string + + beforeEach(async () => { + // Create a temporary directory for test files + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cline-test-')) + }) + + afterEach(async () => { + // Clean up temporary directory after each test + await fs.rm(tempDir, { recursive: true, force: true }) + }) + + describe('addCustomInstructions', () => { + it('should include content from .clinerules and .cursorrules if present', async () => { + // Create test rule files + await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests\nUse TypeScript') + await fs.writeFile(path.join(tempDir, '.cursorrules'), 'Format code before committing') + + const customInstructions = 'Base instructions' + const result = await addCustomInstructions(customInstructions, tempDir) + + // Verify all instructions are included + expect(result).toContain('Base instructions') + expect(result).toContain('Always write tests') + expect(result).toContain('Use TypeScript') + expect(result).toContain('Format code before committing') + expect(result).toContain('Rules from .clinerules:') + expect(result).toContain('Rules from .cursorrules:') + }) + + it('should handle missing rule files gracefully', async () => { + const customInstructions = 'Base instructions' + const result = await addCustomInstructions(customInstructions, tempDir) + + // Should only contain base instructions + expect(result).toContain('Base instructions') + expect(result).not.toContain('Rules from') + }) + + it('should handle empty rule files', async () => { + // Create empty rule files + await fs.writeFile(path.join(tempDir, '.clinerules'), '') + await fs.writeFile(path.join(tempDir, '.cursorrules'), '') + + const customInstructions = 'Base instructions' + const result = await addCustomInstructions(customInstructions, tempDir) + + // Should only contain base instructions + expect(result).toContain('Base instructions') + expect(result).not.toContain('Rules from') + }) + + it('should handle whitespace-only rule files', async () => { + // Create rule files with only whitespace + await fs.writeFile(path.join(tempDir, '.clinerules'), ' \n \t ') + await fs.writeFile(path.join(tempDir, '.cursorrules'), ' \n ') + + const customInstructions = 'Base instructions' + const result = await addCustomInstructions(customInstructions, tempDir) + + // Should only contain base instructions + expect(result).toContain('Base instructions') + expect(result).not.toContain('Rules from') + }) + + it('should handle one rule file present and one missing', async () => { + // Create only .clinerules + await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests') + + const customInstructions = 'Base instructions' + const result = await addCustomInstructions(customInstructions, tempDir) + + // Should contain base instructions and .clinerules content + expect(result).toContain('Base instructions') + expect(result).toContain('Always write tests') + expect(result).toContain('Rules from .clinerules:') + expect(result).not.toContain('Rules from .cursorrules:') + }) + + it('should handle empty custom instructions with rule files', async () => { + await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests') + await fs.writeFile(path.join(tempDir, '.cursorrules'), 'Format code before committing') + + const result = await addCustomInstructions('', tempDir) + + // Should contain rule file content even with empty custom instructions + expect(result).toContain('Always write tests') + expect(result).toContain('Format code before committing') + expect(result).toContain('Rules from .clinerules:') + expect(result).toContain('Rules from .cursorrules:') + }) + + it('should return empty string when no instructions or rules exist', async () => { + const result = await addCustomInstructions('', tempDir) + expect(result).toBe('') + }) + }) +}) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 763788e..abd27b7 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -1,6 +1,8 @@ import osName from "os-name" import defaultShell from "default-shell" import os from "os" +import fs from 'fs/promises' +import path from 'path' export const SYSTEM_PROMPT = async ( cwd: string, @@ -281,13 +283,44 @@ 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 addCustomInstructions(customInstructions: string): string { - return ` +async function loadRuleFiles(cwd: string): Promise { + const ruleFiles = ['.clinerules', '.cursorrules'] + let combinedRules = '' + + for (const file of ruleFiles) { + try { + const content = await fs.readFile(path.join(cwd, file), 'utf-8') + if (content.trim()) { + combinedRules += `\n# Rules from ${file}:\n${content.trim()}\n` + } + } catch (err) { + // Silently skip if file doesn't exist + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { + throw err + } + } + } + + return combinedRules +} + +export async function addCustomInstructions(customInstructions: string, cwd: string): Promise { + const ruleFileContent = await loadRuleFiles(cwd) + const allInstructions = [customInstructions.trim()] + + if (ruleFileContent && ruleFileContent.trim()) { + allInstructions.push(ruleFileContent.trim()) + } + + const joinedInstructions = allInstructions.join('\n\n') + + return joinedInstructions ? ` ==== USER'S CUSTOM INSTRUCTIONS 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. -${customInstructions.trim()}` +${joinedInstructions}` + : "" }