@juspay/neurolink 9.94.7 → 9.95.1
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/analytics/index.d.ts +7 -0
- package/dist/analytics/index.js +7 -0
- package/dist/analytics/parseQualityScore.d.ts +9 -0
- package/dist/analytics/parseQualityScore.js +29 -0
- package/dist/analytics/pricing.d.ts +24 -0
- package/dist/analytics/pricing.js +37 -0
- package/dist/analytics/service.d.ts +36 -0
- package/dist/analytics/service.js +393 -0
- package/dist/analytics/storage.d.ts +19 -0
- package/dist/analytics/storage.js +32 -0
- package/dist/browser/neurolink.min.js +389 -389
- package/dist/cli/factories/commandFactory.js +88 -13
- package/dist/cli/utils/inputValidation.d.ts +22 -9
- package/dist/cli/utils/inputValidation.js +147 -61
- package/dist/lib/analytics/index.d.ts +7 -0
- package/dist/lib/analytics/index.js +8 -0
- package/dist/lib/analytics/parseQualityScore.d.ts +9 -0
- package/dist/lib/analytics/parseQualityScore.js +30 -0
- package/dist/lib/analytics/pricing.d.ts +24 -0
- package/dist/lib/analytics/pricing.js +38 -0
- package/dist/lib/analytics/service.d.ts +36 -0
- package/dist/lib/analytics/service.js +394 -0
- package/dist/lib/analytics/storage.d.ts +19 -0
- package/dist/lib/analytics/storage.js +33 -0
- package/dist/lib/neurolink.d.ts +54 -0
- package/dist/lib/neurolink.js +302 -204
- package/dist/lib/types/analytics.d.ts +127 -0
- package/dist/lib/types/cli.d.ts +7 -2
- package/dist/lib/utils/logger.d.ts +12 -0
- package/dist/lib/utils/logger.js +16 -0
- package/dist/neurolink.d.ts +54 -0
- package/dist/neurolink.js +302 -204
- package/dist/types/analytics.d.ts +127 -0
- package/dist/types/cli.d.ts +7 -2
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +16 -0
- package/package.json +2 -1
|
@@ -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";
|
|
@@ -742,11 +742,18 @@ export class CLICommandFactory {
|
|
|
742
742
|
alias: "skills-dir",
|
|
743
743
|
},
|
|
744
744
|
};
|
|
745
|
-
// Helper method to build options for commands
|
|
746
|
-
|
|
745
|
+
// Helper method to build options for commands. `excludeOptions` drops
|
|
746
|
+
// common flags from registration entirely (not just from validation) —
|
|
747
|
+
// #1191 round-5: batch uses this to omit `commonOptions.file` so `--file`
|
|
748
|
+
// isn't a recognized flag at all, since it would collide with the
|
|
749
|
+
// `<file>` positional (the prompts list).
|
|
750
|
+
static buildOptions(yargs, additionalOptions = {}, excludeOptions = []) {
|
|
751
|
+
const baseOptions = excludeOptions.length === 0
|
|
752
|
+
? CLICommandFactory.commonOptions
|
|
753
|
+
: Object.fromEntries(Object.entries(CLICommandFactory.commonOptions).filter(([key]) => !excludeOptions.includes(key)));
|
|
747
754
|
return (yargs
|
|
748
755
|
.options({
|
|
749
|
-
...
|
|
756
|
+
...baseOptions,
|
|
750
757
|
...additionalOptions,
|
|
751
758
|
})
|
|
752
759
|
// NEW9: implies relationships so users who pass --stt-provider or
|
|
@@ -1543,11 +1550,19 @@ export class CLICommandFactory {
|
|
|
1543
1550
|
*/
|
|
1544
1551
|
static createBatchCommand() {
|
|
1545
1552
|
return {
|
|
1546
|
-
|
|
1553
|
+
// #1191 round-5: the positional is named `promptsFile`, not `file` —
|
|
1554
|
+
// yargs implicitly accepts `--<positionalName>` as a flag alias for
|
|
1555
|
+
// any positional regardless of whether it's separately registered via
|
|
1556
|
+
// `.options()`, so a positional literally named `file` would let
|
|
1557
|
+
// `--file` silently pass strictOptions() even after removing
|
|
1558
|
+
// `commonOptions.file` below. Renaming the key (invocation syntax is
|
|
1559
|
+
// unaffected — it's still just `neurolink batch prompts.txt`) is what
|
|
1560
|
+
// actually makes `--file` an unknown argument.
|
|
1561
|
+
command: "batch <promptsFile>",
|
|
1547
1562
|
describe: "Process multiple prompts from a file",
|
|
1548
1563
|
builder: (yargs) => {
|
|
1549
1564
|
return CLICommandFactory.buildOptions(yargs
|
|
1550
|
-
.positional("
|
|
1565
|
+
.positional("promptsFile", {
|
|
1551
1566
|
type: "string",
|
|
1552
1567
|
description: "File with prompts (one per line)",
|
|
1553
1568
|
demandOption: true,
|
|
@@ -1555,7 +1570,11 @@ export class CLICommandFactory {
|
|
|
1555
1570
|
.example("$0 batch prompts.txt", "Process prompts from file")
|
|
1556
1571
|
.example("$0 batch questions.txt --format json", "Export results as JSON")
|
|
1557
1572
|
.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")
|
|
1573
|
+
.example("$0 batch batch.txt --output results.json", "Save results to file"), {},
|
|
1574
|
+
// `--file` is not a batch flag — the positional above is the
|
|
1575
|
+
// prompts-list path. Omit it from registration so it's rejected
|
|
1576
|
+
// as unknown rather than silently accepted then ignored.
|
|
1577
|
+
["file"]);
|
|
1559
1578
|
},
|
|
1560
1579
|
handler: async (argv) => await CLICommandFactory.executeBatch(argv),
|
|
1561
1580
|
};
|
|
@@ -3349,13 +3368,30 @@ export class CLICommandFactory {
|
|
|
3349
3368
|
CLICommandFactory.validateAnthropicSubscriptionOptions(options);
|
|
3350
3369
|
const spinner = options.quiet ? null : ora().start();
|
|
3351
3370
|
try {
|
|
3352
|
-
if (!argv.
|
|
3371
|
+
if (!argv.promptsFile) {
|
|
3353
3372
|
throw new Error("No file specified");
|
|
3354
3373
|
}
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3374
|
+
// #291/round-4 #1191: validate multimodal flags and resolve their
|
|
3375
|
+
// paths *before* the prompts file is read/parsed below, mirroring
|
|
3376
|
+
// generate/stream (which validate all inputs before any processing).
|
|
3377
|
+
// Previously this ran after `fs.readFileSync(resolvedPromptsPath)`,
|
|
3378
|
+
// so a large prompts file paired with an invalid --image path paid
|
|
3379
|
+
// the read/parse cost before the flag error ever surfaced. `--file`
|
|
3380
|
+
// (the common auto-detect flag) isn't registered on batch at all —
|
|
3381
|
+
// see createBatchCommand — so `argv.file` can never be set here; no
|
|
3382
|
+
// exclusion needed to protect the (differently-named) positional.
|
|
3383
|
+
validateCliInputFiles(argv);
|
|
3384
|
+
const batchImages = CLICommandFactory.processCliImages(argv.image);
|
|
3385
|
+
const batchCsvFiles = CLICommandFactory.processCliCSVFiles(argv.csv);
|
|
3386
|
+
const batchPdfFiles = CLICommandFactory.processCliPDFFiles(argv.pdf);
|
|
3387
|
+
const batchVideoFiles = CLICommandFactory.processCliVideoFiles(argv.video);
|
|
3388
|
+
// #291: route the prompts-list positional through the same friendly
|
|
3389
|
+
// aggregated-error path as the multimodal flags above (not found /
|
|
3390
|
+
// unreadable / directory / malformed file:// URL), instead of a raw
|
|
3391
|
+
// existsSync+readFileSync that throws an unfriendly EISDIR when
|
|
3392
|
+
// pointed at a directory.
|
|
3393
|
+
const resolvedPromptsPath = validatePromptsFilePath(argv.promptsFile);
|
|
3394
|
+
const buffer = fs.readFileSync(resolvedPromptsPath);
|
|
3359
3395
|
const prompts = buffer
|
|
3360
3396
|
.toString("utf8")
|
|
3361
3397
|
.split("\n")
|
|
@@ -3390,6 +3426,19 @@ export class CLICommandFactory {
|
|
|
3390
3426
|
const sessionVariables = CLICommandFactory.normalizeLoopSessionVariables(globalSession.getSessionVariables());
|
|
3391
3427
|
const enhancedOptions = { ...options, ...sessionVariables };
|
|
3392
3428
|
const sessionId = globalSession.getCurrentSessionId();
|
|
3429
|
+
// batchImages/batchCsvFiles/batchPdfFiles/batchVideoFiles were already
|
|
3430
|
+
// validated and resolved above, before the prompts file was read.
|
|
3431
|
+
if (batchImages?.length ||
|
|
3432
|
+
batchCsvFiles?.length ||
|
|
3433
|
+
batchPdfFiles?.length ||
|
|
3434
|
+
batchVideoFiles?.length) {
|
|
3435
|
+
// Pause the spinner so the notice isn't swallowed by its re-renders.
|
|
3436
|
+
// Safety-relevant, so always visible (not gated behind --quiet) and
|
|
3437
|
+
// written to stderr so `--format json` output on stdout stays parseable.
|
|
3438
|
+
spinner?.stop();
|
|
3439
|
+
logger.alwaysStderr(chalk.yellow("⚠️ Multimodal files (--image/--csv/--pdf/--video) are attached to ALL prompts in this batch."));
|
|
3440
|
+
spinner?.start();
|
|
3441
|
+
}
|
|
3393
3442
|
for (let i = 0; i < prompts.length; i++) {
|
|
3394
3443
|
if (spinner) {
|
|
3395
3444
|
spinner.text = `Processing ${i + 1}/${prompts.length}: ${prompts[i].substring(0, 30)}...`;
|
|
@@ -3425,7 +3474,33 @@ export class CLICommandFactory {
|
|
|
3425
3474
|
? { ...contextMetadata, sessionId }
|
|
3426
3475
|
: contextMetadata;
|
|
3427
3476
|
const runBatchGenerate = () => sdk.generate({
|
|
3428
|
-
input: {
|
|
3477
|
+
input: {
|
|
3478
|
+
text: inputText,
|
|
3479
|
+
...(batchImages?.length && { images: batchImages }),
|
|
3480
|
+
...(batchCsvFiles?.length && { csvFiles: batchCsvFiles }),
|
|
3481
|
+
...(batchPdfFiles?.length && { pdfFiles: batchPdfFiles }),
|
|
3482
|
+
...(batchVideoFiles?.length && {
|
|
3483
|
+
videoFiles: batchVideoFiles,
|
|
3484
|
+
}),
|
|
3485
|
+
},
|
|
3486
|
+
// Only construct these when the corresponding files are
|
|
3487
|
+
// actually attached — batch has no CSV/video mode to opt into,
|
|
3488
|
+
// unlike generate/stream, so an empty options object here is
|
|
3489
|
+
// pure noise on every prompt of every batch run.
|
|
3490
|
+
...(batchCsvFiles?.length && {
|
|
3491
|
+
csvOptions: {
|
|
3492
|
+
maxRows: argv.csvMaxRows,
|
|
3493
|
+
formatStyle: argv.csvFormat,
|
|
3494
|
+
},
|
|
3495
|
+
}),
|
|
3496
|
+
...(batchVideoFiles?.length && {
|
|
3497
|
+
videoOptions: {
|
|
3498
|
+
frames: argv.videoFrames,
|
|
3499
|
+
quality: argv.videoQuality,
|
|
3500
|
+
format: argv.videoFormat,
|
|
3501
|
+
transcribeAudio: argv.transcribeAudio,
|
|
3502
|
+
},
|
|
3503
|
+
}),
|
|
3429
3504
|
provider: enhancedOptions.provider,
|
|
3430
3505
|
model: enhancedOptions.model,
|
|
3431
3506
|
temperature: enhancedOptions.temperature,
|
|
@@ -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
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Module Exports (runtime values only — types live in src/lib/types)
|
|
3
|
+
*/
|
|
4
|
+
export { calculateAdvancedCost } from "./pricing.js";
|
|
5
|
+
export { InMemoryAnalyticsStorage, DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS, } from "./storage.js";
|
|
6
|
+
export { AnalyticsService } from "./service.js";
|
|
7
|
+
export { parseAnalyticsQualityScore } from "./parseQualityScore.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Module Exports (runtime values only — types live in src/lib/types)
|
|
3
|
+
*/
|
|
4
|
+
export { calculateAdvancedCost } from "./pricing.js";
|
|
5
|
+
export { InMemoryAnalyticsStorage, DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS, } from "./storage.js";
|
|
6
|
+
export { AnalyticsService } from "./service.js";
|
|
7
|
+
export { parseAnalyticsQualityScore } from "./parseQualityScore.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe narrowing of unknown event payloads into AnalyticsQualityScore.
|
|
3
|
+
*/
|
|
4
|
+
import type { AnalyticsQualityScore } from "../types/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Convert an unknown quality-score payload into AnalyticsQualityScore.
|
|
7
|
+
* Returns undefined for missing, null, or malformed values.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseAnalyticsQualityScore(value: unknown): AnalyticsQualityScore | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe narrowing of unknown event payloads into AnalyticsQualityScore.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Convert an unknown quality-score payload into AnalyticsQualityScore.
|
|
6
|
+
* Returns undefined for missing, null, or malformed values.
|
|
7
|
+
*/
|
|
8
|
+
export function parseAnalyticsQualityScore(value) {
|
|
9
|
+
if (!value || typeof value !== "object") {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const record = value;
|
|
13
|
+
// Reject NaN/Infinity — typeof NaN === "number"
|
|
14
|
+
if (!Number.isFinite(record.overall) ||
|
|
15
|
+
!Number.isFinite(record.relevance) ||
|
|
16
|
+
!Number.isFinite(record.accuracy) ||
|
|
17
|
+
!Number.isFinite(record.completeness)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
overall: record.overall,
|
|
22
|
+
relevance: record.relevance,
|
|
23
|
+
accuracy: record.accuracy,
|
|
24
|
+
completeness: record.completeness,
|
|
25
|
+
...(typeof record.reasoning === "string"
|
|
26
|
+
? { reasoning: record.reasoning }
|
|
27
|
+
: {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=parseQualityScore.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics cost helpers.
|
|
3
|
+
*
|
|
4
|
+
* Prefer costs already computed by the telemetry/pricing pipeline
|
|
5
|
+
* (`utils/pricing.calculateCost` / span `ai.cost.total`). This module
|
|
6
|
+
* is a thin compatibility wrapper — it does not maintain a second
|
|
7
|
+
* hardcoded pricing table.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Calculate cost from token counts using the canonical SDK pricing table.
|
|
11
|
+
* Returns 0 when the provider/model combination is unknown.
|
|
12
|
+
*
|
|
13
|
+
* Partial model-name matching uses longest-prefix preference inside
|
|
14
|
+
* `calculateCost` / `findRates` — shorter keys (e.g. `gpt-4o`) never
|
|
15
|
+
* win over more specific ones (e.g. `gpt-4o-mini`).
|
|
16
|
+
*
|
|
17
|
+
* @param model - Model name
|
|
18
|
+
* @param inputTokens - Number of input tokens
|
|
19
|
+
* @param outputTokens - Number of output tokens
|
|
20
|
+
* @param provider - Provider name (improves rate lookup; defaults to
|
|
21
|
+
* cross-provider search via openrouter alias)
|
|
22
|
+
* @returns Total cost in USD
|
|
23
|
+
*/
|
|
24
|
+
export declare function calculateAdvancedCost(model: string | undefined, inputTokens: number, outputTokens: number, provider?: string): number;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics cost helpers.
|
|
3
|
+
*
|
|
4
|
+
* Prefer costs already computed by the telemetry/pricing pipeline
|
|
5
|
+
* (`utils/pricing.calculateCost` / span `ai.cost.total`). This module
|
|
6
|
+
* is a thin compatibility wrapper — it does not maintain a second
|
|
7
|
+
* hardcoded pricing table.
|
|
8
|
+
*/
|
|
9
|
+
import { calculateCost } from "../utils/pricing.js";
|
|
10
|
+
/**
|
|
11
|
+
* Calculate cost from token counts using the canonical SDK pricing table.
|
|
12
|
+
* Returns 0 when the provider/model combination is unknown.
|
|
13
|
+
*
|
|
14
|
+
* Partial model-name matching uses longest-prefix preference inside
|
|
15
|
+
* `calculateCost` / `findRates` — shorter keys (e.g. `gpt-4o`) never
|
|
16
|
+
* win over more specific ones (e.g. `gpt-4o-mini`).
|
|
17
|
+
*
|
|
18
|
+
* @param model - Model name
|
|
19
|
+
* @param inputTokens - Number of input tokens
|
|
20
|
+
* @param outputTokens - Number of output tokens
|
|
21
|
+
* @param provider - Provider name (improves rate lookup; defaults to
|
|
22
|
+
* cross-provider search via openrouter alias)
|
|
23
|
+
* @returns Total cost in USD
|
|
24
|
+
*/
|
|
25
|
+
export function calculateAdvancedCost(model, inputTokens, outputTokens, provider) {
|
|
26
|
+
if (!model) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
// When provider is unknown, use the openrouter/litellm cross-provider
|
|
30
|
+
// search path in findRates (PROVIDER_ALIASES → __cross_provider__).
|
|
31
|
+
const resolvedProvider = provider && provider.length > 0 ? provider : "openrouter";
|
|
32
|
+
return calculateCost(resolvedProvider, model, {
|
|
33
|
+
input: inputTokens,
|
|
34
|
+
output: outputTokens,
|
|
35
|
+
total: inputTokens + outputTokens,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=pricing.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Analytics Service Layer
|
|
3
|
+
* Handles metrics collection, aggregation, cost analysis, and projections.
|
|
4
|
+
*/
|
|
5
|
+
import type { AnalyticsStorage, CostAnalysisOptions, CostAnalysisResult, ProviderMetricsOptions, ProviderMetricsResult, TeamAnalyticsOptions, TeamAnalyticsResult, TelemetryRecord } from "../types/index.js";
|
|
6
|
+
export declare class AnalyticsService {
|
|
7
|
+
private storage;
|
|
8
|
+
constructor(storage?: AnalyticsStorage);
|
|
9
|
+
/**
|
|
10
|
+
* Helper to parse dynamic time range formats safely
|
|
11
|
+
*/
|
|
12
|
+
private parseTimeRange;
|
|
13
|
+
/**
|
|
14
|
+
* Capture a request lifecycle record
|
|
15
|
+
*/
|
|
16
|
+
trackRequest(record: Omit<TelemetryRecord, "id" | "cost"> & {
|
|
17
|
+
cost?: number;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* getProviderMetrics implementation
|
|
21
|
+
*/
|
|
22
|
+
getProviderMetrics(options?: ProviderMetricsOptions): Promise<ProviderMetricsResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Helper to format timestamps for date groupings.
|
|
25
|
+
* Clones the Date so week arithmetic never mutates the caller's value.
|
|
26
|
+
*/
|
|
27
|
+
private formatGroupDate;
|
|
28
|
+
/**
|
|
29
|
+
* getCostAnalysis implementation
|
|
30
|
+
*/
|
|
31
|
+
getCostAnalysis(options?: CostAnalysisOptions): Promise<CostAnalysisResult>;
|
|
32
|
+
/**
|
|
33
|
+
* getTeamAnalytics implementation
|
|
34
|
+
*/
|
|
35
|
+
getTeamAnalytics(options?: TeamAnalyticsOptions): Promise<TeamAnalyticsResult>;
|
|
36
|
+
}
|