@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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +232 -232
- package/dist/cli/factories/commandFactory.d.ts +10 -0
- package/dist/cli/factories/commandFactory.js +118 -13
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/inputValidation.d.ts +22 -9
- package/dist/cli/utils/inputValidation.js +147 -61
- package/dist/core/baseProvider.js +1 -0
- package/dist/core/constants.d.ts +2 -0
- package/dist/core/constants.js +9 -0
- package/dist/core/modules/MessageBuilder.js +2 -0
- package/dist/lib/core/baseProvider.js +1 -0
- package/dist/lib/core/constants.d.ts +2 -0
- package/dist/lib/core/constants.js +9 -0
- package/dist/lib/core/modules/MessageBuilder.js +2 -0
- package/dist/lib/neurolink.js +8 -4
- package/dist/lib/types/cli.d.ts +7 -2
- package/dist/lib/types/file.d.ts +10 -0
- package/dist/lib/types/generate.d.ts +14 -0
- package/dist/lib/types/stream.d.ts +7 -0
- package/dist/lib/utils/errorHandling.d.ts +10 -0
- package/dist/lib/utils/errorHandling.js +28 -0
- package/dist/lib/utils/logger.d.ts +12 -0
- package/dist/lib/utils/logger.js +16 -0
- package/dist/lib/utils/messageBuilder.js +36 -5
- package/dist/lib/utils/multimodalOptionsBuilder.d.ts +4 -0
- package/dist/lib/utils/multimodalOptionsBuilder.js +1 -0
- package/dist/lib/utils/pdfProcessor.d.ts +15 -0
- package/dist/lib/utils/pdfProcessor.js +83 -3
- package/dist/neurolink.js +8 -4
- package/dist/types/cli.d.ts +7 -2
- package/dist/types/file.d.ts +10 -0
- package/dist/types/generate.d.ts +14 -0
- package/dist/types/stream.d.ts +7 -0
- package/dist/utils/errorHandling.d.ts +10 -0
- package/dist/utils/errorHandling.js +28 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +16 -0
- package/dist/utils/messageBuilder.js +36 -5
- package/dist/utils/multimodalOptionsBuilder.d.ts +4 -0
- package/dist/utils/multimodalOptionsBuilder.js +1 -0
- package/dist/utils/pdfProcessor.d.ts +15 -0
- package/dist/utils/pdfProcessor.js +83 -3
- 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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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("
|
|
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.
|
|
3401
|
+
if (!argv.promptsFile) {
|
|
3353
3402
|
throw new Error("No file specified");
|
|
3354
3403
|
}
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
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: {
|
|
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)
|
|
5
|
-
* --csv-max-rows before a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
28
|
-
* the flags with a soft limit — triggers a size
|
|
29
|
-
* threshold.
|
|
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)
|
|
5
|
-
* --csv-max-rows before a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 {
|
|
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
|
|
39
|
-
* the flags with a soft limit — triggers a size
|
|
40
|
-
* threshold.
|
|
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
|
|
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
|
|
73
|
-
|
|
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 (
|
|
119
|
-
throw
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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,
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/constants.js
CHANGED
|
@@ -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,
|