@usenaive-sdk/cli 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +76 -5
  2. package/dist/client.d.ts +74 -1
  3. package/dist/client.js +351 -17
  4. package/dist/client.js.map +1 -1
  5. package/dist/commands/aeo.js +69 -27
  6. package/dist/commands/aeo.js.map +1 -1
  7. package/dist/commands/app-data.js +29 -11
  8. package/dist/commands/app-data.js.map +1 -1
  9. package/dist/commands/apps.d.ts +7 -0
  10. package/dist/commands/apps.js +255 -18
  11. package/dist/commands/apps.js.map +1 -1
  12. package/dist/commands/audio.d.ts +9 -0
  13. package/dist/commands/audio.js +391 -0
  14. package/dist/commands/audio.js.map +1 -0
  15. package/dist/commands/auth.d.ts +31 -0
  16. package/dist/commands/auth.js +403 -0
  17. package/dist/commands/auth.js.map +1 -0
  18. package/dist/commands/billing.js +12 -12
  19. package/dist/commands/billing.js.map +1 -1
  20. package/dist/commands/brain.d.ts +2 -0
  21. package/dist/commands/brain.js +1255 -0
  22. package/dist/commands/brain.js.map +1 -0
  23. package/dist/commands/browser.js +35 -8
  24. package/dist/commands/browser.js.map +1 -1
  25. package/dist/commands/business.js +117 -16
  26. package/dist/commands/business.js.map +1 -1
  27. package/dist/commands/clone.d.ts +2 -0
  28. package/dist/commands/clone.js +203 -0
  29. package/dist/commands/clone.js.map +1 -0
  30. package/dist/commands/companies.d.ts +8 -0
  31. package/dist/commands/companies.js +137 -47
  32. package/dist/commands/companies.js.map +1 -1
  33. package/dist/commands/company-data.d.ts +2 -0
  34. package/dist/commands/company-data.js +168 -0
  35. package/dist/commands/company-data.js.map +1 -0
  36. package/dist/commands/compute.js +10 -3
  37. package/dist/commands/compute.js.map +1 -1
  38. package/dist/commands/config.d.ts +2 -0
  39. package/dist/commands/config.js +36 -0
  40. package/dist/commands/config.js.map +1 -0
  41. package/dist/commands/connections.d.ts +6 -0
  42. package/dist/commands/connections.js +29 -1
  43. package/dist/commands/connections.js.map +1 -1
  44. package/dist/commands/domains.js +1 -1
  45. package/dist/commands/ecommerce.js +19 -9
  46. package/dist/commands/ecommerce.js.map +1 -1
  47. package/dist/commands/email.js +56 -9
  48. package/dist/commands/email.js.map +1 -1
  49. package/dist/commands/env.d.ts +3 -3
  50. package/dist/commands/env.js +68 -4
  51. package/dist/commands/env.js.map +1 -1
  52. package/dist/commands/formation.js +3 -3
  53. package/dist/commands/identity.js +4 -3
  54. package/dist/commands/identity.js.map +1 -1
  55. package/dist/commands/images.js +87 -13
  56. package/dist/commands/images.js.map +1 -1
  57. package/dist/commands/init.d.ts +3 -0
  58. package/dist/commands/init.js +39 -36
  59. package/dist/commands/init.js.map +1 -1
  60. package/dist/commands/jobs.js +1 -0
  61. package/dist/commands/jobs.js.map +1 -1
  62. package/dist/commands/keys.js +26 -3
  63. package/dist/commands/keys.js.map +1 -1
  64. package/dist/commands/legacy-auth.d.ts +27 -0
  65. package/dist/commands/legacy-auth.js +64 -0
  66. package/dist/commands/legacy-auth.js.map +1 -0
  67. package/dist/commands/llm.js +13 -0
  68. package/dist/commands/llm.js.map +1 -1
  69. package/dist/commands/login.js +2 -0
  70. package/dist/commands/login.js.map +1 -1
  71. package/dist/commands/loops.d.ts +2 -0
  72. package/dist/commands/loops.js +260 -0
  73. package/dist/commands/loops.js.map +1 -0
  74. package/dist/commands/media.js +3 -3
  75. package/dist/commands/media.js.map +1 -1
  76. package/dist/commands/mobile.d.ts +4 -0
  77. package/dist/commands/mobile.js +382 -0
  78. package/dist/commands/mobile.js.map +1 -0
  79. package/dist/commands/payments.d.ts +2 -0
  80. package/dist/commands/payments.js +131 -0
  81. package/dist/commands/payments.js.map +1 -0
  82. package/dist/commands/people.d.ts +2 -0
  83. package/dist/commands/people.js +193 -0
  84. package/dist/commands/people.js.map +1 -0
  85. package/dist/commands/phone.js +3 -3
  86. package/dist/commands/phone.js.map +1 -1
  87. package/dist/commands/projects.d.ts +13 -0
  88. package/dist/commands/projects.js +220 -0
  89. package/dist/commands/projects.js.map +1 -0
  90. package/dist/commands/register.js +16 -8
  91. package/dist/commands/register.js.map +1 -1
  92. package/dist/commands/sandbox.d.ts +2 -0
  93. package/dist/commands/sandbox.js +245 -0
  94. package/dist/commands/sandbox.js.map +1 -0
  95. package/dist/commands/search.js +26 -12
  96. package/dist/commands/search.js.map +1 -1
  97. package/dist/commands/seo.js +16 -8
  98. package/dist/commands/seo.js.map +1 -1
  99. package/dist/commands/sessions.d.ts +6 -0
  100. package/dist/commands/sessions.js +109 -0
  101. package/dist/commands/sessions.js.map +1 -0
  102. package/dist/commands/social-data.d.ts +2 -0
  103. package/dist/commands/social-data.js +244 -0
  104. package/dist/commands/social-data.js.map +1 -0
  105. package/dist/commands/social.js +8 -6
  106. package/dist/commands/social.js.map +1 -1
  107. package/dist/commands/teams.d.ts +69 -0
  108. package/dist/commands/teams.js +503 -0
  109. package/dist/commands/teams.js.map +1 -0
  110. package/dist/commands/up.d.ts +150 -0
  111. package/dist/commands/up.js +415 -15
  112. package/dist/commands/up.js.map +1 -1
  113. package/dist/commands/usage.js +15 -4
  114. package/dist/commands/usage.js.map +1 -1
  115. package/dist/commands/use.d.ts +9 -0
  116. package/dist/commands/use.js +76 -11
  117. package/dist/commands/use.js.map +1 -1
  118. package/dist/commands/users.d.ts +8 -0
  119. package/dist/commands/users.js +28 -9
  120. package/dist/commands/users.js.map +1 -1
  121. package/dist/commands/vault.js +28 -1
  122. package/dist/commands/vault.js.map +1 -1
  123. package/dist/commands/video.js +190 -270
  124. package/dist/commands/video.js.map +1 -1
  125. package/dist/commands/voice.d.ts +2 -0
  126. package/dist/commands/voice.js +516 -0
  127. package/dist/commands/voice.js.map +1 -0
  128. package/dist/commands/wallet.d.ts +2 -0
  129. package/dist/commands/wallet.js +244 -0
  130. package/dist/commands/wallet.js.map +1 -0
  131. package/dist/commands/whoami.js +17 -1
  132. package/dist/commands/whoami.js.map +1 -1
  133. package/dist/config.d.ts +24 -1
  134. package/dist/config.js +36 -7
  135. package/dist/config.js.map +1 -1
  136. package/dist/index.d.ts +2 -1
  137. package/dist/index.js +290 -20
  138. package/dist/index.js.map +1 -1
  139. package/dist/lib/brain-select.d.ts +193 -0
  140. package/dist/lib/brain-select.js +403 -0
  141. package/dist/lib/brain-select.js.map +1 -0
  142. package/dist/lib/cli-validation.d.ts +12 -0
  143. package/dist/lib/cli-validation.js +46 -0
  144. package/dist/lib/cli-validation.js.map +1 -0
  145. package/dist/lib/deprecation.d.ts +158 -0
  146. package/dist/lib/deprecation.js +140 -0
  147. package/dist/lib/deprecation.js.map +1 -0
  148. package/dist/lib/duration.d.ts +8 -0
  149. package/dist/lib/duration.js +11 -0
  150. package/dist/lib/duration.js.map +1 -0
  151. package/dist/lib/http-response.d.ts +11 -0
  152. package/dist/lib/http-response.js +96 -0
  153. package/dist/lib/http-response.js.map +1 -0
  154. package/dist/lib/pricing.d.ts +132 -0
  155. package/dist/lib/pricing.js +148 -0
  156. package/dist/lib/pricing.js.map +1 -0
  157. package/dist/lib/stream-frames.d.ts +114 -0
  158. package/dist/lib/stream-frames.js +169 -0
  159. package/dist/lib/stream-frames.js.map +1 -0
  160. package/dist/lib/teams-catalog.d.ts +141 -0
  161. package/dist/lib/teams-catalog.js +765 -0
  162. package/dist/lib/teams-catalog.js.map +1 -0
  163. package/dist/lib/unknown-command.d.ts +57 -0
  164. package/dist/lib/unknown-command.js +93 -0
  165. package/dist/lib/unknown-command.js.map +1 -0
  166. package/dist/lib/version-check.d.ts +26 -0
  167. package/dist/lib/version-check.js +21 -0
  168. package/dist/lib/version-check.js.map +1 -0
  169. package/dist/output.d.ts +119 -10
  170. package/dist/output.js +309 -15
  171. package/dist/output.js.map +1 -1
  172. package/dist/prompt.d.ts +17 -0
  173. package/dist/prompt.js +70 -0
  174. package/dist/prompt.js.map +1 -0
  175. package/package.json +11 -6
  176. package/dist/commands/billing-client.d.ts +0 -2
  177. package/dist/commands/billing-client.js +0 -271
  178. package/dist/commands/billing-client.js.map +0 -1
  179. package/dist/commands/crm-app.d.ts +0 -2
  180. package/dist/commands/crm-app.js +0 -306
  181. package/dist/commands/crm-app.js.map +0 -1
  182. package/dist/commands/crm.d.ts +0 -2
  183. package/dist/commands/crm.js +0 -319
  184. package/dist/commands/crm.js.map +0 -1
  185. package/dist/commands/integrations.d.ts +0 -2
  186. package/dist/commands/integrations.js +0 -199
  187. package/dist/commands/integrations.js.map +0 -1
  188. package/dist/commands/link.d.ts +0 -3
  189. package/dist/commands/link.js +0 -91
  190. package/dist/commands/link.js.map +0 -1
  191. package/dist/commands/operators.d.ts +0 -11
  192. package/dist/commands/operators.js +0 -120
  193. package/dist/commands/operators.js.map +0 -1
@@ -1,28 +1,45 @@
1
- import { Command } from "commander";
1
+ import { Command, Option } from "commander";
2
2
  import { apiRequest, handleApiError } from "../client.js";
3
- import { agentOutput } from "../output.js";
3
+ import { agentOutput, creditCostHint, creditEstimateHint } from "../output.js";
4
+ import { parseDurationArg } from "../lib/duration.js";
5
+ import { REFRAME_FEE_CREDITS } from "../lib/pricing.js";
6
+ // Validate --duration (pure check in lib/duration.ts) and fail fast with a clear,
7
+ // structured message rather than forwarding a bad value that would 422 at fal.
8
+ function durationOrExit(raw) {
9
+ const parsed = parseDurationArg(raw);
10
+ if (!parsed.ok) {
11
+ console.error(JSON.stringify({
12
+ success: false,
13
+ action: "video.generate",
14
+ error: { code: "invalid_argument", message: parsed.message },
15
+ recovery_steps: [
16
+ { command: 'naive video generate "your prompt" --model <model> --duration 5', description: "Pass a numeric duration a Kling model accepts" },
17
+ { command: "naive video models", description: "List models (note: some, e.g. minimax-video, have a fixed length)" },
18
+ ],
19
+ }, null, 2));
20
+ process.exit(1);
21
+ }
22
+ return parsed.value;
23
+ }
4
24
  export const videoCmd = new Command("video")
5
- .description("Generate AI videos, extract clips, and edit videos with captions/effects")
25
+ .description("Generate AI videos and extract short-form clips")
6
26
  .addHelpText("after", `
7
27
  Subcommands:
8
- naive video generate <prompt> Generate a video from text or image
9
- naive video status <job_id> Check video generation status
10
- naive video models List available video models
11
- naive video clip <youtube_url> Extract short-form clips from a YouTube video
12
- naive video clip-status <job_id> Check clip extraction or editing status
13
- naive video clip-edit <video_url> Add captions, effects, and B-roll to a video
28
+ naive video generate <prompt> Generate a video from text or image
29
+ naive video status <job_id> Check video generation status
30
+ naive video models List available video models
31
+ naive video clip <video_url> Auto-clip a long video into short clips
32
+ naive video clip-status <id> Check clipping job status
14
33
 
15
34
  Examples:
16
35
  $ naive video generate "a rocket launching into space" --model fal-ai/kling-video/v3/pro/text-to-video
17
- $ naive video clip "https://youtube.com/watch?v=..." --title "My Clips"
18
- $ naive video clip-edit "https://example.com/video.mp4" --title "Edited Video" --magic-zooms --magic-brolls
36
+ $ naive video clip "https://example.com/video.mp4" --aspect-ratio 9:16
19
37
  $ naive video clip-status job-uuid-123
20
38
  $ naive video models
21
39
 
22
40
  Cost:
23
- - Video generation: dynamic, model/duration-dependent (use GET /v1/video/pricing to preview)
24
- - Video clipping: 4 credits per YouTube video
25
- - Video editing: 3 credits per video
41
+ - Video generation: dynamic, model/duration-dependent (run 'naive video pricing --model <id>' to preview)
42
+ - Video clipping: metered per input minute
26
43
  - Credits deducted only on successful completion
27
44
  `);
28
45
  videoCmd
@@ -30,7 +47,7 @@ videoCmd
30
47
  .description("Generate a video using AI models (async — returns job ID)")
31
48
  .requiredOption("--model <model>", "Video model ID (required)")
32
49
  .option("--input <json>", "Full input parameters as JSON (overrides other options)")
33
- .option("--duration <seconds>", "Video duration in seconds (model-dependent, e.g., 5 or 10)")
50
+ .option("--duration <seconds>", "Video length in seconds — Kling models only (e.g. 5 or 10); some models have a fixed length and reject it")
34
51
  .option("--image-url <url>", "Source image URL for image-to-video models")
35
52
  .option("--wait", "Wait for job to complete (polls until done)")
36
53
  .addHelpText("after", `
@@ -39,6 +56,12 @@ Examples:
39
56
  $ naive video generate "product showcase" --model fal-ai/kling-video/v3/pro/text-to-video --duration 10 --wait
40
57
  $ naive video generate --model fal-ai/kling-video/v3/pro/image-to-video --image-url https://example.com/img.jpg --input '{"prompt":"zoom in slowly"}'
41
58
 
59
+ Duration:
60
+ --duration is accepted by Kling models (e.g. fal-ai/kling-video/v3/pro/*), which
61
+ is why the example above pairs it with one. Fixed-length models such as
62
+ fal-ai/minimax-video/video-01-live reject --duration — pass a numeric value only
63
+ with a model that supports it, or omit it to use the model's default length.
64
+
42
65
  What this does:
43
66
  1. Submits a video generation request
44
67
  2. Returns a job ID (async processing, typically 30-120s)
@@ -60,15 +83,21 @@ Available models (use 'naive video models' for full list):
60
83
 
61
84
  Parameters (via --input JSON):
62
85
  prompt Text description of the video
63
- duration Duration in seconds: "5" or "10" (model-dependent)
86
+ duration Length in seconds (Kling models only): "5" or "10". Fixed-length
87
+ models (e.g. minimax-video) reject it — see the Duration note above.
64
88
  image_url Source image for image-to-video
65
89
  aspect_ratio "16:9", "9:16", "1:1" (model-dependent)
66
90
  negative_prompt Text describing what to avoid
67
91
  cfg_scale Guidance scale 0-1 (higher = more literal, default 0.5)
68
92
  generate_audio Enable native audio generation (boolean, default true for Kling v3)
69
93
 
70
- Cost: dynamic — based on model pricing ($0.50/credit)
71
- Use 'GET /v1/video/pricing?model=<id>&duration=<s>' to preview cost.
94
+ Reading the result (after completion, via 'naive video status <id>'):
95
+ Submit returns the job under 'id' (job_id is a deprecated alias).
96
+ The output URL is at result.media[0].url — a flat, model-independent path.
97
+ (result.result.video.url is the raw provider payload; media[] is preferred.)
98
+
99
+ Cost: dynamic — based on model pricing ($0.05/credit)
100
+ Run 'naive video pricing --model <id> --duration <s>' to preview cost before generating.
72
101
  Credits deducted only on successful completion. Failed jobs are free.
73
102
  `)
74
103
  .action(async (prompt, opts) => {
@@ -79,14 +108,14 @@ Cost: dynamic — based on model pricing ($0.50/credit)
79
108
  else if (prompt) {
80
109
  input = { prompt };
81
110
  if (opts.duration)
82
- input.duration = opts.duration;
111
+ input.duration = durationOrExit(opts.duration);
83
112
  if (opts.imageUrl)
84
113
  input.image_url = opts.imageUrl;
85
114
  }
86
115
  else if (opts.imageUrl) {
87
116
  input = { image_url: opts.imageUrl };
88
117
  if (opts.duration)
89
- input.duration = opts.duration;
118
+ input.duration = durationOrExit(opts.duration);
90
119
  }
91
120
  else {
92
121
  console.error(JSON.stringify({
@@ -111,23 +140,28 @@ Cost: dynamic — based on model pricing ($0.50/credit)
111
140
  if (opts.wait) {
112
141
  agentOutput({
113
142
  action: "video.generate.submitted",
114
- result: { job_id: data.job_id, status: "polling", model: opts.model },
143
+ // Surface the estimate at submit so spend is visible before the wait (N6).
144
+ result: { job_id: data.job_id, status: "polling", model: opts.model, estimated_credits: data.estimated_credits ?? null },
115
145
  next_steps: [],
116
- hints: ["Waiting for video generation to complete (typically 30-120s)..."],
146
+ hints: [
147
+ "Waiting for video generation to complete (typically 30-120s)...",
148
+ ...(creditEstimateHint(resp.data) ? [creditEstimateHint(resp.data)] : []),
149
+ ],
117
150
  });
118
151
  await pollJob(data.job_id);
119
152
  }
120
153
  else {
121
154
  agentOutput({
122
155
  action: "video.generate.submitted",
123
- result: { job_id: data.job_id, status: "queued", model: opts.model },
156
+ result: { job_id: data.job_id, status: "queued", model: opts.model, estimated_credits: data.estimated_credits ?? null },
124
157
  next_steps: [
125
158
  { command: `naive video status ${data.job_id}`, description: "Check generation progress" },
126
159
  { command: `naive jobs get ${data.job_id}`, description: "Get full job details" },
127
160
  ],
128
161
  hints: [
129
162
  `Video generation submitted (model: ${opts.model})`,
130
- "Credits deducted only on successful completion",
163
+ // Echoes the API's pre-flight estimate (same source that bills at completion) — N6.
164
+ ...(creditEstimateHint(resp.data) ? [creditEstimateHint(resp.data)] : ["Credits deducted only on successful completion"]),
131
165
  "Typical time: 30-120 seconds depending on model and duration",
132
166
  ],
133
167
  related_commands: ["naive video status", "naive jobs get", "naive jobs"],
@@ -146,6 +180,83 @@ Cost: dynamic — based on model pricing ($0.50/credit)
146
180
  });
147
181
  }
148
182
  });
183
+ videoCmd
184
+ .command("clip <video_url>")
185
+ .description("Auto-clip a long video into short clips (async — returns job ID)")
186
+ // Strict enum via commander's Option.choices — rejects anything outside
187
+ // the two supported aspect ratios at parse time, so the API never sees a
188
+ // typo'd value. Default "9:16" matches the REST POST handler's default
189
+ // (vertical-first social baseline). "16:9" preserves the source aspect.
190
+ .addOption(new Option("--aspect <ratio>", "Output aspect ratio for generated clips")
191
+ .choices(["9:16", "16:9"])
192
+ .default("9:16"))
193
+ .addHelpText("after", `
194
+ Examples:
195
+ $ naive video clip https://example.com/podcast.mp4
196
+ $ naive video clip https://example.com/podcast.mp4 --aspect 16:9
197
+
198
+ What this does:
199
+ 1. Submits a long video for AI auto-clipping
200
+ 2. Returns a job ID (async processing)
201
+ 3. Check progress with 'naive video clip-status <job_id>'
202
+
203
+ Input:
204
+ A public https URL to a video no longer than 90 minutes.
205
+
206
+ Options:
207
+ --aspect <ratio> "9:16" (default, vertical via SAM-3 smart-crop) or "16:9" (preserve source)
208
+
209
+ Cost: per input minute — credits deducted only on successful completion.
210
+ 9:16 reframe adds a flat ${REFRAME_FEE_CREDITS}-credit face-tracking fee per job.
211
+ `)
212
+ .action(async (videoUrl, opts) => {
213
+ const resp = await apiRequest("POST", "/v1/video/clip", {
214
+ video_url: videoUrl,
215
+ aspect_ratio: opts.aspect ?? "9:16",
216
+ });
217
+ handleApiError("video.clip", resp);
218
+ const data = resp.data;
219
+ agentOutput({
220
+ action: "video.clip.submitted",
221
+ result: { job_id: data.job_id, status: data.status },
222
+ next_steps: [
223
+ { command: `naive video clip-status ${data.job_id}`, description: "Check clipping progress" },
224
+ { command: `naive jobs get ${data.job_id}`, description: "Get full job details" },
225
+ ],
226
+ hints: [
227
+ "Clip job submitted",
228
+ "Input must be a public https URL to a video 90 minutes or shorter",
229
+ "Credits deducted only on successful completion",
230
+ ],
231
+ related_commands: ["naive video clip-status", "naive jobs"],
232
+ });
233
+ });
234
+ videoCmd
235
+ .command("clip-status <job_id>")
236
+ .description("Check the status of a video clipping job")
237
+ .addHelpText("after", `
238
+ Examples:
239
+ $ naive video clip-status 550e8400-e29b-41d4-a716-446655440000
240
+
241
+ When completed, the result includes the generated clip URLs.
242
+ `)
243
+ .action(async (jobId) => {
244
+ const resp = await apiRequest("GET", `/v1/video/clip/${jobId}`);
245
+ handleApiError("video.clip-status", resp);
246
+ const data = resp.data;
247
+ agentOutput({
248
+ action: "video.clip-status",
249
+ result: resp.data,
250
+ next_steps: [
251
+ ...(data.status === "queued" || data.status === "processing"
252
+ ? [{ command: `naive video clip-status ${jobId}`, description: "Check again in 20-30 seconds" }]
253
+ : []),
254
+ { command: "naive video clip <video_url>", description: "Clip another video" },
255
+ ],
256
+ hints: [`Job status: ${data.status}`],
257
+ related_commands: ["naive video clip", "naive jobs"],
258
+ });
259
+ });
149
260
  videoCmd
150
261
  .command("status <job_id>")
151
262
  .description("Check the status of a video generation job")
@@ -166,12 +277,15 @@ When completed, the result includes a video URL.
166
277
  const resp = await apiRequest("GET", `/v1/video/${jobId}`);
167
278
  handleApiError("video.status", resp);
168
279
  const data = resp.data;
280
+ // N12: prefer the flat, model-independent media[] path; fall back to the raw
281
+ // provider nesting for older API responses that predate media[].
282
+ const videoUrl = data.media?.[0]?.url ?? data.result?.video?.url;
169
283
  agentOutput({
170
284
  action: "video.status",
171
285
  result: resp.data,
172
286
  next_steps: [
173
- ...(data.status === "completed" && data.result?.video
174
- ? [{ command: `# Download: curl -o video.mp4 "${data.result.video.url}"`, description: "Download the generated video" }]
287
+ ...(data.status === "completed" && videoUrl
288
+ ? [{ command: `# Download: curl -o video.mp4 "${videoUrl}"`, description: "Download the generated video (result.media[0].url)" }]
175
289
  : []),
176
290
  ...(data.status === "queued" || data.status === "processing"
177
291
  ? [{ command: `naive video status ${jobId}`, description: "Check again in 10-30 seconds" }]
@@ -181,6 +295,10 @@ When completed, the result includes a video URL.
181
295
  hints: [
182
296
  `Job status: ${data.status}`,
183
297
  ...(data.status === "processing" ? ["Video generation typically takes 30-120 seconds"] : []),
298
+ // Post-hoc: on completion the job carries the ACTUAL credits_used — same
299
+ // single source as the pre-flight estimate, so before/at/after agree (N6).
300
+ ...(creditCostHint(resp.data) ? [creditCostHint(resp.data)] : []),
301
+ ...(data.status === "failed" ? [`Failed (no credits deducted). Reason: ${data.error || "unknown"}`] : []),
184
302
  ],
185
303
  related_commands: ["naive video generate", "naive jobs"],
186
304
  });
@@ -212,282 +330,84 @@ What this does:
212
330
  hints: [
213
331
  "Use the model 'id' field as the --model parameter in 'naive video generate'",
214
332
  "Text-to-video models accept a text prompt; image-to-video models require --image-url",
333
+ "Each model includes credits_estimate (~5s clip; null when pricing is unavailable)",
334
+ "For an exact quote at a given length: naive video pricing --model <id> --duration <s>",
215
335
  ],
216
- related_commands: ["naive video generate", "naive video status"],
336
+ related_commands: ["naive video generate", "naive video pricing", "naive video status"],
217
337
  });
218
338
  });
219
- // ── Video Clipping ──────────────────────────────────────────────────────
220
339
  videoCmd
221
- .command("clip <youtube_url>")
222
- .description("Extract short-form clips from a YouTube video (async — 5-10 min)")
223
- .requiredOption("--title <title>", "Project title for the clipping job")
224
- .option("--language <lang>", "Video language code (default: en)")
225
- .option("--template <name>", "Caption template name")
226
- .option("--min-length <seconds>", "Minimum clip length in seconds (15-300, default: 15)")
227
- .option("--max-length <seconds>", "Maximum clip length in seconds (15-300, default: 60)")
228
- .option("--wait", "Wait for job to complete (polls until done)")
340
+ .command("pricing")
341
+ .description("Preview the estimated credit cost of a video model before generating")
342
+ .requiredOption("--model <model>", "Video model ID (get IDs from 'naive video models')")
343
+ .option("--duration <seconds>", "Clip length in seconds to price (default: 5)", "5")
229
344
  .addHelpText("after", `
230
345
  Examples:
231
- $ naive video clip "https://youtube.com/watch?v=abc123" --title "Podcast Highlights"
232
- $ naive video clip "https://youtu.be/abc123" --title "Best Moments" --min-length 30 --max-length 90 --wait
346
+ $ naive video pricing --model fal-ai/kling-video/v3/pro/text-to-video
347
+ $ naive video pricing --model fal-ai/kling-video/v3/pro/text-to-video --duration 10
233
348
 
234
349
  What this does:
235
- 1. Submits a YouTube video for AI clip extraction
236
- 2. Returns a job ID (async processing, typically 5-10 minutes)
237
- 3. When complete, returns multiple clips ranked by virality score
238
- 4. Use --wait to block until done, or check with 'naive video clip-status'
239
-
240
- Virality scores (0-100):
241
- 80+ Strong — prioritize for distribution
242
- 60-79 Decent — usable content
243
- <60 Weak — skip unless no alternatives
350
+ Returns the ESTIMATED credit cost for one generation, derived from the model's
351
+ live fal.ai price — the same source that bills the job on completion, so the
352
+ estimate tracks the real charge. Nothing is spent by this command.
244
353
 
245
- Cost: 4 credits per clipping job. Credits deducted only on completion.
354
+ Cost: free (this is a price preview; generation itself is charged on completion)
246
355
  `)
247
- .action(async (youtubeUrl, opts) => {
248
- const body = {
249
- youtube_url: youtubeUrl,
250
- title: opts.title,
251
- };
252
- if (opts.language)
253
- body.language = opts.language;
254
- if (opts.template)
255
- body.template = opts.template;
256
- if (opts.minLength)
257
- body.min_clip_length = parseInt(opts.minLength);
258
- if (opts.maxLength)
259
- body.max_clip_length = parseInt(opts.maxLength);
260
- const resp = await apiRequest("POST", "/v1/video/clips", body);
261
- handleApiError("video.clip", resp);
262
- const data = resp.data;
263
- if (opts.wait) {
264
- agentOutput({
265
- action: "video.clip.submitted",
266
- result: { job_id: data.job_id, status: "polling" },
267
- next_steps: [],
268
- hints: ["Waiting for clip extraction to complete (typically 5-10 minutes)..."],
269
- });
270
- await pollClipJob(data.job_id);
271
- }
272
- else {
273
- agentOutput({
274
- action: "video.clip.submitted",
275
- result: { job_id: data.job_id, status: "queued" },
276
- next_steps: [
277
- { command: `naive video clip-status ${data.job_id}`, description: "Check clipping progress" },
278
- { command: `naive jobs get ${data.job_id}`, description: "Get full job details" },
279
- ],
280
- hints: [
281
- "Clip extraction submitted — typically takes 5-10 minutes",
282
- "Credits deducted only on successful completion (4 credits)",
283
- "When complete, clips are ranked by AI virality score (0-100)",
356
+ .action(async (opts) => {
357
+ const duration = parseInt(opts.duration ?? "5", 10);
358
+ if (!Number.isFinite(duration) || duration <= 0) {
359
+ console.error(JSON.stringify({
360
+ success: false,
361
+ action: "video.pricing",
362
+ error: { code: "invalid_argument", message: "--duration must be a positive number of seconds" },
363
+ recovery_steps: [
364
+ { command: `naive video pricing --model ${opts.model} --duration 5`, description: "Price a 5-second clip" },
284
365
  ],
285
- related_commands: ["naive video clip-status", "naive jobs get", "naive social post"],
286
- });
366
+ }, null, 2));
367
+ process.exit(1);
287
368
  }
288
- });
289
- videoCmd
290
- .command("clip-status <job_id>")
291
- .description("Check the status of a clip extraction or video editing job")
292
- .addHelpText("after", `
293
- Examples:
294
- $ naive video clip-status 550e8400-e29b-41d4-a716-446655440000
295
-
296
- What this does:
297
- Returns the current status of a clipping/editing job:
298
- - queued: submitted, processing typically takes 5-10 minutes
299
- - completed: clips or edited video ready
300
- - failed: processing failed (no credits deducted)
301
-
302
- When completed, clip jobs include multiple clips with virality scores.
303
- Edit jobs include the download URL for the processed video.
304
- `)
305
- .action(async (jobId) => {
306
- const resp = await apiRequest("GET", `/v1/video/clips/${jobId}`);
307
- handleApiError("video.clip-status", resp);
369
+ const params = new URLSearchParams({ model: opts.model, duration: String(duration) });
370
+ const resp = await apiRequest("GET", `/v1/video/pricing?${params}`);
371
+ handleApiError("video.pricing", resp);
308
372
  const data = resp.data;
309
- const clips = data.result?.clips ?? [];
310
- const topClips = clips.filter((c) => (c.virality_score?.total ?? 0) >= 75);
311
373
  agentOutput({
312
- action: "video.clip-status",
374
+ action: "video.pricing",
313
375
  result: resp.data,
314
376
  next_steps: [
315
- ...(data.status === "completed" && clips.length > 0
316
- ? [
317
- ...topClips.slice(0, 3).map((c, i) => ({
318
- command: `naive social post "${c.title ?? `Clip ${i + 1}`}" --platforms youtube --youtube-type SHORT --media-url "${c.direct_url}" --publish`,
319
- description: `Post clip ${i + 1} (score: ${c.virality_score?.total ?? "N/A"}) as YouTube Short`,
320
- })),
321
- { command: `# Download top clip: curl -o clip.mp4 "${topClips[0]?.direct_url ?? clips[0]?.direct_url}"`, description: "Download the top-ranked clip" },
322
- ]
323
- : []),
324
- ...(data.status === "completed" && data.result?.video_url
325
- ? [{ command: `# Download: curl -o edited.mp4 "${data.result.video_url}"`, description: "Download the edited video" }]
326
- : []),
327
- ...(data.status === "queued"
328
- ? [{ command: `naive video clip-status ${jobId}`, description: "Check again in 30-60 seconds" }]
329
- : []),
330
- { command: `naive video clip "https://youtube.com/watch?v=..." --title "New Clips"`, description: "Start another clipping job" },
377
+ { command: `naive video generate "your prompt" --model ${opts.model}${duration ? ` --duration ${duration}` : ""}`, description: "Generate a video with this model" },
378
+ { command: "naive video models", description: "Compare estimates across all models" },
331
379
  ],
332
380
  hints: [
333
- `Job status: ${data.status}`,
334
- ...(data.status === "queued" ? ["Clip extraction typically takes 5-10 minutes"] : []),
335
- ...(data.status === "completed" && clips.length > 0
336
- ? [
337
- `${clips.length} clips extracted`,
338
- "Scores 80+ are strong candidates for social media distribution",
339
- "Use 'naive social post' to publish top clips directly",
340
- ]
341
- : []),
381
+ typeof data.estimated_credits === "number"
382
+ ? `Estimated cost: ${data.estimated_credits} credits for a ${data.duration_seconds ?? duration}s clip (charged only on successful completion)`
383
+ : "Pricing unavailable for this model",
384
+ "This is an estimate; the exact charge is metered from the delivered video's duration",
342
385
  ],
343
- related_commands: ["naive video clip", "naive video clip-edit", "naive social post", "naive jobs"],
386
+ related_commands: ["naive video generate", "naive video models"],
344
387
  });
345
388
  });
346
- videoCmd
347
- .command("clip-edit <video_url>")
348
- .description("Add captions, effects, zooms, and B-roll to a video (async — 5-10 min)")
349
- .requiredOption("--title <title>", "Project title for the editing job")
350
- .option("--language <lang>", "Video language code (default: en)")
351
- .option("--template <name>", "Caption template name")
352
- .option("--magic-zooms", "Enable AI-powered zooms")
353
- .option("--magic-brolls", "Enable AI-generated B-roll")
354
- .option("--brolls-percentage <n>", "B-roll percentage (0-100)")
355
- .option("--wait", "Wait for job to complete (polls until done)")
356
- .addHelpText("after", `
357
- Examples:
358
- $ naive video clip-edit "https://example.com/video.mp4" --title "Edited Podcast"
359
- $ naive video clip-edit "https://example.com/video.mp4" --title "Enhanced" --magic-zooms --magic-brolls --wait
360
-
361
- What this does:
362
- 1. Submits a video for AI-powered editing (captions, zooms, B-roll)
363
- 2. Returns a job ID (async processing, typically 5-10 minutes)
364
- 3. When complete, returns a download URL for the edited video
365
-
366
- Input requirements:
367
- - Public URL to an MP4 or MOV file
368
- - Maximum file size: 2GB, maximum duration: 2 hours
369
-
370
- Cost: 3 credits per edit. Credits deducted only on completion.
371
- `)
372
- .action(async (videoUrl, opts) => {
373
- const body = {
374
- video_url: videoUrl,
375
- title: opts.title,
376
- };
377
- if (opts.language)
378
- body.language = opts.language;
379
- if (opts.template)
380
- body.template = opts.template;
381
- if (opts.magicZooms)
382
- body.magic_zooms = true;
383
- if (opts.magicBrolls)
384
- body.magic_brolls = true;
385
- if (opts.brollsPercentage)
386
- body.magic_brolls_percentage = parseInt(opts.brollsPercentage);
387
- const resp = await apiRequest("POST", "/v1/video/clips/edit", body);
388
- handleApiError("video.clip-edit", resp);
389
- const data = resp.data;
390
- if (opts.wait) {
391
- agentOutput({
392
- action: "video.clip-edit.submitted",
393
- result: { job_id: data.job_id, status: "polling" },
394
- next_steps: [],
395
- hints: ["Waiting for video editing to complete (typically 5-10 minutes)..."],
396
- });
397
- await pollClipJob(data.job_id);
398
- }
399
- else {
400
- agentOutput({
401
- action: "video.clip-edit.submitted",
402
- result: { job_id: data.job_id, status: "queued" },
403
- next_steps: [
404
- { command: `naive video clip-status ${data.job_id}`, description: "Check editing progress" },
405
- { command: `naive jobs get ${data.job_id}`, description: "Get full job details" },
406
- ],
407
- hints: [
408
- "Video edit submitted — typically takes 5-10 minutes",
409
- "Credits deducted only on successful completion (3 credits)",
410
- ],
411
- related_commands: ["naive video clip-status", "naive jobs get"],
412
- });
413
- }
414
- });
415
- async function pollClipJob(jobId) {
416
- while (true) {
417
- await new Promise((r) => setTimeout(r, 15000));
418
- const resp = await apiRequest("GET", `/v1/jobs/${jobId}`);
419
- const data = resp.data;
420
- if (data.status === "completed" || data.status === "failed") {
421
- const clips = data.result?.clips ?? [];
422
- const topClips = clips.filter((c) => (c.virality_score?.total ?? 0) >= 75);
423
- agentOutput({
424
- action: data.type === "video_clip_edit" ? "video.clip-edit.completed" : "video.clip.completed",
425
- result: resp.data,
426
- next_steps: [
427
- ...(data.status === "completed" && clips.length > 0
428
- ? topClips.slice(0, 2).map((c, i) => ({
429
- command: `naive social post "Your caption" --platforms youtube --youtube-type SHORT --media-url "${c.direct_url}" --publish`,
430
- description: `Post top clip ${i + 1} (score: ${c.virality_score?.total}) as YouTube Short`,
431
- }))
432
- : []),
433
- ...(data.status === "completed" && data.result?.video_url
434
- ? [{ command: `# Download: curl -o edited.mp4 "${data.result.video_url}"`, description: "Download edited video" }]
435
- : []),
436
- { command: `naive video clip "https://youtube.com/watch?v=..." --title "New Clips"`, description: "Start another job" },
437
- ],
438
- hints: [
439
- data.status === "completed"
440
- ? clips.length > 0
441
- ? `${data.result?.clip_count ?? clips.length} clips ready (top score: ${data.result?.top_virality_score ?? "N/A"})`
442
- : "Video editing complete — download URL available"
443
- : "Job failed — no credits deducted",
444
- ...(data.status === "completed" && clips.length > 0
445
- ? [
446
- "Tip: Use 'naive social post' to publish top clips to YouTube, TikTok, Instagram",
447
- "Tip: Clips are auto-ingested to Media Assets. Use 'naive media list --source video_clipping' to find them.",
448
- ]
449
- : []),
450
- ...(data.status === "completed" && clips.length === 0
451
- ? ["Tip: Edited video is auto-ingested to Media Assets. Use 'naive media list --source video_clipping' to find it."]
452
- : []),
453
- ],
454
- related_commands: ["naive video clip", "naive video clip-edit", "naive social post", "naive jobs"],
455
- });
456
- return;
457
- }
458
- process.stderr.write(` Status: ${data.status} (clipping typically takes 5-10 minutes)\n`);
459
- }
460
- }
389
+ // Polls a job to completion for `video generate --wait`. Local helper mirroring
390
+ // images.ts/search.ts; the prior copy lived in main's Submagic clip block that
391
+ // was removed during the main->branch merge.
461
392
  async function pollJob(jobId) {
462
393
  while (true) {
463
- await new Promise((r) => setTimeout(r, 5000));
394
+ await new Promise((r) => setTimeout(r, 2000));
464
395
  const resp = await apiRequest("GET", `/v1/jobs/${jobId}`);
396
+ handleApiError("video.generate.poll", resp);
465
397
  const data = resp.data;
466
398
  if (data.status === "completed" || data.status === "failed") {
467
399
  agentOutput({
468
400
  action: "video.generate.completed",
469
401
  result: resp.data,
470
- next_steps: [
471
- ...(data.status === "completed" && data.result?.video
472
- ? [
473
- { command: `# Download: curl -o video.mp4 "${data.result.video.url}"`, description: "Download the generated video" },
474
- { command: `naive social post "Your caption here" --platforms youtube --youtube-type SHORT --media-url "${data.result.video.url}" --publish`, description: "Post as YouTube Short" },
475
- { command: `naive social post "Your caption here" --platforms tiktok --media-url "${data.result.video.url}" --publish`, description: "Post to TikTok" },
476
- { command: `naive social post "Your caption here" --platforms instagram --media-url "${data.result.video.url}" --publish`, description: "Post as Instagram Reel" },
477
- ]
478
- : []),
479
- { command: 'naive video generate "another prompt" --model <model>', description: "Generate another video" },
480
- ],
402
+ next_steps: [{ command: `naive jobs get ${jobId}`, description: "Get full job details" }],
481
403
  hints: [
482
404
  data.status === "completed"
483
- ? "Video generation complete — download URL available. Auto-ingested to Media Assets."
484
- : "Video generation failed — no credits deducted",
485
- ...(data.status === "completed" ? [
486
- "Tip: Use 'naive social post' to publish directly to YouTube, TikTok, Instagram, etc.",
487
- "Tip: Use 'naive media list --source video_generation' to find this video in your asset library.",
488
- ] : []),
405
+ ? "Video generation complete"
406
+ : `Generation failed — no credits deducted. Reason: ${data.error || "unknown"}`,
407
+ // Actual charge, derived from the completed job's credits_used (N6/N7).
408
+ ...(creditCostHint(resp.data) ? [creditCostHint(resp.data)] : []),
489
409
  ],
490
- related_commands: ["naive video generate", "naive video models", "naive social post", "naive jobs"],
410
+ related_commands: ["naive video generate", "naive video status", "naive jobs"],
491
411
  });
492
412
  return;
493
413
  }