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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +294 -0
  3. package/bin/agent-dashboard.sh +168 -0
  4. package/bin/monitor-agent.sh +264 -0
  5. package/bin/ralphblaster.js +247 -0
  6. package/package.json +64 -0
  7. package/postinstall-colored.js +66 -0
  8. package/src/api-client.js +764 -0
  9. package/src/claude-plugin/.claude-plugin/plugin.json +9 -0
  10. package/src/claude-plugin/README.md +42 -0
  11. package/src/claude-plugin/skills/ralph/SKILL.md +259 -0
  12. package/src/commands/add-project.js +257 -0
  13. package/src/commands/init.js +79 -0
  14. package/src/config-file-manager.js +84 -0
  15. package/src/config.js +66 -0
  16. package/src/error-window.js +86 -0
  17. package/src/executor/claude-runner.js +716 -0
  18. package/src/executor/error-handler.js +65 -0
  19. package/src/executor/git-helper.js +196 -0
  20. package/src/executor/index.js +296 -0
  21. package/src/executor/job-handlers/clarifying-questions.js +213 -0
  22. package/src/executor/job-handlers/code-execution.js +145 -0
  23. package/src/executor/job-handlers/prd-generation.js +259 -0
  24. package/src/executor/path-helper.js +74 -0
  25. package/src/executor/prompt-validator.js +51 -0
  26. package/src/executor.js +4 -0
  27. package/src/index.js +342 -0
  28. package/src/logger.js +193 -0
  29. package/src/logging/README.md +93 -0
  30. package/src/logging/config.js +179 -0
  31. package/src/logging/destinations/README.md +290 -0
  32. package/src/logging/destinations/api-destination-unbatched.js +118 -0
  33. package/src/logging/destinations/api-destination.js +40 -0
  34. package/src/logging/destinations/base-destination.js +85 -0
  35. package/src/logging/destinations/batched-destination.js +198 -0
  36. package/src/logging/destinations/console-destination.js +172 -0
  37. package/src/logging/destinations/file-destination.js +208 -0
  38. package/src/logging/destinations/index.js +29 -0
  39. package/src/logging/destinations/progress-batch-destination-unbatched.js +92 -0
  40. package/src/logging/destinations/progress-batch-destination.js +41 -0
  41. package/src/logging/formatter.js +288 -0
  42. package/src/logging/log-manager.js +426 -0
  43. package/src/progress-throttle.js +101 -0
  44. package/src/system-monitor.js +64 -0
  45. package/src/utils/format.js +16 -0
  46. package/src/utils/log-file-helper.js +265 -0
  47. package/src/utils/progress-parser.js +250 -0
  48. 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;