ralphblaster-agent 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +294 -0
- package/bin/agent-dashboard.sh +168 -0
- package/bin/monitor-agent.sh +264 -0
- package/bin/ralphblaster.js +247 -0
- package/package.json +64 -0
- package/postinstall-colored.js +66 -0
- package/src/api-client.js +764 -0
- package/src/claude-plugin/.claude-plugin/plugin.json +9 -0
- package/src/claude-plugin/README.md +42 -0
- package/src/claude-plugin/skills/ralph/SKILL.md +259 -0
- package/src/commands/add-project.js +257 -0
- package/src/commands/init.js +79 -0
- package/src/config-file-manager.js +84 -0
- package/src/config.js +66 -0
- package/src/error-window.js +86 -0
- package/src/executor/claude-runner.js +716 -0
- package/src/executor/error-handler.js +65 -0
- package/src/executor/git-helper.js +196 -0
- package/src/executor/index.js +296 -0
- package/src/executor/job-handlers/clarifying-questions.js +213 -0
- package/src/executor/job-handlers/code-execution.js +145 -0
- package/src/executor/job-handlers/prd-generation.js +259 -0
- package/src/executor/path-helper.js +74 -0
- package/src/executor/prompt-validator.js +51 -0
- package/src/executor.js +4 -0
- package/src/index.js +342 -0
- package/src/logger.js +193 -0
- package/src/logging/README.md +93 -0
- package/src/logging/config.js +179 -0
- package/src/logging/destinations/README.md +290 -0
- package/src/logging/destinations/api-destination-unbatched.js +118 -0
- package/src/logging/destinations/api-destination.js +40 -0
- package/src/logging/destinations/base-destination.js +85 -0
- package/src/logging/destinations/batched-destination.js +198 -0
- package/src/logging/destinations/console-destination.js +172 -0
- package/src/logging/destinations/file-destination.js +208 -0
- package/src/logging/destinations/index.js +29 -0
- package/src/logging/destinations/progress-batch-destination-unbatched.js +92 -0
- package/src/logging/destinations/progress-batch-destination.js +41 -0
- package/src/logging/formatter.js +288 -0
- package/src/logging/log-manager.js +426 -0
- package/src/progress-throttle.js +101 -0
- package/src/system-monitor.js +64 -0
- package/src/utils/format.js +16 -0
- package/src/utils/log-file-helper.js +265 -0
- package/src/utils/progress-parser.js +250 -0
- package/src/worktree-manager.js +255 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const logger = require('../logger');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Categorize error for user-friendly messaging
|
|
5
|
+
* @param {Error} error - The error object
|
|
6
|
+
* @param {string} stderr - Standard error output
|
|
7
|
+
* @param {number} exitCode - Process exit code
|
|
8
|
+
* @returns {Object} Object with category, userMessage, and technicalDetails
|
|
9
|
+
*/
|
|
10
|
+
function categorizeError(error, stderr = '', exitCode = null) {
|
|
11
|
+
let category = 'unknown';
|
|
12
|
+
let userMessage = error.message || String(error);
|
|
13
|
+
let technicalDetails = `Error: ${error.message}\nStderr: ${stderr}\nExit Code: ${exitCode}`;
|
|
14
|
+
|
|
15
|
+
// Check for Claude CLI not installed
|
|
16
|
+
if (error.code === 'ENOENT') {
|
|
17
|
+
category = 'claude_not_installed';
|
|
18
|
+
userMessage = 'Claude Code CLI is not installed or not found in PATH';
|
|
19
|
+
}
|
|
20
|
+
// Check for authentication issues
|
|
21
|
+
else if (stderr.match(/not authenticated/i) || stderr.match(/authentication failed/i) || stderr.match(/please log in/i)) {
|
|
22
|
+
category = 'not_authenticated';
|
|
23
|
+
userMessage = 'Claude CLI is not authenticated. Please run "claude auth"';
|
|
24
|
+
}
|
|
25
|
+
// Check for token limit exceeded
|
|
26
|
+
else if (stderr.match(/token limit exceeded/i) || stderr.match(/quota exceeded/i) || stderr.match(/insufficient credits/i)) {
|
|
27
|
+
category = 'out_of_tokens';
|
|
28
|
+
userMessage = 'Claude API token limit has been exceeded';
|
|
29
|
+
}
|
|
30
|
+
// Check for rate limiting
|
|
31
|
+
else if (stderr.match(/rate limit/i) || stderr.match(/too many requests/i) || stderr.match(/429/)) {
|
|
32
|
+
category = 'rate_limited';
|
|
33
|
+
userMessage = 'Claude API rate limit reached. Please wait before retrying';
|
|
34
|
+
}
|
|
35
|
+
// Check for permission denied
|
|
36
|
+
else if (stderr.match(/permission denied/i) || stderr.match(/EACCES/i) || error.code === 'EACCES') {
|
|
37
|
+
category = 'permission_denied';
|
|
38
|
+
userMessage = 'Permission denied accessing project files or directories';
|
|
39
|
+
}
|
|
40
|
+
// Check for timeout
|
|
41
|
+
else if (error.message && error.message.includes('timed out')) {
|
|
42
|
+
category = 'execution_timeout';
|
|
43
|
+
userMessage = 'Job execution exceeded the maximum timeout';
|
|
44
|
+
}
|
|
45
|
+
// Check for network errors
|
|
46
|
+
else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT') {
|
|
47
|
+
category = 'network_error';
|
|
48
|
+
userMessage = 'Network error connecting to Claude API';
|
|
49
|
+
}
|
|
50
|
+
// Check for non-zero exit code (execution error)
|
|
51
|
+
else if (exitCode !== null && exitCode !== 0) {
|
|
52
|
+
category = 'execution_error';
|
|
53
|
+
userMessage = `Claude CLI execution failed with exit code ${exitCode}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
logger.debug(`Error categorized as: ${category}`);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
category,
|
|
60
|
+
userMessage,
|
|
61
|
+
technicalDetails
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { categorizeError };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const logger = require('../logger');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GitHelper - Handles git operations for worktrees
|
|
7
|
+
* Extracted from Executor class to improve modularity and testability
|
|
8
|
+
*/
|
|
9
|
+
class GitHelper {
|
|
10
|
+
/**
|
|
11
|
+
* Helper to run git commands in worktree
|
|
12
|
+
* @param {string} cwd - Working directory
|
|
13
|
+
* @param {Array<string>} args - Git command arguments
|
|
14
|
+
* @returns {Promise<string>} Command output
|
|
15
|
+
*/
|
|
16
|
+
async runGitCommand(cwd, args) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const git = spawn('git', args, { cwd });
|
|
19
|
+
let output = '';
|
|
20
|
+
|
|
21
|
+
git.stdout.on('data', (data) => {
|
|
22
|
+
output += data.toString();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
git.on('close', (code) => {
|
|
26
|
+
if (code === 0) {
|
|
27
|
+
resolve(output);
|
|
28
|
+
} else {
|
|
29
|
+
reject(new Error(`Git command failed with code ${code}`));
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
git.on('error', reject);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get current branch name from worktree
|
|
39
|
+
* @param {string} worktreePath - Path to worktree
|
|
40
|
+
* @returns {Promise<string>} Branch name
|
|
41
|
+
*/
|
|
42
|
+
async getCurrentBranch(worktreePath) {
|
|
43
|
+
try {
|
|
44
|
+
const branch = await this.runGitCommand(
|
|
45
|
+
worktreePath,
|
|
46
|
+
['rev-parse', '--abbrev-ref', 'HEAD']
|
|
47
|
+
);
|
|
48
|
+
return branch.trim();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Log comprehensive git activity summary for a job
|
|
56
|
+
* @param {string} worktreePath - Path to worktree
|
|
57
|
+
* @param {string} branchName - Name of the branch
|
|
58
|
+
* @param {string} jobId - Job identifier
|
|
59
|
+
* @param {Function} onProgress - Optional progress callback
|
|
60
|
+
* @returns {Promise<Object>} Git activity summary object
|
|
61
|
+
*/
|
|
62
|
+
async logGitActivity(worktreePath, branchName, jobId, onProgress = null) {
|
|
63
|
+
if (!worktreePath || !fs.existsSync(worktreePath)) {
|
|
64
|
+
logger.warn(`Worktree not found for git activity logging: ${worktreePath}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Get commit count on this branch (new commits only, not in origin/main)
|
|
70
|
+
const commitCount = await new Promise((resolve) => {
|
|
71
|
+
const gitLog = spawn('git', ['rev-list', '--count', 'HEAD', `^origin/main`], {
|
|
72
|
+
cwd: worktreePath,
|
|
73
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let stdout = '';
|
|
77
|
+
gitLog.stdout.on('data', (data) => stdout += data.toString());
|
|
78
|
+
gitLog.on('close', () => resolve(parseInt(stdout.trim()) || 0));
|
|
79
|
+
gitLog.on('error', () => resolve(0));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Also check for uncommitted changes
|
|
83
|
+
const hasUncommittedChanges = await new Promise((resolve) => {
|
|
84
|
+
const gitStatus = spawn('git', ['status', '--porcelain'], {
|
|
85
|
+
cwd: worktreePath,
|
|
86
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let stdout = '';
|
|
90
|
+
gitStatus.stdout.on('data', (data) => stdout += data.toString());
|
|
91
|
+
gitStatus.on('close', () => resolve(stdout.trim().length > 0));
|
|
92
|
+
gitStatus.on('error', () => resolve(false));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Get last commit message and hash if commits exist
|
|
96
|
+
let lastCommitInfo = 'No commits yet';
|
|
97
|
+
if (commitCount > 0) {
|
|
98
|
+
lastCommitInfo = await new Promise((resolve) => {
|
|
99
|
+
const gitLog = spawn('git', ['log', '-1', '--pretty=format:%h - %s'], {
|
|
100
|
+
cwd: worktreePath,
|
|
101
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let stdout = '';
|
|
105
|
+
gitLog.stdout.on('data', (data) => stdout += data.toString());
|
|
106
|
+
gitLog.on('close', () => resolve(stdout.trim() || 'No commit info'));
|
|
107
|
+
gitLog.on('error', () => resolve('Failed to get commit info'));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check if branch was pushed to remote
|
|
112
|
+
const wasPushed = await new Promise((resolve) => {
|
|
113
|
+
const gitBranch = spawn('git', ['branch', '-r', '--contains', 'HEAD'], {
|
|
114
|
+
cwd: worktreePath,
|
|
115
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
let stdout = '';
|
|
119
|
+
gitBranch.stdout.on('data', (data) => stdout += data.toString());
|
|
120
|
+
gitBranch.on('close', () => {
|
|
121
|
+
const remoteBranches = stdout.trim();
|
|
122
|
+
resolve(remoteBranches.includes(`origin/${branchName}`));
|
|
123
|
+
});
|
|
124
|
+
gitBranch.on('error', () => resolve(false));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Get file change stats
|
|
128
|
+
const changeStats = await new Promise((resolve) => {
|
|
129
|
+
const gitDiff = spawn('git', ['diff', '--shortstat', 'origin/main...HEAD'], {
|
|
130
|
+
cwd: worktreePath,
|
|
131
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
let stdout = '';
|
|
135
|
+
gitDiff.stdout.on('data', (data) => stdout += data.toString());
|
|
136
|
+
gitDiff.on('close', () => resolve(stdout.trim() || 'No changes'));
|
|
137
|
+
gitDiff.on('error', () => resolve('Failed to get change stats'));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Build comprehensive git activity summary
|
|
141
|
+
const summaryLines = [];
|
|
142
|
+
summaryLines.push('');
|
|
143
|
+
summaryLines.push('═══════════════════════════════════════════════════════════');
|
|
144
|
+
summaryLines.push(`Git Activity Summary for Job #${jobId}`);
|
|
145
|
+
summaryLines.push('═══════════════════════════════════════════════════════════');
|
|
146
|
+
summaryLines.push(`Branch: ${branchName}`);
|
|
147
|
+
summaryLines.push(`New commits: ${commitCount}`);
|
|
148
|
+
|
|
149
|
+
if (commitCount > 0) {
|
|
150
|
+
summaryLines.push(`Latest commit: ${lastCommitInfo}`);
|
|
151
|
+
summaryLines.push(`Changes: ${changeStats}`);
|
|
152
|
+
summaryLines.push(`Pushed to remote: ${wasPushed ? 'YES ✓' : 'NO (local only)'}`);
|
|
153
|
+
} else {
|
|
154
|
+
summaryLines.push('⚠️ NO COMMITS MADE - Ralph did not create any commits');
|
|
155
|
+
if (hasUncommittedChanges) {
|
|
156
|
+
summaryLines.push('⚠️ Uncommitted changes detected - work was done but not committed!');
|
|
157
|
+
} else {
|
|
158
|
+
summaryLines.push('⚠️ No file changes detected - Ralph may have failed or had nothing to do');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
summaryLines.push('═══════════════════════════════════════════════════════════');
|
|
162
|
+
summaryLines.push('');
|
|
163
|
+
|
|
164
|
+
const summaryText = summaryLines.join('\n');
|
|
165
|
+
|
|
166
|
+
// Log to console
|
|
167
|
+
logger.info(summaryText);
|
|
168
|
+
|
|
169
|
+
// Send to server if progress callback provided
|
|
170
|
+
if (onProgress) {
|
|
171
|
+
onProgress(summaryText);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Return structured summary object
|
|
175
|
+
return {
|
|
176
|
+
branchName,
|
|
177
|
+
commitCount,
|
|
178
|
+
lastCommitInfo: commitCount > 0 ? lastCommitInfo : null,
|
|
179
|
+
changeStats: commitCount > 0 ? changeStats : null,
|
|
180
|
+
wasPushed,
|
|
181
|
+
hasUncommittedChanges,
|
|
182
|
+
summaryText
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
logger.warn(`Failed to log git activity: ${error.message}`);
|
|
186
|
+
return {
|
|
187
|
+
branchName,
|
|
188
|
+
commitCount: 0,
|
|
189
|
+
error: error.message,
|
|
190
|
+
summaryText: `Failed to gather git activity: ${error.message}`
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = GitHelper;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const logger = require('../logger');
|
|
3
|
+
const { validatePrompt } = require('./prompt-validator');
|
|
4
|
+
const { categorizeError } = require('./error-handler');
|
|
5
|
+
const GitHelper = require('./git-helper');
|
|
6
|
+
const ClaudeRunner = require('./claude-runner');
|
|
7
|
+
const PathHelper = require('./path-helper');
|
|
8
|
+
const PrdGenerationHandler = require('./job-handlers/prd-generation');
|
|
9
|
+
const CodeExecutionHandler = require('./job-handlers/code-execution');
|
|
10
|
+
const ClarifyingQuestionsHandler = require('./job-handlers/clarifying-questions');
|
|
11
|
+
|
|
12
|
+
// Timeout constants
|
|
13
|
+
const TIMEOUTS = {
|
|
14
|
+
PROCESS_KILL_GRACE_PERIOD_MS: 2000, // 2 seconds grace period for process termination
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
class Executor {
|
|
18
|
+
constructor(apiClient = null) {
|
|
19
|
+
this.apiClient = apiClient; // Optional API client for metadata updates
|
|
20
|
+
this.gitHelper = new GitHelper(); // Git operations helper
|
|
21
|
+
|
|
22
|
+
// Create ClaudeRunner with dependencies
|
|
23
|
+
const errorHandler = { categorizeError };
|
|
24
|
+
this.claudeRunner = new ClaudeRunner(errorHandler, this.gitHelper);
|
|
25
|
+
this.claudeRunner.setApiClient(apiClient);
|
|
26
|
+
|
|
27
|
+
// Create shared validators and helpers
|
|
28
|
+
const promptValidator = { validatePrompt };
|
|
29
|
+
const pathValidator = { validateAndSanitizePath: this.validateAndSanitizePath.bind(this) };
|
|
30
|
+
const pathHelper = new PathHelper(pathValidator);
|
|
31
|
+
|
|
32
|
+
// Create PrdGenerationHandler with dependencies
|
|
33
|
+
this.prdGenerationHandler = new PrdGenerationHandler(
|
|
34
|
+
promptValidator,
|
|
35
|
+
pathHelper,
|
|
36
|
+
this.claudeRunner,
|
|
37
|
+
apiClient
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Create CodeExecutionHandler with dependencies
|
|
41
|
+
this.codeExecutionHandler = new CodeExecutionHandler(
|
|
42
|
+
promptValidator,
|
|
43
|
+
pathHelper,
|
|
44
|
+
this.claudeRunner,
|
|
45
|
+
this.gitHelper,
|
|
46
|
+
apiClient
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Create ClarifyingQuestionsHandler with dependencies
|
|
50
|
+
this.clarifyingQuestionsHandler = new ClarifyingQuestionsHandler(
|
|
51
|
+
promptValidator,
|
|
52
|
+
pathHelper,
|
|
53
|
+
this.claudeRunner,
|
|
54
|
+
apiClient
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Getter for currentProcess (delegates to claudeRunner for backward compatibility)
|
|
60
|
+
*/
|
|
61
|
+
get currentProcess() {
|
|
62
|
+
return this.claudeRunner.currentProcess;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Setter for currentProcess (delegates to claudeRunner for backward compatibility)
|
|
67
|
+
*/
|
|
68
|
+
set currentProcess(value) {
|
|
69
|
+
this.claudeRunner.currentProcess = value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get sanitized environment variables (delegates to claudeRunner for backward compatibility)
|
|
74
|
+
* @returns {Object} Sanitized environment object
|
|
75
|
+
*/
|
|
76
|
+
getSanitizedEnv() {
|
|
77
|
+
return this.claudeRunner.getSanitizedEnv();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Execute a job using Claude CLI
|
|
82
|
+
* @param {Object} job - Job object from API
|
|
83
|
+
* @param {Function} onProgress - Callback for progress updates
|
|
84
|
+
* @returns {Promise<Object>} Execution result
|
|
85
|
+
*/
|
|
86
|
+
async execute(job, onProgress) {
|
|
87
|
+
const startTime = Date.now();
|
|
88
|
+
|
|
89
|
+
// Store job ID for progress tracking
|
|
90
|
+
this.currentJobId = job.id;
|
|
91
|
+
|
|
92
|
+
// Set job ID in ClaudeRunner for progress tracking
|
|
93
|
+
this.claudeRunner.setJobId(job.id);
|
|
94
|
+
|
|
95
|
+
// Display human-friendly job description
|
|
96
|
+
const jobDescription = job.job_type === 'plan_generation'
|
|
97
|
+
? `${job.prd_mode === 'plan' ? 'plan' : 'PRD'} generation`
|
|
98
|
+
: job.job_type;
|
|
99
|
+
|
|
100
|
+
logger.info(`Executing ${jobDescription} job #${job.id}`);
|
|
101
|
+
|
|
102
|
+
// Route to appropriate handler based on job type
|
|
103
|
+
if (job.job_type === 'plan_generation') {
|
|
104
|
+
return await this.executePrdGeneration(job, onProgress, startTime);
|
|
105
|
+
} else if (job.job_type === 'code_execution') {
|
|
106
|
+
return await this.executeCodeImplementation(job, onProgress, startTime);
|
|
107
|
+
} else if (job.job_type === 'clarifying_questions') {
|
|
108
|
+
return await this.executeClarifyingQuestions(job, onProgress, startTime);
|
|
109
|
+
} else {
|
|
110
|
+
throw new Error(`Unknown job type: ${job.job_type}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Execute PRD/Plan generation using Claude
|
|
117
|
+
* @param {Object} job - Job object from API
|
|
118
|
+
* @param {Function} onProgress - Callback for progress updates
|
|
119
|
+
* @param {number} startTime - Start timestamp
|
|
120
|
+
* @returns {Promise<Object>} Execution result
|
|
121
|
+
*/
|
|
122
|
+
async executePrdGeneration(job, onProgress, startTime) {
|
|
123
|
+
// Determine content type from prd_mode field
|
|
124
|
+
const contentType = job.prd_mode === 'plan' ? 'Plan' : 'PRD';
|
|
125
|
+
logger.info(`Generating ${contentType} for: ${job.task_title}`);
|
|
126
|
+
|
|
127
|
+
// Delegate to PrdGenerationHandler based on mode
|
|
128
|
+
if (job.prd_mode === 'plan') {
|
|
129
|
+
return await this.prdGenerationHandler.executePlanGeneration(job, onProgress, startTime);
|
|
130
|
+
} else {
|
|
131
|
+
return await this.prdGenerationHandler.executeStandardPrd(job, onProgress, startTime);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Execute code implementation using Claude
|
|
138
|
+
* Delegates to CodeExecutionHandler
|
|
139
|
+
* @param {Object} job - Job object from API
|
|
140
|
+
* @param {Function} onProgress - Callback for progress updates
|
|
141
|
+
* @param {number} startTime - Start timestamp
|
|
142
|
+
* @returns {Promise<Object>} Execution result
|
|
143
|
+
*/
|
|
144
|
+
async executeCodeImplementation(job, onProgress, startTime) {
|
|
145
|
+
return await this.codeExecutionHandler.executeCodeImplementation(job, onProgress, startTime);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Execute clarifying questions generation using Claude
|
|
150
|
+
* Delegates to ClarifyingQuestionsHandler
|
|
151
|
+
* @param {Object} job - Job object from API
|
|
152
|
+
* @param {Function} onProgress - Callback for progress updates
|
|
153
|
+
* @param {number} startTime - Start timestamp
|
|
154
|
+
* @returns {Promise<Object>} Execution result with JSON output
|
|
155
|
+
*/
|
|
156
|
+
async executeClarifyingQuestions(job, onProgress, startTime) {
|
|
157
|
+
logger.info(`Generating clarifying questions for: ${job.task_title}`);
|
|
158
|
+
return await this.clarifyingQuestionsHandler.executeClarifyingQuestions(job, onProgress, startTime);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Kill the current running process if any
|
|
164
|
+
* Used during shutdown to prevent orphaned processes
|
|
165
|
+
* @returns {Promise<void>} Resolves when process is killed or grace period expires
|
|
166
|
+
*/
|
|
167
|
+
async killCurrentProcess() {
|
|
168
|
+
// Capture process reference to avoid race condition where a new process
|
|
169
|
+
// is assigned to this.currentProcess between SIGTERM and SIGKILL
|
|
170
|
+
const processToKill = this.currentProcess;
|
|
171
|
+
|
|
172
|
+
if (processToKill && !processToKill.killed) {
|
|
173
|
+
logger.warn('Killing current Claude process due to shutdown');
|
|
174
|
+
try {
|
|
175
|
+
processToKill.kill('SIGTERM');
|
|
176
|
+
|
|
177
|
+
// Wait for grace period, then force kill if still alive
|
|
178
|
+
await new Promise((resolve) => {
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
// Check the captured process reference, not this.currentProcess
|
|
181
|
+
if (processToKill && !processToKill.killed) {
|
|
182
|
+
logger.warn('Force killing Claude process with SIGKILL');
|
|
183
|
+
try {
|
|
184
|
+
processToKill.kill('SIGKILL');
|
|
185
|
+
} catch (killError) {
|
|
186
|
+
logger.error('Error force killing process', killError.message);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
resolve();
|
|
190
|
+
}, TIMEOUTS.PROCESS_KILL_GRACE_PERIOD_MS);
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.error('Error killing Claude process', error.message);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Validate and sanitize file system path to prevent directory traversal attacks
|
|
200
|
+
* @param {string} userPath - Path provided by user/API
|
|
201
|
+
* @returns {string|null} Sanitized absolute path or null if invalid
|
|
202
|
+
*/
|
|
203
|
+
validateAndSanitizePath(userPath) {
|
|
204
|
+
if (!userPath || typeof userPath !== 'string') {
|
|
205
|
+
logger.warn('Path is empty or not a string');
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
// Resolve to absolute path and normalize (removes .., ., etc.)
|
|
211
|
+
const resolvedPath = path.resolve(userPath);
|
|
212
|
+
|
|
213
|
+
// Check for null bytes (path traversal attack vector)
|
|
214
|
+
if (resolvedPath.includes('\0')) {
|
|
215
|
+
logger.error('Path contains null bytes (potential attack)');
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Comprehensive blacklist of protected system directories
|
|
220
|
+
const dangerousPaths = [
|
|
221
|
+
'/etc', '/bin', '/sbin', '/usr/bin', '/usr/sbin', // Core system dirs
|
|
222
|
+
'/System', '/Library', '/private', // macOS system dirs
|
|
223
|
+
'/Windows', '/Program Files', '/Program Files (x86)', // Windows system dirs
|
|
224
|
+
'/root', '/boot', '/dev', '/proc', '/sys' // Linux/Unix system dirs
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
for (const dangerousPath of dangerousPaths) {
|
|
228
|
+
if (resolvedPath === dangerousPath || resolvedPath.startsWith(dangerousPath + '/')) {
|
|
229
|
+
logger.error(`Path points to protected system directory: ${resolvedPath}`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Block access to sensitive subdirectories even within user directories
|
|
235
|
+
const sensitiveSubdirectories = [
|
|
236
|
+
'.ssh', // SSH keys
|
|
237
|
+
'.aws', // AWS credentials
|
|
238
|
+
'.config/gcloud', // Google Cloud credentials
|
|
239
|
+
'.azure', // Azure credentials
|
|
240
|
+
'.kube', // Kubernetes configs
|
|
241
|
+
'.docker', // Docker credentials
|
|
242
|
+
'.gnupg', // GPG keys
|
|
243
|
+
'Library/Keychains', // macOS keychains
|
|
244
|
+
'AppData/Roaming', // Windows credential storage
|
|
245
|
+
'.password-store', // pass password manager
|
|
246
|
+
'.config/1Password', // 1Password
|
|
247
|
+
'.config/Bitwarden' // Bitwarden
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
for (const sensitiveDir of sensitiveSubdirectories) {
|
|
251
|
+
const normalizedDir = sensitiveDir.replace(/\//g, path.sep);
|
|
252
|
+
if (resolvedPath.includes(path.sep + normalizedDir + path.sep) ||
|
|
253
|
+
resolvedPath.endsWith(path.sep + normalizedDir)) {
|
|
254
|
+
logger.error(`Path contains sensitive directory: ${sensitiveDir}`);
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check if path is within allowed base paths (if configured)
|
|
260
|
+
const allowedBasePathsEnv = process.env.RALPHBLASTER_ALLOWED_PATHS;
|
|
261
|
+
const allowedBasePaths = allowedBasePathsEnv
|
|
262
|
+
? allowedBasePathsEnv.split(':')
|
|
263
|
+
: null;
|
|
264
|
+
|
|
265
|
+
if (allowedBasePaths && allowedBasePaths.length > 0) {
|
|
266
|
+
const isAllowed = allowedBasePaths.some(basePath => {
|
|
267
|
+
const resolvedBase = path.resolve(basePath);
|
|
268
|
+
return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path.sep);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (!isAllowed) {
|
|
272
|
+
logger.error(`Path is outside allowed base paths: ${resolvedPath}`);
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
// If no whitelist is configured, warn if path is outside typical user directories
|
|
277
|
+
const isUserPath = resolvedPath.startsWith('/Users/') || // macOS
|
|
278
|
+
resolvedPath.startsWith('/home/') || // Linux
|
|
279
|
+
/^[A-Z]:\\Users\\/i.test(resolvedPath); // Windows
|
|
280
|
+
|
|
281
|
+
if (!isUserPath) {
|
|
282
|
+
logger.warn(`Path is outside typical user directories: ${resolvedPath}`);
|
|
283
|
+
// Don't reject, just warn - some valid projects might be elsewhere
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
logger.debug(`Path sanitized: ${userPath} -> ${resolvedPath}`);
|
|
288
|
+
return resolvedPath;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
logger.error('Error sanitizing path', error.message);
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = Executor;
|