@probelabs/probe 0.6.0-rc290 → 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.
Files changed (26) hide show
  1. package/bin/binaries/{probe-v0.6.0-rc290-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc292-aarch64-apple-darwin.tar.gz} +0 -0
  2. package/bin/binaries/{probe-v0.6.0-rc290-aarch64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc292-aarch64-unknown-linux-musl.tar.gz} +0 -0
  3. package/bin/binaries/{probe-v0.6.0-rc290-x86_64-apple-darwin.tar.gz → probe-v0.6.0-rc292-x86_64-apple-darwin.tar.gz} +0 -0
  4. package/bin/binaries/{probe-v0.6.0-rc290-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc292-x86_64-pc-windows-msvc.zip} +0 -0
  5. package/bin/binaries/{probe-v0.6.0-rc290-x86_64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc292-x86_64-unknown-linux-musl.tar.gz} +0 -0
  6. package/build/agent/ProbeAgent.js +34 -4
  7. package/build/agent/dsl/environment.js +8 -1
  8. package/build/agent/dsl/runtime.js +9 -1
  9. package/build/tools/executePlan.js +1 -0
  10. package/build/tools/fuzzyMatch.js +52 -1
  11. package/build/tools/lineEditHeuristics.js +11 -0
  12. package/build/tools/vercel.js +5 -1
  13. package/build/utils/error-types.js +2 -2
  14. package/build/utils/path-validation.js +1 -1
  15. package/cjs/agent/ProbeAgent.cjs +112 -10
  16. package/cjs/index.cjs +112 -10
  17. package/package.json +1 -1
  18. package/src/agent/ProbeAgent.js +34 -4
  19. package/src/agent/dsl/environment.js +8 -1
  20. package/src/agent/dsl/runtime.js +9 -1
  21. package/src/tools/executePlan.js +1 -0
  22. package/src/tools/fuzzyMatch.js +52 -1
  23. package/src/tools/lineEditHeuristics.js +11 -0
  24. package/src/tools/vercel.js +5 -1
  25. package/src/utils/error-types.js +2 -2
  26. package/src/utils/path-validation.js +1 -1
@@ -1444,11 +1444,13 @@ export class ProbeAgent {
1444
1444
  result = await this._executeWithVercelProvider(options, controller);
1445
1445
  }
1446
1446
 
1447
- // Wrap textStream so limiter slot is held until stream completes
1447
+ // Wrap textStream so limiter slot is held until stream completes.
1448
+ // result.textStream is a read-only getter on DefaultStreamTextResult,
1449
+ // so we wrap the result in a Proxy that intercepts the textStream property.
1448
1450
  if (limiter && result.textStream) {
1449
1451
  const originalStream = result.textStream;
1450
1452
  const debug = this.debug;
1451
- result.textStream = (async function* () {
1453
+ const wrappedStream = (async function* () {
1452
1454
  try {
1453
1455
  for await (const chunk of originalStream) {
1454
1456
  yield chunk;
@@ -1461,6 +1463,13 @@ export class ProbeAgent {
1461
1463
  }
1462
1464
  }
1463
1465
  })();
1466
+ return new Proxy(result, {
1467
+ get(target, prop) {
1468
+ if (prop === 'textStream') return wrappedStream;
1469
+ const value = target[prop];
1470
+ return typeof value === 'function' ? value.bind(target) : value;
1471
+ }
1472
+ });
1464
1473
  } else if (limiter) {
1465
1474
  // No textStream (shouldn't happen, but release just in case)
1466
1475
  limiter.release(null);
@@ -3499,6 +3508,7 @@ Follow these instructions carefully:
3499
3508
  return true;
3500
3509
  }
3501
3510
  }
3511
+
3502
3512
  }
3503
3513
 
3504
3514
  return false;
@@ -3529,6 +3539,24 @@ Follow these instructions carefully:
3529
3539
  }
3530
3540
  }
3531
3541
 
3542
+ // Force text-only response after 3 consecutive tool errors
3543
+ // (e.g. workspace deleted mid-run — let the model produce its answer)
3544
+ if (steps.length >= 3) {
3545
+ const last3 = steps.slice(-3);
3546
+ const allErrors = last3.every(s =>
3547
+ s.toolResults?.length > 0 && s.toolResults.every(tr => {
3548
+ const r = typeof tr.result === 'string' ? tr.result : '';
3549
+ return r.includes('<error ') || r.includes('does not exist');
3550
+ })
3551
+ );
3552
+ if (allErrors) {
3553
+ if (this.debug) {
3554
+ console.log(`[DEBUG] prepareStep: 3 consecutive tool errors, forcing toolChoice=none`);
3555
+ }
3556
+ return { toolChoice: 'none' };
3557
+ }
3558
+ }
3559
+
3532
3560
  const lastStep = steps[steps.length - 1];
3533
3561
  const modelJustStopped = lastStep?.finishReason === 'stop'
3534
3562
  && (!lastStep?.toolCalls || lastStep.toolCalls.length === 0);
@@ -3565,7 +3593,8 @@ Here is the result to review:
3565
3593
  ${resultToReview}
3566
3594
  </result>
3567
3595
 
3568
- Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If something needs to be fixed or is missing, do it now, then respond with the COMPLETE updated answer (everything you did in total, not just the fix).`;
3596
+ IMPORTANT: First review ALL completed work in the conversation above before taking any action.
3597
+ Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If your text has inaccuracies, fix the text. Only call a tool if you find a genuinely MISSING action — NEVER redo work that was already completed successfully. Respond with the COMPLETE corrected answer.`;
3569
3598
 
3570
3599
  return {
3571
3600
  userMessage: completionPromptMessage,
@@ -3774,7 +3803,8 @@ Here is the result to review:
3774
3803
  ${finalResult}
3775
3804
  </result>
3776
3805
 
3777
- Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If something needs to be fixed or is missing, do it now, then respond with the COMPLETE updated answer (everything you did in total, not just the fix).`;
3806
+ IMPORTANT: First review ALL completed work in the conversation above before taking any action.
3807
+ Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If your text has inaccuracies, fix the text. Only call a tool if you find a genuinely MISSING action — NEVER redo work that was already completed successfully. Respond with the COMPLETE corrected answer.`;
3778
3808
 
3779
3809
  currentMessages.push({ role: 'user', content: completionPromptMessage });
3780
3810
 
@@ -234,7 +234,14 @@ export function generateSandboxGlobals(options) {
234
234
  }
235
235
  return tryParseJSONValue(text);
236
236
  };
237
- globals[name] = traceToolCall(name, rawMcpFn, tracer, logFn);
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: `Execution failed: ${e.message}`,
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
- const matchedText = contentLines.slice(i, i + windowSize).join('\n');
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}"`);
@@ -478,7 +478,11 @@ export const searchTool = (options = {}) => {
478
478
  return result;
479
479
  } catch (error) {
480
480
  console.error('Error executing search command:', error);
481
- return formatErrorForAI(error);
481
+ const formatted = formatErrorForAI(error);
482
+ if (error.category === 'path_error' || error.message?.includes('does not exist')) {
483
+ return formatted + '\n\nThe path does not exist. Use the listFiles tool to verify the correct directory structure before retrying. If the workspace itself is gone, output your final answer with whatever information you have.';
484
+ }
485
+ return formatted;
482
486
  }
483
487
  }
484
488
 
@@ -181,14 +181,14 @@ export function categorizeError(error) {
181
181
  errorCode === 'enoent') {
182
182
  return new PathError(message, {
183
183
  originalError: error,
184
- suggestion: 'The specified path does not exist. Please verify the path or use a different directory.'
184
+ suggestion: 'The specified path does not exist. Use the listFiles tool to check the correct directory structure, then retry with a valid path.'
185
185
  });
186
186
  }
187
187
 
188
188
  if (lowerMessage.includes('not a directory') || errorCode === 'enotdir') {
189
189
  return new PathError(message, {
190
190
  originalError: error,
191
- suggestion: 'The path is not a directory. Please provide a valid directory path.'
191
+ suggestion: 'The path is not a directory. Use the listFiles tool to find the correct directory, then retry.'
192
192
  });
193
193
  }
194
194
 
@@ -110,7 +110,7 @@ export async function validateCwdPath(inputPath, defaultPath = process.cwd()) {
110
110
  }
111
111
  if (error.code === 'ENOENT') {
112
112
  throw new PathError(`Path does not exist: ${normalizedPath}`, {
113
- suggestion: 'The specified path does not exist. Please verify the path is correct or use a different directory.',
113
+ suggestion: 'The specified path does not exist. Use the listFiles tool to check the correct directory structure, then retry with a valid path.',
114
114
  details: { path: normalizedPath }
115
115
  });
116
116
  }
@@ -25324,13 +25324,13 @@ function categorizeError(error40) {
25324
25324
  if (lowerMessage.includes("path does not exist") || lowerMessage.includes("no such file or directory") || errorCode === "enoent") {
25325
25325
  return new PathError(message, {
25326
25326
  originalError: error40,
25327
- suggestion: "The specified path does not exist. Please verify the path or use a different directory."
25327
+ suggestion: "The specified path does not exist. Use the listFiles tool to check the correct directory structure, then retry with a valid path."
25328
25328
  });
25329
25329
  }
25330
25330
  if (lowerMessage.includes("not a directory") || errorCode === "enotdir") {
25331
25331
  return new PathError(message, {
25332
25332
  originalError: error40,
25333
- suggestion: "The path is not a directory. Please provide a valid directory path."
25333
+ suggestion: "The path is not a directory. Use the listFiles tool to find the correct directory, then retry."
25334
25334
  });
25335
25335
  }
25336
25336
  if (lowerMessage.includes("permission denied") || errorCode === "eacces") {
@@ -25586,7 +25586,7 @@ async function validateCwdPath(inputPath, defaultPath = process.cwd()) {
25586
25586
  }
25587
25587
  if (error40.code === "ENOENT") {
25588
25588
  throw new PathError(`Path does not exist: ${normalizedPath}`, {
25589
- suggestion: "The specified path does not exist. Please verify the path is correct or use a different directory.",
25589
+ suggestion: "The specified path does not exist. Use the listFiles tool to check the correct directory structure, then retry with a valid path.",
25590
25590
  details: { path: normalizedPath }
25591
25591
  });
25592
25592
  }
@@ -27643,7 +27643,11 @@ var init_vercel = __esm({
27643
27643
  return result;
27644
27644
  } catch (error40) {
27645
27645
  console.error("Error executing search command:", error40);
27646
- return formatErrorForAI(error40);
27646
+ const formatted = formatErrorForAI(error40);
27647
+ if (error40.category === "path_error" || error40.message?.includes("does not exist")) {
27648
+ return formatted + "\n\nThe path does not exist. Use the listFiles tool to verify the correct directory structure before retrying. If the workspace itself is gone, output your final answer with whatever information you have.";
27649
+ }
27650
+ return formatted;
27647
27651
  }
27648
27652
  }
27649
27653
  try {
@@ -29904,7 +29908,14 @@ function lineTrimmedMatch(contentLines, searchLines) {
29904
29908
  }
29905
29909
  }
29906
29910
  if (allMatch) {
29907
- const matchedText = contentLines.slice(i, i + windowSize).join("\n");
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");
29908
29919
  matches.push(matchedText);
29909
29920
  }
29910
29921
  }
@@ -29934,6 +29945,15 @@ function whitespaceNormalizedMatch(content, search2) {
29934
29945
  actualEnd++;
29935
29946
  }
29936
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
+ }
29937
29957
  matches.push(matchedText);
29938
29958
  searchStart = idx + 1;
29939
29959
  }
@@ -29985,6 +30005,10 @@ function indentFlexibleMatch(contentLines, searchLines) {
29985
30005
  }
29986
30006
  }
29987
30007
  if (allMatch) {
30008
+ const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
30009
+ if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
30010
+ continue;
30011
+ }
29988
30012
  const matchedText = windowLines.join("\n");
29989
30013
  matches.push(matchedText);
29990
30014
  }
@@ -29995,6 +30019,14 @@ function indentFlexibleMatch(contentLines, searchLines) {
29995
30019
  count: matches.length
29996
30020
  };
29997
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
+ }
29998
30030
  function getMinIndent(lines) {
29999
30031
  let min = Infinity;
30000
30032
  for (const line of lines) {
@@ -30154,6 +30186,12 @@ function restoreIndentation(newStr, originalLines) {
30154
30186
  const targetIndent = detectBaseIndent(originalCode);
30155
30187
  const newIndent = detectBaseIndent(newStr);
30156
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
+ }
30157
30195
  const reindented = reindent(newStr, targetIndent);
30158
30196
  if (reindented !== newStr) {
30159
30197
  modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);
@@ -31008,6 +31046,9 @@ var require_utils = __commonJS({
31008
31046
  sandboxGlobal
31009
31047
  };
31010
31048
  context.prototypeWhitelist.set(Object.getPrototypeOf([][Symbol.iterator]()), /* @__PURE__ */ new Set());
31049
+ context.prototypeWhitelist.set(Object.getPrototypeOf(""[Symbol.iterator]()), /* @__PURE__ */ new Set());
31050
+ context.prototypeWhitelist.set(Object.getPrototypeOf((/* @__PURE__ */ new Set())[Symbol.iterator]()), /* @__PURE__ */ new Set());
31051
+ context.prototypeWhitelist.set(Object.getPrototypeOf((/* @__PURE__ */ new Map())[Symbol.iterator]()), /* @__PURE__ */ new Set());
31011
31052
  return context;
31012
31053
  }
31013
31054
  function createExecContext(sandbox, executionTree, evalContext) {
@@ -32146,6 +32187,18 @@ var require_executor = __commonJS({
32146
32187
  a = void 0;
32147
32188
  }
32148
32189
  }
32190
+ if (op === 29 && !a) {
32191
+ done(void 0, a);
32192
+ return;
32193
+ }
32194
+ if (op === 30 && a) {
32195
+ done(void 0, a);
32196
+ return;
32197
+ }
32198
+ if (op === 85 && a !== null && a !== void 0) {
32199
+ done(void 0, a);
32200
+ return;
32201
+ }
32149
32202
  let bobj;
32150
32203
  try {
32151
32204
  let ad;
@@ -32208,6 +32261,18 @@ var require_executor = __commonJS({
32208
32261
  a = void 0;
32209
32262
  }
32210
32263
  }
32264
+ if (op === 29 && !a) {
32265
+ done(void 0, a);
32266
+ return;
32267
+ }
32268
+ if (op === 30 && a) {
32269
+ done(void 0, a);
32270
+ return;
32271
+ }
32272
+ if (op === 85 && a !== null && a !== void 0) {
32273
+ done(void 0, a);
32274
+ return;
32275
+ }
32211
32276
  let bobj;
32212
32277
  try {
32213
32278
  bobj = syncDone((d) => execSync(ticks, tree[2], scope, context, d, inLoopOrSwitch)).result;
@@ -40500,7 +40565,12 @@ function generateSandboxGlobals(options) {
40500
40565
  }
40501
40566
  return tryParseJSONValue(text);
40502
40567
  };
40503
- globals[name15] = traceToolCall(name15, rawMcpFn, tracer, logFn);
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
+ }
40504
40574
  }
40505
40575
  }
40506
40576
  if (llmCall) {
@@ -40855,9 +40925,17 @@ ${validation.errors.join("\n")}`,
40855
40925
  "dsl.duration_ms": elapsed,
40856
40926
  "dsl.error": e.message?.substring(0, 500)
40857
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
+ }
40858
40936
  return {
40859
40937
  status: "error",
40860
- error: `Execution failed: ${e.message}`,
40938
+ error: errorMsg,
40861
40939
  logs
40862
40940
  };
40863
40941
  }
@@ -100588,7 +100666,7 @@ var init_ProbeAgent = __esm({
100588
100666
  if (limiter && result.textStream) {
100589
100667
  const originalStream = result.textStream;
100590
100668
  const debug = this.debug;
100591
- result.textStream = (async function* () {
100669
+ const wrappedStream = (async function* () {
100592
100670
  try {
100593
100671
  for await (const chunk of originalStream) {
100594
100672
  yield chunk;
@@ -100601,6 +100679,13 @@ var init_ProbeAgent = __esm({
100601
100679
  }
100602
100680
  }
100603
100681
  })();
100682
+ return new Proxy(result, {
100683
+ get(target, prop) {
100684
+ if (prop === "textStream") return wrappedStream;
100685
+ const value = target[prop];
100686
+ return typeof value === "function" ? value.bind(target) : value;
100687
+ }
100688
+ });
100604
100689
  } else if (limiter) {
100605
100690
  limiter.release(null);
100606
100691
  }
@@ -102254,6 +102339,21 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102254
102339
  }
102255
102340
  }
102256
102341
  }
102342
+ if (steps.length >= 3) {
102343
+ const last3 = steps.slice(-3);
102344
+ const allErrors = last3.every(
102345
+ (s) => s.toolResults?.length > 0 && s.toolResults.every((tr) => {
102346
+ const r = typeof tr.result === "string" ? tr.result : "";
102347
+ return r.includes("<error ") || r.includes("does not exist");
102348
+ })
102349
+ );
102350
+ if (allErrors) {
102351
+ if (this.debug) {
102352
+ console.log(`[DEBUG] prepareStep: 3 consecutive tool errors, forcing toolChoice=none`);
102353
+ }
102354
+ return { toolChoice: "none" };
102355
+ }
102356
+ }
102257
102357
  const lastStep = steps[steps.length - 1];
102258
102358
  const modelJustStopped = lastStep?.finishReason === "stop" && (!lastStep?.toolCalls || lastStep.toolCalls.length === 0);
102259
102359
  if (modelJustStopped) {
@@ -102282,7 +102382,8 @@ Here is the result to review:
102282
102382
  ${resultToReview}
102283
102383
  </result>
102284
102384
 
102285
- Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If something needs to be fixed or is missing, do it now, then respond with the COMPLETE updated answer (everything you did in total, not just the fix).`;
102385
+ IMPORTANT: First review ALL completed work in the conversation above before taking any action.
102386
+ Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If your text has inaccuracies, fix the text. Only call a tool if you find a genuinely MISSING action \u2014 NEVER redo work that was already completed successfully. Respond with the COMPLETE corrected answer.`;
102286
102387
  return {
102287
102388
  userMessage: completionPromptMessage,
102288
102389
  toolChoice: "none"
@@ -102441,7 +102542,8 @@ Here is the result to review:
102441
102542
  ${finalResult}
102442
102543
  </result>
102443
102544
 
102444
- Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If something needs to be fixed or is missing, do it now, then respond with the COMPLETE updated answer (everything you did in total, not just the fix).`;
102545
+ IMPORTANT: First review ALL completed work in the conversation above before taking any action.
102546
+ Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If your text has inaccuracies, fix the text. Only call a tool if you find a genuinely MISSING action \u2014 NEVER redo work that was already completed successfully. Respond with the COMPLETE corrected answer.`;
102445
102547
  currentMessages.push({ role: "user", content: completionPromptMessage });
102446
102548
  const completionStreamOptions = {
102447
102549
  model: this.provider ? this.provider(this.model) : this.model,
package/cjs/index.cjs CHANGED
@@ -1447,13 +1447,13 @@ function categorizeError(error40) {
1447
1447
  if (lowerMessage.includes("path does not exist") || lowerMessage.includes("no such file or directory") || errorCode === "enoent") {
1448
1448
  return new PathError(message, {
1449
1449
  originalError: error40,
1450
- suggestion: "The specified path does not exist. Please verify the path or use a different directory."
1450
+ suggestion: "The specified path does not exist. Use the listFiles tool to check the correct directory structure, then retry with a valid path."
1451
1451
  });
1452
1452
  }
1453
1453
  if (lowerMessage.includes("not a directory") || errorCode === "enotdir") {
1454
1454
  return new PathError(message, {
1455
1455
  originalError: error40,
1456
- suggestion: "The path is not a directory. Please provide a valid directory path."
1456
+ suggestion: "The path is not a directory. Use the listFiles tool to find the correct directory, then retry."
1457
1457
  });
1458
1458
  }
1459
1459
  if (lowerMessage.includes("permission denied") || errorCode === "eacces") {
@@ -1709,7 +1709,7 @@ async function validateCwdPath(inputPath, defaultPath = process.cwd()) {
1709
1709
  }
1710
1710
  if (error40.code === "ENOENT") {
1711
1711
  throw new PathError(`Path does not exist: ${normalizedPath}`, {
1712
- suggestion: "The specified path does not exist. Please verify the path is correct or use a different directory.",
1712
+ suggestion: "The specified path does not exist. Use the listFiles tool to check the correct directory structure, then retry with a valid path.",
1713
1713
  details: { path: normalizedPath }
1714
1714
  });
1715
1715
  }
@@ -82700,6 +82700,9 @@ var require_utils2 = __commonJS({
82700
82700
  sandboxGlobal
82701
82701
  };
82702
82702
  context.prototypeWhitelist.set(Object.getPrototypeOf([][Symbol.iterator]()), /* @__PURE__ */ new Set());
82703
+ context.prototypeWhitelist.set(Object.getPrototypeOf(""[Symbol.iterator]()), /* @__PURE__ */ new Set());
82704
+ context.prototypeWhitelist.set(Object.getPrototypeOf((/* @__PURE__ */ new Set())[Symbol.iterator]()), /* @__PURE__ */ new Set());
82705
+ context.prototypeWhitelist.set(Object.getPrototypeOf((/* @__PURE__ */ new Map())[Symbol.iterator]()), /* @__PURE__ */ new Set());
82703
82706
  return context;
82704
82707
  }
82705
82708
  function createExecContext(sandbox, executionTree, evalContext) {
@@ -83838,6 +83841,18 @@ var require_executor = __commonJS({
83838
83841
  a = void 0;
83839
83842
  }
83840
83843
  }
83844
+ if (op === 29 && !a) {
83845
+ done(void 0, a);
83846
+ return;
83847
+ }
83848
+ if (op === 30 && a) {
83849
+ done(void 0, a);
83850
+ return;
83851
+ }
83852
+ if (op === 85 && a !== null && a !== void 0) {
83853
+ done(void 0, a);
83854
+ return;
83855
+ }
83841
83856
  let bobj;
83842
83857
  try {
83843
83858
  let ad;
@@ -83900,6 +83915,18 @@ var require_executor = __commonJS({
83900
83915
  a = void 0;
83901
83916
  }
83902
83917
  }
83918
+ if (op === 29 && !a) {
83919
+ done(void 0, a);
83920
+ return;
83921
+ }
83922
+ if (op === 30 && a) {
83923
+ done(void 0, a);
83924
+ return;
83925
+ }
83926
+ if (op === 85 && a !== null && a !== void 0) {
83927
+ done(void 0, a);
83928
+ return;
83929
+ }
83903
83930
  let bobj;
83904
83931
  try {
83905
83932
  bobj = syncDone((d) => execSync(ticks, tree[2], scope, context, d, inLoopOrSwitch)).result;
@@ -92192,7 +92219,12 @@ function generateSandboxGlobals(options) {
92192
92219
  }
92193
92220
  return tryParseJSONValue(text);
92194
92221
  };
92195
- globals[name15] = traceToolCall(name15, rawMcpFn, tracer, logFn);
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
+ }
92196
92228
  }
92197
92229
  }
92198
92230
  if (llmCall) {
@@ -92547,9 +92579,17 @@ ${validation.errors.join("\n")}`,
92547
92579
  "dsl.duration_ms": elapsed,
92548
92580
  "dsl.error": e.message?.substring(0, 500)
92549
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
+ }
92550
92590
  return {
92551
92591
  status: "error",
92552
- error: `Execution failed: ${e.message}`,
92592
+ error: errorMsg,
92553
92593
  logs
92554
92594
  };
92555
92595
  }
@@ -97538,7 +97578,7 @@ var init_ProbeAgent = __esm({
97538
97578
  if (limiter && result.textStream) {
97539
97579
  const originalStream = result.textStream;
97540
97580
  const debug = this.debug;
97541
- result.textStream = (async function* () {
97581
+ const wrappedStream = (async function* () {
97542
97582
  try {
97543
97583
  for await (const chunk of originalStream) {
97544
97584
  yield chunk;
@@ -97551,6 +97591,13 @@ var init_ProbeAgent = __esm({
97551
97591
  }
97552
97592
  }
97553
97593
  })();
97594
+ return new Proxy(result, {
97595
+ get(target, prop) {
97596
+ if (prop === "textStream") return wrappedStream;
97597
+ const value = target[prop];
97598
+ return typeof value === "function" ? value.bind(target) : value;
97599
+ }
97600
+ });
97554
97601
  } else if (limiter) {
97555
97602
  limiter.release(null);
97556
97603
  }
@@ -99204,6 +99251,21 @@ You are working with a workspace. Available paths: ${workspaceDesc}
99204
99251
  }
99205
99252
  }
99206
99253
  }
99254
+ if (steps.length >= 3) {
99255
+ const last3 = steps.slice(-3);
99256
+ const allErrors = last3.every(
99257
+ (s) => s.toolResults?.length > 0 && s.toolResults.every((tr) => {
99258
+ const r = typeof tr.result === "string" ? tr.result : "";
99259
+ return r.includes("<error ") || r.includes("does not exist");
99260
+ })
99261
+ );
99262
+ if (allErrors) {
99263
+ if (this.debug) {
99264
+ console.log(`[DEBUG] prepareStep: 3 consecutive tool errors, forcing toolChoice=none`);
99265
+ }
99266
+ return { toolChoice: "none" };
99267
+ }
99268
+ }
99207
99269
  const lastStep = steps[steps.length - 1];
99208
99270
  const modelJustStopped = lastStep?.finishReason === "stop" && (!lastStep?.toolCalls || lastStep.toolCalls.length === 0);
99209
99271
  if (modelJustStopped) {
@@ -99232,7 +99294,8 @@ Here is the result to review:
99232
99294
  ${resultToReview}
99233
99295
  </result>
99234
99296
 
99235
- Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If something needs to be fixed or is missing, do it now, then respond with the COMPLETE updated answer (everything you did in total, not just the fix).`;
99297
+ IMPORTANT: First review ALL completed work in the conversation above before taking any action.
99298
+ Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If your text has inaccuracies, fix the text. Only call a tool if you find a genuinely MISSING action \u2014 NEVER redo work that was already completed successfully. Respond with the COMPLETE corrected answer.`;
99236
99299
  return {
99237
99300
  userMessage: completionPromptMessage,
99238
99301
  toolChoice: "none"
@@ -99391,7 +99454,8 @@ Here is the result to review:
99391
99454
  ${finalResult}
99392
99455
  </result>
99393
99456
 
99394
- Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If something needs to be fixed or is missing, do it now, then respond with the COMPLETE updated answer (everything you did in total, not just the fix).`;
99457
+ IMPORTANT: First review ALL completed work in the conversation above before taking any action.
99458
+ Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If your text has inaccuracies, fix the text. Only call a tool if you find a genuinely MISSING action \u2014 NEVER redo work that was already completed successfully. Respond with the COMPLETE corrected answer.`;
99395
99459
  currentMessages.push({ role: "user", content: completionPromptMessage });
99396
99460
  const completionStreamOptions = {
99397
99461
  model: this.provider ? this.provider(this.model) : this.model,
@@ -101417,7 +101481,11 @@ var init_vercel = __esm({
101417
101481
  return result;
101418
101482
  } catch (error40) {
101419
101483
  console.error("Error executing search command:", error40);
101420
- return formatErrorForAI(error40);
101484
+ const formatted = formatErrorForAI(error40);
101485
+ if (error40.category === "path_error" || error40.message?.includes("does not exist")) {
101486
+ return formatted + "\n\nThe path does not exist. Use the listFiles tool to verify the correct directory structure before retrying. If the workspace itself is gone, output your final answer with whatever information you have.";
101487
+ }
101488
+ return formatted;
101421
101489
  }
101422
101490
  }
101423
101491
  try {
@@ -101838,7 +101906,14 @@ function lineTrimmedMatch(contentLines, searchLines) {
101838
101906
  }
101839
101907
  }
101840
101908
  if (allMatch) {
101841
- const matchedText = contentLines.slice(i, i + windowSize).join("\n");
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");
101842
101917
  matches.push(matchedText);
101843
101918
  }
101844
101919
  }
@@ -101868,6 +101943,15 @@ function whitespaceNormalizedMatch(content, search2) {
101868
101943
  actualEnd++;
101869
101944
  }
101870
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
+ }
101871
101955
  matches.push(matchedText);
101872
101956
  searchStart = idx + 1;
101873
101957
  }
@@ -101919,6 +102003,10 @@ function indentFlexibleMatch(contentLines, searchLines) {
101919
102003
  }
101920
102004
  }
101921
102005
  if (allMatch) {
102006
+ const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
102007
+ if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
102008
+ continue;
102009
+ }
101922
102010
  const matchedText = windowLines.join("\n");
101923
102011
  matches.push(matchedText);
101924
102012
  }
@@ -101929,6 +102017,14 @@ function indentFlexibleMatch(contentLines, searchLines) {
101929
102017
  count: matches.length
101930
102018
  };
101931
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
+ }
101932
102028
  function getMinIndent(lines) {
101933
102029
  let min = Infinity;
101934
102030
  for (const line of lines) {
@@ -102003,6 +102099,12 @@ function restoreIndentation(newStr, originalLines) {
102003
102099
  const targetIndent = detectBaseIndent(originalCode);
102004
102100
  const newIndent = detectBaseIndent(newStr);
102005
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
+ }
102006
102108
  const reindented = reindent(newStr, targetIndent);
102007
102109
  if (reindented !== newStr) {
102008
102110
  modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc290",
3
+ "version": "0.6.0-rc292",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -1444,11 +1444,13 @@ export class ProbeAgent {
1444
1444
  result = await this._executeWithVercelProvider(options, controller);
1445
1445
  }
1446
1446
 
1447
- // Wrap textStream so limiter slot is held until stream completes
1447
+ // Wrap textStream so limiter slot is held until stream completes.
1448
+ // result.textStream is a read-only getter on DefaultStreamTextResult,
1449
+ // so we wrap the result in a Proxy that intercepts the textStream property.
1448
1450
  if (limiter && result.textStream) {
1449
1451
  const originalStream = result.textStream;
1450
1452
  const debug = this.debug;
1451
- result.textStream = (async function* () {
1453
+ const wrappedStream = (async function* () {
1452
1454
  try {
1453
1455
  for await (const chunk of originalStream) {
1454
1456
  yield chunk;
@@ -1461,6 +1463,13 @@ export class ProbeAgent {
1461
1463
  }
1462
1464
  }
1463
1465
  })();
1466
+ return new Proxy(result, {
1467
+ get(target, prop) {
1468
+ if (prop === 'textStream') return wrappedStream;
1469
+ const value = target[prop];
1470
+ return typeof value === 'function' ? value.bind(target) : value;
1471
+ }
1472
+ });
1464
1473
  } else if (limiter) {
1465
1474
  // No textStream (shouldn't happen, but release just in case)
1466
1475
  limiter.release(null);
@@ -3499,6 +3508,7 @@ Follow these instructions carefully:
3499
3508
  return true;
3500
3509
  }
3501
3510
  }
3511
+
3502
3512
  }
3503
3513
 
3504
3514
  return false;
@@ -3529,6 +3539,24 @@ Follow these instructions carefully:
3529
3539
  }
3530
3540
  }
3531
3541
 
3542
+ // Force text-only response after 3 consecutive tool errors
3543
+ // (e.g. workspace deleted mid-run — let the model produce its answer)
3544
+ if (steps.length >= 3) {
3545
+ const last3 = steps.slice(-3);
3546
+ const allErrors = last3.every(s =>
3547
+ s.toolResults?.length > 0 && s.toolResults.every(tr => {
3548
+ const r = typeof tr.result === 'string' ? tr.result : '';
3549
+ return r.includes('<error ') || r.includes('does not exist');
3550
+ })
3551
+ );
3552
+ if (allErrors) {
3553
+ if (this.debug) {
3554
+ console.log(`[DEBUG] prepareStep: 3 consecutive tool errors, forcing toolChoice=none`);
3555
+ }
3556
+ return { toolChoice: 'none' };
3557
+ }
3558
+ }
3559
+
3532
3560
  const lastStep = steps[steps.length - 1];
3533
3561
  const modelJustStopped = lastStep?.finishReason === 'stop'
3534
3562
  && (!lastStep?.toolCalls || lastStep.toolCalls.length === 0);
@@ -3565,7 +3593,8 @@ Here is the result to review:
3565
3593
  ${resultToReview}
3566
3594
  </result>
3567
3595
 
3568
- Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If something needs to be fixed or is missing, do it now, then respond with the COMPLETE updated answer (everything you did in total, not just the fix).`;
3596
+ IMPORTANT: First review ALL completed work in the conversation above before taking any action.
3597
+ Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If your text has inaccuracies, fix the text. Only call a tool if you find a genuinely MISSING action — NEVER redo work that was already completed successfully. Respond with the COMPLETE corrected answer.`;
3569
3598
 
3570
3599
  return {
3571
3600
  userMessage: completionPromptMessage,
@@ -3774,7 +3803,8 @@ Here is the result to review:
3774
3803
  ${finalResult}
3775
3804
  </result>
3776
3805
 
3777
- Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If something needs to be fixed or is missing, do it now, then respond with the COMPLETE updated answer (everything you did in total, not just the fix).`;
3806
+ IMPORTANT: First review ALL completed work in the conversation above before taking any action.
3807
+ Double-check your response based on the criteria above. If everything looks good, respond with your previous answer exactly as-is. If your text has inaccuracies, fix the text. Only call a tool if you find a genuinely MISSING action — NEVER redo work that was already completed successfully. Respond with the COMPLETE corrected answer.`;
3778
3808
 
3779
3809
  currentMessages.push({ role: 'user', content: completionPromptMessage });
3780
3810
 
@@ -234,7 +234,14 @@ export function generateSandboxGlobals(options) {
234
234
  }
235
235
  return tryParseJSONValue(text);
236
236
  };
237
- globals[name] = traceToolCall(name, rawMcpFn, tracer, logFn);
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: `Execution failed: ${e.message}`,
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
- const matchedText = contentLines.slice(i, i + windowSize).join('\n');
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}"`);
@@ -478,7 +478,11 @@ export const searchTool = (options = {}) => {
478
478
  return result;
479
479
  } catch (error) {
480
480
  console.error('Error executing search command:', error);
481
- return formatErrorForAI(error);
481
+ const formatted = formatErrorForAI(error);
482
+ if (error.category === 'path_error' || error.message?.includes('does not exist')) {
483
+ return formatted + '\n\nThe path does not exist. Use the listFiles tool to verify the correct directory structure before retrying. If the workspace itself is gone, output your final answer with whatever information you have.';
484
+ }
485
+ return formatted;
482
486
  }
483
487
  }
484
488
 
@@ -181,14 +181,14 @@ export function categorizeError(error) {
181
181
  errorCode === 'enoent') {
182
182
  return new PathError(message, {
183
183
  originalError: error,
184
- suggestion: 'The specified path does not exist. Please verify the path or use a different directory.'
184
+ suggestion: 'The specified path does not exist. Use the listFiles tool to check the correct directory structure, then retry with a valid path.'
185
185
  });
186
186
  }
187
187
 
188
188
  if (lowerMessage.includes('not a directory') || errorCode === 'enotdir') {
189
189
  return new PathError(message, {
190
190
  originalError: error,
191
- suggestion: 'The path is not a directory. Please provide a valid directory path.'
191
+ suggestion: 'The path is not a directory. Use the listFiles tool to find the correct directory, then retry.'
192
192
  });
193
193
  }
194
194
 
@@ -110,7 +110,7 @@ export async function validateCwdPath(inputPath, defaultPath = process.cwd()) {
110
110
  }
111
111
  if (error.code === 'ENOENT') {
112
112
  throw new PathError(`Path does not exist: ${normalizedPath}`, {
113
- suggestion: 'The specified path does not exist. Please verify the path is correct or use a different directory.',
113
+ suggestion: 'The specified path does not exist. Use the listFiles tool to check the correct directory structure, then retry with a valid path.',
114
114
  details: { path: normalizedPath }
115
115
  });
116
116
  }