@vibeframe/cli 0.27.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 (420) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-lint.log +21 -0
  3. package/.turbo/turbo-test.log +689 -0
  4. package/dist/agent/adapters/claude.d.ts +15 -0
  5. package/dist/agent/adapters/claude.d.ts.map +1 -0
  6. package/dist/agent/adapters/claude.js +119 -0
  7. package/dist/agent/adapters/claude.js.map +1 -0
  8. package/dist/agent/adapters/gemini.d.ts +15 -0
  9. package/dist/agent/adapters/gemini.d.ts.map +1 -0
  10. package/dist/agent/adapters/gemini.js +132 -0
  11. package/dist/agent/adapters/gemini.js.map +1 -0
  12. package/dist/agent/adapters/index.d.ts +27 -0
  13. package/dist/agent/adapters/index.d.ts.map +1 -0
  14. package/dist/agent/adapters/index.js +38 -0
  15. package/dist/agent/adapters/index.js.map +1 -0
  16. package/dist/agent/adapters/ollama.d.ts +20 -0
  17. package/dist/agent/adapters/ollama.d.ts.map +1 -0
  18. package/dist/agent/adapters/ollama.js +186 -0
  19. package/dist/agent/adapters/ollama.js.map +1 -0
  20. package/dist/agent/adapters/openai.d.ts +15 -0
  21. package/dist/agent/adapters/openai.d.ts.map +1 -0
  22. package/dist/agent/adapters/openai.js +92 -0
  23. package/dist/agent/adapters/openai.js.map +1 -0
  24. package/dist/agent/adapters/xai.d.ts +15 -0
  25. package/dist/agent/adapters/xai.d.ts.map +1 -0
  26. package/dist/agent/adapters/xai.js +95 -0
  27. package/dist/agent/adapters/xai.js.map +1 -0
  28. package/dist/agent/index.d.ts +69 -0
  29. package/dist/agent/index.d.ts.map +1 -0
  30. package/dist/agent/index.js +180 -0
  31. package/dist/agent/index.js.map +1 -0
  32. package/dist/agent/memory/index.d.ts +70 -0
  33. package/dist/agent/memory/index.d.ts.map +1 -0
  34. package/dist/agent/memory/index.js +132 -0
  35. package/dist/agent/memory/index.js.map +1 -0
  36. package/dist/agent/prompts/system.d.ts +6 -0
  37. package/dist/agent/prompts/system.d.ts.map +1 -0
  38. package/dist/agent/prompts/system.js +103 -0
  39. package/dist/agent/prompts/system.js.map +1 -0
  40. package/dist/agent/tools/ai-editing.d.ts +15 -0
  41. package/dist/agent/tools/ai-editing.d.ts.map +1 -0
  42. package/dist/agent/tools/ai-editing.js +763 -0
  43. package/dist/agent/tools/ai-editing.js.map +1 -0
  44. package/dist/agent/tools/ai-generation.d.ts +13 -0
  45. package/dist/agent/tools/ai-generation.d.ts.map +1 -0
  46. package/dist/agent/tools/ai-generation.js +973 -0
  47. package/dist/agent/tools/ai-generation.js.map +1 -0
  48. package/dist/agent/tools/ai-pipeline.d.ts +14 -0
  49. package/dist/agent/tools/ai-pipeline.d.ts.map +1 -0
  50. package/dist/agent/tools/ai-pipeline.js +961 -0
  51. package/dist/agent/tools/ai-pipeline.js.map +1 -0
  52. package/dist/agent/tools/ai.d.ts +13 -0
  53. package/dist/agent/tools/ai.d.ts.map +1 -0
  54. package/dist/agent/tools/ai.js +19 -0
  55. package/dist/agent/tools/ai.js.map +1 -0
  56. package/dist/agent/tools/batch.d.ts +6 -0
  57. package/dist/agent/tools/batch.d.ts.map +1 -0
  58. package/dist/agent/tools/batch.js +383 -0
  59. package/dist/agent/tools/batch.js.map +1 -0
  60. package/dist/agent/tools/e2e.test.d.ts +26 -0
  61. package/dist/agent/tools/e2e.test.d.ts.map +1 -0
  62. package/dist/agent/tools/e2e.test.js +397 -0
  63. package/dist/agent/tools/e2e.test.js.map +1 -0
  64. package/dist/agent/tools/export.d.ts +6 -0
  65. package/dist/agent/tools/export.d.ts.map +1 -0
  66. package/dist/agent/tools/export.js +171 -0
  67. package/dist/agent/tools/export.js.map +1 -0
  68. package/dist/agent/tools/filesystem.d.ts +6 -0
  69. package/dist/agent/tools/filesystem.d.ts.map +1 -0
  70. package/dist/agent/tools/filesystem.js +212 -0
  71. package/dist/agent/tools/filesystem.js.map +1 -0
  72. package/dist/agent/tools/index.d.ts +65 -0
  73. package/dist/agent/tools/index.d.ts.map +1 -0
  74. package/dist/agent/tools/index.js +120 -0
  75. package/dist/agent/tools/index.js.map +1 -0
  76. package/dist/agent/tools/integration.test.d.ts +11 -0
  77. package/dist/agent/tools/integration.test.d.ts.map +1 -0
  78. package/dist/agent/tools/integration.test.js +659 -0
  79. package/dist/agent/tools/integration.test.js.map +1 -0
  80. package/dist/agent/tools/media.d.ts +6 -0
  81. package/dist/agent/tools/media.d.ts.map +1 -0
  82. package/dist/agent/tools/media.js +616 -0
  83. package/dist/agent/tools/media.js.map +1 -0
  84. package/dist/agent/tools/project.d.ts +6 -0
  85. package/dist/agent/tools/project.d.ts.map +1 -0
  86. package/dist/agent/tools/project.js +284 -0
  87. package/dist/agent/tools/project.js.map +1 -0
  88. package/dist/agent/tools/timeline.d.ts +6 -0
  89. package/dist/agent/tools/timeline.d.ts.map +1 -0
  90. package/dist/agent/tools/timeline.js +873 -0
  91. package/dist/agent/tools/timeline.js.map +1 -0
  92. package/dist/agent/types.d.ts +59 -0
  93. package/dist/agent/types.d.ts.map +1 -0
  94. package/dist/agent/types.js +5 -0
  95. package/dist/agent/types.js.map +1 -0
  96. package/dist/commands/agent.d.ts +21 -0
  97. package/dist/commands/agent.d.ts.map +1 -0
  98. package/dist/commands/agent.js +290 -0
  99. package/dist/commands/agent.js.map +1 -0
  100. package/dist/commands/ai-analyze.d.ts +106 -0
  101. package/dist/commands/ai-analyze.d.ts.map +1 -0
  102. package/dist/commands/ai-analyze.js +327 -0
  103. package/dist/commands/ai-analyze.js.map +1 -0
  104. package/dist/commands/ai-animated-caption.d.ts +64 -0
  105. package/dist/commands/ai-animated-caption.d.ts.map +1 -0
  106. package/dist/commands/ai-animated-caption.js +272 -0
  107. package/dist/commands/ai-animated-caption.js.map +1 -0
  108. package/dist/commands/ai-audio.d.ts +20 -0
  109. package/dist/commands/ai-audio.d.ts.map +1 -0
  110. package/dist/commands/ai-audio.js +808 -0
  111. package/dist/commands/ai-audio.js.map +1 -0
  112. package/dist/commands/ai-broll.d.ts +15 -0
  113. package/dist/commands/ai-broll.d.ts.map +1 -0
  114. package/dist/commands/ai-broll.js +406 -0
  115. package/dist/commands/ai-broll.js.map +1 -0
  116. package/dist/commands/ai-edit-cli.d.ts +14 -0
  117. package/dist/commands/ai-edit-cli.d.ts.map +1 -0
  118. package/dist/commands/ai-edit-cli.js +579 -0
  119. package/dist/commands/ai-edit-cli.js.map +1 -0
  120. package/dist/commands/ai-edit.d.ts +398 -0
  121. package/dist/commands/ai-edit.d.ts.map +1 -0
  122. package/dist/commands/ai-edit.js +1019 -0
  123. package/dist/commands/ai-edit.js.map +1 -0
  124. package/dist/commands/ai-fill-gaps.d.ts +14 -0
  125. package/dist/commands/ai-fill-gaps.d.ts.map +1 -0
  126. package/dist/commands/ai-fill-gaps.js +451 -0
  127. package/dist/commands/ai-fill-gaps.js.map +1 -0
  128. package/dist/commands/ai-helpers.d.ts +20 -0
  129. package/dist/commands/ai-helpers.d.ts.map +1 -0
  130. package/dist/commands/ai-helpers.js +59 -0
  131. package/dist/commands/ai-helpers.js.map +1 -0
  132. package/dist/commands/ai-highlights.d.ts +127 -0
  133. package/dist/commands/ai-highlights.d.ts.map +1 -0
  134. package/dist/commands/ai-highlights.js +1026 -0
  135. package/dist/commands/ai-highlights.js.map +1 -0
  136. package/dist/commands/ai-image.d.ts +34 -0
  137. package/dist/commands/ai-image.d.ts.map +1 -0
  138. package/dist/commands/ai-image.js +653 -0
  139. package/dist/commands/ai-image.js.map +1 -0
  140. package/dist/commands/ai-motion.d.ts +50 -0
  141. package/dist/commands/ai-motion.d.ts.map +1 -0
  142. package/dist/commands/ai-motion.js +271 -0
  143. package/dist/commands/ai-motion.js.map +1 -0
  144. package/dist/commands/ai-narrate.d.ts +66 -0
  145. package/dist/commands/ai-narrate.d.ts.map +1 -0
  146. package/dist/commands/ai-narrate.js +329 -0
  147. package/dist/commands/ai-narrate.js.map +1 -0
  148. package/dist/commands/ai-review.d.ts +57 -0
  149. package/dist/commands/ai-review.d.ts.map +1 -0
  150. package/dist/commands/ai-review.js +251 -0
  151. package/dist/commands/ai-review.js.map +1 -0
  152. package/dist/commands/ai-script-pipeline-cli.d.ts +9 -0
  153. package/dist/commands/ai-script-pipeline-cli.d.ts.map +1 -0
  154. package/dist/commands/ai-script-pipeline-cli.js +1494 -0
  155. package/dist/commands/ai-script-pipeline-cli.js.map +1 -0
  156. package/dist/commands/ai-script-pipeline.d.ts +259 -0
  157. package/dist/commands/ai-script-pipeline.d.ts.map +1 -0
  158. package/dist/commands/ai-script-pipeline.js +1027 -0
  159. package/dist/commands/ai-script-pipeline.js.map +1 -0
  160. package/dist/commands/ai-suggest-edit.d.ts +14 -0
  161. package/dist/commands/ai-suggest-edit.d.ts.map +1 -0
  162. package/dist/commands/ai-suggest-edit.js +220 -0
  163. package/dist/commands/ai-suggest-edit.js.map +1 -0
  164. package/dist/commands/ai-video-fx.d.ts +14 -0
  165. package/dist/commands/ai-video-fx.d.ts.map +1 -0
  166. package/dist/commands/ai-video-fx.js +395 -0
  167. package/dist/commands/ai-video-fx.js.map +1 -0
  168. package/dist/commands/ai-video.d.ts +15 -0
  169. package/dist/commands/ai-video.d.ts.map +1 -0
  170. package/dist/commands/ai-video.js +785 -0
  171. package/dist/commands/ai-video.js.map +1 -0
  172. package/dist/commands/ai-viral.d.ts +15 -0
  173. package/dist/commands/ai-viral.d.ts.map +1 -0
  174. package/dist/commands/ai-viral.js +519 -0
  175. package/dist/commands/ai-viral.js.map +1 -0
  176. package/dist/commands/ai-visual-fx.d.ts +14 -0
  177. package/dist/commands/ai-visual-fx.d.ts.map +1 -0
  178. package/dist/commands/ai-visual-fx.js +505 -0
  179. package/dist/commands/ai-visual-fx.js.map +1 -0
  180. package/dist/commands/ai.d.ts +38 -0
  181. package/dist/commands/ai.d.ts.map +1 -0
  182. package/dist/commands/ai.js +225 -0
  183. package/dist/commands/ai.js.map +1 -0
  184. package/dist/commands/ai.test.d.ts +2 -0
  185. package/dist/commands/ai.test.d.ts.map +1 -0
  186. package/dist/commands/ai.test.js +554 -0
  187. package/dist/commands/ai.test.js.map +1 -0
  188. package/dist/commands/analyze.d.ts +16 -0
  189. package/dist/commands/analyze.d.ts.map +1 -0
  190. package/dist/commands/analyze.js +247 -0
  191. package/dist/commands/analyze.js.map +1 -0
  192. package/dist/commands/audio.d.ts +18 -0
  193. package/dist/commands/audio.d.ts.map +1 -0
  194. package/dist/commands/audio.js +539 -0
  195. package/dist/commands/audio.js.map +1 -0
  196. package/dist/commands/batch.d.ts +3 -0
  197. package/dist/commands/batch.d.ts.map +1 -0
  198. package/dist/commands/batch.js +366 -0
  199. package/dist/commands/batch.js.map +1 -0
  200. package/dist/commands/batch.test.d.ts +2 -0
  201. package/dist/commands/batch.test.d.ts.map +1 -0
  202. package/dist/commands/batch.test.js +203 -0
  203. package/dist/commands/batch.test.js.map +1 -0
  204. package/dist/commands/detect.d.ts +3 -0
  205. package/dist/commands/detect.d.ts.map +1 -0
  206. package/dist/commands/detect.js +273 -0
  207. package/dist/commands/detect.js.map +1 -0
  208. package/dist/commands/doctor.d.ts +6 -0
  209. package/dist/commands/doctor.d.ts.map +1 -0
  210. package/dist/commands/doctor.js +191 -0
  211. package/dist/commands/doctor.js.map +1 -0
  212. package/dist/commands/edit-cmd.d.ts +26 -0
  213. package/dist/commands/edit-cmd.d.ts.map +1 -0
  214. package/dist/commands/edit-cmd.js +870 -0
  215. package/dist/commands/edit-cmd.js.map +1 -0
  216. package/dist/commands/export.d.ts +39 -0
  217. package/dist/commands/export.d.ts.map +1 -0
  218. package/dist/commands/export.js +730 -0
  219. package/dist/commands/export.js.map +1 -0
  220. package/dist/commands/generate.d.ts +25 -0
  221. package/dist/commands/generate.d.ts.map +1 -0
  222. package/dist/commands/generate.js +1885 -0
  223. package/dist/commands/generate.js.map +1 -0
  224. package/dist/commands/media.d.ts +3 -0
  225. package/dist/commands/media.d.ts.map +1 -0
  226. package/dist/commands/media.js +165 -0
  227. package/dist/commands/media.js.map +1 -0
  228. package/dist/commands/output.d.ts +45 -0
  229. package/dist/commands/output.d.ts.map +1 -0
  230. package/dist/commands/output.js +122 -0
  231. package/dist/commands/output.js.map +1 -0
  232. package/dist/commands/pipeline.d.ts +19 -0
  233. package/dist/commands/pipeline.d.ts.map +1 -0
  234. package/dist/commands/pipeline.js +345 -0
  235. package/dist/commands/pipeline.js.map +1 -0
  236. package/dist/commands/project.d.ts +3 -0
  237. package/dist/commands/project.d.ts.map +1 -0
  238. package/dist/commands/project.js +139 -0
  239. package/dist/commands/project.js.map +1 -0
  240. package/dist/commands/project.test.d.ts +2 -0
  241. package/dist/commands/project.test.d.ts.map +1 -0
  242. package/dist/commands/project.test.js +105 -0
  243. package/dist/commands/project.test.js.map +1 -0
  244. package/dist/commands/sanitize.d.ts +21 -0
  245. package/dist/commands/sanitize.d.ts.map +1 -0
  246. package/dist/commands/sanitize.js +56 -0
  247. package/dist/commands/sanitize.js.map +1 -0
  248. package/dist/commands/schema.d.ts +11 -0
  249. package/dist/commands/schema.d.ts.map +1 -0
  250. package/dist/commands/schema.js +101 -0
  251. package/dist/commands/schema.js.map +1 -0
  252. package/dist/commands/setup.d.ts +6 -0
  253. package/dist/commands/setup.d.ts.map +1 -0
  254. package/dist/commands/setup.js +440 -0
  255. package/dist/commands/setup.js.map +1 -0
  256. package/dist/commands/timeline.d.ts +3 -0
  257. package/dist/commands/timeline.d.ts.map +1 -0
  258. package/dist/commands/timeline.js +469 -0
  259. package/dist/commands/timeline.js.map +1 -0
  260. package/dist/commands/timeline.test.d.ts +2 -0
  261. package/dist/commands/timeline.test.d.ts.map +1 -0
  262. package/dist/commands/timeline.test.js +320 -0
  263. package/dist/commands/timeline.test.js.map +1 -0
  264. package/dist/commands/validate.d.ts +32 -0
  265. package/dist/commands/validate.d.ts.map +1 -0
  266. package/dist/commands/validate.js +63 -0
  267. package/dist/commands/validate.js.map +1 -0
  268. package/dist/config/config.test.d.ts +2 -0
  269. package/dist/config/config.test.d.ts.map +1 -0
  270. package/dist/config/config.test.js +164 -0
  271. package/dist/config/config.test.js.map +1 -0
  272. package/dist/config/index.d.ts +35 -0
  273. package/dist/config/index.d.ts.map +1 -0
  274. package/dist/config/index.js +101 -0
  275. package/dist/config/index.js.map +1 -0
  276. package/dist/config/schema.d.ts +43 -0
  277. package/dist/config/schema.d.ts.map +1 -0
  278. package/dist/config/schema.js +42 -0
  279. package/dist/config/schema.js.map +1 -0
  280. package/dist/engine/index.d.ts +3 -0
  281. package/dist/engine/index.d.ts.map +1 -0
  282. package/dist/engine/index.js +2 -0
  283. package/dist/engine/index.js.map +1 -0
  284. package/dist/engine/project.d.ts +84 -0
  285. package/dist/engine/project.d.ts.map +1 -0
  286. package/dist/engine/project.js +355 -0
  287. package/dist/engine/project.js.map +1 -0
  288. package/dist/engine/project.test.d.ts +2 -0
  289. package/dist/engine/project.test.d.ts.map +1 -0
  290. package/dist/engine/project.test.js +599 -0
  291. package/dist/engine/project.test.js.map +1 -0
  292. package/dist/index.d.ts +7 -0
  293. package/dist/index.d.ts.map +1 -0
  294. package/dist/index.js +131 -0
  295. package/dist/index.js.map +1 -0
  296. package/dist/utils/api-key.d.ts +36 -0
  297. package/dist/utils/api-key.d.ts.map +1 -0
  298. package/dist/utils/api-key.js +211 -0
  299. package/dist/utils/api-key.js.map +1 -0
  300. package/dist/utils/api-key.test.d.ts +2 -0
  301. package/dist/utils/api-key.test.d.ts.map +1 -0
  302. package/dist/utils/api-key.test.js +35 -0
  303. package/dist/utils/api-key.test.js.map +1 -0
  304. package/dist/utils/audio.d.ts +23 -0
  305. package/dist/utils/audio.d.ts.map +1 -0
  306. package/dist/utils/audio.js +79 -0
  307. package/dist/utils/audio.js.map +1 -0
  308. package/dist/utils/exec-safe.d.ts +22 -0
  309. package/dist/utils/exec-safe.d.ts.map +1 -0
  310. package/dist/utils/exec-safe.js +62 -0
  311. package/dist/utils/exec-safe.js.map +1 -0
  312. package/dist/utils/first-run.d.ts +13 -0
  313. package/dist/utils/first-run.d.ts.map +1 -0
  314. package/dist/utils/first-run.js +48 -0
  315. package/dist/utils/first-run.js.map +1 -0
  316. package/dist/utils/provider-resolver.d.ts +15 -0
  317. package/dist/utils/provider-resolver.d.ts.map +1 -0
  318. package/dist/utils/provider-resolver.js +42 -0
  319. package/dist/utils/provider-resolver.js.map +1 -0
  320. package/dist/utils/remotion.d.ts +210 -0
  321. package/dist/utils/remotion.d.ts.map +1 -0
  322. package/dist/utils/remotion.js +731 -0
  323. package/dist/utils/remotion.js.map +1 -0
  324. package/dist/utils/subtitle.d.ts +65 -0
  325. package/dist/utils/subtitle.d.ts.map +1 -0
  326. package/dist/utils/subtitle.js +135 -0
  327. package/dist/utils/subtitle.js.map +1 -0
  328. package/dist/utils/subtitle.test.d.ts +2 -0
  329. package/dist/utils/subtitle.test.d.ts.map +1 -0
  330. package/dist/utils/subtitle.test.js +175 -0
  331. package/dist/utils/subtitle.test.js.map +1 -0
  332. package/dist/utils/tty.d.ts +45 -0
  333. package/dist/utils/tty.d.ts.map +1 -0
  334. package/dist/utils/tty.js +172 -0
  335. package/dist/utils/tty.js.map +1 -0
  336. package/package.json +102 -0
  337. package/src/agent/adapters/claude.ts +143 -0
  338. package/src/agent/adapters/gemini.ts +159 -0
  339. package/src/agent/adapters/index.ts +61 -0
  340. package/src/agent/adapters/ollama.ts +231 -0
  341. package/src/agent/adapters/openai.ts +116 -0
  342. package/src/agent/adapters/xai.ts +119 -0
  343. package/src/agent/index.ts +251 -0
  344. package/src/agent/memory/index.ts +151 -0
  345. package/src/agent/prompts/system.ts +106 -0
  346. package/src/agent/tools/ai-editing.ts +845 -0
  347. package/src/agent/tools/ai-generation.ts +1073 -0
  348. package/src/agent/tools/ai-pipeline.ts +1055 -0
  349. package/src/agent/tools/ai.ts +21 -0
  350. package/src/agent/tools/batch.ts +429 -0
  351. package/src/agent/tools/e2e.test.ts +545 -0
  352. package/src/agent/tools/export.ts +184 -0
  353. package/src/agent/tools/filesystem.ts +237 -0
  354. package/src/agent/tools/index.ts +150 -0
  355. package/src/agent/tools/integration.test.ts +775 -0
  356. package/src/agent/tools/media.ts +697 -0
  357. package/src/agent/tools/project.ts +313 -0
  358. package/src/agent/tools/timeline.ts +951 -0
  359. package/src/agent/types.ts +68 -0
  360. package/src/commands/agent.ts +340 -0
  361. package/src/commands/ai-analyze.ts +429 -0
  362. package/src/commands/ai-animated-caption.ts +390 -0
  363. package/src/commands/ai-audio.ts +941 -0
  364. package/src/commands/ai-broll.ts +490 -0
  365. package/src/commands/ai-edit-cli.ts +658 -0
  366. package/src/commands/ai-edit.ts +1542 -0
  367. package/src/commands/ai-fill-gaps.ts +566 -0
  368. package/src/commands/ai-helpers.ts +65 -0
  369. package/src/commands/ai-highlights.ts +1303 -0
  370. package/src/commands/ai-image.ts +761 -0
  371. package/src/commands/ai-motion.ts +347 -0
  372. package/src/commands/ai-narrate.ts +451 -0
  373. package/src/commands/ai-review.ts +309 -0
  374. package/src/commands/ai-script-pipeline-cli.ts +1710 -0
  375. package/src/commands/ai-script-pipeline.ts +1365 -0
  376. package/src/commands/ai-suggest-edit.ts +264 -0
  377. package/src/commands/ai-video-fx.ts +445 -0
  378. package/src/commands/ai-video.ts +915 -0
  379. package/src/commands/ai-viral.ts +595 -0
  380. package/src/commands/ai-visual-fx.ts +601 -0
  381. package/src/commands/ai.test.ts +627 -0
  382. package/src/commands/ai.ts +307 -0
  383. package/src/commands/analyze.ts +282 -0
  384. package/src/commands/audio.ts +644 -0
  385. package/src/commands/batch.test.ts +279 -0
  386. package/src/commands/batch.ts +440 -0
  387. package/src/commands/detect.ts +329 -0
  388. package/src/commands/doctor.ts +237 -0
  389. package/src/commands/edit-cmd.ts +1014 -0
  390. package/src/commands/export.ts +918 -0
  391. package/src/commands/generate.ts +2146 -0
  392. package/src/commands/media.ts +177 -0
  393. package/src/commands/output.ts +142 -0
  394. package/src/commands/pipeline.ts +398 -0
  395. package/src/commands/project.test.ts +127 -0
  396. package/src/commands/project.ts +149 -0
  397. package/src/commands/sanitize.ts +60 -0
  398. package/src/commands/schema.ts +130 -0
  399. package/src/commands/setup.ts +509 -0
  400. package/src/commands/timeline.test.ts +499 -0
  401. package/src/commands/timeline.ts +529 -0
  402. package/src/commands/validate.ts +77 -0
  403. package/src/config/config.test.ts +197 -0
  404. package/src/config/index.ts +125 -0
  405. package/src/config/schema.ts +82 -0
  406. package/src/engine/index.ts +2 -0
  407. package/src/engine/project.test.ts +702 -0
  408. package/src/engine/project.ts +439 -0
  409. package/src/index.ts +146 -0
  410. package/src/utils/api-key.test.ts +41 -0
  411. package/src/utils/api-key.ts +247 -0
  412. package/src/utils/audio.ts +83 -0
  413. package/src/utils/exec-safe.ts +75 -0
  414. package/src/utils/first-run.ts +52 -0
  415. package/src/utils/provider-resolver.ts +56 -0
  416. package/src/utils/remotion.ts +951 -0
  417. package/src/utils/subtitle.test.ts +227 -0
  418. package/src/utils/subtitle.ts +169 -0
  419. package/src/utils/tty.ts +196 -0
  420. package/tsconfig.json +20 -0
@@ -0,0 +1,1885 @@
1
+ /**
2
+ * @module generate
3
+ *
4
+ * Top-level `vibe generate` command group for AI asset generation.
5
+ *
6
+ * Commands:
7
+ * generate image - Generate image (Gemini, OpenAI, Grok, Runway)
8
+ * generate video - Generate video (Kling, Runway, Veo, Grok)
9
+ * generate speech - Text-to-speech (ElevenLabs)
10
+ * generate sound-effect - Sound effects (ElevenLabs)
11
+ * generate music - Music generation (ElevenLabs default, Replicate MusicGen)
12
+ * generate music-status - Check music generation status
13
+ * generate storyboard - Script-to-storyboard (Claude)
14
+ * generate motion - Motion graphics (Claude/Gemini + Remotion)
15
+ * generate thumbnail - Thumbnail generation/extraction
16
+ * generate background - AI background generation (OpenAI)
17
+ * generate video-status - Check video generation status (Grok/Runway/Kling)
18
+ * generate video-cancel - Cancel video generation (Grok/Runway)
19
+ * generate video-extend - Extend video (Kling/Veo)
20
+ *
21
+ * @dependencies OpenAI, Gemini, Runway, Kling, ElevenLabs, Replicate, Claude, FFmpeg
22
+ */
23
+ import { Command } from "commander";
24
+ import { resolve, dirname, basename, extname } from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
27
+ import { existsSync } from "node:fs";
28
+ import chalk from "chalk";
29
+ import ora from "ora";
30
+ import imageSize from "image-size";
31
+ import { GeminiProvider, OpenAIImageProvider, KlingProvider, RunwayProvider, ElevenLabsProvider, ReplicateProvider, ClaudeProvider, GrokProvider, } from "@vibeframe/ai-providers";
32
+ import { requireApiKey, hasApiKey } from "../utils/api-key.js";
33
+ import { hasTTY, prompt as promptText } from "../utils/tty.js";
34
+ import { getApiKeyFromConfig } from "../config/index.js";
35
+ import { sanitizeLLMResponse } from "./sanitize.js";
36
+ import { isJsonMode, outputResult, log, exitWithError, usageError, apiError } from "./output.js";
37
+ import { commandExists } from "../utils/exec-safe.js";
38
+ import { uploadToImgbb } from "./ai-script-pipeline.js";
39
+ import { downloadVideo, formatTime } from "./ai-helpers.js";
40
+ import { rejectControlChars } from "./validate.js";
41
+ import { resolveProvider } from "../utils/provider-resolver.js";
42
+ import { executeThumbnailBestFrame } from "./ai-image.js";
43
+ import { registerMotionCommand } from "./ai-motion.js";
44
+ // ── Helpers ──────────────────────────────────────────────────────────────────
45
+ function getStatusColor(status) {
46
+ switch (status) {
47
+ case "completed":
48
+ return chalk.green(status);
49
+ case "processing":
50
+ case "running":
51
+ case "in_progress":
52
+ return chalk.yellow(status);
53
+ case "failed":
54
+ case "error":
55
+ return chalk.red(status);
56
+ default:
57
+ return chalk.gray(status);
58
+ }
59
+ }
60
+ // ── Command group ────────────────────────────────────────────────────────────
61
+ export const generateCommand = new Command("generate")
62
+ .alias("gen")
63
+ .description("Generate assets using AI (images, videos, speech, music, motion)")
64
+ .addHelpText("after", `
65
+ Examples:
66
+ $ vibe generate image "a sunset over the ocean" -o sunset.png
67
+ $ vibe generate image "logo design" -o logo.png -p openai
68
+ $ vibe generate video "dancing cat" -o cat.mp4 # Grok (default, native audio)
69
+ $ vibe generate video "city timelapse" -o city.mp4 -p kling # Kling
70
+ $ vibe generate video "epic scene" -i frame.png -o out.mp4 -p runway # Image-to-video
71
+ $ vibe generate speech "Hello world" -o hello.mp3
72
+ $ vibe generate music "upbeat jazz" -o jazz.mp3 -d 30
73
+ $ vibe generate motion "animated logo intro" -o intro.mp4 --render
74
+
75
+ API Keys (per provider):
76
+ GOOGLE_API_KEY Image (default), Veo video
77
+ OPENAI_API_KEY Image (-p openai)
78
+ XAI_API_KEY Grok image/video (default video)
79
+ KLING_API_KEY Kling video (-p kling)
80
+ RUNWAY_API_SECRET Runway video (-p runway)
81
+ ELEVENLABS_API_KEY Speech, sound effects, music
82
+ ANTHROPIC_API_KEY Storyboard, motion graphics
83
+
84
+ Run 'vibe setup --show' to check API key status.
85
+ Run 'vibe schema generate.<command>' for structured parameter info.
86
+ `);
87
+ // ============================================================================
88
+ // 1. Image
89
+ // ============================================================================
90
+ generateCommand
91
+ .command("image")
92
+ .alias("img")
93
+ .description("Generate image using AI (Gemini, DALL-E, or Runway)")
94
+ .argument("[prompt]", "Image description prompt (interactive if omitted)")
95
+ .option("-p, --provider <provider>", "Provider: gemini, openai, grok, runway (dalle is deprecated)", "gemini")
96
+ .option("-k, --api-key <key>", "API key (or set env: OPENAI_API_KEY, GOOGLE_API_KEY)")
97
+ .option("-o, --output <path>", "Output file path (downloads image)")
98
+ .option("-s, --size <size>", "Image size (openai: 1024x1024, 1536x1024, 1024x1536)", "1024x1024")
99
+ .option("-r, --ratio <ratio>", "Aspect ratio (gemini: 1:1, 1:4, 1:8, 4:1, 8:1, 16:9, 9:16, 3:4, 4:3, etc.)", "1:1")
100
+ .option("-q, --quality <quality>", "Quality: standard, hd (openai only)", "standard")
101
+ .option("--style <style>", "Style: vivid, natural (openai only)", "vivid")
102
+ .option("-n, --count <n>", "Number of images to generate", "1")
103
+ .option("-m, --model <model>", "Gemini model: flash, 3.1-flash, latest (Nano Banana 2), pro (4K)")
104
+ .option("--dry-run", "Preview parameters without executing")
105
+ .action(async (prompt, options) => {
106
+ try {
107
+ // Interactive prompt if no argument provided
108
+ if (!prompt) {
109
+ if (hasTTY()) {
110
+ prompt = await promptText(chalk.cyan("What would you like to generate? "));
111
+ if (!prompt?.trim()) {
112
+ console.error(chalk.red("Prompt is required."));
113
+ return;
114
+ }
115
+ }
116
+ else {
117
+ console.error(chalk.red("Prompt argument is required."));
118
+ return;
119
+ }
120
+ }
121
+ rejectControlChars(prompt);
122
+ // Auto-resolve provider if user didn't explicitly set one
123
+ let provider = options.provider.toLowerCase();
124
+ const validProviders = ["openai", "dalle", "gemini", "grok", "runway"];
125
+ if (!validProviders.includes(provider)) {
126
+ exitWithError(usageError(`Invalid provider: ${provider}`, `Available providers: openai, gemini, grok, runway`));
127
+ }
128
+ // Auto-fallback: if default provider's key is missing, find one that works
129
+ const providerEnvMap = {
130
+ gemini: "GOOGLE_API_KEY", openai: "OPENAI_API_KEY", grok: "XAI_API_KEY",
131
+ };
132
+ if (providerEnvMap[provider] && !hasApiKey(providerEnvMap[provider]) && !options.apiKey) {
133
+ const resolved = resolveProvider("image");
134
+ if (resolved) {
135
+ log(chalk.dim(` ${provider} key not found. Using ${resolved.label} instead.`));
136
+ provider = resolved.name;
137
+ }
138
+ }
139
+ // Show deprecation warning for "dalle"
140
+ if (provider === "dalle") {
141
+ console.log(chalk.yellow('Warning: "dalle" is deprecated. Use "openai" instead.'));
142
+ }
143
+ // Dry-run check
144
+ if (options.dryRun) {
145
+ outputResult({ dryRun: true, command: "generate image", params: { prompt, provider, model: options.model, ratio: options.ratio, size: options.size, quality: options.quality, count: options.count, output: options.output } });
146
+ return;
147
+ }
148
+ // Get API key based on provider
149
+ const envKeyMap = {
150
+ openai: "OPENAI_API_KEY",
151
+ dalle: "OPENAI_API_KEY",
152
+ gemini: "GOOGLE_API_KEY",
153
+ grok: "XAI_API_KEY",
154
+ runway: "RUNWAY_API_SECRET",
155
+ };
156
+ const providerNameMap = {
157
+ openai: "OpenAI",
158
+ dalle: "OpenAI",
159
+ gemini: "Google",
160
+ grok: "xAI Grok",
161
+ runway: "Runway",
162
+ };
163
+ const envKey = envKeyMap[provider];
164
+ const providerName = providerNameMap[provider];
165
+ const apiKey = await requireApiKey(envKey, providerName, options.apiKey);
166
+ const spinner = ora(`Generating image with ${providerName}...`).start();
167
+ if (provider === "dalle" || provider === "openai") {
168
+ const openaiImage = new OpenAIImageProvider();
169
+ await openaiImage.initialize({ apiKey });
170
+ const result = await openaiImage.generateImage(prompt, {
171
+ size: options.size,
172
+ quality: options.quality,
173
+ style: options.style,
174
+ n: parseInt(options.count),
175
+ });
176
+ if (!result.success || !result.images) {
177
+ spinner.fail(chalk.red(result.error || "Image generation failed"));
178
+ process.exit(1);
179
+ }
180
+ spinner.succeed(chalk.green(`Generated ${result.images.length} image(s) with OpenAI GPT Image 1.5`));
181
+ if (isJsonMode()) {
182
+ const outputPath = options.output ? resolve(process.cwd(), options.output) : undefined;
183
+ // Still save the file in JSON mode
184
+ if (outputPath && result.images.length > 0) {
185
+ const img = result.images[0];
186
+ let buffer;
187
+ if (img.url) {
188
+ const response = await fetch(img.url);
189
+ buffer = Buffer.from(await response.arrayBuffer());
190
+ }
191
+ else if (img.base64) {
192
+ buffer = Buffer.from(img.base64, "base64");
193
+ }
194
+ else {
195
+ throw new Error("No image data available");
196
+ }
197
+ await mkdir(dirname(outputPath), { recursive: true });
198
+ await writeFile(outputPath, buffer);
199
+ }
200
+ outputResult({ success: true, provider: "openai", images: result.images.map(img => ({ url: img.url, revisedPrompt: img.revisedPrompt })), outputPath });
201
+ return;
202
+ }
203
+ console.log();
204
+ console.log(chalk.bold.cyan("Generated Images"));
205
+ console.log(chalk.dim("─".repeat(60)));
206
+ for (let i = 0; i < result.images.length; i++) {
207
+ const img = result.images[i];
208
+ console.log();
209
+ if (img.url) {
210
+ console.log(`${chalk.yellow(`[${i + 1}]`)} ${img.url}`);
211
+ }
212
+ else if (img.base64) {
213
+ console.log(`${chalk.yellow(`[${i + 1}]`)} (base64 image data)`);
214
+ }
215
+ if (img.revisedPrompt) {
216
+ console.log(chalk.dim(` Revised: ${img.revisedPrompt.slice(0, 100)}...`));
217
+ }
218
+ }
219
+ console.log();
220
+ // Save if output specified
221
+ if (options.output && result.images.length > 0) {
222
+ const img = result.images[0];
223
+ const saveSpinner = ora("Saving image...").start();
224
+ try {
225
+ let buffer;
226
+ if (img.url) {
227
+ const response = await fetch(img.url);
228
+ buffer = Buffer.from(await response.arrayBuffer());
229
+ }
230
+ else if (img.base64) {
231
+ buffer = Buffer.from(img.base64, "base64");
232
+ }
233
+ else {
234
+ throw new Error("No image data available");
235
+ }
236
+ const outputPath = resolve(process.cwd(), options.output);
237
+ await mkdir(dirname(outputPath), { recursive: true });
238
+ await writeFile(outputPath, buffer);
239
+ saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
240
+ }
241
+ catch (err) {
242
+ saveSpinner.fail(chalk.red(`Failed to save image: ${err instanceof Error ? err.message : err}`));
243
+ }
244
+ }
245
+ }
246
+ else if (provider === "gemini") {
247
+ // Validate model name
248
+ const validGeminiModels = ["flash", "3.1-flash", "latest", "pro"];
249
+ if (options.model && !validGeminiModels.includes(options.model)) {
250
+ console.warn(chalk.yellow(`Unknown model "${options.model}", using flash. Valid: ${validGeminiModels.join(", ")}`));
251
+ options.model = "flash";
252
+ }
253
+ // Validate aspect ratio
254
+ const validRatios = ["1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"];
255
+ if (options.ratio && !validRatios.includes(options.ratio)) {
256
+ console.error(chalk.red(`Invalid ratio "${options.ratio}". Valid: ${validRatios.join(", ")}`));
257
+ process.exit(1);
258
+ }
259
+ const gemini = new GeminiProvider();
260
+ await gemini.initialize({ apiKey });
261
+ const geminiModelNames = {
262
+ flash: "Nano Banana",
263
+ "3.1-flash": "Nano Banana 2",
264
+ latest: "Nano Banana 2",
265
+ pro: "Nano Banana Pro",
266
+ };
267
+ const modelLabel = geminiModelNames[options.model] || "Nano Banana";
268
+ let result = await gemini.generateImage(prompt, {
269
+ model: options.model,
270
+ aspectRatio: options.ratio,
271
+ });
272
+ // Auto-fallback: if latest/3.1-flash fails, retry with flash
273
+ let usedLabel = modelLabel;
274
+ const fallbackModels = ["latest", "3.1-flash"];
275
+ if (!result.success && options.model && fallbackModels.includes(options.model)) {
276
+ spinner.text = `${chalk.dim(result.error || "Failed")} — retrying with Nano Banana (flash)...`;
277
+ result = await gemini.generateImage(prompt, {
278
+ model: "flash",
279
+ aspectRatio: options.ratio,
280
+ });
281
+ usedLabel = "Nano Banana (fallback)";
282
+ }
283
+ if (!result.success || !result.images) {
284
+ spinner.fail(chalk.red(result.error || "Image generation failed"));
285
+ process.exit(1);
286
+ }
287
+ spinner.succeed(chalk.green(`Generated ${result.images.length} image(s) with Gemini (${usedLabel})`));
288
+ if (isJsonMode()) {
289
+ const outputPath = options.output ? resolve(process.cwd(), options.output) : undefined;
290
+ if (outputPath && result.images.length > 0) {
291
+ const img = result.images[0];
292
+ const buffer = Buffer.from(img.base64, "base64");
293
+ await mkdir(dirname(outputPath), { recursive: true });
294
+ await writeFile(outputPath, buffer);
295
+ }
296
+ outputResult({ success: true, provider: "gemini", images: result.images.map((img) => ({ mimeType: img.mimeType })), outputPath });
297
+ return;
298
+ }
299
+ console.log();
300
+ console.log(chalk.bold.cyan("Generated Images"));
301
+ console.log(chalk.dim("─".repeat(60)));
302
+ for (let i = 0; i < result.images.length; i++) {
303
+ const img = result.images[i];
304
+ console.log();
305
+ console.log(`${chalk.yellow(`[${i + 1}]`)} (base64 image, ${img.mimeType})`);
306
+ }
307
+ console.log();
308
+ // Save if output specified
309
+ if (options.output && result.images.length > 0) {
310
+ const saveSpinner = ora("Saving image...").start();
311
+ try {
312
+ const img = result.images[0];
313
+ const buffer = Buffer.from(img.base64, "base64");
314
+ const outputPath = resolve(process.cwd(), options.output);
315
+ await mkdir(dirname(outputPath), { recursive: true });
316
+ await writeFile(outputPath, buffer);
317
+ saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
318
+ }
319
+ catch (err) {
320
+ saveSpinner.fail(chalk.red(`Failed to save image: ${err instanceof Error ? err.message : err}`));
321
+ }
322
+ }
323
+ else {
324
+ console.log(chalk.yellow("Use -o to save the generated image to a file"));
325
+ }
326
+ }
327
+ else if (provider === "grok") {
328
+ const grok = new GrokProvider();
329
+ await grok.initialize({ apiKey });
330
+ // Validate aspect ratio for Grok
331
+ const validGrokRatios = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "2:1", "1:2", "19.5:9", "9:19.5", "20:9", "9:20", "auto"];
332
+ if (options.ratio && !validGrokRatios.includes(options.ratio)) {
333
+ console.warn(chalk.yellow(`Unknown ratio "${options.ratio}" for Grok, using 1:1. Valid: ${validGrokRatios.join(", ")}`));
334
+ options.ratio = "1:1";
335
+ }
336
+ const result = await grok.generateImage(prompt, {
337
+ aspectRatio: options.ratio || "1:1",
338
+ n: parseInt(options.count),
339
+ });
340
+ if (!result.success || !result.images) {
341
+ spinner.fail(chalk.red(result.error || "Image generation failed"));
342
+ process.exit(1);
343
+ }
344
+ spinner.succeed(chalk.green(`Generated ${result.images.length} image(s) with xAI Grok`));
345
+ if (isJsonMode()) {
346
+ const outputPath = options.output ? resolve(process.cwd(), options.output) : undefined;
347
+ if (outputPath && result.images.length > 0) {
348
+ const img = result.images[0];
349
+ let buffer;
350
+ if (img.url) {
351
+ const response = await fetch(img.url);
352
+ buffer = Buffer.from(await response.arrayBuffer());
353
+ }
354
+ else if (img.base64) {
355
+ buffer = Buffer.from(img.base64, "base64");
356
+ }
357
+ else {
358
+ throw new Error("No image data available");
359
+ }
360
+ await mkdir(dirname(outputPath), { recursive: true });
361
+ await writeFile(outputPath, buffer);
362
+ }
363
+ outputResult({ success: true, provider: "grok", images: result.images.map(img => ({ url: img.url })), outputPath });
364
+ return;
365
+ }
366
+ console.log();
367
+ console.log(chalk.bold.cyan("Generated Images"));
368
+ console.log(chalk.dim("─".repeat(60)));
369
+ for (let i = 0; i < result.images.length; i++) {
370
+ const img = result.images[i];
371
+ console.log();
372
+ if (img.url) {
373
+ console.log(`${chalk.yellow(`[${i + 1}]`)} ${img.url}`);
374
+ }
375
+ else if (img.base64) {
376
+ console.log(`${chalk.yellow(`[${i + 1}]`)} (base64 image data)`);
377
+ }
378
+ }
379
+ console.log();
380
+ // Save if output specified
381
+ if (options.output && result.images.length > 0) {
382
+ const img = result.images[0];
383
+ const saveSpinner = ora("Saving image...").start();
384
+ try {
385
+ let buffer;
386
+ if (img.url) {
387
+ const response = await fetch(img.url);
388
+ buffer = Buffer.from(await response.arrayBuffer());
389
+ }
390
+ else if (img.base64) {
391
+ buffer = Buffer.from(img.base64, "base64");
392
+ }
393
+ else {
394
+ throw new Error("No image data available");
395
+ }
396
+ const outputPath = resolve(process.cwd(), options.output);
397
+ await mkdir(dirname(outputPath), { recursive: true });
398
+ await writeFile(outputPath, buffer);
399
+ saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
400
+ }
401
+ catch (err) {
402
+ saveSpinner.fail(chalk.red(`Failed to save image: ${err instanceof Error ? err.message : err}`));
403
+ }
404
+ }
405
+ }
406
+ else if (provider === "runway") {
407
+ const { spawn } = await import("child_process");
408
+ const __filename = fileURLToPath(import.meta.url);
409
+ const __dirname = dirname(__filename);
410
+ const scriptPath = resolve(__dirname, "../../../../.claude/skills/runway-video/scripts/image.py");
411
+ if (!options.output) {
412
+ spinner.fail(chalk.red("Output path required for Runway. Use -o option."));
413
+ process.exit(1);
414
+ }
415
+ const outputPath = resolve(process.cwd(), options.output);
416
+ const args = [scriptPath, prompt, "-o", outputPath, "-r", options.ratio || "16:9"];
417
+ spinner.text = "Generating image with Runway (gemini_2.5_flash)...";
418
+ await new Promise((resolvePromise, reject) => {
419
+ const proc = spawn("python3", args, {
420
+ env: { ...process.env, RUNWAY_API_SECRET: apiKey },
421
+ stdio: ["ignore", "pipe", "pipe"],
422
+ });
423
+ let stdout = "";
424
+ let stderr = "";
425
+ proc.stdout.on("data", (data) => {
426
+ stdout += data.toString();
427
+ });
428
+ proc.stderr.on("data", (data) => {
429
+ stderr += data.toString();
430
+ });
431
+ proc.on("close", (code) => {
432
+ if (code === 0) {
433
+ if (isJsonMode()) {
434
+ outputResult({ success: true, provider: "runway", images: [{ format: "file" }], outputPath });
435
+ }
436
+ else {
437
+ spinner.succeed(chalk.green("Generated image with Runway"));
438
+ console.log(chalk.dim(stdout.trim()));
439
+ }
440
+ resolvePromise();
441
+ }
442
+ else {
443
+ spinner.fail(chalk.red("Runway image generation failed"));
444
+ console.error(chalk.red(stderr || stdout));
445
+ reject(new Error("Runway generation failed"));
446
+ }
447
+ });
448
+ proc.on("error", (err) => {
449
+ spinner.fail(chalk.red("Failed to run Runway script"));
450
+ reject(err);
451
+ });
452
+ });
453
+ }
454
+ }
455
+ catch (error) {
456
+ exitWithError(apiError(`Image generation failed: ${error.message}`));
457
+ }
458
+ });
459
+ // ============================================================================
460
+ // 2. Video (merged: ai video + ai kling, unified via --provider)
461
+ // ============================================================================
462
+ generateCommand
463
+ .command("video")
464
+ .alias("vid")
465
+ .description("Generate video using AI (Kling, Runway, Veo, or Grok)")
466
+ .argument("[prompt]", "Text prompt describing the video (interactive if omitted)")
467
+ .option("-p, --provider <provider>", "Provider: grok (default), kling, runway, veo", "grok")
468
+ .option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY / GOOGLE_API_KEY env)")
469
+ .option("-o, --output <path>", "Output file path (downloads video)")
470
+ .option("-i, --image <path>", "Reference image for image-to-video")
471
+ .option("-d, --duration <sec>", "Duration: 5 or 10 seconds", "5")
472
+ .option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, or 1:1 (auto-detected from image if omitted)")
473
+ .option("-s, --seed <number>", "Random seed for reproducibility (Runway only)")
474
+ .option("-m, --mode <mode>", "Generation mode: std or pro (Kling only)", "std")
475
+ .option("-n, --negative <prompt>", "Negative prompt - what to avoid (Kling/Veo)")
476
+ .option("--resolution <res>", "Video resolution: 720p, 1080p, 4k (Veo only)")
477
+ .option("--last-frame <path>", "Last frame image for frame interpolation (Veo only)")
478
+ .option("--ref-images <paths...>", "Reference images for character consistency (Veo 3.1 only, max 3)")
479
+ .option("--person <mode>", "Person generation: allow_all, allow_adult (Veo only)")
480
+ .option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast (default: 3.1-fast)", "3.1-fast")
481
+ .option("--runway-model <model>", "Runway model: gen4.5 (default, text+image-to-video), gen4_turbo (image-to-video only)", "gen4.5")
482
+ .option("--no-wait", "Start generation and return task ID without waiting")
483
+ .option("--dry-run", "Preview parameters without executing")
484
+ .action(async (prompt, options) => {
485
+ try {
486
+ // Interactive prompt if no argument provided
487
+ if (!prompt) {
488
+ if (hasTTY()) {
489
+ prompt = await promptText(chalk.cyan("Describe your video: "));
490
+ if (!prompt?.trim()) {
491
+ console.error(chalk.red("Prompt is required."));
492
+ return;
493
+ }
494
+ }
495
+ else {
496
+ console.error(chalk.red("Prompt argument is required."));
497
+ return;
498
+ }
499
+ }
500
+ rejectControlChars(prompt);
501
+ let provider = options.provider.toLowerCase();
502
+ const validProviders = ["runway", "kling", "veo", "grok"];
503
+ if (!validProviders.includes(provider)) {
504
+ exitWithError(usageError(`Invalid provider: ${provider}`, `Available providers: ${validProviders.join(", ")}`));
505
+ }
506
+ // Auto-fallback: if default provider's key is missing, find one that works
507
+ const videoEnvMap = {
508
+ grok: "XAI_API_KEY", veo: "GOOGLE_API_KEY", kling: "KLING_API_KEY", runway: "RUNWAY_API_SECRET",
509
+ };
510
+ if (videoEnvMap[provider] && !hasApiKey(videoEnvMap[provider]) && !options.apiKey) {
511
+ const resolved = resolveProvider("video");
512
+ if (resolved) {
513
+ log(chalk.dim(` ${provider} key not found. Using ${resolved.label} instead.`));
514
+ provider = resolved.name;
515
+ }
516
+ }
517
+ // Read image early so we can auto-detect aspect ratio before dry-run
518
+ let referenceImage;
519
+ let isImageToVideo = false;
520
+ if (options.image) {
521
+ const imagePath = resolve(process.cwd(), options.image);
522
+ const imageBuffer = await readFile(imagePath);
523
+ const ext = options.image.toLowerCase().split(".").pop();
524
+ const mimeTypes = {
525
+ jpg: "image/jpeg",
526
+ jpeg: "image/jpeg",
527
+ png: "image/png",
528
+ gif: "image/gif",
529
+ webp: "image/webp",
530
+ };
531
+ const mimeType = mimeTypes[ext || "png"] || "image/png";
532
+ referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
533
+ isImageToVideo = true;
534
+ // Auto-detect aspect ratio from image dimensions when not explicitly set
535
+ if (!options.ratio) {
536
+ const dimensions = imageSize(imageBuffer);
537
+ if (dimensions.width && dimensions.height) {
538
+ const ratio = dimensions.width / dimensions.height;
539
+ if (ratio > 1.2) {
540
+ options.ratio = "16:9";
541
+ }
542
+ else if (ratio < 0.8) {
543
+ options.ratio = "9:16";
544
+ }
545
+ else {
546
+ options.ratio = "1:1";
547
+ }
548
+ log(`Auto-detected aspect ratio: ${options.ratio} (${dimensions.width}x${dimensions.height})`);
549
+ }
550
+ }
551
+ }
552
+ // Default to 16:9 when no image and no explicit ratio
553
+ if (!options.ratio) {
554
+ options.ratio = "16:9";
555
+ }
556
+ // Veo and Runway only support 16:9 and 9:16 — clamp 1:1 to 16:9
557
+ if ((provider === "veo" || provider === "runway") && options.ratio === "1:1") {
558
+ log(`${provider} does not support 1:1 — falling back to 16:9`);
559
+ options.ratio = "16:9";
560
+ }
561
+ if (options.dryRun) {
562
+ outputResult({ dryRun: true, command: "generate video", params: { prompt, provider, duration: options.duration, ratio: options.ratio, image: options.image, mode: options.mode, negative: options.negative, resolution: options.resolution, veoModel: options.veoModel } });
563
+ return;
564
+ }
565
+ const envKeyMap = {
566
+ runway: "RUNWAY_API_SECRET",
567
+ kling: "KLING_API_KEY",
568
+ veo: "GOOGLE_API_KEY",
569
+ grok: "XAI_API_KEY",
570
+ };
571
+ const providerNameMap = {
572
+ runway: "Runway",
573
+ kling: "Kling",
574
+ veo: "Veo",
575
+ grok: "Grok",
576
+ };
577
+ const envKey = envKeyMap[provider];
578
+ const providerName = providerNameMap[provider];
579
+ const apiKey = await requireApiKey(envKey, providerName, options.apiKey);
580
+ // Runway gen4_turbo requires an input image; gen4.5 supports text-to-video
581
+ const runwayModel = options.runwayModel || "gen4.5";
582
+ if (provider === "runway" && !options.image && runwayModel !== "gen4.5") {
583
+ console.error(chalk.red(`Runway ${runwayModel} requires an input image. Use -i <image> or use gen4.5 for text-to-video.`));
584
+ console.error(chalk.dim("Example: vibe generate video \"prompt\" -p runway -i image.png -o out.mp4"));
585
+ process.exit(1);
586
+ }
587
+ const spinner = ora(`Initializing ${providerName}...`).start();
588
+ spinner.text = "Starting video generation...";
589
+ let result;
590
+ let finalResult;
591
+ if (provider === "runway") {
592
+ const runway = new RunwayProvider();
593
+ await runway.initialize({ apiKey });
594
+ result = await runway.generateVideo(prompt, {
595
+ prompt,
596
+ referenceImage,
597
+ model: runwayModel,
598
+ duration: parseInt(options.duration),
599
+ aspectRatio: options.ratio,
600
+ seed: options.seed ? parseInt(options.seed) : undefined,
601
+ });
602
+ if (result.status === "failed") {
603
+ spinner.fail(chalk.red(result.error || "Failed to start generation"));
604
+ process.exit(1);
605
+ }
606
+ console.log();
607
+ console.log(chalk.bold.cyan("Video Generation Started"));
608
+ console.log(chalk.dim("─".repeat(60)));
609
+ console.log(`Provider: ${chalk.bold(`Runway ${runwayModel}`)}`);
610
+ console.log(`Task ID: ${chalk.bold(result.id)}`);
611
+ if (!options.wait) {
612
+ spinner.succeed(chalk.green("Generation started"));
613
+ console.log();
614
+ console.log(chalk.dim("Check status with:"));
615
+ console.log(chalk.dim(` vibe generate video-status ${result.id} -p runway`));
616
+ console.log();
617
+ return;
618
+ }
619
+ spinner.text = "Generating video (this may take 1-2 minutes)...";
620
+ finalResult = await runway.waitForCompletion(result.id, (status) => {
621
+ if (status.progress !== undefined) {
622
+ spinner.text = `Generating video... ${status.progress}%`;
623
+ }
624
+ }, 300000);
625
+ }
626
+ else if (provider === "kling") {
627
+ const kling = new KlingProvider();
628
+ await kling.initialize({ apiKey });
629
+ if (!kling.isConfigured()) {
630
+ spinner.fail(chalk.red("Invalid API key format. Use ACCESS_KEY:SECRET_KEY"));
631
+ process.exit(1);
632
+ }
633
+ // Kling v2.x requires image URL, not base64 — auto-upload to ImgBB
634
+ let klingImage = referenceImage;
635
+ if (klingImage && klingImage.startsWith("data:")) {
636
+ spinner.text = "Uploading image to ImgBB for Kling...";
637
+ const imgbbKey = (await getApiKeyFromConfig("imgbb")) || process.env.IMGBB_API_KEY;
638
+ if (!imgbbKey) {
639
+ spinner.fail(chalk.red("Kling requires image URL. Set IMGBB_API_KEY for auto-upload."));
640
+ console.error(chalk.dim("Run: vibe setup --full to configure ImgBB"));
641
+ process.exit(1);
642
+ }
643
+ // Extract raw base64 from data URI
644
+ const base64Data = klingImage.split(",")[1];
645
+ const imageBuffer = Buffer.from(base64Data, "base64");
646
+ const uploadResult = await uploadToImgbb(imageBuffer, imgbbKey);
647
+ if (!uploadResult.success || !uploadResult.url) {
648
+ spinner.fail(chalk.red(`ImgBB upload failed: ${uploadResult.error}`));
649
+ process.exit(1);
650
+ }
651
+ klingImage = uploadResult.url;
652
+ spinner.text = "Starting video generation...";
653
+ }
654
+ result = await kling.generateVideo(prompt, {
655
+ prompt,
656
+ referenceImage: klingImage,
657
+ duration: parseInt(options.duration),
658
+ aspectRatio: options.ratio,
659
+ negativePrompt: options.negative,
660
+ mode: options.mode,
661
+ });
662
+ if (result.status === "failed") {
663
+ spinner.fail(chalk.red(result.error || "Failed to start generation"));
664
+ process.exit(1);
665
+ }
666
+ console.log();
667
+ console.log(chalk.bold.cyan("Video Generation Started"));
668
+ console.log(chalk.dim("─".repeat(60)));
669
+ console.log(`Provider: ${chalk.bold("Kling AI")}`);
670
+ console.log(`Task ID: ${chalk.bold(result.id)}`);
671
+ console.log(`Type: ${isImageToVideo ? "image2video" : "text2video"}`);
672
+ if (!options.wait) {
673
+ spinner.succeed(chalk.green("Generation started"));
674
+ console.log();
675
+ console.log(chalk.dim("Check status with:"));
676
+ console.log(chalk.dim(` vibe generate video-status ${result.id} -p kling${isImageToVideo ? " --type image2video" : ""}`));
677
+ console.log();
678
+ return;
679
+ }
680
+ spinner.text = "Generating video (this may take 2-5 minutes)...";
681
+ const taskType = isImageToVideo ? "image2video" : "text2video";
682
+ finalResult = await kling.waitForCompletion(result.id, taskType, (status) => {
683
+ spinner.text = `Generating video... ${status.status}`;
684
+ }, 600000);
685
+ }
686
+ else if (provider === "veo") {
687
+ const gemini = new GeminiProvider();
688
+ await gemini.initialize({ apiKey });
689
+ // Map Veo model alias to full model ID
690
+ const veoModelMap = {
691
+ "3.0": "veo-3.0-generate-preview",
692
+ "3.1": "veo-3.1-generate-preview",
693
+ "3.1-fast": "veo-3.1-fast-generate-preview",
694
+ };
695
+ const veoModel = veoModelMap[options.veoModel] || "veo-3.1-fast-generate-preview";
696
+ const veoDuration = parseInt(options.duration) <= 6 ? 6 : 8;
697
+ // Prepare last frame if provided
698
+ let lastFrame;
699
+ if (options.lastFrame) {
700
+ const lastFramePath = resolve(process.cwd(), options.lastFrame);
701
+ const lastFrameBuffer = await readFile(lastFramePath);
702
+ const ext = options.lastFrame.toLowerCase().split(".").pop();
703
+ const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext || "png"}`;
704
+ lastFrame = `data:${mimeType};base64,${lastFrameBuffer.toString("base64")}`;
705
+ }
706
+ // Prepare reference images if provided
707
+ let refImages;
708
+ if (options.refImages && options.refImages.length > 0) {
709
+ refImages = [];
710
+ for (const refPath of options.refImages.slice(0, 3)) {
711
+ const absRefPath = resolve(process.cwd(), refPath);
712
+ const refBuffer = await readFile(absRefPath);
713
+ const ext = refPath.toLowerCase().split(".").pop();
714
+ const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext || "png"}`;
715
+ refImages.push({ base64: refBuffer.toString("base64"), mimeType });
716
+ }
717
+ }
718
+ result = await gemini.generateVideo(prompt, {
719
+ prompt,
720
+ referenceImage,
721
+ duration: veoDuration,
722
+ aspectRatio: options.ratio,
723
+ model: veoModel,
724
+ negativePrompt: options.negative,
725
+ resolution: options.resolution,
726
+ lastFrame,
727
+ referenceImages: refImages,
728
+ personGeneration: options.person,
729
+ });
730
+ if (result.status === "failed") {
731
+ spinner.fail(chalk.red(result.error || "Failed to start generation"));
732
+ process.exit(1);
733
+ }
734
+ console.log();
735
+ console.log(chalk.bold.cyan("Video Generation Started"));
736
+ console.log(chalk.dim("─".repeat(60)));
737
+ console.log(`Provider: ${chalk.bold("Google Veo 3.1")}`);
738
+ console.log(`Task ID: ${chalk.bold(result.id)}`);
739
+ if (!options.wait) {
740
+ spinner.succeed(chalk.green("Generation started"));
741
+ console.log();
742
+ console.log(chalk.dim("Veo generation is synchronous - video URL available above"));
743
+ console.log();
744
+ return;
745
+ }
746
+ spinner.text = "Generating video (this may take 1-3 minutes)...";
747
+ finalResult = await gemini.waitForVideoCompletion(result.id, (status) => {
748
+ spinner.text = `Generating video... ${status.status}`;
749
+ }, 300000);
750
+ }
751
+ else if (provider === "grok") {
752
+ const grok = new GrokProvider();
753
+ await grok.initialize({ apiKey });
754
+ result = await grok.generateVideo(prompt, {
755
+ prompt,
756
+ referenceImage,
757
+ duration: parseInt(options.duration),
758
+ aspectRatio: options.ratio,
759
+ });
760
+ if (result.status === "failed") {
761
+ spinner.fail(chalk.red(result.error || "Failed to start generation"));
762
+ process.exit(1);
763
+ }
764
+ console.log();
765
+ console.log(chalk.bold.cyan("Video Generation Started"));
766
+ console.log(chalk.dim("─".repeat(60)));
767
+ console.log(`Provider: ${chalk.bold("xAI Grok Imagine")}`);
768
+ console.log(`Task ID: ${chalk.bold(result.id)}`);
769
+ if (!options.wait) {
770
+ spinner.succeed(chalk.green("Generation started"));
771
+ console.log();
772
+ console.log(chalk.dim("Check status with:"));
773
+ console.log(chalk.dim(` vibe generate video-status ${result.id} -p grok`));
774
+ console.log();
775
+ return;
776
+ }
777
+ spinner.text = "Generating video (this may take 1-3 minutes)...";
778
+ finalResult = await grok.waitForCompletion(result.id, (status) => {
779
+ spinner.text = `Generating video... ${status.status}`;
780
+ }, 300000);
781
+ }
782
+ if (!finalResult || finalResult.status !== "completed") {
783
+ spinner.fail(chalk.red(finalResult?.error || "Generation failed"));
784
+ process.exit(1);
785
+ }
786
+ spinner.succeed(chalk.green("Video generated"));
787
+ if (isJsonMode()) {
788
+ let outputPath;
789
+ if (options.output && finalResult.videoUrl) {
790
+ const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
791
+ outputPath = resolve(process.cwd(), options.output);
792
+ await writeFile(outputPath, buffer);
793
+ }
794
+ outputResult({ success: true, provider, taskId: result?.id, videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath });
795
+ return;
796
+ }
797
+ console.log();
798
+ if (finalResult.videoUrl) {
799
+ console.log(`Video URL: ${finalResult.videoUrl}`);
800
+ }
801
+ if (finalResult.duration) {
802
+ console.log(`Duration: ${finalResult.duration}s`);
803
+ }
804
+ console.log();
805
+ if (options.output && finalResult.videoUrl) {
806
+ const downloadSpinner = ora("Downloading video...").start();
807
+ try {
808
+ const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
809
+ const outputPath = resolve(process.cwd(), options.output);
810
+ await writeFile(outputPath, buffer);
811
+ downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
812
+ }
813
+ catch (err) {
814
+ downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
815
+ }
816
+ }
817
+ }
818
+ catch (error) {
819
+ exitWithError(apiError(`Video generation failed: ${error.message}`));
820
+ }
821
+ });
822
+ // ============================================================================
823
+ // 3. Speech (was: ai tts)
824
+ // ============================================================================
825
+ generateCommand
826
+ .command("speech")
827
+ .alias("tts")
828
+ .description("Generate speech from text using ElevenLabs")
829
+ .argument("[text]", "Text to convert to speech (interactive if omitted)")
830
+ .option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)")
831
+ .option("-o, --output <path>", "Output audio file path", "output.mp3")
832
+ .option("-v, --voice <id>", "Voice ID (default: Rachel)", "21m00Tcm4TlvDq8ikWAM")
833
+ .option("--list-voices", "List available voices")
834
+ .option("--fit-duration <seconds>", "Speed up audio to fit target duration (via FFmpeg atempo)", parseFloat)
835
+ .option("--dry-run", "Preview parameters without executing")
836
+ .action(async (text, options) => {
837
+ try {
838
+ // Interactive prompt if no argument provided
839
+ if (!text) {
840
+ if (hasTTY()) {
841
+ text = await promptText(chalk.cyan("What text to speak? "));
842
+ if (!text?.trim()) {
843
+ console.error(chalk.red("Text is required."));
844
+ return;
845
+ }
846
+ }
847
+ else {
848
+ console.error(chalk.red("Text argument is required."));
849
+ return;
850
+ }
851
+ }
852
+ rejectControlChars(text);
853
+ if (options.dryRun) {
854
+ outputResult({ dryRun: true, command: "generate speech", params: { text, voice: options.voice, output: options.output } });
855
+ return;
856
+ }
857
+ const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
858
+ const elevenlabs = new ElevenLabsProvider();
859
+ await elevenlabs.initialize({ apiKey });
860
+ // List voices mode
861
+ if (options.listVoices) {
862
+ const spinner = ora("Fetching voices...").start();
863
+ const voices = await elevenlabs.getVoices();
864
+ spinner.succeed(chalk.green(`Found ${voices.length} voices`));
865
+ console.log();
866
+ console.log(chalk.bold.cyan("Available Voices"));
867
+ console.log(chalk.dim("─".repeat(60)));
868
+ for (const voice of voices) {
869
+ console.log();
870
+ console.log(`${chalk.bold(voice.name)} ${chalk.dim(`(${voice.voice_id})`)}`);
871
+ console.log(` Category: ${voice.category}`);
872
+ if (voice.labels) {
873
+ const labels = Object.entries(voice.labels)
874
+ .map(([k, v]) => `${k}: ${v}`)
875
+ .join(", ");
876
+ console.log(` ${chalk.dim(labels)}`);
877
+ }
878
+ }
879
+ console.log();
880
+ return;
881
+ }
882
+ const spinner = ora("Generating speech...").start();
883
+ const result = await elevenlabs.textToSpeech(text, {
884
+ voiceId: options.voice,
885
+ });
886
+ if (!result.success || !result.audioBuffer) {
887
+ spinner.fail(chalk.red(result.error || "TTS generation failed"));
888
+ process.exit(1);
889
+ }
890
+ const outputPath = resolve(process.cwd(), options.output);
891
+ await writeFile(outputPath, result.audioBuffer);
892
+ spinner.succeed(chalk.green("Speech generated"));
893
+ // Post-process: fit to target duration via atempo
894
+ if (options.fitDuration && options.fitDuration > 0) {
895
+ const { ffprobeDuration, execSafe } = await import("../utils/exec-safe.js");
896
+ const actualDuration = await ffprobeDuration(outputPath);
897
+ if (actualDuration > options.fitDuration) {
898
+ const tempo = actualDuration / options.fitDuration;
899
+ if (tempo > 2.0) {
900
+ log(chalk.yellow(`Warning: Audio is ${tempo.toFixed(1)}x longer than target — would sound unnatural. Skipping tempo adjustment.`));
901
+ }
902
+ else {
903
+ const fitSpinner = ora(`Adjusting tempo (${tempo.toFixed(3)}x) to fit ${options.fitDuration}s...`).start();
904
+ const tempPath = outputPath.replace(/(\.\w+)$/, `.tempo$1`);
905
+ try {
906
+ await execSafe("ffmpeg", [
907
+ "-y", "-i", outputPath,
908
+ "-filter:a", `atempo=${tempo.toFixed(4)}`,
909
+ "-vn", tempPath,
910
+ ]);
911
+ const { rename } = await import("node:fs/promises");
912
+ await rename(tempPath, outputPath);
913
+ fitSpinner.succeed(chalk.green(`Adjusted to fit ${options.fitDuration}s (${tempo.toFixed(3)}x speed)`));
914
+ }
915
+ catch (err) {
916
+ fitSpinner.fail(chalk.yellow("Tempo adjustment failed — keeping original audio"));
917
+ }
918
+ }
919
+ }
920
+ else {
921
+ log(chalk.dim(`Audio (${actualDuration.toFixed(2)}s) already fits within ${options.fitDuration}s`));
922
+ }
923
+ }
924
+ if (isJsonMode()) {
925
+ outputResult({ success: true, characterCount: result.characterCount, outputPath });
926
+ return;
927
+ }
928
+ console.log();
929
+ console.log(chalk.dim(`Characters: ${result.characterCount}`));
930
+ console.log(chalk.green(`Saved to: ${outputPath}`));
931
+ console.log();
932
+ }
933
+ catch (error) {
934
+ console.error(chalk.red("TTS generation failed"));
935
+ console.error(error);
936
+ process.exit(1);
937
+ }
938
+ });
939
+ // ============================================================================
940
+ // 4. Sound Effect (was: ai sfx)
941
+ // Note: -p is reserved for --provider; --prompt-influence is long-only
942
+ // ============================================================================
943
+ generateCommand
944
+ .command("sound-effect")
945
+ .description("Generate sound effect using ElevenLabs")
946
+ .argument("<prompt>", "Description of the sound effect")
947
+ .option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)")
948
+ .option("-o, --output <path>", "Output audio file path", "sound-effect.mp3")
949
+ .option("-d, --duration <seconds>", "Duration in seconds (0.5-22, default: auto)")
950
+ .option("--prompt-influence <value>", "Prompt influence (0-1, default: 0.3)")
951
+ .option("--dry-run", "Preview parameters without executing")
952
+ .action(async (prompt, options) => {
953
+ try {
954
+ rejectControlChars(prompt);
955
+ if (options.dryRun) {
956
+ outputResult({ dryRun: true, command: "generate sound-effect", params: { prompt, duration: options.duration, promptInfluence: options.promptInfluence, output: options.output } });
957
+ return;
958
+ }
959
+ const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
960
+ const spinner = ora("Generating sound effect...").start();
961
+ const elevenlabs = new ElevenLabsProvider();
962
+ await elevenlabs.initialize({ apiKey });
963
+ const result = await elevenlabs.generateSoundEffect(prompt, {
964
+ duration: options.duration ? parseFloat(options.duration) : undefined,
965
+ promptInfluence: options.promptInfluence ? parseFloat(options.promptInfluence) : undefined,
966
+ });
967
+ if (!result.success || !result.audioBuffer) {
968
+ spinner.fail(chalk.red(result.error || "Sound effect generation failed"));
969
+ process.exit(1);
970
+ }
971
+ const outputPath = resolve(process.cwd(), options.output);
972
+ await writeFile(outputPath, result.audioBuffer);
973
+ spinner.succeed(chalk.green("Sound effect generated"));
974
+ if (isJsonMode()) {
975
+ outputResult({ success: true, outputPath });
976
+ return;
977
+ }
978
+ console.log(chalk.green(`Saved to: ${outputPath}`));
979
+ console.log();
980
+ }
981
+ catch (error) {
982
+ console.error(chalk.red("Sound effect generation failed"));
983
+ console.error(error);
984
+ process.exit(1);
985
+ }
986
+ });
987
+ // ============================================================================
988
+ // 5. Music
989
+ // ============================================================================
990
+ generateCommand
991
+ .command("music")
992
+ .description("Generate background music from a text prompt (ElevenLabs or Replicate MusicGen)")
993
+ .argument("<prompt>", "Description of the music to generate")
994
+ .option("-p, --provider <provider>", "Provider: elevenlabs (default, up to 10min), replicate (MusicGen, max 30s)", "elevenlabs")
995
+ .option("-k, --api-key <key>", "API key (or set ELEVENLABS_API_KEY / REPLICATE_API_TOKEN env)")
996
+ .option("-d, --duration <seconds>", "Duration in seconds (elevenlabs: 3-600, replicate: 1-30)", "8")
997
+ .option("--instrumental", "Force instrumental music, no vocals (ElevenLabs only)")
998
+ .option("-m, --melody <file>", "Reference melody audio file for conditioning (Replicate only)")
999
+ .option("--model <model>", "Model variant (Replicate only): large, stereo-large, melody-large, stereo-melody-large", "stereo-large")
1000
+ .option("-o, --output <path>", "Output audio file path", "music.mp3")
1001
+ .option("--no-wait", "Don't wait for generation to complete (Replicate async mode)")
1002
+ .option("--dry-run", "Preview parameters without executing")
1003
+ .action(async (prompt, options) => {
1004
+ try {
1005
+ rejectControlChars(prompt);
1006
+ const provider = (options.provider || "elevenlabs").toLowerCase();
1007
+ if (options.dryRun) {
1008
+ outputResult({ dryRun: true, command: "generate music", params: { prompt, provider, duration: options.duration, model: options.model, output: options.output, instrumental: options.instrumental } });
1009
+ return;
1010
+ }
1011
+ if (provider === "elevenlabs") {
1012
+ // ElevenLabs Music API — synchronous, up to 10 minutes
1013
+ const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
1014
+ const elevenlabs = new ElevenLabsProvider();
1015
+ await elevenlabs.initialize({ apiKey });
1016
+ const duration = Math.max(3, Math.min(600, parseFloat(options.duration)));
1017
+ const spinner = ora(`Generating music (${duration}s)...`).start();
1018
+ const result = await elevenlabs.generateMusic(prompt, {
1019
+ duration,
1020
+ forceInstrumental: options.instrumental || false,
1021
+ });
1022
+ if (!result.success || !result.audioBuffer) {
1023
+ spinner.fail(chalk.red(result.error || "Music generation failed"));
1024
+ process.exit(1);
1025
+ }
1026
+ const outputPath = resolve(process.cwd(), options.output);
1027
+ await writeFile(outputPath, result.audioBuffer);
1028
+ spinner.succeed(chalk.green("Music generated successfully"));
1029
+ if (isJsonMode()) {
1030
+ outputResult({ success: true, provider: "elevenlabs", outputPath, duration });
1031
+ return;
1032
+ }
1033
+ console.log();
1034
+ console.log(`Saved to: ${chalk.bold(outputPath)}`);
1035
+ console.log(`Duration: ${duration}s`);
1036
+ console.log(`Provider: ElevenLabs (music_v1)`);
1037
+ if (options.instrumental)
1038
+ console.log(`Mode: Instrumental`);
1039
+ console.log();
1040
+ }
1041
+ else {
1042
+ // Replicate MusicGen — async, max 30 seconds
1043
+ const apiKey = await requireApiKey("REPLICATE_API_TOKEN", "Replicate", options.apiKey);
1044
+ const replicate = new ReplicateProvider();
1045
+ await replicate.initialize({ apiKey });
1046
+ const spinner = ora("Starting music generation...").start();
1047
+ const duration = Math.max(1, Math.min(30, parseFloat(options.duration)));
1048
+ // If melody file provided, upload it first
1049
+ if (options.melody) {
1050
+ spinner.text = "Uploading melody reference...";
1051
+ const absPath = resolve(process.cwd(), options.melody);
1052
+ if (!existsSync(absPath)) {
1053
+ spinner.fail(chalk.red(`Melody file not found: ${options.melody}`));
1054
+ process.exit(1);
1055
+ }
1056
+ console.log(chalk.yellow("Note: Melody conditioning requires a publicly accessible URL"));
1057
+ console.log(chalk.yellow("Please upload your melody file and provide the URL"));
1058
+ process.exit(1);
1059
+ }
1060
+ const result = await replicate.generateMusic(prompt, {
1061
+ duration,
1062
+ model: options.model,
1063
+ });
1064
+ if (!result.success || !result.taskId) {
1065
+ spinner.fail(chalk.red(result.error || "Music generation failed"));
1066
+ process.exit(1);
1067
+ }
1068
+ if (!options.wait) {
1069
+ spinner.succeed(chalk.green("Music generation started"));
1070
+ console.log();
1071
+ console.log(`Task ID: ${chalk.bold(result.taskId)}`);
1072
+ console.log(chalk.dim("Check status with: vibe generate music-status " + result.taskId));
1073
+ return;
1074
+ }
1075
+ spinner.text = "Generating music (this may take a few minutes)...";
1076
+ const finalResult = await replicate.waitForMusic(result.taskId);
1077
+ if (!finalResult.success || !finalResult.audioUrl) {
1078
+ spinner.fail(chalk.red(finalResult.error || "Music generation failed"));
1079
+ process.exit(1);
1080
+ }
1081
+ spinner.text = "Downloading generated audio...";
1082
+ const response = await fetch(finalResult.audioUrl);
1083
+ if (!response.ok) {
1084
+ spinner.fail(chalk.red("Failed to download generated audio"));
1085
+ process.exit(1);
1086
+ }
1087
+ const audioBuffer = Buffer.from(await response.arrayBuffer());
1088
+ const outputPath = resolve(process.cwd(), options.output);
1089
+ await writeFile(outputPath, audioBuffer);
1090
+ spinner.succeed(chalk.green("Music generated successfully"));
1091
+ if (isJsonMode()) {
1092
+ outputResult({ success: true, provider: "replicate", taskId: result.taskId, audioUrl: finalResult.audioUrl, outputPath });
1093
+ return;
1094
+ }
1095
+ console.log();
1096
+ console.log(`Saved to: ${chalk.bold(outputPath)}`);
1097
+ console.log(`Duration: ${duration}s`);
1098
+ console.log(`Model: ${options.model}`);
1099
+ console.log();
1100
+ }
1101
+ }
1102
+ catch (error) {
1103
+ console.error(chalk.red("Music generation failed"));
1104
+ console.error(error);
1105
+ process.exit(1);
1106
+ }
1107
+ });
1108
+ // ============================================================================
1109
+ // 6. Music Status
1110
+ // ============================================================================
1111
+ generateCommand
1112
+ .command("music-status", { hidden: true })
1113
+ .description("Check music generation status")
1114
+ .argument("<task-id>", "Task ID from music generation")
1115
+ .option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)")
1116
+ .action(async (taskId, options) => {
1117
+ try {
1118
+ const apiKey = await requireApiKey("REPLICATE_API_TOKEN", "Replicate", options.apiKey);
1119
+ const replicate = new ReplicateProvider();
1120
+ await replicate.initialize({ apiKey });
1121
+ const result = await replicate.getMusicStatus(taskId);
1122
+ if (isJsonMode()) {
1123
+ const status = result.audioUrl ? "completed" : result.error ? "failed" : "processing";
1124
+ outputResult({ success: true, taskId, status, audioUrl: result.audioUrl, error: result.error });
1125
+ return;
1126
+ }
1127
+ console.log();
1128
+ console.log(chalk.bold.cyan("Music Generation Status"));
1129
+ console.log(chalk.dim("─".repeat(60)));
1130
+ console.log(`Task ID: ${taskId}`);
1131
+ if (result.audioUrl) {
1132
+ console.log(`Status: ${chalk.green("completed")}`);
1133
+ console.log(`Audio URL: ${result.audioUrl}`);
1134
+ }
1135
+ else if (result.error) {
1136
+ console.log(`Status: ${chalk.red("failed")}`);
1137
+ console.log(`Error: ${result.error}`);
1138
+ }
1139
+ else {
1140
+ console.log(`Status: ${chalk.yellow("processing")}`);
1141
+ }
1142
+ console.log();
1143
+ }
1144
+ catch (error) {
1145
+ console.error(chalk.red("Failed to get music status"));
1146
+ console.error(error);
1147
+ process.exit(1);
1148
+ }
1149
+ });
1150
+ // ============================================================================
1151
+ // 7. Storyboard
1152
+ // ============================================================================
1153
+ generateCommand
1154
+ .command("storyboard")
1155
+ .description("Generate video storyboard from content using Claude")
1156
+ .argument("<content>", "Content to analyze (text or file path)")
1157
+ .option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)")
1158
+ .option("-o, --output <path>", "Output JSON file path")
1159
+ .option("-d, --duration <sec>", "Target total duration in seconds")
1160
+ .option("-f, --file", "Treat content argument as file path")
1161
+ .option("-c, --creativity <level>", "Creativity level: low (default, consistent) or high (varied, unexpected)", "low")
1162
+ .option("--dry-run", "Preview parameters without executing")
1163
+ .action(async (content, options) => {
1164
+ try {
1165
+ rejectControlChars(content);
1166
+ // Validate creativity level
1167
+ const creativity = options.creativity?.toLowerCase();
1168
+ if (creativity && creativity !== "low" && creativity !== "high") {
1169
+ console.error(chalk.red("Invalid creativity level. Use 'low' or 'high'."));
1170
+ process.exit(1);
1171
+ }
1172
+ let textContent = content;
1173
+ if (options.file) {
1174
+ const filePath = resolve(process.cwd(), content);
1175
+ textContent = await readFile(filePath, "utf-8");
1176
+ }
1177
+ if (options.dryRun) {
1178
+ outputResult({ dryRun: true, command: "generate storyboard", params: { content: textContent.substring(0, 200), duration: options.duration, creativity } });
1179
+ return;
1180
+ }
1181
+ const apiKey = await requireApiKey("ANTHROPIC_API_KEY", "Anthropic", options.apiKey);
1182
+ const spinnerText = creativity === "high"
1183
+ ? "Analyzing content with high creativity..."
1184
+ : "Analyzing content...";
1185
+ const spinner = ora(spinnerText).start();
1186
+ const claude = new ClaudeProvider();
1187
+ await claude.initialize({ apiKey });
1188
+ const segments = await claude.analyzeContent(textContent, options.duration ? parseFloat(options.duration) : undefined, { creativity: creativity });
1189
+ if (segments.length === 0) {
1190
+ spinner.fail(chalk.red("Could not generate storyboard"));
1191
+ process.exit(1);
1192
+ }
1193
+ spinner.succeed(chalk.green(`Generated ${segments.length} segments`));
1194
+ for (const seg of segments) {
1195
+ seg.description = sanitizeLLMResponse(seg.description);
1196
+ if (seg.visuals)
1197
+ seg.visuals = sanitizeLLMResponse(seg.visuals);
1198
+ }
1199
+ if (options.output) {
1200
+ const outputPath = resolve(process.cwd(), options.output);
1201
+ await writeFile(outputPath, JSON.stringify(segments, null, 2), "utf-8");
1202
+ if (isJsonMode()) {
1203
+ outputResult({ success: true, segmentCount: segments.length, segments, outputPath });
1204
+ return;
1205
+ }
1206
+ }
1207
+ else if (isJsonMode()) {
1208
+ outputResult({ success: true, segmentCount: segments.length, segments });
1209
+ return;
1210
+ }
1211
+ console.log();
1212
+ console.log(chalk.bold.cyan("Storyboard"));
1213
+ console.log(chalk.dim("─".repeat(60)));
1214
+ for (const seg of segments) {
1215
+ console.log();
1216
+ console.log(chalk.yellow(`[${seg.index + 1}] ${formatTime(seg.startTime)} - ${formatTime(seg.startTime + seg.duration)}`));
1217
+ console.log(` ${seg.description}`);
1218
+ console.log(chalk.dim(` Visuals: ${seg.visuals}`));
1219
+ if (seg.audio) {
1220
+ console.log(chalk.dim(` Audio: ${seg.audio}`));
1221
+ }
1222
+ if (seg.textOverlays && seg.textOverlays.length > 0) {
1223
+ console.log(chalk.dim(` Text: ${seg.textOverlays.join(", ")}`));
1224
+ }
1225
+ }
1226
+ console.log();
1227
+ if (options.output) {
1228
+ console.log(chalk.green(`Saved to: ${resolve(process.cwd(), options.output)}`));
1229
+ }
1230
+ }
1231
+ catch (error) {
1232
+ console.error(chalk.red("Storyboard generation failed"));
1233
+ console.error(error);
1234
+ process.exit(1);
1235
+ }
1236
+ });
1237
+ // ============================================================================
1238
+ // 8. Motion (delegated to registerMotionCommand)
1239
+ // ============================================================================
1240
+ registerMotionCommand(generateCommand);
1241
+ // ============================================================================
1242
+ // 9. Thumbnail
1243
+ // ============================================================================
1244
+ generateCommand
1245
+ .command("thumbnail")
1246
+ .description("Generate video thumbnail (DALL-E) or extract best frame from video (Gemini)")
1247
+ .argument("[description]", "Thumbnail description (for DALL-E generation)")
1248
+ .option("-k, --api-key <key>", "API key (OpenAI for generation, Google for best-frame)")
1249
+ .option("-o, --output <path>", "Output file path")
1250
+ .option("-s, --style <style>", "Platform style: youtube, instagram, tiktok, twitter")
1251
+ .option("--best-frame <video>", "Extract best thumbnail frame from video using Gemini AI")
1252
+ .option("--prompt <prompt>", "Custom prompt for best-frame analysis")
1253
+ .option("--model <model>", "Gemini model: flash, latest, pro (default: flash)", "flash")
1254
+ .action(async (description, options) => {
1255
+ try {
1256
+ if (description)
1257
+ rejectControlChars(description);
1258
+ // Best-frame mode: analyze video with Gemini and extract frame
1259
+ if (options.bestFrame) {
1260
+ const absVideoPath = resolve(process.cwd(), options.bestFrame);
1261
+ if (!existsSync(absVideoPath)) {
1262
+ console.error(chalk.red(`Video not found: ${absVideoPath}`));
1263
+ process.exit(1);
1264
+ }
1265
+ if (!commandExists("ffmpeg")) {
1266
+ console.error(chalk.red("FFmpeg not found. Please install FFmpeg."));
1267
+ process.exit(1);
1268
+ }
1269
+ const apiKey = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
1270
+ const name = basename(options.bestFrame, extname(options.bestFrame));
1271
+ const outputPath = options.output || `${name}-thumbnail.png`;
1272
+ const spinner = ora("Analyzing video for best frame...").start();
1273
+ const result = await executeThumbnailBestFrame({
1274
+ videoPath: absVideoPath,
1275
+ outputPath: resolve(process.cwd(), outputPath),
1276
+ prompt: options.prompt,
1277
+ model: options.model,
1278
+ apiKey,
1279
+ });
1280
+ if (!result.success) {
1281
+ spinner.fail(chalk.red(result.error || "Best frame extraction failed"));
1282
+ process.exit(1);
1283
+ }
1284
+ spinner.succeed(chalk.green("Best frame extracted"));
1285
+ if (isJsonMode()) {
1286
+ outputResult({ success: true, timestamp: result.timestamp, reason: result.reason, outputPath: result.outputPath });
1287
+ return;
1288
+ }
1289
+ console.log();
1290
+ console.log(chalk.bold.cyan("Best Frame Result"));
1291
+ console.log(chalk.dim("─".repeat(60)));
1292
+ console.log(`Timestamp: ${chalk.bold(result.timestamp.toFixed(2))}s`);
1293
+ if (result.reason)
1294
+ console.log(`Reason: ${chalk.dim(result.reason)}`);
1295
+ console.log(`Output: ${chalk.green(result.outputPath)}`);
1296
+ console.log();
1297
+ return;
1298
+ }
1299
+ // Generation mode: create thumbnail with DALL-E
1300
+ if (!description) {
1301
+ console.error(chalk.red("Description required for thumbnail generation."));
1302
+ console.error(chalk.dim("Usage: vibe generate thumbnail <description> or vibe generate thumbnail --best-frame <video>"));
1303
+ process.exit(1);
1304
+ }
1305
+ const apiKey = await requireApiKey("OPENAI_API_KEY", "OpenAI", options.apiKey);
1306
+ const spinner = ora("Generating thumbnail...").start();
1307
+ const openaiImage = new OpenAIImageProvider();
1308
+ await openaiImage.initialize({ apiKey });
1309
+ const result = await openaiImage.generateThumbnail(description, options.style);
1310
+ if (!result.success || !result.images) {
1311
+ spinner.fail(chalk.red(result.error || "Thumbnail generation failed"));
1312
+ process.exit(1);
1313
+ }
1314
+ spinner.succeed(chalk.green("Thumbnail generated"));
1315
+ const img = result.images[0];
1316
+ if (isJsonMode()) {
1317
+ let outputPath;
1318
+ if (options.output) {
1319
+ let buffer;
1320
+ if (img.url) {
1321
+ const response = await fetch(img.url);
1322
+ buffer = Buffer.from(await response.arrayBuffer());
1323
+ }
1324
+ else if (img.base64) {
1325
+ buffer = Buffer.from(img.base64, "base64");
1326
+ }
1327
+ else {
1328
+ throw new Error("No image data available");
1329
+ }
1330
+ outputPath = resolve(process.cwd(), options.output);
1331
+ await mkdir(dirname(outputPath), { recursive: true });
1332
+ await writeFile(outputPath, buffer);
1333
+ }
1334
+ outputResult({ success: true, imageUrl: img.url, outputPath });
1335
+ return;
1336
+ }
1337
+ console.log();
1338
+ console.log(chalk.bold.cyan("Generated Thumbnail"));
1339
+ console.log(chalk.dim("─".repeat(60)));
1340
+ console.log(`URL: ${img.url}`);
1341
+ if (img.revisedPrompt) {
1342
+ console.log(chalk.dim(`Prompt: ${img.revisedPrompt.slice(0, 100)}...`));
1343
+ }
1344
+ console.log();
1345
+ // Save if output specified
1346
+ if (options.output) {
1347
+ const saveSpinner = ora("Saving thumbnail...").start();
1348
+ try {
1349
+ let buffer;
1350
+ if (img.url) {
1351
+ const response = await fetch(img.url);
1352
+ buffer = Buffer.from(await response.arrayBuffer());
1353
+ }
1354
+ else if (img.base64) {
1355
+ buffer = Buffer.from(img.base64, "base64");
1356
+ }
1357
+ else {
1358
+ throw new Error("No image data available");
1359
+ }
1360
+ const outputPath = resolve(process.cwd(), options.output);
1361
+ await mkdir(dirname(outputPath), { recursive: true });
1362
+ await writeFile(outputPath, buffer);
1363
+ saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
1364
+ }
1365
+ catch (err) {
1366
+ saveSpinner.fail(chalk.red("Failed to save thumbnail"));
1367
+ }
1368
+ }
1369
+ }
1370
+ catch (error) {
1371
+ console.error(chalk.red("Thumbnail generation failed"));
1372
+ console.error(error);
1373
+ process.exit(1);
1374
+ }
1375
+ });
1376
+ // ============================================================================
1377
+ // 10. Background
1378
+ // ============================================================================
1379
+ generateCommand
1380
+ .command("background")
1381
+ .description("Generate video background using DALL-E")
1382
+ .argument("<description>", "Background description")
1383
+ .option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)")
1384
+ .option("-o, --output <path>", "Output file path (downloads image)")
1385
+ .option("-a, --aspect <ratio>", "Aspect ratio: 16:9, 9:16, 1:1", "16:9")
1386
+ .option("--dry-run", "Preview parameters without executing")
1387
+ .action(async (description, options) => {
1388
+ try {
1389
+ rejectControlChars(description);
1390
+ if (options.dryRun) {
1391
+ outputResult({ dryRun: true, command: "generate background", params: { description, aspect: options.aspect, output: options.output } });
1392
+ return;
1393
+ }
1394
+ const apiKey = await requireApiKey("OPENAI_API_KEY", "OpenAI", options.apiKey);
1395
+ const spinner = ora("Generating background...").start();
1396
+ const openaiImage = new OpenAIImageProvider();
1397
+ await openaiImage.initialize({ apiKey });
1398
+ const result = await openaiImage.generateBackground(description, options.aspect);
1399
+ if (!result.success || !result.images) {
1400
+ spinner.fail(chalk.red(result.error || "Background generation failed"));
1401
+ process.exit(1);
1402
+ }
1403
+ spinner.succeed(chalk.green("Background generated"));
1404
+ const img = result.images[0];
1405
+ if (isJsonMode()) {
1406
+ let outputPath;
1407
+ if (options.output) {
1408
+ let buffer;
1409
+ if (img.url) {
1410
+ const response = await fetch(img.url);
1411
+ buffer = Buffer.from(await response.arrayBuffer());
1412
+ }
1413
+ else if (img.base64) {
1414
+ buffer = Buffer.from(img.base64, "base64");
1415
+ }
1416
+ else {
1417
+ throw new Error("No image data available");
1418
+ }
1419
+ outputPath = resolve(process.cwd(), options.output);
1420
+ await mkdir(dirname(outputPath), { recursive: true });
1421
+ await writeFile(outputPath, buffer);
1422
+ }
1423
+ outputResult({ success: true, imageUrl: img.url, outputPath });
1424
+ return;
1425
+ }
1426
+ console.log();
1427
+ console.log(chalk.bold.cyan("Generated Background"));
1428
+ console.log(chalk.dim("─".repeat(60)));
1429
+ console.log(`Image: ${img.url || "(base64 data)"}`);
1430
+ if (img.revisedPrompt) {
1431
+ console.log(chalk.dim(`Prompt: ${img.revisedPrompt.slice(0, 100)}...`));
1432
+ }
1433
+ console.log();
1434
+ // Save if output specified
1435
+ if (options.output) {
1436
+ const saveSpinner = ora("Saving background...").start();
1437
+ try {
1438
+ let buffer;
1439
+ if (img.url) {
1440
+ const response = await fetch(img.url);
1441
+ buffer = Buffer.from(await response.arrayBuffer());
1442
+ }
1443
+ else if (img.base64) {
1444
+ buffer = Buffer.from(img.base64, "base64");
1445
+ }
1446
+ else {
1447
+ throw new Error("No image data available");
1448
+ }
1449
+ const outputPath = resolve(process.cwd(), options.output);
1450
+ await mkdir(dirname(outputPath), { recursive: true });
1451
+ await writeFile(outputPath, buffer);
1452
+ saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
1453
+ }
1454
+ catch (err) {
1455
+ saveSpinner.fail(chalk.red("Failed to save background"));
1456
+ }
1457
+ }
1458
+ }
1459
+ catch (error) {
1460
+ console.error(chalk.red("Background generation failed"));
1461
+ console.error(error);
1462
+ process.exit(1);
1463
+ }
1464
+ });
1465
+ // ============================================================================
1466
+ // 11. Video Status (merged: ai video-status + ai kling-status)
1467
+ // ============================================================================
1468
+ generateCommand
1469
+ .command("video-status", { hidden: true })
1470
+ .description("Check video generation status (Grok, Runway, or Kling)")
1471
+ .argument("<task-id>", "Task ID from video generation")
1472
+ .option("-p, --provider <provider>", "Provider: grok, runway, kling", "grok")
1473
+ .option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY env)")
1474
+ .option("-t, --type <type>", "Task type: text2video or image2video (Kling only)", "text2video")
1475
+ .option("-w, --wait", "Wait for completion")
1476
+ .option("-o, --output <path>", "Download video when complete")
1477
+ .action(async (taskId, options) => {
1478
+ try {
1479
+ const provider = (options.provider || "grok").toLowerCase();
1480
+ if (provider === "grok") {
1481
+ const apiKey = await requireApiKey("XAI_API_KEY", "xAI", options.apiKey);
1482
+ const spinner = ora("Checking status...").start();
1483
+ const grok = new GrokProvider();
1484
+ await grok.initialize({ apiKey });
1485
+ let result = await grok.getGenerationStatus(taskId);
1486
+ if (options.wait && result.status !== "completed" && result.status !== "failed") {
1487
+ spinner.text = "Waiting for completion...";
1488
+ result = await grok.waitForCompletion(taskId, (status) => {
1489
+ spinner.text = `Generating... ${status.status}`;
1490
+ });
1491
+ }
1492
+ spinner.stop();
1493
+ if (isJsonMode()) {
1494
+ let outputPath;
1495
+ if (options.output && result.videoUrl) {
1496
+ const buffer = await downloadVideo(result.videoUrl);
1497
+ outputPath = resolve(process.cwd(), options.output);
1498
+ await writeFile(outputPath, buffer);
1499
+ }
1500
+ outputResult({ success: true, taskId, provider: "grok", status: result.status, videoUrl: result.videoUrl, error: result.error, outputPath });
1501
+ return;
1502
+ }
1503
+ console.log();
1504
+ console.log(chalk.bold.cyan("Generation Status"));
1505
+ console.log(chalk.dim("─".repeat(60)));
1506
+ console.log(`Task ID: ${taskId}`);
1507
+ console.log(`Provider: Grok Imagine`);
1508
+ console.log(`Status: ${getStatusColor(result.status)}`);
1509
+ if (result.videoUrl) {
1510
+ console.log(`Video URL: ${result.videoUrl}`);
1511
+ }
1512
+ if (result.error) {
1513
+ console.log(`Error: ${chalk.red(result.error)}`);
1514
+ }
1515
+ console.log();
1516
+ if (options.output && result.videoUrl) {
1517
+ const downloadSpinner = ora("Downloading video...").start();
1518
+ try {
1519
+ const buffer = await downloadVideo(result.videoUrl);
1520
+ const outputPath = resolve(process.cwd(), options.output);
1521
+ await writeFile(outputPath, buffer);
1522
+ downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
1523
+ }
1524
+ catch (err) {
1525
+ downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
1526
+ }
1527
+ }
1528
+ }
1529
+ else if (provider === "runway") {
1530
+ const apiKey = await requireApiKey("RUNWAY_API_SECRET", "Runway", options.apiKey);
1531
+ const spinner = ora("Checking status...").start();
1532
+ const runway = new RunwayProvider();
1533
+ await runway.initialize({ apiKey });
1534
+ let result = await runway.getGenerationStatus(taskId);
1535
+ if (options.wait && result.status !== "completed" && result.status !== "failed" && result.status !== "cancelled") {
1536
+ spinner.text = "Waiting for completion...";
1537
+ result = await runway.waitForCompletion(taskId, (status) => {
1538
+ if (status.progress !== undefined) {
1539
+ spinner.text = `Generating... ${status.progress}%`;
1540
+ }
1541
+ });
1542
+ }
1543
+ spinner.stop();
1544
+ if (isJsonMode()) {
1545
+ let outputPath;
1546
+ if (options.output && result.videoUrl) {
1547
+ const buffer = await downloadVideo(result.videoUrl, apiKey);
1548
+ outputPath = resolve(process.cwd(), options.output);
1549
+ await writeFile(outputPath, buffer);
1550
+ }
1551
+ outputResult({ success: true, taskId, provider: "runway", status: result.status, videoUrl: result.videoUrl, progress: result.progress, error: result.error, outputPath });
1552
+ return;
1553
+ }
1554
+ console.log();
1555
+ console.log(chalk.bold.cyan("Generation Status"));
1556
+ console.log(chalk.dim("─".repeat(60)));
1557
+ console.log(`Task ID: ${taskId}`);
1558
+ console.log(`Provider: Runway`);
1559
+ console.log(`Status: ${getStatusColor(result.status)}`);
1560
+ if (result.progress !== undefined) {
1561
+ console.log(`Progress: ${result.progress}%`);
1562
+ }
1563
+ if (result.videoUrl) {
1564
+ console.log(`Video URL: ${result.videoUrl}`);
1565
+ }
1566
+ if (result.error) {
1567
+ console.log(`Error: ${chalk.red(result.error)}`);
1568
+ }
1569
+ console.log();
1570
+ if (options.output && result.videoUrl) {
1571
+ const downloadSpinner = ora("Downloading video...").start();
1572
+ try {
1573
+ const buffer = await downloadVideo(result.videoUrl, apiKey);
1574
+ const outputPath = resolve(process.cwd(), options.output);
1575
+ await writeFile(outputPath, buffer);
1576
+ downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
1577
+ }
1578
+ catch (err) {
1579
+ downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
1580
+ }
1581
+ }
1582
+ }
1583
+ else if (provider === "kling") {
1584
+ const apiKey = await requireApiKey("KLING_API_KEY", "Kling", options.apiKey);
1585
+ const spinner = ora("Checking status...").start();
1586
+ const kling = new KlingProvider();
1587
+ await kling.initialize({ apiKey });
1588
+ const taskType = options.type;
1589
+ let result = await kling.getGenerationStatus(taskId, taskType);
1590
+ if (options.wait && result.status !== "completed" && result.status !== "failed" && result.status !== "cancelled") {
1591
+ spinner.text = "Waiting for completion...";
1592
+ result = await kling.waitForCompletion(taskId, taskType, (status) => {
1593
+ spinner.text = `Generating... ${status.status}`;
1594
+ });
1595
+ }
1596
+ spinner.stop();
1597
+ if (isJsonMode()) {
1598
+ let outputPath;
1599
+ if (options.output && result.videoUrl) {
1600
+ const buffer = await downloadVideo(result.videoUrl, apiKey);
1601
+ outputPath = resolve(process.cwd(), options.output);
1602
+ await writeFile(outputPath, buffer);
1603
+ }
1604
+ outputResult({ success: true, taskId, provider: "kling", status: result.status, videoUrl: result.videoUrl, duration: result.duration, error: result.error, outputPath });
1605
+ return;
1606
+ }
1607
+ console.log();
1608
+ console.log(chalk.bold.cyan("Generation Status"));
1609
+ console.log(chalk.dim("─".repeat(60)));
1610
+ console.log(`Task ID: ${taskId}`);
1611
+ console.log(`Provider: Kling`);
1612
+ console.log(`Type: ${taskType}`);
1613
+ console.log(`Status: ${getStatusColor(result.status)}`);
1614
+ if (result.videoUrl) {
1615
+ console.log(`Video URL: ${result.videoUrl}`);
1616
+ }
1617
+ if (result.duration) {
1618
+ console.log(`Duration: ${result.duration}s`);
1619
+ }
1620
+ if (result.error) {
1621
+ console.log(`Error: ${chalk.red(result.error)}`);
1622
+ }
1623
+ console.log();
1624
+ if (options.output && result.videoUrl) {
1625
+ const downloadSpinner = ora("Downloading video...").start();
1626
+ try {
1627
+ const buffer = await downloadVideo(result.videoUrl, apiKey);
1628
+ const outputPath = resolve(process.cwd(), options.output);
1629
+ await writeFile(outputPath, buffer);
1630
+ downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
1631
+ }
1632
+ catch (err) {
1633
+ downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
1634
+ }
1635
+ }
1636
+ }
1637
+ else {
1638
+ console.error(chalk.red(`Invalid provider: ${provider}. Use grok, runway, or kling.`));
1639
+ process.exit(1);
1640
+ }
1641
+ }
1642
+ catch (error) {
1643
+ console.error(chalk.red("Failed to get status"));
1644
+ console.error(error);
1645
+ process.exit(1);
1646
+ }
1647
+ });
1648
+ // ============================================================================
1649
+ // 12. Video Cancel
1650
+ // ============================================================================
1651
+ generateCommand
1652
+ .command("video-cancel", { hidden: true })
1653
+ .description("Cancel video generation (Grok or Runway)")
1654
+ .argument("<task-id>", "Task ID to cancel")
1655
+ .option("-p, --provider <provider>", "Provider: grok, runway", "grok")
1656
+ .option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET env)")
1657
+ .action(async (taskId, options) => {
1658
+ try {
1659
+ const provider = (options.provider || "grok").toLowerCase();
1660
+ let success = false;
1661
+ if (provider === "grok") {
1662
+ const apiKey = await requireApiKey("XAI_API_KEY", "xAI", options.apiKey);
1663
+ const spinner = ora("Cancelling generation...").start();
1664
+ const grok = new GrokProvider();
1665
+ await grok.initialize({ apiKey });
1666
+ success = await grok.cancelGeneration(taskId);
1667
+ if (success) {
1668
+ spinner.succeed(chalk.green("Generation cancelled"));
1669
+ if (isJsonMode()) {
1670
+ outputResult({ success: true, taskId, provider: "grok", cancelled: true });
1671
+ return;
1672
+ }
1673
+ }
1674
+ else {
1675
+ spinner.fail(chalk.red("Failed to cancel generation"));
1676
+ process.exit(1);
1677
+ }
1678
+ }
1679
+ else if (provider === "runway") {
1680
+ const apiKey = await requireApiKey("RUNWAY_API_SECRET", "Runway", options.apiKey);
1681
+ const spinner = ora("Cancelling generation...").start();
1682
+ const runway = new RunwayProvider();
1683
+ await runway.initialize({ apiKey });
1684
+ success = await runway.cancelGeneration(taskId);
1685
+ if (success) {
1686
+ spinner.succeed(chalk.green("Generation cancelled"));
1687
+ if (isJsonMode()) {
1688
+ outputResult({ success: true, taskId, provider: "runway", cancelled: true });
1689
+ return;
1690
+ }
1691
+ }
1692
+ else {
1693
+ spinner.fail(chalk.red("Failed to cancel generation"));
1694
+ process.exit(1);
1695
+ }
1696
+ }
1697
+ else {
1698
+ console.error(chalk.red(`Invalid provider: ${provider}. Use grok or runway.`));
1699
+ process.exit(1);
1700
+ }
1701
+ }
1702
+ catch (error) {
1703
+ console.error(chalk.red("Failed to cancel"));
1704
+ console.error(error);
1705
+ process.exit(1);
1706
+ }
1707
+ });
1708
+ // ============================================================================
1709
+ // 13. Video Extend (merged: ai video-extend + ai veo-extend)
1710
+ // Note: --prompt is long-only (-p is reserved for --provider)
1711
+ // ============================================================================
1712
+ generateCommand
1713
+ .command("video-extend", { hidden: true })
1714
+ .description("Extend video duration (Kling by video ID, Veo by operation name)")
1715
+ .argument("<id>", "Kling video ID or Veo operation name")
1716
+ .option("-p, --provider <provider>", "Provider: kling, veo", "kling")
1717
+ .option("-k, --api-key <key>", "API key (KLING_API_KEY or GOOGLE_API_KEY)")
1718
+ .option("-o, --output <path>", "Output file path")
1719
+ .option("--prompt <text>", "Continuation prompt")
1720
+ .option("-d, --duration <sec>", "Duration: 5 or 10 (Kling), 4/6/8 (Veo)", "5")
1721
+ .option("-n, --negative <prompt>", "Negative prompt (what to avoid, Kling only)")
1722
+ .option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast", "3.1")
1723
+ .option("--no-wait", "Start extension and return task ID without waiting")
1724
+ .option("--dry-run", "Preview parameters without executing")
1725
+ .action(async (id, options) => {
1726
+ try {
1727
+ const provider = (options.provider || "kling").toLowerCase();
1728
+ if (options.dryRun) {
1729
+ outputResult({ dryRun: true, command: "generate video-extend", params: { id, provider, prompt: options.prompt, duration: options.duration, negative: options.negative, veoModel: options.veoModel } });
1730
+ return;
1731
+ }
1732
+ if (provider === "kling") {
1733
+ const apiKey = await requireApiKey("KLING_API_KEY", "Kling", options.apiKey);
1734
+ const spinner = ora("Initializing Kling AI...").start();
1735
+ const kling = new KlingProvider();
1736
+ await kling.initialize({ apiKey });
1737
+ if (!kling.isConfigured()) {
1738
+ spinner.fail(chalk.red("Invalid API key format. Use ACCESS_KEY:SECRET_KEY"));
1739
+ process.exit(1);
1740
+ }
1741
+ spinner.text = "Starting video extension...";
1742
+ const result = await kling.extendVideo(id, {
1743
+ prompt: options.prompt,
1744
+ negativePrompt: options.negative,
1745
+ duration: options.duration,
1746
+ });
1747
+ if (result.status === "failed") {
1748
+ spinner.fail(chalk.red(result.error || "Failed to start extension"));
1749
+ process.exit(1);
1750
+ }
1751
+ console.log();
1752
+ console.log(chalk.bold.cyan("Video Extension Started"));
1753
+ console.log(chalk.dim("─".repeat(60)));
1754
+ console.log(`Provider: Kling`);
1755
+ console.log(`Task ID: ${chalk.bold(result.id)}`);
1756
+ if (!options.wait) {
1757
+ spinner.succeed(chalk.green("Extension started"));
1758
+ console.log();
1759
+ console.log(chalk.dim("Check status with:"));
1760
+ console.log(chalk.dim(` vibe generate video-status ${result.id} -p kling`));
1761
+ console.log();
1762
+ return;
1763
+ }
1764
+ spinner.text = "Extending video (this may take 2-5 minutes)...";
1765
+ const finalResult = await kling.waitForExtendCompletion(result.id, (status) => {
1766
+ spinner.text = `Extending video... ${status.status}`;
1767
+ }, 600000);
1768
+ if (finalResult.status !== "completed") {
1769
+ spinner.fail(chalk.red(finalResult.error || "Extension failed"));
1770
+ process.exit(1);
1771
+ }
1772
+ spinner.succeed(chalk.green("Video extended"));
1773
+ if (isJsonMode()) {
1774
+ let outputPath;
1775
+ if (options.output && finalResult.videoUrl) {
1776
+ const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
1777
+ outputPath = resolve(process.cwd(), options.output);
1778
+ await writeFile(outputPath, buffer);
1779
+ }
1780
+ outputResult({ success: true, provider: "kling", taskId: result.id, videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath });
1781
+ return;
1782
+ }
1783
+ console.log();
1784
+ if (finalResult.videoUrl) {
1785
+ console.log(`Video URL: ${finalResult.videoUrl}`);
1786
+ }
1787
+ if (finalResult.duration) {
1788
+ console.log(`Duration: ${finalResult.duration}s`);
1789
+ }
1790
+ console.log();
1791
+ if (options.output && finalResult.videoUrl) {
1792
+ const downloadSpinner = ora("Downloading video...").start();
1793
+ try {
1794
+ const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
1795
+ const outputPath = resolve(process.cwd(), options.output);
1796
+ await writeFile(outputPath, buffer);
1797
+ downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
1798
+ }
1799
+ catch (err) {
1800
+ downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
1801
+ }
1802
+ }
1803
+ }
1804
+ else if (provider === "veo") {
1805
+ const apiKey = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
1806
+ const spinner = ora("Initializing Veo...").start();
1807
+ const gemini = new GeminiProvider();
1808
+ await gemini.initialize({ apiKey });
1809
+ const veoModelMap = {
1810
+ "3.0": "veo-3.0-generate-preview",
1811
+ "3.1": "veo-3.1-generate-preview",
1812
+ "3.1-fast": "veo-3.1-fast-generate-preview",
1813
+ };
1814
+ const veoModel = veoModelMap[options.veoModel] || "veo-3.1-generate-preview";
1815
+ spinner.text = "Starting video extension...";
1816
+ const result = await gemini.extendVideo(id, options.prompt, {
1817
+ duration: parseInt(options.duration),
1818
+ model: veoModel,
1819
+ });
1820
+ if (result.status === "failed") {
1821
+ spinner.fail(chalk.red(result.error || "Failed to start extension"));
1822
+ process.exit(1);
1823
+ }
1824
+ console.log();
1825
+ console.log(chalk.bold.cyan("Veo Video Extension Started"));
1826
+ console.log(chalk.dim("─".repeat(60)));
1827
+ console.log(`Provider: Veo`);
1828
+ console.log(`Operation: ${chalk.bold(result.id)}`);
1829
+ if (!options.wait) {
1830
+ spinner.succeed(chalk.green("Extension started"));
1831
+ console.log();
1832
+ console.log(chalk.dim("Check status or wait with:"));
1833
+ console.log(chalk.dim(` vibe generate video-extend ${result.id} -p veo`));
1834
+ console.log();
1835
+ return;
1836
+ }
1837
+ spinner.text = "Extending video (this may take 1-3 minutes)...";
1838
+ const finalResult = await gemini.waitForVideoCompletion(result.id, (status) => {
1839
+ spinner.text = `Extending video... ${status.status}`;
1840
+ }, 300000);
1841
+ if (finalResult.status !== "completed") {
1842
+ spinner.fail(chalk.red(finalResult.error || "Extension failed"));
1843
+ process.exit(1);
1844
+ }
1845
+ spinner.succeed(chalk.green("Video extended"));
1846
+ if (isJsonMode()) {
1847
+ let outputPath;
1848
+ if (options.output && finalResult.videoUrl) {
1849
+ const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
1850
+ outputPath = resolve(process.cwd(), options.output);
1851
+ await writeFile(outputPath, buffer);
1852
+ }
1853
+ outputResult({ success: true, provider: "veo", taskId: result.id, videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath });
1854
+ return;
1855
+ }
1856
+ console.log();
1857
+ if (finalResult.videoUrl) {
1858
+ console.log(`Video URL: ${finalResult.videoUrl}`);
1859
+ }
1860
+ console.log();
1861
+ if (options.output && finalResult.videoUrl) {
1862
+ const downloadSpinner = ora("Downloading video...").start();
1863
+ try {
1864
+ const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
1865
+ const outputPath = resolve(process.cwd(), options.output);
1866
+ await writeFile(outputPath, buffer);
1867
+ downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
1868
+ }
1869
+ catch (err) {
1870
+ downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
1871
+ }
1872
+ }
1873
+ }
1874
+ else {
1875
+ console.error(chalk.red(`Invalid provider: ${provider}. Video extend supports: kling, veo`));
1876
+ process.exit(1);
1877
+ }
1878
+ }
1879
+ catch (error) {
1880
+ console.error(chalk.red("Video extension failed"));
1881
+ console.error(error);
1882
+ process.exit(1);
1883
+ }
1884
+ });
1885
+ //# sourceMappingURL=generate.js.map