@probelabs/probe 0.6.0-rc253 → 0.6.0-rc255
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 +166 -3
- package/bin/binaries/probe-v0.6.0-rc255-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.d.ts +1 -1
- package/build/agent/ProbeAgent.js +51 -16
- package/build/agent/acp/tools.js +2 -1
- package/build/agent/acp/tools.test.js +2 -1
- package/build/agent/dsl/environment.js +19 -0
- package/build/agent/index.js +1512 -413
- package/build/agent/schemaUtils.js +91 -2
- package/build/agent/tools.js +0 -28
- package/build/delegate.js +3 -0
- package/build/index.js +2 -0
- package/build/tools/common.js +6 -5
- package/build/tools/edit.js +457 -65
- package/build/tools/executePlan.js +3 -1
- package/build/tools/fileTracker.js +318 -0
- package/build/tools/fuzzyMatch.js +271 -0
- package/build/tools/hashline.js +131 -0
- package/build/tools/lineEditHeuristics.js +138 -0
- package/build/tools/symbolEdit.js +119 -0
- package/build/tools/vercel.js +40 -9
- package/cjs/agent/ProbeAgent.cjs +1615 -517
- package/cjs/index.cjs +1643 -543
- package/index.d.ts +189 -1
- package/package.json +1 -1
- package/src/agent/ProbeAgent.d.ts +1 -1
- package/src/agent/ProbeAgent.js +51 -16
- package/src/agent/acp/tools.js +2 -1
- package/src/agent/acp/tools.test.js +2 -1
- package/src/agent/dsl/environment.js +19 -0
- package/src/agent/index.js +14 -3
- package/src/agent/schemaUtils.js +91 -2
- package/src/agent/tools.js +0 -28
- package/src/delegate.js +3 -0
- package/src/index.js +2 -0
- package/src/tools/common.js +6 -5
- package/src/tools/edit.js +457 -65
- package/src/tools/executePlan.js +3 -1
- package/src/tools/fileTracker.js +318 -0
- package/src/tools/fuzzyMatch.js +271 -0
- package/src/tools/hashline.js +131 -0
- package/src/tools/lineEditHeuristics.js +138 -0
- package/src/tools/symbolEdit.js +119 -0
- package/src/tools/vercel.js +40 -9
- package/bin/binaries/probe-v0.6.0-rc253-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-x86_64-unknown-linux-musl.tar.gz +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash-based line integrity utilities for line-targeted editing.
|
|
3
|
+
* Uses DJB2 hash of whitespace-stripped content, mod 256, as 2-char hex.
|
|
4
|
+
* Pure functions, zero external dependencies.
|
|
5
|
+
* @module tools/hashline
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compute a 2-char hex hash for a line of code.
|
|
10
|
+
* DJB2 hash of whitespace-stripped content mod 256.
|
|
11
|
+
* @param {string} line - The line content
|
|
12
|
+
* @returns {string} 2-char hex hash (e.g. "ab")
|
|
13
|
+
*/
|
|
14
|
+
export function computeLineHash(line) {
|
|
15
|
+
const stripped = (line || '').replace(/\s+/g, '');
|
|
16
|
+
let h = 5381;
|
|
17
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
18
|
+
h = ((h << 5) + h + stripped.charCodeAt(i)) & 0xFFFFFFFF;
|
|
19
|
+
}
|
|
20
|
+
return ((h >>> 0) % 256).toString(16).padStart(2, '0');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a line reference string into line number and optional hash.
|
|
25
|
+
* Handles XML coercion: number 42 → {line:42, hash:null}
|
|
26
|
+
* String formats: "42" → {line:42, hash:null}, "42:ab" → {line:42, hash:"ab"}
|
|
27
|
+
* @param {string|number} ref - Line reference
|
|
28
|
+
* @returns {{line: number, hash: string|null}|null} Parsed ref or null if invalid
|
|
29
|
+
*/
|
|
30
|
+
export function parseLineRef(ref) {
|
|
31
|
+
if (ref === undefined || ref === null) return null;
|
|
32
|
+
|
|
33
|
+
const str = String(ref).trim();
|
|
34
|
+
if (!str) return null;
|
|
35
|
+
|
|
36
|
+
// Format: "42:ab" (line with hash)
|
|
37
|
+
const hashMatch = str.match(/^(\d+):([0-9a-fA-F]{2})$/);
|
|
38
|
+
if (hashMatch) {
|
|
39
|
+
const line = parseInt(hashMatch[1], 10);
|
|
40
|
+
if (line < 1 || !isFinite(line)) return null;
|
|
41
|
+
return { line, hash: hashMatch[2].toLowerCase() };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Format: "42" (plain line number)
|
|
45
|
+
const lineMatch = str.match(/^(\d+)$/);
|
|
46
|
+
if (lineMatch) {
|
|
47
|
+
const line = parseInt(lineMatch[1], 10);
|
|
48
|
+
if (line < 1 || !isFinite(line)) return null;
|
|
49
|
+
return { line, hash: null };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate a hash against the actual file content at a line number.
|
|
57
|
+
* @param {number} lineNum - 1-indexed line number
|
|
58
|
+
* @param {string} hash - Expected 2-char hex hash
|
|
59
|
+
* @param {string[]} fileLines - Array of file lines
|
|
60
|
+
* @returns {{valid: boolean, actualHash: string, actualContent: string}}
|
|
61
|
+
*/
|
|
62
|
+
export function validateLineHash(lineNum, hash, fileLines) {
|
|
63
|
+
const idx = lineNum - 1;
|
|
64
|
+
if (idx < 0 || idx >= fileLines.length) {
|
|
65
|
+
return { valid: false, actualHash: '', actualContent: '' };
|
|
66
|
+
}
|
|
67
|
+
const actualContent = fileLines[idx];
|
|
68
|
+
const actualHash = computeLineHash(actualContent);
|
|
69
|
+
return {
|
|
70
|
+
valid: actualHash === hash.toLowerCase(),
|
|
71
|
+
actualHash,
|
|
72
|
+
actualContent
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Annotate probe output with line hashes.
|
|
78
|
+
* Transforms " 42 |" to " 42:ab |" in each line.
|
|
79
|
+
* Handles the probe output format: optional whitespace + line number + space(s) + pipe.
|
|
80
|
+
* @param {string} output - Raw probe output
|
|
81
|
+
* @returns {string} Annotated output with hashes
|
|
82
|
+
*/
|
|
83
|
+
export function annotateOutputWithHashes(output) {
|
|
84
|
+
if (!output || typeof output !== 'string') return output;
|
|
85
|
+
|
|
86
|
+
return output.split('\n').map(line => {
|
|
87
|
+
// Strip trailing \r from CRLF line endings before matching
|
|
88
|
+
const cleanLine = line.endsWith('\r') ? line.slice(0, -1) : line;
|
|
89
|
+
// Match probe output format: leading whitespace + digits + whitespace + pipe
|
|
90
|
+
const match = cleanLine.match(/^(\s*)(\d+)(\s*\|)(.*)$/);
|
|
91
|
+
if (!match) return line;
|
|
92
|
+
|
|
93
|
+
const [, prefix, lineNum, pipeSection, content] = match;
|
|
94
|
+
const hash = computeLineHash(content);
|
|
95
|
+
const cr = line.endsWith('\r') ? '\r' : '';
|
|
96
|
+
return `${prefix}${lineNum}:${hash}${pipeSection}${content}${cr}`;
|
|
97
|
+
}).join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Strip accidental line-number or line:hash prefixes from LLM new_string content.
|
|
102
|
+
* LLMs sometimes echo the "42:ab | " or "42 | " prefix format in their replacement text.
|
|
103
|
+
* @param {string} text - The new_string content
|
|
104
|
+
* @returns {{cleaned: string, stripped: boolean}} Cleaned text and whether stripping occurred
|
|
105
|
+
*/
|
|
106
|
+
export function stripHashlinePrefixes(text) {
|
|
107
|
+
if (!text || typeof text !== 'string') return { cleaned: text || '', stripped: false };
|
|
108
|
+
|
|
109
|
+
const lines = text.split('\n');
|
|
110
|
+
if (lines.length === 0) return { cleaned: '', stripped: false };
|
|
111
|
+
|
|
112
|
+
// Check if majority of non-empty lines have the prefix pattern
|
|
113
|
+
const nonEmptyLines = lines.filter(l => l.trim().length > 0);
|
|
114
|
+
if (nonEmptyLines.length === 0) return { cleaned: text, stripped: false };
|
|
115
|
+
|
|
116
|
+
// Pattern: optional whitespace + digits + optional ":xx" + space(s) + pipe + space
|
|
117
|
+
const prefixPattern = /^\s*\d+(?::[0-9a-fA-F]{2})?\s*\|\s?/;
|
|
118
|
+
const matchCount = nonEmptyLines.filter(l => prefixPattern.test(l)).length;
|
|
119
|
+
|
|
120
|
+
// Only strip if majority (>50%) of non-empty lines have prefixes
|
|
121
|
+
if (matchCount / nonEmptyLines.length <= 0.5) {
|
|
122
|
+
return { cleaned: text, stripped: false };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const cleaned = lines.map(line => {
|
|
126
|
+
if (line.trim().length === 0) return line;
|
|
127
|
+
return line.replace(prefixPattern, '');
|
|
128
|
+
}).join('\n');
|
|
129
|
+
|
|
130
|
+
return { cleaned, stripped: true };
|
|
131
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic corrections for common LLM mistakes in line-targeted edits.
|
|
3
|
+
* Handles echo stripping, indent restoration, and prefix stripping.
|
|
4
|
+
* @module tools/lineEditHeuristics
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { detectBaseIndent, reindent } from './symbolEdit.js';
|
|
8
|
+
import { stripHashlinePrefixes } from './hashline.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Strip boundary lines that LLMs accidentally echo from the file context.
|
|
12
|
+
*
|
|
13
|
+
* Rules:
|
|
14
|
+
* - Replace: if first line of new_string matches the line before start_line → strip it.
|
|
15
|
+
* if last line matches the line after end_line → strip it.
|
|
16
|
+
* - Insert-after: if first line matches the anchor line → strip it.
|
|
17
|
+
* - Insert-before: if last line matches the anchor line → strip it.
|
|
18
|
+
* - Blank lines are never considered matches (two blanks matching is coincidence).
|
|
19
|
+
*
|
|
20
|
+
* @param {string} newStr - The new_string content
|
|
21
|
+
* @param {string[]} fileLines - Array of file lines (0-indexed)
|
|
22
|
+
* @param {number} startLine - 1-indexed start line
|
|
23
|
+
* @param {number} endLine - 1-indexed end line (same as startLine for single-line or insert)
|
|
24
|
+
* @param {string|undefined} position - "before", "after", or undefined (replace mode)
|
|
25
|
+
* @returns {{result: string, modifications: string[]}}
|
|
26
|
+
*/
|
|
27
|
+
export function stripEchoedBoundaries(newStr, fileLines, startLine, endLine, position) {
|
|
28
|
+
const modifications = [];
|
|
29
|
+
let lines = newStr.split('\n');
|
|
30
|
+
|
|
31
|
+
if (lines.length === 0) return { result: newStr, modifications };
|
|
32
|
+
|
|
33
|
+
if (position === 'after') {
|
|
34
|
+
// Insert-after: anchor line is at startLine (1-indexed)
|
|
35
|
+
const anchorIdx = startLine - 1;
|
|
36
|
+
if (anchorIdx >= 0 && anchorIdx < fileLines.length) {
|
|
37
|
+
const anchorTrimmed = fileLines[anchorIdx].trim();
|
|
38
|
+
if (anchorTrimmed.length > 0 && lines.length > 0 && lines[0].trim() === anchorTrimmed) {
|
|
39
|
+
lines = lines.slice(1);
|
|
40
|
+
modifications.push('stripped echoed anchor line (insert-after)');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} else if (position === 'before') {
|
|
44
|
+
// Insert-before: anchor line is at startLine (1-indexed)
|
|
45
|
+
const anchorIdx = startLine - 1;
|
|
46
|
+
if (anchorIdx >= 0 && anchorIdx < fileLines.length) {
|
|
47
|
+
const anchorTrimmed = fileLines[anchorIdx].trim();
|
|
48
|
+
if (anchorTrimmed.length > 0 && lines.length > 0 && lines[lines.length - 1].trim() === anchorTrimmed) {
|
|
49
|
+
lines = lines.slice(0, -1);
|
|
50
|
+
modifications.push('stripped echoed anchor line (insert-before)');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// Replace mode: check line before start and line after end
|
|
55
|
+
const beforeIdx = startLine - 2; // line before start (0-indexed)
|
|
56
|
+
if (beforeIdx >= 0 && beforeIdx < fileLines.length) {
|
|
57
|
+
const beforeTrimmed = fileLines[beforeIdx].trim();
|
|
58
|
+
if (beforeTrimmed.length > 0 && lines.length > 0 && lines[0].trim() === beforeTrimmed) {
|
|
59
|
+
lines = lines.slice(1);
|
|
60
|
+
modifications.push('stripped echoed line before range');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const afterIdx = endLine; // line after end (0-indexed, since endLine is 1-indexed)
|
|
65
|
+
if (afterIdx >= 0 && afterIdx < fileLines.length) {
|
|
66
|
+
const afterTrimmed = fileLines[afterIdx].trim();
|
|
67
|
+
if (afterTrimmed.length > 0 && lines.length > 0 && lines[lines.length - 1].trim() === afterTrimmed) {
|
|
68
|
+
lines = lines.slice(0, -1);
|
|
69
|
+
modifications.push('stripped echoed line after range');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { result: lines.join('\n'), modifications };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Restore indentation if the replacement has a different base indent than the original lines.
|
|
79
|
+
* @param {string} newStr - The new_string content
|
|
80
|
+
* @param {string[]} originalLines - The original lines being replaced (from the file)
|
|
81
|
+
* @returns {{result: string, modifications: string[]}}
|
|
82
|
+
*/
|
|
83
|
+
export function restoreIndentation(newStr, originalLines) {
|
|
84
|
+
const modifications = [];
|
|
85
|
+
|
|
86
|
+
if (!newStr || !originalLines || originalLines.length === 0) {
|
|
87
|
+
return { result: newStr || '', modifications };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const originalCode = originalLines.join('\n');
|
|
91
|
+
const targetIndent = detectBaseIndent(originalCode);
|
|
92
|
+
const newIndent = detectBaseIndent(newStr);
|
|
93
|
+
|
|
94
|
+
if (targetIndent !== newIndent) {
|
|
95
|
+
const reindented = reindent(newStr, targetIndent);
|
|
96
|
+
if (reindented !== newStr) {
|
|
97
|
+
modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);
|
|
98
|
+
return { result: reindented, modifications };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { result: newStr, modifications };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Pipeline: stripHashlinePrefixes → stripEchoedBoundaries → restoreIndentation.
|
|
107
|
+
* @param {string} newStr - The new_string content
|
|
108
|
+
* @param {string[]} fileLines - Array of all file lines (0-indexed)
|
|
109
|
+
* @param {number} startLine - 1-indexed start line
|
|
110
|
+
* @param {number} endLine - 1-indexed end line
|
|
111
|
+
* @param {string|undefined} position - "before", "after", or undefined
|
|
112
|
+
* @returns {{cleaned: string, modifications: string[]}}
|
|
113
|
+
*/
|
|
114
|
+
export function cleanNewString(newStr, fileLines, startLine, endLine, position) {
|
|
115
|
+
const modifications = [];
|
|
116
|
+
|
|
117
|
+
if (!newStr && newStr !== '') return { cleaned: '', modifications };
|
|
118
|
+
|
|
119
|
+
// Step 1: Strip hashline prefixes
|
|
120
|
+
const { cleaned: afterPrefixes, stripped } = stripHashlinePrefixes(newStr);
|
|
121
|
+
if (stripped) modifications.push('stripped line-number prefixes');
|
|
122
|
+
|
|
123
|
+
// Step 2: Strip echoed boundaries
|
|
124
|
+
const { result: afterEchoes, modifications: echoMods } = stripEchoedBoundaries(
|
|
125
|
+
afterPrefixes, fileLines, startLine, endLine, position
|
|
126
|
+
);
|
|
127
|
+
modifications.push(...echoMods);
|
|
128
|
+
|
|
129
|
+
// Step 3: Restore indentation (only for replace mode, not insert)
|
|
130
|
+
if (!position) {
|
|
131
|
+
const originalLines = fileLines.slice(startLine - 1, endLine);
|
|
132
|
+
const { result: afterIndent, modifications: indentMods } = restoreIndentation(afterEchoes, originalLines);
|
|
133
|
+
modifications.push(...indentMods);
|
|
134
|
+
return { cleaned: afterIndent, modifications };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { cleaned: afterEchoes, modifications };
|
|
138
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-aware symbol editing helpers
|
|
3
|
+
* Uses probe's tree-sitter AST parsing to find and manipulate code symbols.
|
|
4
|
+
* @module tools/symbolEdit
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { extract } from '../extract.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Look up a symbol in a file using probe's AST-based extract
|
|
11
|
+
* @param {string} filePath - Absolute path to the file
|
|
12
|
+
* @param {string} symbolName - Name of the symbol to find
|
|
13
|
+
* @param {string} cwd - Working directory for extract
|
|
14
|
+
* @returns {Promise<Object|null>} Symbol info with startLine, endLine, code, nodeType, file; or null
|
|
15
|
+
*/
|
|
16
|
+
export async function findSymbol(filePath, symbolName, cwd) {
|
|
17
|
+
try {
|
|
18
|
+
const result = await extract({
|
|
19
|
+
files: [`${filePath}#${symbolName}`],
|
|
20
|
+
format: 'json',
|
|
21
|
+
json: true,
|
|
22
|
+
cwd
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!result || !result.results || result.results.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const match = result.results[0];
|
|
30
|
+
return {
|
|
31
|
+
startLine: match.lines[0], // 1-indexed
|
|
32
|
+
endLine: match.lines[1], // 1-indexed
|
|
33
|
+
code: match.code,
|
|
34
|
+
nodeType: match.node_type,
|
|
35
|
+
file: match.file
|
|
36
|
+
};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (process.env.DEBUG === '1') {
|
|
39
|
+
console.error(`[SymbolEdit] findSymbol error for "${symbolName}" in ${filePath}: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Look up ALL matching symbols in a file using probe's AST-based extract.
|
|
47
|
+
* When a bare name like "process" matches multiple definitions (e.g. a top-level
|
|
48
|
+
* function AND class methods), this returns all of them with qualified names.
|
|
49
|
+
* @param {string} filePath - Absolute path to the file
|
|
50
|
+
* @param {string} symbolName - Name of the symbol to find
|
|
51
|
+
* @param {string} cwd - Working directory for extract
|
|
52
|
+
* @returns {Promise<Array<Object>>} Array of symbol info objects (may be empty)
|
|
53
|
+
*/
|
|
54
|
+
export async function findAllSymbols(filePath, symbolName, cwd) {
|
|
55
|
+
try {
|
|
56
|
+
const result = await extract({
|
|
57
|
+
files: [`${filePath}#${symbolName}`],
|
|
58
|
+
format: 'json',
|
|
59
|
+
json: true,
|
|
60
|
+
cwd
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!result || !result.results || result.results.length === 0) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result.results.map(match => ({
|
|
68
|
+
startLine: match.lines[0],
|
|
69
|
+
endLine: match.lines[1],
|
|
70
|
+
code: match.code,
|
|
71
|
+
nodeType: match.node_type,
|
|
72
|
+
file: match.file,
|
|
73
|
+
qualifiedName: match.symbol_signature || symbolName,
|
|
74
|
+
}));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (process.env.DEBUG === '1') {
|
|
77
|
+
console.error(`[SymbolEdit] findAllSymbols error for "${symbolName}" in ${filePath}: ${error.message}`);
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detect the base indentation of a code block (leading whitespace of first non-empty line)
|
|
85
|
+
* @param {string} code - The code block
|
|
86
|
+
* @returns {string} The leading whitespace string
|
|
87
|
+
*/
|
|
88
|
+
export function detectBaseIndent(code) {
|
|
89
|
+
const lines = code.split('\n');
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
if (line.trim().length > 0) {
|
|
92
|
+
const match = line.match(/^(\s*)/);
|
|
93
|
+
return match ? match[1] : '';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reindent new content to match a target indentation level.
|
|
101
|
+
* Strips the existing base indent from the new content and replaces it with the target indent.
|
|
102
|
+
* @param {string} newContent - The new code content to reindent
|
|
103
|
+
* @param {string} targetIndent - The target indentation string
|
|
104
|
+
* @returns {string} Reindented content
|
|
105
|
+
*/
|
|
106
|
+
export function reindent(newContent, targetIndent) {
|
|
107
|
+
const lines = newContent.split('\n');
|
|
108
|
+
const sourceIndent = detectBaseIndent(newContent);
|
|
109
|
+
|
|
110
|
+
return lines.map(line => {
|
|
111
|
+
if (line.trim().length === 0) {
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
if (line.startsWith(sourceIndent)) {
|
|
115
|
+
return targetIndent + line.slice(sourceIndent.length);
|
|
116
|
+
}
|
|
117
|
+
return line;
|
|
118
|
+
}).join('\n');
|
|
119
|
+
}
|
package/src/tools/vercel.js
CHANGED
|
@@ -11,6 +11,7 @@ import { delegate } from '../delegate.js';
|
|
|
11
11
|
import { analyzeAll } from './analyzeAll.js';
|
|
12
12
|
import { searchSchema, querySchema, extractSchema, delegateSchema, analyzeAllSchema, searchDescription, queryDescription, extractDescription, delegateDescription, analyzeAllDescription, parseTargets, parseAndResolvePaths, resolveTargetPath } from './common.js';
|
|
13
13
|
import { formatErrorForAI } from '../utils/error-types.js';
|
|
14
|
+
import { annotateOutputWithHashes } from './hashline.js';
|
|
14
15
|
|
|
15
16
|
const CODE_SEARCH_SCHEMA = {
|
|
16
17
|
type: 'object',
|
|
@@ -161,9 +162,17 @@ export const searchTool = (options = {}) => {
|
|
|
161
162
|
maxTokens = 20000,
|
|
162
163
|
debug = false,
|
|
163
164
|
outline = false,
|
|
164
|
-
searchDelegate = false
|
|
165
|
+
searchDelegate = false,
|
|
166
|
+
hashLines = false
|
|
165
167
|
} = options;
|
|
166
168
|
|
|
169
|
+
const maybeAnnotate = (result) => {
|
|
170
|
+
if (hashLines && typeof result === 'string') {
|
|
171
|
+
return annotateOutputWithHashes(result);
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
};
|
|
175
|
+
|
|
167
176
|
return tool({
|
|
168
177
|
name: 'search',
|
|
169
178
|
description: searchDelegate
|
|
@@ -215,7 +224,12 @@ export const searchTool = (options = {}) => {
|
|
|
215
224
|
|
|
216
225
|
if (!searchDelegate) {
|
|
217
226
|
try {
|
|
218
|
-
|
|
227
|
+
const result = maybeAnnotate(await runRawSearch());
|
|
228
|
+
// Track files found in search results for staleness detection
|
|
229
|
+
if (options.fileTracker && typeof result === 'string') {
|
|
230
|
+
options.fileTracker.trackFilesFromOutput(result, options.cwd || '.').catch(() => {});
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
219
233
|
} catch (error) {
|
|
220
234
|
console.error('Error executing search command:', error);
|
|
221
235
|
return formatErrorForAI(error);
|
|
@@ -265,7 +279,11 @@ export const searchTool = (options = {}) => {
|
|
|
265
279
|
if (debug) {
|
|
266
280
|
console.error('Delegated search returned no targets; falling back to raw search');
|
|
267
281
|
}
|
|
268
|
-
|
|
282
|
+
const fallbackResult = maybeAnnotate(await runRawSearch());
|
|
283
|
+
if (options.fileTracker && typeof fallbackResult === 'string') {
|
|
284
|
+
options.fileTracker.trackFilesFromOutput(fallbackResult, options.cwd || '.').catch(() => {});
|
|
285
|
+
}
|
|
286
|
+
return fallbackResult;
|
|
269
287
|
}
|
|
270
288
|
|
|
271
289
|
// Resolve relative paths against the actual search directory, not the general cwd.
|
|
@@ -288,14 +306,18 @@ export const searchTool = (options = {}) => {
|
|
|
288
306
|
// Strip workspace root prefix from extract output so paths are relative
|
|
289
307
|
if (resolutionBase && typeof extractResult === 'string') {
|
|
290
308
|
const wsPrefix = resolutionBase.endsWith('/') ? resolutionBase : resolutionBase + '/';
|
|
291
|
-
return extractResult.split(wsPrefix).join('');
|
|
309
|
+
return maybeAnnotate(extractResult.split(wsPrefix).join(''));
|
|
292
310
|
}
|
|
293
311
|
|
|
294
|
-
return extractResult;
|
|
312
|
+
return maybeAnnotate(extractResult);
|
|
295
313
|
} catch (error) {
|
|
296
314
|
console.error('Delegated search failed, falling back to raw search:', error);
|
|
297
315
|
try {
|
|
298
|
-
|
|
316
|
+
const fallbackResult2 = maybeAnnotate(await runRawSearch());
|
|
317
|
+
if (options.fileTracker && typeof fallbackResult2 === 'string') {
|
|
318
|
+
options.fileTracker.trackFilesFromOutput(fallbackResult2, options.cwd || '.').catch(() => {});
|
|
319
|
+
}
|
|
320
|
+
return fallbackResult2;
|
|
299
321
|
} catch (fallbackError) {
|
|
300
322
|
console.error('Error executing search command:', fallbackError);
|
|
301
323
|
// Both delegation and fallback failed - provide detailed error
|
|
@@ -366,7 +388,7 @@ export const queryTool = (options = {}) => {
|
|
|
366
388
|
* @returns {Object} Configured extract tool
|
|
367
389
|
*/
|
|
368
390
|
export const extractTool = (options = {}) => {
|
|
369
|
-
const { debug = false, outline = false } = options;
|
|
391
|
+
const { debug = false, outline = false, hashLines = false } = options;
|
|
370
392
|
|
|
371
393
|
return tool({
|
|
372
394
|
name: 'extract',
|
|
@@ -388,6 +410,7 @@ export const extractTool = (options = {}) => {
|
|
|
388
410
|
// Create a temporary file for input content if provided
|
|
389
411
|
let tempFilePath = null;
|
|
390
412
|
let extractOptions = { cwd: effectiveCwd };
|
|
413
|
+
let extractFiles = null; // Track resolved file targets for content hashing
|
|
391
414
|
|
|
392
415
|
if (input_content) {
|
|
393
416
|
// Import required modules
|
|
@@ -424,7 +447,7 @@ export const extractTool = (options = {}) => {
|
|
|
424
447
|
const parsedTargets = parseTargets(targets);
|
|
425
448
|
|
|
426
449
|
// Resolve relative paths in targets against cwd
|
|
427
|
-
|
|
450
|
+
extractFiles = parsedTargets.map(target => resolveTargetPath(target, effectiveCwd));
|
|
428
451
|
|
|
429
452
|
// Apply format mapping for outline-xml to xml
|
|
430
453
|
let effectiveFormat = format;
|
|
@@ -434,7 +457,7 @@ export const extractTool = (options = {}) => {
|
|
|
434
457
|
|
|
435
458
|
// Set up extract options with files
|
|
436
459
|
extractOptions = {
|
|
437
|
-
files,
|
|
460
|
+
files: extractFiles,
|
|
438
461
|
cwd: effectiveCwd,
|
|
439
462
|
allowTests: allow_tests ?? true,
|
|
440
463
|
contextLines: context_lines,
|
|
@@ -447,6 +470,11 @@ export const extractTool = (options = {}) => {
|
|
|
447
470
|
// Execute the extract command
|
|
448
471
|
const results = await extract(extractOptions);
|
|
449
472
|
|
|
473
|
+
// Track files and symbol content for staleness detection (post-extract)
|
|
474
|
+
if (options.fileTracker && extractFiles && extractFiles.length > 0) {
|
|
475
|
+
options.fileTracker.trackFilesFromExtract(extractFiles, effectiveCwd).catch(() => {});
|
|
476
|
+
}
|
|
477
|
+
|
|
450
478
|
// Clean up temporary file if created
|
|
451
479
|
if (tempFilePath) {
|
|
452
480
|
const { unlinkSync } = await import('fs');
|
|
@@ -460,6 +488,9 @@ export const extractTool = (options = {}) => {
|
|
|
460
488
|
}
|
|
461
489
|
}
|
|
462
490
|
|
|
491
|
+
if (hashLines && typeof results === 'string') {
|
|
492
|
+
return annotateOutputWithHashes(results);
|
|
493
|
+
}
|
|
463
494
|
return results;
|
|
464
495
|
} catch (error) {
|
|
465
496
|
console.error('Error executing extract command:', error);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|