feat: add Coderrr Insights dashboard for productivity tracking and Skills for better workflow#124
feat: add Coderrr Insights dashboard for productivity tracking and Skills for better workflow#124Akash-nath29 merged 6 commits intomainfrom
Conversation
feat: add Coderrr Insights dashboard for productivity tracking
feat: add Coderrr Insights dashboard for productivity tracking and Skills for better workflow
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
🚀 Thanks for opening a Pull Request! A maintainer will review this soon. Meanwhile: Your contribution helps make this project better! |
There was a problem hiding this comment.
Pull request overview
This pull request introduces a Coderrr Insights dashboard for productivity tracking, adds support for Skills.md/Coderrr.md customization files, and implements a separate terminal execution feature for long-running commands. However, several critical issues prevent this from being ready for merge.
Changes:
- Added insights tracking infrastructure (module, UI, metrics utility) but failed to integrate it with the agent - insights are never actually recorded
- Implemented cross-platform separate terminal execution with process tracking and cleanup
- Enhanced file patching with multiple matching strategies for better robustness
- Added protected files rules to backend and self-healing improvements with completed steps context
- Updated documentation for new features and project customization options
Reviewed changes
Copilot reviewed 11 out of 13 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
| src/insights.js | New insights tracking module - NOT INTEGRATED with agent |
| src/insightsUI.js | Display dashboard for insights - lacks error handling |
| src/utils/metrics.js | Calculate time savings - NEVER USED |
| test/insights.test.js | Basic test for insights module |
| src/executor.js | Separate terminal execution - CRITICAL SECURITY ISSUES with command injection |
| src/agent.js | Process tracking/cleanup and self-healing - BREAKS ALL COMMANDS by running in separate terminals |
| src/fileOps.js | Enhanced patch matching - bug in empty line handling |
| backend/main.py | Protected files guidance - good additions |
| bin/coderrr.js | Adds insights command - minor ordering issue |
| docs/insights-guide.md | Documentation - claims unimplemented feature |
| README.md | Documentation updates - good additions |
| test/test-custom-prompt.js | Fixed import path |
| package-lock.json | Version bump to 1.1.1 |
Comments suppressed due to low confidence (1)
src/fileOps.js:247
- The enhanced
patchFilemethod with three matching strategies adds significant complexity (nearly 90 lines of new logic) but lacks test coverage. Given that this project uses Jest and has test files, you should add tests to verify the different matching strategies work correctly, handle edge cases (e.g., multiple matches, partial matches, files with mixed line endings), and provide helpful error messages when patterns aren't found.
async patchFile(filePath, oldContent, newContent) {
try {
const absolutePath = this.resolvePath(filePath);
// Check if file exists
if (!fs.existsSync(absolutePath)) {
throw new Error(`File not found: ${filePath}`);
}
// Read current content
const originalContent = fs.readFileSync(absolutePath, 'utf8');
// Try multiple matching strategies
let patchedContent = null;
// Strategy 1: Exact match
if (originalContent.includes(oldContent)) {
patchedContent = originalContent.replace(oldContent, newContent);
}
// Strategy 2: Normalize line endings (CRLF -> LF) and try again
if (!patchedContent) {
const normalizedOriginal = originalContent.replace(/\r\n/g, '\n');
const normalizedOld = oldContent.replace(/\r\n/g, '\n');
const normalizedNew = newContent.replace(/\r\n/g, '\n');
if (normalizedOriginal.includes(normalizedOld)) {
// Found with normalized line endings - apply patch and restore original line endings
const hasWindowsLineEndings = originalContent.includes('\r\n');
patchedContent = normalizedOriginal.replace(normalizedOld, normalizedNew);
if (hasWindowsLineEndings) {
patchedContent = patchedContent.replace(/\n/g, '\r\n');
}
}
}
// Strategy 3: Trim whitespace from each line and match
if (!patchedContent) {
const trimLines = (str) => str.split('\n').map(line => line.trim()).join('\n');
const trimmedOriginal = trimLines(originalContent.replace(/\r\n/g, '\n'));
const trimmedOld = trimLines(oldContent.replace(/\r\n/g, '\n'));
if (trimmedOriginal.includes(trimmedOld)) {
// Found with trimmed lines - need to find actual position
// This is a fallback, so we'll do a line-by-line search
const originalLines = originalContent.replace(/\r\n/g, '\n').split('\n');
const oldLines = oldContent.replace(/\r\n/g, '\n').split('\n').filter(l => l.trim());
// Find the starting line by matching first non-empty line
let startIdx = -1;
for (let i = 0; i < originalLines.length; i++) {
if (originalLines[i].trim() === oldLines[0].trim()) {
// Check if subsequent lines match
let matches = true;
for (let j = 0; j < oldLines.length && (i + j) < originalLines.length; j++) {
if (originalLines[i + j].trim() !== oldLines[j].trim()) {
matches = false;
break;
}
}
if (matches) {
startIdx = i;
break;
}
}
}
if (startIdx >= 0) {
// Replace the lines
const newLines = newContent.replace(/\r\n/g, '\n').split('\n');
const beforeLines = originalLines.slice(0, startIdx);
const afterLines = originalLines.slice(startIdx + oldLines.length);
const hasWindowsLineEndings = originalContent.includes('\r\n');
const lineEnding = hasWindowsLineEndings ? '\r\n' : '\n';
patchedContent = [...beforeLines, ...newLines, ...afterLines].join(lineEnding);
}
}
}
// If no strategy worked, throw error with helpful message
if (!patchedContent) {
// Show what we were looking for to help debug
const preview = oldContent.substring(0, 100).replace(/\n/g, '\\n');
throw new Error(`Pattern not found in file: ${filePath}\nLooking for: "${preview}${oldContent.length > 100 ? '...' : ''}"`);
}
// Write back
fs.writeFileSync(absolutePath, patchedContent, 'utf8');
ui.displayFileOp('patch_file', filePath, 'success');
return {
success: true,
path: absolutePath,
oldContent: originalContent,
newContent: patchedContent
};
} catch (error) {
ui.displayFileOp('patch_file', filePath, 'error');
throw error;
}
}
| { cmd: 'gnome-terminal', args: ['--', 'bash', '-c', `${shellScript}; echo $$ > ${pidFile}`] }, | ||
| { cmd: 'konsole', args: ['-e', 'bash', '-c', `${shellScript}; echo $$ > ${pidFile}`] }, | ||
| { cmd: 'xfce4-terminal', args: ['-e', `bash -c "${shellScript}; echo $$ > ${pidFile}"`] }, | ||
| { cmd: 'xterm', args: ['-e', `bash -c "${shellScript}; echo $$ > ${pidFile}"`] } |
There was a problem hiding this comment.
The Linux shell script has the same command injection vulnerabilities as macOS. Additionally, the terminal args array (lines 497-500) directly interpolates shellScript and pidFile without proper escaping. If these paths contain shell special characters, they could be interpreted as shell commands. Use proper shell escaping or pass arguments through safer mechanisms.
| { cmd: 'gnome-terminal', args: ['--', 'bash', '-c', `${shellScript}; echo $$ > ${pidFile}`] }, | |
| { cmd: 'konsole', args: ['-e', 'bash', '-c', `${shellScript}; echo $$ > ${pidFile}`] }, | |
| { cmd: 'xfce4-terminal', args: ['-e', `bash -c "${shellScript}; echo $$ > ${pidFile}"`] }, | |
| { cmd: 'xterm', args: ['-e', `bash -c "${shellScript}; echo $$ > ${pidFile}"`] } | |
| { | |
| cmd: 'gnome-terminal', | |
| args: ['--', 'bash', '-c', 'script="$1"; pid="$2"; "$script"; echo $$ > "$pid"', '--', shellScript, pidFile] | |
| }, | |
| { | |
| cmd: 'konsole', | |
| args: ['-e', 'bash', '-c', 'script="$1"; pid="$2"; "$script"; echo $$ > "$pid"', '--', shellScript, pidFile] | |
| }, | |
| { | |
| cmd: 'xfce4-terminal', | |
| args: ['-e', 'bash', '-c', 'script="$1"; pid="$2"; "$script"; echo $$ > "$pid"', '--', shellScript, pidFile] | |
| }, | |
| { | |
| cmd: 'xterm', | |
| args: ['-e', 'bash', '-c', 'script="$1"; pid="$2"; "$script"; echo $$ > "$pid"', '--', shellScript, pidFile] | |
| } |
| }; | ||
|
|
||
| // Handle various exit signals | ||
| process.on('exit', cleanup); |
There was a problem hiding this comment.
The cleanup function is defined as async but the 'exit' event handler (line 92) doesn't await it. The Node.js 'exit' event is synchronous and cannot wait for async operations. This means spawned processes might not be properly terminated when the process exits normally. Consider using 'beforeExit' event for async cleanup, or make the cleanup synchronous where possible.
| process.on('exit', cleanup); | |
| process.on('beforeExit', cleanup); |
|
|
||
| ## Features | ||
| - **Usage Statistics:** Track total tasks, file modifications, and self-healing successes. | ||
| - **Productivity Estimation:** See how much manual coding time you've potentially saved. |
There was a problem hiding this comment.
The documentation mentions "Productivity Estimation" as a feature (line 7), but the calculateSavings function from src/utils/metrics.js is never actually used in the insights display. This creates a discrepancy between what's documented and what's actually implemented.
| const data = insights.getStats(); | ||
| console.log('\n' + chalk.cyan.bold('📊 CODERRR INSIGHTS DASHBOARD')); | ||
| console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); | ||
|
|
||
| console.log(`${chalk.white('Total Tasks Processed: ')} ${chalk.green.bold(data.totals.tasks)}`); | ||
| console.log(`${chalk.white('Files Modified: ')} ${chalk.yellow.bold(data.totals.filesChanged)}`); | ||
| console.log(`${chalk.white('Self-Healing Events: ')} ${chalk.magenta.bold(data.totals.healings)}`); | ||
|
|
||
| console.log('\n' + chalk.cyan.bold('🕒 RECENT ACTIVITY')); | ||
| data.sessions.slice(-5).reverse().forEach(s => { | ||
| const status = s.success ? chalk.green('✔') : chalk.red('✘'); | ||
| const date = new Date(s.timestamp).toLocaleDateString(); | ||
| console.log(`${status} ${chalk.gray(`[${date}]`)} ${s.task.substring(0, 40)}...`); | ||
| }); |
There was a problem hiding this comment.
The displayInsights() function can crash if getStats() throws an error or returns undefined/null. Additionally, if there are no sessions yet, data.sessions.slice(-5) will work but iterating may produce an error if sessions is not an array. Add error handling to gracefully handle cases where insights data is missing or corrupted.
| const data = insights.getStats(); | |
| console.log('\n' + chalk.cyan.bold('📊 CODERRR INSIGHTS DASHBOARD')); | |
| console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); | |
| console.log(`${chalk.white('Total Tasks Processed: ')} ${chalk.green.bold(data.totals.tasks)}`); | |
| console.log(`${chalk.white('Files Modified: ')} ${chalk.yellow.bold(data.totals.filesChanged)}`); | |
| console.log(`${chalk.white('Self-Healing Events: ')} ${chalk.magenta.bold(data.totals.healings)}`); | |
| console.log('\n' + chalk.cyan.bold('🕒 RECENT ACTIVITY')); | |
| data.sessions.slice(-5).reverse().forEach(s => { | |
| const status = s.success ? chalk.green('✔') : chalk.red('✘'); | |
| const date = new Date(s.timestamp).toLocaleDateString(); | |
| console.log(`${status} ${chalk.gray(`[${date}]`)} ${s.task.substring(0, 40)}...`); | |
| }); | |
| let data; | |
| try { | |
| data = insights.getStats(); | |
| } catch (error) { | |
| console.error(chalk.red('Failed to load insights data:'), error && error.message ? error.message : error); | |
| console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')); | |
| return; | |
| } | |
| if (!data || typeof data !== 'object') { | |
| console.error(chalk.red('Insights data is missing or invalid.')); | |
| console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')); | |
| return; | |
| } | |
| const totals = (data.totals && typeof data.totals === 'object') ? data.totals : {}; | |
| const totalTasks = Number.isFinite(totals.tasks) ? totals.tasks : 0; | |
| const filesChanged = Number.isFinite(totals.filesChanged) ? totals.filesChanged : 0; | |
| const healings = Number.isFinite(totals.healings) ? totals.healings : 0; | |
| console.log('\n' + chalk.cyan.bold('📊 CODERRR INSIGHTS DASHBOARD')); | |
| console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); | |
| console.log(`${chalk.white('Total Tasks Processed: ')} ${chalk.green.bold(totalTasks)}`); | |
| console.log(`${chalk.white('Files Modified: ')} ${chalk.yellow.bold(filesChanged)}`); | |
| console.log(`${chalk.white('Self-Healing Events: ')} ${chalk.magenta.bold(healings)}`); | |
| console.log('\n' + chalk.cyan.bold('🕒 RECENT ACTIVITY')); | |
| const sessions = Array.isArray(data.sessions) ? data.sessions : []; | |
| if (sessions.length === 0) { | |
| console.log(chalk.gray('No recent activity available.')); | |
| } else { | |
| sessions.slice(-5).reverse().forEach(s => { | |
| const status = s && s.success ? chalk.green('✔') : chalk.red('✘'); | |
| const date = s && s.timestamp ? new Date(s.timestamp).toLocaleDateString() : 'Unknown date'; | |
| const task = s && typeof s.task === 'string' ? s.task : 'Unknown task'; | |
| console.log(`${status} ${chalk.gray(`[${date}]`)} ${task.substring(0, 40)}${task.length > 40 ? '...' : ''}`); | |
| }); | |
| } |
| Write-Host "Command: ${command.replace(/"/g, '\\"')}" -ForegroundColor Yellow | ||
| Write-Host "Started at: $(Get-Date)" -ForegroundColor Gray | ||
| Write-Host "========================================" -ForegroundColor Cyan | ||
| Write-Host "" | ||
|
|
||
| # Log start to file | ||
| "Command started at $(Get-Date)" | Out-File -FilePath "${outputFile.replace(/\\/g, '\\\\')}" -Encoding UTF8 | ||
|
|
||
| try { | ||
| # Run command and tee output to both console and file | ||
| Invoke-Expression "${command.replace(/"/g, '\\"')}" 2>&1 | Tee-Object -FilePath "${outputFile.replace(/\\/g, '\\\\')}" -Append |
There was a problem hiding this comment.
The command string interpolation in the PowerShell script is vulnerable to command injection. If the command parameter contains backticks, dollar signs, or other PowerShell special characters, they could be interpreted as PowerShell code. The .replace(/"/g, '\\"') only escapes double quotes but doesn't handle other special characters. Consider using PowerShell's -Command parameter with proper escaping or writing the command to a temporary file instead of embedding it in the script.
| const fs = require('fs'); | ||
|
|
There was a problem hiding this comment.
Unused variable fs.
| const fs = require('fs'); |
| ui.info('The terminal window will show the command output.'); | ||
| } else { | ||
| const errorMsg = result.error || 'Unknown error'; | ||
| stepResult = `Failed to start command: "${step.command}". Error: ${errorMsg}`; |
There was a problem hiding this comment.
The value assigned to stepResult here is unused.
| stepResult = `Failed to start command: "${step.command}". Error: ${errorMsg}`; | |
| stepResult = `Failed to start command: "${step.command}". Error: ${errorMsg}`; | |
| ui.error(stepResult); |
| let terminalProcess = null; | ||
| let childPid = null; | ||
|
|
||
| try { | ||
| if (platform === 'win32') { | ||
| // Windows: Use start command with cmd.exe | ||
| const result = await this.spawnWindowsTerminal(normalizedCommand, cwd, outputFile, pidFile); | ||
| terminalProcess = result.process; | ||
| childPid = result.pid; | ||
| } else if (platform === 'darwin') { | ||
| // macOS: Use osascript to open Terminal.app | ||
| const result = await this.spawnMacTerminal(normalizedCommand, cwd, outputFile, pidFile); | ||
| terminalProcess = result.process; | ||
| childPid = result.pid; | ||
| } else { | ||
| // Linux: Try gnome-terminal, konsole, or xterm | ||
| const result = await this.spawnLinuxTerminal(normalizedCommand, cwd, outputFile, pidFile); | ||
| terminalProcess = result.process; |
There was a problem hiding this comment.
The value assigned to terminalProcess here is unused.
| let terminalProcess = null; | |
| let childPid = null; | |
| try { | |
| if (platform === 'win32') { | |
| // Windows: Use start command with cmd.exe | |
| const result = await this.spawnWindowsTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| terminalProcess = result.process; | |
| childPid = result.pid; | |
| } else if (platform === 'darwin') { | |
| // macOS: Use osascript to open Terminal.app | |
| const result = await this.spawnMacTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| terminalProcess = result.process; | |
| childPid = result.pid; | |
| } else { | |
| // Linux: Try gnome-terminal, konsole, or xterm | |
| const result = await this.spawnLinuxTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| terminalProcess = result.process; | |
| let childPid = null; | |
| try { | |
| if (platform === 'win32') { | |
| // Windows: Use start command with cmd.exe | |
| const result = await this.spawnWindowsTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| childPid = result.pid; | |
| } else if (platform === 'darwin') { | |
| // macOS: Use osascript to open Terminal.app | |
| const result = await this.spawnMacTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| childPid = result.pid; | |
| } else { | |
| // Linux: Try gnome-terminal, konsole, or xterm | |
| const result = await this.spawnLinuxTerminal(normalizedCommand, cwd, outputFile, pidFile); |
| let terminalProcess = null; | ||
| let childPid = null; | ||
|
|
||
| try { | ||
| if (platform === 'win32') { | ||
| // Windows: Use start command with cmd.exe | ||
| const result = await this.spawnWindowsTerminal(normalizedCommand, cwd, outputFile, pidFile); | ||
| terminalProcess = result.process; | ||
| childPid = result.pid; | ||
| } else if (platform === 'darwin') { | ||
| // macOS: Use osascript to open Terminal.app | ||
| const result = await this.spawnMacTerminal(normalizedCommand, cwd, outputFile, pidFile); | ||
| terminalProcess = result.process; | ||
| childPid = result.pid; | ||
| } else { | ||
| // Linux: Try gnome-terminal, konsole, or xterm | ||
| const result = await this.spawnLinuxTerminal(normalizedCommand, cwd, outputFile, pidFile); | ||
| terminalProcess = result.process; |
There was a problem hiding this comment.
The value assigned to terminalProcess here is unused.
| let terminalProcess = null; | |
| let childPid = null; | |
| try { | |
| if (platform === 'win32') { | |
| // Windows: Use start command with cmd.exe | |
| const result = await this.spawnWindowsTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| terminalProcess = result.process; | |
| childPid = result.pid; | |
| } else if (platform === 'darwin') { | |
| // macOS: Use osascript to open Terminal.app | |
| const result = await this.spawnMacTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| terminalProcess = result.process; | |
| childPid = result.pid; | |
| } else { | |
| // Linux: Try gnome-terminal, konsole, or xterm | |
| const result = await this.spawnLinuxTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| terminalProcess = result.process; | |
| let childPid = null; | |
| try { | |
| if (platform === 'win32') { | |
| // Windows: Use start command with cmd.exe | |
| const result = await this.spawnWindowsTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| childPid = result.pid; | |
| } else if (platform === 'darwin') { | |
| // macOS: Use osascript to open Terminal.app | |
| const result = await this.spawnMacTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| childPid = result.pid; | |
| } else { | |
| // Linux: Try gnome-terminal, konsole, or xterm | |
| const result = await this.spawnLinuxTerminal(normalizedCommand, cwd, outputFile, pidFile); |
| let terminalProcess = null; | ||
| let childPid = null; | ||
|
|
||
| try { | ||
| if (platform === 'win32') { | ||
| // Windows: Use start command with cmd.exe | ||
| const result = await this.spawnWindowsTerminal(normalizedCommand, cwd, outputFile, pidFile); | ||
| terminalProcess = result.process; | ||
| childPid = result.pid; | ||
| } else if (platform === 'darwin') { | ||
| // macOS: Use osascript to open Terminal.app | ||
| const result = await this.spawnMacTerminal(normalizedCommand, cwd, outputFile, pidFile); | ||
| terminalProcess = result.process; | ||
| childPid = result.pid; | ||
| } else { | ||
| // Linux: Try gnome-terminal, konsole, or xterm | ||
| const result = await this.spawnLinuxTerminal(normalizedCommand, cwd, outputFile, pidFile); | ||
| terminalProcess = result.process; |
There was a problem hiding this comment.
The value assigned to terminalProcess here is unused.
| let terminalProcess = null; | |
| let childPid = null; | |
| try { | |
| if (platform === 'win32') { | |
| // Windows: Use start command with cmd.exe | |
| const result = await this.spawnWindowsTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| terminalProcess = result.process; | |
| childPid = result.pid; | |
| } else if (platform === 'darwin') { | |
| // macOS: Use osascript to open Terminal.app | |
| const result = await this.spawnMacTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| terminalProcess = result.process; | |
| childPid = result.pid; | |
| } else { | |
| // Linux: Try gnome-terminal, konsole, or xterm | |
| const result = await this.spawnLinuxTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| terminalProcess = result.process; | |
| let childPid = null; | |
| try { | |
| if (platform === 'win32') { | |
| // Windows: Use start command with cmd.exe | |
| const result = await this.spawnWindowsTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| childPid = result.pid; | |
| } else if (platform === 'darwin') { | |
| // macOS: Use osascript to open Terminal.app | |
| const result = await this.spawnMacTerminal(normalizedCommand, cwd, outputFile, pidFile); | |
| childPid = result.pid; | |
| } else { | |
| // Linux: Try gnome-terminal, konsole, or xterm | |
| const result = await this.spawnLinuxTerminal(normalizedCommand, cwd, outputFile, pidFile); |
This pull request introduces several significant improvements to Coderrr, focusing on enhanced project customization, safer file operations, robust command execution, and better documentation. The most important changes are grouped below:
Project Customization and Documentation:
README.md, detailing support forSkills.md(project-wide coding guidelines),Coderrr.md(task-specific instructions), and persistent cross-session memory via.coderrr/memory.json. Also, a newdocs/insights-guide.mdwas added to document the Coderrr Insights analytics feature. [1] [2] [3] [4]File Operation Safety and Guidance:
Coderrr.md,Skills.md,.coderrr/). Added explicit rules and reminders to never delete these files, clarified when to usepatch_filevsupdate_file, and required reading a file before patching. These rules are now included in both the backend prompt and self-healing instructions. [1] [2] [3] [4]Command Execution Improvements:
Self-Healing and Plan Execution Context:
File Patching Robustness:
patchFilemethod insrc/fileOps.jsto support more flexible and robust matching, including normalization of line endings and fuzzy line-by-line matching, to better handle differences between AI output and actual file content.