@probelabs/probe 0.6.0-rc255 → 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.
- package/bin/binaries/probe-v0.6.0-rc256-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc256-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc256-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc256-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc256-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +15 -7
- package/build/agent/bashDefaults.js +75 -6
- package/build/agent/index.js +327 -16
- package/build/agent/mcp/xmlBridge.js +3 -2
- package/build/agent/schemaUtils.js +127 -0
- package/build/tools/common.js +20 -3
- package/cjs/agent/ProbeAgent.cjs +335 -14
- package/cjs/index.cjs +335 -14
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +15 -7
- package/src/agent/bashDefaults.js +75 -6
- package/src/agent/index.js +4 -4
- package/src/agent/mcp/xmlBridge.js +3 -2
- package/src/agent/schemaUtils.js +127 -0
- package/src/tools/common.js +20 -3
- package/bin/binaries/probe-v0.6.0-rc255-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-x86_64-unknown-linux-musl.tar.gz +0 -0
|
@@ -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
|
package/build/tools/common.js
CHANGED
|
@@ -521,6 +521,23 @@ function getValidParamsForTool(toolName) {
|
|
|
521
521
|
return [];
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Unescape standard XML entities in a string value.
|
|
526
|
+
* Order matters: & must be decoded LAST to avoid double-decoding
|
|
527
|
+
* (e.g., &lt; should become <, 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(/</g, '<')
|
|
535
|
+
.replace(/>/g, '>')
|
|
536
|
+
.replace(/"/g, '"')
|
|
537
|
+
.replace(/'/g, "'")
|
|
538
|
+
.replace(/&/g, '&');
|
|
539
|
+
}
|
|
540
|
+
|
|
524
541
|
// Simple XML parser helper - safer string-based approach
|
|
525
542
|
export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
|
|
526
543
|
// Find the tool that appears EARLIEST in the string
|
|
@@ -609,10 +626,10 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
|
|
|
609
626
|
paramCloseIndex = nextTagIndex;
|
|
610
627
|
}
|
|
611
628
|
|
|
612
|
-
let paramValue = innerContent.substring(
|
|
629
|
+
let paramValue = unescapeXmlEntities(innerContent.substring(
|
|
613
630
|
paramOpenIndex + paramOpenTag.length,
|
|
614
631
|
paramCloseIndex
|
|
615
|
-
).trim();
|
|
632
|
+
).trim());
|
|
616
633
|
|
|
617
634
|
// Basic type inference (can be improved)
|
|
618
635
|
if (paramValue.toLowerCase() === 'true') {
|
|
@@ -633,7 +650,7 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
|
|
|
633
650
|
|
|
634
651
|
// Special handling for attempt_completion - use entire inner content as result
|
|
635
652
|
if (toolName === 'attempt_completion') {
|
|
636
|
-
params['result'] = innerContent.trim();
|
|
653
|
+
params['result'] = unescapeXmlEntities(innerContent.trim());
|
|
637
654
|
// Remove command parameter if it was parsed by generic logic above (legacy compatibility)
|
|
638
655
|
if (params.command) {
|
|
639
656
|
delete params.command;
|