@probelabs/probe 0.6.0-rc254 → 0.6.0-rc256

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +166 -3
  2. package/bin/binaries/probe-v0.6.0-rc256-aarch64-apple-darwin.tar.gz +0 -0
  3. package/bin/binaries/probe-v0.6.0-rc256-aarch64-unknown-linux-musl.tar.gz +0 -0
  4. package/bin/binaries/probe-v0.6.0-rc256-x86_64-apple-darwin.tar.gz +0 -0
  5. package/bin/binaries/probe-v0.6.0-rc256-x86_64-pc-windows-msvc.zip +0 -0
  6. package/bin/binaries/probe-v0.6.0-rc256-x86_64-unknown-linux-musl.tar.gz +0 -0
  7. package/build/agent/ProbeAgent.d.ts +1 -1
  8. package/build/agent/ProbeAgent.js +39 -23
  9. package/build/agent/acp/tools.js +2 -1
  10. package/build/agent/acp/tools.test.js +2 -1
  11. package/build/agent/bashDefaults.js +75 -6
  12. package/build/agent/index.js +1752 -426
  13. package/build/agent/mcp/xmlBridge.js +3 -2
  14. package/build/agent/schemaUtils.js +127 -0
  15. package/build/agent/tools.js +0 -28
  16. package/build/delegate.js +3 -0
  17. package/build/index.js +2 -0
  18. package/build/tools/common.js +26 -8
  19. package/build/tools/edit.js +457 -65
  20. package/build/tools/fileTracker.js +318 -0
  21. package/build/tools/fuzzyMatch.js +271 -0
  22. package/build/tools/hashline.js +131 -0
  23. package/build/tools/lineEditHeuristics.js +138 -0
  24. package/build/tools/symbolEdit.js +119 -0
  25. package/build/tools/vercel.js +40 -9
  26. package/cjs/agent/ProbeAgent.cjs +1863 -528
  27. package/cjs/index.cjs +1891 -554
  28. package/index.d.ts +189 -1
  29. package/package.json +1 -1
  30. package/src/agent/ProbeAgent.d.ts +1 -1
  31. package/src/agent/ProbeAgent.js +39 -23
  32. package/src/agent/acp/tools.js +2 -1
  33. package/src/agent/acp/tools.test.js +2 -1
  34. package/src/agent/bashDefaults.js +75 -6
  35. package/src/agent/index.js +18 -7
  36. package/src/agent/mcp/xmlBridge.js +3 -2
  37. package/src/agent/schemaUtils.js +127 -0
  38. package/src/agent/tools.js +0 -28
  39. package/src/delegate.js +3 -0
  40. package/src/index.js +2 -0
  41. package/src/tools/common.js +26 -8
  42. package/src/tools/edit.js +457 -65
  43. package/src/tools/fileTracker.js +318 -0
  44. package/src/tools/fuzzyMatch.js +271 -0
  45. package/src/tools/hashline.js +131 -0
  46. package/src/tools/lineEditHeuristics.js +138 -0
  47. package/src/tools/symbolEdit.js +119 -0
  48. package/src/tools/vercel.js +40 -9
  49. package/bin/binaries/probe-v0.6.0-rc254-aarch64-apple-darwin.tar.gz +0 -0
  50. package/bin/binaries/probe-v0.6.0-rc254-aarch64-unknown-linux-musl.tar.gz +0 -0
  51. package/bin/binaries/probe-v0.6.0-rc254-x86_64-apple-darwin.tar.gz +0 -0
  52. package/bin/binaries/probe-v0.6.0-rc254-x86_64-pc-windows-msvc.zip +0 -0
  53. package/bin/binaries/probe-v0.6.0-rc254-x86_64-unknown-linux-musl.tar.gz +0 -0
@@ -6,6 +6,7 @@
6
6
  import { MCPClientManager } from './client.js';
7
7
  import { loadMCPConfiguration } from './config.js';
8
8
  import { processXmlWithThinkingAndRecovery } from '../xmlParsingUtils.js';
9
+ import { unescapeXmlEntities } from '../../tools/common.js';
9
10
 
10
11
  /**
11
12
  * Convert MCP tool to XML definition format
@@ -111,7 +112,7 @@ export function parseXmlMcpToolCall(xmlString, mcpToolNames = []) {
111
112
  let match;
112
113
  while ((match = paramPattern.exec(content)) !== null) {
113
114
  const [, paramName, paramValue] = match;
114
- params[paramName] = paramValue.trim();
115
+ params[paramName] = unescapeXmlEntities(paramValue.trim());
115
116
  }
116
117
  }
117
118
 
@@ -393,7 +394,7 @@ function parseNativeXmlTool(xmlString, toolName) {
393
394
  const [, paramName, paramValue] = match;
394
395
  // Skip if this is the params tag itself (MCP format)
395
396
  if (paramName !== 'params') {
396
- params[paramName] = paramValue.trim();
397
+ params[paramName] = unescapeXmlEntities(paramValue.trim());
397
398
  }
398
399
  }
399
400
 
@@ -603,6 +603,17 @@ export function validateJsonResponse(response, options = {}) {
603
603
  }
604
604
  }
605
605
 
606
+ // Quick recovery: try to extract a valid JSON prefix before returning error.
607
+ // This handles the common case where AI returns valid JSON followed by markdown content,
608
+ // e.g., "Unexpected non-whitespace character after JSON at position 477" (issue #447).
609
+ const prefixResult = tryExtractValidJsonPrefix(responseToValidate, { schema, debug });
610
+ if (prefixResult && prefixResult.isValid) {
611
+ if (debug) {
612
+ console.log(`[DEBUG] JSON validation: Recovered valid JSON prefix (${prefixResult.extracted.length} chars) from response with trailing content`);
613
+ }
614
+ return { isValid: true, parsed: prefixResult.parsed };
615
+ }
616
+
606
617
  // Create enhanced error message with context snippet
607
618
  let enhancedError = error.message;
608
619
  let errorContext = null;
@@ -667,6 +678,122 @@ ${pointer} here`;
667
678
  }
668
679
  }
669
680
 
681
+ /**
682
+ * Try to extract a valid JSON prefix from a string that has trailing non-JSON content.
683
+ * This handles the common case where an AI returns valid JSON followed by markdown text.
684
+ *
685
+ * Uses bracket-matching to find the end of the first complete JSON object/array,
686
+ * then validates the prefix with JSON.parse(). Optionally validates against a schema.
687
+ *
688
+ * @param {string} response - The full response string
689
+ * @param {Object} [options] - Options
690
+ * @param {Object|string} [options.schema] - JSON schema to validate against
691
+ * @param {boolean} [options.debug=false] - Enable debug logging
692
+ * @returns {Object|null} - {isValid: true, parsed: Object, extracted: string} or null if no valid prefix found
693
+ */
694
+ export function tryExtractValidJsonPrefix(response, options = {}) {
695
+ const { schema = null, debug = false } = options;
696
+
697
+ if (!response || typeof response !== 'string') {
698
+ return null;
699
+ }
700
+
701
+ const trimmed = response.trim();
702
+ if (trimmed.length === 0) {
703
+ return null;
704
+ }
705
+
706
+ // Must start with { or [
707
+ const firstChar = trimmed[0];
708
+ if (firstChar !== '{' && firstChar !== '[') {
709
+ return null;
710
+ }
711
+
712
+ // First, check if the full string already parses - no need to extract prefix
713
+ try {
714
+ JSON.parse(trimmed);
715
+ return null; // Full string is valid JSON, no extraction needed
716
+ } catch {
717
+ // Expected - continue with prefix extraction
718
+ }
719
+
720
+ // Find the end of the first complete JSON object/array using bracket matching
721
+ const openChar = firstChar;
722
+ const closeChar = openChar === '{' ? '}' : ']';
723
+ let depth = 0;
724
+ let inString = false;
725
+ let escapeNext = false;
726
+ let endPos = -1;
727
+
728
+ for (let i = 0; i < trimmed.length; i++) {
729
+ const char = trimmed[i];
730
+
731
+ if (escapeNext) {
732
+ escapeNext = false;
733
+ continue;
734
+ }
735
+
736
+ if (char === '\\' && inString) {
737
+ escapeNext = true;
738
+ continue;
739
+ }
740
+
741
+ if (char === '"') {
742
+ inString = !inString;
743
+ continue;
744
+ }
745
+
746
+ if (inString) {
747
+ continue;
748
+ }
749
+
750
+ if (char === openChar) {
751
+ depth++;
752
+ } else if (char === closeChar) {
753
+ depth--;
754
+ if (depth === 0) {
755
+ endPos = i + 1;
756
+ break;
757
+ }
758
+ }
759
+ }
760
+
761
+ if (endPos <= 0 || endPos >= trimmed.length) {
762
+ return null; // No complete JSON found, or JSON spans the entire string
763
+ }
764
+
765
+ // Check that there's actual non-whitespace trailing content
766
+ const remainder = trimmed.substring(endPos).trim();
767
+ if (remainder.length === 0) {
768
+ return null; // Only whitespace after JSON, no trailing content issue
769
+ }
770
+
771
+ // Try to parse the prefix
772
+ const prefix = trimmed.substring(0, endPos);
773
+ try {
774
+ const parsed = JSON.parse(prefix);
775
+
776
+ if (debug) {
777
+ console.log(`[DEBUG] tryExtractValidJsonPrefix: Extracted valid JSON prefix (${prefix.length} chars), stripped trailing content (${remainder.length} chars)`);
778
+ }
779
+
780
+ // If schema provided, validate against it
781
+ if (schema) {
782
+ const schemaValidation = validateJsonResponse(prefix, { debug, schema });
783
+ if (!schemaValidation.isValid) {
784
+ if (debug) {
785
+ console.log(`[DEBUG] tryExtractValidJsonPrefix: Prefix is valid JSON but fails schema validation: ${schemaValidation.error}`);
786
+ }
787
+ return null;
788
+ }
789
+ }
790
+
791
+ return { isValid: true, parsed, extracted: prefix };
792
+ } catch {
793
+ return null;
794
+ }
795
+ }
796
+
670
797
  /**
671
798
  * Validate that the cleaned response is valid XML if expected
672
799
  * @param {string} response - Cleaned response
@@ -87,7 +87,6 @@ export function createTools(configOptions) {
87
87
  if (configOptions.allowEdit && isToolAllowed('create')) {
88
88
  tools.createTool = createTool(configOptions);
89
89
  }
90
-
91
90
  return tools;
92
91
  }
93
92
 
@@ -132,33 +131,6 @@ export {
132
131
  parseXmlToolCall
133
132
  };
134
133
 
135
- // Define the implement tool XML definition
136
- export const implementToolDefinition = `
137
- ## implement
138
- Description: Implement a given task. Can modify files. Can be used ONLY if task explicitly stated that something requires modification or implementation.
139
-
140
- Parameters:
141
- - task: (required) The task description. Should be as detailed as possible, ideally pointing to exact files which needs be modified or created.
142
- - autoCommits: (optional) Whether to enable auto-commits in aider. Default is false.
143
-
144
- Usage Example:
145
-
146
- <examples>
147
-
148
- User: Can you implement a function to calculate Fibonacci numbers in main.js?
149
- <implement>
150
- <task>Implement a recursive function to calculate the nth Fibonacci number in main.js</task>
151
- </implement>
152
-
153
- User: Can you implement a function to calculate Fibonacci numbers in main.js with auto-commits?
154
- <implement>
155
- <task>Implement a recursive function to calculate the nth Fibonacci number in main.js</task>
156
- <autoCommits>true</autoCommits>
157
- </implement>
158
-
159
- </examples>
160
- `;
161
-
162
134
  // Define the listFiles tool XML definition
163
135
  export const listFilesToolDefinition = `
164
136
  ## listFiles
package/build/delegate.js CHANGED
@@ -346,6 +346,7 @@ const DEFAULT_DELEGATE_TIMEOUT = parseInt(process.env.DELEGATE_TIMEOUT, 10) || 3
346
346
  * @param {Object} [options.tracer=null] - Telemetry tracer instance
347
347
  * @param {boolean} [options.enableBash=false] - Enable bash tool (inherited from parent)
348
348
  * @param {Object} [options.bashConfig] - Bash configuration (inherited from parent)
349
+ * @param {boolean} [options.allowEdit=false] - Allow edit/create tools (inherited from parent)
349
350
  * @param {string} [options.architectureFileName] - Architecture context filename to embed from repo root
350
351
  * @param {string} [options.promptType='code-researcher'] - Prompt type for the subagent
351
352
  * @param {Array<string>|null} [options.allowedTools] - Allowed tools for the subagent (null = default)
@@ -373,6 +374,7 @@ export async function delegate({
373
374
  model = null,
374
375
  enableBash = false,
375
376
  bashConfig = null,
377
+ allowEdit = false,
376
378
  architectureFileName = null,
377
379
  promptType = 'code-researcher',
378
380
  allowedTools = null,
@@ -462,6 +464,7 @@ export async function delegate({
462
464
  model, // Inherit from parent
463
465
  enableBash, // Inherit from parent
464
466
  bashConfig, // Inherit from parent
467
+ allowEdit, // Inherit from parent
465
468
  architectureFileName,
466
469
  allowedTools,
467
470
  disableTools,
package/build/index.js CHANGED
@@ -51,6 +51,7 @@ import { searchTool, queryTool, extractTool, delegateTool, analyzeAllTool } from
51
51
  import { createExecutePlanTool, getExecutePlanToolDefinition, createCleanupExecutePlanTool, getCleanupExecutePlanToolDefinition } from './tools/executePlan.js';
52
52
  import { bashTool } from './tools/bash.js';
53
53
  import { editTool, createTool } from './tools/edit.js';
54
+ import { FileTracker } from './tools/fileTracker.js';
54
55
  import { ProbeAgent } from './agent/ProbeAgent.js';
55
56
  import { SimpleTelemetry, SimpleAppTracer, initializeSimpleTelemetryFromOptions } from './agent/simpleTelemetry.js';
56
57
  import { listFilesToolInstance, searchFilesToolInstance } from './agent/probeTool.js';
@@ -98,6 +99,7 @@ export {
98
99
  bashTool,
99
100
  editTool,
100
101
  createTool,
102
+ FileTracker,
101
103
  // Export tool instances
102
104
  listFilesToolInstance,
103
105
  searchFilesToolInstance,
@@ -269,6 +269,8 @@ User: Read file inside the dependency
269
269
  </extract>
270
270
 
271
271
  </examples>
272
+
273
+ **Edit Integration:** The line numbers shown in extract output (e.g. "42 | code") can be used directly with the edit tool's start_line/end_line parameters for precise line-targeted editing. To edit inside a large function: extract it by symbol name first (e.g. "file.js#myFunction"), then use the line numbers from the output to make surgical edits with start_line/end_line.
272
274
  `;
273
275
 
274
276
  export const delegateToolDefinition = `
@@ -431,7 +433,7 @@ Capabilities:
431
433
 
432
434
  export const searchDescription = 'Search code in the repository. Free-form questions are accepted, but Elasticsearch-style keyword queries work best. Use this tool first for any code-related questions.';
433
435
  export const queryDescription = 'Search code using ast-grep structural pattern matching. Use this tool to find specific code structures like functions, classes, or methods.';
434
- export const extractDescription = 'Extract code blocks from files based on file paths and optional line numbers. Use this tool to see complete context after finding relevant files.';
436
+ export const extractDescription = 'Extract code blocks from files based on file paths and optional line numbers. Use this tool to see complete context after finding relevant files. Line numbers from output can be used with edit start_line/end_line for precise editing.';
435
437
  export const delegateDescription = 'Automatically delegate big distinct tasks to specialized probe subagents within the agentic loop. Used by AI agents to break down complex requests into focused, parallel tasks.';
436
438
  export const bashDescription = 'Execute bash commands for system exploration and development tasks. Secure by default with built-in allow/deny lists.';
437
439
  export const analyzeAllDescription = 'Answer questions that require analyzing ALL matching data in the codebase. Use for aggregate questions like "What features exist?", "List all API endpoints", "Count TODO comments". The AI automatically plans the search strategy, processes all results via map-reduce, and synthesizes a comprehensive answer. WARNING: Slower than search - only use when you need complete coverage.';
@@ -450,7 +452,6 @@ export const DEFAULT_VALID_TOOLS = [
450
452
  'useSkill',
451
453
  'listFiles',
452
454
  'searchFiles',
453
- 'implement',
454
455
  'bash',
455
456
  'task',
456
457
  'attempt_completion'
@@ -496,9 +497,9 @@ function getValidParamsForTool(toolName) {
496
497
 
497
498
  const schema = schemaMap[toolName];
498
499
  if (!schema) {
499
- // For tools without schema (listFiles, searchFiles, implement), return common params
500
+ // For tools without schema (listFiles, searchFiles), return common params
500
501
  // These are the shared params that appear across multiple tools
501
- return ['path', 'directory', 'pattern', 'recursive', 'includeHidden', 'task', 'files', 'autoCommits', 'result'];
502
+ return ['path', 'directory', 'pattern', 'recursive', 'includeHidden', 'task', 'files', 'result'];
502
503
  }
503
504
 
504
505
  // For attempt_completion, it has custom validation, just return 'result'
@@ -520,6 +521,23 @@ function getValidParamsForTool(toolName) {
520
521
  return [];
521
522
  }
522
523
 
524
+ /**
525
+ * Unescape standard XML entities in a string value.
526
+ * Order matters: &amp; must be decoded LAST to avoid double-decoding
527
+ * (e.g., &amp;lt; should become &lt;, not <).
528
+ * @param {string} str - The string to unescape
529
+ * @returns {string} The unescaped string
530
+ */
531
+ export function unescapeXmlEntities(str) {
532
+ if (typeof str !== 'string') return str;
533
+ return str
534
+ .replace(/&lt;/g, '<')
535
+ .replace(/&gt;/g, '>')
536
+ .replace(/&quot;/g, '"')
537
+ .replace(/&apos;/g, "'")
538
+ .replace(/&amp;/g, '&');
539
+ }
540
+
523
541
  // Simple XML parser helper - safer string-based approach
524
542
  export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
525
543
  // Find the tool that appears EARLIEST in the string
@@ -608,10 +626,10 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
608
626
  paramCloseIndex = nextTagIndex;
609
627
  }
610
628
 
611
- let paramValue = innerContent.substring(
629
+ let paramValue = unescapeXmlEntities(innerContent.substring(
612
630
  paramOpenIndex + paramOpenTag.length,
613
631
  paramCloseIndex
614
- ).trim();
632
+ ).trim());
615
633
 
616
634
  // Basic type inference (can be improved)
617
635
  if (paramValue.toLowerCase() === 'true') {
@@ -632,7 +650,7 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
632
650
 
633
651
  // Special handling for attempt_completion - use entire inner content as result
634
652
  if (toolName === 'attempt_completion') {
635
- params['result'] = innerContent.trim();
653
+ params['result'] = unescapeXmlEntities(innerContent.trim());
636
654
  // Remove command parameter if it was parsed by generic logic above (legacy compatibility)
637
655
  if (params.command) {
638
656
  delete params.command;
@@ -688,7 +706,7 @@ export function detectUnrecognizedToolCall(xmlString, validTools) {
688
706
  // Common tool names that AI might try to use (these should appear as top-level tags)
689
707
  const knownToolNames = [
690
708
  'search', 'query', 'extract', 'listFiles', 'searchFiles',
691
- 'listSkills', 'useSkill', 'readImage', 'implement', 'edit',
709
+ 'listSkills', 'useSkill', 'readImage', 'edit',
692
710
  'create', 'delegate', 'bash', 'task', 'attempt_completion',
693
711
  'attempt_complete', 'read_file', 'write_file', 'run_command',
694
712
  'grep', 'find', 'cat', 'list_directory'