@juspay/neurolink 9.88.6 → 9.88.8
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/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +287 -287
- package/dist/lib/utils/csvProcessor.js +73 -8
- package/dist/lib/utils/fileDetector.js +28 -3
- package/dist/utils/csvProcessor.js +73 -8
- package/dist/utils/fileDetector.js +28 -3
- package/package.json +1 -1
|
@@ -337,6 +337,20 @@ function calculateDataQualityScore(columns, warnings, totalRows) {
|
|
|
337
337
|
/**
|
|
338
338
|
* Analyze all columns in parsed CSV data
|
|
339
339
|
*/
|
|
340
|
+
/**
|
|
341
|
+
* Confidence for a CSV processing result, weighted by data quality.
|
|
342
|
+
*
|
|
343
|
+
* The result used to report a static `confidence: 100` regardless of how clean
|
|
344
|
+
* the data was. This ties confidence to `dataQualityScore` so it actually
|
|
345
|
+
* varies with quality factors (ragged rows, high null rate, type
|
|
346
|
+
* inconsistency): a cleanly-parsed CSV stays at 100, a messy one is pulled
|
|
347
|
+
* halfway toward its quality score. It never drops below 50 for a CSV that
|
|
348
|
+
* parsed — the *format* is certain, only the *data* is imperfect.
|
|
349
|
+
*/
|
|
350
|
+
function csvResultConfidence(dataQualityScore) {
|
|
351
|
+
const clamped = Math.max(0, Math.min(100, dataQualityScore));
|
|
352
|
+
return Math.round((100 + clamped) / 2);
|
|
353
|
+
}
|
|
340
354
|
function analyzeColumns(rows) {
|
|
341
355
|
if (rows.length === 0) {
|
|
342
356
|
return {
|
|
@@ -425,6 +439,33 @@ function detectHasHeaders(headerValues, dataRows) {
|
|
|
425
439
|
* - Lines with significantly different delimiter count than line 2
|
|
426
440
|
* - Lines that don't match CSV structure of subsequent lines
|
|
427
441
|
*/
|
|
442
|
+
/** Strip a leading UTF-8 BOM (U+FEFF) if present. */
|
|
443
|
+
function stripBom(text) {
|
|
444
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Count commas that fall OUTSIDE double-quoted fields (RFC 4180, escaped-quote
|
|
448
|
+
* aware). A quoted comma is field content, not a column separator, so counting
|
|
449
|
+
* naively would misclassify a quoted-comma header line.
|
|
450
|
+
*/
|
|
451
|
+
function countUnquotedCommas(line) {
|
|
452
|
+
let count = 0;
|
|
453
|
+
let inQuotes = false;
|
|
454
|
+
for (let i = 0; i < line.length; i++) {
|
|
455
|
+
const ch = line[i];
|
|
456
|
+
if (ch === '"') {
|
|
457
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
458
|
+
i++;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
inQuotes = !inQuotes;
|
|
462
|
+
}
|
|
463
|
+
else if (ch === "," && !inQuotes) {
|
|
464
|
+
count++;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return count;
|
|
468
|
+
}
|
|
428
469
|
function isMetadataLine(lines) {
|
|
429
470
|
if (!lines[0] || lines.length < 2) {
|
|
430
471
|
return false;
|
|
@@ -434,8 +475,8 @@ function isMetadataLine(lines) {
|
|
|
434
475
|
if (firstLine.match(/^sep=/i)) {
|
|
435
476
|
return true;
|
|
436
477
|
}
|
|
437
|
-
const firstCommaCount = (firstLine
|
|
438
|
-
const secondCommaCount = (secondLine
|
|
478
|
+
const firstCommaCount = countUnquotedCommas(firstLine);
|
|
479
|
+
const secondCommaCount = countUnquotedCommas(secondLine);
|
|
439
480
|
if (firstCommaCount === 0 && secondCommaCount > 0) {
|
|
440
481
|
return true;
|
|
441
482
|
}
|
|
@@ -574,7 +615,7 @@ export class CSVProcessor {
|
|
|
574
615
|
content: limitedCSV,
|
|
575
616
|
mimeType: "text/csv",
|
|
576
617
|
metadata: {
|
|
577
|
-
confidence:
|
|
618
|
+
confidence: csvResultConfidence(dataQualityScore),
|
|
578
619
|
size: content.length,
|
|
579
620
|
rowCount,
|
|
580
621
|
totalLines: limitedLines.length,
|
|
@@ -648,7 +689,7 @@ export class CSVProcessor {
|
|
|
648
689
|
content: formatted,
|
|
649
690
|
mimeType: "text/csv",
|
|
650
691
|
metadata: {
|
|
651
|
-
confidence:
|
|
692
|
+
confidence: csvResultConfidence(dataQualityScore),
|
|
652
693
|
size: content.length,
|
|
653
694
|
rowCount,
|
|
654
695
|
columnCount,
|
|
@@ -676,6 +717,9 @@ export class CSVProcessor {
|
|
|
676
717
|
* @returns Array of row objects
|
|
677
718
|
*/
|
|
678
719
|
static async parseCSVFile(filePath, maxRows = 1000) {
|
|
720
|
+
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
721
|
+
throw new Error("CSVProcessor.parseCSVFile: filePath must be a non-empty string");
|
|
722
|
+
}
|
|
679
723
|
const clampedMaxRows = Math.max(1, Math.min(10000, maxRows));
|
|
680
724
|
const fs = await import("fs");
|
|
681
725
|
logger.debug("[CSVProcessor] Starting file parsing", {
|
|
@@ -701,6 +745,8 @@ export class CSVProcessor {
|
|
|
701
745
|
}
|
|
702
746
|
});
|
|
703
747
|
lineReader.on("end", () => resolve());
|
|
748
|
+
// Metadata sniffing is best-effort — a read error here must not crash.
|
|
749
|
+
lineReader.on("error", () => resolve());
|
|
704
750
|
});
|
|
705
751
|
await fileHandle.close();
|
|
706
752
|
const hasMetadataLine = isMetadataLine(firstLines);
|
|
@@ -718,6 +764,12 @@ export class CSVProcessor {
|
|
|
718
764
|
source.destroy();
|
|
719
765
|
parser.destroy();
|
|
720
766
|
};
|
|
767
|
+
// .pipe() does not forward source errors (disk read/permission failures)
|
|
768
|
+
// to the parser, so listen on the source directly or it throws unhandled.
|
|
769
|
+
source.on("error", (error) => {
|
|
770
|
+
abort();
|
|
771
|
+
reject(new Error(`[CSVProcessor] CSV file read failed after ${count} row(s) (${filePath}): ${error.message}`));
|
|
772
|
+
});
|
|
721
773
|
source
|
|
722
774
|
.pipe(parser)
|
|
723
775
|
.on("data", (row) => {
|
|
@@ -739,7 +791,7 @@ export class CSVProcessor {
|
|
|
739
791
|
})
|
|
740
792
|
.on("error", (error) => {
|
|
741
793
|
logger.error("[CSVProcessor] File parsing failed:", error);
|
|
742
|
-
reject(error);
|
|
794
|
+
reject(new Error(`[CSVProcessor] CSV parsing failed after ${count} row(s): ${error.message}`));
|
|
743
795
|
});
|
|
744
796
|
});
|
|
745
797
|
}
|
|
@@ -752,13 +804,19 @@ export class CSVProcessor {
|
|
|
752
804
|
* @returns Array of row objects
|
|
753
805
|
*/
|
|
754
806
|
static async parseCSVString(csvString, maxRows = 1000) {
|
|
807
|
+
if (typeof csvString !== "string" || csvString.trim().length === 0) {
|
|
808
|
+
throw new Error("CSVProcessor.parseCSVString: csvString must be a non-empty string");
|
|
809
|
+
}
|
|
810
|
+
// Strip a leading UTF-8 BOM (common in Excel exports) so it does not glue
|
|
811
|
+
// onto the first column name and break downstream key lookups.
|
|
812
|
+
const normalized = stripBom(csvString);
|
|
755
813
|
const clampedMaxRows = Math.max(1, Math.min(10000, maxRows));
|
|
756
814
|
logger.debug("[CSVProcessor] Starting string parsing", {
|
|
757
|
-
inputLength:
|
|
815
|
+
inputLength: normalized.length,
|
|
758
816
|
maxRows: clampedMaxRows,
|
|
759
817
|
});
|
|
760
818
|
// Detect and skip metadata line
|
|
761
|
-
const lines = splitCsvLines(
|
|
819
|
+
const lines = splitCsvLines(normalized);
|
|
762
820
|
const hasMetadataLine = isMetadataLine(lines);
|
|
763
821
|
const csvData = hasMetadataLine
|
|
764
822
|
? lines.slice(1).join("\n")
|
|
@@ -775,6 +833,13 @@ export class CSVProcessor {
|
|
|
775
833
|
source.destroy();
|
|
776
834
|
parser.destroy();
|
|
777
835
|
};
|
|
836
|
+
// .pipe() does not forward source-stream errors to the destination, so a
|
|
837
|
+
// source failure must be listened for directly or it throws as an
|
|
838
|
+
// unhandled EventEmitter error and can crash the process.
|
|
839
|
+
source.on("error", (error) => {
|
|
840
|
+
abort();
|
|
841
|
+
reject(new Error(`[CSVProcessor] CSV source stream failed after ${count} row(s): ${error.message}`));
|
|
842
|
+
});
|
|
778
843
|
source
|
|
779
844
|
.pipe(parser)
|
|
780
845
|
.on("data", (row) => {
|
|
@@ -792,7 +857,7 @@ export class CSVProcessor {
|
|
|
792
857
|
})
|
|
793
858
|
.on("error", (error) => {
|
|
794
859
|
logger.error("[CSVProcessor] Parsing failed:", error);
|
|
795
|
-
reject(error);
|
|
860
|
+
reject(new Error(`[CSVProcessor] CSV parsing failed after ${count} row(s): ${error.message}`));
|
|
796
861
|
});
|
|
797
862
|
});
|
|
798
863
|
}
|
|
@@ -2166,13 +2166,38 @@ class ContentHeuristicStrategy {
|
|
|
2166
2166
|
const hasUniformLengths = stdDev / avgLength < 0.75;
|
|
2167
2167
|
return hasReasonableLengths && noBinaryChars && hasUniformLengths;
|
|
2168
2168
|
}
|
|
2169
|
-
// Count delimiters per line and check consistency
|
|
2170
|
-
|
|
2171
|
-
|
|
2169
|
+
// Count delimiters per line and check consistency. Delimiters inside a
|
|
2170
|
+
// double-quoted field are field content, not column separators (RFC 4180) —
|
|
2171
|
+
// a naive count inflates rows with quoted delimiters (e.g. `"Smith, John"`),
|
|
2172
|
+
// which used to make consistency collapse and reject a valid CSV.
|
|
2173
|
+
const counts = lines.map((line) => ContentHeuristicStrategy.countDelimitersOutsideQuotes(line, delimiter));
|
|
2172
2174
|
const firstCount = counts[0];
|
|
2173
2175
|
const consistentLines = counts.filter((c) => c === firstCount).length;
|
|
2174
2176
|
return consistentLines / lines.length >= 0.8;
|
|
2175
2177
|
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Count occurrences of `delimiter` in `line` that fall OUTSIDE double-quoted
|
|
2180
|
+
* fields, honoring RFC-4180 escaped quotes (`""`). Used by CSV detection so a
|
|
2181
|
+
* delimiter embedded in a quoted value is not mistaken for a column break.
|
|
2182
|
+
*/
|
|
2183
|
+
static countDelimitersOutsideQuotes(line, delimiter) {
|
|
2184
|
+
let count = 0;
|
|
2185
|
+
let inQuotes = false;
|
|
2186
|
+
for (let i = 0; i < line.length; i++) {
|
|
2187
|
+
const ch = line[i];
|
|
2188
|
+
if (ch === '"') {
|
|
2189
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
2190
|
+
i++; // escaped quote inside a quoted field — skip the pair
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
inQuotes = !inQuotes;
|
|
2194
|
+
}
|
|
2195
|
+
else if (ch === delimiter && !inQuotes) {
|
|
2196
|
+
count++;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return count;
|
|
2200
|
+
}
|
|
2176
2201
|
looksLikeJSON(text) {
|
|
2177
2202
|
// hasJsonMarkers now does full validation including JSON.parse
|
|
2178
2203
|
return hasJsonMarkers(text);
|
|
@@ -337,6 +337,20 @@ function calculateDataQualityScore(columns, warnings, totalRows) {
|
|
|
337
337
|
/**
|
|
338
338
|
* Analyze all columns in parsed CSV data
|
|
339
339
|
*/
|
|
340
|
+
/**
|
|
341
|
+
* Confidence for a CSV processing result, weighted by data quality.
|
|
342
|
+
*
|
|
343
|
+
* The result used to report a static `confidence: 100` regardless of how clean
|
|
344
|
+
* the data was. This ties confidence to `dataQualityScore` so it actually
|
|
345
|
+
* varies with quality factors (ragged rows, high null rate, type
|
|
346
|
+
* inconsistency): a cleanly-parsed CSV stays at 100, a messy one is pulled
|
|
347
|
+
* halfway toward its quality score. It never drops below 50 for a CSV that
|
|
348
|
+
* parsed — the *format* is certain, only the *data* is imperfect.
|
|
349
|
+
*/
|
|
350
|
+
function csvResultConfidence(dataQualityScore) {
|
|
351
|
+
const clamped = Math.max(0, Math.min(100, dataQualityScore));
|
|
352
|
+
return Math.round((100 + clamped) / 2);
|
|
353
|
+
}
|
|
340
354
|
function analyzeColumns(rows) {
|
|
341
355
|
if (rows.length === 0) {
|
|
342
356
|
return {
|
|
@@ -425,6 +439,33 @@ function detectHasHeaders(headerValues, dataRows) {
|
|
|
425
439
|
* - Lines with significantly different delimiter count than line 2
|
|
426
440
|
* - Lines that don't match CSV structure of subsequent lines
|
|
427
441
|
*/
|
|
442
|
+
/** Strip a leading UTF-8 BOM (U+FEFF) if present. */
|
|
443
|
+
function stripBom(text) {
|
|
444
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Count commas that fall OUTSIDE double-quoted fields (RFC 4180, escaped-quote
|
|
448
|
+
* aware). A quoted comma is field content, not a column separator, so counting
|
|
449
|
+
* naively would misclassify a quoted-comma header line.
|
|
450
|
+
*/
|
|
451
|
+
function countUnquotedCommas(line) {
|
|
452
|
+
let count = 0;
|
|
453
|
+
let inQuotes = false;
|
|
454
|
+
for (let i = 0; i < line.length; i++) {
|
|
455
|
+
const ch = line[i];
|
|
456
|
+
if (ch === '"') {
|
|
457
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
458
|
+
i++;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
inQuotes = !inQuotes;
|
|
462
|
+
}
|
|
463
|
+
else if (ch === "," && !inQuotes) {
|
|
464
|
+
count++;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return count;
|
|
468
|
+
}
|
|
428
469
|
function isMetadataLine(lines) {
|
|
429
470
|
if (!lines[0] || lines.length < 2) {
|
|
430
471
|
return false;
|
|
@@ -434,8 +475,8 @@ function isMetadataLine(lines) {
|
|
|
434
475
|
if (firstLine.match(/^sep=/i)) {
|
|
435
476
|
return true;
|
|
436
477
|
}
|
|
437
|
-
const firstCommaCount = (firstLine
|
|
438
|
-
const secondCommaCount = (secondLine
|
|
478
|
+
const firstCommaCount = countUnquotedCommas(firstLine);
|
|
479
|
+
const secondCommaCount = countUnquotedCommas(secondLine);
|
|
439
480
|
if (firstCommaCount === 0 && secondCommaCount > 0) {
|
|
440
481
|
return true;
|
|
441
482
|
}
|
|
@@ -574,7 +615,7 @@ export class CSVProcessor {
|
|
|
574
615
|
content: limitedCSV,
|
|
575
616
|
mimeType: "text/csv",
|
|
576
617
|
metadata: {
|
|
577
|
-
confidence:
|
|
618
|
+
confidence: csvResultConfidence(dataQualityScore),
|
|
578
619
|
size: content.length,
|
|
579
620
|
rowCount,
|
|
580
621
|
totalLines: limitedLines.length,
|
|
@@ -648,7 +689,7 @@ export class CSVProcessor {
|
|
|
648
689
|
content: formatted,
|
|
649
690
|
mimeType: "text/csv",
|
|
650
691
|
metadata: {
|
|
651
|
-
confidence:
|
|
692
|
+
confidence: csvResultConfidence(dataQualityScore),
|
|
652
693
|
size: content.length,
|
|
653
694
|
rowCount,
|
|
654
695
|
columnCount,
|
|
@@ -676,6 +717,9 @@ export class CSVProcessor {
|
|
|
676
717
|
* @returns Array of row objects
|
|
677
718
|
*/
|
|
678
719
|
static async parseCSVFile(filePath, maxRows = 1000) {
|
|
720
|
+
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
721
|
+
throw new Error("CSVProcessor.parseCSVFile: filePath must be a non-empty string");
|
|
722
|
+
}
|
|
679
723
|
const clampedMaxRows = Math.max(1, Math.min(10000, maxRows));
|
|
680
724
|
const fs = await import("fs");
|
|
681
725
|
logger.debug("[CSVProcessor] Starting file parsing", {
|
|
@@ -701,6 +745,8 @@ export class CSVProcessor {
|
|
|
701
745
|
}
|
|
702
746
|
});
|
|
703
747
|
lineReader.on("end", () => resolve());
|
|
748
|
+
// Metadata sniffing is best-effort — a read error here must not crash.
|
|
749
|
+
lineReader.on("error", () => resolve());
|
|
704
750
|
});
|
|
705
751
|
await fileHandle.close();
|
|
706
752
|
const hasMetadataLine = isMetadataLine(firstLines);
|
|
@@ -718,6 +764,12 @@ export class CSVProcessor {
|
|
|
718
764
|
source.destroy();
|
|
719
765
|
parser.destroy();
|
|
720
766
|
};
|
|
767
|
+
// .pipe() does not forward source errors (disk read/permission failures)
|
|
768
|
+
// to the parser, so listen on the source directly or it throws unhandled.
|
|
769
|
+
source.on("error", (error) => {
|
|
770
|
+
abort();
|
|
771
|
+
reject(new Error(`[CSVProcessor] CSV file read failed after ${count} row(s) (${filePath}): ${error.message}`));
|
|
772
|
+
});
|
|
721
773
|
source
|
|
722
774
|
.pipe(parser)
|
|
723
775
|
.on("data", (row) => {
|
|
@@ -739,7 +791,7 @@ export class CSVProcessor {
|
|
|
739
791
|
})
|
|
740
792
|
.on("error", (error) => {
|
|
741
793
|
logger.error("[CSVProcessor] File parsing failed:", error);
|
|
742
|
-
reject(error);
|
|
794
|
+
reject(new Error(`[CSVProcessor] CSV parsing failed after ${count} row(s): ${error.message}`));
|
|
743
795
|
});
|
|
744
796
|
});
|
|
745
797
|
}
|
|
@@ -752,13 +804,19 @@ export class CSVProcessor {
|
|
|
752
804
|
* @returns Array of row objects
|
|
753
805
|
*/
|
|
754
806
|
static async parseCSVString(csvString, maxRows = 1000) {
|
|
807
|
+
if (typeof csvString !== "string" || csvString.trim().length === 0) {
|
|
808
|
+
throw new Error("CSVProcessor.parseCSVString: csvString must be a non-empty string");
|
|
809
|
+
}
|
|
810
|
+
// Strip a leading UTF-8 BOM (common in Excel exports) so it does not glue
|
|
811
|
+
// onto the first column name and break downstream key lookups.
|
|
812
|
+
const normalized = stripBom(csvString);
|
|
755
813
|
const clampedMaxRows = Math.max(1, Math.min(10000, maxRows));
|
|
756
814
|
logger.debug("[CSVProcessor] Starting string parsing", {
|
|
757
|
-
inputLength:
|
|
815
|
+
inputLength: normalized.length,
|
|
758
816
|
maxRows: clampedMaxRows,
|
|
759
817
|
});
|
|
760
818
|
// Detect and skip metadata line
|
|
761
|
-
const lines = splitCsvLines(
|
|
819
|
+
const lines = splitCsvLines(normalized);
|
|
762
820
|
const hasMetadataLine = isMetadataLine(lines);
|
|
763
821
|
const csvData = hasMetadataLine
|
|
764
822
|
? lines.slice(1).join("\n")
|
|
@@ -775,6 +833,13 @@ export class CSVProcessor {
|
|
|
775
833
|
source.destroy();
|
|
776
834
|
parser.destroy();
|
|
777
835
|
};
|
|
836
|
+
// .pipe() does not forward source-stream errors to the destination, so a
|
|
837
|
+
// source failure must be listened for directly or it throws as an
|
|
838
|
+
// unhandled EventEmitter error and can crash the process.
|
|
839
|
+
source.on("error", (error) => {
|
|
840
|
+
abort();
|
|
841
|
+
reject(new Error(`[CSVProcessor] CSV source stream failed after ${count} row(s): ${error.message}`));
|
|
842
|
+
});
|
|
778
843
|
source
|
|
779
844
|
.pipe(parser)
|
|
780
845
|
.on("data", (row) => {
|
|
@@ -792,7 +857,7 @@ export class CSVProcessor {
|
|
|
792
857
|
})
|
|
793
858
|
.on("error", (error) => {
|
|
794
859
|
logger.error("[CSVProcessor] Parsing failed:", error);
|
|
795
|
-
reject(error);
|
|
860
|
+
reject(new Error(`[CSVProcessor] CSV parsing failed after ${count} row(s): ${error.message}`));
|
|
796
861
|
});
|
|
797
862
|
});
|
|
798
863
|
}
|
|
@@ -2166,13 +2166,38 @@ class ContentHeuristicStrategy {
|
|
|
2166
2166
|
const hasUniformLengths = stdDev / avgLength < 0.75;
|
|
2167
2167
|
return hasReasonableLengths && noBinaryChars && hasUniformLengths;
|
|
2168
2168
|
}
|
|
2169
|
-
// Count delimiters per line and check consistency
|
|
2170
|
-
|
|
2171
|
-
|
|
2169
|
+
// Count delimiters per line and check consistency. Delimiters inside a
|
|
2170
|
+
// double-quoted field are field content, not column separators (RFC 4180) —
|
|
2171
|
+
// a naive count inflates rows with quoted delimiters (e.g. `"Smith, John"`),
|
|
2172
|
+
// which used to make consistency collapse and reject a valid CSV.
|
|
2173
|
+
const counts = lines.map((line) => ContentHeuristicStrategy.countDelimitersOutsideQuotes(line, delimiter));
|
|
2172
2174
|
const firstCount = counts[0];
|
|
2173
2175
|
const consistentLines = counts.filter((c) => c === firstCount).length;
|
|
2174
2176
|
return consistentLines / lines.length >= 0.8;
|
|
2175
2177
|
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Count occurrences of `delimiter` in `line` that fall OUTSIDE double-quoted
|
|
2180
|
+
* fields, honoring RFC-4180 escaped quotes (`""`). Used by CSV detection so a
|
|
2181
|
+
* delimiter embedded in a quoted value is not mistaken for a column break.
|
|
2182
|
+
*/
|
|
2183
|
+
static countDelimitersOutsideQuotes(line, delimiter) {
|
|
2184
|
+
let count = 0;
|
|
2185
|
+
let inQuotes = false;
|
|
2186
|
+
for (let i = 0; i < line.length; i++) {
|
|
2187
|
+
const ch = line[i];
|
|
2188
|
+
if (ch === '"') {
|
|
2189
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
2190
|
+
i++; // escaped quote inside a quoted field — skip the pair
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
inQuotes = !inQuotes;
|
|
2194
|
+
}
|
|
2195
|
+
else if (ch === delimiter && !inQuotes) {
|
|
2196
|
+
count++;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return count;
|
|
2200
|
+
}
|
|
2176
2201
|
looksLikeJSON(text) {
|
|
2177
2202
|
// hasJsonMarkers now does full validation including JSON.parse
|
|
2178
2203
|
return hasJsonMarkers(text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.88.
|
|
3
|
+
"version": "9.88.8",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (58+ servers), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|