@juspay/neurolink 8.31.2 → 8.32.0

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 CHANGED
@@ -1,3 +1,15 @@
1
+ ## [8.32.0](https://github.com/juspay/neurolink/compare/v8.31.3...v8.32.0) (2026-01-05)
2
+
3
+ ### Features
4
+
5
+ - **(cli):** Add video generation to CLI ([8e7f0cf](https://github.com/juspay/neurolink/commit/8e7f0cfd983f6c09aa3eaf3bdb09d9c22c0d5b02))
6
+
7
+ ## [8.31.3](https://github.com/juspay/neurolink/compare/v8.31.2...v8.31.3) (2026-01-05)
8
+
9
+ ### Bug Fixes
10
+
11
+ - **(csv):** standardize rowCount to exclude empty lines across all formats (CSV-025) ([c898521](https://github.com/juspay/neurolink/commit/c8985212ff6bb43a6cefbf7a5551ed603b32bc55)), closes [#390](https://github.com/juspay/neurolink/issues/390)
12
+
1
13
  ## [8.31.2](https://github.com/juspay/neurolink/compare/v8.31.1...v8.31.2) (2026-01-05)
2
14
 
3
15
  ### Bug Fixes
@@ -17,6 +17,16 @@ export declare class CLICommandFactory {
17
17
  * Saves audio to file when --tts-output flag is provided
18
18
  */
19
19
  private static handleTTSOutput;
20
+ /**
21
+ * Helper method to configure options for video generation mode
22
+ * Auto-configures provider, model, and tools settings for video generation
23
+ */
24
+ private static configureVideoMode;
25
+ /**
26
+ * Helper method to handle video file output
27
+ * Saves generated video to file when --videoOutput flag is provided
28
+ */
29
+ private static handleVideoOutput;
20
30
  private static isValidTokenUsage;
21
31
  private static normalizeTokenUsage;
22
32
  private static formatAnalyticsForTextMode;
@@ -14,12 +14,14 @@ import { ModelsCommandFactory } from "../commands/models.js";
14
14
  import { MCPCommandFactory } from "../commands/mcp.js";
15
15
  import { OllamaCommandFactory } from "./ollamaCommandFactory.js";
16
16
  import { SageMakerCommandFactory } from "./sagemakerCommandFactory.js";
17
+ import { ModelResolver } from "../../lib/models/modelResolver.js";
17
18
  import ora from "ora";
18
19
  import chalk from "chalk";
19
20
  import { logger } from "../../lib/utils/logger.js";
20
21
  import { handleSetup } from "../commands/setup.js";
21
22
  import { checkRedisAvailability } from "../../lib/utils/conversationMemoryUtils.js";
22
23
  import { saveAudioToFile, formatFileSize } from "../utils/audioFileUtils.js";
24
+ import { saveVideoToFile, formatVideoFileSize, getVideoMetadataSummary, } from "../utils/videoFileUtils.js";
23
25
  /**
24
26
  * CLI Command Factory for generate commands
25
27
  */
@@ -266,6 +268,41 @@ export class CLICommandFactory {
266
268
  default: false,
267
269
  description: "Auto-play generated audio",
268
270
  },
271
+ // Video Generation options (Veo 3.1)
272
+ outputMode: {
273
+ type: "string",
274
+ choices: ["text", "video"],
275
+ default: "text",
276
+ description: "Output mode: 'text' for standard generation, 'video' for video generation",
277
+ },
278
+ videoOutput: {
279
+ type: "string",
280
+ alias: "vo",
281
+ description: "Path to save generated video file (e.g., ./output.mp4)",
282
+ },
283
+ videoResolution: {
284
+ type: "string",
285
+ choices: ["720p", "1080p"],
286
+ default: "720p",
287
+ description: "Video output resolution (720p or 1080p)",
288
+ },
289
+ videoLength: {
290
+ type: "number",
291
+ choices: [4, 6, 8],
292
+ default: 4,
293
+ description: "Video duration in seconds (4, 6, or 8)",
294
+ },
295
+ videoAspectRatio: {
296
+ type: "string",
297
+ choices: ["9:16", "16:9"],
298
+ default: "16:9",
299
+ description: "Video aspect ratio (9:16 for portrait, 16:9 for landscape)",
300
+ },
301
+ videoAudio: {
302
+ type: "boolean",
303
+ default: true,
304
+ description: "Enable/disable audio generation in video",
305
+ },
269
306
  thinking: {
270
307
  alias: "think",
271
308
  type: "boolean",
@@ -423,6 +460,13 @@ export class CLICommandFactory {
423
460
  ttsQuality: argv.ttsQuality,
424
461
  ttsOutput: argv.ttsOutput,
425
462
  ttsPlay: argv.ttsPlay,
463
+ // Video generation options (Veo 3.1)
464
+ outputMode: argv.outputMode,
465
+ videoOutput: argv.videoOutput,
466
+ videoResolution: argv.videoResolution,
467
+ videoLength: argv.videoLength,
468
+ videoAspectRatio: argv.videoAspectRatio,
469
+ videoAudio: argv.videoAudio,
426
470
  // Extended thinking options for Claude and Gemini models
427
471
  thinking: argv.thinking,
428
472
  thinkingBudget: argv.thinkingBudget,
@@ -552,6 +596,98 @@ export class CLICommandFactory {
552
596
  handleError(error, "TTS Output");
553
597
  }
554
598
  }
599
+ /**
600
+ * Helper method to configure options for video generation mode
601
+ * Auto-configures provider, model, and tools settings for video generation
602
+ */
603
+ static configureVideoMode(enhancedOptions, argv, options) {
604
+ const userEnabledTools = !argv.disableTools; // Tools are enabled by default
605
+ enhancedOptions.disableTools = true;
606
+ // Auto-set provider to vertex for video generation if not explicitly specified
607
+ if (!enhancedOptions.provider) {
608
+ enhancedOptions.provider = "vertex";
609
+ if (options.debug) {
610
+ logger.debug("Auto-setting provider to 'vertex' for video generation mode");
611
+ }
612
+ }
613
+ else if (enhancedOptions.provider !== "vertex") {
614
+ // Warn if user specified a non-vertex provider
615
+ if (!options.quiet) {
616
+ logger.always(chalk.yellow(`⚠️ Warning: Video generation only supports Vertex AI. Overriding provider '${enhancedOptions.provider}' to 'vertex'.`));
617
+ }
618
+ enhancedOptions.provider = "vertex";
619
+ }
620
+ // Auto-set model to veo-3.1 if not explicitly specified
621
+ if (!enhancedOptions.model) {
622
+ // Resolve the alias to the full model ID for Vertex AI
623
+ const modelAlias = "veo-3.1";
624
+ const resolvedModel = ModelResolver.resolveModel(modelAlias);
625
+ const fullModelId = resolvedModel?.id || "veo-3.1-generate-001";
626
+ enhancedOptions.model = fullModelId;
627
+ if (options.debug) {
628
+ logger.debug(`Auto-setting model to '${fullModelId}' for video generation mode`);
629
+ }
630
+ }
631
+ // Warn user if they explicitly enabled tools
632
+ if (userEnabledTools && !options.quiet) {
633
+ logger.always(chalk.yellow("⚠️ Note: MCP tools are not supported in video generation mode and have been disabled."));
634
+ }
635
+ if (options.debug) {
636
+ logger.debug("Video generation mode enabled (tools auto-disabled):", {
637
+ provider: enhancedOptions.provider,
638
+ model: enhancedOptions.model,
639
+ resolution: enhancedOptions.videoResolution,
640
+ length: enhancedOptions.videoLength,
641
+ aspectRatio: enhancedOptions.videoAspectRatio,
642
+ audio: enhancedOptions.videoAudio,
643
+ outputPath: enhancedOptions.videoOutput,
644
+ });
645
+ }
646
+ }
647
+ /**
648
+ * Helper method to handle video file output
649
+ * Saves generated video to file when --videoOutput flag is provided
650
+ */
651
+ static async handleVideoOutput(result, options) {
652
+ // Check if --videoOutput flag is provided
653
+ const videoOutputPath = options.videoOutput;
654
+ if (!videoOutputPath) {
655
+ return;
656
+ }
657
+ // Extract video from result with proper type checking
658
+ if (!result || typeof result !== "object") {
659
+ return;
660
+ }
661
+ const generateResult = result;
662
+ const video = generateResult.video;
663
+ if (!video) {
664
+ if (!options.quiet) {
665
+ logger.always(chalk.yellow("⚠️ No video available in result. Video generation may not be enabled or the request failed."));
666
+ }
667
+ return;
668
+ }
669
+ try {
670
+ // Save video to file
671
+ const saveResult = await saveVideoToFile(video, videoOutputPath);
672
+ if (saveResult.success) {
673
+ if (!options.quiet) {
674
+ // Format video info output
675
+ const sizeInfo = formatVideoFileSize(saveResult.size);
676
+ const metadataSummary = getVideoMetadataSummary(video);
677
+ logger.always(chalk.green(`🎬 Video saved to: ${saveResult.path} (${sizeInfo})`));
678
+ if (metadataSummary) {
679
+ logger.always(chalk.gray(` ${metadataSummary}`));
680
+ }
681
+ }
682
+ }
683
+ else {
684
+ handleError(new Error(saveResult.error || "Failed to save video file"), "Video Output");
685
+ }
686
+ }
687
+ catch (error) {
688
+ handleError(error, "Video Output");
689
+ }
690
+ }
555
691
  // Helper method to validate token usage data with fallback handling
556
692
  static isValidTokenUsage(tokens) {
557
693
  if (!tokens || typeof tokens !== "object" || tokens === null) {
@@ -672,7 +808,9 @@ export class CLICommandFactory {
672
808
  .example('$0 generate "Analyze data" --enable-analytics', "Enable usage analytics")
673
809
  .example('$0 generate "Futuristic city" --model gemini-2.5-flash-image', "Generate an image")
674
810
  .example('$0 generate "Mountain landscape" --model gemini-2.5-flash-image --imageOutput ./my-images/mountain.png', "Generate image with custom path")
675
- .example('$0 generate "Describe this video" --video path/to/video.mp4', "Analyze video content"));
811
+ .example('$0 generate "Describe this video" --video path/to/video.mp4', "Analyze video content")
812
+ .example('$0 generate "Product showcase video" --image ./product.jpg --outputMode video --videoOutput ./output.mp4', "Generate video from image")
813
+ .example('$0 generate "Smooth camera movement" --image ./input.jpg --provider vertex --model veo-3.1-generate-001 --outputMode video --videoResolution 720p --videoLength 6 --videoAspectRatio 16:9 --videoOutput ./output.mp4', "Video generation with full options"));
676
814
  },
677
815
  handler: async (argv) => await this.executeGenerate(argv),
678
816
  };
@@ -1196,7 +1334,12 @@ export class CLICommandFactory {
1196
1334
  throw new Error('Input required. Use: neurolink generate "your prompt" or echo "prompt" | neurolink generate');
1197
1335
  }
1198
1336
  const options = this.processOptions(argv);
1199
- const spinner = argv.quiet ? null : ora("🤖 Generating text...").start();
1337
+ // Determine if video generation mode is enabled
1338
+ const isVideoMode = options.outputMode === "video";
1339
+ const spinnerMessage = isVideoMode
1340
+ ? "🎬 Generating video... (this may take 1-2 minutes)"
1341
+ : "🤖 Generating text...";
1342
+ const spinner = argv.quiet ? null : ora(spinnerMessage).start();
1200
1343
  try {
1201
1344
  // Add delay if specified
1202
1345
  if (options.delay) {
@@ -1287,6 +1430,10 @@ export class CLICommandFactory {
1287
1430
  toolsEnabled: !options.disableTools,
1288
1431
  });
1289
1432
  }
1433
+ // Video generation doesn't support tools, so auto-disable them
1434
+ if (isVideoMode) {
1435
+ this.configureVideoMode(enhancedOptions, argv, options);
1436
+ }
1290
1437
  // Process CLI multimodal inputs
1291
1438
  const imageBuffers = CLICommandFactory.processCliImages(argv.image);
1292
1439
  const csvFiles = CLICommandFactory.processCliCSVFiles(argv.csv);
@@ -1313,6 +1460,18 @@ export class CLICommandFactory {
1313
1460
  format: argv.videoFormat,
1314
1461
  transcribeAudio: argv.transcribeAudio,
1315
1462
  },
1463
+ // Video generation output configuration
1464
+ output: isVideoMode
1465
+ ? {
1466
+ mode: "video",
1467
+ video: {
1468
+ resolution: enhancedOptions.videoResolution,
1469
+ length: enhancedOptions.videoLength,
1470
+ aspectRatio: enhancedOptions.videoAspectRatio,
1471
+ audio: enhancedOptions.videoAudio,
1472
+ },
1473
+ }
1474
+ : undefined,
1316
1475
  provider: enhancedOptions.provider,
1317
1476
  model: enhancedOptions.model,
1318
1477
  temperature: enhancedOptions.temperature,
@@ -1338,7 +1497,12 @@ export class CLICommandFactory {
1338
1497
  : undefined,
1339
1498
  });
1340
1499
  if (spinner) {
1341
- spinner.succeed(chalk.green("✅ Text generated successfully!"));
1500
+ if (isVideoMode) {
1501
+ spinner.succeed(chalk.green("✅ Video generated successfully!"));
1502
+ }
1503
+ else {
1504
+ spinner.succeed(chalk.green("✅ Text generated successfully!"));
1505
+ }
1342
1506
  }
1343
1507
  // Display provider and model info by default (unless quiet mode)
1344
1508
  if (!options.quiet) {
@@ -1346,10 +1510,14 @@ export class CLICommandFactory {
1346
1510
  const modelInfo = result.model || "default";
1347
1511
  logger.always(chalk.gray(`🔧 Provider: ${providerInfo} | Model: ${modelInfo}`));
1348
1512
  }
1349
- // Handle output with universal formatting
1350
- this.handleOutput(result, options);
1513
+ // Handle output with universal formatting (for text mode)
1514
+ if (!isVideoMode) {
1515
+ this.handleOutput(result, options);
1516
+ }
1351
1517
  // Handle TTS audio file output if --tts-output is provided
1352
1518
  await this.handleTTSOutput(result, options);
1519
+ // Handle video file output if --videoOutput is provided
1520
+ await this.handleVideoOutput(result, options);
1353
1521
  if (options.debug) {
1354
1522
  logger.debug("\n" + chalk.yellow("Debug Information:"));
1355
1523
  logger.debug("Provider:", result.provider);
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Video file utilities for CLI
3
+ *
4
+ * Provides functionality for saving generated video to files with proper
5
+ * error handling and directory creation. Follows the pattern established
6
+ * by audioFileUtils.ts for TTS output handling.
7
+ *
8
+ * @module cli/utils/videoFileUtils
9
+ */
10
+ import type { VideoGenerationResult } from "../../lib/types/multimodal.js";
11
+ /**
12
+ * Result of saving video to file
13
+ */
14
+ export type VideoSaveResult = {
15
+ /** Whether the save was successful */
16
+ success: boolean;
17
+ /** Full path to the saved file */
18
+ path: string;
19
+ /** File size in bytes */
20
+ size: number;
21
+ /** Error message if failed */
22
+ error?: string;
23
+ };
24
+ /**
25
+ * Format file size in human-readable format
26
+ *
27
+ * @param bytes - Size in bytes
28
+ * @returns Formatted string (e.g., "32 KB", "1.5 MB")
29
+ */
30
+ export declare function formatVideoFileSize(bytes: number): string;
31
+ /**
32
+ * Resolve the output path, handling both absolute and relative paths
33
+ *
34
+ * @param outputPath - User-specified output path
35
+ * @returns Resolved absolute path
36
+ */
37
+ export declare function resolveVideoOutputPath(outputPath: string): string;
38
+ /**
39
+ * Ensure parent directories exist, creating them if necessary
40
+ *
41
+ * @param filePath - Full path to the file
42
+ */
43
+ export declare function ensureVideoDirectoryExists(filePath: string): Promise<void>;
44
+ /**
45
+ * Get appropriate file extension for video media type
46
+ *
47
+ * @param mediaType - Video media type
48
+ * @returns File extension (including dot)
49
+ */
50
+ export declare function getVideoExtension(mediaType: "video/mp4" | "video/webm"): string;
51
+ /**
52
+ * Validate and normalize output path, adding extension if needed
53
+ *
54
+ * If the specified path has no extension or an invalid extension, the appropriate
55
+ * extension (.mp4 or .webm) will be added based on the mediaType.
56
+ * If the extension doesn't match the mediaType (e.g., .webm specified but mediaType
57
+ * is video/mp4), the extension will be replaced to match the actual format.
58
+ *
59
+ * @param outputPath - User-specified output path
60
+ * @param mediaType - Video media type for extension
61
+ * @returns Normalized output path with correct extension
62
+ */
63
+ export declare function normalizeVideoOutputPath(outputPath: string, mediaType?: "video/mp4" | "video/webm"): string;
64
+ /**
65
+ * Format video duration in human-readable format
66
+ *
67
+ * @param seconds - Duration in seconds
68
+ * @returns Formatted string (e.g., "4s", "1m 30s")
69
+ */
70
+ export declare function formatVideoDuration(seconds: number): string;
71
+ /**
72
+ * Format video dimensions in human-readable format
73
+ *
74
+ * @param width - Video width in pixels
75
+ * @param height - Video height in pixels
76
+ * @returns Formatted string (e.g., "1920x1080")
77
+ */
78
+ export declare function formatVideoDimensions(width: number, height: number): string;
79
+ /**
80
+ * Save generated video result to a file
81
+ *
82
+ * Creates parent directories if they don't exist and handles both
83
+ * absolute and relative paths.
84
+ *
85
+ * @param video - Video generation result containing video buffer
86
+ * @param outputPath - Path where the video should be saved
87
+ * @returns Save result with success status, path, and size
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * const result = await saveVideoToFile(videoResult, "./output/video.mp4");
92
+ * if (result.success) {
93
+ * console.log(`Saved to ${result.path} (${formatVideoFileSize(result.size)})`);
94
+ * }
95
+ * ```
96
+ */
97
+ export declare function saveVideoToFile(video: VideoGenerationResult, outputPath: string): Promise<VideoSaveResult>;
98
+ /**
99
+ * Type guard to check if an object is a valid VideoGenerationResult
100
+ *
101
+ * @param obj - Object to check
102
+ * @returns True if object is a valid VideoGenerationResult
103
+ */
104
+ export declare function isVideoGenerationResult(obj: unknown): obj is VideoGenerationResult;
105
+ /**
106
+ * Get video metadata summary for display
107
+ *
108
+ * @param video - Video generation result
109
+ * @returns Formatted metadata summary string
110
+ */
111
+ export declare function getVideoMetadataSummary(video: VideoGenerationResult): string;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Video file utilities for CLI
3
+ *
4
+ * Provides functionality for saving generated video to files with proper
5
+ * error handling and directory creation. Follows the pattern established
6
+ * by audioFileUtils.ts for TTS output handling.
7
+ *
8
+ * @module cli/utils/videoFileUtils
9
+ */
10
+ import fs from "fs";
11
+ import path from "path";
12
+ /**
13
+ * Format file size in human-readable format
14
+ *
15
+ * @param bytes - Size in bytes
16
+ * @returns Formatted string (e.g., "32 KB", "1.5 MB")
17
+ */
18
+ export function formatVideoFileSize(bytes) {
19
+ if (bytes < 1024) {
20
+ return `${bytes} B`;
21
+ }
22
+ else if (bytes < 1024 * 1024) {
23
+ return `${(bytes / 1024).toFixed(1)} KB`;
24
+ }
25
+ else if (bytes < 1024 * 1024 * 1024) {
26
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
27
+ }
28
+ else {
29
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
30
+ }
31
+ }
32
+ /**
33
+ * Resolve the output path, handling both absolute and relative paths
34
+ *
35
+ * @param outputPath - User-specified output path
36
+ * @returns Resolved absolute path
37
+ */
38
+ export function resolveVideoOutputPath(outputPath) {
39
+ if (path.isAbsolute(outputPath)) {
40
+ return outputPath;
41
+ }
42
+ return path.resolve(process.cwd(), outputPath);
43
+ }
44
+ /**
45
+ * Ensure parent directories exist, creating them if necessary
46
+ *
47
+ * @param filePath - Full path to the file
48
+ */
49
+ export async function ensureVideoDirectoryExists(filePath) {
50
+ const directory = path.dirname(filePath);
51
+ try {
52
+ await fs.promises.access(directory, fs.constants.F_OK);
53
+ }
54
+ catch {
55
+ // Directory doesn't exist, create it recursively
56
+ await fs.promises.mkdir(directory, { recursive: true });
57
+ }
58
+ }
59
+ /**
60
+ * Get appropriate file extension for video media type
61
+ *
62
+ * @param mediaType - Video media type
63
+ * @returns File extension (including dot)
64
+ */
65
+ export function getVideoExtension(mediaType) {
66
+ switch (mediaType) {
67
+ case "video/mp4":
68
+ return ".mp4";
69
+ case "video/webm":
70
+ return ".webm";
71
+ default:
72
+ // Unreachable but kept for runtime safety
73
+ return ".mp4";
74
+ }
75
+ }
76
+ /**
77
+ * Validate and normalize output path, adding extension if needed
78
+ *
79
+ * If the specified path has no extension or an invalid extension, the appropriate
80
+ * extension (.mp4 or .webm) will be added based on the mediaType.
81
+ * If the extension doesn't match the mediaType (e.g., .webm specified but mediaType
82
+ * is video/mp4), the extension will be replaced to match the actual format.
83
+ *
84
+ * @param outputPath - User-specified output path
85
+ * @param mediaType - Video media type for extension
86
+ * @returns Normalized output path with correct extension
87
+ */
88
+ export function normalizeVideoOutputPath(outputPath, mediaType = "video/mp4") {
89
+ const resolvedPath = resolveVideoOutputPath(outputPath);
90
+ const ext = path.extname(resolvedPath).toLowerCase();
91
+ const expectedExt = getVideoExtension(mediaType);
92
+ const validExtensions = [".mp4", ".webm"];
93
+ if (!ext || !validExtensions.includes(ext)) {
94
+ return resolvedPath + expectedExt;
95
+ }
96
+ // If extension doesn't match mediaType, replace it
97
+ if (ext !== expectedExt) {
98
+ const basePath = resolvedPath.slice(0, -ext.length);
99
+ return basePath + expectedExt;
100
+ }
101
+ return resolvedPath;
102
+ }
103
+ /**
104
+ * Format video duration in human-readable format
105
+ *
106
+ * @param seconds - Duration in seconds
107
+ * @returns Formatted string (e.g., "4s", "1m 30s")
108
+ */
109
+ export function formatVideoDuration(seconds) {
110
+ if (seconds < 60) {
111
+ return `${seconds}s`;
112
+ }
113
+ const minutes = Math.floor(seconds / 60);
114
+ const remainingSeconds = seconds % 60;
115
+ if (remainingSeconds === 0) {
116
+ return `${minutes}m`;
117
+ }
118
+ return `${minutes}m ${remainingSeconds}s`;
119
+ }
120
+ /**
121
+ * Format video dimensions in human-readable format
122
+ *
123
+ * @param width - Video width in pixels
124
+ * @param height - Video height in pixels
125
+ * @returns Formatted string (e.g., "1920x1080")
126
+ */
127
+ export function formatVideoDimensions(width, height) {
128
+ return `${width}x${height}`;
129
+ }
130
+ /**
131
+ * Save generated video result to a file
132
+ *
133
+ * Creates parent directories if they don't exist and handles both
134
+ * absolute and relative paths.
135
+ *
136
+ * @param video - Video generation result containing video buffer
137
+ * @param outputPath - Path where the video should be saved
138
+ * @returns Save result with success status, path, and size
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * const result = await saveVideoToFile(videoResult, "./output/video.mp4");
143
+ * if (result.success) {
144
+ * console.log(`Saved to ${result.path} (${formatVideoFileSize(result.size)})`);
145
+ * }
146
+ * ```
147
+ */
148
+ export async function saveVideoToFile(video, outputPath) {
149
+ let normalizedPath = outputPath;
150
+ try {
151
+ // Normalize the output path
152
+ normalizedPath = normalizeVideoOutputPath(outputPath, video.mediaType);
153
+ // Ensure parent directories exist
154
+ await ensureVideoDirectoryExists(normalizedPath);
155
+ // Write the video buffer to file
156
+ await fs.promises.writeFile(normalizedPath, video.data);
157
+ // Get the actual file size
158
+ const stats = await fs.promises.stat(normalizedPath);
159
+ return {
160
+ success: true,
161
+ path: normalizedPath,
162
+ size: stats.size,
163
+ };
164
+ }
165
+ catch (error) {
166
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
167
+ return {
168
+ success: false,
169
+ path: normalizedPath,
170
+ size: 0,
171
+ error: errorMessage,
172
+ };
173
+ }
174
+ }
175
+ /**
176
+ * Type guard to check if an object is a valid VideoGenerationResult
177
+ *
178
+ * @param obj - Object to check
179
+ * @returns True if object is a valid VideoGenerationResult
180
+ */
181
+ export function isVideoGenerationResult(obj) {
182
+ if (!obj || typeof obj !== "object") {
183
+ return false;
184
+ }
185
+ const video = obj;
186
+ // Check required fields
187
+ if (!(video.data instanceof Buffer)) {
188
+ return false;
189
+ }
190
+ // Check mediaType is valid
191
+ if (video.mediaType !== "video/mp4" && video.mediaType !== "video/webm") {
192
+ return false;
193
+ }
194
+ return true;
195
+ }
196
+ /**
197
+ * Get video metadata summary for display
198
+ *
199
+ * @param video - Video generation result
200
+ * @returns Formatted metadata summary string
201
+ */
202
+ export function getVideoMetadataSummary(video) {
203
+ const parts = [];
204
+ if (video.metadata?.duration) {
205
+ parts.push(`Duration: ${formatVideoDuration(video.metadata.duration)}`);
206
+ }
207
+ if (video.metadata?.dimensions) {
208
+ parts.push(`Resolution: ${formatVideoDimensions(video.metadata.dimensions.width, video.metadata.dimensions.height)}`);
209
+ }
210
+ if (video.metadata?.aspectRatio) {
211
+ parts.push(`Aspect: ${video.metadata.aspectRatio}`);
212
+ }
213
+ if (video.metadata?.audioEnabled !== undefined) {
214
+ parts.push(`Audio: ${video.metadata.audioEnabled ? "Yes" : "No"}`);
215
+ }
216
+ if (video.metadata?.processingTime) {
217
+ const seconds = (video.metadata.processingTime / 1000).toFixed(1);
218
+ parts.push(`Processing: ${seconds}s`);
219
+ }
220
+ return parts.join(" | ");
221
+ }
222
+ //# sourceMappingURL=videoFileUtils.js.map
@@ -68,6 +68,18 @@ export type GenerateCommandArgs = BaseCommandArgs & {
68
68
  thinkingLevel?: "minimal" | "low" | "medium" | "high";
69
69
  /** Vertex AI region */
70
70
  region?: string;
71
+ /** Output mode - 'text' for standard generation, 'video' for video generation */
72
+ outputMode?: "text" | "video";
73
+ /** Path to save generated video file */
74
+ videoOutput?: string;
75
+ /** Video output resolution (720p or 1080p) */
76
+ videoResolution?: "720p" | "1080p";
77
+ /** Video duration in seconds (4, 6, or 8) */
78
+ videoLength?: 4 | 6 | 8;
79
+ /** Video aspect ratio (9:16 for portrait, 16:9 for landscape) */
80
+ videoAspectRatio?: "9:16" | "16:9";
81
+ /** Enable/disable audio generation in video */
82
+ videoAudio?: boolean;
71
83
  /** Custom path for generated image output */
72
84
  imageOutput?: string;
73
85
  };
@@ -357,6 +369,8 @@ export type GenerateResult = CommandResult & {
357
369
  }>;
358
370
  /** TTS audio result when TTS is enabled */
359
371
  audio?: import("./index.js").TTSResult;
372
+ /** Video generation result when video mode is enabled */
373
+ video?: import("./multimodal.js").VideoGenerationResult;
360
374
  imageOutput?: {
361
375
  base64: string;
362
376
  savedPath?: string;
@@ -44,6 +44,7 @@ export type FileProcessingResult = {
44
44
  filename?: string;
45
45
  extension?: string | null;
46
46
  rowCount?: number;
47
+ totalLines?: number;
47
48
  columnCount?: number;
48
49
  columnNames?: string[];
49
50
  sampleData?: string | unknown[];
@@ -85,8 +85,12 @@ export class CSVProcessor {
85
85
  : lines;
86
86
  const limitedLines = csvLines.slice(0, 1 + maxRows); // header + data rows
87
87
  const limitedCSV = limitedLines.join("\n");
88
- const rowCount = limitedLines.length - 1; // Subtract header
89
- const originalRowCount = csvLines.length - 1; // Subtract header from original
88
+ const rowCount = limitedLines
89
+ .slice(1)
90
+ .filter((line) => line.trim() !== "").length;
91
+ const originalRowCount = csvLines
92
+ .slice(1)
93
+ .filter((line) => line.trim() !== "").length;
90
94
  const wasTruncated = rowCount < originalRowCount;
91
95
  if (wasTruncated) {
92
96
  logger.warn(`[CSVProcessor] CSV data truncated: showing ${rowCount} of ${originalRowCount} rows (limit: ${maxRows})`);
@@ -110,6 +114,7 @@ export class CSVProcessor {
110
114
  confidence: 100,
111
115
  size: content.length,
112
116
  rowCount,
117
+ totalLines: limitedLines.length,
113
118
  columnCount: (limitedLines[0] || "").split(",").length,
114
119
  extension,
115
120
  },
@@ -121,12 +126,26 @@ export class CSVProcessor {
121
126
  maxRows,
122
127
  });
123
128
  const rows = await this.parseCSVString(csvString, maxRows);
129
+ // Filter out empty rows (empty objects or rows with only whitespace values from blank lines)
130
+ const nonEmptyRows = rows.filter((row) => {
131
+ if (!row || typeof row !== "object") {
132
+ return false;
133
+ }
134
+ const keys = Object.keys(row);
135
+ if (keys.length === 0) {
136
+ return false;
137
+ }
138
+ // Check if all values are empty or whitespace-only
139
+ return !Object.values(row).every((val) => val === "" || (typeof val === "string" && val.trim() === ""));
140
+ });
124
141
  // Extract metadata from parsed results
125
- const rowCount = rows.length;
126
- const columnNames = rows.length > 0 ? Object.keys(rows[0]) : [];
142
+ const rowCount = nonEmptyRows.length;
143
+ const columnNames = nonEmptyRows.length > 0
144
+ ? Object.keys(nonEmptyRows[0])
145
+ : [];
127
146
  const columnCount = columnNames.length;
128
147
  const hasEmptyColumns = columnNames.some((col) => !col || col.trim() === "");
129
- const sampleRows = rows.slice(0, 3);
148
+ const sampleRows = nonEmptyRows.slice(0, 3);
130
149
  const sampleData = this.formatSampleData(sampleRows, sampleDataFormat, includeHeaders);
131
150
  if (hasEmptyColumns) {
132
151
  logger.warn("[CSVProcessor] CSV contains empty or blank column headers", {
@@ -138,7 +157,7 @@ export class CSVProcessor {
138
157
  }
139
158
  // Format parsed data
140
159
  logger.debug(`[CSVProcessor] Converting ${rowCount} rows to ${formatStyle} format`);
141
- const formatted = this.formatForLLM(rows, formatStyle, includeHeaders);
160
+ const formatted = this.formatForLLM(nonEmptyRows, formatStyle, includeHeaders);
142
161
  logger.info("[CSVProcessor] ✅ Processed CSV file", {
143
162
  formatStyle,
144
163
  rowCount,
@@ -16,8 +16,8 @@ export type PDFImageConversionOptions = {
16
16
  scale?: number;
17
17
  /** Maximum number of pages to convert (default: 20 from PDF_LIMITS.DEFAULT_MAX_PAGES) */
18
18
  maxPages?: number;
19
- /** Output format hint (currently only PNG supported by pdf-to-img) */
20
- format?: "png";
19
+ /** Output format (png or jpeg, default: png). Note: pdf-to-img outputs PNG, JPEG conversion would require additional processing */
20
+ format?: "png" | "jpeg";
21
21
  };
22
22
  /**
23
23
  * Result of PDF to image conversion
@@ -249,12 +249,17 @@ export class PDFProcessor {
249
249
  */
250
250
  static async convertToImages(pdfBuffer, options) {
251
251
  const startTime = Date.now();
252
- const { scale = 2, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES } = options || {};
252
+ const { scale = 2, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", } = options || {};
253
253
  const images = [];
254
254
  const warnings = [];
255
255
  // ============================================================================
256
256
  // INPUT VALIDATION (Security: Prevent malformed/malicious PDF processing)
257
257
  // ============================================================================
258
+ // 0. Validate format is supported and case-sensitive
259
+ const SUPPORTED_FORMATS = ["png", "jpeg"];
260
+ if (!SUPPORTED_FORMATS.includes(format)) {
261
+ throw new Error(`Invalid format: "${format}". Supported formats: "png", "jpeg".`);
262
+ }
258
263
  // 1. Validate buffer is not empty or too small
259
264
  if (!pdfBuffer || pdfBuffer.length < 5) {
260
265
  throw new Error("Invalid PDF: Buffer is too small or empty. " +
@@ -68,6 +68,18 @@ export type GenerateCommandArgs = BaseCommandArgs & {
68
68
  thinkingLevel?: "minimal" | "low" | "medium" | "high";
69
69
  /** Vertex AI region */
70
70
  region?: string;
71
+ /** Output mode - 'text' for standard generation, 'video' for video generation */
72
+ outputMode?: "text" | "video";
73
+ /** Path to save generated video file */
74
+ videoOutput?: string;
75
+ /** Video output resolution (720p or 1080p) */
76
+ videoResolution?: "720p" | "1080p";
77
+ /** Video duration in seconds (4, 6, or 8) */
78
+ videoLength?: 4 | 6 | 8;
79
+ /** Video aspect ratio (9:16 for portrait, 16:9 for landscape) */
80
+ videoAspectRatio?: "9:16" | "16:9";
81
+ /** Enable/disable audio generation in video */
82
+ videoAudio?: boolean;
71
83
  /** Custom path for generated image output */
72
84
  imageOutput?: string;
73
85
  };
@@ -357,6 +369,8 @@ export type GenerateResult = CommandResult & {
357
369
  }>;
358
370
  /** TTS audio result when TTS is enabled */
359
371
  audio?: import("./index.js").TTSResult;
372
+ /** Video generation result when video mode is enabled */
373
+ video?: import("./multimodal.js").VideoGenerationResult;
360
374
  imageOutput?: {
361
375
  base64: string;
362
376
  savedPath?: string;
@@ -44,6 +44,7 @@ export type FileProcessingResult = {
44
44
  filename?: string;
45
45
  extension?: string | null;
46
46
  rowCount?: number;
47
+ totalLines?: number;
47
48
  columnCount?: number;
48
49
  columnNames?: string[];
49
50
  sampleData?: string | unknown[];
@@ -85,8 +85,12 @@ export class CSVProcessor {
85
85
  : lines;
86
86
  const limitedLines = csvLines.slice(0, 1 + maxRows); // header + data rows
87
87
  const limitedCSV = limitedLines.join("\n");
88
- const rowCount = limitedLines.length - 1; // Subtract header
89
- const originalRowCount = csvLines.length - 1; // Subtract header from original
88
+ const rowCount = limitedLines
89
+ .slice(1)
90
+ .filter((line) => line.trim() !== "").length;
91
+ const originalRowCount = csvLines
92
+ .slice(1)
93
+ .filter((line) => line.trim() !== "").length;
90
94
  const wasTruncated = rowCount < originalRowCount;
91
95
  if (wasTruncated) {
92
96
  logger.warn(`[CSVProcessor] CSV data truncated: showing ${rowCount} of ${originalRowCount} rows (limit: ${maxRows})`);
@@ -110,6 +114,7 @@ export class CSVProcessor {
110
114
  confidence: 100,
111
115
  size: content.length,
112
116
  rowCount,
117
+ totalLines: limitedLines.length,
113
118
  columnCount: (limitedLines[0] || "").split(",").length,
114
119
  extension,
115
120
  },
@@ -121,12 +126,26 @@ export class CSVProcessor {
121
126
  maxRows,
122
127
  });
123
128
  const rows = await this.parseCSVString(csvString, maxRows);
129
+ // Filter out empty rows (empty objects or rows with only whitespace values from blank lines)
130
+ const nonEmptyRows = rows.filter((row) => {
131
+ if (!row || typeof row !== "object") {
132
+ return false;
133
+ }
134
+ const keys = Object.keys(row);
135
+ if (keys.length === 0) {
136
+ return false;
137
+ }
138
+ // Check if all values are empty or whitespace-only
139
+ return !Object.values(row).every((val) => val === "" || (typeof val === "string" && val.trim() === ""));
140
+ });
124
141
  // Extract metadata from parsed results
125
- const rowCount = rows.length;
126
- const columnNames = rows.length > 0 ? Object.keys(rows[0]) : [];
142
+ const rowCount = nonEmptyRows.length;
143
+ const columnNames = nonEmptyRows.length > 0
144
+ ? Object.keys(nonEmptyRows[0])
145
+ : [];
127
146
  const columnCount = columnNames.length;
128
147
  const hasEmptyColumns = columnNames.some((col) => !col || col.trim() === "");
129
- const sampleRows = rows.slice(0, 3);
148
+ const sampleRows = nonEmptyRows.slice(0, 3);
130
149
  const sampleData = this.formatSampleData(sampleRows, sampleDataFormat, includeHeaders);
131
150
  if (hasEmptyColumns) {
132
151
  logger.warn("[CSVProcessor] CSV contains empty or blank column headers", {
@@ -138,7 +157,7 @@ export class CSVProcessor {
138
157
  }
139
158
  // Format parsed data
140
159
  logger.debug(`[CSVProcessor] Converting ${rowCount} rows to ${formatStyle} format`);
141
- const formatted = this.formatForLLM(rows, formatStyle, includeHeaders);
160
+ const formatted = this.formatForLLM(nonEmptyRows, formatStyle, includeHeaders);
142
161
  logger.info("[CSVProcessor] ✅ Processed CSV file", {
143
162
  formatStyle,
144
163
  rowCount,
@@ -16,8 +16,8 @@ export type PDFImageConversionOptions = {
16
16
  scale?: number;
17
17
  /** Maximum number of pages to convert (default: 20 from PDF_LIMITS.DEFAULT_MAX_PAGES) */
18
18
  maxPages?: number;
19
- /** Output format hint (currently only PNG supported by pdf-to-img) */
20
- format?: "png";
19
+ /** Output format (png or jpeg, default: png). Note: pdf-to-img outputs PNG, JPEG conversion would require additional processing */
20
+ format?: "png" | "jpeg";
21
21
  };
22
22
  /**
23
23
  * Result of PDF to image conversion
@@ -249,12 +249,17 @@ export class PDFProcessor {
249
249
  */
250
250
  static async convertToImages(pdfBuffer, options) {
251
251
  const startTime = Date.now();
252
- const { scale = 2, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES } = options || {};
252
+ const { scale = 2, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", } = options || {};
253
253
  const images = [];
254
254
  const warnings = [];
255
255
  // ============================================================================
256
256
  // INPUT VALIDATION (Security: Prevent malformed/malicious PDF processing)
257
257
  // ============================================================================
258
+ // 0. Validate format is supported and case-sensitive
259
+ const SUPPORTED_FORMATS = ["png", "jpeg"];
260
+ if (!SUPPORTED_FORMATS.includes(format)) {
261
+ throw new Error(`Invalid format: "${format}". Supported formats: "png", "jpeg".`);
262
+ }
258
263
  // 1. Validate buffer is not empty or too small
259
264
  if (!pdfBuffer || pdfBuffer.length < 5) {
260
265
  throw new Error("Invalid PDF: Buffer is too small or empty. " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "8.31.2",
3
+ "version": "8.32.0",
4
4
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
5
5
  "author": {
6
6
  "name": "Juspay Technologies",