ima2-gen 1.1.19 → 1.1.21

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 (95) hide show
  1. package/README.md +24 -25
  2. package/bin/commands/capabilities.js +2 -2
  3. package/bin/commands/capabilities.ts +2 -2
  4. package/bin/commands/defaults.js +2 -2
  5. package/bin/commands/defaults.ts +2 -2
  6. package/bin/commands/doctor.js +3 -3
  7. package/bin/commands/doctor.ts +3 -3
  8. package/bin/commands/edit.js +1 -1
  9. package/bin/commands/edit.ts +1 -1
  10. package/bin/commands/gen.js +1 -1
  11. package/bin/commands/gen.ts +1 -1
  12. package/bin/commands/grok.js +25 -22
  13. package/bin/commands/grok.ts +26 -22
  14. package/bin/commands/multimode.js +1 -1
  15. package/bin/commands/multimode.ts +1 -1
  16. package/bin/commands/observability.js +2 -2
  17. package/bin/commands/observability.ts +2 -2
  18. package/bin/commands/video.js +335 -13
  19. package/bin/commands/video.ts +249 -12
  20. package/bin/ima2.js +9 -9
  21. package/bin/ima2.ts +9 -9
  22. package/bin/lib/error-hints.js +2 -2
  23. package/bin/lib/error-hints.ts +2 -2
  24. package/docs/API.md +112 -3
  25. package/docs/CLI.md +61 -7
  26. package/docs/FAQ.ko.md +15 -20
  27. package/docs/FAQ.md +14 -19
  28. package/docs/NPX_QUICKSTART.md +40 -0
  29. package/docs/PROMPT_STUDIO.ko.md +1 -1
  30. package/docs/PROMPT_STUDIO.md +1 -1
  31. package/docs/README.ja.md +6 -16
  32. package/docs/README.ko.md +10 -20
  33. package/docs/README.zh-CN.md +7 -17
  34. package/docs/migration/runtime-test-inventory.md +9 -1
  35. package/lib/agentGenerationPlanner.js +20 -1
  36. package/lib/agentGenerationPlanner.ts +25 -1
  37. package/lib/agentRuntime.js +24 -8
  38. package/lib/agentRuntime.ts +23 -8
  39. package/lib/capabilities.js +1 -1
  40. package/lib/capabilities.ts +1 -1
  41. package/lib/generationErrors.js +1 -1
  42. package/lib/generationErrors.ts +1 -1
  43. package/lib/grokProxyLauncher.js +26 -3
  44. package/lib/grokProxyLauncher.ts +27 -3
  45. package/lib/grokVideoAdapter.js +18 -89
  46. package/lib/grokVideoAdapter.ts +27 -88
  47. package/lib/grokVideoCanvas.js +25 -0
  48. package/lib/grokVideoCanvas.ts +26 -0
  49. package/lib/grokVideoDownload.js +58 -0
  50. package/lib/grokVideoDownload.ts +59 -0
  51. package/lib/grokVideoPlannerPrompt.js +64 -0
  52. package/lib/grokVideoPlannerPrompt.ts +67 -0
  53. package/lib/historyList.js +7 -1
  54. package/lib/historyList.ts +5 -1
  55. package/lib/oauthLauncher.js +21 -6
  56. package/lib/oauthLauncher.ts +22 -6
  57. package/lib/videoContinuity.js +149 -0
  58. package/lib/videoContinuity.ts +180 -0
  59. package/lib/videoFrameExtract.js +80 -0
  60. package/lib/videoFrameExtract.ts +78 -0
  61. package/node_modules/progrok/dist/index.js +187 -88
  62. package/node_modules/progrok/dist/index.js.map +1 -1
  63. package/node_modules/progrok/package.json +1 -1
  64. package/node_modules/progrok/skills/progrok/SKILL.md +33 -4
  65. package/package.json +2 -2
  66. package/routes/index.js +4 -0
  67. package/routes/index.ts +4 -0
  68. package/routes/quota.js +66 -0
  69. package/routes/quota.ts +89 -0
  70. package/routes/video.js +77 -15
  71. package/routes/video.ts +82 -14
  72. package/routes/videoExtended.js +293 -0
  73. package/routes/videoExtended.ts +284 -0
  74. package/server.js +6 -2
  75. package/server.ts +5 -2
  76. package/skills/ima2/SKILL.md +381 -3
  77. package/ui/dist/.vite/manifest.json +12 -12
  78. package/ui/dist/assets/{AgentWorkspace-DE_wg90f.js → AgentWorkspace-B_hq9CLg.js} +2 -2
  79. package/ui/dist/assets/{CardNewsWorkspace--Myc5pAp.js → CardNewsWorkspace-wD12J7qk.js} +1 -1
  80. package/ui/dist/assets/{NodeCanvas-4U5oOT2y.js → NodeCanvas-CI_wuPMf.js} +1 -1
  81. package/ui/dist/assets/{PromptBuilderPanel-DNW1U8zI.js → PromptBuilderPanel-CUTujJUV.js} +1 -1
  82. package/ui/dist/assets/{PromptImportDialog-o-4Sqki1.js → PromptImportDialog-CUi66jPK.js} +2 -2
  83. package/ui/dist/assets/{PromptImportDiscoverySection-BAbrRP8B.js → PromptImportDiscoverySection-Cm3vrjY4.js} +1 -1
  84. package/ui/dist/assets/{PromptImportFolderSection-L-XI2noz.js → PromptImportFolderSection-DOtWTD9n.js} +1 -1
  85. package/ui/dist/assets/{PromptLibraryPanel-CrW9LYGD.js → PromptLibraryPanel-BMjQegRa.js} +2 -2
  86. package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +1 -0
  87. package/ui/dist/assets/{index-BONbNNIi.js → index-31uVIdt4.js} +1 -1
  88. package/ui/dist/assets/index-CjgnNtgt.css +1 -0
  89. package/ui/dist/assets/index-Da2s4_-5.js +36 -0
  90. package/ui/dist/index.html +2 -2
  91. package/vendor/progrok-0.2.0.tgz +0 -0
  92. package/ui/dist/assets/SettingsWorkspace-Dn4SYTyZ.js +0 -1
  93. package/ui/dist/assets/index-B6tcw_UF.css +0 -1
  94. package/ui/dist/assets/index-CeSZ2L3-.js +0 -32
  95. package/vendor/progrok-0.1.1.tgz +0 -0
@@ -4,11 +4,56 @@ import { streamSse } from "../lib/sse.js";
4
4
  import { out, die, color, json, exitCodeForError } from "../lib/output.js";
5
5
  import { config } from "../../config.js";
6
6
  import { readFile, writeFile, mkdir } from "node:fs/promises";
7
- import { dirname, join } from "node:path";
7
+ import { basename, dirname, join } from "node:path";
8
8
 
9
9
  const VALID_RESOLUTIONS = new Set(["480p", "720p"]);
10
10
  const VALID_ASPECT_RATIOS = new Set(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "auto"]);
11
11
  const VALID_MODELS = new Set(["grok-imagine-video", "grok-imagine-video-1.5-preview"]);
12
+ const ACTIVE_VIDEO_PROMPT_GUIDANCE = "Active video prompt required: describe visual flow, motion flow, sound/no-music intent, dialogue/no-dialogue intent, and the desired ending frame. Pace the scene to naturally fill the selected duration with an opening composition, connected motion/emotion change, and stable ending frame.";
13
+
14
+ function parseIntegerFlag(value: unknown, fallback: number, label: string): number {
15
+ const raw = value === undefined ? String(fallback) : String(value);
16
+ if (!/^\d+$/.test(raw)) die(2, `${label} must be an integer`);
17
+ return Number(raw);
18
+ }
19
+
20
+ function rejectUnknownFlags(args: { _unknown?: string[] }): void {
21
+ if (args._unknown?.length) die(2, `unknown option: ${args._unknown[0]}`);
22
+ }
23
+
24
+ async function readJsonResponse(res: Response, label: string): Promise<Record<string, unknown>> {
25
+ const text = await res.text();
26
+ try {
27
+ return text ? JSON.parse(text) as Record<string, unknown> : {};
28
+ } catch {
29
+ die(1, `${label} failed: expected JSON response, got ${text.slice(0, 120) || `HTTP ${res.status}`}`);
30
+ }
31
+ }
32
+
33
+ function parseTimeoutSeconds(seconds: unknown): number {
34
+ const sec = parseIntegerFlag(seconds, 600, "--timeout");
35
+ if (sec < 1) die(2, "--timeout must be at least 1");
36
+ return sec;
37
+ }
38
+
39
+ function timeoutSignal(seconds: unknown): AbortSignal {
40
+ const sec = parseTimeoutSeconds(seconds);
41
+ return AbortSignal.timeout(sec * 1000);
42
+ }
43
+
44
+ async function writeBuffer(path: string, buf: Buffer): Promise<void> {
45
+ await mkdir(dirname(path), { recursive: true }).catch(() => {});
46
+ await writeFile(path, buf);
47
+ }
48
+
49
+ async function downloadReturnedVideo(serverBase: string, data: Record<string, unknown>, outPath: string, signal: AbortSignal): Promise<void> {
50
+ const rawUrl = typeof data.url === "string" ? data.url : "";
51
+ const url = rawUrl.startsWith("/") ? `${serverBase}${rawUrl}` : rawUrl;
52
+ if (!url) die(1, "server did not return a video url");
53
+ const res = await fetch(url, { signal });
54
+ if (!res.ok) die(1, `failed to download video: HTTP ${res.status}`);
55
+ await writeBuffer(outPath, Buffer.from(await res.arrayBuffer()));
56
+ }
12
57
 
13
58
  const SPEC = {
14
59
  flags: {
@@ -30,11 +75,24 @@ const SPEC = {
30
75
 
31
76
  const HELP = `
32
77
  ima2 video <prompt...> [options]
78
+ ima2 video edit <prompt> --video <url|file_id|generated-file>
79
+ ima2 video extend <prompt> --video <url|file_id|generated-file> [--duration 6]
80
+ ima2 video continue <prompt> --video <generated-file>
81
+ ima2 video frame <file> [--last] [-o output.png]
82
+ ima2 video analyze <generated-file>
33
83
 
34
- Generate a video via the Grok video provider (SSE streaming).
84
+ Generate, edit, extend, or analyze video via Grok.
35
85
 
36
- Options:
37
- --duration <1..15> Duration in seconds. Default: 5
86
+ Subcommands:
87
+ (default) Generate video (T2V / I2V / Ref2V)
88
+ edit Edit existing video with text prompt (V2V)
89
+ extend Continue video from last frame
90
+ continue Generate a new I2V clip from a generated video's last frame with lineage
91
+ frame Extract a frame from video (requires ffmpeg on server)
92
+ analyze Analyze video with Grok 4.3 vision
93
+
94
+ Options (generate mode):
95
+ --duration <1..15> Duration in seconds. Default: 5. Prompt motion should naturally fill this length
38
96
  --resolution <480p|720p> Default: 480p
39
97
  --aspect-ratio <ratio|auto> 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, auto. Default: auto
40
98
  --model <name> grok-imagine-video, grok-imagine-video-1.5-preview
@@ -47,6 +105,10 @@ const HELP = `
47
105
  --server <url> Override server URL
48
106
  --session <id> Session ID
49
107
 
108
+ Edit/extend subcommands:
109
+ --video <value> HTTPS URL, xAI file_id, data URL, or generated filename
110
+ --duration <2..10> Extension duration only. Default: 6
111
+
50
112
  Modes (auto-detected from --ref count):
51
113
  0 refs → text-to-video
52
114
  1 ref → image-to-video
@@ -55,17 +117,29 @@ const HELP = `
55
117
  Examples:
56
118
  ima2 video "a cat playing piano"
57
119
  ima2 video "animate this" --ref photo.png --duration 10
58
- ima2 video "cinematic" --resolution 720p --aspect-ratio 16:9 -o out.mp4
120
+ ima2 video edit "make it sunset" --video https://vidgen.x.ai/.../clip.mp4
121
+ ima2 video extend "camera pulls back" --video https://vidgen.x.ai/.../clip.mp4 --duration 5
122
+ ima2 video continue "she turns back as the music cuts to room tone" --video 1780226256355_50252101.mp4
123
+ ima2 video frame 1780226256355_50252101.mp4 --last -o lastframe.png
124
+ ima2 video analyze 1780226256355_50252101.mp4
59
125
  `;
60
126
 
61
127
  export default async function videoCmd(argv: string[]) {
128
+ const sub = argv[0];
129
+ if (sub === "edit") return videoEditCmd(argv.slice(1));
130
+ if (sub === "extend") return videoExtendCmd(argv.slice(1));
131
+ if (sub === "continue") return videoContinueCmd(argv.slice(1));
132
+ if (sub === "frame") return videoFrameCmd(argv.slice(1));
133
+ if (sub === "analyze") return videoAnalyzeCmd(argv.slice(1));
134
+
62
135
  const args = parseArgs(argv, SPEC);
136
+ rejectUnknownFlags(args);
63
137
  if (args.help) { out(HELP); return; }
64
138
 
65
139
  const prompt = args.positional.join(" ");
66
- if (!prompt) die(2, "prompt is required");
140
+ if (!prompt.trim()) die(2, ACTIVE_VIDEO_PROMPT_GUIDANCE);
67
141
 
68
- const duration = parseInt(String(args.duration)) || 5;
142
+ const duration = parseIntegerFlag(args.duration, 5, "--duration");
69
143
  if (duration < 1 || duration > 15) die(2, "--duration must be between 1 and 15");
70
144
 
71
145
  const resolution = String(args.resolution);
@@ -80,6 +154,11 @@ export default async function videoCmd(argv: string[]) {
80
154
 
81
155
  const refs = (Array.isArray(args.ref) ? args.ref : []) as string[];
82
156
  if (refs.length > 7) die(2, "max 7 --ref attachments for video");
157
+ if (refs.length >= 2 && duration > 10) die(2, "--duration must be between 1 and 10 when using 2 or more --ref attachments");
158
+
159
+ const timeoutSeconds = parseIntegerFlag(args.timeout, 600, "--timeout");
160
+ if (timeoutSeconds < 1) die(2, "--timeout must be at least 1");
161
+ const timeoutMs = timeoutSeconds * 1000;
83
162
 
84
163
  let server;
85
164
  try { server = await resolveServer({ serverFlag: args.server }); }
@@ -90,7 +169,6 @@ export default async function videoCmd(argv: string[]) {
90
169
  return buf.toString("base64");
91
170
  }));
92
171
 
93
- const timeoutMs = (parseInt(String(args.timeout)) || 600) * 1000;
94
172
  const requestId = `req_cli_video_${Date.now().toString(36)}`;
95
173
 
96
174
  const body: Record<string, unknown> = {
@@ -145,7 +223,7 @@ export default async function videoCmd(argv: string[]) {
145
223
  break;
146
224
  case "error":
147
225
  if (!args.json && lastProgress >= 0) process.stdout.write("\n");
148
- die(1, `video error: ${ev.data.error || ev.data}${ev.data.code ? ` (${ev.data.code})` : ""}`);
226
+ die(1, `video error: ${ev.data.error || ev.data}${ev.data.guidance ? `\n${ev.data.guidance}` : ""}${ev.data.code ? ` (${ev.data.code})` : ""}`);
149
227
  }
150
228
  }
151
229
  } catch (e: unknown) {
@@ -175,11 +253,10 @@ export default async function videoCmd(argv: string[]) {
175
253
 
176
254
  // Download the video file from server
177
255
  const videoUrl = `${server.base}${doneData.url || `/generated/${encodeURIComponent(filename)}`}`;
178
- const dlRes = await fetch(videoUrl, { signal: AbortSignal.timeout(30_000) });
256
+ const dlRes = await fetch(videoUrl, { signal: timeoutSignal(args.timeout) });
179
257
  if (!dlRes.ok) die(1, `failed to download video: HTTP ${dlRes.status}`);
180
258
  const videoBuf = Buffer.from(await dlRes.arrayBuffer());
181
- await mkdir(dirname(target), { recursive: true }).catch(() => {});
182
- await writeFile(target, videoBuf);
259
+ await writeBuffer(target, videoBuf);
183
260
 
184
261
  if (args.json) {
185
262
  json({
@@ -203,3 +280,163 @@ function renderBar(pct: number): string {
203
280
  const filled = Math.round((pct / 100) * width);
204
281
  return color.green("█".repeat(filled)) + color.dim("░".repeat(width - filled));
205
282
  }
283
+
284
+ async function runVideoGenerateRequest(serverBase: string, body: Record<string, unknown>, timeout: unknown, silent: boolean): Promise<Record<string, unknown>> {
285
+ let doneData: Record<string, unknown> | null = null;
286
+ let lastProgress = -1;
287
+ for await (const ev of streamSse(`${serverBase}/api/video/generate`, {
288
+ body: { provider: "grok", ...body },
289
+ signal: timeoutSignal(timeout),
290
+ headers: typeof body.requestId === "string" ? { "X-Request-Id": body.requestId } : undefined,
291
+ })) {
292
+ if (ev.event === "progress") {
293
+ const pct = typeof ev.data.progress === "number" ? Math.round(ev.data.progress * 100) : null;
294
+ if (pct !== null && pct !== lastProgress && !silent) {
295
+ process.stdout.write(`\r ${renderBar(pct)} ${pct}%`);
296
+ lastProgress = pct;
297
+ }
298
+ } else if (ev.event === "done") {
299
+ if (!silent && lastProgress >= 0) process.stdout.write("\n");
300
+ doneData = ev.data;
301
+ } else if (ev.event === "error") {
302
+ if (!silent && lastProgress >= 0) process.stdout.write("\n");
303
+ die(1, `video error: ${ev.data.error || ev.data}${ev.data.guidance ? `\n${ev.data.guidance}` : ""}${ev.data.code ? ` (${ev.data.code})` : ""}`);
304
+ }
305
+ }
306
+ if (!doneData) die(1, "server did not return a video result");
307
+ return doneData;
308
+ }
309
+
310
+ // --- Subcommands ---
311
+
312
+ async function videoEditCmd(argv: string[]) {
313
+ const spec = { flags: { video: { type: "string" }, out: { short: "o", type: "string" }, output: { type: "string" }, json: { type: "boolean" }, timeout: { type: "string", default: "600" }, server: { type: "string" }, help: { short: "h", type: "boolean" } } };
314
+ const args = parseArgs(argv, spec);
315
+ rejectUnknownFlags(args);
316
+ if (args.help) { out(` ima2 video edit <prompt> --video <url|file_id|generated-file>\n\n Edit existing video with text prompt (real V2V).\n Model: grok-imagine-video only. Input: mp4, max 8.7s.\n\n Options:\n --video <value> Source video HTTPS URL, xAI file_id, data URL, or generated filename (required)\n -o, --out <file> Download edited video to file\n --output <file> Alias for --out\n --json Print JSON result\n --timeout <sec> Default: 600\n --server <url> Override server URL`); return; }
317
+ const prompt = args.positional.join(" ");
318
+ if (!prompt.trim()) die(2, ACTIVE_VIDEO_PROMPT_GUIDANCE);
319
+ if (!args.video) die(2, "--video <url> is required");
320
+ parseTimeoutSeconds(args.timeout);
321
+ let server;
322
+ try { server = await resolveServer({ serverFlag: args.server }); } catch (e: unknown) { die(exitCodeForError(e), (e as Error).message); throw e; }
323
+ const res = await fetch(`${server.base}/api/video/edit`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt, videoUrl: args.video }), signal: timeoutSignal(args.timeout) });
324
+ const data = await readJsonResponse(res, "edit");
325
+ if (!res.ok) die(1, `edit failed: ${data.error ?? res.status}`);
326
+ const outPath = (args.out || args.output) as string | undefined;
327
+ if (outPath) await downloadReturnedVideo(server.base, data, outPath, timeoutSignal(args.timeout));
328
+ if (args.json) { out(JSON.stringify(data, null, 2)); } else { out(color.green("✓ ") + `Edited video: ${data.url}`); }
329
+ }
330
+
331
+ async function videoExtendCmd(argv: string[]) {
332
+ const spec = { flags: { video: { type: "string" }, duration: { type: "string", default: "6" }, out: { short: "o", type: "string" }, output: { type: "string" }, json: { type: "boolean" }, timeout: { type: "string", default: "600" }, server: { type: "string" }, help: { short: "h", type: "boolean" } } };
333
+ const args = parseArgs(argv, spec);
334
+ rejectUnknownFlags(args);
335
+ if (args.help) { out(` ima2 video extend <prompt> --video <url|file_id|generated-file> [--duration 6]\n\n Extend video from its last frame.\n Model: grok-imagine-video only. Extension: 2-10s.\n\n Options:\n --video <value> Source video HTTPS URL, xAI file_id, data URL, or generated filename (required)\n --duration <2-10> Extension duration (default: 6)\n -o, --out <file> Download extended video to file\n --output <file> Alias for --out\n --json Print JSON result\n --timeout <sec> Default: 600\n --server <url> Override server URL`); return; }
336
+ const prompt = args.positional.join(" ");
337
+ if (!prompt.trim()) die(2, ACTIVE_VIDEO_PROMPT_GUIDANCE);
338
+ if (!args.video) die(2, "--video <url> is required");
339
+ const duration = parseIntegerFlag(args.duration, 6, "--duration");
340
+ if (duration < 2 || duration > 10) die(2, "--duration must be between 2 and 10");
341
+ parseTimeoutSeconds(args.timeout);
342
+ let server;
343
+ try { server = await resolveServer({ serverFlag: args.server }); } catch (e: unknown) { die(exitCodeForError(e), (e as Error).message); throw e; }
344
+ const res = await fetch(`${server.base}/api/video/extend`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt, videoUrl: args.video, duration }), signal: timeoutSignal(args.timeout) });
345
+ const data = await readJsonResponse(res, "extend");
346
+ if (!res.ok) die(1, `extend failed: ${data.error ?? res.status}`);
347
+ const outPath = (args.out || args.output) as string | undefined;
348
+ if (outPath) await downloadReturnedVideo(server.base, data, outPath, timeoutSignal(args.timeout));
349
+ if (args.json) { out(JSON.stringify(data, null, 2)); } else { out(color.green("✓ ") + `Extended video (${data.duration}s): ${data.url}`); }
350
+ }
351
+
352
+ async function videoContinueCmd(argv: string[]) {
353
+ const spec = {
354
+ flags: {
355
+ video: { type: "string" },
356
+ duration: { type: "string", default: "5" },
357
+ resolution: { type: "string", default: "720p" },
358
+ "aspect-ratio": { type: "string", default: "auto" },
359
+ model: { type: "string" },
360
+ out: { short: "o", type: "string" },
361
+ output: { type: "string" },
362
+ json: { type: "boolean" },
363
+ timeout: { type: "string", default: "600" },
364
+ server: { type: "string" },
365
+ help: { short: "h", type: "boolean" },
366
+ },
367
+ };
368
+ const args = parseArgs(argv, spec);
369
+ rejectUnknownFlags(args);
370
+ if (args.help) {
371
+ out(` ima2 video continue <prompt> --video <generated-file>\n\n Generate a new clip from a generated video's last frame and carry branch-local revisedPrompt lineage.\n\n Prompt must describe visual flow, motion, sound/music/no-music, dialogue/no-dialogue, ending frame, and how the selected duration should feel naturally filled.\n\n Options:\n --video <file> Generated .mp4 filename (required)\n --duration <1..15> Default: 5. Prompt motion should naturally fill this length\n --resolution <480p|720p> Default: 720p\n --aspect-ratio <ratio|auto> Default: auto\n --model <name> grok-imagine-video, grok-imagine-video-1.5-preview\n -o, --out <file> Download continued video to file\n --output <file> Alias for --out\n --json Print JSON result\n --timeout <sec> Default: 600\n --server <url> Override server URL`);
372
+ return;
373
+ }
374
+ const prompt = args.positional.join(" ");
375
+ if (!prompt.trim()) die(2, ACTIVE_VIDEO_PROMPT_GUIDANCE);
376
+ if (!args.video) die(2, "--video <generated-file> is required");
377
+ const duration = parseIntegerFlag(args.duration, 5, "--duration");
378
+ if (duration < 1 || duration > 15) die(2, "--duration must be between 1 and 15");
379
+ const resolution = String(args.resolution);
380
+ if (!VALID_RESOLUTIONS.has(resolution)) die(2, "--resolution must be one of: 480p, 720p");
381
+ const aspectRatio = String(args["aspect-ratio"]);
382
+ if (!VALID_ASPECT_RATIOS.has(aspectRatio)) die(2, "--aspect-ratio must be one of: 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, auto");
383
+ if (args.model && !VALID_MODELS.has(String(args.model))) {
384
+ die(2, "--model must be one of: grok-imagine-video, grok-imagine-video-1.5-preview");
385
+ }
386
+ parseTimeoutSeconds(args.timeout);
387
+ let server;
388
+ try { server = await resolveServer({ serverFlag: args.server }); } catch (e: unknown) { die(exitCodeForError(e), (e as Error).message); throw e; }
389
+ const requestId = `req_cli_video_continue_${Date.now().toString(36)}`;
390
+ const body: Record<string, unknown> = {
391
+ prompt,
392
+ requestId,
393
+ duration,
394
+ resolution,
395
+ aspectRatio,
396
+ continueFromVideo: args.video,
397
+ };
398
+ if (args.model) body.model = args.model;
399
+ const data = await runVideoGenerateRequest(server.base, body, args.timeout, Boolean(args.json));
400
+ const outPath = (args.out || args.output) as string | undefined;
401
+ if (outPath) await downloadReturnedVideo(server.base, data, outPath, timeoutSignal(args.timeout));
402
+ if (args.json) out(JSON.stringify(data, null, 2));
403
+ else out(color.green("✓ ") + `Continued video: ${data.url}`);
404
+ }
405
+
406
+ async function videoFrameCmd(argv: string[]) {
407
+ const spec = { flags: { last: { type: "boolean" }, position: { type: "string" }, out: { type: "string" }, output: { short: "o", type: "string" }, timeout: { type: "string", default: "60" }, server: { type: "string" }, help: { short: "h", type: "boolean" } } };
408
+ const args = parseArgs(argv, spec);
409
+ rejectUnknownFlags(args);
410
+ if (args.help) { out(` ima2 video frame <generated-file> [--last] [--position <sec>] [-o output.png]\n\n Extract a frame from a generated video file.\n\n Options:\n --last Extract last frame (default)\n --position <sec> Extract frame at specific second\n -o, --output <path> Output file path\n --out <path> Alias for --output\n --timeout <sec> Default: 60\n --server <url> Override server URL`); return; }
411
+ const file = args.positional[0];
412
+ if (!file) die(2, "file argument required");
413
+ if (args.last && args.position) die(2, "use either --last or --position, not both");
414
+ const position = args.last ? "last" : (String(args.position || "last"));
415
+ if (position !== "last" && !/^\d+(\.\d+)?$/.test(position)) die(2, "--position must be a non-negative number");
416
+ parseTimeoutSeconds(args.timeout);
417
+ let server;
418
+ try { server = await resolveServer({ serverFlag: args.server }); } catch (e: unknown) { die(exitCodeForError(e), (e as Error).message); throw e; }
419
+ const url = `${server.base}/api/video/frame?file=${encodeURIComponent(file)}&position=${encodeURIComponent(position)}`;
420
+ const res = await fetch(url, { signal: timeoutSignal(args.timeout) });
421
+ if (!res.ok) { const d = await readJsonResponse(res, "frame extraction"); die(1, `frame extraction failed: ${(d as any).error || res.status}`); }
422
+ const buf = Buffer.from(await res.arrayBuffer());
423
+ const outPath = (args.output || args.out) as string || `frame-${basename(file).replace(/\.[^.]+$/, "")}.png`;
424
+ await writeBuffer(outPath, buf);
425
+ out(color.green("✓ ") + `Frame saved: ${outPath} (${buf.length} bytes)`);
426
+ }
427
+
428
+ async function videoAnalyzeCmd(argv: string[]) {
429
+ const spec = { flags: { json: { type: "boolean" }, timeout: { type: "string", default: "180" }, server: { type: "string" }, help: { short: "h", type: "boolean" } } };
430
+ const args = parseArgs(argv, spec);
431
+ rejectUnknownFlags(args);
432
+ if (args.help) { out(` ima2 video analyze <generated-file>\n\n Analyze first/last frames from a generated .mp4 with Grok 4.3 image understanding. Outputs structured recreation prompt.\n\n Options:\n --json Print JSON result\n --timeout <sec> Default: 180\n --server <url> Override server URL`); return; }
433
+ const videoUrl = args.positional[0];
434
+ if (!videoUrl) die(2, "generated video filename required");
435
+ parseTimeoutSeconds(args.timeout);
436
+ let server;
437
+ try { server = await resolveServer({ serverFlag: args.server }); } catch (e: unknown) { die(exitCodeForError(e), (e as Error).message); throw e; }
438
+ const res = await fetch(`${server.base}/api/video/analyze`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ videoUrl }), signal: timeoutSignal(args.timeout) });
439
+ const data = await readJsonResponse(res, "analyze");
440
+ if (!res.ok) die(1, `analyze failed: ${(data as any).error || res.status}`);
441
+ if (args.json) { out(JSON.stringify(data, null, 2)); } else { out((data as any).analysis); }
442
+ }
package/bin/ima2.js CHANGED
@@ -88,7 +88,7 @@ async function setup() {
88
88
  saveConfig(config);
89
89
  console.log("\n Starting Grok OAuth login...\n");
90
90
  try {
91
- execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login`, { stdio: "inherit" });
91
+ execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login --manual-paste`, { stdio: "inherit" });
92
92
  }
93
93
  catch {
94
94
  console.log("\n Grok login failed or cancelled. You can retry with 'ima2 grok login'.\n");
@@ -121,7 +121,7 @@ async function setup() {
121
121
  // Grok OAuth
122
122
  console.log(" Running Grok OAuth login...\n");
123
123
  try {
124
- execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login`, { stdio: "inherit" });
124
+ execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login --manual-paste`, { stdio: "inherit" });
125
125
  }
126
126
  catch {
127
127
  console.log("\n Grok login failed. You can retry with 'ima2 grok login'.\n");
@@ -135,7 +135,7 @@ async function setup() {
135
135
  config.oauth.disableAutoStart = false;
136
136
  delete config.apiKey;
137
137
  saveConfig(config);
138
- console.log("\n Starting OAuth login...\n");
138
+ console.log("\n Starting GPT OAuth login...\n");
139
139
  const auth = detectCodexAuth();
140
140
  const hasAuth = auth.authed;
141
141
  if (!hasAuth) {
@@ -154,10 +154,10 @@ async function setup() {
154
154
  }
155
155
  else {
156
156
  const how = auth.probe === "authed" ? "codex CLI" : "auth file";
157
- console.log(` Existing OAuth session found (${how}).\n`);
157
+ console.log(` Existing GPT OAuth session found (${how}).\n`);
158
158
  }
159
159
  saveConfig(config);
160
- console.log(" OAuth configured. Starting server...\n");
160
+ console.log(" GPT OAuth configured. Starting server...\n");
161
161
  }
162
162
  rl.close();
163
163
  return config;
@@ -222,7 +222,7 @@ async function showStatus() {
222
222
  }
223
223
  // Check OAuth auth files + codex CLI probe
224
224
  const auth = detectCodexAuth();
225
- console.log(` OAuth sessions:`);
225
+ console.log(` GPT OAuth sessions:`);
226
226
  console.log(` ${auth.files.codex} ${auth.fileHits.codex ? "✓" : "✗"}`);
227
227
  console.log(` ${auth.files.chatgpt} ${auth.fileHits.chatgpt ? "✓" : "✗"}`);
228
228
  if (auth.fileHits.xdgCodex) {
@@ -255,7 +255,7 @@ function showHelp() {
255
255
 
256
256
  Server commands:
257
257
  serve [--dev] Start the image generation server
258
- setup, login Configure API key or OAuth (interactive)
258
+ setup, login Configure API key or GPT OAuth (interactive)
259
259
  status Show current configuration status
260
260
  doctor Diagnose environment and setup
261
261
  open Open web UI in browser
@@ -283,8 +283,8 @@ function showHelp() {
283
283
  storage <sub> Storage status / open-dir (ima2 storage --help)
284
284
  billing API usage / quota
285
285
  providers Configured providers
286
- oauth <sub> OAuth proxy status (ima2 oauth --help)
287
- grok <sub> Bundled progrok login/status (ima2 grok --help)
286
+ oauth <sub> GPT OAuth proxy status (ima2 oauth --help)
287
+ grok <sub> Bundled Grok auth/status (ima2 grok --help)
288
288
  config <sub> Config get/set/ls/path/rm (ima2 config --help)
289
289
  defaults <sub> Inspect/change model defaults (ima2 defaults --help)
290
290
  capabilities Agent capability metadata (ima2 capabilities --help)
package/bin/ima2.ts CHANGED
@@ -91,7 +91,7 @@ async function setup() {
91
91
  saveConfig(config);
92
92
  console.log("\n Starting Grok OAuth login...\n");
93
93
  try {
94
- execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login`, { stdio: "inherit" });
94
+ execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login --manual-paste`, { stdio: "inherit" });
95
95
  } catch {
96
96
  console.log("\n Grok login failed or cancelled. You can retry with 'ima2 grok login'.\n");
97
97
  rl.close();
@@ -119,7 +119,7 @@ async function setup() {
119
119
  // Grok OAuth
120
120
  console.log(" Running Grok OAuth login...\n");
121
121
  try {
122
- execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login`, { stdio: "inherit" });
122
+ execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login --manual-paste`, { stdio: "inherit" });
123
123
  } catch {
124
124
  console.log("\n Grok login failed. You can retry with 'ima2 grok login'.\n");
125
125
  }
@@ -131,7 +131,7 @@ async function setup() {
131
131
  config.oauth.disableAutoStart = false;
132
132
  delete config.apiKey;
133
133
  saveConfig(config);
134
- console.log("\n Starting OAuth login...\n");
134
+ console.log("\n Starting GPT OAuth login...\n");
135
135
 
136
136
  const auth = detectCodexAuth();
137
137
  const hasAuth = auth.authed;
@@ -152,11 +152,11 @@ async function setup() {
152
152
  }
153
153
  } else {
154
154
  const how = auth.probe === "authed" ? "codex CLI" : "auth file";
155
- console.log(` Existing OAuth session found (${how}).\n`);
155
+ console.log(` Existing GPT OAuth session found (${how}).\n`);
156
156
  }
157
157
 
158
158
  saveConfig(config);
159
- console.log(" OAuth configured. Starting server...\n");
159
+ console.log(" GPT OAuth configured. Starting server...\n");
160
160
  }
161
161
 
162
162
  rl.close();
@@ -234,7 +234,7 @@ async function showStatus() {
234
234
 
235
235
  // Check OAuth auth files + codex CLI probe
236
236
  const auth = detectCodexAuth();
237
- console.log(` OAuth sessions:`);
237
+ console.log(` GPT OAuth sessions:`);
238
238
  console.log(` ${auth.files.codex} ${auth.fileHits.codex ? "✓" : "✗"}`);
239
239
  console.log(` ${auth.files.chatgpt} ${auth.fileHits.chatgpt ? "✓" : "✗"}`);
240
240
  if (auth.fileHits.xdgCodex) {
@@ -269,7 +269,7 @@ function showHelp() {
269
269
 
270
270
  Server commands:
271
271
  serve [--dev] Start the image generation server
272
- setup, login Configure API key or OAuth (interactive)
272
+ setup, login Configure API key or GPT OAuth (interactive)
273
273
  status Show current configuration status
274
274
  doctor Diagnose environment and setup
275
275
  open Open web UI in browser
@@ -297,8 +297,8 @@ function showHelp() {
297
297
  storage <sub> Storage status / open-dir (ima2 storage --help)
298
298
  billing API usage / quota
299
299
  providers Configured providers
300
- oauth <sub> OAuth proxy status (ima2 oauth --help)
301
- grok <sub> Bundled progrok login/status (ima2 grok --help)
300
+ oauth <sub> GPT OAuth proxy status (ima2 oauth --help)
301
+ grok <sub> Bundled Grok auth/status (ima2 grok --help)
302
302
  config <sub> Config get/set/ls/path/rm (ima2 config --help)
303
303
  defaults <sub> Inspect/change model defaults (ima2 defaults --help)
304
304
  capabilities Agent capability metadata (ima2 capabilities --help)
@@ -3,11 +3,11 @@ const HINTS = {
3
3
  APIKEY_DISABLED: "API-key generation is supported in current builds; switch providers or update the configured API key.",
4
4
  IMAGE_MODEL_UNSUPPORTED: "This model is visible but cannot generate images here. Use gpt-5.4 or gpt-5.4-mini.",
5
5
  INVALID_IMAGE_MODEL: "Use one of: gpt-5.5, gpt-5.4, gpt-5.4-mini.",
6
- OAUTH_UNAVAILABLE: "OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
6
+ OAUTH_UNAVAILABLE: "GPT OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
7
7
  NETWORK_FAILED: "Network/proxy failed. This is not a moderation refusal.",
8
8
  SAFETY_REFUSAL: "The image backend refused this generation.",
9
9
  MODERATION_REFUSED: "The prompt or image was rejected by moderation.",
10
- AUTH_CHATGPT_EXPIRED: "Run `npx @openai/codex login`, then restart `ima2 serve`.",
10
+ AUTH_CHATGPT_EXPIRED: "Re-run `ima2 setup` (option 1), then restart `ima2 serve`.",
11
11
  REF_TOO_LARGE: "Reference image is too large. Resize/compress it and retry.",
12
12
  REF_NOT_BASE64: "Reference payload is invalid. Use a normal PNG/JPEG/WebP file.",
13
13
  };
@@ -4,11 +4,11 @@ const HINTS: Record<string, string> = {
4
4
  IMAGE_MODEL_UNSUPPORTED:
5
5
  "This model is visible but cannot generate images here. Use gpt-5.4 or gpt-5.4-mini.",
6
6
  INVALID_IMAGE_MODEL: "Use one of: gpt-5.5, gpt-5.4, gpt-5.4-mini.",
7
- OAUTH_UNAVAILABLE: "OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
7
+ OAUTH_UNAVAILABLE: "GPT OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
8
8
  NETWORK_FAILED: "Network/proxy failed. This is not a moderation refusal.",
9
9
  SAFETY_REFUSAL: "The image backend refused this generation.",
10
10
  MODERATION_REFUSED: "The prompt or image was rejected by moderation.",
11
- AUTH_CHATGPT_EXPIRED: "Run `npx @openai/codex login`, then restart `ima2 serve`.",
11
+ AUTH_CHATGPT_EXPIRED: "Re-run `ima2 setup` (option 1), then restart `ima2 serve`.",
12
12
  REF_TOO_LARGE: "Reference image is too large. Resize/compress it and retry.",
13
13
  REF_NOT_BASE64: "Reference payload is invalid. Use a normal PNG/JPEG/WebP file.",
14
14
  };
package/docs/API.md CHANGED
@@ -234,6 +234,8 @@ Generate a video via the Grok video provider. Returns Server-Sent Events.
234
234
  "sourceImage": "<base64>",
235
235
  "referenceImages": ["<base64>", "<base64>"],
236
236
  "referenceFilenames": ["existing-file.png"],
237
+ "continueFromVideo": "1780226256355_50252101.mp4",
238
+ "continuityLineage": { "lineageId": "optional-client-hint", "entries": [] },
237
239
  "sessionId": "optional",
238
240
  "requestId": "optional-client-id"
239
241
  }
@@ -263,16 +265,67 @@ Generate a video via the Grok video provider. Returns Server-Sent Events.
263
265
  | `sourceFilename` | string | — | Existing generated file for image-to-video |
264
266
  | `referenceImages` | string[] | — | Base64 images for reference-to-video |
265
267
  | `referenceFilenames` | string[] | — | Existing generated files for reference-to-video |
268
+ | `continueFromVideo` | string | — | Generated `.mp4` parent; server extracts its last frame and rebuilds lineage from sidecar |
269
+ | `continuityLineage` | object | — | Optional client hint; used only when `continueFromVideo` is absent |
270
+
271
+ Blank prompts return `PROMPT_REQUIRED` with a `guidance` string. The active
272
+ prompt should describe visual flow, motion flow, sound/music/no-music,
273
+ dialogue/no-dialogue, ending frame, and duration pacing. The video planner uses
274
+ the selected duration as the full clip runtime and expands short requests into a
275
+ production-level sequence with opening composition, connected motion/emotion
276
+ change, and a stable ending frame suitable for continuation.
277
+
278
+ When `continueFromVideo` is present, the server treats the generated `.mp4`
279
+ sidecar as authoritative. Client `continuityLineage` cannot override it. The
280
+ saved child sidecar includes `videoContinuity`, a branch-local max-4 stack using
281
+ `keep-start-plus-latest-3` retention.
282
+
283
+ `videoContinuity` shape:
284
+
285
+ ```json
286
+ {
287
+ "lineageId": "lineage:parent",
288
+ "parentFilename": "parent.mp4",
289
+ "sourceFrame": "last",
290
+ "maxEntries": 4,
291
+ "retention": "keep-start-plus-latest-3",
292
+ "entries": [
293
+ {
294
+ "id": "clip:parent.mp4",
295
+ "ordinal": 1,
296
+ "role": "start",
297
+ "filename": "parent.mp4",
298
+ "userPrompt": "original user prompt",
299
+ "revisedPrompt": "planner prompt actually sent to Grok video",
300
+ "createdAt": 1780300000000
301
+ }
302
+ ]
303
+ }
304
+ ```
305
+
306
+ Entry `role` is `start`, `ancestor`, `parent`, or `current`. The first clip is
307
+ kept as the start anchor; later generations keep only the latest three entries.
308
+ `lineageId` uses the generated video basename without the `.mp4` extension.
309
+ This metadata is stored in the generated `.mp4.json` sidecar and returned in
310
+ history rows and video `done` events; `/generated/*.json` remains private.
311
+
312
+ Grok prompt surfaces used by video APIs:
313
+
314
+ | Surface | Model | Responsibility |
315
+ |---|---|---|
316
+ | Video planner | `grok-4.3` | Converts user prompt, search context, refs, and optional continuity lineage into the final English video prompt. It must structure core subject, action/motion, camera/composition, environment/style, dialogue/audio, ending-frame handoff, and constraints. |
317
+ | Video generation | xAI video model | Receives the planner prompt plus `sourceImage` or `referenceImages` when present. |
318
+ | Video analysis | `grok-4.3` | Reads first/last frame images from `/api/video/analyze` and returns recreation/continuation guidance. |
266
319
 
267
320
  **SSE events**:
268
321
 
269
322
  | Event | Data | Description |
270
323
  |---|---|---|
271
324
  | `planning` | `{ requestId }` | Preparing video generation |
272
- | `submitted` | `{ requestId, xaiVideoRequestId }` | Submitted to xAI |
325
+ | `submitted` | `{ requestId, xaiVideoRequestId, requestedModel, effectiveModel, modelFallback }` | Submitted to xAI |
273
326
  | `progress` | `{ requestId, progress, stalled }` | Progress 0.0–1.0 |
274
- | `done` | `{ requestId, filename, url, mediaType, revisedPrompt, elapsed, usage, video }` | Video ready |
275
- | `error` | `{ error, code, status, requestId }` | Generation failed |
327
+ | `done` | `{ requestId, filename, url, mediaType, revisedPrompt, elapsed, usage, requestedModel, effectiveModel, modelFallback, video, videoContinuity }` | Video ready |
328
+ | `error` | `{ error, code, status, requestId, guidance? }` | Generation failed |
276
329
 
277
330
  **Video error codes**:
278
331
 
@@ -286,6 +339,57 @@ Generate a video via the Grok video provider. Returns Server-Sent Events.
286
339
  | `INVALID_VIDEO_DURATION` | Duration not 1–15 integer |
287
340
  | `GROK_VIDEO_REF_TOO_MANY` | More than 7 reference images |
288
341
  | `GROK_VIDEO_FAILED` | Upstream xAI video generation failed |
342
+ | `GROK_VIDEO_FRAME_FAILED` | Server could not extract the parent video's last frame |
343
+
344
+ ### `POST /api/video/edit`
345
+
346
+ Edit an existing video via Grok V2V. This is a blocking JSON endpoint that starts the xAI edit job, polls it, downloads the final MP4, and saves it as a generated video artifact.
347
+
348
+ ```json
349
+ {
350
+ "prompt": "make it sunset",
351
+ "videoUrl": "https://vidgen.x.ai/.../clip.mp4",
352
+ "model": "grok-imagine-video"
353
+ }
354
+ ```
355
+
356
+ `videoUrl` may be an HTTPS video URL, xAI `file_id`, `data:video/*` URL, or generated `.mp4` filename. Generated-file inputs are restricted to real `.mp4` files under the generated directory.
357
+
358
+ ### `POST /api/video/extend`
359
+
360
+ Extend a video from its last frame. This is a blocking JSON endpoint that starts the xAI extension job, polls it, downloads the combined output MP4, and saves it as a generated video artifact.
361
+
362
+ ```json
363
+ {
364
+ "prompt": "camera pulls back",
365
+ "videoUrl": "1780226256355_50252101.mp4",
366
+ "duration": 6,
367
+ "model": "grok-imagine-video"
368
+ }
369
+ ```
370
+
371
+ `duration` must be an integer from 2 to 10 seconds. Edit and extension support `grok-imagine-video` only; `grok-imagine-video-1.5-preview` is not accepted for these endpoints.
372
+
373
+ ### `GET /api/video/frame`
374
+
375
+ Extract a PNG frame from a generated `.mp4` file.
376
+
377
+ | Query | Notes |
378
+ |---|---|
379
+ | `file` | Required generated `.mp4` filename or generated-dir absolute path |
380
+ | `position` | `last` (default) or non-negative seconds |
381
+
382
+ ### `POST /api/video/analyze`
383
+
384
+ Analyze first and last frames from a generated `.mp4` using Grok 4.3 image understanding. This does not upload the video as temporal video; it extracts two PNG frames and asks the vision model to infer likely motion.
385
+
386
+ ```json
387
+ {
388
+ "videoUrl": "1780226256355_50252101.mp4"
389
+ }
390
+ ```
391
+
392
+ Remote URLs and `data:` inputs are intentionally rejected to avoid server-side URL fetching through `ffmpeg`.
289
393
 
290
394
  ## History
291
395
 
@@ -368,6 +472,11 @@ Most server routes under `/api/*` have a CLI wrapper. The exception is **Agent M
368
472
  | `POST /api/edit` | `ima2 edit` |
369
473
  | `POST /api/generate/multimode` (SSE) | `ima2 multimode` |
370
474
  | `POST /api/video/generate` (SSE) | `ima2 video` |
475
+ | `POST /api/video/generate` with `continueFromVideo` | `ima2 video continue` |
476
+ | `POST /api/video/edit` | `ima2 video edit` |
477
+ | `POST /api/video/extend` | `ima2 video extend` |
478
+ | `GET /api/video/frame` | `ima2 video frame` |
479
+ | `POST /api/video/analyze` | `ima2 video analyze` |
371
480
  | `POST /api/node/generate` (SSE) / `GET /api/node/:id` | `ima2 node generate` / `ima2 node show` |
372
481
  | `GET /api/history` | `ima2 ls` |
373
482
  | `DELETE /api/history/:name` / `…/permanent` | `ima2 history rm [--permanent]` |