@juspay/neurolink 10.11.2 → 10.11.3

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 (39) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/adapters/audioFormatSupport.d.ts +53 -0
  3. package/dist/adapters/audioFormatSupport.js +200 -0
  4. package/dist/browser/neurolink.min.js +398 -397
  5. package/dist/lib/adapters/audioFormatSupport.d.ts +53 -0
  6. package/dist/lib/adapters/audioFormatSupport.js +201 -0
  7. package/dist/lib/processors/archive/ArchiveProcessor.d.ts +37 -0
  8. package/dist/lib/processors/archive/ArchiveProcessor.js +347 -32
  9. package/dist/lib/providers/googleAiStudio/client.d.ts +17 -0
  10. package/dist/lib/providers/googleAiStudio/client.js +45 -19
  11. package/dist/lib/providers/googleNativeGemini3/utils.d.ts +22 -1
  12. package/dist/lib/providers/googleNativeGemini3/utils.js +54 -0
  13. package/dist/lib/providers/googleVertex/client.js +3 -0
  14. package/dist/lib/types/file.d.ts +41 -0
  15. package/dist/lib/types/generate.d.ts +12 -1
  16. package/dist/lib/types/processor.d.ts +20 -1
  17. package/dist/lib/types/providers.d.ts +7 -0
  18. package/dist/lib/utils/fileDetector.d.ts +27 -0
  19. package/dist/lib/utils/fileDetector.js +130 -7
  20. package/dist/lib/utils/imageProcessor.js +31 -0
  21. package/dist/lib/utils/messageBuilder.d.ts +0 -9
  22. package/dist/lib/utils/messageBuilder.js +380 -56
  23. package/dist/processors/archive/ArchiveProcessor.d.ts +37 -0
  24. package/dist/processors/archive/ArchiveProcessor.js +347 -32
  25. package/dist/providers/googleAiStudio/client.d.ts +17 -0
  26. package/dist/providers/googleAiStudio/client.js +45 -19
  27. package/dist/providers/googleNativeGemini3/utils.d.ts +22 -1
  28. package/dist/providers/googleNativeGemini3/utils.js +54 -0
  29. package/dist/providers/googleVertex/client.js +3 -0
  30. package/dist/types/file.d.ts +41 -0
  31. package/dist/types/generate.d.ts +12 -1
  32. package/dist/types/processor.d.ts +20 -1
  33. package/dist/types/providers.d.ts +7 -0
  34. package/dist/utils/fileDetector.d.ts +27 -0
  35. package/dist/utils/fileDetector.js +130 -7
  36. package/dist/utils/imageProcessor.js +31 -0
  37. package/dist/utils/messageBuilder.d.ts +0 -9
  38. package/dist/utils/messageBuilder.js +380 -56
  39. package/package.json +1 -1
@@ -193,6 +193,43 @@ export declare class ArchiveProcessor extends BaseFileProcessor<ProcessedArchive
193
193
  * @param entries - Previously extracted entry metadata
194
194
  * @returns Map of entry name to extracted text content
195
195
  */
196
+ /**
197
+ * Whether an entry name looks like something worth inlining as text.
198
+ *
199
+ * Shared by the ZIP and TAR paths so the two cannot drift into disagreeing
200
+ * about which members are worth reading — they did, because only ZIP had the
201
+ * rule at all.
202
+ */
203
+ private isExtractableEntryName;
204
+ /**
205
+ * Decode bytes to text, or null when they are not text.
206
+ *
207
+ * A NUL byte in the first 512 bytes, or a high proportion of replacement
208
+ * characters after decoding, means binary — inlining that would spend the
209
+ * extraction budget on mojibake.
210
+ */
211
+ private decodeEntryText;
212
+ /**
213
+ * Decompress a single-stream archive (.bz2, .xz, .zst).
214
+ *
215
+ * Node ships zstd from v22.15/23, so that one needs no help. bzip2 and xz
216
+ * have no Node binding, and adding a native module for them would make an
217
+ * optional format a build-time dependency for every consumer — so the system
218
+ * tools are used when present, the same soft-dependency arrangement this
219
+ * codebase already has with ffmpeg. Absent tooling returns null and the
220
+ * caller reports the format as unsupported *on this machine* rather than
221
+ * unsupported in principle.
222
+ */
223
+ private decompressSingleStream;
224
+ /**
225
+ * Extract a single-stream archive: decompress, then treat the result as a
226
+ * TAR when it is one and as a lone file otherwise.
227
+ *
228
+ * The tar check matters because `.tar.xz` and `.tar.zst` are how these
229
+ * formats are usually met — reporting one opaque "decompressed-content" blob
230
+ * for an archive of forty files would be technically true and useless.
231
+ */
232
+ private extractSingleStreamEntries;
196
233
  private extractEntryContents;
197
234
  /**
198
235
  * Build a structured text description of the archive for LLM consumption.
@@ -143,10 +143,27 @@ const SUPPORTED_ARCHIVE_MIME_TYPES = [
143
143
  "application/x-gzip",
144
144
  "application/x-compressed-tar",
145
145
  "application/x-bzip2",
146
+ // XZ and Zstandard, added alongside the extensions above: a caller that
147
+ // uploads bytes with a declared mimetype and no filename was rejected at the
148
+ // type gate for the two formats this change teaches the processor to read.
149
+ "application/x-xz",
150
+ "application/zstd",
146
151
  "application/java-archive",
147
152
  ];
153
+ /**
154
+ * External decompressor per single-stream format.
155
+ *
156
+ * One map rather than a ternary repeated at each use: the decompressor and the
157
+ * error message that names it were derived separately, so they could disagree
158
+ * about which binary the user is being told to install.
159
+ */
160
+ const SINGLE_STREAM_TOOLS = {
161
+ bz2: "bzip2",
162
+ xz: "xz",
163
+ zst: "zstd",
164
+ };
148
165
  /** File extensions recognized as archive formats */
149
- const SUPPORTED_ARCHIVE_EXTENSIONS = [".zip", ".tar", ".gz", ".tgz", ".bz2", ".tbz2", ".jar"];
166
+ const SUPPORTED_ARCHIVE_EXTENSIONS = [".zip", ".tar", ".gz", ".tgz", ".bz2", ".tbz2", ".jar", ".xz", ".txz", ".zst", ".tzst"];
150
167
  // =============================================================================
151
168
  // MAGIC BYTE SIGNATURES
152
169
  // =============================================================================
@@ -163,8 +180,20 @@ const MAGIC_BYTES = {
163
180
  ZIP_SPANNED: [0x50, 0x4b, 0x07, 0x08],
164
181
  /** GZIP: \x1f\x8b */
165
182
  GZIP: [0x1f, 0x8b],
166
- /** BZIP2: BZ */
167
- BZIP2: [0x42, 0x5a],
183
+ /**
184
+ * BZIP2: `BZh` — all three bytes, not just `BZ`.
185
+ *
186
+ * Two bytes was harmless while every BZIP2 match was rejected outright as an
187
+ * unsupported format. Now that a match actually spawns `bzip2`, any buffer
188
+ * beginning with the ASCII letters "BZ" is handed to the decompressor and
189
+ * comes back reported as a corrupt BZ2 stream — a confident wrong answer
190
+ * about a file that was never bzip2 at all.
191
+ */
192
+ BZIP2: [0x42, 0x5a, 0x68],
193
+ /** XZ: \xfd7zXZ\x00 */
194
+ XZ: [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00],
195
+ /** Zstandard frame: \x28\xb5\x2f\xfd */
196
+ ZSTD: [0x28, 0xb5, 0x2f, 0xfd],
168
197
  /** RAR: Rar!\x1a\x07 */
169
198
  RAR: [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07],
170
199
  /** 7-Zip: 7z\xbc\xaf\x27\x1c */
@@ -315,7 +344,7 @@ export class ArchiveProcessor extends BaseFileProcessor {
315
344
  error: this.createError(FileErrorCode.UNSUPPORTED_TYPE, {
316
345
  format,
317
346
  reason: `${format.toUpperCase()} archives are not yet supported. Please convert to ZIP or TAR format.`,
318
- supportedFormats: "ZIP, TAR, TAR.GZ, GZ",
347
+ supportedFormats: "ZIP, TAR, TAR.GZ, TAR.BZ2, GZ, BZ2, XZ, ZST, JAR",
319
348
  }),
320
349
  };
321
350
  }
@@ -356,7 +385,9 @@ export class ArchiveProcessor extends BaseFileProcessor {
356
385
  // Step 9: Extract content from text-based entries (Phase 2 sub-processing)
357
386
  // For ZIP archives, extract and include content from small text-based files.
358
387
  // Skips nested archives and binary files for safety.
359
- let extractedContents = new Map();
388
+ // ZIP re-parses to pull member text; every other format collects it while
389
+ // decompressing, because their bytes stream past exactly once.
390
+ let extractedContents = extractionResult.contents ?? new Map();
360
391
  if (format === "zip") {
361
392
  extractedContents = await this.extractEntryContents(buffer, entries);
362
393
  }
@@ -417,6 +448,18 @@ export class ArchiveProcessor extends BaseFileProcessor {
417
448
  // Could still be a tar.gz without the extension - we'll detect during extraction
418
449
  return "gz";
419
450
  }
451
+ // Bzip2 wraps a tar just as often, and now that a BZIP2 magic match
452
+ // resolves to a real format rather than being rejected outright, the
453
+ // same filename check has to run here too. Without it every genuine
454
+ // `.tar.bz2` reported its format as "BZ2" to the model — extraction was
455
+ // right, the label was not — and the `.tar.bz2` extension mapping below
456
+ // became unreachable for any well-formed file.
457
+ if (magicFormat === "bz2") {
458
+ const ext = filename.toLowerCase();
459
+ return ext.endsWith(".tar.bz2") || ext.endsWith(".tbz2")
460
+ ? "tar.bz2"
461
+ : "bz2";
462
+ }
420
463
  return magicFormat;
421
464
  }
422
465
  // Fallback to extension-based detection
@@ -451,9 +494,19 @@ export class ArchiveProcessor extends BaseFileProcessor {
451
494
  if (this.matchesMagic(buffer, MAGIC_BYTES.GZIP)) {
452
495
  return "gz";
453
496
  }
454
- // Check for BZIP2 (2 bytes)
497
+ // Check for BZIP2 (3 bytes: "BZh"). A bare .bz2 is a single compressed
498
+ // file; the caller decides whether the decompressed bytes turn out to be a
499
+ // tar.
455
500
  if (this.matchesMagic(buffer, MAGIC_BYTES.BZIP2)) {
456
- return "tar.bz2";
501
+ return "bz2";
502
+ }
503
+ // Check for XZ (6 bytes)
504
+ if (buffer.length >= 6 && this.matchesMagic(buffer, MAGIC_BYTES.XZ)) {
505
+ return "xz";
506
+ }
507
+ // Check for Zstandard (4 bytes)
508
+ if (buffer.length >= 4 && this.matchesMagic(buffer, MAGIC_BYTES.ZSTD)) {
509
+ return "zst";
457
510
  }
458
511
  return null;
459
512
  }
@@ -478,7 +531,21 @@ export class ArchiveProcessor extends BaseFileProcessor {
478
531
  return "gz";
479
532
  }
480
533
  if (lowerFilename.endsWith(".bz2")) {
481
- return "tar.bz2";
534
+ // A bare `.bz2` is a single compressed file, not a tarball. Reporting it
535
+ // as "tar.bz2" sent it down a branch that refused the format outright.
536
+ return "bz2";
537
+ }
538
+ if (lowerFilename.endsWith(".tar.xz") || lowerFilename.endsWith(".txz")) {
539
+ return "xz";
540
+ }
541
+ if (lowerFilename.endsWith(".xz")) {
542
+ return "xz";
543
+ }
544
+ if (lowerFilename.endsWith(".tar.zst") || lowerFilename.endsWith(".tzst")) {
545
+ return "zst";
546
+ }
547
+ if (lowerFilename.endsWith(".zst")) {
548
+ return "zst";
482
549
  }
483
550
  if (lowerFilename.endsWith(".zip") || lowerFilename.endsWith(".jar")) {
484
551
  return "zip";
@@ -526,16 +593,12 @@ export class ArchiveProcessor extends BaseFileProcessor {
526
593
  case "tar.gz":
527
594
  return this.extractTarGzEntries(buffer);
528
595
  case "tar.bz2":
529
- return {
530
- success: false,
531
- entries: [],
532
- securityWarnings: [],
533
- error: this.createError(FileErrorCode.UNSUPPORTED_TYPE, {
534
- format: "tar.bz2",
535
- reason: "TAR.BZ2 archives are not yet supported. Please convert to ZIP or TAR.GZ format.",
536
- supportedFormats: "ZIP, TAR, TAR.GZ, GZ",
537
- }),
538
- };
596
+ case "bz2":
597
+ return this.extractSingleStreamEntries(buffer, "bz2");
598
+ case "xz":
599
+ return this.extractSingleStreamEntries(buffer, "xz");
600
+ case "zst":
601
+ return this.extractSingleStreamEntries(buffer, "zst");
539
602
  case "gz":
540
603
  return this.extractGzEntries(buffer);
541
604
  default:
@@ -546,7 +609,7 @@ export class ArchiveProcessor extends BaseFileProcessor {
546
609
  error: this.createError(FileErrorCode.UNSUPPORTED_TYPE, {
547
610
  format,
548
611
  reason: `${format} archives are not supported`,
549
- supportedFormats: "ZIP, TAR, TAR.GZ, GZ",
612
+ supportedFormats: "ZIP, TAR, TAR.GZ, TAR.BZ2, GZ, BZ2, XZ, ZST, JAR",
550
613
  }),
551
614
  };
552
615
  }
@@ -767,8 +830,10 @@ export class ArchiveProcessor extends BaseFileProcessor {
767
830
  return new Promise((resolve) => {
768
831
  const entries = [];
769
832
  const securityWarnings = [];
833
+ const contents = new Map();
770
834
  let entryCount = 0;
771
835
  let cumulativeSize = 0;
836
+ let extractedBytes = 0;
772
837
  let earlyError = null;
773
838
  const extract = tarStream.extract();
774
839
  extract.on("entry", (header, stream, next) => {
@@ -827,6 +892,33 @@ export class ArchiveProcessor extends BaseFileProcessor {
827
892
  compressedSize: 0, // TAR doesn't compress individual entries
828
893
  isDirectory,
829
894
  });
895
+ // Capture the text of eligible members as they go past.
896
+ //
897
+ // A listing alone is not an answer. Asked what a .tar contains, the
898
+ // model could previously only recite filenames and sizes — the same
899
+ // archive as a .zip returned its text, because only the ZIP path
900
+ // extracted anything. A tar entry's bytes are available exactly once,
901
+ // here, while the stream is open; re-reading later would mean parsing
902
+ // the whole archive a second time.
903
+ if (!isDirectory &&
904
+ this.isExtractableEntryName(entryName) &&
905
+ entrySize > 0 &&
906
+ entrySize <= ARCHIVE_CONFIG.MAX_EXTRACT_ENTRY_SIZE &&
907
+ contents.size < ARCHIVE_CONFIG.MAX_EXTRACT_ENTRIES &&
908
+ extractedBytes + entrySize <= ARCHIVE_CONFIG.MAX_TOTAL_EXTRACT_SIZE) {
909
+ const chunks = [];
910
+ stream.on("data", (chunk) => chunks.push(chunk));
911
+ stream.on("end", () => {
912
+ const text = this.decodeEntryText(Buffer.concat(chunks));
913
+ if (text !== null) {
914
+ contents.set(entryName, text);
915
+ extractedBytes += entrySize;
916
+ }
917
+ next();
918
+ });
919
+ stream.on("error", () => next());
920
+ return;
921
+ }
830
922
  // Consume the stream without buffering (we only need metadata)
831
923
  stream.resume();
832
924
  next();
@@ -841,7 +933,7 @@ export class ArchiveProcessor extends BaseFileProcessor {
841
933
  });
842
934
  }
843
935
  else {
844
- resolve({ success: true, entries, securityWarnings });
936
+ resolve({ success: true, entries, securityWarnings, contents });
845
937
  }
846
938
  });
847
939
  extract.on("error", (err) => {
@@ -929,7 +1021,14 @@ export class ArchiveProcessor extends BaseFileProcessor {
929
1021
  isDirectory: false,
930
1022
  },
931
1023
  ];
932
- return { success: true, entries, securityWarnings };
1024
+ // The decompressed bytes ARE the content here; a listing that says
1025
+ // "decompressed-content (1.02 MB)" answers nothing about the file.
1026
+ const contents = new Map();
1027
+ const gzText = this.decodeEntryText(decompressed);
1028
+ if (gzText !== null) {
1029
+ contents.set(innerFilename, gzText.slice(0, ARCHIVE_CONFIG.MAX_EXTRACT_ENTRY_SIZE));
1030
+ }
1031
+ return { success: true, entries, securityWarnings, contents };
933
1032
  }
934
1033
  catch (error) {
935
1034
  return {
@@ -999,6 +1098,232 @@ export class ArchiveProcessor extends BaseFileProcessor {
999
1098
  * @param entries - Previously extracted entry metadata
1000
1099
  * @returns Map of entry name to extracted text content
1001
1100
  */
1101
+ /**
1102
+ * Whether an entry name looks like something worth inlining as text.
1103
+ *
1104
+ * Shared by the ZIP and TAR paths so the two cannot drift into disagreeing
1105
+ * about which members are worth reading — they did, because only ZIP had the
1106
+ * rule at all.
1107
+ */
1108
+ isExtractableEntryName(name) {
1109
+ const ext = path.extname(name).toLowerCase();
1110
+ if (ARCHIVE_CONFIG.EXTRACTABLE_EXTENSIONS.has(ext)) {
1111
+ return true;
1112
+ }
1113
+ const base = path.basename(name).toLowerCase();
1114
+ return (base === "readme" ||
1115
+ base === "license" ||
1116
+ base === "makefile" ||
1117
+ base === "dockerfile");
1118
+ }
1119
+ /**
1120
+ * Decode bytes to text, or null when they are not text.
1121
+ *
1122
+ * A NUL byte in the first 512 bytes, or a high proportion of replacement
1123
+ * characters after decoding, means binary — inlining that would spend the
1124
+ * extraction budget on mojibake.
1125
+ */
1126
+ decodeEntryText(data) {
1127
+ if (!data || data.length === 0) {
1128
+ return null;
1129
+ }
1130
+ if (data.subarray(0, Math.min(512, data.length)).includes(0)) {
1131
+ return null;
1132
+ }
1133
+ const text = data.toString("utf-8");
1134
+ const replacements = (text.match(/\ufffd/g) || []).length;
1135
+ return replacements > text.length * 0.05 ? null : text;
1136
+ }
1137
+ /**
1138
+ * Decompress a single-stream archive (.bz2, .xz, .zst).
1139
+ *
1140
+ * Node ships zstd from v22.15/23, so that one needs no help. bzip2 and xz
1141
+ * have no Node binding, and adding a native module for them would make an
1142
+ * optional format a build-time dependency for every consumer — so the system
1143
+ * tools are used when present, the same soft-dependency arrangement this
1144
+ * codebase already has with ffmpeg. Absent tooling returns null and the
1145
+ * caller reports the format as unsupported *on this machine* rather than
1146
+ * unsupported in principle.
1147
+ */
1148
+ async decompressSingleStream(buffer, format) {
1149
+ if (format === "zst") {
1150
+ // Node gained zstd in v22.15/23. Older runtimes simply lack the export,
1151
+ // so it is probed at runtime rather than assumed from the typings —
1152
+ // which is also why this is a property check on an `unknown` module
1153
+ // instead of a cast asserting it exists.
1154
+ const zlibModule = await import("zlib");
1155
+ const candidate = typeof zlibModule === "object" && zlibModule !== null
1156
+ ? zlibModule.zstdDecompress
1157
+ : undefined;
1158
+ if (typeof candidate === "function") {
1159
+ const zstdDecompress = candidate;
1160
+ return await new Promise((resolve) => {
1161
+ // Bounded at the decoder rather than after the fact. The size guard
1162
+ // downstream only runs once the whole buffer exists, which is too
1163
+ // late for a zip-bomb: a few KB of zstd expands to gigabytes and the
1164
+ // allocation is what hurts. `maxOutputLength` makes it fail fast.
1165
+ zstdDecompress(buffer, { maxOutputLength: ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE }, (err, res) => resolve(err
1166
+ ? {
1167
+ status: err.code ===
1168
+ "ERR_BUFFER_TOO_LARGE"
1169
+ ? "too-large"
1170
+ : "failed",
1171
+ }
1172
+ : { status: "ok", buffer: res }));
1173
+ });
1174
+ }
1175
+ }
1176
+ const tool = SINGLE_STREAM_TOOLS[format];
1177
+ try {
1178
+ const { execFile } = await import("node:child_process");
1179
+ return await new Promise((resolve) => {
1180
+ // ENOENT means the binary is absent; anything else means it ran and
1181
+ // could not do the job. Only the former justifies telling the caller to
1182
+ // install something.
1183
+ //
1184
+ // Read off the error itself rather than a flag set by the `error`
1185
+ // listener below, because execFile invokes this callback BEFORE that
1186
+ // listener runs — measured, not assumed:
1187
+ // callback(missing=false, code=ENOENT) → errorListener(code=ENOENT)
1188
+ // The promise is already settled by the time the listener could set the
1189
+ // flag, so a missing decompressor reported itself as a corrupt stream
1190
+ // and told the user to re-upload a perfectly good file.
1191
+ const isMissingTool = (error) => error?.code === "ENOENT";
1192
+ // Hitting `maxBuffer` is the zip-bomb guard firing, not a corrupt
1193
+ // stream: it will fail identically on every retry, so it must not be
1194
+ // reported as retryable the way a truncated upload is.
1195
+ const isTooLarge = (error) => error?.code ===
1196
+ "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
1197
+ const child = execFile(tool, ["-dc"], {
1198
+ encoding: "buffer",
1199
+ maxBuffer: ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE,
1200
+ // Without this a crafted or truncated stream that makes the tool
1201
+ // block forever never fires the callback, so the promise never
1202
+ // settles and the request that awaits it hangs indefinitely.
1203
+ timeout: ARCHIVE_CONFIG.TIMEOUT_MS,
1204
+ }, (error, stdout) => {
1205
+ // Only a non-zero exit means failure. Empty output does not:
1206
+ // compressing an empty file is legal and round-trips — a 32-byte
1207
+ // `empty.txt.xz` decompresses to nothing with exit 0 — so treating
1208
+ // zero length as an error reported a valid archive as corrupt. The
1209
+ // in-process zstd path above already keyed on the error alone, so
1210
+ // the two backends disagreed about the same input.
1211
+ if (error || !stdout) {
1212
+ resolve({
1213
+ status: isMissingTool(error)
1214
+ ? "tool-unavailable"
1215
+ : isTooLarge(error)
1216
+ ? "too-large"
1217
+ : "failed",
1218
+ });
1219
+ return;
1220
+ }
1221
+ resolve({ status: "ok", buffer: Buffer.from(stdout) });
1222
+ });
1223
+ // Kept as a backstop for spawn failures that never reach the
1224
+ // callback. Whichever settles first wins; both now classify the same
1225
+ // way, so the outcome no longer depends on the order.
1226
+ child.on("error", (error) => {
1227
+ resolve({
1228
+ status: isMissingTool(error) ? "tool-unavailable" : "failed",
1229
+ });
1230
+ });
1231
+ // Swallowed, deliberately not resolved from. A stdin write fails with
1232
+ // EPIPE precisely because the child already died — including when Node
1233
+ // killed it for exceeding `maxBuffer`, which is the zip-bomb guard.
1234
+ // Resolving here raced the exec callback and, when it won, downgraded a
1235
+ // "too-large" verdict to a retryable "failed", telling the caller to
1236
+ // retry a bomb. The callback is the authoritative signal: it always
1237
+ // fires once the process exits or fails to spawn, and the `timeout`
1238
+ // above bounds the wait. This listener exists only so an unhandled
1239
+ // 'error' event cannot take the process down.
1240
+ child.stdin?.on("error", () => undefined);
1241
+ child.stdin?.end(buffer);
1242
+ });
1243
+ }
1244
+ catch {
1245
+ return { status: "tool-unavailable" };
1246
+ }
1247
+ }
1248
+ /**
1249
+ * Extract a single-stream archive: decompress, then treat the result as a
1250
+ * TAR when it is one and as a lone file otherwise.
1251
+ *
1252
+ * The tar check matters because `.tar.xz` and `.tar.zst` are how these
1253
+ * formats are usually met — reporting one opaque "decompressed-content" blob
1254
+ * for an archive of forty files would be technically true and useless.
1255
+ */
1256
+ async extractSingleStreamEntries(buffer, format) {
1257
+ const result = await this.decompressSingleStream(buffer, format);
1258
+ if (result.status !== "ok") {
1259
+ const tool = SINGLE_STREAM_TOOLS[format];
1260
+ // Three distinct outcomes, and the codes carry contracts a caller acts
1261
+ // on. UNSUPPORTED_TYPE is `retryable: false` and advises converting the
1262
+ // file; DECOMPRESSION_FAILED is `retryable: true` and advises
1263
+ // re-uploading; SECURITY_VALIDATION_FAILED is how the GZIP and TAR.GZ
1264
+ // paths in this file already report a decompression bomb. `retryable` is
1265
+ // read downstream by `isRetryableErrorCode`, so a stream that tripped the
1266
+ // decoder's size cap must not be advertised as worth retrying — it will
1267
+ // fail identically every time — and a merely truncated upload must not be
1268
+ // reported as an unsupported format.
1269
+ const code = result.status === "tool-unavailable"
1270
+ ? FileErrorCode.UNSUPPORTED_TYPE
1271
+ : result.status === "too-large"
1272
+ ? FileErrorCode.SECURITY_VALIDATION_FAILED
1273
+ : FileErrorCode.DECOMPRESSION_FAILED;
1274
+ return {
1275
+ success: false,
1276
+ entries: [],
1277
+ securityWarnings: [],
1278
+ error: this.createError(code, {
1279
+ format,
1280
+ reason: result.status === "tool-unavailable"
1281
+ ? `${format.toUpperCase()} could not be decompressed. Node has no built-in ` +
1282
+ `decoder for it and the "${tool}" command is unavailable on this machine.`
1283
+ : result.status === "too-large"
1284
+ ? `${format.toUpperCase()} expands beyond the ` +
1285
+ `${this.formatSizeMB(ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE)} MB ` +
1286
+ `decompression limit and was refused before it could be read.`
1287
+ : `${format.toUpperCase()} could not be decompressed. The stream is ` +
1288
+ `corrupt or truncated.`,
1289
+ supportedFormats: "ZIP, TAR, TAR.GZ, TAR.BZ2, GZ, BZ2, XZ, ZST, JAR",
1290
+ }),
1291
+ };
1292
+ }
1293
+ const decompressed = result.buffer;
1294
+ if (decompressed.length > ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE) {
1295
+ return {
1296
+ success: false,
1297
+ entries: [],
1298
+ securityWarnings: [],
1299
+ error: this.createError(FileErrorCode.SECURITY_VALIDATION_FAILED, {
1300
+ reason: `Decompressed size (${this.formatSizeMB(decompressed.length)} MB) exceeds limit (${this.formatSizeMB(ARCHIVE_SECURITY.MAX_DECOMPRESSED_SIZE)} MB)`,
1301
+ }),
1302
+ };
1303
+ }
1304
+ if (this.looksLikeTar(decompressed)) {
1305
+ const tarStream = await import("tar-stream");
1306
+ return await this.parseTarStream(tarStream, decompressed);
1307
+ }
1308
+ const contents = new Map();
1309
+ const text = this.decodeEntryText(decompressed);
1310
+ if (text !== null) {
1311
+ contents.set("decompressed-content", text.slice(0, ARCHIVE_CONFIG.MAX_EXTRACT_ENTRY_SIZE));
1312
+ }
1313
+ return {
1314
+ success: true,
1315
+ entries: [
1316
+ {
1317
+ name: "decompressed-content",
1318
+ uncompressedSize: decompressed.length,
1319
+ compressedSize: buffer.length,
1320
+ isDirectory: false,
1321
+ },
1322
+ ],
1323
+ securityWarnings: [],
1324
+ contents,
1325
+ };
1326
+ }
1002
1327
  async extractEntryContents(buffer, entries) {
1003
1328
  const contents = new Map();
1004
1329
  try {
@@ -1016,17 +1341,7 @@ export class ArchiveProcessor extends BaseFileProcessor {
1016
1341
  if (e.uncompressedSize === 0) {
1017
1342
  return false;
1018
1343
  }
1019
- const ext = path.extname(e.name).toLowerCase();
1020
- // Check by extension
1021
- if (ARCHIVE_CONFIG.EXTRACTABLE_EXTENSIONS.has(ext)) {
1022
- return true;
1023
- }
1024
- // Check for common extensionless config files
1025
- const basename = path.basename(e.name).toLowerCase();
1026
- if (basename === "readme" || basename === "license" || basename === "makefile" || basename === "dockerfile") {
1027
- return true;
1028
- }
1029
- return false;
1344
+ return this.isExtractableEntryName(e.name);
1030
1345
  })
1031
1346
  // Sort: smaller files first (more likely to fit), then by name
1032
1347
  .sort((a, b) => a.uncompressedSize - b.uncompressedSize);
@@ -30,6 +30,23 @@ export declare class GoogleAIStudioProvider extends BaseProvider {
30
30
  * Estimate token count from text using centralized estimation with provider multipliers
31
31
  */
32
32
  private estimateTokenCount;
33
+ /**
34
+ * Run the file preprocessing this provider's native paths depend on.
35
+ *
36
+ * AI Studio overrides both `generate()` and `executeStream()` and routes
37
+ * straight to the native SDK, so neither reaches
38
+ * `buildMultimodalMessagesArray` — the place that turns `input.files` into
39
+ * text, images, PDFs and `nativeAudioFiles`. `BaseProvider.stream()` does
40
+ * build messages, but onto a throwaway clone whose result is discarded, so
41
+ * the real `options.input` came through untouched.
42
+ *
43
+ * The consequence was asymmetric and easy to miss: `generate()` did this
44
+ * inline and worked, while `stream()` silently dropped every attached file —
45
+ * not just audio, but the metadata summary too. Vertex hit the identical bug
46
+ * (#1258) and solved it with exactly this shape, called from both entry
47
+ * points.
48
+ */
49
+ private preprocessNativeFileInput;
33
50
  protected executeStream(options: StreamOptions, analysisSchema?: ZodUnknownSchema | Schema<unknown>): Promise<StreamResult>;
34
51
  /**
35
52
  * Execute stream using native @google/genai SDK
@@ -1,7 +1,7 @@
1
1
  import { ErrorCategory, ErrorSeverity, GoogleAIModels, } from "../../constants/enums.js";
2
2
  import { BaseProvider } from "../../core/baseProvider.js";
3
3
  import { IMAGE_GENERATION_MODELS, TOOL_STORAGE_TIMEOUT_MS, } from "../../core/constants.js";
4
- import { normalizeVisionImageFormats, processUnifiedFilesArray, } from "../../utils/messageBuilder.js";
4
+ import { mergeMediaFileAliases, normalizeVisionImageFormats, processUnifiedFilesArray, } from "../../utils/messageBuilder.js";
5
5
  import { ATTR, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../../telemetry/index.js";
6
6
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
7
7
  import { ERROR_CODES, NeuroLinkError } from "../../utils/errorHandling.js";
@@ -513,12 +513,55 @@ export class GoogleAIStudioProvider extends BaseProvider {
513
513
  return estimateTokens(text, "google-ai");
514
514
  }
515
515
  // executeGenerate removed - BaseProvider handles all generation with tools
516
+ /**
517
+ * Run the file preprocessing this provider's native paths depend on.
518
+ *
519
+ * AI Studio overrides both `generate()` and `executeStream()` and routes
520
+ * straight to the native SDK, so neither reaches
521
+ * `buildMultimodalMessagesArray` — the place that turns `input.files` into
522
+ * text, images, PDFs and `nativeAudioFiles`. `BaseProvider.stream()` does
523
+ * build messages, but onto a throwaway clone whose result is discarded, so
524
+ * the real `options.input` came through untouched.
525
+ *
526
+ * The consequence was asymmetric and easy to miss: `generate()` did this
527
+ * inline and worked, while `stream()` silently dropped every attached file —
528
+ * not just audio, but the metadata summary too. Vertex hit the identical bug
529
+ * (#1258) and solved it with exactly this shape, called from both entry
530
+ * points.
531
+ */
532
+ async preprocessNativeFileInput(options) {
533
+ // The user-facing aliases (`input.audioFiles`, `input.videoFiles`) are
534
+ // folded into `input.files` here, exactly as the Vertex client does. Only
535
+ // `files` is processed below, so without this a caller who used the
536
+ // documented `audioFiles` field had it silently ignored on both of this
537
+ // provider's paths.
538
+ if (options.input) {
539
+ mergeMediaFileAliases(options.input);
540
+ }
541
+ if (options.input?.files && options.input.files.length > 0) {
542
+ try {
543
+ // Mutates options.input.text / .images / .pdfFiles / .nativeAudioFiles
544
+ // in place.
545
+ await processUnifiedFilesArray(options, 100 * 1024 * 1024, this.providerName);
546
+ }
547
+ catch (fileError) {
548
+ logger.warn(`[GoogleAIStudio] processUnifiedFilesArray threw, continuing without file content: ${fileError instanceof Error ? fileError.message : String(fileError)}`);
549
+ }
550
+ }
551
+ // Runs even without input.files: a caller can populate input.images
552
+ // directly, and this native path never reaches the shared multimodal
553
+ // builder that would otherwise normalize the formats.
554
+ await normalizeVisionImageFormats(options.input);
555
+ }
516
556
  async executeStream(options, analysisSchema) {
517
557
  const modelName = options.model || this.modelName;
518
558
  // Phase 1: if audio input present, bridge to Gemini Live (Studio) using @google/genai
519
559
  if (options.input?.audio) {
520
560
  return await this.executeAudioStreamViaGeminiLive(options);
521
561
  }
562
+ // #1258, for this provider: stream() must run the same file preprocessing
563
+ // generate() does, or attached files are dropped on this path alone.
564
+ await this.preprocessNativeFileInput(options);
522
565
  // Structured output (analysisSchema, JSON format, or schema) is incompatible with tools on Gemini.
523
566
  const wantsStructuredOutput = analysisSchema || options.output?.format === "json" || options.schema;
524
567
  // Tool filter (a0269210): trust options.tools — caller (BaseProvider.stream)
@@ -1171,24 +1214,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
1171
1214
  logger.info("[GoogleAIStudio] Routing TTS direct-synthesis to handleDirectTTSSynthesis", { model: modelName });
1172
1215
  return this.handleDirectTTSSynthesis(options, Date.now());
1173
1216
  }
1174
- // Process the unified `input.files` array before routing to the
1175
- // native SDK. BaseProvider.generate() runs this preprocessing via
1176
- // buildMultimodalMessagesArray, but AI Studio's override skips it,
1177
- // which would otherwise drop text-file content (and the
1178
- // mimetype-hint contract) on the floor. Mutates options.input.text /
1179
- // options.input.images / options.input.pdfFiles in place.
1180
- if (options.input?.files && options.input.files.length > 0) {
1181
- try {
1182
- await processUnifiedFilesArray(options, 100 * 1024 * 1024, this.providerName);
1183
- }
1184
- catch (fileError) {
1185
- logger.warn(`[GoogleAIStudio] processUnifiedFilesArray threw, continuing without file content: ${fileError instanceof Error ? fileError.message : String(fileError)}`);
1186
- }
1187
- }
1188
- // Runs even without input.files: a caller can populate input.images
1189
- // directly, and this native path never reaches the shared multimodal
1190
- // builder that would otherwise normalize the formats.
1191
- await normalizeVisionImageFormats(options.input);
1217
+ await this.preprocessNativeFileInput(options);
1192
1218
  // Merge registered (built-in / MCP) tools with caller-supplied tools.
1193
1219
  // AI Studio's generate() bypasses BaseProvider.generate(), so the
1194
1220
  // ToolsManager-driven merge that normally injects sdk.registerTool()