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.
- package/README.md +418 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/chat.d.ts +7 -0
- package/dist/commands/chat.js +30 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.js +18 -0
- package/dist/commands/logout.d.ts +5 -0
- package/dist/commands/logout.js +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/agent-tools.d.ts +36 -0
- package/dist/services/agent-tools.js +764 -0
- package/dist/services/agent.d.ts +21 -0
- package/dist/services/agent.js +648 -0
- package/dist/services/ai.d.ts +60 -0
- package/dist/services/ai.js +331 -0
- package/dist/services/auth.d.ts +3 -0
- package/dist/services/auth.js +183 -0
- package/dist/services/changeLogger.d.ts +23 -0
- package/dist/services/changeLogger.js +57 -0
- package/dist/services/contextAgent.d.ts +20 -0
- package/dist/services/contextAgent.js +440 -0
- package/dist/services/proxyClient.d.ts +21 -0
- package/dist/services/proxyClient.js +119 -0
- package/dist/services/verificationService.d.ts +10 -0
- package/dist/services/verificationService.js +148 -0
- package/dist/utils/atomicWrite.d.ts +6 -0
- package/dist/utils/atomicWrite.js +29 -0
- package/dist/utils/config.d.ts +17 -0
- package/dist/utils/config.js +17 -0
- package/dist/utils/contextPrompts.d.ts +3 -0
- package/dist/utils/contextPrompts.js +34 -0
- package/dist/utils/dependencyTracer.d.ts +48 -0
- package/dist/utils/dependencyTracer.js +647 -0
- package/dist/utils/excludedExtensions.d.ts +8 -0
- package/dist/utils/excludedExtensions.js +125 -0
- package/dist/utils/fuzzyMatch.d.ts +21 -0
- package/dist/utils/fuzzyMatch.js +121 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +17 -0
- package/dist/utils/pathSecurity.d.ts +10 -0
- package/dist/utils/pathSecurity.js +26 -0
- package/dist/utils/symbolExtractor.d.ts +6 -0
- package/dist/utils/symbolExtractor.js +249 -0
- package/dist/utils/syntaxValidator.d.ts +5 -0
- package/dist/utils/syntaxValidator.js +81 -0
- package/dist/utils/systemPrompts.d.ts +5 -0
- package/dist/utils/systemPrompts.js +119 -0
- package/oclif.manifest.json +69 -0
- 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,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,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,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
|
+
}
|