threadlines 0.2.25 → 0.3.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/README.md +87 -42
- package/dist/api/client.js +4 -0
- package/dist/commands/check.js +139 -93
- package/dist/commands/init.js +32 -23
- package/dist/llm/prompt-builder.js +72 -0
- package/dist/processors/expert.js +120 -0
- package/dist/processors/single-expert.js +197 -0
- package/dist/utils/config-file.js +13 -4
- package/dist/utils/config.js +20 -14
- package/dist/utils/diff-filter.js +105 -0
- package/dist/utils/logger.js +13 -6
- package/dist/utils/slim-diff.js +133 -0
- package/package.json +2 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Git Diff Filtering Utilities
|
|
4
|
+
*
|
|
5
|
+
* Filters git diffs to include only specific files.
|
|
6
|
+
* This is used to send only relevant files to each threadline's LLM call.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.filterDiffByFiles = filterDiffByFiles;
|
|
10
|
+
exports.extractFilesFromDiff = extractFilesFromDiff;
|
|
11
|
+
exports.filterHunkToChanges = filterHunkToChanges;
|
|
12
|
+
exports.hasChanges = hasChanges;
|
|
13
|
+
/**
|
|
14
|
+
* Filters a git diff to include only the specified files.
|
|
15
|
+
*
|
|
16
|
+
* Git diff format structure:
|
|
17
|
+
* - Each file section starts with "diff --git a/path b/path"
|
|
18
|
+
* - Followed by metadata lines (index, ---, +++)
|
|
19
|
+
* - Then hunks with "@@ -start,count +start,count @@" headers
|
|
20
|
+
* - Content lines (+, -, space-prefixed)
|
|
21
|
+
*
|
|
22
|
+
* @param diff - The full git diff string
|
|
23
|
+
* @param filesToInclude - Array of file paths to include (must match paths in diff)
|
|
24
|
+
* @returns Filtered diff containing only the specified files
|
|
25
|
+
*/
|
|
26
|
+
function filterDiffByFiles(diff, filesToInclude) {
|
|
27
|
+
if (!diff || diff.trim() === '') {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
if (filesToInclude.length === 0) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
// Normalize file paths for comparison (handle both a/path and b/path formats)
|
|
34
|
+
const normalizedFiles = new Set(filesToInclude.map(file => file.trim()));
|
|
35
|
+
const lines = diff.split('\n');
|
|
36
|
+
const filteredLines = [];
|
|
37
|
+
let currentFile = null;
|
|
38
|
+
let inFileSection = false;
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
const line = lines[i];
|
|
41
|
+
// Check for file header: "diff --git a/path b/path"
|
|
42
|
+
const diffHeaderMatch = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
43
|
+
if (diffHeaderMatch) {
|
|
44
|
+
// Save previous file section if it was included
|
|
45
|
+
if (inFileSection && currentFile && normalizedFiles.has(currentFile)) {
|
|
46
|
+
// File section was included - it's already been added to filteredLines
|
|
47
|
+
// Reset for next file
|
|
48
|
+
}
|
|
49
|
+
// Start new file section
|
|
50
|
+
const filePathB = diffHeaderMatch[2];
|
|
51
|
+
// Use the 'b' path (new file) as the canonical path
|
|
52
|
+
currentFile = filePathB;
|
|
53
|
+
inFileSection = normalizedFiles.has(filePathB);
|
|
54
|
+
if (inFileSection) {
|
|
55
|
+
filteredLines.push(line);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// If we're in a file section that should be included, add all lines
|
|
60
|
+
if (inFileSection && currentFile && normalizedFiles.has(currentFile)) {
|
|
61
|
+
filteredLines.push(line);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return filteredLines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extracts file paths from a git diff.
|
|
68
|
+
*
|
|
69
|
+
* @param diff - The git diff string
|
|
70
|
+
* @returns Array of file paths found in the diff
|
|
71
|
+
*/
|
|
72
|
+
function extractFilesFromDiff(diff) {
|
|
73
|
+
const files = new Set();
|
|
74
|
+
const lines = diff.split('\n');
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const diffHeaderMatch = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
77
|
+
if (diffHeaderMatch) {
|
|
78
|
+
files.add(diffHeaderMatch[2]); // Use 'b' path
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return Array.from(files);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Filters hunk content to show only changed lines (removes context lines).
|
|
85
|
+
* Context lines are lines that start with a space (unchanged code).
|
|
86
|
+
*
|
|
87
|
+
* @param hunkContent - The content of a hunk (array of lines)
|
|
88
|
+
* @returns Filtered hunk content with only changed lines (+ and -)
|
|
89
|
+
*/
|
|
90
|
+
function filterHunkToChanges(hunkContent) {
|
|
91
|
+
return hunkContent.filter(line => {
|
|
92
|
+
// Keep lines that start with + or - (actual changes)
|
|
93
|
+
// Remove lines that start with space (context/unchanged code)
|
|
94
|
+
return line.startsWith('+') || line.startsWith('-');
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Checks if a hunk has any actual changes (not just context).
|
|
99
|
+
*
|
|
100
|
+
* @param hunkContent - The content of a hunk (array of lines)
|
|
101
|
+
* @returns True if the hunk contains actual changes
|
|
102
|
+
*/
|
|
103
|
+
function hasChanges(hunkContent) {
|
|
104
|
+
return hunkContent.some(line => line.startsWith('+') || line.startsWith('-'));
|
|
105
|
+
}
|
package/dist/utils/logger.js
CHANGED
|
@@ -26,7 +26,9 @@ function isDebugEnabled() {
|
|
|
26
26
|
/**
|
|
27
27
|
* Logger utility for CLI output
|
|
28
28
|
*
|
|
29
|
-
* - debug
|
|
29
|
+
* - debug: Only shown when --debug flag is set (technical details)
|
|
30
|
+
* - info: Always shown (important status messages)
|
|
31
|
+
* - output: Always shown (formatted output, no prefix)
|
|
30
32
|
* - warn/error: Always shown (critical information)
|
|
31
33
|
*/
|
|
32
34
|
exports.logger = {
|
|
@@ -40,13 +42,18 @@ exports.logger = {
|
|
|
40
42
|
}
|
|
41
43
|
},
|
|
42
44
|
/**
|
|
43
|
-
* Info-level log (
|
|
44
|
-
*
|
|
45
|
+
* Info-level log (important status messages, progress updates)
|
|
46
|
+
* Always shown (important information users need to see)
|
|
45
47
|
*/
|
|
46
48
|
info: (message) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
console.log(chalk_1.default.blue(`[INFO] ${message}`));
|
|
50
|
+
},
|
|
51
|
+
/**
|
|
52
|
+
* Output formatted text (for structured output like results display)
|
|
53
|
+
* Always shown, no prefix (for custom formatting)
|
|
54
|
+
*/
|
|
55
|
+
output: (message) => {
|
|
56
|
+
console.log(message);
|
|
50
57
|
},
|
|
51
58
|
/**
|
|
52
59
|
* Warning (non-fatal issues, recommendations)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSlimDiff = createSlimDiff;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a slim version of a git diff with reduced context lines.
|
|
6
|
+
*
|
|
7
|
+
* Git diffs typically have 3 lines of context by default, but we may have
|
|
8
|
+
* diffs with much more context (e.g., 200 lines). This function creates
|
|
9
|
+
* a version with only the specified number of context lines around changes.
|
|
10
|
+
*
|
|
11
|
+
* @param fullDiff - The full git diff string
|
|
12
|
+
* @param contextLines - Number of context lines to keep above/below changes (default: 3)
|
|
13
|
+
* @returns A slim diff string with reduced context
|
|
14
|
+
*/
|
|
15
|
+
function createSlimDiff(fullDiff, contextLines = 3) {
|
|
16
|
+
if (!fullDiff || fullDiff.trim() === '') {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
const lines = fullDiff.split('\n');
|
|
20
|
+
const result = [];
|
|
21
|
+
let i = 0;
|
|
22
|
+
while (i < lines.length) {
|
|
23
|
+
const line = lines[i];
|
|
24
|
+
// File header: diff --git a/path b/path
|
|
25
|
+
if (line.startsWith('diff --git ')) {
|
|
26
|
+
result.push(line);
|
|
27
|
+
i++;
|
|
28
|
+
// Copy file metadata lines until we hit the first hunk
|
|
29
|
+
while (i < lines.length && !lines[i].startsWith('@@')) {
|
|
30
|
+
result.push(lines[i]);
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Hunk header: @@ -start,count +start,count @@
|
|
36
|
+
if (line.startsWith('@@')) {
|
|
37
|
+
// Process this hunk
|
|
38
|
+
i++;
|
|
39
|
+
// Collect all lines in this hunk (until next @@ or diff --git or end)
|
|
40
|
+
const hunkLines = [];
|
|
41
|
+
while (i < lines.length && !lines[i].startsWith('@@') && !lines[i].startsWith('diff --git ')) {
|
|
42
|
+
hunkLines.push(lines[i]);
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
// Find which lines are changes
|
|
46
|
+
const changeIndices = [];
|
|
47
|
+
for (let j = 0; j < hunkLines.length; j++) {
|
|
48
|
+
const hunkLine = hunkLines[j];
|
|
49
|
+
if (hunkLine.startsWith('+') || hunkLine.startsWith('-')) {
|
|
50
|
+
changeIndices.push(j);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// If no changes in this hunk, skip it
|
|
54
|
+
if (changeIndices.length === 0) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Determine which lines to keep (changes + context)
|
|
58
|
+
const keepLines = new Set();
|
|
59
|
+
for (const changeIdx of changeIndices) {
|
|
60
|
+
// Keep the change line
|
|
61
|
+
keepLines.add(changeIdx);
|
|
62
|
+
// Keep N lines before
|
|
63
|
+
for (let k = 1; k <= contextLines; k++) {
|
|
64
|
+
if (changeIdx - k >= 0) {
|
|
65
|
+
keepLines.add(changeIdx - k);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Keep N lines after
|
|
69
|
+
for (let k = 1; k <= contextLines; k++) {
|
|
70
|
+
if (changeIdx + k < hunkLines.length) {
|
|
71
|
+
keepLines.add(changeIdx + k);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Build the slim hunk
|
|
76
|
+
const sortedKeepIndices = Array.from(keepLines).sort((a, b) => a - b);
|
|
77
|
+
const slimHunkLines = [];
|
|
78
|
+
for (const idx of sortedKeepIndices) {
|
|
79
|
+
slimHunkLines.push(hunkLines[idx]);
|
|
80
|
+
}
|
|
81
|
+
// Calculate new hunk header
|
|
82
|
+
// Count old and new lines
|
|
83
|
+
let oldLineCount = 0;
|
|
84
|
+
let newLineCount = 0;
|
|
85
|
+
for (const slimLine of slimHunkLines) {
|
|
86
|
+
if (slimLine.startsWith('-')) {
|
|
87
|
+
oldLineCount++;
|
|
88
|
+
}
|
|
89
|
+
else if (slimLine.startsWith('+')) {
|
|
90
|
+
newLineCount++;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Context line counts for both
|
|
94
|
+
oldLineCount++;
|
|
95
|
+
newLineCount++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Parse original hunk header to get starting line numbers
|
|
99
|
+
const hunkHeaderMatch = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)?/);
|
|
100
|
+
if (hunkHeaderMatch) {
|
|
101
|
+
const oldStart = parseInt(hunkHeaderMatch[1], 10);
|
|
102
|
+
const newStart = parseInt(hunkHeaderMatch[2], 10);
|
|
103
|
+
const hunkTitle = hunkHeaderMatch[3] || '';
|
|
104
|
+
// Adjust starting line based on which lines we kept
|
|
105
|
+
// The first kept line's offset from the original hunk start
|
|
106
|
+
const firstKeptIdx = sortedKeepIndices[0];
|
|
107
|
+
let oldOffset = 0;
|
|
108
|
+
let newOffset = 0;
|
|
109
|
+
for (let j = 0; j < firstKeptIdx; j++) {
|
|
110
|
+
const skippedLine = hunkLines[j];
|
|
111
|
+
if (skippedLine.startsWith('-')) {
|
|
112
|
+
oldOffset++;
|
|
113
|
+
}
|
|
114
|
+
else if (skippedLine.startsWith('+')) {
|
|
115
|
+
newOffset++;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
oldOffset++;
|
|
119
|
+
newOffset++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const newOldStart = oldStart + oldOffset;
|
|
123
|
+
const newNewStart = newStart + newOffset;
|
|
124
|
+
result.push(`@@ -${newOldStart},${oldLineCount} +${newNewStart},${newLineCount} @@${hunkTitle}`);
|
|
125
|
+
result.push(...slimHunkLines);
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// Any other line (shouldn't happen in well-formed diffs)
|
|
130
|
+
i++;
|
|
131
|
+
}
|
|
132
|
+
return result.join('\n');
|
|
133
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "threadlines",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Threadlines CLI - AI-powered linter based on your natural language documentation",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"dotenv": "^16.4.7",
|
|
55
55
|
"glob": "^13.0.0",
|
|
56
56
|
"js-yaml": "^4.1.0",
|
|
57
|
+
"openai": "^4.73.1",
|
|
57
58
|
"simple-git": "^3.27.0"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|