@juspay/neurolink 9.94.5 → 9.94.7

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.
@@ -24,8 +24,6 @@ export declare class CLICommandFactory {
24
24
  private static processCliPDFFiles;
25
25
  private static processCliFiles;
26
26
  private static processCliVideoFiles;
27
- private static isNonLocalFileReference;
28
- private static validateCliInputFiles;
29
27
  private static processOptions;
30
28
  /**
31
29
  * Validate Anthropic subscription options
@@ -24,6 +24,7 @@ import { initializeCliParser } from "../parser.js";
24
24
  import { formatFileSize, saveAudioToFile } from "../utils/audioFileUtils.js";
25
25
  import { playAudio } from "../utils/audioPlayer.js";
26
26
  import { resolveFilePaths } from "../utils/pathResolver.js";
27
+ import { validateCliInputFiles, validateCsvMaxRows, } from "../utils/inputValidation.js";
27
28
  import { animatedWrite } from "../utils/typewriter.js";
28
29
  import { createStreamAbortHandler } from "../utils/abortHandler.js";
29
30
  import { formatVideoFileSize, getVideoMetadataSummary, saveVideoToFile, } from "../utils/videoFileUtils.js";
@@ -187,7 +188,7 @@ export class CLICommandFactory {
187
188
  csvMaxRows: {
188
189
  type: "number",
189
190
  default: 1000,
190
- description: "Maximum number of CSV rows to process",
191
+ description: "Maximum number of CSV rows to process (positive integer, range 1-100000, default 1000)",
191
192
  },
192
193
  csvFormat: {
193
194
  type: "string",
@@ -805,42 +806,6 @@ export class CLICommandFactory {
805
806
  // URLs are preserved as-is by resolveFilePaths
806
807
  return resolveFilePaths(paths);
807
808
  }
808
- static isNonLocalFileReference(filePath) {
809
- const lower = filePath.toLowerCase();
810
- return (lower.startsWith("http://") ||
811
- lower.startsWith("https://") ||
812
- lower.startsWith("file://") ||
813
- lower.startsWith("data:"));
814
- }
815
- static validateCliInputFiles(argv) {
816
- const fileArgs = [
817
- { option: "--image", value: argv.image },
818
- { option: "--csv", value: argv.csv },
819
- { option: "--pdf", value: argv.pdf },
820
- { option: "--video", value: argv.video },
821
- { option: "--file", value: argv.file },
822
- ];
823
- const missingPaths = [];
824
- for (const { option, value } of fileArgs) {
825
- if (!value) {
826
- continue;
827
- }
828
- const rawPaths = Array.isArray(value) ? value : [value];
829
- const resolvedPaths = resolveFilePaths(rawPaths);
830
- for (let i = 0; i < resolvedPaths.length; i++) {
831
- const resolvedPath = resolvedPaths[i];
832
- if (CLICommandFactory.isNonLocalFileReference(resolvedPath)) {
833
- continue;
834
- }
835
- if (!fs.existsSync(resolvedPath)) {
836
- missingPaths.push(`${option} path not found: ${rawPaths[i]} (resolved to ${resolvedPath})`);
837
- }
838
- }
839
- }
840
- if (missingPaths.length > 0) {
841
- throw new Error(`One or more input files do not exist:\n${missingPaths.join("\n")}`);
842
- }
843
- }
844
809
  // Helper method to process common options
845
810
  static processOptions(argv) {
846
811
  // Handle noColor option by disabling chalk
@@ -1538,7 +1503,12 @@ export class CLICommandFactory {
1538
1503
  .example('$0 generate "Smooth camera movement" --image ./input.jpg --provider vertex --model veo-3.1-generate-001 --outputMode video --videoResolution 720p --videoLength 6 --videoAspectRatio 16:9 --videoOutput ./output.mp4', "Video generation with full options")
1539
1504
  .example('$0 generate "AI in Healthcare" --pptPages 10', "Generate a PowerPoint presentation")
1540
1505
  .example('$0 generate "Company Q4 Results" --pptPages 15 --pptTheme corporate --pptAudience business', "Generate presentation with options")
1541
- .example('$0 generate "Machine Learning 101" --pptTheme minimal --pptTone educational --pptNoImages', "Generate educational slides without AI images"));
1506
+ .example('$0 generate "Machine Learning 101" --pptTheme minimal --pptTone educational --pptNoImages', "Generate educational slides without AI images")
1507
+ .example('$0 generate "Describe this image" --image ./photo.jpg', "Analyze an image (multimodal input)")
1508
+ .example('$0 generate "Summarize this report" --pdf ./report.pdf', "Analyze a PDF document")
1509
+ .example('$0 generate "Key trends?" --csv large-data.csv --csvMaxRows 100', "Analyze a CSV with a row limit")
1510
+ .example('$0 generate "Compare the chart and the report" --image ./chart.png --pdf ./report.pdf', "Combine multiple file types in one prompt")
1511
+ .example('$0 generate "What is in this file?" --file ./data.json', "Auto-detect a file type with --file"));
1542
1512
  },
1543
1513
  handler: async (argv) => await CLICommandFactory.executeGenerate(argv),
1544
1514
  };
@@ -1560,7 +1530,10 @@ export class CLICommandFactory {
1560
1530
  .example('$0 stream "Explain machine learning" -p anthropic', "Stream with specific provider")
1561
1531
  .example('$0 stream "Code walkthrough" --output story.txt', "Stream to file")
1562
1532
  .example('echo "Live demo" | $0 stream', "Stream from stdin")
1563
- .example('$0 stream "Narrate this video" --video path/to/video.mp4', "Stream video analysis"));
1533
+ .example('$0 stream "Narrate this video" --video path/to/video.mp4', "Stream video analysis")
1534
+ .example('$0 stream "Describe this image" --image ./photo.jpg', "Stream image analysis (multimodal input)")
1535
+ .example('$0 stream "Summarize this document" --pdf ./report.pdf', "Stream PDF analysis")
1536
+ .example('$0 stream "Explain this dataset" --csv ./data.csv --csv-format markdown', "Stream CSV analysis"));
1564
1537
  },
1565
1538
  handler: async (argv) => await CLICommandFactory.executeStream(argv),
1566
1539
  };
@@ -2277,7 +2250,11 @@ export class CLICommandFactory {
2277
2250
  if (isSttOnly) {
2278
2251
  return "";
2279
2252
  }
2280
- throw new Error("No input received from stdin");
2253
+ throw new Error("No input received from stdin.\n" +
2254
+ "💡 Try this:\n" +
2255
+ ' neurolink generate "your prompt"\n' +
2256
+ ' echo "your prompt" | neurolink generate\n' +
2257
+ ' neurolink generate "Describe this image" --image ./photo.jpg');
2281
2258
  }
2282
2259
  return trimmedData;
2283
2260
  }
@@ -2285,7 +2262,11 @@ export class CLICommandFactory {
2285
2262
  if (isSttOnly) {
2286
2263
  return "";
2287
2264
  }
2288
- throw new Error('Input required. Use: neurolink generate "your prompt" or echo "prompt" | neurolink generate');
2265
+ throw new Error("❌ Input required.\n" +
2266
+ "💡 Try this:\n" +
2267
+ ' neurolink generate "your prompt"\n' +
2268
+ ' echo "your prompt" | neurolink generate\n' +
2269
+ ' neurolink generate "Describe this image" --image ./photo.jpg');
2289
2270
  }
2290
2271
  return argv.input;
2291
2272
  }
@@ -2544,7 +2525,8 @@ export class CLICommandFactory {
2544
2525
  if (options.delay) {
2545
2526
  await new Promise((resolve) => setTimeout(resolve, options.delay));
2546
2527
  }
2547
- CLICommandFactory.validateCliInputFiles(argv);
2528
+ validateCliInputFiles(argv);
2529
+ validateCsvMaxRows(argv);
2548
2530
  // Process context
2549
2531
  const { inputText, contextMetadata } = CLICommandFactory.processGenerateContext(rawInput, options);
2550
2532
  // Handle dry-run mode for testing
@@ -3304,7 +3286,11 @@ export class CLICommandFactory {
3304
3286
  argv.input = "";
3305
3287
  return;
3306
3288
  }
3307
- throw new Error("No input received from stdin");
3289
+ throw new Error("No input received from stdin.\n" +
3290
+ "💡 Try this:\n" +
3291
+ ' neurolink stream "your prompt"\n' +
3292
+ ' echo "your prompt" | neurolink stream\n' +
3293
+ ' neurolink stream "Describe this image" --image ./photo.jpg');
3308
3294
  }
3309
3295
  }
3310
3296
  else if (!argv.input) {
@@ -3312,7 +3298,11 @@ export class CLICommandFactory {
3312
3298
  argv.input = "";
3313
3299
  return;
3314
3300
  }
3315
- throw new Error('Input required. Use: neurolink stream "your prompt" or echo "prompt" | neurolink stream');
3301
+ throw new Error("❌ Input required.\n" +
3302
+ "💡 Try this:\n" +
3303
+ ' neurolink stream "your prompt"\n' +
3304
+ ' echo "your prompt" | neurolink stream\n' +
3305
+ ' neurolink stream "Describe this image" --image ./photo.jpg');
3316
3306
  }
3317
3307
  }
3318
3308
  /**
@@ -3331,7 +3321,8 @@ export class CLICommandFactory {
3331
3321
  if (options.delay) {
3332
3322
  await new Promise((resolve) => setTimeout(resolve, options.delay));
3333
3323
  }
3334
- CLICommandFactory.validateCliInputFiles(argv);
3324
+ validateCliInputFiles(argv);
3325
+ validateCsvMaxRows(argv);
3335
3326
  const { inputText, contextMetadata } = await CLICommandFactory.processStreamContext(argv, options);
3336
3327
  // Handle dry-run mode for testing
3337
3328
  if (options.dryRun) {
@@ -414,6 +414,22 @@ export class LoopSession {
414
414
  logger.always(chalk.yellow(` ${c.cmd.padEnd(20)}`) + `${c.desc}`);
415
415
  });
416
416
  logger.always("\nAny other command will be executed as a standard neurolink CLI command.");
417
+ // #315: multimodal file flags are buried in the raw yargs dump below — call
418
+ // them out explicitly so loop users know PDFs/images/CSVs/video are supported.
419
+ logger.always(chalk.cyan("\nMultimodal file inputs (per-command flags):"));
420
+ [
421
+ ["--image <path|url>", "Attach an image for analysis (repeatable)"],
422
+ ["--pdf <path|url>", "Attach a PDF document (repeatable)"],
423
+ [
424
+ "--csv <path|url>",
425
+ "Attach a CSV file (see --csv-format, --csv-max-rows)",
426
+ ],
427
+ ["--video <path|url>", "Attach a video for analysis"],
428
+ ["--file <path|url>", "Attach a file and auto-detect its type"],
429
+ ].forEach(([flag, desc]) => {
430
+ logger.always(chalk.yellow(` ${flag.padEnd(20)}`) + `${desc}`);
431
+ });
432
+ logger.always(chalk.gray(' e.g. generate "Describe this" --image ./photo.jpg --pdf ./report.pdf'));
417
433
  // Also show the standard help output
418
434
  this.initializeCliParser().showHelp("log");
419
435
  }
@@ -429,6 +445,11 @@ export class LoopSession {
429
445
  logger.always(chalk.gray(` Type: ${schema.type}`));
430
446
  }
431
447
  }
448
+ // #350: multimodal file inputs are per-command flags, not session variables,
449
+ // so they don't appear above — point users to them explicitly.
450
+ logger.always(chalk.gray("\n Note: file inputs (--image/--pdf/--csv/--video/--file) are per-command\n" +
451
+ " flags, not session variables — pass them directly on a generate/stream line,\n" +
452
+ ' e.g. generate "Summarize this" --pdf ./report.pdf'));
432
453
  }
433
454
  /**
434
455
  * Get command input with history support using readline
@@ -0,0 +1,38 @@
1
+ /**
2
+ * CLI Input Validation Utilities
3
+ *
4
+ * Validates multimodal file flags (--image/--csv/--pdf/--video/--file) and
5
+ * --csv-max-rows before a generate/stream/batch run starts, so bad input
6
+ * fails fast with a clear message instead of surfacing deep inside provider
7
+ * calls. Exported as standalone functions (rather than private statics on
8
+ * CLICommandFactory) so callers — including tests — can import and invoke
9
+ * them directly, without reaching into the class via an `unknown` cast.
10
+ */
11
+ /**
12
+ * Soft (warn-only) size thresholds for CLI multimodal flags. Intentionally
13
+ * defined here rather than reusing `lib/processors/config/sizeLimits.ts`'s
14
+ * hard processor caps (e.g. PDF's 100MB hard limit): reusing those names/
15
+ * values would couple the CLI layer to processor internals across the
16
+ * CLI/SDK boundary, and would blur the line between a soft "this may be
17
+ * slow" warning and the actual hard cap the processor enforces.
18
+ */
19
+ export declare const CLI_SOFT_LIMITS_MB: {
20
+ readonly IMAGE_MAX_MB: 10;
21
+ readonly CSV_MAX_MB: 50;
22
+ readonly PDF_MAX_MB: 100;
23
+ readonly VIDEO_MAX_MB: 500;
24
+ };
25
+ /**
26
+ * Validate --image/--csv/--pdf/--video/--file inputs before a CLI run
27
+ * starts: every local path must exist, must not be a directory, and — for
28
+ * the flags with a soft limit — triggers a size warning above the
29
+ * threshold. URLs/data URIs skip both existence and size checks (#319).
30
+ */
31
+ export declare function validateCliInputFiles(argv: Record<string, unknown>): void;
32
+ /**
33
+ * #310: validate --csv-max-rows is a positive integer in the range
34
+ * 1-100000, matching the documented option range (the value previously
35
+ * flowed through silently, or — above 100000 — only warned instead of
36
+ * being rejected, contradicting the documented hard range).
37
+ */
38
+ export declare function validateCsvMaxRows(argv: Record<string, unknown>): void;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * CLI Input Validation Utilities
3
+ *
4
+ * Validates multimodal file flags (--image/--csv/--pdf/--video/--file) and
5
+ * --csv-max-rows before a generate/stream/batch run starts, so bad input
6
+ * fails fast with a clear message instead of surfacing deep inside provider
7
+ * calls. Exported as standalone functions (rather than private statics on
8
+ * CLICommandFactory) so callers — including tests — can import and invoke
9
+ * them directly, without reaching into the class via an `unknown` cast.
10
+ */
11
+ import fs from "node:fs";
12
+ import chalk from "chalk";
13
+ import { logger } from "../../lib/utils/logger.js";
14
+ import { resolveFilePaths } from "./pathResolver.js";
15
+ /**
16
+ * Soft (warn-only) size thresholds for CLI multimodal flags. Intentionally
17
+ * defined here rather than reusing `lib/processors/config/sizeLimits.ts`'s
18
+ * hard processor caps (e.g. PDF's 100MB hard limit): reusing those names/
19
+ * values would couple the CLI layer to processor internals across the
20
+ * CLI/SDK boundary, and would blur the line between a soft "this may be
21
+ * slow" warning and the actual hard cap the processor enforces.
22
+ */
23
+ export const CLI_SOFT_LIMITS_MB = {
24
+ IMAGE_MAX_MB: 10,
25
+ CSV_MAX_MB: 50,
26
+ PDF_MAX_MB: 100,
27
+ VIDEO_MAX_MB: 500,
28
+ };
29
+ function isNonLocalFileReference(filePath) {
30
+ const lower = filePath.toLowerCase();
31
+ return (lower.startsWith("http://") ||
32
+ lower.startsWith("https://") ||
33
+ lower.startsWith("file://") ||
34
+ lower.startsWith("data:"));
35
+ }
36
+ /**
37
+ * Validate --image/--csv/--pdf/--video/--file inputs before a CLI run
38
+ * starts: every local path must exist, must not be a directory, and — for
39
+ * the flags with a soft limit — triggers a size warning above the
40
+ * threshold. URLs/data URIs skip both existence and size checks (#319).
41
+ */
42
+ export function validateCliInputFiles(argv) {
43
+ const fileArgs = [
44
+ {
45
+ option: "--image",
46
+ value: argv.image,
47
+ warnAtMB: CLI_SOFT_LIMITS_MB.IMAGE_MAX_MB,
48
+ },
49
+ {
50
+ option: "--csv",
51
+ value: argv.csv,
52
+ warnAtMB: CLI_SOFT_LIMITS_MB.CSV_MAX_MB,
53
+ },
54
+ {
55
+ option: "--pdf",
56
+ value: argv.pdf,
57
+ warnAtMB: CLI_SOFT_LIMITS_MB.PDF_MAX_MB,
58
+ },
59
+ {
60
+ option: "--video",
61
+ value: argv.video,
62
+ warnAtMB: CLI_SOFT_LIMITS_MB.VIDEO_MAX_MB,
63
+ },
64
+ { option: "--file", value: argv.file },
65
+ ];
66
+ const missingPaths = [];
67
+ for (const { option, value, warnAtMB } of fileArgs) {
68
+ if (!value) {
69
+ continue;
70
+ }
71
+ const rawPaths = Array.isArray(value) ? value : [value];
72
+ const resolvedPaths = resolveFilePaths(rawPaths);
73
+ for (let i = 0; i < resolvedPaths.length; i++) {
74
+ const resolvedPath = resolvedPaths[i];
75
+ // URLs / data: refs skip both existence and size checks (#319).
76
+ if (isNonLocalFileReference(resolvedPath)) {
77
+ continue;
78
+ }
79
+ // Single statSync covers existence, directory rejection, and the
80
+ // size warning below — avoids a second syscall and closes the
81
+ // TOCTOU gap a separate existsSync()+statSync() pair would leave.
82
+ let stat;
83
+ try {
84
+ stat = fs.statSync(resolvedPath);
85
+ }
86
+ catch (err) {
87
+ // Only ENOENT actually means "not found" — EACCES (permission
88
+ // denied), ELOOP (symlink loop), ENOTDIR, etc. are real failures
89
+ // that get masked (and made undebuggable) if we blanket-label
90
+ // every statSync error the same way.
91
+ const code = err?.code;
92
+ if (code === "ENOENT") {
93
+ missingPaths.push(`${option} path not found: ${rawPaths[i]} (resolved to ${resolvedPath})`);
94
+ }
95
+ else {
96
+ const reason = err instanceof Error ? err.message : String(err);
97
+ missingPaths.push(`${option} path could not be accessed: ${rawPaths[i]} (resolved to ${resolvedPath}) — ` +
98
+ `${code ? `${code}: ` : ""}${reason}`);
99
+ }
100
+ continue;
101
+ }
102
+ if (stat.isDirectory()) {
103
+ missingPaths.push(`${option} path is a directory, not a file: ${rawPaths[i]} (resolved to ${resolvedPath})`);
104
+ continue;
105
+ }
106
+ // #319: warn (not error) when a local multimodal file is unusually
107
+ // large so the user isn't surprised by slow processing / token blowups.
108
+ if (warnAtMB !== undefined) {
109
+ const sizeMB = stat.size / (1024 * 1024);
110
+ if (sizeMB > warnAtMB) {
111
+ logger.always(chalk.yellow(`⚠️ ${option} file ${rawPaths[i]} is ${sizeMB.toFixed(1)}MB ` +
112
+ `(above the ${warnAtMB}MB soft limit) — processing may be slow ` +
113
+ `or exceed provider token/size limits.`));
114
+ }
115
+ }
116
+ }
117
+ }
118
+ if (missingPaths.length > 0) {
119
+ throw new Error("❌ One or more input files are invalid:\n" +
120
+ `${missingPaths.join("\n")}\n` +
121
+ "💡 Check the path is correct and relative to your current directory, " +
122
+ "or pass an absolute path / URL.");
123
+ }
124
+ }
125
+ /**
126
+ * #310: validate --csv-max-rows is a positive integer in the range
127
+ * 1-100000, matching the documented option range (the value previously
128
+ * flowed through silently, or — above 100000 — only warned instead of
129
+ * being rejected, contradicting the documented hard range).
130
+ */
131
+ export function validateCsvMaxRows(argv) {
132
+ const raw = argv.csvMaxRows;
133
+ if (raw === undefined) {
134
+ return;
135
+ }
136
+ const value = Number(raw);
137
+ if (!Number.isInteger(value) || value < 1 || value > 100000) {
138
+ throw new Error(`Invalid --csv-max-rows (--csvMaxRows) value: ${String(raw)}. ` +
139
+ `Must be a positive integer in the range 1-100000. ` +
140
+ `Example: --csv-max-rows 500`);
141
+ }
142
+ }
143
+ //# sourceMappingURL=inputValidation.js.map
@@ -22,7 +22,18 @@ function computeTotalContentLength(messages) {
22
22
  return total;
23
23
  }
24
24
  /**
25
- * Check whether input contains multimodal content (images, files, PDFs, CSVs).
25
+ * Check whether input contains multimodal content (images, files, PDFs, CSVs,
26
+ * audio, video). #284: audioFiles/videoFiles were previously ignored here, so a
27
+ * request carrying only audio or video was misrouted to the text-only path.
28
+ *
29
+ * Intentional (round-2 review): routing audio/video through `isMultimodal`
30
+ * does NOT mean vision is required. `buildMultimodalMessagesArray` folds
31
+ * `audioFiles`/`videoFiles` into `inp.files` for auto-detection, and
32
+ * `appendDetectedFileResult` injects their content as plain text markers
33
+ * (`## Audio File:` / `## Video File:`), not image/vision blocks — see
34
+ * `test/continuous-test-suite-bugfixes.ts` ("MessageBuilder #284: ...").
35
+ * "Multimodal" here means "needs the multimodal builder so the payload isn't
36
+ * dropped", not "needs a vision model".
26
37
  */
27
38
  function detectMultimodal(opts) {
28
39
  const input = opts.input;
@@ -31,10 +42,18 @@ function detectMultimodal(opts) {
31
42
  const hasCSVFiles = !!input?.csvFiles?.length;
32
43
  const hasPdfFiles = !!input?.pdfFiles?.length;
33
44
  const hasFiles = !!input?.files?.length;
45
+ const hasAudioFiles = !!input?.audioFiles?.length;
46
+ const hasVideoFiles = !!input?.videoFiles?.length;
34
47
  return {
35
- isMultimodal: hasImages || hasContent || hasCSVFiles || hasPdfFiles || hasFiles,
48
+ isMultimodal: hasImages ||
49
+ hasContent ||
50
+ hasCSVFiles ||
51
+ hasPdfFiles ||
52
+ hasFiles ||
53
+ hasAudioFiles ||
54
+ hasVideoFiles,
36
55
  hasImages,
37
- hasFiles: hasCSVFiles || hasPdfFiles || hasFiles,
56
+ hasFiles: hasCSVFiles || hasPdfFiles || hasFiles || hasAudioFiles || hasVideoFiles,
38
57
  };
39
58
  }
40
59
  /**
@@ -75,6 +94,12 @@ export class MessageBuilder {
75
94
  content: input?.content,
76
95
  csvFiles: input?.csvFiles,
77
96
  pdfFiles: input?.pdfFiles,
97
+ // #284: audioFiles/videoFiles must be forwarded too, or the
98
+ // multimodal builder receives an empty payload despite
99
+ // detectMultimodal() correctly routing audio/video-only
100
+ // requests here.
101
+ audioFiles: input?.audioFiles,
102
+ videoFiles: input?.videoFiles,
78
103
  files: input?.files,
79
104
  },
80
105
  csvOptions: options.csvOptions,
@@ -192,6 +217,10 @@ export class MessageBuilder {
192
217
  content: input?.content,
193
218
  csvFiles: input?.csvFiles,
194
219
  pdfFiles: input?.pdfFiles,
220
+ // #284: forward audioFiles/videoFiles here too — see the
221
+ // matching comment in buildMessages() above.
222
+ audioFiles: input?.audioFiles,
223
+ videoFiles: input?.videoFiles,
195
224
  files: input?.files,
196
225
  },
197
226
  csvOptions: options.csvOptions,
@@ -22,7 +22,18 @@ function computeTotalContentLength(messages) {
22
22
  return total;
23
23
  }
24
24
  /**
25
- * Check whether input contains multimodal content (images, files, PDFs, CSVs).
25
+ * Check whether input contains multimodal content (images, files, PDFs, CSVs,
26
+ * audio, video). #284: audioFiles/videoFiles were previously ignored here, so a
27
+ * request carrying only audio or video was misrouted to the text-only path.
28
+ *
29
+ * Intentional (round-2 review): routing audio/video through `isMultimodal`
30
+ * does NOT mean vision is required. `buildMultimodalMessagesArray` folds
31
+ * `audioFiles`/`videoFiles` into `inp.files` for auto-detection, and
32
+ * `appendDetectedFileResult` injects their content as plain text markers
33
+ * (`## Audio File:` / `## Video File:`), not image/vision blocks — see
34
+ * `test/continuous-test-suite-bugfixes.ts` ("MessageBuilder #284: ...").
35
+ * "Multimodal" here means "needs the multimodal builder so the payload isn't
36
+ * dropped", not "needs a vision model".
26
37
  */
27
38
  function detectMultimodal(opts) {
28
39
  const input = opts.input;
@@ -31,10 +42,18 @@ function detectMultimodal(opts) {
31
42
  const hasCSVFiles = !!input?.csvFiles?.length;
32
43
  const hasPdfFiles = !!input?.pdfFiles?.length;
33
44
  const hasFiles = !!input?.files?.length;
45
+ const hasAudioFiles = !!input?.audioFiles?.length;
46
+ const hasVideoFiles = !!input?.videoFiles?.length;
34
47
  return {
35
- isMultimodal: hasImages || hasContent || hasCSVFiles || hasPdfFiles || hasFiles,
48
+ isMultimodal: hasImages ||
49
+ hasContent ||
50
+ hasCSVFiles ||
51
+ hasPdfFiles ||
52
+ hasFiles ||
53
+ hasAudioFiles ||
54
+ hasVideoFiles,
36
55
  hasImages,
37
- hasFiles: hasCSVFiles || hasPdfFiles || hasFiles,
56
+ hasFiles: hasCSVFiles || hasPdfFiles || hasFiles || hasAudioFiles || hasVideoFiles,
38
57
  };
39
58
  }
40
59
  /**
@@ -75,6 +94,12 @@ export class MessageBuilder {
75
94
  content: input?.content,
76
95
  csvFiles: input?.csvFiles,
77
96
  pdfFiles: input?.pdfFiles,
97
+ // #284: audioFiles/videoFiles must be forwarded too, or the
98
+ // multimodal builder receives an empty payload despite
99
+ // detectMultimodal() correctly routing audio/video-only
100
+ // requests here.
101
+ audioFiles: input?.audioFiles,
102
+ videoFiles: input?.videoFiles,
78
103
  files: input?.files,
79
104
  },
80
105
  csvOptions: options.csvOptions,
@@ -192,6 +217,10 @@ export class MessageBuilder {
192
217
  content: input?.content,
193
218
  csvFiles: input?.csvFiles,
194
219
  pdfFiles: input?.pdfFiles,
220
+ // #284: forward audioFiles/videoFiles here too — see the
221
+ // matching comment in buildMessages() above.
222
+ audioFiles: input?.audioFiles,
223
+ videoFiles: input?.videoFiles,
195
224
  files: input?.files,
196
225
  },
197
226
  csvOptions: options.csvOptions,
@@ -53,6 +53,7 @@ export type GenerateOptions = {
53
53
  images?: Array<Buffer | string | ImageWithAltText>;
54
54
  csvFiles?: Array<Buffer | string>;
55
55
  pdfFiles?: Array<Buffer | string>;
56
+ audioFiles?: Array<Buffer | string>;
56
57
  videoFiles?: Array<Buffer | string>;
57
58
  files?: Array<Buffer | string | FileWithMetadata>;
58
59
  content?: Content[];
@@ -419,15 +419,46 @@ export type MultimodalInput = {
419
419
  segments?: DirectorSegment[];
420
420
  };
421
421
  /**
422
- * Content format for multimodal messages (used internally)
423
- * Compatible with Vercel AI SDK message format
422
+ * Content format for multimodal messages (used internally).
423
+ *
424
+ * #325: the loose `[key: string]: unknown` index signature has been replaced
425
+ * with the concrete fields the codebase actually reads/writes across the
426
+ * text / image / file / tool-call / tool-result shapes. This keeps the broad
427
+ * structural compatibility the internal pipeline relies on (a single object
428
+ * type, not a strict discriminated union that would force narrowing at every
429
+ * consumer) while removing the "any key is allowed" hole that let typos and
430
+ * unrelated keys through unchecked.
424
431
  */
425
432
  export type MessageContent = {
426
433
  type: string;
434
+ /** Text content (`type: "text"`). */
427
435
  text?: string;
436
+ /** Base64 / data-URI image (`type: "image"`). */
428
437
  image?: string;
438
+ /** MIME type for image/file parts. */
429
439
  mimeType?: string;
430
- [key: string]: unknown;
440
+ /** Raw file bytes or base64 (`type: "file"`/document parts). */
441
+ data?: string | Buffer;
442
+ /** File name for document/file parts. */
443
+ name?: string;
444
+ /** File name (alias used by some file parts). */
445
+ filename?: string;
446
+ /** Tool-call identifier (`type: "tool-call"`/`"tool-result"`). */
447
+ toolCallId?: string;
448
+ /** Tool name (`type: "tool-call"`/`"tool-result"`). */
449
+ toolName?: string;
450
+ /** Tool-call arguments (`type: "tool-call"`). */
451
+ args?: Record<string, unknown>;
452
+ /** Tool-result payload (`type: "tool-result"`). */
453
+ result?: unknown;
454
+ /** Whether a tool-result represents an error (`type: "tool-result"`). */
455
+ isError?: boolean;
456
+ /**
457
+ * Provider-specific per-block options (e.g. Anthropic cache_control).
458
+ * Read as `item.providerOptions` when converting `MessageContent[]` to
459
+ * `ModelMessage[]` in `MessageBuilder.ts`.
460
+ */
461
+ providerOptions?: Record<string, unknown>;
431
462
  };
432
463
  /**
433
464
  * Extended chat message for multimodal support (internal use)
@@ -193,6 +193,7 @@ export type StreamOptions = {
193
193
  images?: Array<Buffer | string | ImageWithAltText>;
194
194
  csvFiles?: Array<Buffer | string>;
195
195
  pdfFiles?: Array<Buffer | string>;
196
+ audioFiles?: Array<Buffer | string>;
196
197
  videoFiles?: Array<Buffer | string>;
197
198
  files?: Array<Buffer | string | FileWithMetadata>;
198
199
  content?: Content[];
@@ -35,6 +35,9 @@ export declare const ERROR_CODES: {
35
35
  readonly IMAGE_TOO_SMALL: "IMAGE_TOO_SMALL";
36
36
  readonly INVALID_IMAGE_FORMAT: "INVALID_IMAGE_FORMAT";
37
37
  readonly INVALID_IMAGE_SIZE: "INVALID_IMAGE_SIZE";
38
+ readonly IMAGE_BUFFER_INVALID: "IMAGE_BUFFER_INVALID";
39
+ readonly FILE_PROCESSING_FAILED: "FILE_PROCESSING_FAILED";
40
+ readonly CSV_PROCESSING_FAILED: "CSV_PROCESSING_FAILED";
38
41
  readonly PDF_PAGE_LIMIT_EXCEEDED: "PDF_PAGE_LIMIT_EXCEEDED";
39
42
  readonly RATE_LIMITER_QUEUE_FULL: "RATE_LIMITER_QUEUE_FULL";
40
43
  readonly RATE_LIMITER_QUEUE_TIMEOUT: "RATE_LIMITER_QUEUE_TIMEOUT";
@@ -216,6 +219,13 @@ export declare class ErrorFactory {
216
219
  * Create an invalid image format error
217
220
  */
218
221
  static invalidImageFormat(): NeuroLinkError;
222
+ /**
223
+ * Create an image buffer validation error: empty, undersized, or a
224
+ * truncated buffer detected by `ImageProcessor.validateBufferNotEmpty()`.
225
+ * Takes the fully-formed message so call sites keep their specific
226
+ * byte-count detail instead of a fixed generic message.
227
+ */
228
+ static imageBufferInvalid(message: string): NeuroLinkError;
219
229
  /**
220
230
  * Create a rate limiter queue full error
221
231
  */
@@ -287,6 +297,18 @@ export declare class ErrorFactory {
287
297
  * generic `Error` instances.
288
298
  */
289
299
  static proxyWorkerLifecycle(message: string, context?: Record<string, unknown>): NeuroLinkError;
300
+ /**
301
+ * Create a generic file-processing-failed error (e.g. an unrecognized or
302
+ * corrupt file in `processUnifiedFilesArray`). Preserves the original error
303
+ * as `originalError` (stack + message copied onto the new error's context).
304
+ */
305
+ static fileProcessingFailed(filename: string, originalError: Error): NeuroLinkError;
306
+ /**
307
+ * Create a generic CSV-processing-failed error (explicit `csvFiles` path,
308
+ * distinct from `fileProcessingFailed` so callers can classify CSV-specific
309
+ * failures separately). Preserves the original error as `originalError`.
310
+ */
311
+ static csvProcessingFailed(filename: string, originalError: Error): NeuroLinkError;
290
312
  }
291
313
  /**
292
314
  * Timeout wrapper for async operations