@probelabs/probe 0.6.0-rc291 → 0.6.0-rc292
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/bin/binaries/{probe-v0.6.0-rc291-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc292-aarch64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc291-aarch64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc292-aarch64-unknown-linux-musl.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc291-x86_64-apple-darwin.tar.gz → probe-v0.6.0-rc292-x86_64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc291-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc292-x86_64-pc-windows-msvc.zip} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc291-x86_64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc292-x86_64-unknown-linux-musl.tar.gz} +0 -0
- package/build/agent/dsl/environment.js +8 -1
- package/build/agent/dsl/runtime.js +9 -1
- package/build/tools/executePlan.js +1 -0
- package/build/tools/fuzzyMatch.js +52 -1
- package/build/tools/lineEditHeuristics.js +11 -0
- package/cjs/agent/ProbeAgent.cjs +50 -3
- package/cjs/index.cjs +50 -3
- package/package.json +1 -1
- package/src/agent/dsl/environment.js +8 -1
- package/src/agent/dsl/runtime.js +9 -1
- package/src/tools/executePlan.js +1 -0
- package/src/tools/fuzzyMatch.js +52 -1
- package/src/tools/lineEditHeuristics.js +11 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -234,7 +234,14 @@ export function generateSandboxGlobals(options) {
|
|
|
234
234
|
}
|
|
235
235
|
return tryParseJSONValue(text);
|
|
236
236
|
};
|
|
237
|
-
|
|
237
|
+
const tracedFn = traceToolCall(name, rawMcpFn, tracer, logFn);
|
|
238
|
+
globals[name] = tracedFn;
|
|
239
|
+
// Register sanitized alias for names with hyphens/dots/etc that aren't valid JS identifiers
|
|
240
|
+
// e.g. "workable-api" → also available as "workable_api"
|
|
241
|
+
const sanitized = name.replace(/[^a-zA-Z0-9_$]/g, '_');
|
|
242
|
+
if (sanitized !== name) {
|
|
243
|
+
globals[sanitized] = tracedFn;
|
|
244
|
+
}
|
|
238
245
|
}
|
|
239
246
|
}
|
|
240
247
|
|
|
@@ -181,9 +181,17 @@ export function createDSLRuntime(options) {
|
|
|
181
181
|
'dsl.error': e.message?.substring(0, 500),
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
+
// Enrich "X is not defined" errors with available tool names
|
|
185
|
+
let errorMsg = `Execution failed: ${e.message}`;
|
|
186
|
+
if (e.message && e.message.includes('is not defined')) {
|
|
187
|
+
const globalNames = Object.keys(toolGlobals).sort();
|
|
188
|
+
errorMsg += `\nAvailable functions: ${globalNames.join(', ')}`;
|
|
189
|
+
errorMsg += `\nNote: Tools with hyphens (e.g. "my-tool") are available with underscores: my_tool()`;
|
|
190
|
+
}
|
|
191
|
+
|
|
184
192
|
return {
|
|
185
193
|
status: 'error',
|
|
186
|
-
error:
|
|
194
|
+
error: errorMsg,
|
|
187
195
|
logs,
|
|
188
196
|
};
|
|
189
197
|
}
|
|
@@ -778,6 +778,7 @@ return table;
|
|
|
778
778
|
- Do NOT define helper functions that call tools. Write all logic inline or use for..of loops.
|
|
779
779
|
- Do NOT use regex literals (/pattern/) — use String methods like indexOf, includes, startsWith instead.
|
|
780
780
|
- ONLY use functions listed below. Do NOT call functions that are not listed.
|
|
781
|
+
- MCP tools with hyphens in their names (e.g. \`workable-api\`) are available using underscores: \`workable_api()\`. Hyphens are not valid in JS identifiers.
|
|
781
782
|
|
|
782
783
|
### Available functions
|
|
783
784
|
|
|
@@ -73,7 +73,17 @@ export function lineTrimmedMatch(contentLines, searchLines) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
if (allMatch) {
|
|
76
|
-
|
|
76
|
+
// Limit indent tolerance: even though trimmed content matches, reject when
|
|
77
|
+
// the indentation level difference is too large — it likely means the match
|
|
78
|
+
// is in a completely different scope (issue #507).
|
|
79
|
+
const windowLines = contentLines.slice(i, i + windowSize);
|
|
80
|
+
const windowMinIndent = getMinIndent(windowLines);
|
|
81
|
+
const searchMinIndent = getMinIndent(searchLines);
|
|
82
|
+
const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
|
|
83
|
+
if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
|
|
84
|
+
continue; // Skip — too far off in nesting
|
|
85
|
+
}
|
|
86
|
+
const matchedText = windowLines.join('\n');
|
|
77
87
|
matches.push(matchedText);
|
|
78
88
|
}
|
|
79
89
|
}
|
|
@@ -134,6 +144,19 @@ export function whitespaceNormalizedMatch(content, search) {
|
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
const matchedText = content.substring(originalStart, actualEnd);
|
|
147
|
+
|
|
148
|
+
// Limit indent tolerance: reject matches where the indentation level
|
|
149
|
+
// difference is too large — likely a wrong-scope match (issue #507).
|
|
150
|
+
const matchedLines = matchedText.split('\n');
|
|
151
|
+
const searchLines = search.split('\n');
|
|
152
|
+
const matchMinIndent = getMinIndent(matchedLines);
|
|
153
|
+
const searchMinIndent = getMinIndent(searchLines);
|
|
154
|
+
const indentDiff = Math.abs(matchMinIndent - searchMinIndent);
|
|
155
|
+
if (isIndentDiffTooLarge(matchedLines, searchLines, indentDiff)) {
|
|
156
|
+
searchStart = idx + 1;
|
|
157
|
+
continue; // Skip — too far off in nesting
|
|
158
|
+
}
|
|
159
|
+
|
|
137
160
|
matches.push(matchedText);
|
|
138
161
|
|
|
139
162
|
searchStart = idx + 1;
|
|
@@ -219,6 +242,15 @@ export function indentFlexibleMatch(contentLines, searchLines) {
|
|
|
219
242
|
}
|
|
220
243
|
|
|
221
244
|
if (allMatch) {
|
|
245
|
+
// Limit indent tolerance: reject matches where indentation differs by more than
|
|
246
|
+
// 1 level. Larger differences likely mean the match is in a completely different
|
|
247
|
+
// scope/nesting level — silent file corruption risk (issue #507).
|
|
248
|
+
// For tabs: 1 tab = 1 level, so max diff = 1.
|
|
249
|
+
// For spaces: detect indent unit (2 or 4), allow 1 unit of difference.
|
|
250
|
+
const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
|
|
251
|
+
if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
|
|
252
|
+
continue; // Skip — too far off in nesting
|
|
253
|
+
}
|
|
222
254
|
const matchedText = windowLines.join('\n');
|
|
223
255
|
matches.push(matchedText);
|
|
224
256
|
}
|
|
@@ -232,6 +264,25 @@ export function indentFlexibleMatch(contentLines, searchLines) {
|
|
|
232
264
|
};
|
|
233
265
|
}
|
|
234
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Check if an indentation difference exceeds the allowed limit.
|
|
269
|
+
* Uses tab-aware threshold: 1 for tabs, 4 for spaces.
|
|
270
|
+
* Checks BOTH sides for tab usage to avoid asymmetric detection.
|
|
271
|
+
*
|
|
272
|
+
* @param {string[]} linesA - First set of lines
|
|
273
|
+
* @param {string[]} linesB - Second set of lines
|
|
274
|
+
* @param {number} indentDiff - Absolute difference in min indent
|
|
275
|
+
* @returns {boolean} true if the diff exceeds the limit
|
|
276
|
+
*/
|
|
277
|
+
function isIndentDiffTooLarge(linesA, linesB, indentDiff) {
|
|
278
|
+
if (indentDiff <= 0) return false;
|
|
279
|
+
const sampleA = linesA.find(l => l.trim().length > 0) || '';
|
|
280
|
+
const sampleB = linesB.find(l => l.trim().length > 0) || '';
|
|
281
|
+
const useTabs = sampleA.startsWith('\t') || sampleB.startsWith('\t');
|
|
282
|
+
const maxAllowedDiff = useTabs ? 1 : 4;
|
|
283
|
+
return indentDiff > maxAllowedDiff;
|
|
284
|
+
}
|
|
285
|
+
|
|
235
286
|
/**
|
|
236
287
|
* Get the minimum indentation level (number of leading whitespace characters)
|
|
237
288
|
* across all non-empty lines.
|
|
@@ -92,6 +92,17 @@ export function restoreIndentation(newStr, originalLines) {
|
|
|
92
92
|
const newIndent = detectBaseIndent(newStr);
|
|
93
93
|
|
|
94
94
|
if (targetIndent !== newIndent) {
|
|
95
|
+
// Limit auto-reindent tolerance: reject when indentation differs by more than
|
|
96
|
+
// 1 level. Larger differences likely mean the match landed in a completely
|
|
97
|
+
// different scope — allowing it risks silent file corruption (issue #507).
|
|
98
|
+
// For tabs: 1 tab = 1 level, so max diff = 1 char.
|
|
99
|
+
// For spaces: 1 level = up to 4 spaces, so max diff = 4 chars.
|
|
100
|
+
const indentDiff = Math.abs(targetIndent.length - newIndent.length);
|
|
101
|
+
const useTabs = targetIndent.includes('\t') || newIndent.includes('\t');
|
|
102
|
+
const maxAllowedDiff = useTabs ? 1 : 4;
|
|
103
|
+
if (indentDiff > maxAllowedDiff) {
|
|
104
|
+
return { result: newStr, modifications };
|
|
105
|
+
}
|
|
95
106
|
const reindented = reindent(newStr, targetIndent);
|
|
96
107
|
if (reindented !== newStr) {
|
|
97
108
|
modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);
|
package/cjs/agent/ProbeAgent.cjs
CHANGED
|
@@ -29908,7 +29908,14 @@ function lineTrimmedMatch(contentLines, searchLines) {
|
|
|
29908
29908
|
}
|
|
29909
29909
|
}
|
|
29910
29910
|
if (allMatch) {
|
|
29911
|
-
const
|
|
29911
|
+
const windowLines = contentLines.slice(i, i + windowSize);
|
|
29912
|
+
const windowMinIndent = getMinIndent(windowLines);
|
|
29913
|
+
const searchMinIndent = getMinIndent(searchLines);
|
|
29914
|
+
const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
|
|
29915
|
+
if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
|
|
29916
|
+
continue;
|
|
29917
|
+
}
|
|
29918
|
+
const matchedText = windowLines.join("\n");
|
|
29912
29919
|
matches.push(matchedText);
|
|
29913
29920
|
}
|
|
29914
29921
|
}
|
|
@@ -29938,6 +29945,15 @@ function whitespaceNormalizedMatch(content, search2) {
|
|
|
29938
29945
|
actualEnd++;
|
|
29939
29946
|
}
|
|
29940
29947
|
const matchedText = content.substring(originalStart, actualEnd);
|
|
29948
|
+
const matchedLines = matchedText.split("\n");
|
|
29949
|
+
const searchLines = search2.split("\n");
|
|
29950
|
+
const matchMinIndent = getMinIndent(matchedLines);
|
|
29951
|
+
const searchMinIndent = getMinIndent(searchLines);
|
|
29952
|
+
const indentDiff = Math.abs(matchMinIndent - searchMinIndent);
|
|
29953
|
+
if (isIndentDiffTooLarge(matchedLines, searchLines, indentDiff)) {
|
|
29954
|
+
searchStart = idx + 1;
|
|
29955
|
+
continue;
|
|
29956
|
+
}
|
|
29941
29957
|
matches.push(matchedText);
|
|
29942
29958
|
searchStart = idx + 1;
|
|
29943
29959
|
}
|
|
@@ -29989,6 +30005,10 @@ function indentFlexibleMatch(contentLines, searchLines) {
|
|
|
29989
30005
|
}
|
|
29990
30006
|
}
|
|
29991
30007
|
if (allMatch) {
|
|
30008
|
+
const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
|
|
30009
|
+
if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
|
|
30010
|
+
continue;
|
|
30011
|
+
}
|
|
29992
30012
|
const matchedText = windowLines.join("\n");
|
|
29993
30013
|
matches.push(matchedText);
|
|
29994
30014
|
}
|
|
@@ -29999,6 +30019,14 @@ function indentFlexibleMatch(contentLines, searchLines) {
|
|
|
29999
30019
|
count: matches.length
|
|
30000
30020
|
};
|
|
30001
30021
|
}
|
|
30022
|
+
function isIndentDiffTooLarge(linesA, linesB, indentDiff) {
|
|
30023
|
+
if (indentDiff <= 0) return false;
|
|
30024
|
+
const sampleA = linesA.find((l) => l.trim().length > 0) || "";
|
|
30025
|
+
const sampleB = linesB.find((l) => l.trim().length > 0) || "";
|
|
30026
|
+
const useTabs = sampleA.startsWith(" ") || sampleB.startsWith(" ");
|
|
30027
|
+
const maxAllowedDiff = useTabs ? 1 : 4;
|
|
30028
|
+
return indentDiff > maxAllowedDiff;
|
|
30029
|
+
}
|
|
30002
30030
|
function getMinIndent(lines) {
|
|
30003
30031
|
let min = Infinity;
|
|
30004
30032
|
for (const line of lines) {
|
|
@@ -30158,6 +30186,12 @@ function restoreIndentation(newStr, originalLines) {
|
|
|
30158
30186
|
const targetIndent = detectBaseIndent(originalCode);
|
|
30159
30187
|
const newIndent = detectBaseIndent(newStr);
|
|
30160
30188
|
if (targetIndent !== newIndent) {
|
|
30189
|
+
const indentDiff = Math.abs(targetIndent.length - newIndent.length);
|
|
30190
|
+
const useTabs = targetIndent.includes(" ") || newIndent.includes(" ");
|
|
30191
|
+
const maxAllowedDiff = useTabs ? 1 : 4;
|
|
30192
|
+
if (indentDiff > maxAllowedDiff) {
|
|
30193
|
+
return { result: newStr, modifications };
|
|
30194
|
+
}
|
|
30161
30195
|
const reindented = reindent(newStr, targetIndent);
|
|
30162
30196
|
if (reindented !== newStr) {
|
|
30163
30197
|
modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);
|
|
@@ -40531,7 +40565,12 @@ function generateSandboxGlobals(options) {
|
|
|
40531
40565
|
}
|
|
40532
40566
|
return tryParseJSONValue(text);
|
|
40533
40567
|
};
|
|
40534
|
-
|
|
40568
|
+
const tracedFn = traceToolCall(name15, rawMcpFn, tracer, logFn);
|
|
40569
|
+
globals[name15] = tracedFn;
|
|
40570
|
+
const sanitized = name15.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
40571
|
+
if (sanitized !== name15) {
|
|
40572
|
+
globals[sanitized] = tracedFn;
|
|
40573
|
+
}
|
|
40535
40574
|
}
|
|
40536
40575
|
}
|
|
40537
40576
|
if (llmCall) {
|
|
@@ -40886,9 +40925,17 @@ ${validation.errors.join("\n")}`,
|
|
|
40886
40925
|
"dsl.duration_ms": elapsed,
|
|
40887
40926
|
"dsl.error": e.message?.substring(0, 500)
|
|
40888
40927
|
});
|
|
40928
|
+
let errorMsg = `Execution failed: ${e.message}`;
|
|
40929
|
+
if (e.message && e.message.includes("is not defined")) {
|
|
40930
|
+
const globalNames = Object.keys(toolGlobals).sort();
|
|
40931
|
+
errorMsg += `
|
|
40932
|
+
Available functions: ${globalNames.join(", ")}`;
|
|
40933
|
+
errorMsg += `
|
|
40934
|
+
Note: Tools with hyphens (e.g. "my-tool") are available with underscores: my_tool()`;
|
|
40935
|
+
}
|
|
40889
40936
|
return {
|
|
40890
40937
|
status: "error",
|
|
40891
|
-
error:
|
|
40938
|
+
error: errorMsg,
|
|
40892
40939
|
logs
|
|
40893
40940
|
};
|
|
40894
40941
|
}
|
package/cjs/index.cjs
CHANGED
|
@@ -92219,7 +92219,12 @@ function generateSandboxGlobals(options) {
|
|
|
92219
92219
|
}
|
|
92220
92220
|
return tryParseJSONValue(text);
|
|
92221
92221
|
};
|
|
92222
|
-
|
|
92222
|
+
const tracedFn = traceToolCall(name15, rawMcpFn, tracer, logFn);
|
|
92223
|
+
globals[name15] = tracedFn;
|
|
92224
|
+
const sanitized = name15.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
92225
|
+
if (sanitized !== name15) {
|
|
92226
|
+
globals[sanitized] = tracedFn;
|
|
92227
|
+
}
|
|
92223
92228
|
}
|
|
92224
92229
|
}
|
|
92225
92230
|
if (llmCall) {
|
|
@@ -92574,9 +92579,17 @@ ${validation.errors.join("\n")}`,
|
|
|
92574
92579
|
"dsl.duration_ms": elapsed,
|
|
92575
92580
|
"dsl.error": e.message?.substring(0, 500)
|
|
92576
92581
|
});
|
|
92582
|
+
let errorMsg = `Execution failed: ${e.message}`;
|
|
92583
|
+
if (e.message && e.message.includes("is not defined")) {
|
|
92584
|
+
const globalNames = Object.keys(toolGlobals).sort();
|
|
92585
|
+
errorMsg += `
|
|
92586
|
+
Available functions: ${globalNames.join(", ")}`;
|
|
92587
|
+
errorMsg += `
|
|
92588
|
+
Note: Tools with hyphens (e.g. "my-tool") are available with underscores: my_tool()`;
|
|
92589
|
+
}
|
|
92577
92590
|
return {
|
|
92578
92591
|
status: "error",
|
|
92579
|
-
error:
|
|
92592
|
+
error: errorMsg,
|
|
92580
92593
|
logs
|
|
92581
92594
|
};
|
|
92582
92595
|
}
|
|
@@ -101893,7 +101906,14 @@ function lineTrimmedMatch(contentLines, searchLines) {
|
|
|
101893
101906
|
}
|
|
101894
101907
|
}
|
|
101895
101908
|
if (allMatch) {
|
|
101896
|
-
const
|
|
101909
|
+
const windowLines = contentLines.slice(i, i + windowSize);
|
|
101910
|
+
const windowMinIndent = getMinIndent(windowLines);
|
|
101911
|
+
const searchMinIndent = getMinIndent(searchLines);
|
|
101912
|
+
const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
|
|
101913
|
+
if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
|
|
101914
|
+
continue;
|
|
101915
|
+
}
|
|
101916
|
+
const matchedText = windowLines.join("\n");
|
|
101897
101917
|
matches.push(matchedText);
|
|
101898
101918
|
}
|
|
101899
101919
|
}
|
|
@@ -101923,6 +101943,15 @@ function whitespaceNormalizedMatch(content, search2) {
|
|
|
101923
101943
|
actualEnd++;
|
|
101924
101944
|
}
|
|
101925
101945
|
const matchedText = content.substring(originalStart, actualEnd);
|
|
101946
|
+
const matchedLines = matchedText.split("\n");
|
|
101947
|
+
const searchLines = search2.split("\n");
|
|
101948
|
+
const matchMinIndent = getMinIndent(matchedLines);
|
|
101949
|
+
const searchMinIndent = getMinIndent(searchLines);
|
|
101950
|
+
const indentDiff = Math.abs(matchMinIndent - searchMinIndent);
|
|
101951
|
+
if (isIndentDiffTooLarge(matchedLines, searchLines, indentDiff)) {
|
|
101952
|
+
searchStart = idx + 1;
|
|
101953
|
+
continue;
|
|
101954
|
+
}
|
|
101926
101955
|
matches.push(matchedText);
|
|
101927
101956
|
searchStart = idx + 1;
|
|
101928
101957
|
}
|
|
@@ -101974,6 +102003,10 @@ function indentFlexibleMatch(contentLines, searchLines) {
|
|
|
101974
102003
|
}
|
|
101975
102004
|
}
|
|
101976
102005
|
if (allMatch) {
|
|
102006
|
+
const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
|
|
102007
|
+
if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
|
|
102008
|
+
continue;
|
|
102009
|
+
}
|
|
101977
102010
|
const matchedText = windowLines.join("\n");
|
|
101978
102011
|
matches.push(matchedText);
|
|
101979
102012
|
}
|
|
@@ -101984,6 +102017,14 @@ function indentFlexibleMatch(contentLines, searchLines) {
|
|
|
101984
102017
|
count: matches.length
|
|
101985
102018
|
};
|
|
101986
102019
|
}
|
|
102020
|
+
function isIndentDiffTooLarge(linesA, linesB, indentDiff) {
|
|
102021
|
+
if (indentDiff <= 0) return false;
|
|
102022
|
+
const sampleA = linesA.find((l) => l.trim().length > 0) || "";
|
|
102023
|
+
const sampleB = linesB.find((l) => l.trim().length > 0) || "";
|
|
102024
|
+
const useTabs = sampleA.startsWith(" ") || sampleB.startsWith(" ");
|
|
102025
|
+
const maxAllowedDiff = useTabs ? 1 : 4;
|
|
102026
|
+
return indentDiff > maxAllowedDiff;
|
|
102027
|
+
}
|
|
101987
102028
|
function getMinIndent(lines) {
|
|
101988
102029
|
let min = Infinity;
|
|
101989
102030
|
for (const line of lines) {
|
|
@@ -102058,6 +102099,12 @@ function restoreIndentation(newStr, originalLines) {
|
|
|
102058
102099
|
const targetIndent = detectBaseIndent(originalCode);
|
|
102059
102100
|
const newIndent = detectBaseIndent(newStr);
|
|
102060
102101
|
if (targetIndent !== newIndent) {
|
|
102102
|
+
const indentDiff = Math.abs(targetIndent.length - newIndent.length);
|
|
102103
|
+
const useTabs = targetIndent.includes(" ") || newIndent.includes(" ");
|
|
102104
|
+
const maxAllowedDiff = useTabs ? 1 : 4;
|
|
102105
|
+
if (indentDiff > maxAllowedDiff) {
|
|
102106
|
+
return { result: newStr, modifications };
|
|
102107
|
+
}
|
|
102061
102108
|
const reindented = reindent(newStr, targetIndent);
|
|
102062
102109
|
if (reindented !== newStr) {
|
|
102063
102110
|
modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);
|
package/package.json
CHANGED
|
@@ -234,7 +234,14 @@ export function generateSandboxGlobals(options) {
|
|
|
234
234
|
}
|
|
235
235
|
return tryParseJSONValue(text);
|
|
236
236
|
};
|
|
237
|
-
|
|
237
|
+
const tracedFn = traceToolCall(name, rawMcpFn, tracer, logFn);
|
|
238
|
+
globals[name] = tracedFn;
|
|
239
|
+
// Register sanitized alias for names with hyphens/dots/etc that aren't valid JS identifiers
|
|
240
|
+
// e.g. "workable-api" → also available as "workable_api"
|
|
241
|
+
const sanitized = name.replace(/[^a-zA-Z0-9_$]/g, '_');
|
|
242
|
+
if (sanitized !== name) {
|
|
243
|
+
globals[sanitized] = tracedFn;
|
|
244
|
+
}
|
|
238
245
|
}
|
|
239
246
|
}
|
|
240
247
|
|
package/src/agent/dsl/runtime.js
CHANGED
|
@@ -181,9 +181,17 @@ export function createDSLRuntime(options) {
|
|
|
181
181
|
'dsl.error': e.message?.substring(0, 500),
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
+
// Enrich "X is not defined" errors with available tool names
|
|
185
|
+
let errorMsg = `Execution failed: ${e.message}`;
|
|
186
|
+
if (e.message && e.message.includes('is not defined')) {
|
|
187
|
+
const globalNames = Object.keys(toolGlobals).sort();
|
|
188
|
+
errorMsg += `\nAvailable functions: ${globalNames.join(', ')}`;
|
|
189
|
+
errorMsg += `\nNote: Tools with hyphens (e.g. "my-tool") are available with underscores: my_tool()`;
|
|
190
|
+
}
|
|
191
|
+
|
|
184
192
|
return {
|
|
185
193
|
status: 'error',
|
|
186
|
-
error:
|
|
194
|
+
error: errorMsg,
|
|
187
195
|
logs,
|
|
188
196
|
};
|
|
189
197
|
}
|
package/src/tools/executePlan.js
CHANGED
|
@@ -778,6 +778,7 @@ return table;
|
|
|
778
778
|
- Do NOT define helper functions that call tools. Write all logic inline or use for..of loops.
|
|
779
779
|
- Do NOT use regex literals (/pattern/) — use String methods like indexOf, includes, startsWith instead.
|
|
780
780
|
- ONLY use functions listed below. Do NOT call functions that are not listed.
|
|
781
|
+
- MCP tools with hyphens in their names (e.g. \`workable-api\`) are available using underscores: \`workable_api()\`. Hyphens are not valid in JS identifiers.
|
|
781
782
|
|
|
782
783
|
### Available functions
|
|
783
784
|
|
package/src/tools/fuzzyMatch.js
CHANGED
|
@@ -73,7 +73,17 @@ export function lineTrimmedMatch(contentLines, searchLines) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
if (allMatch) {
|
|
76
|
-
|
|
76
|
+
// Limit indent tolerance: even though trimmed content matches, reject when
|
|
77
|
+
// the indentation level difference is too large — it likely means the match
|
|
78
|
+
// is in a completely different scope (issue #507).
|
|
79
|
+
const windowLines = contentLines.slice(i, i + windowSize);
|
|
80
|
+
const windowMinIndent = getMinIndent(windowLines);
|
|
81
|
+
const searchMinIndent = getMinIndent(searchLines);
|
|
82
|
+
const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
|
|
83
|
+
if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
|
|
84
|
+
continue; // Skip — too far off in nesting
|
|
85
|
+
}
|
|
86
|
+
const matchedText = windowLines.join('\n');
|
|
77
87
|
matches.push(matchedText);
|
|
78
88
|
}
|
|
79
89
|
}
|
|
@@ -134,6 +144,19 @@ export function whitespaceNormalizedMatch(content, search) {
|
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
const matchedText = content.substring(originalStart, actualEnd);
|
|
147
|
+
|
|
148
|
+
// Limit indent tolerance: reject matches where the indentation level
|
|
149
|
+
// difference is too large — likely a wrong-scope match (issue #507).
|
|
150
|
+
const matchedLines = matchedText.split('\n');
|
|
151
|
+
const searchLines = search.split('\n');
|
|
152
|
+
const matchMinIndent = getMinIndent(matchedLines);
|
|
153
|
+
const searchMinIndent = getMinIndent(searchLines);
|
|
154
|
+
const indentDiff = Math.abs(matchMinIndent - searchMinIndent);
|
|
155
|
+
if (isIndentDiffTooLarge(matchedLines, searchLines, indentDiff)) {
|
|
156
|
+
searchStart = idx + 1;
|
|
157
|
+
continue; // Skip — too far off in nesting
|
|
158
|
+
}
|
|
159
|
+
|
|
137
160
|
matches.push(matchedText);
|
|
138
161
|
|
|
139
162
|
searchStart = idx + 1;
|
|
@@ -219,6 +242,15 @@ export function indentFlexibleMatch(contentLines, searchLines) {
|
|
|
219
242
|
}
|
|
220
243
|
|
|
221
244
|
if (allMatch) {
|
|
245
|
+
// Limit indent tolerance: reject matches where indentation differs by more than
|
|
246
|
+
// 1 level. Larger differences likely mean the match is in a completely different
|
|
247
|
+
// scope/nesting level — silent file corruption risk (issue #507).
|
|
248
|
+
// For tabs: 1 tab = 1 level, so max diff = 1.
|
|
249
|
+
// For spaces: detect indent unit (2 or 4), allow 1 unit of difference.
|
|
250
|
+
const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
|
|
251
|
+
if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
|
|
252
|
+
continue; // Skip — too far off in nesting
|
|
253
|
+
}
|
|
222
254
|
const matchedText = windowLines.join('\n');
|
|
223
255
|
matches.push(matchedText);
|
|
224
256
|
}
|
|
@@ -232,6 +264,25 @@ export function indentFlexibleMatch(contentLines, searchLines) {
|
|
|
232
264
|
};
|
|
233
265
|
}
|
|
234
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Check if an indentation difference exceeds the allowed limit.
|
|
269
|
+
* Uses tab-aware threshold: 1 for tabs, 4 for spaces.
|
|
270
|
+
* Checks BOTH sides for tab usage to avoid asymmetric detection.
|
|
271
|
+
*
|
|
272
|
+
* @param {string[]} linesA - First set of lines
|
|
273
|
+
* @param {string[]} linesB - Second set of lines
|
|
274
|
+
* @param {number} indentDiff - Absolute difference in min indent
|
|
275
|
+
* @returns {boolean} true if the diff exceeds the limit
|
|
276
|
+
*/
|
|
277
|
+
function isIndentDiffTooLarge(linesA, linesB, indentDiff) {
|
|
278
|
+
if (indentDiff <= 0) return false;
|
|
279
|
+
const sampleA = linesA.find(l => l.trim().length > 0) || '';
|
|
280
|
+
const sampleB = linesB.find(l => l.trim().length > 0) || '';
|
|
281
|
+
const useTabs = sampleA.startsWith('\t') || sampleB.startsWith('\t');
|
|
282
|
+
const maxAllowedDiff = useTabs ? 1 : 4;
|
|
283
|
+
return indentDiff > maxAllowedDiff;
|
|
284
|
+
}
|
|
285
|
+
|
|
235
286
|
/**
|
|
236
287
|
* Get the minimum indentation level (number of leading whitespace characters)
|
|
237
288
|
* across all non-empty lines.
|
|
@@ -92,6 +92,17 @@ export function restoreIndentation(newStr, originalLines) {
|
|
|
92
92
|
const newIndent = detectBaseIndent(newStr);
|
|
93
93
|
|
|
94
94
|
if (targetIndent !== newIndent) {
|
|
95
|
+
// Limit auto-reindent tolerance: reject when indentation differs by more than
|
|
96
|
+
// 1 level. Larger differences likely mean the match landed in a completely
|
|
97
|
+
// different scope — allowing it risks silent file corruption (issue #507).
|
|
98
|
+
// For tabs: 1 tab = 1 level, so max diff = 1 char.
|
|
99
|
+
// For spaces: 1 level = up to 4 spaces, so max diff = 4 chars.
|
|
100
|
+
const indentDiff = Math.abs(targetIndent.length - newIndent.length);
|
|
101
|
+
const useTabs = targetIndent.includes('\t') || newIndent.includes('\t');
|
|
102
|
+
const maxAllowedDiff = useTabs ? 1 : 4;
|
|
103
|
+
if (indentDiff > maxAllowedDiff) {
|
|
104
|
+
return { result: newStr, modifications };
|
|
105
|
+
}
|
|
95
106
|
const reindented = reindent(newStr, targetIndent);
|
|
96
107
|
if (reindented !== newStr) {
|
|
97
108
|
modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);
|