// src/utils/jsonUtils.ts - Centralized JSON parsing and utilities export class JSONParser { static safeParseJSON(jsonString: string, fallback: any = null): any { try { let cleaned = jsonString.trim(); // Remove code block markers const jsonBlockPatterns = [ /```json\s*([\s\S]*?)\s*```/i, /```\s*([\s\S]*?)\s*```/i, /\{[\s\S]*\}/, ]; for (const pattern of jsonBlockPatterns) { const match = cleaned.match(pattern); if (match) { cleaned = match[1] || match[0]; break; } } // Handle truncated JSON if (!cleaned.endsWith('}') && !cleaned.endsWith(']')) { console.warn('[JSON-PARSER] JSON appears truncated, attempting recovery'); cleaned = this.repairTruncatedJSON(cleaned); } const parsed = JSON.parse(cleaned); // Ensure proper structure for tool selection responses if (parsed && typeof parsed === 'object') { if (!parsed.selectedTools) parsed.selectedTools = []; if (!parsed.selectedConcepts) parsed.selectedConcepts = []; if (!Array.isArray(parsed.selectedTools)) parsed.selectedTools = []; if (!Array.isArray(parsed.selectedConcepts)) parsed.selectedConcepts = []; } return parsed; } catch (error) { console.warn('[JSON-PARSER] JSON parsing failed:', error.message); return fallback; } } private static repairTruncatedJSON(cleaned: string): string { let braceCount = 0; let bracketCount = 0; let inString = false; let escaped = false; let lastCompleteStructure = ''; for (let i = 0; i < cleaned.length; i++) { const char = cleaned[i]; if (escaped) { escaped = false; continue; } if (char === '\\') { escaped = true; continue; } if (char === '"' && !escaped) { inString = !inString; continue; } if (!inString) { if (char === '{') braceCount++; if (char === '}') braceCount--; if (char === '[') bracketCount++; if (char === ']') bracketCount--; if (braceCount === 0 && bracketCount === 0 && (char === '}' || char === ']')) { lastCompleteStructure = cleaned.substring(0, i + 1); } } } if (lastCompleteStructure) { return lastCompleteStructure; } else { if (braceCount > 0) cleaned += '}'; if (bracketCount > 0) cleaned += ']'; return cleaned; } } static extractToolsFromMalformedJSON(jsonString: string): { selectedTools: string[]; selectedConcepts: string[] } { const selectedTools: string[] = []; const selectedConcepts: string[] = []; const toolsMatch = jsonString.match(/"selectedTools"\s*:\s*\[([\s\S]*?)\]/i); if (toolsMatch) { const toolMatches = toolsMatch[1].match(/"([^"]+)"/g); if (toolMatches) { selectedTools.push(...toolMatches.map(match => match.replace(/"/g, ''))); } } const conceptsMatch = jsonString.match(/"selectedConcepts"\s*:\s*\[([\s\S]*?)\]/i); if (conceptsMatch) { const conceptMatches = conceptsMatch[1].match(/"([^"]+)"/g); if (conceptMatches) { selectedConcepts.push(...conceptMatches.map(match => match.replace(/"/g, ''))); } } // Fallback: extract any quoted strings that look like tool names if (selectedTools.length === 0 && selectedConcepts.length === 0) { const allMatches = jsonString.match(/"([^"]+)"/g); if (allMatches) { const possibleNames = allMatches .map(match => match.replace(/"/g, '')) .filter(name => name.length > 2 && !['selectedTools', 'selectedConcepts', 'reasoning'].includes(name) && !name.includes(':') && !name.match(/^\d+$/) ) .slice(0, 15); selectedTools.push(...possibleNames); } } return { selectedTools, selectedConcepts }; } static secureParseJSON(jsonString: string, maxSize: number = 10 * 1024 * 1024): any { if (typeof jsonString !== 'string') { throw new Error('Input must be a string'); } if (jsonString.length > maxSize) { throw new Error(`JSON string too large (${jsonString.length} bytes, max ${maxSize})`); } // Security checks for potentially malicious content const suspiciousPatterns = [ /