@juspay/neurolink 12.14.20 → 12.14.21

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.
@@ -1131,6 +1131,15 @@ export type TextGenerationOptions = {
1131
1131
  */
1132
1132
  images?: Array<Buffer | string | ImageWithAltText>;
1133
1133
  pdfFiles?: Array<Buffer | string>;
1134
+ /**
1135
+ * CSV files to inline as tabular text, with tool instructions appended.
1136
+ *
1137
+ * Declared here because `processExplicitCsvFiles` has always read it and
1138
+ * the internal `GenerateOptions` has always carried it — it was missing
1139
+ * only from the public type, so callers reaching the shipped behaviour had
1140
+ * to widen the type themselves to do it.
1141
+ */
1142
+ csvFiles?: Array<Buffer | string>;
1134
1143
  files?: Array<Buffer | string | FileWithMetadata>;
1135
1144
  /** Director Mode segments (2-10). When provided, Director Mode is activated. */
1136
1145
  segments?: DirectorSegment[];
@@ -187,6 +187,10 @@ export declare class FileDetector {
187
187
  * Load file from URL with automatic retry on transient network errors
188
188
  */
189
189
  private static loadFromURL;
190
+ /**
191
+ * Fetch a URL once and retain its response MIME type for detection.
192
+ */
193
+ private static loadUrl;
190
194
  /**
191
195
  * Load file from filesystem path
192
196
  */
@@ -284,7 +284,27 @@ export class FileDetector {
284
284
  [ATTR.FILE_SIZE_BYTES]: inputSizeBytes,
285
285
  },
286
286
  }, async (span) => {
287
- const detection = await FileDetector.detect(input, options);
287
+ // A URL used to be detected before it was loaded: MimeTypeStrategy
288
+ // issued a HEAD for Content-Type, then loadFromURL issued a GET for
289
+ // the body. Fetch it once and reuse the GET headers for the MIME
290
+ // strategy. A caller's explicit mimetypeHint remains authoritative,
291
+ // while a server header still comes after magic bytes.
292
+ const isUrlInput = typeof input === "string" &&
293
+ (input.startsWith("http://") || input.startsWith("https://"));
294
+ const loadedUrl = isUrlInput
295
+ ? await FileDetector.loadUrl(input, options)
296
+ : undefined;
297
+ const detection = loadedUrl
298
+ ? {
299
+ ...(await FileDetector.detect(loadedUrl.content, {
300
+ ...options,
301
+ filenameHint: options?.filenameHint ||
302
+ FileDetector.deriveInputFilename(input),
303
+ }, loadedUrl.contentType, options?.filenameHint ||
304
+ FileDetector.deriveInputFilename(input))),
305
+ source: "url",
306
+ }
307
+ : await FileDetector.detect(input, options);
288
308
  span.setAttribute(ATTR.FILE_CATEGORY, detection.type);
289
309
  span.setAttribute(ATTR.FILE_MIMETYPE, detection.mimeType || "unknown");
290
310
  span.setAttribute(ATTR.FILE_CONFIDENCE, detection.metadata.confidence);
@@ -292,7 +312,8 @@ export class FileDetector {
292
312
  // FD-018: Comprehensive fallback parsing for extension-less files
293
313
  if (options?.allowedTypes &&
294
314
  !options.allowedTypes.includes(detection.type)) {
295
- const content = await FileDetector.loadContent(input, detection, options);
315
+ const content = loadedUrl?.content ??
316
+ (await FileDetector.loadContent(input, detection, options));
296
317
  const errors = [];
297
318
  for (const allowedType of options.allowedTypes) {
298
319
  try {
@@ -322,7 +343,8 @@ export class FileDetector {
322
343
  FileDetector.setFileResultSpanAttributes(span, result, inputFilename, detection.type);
323
344
  return result;
324
345
  }
325
- const content = await FileDetector.loadContent(input, detection, options);
346
+ const content = loadedUrl?.content ??
347
+ (await FileDetector.loadContent(input, detection, options));
326
348
  const csvOptions = options?.csvOptions;
327
349
  const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider, options?.videoOptions);
328
350
  FileDetector.setFileResultSpanAttributes(span, result, inputFilename, detection.type);
@@ -619,7 +641,7 @@ export class FileDetector {
619
641
  * Detect file type using multi-strategy approach
620
642
  * Stops at first strategy with confidence >= threshold (default: 80%)
621
643
  */
622
- static async detect(input, options) {
644
+ static async detect(input, options, responseMimeType, extensionInput) {
623
645
  // Short-circuit on a trustworthy caller-provided mimetype hint. This is
624
646
  // the eager-path counterpart to FileReferenceRegistry.register()'s hint
625
647
  // handling — necessary for tiny files (<= TINY_MAX) that skip the lazy
@@ -648,13 +670,15 @@ export class FileDetector {
648
670
  const confidenceThreshold = options?.confidenceThreshold ?? 80;
649
671
  const strategies = [
650
672
  new MagicBytesStrategy(),
651
- new MimeTypeStrategy(),
673
+ new MimeTypeStrategy(responseMimeType),
652
674
  new ExtensionStrategy(),
653
675
  new ContentHeuristicStrategy(),
654
676
  ];
655
677
  let best = null;
656
678
  for (const strategy of strategies) {
657
- const result = await strategy.detect(input);
679
+ const result = await strategy.detect(strategy instanceof ExtensionStrategy && extensionInput
680
+ ? extensionInput
681
+ : input);
658
682
  if (!best || result.metadata.confidence > best.metadata.confidence) {
659
683
  best = result;
660
684
  }
@@ -1502,52 +1526,14 @@ export class FileDetector {
1502
1526
  * Load file from URL with automatic retry on transient network errors
1503
1527
  */
1504
1528
  static async loadFromURL(url, options) {
1529
+ return (await FileDetector.loadUrl(url, options)).content;
1530
+ }
1531
+ /**
1532
+ * Fetch a URL once and retain its response MIME type for detection.
1533
+ */
1534
+ static async loadUrl(url, options) {
1505
1535
  const maxSize = options?.maxSize || 200 * 1024 * 1024; // 200MB default (matches Curator memory-safety cap)
1506
1536
  const timeout = options?.timeout || FileDetector.DEFAULT_NETWORK_TIMEOUT;
1507
- // #317: pre-flight HEAD to reject an oversized file BEFORE downloading any
1508
- // body. content-length is advisory (chunked responses omit it), so a
1509
- // missing/invalid header — or a server that refuses HEAD — falls through to
1510
- // the streaming byte guard below; only a genuine oversize rejection stops
1511
- // the GET from ever running.
1512
- //
1513
- // #323: skip the pre-flight entirely when this exact URL was recently seen
1514
- // (its Content-Type is still cached, whether from a prior loadFromURL GET
1515
- // or a MimeTypeStrategy HEAD) — issuing a fresh HEAD here would defeat the
1516
- // whole point of that cache. The streaming byte guard in the GET below
1517
- // still enforces maxSize even without a pre-flight, so this doesn't remove
1518
- // the oversize protection — it only skips the redundant round-trip for a
1519
- // URL we've already been talking to within the last 60s.
1520
- if (getCachedUrlContentType(url, Date.now()) === undefined) {
1521
- try {
1522
- const head = await request(url, {
1523
- dispatcher: redirectFollowingDispatcher(5),
1524
- method: "HEAD",
1525
- headersTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
1526
- bodyTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
1527
- });
1528
- // Drain/close the (empty) HEAD body so the connection can be reused.
1529
- await head.body.dump();
1530
- // Only trust `content-length` on a genuine 2xx response. A non-2xx
1531
- // HEAD (redirect the dispatcher didn't follow, 403/404/405 "HEAD not
1532
- // allowed", 5xx, …) can still carry a stale/irrelevant
1533
- // `content-length` header — enforcing size off of that would reject
1534
- // (or silently pass) based on the wrong body. Treat any non-2xx HEAD
1535
- // as if the header were missing and fall through to the streaming
1536
- // GET guard below, which enforces maxSize independently either way.
1537
- if (head.statusCode >= 200 && head.statusCode < 300) {
1538
- const declaredLength = Number(head.headers["content-length"]);
1539
- if (Number.isFinite(declaredLength) && declaredLength > maxSize) {
1540
- throw new Error(`File too large: ${formatFileSize(declaredLength)} (max: ${formatFileSize(maxSize)})`);
1541
- }
1542
- }
1543
- }
1544
- catch (error) {
1545
- if (error instanceof Error && /File too large/.test(error.message)) {
1546
- throw error;
1547
- }
1548
- logger.debug(`[FileDetector] HEAD pre-flight skipped for ${redactUrlForError(url)}: ${sanitizeErrorCause(error).message}`);
1549
- }
1550
- }
1551
1537
  return withRetry(async () => {
1552
1538
  try {
1553
1539
  const response = await request(url, {
@@ -1561,9 +1547,21 @@ export class FileDetector {
1561
1547
  // not be echoed into a thrown error.
1562
1548
  throw new Error(`HTTP ${response.statusCode} fetching ${redactUrlForError(url)}`);
1563
1549
  }
1550
+ const contentType = response.headers["content-type"] || "";
1551
+ const declaredLength = Number(response.headers["content-length"]);
1552
+ if (Number.isFinite(declaredLength) && declaredLength > maxSize) {
1553
+ // `request()` resolves after response headers arrive. Close the
1554
+ // unread stream before throwing so a declared oversize response
1555
+ // never enters the body-reading loop or holds its connection.
1556
+ // Undici emits an abort error asynchronously when the stream is
1557
+ // destroyed, so consume that expected event before closing it.
1558
+ response.body.once("error", () => { });
1559
+ response.body.destroy();
1560
+ throw new Error(`File too large: ${formatFileSize(declaredLength)} (max: ${formatFileSize(maxSize)})`);
1561
+ }
1564
1562
  // #323: cache the Content-Type from this GET so a subsequent detection
1565
1563
  // of the same URL needs no HEAD.
1566
- setCachedUrlContentType(url, response.headers["content-type"] || "", Date.now());
1564
+ setCachedUrlContentType(url, contentType, Date.now());
1567
1565
  const chunks = [];
1568
1566
  let totalSize = 0;
1569
1567
  for await (const chunk of response.body) {
@@ -1573,7 +1571,7 @@ export class FileDetector {
1573
1571
  }
1574
1572
  chunks.push(chunk);
1575
1573
  }
1576
- return Buffer.concat(chunks);
1574
+ return { content: Buffer.concat(chunks), contentType };
1577
1575
  }
1578
1576
  catch (error) {
1579
1577
  // Node/undici DNS, TLS, and connect-timeout errors embed the full
@@ -2270,7 +2268,22 @@ class MagicBytesStrategy {
2270
2268
  * Detects file type from HTTP Content-Type headers
2271
2269
  */
2272
2270
  class MimeTypeStrategy {
2271
+ responseMimeType;
2272
+ constructor(responseMimeType) {
2273
+ this.responseMimeType = responseMimeType;
2274
+ }
2273
2275
  async detect(input) {
2276
+ if (this.responseMimeType !== undefined) {
2277
+ const contentType = this.responseMimeType;
2278
+ const type = this.mimeToFileType(contentType);
2279
+ return {
2280
+ type,
2281
+ mimeType: contentType.split(";")[0].trim(),
2282
+ extension: null,
2283
+ source: "url",
2284
+ metadata: { confidence: type !== "unknown" ? 85 : 0 },
2285
+ };
2286
+ }
2274
2287
  if (typeof input !== "string" || !this.isURL(input)) {
2275
2288
  return this.unknown();
2276
2289
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, statSync } from "fs";
2
2
  import { readFile as readFileAsync, stat as statAsync } from "fs/promises";
3
+ import pLimit from "p-limit";
3
4
  import { request } from "undici";
4
5
  import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
5
6
  import { MultimodalLogger, ProviderImageAdapter, } from "../adapters/providerImageAdapter.js";
@@ -570,6 +571,21 @@ const PREPROCESSED_FILES = new WeakMap();
570
571
  * page cache the read usually hits — detection has just read the same bytes.
571
572
  */
572
573
  const FILE_READ_TIMEOUT_MS = 30_000;
574
+ /**
575
+ * How many files may be detected and processed at once.
576
+ *
577
+ * A cap rather than unbounded `Promise.all`, and the cap is the point: this
578
+ * path admits files up to 100 MB, so N files in flight means N decoded buffers
579
+ * resident at once plus whatever each processor allocates on top. Turning a
580
+ * sequential loop into an unbounded fan-out trades a latency problem for a
581
+ * memory one, and on a large batch that is the worse of the two.
582
+ *
583
+ * Four is chosen to keep the worst case bounded rather than to saturate a
584
+ * disk: the win here is overlapping I/O wait, which most of it is, and the
585
+ * marginal gain past a handful of concurrent reads is small next to the
586
+ * marginal cost in resident bytes.
587
+ */
588
+ const FILE_PROCESSING_CONCURRENCY = 4;
573
589
  /**
574
590
  * Read a file input's bytes, or null when they cannot be had.
575
591
  *
@@ -835,6 +851,57 @@ function markFileProcessed(input, entry) {
835
851
  processed.add(entry);
836
852
  PREPROCESSED_FILES.set(input, processed);
837
853
  }
854
+ /**
855
+ * Detect and process one entry of the unified `files` array.
856
+ *
857
+ * Extracted so the same call can be made from the concurrent pass and from the
858
+ * sequential fall-through, rather than duplicated between them.
859
+ */
860
+ async function detectFileForUnifiedArray(file, options, provider, genericFileMaxSize) {
861
+ const rawFileInput = isFileWithMetadata(file) ? file.buffer : file;
862
+ // Forward the caller's mimetype hint (Slack/Curator-style
863
+ // extension-less buffers) so the eager path classifies correctly
864
+ // for tiny files — the lazy registry path has its own hint wiring.
865
+ const fileMimetypeHint = isFileWithMetadata(file) ? file.mimetype : undefined;
866
+ // The name has to travel the same way, and for the same reason: the
867
+ // line above unwraps the object to its buffer, so by the time
868
+ // detection resolves an extension there is no name left to read one
869
+ // from. Without this a `.tar` supplied as bytes-plus-name is
870
+ // unidentifiable — its "ustar" marker sits at byte 257, not at
871
+ // offset 0 — and reports "Could not extract content" for an archive
872
+ // that extracts perfectly when handed its filename.
873
+ const fileFilenameHint = isFileWithMetadata(file) ? file.filename : undefined;
874
+ return FileDetector.detectAndProcess(rawFileInput, {
875
+ maxSize: genericFileMaxSize,
876
+ allowedTypes: [
877
+ "csv",
878
+ "image",
879
+ "pdf",
880
+ "svg",
881
+ "video",
882
+ "audio",
883
+ "archive",
884
+ "xlsx",
885
+ "docx",
886
+ "pptx",
887
+ "text",
888
+ "unknown",
889
+ ],
890
+ csvOptions: options.csvOptions,
891
+ // #478: videos arrive through this unified `files` path, so this is
892
+ // where the CLI's frame/quality/format request has to be handed on.
893
+ videoOptions: options.videoOptions
894
+ ? {
895
+ frames: options.videoOptions.frames,
896
+ quality: options.videoOptions.quality,
897
+ format: options.videoOptions.format,
898
+ }
899
+ : undefined,
900
+ provider: provider,
901
+ mimetypeHint: fileMimetypeHint,
902
+ filenameHint: fileFilenameHint,
903
+ });
904
+ }
838
905
  export async function processUnifiedFilesArray(options, maxSize, provider) {
839
906
  options.input ??= {};
840
907
  if (!options.input.files || options.input.files.length === 0) {
@@ -882,15 +949,45 @@ export async function processUnifiedFilesArray(options, maxSize, provider) {
882
949
  inp2.text = inp2.text || "";
883
950
  let includedCount = 0;
884
951
  const fileRegistry = options.fileRegistry;
952
+ // Only detection runs concurrently, and only for files that will not
953
+ // take the lazy-registration branch.
954
+ //
955
+ // Everything else in this loop is order-dependent in a way that is not
956
+ // obvious from its shape. `appendDetectedFileResult` appends to
957
+ // `inp2.text`, `inp2.images` and `inp2.pdfFiles`, so running it out of
958
+ // order would reorder the prompt and the attachments against the files
959
+ // the caller supplied. `tryRegisterFileReference` mutates the shared
960
+ // registry and assigns reference ids from it. Both therefore stay in the
961
+ // sequential pass below; what moves off the critical path is
962
+ // `FileDetector.detectAndProcess`, which is where the read and the parse
963
+ // actually happen and which touches nothing shared.
964
+ //
965
+ // Lazy candidates are decided here — the predicate is synchronous — but
966
+ // are not detected up front, because registration usually means the
967
+ // bytes are never processed at all. Pre-detecting them would do exactly
968
+ // the work the lazy path exists to avoid. The rare fall-through, where
969
+ // registration is attempted and declines, detects inline below.
970
+ const isLazyCandidate = files.map((file) => Boolean(fileRegistry) &&
971
+ getFileSize(file) > SIZE_TIER_THRESHOLDS.TINY_MAX &&
972
+ !isEagerMultimodalFile(file));
973
+ const genericFileMaxSize = Math.max(maxSize, 100 * 1024 * 1024);
974
+ const detectLimit = pLimit(FILE_PROCESSING_CONCURRENCY);
975
+ // allSettled, not all: with `all` the rejection that surfaces is the one
976
+ // that happened FIRST IN TIME, so which file is blamed for a batch
977
+ // failure would depend on disk scheduling. The sequential loop reported
978
+ // the first failure BY INDEX, and the ordered walk below preserves that.
979
+ // It also means a second failure cannot become an unhandled rejection
980
+ // after the first has already thrown.
981
+ const detected = await Promise.allSettled(files.map((file, fileIdx) => isLazyCandidate[fileIdx]
982
+ ? Promise.resolve(undefined)
983
+ : detectLimit(() => detectFileForUnifiedArray(file, options, provider, genericFileMaxSize))));
885
984
  for (let fileIdx = 0; fileIdx < files.length; fileIdx++) {
886
985
  const file = files[fileIdx];
887
986
  const filename = extractFilename(file, fileIdx);
888
987
  try {
889
988
  // ─── Lazy file registration path ──────────────────────────────
890
989
  const fileSize = fileRegistry ? getFileSize(file) : 0;
891
- if (fileRegistry &&
892
- fileSize > SIZE_TIER_THRESHOLDS.TINY_MAX &&
893
- !isEagerMultimodalFile(file)) {
990
+ if (fileRegistry && isLazyCandidate[fileIdx]) {
894
991
  const registered = await tryRegisterFileReference(file, fileSize, fileRegistry, fileIdx);
895
992
  if (registered) {
896
993
  logger.info(`[NEUROLINK] File lazily registered: ${filename} (${fileSize} bytes) — deferred processing`);
@@ -900,54 +997,16 @@ export async function processUnifiedFilesArray(options, maxSize, provider) {
900
997
  }
901
998
  }
902
999
  // ─── Full processing path (current behavior) ──────────────────
903
- const genericFileMaxSize = Math.max(maxSize, 100 * 1024 * 1024);
904
- const rawFileInput = isFileWithMetadata(file) ? file.buffer : file;
905
- // Forward the caller's mimetype hint (Slack/Curator-style
906
- // extension-less buffers) so the eager path classifies correctly
907
- // for tiny files — the lazy registry path has its own hint wiring.
908
- const fileMimetypeHint = isFileWithMetadata(file)
909
- ? file.mimetype
910
- : undefined;
911
- // The name has to travel the same way, and for the same reason: the
912
- // line above unwraps the object to its buffer, so by the time
913
- // detection resolves an extension there is no name left to read one
914
- // from. Without this a `.tar` supplied as bytes-plus-name is
915
- // unidentifiable — its "ustar" marker sits at byte 257, not at
916
- // offset 0 — and reports "Could not extract content" for an archive
917
- // that extracts perfectly when handed its filename.
918
- const fileFilenameHint = isFileWithMetadata(file)
919
- ? file.filename
920
- : undefined;
921
- const result = await FileDetector.detectAndProcess(rawFileInput, {
922
- maxSize: genericFileMaxSize,
923
- allowedTypes: [
924
- "csv",
925
- "image",
926
- "pdf",
927
- "svg",
928
- "video",
929
- "audio",
930
- "archive",
931
- "xlsx",
932
- "docx",
933
- "pptx",
934
- "text",
935
- "unknown",
936
- ],
937
- csvOptions: options.csvOptions,
938
- // #478: videos arrive through this unified `files` path, so this is
939
- // where the CLI's frame/quality/format request has to be handed on.
940
- videoOptions: options.videoOptions
941
- ? {
942
- frames: options.videoOptions.frames,
943
- quality: options.videoOptions.quality,
944
- format: options.videoOptions.format,
945
- }
946
- : undefined,
947
- provider: provider,
948
- mimetypeHint: fileMimetypeHint,
949
- filenameHint: fileFilenameHint,
950
- });
1000
+ // Normally already resolved by the concurrent pass above. The inline
1001
+ // call is the fall-through: this file was a lazy candidate, so it was
1002
+ // deliberately not detected up front, and registration then declined
1003
+ // it. Detection has to happen somewhere, and here it is sequential.
1004
+ const outcome = detected[fileIdx];
1005
+ if (outcome.status === "rejected") {
1006
+ throw outcome.reason;
1007
+ }
1008
+ const result = outcome.value ??
1009
+ (await detectFileForUnifiedArray(file, options, provider, genericFileMaxSize));
951
1010
  await appendDetectedFileResult(result, file, options);
952
1011
  includedCount++;
953
1012
  // Log what content type was added to the message
@@ -1023,31 +1082,22 @@ async function processExplicitCsvFiles(options) {
1023
1082
  }
1024
1083
  logger.info(`[CSV] Processing ${options.input.csvFiles.length} explicit CSV file(s)`);
1025
1084
  options.input.text = options.input.text || "";
1026
- for (let i = 0; i < options.input.csvFiles.length; i++) {
1027
- const csvFile = options.input.csvFiles[i];
1028
- try {
1029
- const result = await FileDetector.detectAndProcess(csvFile, {
1030
- allowedTypes: ["csv"],
1031
- csvOptions: options.csvOptions,
1032
- });
1033
- const filename = extractFilename(csvFile, i);
1034
- const filePath = typeof csvFile === "string" ? csvFile : filename;
1035
- let csvSection = `\n\n## CSV Data from "${filename}":\n`;
1036
- if (result.metadata) {
1037
- const metadataText = formatCSVMetadata(result.metadata);
1038
- if (metadataText) {
1039
- csvSection += metadataText + `\n\n`;
1040
- }
1041
- }
1042
- // Put the actual CSV content BEFORE the tool instructions —
1043
- // buildCSVToolInstructions references "the CSV data shown above"
1044
- // and the trailing position keeps that reference accurate.
1045
- csvSection += result.content;
1046
- csvSection += buildCSVToolInstructions(filePath);
1047
- options.input.text += csvSection;
1048
- logger.info(`[CSV] ✅ Processed: ${filename}`);
1049
- }
1050
- catch (error) {
1085
+ const csvFiles = options.input.csvFiles;
1086
+ // Detection is the expensive half — a read plus a parse per file, and
1087
+ // previously each one waited for the last. It is also the only half that is
1088
+ // independent per file, so it is the only half that runs concurrently: the
1089
+ // sections below are appended to a single `text` string, so they stay in a
1090
+ // sequential index-ordered pass and the prompt reads identically.
1091
+ const limit = pLimit(FILE_PROCESSING_CONCURRENCY);
1092
+ const settled = await Promise.allSettled(csvFiles.map((csvFile) => limit(() => FileDetector.detectAndProcess(csvFile, {
1093
+ allowedTypes: ["csv"],
1094
+ csvOptions: options.csvOptions,
1095
+ }))));
1096
+ for (let i = 0; i < csvFiles.length; i++) {
1097
+ const csvFile = csvFiles[i];
1098
+ const outcome = settled[i];
1099
+ if (outcome.status === "rejected") {
1100
+ const error = outcome.reason;
1051
1101
  const filename = extractFilename(csvFile, i);
1052
1102
  const errMsg = error instanceof Error ? error.message : String(error);
1053
1103
  // #273: fail loud instead of embedding the error into the prompt text
@@ -1055,6 +1105,23 @@ async function processExplicitCsvFiles(options) {
1055
1105
  logger.error(`[CSV] ❌ Failed to process ${filename}: ${errMsg}`);
1056
1106
  throw ErrorFactory.csvProcessingFailed(filename, error instanceof Error ? error : new Error(errMsg));
1057
1107
  }
1108
+ const result = outcome.value;
1109
+ const filename = extractFilename(csvFile, i);
1110
+ const filePath = typeof csvFile === "string" ? csvFile : filename;
1111
+ let csvSection = `\n\n## CSV Data from "${filename}":\n`;
1112
+ if (result.metadata) {
1113
+ const metadataText = formatCSVMetadata(result.metadata);
1114
+ if (metadataText) {
1115
+ csvSection += metadataText + `\n\n`;
1116
+ }
1117
+ }
1118
+ // Put the actual CSV content BEFORE the tool instructions —
1119
+ // buildCSVToolInstructions references "the CSV data shown above"
1120
+ // and the trailing position keeps that reference accurate.
1121
+ csvSection += result.content;
1122
+ csvSection += buildCSVToolInstructions(filePath);
1123
+ options.input.text += csvSection;
1124
+ logger.info(`[CSV] ✅ Processed: ${filename}`);
1058
1125
  }
1059
1126
  }
1060
1127
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.14.20",
3
+ "version": "12.14.21",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {