@j-o-r/hello-dave 0.1.1 → 0.1.4

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 (173) hide show
  1. package/CHANGELOG.md +42 -25
  2. package/README.md +81 -221
  3. package/TODO.md +173 -35
  4. package/agents/agent_creator.js +105 -0
  5. package/agents/agent_creator.prompt.md +371 -0
  6. package/agents/ask_agent.js +64 -127
  7. package/agents/claude_agent.js +68 -0
  8. package/agents/code_agent.js +55 -135
  9. package/agents/code_agent.prompt.md +50 -0
  10. package/agents/echo_agent.js +76 -0
  11. package/agents/financial_expert.js +75 -0
  12. package/agents/gpt_agent.js +52 -103
  13. package/agents/gpt_code.js +81 -0
  14. package/agents/grok_agent.js +58 -114
  15. package/agents/minimax_agent.js +92 -0
  16. package/agents/mureka_agent.js +77 -0
  17. package/agents/planner_agent.js +172 -0
  18. package/agents/stability_agent.js +87 -0
  19. package/agents/test_agent.js +75 -157
  20. package/agents/weather_agent.js +73 -0
  21. package/agents/workflow_agent.js +189 -0
  22. package/bin/dave.js +436 -184
  23. package/docs/bin-dave.md +85 -35
  24. package/docs/cdn-ssh.md +100 -0
  25. package/docs/creating-agents.md +301 -0
  26. package/docs/creating-toolsets.md +336 -0
  27. package/docs/docs-organization.md +48 -0
  28. package/docs/project-overview.md +86 -51
  29. package/lib/API/elevenlabs.io/music.compose.md +441 -0
  30. package/lib/API/elevenlabs.io/music.create-composition-plan.md +370 -0
  31. package/lib/API/elevenlabs.io/music.stream.md +425 -0
  32. package/lib/API/lalal.ai/lalal.js +445 -0
  33. package/lib/API/lalal.ai/openapi.json +2614 -0
  34. package/lib/API/minimax/ImageToolset.js +82 -37
  35. package/lib/API/minimax/MusicToolset.js +125 -79
  36. package/lib/API/minimax/VideoToolset.js +170 -167
  37. package/lib/API/minimax/image.js +5 -1
  38. package/lib/API/minimax/music.js +210 -23
  39. package/lib/API/minimax/video.js +242 -53
  40. package/lib/API/mureka/MusicToolset.js +646 -0
  41. package/lib/API/mureka/README.md +41 -0
  42. package/lib/API/mureka/index.js +7 -0
  43. package/lib/API/mureka/music.js +658 -0
  44. package/lib/API/openai.com/index.js +7 -0
  45. package/lib/API/openai.com/{reponses/text.js → responses.js} +64 -18
  46. package/lib/API/openai.com/video.create.character.md +40 -0
  47. package/lib/API/openai.com/video.create.md +219 -0
  48. package/lib/API/openai.com/video.delete.md +44 -0
  49. package/lib/API/openai.com/video.download.md +31 -0
  50. package/lib/API/openai.com/video.edit.md +155 -0
  51. package/lib/API/openai.com/video.extend.md +166 -0
  52. package/lib/API/openai.com/video.fetch.character.md +43 -0
  53. package/lib/API/openai.com/video.js +784 -0
  54. package/lib/API/openai.com/video.list.md +201 -0
  55. package/lib/API/openai.com/video.remix.md +175 -0
  56. package/lib/API/openai.com/video.retrieve.md +139 -0
  57. package/lib/API/openai.com/videoToolset.js +616 -0
  58. package/lib/API/stability.ai/ImageToolset.js +131 -40
  59. package/lib/API/stability.ai/MusicToolset.js +79 -47
  60. package/lib/API/stability.ai/audio.js +63 -131
  61. package/lib/API/x.ai/chat.responses.md +1040 -0
  62. package/lib/API/x.ai/image.js +229 -59
  63. package/lib/API/x.ai/imageToolset.js +376 -0
  64. package/lib/API/x.ai/index.js +1 -1
  65. package/lib/API/x.ai/responses.js +9 -18
  66. package/lib/Agent.js +271 -0
  67. package/lib/Agent.js.old +284 -0
  68. package/lib/AgentLauncher.js +562 -0
  69. package/lib/Cli.js +87 -13
  70. package/lib/Prompt.js +23 -1
  71. package/lib/Session.js +5 -4
  72. package/lib/ToolSet.js +102 -6
  73. package/lib/agentLoader.js +369 -0
  74. package/lib/cdn.js +67 -231
  75. package/lib/{CdnToolset.js → cdnToolset.js} +47 -64
  76. package/lib/defaultToolsets.js +43 -0
  77. package/lib/fafs.js +1 -1
  78. package/lib/genericToolset.js +442 -119
  79. package/lib/handOffToolset.js +179 -0
  80. package/lib/index.js +34 -27
  81. package/lib/toolsetLoader.js +248 -0
  82. package/package.json +11 -5
  83. package/types/API/lalal.ai/lalal.d.ts +116 -0
  84. package/types/API/minimax/image.d.ts +2 -1
  85. package/types/API/minimax/music.d.ts +189 -26
  86. package/types/API/minimax/video.d.ts +100 -31
  87. package/types/API/mureka/index.d.ts +7 -0
  88. package/types/API/mureka/music.d.ts +472 -0
  89. package/types/API/openai.com/index.d.ts +7 -0
  90. package/types/API/openai.com/{reponses/text.d.ts → responses.d.ts} +11 -11
  91. package/types/API/openai.com/video.d.ts +409 -0
  92. package/types/API/openai.com/videoToolset.d.ts +24 -0
  93. package/types/API/stability.ai/audio.d.ts +14 -103
  94. package/types/API/stability.ai/image.d.ts +2 -2
  95. package/types/API/x.ai/image.d.ts +138 -26
  96. package/types/API/x.ai/imageToolset.d.ts +3 -0
  97. package/types/API/x.ai/index.d.ts +1 -1
  98. package/types/API/x.ai/responses.d.ts +4 -4
  99. package/types/Agent.d.ts +123 -0
  100. package/types/AgentLauncher.d.ts +222 -0
  101. package/types/Cli.d.ts +28 -8
  102. package/types/Prompt.d.ts +23 -5
  103. package/types/Session.d.ts +1 -1
  104. package/types/ToolSet.d.ts +10 -0
  105. package/types/agentLoader.d.ts +78 -0
  106. package/types/cdn.d.ts +15 -90
  107. package/types/defaultToolsets.d.ts +9 -0
  108. package/types/fafs.d.ts +1 -1
  109. package/types/genericToolset.d.ts +1 -1
  110. package/types/handOffToolset.d.ts +28 -0
  111. package/types/index.d.ts +19 -17
  112. package/types/toolsetLoader.d.ts +114 -0
  113. package/utils/format_log.js +101 -23
  114. package/utils/launch_agent.js +18 -0
  115. package/utils/list_sessions.sh +13 -5
  116. package/utils/search_sessions.sh +65 -29
  117. package/utils/toolsets.js +33 -0
  118. package/README.md.bak.1779452127 +0 -240
  119. package/agents/codeserver.sh +0 -47
  120. package/agents/daisy_agent.js +0 -173
  121. package/agents/docs_agent.js +0 -148
  122. package/agents/memory_agent.js +0 -263
  123. package/agents/minimax.js +0 -173
  124. package/agents/npm_agent.js +0 -202
  125. package/agents/prompt_agent.js +0 -133
  126. package/agents/readme_agent.js +0 -148
  127. package/agents/spawn_agent.js +0 -160
  128. package/agents/stability.js +0 -173
  129. package/agents/todo_agent.js +0 -175
  130. package/bin/codeDave +0 -58
  131. package/docs/agent-dave-websocket-protocol.md +0 -180
  132. package/docs/agent-manager.md +0 -244
  133. package/docs/codeserver-pattern.md +0 -191
  134. package/docs/generic-toolset.md +0 -326
  135. package/docs/howtos/agent-networking.md +0 -253
  136. package/docs/howtos/spawn-agents.md.bak +0 -200
  137. package/docs/howtos/spawn-agents.md.bak_new +0 -200
  138. package/docs/multi-agent-clusters.md +0 -265
  139. package/docs/music-toolsets.md +0 -137
  140. package/docs/path-resolution-best-practices.md +0 -104
  141. package/docs/plans/minimax-music-generation.md +0 -80
  142. package/docs/plans/unified-agent-architecture.md +0 -146
  143. package/docs/plans/websocket-streaming-plan.md.bak +0 -317
  144. package/docs/prompt/spawn_agent.md +0 -175
  145. package/docs/prompt/spawn_agent.md.bak +0 -201
  146. package/docs/prompt/task_clarification_and_documentation.md +0 -35
  147. package/docs/prompt-class.md +0 -141
  148. package/docs/todo-archive-infra-2026-04-21.md +0 -15
  149. package/docs/todo-archive-v0.0.8.md +0 -1
  150. package/docs/todo-archive-v0.1.0.md +0 -32
  151. package/docs/todo-archive.md +0 -44
  152. package/docs/tools-syntax-validation.md +0 -121
  153. package/docs/toolset.md +0 -164
  154. package/docs/xai-responses.md +0 -111
  155. package/docs/xai_collections.md +0 -106
  156. package/lib/API/x.ai/ImageToolset.js +0 -165
  157. package/lib/API/x.ai/text.js +0 -415
  158. package/lib/AgentClient.js +0 -248
  159. package/lib/AgentManager.js +0 -245
  160. package/lib/AgentServer.js +0 -404
  161. package/lib/wsCli.js +0 -287
  162. package/lib/wsIO.js +0 -90
  163. package/types/API/x.ai/text.d.ts +0 -286
  164. package/types/AgentClient.d.ts +0 -109
  165. package/types/AgentManager.d.ts +0 -100
  166. package/types/AgentServer.d.ts +0 -89
  167. package/types/wsCli.d.ts +0 -17
  168. package/types/wsIO.d.ts +0 -30
  169. package/utils/test.sh +0 -46
  170. /package/docs/{suggestions.md → _notes/token-counts.md} +0 -0
  171. /package/lib/API/openai.com/{reponses/MESSAGES.md → MESSAGES.md} +0 -0
  172. /package/types/API/{x.ai/ImageToolset.d.ts → mureka/MusicToolset.d.ts} +0 -0
  173. /package/types/{CdnToolset.d.ts → cdnToolset.d.ts} +0 -0
@@ -0,0 +1,784 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * @file lib/API/openai.com/video.js
6
+ * @module openai.com/video
7
+ * @description Thin HTTP wrapper around OpenAI video endpoints.
8
+ *
9
+ * Video job calls submit a request and then poll until the video reaches a
10
+ * terminal state. Successful jobs can optionally download the generated asset
11
+ * to `.cache/openai/`.
12
+ *
13
+ * Documentation source files:
14
+ * - video.create.md
15
+ * - video.retrieve.md
16
+ * - video.download.md
17
+ * - video.edit.md
18
+ * - video.extend.md
19
+ * - video.remix.md
20
+ * - video.list.md
21
+ * - video.delete.md
22
+ * - video.create.character.md
23
+ * - video.fetch.character.md
24
+ */
25
+
26
+ const API_URL = 'https://api.openai.com/v1';
27
+ const TMP_DIR = path.join(process.cwd(), '.cache', 'openai');
28
+
29
+ /** Terminal video statuses returned by OpenAI. */
30
+ const TERMINAL_STATUSES = new Set(['completed', 'failed']);
31
+
32
+ /** Non-terminal video statuses returned by OpenAI. */
33
+ const PENDING_STATUSES = new Set(['queued', 'in_progress']);
34
+
35
+
36
+ /**
37
+ * JSON-compatible primitive value.
38
+ * @typedef {string|number|boolean|null} JsonPrimitive
39
+ */
40
+
41
+ /**
42
+ * JSON-compatible value accepted in OpenAI JSON request bodies.
43
+ * @typedef {JsonPrimitive|JsonObject|JsonArray} JsonValue
44
+ */
45
+
46
+ /**
47
+ * JSON-compatible object accepted in OpenAI JSON request bodies.
48
+ * @typedef {{[key: string]: JsonValue|undefined}} JsonObject
49
+ */
50
+
51
+ /**
52
+ * JSON-compatible array accepted in OpenAI JSON request bodies.
53
+ * @typedef {Array<JsonValue|undefined>} JsonArray
54
+ */
55
+
56
+ /**
57
+ * HTTP header map used for OpenAI requests.
58
+ * @typedef {Object} OpenAIHeaders
59
+ * @property {string} ['User-Agent'] - User-Agent header value.
60
+ * @property {string} ['Content-Type'] - Content type for JSON requests. Omitted for multipart and raw downloads.
61
+ * @property {string} Authorization - Bearer token authorization header.
62
+ */
63
+
64
+ /**
65
+ * Common wrapper request options.
66
+ * @typedef {Object} RequestOptions
67
+ * @property {string} [userAgent='@j-o-r/agents'] - User-Agent header value.
68
+ */
69
+
70
+ /**
71
+ * Query parameters serialized onto an OpenAI request URL.
72
+ * Undefined and null values are skipped.
73
+ * @typedef {{[key: string]: string|number|boolean|null|undefined}} QueryParams
74
+ */
75
+
76
+ /**
77
+ * Parsed response body returned by fetch helpers.
78
+ * @typedef {JsonValue|string|ArrayBuffer|Blob|undefined} ParsedResponseBody
79
+ */
80
+
81
+ /**
82
+ * Downloadable video content variant.
83
+ * @typedef {'video'|'thumbnail'|'spritesheet'} VideoAssetVariant
84
+ */
85
+
86
+ /**
87
+ * Supported OpenAI video model identifier.
88
+ * @typedef {'sora-2'|'sora-2-pro'|'sora-2-2025-10-06'|'sora-2-pro-2025-10-06'|'sora-2-2025-12-08'|string} VideoModel
89
+ */
90
+
91
+ /**
92
+ * Supported base clip duration for video creation.
93
+ * @typedef {'4'|'8'|'12'|4|8|12|number} VideoSeconds
94
+ */
95
+
96
+ /**
97
+ * Supported generated segment duration for video extensions.
98
+ * @typedef {'4'|'8'|'12'|'16'|'20'|4|8|12|16|20|number} VideoExtensionSeconds
99
+ */
100
+
101
+ /**
102
+ * Supported generated video output resolution.
103
+ * @typedef {'720x1280'|'1280x720'|'1024x1792'|'1792x1024'} VideoSize
104
+ */
105
+
106
+ /**
107
+ * Lifecycle status of an OpenAI video job.
108
+ * @typedef {'queued'|'in_progress'|'completed'|'failed'|string} VideoStatus
109
+ */
110
+
111
+ /**
112
+ * Error payload returned for a failed video generation job.
113
+ * @typedef {Object} VideoCreateError
114
+ * @property {string} code - Machine-readable error code.
115
+ * @property {string} message - Human-readable error description.
116
+ */
117
+
118
+ /**
119
+ * Optional image reference object used to guide video creation.
120
+ * Exactly one of `image_url` or `file_id` should be provided.
121
+ * @typedef {Object} VideoInputReference
122
+ * @property {string} [image_url] - Fully-qualified image URL or base64 data URL.
123
+ * @property {string} [file_id] - OpenAI file identifier for an uploaded image reference.
124
+ */
125
+
126
+ /**
127
+ * Reference to an existing generated video.
128
+ * @typedef {Object} VideoReference
129
+ * @property {string} id - Identifier of the completed source video.
130
+ */
131
+
132
+ /**
133
+ * Structured metadata for an OpenAI video generation job.
134
+ * @typedef {Object} VideoObject
135
+ * @property {string} id - Unique identifier for the video job.
136
+ * @property {number|null} [completed_at] - Unix timestamp in seconds for completion, when finished.
137
+ * @property {number} [created_at] - Unix timestamp in seconds when the job was created.
138
+ * @property {VideoCreateError|null} [error] - Error payload when generation failed.
139
+ * @property {number|null} [expires_at] - Unix timestamp in seconds when downloadable assets expire.
140
+ * @property {VideoModel} [model] - Video generation model that produced the job.
141
+ * @property {'video'} [object] - Object type marker for video job responses.
142
+ * @property {number} [progress] - Approximate completion percentage from 0 to 100.
143
+ * @property {string} [prompt] - Prompt used to generate, edit, extend, or remix the video.
144
+ * @property {string|null} [remixed_from_video_id] - Source video id when this video is a remix.
145
+ * @property {string} [seconds] - Duration of the generated clip, or stitched total duration for extensions.
146
+ * @property {VideoSize} [size] - Output resolution.
147
+ * @property {VideoStatus} status - Current lifecycle status of the video job.
148
+ * @property {VideoDownloadResult} [content] - Downloaded media data when a wrapper is called with download enabled.
149
+ */
150
+
151
+ /**
152
+ * Response returned after deleting a video.
153
+ * @typedef {Object} VideoDeleteResponse
154
+ * @property {string} id - Identifier of the deleted video.
155
+ * @property {boolean} deleted - Whether the resource was deleted.
156
+ * @property {'video.deleted'} object - Object type marker for delete responses.
157
+ */
158
+
159
+ /**
160
+ * Query parameters for listing generated videos.
161
+ * @typedef {Object} VideoListQuery
162
+ * @property {string} [after] - Pagination cursor from the previous response.
163
+ * @property {number} [limit] - Maximum number of videos to retrieve.
164
+ * @property {'asc'|'desc'} [order] - Sort order by timestamp.
165
+ */
166
+
167
+ /**
168
+ * Paginated list response for generated videos.
169
+ * @typedef {Object} VideoListResponse
170
+ * @property {VideoObject[]} data - Page of video jobs.
171
+ * @property {string} [first_id] - Identifier of the first item in the page.
172
+ * @property {boolean} [has_more] - Whether more items are available.
173
+ * @property {string} [last_id] - Identifier of the last item in the page.
174
+ * @property {'list'} object - Object type marker for list responses.
175
+ */
176
+
177
+ /**
178
+ * Response returned for character creation and retrieval.
179
+ * @typedef {Object} VideoCharacter
180
+ * @property {string} id - Identifier for the character cameo.
181
+ * @property {number} created_at - Unix timestamp in seconds when the character was created.
182
+ * @property {string} name - Display name for the character.
183
+ */
184
+
185
+ /**
186
+ * Result returned by authenticated media download helpers.
187
+ * @typedef {Object} VideoDownloadResult
188
+ * @property {Buffer} buffer - Downloaded media bytes.
189
+ * @property {string} contentType - Response content type.
190
+ * @property {number} bytes - Number of downloaded bytes.
191
+ * @property {string} [local_path] - Absolute local path when media was saved to disk.
192
+ */
193
+
194
+ /**
195
+ * Options for polling a video job until it reaches a terminal state.
196
+ * @typedef {RequestOptions & Object} PollOptions
197
+ * @property {number} [maxWaitMs=600000] - Maximum total polling time in milliseconds.
198
+ * @property {number} [pollIntervalMs=5000] - Delay between poll requests in milliseconds.
199
+ */
200
+
201
+ /**
202
+ * Options for submitting a create-video request.
203
+ * @typedef {RequestOptions & PollOptions & Object} SubmitVideoOptions
204
+ * @property {VideoModel} [model='sora-2'] - Video generation model.
205
+ * @property {VideoSeconds} [seconds='4'] - Clip duration in seconds.
206
+ * @property {VideoSize} [size='720x1280'] - Output resolution.
207
+ * @property {string|VideoInputReference} [input_reference] - Optional image reference URL/data URL/file reference.
208
+ * @property {JsonObject} [extra] - Additional request fields merged into the JSON body.
209
+ */
210
+
211
+ /**
212
+ * Options for creating a video and optionally downloading the completed result.
213
+ * @typedef {SubmitVideoOptions & Object} CreateVideoOptions
214
+ * @property {boolean} [download=false] - Download final video after completion.
215
+ * @property {VideoAssetVariant} [variant='video'] - Asset variant to download when download is enabled.
216
+ * @property {string} [filenamePrefix='openai-video'] - Local filename prefix used when saving downloaded media.
217
+ */
218
+
219
+ /**
220
+ * Options for emergency polling after a previous create/edit/extend/remix timeout.
221
+ * @typedef {PollOptions & Object} EmergencyPollVideoOptions
222
+ * @property {number} [maxWaitMs=3600000] - Maximum total polling time in milliseconds.
223
+ * @property {number} [pollIntervalMs=30000] - Delay between poll requests in milliseconds.
224
+ * @property {boolean} [download=false] - Download final media when completed.
225
+ * @property {VideoAssetVariant} [variant='video'] - Asset variant to download.
226
+ * @property {string} [filenamePrefix='openai-video'] - Local filename prefix used when saving downloaded media.
227
+ */
228
+
229
+ /**
230
+ * Options for downloading generated video media or preview assets.
231
+ * @typedef {RequestOptions & Object} DownloadVideoOptions
232
+ * @property {VideoAssetVariant} [variant='video'] - Asset variant to download.
233
+ * @property {boolean} [save=false] - Save bytes to `.cache/openai/`.
234
+ * @property {string} [filenamePrefix='openai-video'] - Local filename prefix.
235
+ */
236
+
237
+ /**
238
+ * Options for edit, extension, and remix requests.
239
+ * @typedef {RequestOptions & PollOptions & Object} VideoMutationOptions
240
+ * @property {JsonObject} [extra] - Additional request fields merged into the JSON body.
241
+ */
242
+
243
+ /**
244
+ * Local or in-memory video input accepted by createCharacter().
245
+ * @typedef {string|Buffer|Blob|ArrayBuffer} CharacterVideoInput
246
+ */
247
+
248
+ /**
249
+ * Blob conversion result used for multipart character upload.
250
+ * @typedef {Object} VideoBlobResult
251
+ * @property {Blob} blob - Blob ready to append to FormData.
252
+ * @property {string} filename - Filename sent with the multipart upload.
253
+ */
254
+
255
+ /**
256
+ * Get the default JSON headers.
257
+ * @param {string} [USER_AGENT='@j-o-r/agents'] - User-Agent header value.
258
+ * @returns {OpenAIHeaders} Headers object.
259
+ * @throws {Error} If OPENAIKEY is missing.
260
+ */
261
+ const getHeaders = (USER_AGENT = '@j-o-r/agents') => {
262
+ if (!process.env['OPENAIKEY']) {
263
+ throw new Error('Missing OPENAIKEY!export OPENAIKEY=<OPENAI_API_KEY>');
264
+ }
265
+ const KEY = process.env['OPENAIKEY'];
266
+ return {
267
+ 'User-Agent': USER_AGENT,
268
+ 'Content-Type': 'application/json',
269
+ 'Authorization': `Bearer ${KEY}`
270
+ };
271
+ };
272
+
273
+ /**
274
+ * Get auth headers for multipart/form-data requests.
275
+ * Do not set Content-Type; fetch/FormData must set the boundary.
276
+ * @param {string} [USER_AGENT='@j-o-r/agents'] - User-Agent header value.
277
+ * @returns {OpenAIHeaders} Headers object.
278
+ * @throws {Error} If OPENAIKEY is missing.
279
+ */
280
+ const getMultipartHeaders = (USER_AGENT = '@j-o-r/agents') => {
281
+ const headers = getHeaders(USER_AGENT);
282
+ delete headers['Content-Type'];
283
+ return headers;
284
+ };
285
+
286
+ /**
287
+ * Wait for a number of milliseconds.
288
+ * @param {number} ms - Milliseconds to sleep.
289
+ * @returns {Promise<void>}
290
+ */
291
+ function sleep(ms) {
292
+ return new Promise(resolve => setTimeout(resolve, ms));
293
+ }
294
+
295
+ /**
296
+ * Ensure the local cache directory exists.
297
+ * @returns {Promise<void>}
298
+ */
299
+ async function ensureTmpDir() {
300
+ await fs.mkdir(TMP_DIR, { recursive: true });
301
+ }
302
+
303
+ /**
304
+ * Convert a plain object to a URL query string.
305
+ * Undefined/null values are skipped.
306
+ * @param {QueryParams} [query={}] - Query parameters.
307
+ * @returns {string} Query string, including leading `?` when non-empty.
308
+ */
309
+ function toQueryString(query = {}) {
310
+ const params = new URLSearchParams();
311
+ for (const [key, value] of Object.entries(query)) {
312
+ if (value === undefined || value === null) continue;
313
+ params.set(key, String(value));
314
+ }
315
+ const text = params.toString();
316
+ return text ? `?${text}` : '';
317
+ }
318
+
319
+ /**
320
+ * Parse a fetch Response as JSON when possible, otherwise text.
321
+ * @param {Response} response - Fetch response.
322
+ * @returns {Promise<ParsedResponseBody>} Parsed body.
323
+ */
324
+ async function parseResponseBody(response) {
325
+ const contentType = response.headers.get('content-type') || '';
326
+ if (contentType.includes('application/json')) return response.json();
327
+ const text = await response.text();
328
+ try {
329
+ return JSON.parse(text);
330
+ } catch {
331
+ return text;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Make a JSON API request to OpenAI.
337
+ * @param {string} endpoint - Endpoint path, e.g. `/videos`.
338
+ * @param {'GET'|'POST'|'DELETE'} [method='GET'] - HTTP method.
339
+ * @param {JsonObject|null} [body=null] - JSON body.
340
+ * @param {QueryParams} [query={}] - Query parameters.
341
+ * @param {RequestOptions} [options={}] - Request options.
342
+ * @param {string} [options.userAgent='@j-o-r/agents'] - User-Agent header value.
343
+ * @returns {Promise<ParsedResponseBody>} Parsed API response.
344
+ * @throws {Error} On non-2xx responses.
345
+ */
346
+ async function apiRequest(endpoint, method = 'GET', body = null, query = {}, options = {}) {
347
+ const url = `${API_URL}${endpoint}${toQueryString(query)}`;
348
+ const response = await fetch(url, {
349
+ method,
350
+ headers: getHeaders(options.userAgent),
351
+ body: body === null || body === undefined ? undefined : JSON.stringify(body)
352
+ });
353
+ const parsed = await parseResponseBody(response);
354
+
355
+ if (!response.ok) {
356
+ throw new Error(`OpenAI video API error ${response.status}: ${JSON.stringify(parsed)}`);
357
+ }
358
+
359
+ return parsed;
360
+ }
361
+
362
+ /**
363
+ * Download bytes from an authenticated OpenAI endpoint.
364
+ * @param {string} endpoint - Endpoint path.
365
+ * @param {QueryParams} [query={}] - Query parameters.
366
+ * @param {RequestOptions} [options={}] - Request options.
367
+ * @param {string} [options.userAgent='@j-o-r/agents'] - User-Agent header value.
368
+ * @returns {Promise<VideoDownloadResult>}
369
+ * @throws {Error} On non-2xx responses.
370
+ */
371
+ async function apiDownload(endpoint, query = {}, options = {}) {
372
+ const url = `${API_URL}${endpoint}${toQueryString(query)}`;
373
+ const headers = getHeaders(options.userAgent);
374
+ delete headers['Content-Type'];
375
+
376
+ const response = await fetch(url, { method: 'GET', headers });
377
+ if (!response.ok) {
378
+ const parsed = await parseResponseBody(response);
379
+ throw new Error(`OpenAI video download error ${response.status}: ${JSON.stringify(parsed)}`);
380
+ }
381
+
382
+ const buffer = Buffer.from(await response.arrayBuffer());
383
+ return {
384
+ buffer,
385
+ contentType: response.headers.get('content-type') || 'application/octet-stream',
386
+ bytes: buffer.length
387
+ };
388
+ }
389
+
390
+ /**
391
+ * Return a useful file extension for a content variant / content type.
392
+ * @param {VideoAssetVariant} variant - Asset variant.
393
+ * @param {string} contentType - Response content type.
394
+ * @returns {string} File extension without a dot.
395
+ */
396
+ function getVariantExtension(variant = 'video', contentType = '') {
397
+ if (variant === 'thumbnail') return contentType.includes('png') ? 'png' : 'jpg';
398
+ if (variant === 'spritesheet') return contentType.includes('png') ? 'png' : 'jpg';
399
+ return 'mp4';
400
+ }
401
+
402
+ /**
403
+ * Save downloaded media bytes to `.cache/openai/`.
404
+ * @param {Buffer} buffer - Media bytes.
405
+ * @param {string} [filenamePrefix='openai-video'] - Filename prefix.
406
+ * @param {string} [ext='mp4'] - File extension.
407
+ * @returns {Promise<string>} Absolute local path.
408
+ */
409
+ async function saveMediaToLocal(buffer, filenamePrefix = 'openai-video', ext = 'mp4') {
410
+ await ensureTmpDir();
411
+ const filename = `${filenamePrefix}-${Date.now()}.${ext}`;
412
+ const localPath = path.join(TMP_DIR, filename);
413
+ await fs.writeFile(localPath, buffer);
414
+ return localPath;
415
+ }
416
+
417
+ /**
418
+ * Normalize an image reference for video creation.
419
+ * @param {string|VideoInputReference} inputReference - URL/data URL/file reference.
420
+ * @returns {VideoInputReference|undefined} OpenAI input_reference object.
421
+ */
422
+ function normalizeInputReference(inputReference) {
423
+ if (!inputReference) return undefined;
424
+ if (typeof inputReference === 'string') return { image_url: inputReference };
425
+ return inputReference;
426
+ }
427
+
428
+ /**
429
+ * Validate a completed/failed video response and throw on failed jobs.
430
+ * @param {VideoObject} video - Video object.
431
+ * @returns {VideoObject} Video object.
432
+ * @throws {Error} If the video job failed.
433
+ */
434
+ function assertTerminalVideo(video) {
435
+ if (video?.status === 'failed') {
436
+ throw new Error(`OpenAI video generation failed: ${JSON.stringify(video.error || video)}`);
437
+ }
438
+ return video;
439
+ }
440
+
441
+ /**
442
+ * Poll a video until it reaches a terminal state.
443
+ *
444
+ * This is the normal polling helper used by create/edit/extend/remix wrappers.
445
+ * Use `emergencyPollVideo()` for edge cases after a caller-side timeout.
446
+ *
447
+ * @param {string} videoId - OpenAI video id.
448
+ * @param {PollOptions} [options={}] - Polling options.
449
+ * @param {number} [options.maxWaitMs=600000] - Maximum wait time in ms.
450
+ * @param {number} [options.pollIntervalMs=5000] - Delay between polls in ms.
451
+ * @param {string} [options.userAgent='@j-o-r/agents'] - User-Agent header value.
452
+ * @returns {Promise<VideoObject>} Completed video object.
453
+ * @throws {Error} On failed job or timeout.
454
+ */
455
+ async function pollVideo(videoId, options = {}) {
456
+ if (!videoId) throw new Error('pollVideo() requires a videoId');
457
+
458
+ const maxWaitMs = options.maxWaitMs ?? 600000;
459
+ const pollIntervalMs = options.pollIntervalMs ?? 5000;
460
+ const start = Date.now();
461
+ let lastVideo = null;
462
+
463
+ while (Date.now() - start <= maxWaitMs) {
464
+ lastVideo = await retrieveVideo(videoId, options);
465
+ const status = lastVideo?.status;
466
+
467
+ if (TERMINAL_STATUSES.has(status)) return assertTerminalVideo(lastVideo);
468
+ if (status && !PENDING_STATUSES.has(status)) return lastVideo;
469
+
470
+ await sleep(pollIntervalMs);
471
+ }
472
+
473
+ const timeout = new Error(
474
+ `OpenAI video polling timed out after ${maxWaitMs}ms for ${videoId}. ` +
475
+ `Use emergencyPollVideo('${videoId}') to continue polling. Last response: ${JSON.stringify(lastVideo)}`
476
+ );
477
+ timeout.videoId = videoId;
478
+ timeout.lastResponse = lastVideo;
479
+ throw timeout;
480
+ }
481
+
482
+ /**
483
+ * Emergency polling call for jobs that outlive a previous request timeout.
484
+ *
485
+ * Call this with the video id from a timed-out create/edit/extend/remix response
486
+ * or from the thrown timeout error's `videoId` property. Defaults are longer and
487
+ * less aggressive than normal polling.
488
+ *
489
+ * @param {string} videoId - OpenAI video id.
490
+ * @param {PollOptions} [options={}] - Polling options.
491
+ * @param {number} [options.maxWaitMs=3600000] - Maximum wait time in ms.
492
+ * @param {number} [options.pollIntervalMs=30000] - Delay between polls in ms.
493
+ * @param {boolean} [options.download=false] - Download final media when completed.
494
+ * @param {VideoAssetVariant} [options.variant='video'] - Asset variant to download.
495
+ * @param {string} [options.userAgent='@j-o-r/agents'] - User-Agent header value.
496
+ * @returns {Promise<VideoObject>} Completed video response, optionally with download data.
497
+ */
498
+ async function emergencyPollVideo(videoId, options = {}) {
499
+ const video = await pollVideo(videoId, {
500
+ ...options,
501
+ maxWaitMs: options.maxWaitMs ?? 3600000,
502
+ pollIntervalMs: options.pollIntervalMs ?? 30000
503
+ });
504
+
505
+ if (!options.download) return video;
506
+
507
+ const content = await downloadVideoContent(videoId, {
508
+ ...options,
509
+ variant: options.variant ?? 'video',
510
+ save: true
511
+ });
512
+
513
+ return { ...video, content };
514
+ }
515
+
516
+ /**
517
+ * Submit a video create job without waiting.
518
+ * @param {string} prompt - Prompt describing the video.
519
+ * @param {SubmitVideoOptions} [options={}] - Create options.
520
+ * @param {VideoModel} [options.model='sora-2'] - Video model.
521
+ * @param {VideoSeconds} [options.seconds='4'] - Duration in seconds.
522
+ * @param {VideoSize} [options.size='720x1280'] - Output size.
523
+ * @param {string|VideoInputReference} [options.input_reference] - Optional image reference.
524
+ * @param {JsonObject} [options.extra] - Extra request fields.
525
+ * @returns {Promise<VideoObject>} Created video job response.
526
+ */
527
+ async function submitVideo(prompt, options = {}) {
528
+ if (!prompt || typeof prompt !== 'string') throw new Error('submitVideo() requires a prompt string');
529
+
530
+ const body = {
531
+ model: options.model ?? 'sora-2',
532
+ prompt,
533
+ seconds: String(options.seconds ?? '4'),
534
+ size: options.size ?? '720x1280',
535
+ ...options.extra
536
+ };
537
+
538
+ const inputReference = normalizeInputReference(options.input_reference);
539
+ if (inputReference) body.input_reference = inputReference;
540
+
541
+ return apiRequest('/videos', 'POST', body, {}, options);
542
+ }
543
+
544
+ /**
545
+ * Create a video and wait until the job completes.
546
+ * @param {string} prompt - Prompt describing the video.
547
+ * @param {CreateVideoOptions} [options={}] - Create and polling options.
548
+ * @param {boolean} [options.download=false] - Download final video after completion.
549
+ * @returns {Promise<VideoObject>} Completed video response, optionally with `content`.
550
+ */
551
+ async function createVideo(prompt, options = {}) {
552
+ const created = await submitVideo(prompt, options);
553
+ const video = await pollVideo(created.id, options);
554
+
555
+ if (!options.download) return video;
556
+
557
+ const content = await downloadVideoContent(video.id, {
558
+ ...options,
559
+ variant: options.variant ?? 'video',
560
+ save: true
561
+ });
562
+ return { ...video, content };
563
+ }
564
+
565
+ /**
566
+ * Retrieve the latest metadata for a generated video.
567
+ * @param {string} videoId - OpenAI video id.
568
+ * @param {RequestOptions} [options={}] - Request options.
569
+ * @returns {Promise<VideoObject>} Video object.
570
+ */
571
+ async function retrieveVideo(videoId, options = {}) {
572
+ if (!videoId) throw new Error('retrieveVideo() requires a videoId');
573
+ return apiRequest(`/videos/${encodeURIComponent(videoId)}`, 'GET', null, {}, options);
574
+ }
575
+
576
+ /**
577
+ * List recently generated videos for the current project.
578
+ * @param {VideoListQuery} [query={}] - List query parameters.
579
+ * @param {string} [query.after] - Pagination cursor.
580
+ * @param {number} [query.limit] - Number of items to retrieve.
581
+ * @param {'asc'|'desc'} [query.order] - Sort order.
582
+ * @param {RequestOptions} [options={}] - Request options.
583
+ * @returns {Promise<VideoListResponse>} List response.
584
+ */
585
+ async function listVideos(query = {}, options = {}) {
586
+ return apiRequest('/videos', 'GET', null, query, options);
587
+ }
588
+
589
+ /**
590
+ * Delete a completed or failed video and its stored assets.
591
+ * @param {string} videoId - OpenAI video id.
592
+ * @param {RequestOptions} [options={}] - Request options.
593
+ * @returns {Promise<VideoDeleteResponse>} Delete response.
594
+ */
595
+ async function deleteVideo(videoId, options = {}) {
596
+ if (!videoId) throw new Error('deleteVideo() requires a videoId');
597
+ return apiRequest(`/videos/${encodeURIComponent(videoId)}`, 'DELETE', null, {}, options);
598
+ }
599
+
600
+ /**
601
+ * Download generated video bytes or a preview asset.
602
+ * @param {string} videoId - OpenAI video id.
603
+ * @param {DownloadVideoOptions} [options={}] - Download options.
604
+ * @param {VideoAssetVariant} [options.variant='video'] - Asset variant.
605
+ * @param {boolean} [options.save=false] - Save bytes to `.cache/openai/`.
606
+ * @param {string} [options.filenamePrefix='openai-video'] - Local filename prefix.
607
+ * @returns {Promise<VideoDownloadResult>}
608
+ */
609
+ async function downloadVideoContent(videoId, options = {}) {
610
+ if (!videoId) throw new Error('downloadVideoContent() requires a videoId');
611
+
612
+ const variant = options.variant ?? 'video';
613
+ const result = await apiDownload(`/videos/${encodeURIComponent(videoId)}/content`, { variant }, options);
614
+
615
+ if (options.save) {
616
+ const ext = getVariantExtension(variant, result.contentType);
617
+ result.local_path = await saveMediaToLocal(result.buffer, options.filenamePrefix ?? 'openai-video', ext);
618
+ }
619
+
620
+ return result;
621
+ }
622
+
623
+ /**
624
+ * Submit a video edit job without waiting.
625
+ * @param {string} videoId - Completed source video id.
626
+ * @param {string} prompt - Edit prompt.
627
+ * @param {VideoMutationOptions} [options={}] - Request options.
628
+ * @returns {Promise<VideoObject>} Created video job response.
629
+ */
630
+ async function submitVideoEdit(videoId, prompt, options = {}) {
631
+ if (!videoId) throw new Error('submitVideoEdit() requires a videoId');
632
+ if (!prompt || typeof prompt !== 'string') throw new Error('submitVideoEdit() requires a prompt string');
633
+
634
+ return apiRequest('/videos/edits', 'POST', {
635
+ prompt,
636
+ video: { id: videoId },
637
+ ...options.extra
638
+ }, {}, options);
639
+ }
640
+
641
+ /**
642
+ * Edit a video and wait until the job completes.
643
+ * @param {string} videoId - Completed source video id.
644
+ * @param {string} prompt - Edit prompt.
645
+ * @param {VideoMutationOptions} [options={}] - Request and polling options.
646
+ * @returns {Promise<VideoObject>} Completed video response.
647
+ */
648
+ async function editVideo(videoId, prompt, options = {}) {
649
+ const created = await submitVideoEdit(videoId, prompt, options);
650
+ return pollVideo(created.id, options);
651
+ }
652
+
653
+ /**
654
+ * Submit a video extension job without waiting.
655
+ * @param {string} videoId - Completed source video id.
656
+ * @param {string} prompt - Extension prompt.
657
+ * @param {VideoExtensionSeconds} [seconds='4'] - New segment length.
658
+ * @param {VideoMutationOptions} [options={}] - Request options.
659
+ * @returns {Promise<VideoObject>} Created video job response.
660
+ */
661
+ async function submitVideoExtension(videoId, prompt, seconds = '4', options = {}) {
662
+ if (!videoId) throw new Error('submitVideoExtension() requires a videoId');
663
+ if (!prompt || typeof prompt !== 'string') throw new Error('submitVideoExtension() requires a prompt string');
664
+
665
+ return apiRequest('/videos/extensions', 'POST', {
666
+ prompt,
667
+ seconds: String(seconds),
668
+ video: { id: videoId },
669
+ ...options.extra
670
+ }, {}, options);
671
+ }
672
+
673
+ /**
674
+ * Extend a video and wait until the job completes.
675
+ * @param {string} videoId - Completed source video id.
676
+ * @param {string} prompt - Extension prompt.
677
+ * @param {VideoExtensionSeconds} [seconds='4'] - New segment length.
678
+ * @param {VideoMutationOptions} [options={}] - Request and polling options.
679
+ * @returns {Promise<VideoObject>} Completed video response.
680
+ */
681
+ async function extendVideo(videoId, prompt, seconds = '4', options = {}) {
682
+ const created = await submitVideoExtension(videoId, prompt, seconds, options);
683
+ return pollVideo(created.id, options);
684
+ }
685
+
686
+ /**
687
+ * Submit a video remix job without waiting.
688
+ * @param {string} videoId - Completed source video id.
689
+ * @param {string} prompt - Remix prompt.
690
+ * @param {VideoMutationOptions} [options={}] - Request options.
691
+ * @returns {Promise<VideoObject>} Created video job response.
692
+ */
693
+ async function submitVideoRemix(videoId, prompt, options = {}) {
694
+ if (!videoId) throw new Error('submitVideoRemix() requires a videoId');
695
+ if (!prompt || typeof prompt !== 'string') throw new Error('submitVideoRemix() requires a prompt string');
696
+
697
+ return apiRequest(`/videos/${encodeURIComponent(videoId)}/remix`, 'POST', {
698
+ prompt,
699
+ ...options.extra
700
+ }, {}, options);
701
+ }
702
+
703
+ /**
704
+ * Remix a video and wait until the job completes.
705
+ * @param {string} videoId - Completed source video id.
706
+ * @param {string} prompt - Remix prompt.
707
+ * @param {VideoMutationOptions} [options={}] - Request and polling options.
708
+ * @returns {Promise<VideoObject>} Completed video response.
709
+ */
710
+ async function remixVideo(videoId, prompt, options = {}) {
711
+ const created = await submitVideoRemix(videoId, prompt, options);
712
+ return pollVideo(created.id, options);
713
+ }
714
+
715
+ /**
716
+ * Create a Blob from a local file path, Buffer, Blob, or ArrayBuffer.
717
+ * @param {CharacterVideoInput} video - Video input.
718
+ * @returns {Promise<VideoBlobResult>} Blob and filename.
719
+ */
720
+ async function toVideoBlob(video) {
721
+ if (typeof video === 'string') {
722
+ const buffer = await fs.readFile(video);
723
+ return { blob: new Blob([buffer], { type: 'video/mp4' }), filename: path.basename(video) };
724
+ }
725
+ if (Buffer.isBuffer(video)) return { blob: new Blob([video], { type: 'video/mp4' }), filename: 'video.mp4' };
726
+ if (video instanceof ArrayBuffer) return { blob: new Blob([video], { type: 'video/mp4' }), filename: 'video.mp4' };
727
+ if (video instanceof Blob) return { blob: video, filename: 'video.mp4' };
728
+ throw new Error('Unsupported video input for createCharacter()');
729
+ }
730
+
731
+ /**
732
+ * Create a character from an uploaded video.
733
+ * @param {string} name - Display name for the character.
734
+ * @param {CharacterVideoInput} video - Local file path or video bytes.
735
+ * @param {RequestOptions} [options={}] - Request options.
736
+ * @returns {Promise<VideoCharacter>} Character response.
737
+ */
738
+ async function createCharacter(name, video, options = {}) {
739
+ if (!name || typeof name !== 'string') throw new Error('createCharacter() requires a name string');
740
+ if (!video) throw new Error('createCharacter() requires a video input');
741
+
742
+ const { blob, filename } = await toVideoBlob(video);
743
+ const form = new FormData();
744
+ form.append('name', name);
745
+ form.append('video', blob, filename);
746
+
747
+ const response = await fetch(`${API_URL}/videos/characters`, {
748
+ method: 'POST',
749
+ headers: getMultipartHeaders(options.userAgent),
750
+ body: form
751
+ });
752
+ const parsed = await parseResponseBody(response);
753
+
754
+ if (!response.ok) {
755
+ throw new Error(`OpenAI character API error ${response.status}: ${JSON.stringify(parsed)}`);
756
+ }
757
+
758
+ return parsed;
759
+ }
760
+
761
+ /**
762
+ * Fetch a character by id.
763
+ * @param {string} characterId - Character id.
764
+ * @param {RequestOptions} [options={}] - Request options.
765
+ * @returns {Promise<VideoCharacter>} Character response.
766
+ */
767
+ async function fetchCharacter(characterId, options = {}) {
768
+ if (!characterId) throw new Error('fetchCharacter() requires a characterId');
769
+ return apiRequest(`/videos/characters/${encodeURIComponent(characterId)}`, 'GET', null, {}, options);
770
+ }
771
+
772
+ export {
773
+ createVideo,
774
+ retrieveVideo,
775
+ listVideos,
776
+ deleteVideo,
777
+ downloadVideoContent,
778
+ emergencyPollVideo,
779
+ editVideo,
780
+ extendVideo,
781
+ remixVideo,
782
+ createCharacter,
783
+ fetchCharacter
784
+ };