Merge branch 'main' of https://github.com/RooVetGit/Roo-Cline into feature/pupeteer-updates

This commit is contained in:
a8trejo 2024-12-10 11:36:09 -08:00
commit 4d53bd2104
8 changed files with 784 additions and 8 deletions

92
.github/workflows/codeql.yml vendored Normal file
View file

@ -0,0 +1,92 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '24 19 * * 3'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View file

@ -1,5 +1,13 @@
# Roo Cline Changelog
## [2.1.17]
- Switch to search/replace diffs in experimental diff editing mode
## [2.1.16]
- Allow copying prompts from the history screen
## [2.1.15]
- Incorporate dbasclpy's [PR](https://github.com/RooVetGit/Roo-Cline/pull/54) to add support for gemini-exp-1206

View file

@ -3,11 +3,12 @@ A fork of Cline, an autonomous coding agent, with some added experimental config
- Auto-approval capabilities for commands, write, and browser operations
- Support for .clinerules per-project custom instructions
- Ability to run side-by-side with Cline
- Code is unit-tested
- Unit test coverage (written almost entirely by Roo Cline!)
- Support for playing sound effects
- Support for OpenRouter compression
- Support for editing through diffs (very experimental)
- Support for gemini-exp-1206
- Support for copying prompts from the history screen
Here's an example of Roo-Cline autonomously creating a snake game with "Always approve write operations" and "Always approve browser actions" turned on:

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "roo-cline",
"version": "2.1.15",
"version": "2.1.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
"version": "2.1.15",
"version": "2.1.17",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",

View file

@ -3,7 +3,7 @@
"displayName": "Roo Cline",
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
"publisher": "RooVeterinaryInc",
"version": "2.1.16",
"version": "2.1.17",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",

View file

@ -1,16 +1,16 @@
import type { DiffStrategy } from './types'
import { UnifiedDiffStrategy } from './strategies/unified'
import { SearchReplaceDiffStrategy } from './strategies/search-replace'
/**
* Get the appropriate diff strategy for the given model
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
* @returns The appropriate diff strategy for the model
*/
export function getDiffStrategy(model: string): DiffStrategy {
// For now, return UnifiedDiffStrategy for all models
// For now, return SearchReplaceDiffStrategy for all models
// This architecture allows for future optimizations based on model capabilities
return new UnifiedDiffStrategy()
return new SearchReplaceDiffStrategy()
}
export type { DiffStrategy }
export { UnifiedDiffStrategy }
export { UnifiedDiffStrategy, SearchReplaceDiffStrategy }

View file

@ -0,0 +1,504 @@
import { SearchReplaceDiffStrategy } from '../search-replace'
describe('SearchReplaceDiffStrategy', () => {
let strategy: SearchReplaceDiffStrategy
beforeEach(() => {
strategy = new SearchReplaceDiffStrategy()
})
describe('applyDiff', () => {
it('should replace matching content', () => {
const originalContent = `function hello() {
console.log("hello")
}
`
const diffContent = `test.ts
<<<<<<< SEARCH
function hello() {
console.log("hello")
}
=======
function hello() {
console.log("hello world")
}
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(`function hello() {
console.log("hello world")
}
`)
})
it('should handle extra whitespace in search/replace blocks', () => {
const originalContent = `function test() {
return true;
}
`
const diffContent = `test.ts
<<<<<<< SEARCH
function test() {
return true;
}
=======
function test() {
return false;
}
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(`function test() {
return false;
}
`)
})
it('should match content with different surrounding whitespace', () => {
const originalContent = `
function example() {
return 42;
}
`
const diffContent = `test.ts
<<<<<<< SEARCH
function example() {
return 42;
}
=======
function example() {
return 43;
}
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(`
function example() {
return 43;
}
`)
})
it('should match content with different indentation in search block', () => {
const originalContent = ` function test() {
return true;
}
`
const diffContent = `test.ts
<<<<<<< SEARCH
function test() {
return true;
}
=======
function test() {
return false;
}
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(` function test() {
return false;
}
`)
})
it('should handle tab-based indentation', () => {
const originalContent = "function test() {\n\treturn true;\n}\n"
const diffContent = `test.ts
<<<<<<< SEARCH
function test() {
\treturn true;
}
=======
function test() {
\treturn false;
}
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe("function test() {\n\treturn false;\n}\n")
})
it('should preserve mixed tabs and spaces', () => {
const originalContent = "\tclass Example {\n\t constructor() {\n\t\tthis.value = 0;\n\t }\n\t}"
const diffContent = `test.ts
<<<<<<< SEARCH
\tclass Example {
\t constructor() {
\t\tthis.value = 0;
\t }
\t}
=======
\tclass Example {
\t constructor() {
\t\tthis.value = 1;
\t }
\t}
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe("\tclass Example {\n\t constructor() {\n\t\tthis.value = 1;\n\t }\n\t}")
})
it('should handle additional indentation with tabs', () => {
const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}"
const diffContent = `test.ts
<<<<<<< SEARCH
function test() {
\treturn true;
}
=======
function test() {
\t// Add comment
\treturn false;
}
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe("\tfunction test() {\n\t\t// Add comment\n\t\treturn false;\n\t}")
})
it('should preserve exact indentation characters when adding lines', () => {
const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}"
const diffContent = `test.ts
<<<<<<< SEARCH
\tfunction test() {
\t\treturn true;
\t}
=======
\tfunction test() {
\t\t// First comment
\t\t// Second comment
\t\treturn true;
\t}
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe("\tfunction test() {\n\t\t// First comment\n\t\t// Second comment\n\t\treturn true;\n\t}")
})
it('should return false if search content does not match', () => {
const originalContent = `function hello() {
console.log("hello")
}
`
const diffContent = `test.ts
<<<<<<< SEARCH
function hello() {
console.log("wrong")
}
=======
function hello() {
console.log("hello world")
}
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(false)
})
it('should return false if diff format is invalid', () => {
const originalContent = `function hello() {
console.log("hello")
}
`
const diffContent = `test.ts
Invalid diff format`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(false)
})
it('should handle multiple lines with proper indentation', () => {
const originalContent = `class Example {
constructor() {
this.value = 0
}
getValue() {
return this.value
}
}
`
const diffContent = `test.ts
<<<<<<< SEARCH
getValue() {
return this.value
}
=======
getValue() {
// Add logging
console.log("Getting value")
return this.value
}
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(`class Example {
constructor() {
this.value = 0
}
getValue() {
// Add logging
console.log("Getting value")
return this.value
}
}
`)
})
it('should preserve whitespace exactly in the output', () => {
const originalContent = " indented\n more indented\n back\n"
const diffContent = `test.ts
<<<<<<< SEARCH
indented
more indented
back
=======
modified
still indented
end
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(" modified\n still indented\n end\n")
})
it('should handle complex refactoring with multiple functions', () => {
const originalContent = `export async function extractTextFromFile(filePath: string): Promise<string> {
try {
await fs.access(filePath)
} catch (error) {
throw new Error(\`File not found: \${filePath}\`)
}
const fileExtension = path.extname(filePath).toLowerCase()
switch (fileExtension) {
case ".pdf":
return extractTextFromPDF(filePath)
case ".docx":
return extractTextFromDOCX(filePath)
case ".ipynb":
return extractTextFromIPYNB(filePath)
default:
const isBinary = await isBinaryFile(filePath).catch(() => false)
if (!isBinary) {
return addLineNumbers(await fs.readFile(filePath, "utf8"))
} else {
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
}
}
}
export function addLineNumbers(content: string): string {
const lines = content.split('\\n')
const maxLineNumberWidth = String(lines.length).length
return lines
.map((line, index) => {
const lineNumber = String(index + 1).padStart(maxLineNumberWidth, ' ')
return \`\${lineNumber} | \${line}\`
}).join('\\n')
}`
const diffContent = `test.ts
<<<<<<< SEARCH
export async function extractTextFromFile(filePath: string): Promise<string> {
try {
await fs.access(filePath)
} catch (error) {
throw new Error(\`File not found: \${filePath}\`)
}
const fileExtension = path.extname(filePath).toLowerCase()
switch (fileExtension) {
case ".pdf":
return extractTextFromPDF(filePath)
case ".docx":
return extractTextFromDOCX(filePath)
case ".ipynb":
return extractTextFromIPYNB(filePath)
default:
const isBinary = await isBinaryFile(filePath).catch(() => false)
if (!isBinary) {
return addLineNumbers(await fs.readFile(filePath, "utf8"))
} else {
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
}
}
}
export function addLineNumbers(content: string): string {
const lines = content.split('\\n')
const maxLineNumberWidth = String(lines.length).length
return lines
.map((line, index) => {
const lineNumber = String(index + 1).padStart(maxLineNumberWidth, ' ')
return \`\${lineNumber} | \${line}\`
}).join('\\n')
}
=======
function extractLineRange(content: string, startLine?: number, endLine?: number): string {
const lines = content.split('\\n')
const start = startLine ? Math.max(1, startLine) : 1
const end = endLine ? Math.min(lines.length, endLine) : lines.length
if (start > end || start > lines.length) {
throw new Error(\`Invalid line range: start=\${start}, end=\${end}, total lines=\${lines.length}\`)
}
return lines.slice(start - 1, end).join('\\n')
}
export async function extractTextFromFile(filePath: string, startLine?: number, endLine?: number): Promise<string> {
try {
await fs.access(filePath)
} catch (error) {
throw new Error(\`File not found: \${filePath}\`)
}
const fileExtension = path.extname(filePath).toLowerCase()
let content: string
switch (fileExtension) {
case ".pdf": {
const dataBuffer = await fs.readFile(filePath)
const data = await pdf(dataBuffer)
content = extractLineRange(data.text, startLine, endLine)
break
}
case ".docx": {
const result = await mammoth.extractRawText({ path: filePath })
content = extractLineRange(result.value, startLine, endLine)
break
}
case ".ipynb": {
const data = await fs.readFile(filePath, "utf8")
const notebook = JSON.parse(data)
let extractedText = ""
for (const cell of notebook.cells) {
if ((cell.cell_type === "markdown" || cell.cell_type === "code") && cell.source) {
extractedText += cell.source.join("\\n") + "\\n"
}
}
content = extractLineRange(extractedText, startLine, endLine)
break
}
default: {
const isBinary = await isBinaryFile(filePath).catch(() => false)
if (!isBinary) {
const fileContent = await fs.readFile(filePath, "utf8")
content = extractLineRange(fileContent, startLine, endLine)
} else {
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
}
}
}
return addLineNumbers(content, startLine)
}
export function addLineNumbers(content: string, startLine: number = 1): string {
const lines = content.split('\\n')
const maxLineNumberWidth = String(startLine + lines.length - 1).length
return lines
.map((line, index) => {
const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ')
return \`\${lineNumber} | \${line}\`
}).join('\\n')
}
>>>>>>> REPLACE`
const result = strategy.applyDiff(originalContent, diffContent)
const expected = `function extractLineRange(content: string, startLine?: number, endLine?: number): string {
const lines = content.split('\\n')
const start = startLine ? Math.max(1, startLine) : 1
const end = endLine ? Math.min(lines.length, endLine) : lines.length
if (start > end || start > lines.length) {
throw new Error(\`Invalid line range: start=\${start}, end=\${end}, total lines=\${lines.length}\`)
}
return lines.slice(start - 1, end).join('\\n')
}
export async function extractTextFromFile(filePath: string, startLine?: number, endLine?: number): Promise<string> {
try {
await fs.access(filePath)
} catch (error) {
throw new Error(\`File not found: \${filePath}\`)
}
const fileExtension = path.extname(filePath).toLowerCase()
let content: string
switch (fileExtension) {
case ".pdf": {
const dataBuffer = await fs.readFile(filePath)
const data = await pdf(dataBuffer)
content = extractLineRange(data.text, startLine, endLine)
break
}
case ".docx": {
const result = await mammoth.extractRawText({ path: filePath })
content = extractLineRange(result.value, startLine, endLine)
break
}
case ".ipynb": {
const data = await fs.readFile(filePath, "utf8")
const notebook = JSON.parse(data)
let extractedText = ""
for (const cell of notebook.cells) {
if ((cell.cell_type === "markdown" || cell.cell_type === "code") && cell.source) {
extractedText += cell.source.join("\\n") + "\\n"
}
}
content = extractLineRange(extractedText, startLine, endLine)
break
}
default: {
const isBinary = await isBinaryFile(filePath).catch(() => false)
if (!isBinary) {
const fileContent = await fs.readFile(filePath, "utf8")
content = extractLineRange(fileContent, startLine, endLine)
} else {
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
}
}
}
return addLineNumbers(content, startLine)
}
export function addLineNumbers(content: string, startLine: number = 1): string {
const lines = content.split('\\n')
const maxLineNumberWidth = String(startLine + lines.length - 1).length
return lines
.map((line, index) => {
const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ')
return \`\${lineNumber} | \${line}\`
}).join('\\n')
}`
expect(result).toBe(expected)
})
})
describe('getToolDescription', () => {
it('should include the current working directory', () => {
const cwd = '/test/dir'
const description = strategy.getToolDescription(cwd)
expect(description).toContain(`relative to the current working directory ${cwd}`)
})
it('should include required format elements', () => {
const description = strategy.getToolDescription('/test')
expect(description).toContain('<<<<<<< SEARCH')
expect(description).toContain('=======')
expect(description).toContain('>>>>>>> REPLACE')
expect(description).toContain('<apply_diff>')
expect(description).toContain('</apply_diff>')
})
})
})

View file

@ -0,0 +1,171 @@
import { DiffStrategy } from "../types"
export class SearchReplaceDiffStrategy implements DiffStrategy {
getToolDescription(cwd: string): string {
return `## apply_diff
Description: Request to replace existing code using search and replace blocks.
This tool allows for precise, surgical replaces to files by specifying exactly what content to search for and what to replace it with.
Only use this tool when you need to replace/fix existing code.
The tool will maintain proper indentation and formatting while making changes.
Only a single operation is allowed per tool use.
The SEARCH section must exactly match existing content including whitespace and indentation.
Parameters:
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
- diff: (required) The search/replace blocks defining the changes.
Format:
1. First line must be the file path
2. Followed by search/replace blocks:
\`\`\`
<<<<<<< SEARCH
[exact content to find including whitespace]
=======
[new content to replace with]
>>>>>>> REPLACE
\`\`\`
Example:
Original file:
\`\`\`
def calculate_total(items):
total = 0
for item in items:
total += item
return total
\`\`\`
Search/Replace content:
\`\`\`
main.py
<<<<<<< SEARCH
def calculate_total(items):
total = 0
for item in items:
total += item
return total
=======
def calculate_total(items):
"""Calculate total with 10% markup"""
return sum(item * 1.1 for item in items)
>>>>>>> REPLACE
\`\`\`
Usage:
<apply_diff>
<path>File path here</path>
<diff>
Your search/replace content here
</diff>
</apply_diff>`
}
applyDiff(originalContent: string, diffContent: string): string | false {
// Extract the search and replace blocks
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/);
if (!match) {
return false;
}
const [_, searchContent, replaceContent] = match;
// Split content into lines
const searchLines = searchContent.trim().split('\n');
const replaceLines = replaceContent.trim().split('\n');
const originalLines = originalContent.split('\n');
// Find the search content in the original
let matchIndex = -1;
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
let found = true;
for (let j = 0; j < searchLines.length; j++) {
const originalLine = originalLines[i + j];
const searchLine = searchLines[j];
// Compare lines after removing leading/trailing whitespace
if (originalLine.trim() !== searchLine.trim()) {
found = false;
break;
}
}
if (found) {
matchIndex = i;
break;
}
}
if (matchIndex === -1) {
return false;
}
// Get the matched lines from the original content
const matchedLines = originalLines.slice(matchIndex, matchIndex + searchLines.length);
// Get the exact indentation (preserving tabs/spaces) of each line
const originalIndents = matchedLines.map(line => {
const match = line.match(/^[\t ]*/);
return match ? match[0] : '';
});
// Get the exact indentation of each line in the search block
const searchIndents = searchLines.map(line => {
const match = line.match(/^[\t ]*/);
return match ? match[0] : '';
});
// Apply the replacement while preserving exact indentation
const indentedReplace = replaceLines.map((line, i) => {
// Get the corresponding original and search indentations
const originalIndent = originalIndents[Math.min(i, originalIndents.length - 1)];
const searchIndent = searchIndents[Math.min(i, searchIndents.length - 1)];
// Get the current line's indentation
const currentIndentMatch = line.match(/^[\t ]*/);
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : '';
// If this line has the same indentation level as the search block,
// use the original indentation. Otherwise, calculate the difference
// and preserve the exact type of whitespace characters
if (currentIndent.length === searchIndent.length) {
return originalIndent + line.trim();
} else {
// Get the corresponding search line's indentation
const searchLineIndex = Math.min(i, searchLines.length - 1);
const searchLineIndent = searchIndents[searchLineIndex];
// Get the corresponding original line's indentation
const originalLineIndex = Math.min(i, originalIndents.length - 1);
const originalLineIndent = originalIndents[originalLineIndex];
// If this line has the same indentation as its corresponding search line,
// use the original indentation
if (currentIndent === searchLineIndent) {
return originalLineIndent + line.trim();
}
// Otherwise, preserve the original indentation structure
const indentChar = originalLineIndent.charAt(0) || '\t';
const indentLevel = Math.floor(originalLineIndent.length / indentChar.length);
// Calculate the relative indentation from the search line
const searchLevel = Math.floor(searchLineIndent.length / indentChar.length);
const currentLevel = Math.floor(currentIndent.length / indentChar.length);
const relativeLevel = currentLevel - searchLevel;
// Apply the relative indentation to the original level
const targetLevel = Math.max(0, indentLevel + relativeLevel);
return indentChar.repeat(targetLevel) + line.trim();
}
});
// Construct the final content
const beforeMatch = originalLines.slice(0, matchIndex);
const afterMatch = originalLines.slice(matchIndex + searchLines.length);
return [...beforeMatch, ...indentedReplace, ...afterMatch].join('\n');
}
}