@vibeframe/cli 0.27.0 → 0.29.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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/adapters/index.d.ts +1 -0
  3. package/dist/agent/adapters/index.d.ts.map +1 -1
  4. package/dist/agent/adapters/index.js +5 -0
  5. package/dist/agent/adapters/index.js.map +1 -1
  6. package/dist/agent/adapters/openrouter.d.ts +16 -0
  7. package/dist/agent/adapters/openrouter.d.ts.map +1 -0
  8. package/dist/agent/adapters/openrouter.js +100 -0
  9. package/dist/agent/adapters/openrouter.js.map +1 -0
  10. package/dist/agent/types.d.ts +1 -1
  11. package/dist/agent/types.d.ts.map +1 -1
  12. package/dist/commands/agent.d.ts.map +1 -1
  13. package/dist/commands/agent.js +3 -1
  14. package/dist/commands/agent.js.map +1 -1
  15. package/dist/commands/setup.js +5 -2
  16. package/dist/commands/setup.js.map +1 -1
  17. package/dist/config/schema.d.ts +2 -1
  18. package/dist/config/schema.d.ts.map +1 -1
  19. package/dist/config/schema.js +2 -0
  20. package/dist/config/schema.js.map +1 -1
  21. package/dist/index.js +0 -0
  22. package/package.json +16 -12
  23. package/.turbo/turbo-build.log +0 -4
  24. package/.turbo/turbo-lint.log +0 -21
  25. package/.turbo/turbo-test.log +0 -689
  26. package/src/agent/adapters/claude.ts +0 -143
  27. package/src/agent/adapters/gemini.ts +0 -159
  28. package/src/agent/adapters/index.ts +0 -61
  29. package/src/agent/adapters/ollama.ts +0 -231
  30. package/src/agent/adapters/openai.ts +0 -116
  31. package/src/agent/adapters/xai.ts +0 -119
  32. package/src/agent/index.ts +0 -251
  33. package/src/agent/memory/index.ts +0 -151
  34. package/src/agent/prompts/system.ts +0 -106
  35. package/src/agent/tools/ai-editing.ts +0 -845
  36. package/src/agent/tools/ai-generation.ts +0 -1073
  37. package/src/agent/tools/ai-pipeline.ts +0 -1055
  38. package/src/agent/tools/ai.ts +0 -21
  39. package/src/agent/tools/batch.ts +0 -429
  40. package/src/agent/tools/e2e.test.ts +0 -545
  41. package/src/agent/tools/export.ts +0 -184
  42. package/src/agent/tools/filesystem.ts +0 -237
  43. package/src/agent/tools/index.ts +0 -150
  44. package/src/agent/tools/integration.test.ts +0 -775
  45. package/src/agent/tools/media.ts +0 -697
  46. package/src/agent/tools/project.ts +0 -313
  47. package/src/agent/tools/timeline.ts +0 -951
  48. package/src/agent/types.ts +0 -68
  49. package/src/commands/agent.ts +0 -340
  50. package/src/commands/ai-analyze.ts +0 -429
  51. package/src/commands/ai-animated-caption.ts +0 -390
  52. package/src/commands/ai-audio.ts +0 -941
  53. package/src/commands/ai-broll.ts +0 -490
  54. package/src/commands/ai-edit-cli.ts +0 -658
  55. package/src/commands/ai-edit.ts +0 -1542
  56. package/src/commands/ai-fill-gaps.ts +0 -566
  57. package/src/commands/ai-helpers.ts +0 -65
  58. package/src/commands/ai-highlights.ts +0 -1303
  59. package/src/commands/ai-image.ts +0 -761
  60. package/src/commands/ai-motion.ts +0 -347
  61. package/src/commands/ai-narrate.ts +0 -451
  62. package/src/commands/ai-review.ts +0 -309
  63. package/src/commands/ai-script-pipeline-cli.ts +0 -1710
  64. package/src/commands/ai-script-pipeline.ts +0 -1365
  65. package/src/commands/ai-suggest-edit.ts +0 -264
  66. package/src/commands/ai-video-fx.ts +0 -445
  67. package/src/commands/ai-video.ts +0 -915
  68. package/src/commands/ai-viral.ts +0 -595
  69. package/src/commands/ai-visual-fx.ts +0 -601
  70. package/src/commands/ai.test.ts +0 -627
  71. package/src/commands/ai.ts +0 -307
  72. package/src/commands/analyze.ts +0 -282
  73. package/src/commands/audio.ts +0 -644
  74. package/src/commands/batch.test.ts +0 -279
  75. package/src/commands/batch.ts +0 -440
  76. package/src/commands/detect.ts +0 -329
  77. package/src/commands/doctor.ts +0 -237
  78. package/src/commands/edit-cmd.ts +0 -1014
  79. package/src/commands/export.ts +0 -918
  80. package/src/commands/generate.ts +0 -2146
  81. package/src/commands/media.ts +0 -177
  82. package/src/commands/output.ts +0 -142
  83. package/src/commands/pipeline.ts +0 -398
  84. package/src/commands/project.test.ts +0 -127
  85. package/src/commands/project.ts +0 -149
  86. package/src/commands/sanitize.ts +0 -60
  87. package/src/commands/schema.ts +0 -130
  88. package/src/commands/setup.ts +0 -509
  89. package/src/commands/timeline.test.ts +0 -499
  90. package/src/commands/timeline.ts +0 -529
  91. package/src/commands/validate.ts +0 -77
  92. package/src/config/config.test.ts +0 -197
  93. package/src/config/index.ts +0 -125
  94. package/src/config/schema.ts +0 -82
  95. package/src/engine/index.ts +0 -2
  96. package/src/engine/project.test.ts +0 -702
  97. package/src/engine/project.ts +0 -439
  98. package/src/index.ts +0 -146
  99. package/src/utils/api-key.test.ts +0 -41
  100. package/src/utils/api-key.ts +0 -247
  101. package/src/utils/audio.ts +0 -83
  102. package/src/utils/exec-safe.ts +0 -75
  103. package/src/utils/first-run.ts +0 -52
  104. package/src/utils/provider-resolver.ts +0 -56
  105. package/src/utils/remotion.ts +0 -951
  106. package/src/utils/subtitle.test.ts +0 -227
  107. package/src/utils/subtitle.ts +0 -169
  108. package/src/utils/tty.ts +0 -196
  109. package/tsconfig.json +0 -20
@@ -1,307 +0,0 @@
1
- /**
2
- * @module ai
3
- * @description AI command orchestrator - registers all AI subcommands.
4
- *
5
- * This file is a thin wiring layer. Each command group lives in its own module:
6
- * - ai-audio.ts — TTS, SFX, music generation
7
- * - ai-image.ts — Image generation (OpenAI, Gemini)
8
- * - ai-edit.ts — Post-production editing (silence-cut, caption, etc.)
9
- * - ai-video.ts — Video generation (Runway, Kling, Grok)
10
- * - ai-analyze.ts — Unified media analysis
11
- * - ai-review.ts — AI video review & auto-fix
12
- * - ai-highlights.ts — Highlight extraction + auto-shorts
13
- * - ai-script-pipeline.ts — Script-to-video pipeline
14
- * - ai-motion.ts — Remotion motion graphics
15
- * - ai-suggest-edit.ts — Suggest, edit, storyboard commands
16
- * - ai-fill-gaps.ts — Fill timeline gaps with AI video
17
- * - ai-video-fx.ts — Video upscale, interpolate, inpaint, track
18
- * - ai-broll.ts — B-roll matching
19
- * - ai-viral.ts — Viral optimizer
20
- * - ai-visual-fx.ts — Grade, speed-ramp, reframe, style-transfer
21
- * - ai-narrate.ts — Auto-narration + providers list
22
- *
23
- * @see MODELS.md for AI model configuration
24
- */
25
-
26
- import { Command } from "commander";
27
- import { Project } from "../engine/index.js";
28
- import type { EffectType } from "@vibeframe/core/timeline";
29
- import type { TimelineCommand } from "@vibeframe/ai-providers";
30
-
31
- // Module registrations
32
- import { registerAudioCommands } from "./ai-audio.js";
33
- import { registerImageCommands } from "./ai-image.js";
34
- import { registerEditCommands } from "./ai-edit-cli.js";
35
- import { registerVideoCommands } from "./ai-video.js";
36
- import { registerAnalyzeCommands } from "./ai-analyze.js";
37
- import { registerReviewCommand } from "./ai-review.js";
38
- import { registerHighlightsCommands } from "./ai-highlights.js";
39
- import { registerScriptPipelineCommands } from "./ai-script-pipeline-cli.js";
40
- import { registerMotionCommand } from "./ai-motion.js";
41
- import { registerSuggestEditCommands } from "./ai-suggest-edit.js";
42
- import { registerFillGapsCommand } from "./ai-fill-gaps.js";
43
- import { registerVideoFxCommands } from "./ai-video-fx.js";
44
- import { registerBrollCommand } from "./ai-broll.js";
45
- import { registerViralCommand } from "./ai-viral.js";
46
- import { registerVisualFxCommands } from "./ai-visual-fx.js";
47
- import { registerNarrateCommands } from "./ai-narrate.js";
48
-
49
- // ============================================================================
50
- // Re-exports for backward compatibility (agent tools import from this file)
51
- // ============================================================================
52
-
53
- export {
54
- executeMotion,
55
- type MotionCommandOptions,
56
- type MotionCommandResult,
57
- } from "./ai-motion.js";
58
-
59
- export {
60
- executeSilenceCut, executeJumpCut, executeCaption, executeNoiseReduce,
61
- executeFade, executeTranslateSrt, applyTextOverlays, executeTextOverlay,
62
- type TextOverlayStyle, type TextOverlayOptions, type TextOverlayResult,
63
- type CaptionStyle, type CaptionOptions, type CaptionResult,
64
- type SilencePeriod, type SilenceCutOptions, type SilenceCutResult,
65
- type FillerWord, type JumpCutOptions, type JumpCutResult,
66
- type NoiseReduceOptions, type NoiseReduceResult,
67
- type FadeOptions, type FadeResult,
68
- type TranslateSrtOptions, type TranslateSrtResult,
69
- DEFAULT_FILLER_WORDS, detectFillerRanges,
70
- } from "./ai-edit.js";
71
-
72
- export {
73
- executeThumbnailBestFrame,
74
- type ThumbnailBestFrameOptions,
75
- type ThumbnailBestFrameResult,
76
- } from "./ai-image.js";
77
-
78
- export {
79
- executeReview,
80
- type ReviewOptions,
81
- type ReviewResult,
82
- } from "./ai-review.js";
83
-
84
- export {
85
- executeHighlights,
86
- executeAutoShorts,
87
- type HighlightsOptions,
88
- type HighlightsExtractResult,
89
- type AutoShortsOptions,
90
- type AutoShortsResult,
91
- } from "./ai-highlights.js";
92
-
93
- export {
94
- executeGeminiVideo,
95
- executeAnalyze,
96
- type GeminiVideoOptions,
97
- type GeminiVideoResult,
98
- type AnalyzeOptions,
99
- type AnalyzeResult,
100
- } from "./ai-analyze.js";
101
-
102
- export {
103
- executeScriptToVideo,
104
- executeRegenerateScene,
105
- type ScriptToVideoOptions,
106
- type ScriptToVideoResult,
107
- type NarrationEntry,
108
- type RegenerateSceneOptions,
109
- type RegenerateSceneResult,
110
- } from "./ai-script-pipeline.js";
111
-
112
- export {
113
- autoNarrate,
114
- type AutoNarrateOptions,
115
- type AutoNarrateResult,
116
- } from "./ai-narrate.js";
117
-
118
- // ============================================================================
119
- // AI Command — register all subcommands
120
- // ============================================================================
121
-
122
- export const aiCommand = new Command("ai")
123
- .description("AI provider commands");
124
-
125
- // Previously extracted modules
126
- registerAudioCommands(aiCommand);
127
- registerImageCommands(aiCommand);
128
- registerEditCommands(aiCommand);
129
- registerVideoCommands(aiCommand);
130
- registerAnalyzeCommands(aiCommand);
131
- registerReviewCommand(aiCommand);
132
- registerHighlightsCommands(aiCommand);
133
- registerScriptPipelineCommands(aiCommand);
134
- registerMotionCommand(aiCommand);
135
-
136
- // Newly extracted modules
137
- registerSuggestEditCommands(aiCommand);
138
- registerFillGapsCommand(aiCommand);
139
- registerVideoFxCommands(aiCommand);
140
- registerBrollCommand(aiCommand);
141
- registerViralCommand(aiCommand);
142
- registerVisualFxCommands(aiCommand);
143
- registerNarrateCommands(aiCommand);
144
-
145
- // ============================================================================
146
- // executeCommand — applies parsed timeline commands to a project
147
- // ============================================================================
148
-
149
- export function executeCommand(project: Project, cmd: TimelineCommand): boolean {
150
- const { action, clipIds, params } = cmd;
151
-
152
- try {
153
- switch (action) {
154
- case "trim":
155
- for (const clipId of clipIds) {
156
- if (params.newDuration) {
157
- project.trimClipEnd(clipId, params.newDuration as number);
158
- }
159
- if (params.startTrim) {
160
- project.trimClipStart(clipId, params.startTrim as number);
161
- }
162
- }
163
- return true;
164
-
165
- case "remove-clip":
166
- for (const clipId of clipIds) {
167
- project.removeClip(clipId);
168
- }
169
- return true;
170
-
171
- case "split":
172
- if (clipIds.length > 0 && params.splitTime) {
173
- project.splitClip(clipIds[0], params.splitTime as number);
174
- }
175
- return true;
176
-
177
- case "duplicate":
178
- for (const clipId of clipIds) {
179
- project.duplicateClip(clipId, params.newStartTime as number | undefined);
180
- }
181
- return true;
182
-
183
- case "move":
184
- for (const clipId of clipIds) {
185
- const clip = project.getClips().find((c) => c.id === clipId);
186
- if (clip) {
187
- const newTrackId = (params.newTrackId as string) || clip.trackId;
188
- const newStartTime = (params.newStartTime as number) ?? clip.startTime;
189
- project.moveClip(clipId, newTrackId, newStartTime);
190
- }
191
- }
192
- return true;
193
-
194
- case "add-effect":
195
- for (const clipId of clipIds) {
196
- const effectType = ((params.effectType as string) || "fadeIn") as EffectType;
197
- project.addEffect(clipId, {
198
- type: effectType,
199
- startTime: (params.startTime as number) || 0,
200
- duration: (params.duration as number) || 1,
201
- params: {},
202
- });
203
- }
204
- return true;
205
-
206
- case "remove-effect":
207
- console.warn("remove-effect is not yet supported. Use the timeline UI to remove effects.");
208
- return false;
209
-
210
- case "set-volume":
211
- console.warn("set-volume is not yet supported. Audio ducking via 'vibe ai duck' can adjust levels.");
212
- return false;
213
-
214
- case "add-track": {
215
- const trackType = (params.trackType as "video" | "audio") || "video";
216
- const tracks = project.getTracks();
217
- project.addTrack({
218
- type: trackType,
219
- name: `${trackType}-track-${tracks.length + 1}`,
220
- order: tracks.length,
221
- isMuted: false,
222
- isLocked: false,
223
- isVisible: true,
224
- });
225
- return true;
226
- }
227
-
228
- case "speed-change":
229
- for (const clipId of clipIds) {
230
- const clip = project.getClips().find((c) => c.id === clipId);
231
- if (clip) {
232
- const speed = (params.speed as number) || 1.0;
233
- project.addEffect(clipId, {
234
- type: "speed" as EffectType,
235
- startTime: 0,
236
- duration: clip.duration,
237
- params: { speed },
238
- });
239
- }
240
- }
241
- return true;
242
-
243
- case "reverse":
244
- for (const clipId of clipIds) {
245
- const clip = project.getClips().find((c) => c.id === clipId);
246
- if (clip) {
247
- project.addEffect(clipId, {
248
- type: "reverse" as EffectType,
249
- startTime: 0,
250
- duration: clip.duration,
251
- params: {},
252
- });
253
- }
254
- }
255
- return true;
256
-
257
- case "crop":
258
- for (const clipId of clipIds) {
259
- const clip = project.getClips().find((c) => c.id === clipId);
260
- if (clip) {
261
- project.addEffect(clipId, {
262
- type: "crop" as EffectType,
263
- startTime: 0,
264
- duration: clip.duration,
265
- params: {
266
- aspectRatio: params.aspectRatio as string,
267
- x: params.x as number,
268
- y: params.y as number,
269
- width: params.width as number,
270
- height: params.height as number,
271
- },
272
- });
273
- }
274
- }
275
- return true;
276
-
277
- case "position":
278
- for (const clipId of clipIds) {
279
- const clip = project.getClips().find((c) => c.id === clipId);
280
- if (clip) {
281
- const position = params.position as string;
282
- const allClips = project.getClips().filter((c) => c.trackId === clip.trackId);
283
- let newStartTime = 0;
284
-
285
- if (position === "end") {
286
- const maxEnd = Math.max(...allClips.filter((c) => c.id !== clipId).map((c) => c.startTime + c.duration));
287
- newStartTime = maxEnd;
288
- } else if (position === "middle") {
289
- const totalDuration = allClips.reduce((sum, c) => sum + c.duration, 0);
290
- newStartTime = (totalDuration - clip.duration) / 2;
291
- }
292
- // "beginning" stays at 0
293
-
294
- project.moveClip(clipId, clip.trackId, newStartTime);
295
- }
296
- }
297
- return true;
298
-
299
- default:
300
- console.warn(`Unknown action: ${action}`);
301
- return false;
302
- }
303
- } catch (error) {
304
- console.error(`Error executing ${action}:`, error);
305
- return false;
306
- }
307
- }
@@ -1,282 +0,0 @@
1
- /**
2
- * @module analyze
3
- *
4
- * Top-level `vibe analyze` command group for media analysis.
5
- *
6
- * Commands:
7
- * analyze media - Unified analysis for images, videos, and YouTube URLs (Gemini)
8
- * analyze video - Analyze video files or YouTube URLs with Gemini
9
- * analyze review - AI video quality review and auto-fix (Gemini)
10
- * analyze suggest - Get AI edit suggestions using Gemini
11
- *
12
- * @dependencies Gemini (Google), FFmpeg (auto-fix filters)
13
- */
14
-
15
- import { Command } from "commander";
16
- import { readFile, writeFile } from "node:fs/promises";
17
- import { resolve } from "node:path";
18
- import chalk from "chalk";
19
- import ora from "ora";
20
- import { GeminiProvider } from "@vibeframe/ai-providers";
21
- import { Project, type ProjectFile } from "../engine/index.js";
22
- import { requireApiKey } from "../utils/api-key.js";
23
- import { applySuggestion } from "./ai-helpers.js";
24
- import { executeAnalyze, executeGeminiVideo } from "./ai-analyze.js";
25
- import { registerReviewCommand } from "./ai-review.js";
26
- import { isJsonMode, outputResult, exitWithError, apiError } from "./output.js";
27
- import { sanitizeLLMResponse } from "./sanitize.js";
28
- import { rejectControlChars } from "./validate.js";
29
-
30
- export const analyzeCommand = new Command("analyze")
31
- .alias("az")
32
- .description("Analyze media using AI (images, videos, YouTube URLs)")
33
- .addHelpText(
34
- "after",
35
- `
36
- Examples:
37
- $ vibe analyze media image.png "Describe this image"
38
- $ vibe analyze media video.mp4 "Summarize this video"
39
- $ vibe analyze media "https://youtube.com/watch?v=..." "Key takeaways"
40
- $ vibe analyze video video.mp4 "List all scene changes" --low-res
41
- $ vibe analyze review video.mp4 --auto-apply -o fixed.mp4
42
- $ vibe analyze suggest project.vibe.json "make it more dramatic"
43
-
44
- API Keys:
45
- GOOGLE_API_KEY Required for all analyze commands (Gemini)
46
-
47
- Use '--fields response,model' to limit output size.
48
- Run 'vibe schema analyze.<command>' for structured parameter info.
49
- `
50
- );
51
-
52
- // ── analyze media ──────────────────────────────────────────────────────
53
-
54
- analyzeCommand
55
- .command("media")
56
- .description("Analyze any media: images, videos, or YouTube URLs using Gemini")
57
- .argument("<source>", "Image/video file path, image URL, or YouTube URL")
58
- .argument("<prompt>", "Analysis prompt (e.g., 'Describe this image', 'Summarize this video')")
59
- .option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)")
60
- .option("-m, --model <model>", "Model: flash (default), flash-2.5, pro", "flash")
61
- .option("--fps <number>", "Frames per second for video (default: 1)")
62
- .option("--start <seconds>", "Start offset in seconds (video only)")
63
- .option("--end <seconds>", "End offset in seconds (video only)")
64
- .option("--low-res", "Use low resolution mode (fewer tokens)")
65
- .option("-v, --verbose", "Show token usage")
66
- .option("--fields <fields>", "Comma-separated fields to include in output (e.g., response,model)")
67
- .action(async (source: string, prompt: string, options) => {
68
- try {
69
- rejectControlChars(prompt);
70
-
71
- if (options.apiKey) {
72
- process.env.GOOGLE_API_KEY = options.apiKey;
73
- } else {
74
- await requireApiKey("GOOGLE_API_KEY", "Google");
75
- }
76
-
77
- const spinner = ora("Analyzing source...").start();
78
- const result = await executeAnalyze({
79
- source,
80
- prompt,
81
- model: options.model as "flash" | "flash-2.5" | "pro",
82
- fps: options.fps ? parseFloat(options.fps) : undefined,
83
- start: options.start ? parseInt(options.start, 10) : undefined,
84
- end: options.end ? parseInt(options.end, 10) : undefined,
85
- lowRes: options.lowRes,
86
- });
87
-
88
- if (!result.success) {
89
- spinner.fail(chalk.red(result.error || "Analysis failed"));
90
- process.exit(1);
91
- }
92
-
93
- spinner.succeed(chalk.green("Analysis complete"));
94
-
95
- const response = sanitizeLLMResponse(result.response || "");
96
-
97
- if (isJsonMode()) {
98
- let result_obj: Record<string, unknown> = { success: true, response, sourceType: result.sourceType, model: result.model };
99
- if (result.totalTokens) {
100
- result_obj = { ...result_obj, promptTokens: result.promptTokens, responseTokens: result.responseTokens, totalTokens: result.totalTokens };
101
- }
102
- if (options.fields) {
103
- const fields = options.fields.split(",").map((f: string) => f.trim());
104
- result_obj = Object.fromEntries(Object.entries(result_obj).filter(([k]) => fields.includes(k) || k === "success"));
105
- }
106
- outputResult(result_obj);
107
- return;
108
- }
109
-
110
- console.log();
111
- console.log(response);
112
- console.log();
113
-
114
- if (options.verbose && result.totalTokens) {
115
- console.log(chalk.dim("-".repeat(40)));
116
- console.log(chalk.dim(`Source type: ${result.sourceType}`));
117
- console.log(chalk.dim(`Model: ${result.model}`));
118
- if (result.promptTokens) {
119
- console.log(chalk.dim(`Prompt tokens: ${result.promptTokens.toLocaleString()}`));
120
- }
121
- if (result.responseTokens) {
122
- console.log(chalk.dim(`Response tokens: ${result.responseTokens.toLocaleString()}`));
123
- }
124
- console.log(chalk.dim(`Total tokens: ${result.totalTokens.toLocaleString()}`));
125
- }
126
- } catch (error) {
127
- exitWithError(apiError(`Analysis failed: ${(error as Error).message}`));
128
- }
129
- });
130
-
131
- // ── analyze video ──────────────────────────────────────────────────────
132
-
133
- analyzeCommand
134
- .command("video")
135
- .description("Analyze video using Gemini (summarize, Q&A, extract info)")
136
- .argument("<source>", "Video file path or YouTube URL")
137
- .argument("<prompt>", "Analysis prompt (e.g., 'Summarize this video')")
138
- .option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)")
139
- .option("-m, --model <model>", "Model: flash (default), flash-2.5, pro", "flash")
140
- .option("--fps <number>", "Frames per second (default: 1, higher for action)")
141
- .option("--start <seconds>", "Start offset in seconds (for clipping)")
142
- .option("--end <seconds>", "End offset in seconds (for clipping)")
143
- .option("--low-res", "Use low resolution mode (fewer tokens, longer videos)")
144
- .option("-v, --verbose", "Show token usage")
145
- .option("--fields <fields>", "Comma-separated fields to include in output (e.g., response,model)")
146
- .action(async (source: string, prompt: string, options) => {
147
- try {
148
- rejectControlChars(prompt);
149
-
150
- if (options.apiKey) {
151
- process.env.GOOGLE_API_KEY = options.apiKey;
152
- } else {
153
- await requireApiKey("GOOGLE_API_KEY", "Google");
154
- }
155
-
156
- const spinner = ora("Analyzing video...").start();
157
- const result = await executeGeminiVideo({
158
- source,
159
- prompt,
160
- model: options.model as "flash" | "flash-2.5" | "pro",
161
- fps: options.fps ? parseFloat(options.fps) : undefined,
162
- start: options.start ? parseInt(options.start, 10) : undefined,
163
- end: options.end ? parseInt(options.end, 10) : undefined,
164
- lowRes: options.lowRes,
165
- });
166
-
167
- if (!result.success) {
168
- spinner.fail(chalk.red(result.error || "Video analysis failed"));
169
- process.exit(1);
170
- }
171
-
172
- spinner.succeed(chalk.green("Video analyzed"));
173
-
174
- const response = sanitizeLLMResponse(result.response || "");
175
-
176
- if (isJsonMode()) {
177
- let result_obj: Record<string, unknown> = { success: true, response, model: result.model };
178
- if (result.totalTokens) {
179
- result_obj = { ...result_obj, promptTokens: result.promptTokens, responseTokens: result.responseTokens, totalTokens: result.totalTokens };
180
- }
181
- if (options.fields) {
182
- const fields = options.fields.split(",").map((f: string) => f.trim());
183
- result_obj = Object.fromEntries(Object.entries(result_obj).filter(([k]) => fields.includes(k) || k === "success"));
184
- }
185
- outputResult(result_obj);
186
- return;
187
- }
188
-
189
- console.log();
190
- console.log(response);
191
- console.log();
192
-
193
- if (options.verbose && result.totalTokens) {
194
- console.log(chalk.dim("-".repeat(40)));
195
- console.log(chalk.dim(`Model: ${result.model}`));
196
- if (result.promptTokens) {
197
- console.log(chalk.dim(`Prompt tokens: ${result.promptTokens.toLocaleString()}`));
198
- }
199
- if (result.responseTokens) {
200
- console.log(chalk.dim(`Response tokens: ${result.responseTokens.toLocaleString()}`));
201
- }
202
- console.log(chalk.dim(`Total tokens: ${result.totalTokens.toLocaleString()}`));
203
- }
204
- } catch (error) {
205
- exitWithError(apiError(`Video analysis failed: ${(error as Error).message}`));
206
- }
207
- });
208
-
209
- // ── analyze review ─────────────────────────────────────────────────────
210
-
211
- registerReviewCommand(analyzeCommand);
212
-
213
- // ── analyze suggest ────────────────────────────────────────────────────
214
-
215
- analyzeCommand
216
- .command("suggest")
217
- .description("Get AI edit suggestions using Gemini")
218
- .argument("<project>", "Project file path")
219
- .argument("<instruction>", "Natural language instruction")
220
- .option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)")
221
- .option("--apply", "Apply the first suggestion automatically")
222
- .action(async (projectPath: string, instruction: string, options) => {
223
- try {
224
- rejectControlChars(instruction);
225
-
226
- const apiKey = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
227
-
228
- const spinner = ora("Initializing Gemini...").start();
229
-
230
- const filePath = resolve(process.cwd(), projectPath);
231
- const content = await readFile(filePath, "utf-8");
232
- const data: ProjectFile = JSON.parse(content);
233
- const project = Project.fromJSON(data);
234
-
235
- const gemini = new GeminiProvider();
236
- await gemini.initialize({ apiKey });
237
-
238
- spinner.text = "Analyzing...";
239
- const clips = project.getClips();
240
- const suggestions = await gemini.autoEdit(clips, instruction);
241
-
242
- spinner.succeed(chalk.green(`Found ${suggestions.length} suggestion(s)`));
243
-
244
- if (isJsonMode()) {
245
- outputResult({ success: true, suggestions: suggestions.map(s => ({ type: s.type, description: s.description, confidence: s.confidence, clipIds: s.clipIds, params: s.params })) });
246
- return;
247
- }
248
-
249
- console.log();
250
- console.log(chalk.bold.cyan("Edit Suggestions"));
251
- console.log(chalk.dim("─".repeat(60)));
252
-
253
- for (let i = 0; i < suggestions.length; i++) {
254
- const sug = suggestions[i];
255
- console.log();
256
- console.log(chalk.yellow(`[${i + 1}] ${sug.type.toUpperCase()}`));
257
- console.log(` ${sug.description}`);
258
- console.log(chalk.dim(` Confidence: ${(sug.confidence * 100).toFixed(0)}%`));
259
- console.log(chalk.dim(` Clips: ${sug.clipIds.join(", ")}`));
260
- console.log(chalk.dim(` Params: ${JSON.stringify(sug.params)}`));
261
- }
262
-
263
- if (options.apply && suggestions.length > 0) {
264
- console.log();
265
- spinner.start("Applying first suggestion...");
266
-
267
- const sug = suggestions[0];
268
- const applied = applySuggestion(project, sug);
269
-
270
- if (applied) {
271
- await writeFile(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
272
- spinner.succeed(chalk.green("Suggestion applied"));
273
- } else {
274
- spinner.warn(chalk.yellow("Could not apply suggestion automatically"));
275
- }
276
- }
277
-
278
- console.log();
279
- } catch (error) {
280
- exitWithError(apiError(`AI suggestion failed: ${(error as Error).message}`));
281
- }
282
- });