minovative-mind-cli 1.0.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.
Files changed (53) hide show
  1. package/README.md +418 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/commands/chat.d.ts +7 -0
  7. package/dist/commands/chat.js +30 -0
  8. package/dist/commands/login.d.ts +5 -0
  9. package/dist/commands/login.js +18 -0
  10. package/dist/commands/logout.d.ts +5 -0
  11. package/dist/commands/logout.js +12 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/services/agent-tools.d.ts +36 -0
  15. package/dist/services/agent-tools.js +764 -0
  16. package/dist/services/agent.d.ts +21 -0
  17. package/dist/services/agent.js +648 -0
  18. package/dist/services/ai.d.ts +60 -0
  19. package/dist/services/ai.js +331 -0
  20. package/dist/services/auth.d.ts +3 -0
  21. package/dist/services/auth.js +183 -0
  22. package/dist/services/changeLogger.d.ts +23 -0
  23. package/dist/services/changeLogger.js +57 -0
  24. package/dist/services/contextAgent.d.ts +20 -0
  25. package/dist/services/contextAgent.js +440 -0
  26. package/dist/services/proxyClient.d.ts +21 -0
  27. package/dist/services/proxyClient.js +119 -0
  28. package/dist/services/verificationService.d.ts +10 -0
  29. package/dist/services/verificationService.js +148 -0
  30. package/dist/utils/atomicWrite.d.ts +6 -0
  31. package/dist/utils/atomicWrite.js +29 -0
  32. package/dist/utils/config.d.ts +17 -0
  33. package/dist/utils/config.js +17 -0
  34. package/dist/utils/contextPrompts.d.ts +3 -0
  35. package/dist/utils/contextPrompts.js +34 -0
  36. package/dist/utils/dependencyTracer.d.ts +48 -0
  37. package/dist/utils/dependencyTracer.js +647 -0
  38. package/dist/utils/excludedExtensions.d.ts +8 -0
  39. package/dist/utils/excludedExtensions.js +125 -0
  40. package/dist/utils/fuzzyMatch.d.ts +21 -0
  41. package/dist/utils/fuzzyMatch.js +121 -0
  42. package/dist/utils/logger.d.ts +8 -0
  43. package/dist/utils/logger.js +17 -0
  44. package/dist/utils/pathSecurity.d.ts +10 -0
  45. package/dist/utils/pathSecurity.js +26 -0
  46. package/dist/utils/symbolExtractor.d.ts +6 -0
  47. package/dist/utils/symbolExtractor.js +249 -0
  48. package/dist/utils/syntaxValidator.d.ts +5 -0
  49. package/dist/utils/syntaxValidator.js +81 -0
  50. package/dist/utils/systemPrompts.d.ts +5 -0
  51. package/dist/utils/systemPrompts.js +119 -0
  52. package/oclif.manifest.json +69 -0
  53. package/package.json +81 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Comprehensive list of binary/generated file extensions that grep should skip.
3
+ * Covers images, video, audio, fonts, archives, documents, compiled artifacts,
4
+ * IDE files, and tool-specific generated files across all major languages.
5
+ * These are used to prevent commands like `grep` from searching through
6
+ * non-textual or compiled files, which can lead to incorrect results or errors.
7
+ */
8
+ export const EXCLUDED_EXTENSIONS = [
9
+ // --- Images ---
10
+ "*.png",
11
+ "*.jpg",
12
+ "*.jpeg",
13
+ "*.gif",
14
+ "*.bmp",
15
+ "*.ico",
16
+ "*.webp",
17
+ "*.svg",
18
+ "*.tiff",
19
+ // --- Video ---
20
+ "*.mp4",
21
+ "*.webm",
22
+ "*.avi",
23
+ "*.mov",
24
+ "*.mkv",
25
+ "*.wmv",
26
+ "*.flv",
27
+ // --- Audio ---
28
+ "*.mp3",
29
+ "*.wav",
30
+ "*.ogg",
31
+ "*.aac",
32
+ // --- Fonts ---
33
+ "*.woff",
34
+ "*.woff2",
35
+ "*.ttf",
36
+ "*.otf",
37
+ "*.eot",
38
+ // --- Archives ---
39
+ "*.zip",
40
+ "*.rar",
41
+ "*.7z",
42
+ "*.tar",
43
+ "*.gz",
44
+ "*.tgz",
45
+ // --- Documents ---
46
+ "*.pdf",
47
+ "*.doc",
48
+ "*.docx",
49
+ "*.ppt",
50
+ "*.pptx",
51
+ "*.xls",
52
+ "*.xlsx",
53
+ // --- Compiled/Binary Artifacts (C/C++/.NET/Go) ---
54
+ "*.exe",
55
+ "*.dll",
56
+ "*.so",
57
+ "*.dylib",
58
+ "*.o",
59
+ "*.a",
60
+ "*.obj",
61
+ "*.lib",
62
+ "*.exp",
63
+ "*.ilk",
64
+ "*.pch",
65
+ "*.gch",
66
+ "*.d",
67
+ "*.pdb",
68
+ "*.aps",
69
+ "*.ncb",
70
+ "*.opensdf",
71
+ "*.sdf",
72
+ // --- Java/Kotlin ---
73
+ "*.class",
74
+ "*.jar",
75
+ "*.war",
76
+ "*.ear",
77
+ // --- Python ---
78
+ "*.pyc",
79
+ "*.pyo",
80
+ "*.pyd",
81
+ "*.spec",
82
+ // --- Go ---
83
+ "*.test",
84
+ // --- Ruby ---
85
+ "*.gem",
86
+ // --- iOS/macOS ---
87
+ "*.ipa",
88
+ "*.app",
89
+ "*.dSYM",
90
+ "*.xcuserdatad",
91
+ // --- .NET/Visual Studio ---
92
+ "*.user",
93
+ "*.filters",
94
+ "*.suo",
95
+ // --- IDE/Editor Files ---
96
+ "*.iml",
97
+ "*.ipr",
98
+ "*.iws",
99
+ "*.swp",
100
+ "*.swo",
101
+ "*.swn",
102
+ "*.elc",
103
+ "*.sublime-project",
104
+ "*.sublime-workspace",
105
+ // --- Terraform ---
106
+ "*.tfstate",
107
+ // --- TypeScript ---
108
+ "*.tsbuildinfo",
109
+ // --- Database ---
110
+ "*.vsix",
111
+ "*.db",
112
+ "*.sqlite",
113
+ "*.sqlite3",
114
+ // --- Source Maps / Logs / Locks ---
115
+ "*.log",
116
+ "*.lock",
117
+ "*.map",
118
+ // --- Misc Generated/Temp ---
119
+ "*.tmp",
120
+ "*.bak",
121
+ "*.orig",
122
+ "*.rej",
123
+ "*.patch",
124
+ "*.diff",
125
+ ];
@@ -0,0 +1,21 @@
1
+ export interface MatchResult {
2
+ start: number;
3
+ end: number;
4
+ strategy: string;
5
+ }
6
+ /**
7
+ * Trims each line, collapses multiple spaces/tabs to single space,
8
+ * and normalizes line endings.
9
+ */
10
+ export declare function normalizeWhitespace(text: string): string;
11
+ /**
12
+ * Returns 0-1 similarity ratio.
13
+ * 1.0 is exact match, 0.0 is completely different.
14
+ */
15
+ export declare function levenshteinSimilarity(a: string, b: string): number;
16
+ /**
17
+ * Finds the best match for `searchContent` inside `fileContent`.
18
+ * Uses a pipeline of strategies: Exact -> Whitespace-normalized -> Levenshtein.
19
+ */
20
+ export declare function findBestMatch(fileContent: string, searchContent: string): MatchResult | null;
21
+ export declare function applyMatch(fileContent: string, match: MatchResult, replaceContent: string): string;
@@ -0,0 +1,121 @@
1
+ import { distance } from 'fastest-levenshtein';
2
+ /**
3
+ * Trims each line, collapses multiple spaces/tabs to single space,
4
+ * and normalizes line endings.
5
+ */
6
+ export function normalizeWhitespace(text) {
7
+ return text
8
+ .split(/\r?\n/)
9
+ .map((line) => line.trim().replace(/\s+/g, ' '))
10
+ .join('\n')
11
+ .trim();
12
+ }
13
+ /**
14
+ * Returns 0-1 similarity ratio.
15
+ * 1.0 is exact match, 0.0 is completely different.
16
+ */
17
+ export function levenshteinSimilarity(a, b) {
18
+ const maxLength = Math.max(a.length, b.length);
19
+ if (maxLength === 0)
20
+ return 1.0;
21
+ const d = distance(a, b);
22
+ return 1 - d / maxLength;
23
+ }
24
+ /**
25
+ * Finds the best match for `searchContent` inside `fileContent`.
26
+ * Uses a pipeline of strategies: Exact -> Whitespace-normalized -> Levenshtein.
27
+ */
28
+ export function findBestMatch(fileContent, searchContent) {
29
+ if (!fileContent || !searchContent)
30
+ return null;
31
+ // Strategy 1: Exact Match
32
+ const exactIndex = fileContent.indexOf(searchContent);
33
+ if (exactIndex !== -1) {
34
+ return {
35
+ start: exactIndex,
36
+ end: exactIndex + searchContent.length,
37
+ strategy: 'Exact Match',
38
+ };
39
+ }
40
+ // Strategy 2: Whitespace-normalized Match
41
+ const normFileLines = fileContent.split(/\r?\n/);
42
+ const normSearchContent = normalizeWhitespace(searchContent);
43
+ // Fast check: does the normalized file content contain the normalized search content at all?
44
+ // Since newlines are preserved by normalizeWhitespace (it only trims lines), we can do this:
45
+ const normFileContent = normalizeWhitespace(fileContent);
46
+ // To map back to original lines, we try matching line by line.
47
+ const searchLines = searchContent.split(/\r?\n/);
48
+ const numSearchLines = searchLines.length;
49
+ if (numSearchLines > 0 && numSearchLines <= normFileLines.length) {
50
+ for (let i = 0; i <= normFileLines.length - numSearchLines; i++) {
51
+ let isMatch = true;
52
+ let charMatchCount = 0;
53
+ for (let j = 0; j < numSearchLines; j++) {
54
+ const fileLineNorm = normalizeWhitespace(normFileLines[i + j]);
55
+ const searchLineNorm = normalizeWhitespace(searchLines[j]);
56
+ if (fileLineNorm !== searchLineNorm) {
57
+ isMatch = false;
58
+ break;
59
+ }
60
+ }
61
+ if (isMatch) {
62
+ // Map back to original byte offsets
63
+ const start = getByteOffsetOfLine(fileContent, i);
64
+ const endLineOffset = getByteOffsetOfLine(fileContent, i + numSearchLines - 1);
65
+ const originalEndLine = normFileLines[i + numSearchLines - 1];
66
+ const end = endLineOffset + originalEndLine.length;
67
+ return {
68
+ start,
69
+ end,
70
+ strategy: 'Whitespace-Normalized Match',
71
+ };
72
+ }
73
+ }
74
+ }
75
+ // Strategy 3: Levenshtein Similarity
76
+ // We use a sliding window of size `numSearchLines`, `+ 1`, and `- 1` to handle line hallucinations.
77
+ const THRESHOLD = 0.8;
78
+ let bestSim = 0;
79
+ let bestMatch = null;
80
+ if (numSearchLines > 0) {
81
+ for (let i = 0; i < normFileLines.length; i++) {
82
+ for (const windowSize of [numSearchLines, numSearchLines + 1, numSearchLines - 1]) {
83
+ if (windowSize <= 0 || i + windowSize > normFileLines.length)
84
+ continue;
85
+ const windowLines = normFileLines.slice(i, i + windowSize);
86
+ // Normalize both strings before comparing to ignore indentation/whitespace differences
87
+ const windowStr = normalizeWhitespace(windowLines.join('\n'));
88
+ const targetStr = normalizeWhitespace(searchLines.join('\n'));
89
+ const sim = levenshteinSimilarity(windowStr, targetStr);
90
+ if (sim >= THRESHOLD && sim > bestSim) {
91
+ bestSim = sim;
92
+ const start = getByteOffsetOfLine(fileContent, i);
93
+ const endLineOffset = getByteOffsetOfLine(fileContent, i + windowSize - 1);
94
+ const originalEndLine = normFileLines[i + windowSize - 1];
95
+ const end = endLineOffset + originalEndLine.length;
96
+ bestMatch = {
97
+ start,
98
+ end,
99
+ strategy: `Levenshtein Match (${(sim * 100).toFixed(1)}%) [Window: ${windowSize}]`,
100
+ };
101
+ }
102
+ }
103
+ }
104
+ }
105
+ return bestMatch;
106
+ }
107
+ export function applyMatch(fileContent, match, replaceContent) {
108
+ return fileContent.slice(0, match.start) + replaceContent + fileContent.slice(match.end);
109
+ }
110
+ function getByteOffsetOfLine(text, lineIndex) {
111
+ let offset = 0;
112
+ const lines = text.split(/\r?\n/);
113
+ for (let i = 0; i < lineIndex; i++) {
114
+ offset += lines[i].length + 1; // +1 for the newline character
115
+ // Account for \r\n
116
+ if (text[offset - 1] === '\n' && text[offset - 2] === '\r') {
117
+ offset += 1;
118
+ }
119
+ }
120
+ return offset;
121
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Toggles the internal debug mode on/off.
3
+ */
4
+ export declare function toggleDebugMode(): boolean;
5
+ /**
6
+ * Prints a debug log to the console if debug mode is enabled.
7
+ */
8
+ export declare function debugLog(message: string): void;
@@ -0,0 +1,17 @@
1
+ import pc from 'picocolors';
2
+ let isDebugEnabled = process.env.MINO_DEBUG === 'true';
3
+ /**
4
+ * Toggles the internal debug mode on/off.
5
+ */
6
+ export function toggleDebugMode() {
7
+ isDebugEnabled = !isDebugEnabled;
8
+ return isDebugEnabled;
9
+ }
10
+ /**
11
+ * Prints a debug log to the console if debug mode is enabled.
12
+ */
13
+ export function debugLog(message) {
14
+ if (isDebugEnabled) {
15
+ console.log(pc.magenta(`[DEBUG] ${message}`));
16
+ }
17
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Resolves a file path against the workspace root and ensures it does not
3
+ * break out of the workspace directory (path traversal defense).
4
+ *
5
+ * @param workspaceRoot The normalized absolute path to the workspace root
6
+ * @param filePath The user or AI provided file path (relative or absolute)
7
+ * @returns The validated absolute path
8
+ * @throws Error if the resolved path is outside the workspace
9
+ */
10
+ export declare function resolveAndValidatePath(workspaceRoot: string, filePath: string): string;
@@ -0,0 +1,26 @@
1
+ import * as path from 'path';
2
+ /**
3
+ * Resolves a file path against the workspace root and ensures it does not
4
+ * break out of the workspace directory (path traversal defense).
5
+ *
6
+ * @param workspaceRoot The normalized absolute path to the workspace root
7
+ * @param filePath The user or AI provided file path (relative or absolute)
8
+ * @returns The validated absolute path
9
+ * @throws Error if the resolved path is outside the workspace
10
+ */
11
+ export function resolveAndValidatePath(workspaceRoot, filePath) {
12
+ // Prevent absolute paths from bypassing the workspace root entirely
13
+ if (path.isAbsolute(filePath)) {
14
+ throw new Error(`Path security violation: Absolute paths are not allowed ("${filePath}"). Please use relative paths.`);
15
+ }
16
+ // Resolve the path against the workspace root
17
+ const resolvedPath = path.resolve(workspaceRoot, filePath);
18
+ // Normalize both paths to ensure consistent matching (removes .., ., extra slashes)
19
+ const normalizedRoot = path.normalize(workspaceRoot);
20
+ const normalizedResolved = path.normalize(resolvedPath);
21
+ // Ensure the resolved path starts with the workspace root directory
22
+ if (!normalizedResolved.startsWith(normalizedRoot)) {
23
+ throw new Error(`Path security violation: Path "${filePath}" escapes the workspace root.`);
24
+ }
25
+ return normalizedResolved;
26
+ }
@@ -0,0 +1,6 @@
1
+ export interface ExtractedSymbol {
2
+ symbol: string;
3
+ startLine: number;
4
+ endLine: number;
5
+ }
6
+ export declare function extractSymbols(content: string, filePath: string, targetElements: string[]): string;
@@ -0,0 +1,249 @@
1
+ import * as path from 'node:path';
2
+ function getIndentation(line) {
3
+ const match = line.match(/^([ \t]*)/);
4
+ return match ? match[1].length : 0;
5
+ }
6
+ function isCommentOrEmpty(line, ext) {
7
+ const trimmed = line.trim();
8
+ if (!trimmed)
9
+ return true;
10
+ if (ext === '.py')
11
+ return trimmed.startsWith('#');
12
+ return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*');
13
+ }
14
+ function buildDeclarationRegex(symbolName, ext) {
15
+ const s = symbolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
+ if (ext === '.py') {
17
+ return new RegExp(`^\\s*(?:async\\s+)?(?:def|class)\\s+${s}\\b|^\\s*${s}\\s*[:=]`);
18
+ }
19
+ if (ext === '.go') {
20
+ return new RegExp(`^\\s*(?:func(?:\\s+\\([^)]+\\))?\\s+${s}\\b|type\\s+${s}\\b|var\\s+${s}\\b|const\\s+${s}\\b)`);
21
+ }
22
+ if (ext === '.rs') {
23
+ return new RegExp(`^\\s*(?:(?:pub\\s+)?(?:async\\s+)?fn\\s+${s}\\b|(?:pub\\s+)?struct\\s+${s}\\b|(?:pub\\s+)?enum\\s+${s}\\b|(?:pub\\s+)?trait\\s+${s}\\b|(?:pub\\s+)?impl(?:\\s+.*)?\\s+for\\s+${s}\\b)`);
24
+ }
25
+ // Default for JS, TS, Java, C++, etc.
26
+ return new RegExp(`^\\s*(?:(?:export\\s+|default\\s+|async\\s+|abstract\\s+|static\\s+|public\\s+|private\\s+|protected\\s+|readonly\\s+)*` +
27
+ `(?:class|function|const|let|var|type|interface|enum)\\s+(?:[\\w<>]+\\s+)?${s}\\b|` +
28
+ `^\\s*${s}\\s*[:=]|` +
29
+ `^\\s*(?:async\\s+)?${s}\\s*[<]?\\w*[>]?\\s*\\()`);
30
+ }
31
+ function findPrecedingContext(lines, declarationIndex, ext) {
32
+ let start = declarationIndex;
33
+ // Walk backwards to include decorators and JSDoc/docstrings
34
+ while (start > 0) {
35
+ const prevLine = lines[start - 1].trim();
36
+ if (!prevLine) {
37
+ start--;
38
+ continue;
39
+ }
40
+ const isDecorator = prevLine.startsWith('@');
41
+ const isPythonComment = ext === '.py' && prevLine.startsWith('#');
42
+ const isJsDocEnd = (ext === '.ts' || ext === '.js' || ext === '.tsx' || ext === '.jsx') && prevLine === '*/';
43
+ const isJsDocLine = (ext === '.ts' || ext === '.js' || ext === '.tsx' || ext === '.jsx') && prevLine.startsWith('*');
44
+ const isJsDocStart = (ext === '.ts' || ext === '.js' || ext === '.tsx' || ext === '.jsx') && prevLine.startsWith('/**');
45
+ const isLineComment = prevLine.startsWith('//');
46
+ if (isDecorator || isPythonComment || isLineComment || isJsDocEnd || isJsDocLine || isJsDocStart) {
47
+ start--;
48
+ if (isJsDocStart)
49
+ break; // Usually stop scanning up after /**
50
+ }
51
+ else {
52
+ break;
53
+ }
54
+ }
55
+ return start;
56
+ }
57
+ export function extractSymbols(content, filePath, targetElements) {
58
+ if (!targetElements || targetElements.length === 0)
59
+ return content;
60
+ const ext = path.extname(filePath).toLowerCase();
61
+ const lines = content.split('\n');
62
+ const linesToKeep = new Set();
63
+ // To avoid false positives inside large strings/comments, we do a basic stateful scan
64
+ let inMultiLineComment = false;
65
+ let inMultiLineString = null;
66
+ // Fast tracking of states per line so we don't start symbols inside comments
67
+ const validLines = new Array(lines.length).fill(true);
68
+ for (let i = 0; i < lines.length; i++) {
69
+ let line = lines[i];
70
+ let j = 0;
71
+ let lineHasValidCode = false;
72
+ while (j < line.length) {
73
+ // String logic
74
+ if (!inMultiLineComment && inMultiLineString) {
75
+ if (line[j] === '\\') {
76
+ j += 2;
77
+ continue;
78
+ }
79
+ if (line[j] === inMultiLineString) {
80
+ inMultiLineString = null;
81
+ }
82
+ j++;
83
+ continue;
84
+ }
85
+ if (!inMultiLineComment && !inMultiLineString) {
86
+ // Comment start
87
+ if (line[j] === '/' && line[j + 1] === '/') {
88
+ break;
89
+ } // Line comment ends processing for this line
90
+ if (ext === '.py' && line[j] === '#') {
91
+ break;
92
+ }
93
+ if (line[j] === '/' && line[j + 1] === '*') {
94
+ inMultiLineComment = true;
95
+ j += 2;
96
+ continue;
97
+ }
98
+ // String start
99
+ if (line[j] === '"' || line[j] === "'" || line[j] === '`') {
100
+ inMultiLineString = line[j];
101
+ j++;
102
+ continue;
103
+ }
104
+ if (line[j].trim())
105
+ lineHasValidCode = true;
106
+ }
107
+ // Comment end
108
+ if (inMultiLineComment && line[j] === '*' && line[j + 1] === '/') {
109
+ inMultiLineComment = false;
110
+ j += 2;
111
+ continue;
112
+ }
113
+ j++;
114
+ }
115
+ if (!lineHasValidCode && (inMultiLineComment || inMultiLineString)) {
116
+ validLines[i] = false;
117
+ }
118
+ }
119
+ // Iterate over requested symbols
120
+ for (const symbol of targetElements) {
121
+ const regex = buildDeclarationRegex(symbol, ext);
122
+ for (let i = 0; i < lines.length; i++) {
123
+ if (!validLines[i])
124
+ continue;
125
+ if (regex.test(lines[i])) {
126
+ const startIndex = findPrecedingContext(lines, i, ext);
127
+ let endIndex = i;
128
+ // Python indentation-based termination
129
+ if (ext === '.py') {
130
+ const baseIndentation = getIndentation(lines[i]);
131
+ // Move forward until we find a line with less or equal indentation that isn't empty/comment
132
+ for (let j = i + 1; j < lines.length; j++) {
133
+ if (isCommentOrEmpty(lines[j], ext)) {
134
+ endIndex = j;
135
+ continue;
136
+ }
137
+ if (getIndentation(lines[j]) <= baseIndentation) {
138
+ break;
139
+ }
140
+ endIndex = j;
141
+ }
142
+ }
143
+ // Bracket/brace-based termination
144
+ else {
145
+ let braces = 0;
146
+ let brackets = 0;
147
+ let parens = 0;
148
+ let foundOpen = false;
149
+ let inStr = null;
150
+ let inComment = false;
151
+ for (let j = i; j < lines.length; j++) {
152
+ endIndex = j;
153
+ const line = lines[j];
154
+ let charIdx = 0;
155
+ while (charIdx < line.length) {
156
+ const char = line[charIdx];
157
+ if (inComment) {
158
+ if (char === '*' && line[charIdx + 1] === '/') {
159
+ inComment = false;
160
+ charIdx += 2;
161
+ continue;
162
+ }
163
+ charIdx++;
164
+ continue;
165
+ }
166
+ if (inStr) {
167
+ if (char === '\\') {
168
+ charIdx += 2;
169
+ continue;
170
+ }
171
+ if (char === inStr)
172
+ inStr = null;
173
+ charIdx++;
174
+ continue;
175
+ }
176
+ if (char === '/' && line[charIdx + 1] === '/')
177
+ break;
178
+ if (char === '/' && line[charIdx + 1] === '*') {
179
+ inComment = true;
180
+ charIdx += 2;
181
+ continue;
182
+ }
183
+ if (char === '"' || char === "'" || char === '`') {
184
+ inStr = char;
185
+ charIdx++;
186
+ continue;
187
+ }
188
+ if (char === '{') {
189
+ braces++;
190
+ foundOpen = true;
191
+ }
192
+ else if (char === '}')
193
+ braces--;
194
+ else if (char === '[') {
195
+ brackets++;
196
+ foundOpen = true;
197
+ }
198
+ else if (char === ']')
199
+ brackets--;
200
+ else if (char === '(') {
201
+ parens++;
202
+ foundOpen = true;
203
+ }
204
+ else if (char === ')')
205
+ parens--;
206
+ charIdx++;
207
+ }
208
+ // If we found some opening block and it all balanced out to 0, we're done
209
+ if (foundOpen && braces === 0 && brackets === 0 && parens === 0) {
210
+ break;
211
+ }
212
+ // For one-liners without braces
213
+ if (!foundOpen && j > i && braces === 0 && brackets === 0 && parens === 0) {
214
+ // If we went to a new line and still no open braces, maybe it was a single line statement
215
+ // Let's just break. But wait, if it's a multi-line chain, let's keep going if it ends with . or ,
216
+ const prevLine = lines[j - 1].trim();
217
+ if (!prevLine.endsWith(',') && !prevLine.endsWith('.')) {
218
+ endIndex = j - 1;
219
+ break;
220
+ }
221
+ }
222
+ }
223
+ }
224
+ // Add lines to keep
225
+ for (let k = startIndex; k <= endIndex; k++) {
226
+ linesToKeep.add(k);
227
+ }
228
+ }
229
+ }
230
+ }
231
+ // If we found nothing, fall back to returning the whole file (or maybe just an empty string?
232
+ // Returning the whole file might defeat the purpose, but returning an error is better handled by the caller.
233
+ // We'll return what we found. If it's empty, we return a message.)
234
+ if (linesToKeep.size === 0) {
235
+ return `// No specific symbols matching \${JSON.stringify(targetElements)} were found in this file.\n// Try using normal read_file without targetElements, or check spelling.`;
236
+ }
237
+ // Construct output with omitted blocks
238
+ const output = [];
239
+ let previousLineNum = -2;
240
+ const sortedLines = Array.from(linesToKeep).sort((a, b) => a - b);
241
+ for (const lineNum of sortedLines) {
242
+ if (lineNum > previousLineNum + 1 && previousLineNum !== -2) {
243
+ output.push('\n// ... [Code Omitted] ...\n');
244
+ }
245
+ output.push(lines[lineNum]);
246
+ previousLineNum = lineNum;
247
+ }
248
+ return output.join('\n');
249
+ }
@@ -0,0 +1,5 @@
1
+ export interface ValidationResult {
2
+ valid: boolean;
3
+ errors: string[];
4
+ }
5
+ export declare function validateSyntax(content: string, filePath: string): ValidationResult;
@@ -0,0 +1,81 @@
1
+ import * as path from 'node:path';
2
+ export function validateSyntax(content, filePath) {
3
+ const ext = path.extname(filePath).toLowerCase();
4
+ const errors = [];
5
+ // Fast check for truncation markers from AI
6
+ if (/(\/\/|\/\*)\s*\.\.\./.test(content) || /<!--\s*\.\.\.\s*-->/.test(content)) {
7
+ errors.push('File contains a truncation marker (e.g., "// ..."). Please output the complete file content without truncating.');
8
+ }
9
+ // Language specific checks
10
+ if (ext === '.json') {
11
+ try {
12
+ JSON.parse(content);
13
+ }
14
+ catch (err) {
15
+ errors.push(`Invalid JSON syntax: ${err instanceof Error ? err.message : String(err)}`);
16
+ }
17
+ }
18
+ else if (['.ts', '.tsx', '.js', '.jsx', '.css', '.scss'].includes(ext)) {
19
+ const braceBalance = countBalance(content, '{', '}');
20
+ const bracketBalance = countBalance(content, '[', ']');
21
+ const parenBalance = countBalance(content, '(', ')');
22
+ if (braceBalance > 0)
23
+ errors.push(`Unmatched opening brace '{' (missing ${braceBalance} closing braces)`);
24
+ if (braceBalance < 0)
25
+ errors.push(`Unmatched closing brace '}' (missing ${-braceBalance} opening braces)`);
26
+ if (bracketBalance > 0)
27
+ errors.push(`Unmatched opening bracket '[' (missing ${bracketBalance} closing brackets)`);
28
+ if (bracketBalance < 0)
29
+ errors.push(`Unmatched closing bracket ']' (missing ${-bracketBalance} opening brackets)`);
30
+ if (parenBalance > 0)
31
+ errors.push(`Unmatched opening parenthesis '(' (missing ${parenBalance} closing parentheses)`);
32
+ if (parenBalance < 0)
33
+ errors.push(`Unmatched closing parenthesis ')' (missing ${-parenBalance} opening parentheses)`);
34
+ }
35
+ return {
36
+ valid: errors.length === 0,
37
+ errors,
38
+ };
39
+ }
40
+ function countBalance(text, openChar, closeChar) {
41
+ let balance = 0;
42
+ let inString = false;
43
+ let stringChar = '';
44
+ for (let i = 0; i < text.length; i++) {
45
+ const char = text[i];
46
+ // Skip string contents
47
+ if (inString) {
48
+ if (char === '\\') {
49
+ i++; // Skip escaped character
50
+ continue;
51
+ }
52
+ if (char === stringChar) {
53
+ inString = false;
54
+ }
55
+ continue;
56
+ }
57
+ // Entering a string
58
+ if (char === '"' || char === "'" || char === '`') {
59
+ inString = true;
60
+ stringChar = char;
61
+ continue;
62
+ }
63
+ // Basic comment skipping
64
+ if (char === '/' && text[i + 1] === '/') {
65
+ const nextLine = text.indexOf('\n', i);
66
+ i = nextLine !== -1 ? nextLine : text.length;
67
+ continue;
68
+ }
69
+ // Block comment skip
70
+ if (char === '/' && text[i + 1] === '*') {
71
+ const nextEnd = text.indexOf('*/', i + 2);
72
+ i = nextEnd !== -1 ? nextEnd + 1 : text.length;
73
+ continue;
74
+ }
75
+ if (char === openChar)
76
+ balance++;
77
+ else if (char === closeChar)
78
+ balance--;
79
+ }
80
+ return balance;
81
+ }