ima2-gen 1.1.20 → 1.1.22

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 (239) hide show
  1. package/README.md +42 -26
  2. package/bin/commands/capabilities.js +2 -2
  3. package/bin/commands/defaults.js +2 -2
  4. package/bin/commands/doctor.js +3 -3
  5. package/bin/commands/edit.js +1 -1
  6. package/bin/commands/gen.js +1 -1
  7. package/bin/commands/grok.js +16 -11
  8. package/bin/commands/multimode.js +1 -1
  9. package/bin/commands/observability.js +2 -2
  10. package/bin/commands/video.js +335 -13
  11. package/bin/ima2.js +23 -13
  12. package/bin/lib/error-hints.js +2 -2
  13. package/bin/lib/platform.js +34 -5
  14. package/docs/API.md +112 -3
  15. package/docs/CLI.md +61 -7
  16. package/docs/FAQ.ko.md +15 -20
  17. package/docs/FAQ.md +14 -19
  18. package/docs/NPX_QUICKSTART.md +40 -0
  19. package/docs/PROMPT_STUDIO.ko.md +1 -1
  20. package/docs/PROMPT_STUDIO.md +1 -1
  21. package/docs/README.ja.md +6 -16
  22. package/docs/README.ko.md +35 -14
  23. package/docs/README.zh-CN.md +7 -17
  24. package/docs/migration/runtime-test-inventory.md +8 -1
  25. package/lib/agentQueueWorker.js +6 -0
  26. package/lib/agentRuntime.js +20 -5
  27. package/lib/atomicWrite.js +14 -0
  28. package/lib/capabilities.js +1 -1
  29. package/lib/generationErrors.js +1 -1
  30. package/lib/grokProxyLauncher.js +31 -6
  31. package/lib/grokVideoAdapter.js +18 -89
  32. package/lib/grokVideoCanvas.js +25 -0
  33. package/lib/grokVideoDownload.js +58 -0
  34. package/lib/grokVideoPlannerPrompt.js +64 -0
  35. package/lib/historyList.js +7 -1
  36. package/lib/inflight.js +1 -1
  37. package/lib/oauthLauncher.js +26 -6
  38. package/lib/videoContinuity.js +149 -0
  39. package/lib/videoFrameExtract.js +80 -0
  40. package/node_modules/progrok/dist/index.js +187 -88
  41. package/node_modules/progrok/dist/index.js.map +1 -1
  42. package/node_modules/progrok/package.json +1 -1
  43. package/node_modules/progrok/skills/progrok/SKILL.md +33 -4
  44. package/package.json +6 -8
  45. package/routes/edit.js +2 -1
  46. package/routes/generate.js +4 -3
  47. package/routes/health.js +4 -3
  48. package/routes/index.js +4 -0
  49. package/routes/multimode.js +2 -1
  50. package/routes/quota.js +66 -0
  51. package/routes/video.js +80 -16
  52. package/routes/videoExtended.js +293 -0
  53. package/server.js +35 -4
  54. package/skills/ima2/SKILL.md +320 -7
  55. package/ui/dist/.vite/manifest.json +12 -12
  56. package/ui/dist/assets/{AgentWorkspace-DS8uvoLI.js → AgentWorkspace-COxQ5TjU.js} +2 -2
  57. package/ui/dist/assets/{CardNewsWorkspace-CYxMsE67.js → CardNewsWorkspace-B0OkcuVz.js} +1 -1
  58. package/ui/dist/assets/{NodeCanvas-DccIc347.js → NodeCanvas-BSsclEBh.js} +1 -1
  59. package/ui/dist/assets/{PromptBuilderPanel-BvxxwSJp.js → PromptBuilderPanel-DpC9A5Rz.js} +1 -1
  60. package/ui/dist/assets/{PromptImportDialog-u1_BFDRd.js → PromptImportDialog-CVwT0rLd.js} +2 -2
  61. package/ui/dist/assets/{PromptImportDiscoverySection-C5uvkVSz.js → PromptImportDiscoverySection-BDCkRCRs.js} +1 -1
  62. package/ui/dist/assets/{PromptImportFolderSection-D3E_O1SD.js → PromptImportFolderSection-QoKbZD83.js} +1 -1
  63. package/ui/dist/assets/{PromptLibraryPanel-4gyf9CB9.js → PromptLibraryPanel-BhFgeKnY.js} +2 -2
  64. package/ui/dist/assets/SettingsWorkspace-CfjrlH5R.js +1 -0
  65. package/ui/dist/assets/index-C-mur7pa.css +1 -0
  66. package/ui/dist/assets/index-CCP5nUOj.js +42 -0
  67. package/ui/dist/assets/{index-DoKtXbod.js → index-Cxhzi3bs.js} +1 -1
  68. package/ui/dist/index.html +2 -2
  69. package/vendor/progrok-0.2.0.tgz +0 -0
  70. package/bin/commands/annotate.ts +0 -119
  71. package/bin/commands/cancel.ts +0 -48
  72. package/bin/commands/canvas-versions.ts +0 -80
  73. package/bin/commands/capabilities.ts +0 -110
  74. package/bin/commands/cardnews.ts +0 -249
  75. package/bin/commands/comfy.ts +0 -54
  76. package/bin/commands/config.ts +0 -186
  77. package/bin/commands/defaults.ts +0 -192
  78. package/bin/commands/doctor.ts +0 -202
  79. package/bin/commands/edit.ts +0 -150
  80. package/bin/commands/gen.ts +0 -214
  81. package/bin/commands/grok.ts +0 -85
  82. package/bin/commands/history.ts +0 -146
  83. package/bin/commands/ls.ts +0 -64
  84. package/bin/commands/metadata.ts +0 -39
  85. package/bin/commands/multimode.ts +0 -196
  86. package/bin/commands/node.ts +0 -166
  87. package/bin/commands/observability.ts +0 -176
  88. package/bin/commands/ping.ts +0 -31
  89. package/bin/commands/prompt-sub/build.ts +0 -101
  90. package/bin/commands/prompt.ts +0 -492
  91. package/bin/commands/ps.ts +0 -81
  92. package/bin/commands/session.ts +0 -266
  93. package/bin/commands/show.ts +0 -72
  94. package/bin/commands/skill.ts +0 -70
  95. package/bin/commands/video.ts +0 -205
  96. package/bin/ima2.ts +0 -430
  97. package/bin/lib/args.ts +0 -92
  98. package/bin/lib/browser-id.ts +0 -16
  99. package/bin/lib/client.ts +0 -122
  100. package/bin/lib/config-store.ts +0 -120
  101. package/bin/lib/destructive-confirm.ts +0 -19
  102. package/bin/lib/doctor-checks.ts +0 -91
  103. package/bin/lib/error-hints.ts +0 -23
  104. package/bin/lib/files.ts +0 -39
  105. package/bin/lib/output.ts +0 -73
  106. package/bin/lib/platform.ts +0 -99
  107. package/bin/lib/recover-output.ts +0 -139
  108. package/bin/lib/sse.ts +0 -73
  109. package/bin/lib/star-prompt.ts +0 -97
  110. package/bin/lib/storage-doctor.ts +0 -39
  111. package/bin/lib/ui-build.ts +0 -85
  112. package/config.ts +0 -354
  113. package/lib/agentCommandParser.ts +0 -69
  114. package/lib/agentGenerationPlanner.ts +0 -273
  115. package/lib/agentQuestionResponder.ts +0 -266
  116. package/lib/agentQueueStore.ts +0 -270
  117. package/lib/agentQueueWorker.ts +0 -89
  118. package/lib/agentRuntime.ts +0 -592
  119. package/lib/agentSettings.ts +0 -72
  120. package/lib/agentStore.ts +0 -422
  121. package/lib/agentStoreRows.ts +0 -136
  122. package/lib/agentTypes.ts +0 -154
  123. package/lib/apiCachePolicy.ts +0 -11
  124. package/lib/assetLifecycle.ts +0 -146
  125. package/lib/canvasVersionStore.ts +0 -223
  126. package/lib/capabilities.ts +0 -126
  127. package/lib/cardNewsGenerator.ts +0 -271
  128. package/lib/cardNewsJobStore.ts +0 -142
  129. package/lib/cardNewsManifestStore.ts +0 -154
  130. package/lib/cardNewsPlanner.ts +0 -236
  131. package/lib/cardNewsPlannerClient.ts +0 -155
  132. package/lib/cardNewsPlannerPrompt.ts +0 -62
  133. package/lib/cardNewsPlannerSchema.ts +0 -321
  134. package/lib/cardNewsRoleTemplateStore.ts +0 -47
  135. package/lib/cardNewsTemplateStore.ts +0 -252
  136. package/lib/codexDetect.ts +0 -71
  137. package/lib/comfyBridge.ts +0 -235
  138. package/lib/composerSnapshot.ts +0 -33
  139. package/lib/configKeys.ts +0 -62
  140. package/lib/db.ts +0 -295
  141. package/lib/errInfo.ts +0 -43
  142. package/lib/errorClassify.ts +0 -100
  143. package/lib/generationCancel.ts +0 -28
  144. package/lib/generationErrors.ts +0 -238
  145. package/lib/grokImageAdapter.ts +0 -513
  146. package/lib/grokMultimodeAdapter.ts +0 -84
  147. package/lib/grokProxyLauncher.ts +0 -129
  148. package/lib/grokRuntime.ts +0 -23
  149. package/lib/grokSizeMapper.ts +0 -71
  150. package/lib/grokVideoAdapter.ts +0 -519
  151. package/lib/historyIndex.ts +0 -51
  152. package/lib/historyList.ts +0 -177
  153. package/lib/imageMetadata.ts +0 -113
  154. package/lib/imageMetadataStore.ts +0 -67
  155. package/lib/imageModels.ts +0 -165
  156. package/lib/inflight.ts +0 -281
  157. package/lib/localImportStore.ts +0 -114
  158. package/lib/logger.ts +0 -161
  159. package/lib/nodeStore.ts +0 -91
  160. package/lib/oauthLauncher.ts +0 -78
  161. package/lib/oauthNormalize.ts +0 -30
  162. package/lib/oauthProxy/errors.ts +0 -128
  163. package/lib/oauthProxy/generators.ts +0 -494
  164. package/lib/oauthProxy/index.ts +0 -28
  165. package/lib/oauthProxy/prompts.ts +0 -123
  166. package/lib/oauthProxy/references.ts +0 -45
  167. package/lib/oauthProxy/runtime.ts +0 -115
  168. package/lib/oauthProxy/streams.ts +0 -232
  169. package/lib/oauthProxy/types.ts +0 -9
  170. package/lib/oauthProxy.ts +0 -3
  171. package/lib/openDirectory.ts +0 -47
  172. package/lib/pngInfo.ts +0 -26
  173. package/lib/promptBuilder/attachments.ts +0 -74
  174. package/lib/promptBuilder/client.ts +0 -130
  175. package/lib/promptBuilder/constants.ts +0 -9
  176. package/lib/promptBuilder/context.ts +0 -36
  177. package/lib/promptBuilder/errors.ts +0 -12
  178. package/lib/promptBuilder/requestSchema.ts +0 -56
  179. package/lib/promptBuilder/responseParser.ts +0 -219
  180. package/lib/promptBuilder/systemPrompt.ts +0 -135
  181. package/lib/promptBuilder/transport.ts +0 -94
  182. package/lib/promptBuilder/types.ts +0 -109
  183. package/lib/promptImport/curatedSources.ts +0 -141
  184. package/lib/promptImport/discoveryRegistry.ts +0 -329
  185. package/lib/promptImport/errors.ts +0 -18
  186. package/lib/promptImport/githubDiscovery.ts +0 -309
  187. package/lib/promptImport/githubFolder.ts +0 -397
  188. package/lib/promptImport/githubSource.ts +0 -257
  189. package/lib/promptImport/gptImageHints.ts +0 -70
  190. package/lib/promptImport/parsePromptCandidates.ts +0 -179
  191. package/lib/promptImport/promptIndex.ts +0 -326
  192. package/lib/promptImport/rankPromptCandidates.ts +0 -65
  193. package/lib/promptImport/types.ts +0 -103
  194. package/lib/promptSafetyPolicy.ts +0 -5
  195. package/lib/providerOptions.ts +0 -56
  196. package/lib/referenceImageCompress.ts +0 -84
  197. package/lib/refs.ts +0 -133
  198. package/lib/requestLogger.ts +0 -49
  199. package/lib/responsesDoctor.ts +0 -456
  200. package/lib/responsesErrors.ts +0 -83
  201. package/lib/responsesFallback.ts +0 -114
  202. package/lib/responsesImageAdapter.ts +0 -466
  203. package/lib/responsesParse.ts +0 -452
  204. package/lib/responsesTools.ts +0 -28
  205. package/lib/runtimeContext.ts +0 -146
  206. package/lib/runtimePorts.ts +0 -105
  207. package/lib/sessionStore.ts +0 -308
  208. package/lib/storageMigration.ts +0 -310
  209. package/lib/styleSheet.ts +0 -139
  210. package/lib/systemTrash.ts +0 -20
  211. package/lib/videoSeriesChain.ts +0 -29
  212. package/lib/visibleTextLanguagePolicy.ts +0 -7
  213. package/routes/agent.ts +0 -308
  214. package/routes/annotations.ts +0 -118
  215. package/routes/canvasVersions.ts +0 -69
  216. package/routes/capabilities.ts +0 -18
  217. package/routes/cardNews.ts +0 -211
  218. package/routes/comfy.ts +0 -43
  219. package/routes/edit.ts +0 -352
  220. package/routes/generate.ts +0 -492
  221. package/routes/grok.ts +0 -24
  222. package/routes/health.ts +0 -123
  223. package/routes/history.ts +0 -221
  224. package/routes/imageImport.ts +0 -37
  225. package/routes/index.ts +0 -48
  226. package/routes/metadata.ts +0 -77
  227. package/routes/multimode.ts +0 -499
  228. package/routes/nodes.ts +0 -578
  229. package/routes/promptBuilder.ts +0 -37
  230. package/routes/promptImport.ts +0 -379
  231. package/routes/prompts.ts +0 -428
  232. package/routes/sessions.ts +0 -317
  233. package/routes/storage.ts +0 -47
  234. package/routes/video.ts +0 -232
  235. package/server.ts +0 -290
  236. package/ui/dist/assets/SettingsWorkspace-F3eNu3mJ.js +0 -1
  237. package/ui/dist/assets/index-B6tcw_UF.css +0 -1
  238. package/ui/dist/assets/index-DYOh6gQD.js +0 -32
  239. package/vendor/progrok-0.1.1.tgz +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progrok",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Activate your xAI OAuth session as a local Grok API proxy and CLI tool surface.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -95,19 +95,48 @@ curl http://127.0.0.1:18645/v1/images/edits \
95
95
  ### Pattern 4: Video generation (async — poll)
96
96
 
97
97
  ```bash
98
- # Start: text-to-video, image-to-video (image), or reference-to-video (reference_images)
98
+ # Text-to-video
99
+ progrok video "Ocean waves crashing on rocks" --duration 8 --resolution 720p
100
+
101
+ # Image-to-video
102
+ progrok video "Animate this scene" --image photo.jpg --duration 10
103
+
104
+ # Video editing (real V2V — modify existing video, keep motion)
105
+ progrok video edit "Make the water glow neon blue" --video https://vidgen.x.ai/.../clip.mp4
106
+
107
+ # Video extension (continue from last frame)
108
+ progrok video extend "Camera slowly pulls back" --video https://vidgen.x.ai/.../clip.mp4 --duration 5
109
+ ```
110
+
111
+ **API endpoints** (via proxy at 127.0.0.1:18645):
112
+ ```bash
113
+ # Generate: POST /v1/videos/generations
99
114
  curl -s http://127.0.0.1:18645/v1/videos/generations \
100
115
  -H "Content-Type: application/json" \
101
116
  -d '{"model": "grok-imagine-video", "prompt": "Ocean waves", "duration": 8, "resolution": "720p"}'
102
117
  # → {"request_id": "abc-123"}
103
118
 
104
- # Poll until done
119
+ # Edit (V2V): POST /v1/videos/edits — grok-imagine-video only
120
+ curl -s http://127.0.0.1:18645/v1/videos/edits \
121
+ -H "Content-Type: application/json" \
122
+ -d '{"model": "grok-imagine-video", "prompt": "Add sunset colors", "video": {"url": "https://..."}}'
123
+
124
+ # Extend: POST /v1/videos/extensions — grok-imagine-video only, 2-10s
125
+ curl -s http://127.0.0.1:18645/v1/videos/extensions \
126
+ -H "Content-Type: application/json" \
127
+ -d '{"model": "grok-imagine-video", "prompt": "Continue scene", "duration": 5, "video": {"url": "https://..."}}'
128
+
129
+ # Poll: GET /v1/videos/{request_id}
105
130
  curl http://127.0.0.1:18645/v1/videos/abc-123
106
- # → {"status": "pending", "progress": 45}
107
131
  # → {"status": "done", "video": {"url": "https://...", "duration": 8}}
108
132
  ```
109
133
 
110
- Also: `POST /v1/videos/edits` (edit a clip), `POST /v1/videos/extensions` (extend 1-10s).
134
+ **Model constraints:**
135
+ - `grok-imagine-video`: T2V, I2V, Ref2V, Edit, Extend — all modes
136
+ - `grok-imagine-video-1.5-preview`: I2V, Ref2V only (no T2V, no Edit, no Extend)
137
+ - Edit/Extend input: mp4, H.264/H.265/AV1, max 8.7s (edit) / 2-15s (extend)
138
+ - Edit output inherits input duration/aspect/resolution (max 720p)
139
+ - Extend duration: 2-10s added to original
111
140
 
112
141
  ### Pattern 5: Voice — TTS / STT (HTTP)
113
142
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "1.1.20",
3
+ "version": "1.1.22",
4
4
  "description": "Local OAuth image generation studio with classic and node workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "setup": "node bin/ima2.js setup",
21
21
  "prepack": "npm run ui:build && npm run build:server && npm run build:cli",
22
22
  "prepublishOnly": "npm run typecheck && npm run typecheck:tests && npm run test:inventory && npm run ui:build && npm run build:server && npm run build:cli && npm run lint:pkg && npm run test:package-install",
23
- "lint:pkg": "node -e \"const p=require('./package.json'); const req=['name','version','bin']; for(const k of req){if(!p[k])throw new Error('missing '+k)} const mustInclude=['bin/','lib/','routes/','skills/','integrations/comfyui/ima2_gen_bridge/__init__.py','integrations/comfyui/ima2_gen_bridge/nodes.py','integrations/comfyui/ima2_gen_bridge/README.md','assets/card-news/templates/','vendor/','server.js','LICENSE']; for(const f of mustInclude){if(!p.files.includes(f))throw new Error('files[] must include '+f)}\"",
23
+ "lint:pkg": "node -e \"const p=require('./package.json'); const req=['name','version','bin']; for(const k of req){if(!p[k])throw new Error('missing '+k)} const mustInclude=['bin/**/*.js','lib/**/*.js','routes/**/*.js','skills/','integrations/comfyui/ima2_gen_bridge/__init__.py','integrations/comfyui/ima2_gen_bridge/nodes.py','integrations/comfyui/ima2_gen_bridge/README.md','assets/card-news/templates/','vendor/','server.js','LICENSE']; for(const f of mustInclude){if(!p.files.includes(f))throw new Error('files[] must include '+f)}\"",
24
24
  "publish:dry-run": "node scripts/publish-dry-run.mjs",
25
25
  "release:patch": "npm version patch && npm publish && git push origin main --tags",
26
26
  "release:minor": "npm version minor && npm publish && git push origin main --tags",
@@ -43,9 +43,9 @@
43
43
  "url": "git+https://github.com/lidge-jun/ima2-gen.git"
44
44
  },
45
45
  "files": [
46
- "bin/",
47
- "lib/",
48
- "routes/",
46
+ "bin/**/*.js",
47
+ "lib/**/*.js",
48
+ "routes/**/*.js",
49
49
  "skills/",
50
50
  "integrations/comfyui/ima2_gen_bridge/__init__.py",
51
51
  "integrations/comfyui/ima2_gen_bridge/nodes.py",
@@ -54,8 +54,6 @@
54
54
  "docs/",
55
55
  "vendor/",
56
56
  "assets/card-news/templates/",
57
- "server.ts",
58
- "config.ts",
59
57
  ".env.example",
60
58
  "README.md",
61
59
  "LICENSE",
@@ -71,7 +69,7 @@
71
69
  "express": "^5.1.0",
72
70
  "openai": "^5.8.2",
73
71
  "openai-oauth": "^1.0.2",
74
- "progrok": "file:vendor/progrok-0.1.1.tgz",
72
+ "progrok": "file:vendor/progrok-0.2.0.tgz",
75
73
  "sharp": "^0.34.5",
76
74
  "trash": "^10.1.1",
77
75
  "ulid": "^3.0.2"
package/routes/edit.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { mkdir, writeFile } from "fs/promises";
2
+ import { safeWriteSidecar } from "../lib/atomicWrite.js";
2
3
  import { join } from "path";
3
4
  import { randomBytes } from "crypto";
4
5
  import { detectImageMimeFromB64 } from "../lib/refs.js";
@@ -222,7 +223,7 @@ export function registerEditRoutes(app, ctxRaw) {
222
223
  webSearchCalls,
223
224
  webSearchEnabled,
224
225
  };
225
- await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
226
+ await safeWriteSidecar(join(ctx.config.storage.generatedDir, filename + ".json"), meta);
226
227
  invalidateHistoryIndex();
227
228
  finishHttpStatus = 200;
228
229
  finishMeta = { filename, imageChars: resultB64.length };
@@ -1,4 +1,5 @@
1
1
  import { mkdir, readFile, writeFile } from "fs/promises";
2
+ import { safeWriteSidecar, atomicWriteJson } from "../lib/atomicWrite.js";
2
3
  import { join } from "path";
3
4
  import { randomBytes } from "crypto";
4
5
  import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
@@ -260,7 +261,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
260
261
  });
261
262
  }
262
263
  await writeFile(join(ctx.config.storage.generatedDir, filename), embedded.buffer);
263
- await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
264
+ await safeWriteSidecar(join(ctx.config.storage.generatedDir, filename + ".json"), meta);
264
265
  invalidateHistoryIndex();
265
266
  images.push({
266
267
  image: `data:${resultMime};base64,${r.value.b64}`,
@@ -346,10 +347,10 @@ export function registerGenerateRoutes(app, ctxRaw) {
346
347
  const sidecarPath = join(ctx.config.storage.generatedDir, filename + ".json");
347
348
  const sidecarMeta = JSON.parse(await readFile(sidecarPath, "utf-8"));
348
349
  sidecarMeta.elapsed = elapsed;
349
- await writeFile(sidecarPath, JSON.stringify(sidecarMeta));
350
+ await atomicWriteJson(sidecarPath, sidecarMeta);
350
351
  }
351
352
  catch {
352
- /* best-effort, matches the sidecar write .catch(() => {}) above */
353
+ /* best-effort elapsed patch */
353
354
  }
354
355
  }));
355
356
  const firstRevised = images[0]?.revisedPrompt || null;
package/routes/health.js CHANGED
@@ -95,10 +95,11 @@ export function registerHealthRoutes(app, ctxRaw) {
95
95
  const headers = { Authorization: `Bearer ${ctx.apiKey}`, "Content-Type": "application/json" };
96
96
  const start = Math.floor(new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime() / 1000);
97
97
  const end = Math.floor(Date.now() / 1000);
98
+ const billingSignal = AbortSignal.timeout(10_000);
98
99
  const [subRes, usageRes, modelsRes] = await Promise.allSettled([
99
- fetch(`https://api.openai.com/v1/organization/costs?start_time=${start}&end_time=${end}&bucket_width=1d&limit=31`, { headers }),
100
- fetch("https://api.openai.com/dashboard/billing/credit_grants", { headers }),
101
- fetch("https://api.openai.com/v1/models", { headers }),
100
+ fetch(`https://api.openai.com/v1/organization/costs?start_time=${start}&end_time=${end}&bucket_width=1d&limit=31`, { headers, signal: billingSignal }),
101
+ fetch("https://api.openai.com/dashboard/billing/credit_grants", { headers, signal: billingSignal }),
102
+ fetch("https://api.openai.com/v1/models", { headers, signal: billingSignal }),
102
103
  ]);
103
104
  const billing = { apiKeySource: ctx.apiKeySource ?? "env" };
104
105
  if (subRes.status === "fulfilled" && subRes.value.ok)
package/routes/index.js CHANGED
@@ -19,6 +19,8 @@ import { registerPromptBuilderRoutes } from "./promptBuilder.js";
19
19
  import { registerAgentRoutes } from "./agent.js";
20
20
  import { registerGrokRoutes } from "./grok.js";
21
21
  import { registerVideoRoutes } from "./video.js";
22
+ import { registerVideoExtendedRoutes } from "./videoExtended.js";
23
+ import { registerQuotaRoutes } from "./quota.js";
22
24
  import { requireRuntimeContext } from "../lib/runtimeContext.js";
23
25
  export function configureRoutes(app, ctxRaw) {
24
26
  const ctx = requireRuntimeContext(ctxRaw);
@@ -44,4 +46,6 @@ export function configureRoutes(app, ctxRaw) {
44
46
  registerPromptImportRoutes(app, ctx);
45
47
  registerGrokRoutes(app, ctx);
46
48
  registerVideoRoutes(app, ctx);
49
+ registerVideoExtendedRoutes(app, ctx);
50
+ registerQuotaRoutes(app, ctx);
47
51
  }
@@ -1,4 +1,5 @@
1
1
  import { mkdir, writeFile } from "fs/promises";
2
+ import { safeWriteSidecar } from "../lib/atomicWrite.js";
2
3
  import { join } from "path";
3
4
  import { randomBytes } from "crypto";
4
5
  import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
@@ -217,7 +218,7 @@ export function registerMultimodeRoutes(app, ctxRaw) {
217
218
  version: ctx.packageVersion,
218
219
  });
219
220
  await writeFile(join(ctx.config.storage.generatedDir, filename), embedded.buffer);
220
- await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
221
+ await safeWriteSidecar(join(ctx.config.storage.generatedDir, filename + ".json"), meta);
221
222
  invalidateHistoryIndex();
222
223
  const item = {
223
224
  image: `data:${resultMime};base64,${image.b64}`,
@@ -0,0 +1,66 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ function readCodexTokens() {
5
+ const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
6
+ try {
7
+ const j = JSON.parse(readFileSync(join(codexHome, "auth.json"), "utf8"));
8
+ if (j?.tokens?.access_token) {
9
+ return { access_token: j.tokens.access_token, account_id: j.tokens.account_id ?? "" };
10
+ }
11
+ }
12
+ catch { }
13
+ return null;
14
+ }
15
+ async function fetchCodexUsage(tokens) {
16
+ try {
17
+ const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
18
+ headers: {
19
+ Authorization: `Bearer ${tokens.access_token}`,
20
+ "ChatGPT-Account-Id": tokens.account_id,
21
+ },
22
+ signal: AbortSignal.timeout(8000),
23
+ });
24
+ if (!resp.ok) {
25
+ if (resp.status === 401 || resp.status === 403)
26
+ return { provider: "codex", authenticated: false, windows: [] };
27
+ return { provider: "codex", error: true, windows: [] };
28
+ }
29
+ const data = await resp.json();
30
+ const account = { email: data.email ?? null, plan: data.plan_type ?? null };
31
+ const windows = [];
32
+ if (data.rate_limit?.primary_window) {
33
+ windows.push({
34
+ label: "5h",
35
+ percent: Math.round(data.rate_limit.primary_window.used_percent ?? 0),
36
+ resetsAt: data.rate_limit.primary_window.reset_at
37
+ ? new Date(data.rate_limit.primary_window.reset_at * 1000).toISOString()
38
+ : null,
39
+ });
40
+ }
41
+ if (data.rate_limit?.secondary_window) {
42
+ windows.push({
43
+ label: "7d",
44
+ percent: Math.round(data.rate_limit.secondary_window.used_percent ?? 0),
45
+ resetsAt: data.rate_limit.secondary_window.reset_at
46
+ ? new Date(data.rate_limit.secondary_window.reset_at * 1000).toISOString()
47
+ : null,
48
+ });
49
+ }
50
+ return { provider: "codex", account, windows };
51
+ }
52
+ catch {
53
+ return { provider: "codex", error: true, windows: [] };
54
+ }
55
+ }
56
+ export function registerQuotaRoutes(app, _ctx) {
57
+ app.get("/api/quota", async (_req, res) => {
58
+ const tokens = readCodexTokens();
59
+ if (!tokens) {
60
+ res.json({ codex: { provider: "codex", authenticated: false, windows: [] } });
61
+ return;
62
+ }
63
+ const codex = await fetchCodexUsage(tokens);
64
+ res.json({ codex });
65
+ });
66
+ }
package/routes/video.js CHANGED
@@ -1,4 +1,5 @@
1
- import { mkdir, readFile, writeFile } from "fs/promises";
1
+ import { mkdir, readFile, unlink, writeFile } from "fs/promises";
2
+ import { atomicWriteJson } from "../lib/atomicWrite.js";
2
3
  import { join } from "path";
3
4
  import { randomBytes } from "crypto";
4
5
  import { startJob, finishJob, registerJobAbortController, isJobCanceled, setJobPhase } from "../lib/inflight.js";
@@ -7,6 +8,8 @@ import { logEvent, logError } from "../lib/logger.js";
7
8
  import { invalidateHistoryIndex } from "../lib/historyIndex.js";
8
9
  import { generateVideoViaGrok } from "../lib/grokVideoAdapter.js";
9
10
  import { getVideoSeriesChain } from "../lib/videoSeriesChain.js";
11
+ import { ACTIVE_VIDEO_PROMPT_GUIDANCE, appendVideoContinuityEntry, lineageFromVideoMetadata, normalizeVideoContinuityLineage, readVideoSidecar, requireActiveVideoPrompt, safeGeneratedVideoFilename, } from "../lib/videoContinuity.js";
12
+ import { extractGeneratedVideoFrameB64 } from "../lib/videoFrameExtract.js";
10
13
  import { normalizeGrokVideoModel, normalizeVideoResolution, normalizeVideoAspectRatio, normalizeVideoDuration, deriveVideoMode, clampVideoDuration, MAX_REF2V_REFERENCES, } from "../lib/imageModels.js";
11
14
  import { errInfo } from "../lib/errInfo.js";
12
15
  import { requireRuntimeContext } from "../lib/runtimeContext.js";
@@ -20,11 +23,25 @@ function toArray(v) {
20
23
  function isNormalizeError(x) {
21
24
  return typeof x === "object" && x !== null && typeof x.error === "string";
22
25
  }
26
+ export async function saveGeneratedVideoArtifact(ctx, filename, buffer, metadata) {
27
+ const filePath = join(ctx.config.storage.generatedDir, filename);
28
+ await writeFile(filePath, buffer);
29
+ try {
30
+ await atomicWriteJson(`${filePath}.json`, metadata);
31
+ }
32
+ catch (err) {
33
+ await unlink(filePath).catch(() => { });
34
+ throw err;
35
+ }
36
+ }
23
37
  async function resolveSourceImage(ctx, sourceImage, sourceFilename) {
24
38
  if (typeof sourceFilename === "string" && sourceFilename) {
25
39
  const safe = sourceFilename.replace(/^\/+/, "");
26
- if (safe.includes(".."))
40
+ if (safe.includes("..") || safe.includes("/") || safe.includes("\\")) {
27
41
  throw { status: 400, code: "GROK_VIDEO_INVALID_MODE", message: "invalid source filename" };
42
+ }
43
+ if (/\.mp4$/i.test(safe))
44
+ throw { status: 400, code: "GROK_VIDEO_INVALID_MODE", message: "use continueFromVideo for generated video continuation" };
28
45
  const buf = await readFile(join(ctx.config.storage.generatedDir, safe));
29
46
  return { b64: buf.toString("base64"), filename: safe };
30
47
  }
@@ -51,12 +68,12 @@ export function registerVideoRoutes(app, ctxRaw) {
51
68
  res.setHeader("Cache-Control", "no-cache, no-transform");
52
69
  res.setHeader("Connection", "keep-alive");
53
70
  res.flushHeaders?.();
54
- const fail = (status, code, error) => {
71
+ const fail = (status, code, error, extra = {}) => {
55
72
  const httpStatus = status ?? 500;
56
73
  finishStatus = "error";
57
74
  finishHttpStatus = httpStatus;
58
75
  finishErrorCode = code;
59
- sendSse(res, "error", { error, code, status: httpStatus, requestId });
76
+ sendSse(res, "error", { error, code, status: httpStatus, requestId, ...extra });
60
77
  };
61
78
  try {
62
79
  const { prompt, provider = "grok", model: rawModel } = req.body || {};
@@ -65,8 +82,9 @@ export function registerVideoRoutes(app, ctxRaw) {
65
82
  const topic = typeof req.body?.topic === "string" ? req.body.topic.trim() : "";
66
83
  if (provider !== "grok")
67
84
  return fail(400, "VIDEO_PROVIDER_UNSUPPORTED", "video generation requires provider 'grok'");
68
- if (typeof prompt !== "string" || !prompt.trim())
69
- return fail(400, "PROMPT_REQUIRED", "Prompt is required");
85
+ const activePrompt = requireActiveVideoPrompt(prompt);
86
+ if (!activePrompt)
87
+ return fail(400, "PROMPT_REQUIRED", "Prompt is required", { guidance: ACTIVE_VIDEO_PROMPT_GUIDANCE });
70
88
  const modelCheck = normalizeGrokVideoModel(rawModel);
71
89
  if (isNormalizeError(modelCheck))
72
90
  return fail(modelCheck.status, modelCheck.code, modelCheck.error);
@@ -80,6 +98,21 @@ export function registerVideoRoutes(app, ctxRaw) {
80
98
  if (isNormalizeError(aspectCheck))
81
99
  return fail(aspectCheck.status, aspectCheck.code, aspectCheck.error);
82
100
  // Resolve reference inputs: base64 list + existing-file list + legacy single source.
101
+ let parentLineage = null;
102
+ let continueFromVideoFilename = null;
103
+ if (typeof req.body?.continueFromVideo === "string" && req.body.continueFromVideo.trim()) {
104
+ try {
105
+ continueFromVideoFilename = safeGeneratedVideoFilename(req.body.continueFromVideo);
106
+ const parentMeta = await readVideoSidecar(ctx.config.storage.generatedDir, continueFromVideoFilename);
107
+ parentLineage = lineageFromVideoMetadata(continueFromVideoFilename, parentMeta);
108
+ }
109
+ catch (e) {
110
+ return fail(e?.status || 400, "GROK_VIDEO_INVALID_MODE", e?.message || "invalid continuation video");
111
+ }
112
+ }
113
+ else {
114
+ parentLineage = normalizeVideoContinuityLineage(req.body?.continuityLineage);
115
+ }
83
116
  const refInputs = [
84
117
  ...toArray(req.body?.referenceImages).map((image) => ({ image })),
85
118
  ...toArray(req.body?.referenceFilenames).map((filename) => ({ filename })),
@@ -87,6 +120,14 @@ export function registerVideoRoutes(app, ctxRaw) {
87
120
  ? [{ image: req.body?.sourceImage, filename: req.body?.sourceFilename }]
88
121
  : []),
89
122
  ];
123
+ if (continueFromVideoFilename && !req.body?.sourceImage && !req.body?.sourceFilename) {
124
+ try {
125
+ refInputs.push({ image: await extractGeneratedVideoFrameB64(ctx.config.storage.generatedDir, continueFromVideoFilename) });
126
+ }
127
+ catch (e) {
128
+ return fail(e?.status || 500, "GROK_VIDEO_FRAME_FAILED", e?.message || "failed to extract continuation frame");
129
+ }
130
+ }
90
131
  let resolved;
91
132
  try {
92
133
  const all = await Promise.all(refInputs.map((r) => resolveSourceImage(ctx, r.image, r.filename)));
@@ -105,7 +146,7 @@ export function registerVideoRoutes(app, ctxRaw) {
105
146
  startJob({
106
147
  requestId,
107
148
  kind: "video",
108
- prompt,
149
+ prompt: activePrompt,
109
150
  meta: { kind: "video", sessionId, clientNodeId, model: modelCheck.model, mode, duration, resolution: resolutionCheck.resolution },
110
151
  });
111
152
  registerJobAbortController(requestId, cancelController);
@@ -115,7 +156,13 @@ export function registerVideoRoutes(app, ctxRaw) {
115
156
  const onEvent = (ev) => {
116
157
  if (ev.phase === "submitted") {
117
158
  setJobPhase(requestId, "streaming");
118
- sendSse(res, "submitted", { requestId, xaiVideoRequestId: ev.xaiVideoRequestId });
159
+ sendSse(res, "submitted", {
160
+ requestId,
161
+ xaiVideoRequestId: ev.xaiVideoRequestId,
162
+ requestedModel: ev.requestedModel,
163
+ effectiveModel: ev.effectiveModel,
164
+ modelFallback: ev.modelFallback ?? null,
165
+ });
119
166
  }
120
167
  else if (ev.phase === "progress") {
121
168
  sendSse(res, "progress", { requestId, progress: typeof ev.progress === "number" ? ev.progress / 100 : null, stalled: Boolean(ev.stalled) });
@@ -126,10 +173,10 @@ export function registerVideoRoutes(app, ctxRaw) {
126
173
  }
127
174
  };
128
175
  // Build prompt with series chain context
129
- const chain = topic ? await getVideoSeriesChain(ctx.config.storage.generatedDir, topic) : [];
176
+ const chain = !parentLineage && topic ? await getVideoSeriesChain(ctx.config.storage.generatedDir, topic) : [];
130
177
  const effectivePrompt = chain.length > 0
131
- ? `[Series topic: ${topic}]\n[Previous prompts in series:\n${chain.map((p, i) => `${i + 1}. ${p}`).join("\n")}\n]\n\n${prompt}`
132
- : prompt;
178
+ ? `[Series topic: ${topic}]\n[Previous prompts in series:\n${chain.map((p, i) => `${i + 1}. ${p}`).join("\n")}\n]\n\n${activePrompt}`
179
+ : activePrompt;
133
180
  const result = await generateVideoViaGrok(effectivePrompt, ctx, {
134
181
  model: modelCheck.model,
135
182
  mode,
@@ -140,22 +187,32 @@ export function registerVideoRoutes(app, ctxRaw) {
140
187
  referenceImages,
141
188
  signal: cancelController.signal,
142
189
  requestId,
190
+ continuityLineage: parentLineage,
143
191
  onEvent,
144
192
  });
145
193
  const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
146
194
  const filename = `${Date.now()}_${rand}.mp4`;
147
195
  const elapsed = +((Date.now() - startTime) / 1000).toFixed(1);
196
+ const videoContinuity = appendVideoContinuityEntry(parentLineage, {
197
+ filename,
198
+ userPrompt: activePrompt,
199
+ revisedPrompt: result.revisedPrompt,
200
+ createdAt: Date.now(),
201
+ });
148
202
  const meta = {
149
203
  kind: "video",
150
204
  mediaType: "video",
151
205
  requestId,
152
206
  sessionId,
153
207
  clientNodeId,
154
- prompt,
155
- userPrompt: prompt,
208
+ prompt: activePrompt,
209
+ userPrompt: activePrompt,
156
210
  revisedPrompt: result.revisedPrompt,
157
211
  provider: "grok",
158
- model: modelCheck.model,
212
+ model: result.effectiveModel,
213
+ requestedModel: result.requestedModel,
214
+ effectiveModel: result.effectiveModel,
215
+ modelFallback: result.modelFallback,
159
216
  createdAt: Date.now(),
160
217
  elapsed,
161
218
  usage: result.usage,
@@ -166,11 +223,14 @@ export function registerVideoRoutes(app, ctxRaw) {
166
223
  aspectRatio: result.aspectRatio,
167
224
  sourceImageFilename: sourceFilename,
168
225
  xaiVideoRequestId: result.xaiVideoRequestId,
226
+ requestedModel: result.requestedModel,
227
+ effectiveModel: result.effectiveModel,
228
+ modelFallback: result.modelFallback,
169
229
  },
230
+ videoContinuity,
170
231
  ...(topic ? { videoSeries: { topic, chainIndex: chain.length } } : {}),
171
232
  };
172
- await writeFile(join(ctx.config.storage.generatedDir, filename), result.videoBuffer);
173
- await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
233
+ await saveGeneratedVideoArtifact(ctx, filename, result.videoBuffer, meta);
174
234
  invalidateHistoryIndex();
175
235
  finishMeta = { filename, xaiVideoRequestId: result.xaiVideoRequestId };
176
236
  logEvent("video", "saved", { requestId, filename, bytes: result.videoBuffer.length, elapsedMs: Date.now() - startTime });
@@ -182,7 +242,11 @@ export function registerVideoRoutes(app, ctxRaw) {
182
242
  revisedPrompt: result.revisedPrompt,
183
243
  elapsed,
184
244
  usage: result.usage,
245
+ requestedModel: result.requestedModel,
246
+ effectiveModel: result.effectiveModel,
247
+ modelFallback: result.modelFallback,
185
248
  video: meta.video,
249
+ videoContinuity,
186
250
  ...(meta.videoSeries ? { videoSeries: meta.videoSeries } : {}),
187
251
  });
188
252
  }