@contextrail/code-review-agent 0.1.1-alpha.0
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 +26 -0
- package/MODEL_RECOMMENDATIONS.md +178 -0
- package/README.md +177 -0
- package/dist/config/defaults.d.ts +72 -0
- package/dist/config/defaults.js +113 -0
- package/dist/config/index.d.ts +34 -0
- package/dist/config/index.js +89 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +603 -0
- package/dist/llm/factory.d.ts +21 -0
- package/dist/llm/factory.js +50 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.js +2 -0
- package/dist/llm/service.d.ts +38 -0
- package/dist/llm/service.js +191 -0
- package/dist/llm/types.d.ts +119 -0
- package/dist/llm/types.js +1 -0
- package/dist/logging/logger.d.ts +9 -0
- package/dist/logging/logger.js +52 -0
- package/dist/mcp/client.d.ts +429 -0
- package/dist/mcp/client.js +173 -0
- package/dist/mcp/mcp-tools.d.ts +292 -0
- package/dist/mcp/mcp-tools.js +40 -0
- package/dist/mcp/token-validation.d.ts +31 -0
- package/dist/mcp/token-validation.js +57 -0
- package/dist/mcp/tools-provider.d.ts +18 -0
- package/dist/mcp/tools-provider.js +24 -0
- package/dist/observability/index.d.ts +2 -0
- package/dist/observability/index.js +1 -0
- package/dist/observability/metrics.d.ts +48 -0
- package/dist/observability/metrics.js +86 -0
- package/dist/orchestrator/agentic-orchestrator.d.ts +29 -0
- package/dist/orchestrator/agentic-orchestrator.js +136 -0
- package/dist/orchestrator/prompts.d.ts +25 -0
- package/dist/orchestrator/prompts.js +98 -0
- package/dist/orchestrator/validation.d.ts +2 -0
- package/dist/orchestrator/validation.js +7 -0
- package/dist/orchestrator/writer.d.ts +4 -0
- package/dist/orchestrator/writer.js +17 -0
- package/dist/output/aggregator.d.ts +30 -0
- package/dist/output/aggregator.js +132 -0
- package/dist/output/prompts.d.ts +32 -0
- package/dist/output/prompts.js +153 -0
- package/dist/output/schema.d.ts +1515 -0
- package/dist/output/schema.js +224 -0
- package/dist/output/writer.d.ts +31 -0
- package/dist/output/writer.js +120 -0
- package/dist/review-inputs/chunking.d.ts +29 -0
- package/dist/review-inputs/chunking.js +113 -0
- package/dist/review-inputs/diff-summary.d.ts +52 -0
- package/dist/review-inputs/diff-summary.js +83 -0
- package/dist/review-inputs/file-patterns.d.ts +40 -0
- package/dist/review-inputs/file-patterns.js +182 -0
- package/dist/review-inputs/filtering.d.ts +31 -0
- package/dist/review-inputs/filtering.js +53 -0
- package/dist/review-inputs/git-diff-provider.d.ts +2 -0
- package/dist/review-inputs/git-diff-provider.js +42 -0
- package/dist/review-inputs/index.d.ts +46 -0
- package/dist/review-inputs/index.js +122 -0
- package/dist/review-inputs/path-validation.d.ts +10 -0
- package/dist/review-inputs/path-validation.js +37 -0
- package/dist/review-inputs/surrounding-context.d.ts +35 -0
- package/dist/review-inputs/surrounding-context.js +180 -0
- package/dist/review-inputs/triage.d.ts +57 -0
- package/dist/review-inputs/triage.js +81 -0
- package/dist/reviewers/executor.d.ts +41 -0
- package/dist/reviewers/executor.js +357 -0
- package/dist/reviewers/findings-merge.d.ts +9 -0
- package/dist/reviewers/findings-merge.js +131 -0
- package/dist/reviewers/iteration.d.ts +17 -0
- package/dist/reviewers/iteration.js +95 -0
- package/dist/reviewers/persistence.d.ts +17 -0
- package/dist/reviewers/persistence.js +55 -0
- package/dist/reviewers/progress-tracker.d.ts +115 -0
- package/dist/reviewers/progress-tracker.js +194 -0
- package/dist/reviewers/prompt.d.ts +42 -0
- package/dist/reviewers/prompt.js +246 -0
- package/dist/reviewers/tool-call-tracker.d.ts +18 -0
- package/dist/reviewers/tool-call-tracker.js +40 -0
- package/dist/reviewers/types.d.ts +12 -0
- package/dist/reviewers/types.js +1 -0
- package/dist/reviewers/validation-rules.d.ts +27 -0
- package/dist/reviewers/validation-rules.js +189 -0
- package/package.json +79 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File pattern configuration from prompt metadata
|
|
3
|
+
*/
|
|
4
|
+
export type FilePatterns = {
|
|
5
|
+
include?: string[];
|
|
6
|
+
exclude?: string[];
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Check if a file matches file patterns from prompt metadata.
|
|
10
|
+
*
|
|
11
|
+
* Security improvements:
|
|
12
|
+
* - Validates and sanitizes patterns to prevent path traversal
|
|
13
|
+
* - Error handling for malformed glob patterns
|
|
14
|
+
* - Limits pattern complexity
|
|
15
|
+
*
|
|
16
|
+
* @param filename - File path to check
|
|
17
|
+
* @param filePatterns - File pattern configuration from prompt metadata
|
|
18
|
+
* @returns True if file should be included for review
|
|
19
|
+
*/
|
|
20
|
+
export declare function fileMatchesPatterns(filename: string, filePatterns?: FilePatterns): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Filter files based on file patterns from prompt metadata.
|
|
23
|
+
*
|
|
24
|
+
* @param files - Array of file paths to filter
|
|
25
|
+
* @param filePatterns - File pattern configuration from prompt metadata
|
|
26
|
+
* @returns Filtered array of file paths
|
|
27
|
+
*/
|
|
28
|
+
export declare function filterFilesByPatterns(files: string[], filePatterns?: FilePatterns): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Filter diff inputs by file patterns from prompt metadata.
|
|
31
|
+
*
|
|
32
|
+
* @param files - Array of file paths
|
|
33
|
+
* @param diffs - Record of file paths to diff content
|
|
34
|
+
* @param filePatterns - File pattern configuration from prompt metadata
|
|
35
|
+
* @returns Filtered files and diffs
|
|
36
|
+
*/
|
|
37
|
+
export declare function filterDiffInputsByPatterns(files: string[], diffs: Record<string, string>, filePatterns?: FilePatterns): {
|
|
38
|
+
files: string[];
|
|
39
|
+
diffs: Record<string, string>;
|
|
40
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Minimatch } from 'minimatch';
|
|
2
|
+
/**
|
|
3
|
+
* Security constants for input validation
|
|
4
|
+
*/
|
|
5
|
+
const MAX_STRING_LENGTH = 10000;
|
|
6
|
+
/**
|
|
7
|
+
* Cache for compiled minimatch patterns to avoid recompilation
|
|
8
|
+
*/
|
|
9
|
+
const minimatchCache = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Check if a pattern contains dangerous characters using safe string methods
|
|
12
|
+
*/
|
|
13
|
+
function hasDangerousPatternChars(pattern) {
|
|
14
|
+
// Use simple string methods instead of regex to avoid ReDoS
|
|
15
|
+
return (pattern.includes('..') || // Path traversal
|
|
16
|
+
pattern.startsWith('/') || // Absolute path
|
|
17
|
+
pattern.includes('\\') || // Backslash (Windows path separator)
|
|
18
|
+
pattern.includes('\x00') // Null byte
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate and sanitize a file pattern to prevent path traversal
|
|
23
|
+
*/
|
|
24
|
+
function sanitizeFilePattern(pattern) {
|
|
25
|
+
if (!pattern || typeof pattern !== 'string') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// Check for dangerous patterns using safe string methods (no ReDoS risk)
|
|
29
|
+
if (hasDangerousPatternChars(pattern)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// Limit length to prevent ReDoS in minimatch
|
|
33
|
+
if (pattern.length > MAX_STRING_LENGTH) {
|
|
34
|
+
return pattern.substring(0, MAX_STRING_LENGTH);
|
|
35
|
+
}
|
|
36
|
+
return pattern;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get or compile a minimatch pattern with caching
|
|
40
|
+
*/
|
|
41
|
+
function getCompiledPattern(pattern) {
|
|
42
|
+
const cached = minimatchCache.get(pattern);
|
|
43
|
+
if (cached !== undefined) {
|
|
44
|
+
return cached;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const compiled = new Minimatch(pattern, { dot: true, nocomment: true });
|
|
48
|
+
minimatchCache.set(pattern, compiled);
|
|
49
|
+
return compiled;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
minimatchCache.set(pattern, null);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Normalize file path separators for cross-platform compatibility.
|
|
58
|
+
* Converts Windows backslashes to forward slashes for consistent glob matching.
|
|
59
|
+
*/
|
|
60
|
+
function normalizePath(path) {
|
|
61
|
+
return path.replace(/\\/g, '/');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Safely match a file against a compiled pattern
|
|
65
|
+
*/
|
|
66
|
+
function safeMinimatch(filename, pattern) {
|
|
67
|
+
const compiled = getCompiledPattern(pattern);
|
|
68
|
+
if (!compiled) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
// Normalize filename to use forward slashes for consistent matching
|
|
73
|
+
const normalizedFilename = normalizePath(filename);
|
|
74
|
+
return compiled.match(normalizedFilename);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if a file matches file patterns from prompt metadata.
|
|
82
|
+
*
|
|
83
|
+
* Security improvements:
|
|
84
|
+
* - Validates and sanitizes patterns to prevent path traversal
|
|
85
|
+
* - Error handling for malformed glob patterns
|
|
86
|
+
* - Limits pattern complexity
|
|
87
|
+
*
|
|
88
|
+
* @param filename - File path to check
|
|
89
|
+
* @param filePatterns - File pattern configuration from prompt metadata
|
|
90
|
+
* @returns True if file should be included for review
|
|
91
|
+
*/
|
|
92
|
+
export function fileMatchesPatterns(filename, filePatterns) {
|
|
93
|
+
// If no file patterns, include all files (backward compatible)
|
|
94
|
+
if (!filePatterns) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
// Validate filename
|
|
98
|
+
if (!filename || typeof filename !== 'string') {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const { include, exclude } = filePatterns;
|
|
102
|
+
try {
|
|
103
|
+
// Check exclude patterns first (higher priority)
|
|
104
|
+
if (exclude && Array.isArray(exclude) && exclude.length > 0) {
|
|
105
|
+
for (const pattern of exclude) {
|
|
106
|
+
const sanitized = sanitizeFilePattern(pattern);
|
|
107
|
+
if (!sanitized) {
|
|
108
|
+
continue; // Skip invalid patterns
|
|
109
|
+
}
|
|
110
|
+
if (safeMinimatch(filename, sanitized)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// If include patterns are specified, file must match at least one
|
|
116
|
+
if (include && Array.isArray(include) && include.length > 0) {
|
|
117
|
+
let hasMatch = false;
|
|
118
|
+
let hasValidPattern = false;
|
|
119
|
+
for (const pattern of include) {
|
|
120
|
+
const sanitized = sanitizeFilePattern(pattern);
|
|
121
|
+
if (!sanitized) {
|
|
122
|
+
continue; // Skip invalid patterns
|
|
123
|
+
}
|
|
124
|
+
hasValidPattern = true;
|
|
125
|
+
if (safeMinimatch(filename, sanitized)) {
|
|
126
|
+
hasMatch = true;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Fail-safe: if all patterns were invalid, include the file
|
|
131
|
+
if (!hasValidPattern) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (!hasMatch) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Fail-safe: include file if pattern matching fails
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Filter files based on file patterns from prompt metadata.
|
|
147
|
+
*
|
|
148
|
+
* @param files - Array of file paths to filter
|
|
149
|
+
* @param filePatterns - File pattern configuration from prompt metadata
|
|
150
|
+
* @returns Filtered array of file paths
|
|
151
|
+
*/
|
|
152
|
+
export function filterFilesByPatterns(files, filePatterns) {
|
|
153
|
+
if (!filePatterns) {
|
|
154
|
+
return files;
|
|
155
|
+
}
|
|
156
|
+
return files.filter((file) => fileMatchesPatterns(file, filePatterns));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Filter diff inputs by file patterns from prompt metadata.
|
|
160
|
+
*
|
|
161
|
+
* @param files - Array of file paths
|
|
162
|
+
* @param diffs - Record of file paths to diff content
|
|
163
|
+
* @param filePatterns - File pattern configuration from prompt metadata
|
|
164
|
+
* @returns Filtered files and diffs
|
|
165
|
+
*/
|
|
166
|
+
export function filterDiffInputsByPatterns(files, diffs, filePatterns) {
|
|
167
|
+
const filteredFiles = filterFilesByPatterns(files, filePatterns);
|
|
168
|
+
// Filter diffs to only include files that passed filtering
|
|
169
|
+
const filteredDiffs = {};
|
|
170
|
+
for (const file of filteredFiles) {
|
|
171
|
+
if (file in diffs) {
|
|
172
|
+
if (!diffs[file]) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
filteredDiffs[file] = diffs[file];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
files: filteredFiles,
|
|
180
|
+
diffs: filteredDiffs,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for file filtering.
|
|
3
|
+
*/
|
|
4
|
+
export type FilteringConfig = {
|
|
5
|
+
/**
|
|
6
|
+
* Patterns to exclude from review.
|
|
7
|
+
* Uses glob patterns (minimatch) for secure matching.
|
|
8
|
+
* Default: DEFAULT_EXCLUDE_PATTERNS
|
|
9
|
+
*/
|
|
10
|
+
excludePatterns?: ReadonlyArray<string>;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Filter files based on exclude patterns.
|
|
14
|
+
*
|
|
15
|
+
* @param files - Array of file paths to filter
|
|
16
|
+
* @param config - Filtering configuration
|
|
17
|
+
* @returns Filtered array of file paths
|
|
18
|
+
*/
|
|
19
|
+
export declare const filterFiles: (files: string[], config?: FilteringConfig) => string[];
|
|
20
|
+
/**
|
|
21
|
+
* Filter diff inputs by removing excluded files and their diffs.
|
|
22
|
+
*
|
|
23
|
+
* @param files - Array of file paths
|
|
24
|
+
* @param diffs - Record of file paths to diff content
|
|
25
|
+
* @param config - Filtering configuration
|
|
26
|
+
* @returns Filtered files and diffs
|
|
27
|
+
*/
|
|
28
|
+
export declare const filterDiffInputs: (files: string[], diffs: Record<string, string>, config?: FilteringConfig) => {
|
|
29
|
+
files: string[];
|
|
30
|
+
diffs: Record<string, string>;
|
|
31
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { DEFAULT_EXCLUDE_PATTERNS } from '../config/defaults.js';
|
|
2
|
+
import { fileMatchesPatterns } from './file-patterns.js';
|
|
3
|
+
/**
|
|
4
|
+
* Check if a file path matches any exclude pattern.
|
|
5
|
+
* Uses minimatch for secure glob pattern matching (prevents ReDoS).
|
|
6
|
+
*
|
|
7
|
+
* @param filePath - File path to check (relative or absolute)
|
|
8
|
+
* @param patterns - Glob patterns to match against
|
|
9
|
+
* @returns True if file should be excluded
|
|
10
|
+
*/
|
|
11
|
+
const matchesExcludePattern = (filePath, patterns) => {
|
|
12
|
+
// Convert patterns to FilePatterns format for consistent matching
|
|
13
|
+
const filePatterns = {
|
|
14
|
+
exclude: [...patterns],
|
|
15
|
+
};
|
|
16
|
+
// Use fileMatchesPatterns which handles minimatch safely
|
|
17
|
+
// If file matches exclude patterns, it should be excluded
|
|
18
|
+
return !fileMatchesPatterns(filePath, filePatterns);
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Filter files based on exclude patterns.
|
|
22
|
+
*
|
|
23
|
+
* @param files - Array of file paths to filter
|
|
24
|
+
* @param config - Filtering configuration
|
|
25
|
+
* @returns Filtered array of file paths
|
|
26
|
+
*/
|
|
27
|
+
export const filterFiles = (files, config = {}) => {
|
|
28
|
+
const patterns = config.excludePatterns ?? DEFAULT_EXCLUDE_PATTERNS;
|
|
29
|
+
return files.filter((file) => !matchesExcludePattern(file, patterns));
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Filter diff inputs by removing excluded files and their diffs.
|
|
33
|
+
*
|
|
34
|
+
* @param files - Array of file paths
|
|
35
|
+
* @param diffs - Record of file paths to diff content
|
|
36
|
+
* @param config - Filtering configuration
|
|
37
|
+
* @returns Filtered files and diffs
|
|
38
|
+
*/
|
|
39
|
+
export const filterDiffInputs = (files, diffs, config = {}) => {
|
|
40
|
+
const filteredFiles = filterFiles(files, config);
|
|
41
|
+
// Filter diffs to only include files that passed filtering
|
|
42
|
+
const filteredDiffs = {};
|
|
43
|
+
for (const file of filteredFiles) {
|
|
44
|
+
const diff = diffs[file];
|
|
45
|
+
if (diff !== undefined) {
|
|
46
|
+
filteredDiffs[file] = diff;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
files: filteredFiles,
|
|
51
|
+
diffs: filteredDiffs,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import simpleGit from 'simple-git';
|
|
2
|
+
import { validateAndNormalizePath } from './path-validation.js';
|
|
3
|
+
const parseFileList = (filesRaw) => {
|
|
4
|
+
return filesRaw
|
|
5
|
+
.split('\n')
|
|
6
|
+
.map((file) => file.trim())
|
|
7
|
+
.filter(Boolean);
|
|
8
|
+
};
|
|
9
|
+
export const gitDiffProvider = async ({ repoPath, from, to }) => {
|
|
10
|
+
try {
|
|
11
|
+
const git = simpleGit(repoPath);
|
|
12
|
+
const topLevel = (await git.revparse(['--show-toplevel'])).trim();
|
|
13
|
+
const topGit = simpleGit(topLevel);
|
|
14
|
+
// Use array arguments to prevent command injection
|
|
15
|
+
// simple-git handles array args safely by treating each element as a separate argument
|
|
16
|
+
const filesRaw = await topGit.raw(['diff', '--name-only', `${from}..${to}`]);
|
|
17
|
+
const filesRawList = parseFileList(filesRaw);
|
|
18
|
+
const files = [];
|
|
19
|
+
const diffs = {};
|
|
20
|
+
// Validate and normalize all file paths to prevent path traversal
|
|
21
|
+
for (const file of filesRawList) {
|
|
22
|
+
try {
|
|
23
|
+
const validatedPath = validateAndNormalizePath(file, topLevel);
|
|
24
|
+
files.push(validatedPath);
|
|
25
|
+
// Use array arguments for git diff command to prevent injection
|
|
26
|
+
const diff = await topGit.raw(['diff', '--no-color', `${from}..${to}`, '--', validatedPath]);
|
|
27
|
+
diffs[validatedPath] = diff;
|
|
28
|
+
}
|
|
29
|
+
catch (pathError) {
|
|
30
|
+
// Skip files that fail path validation (path traversal attempts)
|
|
31
|
+
// Log error but continue processing other files
|
|
32
|
+
const message = pathError instanceof Error ? pathError.message : 'Path validation failed';
|
|
33
|
+
throw new Error(`Invalid file path "${file}": ${message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { files, diffs };
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
40
|
+
throw new Error(`Failed to compute git diff: ${message}`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type FilteringConfig } from './filtering.js';
|
|
2
|
+
import type { SurroundingContextConfig } from './surrounding-context.js';
|
|
3
|
+
export { triagePr, type TriageConfig, type TriageResult } from './triage.js';
|
|
4
|
+
export type ReviewInputMode = 'diff' | 'file-list';
|
|
5
|
+
export type DiffReviewInputs = {
|
|
6
|
+
mode: 'diff';
|
|
7
|
+
files: string[];
|
|
8
|
+
diffs: Record<string, string>;
|
|
9
|
+
context?: Record<string, string>;
|
|
10
|
+
};
|
|
11
|
+
export type FileListReviewInputs = {
|
|
12
|
+
mode: 'file-list';
|
|
13
|
+
files: string[];
|
|
14
|
+
context?: Record<string, string>;
|
|
15
|
+
};
|
|
16
|
+
export type ReviewInputs = DiffReviewInputs | FileListReviewInputs;
|
|
17
|
+
export type DiffInput = {
|
|
18
|
+
mode: 'diff';
|
|
19
|
+
repoPath: string;
|
|
20
|
+
from: string;
|
|
21
|
+
to: string;
|
|
22
|
+
filtering?: FilteringConfig;
|
|
23
|
+
surroundingContext?: SurroundingContextConfig;
|
|
24
|
+
};
|
|
25
|
+
export type FileListInput = {
|
|
26
|
+
mode: 'file-list';
|
|
27
|
+
files: string[];
|
|
28
|
+
basePath?: string;
|
|
29
|
+
filtering?: FilteringConfig;
|
|
30
|
+
surroundingContext?: SurroundingContextConfig;
|
|
31
|
+
};
|
|
32
|
+
export type ReviewInputOptions = DiffInput | FileListInput;
|
|
33
|
+
export declare const isDiffInputs: (inputs: ReviewInputs) => inputs is DiffReviewInputs;
|
|
34
|
+
export declare const isFileListInputs: (inputs: ReviewInputs) => inputs is FileListReviewInputs;
|
|
35
|
+
export type DiffProvider = (params: {
|
|
36
|
+
repoPath: string;
|
|
37
|
+
from: string;
|
|
38
|
+
to: string;
|
|
39
|
+
}) => Promise<{
|
|
40
|
+
files: string[];
|
|
41
|
+
diffs: Record<string, string>;
|
|
42
|
+
}>;
|
|
43
|
+
export declare const buildReviewInputs: (options: ReviewInputOptions, deps?: {
|
|
44
|
+
diffProvider?: DiffProvider;
|
|
45
|
+
}) => Promise<ReviewInputs>;
|
|
46
|
+
export declare const persistReviewInputs: (inputs: ReviewInputs, outputDir: string) => Promise<void>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { gitDiffProvider } from './git-diff-provider.js';
|
|
4
|
+
import { filterDiffInputs, filterFiles } from './filtering.js';
|
|
5
|
+
export { triagePr } from './triage.js';
|
|
6
|
+
export const isDiffInputs = (inputs) => inputs.mode === 'diff';
|
|
7
|
+
export const isFileListInputs = (inputs) => inputs.mode === 'file-list';
|
|
8
|
+
const resolveFilePath = (file, basePath) => {
|
|
9
|
+
if (!basePath) {
|
|
10
|
+
return file;
|
|
11
|
+
}
|
|
12
|
+
if (path.isAbsolute(file)) {
|
|
13
|
+
return file;
|
|
14
|
+
}
|
|
15
|
+
return path.resolve(basePath, file);
|
|
16
|
+
};
|
|
17
|
+
const toRelativeIfInsideBase = (filePath, basePath) => {
|
|
18
|
+
if (!basePath) {
|
|
19
|
+
return filePath;
|
|
20
|
+
}
|
|
21
|
+
const relative = path.relative(basePath, filePath);
|
|
22
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
23
|
+
return filePath;
|
|
24
|
+
}
|
|
25
|
+
return relative;
|
|
26
|
+
};
|
|
27
|
+
const validateFileList = async (files, basePath) => {
|
|
28
|
+
if (files.length === 0) {
|
|
29
|
+
throw new Error('File list must include at least one file.');
|
|
30
|
+
}
|
|
31
|
+
const missing = [];
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
try {
|
|
34
|
+
const resolved = resolveFilePath(file, basePath);
|
|
35
|
+
await access(resolved);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
missing.push(file);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
throw new Error(`Files not found: ${missing.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const normalizeFiles = (files) => {
|
|
46
|
+
return files.map((file) => file.trim()).filter(Boolean);
|
|
47
|
+
};
|
|
48
|
+
export const buildReviewInputs = async (options, deps) => {
|
|
49
|
+
if (options.mode === 'diff') {
|
|
50
|
+
const diffProvider = deps?.diffProvider ?? gitDiffProvider;
|
|
51
|
+
const result = await diffProvider({
|
|
52
|
+
repoPath: options.repoPath,
|
|
53
|
+
from: options.from,
|
|
54
|
+
to: options.to,
|
|
55
|
+
});
|
|
56
|
+
const normalizedFiles = normalizeFiles(result.files);
|
|
57
|
+
const { files, diffs } = filterDiffInputs(normalizedFiles, result.diffs, options.filtering);
|
|
58
|
+
if (files.length === 0) {
|
|
59
|
+
throw new Error('Diff mode produced no files to review after filtering.');
|
|
60
|
+
}
|
|
61
|
+
const inputs = {
|
|
62
|
+
mode: 'diff',
|
|
63
|
+
files,
|
|
64
|
+
diffs,
|
|
65
|
+
};
|
|
66
|
+
// Extract surrounding context if configured
|
|
67
|
+
if (options.surroundingContext?.enabled !== false) {
|
|
68
|
+
const { extractSurroundingContext } = await import('./surrounding-context.js');
|
|
69
|
+
const context = await extractSurroundingContext(inputs, {
|
|
70
|
+
...options.surroundingContext,
|
|
71
|
+
basePath: options.surroundingContext?.basePath ?? options.repoPath,
|
|
72
|
+
});
|
|
73
|
+
if (Object.keys(context).length > 0) {
|
|
74
|
+
inputs.context = context;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return inputs;
|
|
78
|
+
}
|
|
79
|
+
const normalizedFiles = normalizeFiles(options.files);
|
|
80
|
+
const files = filterFiles(normalizedFiles, options.filtering);
|
|
81
|
+
if (files.length === 0) {
|
|
82
|
+
throw new Error('File list produced no files to review after filtering.');
|
|
83
|
+
}
|
|
84
|
+
await validateFileList(files, options.basePath);
|
|
85
|
+
const relativeFiles = files.map((file) => toRelativeIfInsideBase(resolveFilePath(file, options.basePath), options.basePath));
|
|
86
|
+
const inputs = {
|
|
87
|
+
mode: 'file-list',
|
|
88
|
+
files: relativeFiles,
|
|
89
|
+
};
|
|
90
|
+
// Extract surrounding context if configured
|
|
91
|
+
// For file-list mode, basePath is required for context extraction
|
|
92
|
+
if (options.surroundingContext?.enabled !== false) {
|
|
93
|
+
// Skip context extraction if basePath is missing in file-list mode
|
|
94
|
+
if (options.mode === 'file-list' && !options.basePath) {
|
|
95
|
+
// Context extraction requires basePath for file-list mode, skip it
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const { extractSurroundingContext } = await import('./surrounding-context.js');
|
|
99
|
+
const context = await extractSurroundingContext(inputs, {
|
|
100
|
+
...options.surroundingContext,
|
|
101
|
+
basePath: options.basePath,
|
|
102
|
+
});
|
|
103
|
+
if (Object.keys(context).length > 0) {
|
|
104
|
+
inputs.context = context;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return inputs;
|
|
109
|
+
};
|
|
110
|
+
export const persistReviewInputs = async (inputs, outputDir) => {
|
|
111
|
+
await mkdir(outputDir, { recursive: true });
|
|
112
|
+
const filesPayload = JSON.stringify({ mode: inputs.mode, files: inputs.files }, null, 2);
|
|
113
|
+
await writeFile(path.join(outputDir, 'files.json'), filesPayload);
|
|
114
|
+
if (inputs.mode !== 'diff') {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const [filePath, diff] of Object.entries(inputs.diffs)) {
|
|
118
|
+
const diffPath = path.join(outputDir, 'diff', `${filePath}.diff`);
|
|
119
|
+
await mkdir(path.dirname(diffPath), { recursive: true });
|
|
120
|
+
await writeFile(diffPath, diff);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates that a file path is safe and within the repository boundary.
|
|
3
|
+
* Prevents path traversal attacks by ensuring resolved paths stay within basePath.
|
|
4
|
+
*
|
|
5
|
+
* @param filePath - Relative file path from git or file list
|
|
6
|
+
* @param basePath - Base directory (repository root) to validate against
|
|
7
|
+
* @returns Normalized, validated relative path
|
|
8
|
+
* @throws Error if path traversal is detected or path is outside basePath
|
|
9
|
+
*/
|
|
10
|
+
export declare const validateAndNormalizePath: (filePath: string, basePath: string) => string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
/**
|
|
4
|
+
* Validates that a file path is safe and within the repository boundary.
|
|
5
|
+
* Prevents path traversal attacks by ensuring resolved paths stay within basePath.
|
|
6
|
+
*
|
|
7
|
+
* @param filePath - Relative file path from git or file list
|
|
8
|
+
* @param basePath - Base directory (repository root) to validate against
|
|
9
|
+
* @returns Normalized, validated relative path
|
|
10
|
+
* @throws Error if path traversal is detected or path is outside basePath
|
|
11
|
+
*/
|
|
12
|
+
export const validateAndNormalizePath = (filePath, basePath) => {
|
|
13
|
+
// Normalize the file path (remove . and .. segments)
|
|
14
|
+
const normalized = path.normalize(filePath);
|
|
15
|
+
// Resolve against basePath to get absolute path
|
|
16
|
+
const resolved = path.resolve(basePath, normalized);
|
|
17
|
+
// Resolve basePath to absolute (handles symlinks)
|
|
18
|
+
const baseResolved = path.resolve(basePath);
|
|
19
|
+
// Check if resolved path is within basePath
|
|
20
|
+
// Use realpathSync to resolve symlinks and ensure we're checking actual filesystem paths
|
|
21
|
+
try {
|
|
22
|
+
const resolvedReal = realpathSync(resolved);
|
|
23
|
+
const baseReal = realpathSync(baseResolved);
|
|
24
|
+
// Ensure the resolved path starts with the base path
|
|
25
|
+
if (!resolvedReal.startsWith(baseReal + path.sep) && resolvedReal !== baseReal) {
|
|
26
|
+
throw new Error(`Path traversal detected: "${filePath}" resolves outside repository boundary`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// If realpathSync fails, the file doesn't exist - but we still check the resolved path
|
|
31
|
+
if (!resolved.startsWith(baseResolved + path.sep) && resolved !== baseResolved) {
|
|
32
|
+
throw new Error(`Path traversal detected: "${filePath}" resolves outside repository boundary`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Return normalized relative path
|
|
36
|
+
return normalized;
|
|
37
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReviewInputs } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for surrounding context extraction.
|
|
4
|
+
*/
|
|
5
|
+
export type SurroundingContextConfig = {
|
|
6
|
+
/**
|
|
7
|
+
* Maximum tokens to use for surrounding context per file.
|
|
8
|
+
* Default: 20000 (roughly 80KB of text)
|
|
9
|
+
*/
|
|
10
|
+
maxTokensPerFile?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Number of lines of context to include before and after changed lines.
|
|
13
|
+
* Only used in diff mode when extracting windowed context.
|
|
14
|
+
* Default: 10
|
|
15
|
+
*/
|
|
16
|
+
contextLines?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Whether to extract surrounding context.
|
|
19
|
+
* Default: true
|
|
20
|
+
*/
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Base path for resolving file paths (required for file-list mode).
|
|
24
|
+
*/
|
|
25
|
+
basePath?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Extract surrounding code context for review inputs.
|
|
29
|
+
* Returns a map of file paths to their context (full file or windowed excerpt).
|
|
30
|
+
*
|
|
31
|
+
* @param inputs - Review inputs (diff or file-list)
|
|
32
|
+
* @param config - Context extraction configuration
|
|
33
|
+
* @returns Map of file paths to context strings (null values indicate unavailable context)
|
|
34
|
+
*/
|
|
35
|
+
export declare const extractSurroundingContext: (inputs: ReviewInputs, config?: SurroundingContextConfig) => Promise<Record<string, string>>;
|