@juspay/neurolink 9.95.0 → 9.95.2

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 (44) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +232 -232
  3. package/dist/cli/factories/commandFactory.d.ts +10 -0
  4. package/dist/cli/factories/commandFactory.js +118 -13
  5. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  6. package/dist/cli/utils/inputValidation.d.ts +22 -9
  7. package/dist/cli/utils/inputValidation.js +147 -61
  8. package/dist/core/baseProvider.js +1 -0
  9. package/dist/core/constants.d.ts +2 -0
  10. package/dist/core/constants.js +9 -0
  11. package/dist/core/modules/MessageBuilder.js +2 -0
  12. package/dist/lib/core/baseProvider.js +1 -0
  13. package/dist/lib/core/constants.d.ts +2 -0
  14. package/dist/lib/core/constants.js +9 -0
  15. package/dist/lib/core/modules/MessageBuilder.js +2 -0
  16. package/dist/lib/neurolink.js +8 -4
  17. package/dist/lib/types/cli.d.ts +7 -2
  18. package/dist/lib/types/file.d.ts +10 -0
  19. package/dist/lib/types/generate.d.ts +14 -0
  20. package/dist/lib/types/stream.d.ts +7 -0
  21. package/dist/lib/utils/errorHandling.d.ts +10 -0
  22. package/dist/lib/utils/errorHandling.js +28 -0
  23. package/dist/lib/utils/logger.d.ts +12 -0
  24. package/dist/lib/utils/logger.js +16 -0
  25. package/dist/lib/utils/messageBuilder.js +36 -5
  26. package/dist/lib/utils/multimodalOptionsBuilder.d.ts +4 -0
  27. package/dist/lib/utils/multimodalOptionsBuilder.js +1 -0
  28. package/dist/lib/utils/pdfProcessor.d.ts +15 -0
  29. package/dist/lib/utils/pdfProcessor.js +83 -3
  30. package/dist/neurolink.js +8 -4
  31. package/dist/types/cli.d.ts +7 -2
  32. package/dist/types/file.d.ts +10 -0
  33. package/dist/types/generate.d.ts +14 -0
  34. package/dist/types/stream.d.ts +7 -0
  35. package/dist/utils/errorHandling.d.ts +10 -0
  36. package/dist/utils/errorHandling.js +28 -0
  37. package/dist/utils/logger.d.ts +12 -0
  38. package/dist/utils/logger.js +16 -0
  39. package/dist/utils/messageBuilder.js +36 -5
  40. package/dist/utils/multimodalOptionsBuilder.d.ts +4 -0
  41. package/dist/utils/multimodalOptionsBuilder.js +1 -0
  42. package/dist/utils/pdfProcessor.d.ts +15 -0
  43. package/dist/utils/pdfProcessor.js +83 -3
  44. package/package.json +1 -1
@@ -22,6 +22,16 @@ export declare class CLICommandFactory {
22
22
  private static processCliImages;
23
23
  private static processCliCSVFiles;
24
24
  private static processCliPDFFiles;
25
+ /**
26
+ * Resolve the PDF decryption password, preferring the NEUROLINK_PDF_PASSWORD
27
+ * env var over the `--pdf-password` flag. A plaintext CLI flag leaks into
28
+ * shell history, `ps`/process listings, and CI logs — the env var avoids
29
+ * that, matching the project's existing "credentials via env vars, not
30
+ * flags" stance for the loop session. The flag stays supported (dropping it
31
+ * would be a breaking CLI change), but using it prints a one-line stderr
32
+ * warning recommending the env var instead.
33
+ */
34
+ private static resolvePdfPassword;
25
35
  private static processCliFiles;
26
36
  private static processCliVideoFiles;
27
37
  private static processOptions;
@@ -24,7 +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
+ import { validateCliInputFiles, validateCsvMaxRows, validatePromptsFilePath, } from "../utils/inputValidation.js";
28
28
  import { animatedWrite } from "../utils/typewriter.js";
29
29
  import { createStreamAbortHandler } from "../utils/abortHandler.js";
30
30
  import { formatVideoFileSize, getVideoMetadataSummary, saveVideoToFile, } from "../utils/videoFileUtils.js";
@@ -156,6 +156,12 @@ export class CLICommandFactory {
156
156
  type: "string",
157
157
  description: "Add PDF file for analysis (can be used multiple times)",
158
158
  },
159
+ "pdf-password": {
160
+ type: "string",
161
+ description: "Password for an encrypted PDF (used on the image-conversion path). " +
162
+ "Visible in shell history/process listings — prefer the " +
163
+ "NEUROLINK_PDF_PASSWORD env var instead.",
164
+ },
159
165
  video: {
160
166
  type: "string",
161
167
  description: "Add video file for analysis (can be used multiple times) (MP4, WebM, MOV, AVI, MKV)",
@@ -742,11 +748,18 @@ export class CLICommandFactory {
742
748
  alias: "skills-dir",
743
749
  },
744
750
  };
745
- // Helper method to build options for commands
746
- static buildOptions(yargs, additionalOptions = {}) {
751
+ // Helper method to build options for commands. `excludeOptions` drops
752
+ // common flags from registration entirely (not just from validation)
753
+ // #1191 round-5: batch uses this to omit `commonOptions.file` so `--file`
754
+ // isn't a recognized flag at all, since it would collide with the
755
+ // `<file>` positional (the prompts list).
756
+ static buildOptions(yargs, additionalOptions = {}, excludeOptions = []) {
757
+ const baseOptions = excludeOptions.length === 0
758
+ ? CLICommandFactory.commonOptions
759
+ : Object.fromEntries(Object.entries(CLICommandFactory.commonOptions).filter(([key]) => !excludeOptions.includes(key)));
747
760
  return (yargs
748
761
  .options({
749
- ...CLICommandFactory.commonOptions,
762
+ ...baseOptions,
750
763
  ...additionalOptions,
751
764
  })
752
765
  // NEW9: implies relationships so users who pass --stt-provider or
@@ -786,6 +799,24 @@ export class CLICommandFactory {
786
799
  // URLs are preserved as-is by resolveFilePaths
787
800
  return resolveFilePaths(paths);
788
801
  }
802
+ /**
803
+ * Resolve the PDF decryption password, preferring the NEUROLINK_PDF_PASSWORD
804
+ * env var over the `--pdf-password` flag. A plaintext CLI flag leaks into
805
+ * shell history, `ps`/process listings, and CI logs — the env var avoids
806
+ * that, matching the project's existing "credentials via env vars, not
807
+ * flags" stance for the loop session. The flag stays supported (dropping it
808
+ * would be a breaking CLI change), but using it prints a one-line stderr
809
+ * warning recommending the env var instead.
810
+ */
811
+ static resolvePdfPassword(argv) {
812
+ const flagValue = argv.pdfPassword;
813
+ const envValue = process.env.NEUROLINK_PDF_PASSWORD;
814
+ if (flagValue) {
815
+ process.stderr.write(chalk.yellow("⚠️ --pdf-password is visible in shell history and process listings. " +
816
+ "Prefer the NEUROLINK_PDF_PASSWORD environment variable instead.\n"));
817
+ }
818
+ return flagValue || envValue;
819
+ }
789
820
  // Helper method to process CLI files with auto-detection
790
821
  static processCliFiles(files) {
791
822
  if (!files) {
@@ -1543,11 +1574,19 @@ export class CLICommandFactory {
1543
1574
  */
1544
1575
  static createBatchCommand() {
1545
1576
  return {
1546
- command: "batch <file>",
1577
+ // #1191 round-5: the positional is named `promptsFile`, not `file` —
1578
+ // yargs implicitly accepts `--<positionalName>` as a flag alias for
1579
+ // any positional regardless of whether it's separately registered via
1580
+ // `.options()`, so a positional literally named `file` would let
1581
+ // `--file` silently pass strictOptions() even after removing
1582
+ // `commonOptions.file` below. Renaming the key (invocation syntax is
1583
+ // unaffected — it's still just `neurolink batch prompts.txt`) is what
1584
+ // actually makes `--file` an unknown argument.
1585
+ command: "batch <promptsFile>",
1547
1586
  describe: "Process multiple prompts from a file",
1548
1587
  builder: (yargs) => {
1549
1588
  return CLICommandFactory.buildOptions(yargs
1550
- .positional("file", {
1589
+ .positional("promptsFile", {
1551
1590
  type: "string",
1552
1591
  description: "File with prompts (one per line)",
1553
1592
  demandOption: true,
@@ -1555,7 +1594,11 @@ export class CLICommandFactory {
1555
1594
  .example("$0 batch prompts.txt", "Process prompts from file")
1556
1595
  .example("$0 batch questions.txt --format json", "Export results as JSON")
1557
1596
  .example("$0 batch tasks.txt -p vertex --delay 2000", "Use Vertex AI with 2s delay")
1558
- .example("$0 batch batch.txt --output results.json", "Save results to file"));
1597
+ .example("$0 batch batch.txt --output results.json", "Save results to file"), {},
1598
+ // `--file` is not a batch flag — the positional above is the
1599
+ // prompts-list path. Omit it from registration so it's rejected
1600
+ // as unknown rather than silently accepted then ignored.
1601
+ ["file"]);
1559
1602
  },
1560
1603
  handler: async (argv) => await CLICommandFactory.executeBatch(argv),
1561
1604
  };
@@ -2630,6 +2673,9 @@ export class CLICommandFactory {
2630
2673
  const inputAudioFormat = inferAudioFormatFromPath(inputAudioPath);
2631
2674
  const runGenerate = () => sdk.generate({
2632
2675
  input: generateInput,
2676
+ pdfOptions: {
2677
+ password: CLICommandFactory.resolvePdfPassword(argv),
2678
+ },
2633
2679
  csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
2634
2680
  videoOptions: {
2635
2681
  frames: argv.videoFrames,
@@ -2882,6 +2928,9 @@ export class CLICommandFactory {
2882
2928
  ...(videoFiles && { videoFiles }),
2883
2929
  ...(files && { files }),
2884
2930
  },
2931
+ pdfOptions: {
2932
+ password: CLICommandFactory.resolvePdfPassword(argv),
2933
+ },
2885
2934
  csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
2886
2935
  videoOptions: {
2887
2936
  frames: argv.videoFrames,
@@ -3349,13 +3398,30 @@ export class CLICommandFactory {
3349
3398
  CLICommandFactory.validateAnthropicSubscriptionOptions(options);
3350
3399
  const spinner = options.quiet ? null : ora().start();
3351
3400
  try {
3352
- if (!argv.file) {
3401
+ if (!argv.promptsFile) {
3353
3402
  throw new Error("No file specified");
3354
3403
  }
3355
- if (!fs.existsSync(argv.file)) {
3356
- throw new Error(`File not found: ${argv.file}`);
3357
- }
3358
- const buffer = fs.readFileSync(argv.file);
3404
+ // #291/round-4 #1191: validate multimodal flags and resolve their
3405
+ // paths *before* the prompts file is read/parsed below, mirroring
3406
+ // generate/stream (which validate all inputs before any processing).
3407
+ // Previously this ran after `fs.readFileSync(resolvedPromptsPath)`,
3408
+ // so a large prompts file paired with an invalid --image path paid
3409
+ // the read/parse cost before the flag error ever surfaced. `--file`
3410
+ // (the common auto-detect flag) isn't registered on batch at all —
3411
+ // see createBatchCommand — so `argv.file` can never be set here; no
3412
+ // exclusion needed to protect the (differently-named) positional.
3413
+ validateCliInputFiles(argv);
3414
+ const batchImages = CLICommandFactory.processCliImages(argv.image);
3415
+ const batchCsvFiles = CLICommandFactory.processCliCSVFiles(argv.csv);
3416
+ const batchPdfFiles = CLICommandFactory.processCliPDFFiles(argv.pdf);
3417
+ const batchVideoFiles = CLICommandFactory.processCliVideoFiles(argv.video);
3418
+ // #291: route the prompts-list positional through the same friendly
3419
+ // aggregated-error path as the multimodal flags above (not found /
3420
+ // unreadable / directory / malformed file:// URL), instead of a raw
3421
+ // existsSync+readFileSync that throws an unfriendly EISDIR when
3422
+ // pointed at a directory.
3423
+ const resolvedPromptsPath = validatePromptsFilePath(argv.promptsFile);
3424
+ const buffer = fs.readFileSync(resolvedPromptsPath);
3359
3425
  const prompts = buffer
3360
3426
  .toString("utf8")
3361
3427
  .split("\n")
@@ -3390,6 +3456,19 @@ export class CLICommandFactory {
3390
3456
  const sessionVariables = CLICommandFactory.normalizeLoopSessionVariables(globalSession.getSessionVariables());
3391
3457
  const enhancedOptions = { ...options, ...sessionVariables };
3392
3458
  const sessionId = globalSession.getCurrentSessionId();
3459
+ // batchImages/batchCsvFiles/batchPdfFiles/batchVideoFiles were already
3460
+ // validated and resolved above, before the prompts file was read.
3461
+ if (batchImages?.length ||
3462
+ batchCsvFiles?.length ||
3463
+ batchPdfFiles?.length ||
3464
+ batchVideoFiles?.length) {
3465
+ // Pause the spinner so the notice isn't swallowed by its re-renders.
3466
+ // Safety-relevant, so always visible (not gated behind --quiet) and
3467
+ // written to stderr so `--format json` output on stdout stays parseable.
3468
+ spinner?.stop();
3469
+ logger.alwaysStderr(chalk.yellow("⚠️ Multimodal files (--image/--csv/--pdf/--video) are attached to ALL prompts in this batch."));
3470
+ spinner?.start();
3471
+ }
3393
3472
  for (let i = 0; i < prompts.length; i++) {
3394
3473
  if (spinner) {
3395
3474
  spinner.text = `Processing ${i + 1}/${prompts.length}: ${prompts[i].substring(0, 30)}...`;
@@ -3425,7 +3504,33 @@ export class CLICommandFactory {
3425
3504
  ? { ...contextMetadata, sessionId }
3426
3505
  : contextMetadata;
3427
3506
  const runBatchGenerate = () => sdk.generate({
3428
- input: { text: inputText },
3507
+ input: {
3508
+ text: inputText,
3509
+ ...(batchImages?.length && { images: batchImages }),
3510
+ ...(batchCsvFiles?.length && { csvFiles: batchCsvFiles }),
3511
+ ...(batchPdfFiles?.length && { pdfFiles: batchPdfFiles }),
3512
+ ...(batchVideoFiles?.length && {
3513
+ videoFiles: batchVideoFiles,
3514
+ }),
3515
+ },
3516
+ // Only construct these when the corresponding files are
3517
+ // actually attached — batch has no CSV/video mode to opt into,
3518
+ // unlike generate/stream, so an empty options object here is
3519
+ // pure noise on every prompt of every batch run.
3520
+ ...(batchCsvFiles?.length && {
3521
+ csvOptions: {
3522
+ maxRows: argv.csvMaxRows,
3523
+ formatStyle: argv.csvFormat,
3524
+ },
3525
+ }),
3526
+ ...(batchVideoFiles?.length && {
3527
+ videoOptions: {
3528
+ frames: argv.videoFrames,
3529
+ quality: argv.videoQuality,
3530
+ format: argv.videoFormat,
3531
+ transcribeAudio: argv.transcribeAudio,
3532
+ },
3533
+ }),
3429
3534
  provider: enhancedOptions.provider,
3430
3535
  model: enhancedOptions.model,
3431
3536
  temperature: enhancedOptions.temperature,
@@ -4,4 +4,4 @@ import type { OptionSchema, TextGenerationOptions } from "../../lib/types/index.
4
4
  * This object provides metadata for validation and help text in the CLI loop.
5
5
  * It is derived from the main TextGenerationOptions interface to ensure consistency.
6
6
  */
7
- export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation">, OptionSchema>;
7
+ export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "pdfOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation">, OptionSchema>;
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * CLI Input Validation Utilities
3
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.
4
+ * Validates multimodal file flags (--image/--csv/--pdf/--video/--file), the
5
+ * batch `<promptsFile>` positional, and --csv-max-rows before a
6
+ * generate/stream/batch run starts, so bad input fails fast with a clear
7
+ * message instead of surfacing deep inside provider calls. Exported as
8
+ * standalone functions (rather than private statics on CLICommandFactory)
9
+ * so callers including tests can import and invoke them directly,
10
+ * without reaching into the class via an `unknown` cast.
10
11
  */
11
12
  /**
12
13
  * Soft (warn-only) size thresholds for CLI multimodal flags. Intentionally
@@ -24,11 +25,23 @@ export declare const CLI_SOFT_LIMITS_MB: {
24
25
  };
25
26
  /**
26
27
  * 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).
28
+ * starts: every local path (including `file://` URLs) must exist, must not
29
+ * be a directory, and — for the flags with a soft limit — triggers a size
30
+ * warning above the threshold. Genuine http(s)/data: URLs skip both
31
+ * existence and size checks (#319).
30
32
  */
31
33
  export declare function validateCliInputFiles(argv: Record<string, unknown>): void;
34
+ /**
35
+ * #291: validate the batch `<promptsFile>` positional through the same
36
+ * aggregated-error-friendly path as the multimodal flags above (not found /
37
+ * unreadable / directory / malformed `file://` URL), instead of a raw
38
+ * existsSync+readFileSync that throws an unfriendly EISDIR when pointed at
39
+ * a directory. The `<file>` label (rather than `--file`) keeps the message
40
+ * from being confused with the `--file` flag, which batch doesn't register
41
+ * at all (see `createBatchCommand`). Returns the resolved local path for
42
+ * the caller to read.
43
+ */
44
+ export declare function validatePromptsFilePath(rawPath: string): string;
32
45
  /**
33
46
  * #310: validate --csv-max-rows is a positive integer in the range
34
47
  * 1-100000, matching the documented option range (the value previously
@@ -1,17 +1,20 @@
1
1
  /**
2
2
  * CLI Input Validation Utilities
3
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.
4
+ * Validates multimodal file flags (--image/--csv/--pdf/--video/--file), the
5
+ * batch `<promptsFile>` positional, and --csv-max-rows before a
6
+ * generate/stream/batch run starts, so bad input fails fast with a clear
7
+ * message instead of surfacing deep inside provider calls. Exported as
8
+ * standalone functions (rather than private statics on CLICommandFactory)
9
+ * so callers including tests can import and invoke them directly,
10
+ * without reaching into the class via an `unknown` cast.
10
11
  */
11
12
  import fs from "node:fs";
13
+ import { fileURLToPath } from "node:url";
12
14
  import chalk from "chalk";
13
15
  import { logger } from "../../lib/utils/logger.js";
14
- import { resolveFilePaths } from "./pathResolver.js";
16
+ import { redactUrlForError } from "../../lib/utils/logSanitize.js";
17
+ import { resolveFilePath } from "./pathResolver.js";
15
18
  /**
16
19
  * Soft (warn-only) size thresholds for CLI multimodal flags. Intentionally
17
20
  * defined here rather than reusing `lib/processors/config/sizeLimits.ts`'s
@@ -26,18 +29,126 @@ export const CLI_SOFT_LIMITS_MB = {
26
29
  PDF_MAX_MB: 100,
27
30
  VIDEO_MAX_MB: 500,
28
31
  };
32
+ // `file://` is intentionally excluded here: it addresses a local path, so it
33
+ // must still go through directory/missing-path validation below (via
34
+ // tryResolveFileUrl in validateSingleLocalPath). Only genuine remote/inline
35
+ // references (http(s), data URIs) skip validation entirely (#319).
29
36
  function isNonLocalFileReference(filePath) {
30
37
  const lower = filePath.toLowerCase();
31
38
  return (lower.startsWith("http://") ||
32
39
  lower.startsWith("https://") ||
33
- lower.startsWith("file://") ||
34
40
  lower.startsWith("data:"));
35
41
  }
42
+ // #291: a `file://` URL still points at something on disk, so validation
43
+ // needs the real filesystem path. Returns undefined (instead of throwing)
44
+ // for a malformed URL so the caller can report a friendly error.
45
+ function tryResolveFileUrl(filePath) {
46
+ if (!filePath.toLowerCase().startsWith("file://")) {
47
+ return filePath;
48
+ }
49
+ try {
50
+ return fileURLToPath(filePath);
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ /**
57
+ * Shared aggregated-error format for any invalid local file path (missing,
58
+ * unreadable, a directory, or a malformed `file://` URL). Used by both the
59
+ * flag-based loop in `validateCliInputFiles` and the batch `<promptsFile>`
60
+ * positional check in `validatePromptsFilePath`, so every caller gets the
61
+ * same clear error + troubleshooting hints instead of a raw exception.
62
+ */
63
+ function buildInvalidFileError(errors) {
64
+ return new Error("❌ One or more input files are invalid:\n" +
65
+ `${errors.join("\n")}\n` +
66
+ "💡 Troubleshooting:\n" +
67
+ " • Check the path is correct and relative to your current directory, or pass an absolute path / URL.\n" +
68
+ ` • Check your working directory (${process.cwd()}).\n` +
69
+ " • For remote resources, prefix the value with http:// or https://.\n" +
70
+ " • If you pointed at a folder, point at the file inside it instead.");
71
+ }
72
+ /**
73
+ * Validate a single raw path/URL value for `option`, pushing a friendly
74
+ * message onto `errors` instead of letting fs calls throw a raw exception
75
+ * (ENOENT, EACCES/EPERM TOCTOU, a directory, or a malformed `file://` URL).
76
+ * Returns the resolved local filesystem path on success (or the original
77
+ * value unchanged for a skipped http(s)/data: reference), and `undefined`
78
+ * when an error was pushed.
79
+ *
80
+ * `rawPath` is redacted via `redactUrlForError` before ever being
81
+ * interpolated into a message — a `file://` value can carry a sensitive
82
+ * query string, fragment, or embedded `user:pass@` credentials that must
83
+ * never be echoed back verbatim. This is a no-op for ordinary filesystem
84
+ * paths (they aren't parseable as a URL, so the redaction fallback simply
85
+ * returns them unchanged).
86
+ */
87
+ function validateSingleLocalPath(option, rawPath, errors, warnAtMB) {
88
+ const resolvedPath = resolveFilePath(rawPath);
89
+ if (isNonLocalFileReference(resolvedPath)) {
90
+ // URLs / data: refs skip both existence and size checks (#319).
91
+ return resolvedPath;
92
+ }
93
+ const safeRawPath = redactUrlForError(rawPath);
94
+ // #291: `file://` URLs are local paths in disguise — validate the decoded
95
+ // filesystem path so a directory or missing path is still caught here
96
+ // instead of failing cryptically deep in processing.
97
+ const pathToValidate = tryResolveFileUrl(resolvedPath);
98
+ if (pathToValidate === undefined) {
99
+ errors.push(`${option} is not a valid file:// URL: ${safeRawPath}`);
100
+ return undefined;
101
+ }
102
+ const safeResolvedPath = redactUrlForError(pathToValidate);
103
+ // Single statSync covers existence, directory rejection, and the size
104
+ // warning below — avoids a second syscall and closes the TOCTOU gap a
105
+ // separate existsSync()+statSync() pair would leave.
106
+ let stat;
107
+ try {
108
+ stat = fs.statSync(pathToValidate);
109
+ }
110
+ catch (err) {
111
+ // Only ENOENT actually means "not found" — EACCES (permission denied),
112
+ // ELOOP (symlink loop), ENOTDIR, etc. are real failures that get masked
113
+ // (and made undebuggable) if we blanket-label every statSync error the
114
+ // same way.
115
+ const code = err?.code;
116
+ if (code === "ENOENT") {
117
+ errors.push(`${option} path not found: ${safeRawPath} (resolved to ${safeResolvedPath})`);
118
+ }
119
+ else {
120
+ const reason = err instanceof Error ? err.message : String(err);
121
+ errors.push(`${option} path could not be accessed: ${safeRawPath} (resolved to ${safeResolvedPath}) — ` +
122
+ `${code ? `${code}: ` : ""}${reason}`);
123
+ }
124
+ return undefined;
125
+ }
126
+ // #288: reject a directory before it fails cryptically deep in processing
127
+ // (readFileSync on a dir throws a raw EISDIR).
128
+ if (stat.isDirectory()) {
129
+ errors.push(`${option} path is a directory, not a file: ${safeRawPath} (resolved to ${safeResolvedPath})`);
130
+ return undefined;
131
+ }
132
+ // #319: warn (not error) when a local multimodal file is unusually large
133
+ // so the user isn't surprised by slow processing / token blowups. Safety-
134
+ // relevant, so always visible (not gated behind --quiet) and written to
135
+ // stderr — never stdout — so `--format json` output stays parseable.
136
+ if (warnAtMB !== undefined) {
137
+ const sizeMB = stat.size / (1024 * 1024);
138
+ if (sizeMB > warnAtMB) {
139
+ logger.alwaysStderr(chalk.yellow(`⚠️ ${option} file ${safeRawPath} is ${sizeMB.toFixed(1)}MB ` +
140
+ `(above the ${warnAtMB}MB soft limit) — processing may be slow ` +
141
+ `or exceed provider token/size limits.`));
142
+ }
143
+ }
144
+ return pathToValidate;
145
+ }
36
146
  /**
37
147
  * 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).
148
+ * starts: every local path (including `file://` URLs) must exist, must not
149
+ * be a directory, and — for the flags with a soft limit — triggers a size
150
+ * warning above the threshold. Genuine http(s)/data: URLs skip both
151
+ * existence and size checks (#319).
41
152
  */
42
153
  export function validateCliInputFiles(argv) {
43
154
  const fileArgs = [
@@ -63,64 +174,39 @@ export function validateCliInputFiles(argv) {
63
174
  },
64
175
  { option: "--file", value: argv.file },
65
176
  ];
66
- const missingPaths = [];
177
+ const errors = [];
67
178
  for (const { option, value, warnAtMB } of fileArgs) {
68
179
  if (!value) {
69
180
  continue;
70
181
  }
71
182
  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
- }
183
+ for (const rawPath of rawPaths) {
184
+ validateSingleLocalPath(option, rawPath, errors, warnAtMB);
116
185
  }
117
186
  }
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.");
187
+ if (errors.length > 0) {
188
+ throw buildInvalidFileError(errors);
189
+ }
190
+ }
191
+ /**
192
+ * #291: validate the batch `<promptsFile>` positional through the same
193
+ * aggregated-error-friendly path as the multimodal flags above (not found /
194
+ * unreadable / directory / malformed `file://` URL), instead of a raw
195
+ * existsSync+readFileSync that throws an unfriendly EISDIR when pointed at
196
+ * a directory. The `<file>` label (rather than `--file`) keeps the message
197
+ * from being confused with the `--file` flag, which batch doesn't register
198
+ * at all (see `createBatchCommand`). Returns the resolved local path for
199
+ * the caller to read.
200
+ */
201
+ export function validatePromptsFilePath(rawPath) {
202
+ const errors = [];
203
+ const resolved = validateSingleLocalPath("<file>", rawPath, errors);
204
+ if (errors.length > 0) {
205
+ throw buildInvalidFileError(errors);
123
206
  }
207
+ // `errors` is empty here, so `validateSingleLocalPath` did not return
208
+ // undefined.
209
+ return resolved;
124
210
  }
125
211
  /**
126
212
  * #310: validate --csv-max-rows is a positive integer in the range
@@ -420,6 +420,7 @@ export class BaseProvider {
420
420
  toolUsageContext: options.toolUsageContext,
421
421
  context: options.context,
422
422
  csvOptions: options.csvOptions,
423
+ pdfOptions: options.pdfOptions,
423
424
  // Forward abort, tool filtering, and timeout options to prevent
424
425
  // silent bypass when falling back from real streaming to fake streaming
425
426
  abortSignal: options.abortSignal,
@@ -122,6 +122,8 @@ export declare const PDF_LIMITS: {
122
122
  MAX_SIZE_MB: number;
123
123
  DEFAULT_MAX_PAGES: number;
124
124
  MAX_SCALE: number;
125
+ DEFAULT_MAX_CANVAS_PIXELS: number;
126
+ MIN_EFFECTIVE_SCALE: number;
125
127
  };
126
128
  export declare const SYSTEM_LIMITS: {
127
129
  MAX_PROMPT_LENGTH: number;
@@ -220,6 +220,15 @@ export const PDF_LIMITS = {
220
220
  // Upper bound for the render scale factor. Above this, a single page can
221
221
  // allocate hundreds of MB; scale <= 0 produces a degenerate viewport.
222
222
  MAX_SCALE: 10,
223
+ // Default per-page pixel ceiling for image conversion (#260). 16.7M px ×
224
+ // 4 bytes RGBA ≈ 64 MB per page — safely bounded. A larger page is
225
+ // uniformly downscaled to stay under this instead of allocating gigabytes.
226
+ DEFAULT_MAX_CANVAS_PIXELS: 16_777_216,
227
+ // Floor for the downscaled render scale (#260 follow-up). A crafted/malformed
228
+ // MediaBox can drive the estimated pixel count toward Infinity, which would
229
+ // otherwise collapse `effectiveScale` to 0 and hand `pdf-to-img` a degenerate
230
+ // viewport. Never let the downscale branch go below this.
231
+ MIN_EFFECTIVE_SCALE: 0.1,
223
232
  };
224
233
  // Performance and System Limits
225
234
  export const SYSTEM_LIMITS = {
@@ -103,6 +103,7 @@ export class MessageBuilder {
103
103
  files: input?.files,
104
104
  },
105
105
  csvOptions: options.csvOptions,
106
+ pdfOptions: options.pdfOptions,
106
107
  provider: options.provider,
107
108
  model: options.model,
108
109
  temperature: options.temperature,
@@ -224,6 +225,7 @@ export class MessageBuilder {
224
225
  files: input?.files,
225
226
  },
226
227
  csvOptions: options.csvOptions,
228
+ pdfOptions: options.pdfOptions,
227
229
  provider: options.provider,
228
230
  model: options.model,
229
231
  temperature: options.temperature,
@@ -420,6 +420,7 @@ export class BaseProvider {
420
420
  toolUsageContext: options.toolUsageContext,
421
421
  context: options.context,
422
422
  csvOptions: options.csvOptions,
423
+ pdfOptions: options.pdfOptions,
423
424
  // Forward abort, tool filtering, and timeout options to prevent
424
425
  // silent bypass when falling back from real streaming to fake streaming
425
426
  abortSignal: options.abortSignal,
@@ -122,6 +122,8 @@ export declare const PDF_LIMITS: {
122
122
  MAX_SIZE_MB: number;
123
123
  DEFAULT_MAX_PAGES: number;
124
124
  MAX_SCALE: number;
125
+ DEFAULT_MAX_CANVAS_PIXELS: number;
126
+ MIN_EFFECTIVE_SCALE: number;
125
127
  };
126
128
  export declare const SYSTEM_LIMITS: {
127
129
  MAX_PROMPT_LENGTH: number;
@@ -220,6 +220,15 @@ export const PDF_LIMITS = {
220
220
  // Upper bound for the render scale factor. Above this, a single page can
221
221
  // allocate hundreds of MB; scale <= 0 produces a degenerate viewport.
222
222
  MAX_SCALE: 10,
223
+ // Default per-page pixel ceiling for image conversion (#260). 16.7M px ×
224
+ // 4 bytes RGBA ≈ 64 MB per page — safely bounded. A larger page is
225
+ // uniformly downscaled to stay under this instead of allocating gigabytes.
226
+ DEFAULT_MAX_CANVAS_PIXELS: 16_777_216,
227
+ // Floor for the downscaled render scale (#260 follow-up). A crafted/malformed
228
+ // MediaBox can drive the estimated pixel count toward Infinity, which would
229
+ // otherwise collapse `effectiveScale` to 0 and hand `pdf-to-img` a degenerate
230
+ // viewport. Never let the downscale branch go below this.
231
+ MIN_EFFECTIVE_SCALE: 0.1,
223
232
  };
224
233
  // Performance and System Limits
225
234
  export const SYSTEM_LIMITS = {
@@ -103,6 +103,7 @@ export class MessageBuilder {
103
103
  files: input?.files,
104
104
  },
105
105
  csvOptions: options.csvOptions,
106
+ pdfOptions: options.pdfOptions,
106
107
  provider: options.provider,
107
108
  model: options.model,
108
109
  temperature: options.temperature,
@@ -224,6 +225,7 @@ export class MessageBuilder {
224
225
  files: input?.files,
225
226
  },
226
227
  csvOptions: options.csvOptions,
228
+ pdfOptions: options.pdfOptions,
227
229
  provider: options.provider,
228
230
  model: options.model,
229
231
  temperature: options.temperature,