Skip to content

feat: add primitive CLI commands for modular content creation#21

Open
recoupableorg wants to merge 8 commits intomainfrom
feature/content-primitive-commands
Open

feat: add primitive CLI commands for modular content creation#21
recoupableorg wants to merge 8 commits intomainfrom
feature/content-primitive-commands

Conversation

@recoupableorg
Copy link
Copy Markdown

@recoupableorg recoupableorg commented Apr 2, 2026

Summary

Restructure content CLI commands to match the API route rename and make all parameters generic.

Command renames

  • content image → content generate-image
  • content video → content generate-video
  • content text → content generate-caption
  • content audio → content transcribe-audio
  • content render → content edit (new operations-based command)

Generic parameters

  • --face-guide → --reference-image
  • --song-url → --audio
  • --artist/--song removed from primitives
  • Added --model flag to generate-image, generate-video, transcribe-audio

New edit command

  • Template mode: --template for deterministic edit config
  • Manual mode: --trim-start, --trim-duration, --crop-aspect, --overlay-text, --mux-audio

All 74 CLI tests pass

Summary by CodeRabbit

  • New Features

    • Image generation from prompts with optional reference images
    • Video generation with customizable modes, inputs, aspect/duration/resolution, and optional audio
    • Audio transcription from URLs with selectable model
    • On-screen caption generation with length options
    • Image/video upscaling
    • Video/audio editing: trim, crop, overlay text, templates, audio muxing, and selectable output format
    • Video analysis driven by a prompt with temperature/max-token controls
  • Documentation

    • Clarified templates wording to emphasize optional curated creative recipes

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
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Adds a primitive command factory, seven new content subcommands (image, video, caption, transcribe, audio analyze, upscale, edit), registers them on the content command, and introduces a PATCH client helper; commands build request payloads and call /api/content/* endpoints with unified output/error handling.

Changes

Cohort / File(s) Summary
Command Factory
src/commands/content/createPrimitiveCommand.ts
New createPrimitiveCommand factory and PrimitiveOption interface to standardize primitive CLI commands, option parsing, POSTing, --json output, and error handling.
Primitive Content Commands
src/commands/content/imageCommand.ts, src/commands/content/videoCommand.ts, src/commands/content/textCommand.ts, src/commands/content/audioCommand.ts, src/commands/content/upscaleCommand.ts, src/commands/content/analyzeCommand.ts
Adds primitive commands (image, video, caption, transcribe, upscale, analyze) that declare CLI flags, map options to JSON payloads, and POST to /api/content/* endpoints; analyze includes validation and different text/json output behavior.
Complex Edit Command
src/commands/content/editCommand.ts
New edit subcommand building conditional operations (trim, crop, overlay_text, mux_audio), accepts media/template inputs, sets output_format, and PATCHes /api/content with unified error/output handling.
Main Command Registration
src/commands/content.ts
Imports and registers the added subcommands (imageCommand, videoCommand, textCommand, audioCommand, editCommand, upscaleCommand, analyzeCommand) onto contentCommand.
HTTP client
src/client.ts
Adds exported patch(path, body) helper mirroring get/post behavior: sends PATCH with JSON, parses ApiResponse, and surfaces errors when necessary.
Templates description
src/commands/content/templatesCommand.ts
Updates templates command description to clarify purpose and optionality of templates (text-only change).

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I stitched a prompt into a frame,
I hummed the audio's gentle name,
I painted pixels, scaled them tall,
I trimmed and mixed and sent the call,
Hooray — content blooms for one and all! 🎨🌿🐇

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding multiple primitive CLI commands for modular content creation, which is the primary focus across all file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/content-primitive-commands

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/commands/content/imageCommand.ts (1)

13-18: artist_account_id may be sent as undefined when --artist is not provided.

If --artist is required for this endpoint, consider using Commander's .requiredOption() or validating in the buildBody callback. Otherwise, the API will receive artist_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_url may be sent as undefined when --image is not provided.

Unlike other optional fields (template, song_url, motion_prompt), image_url is always included in the payload. If the --image flag is optional, consider conditionally spreading it to avoid sending undefined:

♻️ Suggested fix
   (opts) => ({
-    image_url: opts.image,
+    ...(opts.image && { image_url: opts.image }),
     ...(opts.template && { template: opts.template }),
     lipsync: !!opts.lipsync,

Alternatively, if --image is required for the non-lipsync mode, consider adding requiredOption semantics 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

📥 Commits

Reviewing files that changed from the base of the PR and between f736a24 and 9be84d8.

📒 Files selected for processing (8)
  • src/commands/content.ts
  • src/commands/content/audioCommand.ts
  • src/commands/content/createPrimitiveCommand.ts
  • src/commands/content/imageCommand.ts
  • src/commands/content/renderCommand.ts
  • src/commands/content/textCommand.ts
  • src/commands/content/upscaleCommand.ts
  • src/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
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 from split().

-      ? 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 textColor and textPosition options 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 for output_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 optional topic compared to other commands.

Unlike imageCommand and videoCommand which use conditional spread (...(opts.prompt && { prompt: opts.prompt })), this command always includes topic in 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

📥 Commits

Reviewing files that changed from the base of the PR and between f736a24 and ac2c062.

📒 Files selected for processing (9)
  • cli-for-agent
  • src/commands/content.ts
  • src/commands/content/audioCommand.ts
  • src/commands/content/createPrimitiveCommand.ts
  • src/commands/content/editCommand.ts
  • src/commands/content/imageCommand.ts
  • src/commands/content/textCommand.ts
  • src/commands/content/upscaleCommand.ts
  • src/commands/content/videoCommand.ts

Comment on lines +11 to +14
(opts) => ({
url: opts.url,
type: opts.type,
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how other commands handle required options in this codebase
rg -n "requiredOption" --type=ts

Repository: recoupable/cli

Length of output: 439


🏁 Script executed:

cat src/commands/content/upscaleCommand.ts

Repository: recoupable/cli

Length of output: 498


🏁 Script executed:

cat src/commands/content/createPrimitiveCommand.ts

Repository: 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
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --template is 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

📥 Commits

Reviewing files that changed from the base of the PR and between f3caf4d and 473dea2.

📒 Files selected for processing (6)
  • src/client.ts
  • src/commands/content/audioCommand.ts
  • src/commands/content/editCommand.ts
  • src/commands/content/imageCommand.ts
  • src/commands/content/textCommand.ts
  • src/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

Comment on lines +29 to +35
if (opts.trimStart || opts.trimDuration) {
operations.push({
type: "trim",
start: Number(opts.trimStart ?? 0),
duration: Number(opts.trimDuration ?? 15),
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +3 to +35
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 }),
}),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +8 to +22
{ 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 }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
{ 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
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/commands/content/editCommand.ts (1)

29-34: ⚠️ Potential issue | 🟡 Minor

Validate trim numeric flags before pushing operation.

At Line 29-Line 34, invalid values like --trim-start abc become NaN and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 473dea2 and 9ae2f32.

📒 Files selected for processing (5)
  • src/commands/content.ts
  • src/commands/content/analyzeCommand.ts
  • src/commands/content/editCommand.ts
  • src/commands/content/templatesCommand.ts
  • src/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

Comment on lines +6 to +11
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /repo && find . -type f -name "analyzeCommand.ts" | head -5

Repository: recoupable/cli

Length of output: 113


🏁 Script executed:

cat -n src/commands/content/analyzeCommand.ts | head -60

Repository: 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()
EOF

Repository: 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")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +20 to +34
.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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "analyzeCommand.ts" -type f

Repository: recoupable/cli

Length of output: 98


🏁 Script executed:

cat -n ./src/commands/content/analyzeCommand.ts

Repository: 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}")

EOF

Repository: recoupable/cli

Length of output: 301


🏁 Script executed:

cat -n ./src/client.js

Repository: recoupable/cli

Length of output: 105


🏁 Script executed:

find . -name "client.*" -type f | head -10

Repository: recoupable/cli

Length of output: 100


🏁 Script executed:

cat -n ./src/client.ts

Repository: 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.

Comment on lines +11 to +82
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));
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants