@juspay/neurolink 10.10.8 → 10.10.10

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.
@@ -228,6 +228,11 @@ export class CLICommandFactory {
228
228
  type: "number",
229
229
  description: "Wall-clock cap (ms) for CSV parsing; returns partial rows on timeout (#379).",
230
230
  },
231
+ "csv-skip-empty-lines": {
232
+ type: "boolean",
233
+ default: true,
234
+ description: "Skip blank/whitespace-only CSV data rows in content and rowCount (default true). Use --no-csv-skip-empty-lines to preserve them (#373).",
235
+ },
231
236
  model: {
232
237
  type: "string",
233
238
  description: "Specific model to use (e.g. gemini-2.5-pro, gemini-2.5-flash)",
@@ -2441,6 +2446,7 @@ export class CLICommandFactory {
2441
2446
  sanitizeColumnNames: argv.csvSanitizeNames,
2442
2447
  columnNameCase: argv.csvNameCase,
2443
2448
  parseTimeoutMs: argv.csvParseTimeoutMs,
2449
+ skipEmptyLines: argv.csvSkipEmptyLines,
2444
2450
  };
2445
2451
  }
2446
2452
  /**
@@ -41,6 +41,7 @@ import { SIZE_LIMITS_MB } from "../config/index.js";
41
41
  import { FileErrorCode } from "../errors/index.js";
42
42
  import { withTimeout } from "../../utils/timeout.js";
43
43
  import { formatMediaDuration } from "../../utils/mediaDuration.js";
44
+ import { logger } from "../../utils/logger.js";
44
45
  import { tryImport } from "../../utils/tryImport.js";
45
46
  let _musicMetadata = null;
46
47
  async function loadMusicMetadata() {
@@ -264,6 +265,11 @@ export class AudioProcessor extends BaseFileProcessor {
264
265
  transcript: transcriptionResult.transcript,
265
266
  hasTranscript: transcriptionResult.hasTranscript,
266
267
  transcriptionProvider: transcriptionResult.transcriptionProvider,
268
+ ...(transcriptionResult.transcriptionSkippedReason
269
+ ? {
270
+ transcriptionSkippedReason: transcriptionResult.transcriptionSkippedReason,
271
+ }
272
+ : {}),
267
273
  coverArt: coverArt ?? undefined,
268
274
  buffer,
269
275
  mimetype: fileInfo.mimetype || "audio/mpeg",
@@ -316,20 +322,25 @@ export class AudioProcessor extends BaseFileProcessor {
316
322
  * @returns Transcription result with transcript text, or empty result
317
323
  */
318
324
  async attemptTranscription(buffer, filename, mimetype) {
319
- const emptyResult = {
320
- transcript: undefined,
321
- hasTranscript: false,
322
- transcriptionProvider: undefined,
325
+ const skipped = (reason) => {
326
+ logger.warn(`[AudioProcessor] No transcript for ${filename}: ${reason}. ` +
327
+ `The model will receive metadata only.`);
328
+ return {
329
+ transcript: undefined,
330
+ hasTranscript: false,
331
+ transcriptionProvider: undefined,
332
+ transcriptionSkippedReason: reason,
333
+ };
323
334
  };
324
335
  // Check if OPENAI_API_KEY is available
325
336
  const apiKey = process.env.OPENAI_API_KEY;
326
337
  if (!apiKey) {
327
- return emptyResult;
338
+ return skipped("OPENAI_API_KEY is not set, and Whisper is the only transcription backend wired up");
328
339
  }
329
340
  // Check file size (Whisper limit is 25MB)
330
341
  const fileSizeMB = buffer.length / (1024 * 1024);
331
342
  if (fileSizeMB > AUDIO_CONFIG.WHISPER_MAX_SIZE_MB) {
332
- return emptyResult;
343
+ return skipped(`file is ${fileSizeMB.toFixed(1)}MB, over Whisper's ${AUDIO_CONFIG.WHISPER_MAX_SIZE_MB}MB limit — split or compress it`);
333
344
  }
334
345
  // Check if file format is supported by Whisper
335
346
  const ext = filename.split(".").pop()?.toLowerCase();
@@ -343,7 +354,7 @@ export class AudioProcessor extends BaseFileProcessor {
343
354
  mimetype.startsWith("audio/ogg") ||
344
355
  mimetype.startsWith("audio/x-m4a"));
345
356
  if (!isFormatSupported && !isMimeSupported) {
346
- return emptyResult;
357
+ return skipped(`format is not one Whisper accepts (extension "${ext ?? "none"}", mimetype "${mimetype ?? "none"}"); supported: ${AUDIO_CONFIG.WHISPER_SUPPORTED_FORMATS.join(", ")}`);
347
358
  }
348
359
  try {
349
360
  // Dynamic imports to avoid loading these modules when transcription is not needed
@@ -351,26 +362,33 @@ export class AudioProcessor extends BaseFileProcessor {
351
362
  const openai = createOpenAI({ apiKey });
352
363
  const model = openai.transcription("whisper-1");
353
364
  // Wrap in withTimeout — large audio files can take a while, but a
354
- // stalled request shouldn't block the processor forever. The outer
355
- // catch swallows any error (transcription is best-effort), so a
356
- // TimeoutError ends up in the same fallback path as other failures.
365
+ // stalled request shouldn't block the processor forever. A TimeoutError
366
+ // lands in the same handler as other failures below, which reports it as
367
+ // the reason rather than discarding it.
357
368
  const result = await withTimeout(experimental_transcribe({
358
369
  model,
359
370
  audio: buffer,
360
371
  }), AUDIO_CONFIG.TRANSCRIPTION_TIMEOUT_MS, "openai-whisper", "generate");
361
372
  if (result.text && result.text.trim().length > 0) {
373
+ logger.debug(`[AudioProcessor] Transcribed ${filename} via openai-whisper (${result.text.trim().length} chars)`);
362
374
  return {
363
375
  transcript: result.text.trim(),
364
376
  hasTranscript: true,
365
377
  transcriptionProvider: "openai-whisper",
378
+ transcriptionSkippedReason: undefined,
366
379
  };
367
380
  }
368
- return emptyResult;
381
+ // A successful call that returned nothing is a legitimate outcome
382
+ // (silence, music, no speech) — distinct from a failure.
383
+ return skipped("Whisper returned an empty transcript for this audio");
369
384
  }
370
- catch {
371
- // Transcription is best-effort — never fail the entire processing pipeline
372
- // Common failures: rate limiting, network issues, unsupported audio encoding
373
- return emptyResult;
385
+ catch (error) {
386
+ // Transcription stays best-effort — a failure must never kill the whole
387
+ // processing pipeline. But discarding the error outright, as this block
388
+ // used to, made a bad API key, a rate limit and a network blip all look
389
+ // identical to "this file has no speech in it".
390
+ const message = error instanceof Error ? error.message : String(error);
391
+ return skipped(`transcription request failed — ${message}`);
374
392
  }
375
393
  }
376
394
  // ===========================================================================
@@ -87,7 +87,14 @@ export class SkillsManager {
87
87
  // name-find would resolve non-deterministically to the stale deprecated one
88
88
  // across store backends. Fall back to any match only when no active skill
89
89
  // carries the name.
90
- const entry = index.find((item) => item.name === idOrName && (item.status ?? "active") === "active") ?? index.find((item) => item.name === idOrName);
90
+ //
91
+ // Matched case-insensitively to agree with assertNameAvailable, which
92
+ // enforces uniqueness that way: names differing only in case cannot
93
+ // coexist, so a case-sensitive lookup could only fail to find a skill that
94
+ // is definitively there (`get("DEPLOY")` returned null for "deploy").
95
+ const wanted = idOrName.toLowerCase();
96
+ const entry = index.find((item) => item.name.toLowerCase() === wanted &&
97
+ (item.status ?? "active") === "active") ?? index.find((item) => item.name.toLowerCase() === wanted);
91
98
  return entry ? this.store.get(entry.id) : null;
92
99
  }
93
100
  /**
@@ -237,11 +244,23 @@ export class SkillsManager {
237
244
  }
238
245
  async assertNameAvailable(name, excludeId) {
239
246
  const index = await this.getIndex(true);
240
- const clash = index.find((item) => item.id !== excludeId &&
241
- item.name.toLowerCase() === name.toLowerCase() &&
242
- (item.status ?? "active") === "active");
247
+ // Deliberately NOT filtered to active (#1139). Soft-delete only flips
248
+ // status to "deprecated" — the entry stays in the index, so allowing a new
249
+ // skill to take the name left two entries sharing it. Every name-based
250
+ // lookup (CLI `skills show/delete <name>`, the skill_update/skill_delete
251
+ // tools, the `:id`-or-name REST routes) then had to guess which one the
252
+ // caller meant, and iteration order differs across store backends.
253
+ //
254
+ // A deprecated skill's name therefore stays reserved. Reusing it requires
255
+ // hard-deleting the old skill first, which is the explicit choice the
256
+ // ambiguity demands.
257
+ const clash = index.find((item) => item.id !== excludeId && item.name.toLowerCase() === name.toLowerCase());
243
258
  if (clash) {
244
- throw new Error(`A skill named "${name}" already exists`);
259
+ const suffix = (clash.status ?? "active") === "active"
260
+ ? ""
261
+ : ` (that name belongs to a deprecated skill, id ${clash.id}; ` +
262
+ `remove it before reusing the name)`;
263
+ throw new Error(`A skill named "${name}" already exists${suffix}`);
245
264
  }
246
265
  }
247
266
  }
@@ -200,6 +200,12 @@ export type CSVProcessorOptions = {
200
200
  * rather than hanging forever. Defaults: 30s for strings, 5min for files.
201
201
  */
202
202
  parseTimeoutMs?: number;
203
+ /**
204
+ * Skip blank / whitespace-only data rows (#373). Default `true`: blank lines
205
+ * are excluded from the returned content (including raw CSV text) and from
206
+ * `metadata.rowCount`. Set to `false` to preserve empty lines literally.
207
+ */
208
+ skipEmptyLines?: boolean;
203
209
  };
204
210
  /**
205
211
  * PDF API types for different providers
@@ -474,6 +480,10 @@ export type MultimodalPdfEntry = {
474
480
  password?: string;
475
481
  /** Per-page pixel ceiling for the image fallback (#260). */
476
482
  maxCanvasPixels?: number;
483
+ /** Render scale for the image fallback (#297). */
484
+ scale?: number;
485
+ /** Max pages converted by the image fallback (#297). */
486
+ maxPages?: number;
477
487
  };
478
488
  /** Result of PDF to image conversion. */
479
489
  export type PDFImageConversionResult = {
@@ -133,6 +133,17 @@ export type GenerateOptions = {
133
133
  password?: string;
134
134
  /** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
135
135
  maxCanvasPixels?: number;
136
+ /**
137
+ * Render scale for the image fallback used by providers without native PDF
138
+ * support (#297). Higher is sharper but costs roughly the square in memory
139
+ * and tokens. Range 0.1-10; defaults to PDF_LIMITS.DEFAULT_SCALE (1.5).
140
+ */
141
+ scale?: number;
142
+ /**
143
+ * Max pages converted by the image fallback (#297). Pages beyond this are
144
+ * not sent to the model at all. Defaults to PDF_LIMITS.DEFAULT_MAX_PAGES (20).
145
+ */
146
+ maxPages?: number;
136
147
  };
137
148
  videoOptions?: {
138
149
  /** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
@@ -1265,6 +1276,10 @@ export type TextGenerationOptions = {
1265
1276
  password?: string;
1266
1277
  /** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
1267
1278
  maxCanvasPixels?: number;
1279
+ /** Render scale for the image fallback (#297); defaults to PDF_LIMITS.DEFAULT_SCALE. */
1280
+ scale?: number;
1281
+ /** Max pages converted by the image fallback (#297); defaults to PDF_LIMITS.DEFAULT_MAX_PAGES. */
1282
+ maxPages?: number;
1268
1283
  };
1269
1284
  enableSummarization?: boolean;
1270
1285
  /**
@@ -703,6 +703,12 @@ export type ProcessedAudio = ProcessedFileBase & {
703
703
  transcript?: string;
704
704
  hasTranscript: boolean;
705
705
  transcriptionProvider?: string;
706
+ /**
707
+ * Why transcription produced nothing, when it did (#416). Absent on success.
708
+ * Lets a caller distinguish "this audio has no speech" from "the transcription
709
+ * backend was never reachable", which previously looked identical.
710
+ */
711
+ transcriptionSkippedReason?: string;
706
712
  coverArt?: Buffer;
707
713
  };
708
714
  /**
@@ -225,6 +225,17 @@ export type StreamOptions = {
225
225
  password?: string;
226
226
  /** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
227
227
  maxCanvasPixels?: number;
228
+ /**
229
+ * Render scale for the image fallback used by providers without native PDF
230
+ * support (#297). Higher is sharper but costs roughly the square in memory
231
+ * and tokens. Range 0.1-10; defaults to PDF_LIMITS.DEFAULT_SCALE (1.5).
232
+ */
233
+ scale?: number;
234
+ /**
235
+ * Max pages converted by the image fallback (#297). Pages beyond this are
236
+ * not sent to the model at all. Defaults to PDF_LIMITS.DEFAULT_MAX_PAGES (20).
237
+ */
238
+ maxPages?: number;
228
239
  };
229
240
  videoOptions?: {
230
241
  /** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
@@ -30,6 +30,18 @@ export declare function isValidCsvRow(row: unknown): row is CSVRow;
30
30
  * match) when a row violates the invariant.
31
31
  */
32
32
  export declare function assertValidCsvRow(row: unknown, rowNumber: number): asserts row is CSVRow;
33
+ /**
34
+ * True when a parsed CSV row is blank: no keys, or every value is empty /
35
+ * whitespace-only (#373). Shared by the structured post-filter and by
36
+ * `streamParse` so blank rows do not consume `maxRows`.
37
+ */
38
+ export declare function isBlankCsvDataRow(row: CSVRow): boolean;
39
+ /**
40
+ * Raw-line counterpart of {@link isBlankCsvDataRow} (#373): a fully blank /
41
+ * whitespace line, or a delimiter-only line such as `,,` / ` , ` whose
42
+ * fields are all empty after a CSV-aware split.
43
+ */
44
+ export declare function isBlankCsvRawLine(line: string, delimiter: string): boolean;
33
45
  /**
34
46
  * Detect if first line is CSV metadata (not actual data/headers)
35
47
  * Common patterns:
@@ -174,11 +186,17 @@ export declare class CSVProcessor {
174
186
  /**
175
187
  * Format parsed CSV data for LLM consumption
176
188
  * Only used for JSON and Markdown formats (raw format handled separately)
189
+ *
190
+ * @param columnNames - Optional schema headers (#373). When preserved blank
191
+ * rows lead `rows`, Markdown must still use the real column names.
177
192
  */
178
193
  private static formatForLLM;
179
194
  /**
180
195
  * Format as markdown table
181
196
  * Best for small datasets (<100 rows)
197
+ *
198
+ * @param columnNames - Optional explicit headers (#373). Falls back to
199
+ * `Object.keys(rows[0])` when omitted.
182
200
  */
183
201
  private static toMarkdownTable;
184
202
  /**
@@ -187,6 +205,7 @@ export declare class CSVProcessor {
187
205
  * @param sampleRows - Array of sample row objects
188
206
  * @param format - Output format for sample data
189
207
  * @param includeHeaders - Whether to include headers in CSV/markdown formats
208
+ * @param columnNames - Optional schema headers for csv/markdown (#373)
190
209
  * @returns Formatted sample data as string or array
191
210
  */
192
211
  private static formatSampleData;
@@ -195,6 +214,7 @@ export declare class CSVProcessor {
195
214
  *
196
215
  * @param rows - Array of row objects
197
216
  * @param includeHeaders - Whether to include header row
217
+ * @param columnNames - Optional explicit headers (#373)
198
218
  * @returns CSV formatted string
199
219
  */
200
220
  private static toCSVString;