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.
@@ -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
+ }
@@ -26,7 +26,9 @@ function isDebugEnabled() {
26
26
  /**
27
27
  * Logger utility for CLI output
28
28
  *
29
- * - debug/info: Only shown when --debug flag is set
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 (what's happening, progress updates)
44
- * Only shown with --debug flag
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
- if (debugEnabled) {
48
- console.log(chalk_1.default.blue(`[INFO] ${message}`));
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.2.25",
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": {