feat: add primitive CLI commands for modular content creation#21
feat: add primitive CLI commands for modular content creation#21recoupableorg wants to merge 8 commits intomainfrom
Conversation
New commands under `recoup content`: - image — generate an AI image - video — generate video from image (or lipsync) - text — generate on-screen text - audio — select a song clip - render — combine video + audio + text - upscale — upscale image or video DRY: shared createPrimitiveCommand factory so each command only defines its name, endpoint, options, and body builder. Existing `recoup content create` (V1 full pipeline) is untouched. Made-with: Cursor
📝 WalkthroughWalkthroughAdds a primitive command factory, seven new content subcommands (image, video, caption, transcribe, audio analyze, upscale, edit), registers them on the Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as CLI (commander)
participant Factory as createPrimitiveCommand / edit action
participant Client as HTTP Client (post / patch)
participant API as /api/content/*
CLI->>Factory: parse flags, build payload
Factory->>Client: POST or PATCH /api/content/* with JSON
Client->>API: HTTP request (JSON)
API-->>Client: HTTP response (200 / error with JSON)
Client-->>Factory: parsed ApiResponse
Factory-->>CLI: print JSON or runId message / error
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
content image → content generate-image content video → content generate-video content text → content generate-caption content audio → content transcribe-audio content render, upscale unchanged (already verbs) Matches API route rename and cli-for-agents naming convention. Made-with: Cursor
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/commands/content/imageCommand.ts (1)
13-18:artist_account_idmay be sent asundefinedwhen--artistis not provided.If
--artistis required for this endpoint, consider using Commander's.requiredOption()or validating in thebuildBodycallback. Otherwise, the API will receiveartist_account_id: undefined.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/content/imageCommand.ts` around lines 13 - 18, The buildBody callback currently includes artist_account_id: opts.artist which can send undefined when --artist is omitted; update the CLI to require or validate this value: either change the option registration for the artist flag to use Commander’s .requiredOption() or add a guard inside the buildBody (the arrow function that returns the body) to throw an error or omit artist_account_id when opts.artist is falsy; reference the buildBody arrow function in imageCommand.ts and the artist option flag to locate where to enforce/validate the value.src/commands/content/videoCommand.ts (1)
16-24:image_urlmay be sent asundefinedwhen--imageis not provided.Unlike other optional fields (template, song_url, motion_prompt),
image_urlis always included in the payload. If the--imageflag is optional, consider conditionally spreading it to avoid sendingundefined:♻️ Suggested fix
(opts) => ({ - image_url: opts.image, + ...(opts.image && { image_url: opts.image }), ...(opts.template && { template: opts.template }), lipsync: !!opts.lipsync,Alternatively, if
--imageis required for the non-lipsync mode, consider addingrequiredOptionsemantics or documenting this constraint.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/content/videoCommand.ts` around lines 16 - 24, The payload builder in videoCommand.ts currently always sets image_url: opts.image which will emit image_url: undefined when --image isn't provided; change the builder (the arrow function that returns the object) to conditionally spread image_url similar to template/songUrl/motion (e.g., ...(opts.image && { image_url: opts.image })) so image_url is omitted when absent, or alternatively enforce the flag as required via the command option setup if image is mandatory for non-lipsync flows (adjust the option definition instead of the builder).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/commands/content/imageCommand.ts`:
- Around line 13-18: The buildBody callback currently includes
artist_account_id: opts.artist which can send undefined when --artist is
omitted; update the CLI to require or validate this value: either change the
option registration for the artist flag to use Commander’s .requiredOption() or
add a guard inside the buildBody (the arrow function that returns the body) to
throw an error or omit artist_account_id when opts.artist is falsy; reference
the buildBody arrow function in imageCommand.ts and the artist option flag to
locate where to enforce/validate the value.
In `@src/commands/content/videoCommand.ts`:
- Around line 16-24: The payload builder in videoCommand.ts currently always
sets image_url: opts.image which will emit image_url: undefined when --image
isn't provided; change the builder (the arrow function that returns the object)
to conditionally spread image_url similar to template/songUrl/motion (e.g.,
...(opts.image && { image_url: opts.image })) so image_url is omitted when
absent, or alternatively enforce the flag as required via the command option
setup if image is mandatory for non-lipsync flows (adjust the option definition
instead of the builder).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8e064515-c91e-48fc-ae42-ffcc28799cf3
📒 Files selected for processing (8)
src/commands/content.tssrc/commands/content/audioCommand.tssrc/commands/content/createPrimitiveCommand.tssrc/commands/content/imageCommand.tssrc/commands/content/renderCommand.tssrc/commands/content/textCommand.tssrc/commands/content/upscaleCommand.tssrc/commands/content/videoCommand.ts
- generate-image: --reference-image replaces --face-guide, --model added - generate-video: --audio replaces --song-url, --model added, trim flags removed - generate-caption: --topic replaces --song/--artist - transcribe-audio: --url replaces --artist/--song, --model added - New edit command replaces render with template mode + manual operations Made-with: Cursor
Address CodeRabbit review: conditionally spread image_url to avoid sending undefined when --image flag is omitted. Made-with: Cursor
Matches API change — video generation accepts prompt, optional image, optional audio. Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
src/commands/content/audioCommand.ts (1)
11-18: Robust URL parsing implementation.The comma-separated URL parsing with trim and filter is well-implemented. The ternary correctly handles the undefined case by returning an empty array.
Minor: The type annotation in
.map((s: string) => s.trim())is redundant since TypeScript infers the type fromsplit().- ? String(opts.url).split(",").map((s: string) => s.trim()).filter(Boolean) + ? String(opts.url).split(",").map((s) => s.trim()).filter(Boolean)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/content/audioCommand.ts` around lines 11 - 18, The map callback in the audio URL parsing is using an unnecessary explicit type annotation "(s: string) => s.trim()"; update the anonymous function inside the audioUrls assignment to rely on TypeScript inference (e.g., change the callback to "s => s.trim()") so the code is cleaner, while keeping the surrounding logic that splits, trims and filters intact; references: the audioUrls variable and the opts.url handling in the anonymous function in audioCommand.ts.src/commands/content/editCommand.ts (2)
20-21: Redundant default value fallbacks.The
textColorandtextPositionoptions already have default values set via.option()(lines 20-21), so the??fallbacks at lines 45-46 are redundant. Commander.js will always provide the default value when the option is not specified.🧹 Optional cleanup
if (opts.overlayText) { operations.push({ type: "overlay_text", content: opts.overlayText, - color: opts.textColor ?? "white", - position: opts.textPosition ?? "bottom", + color: opts.textColor, + position: opts.textPosition, }); }Also applies to: 45-46
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/content/editCommand.ts` around lines 20 - 21, The options .option("--text-color <color>", "Text color", "white") and .option("--text-position <pos>", "Text position: top, center, bottom", "bottom") already provide defaults via Commander, so remove the redundant nullish-coalescing fallbacks when reading the parsed values (the variables textColor and textPosition) and use the parsed values directly; update any assignments that currently do something like textColor = parsed.textColor ?? "white" and textPosition = parsed.textPosition ?? "bottom" to just use parsed.textColor and parsed.textPosition.
63-63: Minor: Redundant default foroutput_format.Same as above—the default is already set at line 23.
- output_format: opts.outputFormat ?? "mp4", + output_format: opts.outputFormat,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/content/editCommand.ts` at line 63, The output_format default in the editCommand.ts diff is redundant: remove the fallback "mp4" from the object property assignment (output_format: opts.outputFormat ?? "mp4") and instead rely on the existing default applied earlier (line 23) so that output_format reads directly from opts.outputFormat (or ensure both places use the same central default constant); update the assignment in the edit command to use opts.outputFormat without a hardcoded fallback to avoid duplicate defaults.src/commands/content/textCommand.ts (1)
11-14: Inconsistent handling of optionaltopiccompared to other commands.Unlike
imageCommandandvideoCommandwhich use conditional spread (...(opts.prompt && { prompt: opts.prompt })), this command always includestopicin the body even when undefined. Consider aligning with the pattern used elsewhere:🧹 Suggested alignment
(opts) => ({ - topic: opts.topic, + ...(opts.topic && { topic: opts.topic }), length: opts.length, }),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/content/textCommand.ts` around lines 11 - 14, The returned body in the text command builder always includes topic even when undefined; change the object construction in the arrow function (the builder that currently returns { topic: opts.topic, length: opts.length }) to use the same conditional-spread pattern used by imageCommand/videoCommand (e.g., spread ...(opts.topic && { topic: opts.topic })) so topic is only included when defined, leaving length unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/commands/content/upscaleCommand.ts`:
- Around line 11-14: The upscale command currently treats --url as optional so
buildBody (the factory mapping (opts) => ({ url: opts.url, type: opts.type }))
can produce url: undefined; fix by making --url required to match other
commands: update the option registration used by createPrimitiveCommand to use
requiredOption for the url flag (or, if changing createPrimitiveCommand API is
not possible, add an explicit validation in buildBody that throws/returns a
clear error when opts.url is missing). Target symbols: the
createPrimitiveCommand call for the upscale command and its buildBody mapping
(the (opts) => ({ url: opts.url, type: opts.type })) and ensure the option
definition for url uses requiredOption('--url <url>') or that buildBody
validates opts.url.
---
Nitpick comments:
In `@src/commands/content/audioCommand.ts`:
- Around line 11-18: The map callback in the audio URL parsing is using an
unnecessary explicit type annotation "(s: string) => s.trim()"; update the
anonymous function inside the audioUrls assignment to rely on TypeScript
inference (e.g., change the callback to "s => s.trim()") so the code is cleaner,
while keeping the surrounding logic that splits, trims and filters intact;
references: the audioUrls variable and the opts.url handling in the anonymous
function in audioCommand.ts.
In `@src/commands/content/editCommand.ts`:
- Around line 20-21: The options .option("--text-color <color>", "Text color",
"white") and .option("--text-position <pos>", "Text position: top, center,
bottom", "bottom") already provide defaults via Commander, so remove the
redundant nullish-coalescing fallbacks when reading the parsed values (the
variables textColor and textPosition) and use the parsed values directly; update
any assignments that currently do something like textColor = parsed.textColor ??
"white" and textPosition = parsed.textPosition ?? "bottom" to just use
parsed.textColor and parsed.textPosition.
- Line 63: The output_format default in the editCommand.ts diff is redundant:
remove the fallback "mp4" from the object property assignment (output_format:
opts.outputFormat ?? "mp4") and instead rely on the existing default applied
earlier (line 23) so that output_format reads directly from opts.outputFormat
(or ensure both places use the same central default constant); update the
assignment in the edit command to use opts.outputFormat without a hardcoded
fallback to avoid duplicate defaults.
In `@src/commands/content/textCommand.ts`:
- Around line 11-14: The returned body in the text command builder always
includes topic even when undefined; change the object construction in the arrow
function (the builder that currently returns { topic: opts.topic, length:
opts.length }) to use the same conditional-spread pattern used by
imageCommand/videoCommand (e.g., spread ...(opts.topic && { topic: opts.topic
})) so topic is only included when defined, leaving length unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 83f25ff3-4874-4372-ac6c-8779bd64a5e3
📒 Files selected for processing (9)
cli-for-agentsrc/commands/content.tssrc/commands/content/audioCommand.tssrc/commands/content/createPrimitiveCommand.tssrc/commands/content/editCommand.tssrc/commands/content/imageCommand.tssrc/commands/content/textCommand.tssrc/commands/content/upscaleCommand.tssrc/commands/content/videoCommand.ts
| (opts) => ({ | ||
| url: opts.url, | ||
| type: opts.type, | ||
| }), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how other commands handle required options in this codebase
rg -n "requiredOption" --type=tsRepository: recoupable/cli
Length of output: 439
🏁 Script executed:
cat src/commands/content/upscaleCommand.tsRepository: recoupable/cli
Length of output: 498
🏁 Script executed:
cat src/commands/content/createPrimitiveCommand.tsRepository: recoupable/cli
Length of output: 1540
Add validation for required --url option or enhance the factory to support required options.
The current implementation uses .option() instead of .requiredOption(), which means --url is optional. If the user calls content upscale without --url, the body will contain url: undefined, causing an API error. Other commands in this codebase (statusCommand, notifications, validateCommand, createCommand) follow the .requiredOption() pattern for their required options. Either add validation in buildBody to ensure url is provided, or extend createPrimitiveCommand to support a required flag in the option definition.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/content/upscaleCommand.ts` around lines 11 - 14, The upscale
command currently treats --url as optional so buildBody (the factory mapping
(opts) => ({ url: opts.url, type: opts.type })) can produce url: undefined; fix
by making --url required to match other commands: update the option registration
used by createPrimitiveCommand to use requiredOption for the url flag (or, if
changing createPrimitiveCommand API is not possible, add an explicit validation
in buildBody that throws/returns a clear error when opts.url is missing). Target
symbols: the createPrimitiveCommand call for the upscale command and its
buildBody mapping (the (opts) => ({ url: opts.url, type: opts.type })) and
ensure the option definition for url uses requiredOption('--url <url>') or that
buildBody validates opts.url.
Supports all 6 modes: prompt, animate, reference, extend, first-last, lipsync. Adds --end-image, --video, --aspect-ratio, --duration, --resolution, --negative-prompt, --generate-audio. Made-with: Cursor
generate-image → image, generate-video → video, generate-caption → caption, transcribe-audio → transcribe. Edit uses PATCH /api/content/video. Added patch() to client. Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/commands/content/editCommand.ts (1)
58-66: Consider validating that required inputs are provided.The command sends a request without ensuring
--video,--audio, or--templateis specified. While the API will reject invalid requests, validating locally provides faster, clearer feedback to users.💡 Optional validation before API call
const body: Record<string, unknown> = { ...(opts.video && { video_url: opts.video }), ...(opts.audio && { audio_url: opts.audio }), ...(opts.template && { template: opts.template }), ...(operations.length > 0 && { operations }), output_format: opts.outputFormat ?? "mp4", }; + if (!opts.video && !opts.audio && !opts.template) { + throw new Error("At least one of --video, --audio, or --template is required"); + } + const data = await patch("/api/content/video", body);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/content/editCommand.ts` around lines 58 - 66, Validate inputs before making the API call: ensure at least one of opts.video, opts.audio, or opts.template is provided (or whatever combination your API requires) before constructing body and calling patch("/api/content/video"). In editCommand.ts, add a guard using the existing opts and operations variables (the same scope that builds body) that logs an error/usage hint and exits/throws if none of opts.video, opts.audio, or opts.template are set, so you fail fast with a clear message instead of sending an invalid request to patch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/commands/content/editCommand.ts`:
- Around line 29-35: The trim block currently converts opts.trimStart and
opts.trimDuration with Number(...) which yields NaN for invalid input; update
the logic around the operations.push for "trim" (referencing opts.trimStart,
opts.trimDuration, and the operations array) to validate both values before
pushing: attempt to coerce to numbers, check with Number.isFinite (or
!Number.isNaN) and if either is invalid, print a clear user-facing error (or
throw/exit) indicating which flag is malformed (e.g., --trim-start or
--trim-duration) and stop the command; only push the trim operation when both
parsed numbers are valid.
In `@src/commands/content/videoCommand.ts`:
- Around line 3-35: The file currently exports a constant videoCommand built via
createPrimitiveCommand; change this to export a function (e.g., export function
createVideoCommand()) that returns the same createPrimitiveCommand(...) object
so the file exports a single function per SRP/export rule; update any callers to
invoke createVideoCommand() instead of importing videoCommand, keep the inner
mapping logic (opts -> payload) and all referenced symbols
(createPrimitiveCommand, opts.mode, opts.prompt, opts.image, opts.endImage,
opts.video, opts.audio, opts.aspectRatio, opts.duration, opts.resolution,
opts.negativePrompt, opts.generateAudio, opts.model) unchanged except for moving
them inside the new function body and returning the command object.
- Around line 8-22: The CLI should validate opts.mode against the allowed values
("prompt", "animate", "reference", "extend", "first-last", "lipsync") before
building the request; inside the factory/handler around the use of opts.mode in
videoCommand.ts (the code that spreads ...(opts.mode && { mode: opts.mode })),
add a fail-fast check that returns a user-friendly error (or throws/prints and
exits) when opts.mode is provided but not one of the allowed strings so invalid
modes are rejected client-side instead of failing at the API boundary.
---
Nitpick comments:
In `@src/commands/content/editCommand.ts`:
- Around line 58-66: Validate inputs before making the API call: ensure at least
one of opts.video, opts.audio, or opts.template is provided (or whatever
combination your API requires) before constructing body and calling
patch("/api/content/video"). In editCommand.ts, add a guard using the existing
opts and operations variables (the same scope that builds body) that logs an
error/usage hint and exits/throws if none of opts.video, opts.audio, or
opts.template are set, so you fail fast with a clear message instead of sending
an invalid request to patch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8b63b930-2b22-4046-8190-0ab7403a0284
📒 Files selected for processing (6)
src/client.tssrc/commands/content/audioCommand.tssrc/commands/content/editCommand.tssrc/commands/content/imageCommand.tssrc/commands/content/textCommand.tssrc/commands/content/videoCommand.ts
✅ Files skipped from review due to trivial changes (1)
- src/commands/content/imageCommand.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/commands/content/textCommand.ts
- src/commands/content/audioCommand.ts
| if (opts.trimStart || opts.trimDuration) { | ||
| operations.push({ | ||
| type: "trim", | ||
| start: Number(opts.trimStart ?? 0), | ||
| duration: Number(opts.trimDuration ?? 15), | ||
| }); | ||
| } |
There was a problem hiding this comment.
Invalid numeric input produces NaN without user feedback.
If a user passes a non-numeric value (e.g., --trim-start abc), Number("abc") yields NaN, which will be sent to the API and likely cause confusing errors.
Consider validating and providing clear feedback:
🛡️ Proposed fix to validate numeric inputs
if (opts.trimStart || opts.trimDuration) {
+ const start = opts.trimStart !== undefined ? Number(opts.trimStart) : 0;
+ const duration = opts.trimDuration !== undefined ? Number(opts.trimDuration) : 15;
+ if (Number.isNaN(start) || Number.isNaN(duration)) {
+ throw new Error("--trim-start and --trim-duration must be valid numbers");
+ }
operations.push({
type: "trim",
- start: Number(opts.trimStart ?? 0),
- duration: Number(opts.trimDuration ?? 15),
+ start,
+ duration,
});
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (opts.trimStart || opts.trimDuration) { | |
| operations.push({ | |
| type: "trim", | |
| start: Number(opts.trimStart ?? 0), | |
| duration: Number(opts.trimDuration ?? 15), | |
| }); | |
| } | |
| if (opts.trimStart || opts.trimDuration) { | |
| const start = opts.trimStart !== undefined ? Number(opts.trimStart) : 0; | |
| const duration = opts.trimDuration !== undefined ? Number(opts.trimDuration) : 15; | |
| if (Number.isNaN(start) || Number.isNaN(duration)) { | |
| throw new Error("--trim-start and --trim-duration must be valid numbers"); | |
| } | |
| operations.push({ | |
| type: "trim", | |
| start, | |
| duration, | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/content/editCommand.ts` around lines 29 - 35, The trim block
currently converts opts.trimStart and opts.trimDuration with Number(...) which
yields NaN for invalid input; update the logic around the operations.push for
"trim" (referencing opts.trimStart, opts.trimDuration, and the operations array)
to validate both values before pushing: attempt to coerce to numbers, check with
Number.isFinite (or !Number.isNaN) and if either is invalid, print a clear
user-facing error (or throw/exit) indicating which flag is malformed (e.g.,
--trim-start or --trim-duration) and stop the command; only push the trim
operation when both parsed numbers are valid.
| export const videoCommand = createPrimitiveCommand( | ||
| "video", | ||
| "Generate a video (prompt, animate, reference, extend, first-last, or lipsync)", | ||
| "/api/content/video", | ||
| [ | ||
| { flag: "--mode <mode>", description: "Mode: prompt, animate, reference, extend, first-last, lipsync" }, | ||
| { flag: "--prompt <text>", description: "Text prompt describing the video" }, | ||
| { flag: "--image <url>", description: "Image URL (animate, reference, first-last, lipsync)" }, | ||
| { flag: "--end-image <url>", description: "End frame image URL (first-last mode)" }, | ||
| { flag: "--video <url>", description: "Video URL to extend (extend mode)" }, | ||
| { flag: "--audio <url>", description: "Audio URL (lipsync mode)" }, | ||
| { flag: "--aspect-ratio <ratio>", description: "auto, 16:9, or 9:16", defaultValue: "auto" }, | ||
| { flag: "--duration <dur>", description: "4s, 6s, 7s, or 8s", defaultValue: "8s" }, | ||
| { flag: "--resolution <res>", description: "720p, 1080p, or 4k", defaultValue: "720p" }, | ||
| { flag: "--negative-prompt <text>", description: "What to avoid in the video" }, | ||
| { flag: "--generate-audio", description: "Generate audio for the video" }, | ||
| { flag: "--model <id>", description: "Override model ID" }, | ||
| ], | ||
| (opts) => ({ | ||
| ...(opts.mode && { mode: opts.mode }), | ||
| ...(opts.prompt && { prompt: opts.prompt }), | ||
| ...(opts.image && { image_url: opts.image }), | ||
| ...(opts.endImage && { end_image_url: opts.endImage }), | ||
| ...(opts.video && { video_url: opts.video }), | ||
| ...(opts.audio && { audio_url: opts.audio }), | ||
| aspect_ratio: opts.aspectRatio, | ||
| duration: opts.duration, | ||
| resolution: opts.resolution, | ||
| ...(opts.negativePrompt && { negative_prompt: opts.negativePrompt }), | ||
| generate_audio: !!opts.generateAudio, | ||
| ...(opts.model && { model: opts.model }), | ||
| }), | ||
| ); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Export a function instead of an exported command object.
This file currently exports a constant (videoCommand) rather than an exported function, which violates the repository SRP/export rule.
♻️ Proposed refactor
-export const videoCommand = createPrimitiveCommand(
- "video",
- "Generate a video (prompt, animate, reference, extend, first-last, or lipsync)",
- "/api/content/video",
- [
- { flag: "--mode <mode>", description: "Mode: prompt, animate, reference, extend, first-last, lipsync" },
- { flag: "--prompt <text>", description: "Text prompt describing the video" },
- { flag: "--image <url>", description: "Image URL (animate, reference, first-last, lipsync)" },
- { flag: "--end-image <url>", description: "End frame image URL (first-last mode)" },
- { flag: "--video <url>", description: "Video URL to extend (extend mode)" },
- { flag: "--audio <url>", description: "Audio URL (lipsync mode)" },
- { flag: "--aspect-ratio <ratio>", description: "auto, 16:9, or 9:16", defaultValue: "auto" },
- { flag: "--duration <dur>", description: "4s, 6s, 7s, or 8s", defaultValue: "8s" },
- { flag: "--resolution <res>", description: "720p, 1080p, or 4k", defaultValue: "720p" },
- { flag: "--negative-prompt <text>", description: "What to avoid in the video" },
- { flag: "--generate-audio", description: "Generate audio for the video" },
- { flag: "--model <id>", description: "Override model ID" },
- ],
- (opts) => ({
- ...(opts.mode && { mode: opts.mode }),
- ...(opts.prompt && { prompt: opts.prompt }),
- ...(opts.image && { image_url: opts.image }),
- ...(opts.endImage && { end_image_url: opts.endImage }),
- ...(opts.video && { video_url: opts.video }),
- ...(opts.audio && { audio_url: opts.audio }),
- aspect_ratio: opts.aspectRatio,
- duration: opts.duration,
- resolution: opts.resolution,
- ...(opts.negativePrompt && { negative_prompt: opts.negativePrompt }),
- generate_audio: !!opts.generateAudio,
- ...(opts.model && { model: opts.model }),
- }),
-);
+export function createVideoCommand() {
+ return createPrimitiveCommand(
+ "video",
+ "Generate a video (prompt, animate, reference, extend, first-last, or lipsync)",
+ "/api/content/video",
+ [
+ { flag: "--mode <mode>", description: "Mode: prompt, animate, reference, extend, first-last, lipsync" },
+ { flag: "--prompt <text>", description: "Text prompt describing the video" },
+ { flag: "--image <url>", description: "Image URL (animate, reference, first-last, lipsync)" },
+ { flag: "--end-image <url>", description: "End frame image URL (first-last mode)" },
+ { flag: "--video <url>", description: "Video URL to extend (extend mode)" },
+ { flag: "--audio <url>", description: "Audio URL (lipsync mode)" },
+ { flag: "--aspect-ratio <ratio>", description: "auto, 16:9, or 9:16", defaultValue: "auto" },
+ { flag: "--duration <dur>", description: "4s, 6s, 7s, or 8s", defaultValue: "8s" },
+ { flag: "--resolution <res>", description: "720p, 1080p, or 4k", defaultValue: "720p" },
+ { flag: "--negative-prompt <text>", description: "What to avoid in the video" },
+ { flag: "--generate-audio", description: "Generate audio for the video" },
+ { flag: "--model <id>", description: "Override model ID" },
+ ],
+ (opts) => ({
+ ...(opts.mode && { mode: opts.mode }),
+ ...(opts.prompt && { prompt: opts.prompt }),
+ ...(opts.image && { image_url: opts.image }),
+ ...(opts.endImage && { end_image_url: opts.endImage }),
+ ...(opts.video && { video_url: opts.video }),
+ ...(opts.audio && { audio_url: opts.audio }),
+ aspect_ratio: opts.aspectRatio,
+ duration: opts.duration,
+ resolution: opts.resolution,
+ ...(opts.negativePrompt && { negative_prompt: opts.negativePrompt }),
+ generate_audio: !!opts.generateAudio,
+ ...(opts.model && { model: opts.model }),
+ }),
+ );
+}As per coding guidelines, src/**/*.ts: Follow Single Responsibility Principle with one exported function per file.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/content/videoCommand.ts` around lines 3 - 35, The file currently
exports a constant videoCommand built via createPrimitiveCommand; change this to
export a function (e.g., export function createVideoCommand()) that returns the
same createPrimitiveCommand(...) object so the file exports a single function
per SRP/export rule; update any callers to invoke createVideoCommand() instead
of importing videoCommand, keep the inner mapping logic (opts -> payload) and
all referenced symbols (createPrimitiveCommand, opts.mode, opts.prompt,
opts.image, opts.endImage, opts.video, opts.audio, opts.aspectRatio,
opts.duration, opts.resolution, opts.negativePrompt, opts.generateAudio,
opts.model) unchanged except for moving them inside the new function body and
returning the command object.
| { flag: "--mode <mode>", description: "Mode: prompt, animate, reference, extend, first-last, lipsync" }, | ||
| { flag: "--prompt <text>", description: "Text prompt describing the video" }, | ||
| { flag: "--image <url>", description: "Image URL (animate, reference, first-last, lipsync)" }, | ||
| { flag: "--end-image <url>", description: "End frame image URL (first-last mode)" }, | ||
| { flag: "--video <url>", description: "Video URL to extend (extend mode)" }, | ||
| { flag: "--audio <url>", description: "Audio URL (lipsync mode)" }, | ||
| { flag: "--aspect-ratio <ratio>", description: "auto, 16:9, or 9:16", defaultValue: "auto" }, | ||
| { flag: "--duration <dur>", description: "4s, 6s, 7s, or 8s", defaultValue: "8s" }, | ||
| { flag: "--resolution <res>", description: "720p, 1080p, or 4k", defaultValue: "720p" }, | ||
| { flag: "--negative-prompt <text>", description: "What to avoid in the video" }, | ||
| { flag: "--generate-audio", description: "Generate audio for the video" }, | ||
| { flag: "--model <id>", description: "Override model ID" }, | ||
| ], | ||
| (opts) => ({ | ||
| ...(opts.mode && { mode: opts.mode }), |
There was a problem hiding this comment.
Validate --mode against allowed values before request build.
Line 8 documents a fixed enum, but Line 22 forwards any value. Invalid modes currently fail late at the API boundary; fail-fast in CLI would improve UX and reduce avoidable requests.
✅ Suggested validation
+const ALLOWED_VIDEO_MODES = new Set([
+ "prompt",
+ "animate",
+ "reference",
+ "extend",
+ "first-last",
+ "lipsync",
+]);
+
export const videoCommand = createPrimitiveCommand(
@@
- (opts) => ({
+ (opts) => ({
+ ...(opts.mode &&
+ !ALLOWED_VIDEO_MODES.has(String(opts.mode)) && {
+ // createPrimitiveCommand catches thrown errors and prints nicely
+ ...(function () {
+ throw new Error("Invalid --mode. Allowed: prompt, animate, reference, extend, first-last, lipsync");
+ })(),
+ }),
...(opts.mode && { mode: opts.mode }),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| { flag: "--mode <mode>", description: "Mode: prompt, animate, reference, extend, first-last, lipsync" }, | |
| { flag: "--prompt <text>", description: "Text prompt describing the video" }, | |
| { flag: "--image <url>", description: "Image URL (animate, reference, first-last, lipsync)" }, | |
| { flag: "--end-image <url>", description: "End frame image URL (first-last mode)" }, | |
| { flag: "--video <url>", description: "Video URL to extend (extend mode)" }, | |
| { flag: "--audio <url>", description: "Audio URL (lipsync mode)" }, | |
| { flag: "--aspect-ratio <ratio>", description: "auto, 16:9, or 9:16", defaultValue: "auto" }, | |
| { flag: "--duration <dur>", description: "4s, 6s, 7s, or 8s", defaultValue: "8s" }, | |
| { flag: "--resolution <res>", description: "720p, 1080p, or 4k", defaultValue: "720p" }, | |
| { flag: "--negative-prompt <text>", description: "What to avoid in the video" }, | |
| { flag: "--generate-audio", description: "Generate audio for the video" }, | |
| { flag: "--model <id>", description: "Override model ID" }, | |
| ], | |
| (opts) => ({ | |
| ...(opts.mode && { mode: opts.mode }), | |
| const ALLOWED_VIDEO_MODES = new Set([ | |
| "prompt", | |
| "animate", | |
| "reference", | |
| "extend", | |
| "first-last", | |
| "lipsync", | |
| ]); | |
| export const videoCommand = createPrimitiveCommand( | |
| { | |
| name: "video", | |
| // ... other configuration | |
| }, | |
| [ | |
| { flag: "--mode <mode>", description: "Mode: prompt, animate, reference, extend, first-last, lipsync" }, | |
| { flag: "--prompt <text>", description: "Text prompt describing the video" }, | |
| { flag: "--image <url>", description: "Image URL (animate, reference, first-last, lipsync)" }, | |
| { flag: "--end-image <url>", description: "End frame image URL (first-last mode)" }, | |
| { flag: "--video <url>", description: "Video URL to extend (extend mode)" }, | |
| { flag: "--audio <url>", description: "Audio URL (lipsync mode)" }, | |
| { flag: "--aspect-ratio <ratio>", description: "auto, 16:9, or 9:16", defaultValue: "auto" }, | |
| { flag: "--duration <dur>", description: "4s, 6s, 7s, or 8s", defaultValue: "8s" }, | |
| { flag: "--resolution <res>", description: "720p, 1080p, or 4k", defaultValue: "720p" }, | |
| { flag: "--negative-prompt <text>", description: "What to avoid in the video" }, | |
| { flag: "--generate-audio", description: "Generate audio for the video" }, | |
| { flag: "--model <id>", description: "Override model ID" }, | |
| ], | |
| (opts) => ({ | |
| ...(opts.mode && | |
| !ALLOWED_VIDEO_MODES.has(String(opts.mode)) && { | |
| // createPrimitiveCommand catches thrown errors and prints nicely | |
| ...(function () { | |
| throw new Error("Invalid --mode. Allowed: prompt, animate, reference, extend, first-last, lipsync"); | |
| })(), | |
| }), | |
| ...(opts.mode && { mode: opts.mode }), | |
| // ... rest of the opts handler | |
| }), | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/content/videoCommand.ts` around lines 8 - 22, The CLI should
validate opts.mode against the allowed values ("prompt", "animate", "reference",
"extend", "first-last", "lipsync") before building the request; inside the
factory/handler around the use of opts.mode in videoCommand.ts (the code that
spreads ...(opts.mode && { mode: opts.mode })), add a fail-fast check that
returns a user-friendly error (or throws/prints and exits) when opts.mode is
provided but not one of the allowed strings so invalid modes are rejected
client-side instead of failing at the API boundary.
- Add analyze command (POST /api/content/analyze) - Update edit command endpoint from /api/content/video to /api/content - Add --template flag to video command - Update templates command description (malleable-first) - Build passes, 74 tests pass Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
src/commands/content/editCommand.ts (1)
29-34:⚠️ Potential issue | 🟡 MinorValidate trim numeric flags before pushing operation.
At Line 29-Line 34, invalid values like
--trim-start abcbecomeNaNand are sent downstream. This should fail fast with a clear CLI error.Suggested fix
- if (opts.trimStart || opts.trimDuration) { + if (opts.trimStart !== undefined || opts.trimDuration !== undefined) { + const start = opts.trimStart !== undefined ? Number(opts.trimStart) : 0; + const duration = opts.trimDuration !== undefined ? Number(opts.trimDuration) : 15; + if (!Number.isFinite(start)) { + throw new Error("--trim-start must be a valid number"); + } + if (!Number.isFinite(duration)) { + throw new Error("--trim-duration must be a valid number"); + } operations.push({ type: "trim", - start: Number(opts.trimStart ?? 0), - duration: Number(opts.trimDuration ?? 15), + start, + duration, }); }
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/commands/content/analyzeCommand.ts`:
- Line 14: The file currently exports a constant analyzeCommand (export const
analyzeCommand = new Command("analyze")) which violates the rule requiring one
exported function per file; change this to export a function (e.g., export
function createAnalyzeCommand()) that constructs and returns the Command
instance, move any command configuration into that function, and update any
import sites to call createAnalyzeCommand() (or the chosen function name)
instead of importing the constant; ensure the function name replaces
analyzeCommand as the exported symbol.
- Around line 6-11: parsePositiveInt currently uses parseInt which accepts
partial/malformed strings (e.g., "12abc", "1.5"); change validation to only
allow a strictly positive integer string before parsing by ensuring the input
matches a whole-digits pattern (e.g., /^\d+$/) and then convert to a number and
confirm it's > 0; update parsePositiveInt to first test the raw value with that
regex (rejecting empty strings, decimals, and embedded letters) and only then
call parseInt/Number and return the integer, throwing the same Error message on
invalid input.
- Around line 20-34: The --temperature option currently uses raw parseFloat
which can yield NaN and be serialized as null; replace parseFloat with a
validator function (modeled after parsePositiveInt) — e.g., create
parseTemperature that parses the input to a number, checks Number.isFinite and
that the value is within an acceptable range (e.g., >= 0 and <= 2 or at least >=
0), and throws an error for invalid values; update the option declaration to use
parseTemperature and keep the existing default fallback in the action where
body.temperature is set so only validated numeric values reach body.temperature.
In `@src/commands/content/editCommand.ts`:
- Line 12: The command description string for the edit command (the .description
call in editCommand.ts) incorrectly advertises "resize" even though no resize
option is implemented; update the .description text to remove "resize" or add
the corresponding resize option implementation; locate the .description("Edit
content — trim, crop, resize, overlay text, or add audio") call in the
editCommand setup and either remove the word "resize" from the string or
implement a resize flag/option (and wire it into the edit handling logic such as
the edit handler function that processes trim/crop/overlay/audio) so the
description matches actual capabilities.
- Around line 11-82: Replace the exported constant editCommand with an exported
function (e.g., export function createEditCommand(): Command) that constructs
and returns the Command instance; move the .description/.option/.action builder
code into that function body and return the new Command. Update usages to call
createEditCommand() where the command is registered. Ensure the function
signature returns a Command and preserve all option names and the action logic
(references: editCommand, the action callback, and the operations/body building
code).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 57b3745a-04de-4259-a5d0-7276821aaf7f
📒 Files selected for processing (5)
src/commands/content.tssrc/commands/content/analyzeCommand.tssrc/commands/content/editCommand.tssrc/commands/content/templatesCommand.tssrc/commands/content/videoCommand.ts
✅ Files skipped from review due to trivial changes (1)
- src/commands/content/templatesCommand.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/commands/content.ts
| function parsePositiveInt(value: string): number { | ||
| const n = parseInt(value, 10); | ||
| if (Number.isNaN(n) || n <= 0) { | ||
| throw new Error(`Expected a positive integer, got "${value}"`); | ||
| } | ||
| return n; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /repo && find . -type f -name "analyzeCommand.ts" | head -5Repository: recoupable/cli
Length of output: 113
🏁 Script executed:
cat -n src/commands/content/analyzeCommand.ts | head -60Repository: recoupable/cli
Length of output: 2120
🏁 Script executed:
python3 << 'EOF'
# Test parseInt behavior with the mentioned inputs
test_cases = [
("12abc", "partial input with letters"),
("1.5", "decimal string"),
("10", "valid integer string"),
("0", "zero"),
("-5", "negative"),
("", "empty string")
]
for test_val, desc in test_cases:
result = int(test_val.split('.')[0].split('a')[0].split('b')[0].split('c')[0]) if test_val and test_val[0].isdigit() else None
print(f"Testing '{test_val}' ({desc})")
# Simulate JavaScript parseInt(value, 10)
import re
match = re.match(r'^[+-]?\d+', test_val)
js_parse_result = int(match.group(0)) if match else float('nan')
print(f" JavaScript parseInt('{test_val}', 10) would return: {js_parse_result if not (isinstance(js_parse_result, float) and js_parse_result != js_parse_result) else 'NaN'}")
# Test the proposed regex
regex_match = re.match(r'^[1-9]\d*$', test_val)
print(f" Regex /^[1-9]\\d*$/.test('{test_val}'): {bool(regex_match)}")
print()
EOFRepository: recoupable/cli
Length of output: 772
parsePositiveInt is currently accepting malformed values.
parseInt will accept partial inputs like "12abc" (returns 12) and "1.5" (returns 1), so this function can silently coerce invalid user input.
Proposed fix
function parsePositiveInt(value: string): number {
- const n = parseInt(value, 10);
- if (Number.isNaN(n) || n <= 0) {
+ if (!/^[1-9]\d*$/.test(value)) {
throw new Error(`Expected a positive integer, got "${value}"`);
}
- return n;
+ return Number(value);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/content/analyzeCommand.ts` around lines 6 - 11, parsePositiveInt
currently uses parseInt which accepts partial/malformed strings (e.g., "12abc",
"1.5"); change validation to only allow a strictly positive integer string
before parsing by ensuring the input matches a whole-digits pattern (e.g.,
/^\d+$/) and then convert to a number and confirm it's > 0; update
parsePositiveInt to first test the raw value with that regex (rejecting empty
strings, decimals, and embedded letters) and only then call parseInt/Number and
return the integer, throwing the same Error message on invalid input.
| return n; | ||
| } | ||
|
|
||
| export const analyzeCommand = new Command("analyze") |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Export a function instead of a constant command instance.
Line 14 violates the repository rule requiring one exported function per file under src/**/*.ts.
Refactor direction
-export const analyzeCommand = new Command("analyze")
- .description(
- "Analyze a video with AI — describe scenes, check quality, evaluate content",
- )
+export function createAnalyzeCommand(): Command {
+ return new Command("analyze")
+ .description(
+ "Analyze a video with AI — describe scenes, check quality, evaluate content",
+ )
@@
- .action(async (opts: Record<string, unknown>) => {
+ .action(async (opts: Record<string, unknown>) => {
@@
- });
+ });
+}As per coding guidelines: src/**/*.ts: Follow Single Responsibility Principle with one exported function per file.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/content/analyzeCommand.ts` at line 14, The file currently
exports a constant analyzeCommand (export const analyzeCommand = new
Command("analyze")) which violates the rule requiring one exported function per
file; change this to export a function (e.g., export function
createAnalyzeCommand()) that constructs and returns the Command instance, move
any command configuration into that function, and update any import sites to
call createAnalyzeCommand() (or the chosen function name) instead of importing
the constant; ensure the function name replaces analyzeCommand as the exported
symbol.
| .option("--temperature <number>", "Sampling temperature (default: 0.2)", parseFloat) | ||
| .option("--max-tokens <number>", "Maximum output tokens", parsePositiveInt) | ||
| .option("--json", "Output raw JSON") | ||
| .action(async (opts: Record<string, unknown>) => { | ||
| try { | ||
| const body: Record<string, unknown> = { | ||
| video_url: opts.video, | ||
| prompt: opts.prompt, | ||
| }; | ||
|
|
||
| if (opts.temperature !== undefined) { | ||
| body.temperature = opts.temperature; | ||
| } else { | ||
| body.temperature = 0.2; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "analyzeCommand.ts" -type fRepository: recoupable/cli
Length of output: 98
🏁 Script executed:
cat -n ./src/commands/content/analyzeCommand.tsRepository: recoupable/cli
Length of output: 2120
🏁 Script executed:
python3 << 'EOF'
import json
# Test JSON behavior with NaN
test_obj = {"temperature": float('nan')}
print("Python JSON with NaN:", json.dumps(test_obj))
# In JavaScript context - verify the claim about NaN serialization
# parseFloat behavior
print("\nparseFloat behavior:")
print("parseFloat('0.5'):", 0.5)
print("parseFloat('invalid'):", float('nan')) # Returns NaN
# JSON.stringify in JS turns NaN to null
print("\nJSON serialization behavior:")
print("JSON.stringify({temp: NaN}) would be {temp: null}")
print("JSON.stringify({temp: 0.5}) would be {temp: 0.5}")
EOFRepository: recoupable/cli
Length of output: 301
🏁 Script executed:
cat -n ./src/client.jsRepository: recoupable/cli
Length of output: 105
🏁 Script executed:
find . -name "client.*" -type f | head -10Repository: recoupable/cli
Length of output: 100
🏁 Script executed:
cat -n ./src/client.tsRepository: recoupable/cli
Length of output: 2980
Add validation to parseFloat for --temperature option to prevent NaN values from becoming null in the API payload.
Line 20 uses parseFloat without validation. Invalid input (e.g., --temperature abc) produces NaN, which JSON serialization converts to null, sending an unexpected value to the API instead of rejecting the input. Use a validation function matching the parsePositiveInt pattern already in the file.
Proposed fix
+function parseTemperature(value: string): number {
+ const n = Number(value);
+ if (!Number.isFinite(n)) {
+ throw new Error(`Expected a valid number, got "${value}"`);
+ }
+ return n;
+}
+
export const analyzeCommand = new Command("analyze")
@@
- .option("--temperature <number>", "Sampling temperature (default: 0.2)", parseFloat)
+ .option("--temperature <number>", "Sampling temperature (default: 0.2)", parseTemperature)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/content/analyzeCommand.ts` around lines 20 - 34, The
--temperature option currently uses raw parseFloat which can yield NaN and be
serialized as null; replace parseFloat with a validator function (modeled after
parsePositiveInt) — e.g., create parseTemperature that parses the input to a
number, checks Number.isFinite and that the value is within an acceptable range
(e.g., >= 0 and <= 2 or at least >= 0), and throws an error for invalid values;
update the option declaration to use parseTemperature and keep the existing
default fallback in the action where body.temperature is set so only validated
numeric values reach body.temperature.
| export const editCommand = new Command("edit") | ||
| .description("Edit content — trim, crop, resize, overlay text, or add audio") | ||
| .option("--video <url>", "Input video URL") | ||
| .option("--audio <url>", "Input audio URL") | ||
| .option("--template <name>", "Template name for deterministic edit config") | ||
| .option("--trim-start <seconds>", "Trim start time in seconds") | ||
| .option("--trim-duration <seconds>", "Trim duration in seconds") | ||
| .option("--crop-aspect <ratio>", "Crop to aspect ratio (e.g. 9:16)") | ||
| .option("--overlay-text <content>", "Overlay text content") | ||
| .option("--text-color <color>", "Text color", "white") | ||
| .option("--text-position <pos>", "Text position: top, center, bottom", "bottom") | ||
| .option("--mux-audio <url>", "Mux audio URL into video") | ||
| .option("--output-format <format>", "Output format: mp4, webm, mov", "mp4") | ||
| .option("--json", "Output as JSON") | ||
| .action(async (opts: Record<string, unknown>) => { | ||
| try { | ||
| const operations: EditOperation[] = []; | ||
|
|
||
| if (opts.trimStart || opts.trimDuration) { | ||
| operations.push({ | ||
| type: "trim", | ||
| start: Number(opts.trimStart ?? 0), | ||
| duration: Number(opts.trimDuration ?? 15), | ||
| }); | ||
| } | ||
|
|
||
| if (opts.cropAspect) { | ||
| operations.push({ type: "crop", aspect: opts.cropAspect }); | ||
| } | ||
|
|
||
| if (opts.overlayText) { | ||
| operations.push({ | ||
| type: "overlay_text", | ||
| content: opts.overlayText, | ||
| color: opts.textColor ?? "white", | ||
| position: opts.textPosition ?? "bottom", | ||
| }); | ||
| } | ||
|
|
||
| if (opts.muxAudio) { | ||
| operations.push({ | ||
| type: "mux_audio", | ||
| audio_url: opts.muxAudio, | ||
| replace: true, | ||
| }); | ||
| } | ||
|
|
||
| const body: Record<string, unknown> = { | ||
| ...(opts.video && { video_url: opts.video }), | ||
| ...(opts.audio && { audio_url: opts.audio }), | ||
| ...(opts.template && { template: opts.template }), | ||
| ...(operations.length > 0 && { operations }), | ||
| output_format: opts.outputFormat ?? "mp4", | ||
| }; | ||
|
|
||
| const data = await patch("/api/content", body); | ||
|
|
||
| if (opts.json) { | ||
| printJson(data); | ||
| return; | ||
| } | ||
|
|
||
| if (data.runId) { | ||
| console.log(`Run started: ${data.runId}`); | ||
| console.log("Use `recoup tasks status --run <runId>` to check progress."); | ||
| } else { | ||
| printJson(data); | ||
| } | ||
| } catch (err) { | ||
| printError(getErrorMessage(err)); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Export a function instead of a constant command object in this file.
This file currently exports editCommand as a constant; project guidance requires one exported function per TypeScript file.
Suggested refactor
-export const editCommand = new Command("edit")
- .description("Edit content — trim, crop, overlay text, or add audio")
+export function editCommand(): Command {
+ return new Command("edit")
+ .description("Edit content — trim, crop, overlay text, or add audio")
.option("--video <url>", "Input video URL")
.option("--audio <url>", "Input audio URL")
.option("--template <name>", "Template name for deterministic edit config")
.option("--trim-start <seconds>", "Trim start time in seconds")
.option("--trim-duration <seconds>", "Trim duration in seconds")
.option("--crop-aspect <ratio>", "Crop to aspect ratio (e.g. 9:16)")
.option("--overlay-text <content>", "Overlay text content")
.option("--text-color <color>", "Text color", "white")
.option("--text-position <pos>", "Text position: top, center, bottom", "bottom")
.option("--mux-audio <url>", "Mux audio URL into video")
.option("--output-format <format>", "Output format: mp4, webm, mov", "mp4")
.option("--json", "Output as JSON")
.action(async (opts: Record<string, unknown>) => {
...
});
+}As per coding guidelines: src/**/*.ts: Follow Single Responsibility Principle with one exported function per file.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const editCommand = new Command("edit") | |
| .description("Edit content — trim, crop, resize, overlay text, or add audio") | |
| .option("--video <url>", "Input video URL") | |
| .option("--audio <url>", "Input audio URL") | |
| .option("--template <name>", "Template name for deterministic edit config") | |
| .option("--trim-start <seconds>", "Trim start time in seconds") | |
| .option("--trim-duration <seconds>", "Trim duration in seconds") | |
| .option("--crop-aspect <ratio>", "Crop to aspect ratio (e.g. 9:16)") | |
| .option("--overlay-text <content>", "Overlay text content") | |
| .option("--text-color <color>", "Text color", "white") | |
| .option("--text-position <pos>", "Text position: top, center, bottom", "bottom") | |
| .option("--mux-audio <url>", "Mux audio URL into video") | |
| .option("--output-format <format>", "Output format: mp4, webm, mov", "mp4") | |
| .option("--json", "Output as JSON") | |
| .action(async (opts: Record<string, unknown>) => { | |
| try { | |
| const operations: EditOperation[] = []; | |
| if (opts.trimStart || opts.trimDuration) { | |
| operations.push({ | |
| type: "trim", | |
| start: Number(opts.trimStart ?? 0), | |
| duration: Number(opts.trimDuration ?? 15), | |
| }); | |
| } | |
| if (opts.cropAspect) { | |
| operations.push({ type: "crop", aspect: opts.cropAspect }); | |
| } | |
| if (opts.overlayText) { | |
| operations.push({ | |
| type: "overlay_text", | |
| content: opts.overlayText, | |
| color: opts.textColor ?? "white", | |
| position: opts.textPosition ?? "bottom", | |
| }); | |
| } | |
| if (opts.muxAudio) { | |
| operations.push({ | |
| type: "mux_audio", | |
| audio_url: opts.muxAudio, | |
| replace: true, | |
| }); | |
| } | |
| const body: Record<string, unknown> = { | |
| ...(opts.video && { video_url: opts.video }), | |
| ...(opts.audio && { audio_url: opts.audio }), | |
| ...(opts.template && { template: opts.template }), | |
| ...(operations.length > 0 && { operations }), | |
| output_format: opts.outputFormat ?? "mp4", | |
| }; | |
| const data = await patch("/api/content", body); | |
| if (opts.json) { | |
| printJson(data); | |
| return; | |
| } | |
| if (data.runId) { | |
| console.log(`Run started: ${data.runId}`); | |
| console.log("Use `recoup tasks status --run <runId>` to check progress."); | |
| } else { | |
| printJson(data); | |
| } | |
| } catch (err) { | |
| printError(getErrorMessage(err)); | |
| } | |
| }); | |
| export function editCommand(): Command { | |
| return new Command("edit") | |
| .description("Edit content — trim, crop, overlay text, or add audio") | |
| .option("--video <url>", "Input video URL") | |
| .option("--audio <url>", "Input audio URL") | |
| .option("--template <name>", "Template name for deterministic edit config") | |
| .option("--trim-start <seconds>", "Trim start time in seconds") | |
| .option("--trim-duration <seconds>", "Trim duration in seconds") | |
| .option("--crop-aspect <ratio>", "Crop to aspect ratio (e.g. 9:16)") | |
| .option("--overlay-text <content>", "Overlay text content") | |
| .option("--text-color <color>", "Text color", "white") | |
| .option("--text-position <pos>", "Text position: top, center, bottom", "bottom") | |
| .option("--mux-audio <url>", "Mux audio URL into video") | |
| .option("--output-format <format>", "Output format: mp4, webm, mov", "mp4") | |
| .option("--json", "Output as JSON") | |
| .action(async (opts: Record<string, unknown>) => { | |
| try { | |
| const operations: EditOperation[] = []; | |
| if (opts.trimStart || opts.trimDuration) { | |
| operations.push({ | |
| type: "trim", | |
| start: Number(opts.trimStart ?? 0), | |
| duration: Number(opts.trimDuration ?? 15), | |
| }); | |
| } | |
| if (opts.cropAspect) { | |
| operations.push({ type: "crop", aspect: opts.cropAspect }); | |
| } | |
| if (opts.overlayText) { | |
| operations.push({ | |
| type: "overlay_text", | |
| content: opts.overlayText, | |
| color: opts.textColor ?? "white", | |
| position: opts.textPosition ?? "bottom", | |
| }); | |
| } | |
| if (opts.muxAudio) { | |
| operations.push({ | |
| type: "mux_audio", | |
| audio_url: opts.muxAudio, | |
| replace: true, | |
| }); | |
| } | |
| const body: Record<string, unknown> = { | |
| ...(opts.video && { video_url: opts.video }), | |
| ...(opts.audio && { audio_url: opts.audio }), | |
| ...(opts.template && { template: opts.template }), | |
| ...(operations.length > 0 && { operations }), | |
| output_format: opts.outputFormat ?? "mp4", | |
| }; | |
| const data = await patch("/api/content", body); | |
| if (opts.json) { | |
| printJson(data); | |
| return; | |
| } | |
| if (data.runId) { | |
| console.log(`Run started: ${data.runId}`); | |
| console.log("Use `recoup tasks status --run <runId>` to check progress."); | |
| } else { | |
| printJson(data); | |
| } | |
| } catch (err) { | |
| printError(getErrorMessage(err)); | |
| } | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/content/editCommand.ts` around lines 11 - 82, Replace the
exported constant editCommand with an exported function (e.g., export function
createEditCommand(): Command) that constructs and returns the Command instance;
move the .description/.option/.action builder code into that function body and
return the new Command. Update usages to call createEditCommand() where the
command is registered. Ensure the function signature returns a Command and
preserve all option names and the action logic (references: editCommand, the
action callback, and the operations/body building code).
| } | ||
|
|
||
| export const editCommand = new Command("edit") | ||
| .description("Edit content — trim, crop, resize, overlay text, or add audio") |
There was a problem hiding this comment.
Description mentions resize, but no resize option exists.
Line 12 currently advertises a capability that this command does not implement, which can mislead users.
Suggested fix
- .description("Edit content — trim, crop, resize, overlay text, or add audio")
+ .description("Edit content — trim, crop, overlay text, or add audio")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .description("Edit content — trim, crop, resize, overlay text, or add audio") | |
| .description("Edit content — trim, crop, overlay text, or add audio") |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/content/editCommand.ts` at line 12, The command description
string for the edit command (the .description call in editCommand.ts)
incorrectly advertises "resize" even though no resize option is implemented;
update the .description text to remove "resize" or add the corresponding resize
option implementation; locate the .description("Edit content — trim, crop,
resize, overlay text, or add audio") call in the editCommand setup and either
remove the word "resize" from the string or implement a resize flag/option (and
wire it into the edit handling logic such as the edit handler function that
processes trim/crop/overlay/audio) so the description matches actual
capabilities.
Summary
Restructure content CLI commands to match the API route rename and make all parameters generic.
Command renames
Generic parameters
New edit command
All 74 CLI tests pass
Summary by CodeRabbit
New Features
Documentation