@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/analytics/index.d.ts +7 -0
  3. package/dist/analytics/index.js +7 -0
  4. package/dist/analytics/parseQualityScore.d.ts +9 -0
  5. package/dist/analytics/parseQualityScore.js +29 -0
  6. package/dist/analytics/pricing.d.ts +24 -0
  7. package/dist/analytics/pricing.js +37 -0
  8. package/dist/analytics/service.d.ts +36 -0
  9. package/dist/analytics/service.js +393 -0
  10. package/dist/analytics/storage.d.ts +19 -0
  11. package/dist/analytics/storage.js +32 -0
  12. package/dist/browser/neurolink.min.js +389 -389
  13. package/dist/cli/factories/commandFactory.js +88 -13
  14. package/dist/cli/utils/inputValidation.d.ts +22 -9
  15. package/dist/cli/utils/inputValidation.js +147 -61
  16. package/dist/lib/analytics/index.d.ts +7 -0
  17. package/dist/lib/analytics/index.js +8 -0
  18. package/dist/lib/analytics/parseQualityScore.d.ts +9 -0
  19. package/dist/lib/analytics/parseQualityScore.js +30 -0
  20. package/dist/lib/analytics/pricing.d.ts +24 -0
  21. package/dist/lib/analytics/pricing.js +38 -0
  22. package/dist/lib/analytics/service.d.ts +36 -0
  23. package/dist/lib/analytics/service.js +394 -0
  24. package/dist/lib/analytics/storage.d.ts +19 -0
  25. package/dist/lib/analytics/storage.js +33 -0
  26. package/dist/lib/neurolink.d.ts +54 -0
  27. package/dist/lib/neurolink.js +302 -204
  28. package/dist/lib/types/analytics.d.ts +127 -0
  29. package/dist/lib/types/cli.d.ts +7 -2
  30. package/dist/lib/utils/logger.d.ts +12 -0
  31. package/dist/lib/utils/logger.js +16 -0
  32. package/dist/neurolink.d.ts +54 -0
  33. package/dist/neurolink.js +302 -204
  34. package/dist/types/analytics.d.ts +127 -0
  35. package/dist/types/cli.d.ts +7 -2
  36. package/dist/utils/logger.d.ts +12 -0
  37. package/dist/utils/logger.js +16 -0
  38. 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
- static buildOptions(yargs, additionalOptions = {}) {
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
- ...CLICommandFactory.commonOptions,
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
- command: "batch <file>",
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("file", {
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.file) {
3371
+ if (!argv.promptsFile) {
3353
3372
  throw new Error("No file specified");
3354
3373
  }
3355
- if (!fs.existsSync(argv.file)) {
3356
- throw new Error(`File not found: ${argv.file}`);
3357
- }
3358
- const buffer = fs.readFileSync(argv.file);
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: { text: inputText },
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) 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
@@ -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
+ }