@juspay/neurolink 9.94.6 → 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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [9.94.7](https://github.com/juspay/neurolink/compare/v9.94.6...v9.94.7) (2026-07-19)
2
+
3
+ ### Bug Fixes
4
+
5
+ - **(cli):** validate csv-max-rows, warn on large files, and document multimodal inputs ([124f0d6](https://github.com/juspay/neurolink/commit/124f0d6ca95d7d0819ab001cb9cf753052308ed1))
6
+
1
7
  ## [9.94.6](https://github.com/juspay/neurolink/compare/v9.94.5...v9.94.6) (2026-07-19)
2
8
 
3
9
  ### Bug Fixes
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.94.6",
3
+ "version": "9.94.7",
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": {