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

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 +593 -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 +10 -4
  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 +250 -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,445 @@
1
+ /**
2
+ * @file lib/API/lalal.ai/lalal.js
3
+ * @module lalal.ai/lalal
4
+ * @description Pure HTTP wrapper for the LALAL.AI API v1 (https://www.lalal.ai/api/v1/).
5
+ *
6
+ * Provides a clean, production-ready interface for:
7
+ * - File upload (/upload/)
8
+ * - Task management: split (stem_separator, demuser, voice_clean, multistem), voice change
9
+ * - Status checking and polling (/check/)
10
+ * - Cancellation, deletion, limits
11
+ * - Batch operations
12
+ * - Voice packs listing
13
+ *
14
+ * Authentication via `X-License-Key` header (env: LALAL_AI_LICENSE_KEY).
15
+ * All processing is asynchronous — use check/poll helpers.
16
+ *
17
+ * High-level methods handle upload → start task → poll → result processing.
18
+ * Supports local files, Buffers, Blobs, remote URLs (auto-download for upload).
19
+ *
20
+ * @see OpenAPI: lib/API/lalal.ai/openapi.json
21
+ * @see Official examples: https://github.com/OmniSaleGmbH/lalalai/tree/master/api-v1/python
22
+ */
23
+
24
+ /* ============================================================
25
+ IMPORTS & CONSTANTS
26
+ ============================================================ */
27
+
28
+ import { request as doRequest } from '@j-o-r/apiserver';
29
+ import fs from 'fs/promises';
30
+ import path from 'path';
31
+
32
+ const BASE_URL = 'https://www.lalal.ai/api/v1';
33
+ const TMP_DIR = path.join(process.cwd(), '.cache', 'lalalai');
34
+
35
+ /* ============================================================
36
+ AUTHENTICATION & HEADERS
37
+ ============================================================ */
38
+
39
+ /**
40
+ * Builds authenticated request headers for LALAL.AI API.
41
+ * Requires the `LALAL_AI_LICENSE_KEY` environment variable.
42
+ *
43
+ * @function getHeaders
44
+ * @param {Object} [extraHeaders={}] - Additional headers (e.g. Content-Disposition)
45
+ * @returns {Object} Headers with X-License-Key
46
+ * @throws {Error} If license key missing
47
+ */
48
+ const getHeaders = (extraHeaders = {}) => {
49
+ if (!process.env.LALAL_AI_LICENSE_KEY) {
50
+ throw new Error('Missing LALAL_AI_LICENSE_KEY! Please export LALAL_AI_LICENSE_KEY=your_license_key');
51
+ }
52
+ return {
53
+ 'X-License-Key': process.env.LALAL_AI_LICENSE_KEY,
54
+ ...extraHeaders
55
+ };
56
+ };
57
+
58
+ /* ============================================================
59
+ TEMPORARY FILE MANAGEMENT
60
+ ============================================================ */
61
+
62
+ async function ensureTmpDir() {
63
+ await fs.mkdir(TMP_DIR, { recursive: true });
64
+ }
65
+
66
+ /**
67
+ * Downloads a remote file (audio) to temp dir.
68
+ *
69
+ * @async
70
+ * @function downloadRemoteToTemp
71
+ * @param {string} url - Remote URL
72
+ * @param {string} [prefix='lalal-download'] - Filename prefix
73
+ * @returns {Promise<string>} Local file path
74
+ */
75
+ async function downloadRemoteToTemp(url, prefix = 'lalal-download') {
76
+ await ensureTmpDir();
77
+ const response = await fetch(url);
78
+ if (!response.ok) {
79
+ throw new Error(`Failed to download: ${response.status} ${response.statusText}`);
80
+ }
81
+ const buffer = Buffer.from(await response.arrayBuffer());
82
+ let filename = `${prefix}-${Date.now()}.mp3`;
83
+ try {
84
+ const urlObj = new URL(url);
85
+ const base = path.basename(urlObj.pathname);
86
+ if (base) filename = base;
87
+ } catch (_) {}
88
+ const localPath = path.join(TMP_DIR, filename);
89
+ await fs.writeFile(localPath, buffer);
90
+ return localPath;
91
+ }
92
+
93
+ /**
94
+ * Prepares upload payload: returns { body: Buffer, headers: { 'Content-Disposition': ... } }
95
+ *
96
+ * @async
97
+ * @function prepareUploadPayload
98
+ * @param {string|Buffer|Blob} input - File path, URL, Buffer or Blob
99
+ * @param {string} [filename] - Optional filename (required for Buffers/Blobs)
100
+ * @returns {Promise<{body: Buffer, contentDisposition: string}>}
101
+ */
102
+ async function prepareUploadPayload(input, filename) {
103
+ let buffer;
104
+ let finalFilename = filename;
105
+
106
+ if (typeof input === 'string') {
107
+ if (input.startsWith('http://') || input.startsWith('https://')) {
108
+ const localPath = await downloadRemoteToTemp(input, 'upload-source');
109
+ buffer = await fs.readFile(localPath);
110
+ finalFilename = finalFilename || path.basename(localPath);
111
+ } else {
112
+ buffer = await fs.readFile(input);
113
+ finalFilename = finalFilename || path.basename(input);
114
+ }
115
+ } else if (input instanceof Buffer) {
116
+ buffer = input;
117
+ if (!finalFilename) throw new Error('filename required for Buffer input');
118
+ } else if (input instanceof Blob) {
119
+ buffer = Buffer.from(await input.arrayBuffer());
120
+ finalFilename = finalFilename || input.name || `upload-${Date.now()}.mp3`;
121
+ } else {
122
+ throw new Error('Unsupported input type for upload');
123
+ }
124
+
125
+ // Build Content-Disposition (simple version; production should handle RFC 6266/5987)
126
+ const disposition = `attachment; filename="${finalFilename.replace(/"/g, '')}"`;
127
+ return { body: buffer, contentDisposition: disposition };
128
+ }
129
+
130
+ /* ============================================================
131
+ CORE REQUEST HELPERS
132
+ ============================================================ */
133
+
134
+ /**
135
+ * Generic POST helper with JSON body.
136
+ *
137
+ * @async
138
+ * @function postJson
139
+ * @param {string} endpoint - Relative path e.g. 'split/stem_separator/'
140
+ * @param {Object} body - JSON payload
141
+ * @param {Object} [extraHeaders={}] - Extra headers
142
+ * @returns {Promise<Object>} Response object from doRequest
143
+ */
144
+ async function postJson(endpoint, body, extraHeaders = {}) {
145
+ const url = `${BASE_URL}/${endpoint}`;
146
+ const headers = getHeaders({ 'Content-Type': 'application/json', ...extraHeaders });
147
+ return doRequest(url, 'POST', headers, JSON.stringify(body));
148
+ }
149
+
150
+ /**
151
+ * Upload helper (binary octet-stream with Content-Disposition).
152
+ *
153
+ * @async
154
+ * @function uploadFile
155
+ * @param {string|Buffer|Blob} input
156
+ * @param {string} [filename]
157
+ * @returns {Promise<Object>} { id, name, size, duration, expires }
158
+ */
159
+ async function uploadFile(input, filename) {
160
+ const { body, contentDisposition } = await prepareUploadPayload(input, filename);
161
+ const url = `${BASE_URL}/upload/`;
162
+ const headers = getHeaders({ 'Content-Disposition': contentDisposition, 'Content-Type': 'application/octet-stream' });
163
+
164
+ const res = await doRequest(url, 'POST', headers, body);
165
+
166
+ if (res.status === 200) {
167
+ return res.response;
168
+ }
169
+ throw new Error(`Upload failed: ${res.status} ${JSON.stringify(res.response)}`);
170
+ }
171
+
172
+ /* ============================================================
173
+ POLLING & CHECK HELPERS
174
+ ============================================================ */
175
+
176
+ /**
177
+ * Checks task status.
178
+ *
179
+ * @async
180
+ * @function checkTasks
181
+ * @param {string|string[]} taskIds - Single ID or array
182
+ * @returns {Promise<Object>} CheckV1Response
183
+ */
184
+ async function checkTasks(taskIds) {
185
+ const ids = Array.isArray(taskIds) ? taskIds : [taskIds];
186
+ const res = await postJson('check/', { task_ids: ids });
187
+ if (res.status === 200) {
188
+ return res.response;
189
+ }
190
+ throw new Error(`Check failed: ${res.status} ${JSON.stringify(res.response)}`);
191
+ }
192
+
193
+ /**
194
+ * Polls /check/ until task completes, errors, or times out.
195
+ *
196
+ * @async
197
+ * @function pollForTask
198
+ * @param {string} taskId
199
+ * @param {number} [maxAttempts=120] - ~10 minutes at 5s interval
200
+ * @param {number} [intervalMs=5000]
201
+ * @returns {Promise<Object>} The success result or throws on error/cancel/timeout
202
+ */
203
+ async function pollForTask(taskId, maxAttempts = 120, intervalMs = 5000) {
204
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
205
+ const checkRes = await checkTasks(taskId);
206
+ const taskResult = checkRes.result?.[taskId];
207
+
208
+ if (!taskResult) {
209
+ throw new Error(`Task ${taskId} not found in check response`);
210
+ }
211
+
212
+ const status = taskResult.status;
213
+
214
+ if (status === 'success') {
215
+ return taskResult;
216
+ }
217
+ if (status === 'error' || status === 'server_error') {
218
+ throw new Error(`Task ${taskId} failed: ${JSON.stringify(taskResult.error || taskResult)}`);
219
+ }
220
+ if (status === 'cancelled') {
221
+ throw new Error(`Task ${taskId} was cancelled`);
222
+ }
223
+
224
+ // progress or queued
225
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
226
+ }
227
+ throw new Error(`Timeout polling task ${taskId} after ${maxAttempts} attempts`);
228
+ }
229
+
230
+ /* ============================================================
231
+ SPLIT HELPERS (high-level)
232
+ ============================================================ */
233
+
234
+ /**
235
+ * Starts a stem separator split task.
236
+ *
237
+ * @async
238
+ * @function splitStemSeparator
239
+ * @param {string} sourceId
240
+ * @param {Object} presets - StemSeparatorSplitterPresetsV1
241
+ * @param {string} [idempotencyKey]
242
+ * @returns {Promise<{task_id: string}>}
243
+ */
244
+ async function splitStemSeparator(sourceId, presets, idempotencyKey = null) {
245
+ const body = { source_id: sourceId, presets };
246
+ if (idempotencyKey) body.idempotency_key = idempotencyKey;
247
+ const res = await postJson('split/stem_separator/', body);
248
+ if (res.status === 200) return res.response;
249
+ throw new Error(`Split failed: ${res.status} ${JSON.stringify(res.response)}`);
250
+ }
251
+
252
+ /**
253
+ * Starts a demuser (music removal) task.
254
+ */
255
+ async function splitDemuser(sourceId, presets = {}, idempotencyKey = null) {
256
+ const body = { source_id: sourceId, presets: { stem: 'music', ...presets } };
257
+ if (idempotencyKey) body.idempotency_key = idempotencyKey;
258
+ const res = await postJson('split/demuser/', body);
259
+ if (res.status === 200) return res.response;
260
+ throw new Error(`Demuser split failed: ${res.status} ${JSON.stringify(res.response)}`);
261
+ }
262
+
263
+ /**
264
+ * Starts a voice clean task.
265
+ */
266
+ async function splitVoiceClean(sourceId, presets = {}, idempotencyKey = null) {
267
+ const body = { source_id: sourceId, presets: { stem: 'voice', ...presets } };
268
+ if (idempotencyKey) body.idempotency_key = idempotencyKey;
269
+ const res = await postJson('split/voice_clean/', body);
270
+ if (res.status === 200) return res.response;
271
+ throw new Error(`Voice clean split failed: ${res.status} ${JSON.stringify(res.response)}`);
272
+ }
273
+
274
+ /**
275
+ * Starts a multistem split task.
276
+ */
277
+ async function splitMultistem(sourceId, presets, idempotencyKey = null) {
278
+ const body = { source_id: sourceId, presets };
279
+ if (idempotencyKey) body.idempotency_key = idempotencyKey;
280
+ const res = await postJson('split/multistem/', body);
281
+ if (res.status === 200) return res.response;
282
+ throw new Error(`Multistem split failed: ${res.status} ${JSON.stringify(res.response)}`);
283
+ }
284
+
285
+ /* ============================================================
286
+ BATCH SPLIT
287
+ ============================================================ */
288
+
289
+ async function batchSplitStemSeparator(items) {
290
+ const res = await postJson('split/batch/stem_separator/', { items });
291
+ if (res.status === 200) return res.response;
292
+ throw new Error(`Batch split failed: ${res.status}`);
293
+ }
294
+ // Similar batch functions can be added for demuser, voice_clean
295
+
296
+ /* ============================================================
297
+ VOICE CHANGE & PACKS
298
+ ============================================================ */
299
+
300
+ async function changeVoice(sourceId, presets, idempotencyKey = null) {
301
+ const body = { source_id: sourceId, presets };
302
+ if (idempotencyKey) body.idempotency_key = idempotencyKey;
303
+ const res = await postJson('change_voice/', body);
304
+ if (res.status === 200) return res.response;
305
+ throw new Error(`Voice change failed: ${res.status} ${JSON.stringify(res.response)}`);
306
+ }
307
+
308
+ async function listVoicePacks() {
309
+ const res = await postJson('voice_packs/list/');
310
+ if (res.status === 200) return res.response;
311
+ throw new Error(`List voice packs failed: ${res.status}`);
312
+ }
313
+
314
+ /* ============================================================
315
+ OTHER ENDPOINTS
316
+ ============================================================ */
317
+
318
+ async function cancelTasks(taskIds) {
319
+ const res = await postJson('cancel/', { task_ids: Array.isArray(taskIds) ? taskIds : [taskIds] });
320
+ if (res.status === 200) return res.response;
321
+ throw new Error(`Cancel failed: ${res.status}`);
322
+ }
323
+
324
+ async function cancelAllTasks() {
325
+ const res = await postJson('cancel/all/');
326
+ if (res.status === 200) return res.response;
327
+ throw new Error(`Cancel all failed: ${res.status}`);
328
+ }
329
+
330
+ async function deleteSource(sourceId) {
331
+ const res = await postJson('delete/', { source_id: sourceId });
332
+ if (res.status === 200) return res.response;
333
+ throw new Error(`Delete failed: ${res.status}`);
334
+ }
335
+
336
+ async function getMinutesLeft() {
337
+ const res = await postJson('limits/minutes_left/');
338
+ if (res.status === 200) return res.response;
339
+ throw new Error(`Limits failed: ${res.status}`);
340
+ }
341
+
342
+ /* ============================================================
343
+ HIGH-LEVEL WORKFLOW HELPERS
344
+ ============================================================ */
345
+
346
+ /**
347
+ * Full workflow: upload file → start split → poll → return result with tracks.
348
+ *
349
+ * @async
350
+ * @function processSplit
351
+ * @param {string|Buffer|Blob} input - Audio source
352
+ * @param {string} stem - 'vocals' | 'voice' | 'music' | etc. (determines endpoint)
353
+ * @param {Object} [options] - presets + other options
354
+ * @returns {Promise<Object>} Success result from check (with tracks)
355
+ */
356
+ async function processSplit(input, stem, options = {}) {
357
+ const source = await uploadFile(input, options.filename);
358
+ const sourceId = source.id;
359
+
360
+ let task;
361
+ const presets = options.presets || {};
362
+
363
+ if (stem === 'voice') {
364
+ task = await splitVoiceClean(sourceId, { stem: 'voice', noise_cancelling_level: options.noise_cancelling_level ?? 1, ...presets });
365
+ } else if (stem === 'music') {
366
+ task = await splitDemuser(sourceId, { stem: 'music', ...presets });
367
+ } else {
368
+ task = await splitStemSeparator(sourceId, { stem, extraction_level: options.extraction_level ?? 'deep_extraction', ...presets });
369
+ }
370
+
371
+ const taskId = task.task_id;
372
+ const result = await pollForTask(taskId);
373
+
374
+ // Optionally auto-download tracks
375
+ if (options.autoDownload) {
376
+ // implement download logic here or return urls
377
+ }
378
+
379
+ return { sourceId, taskId, result };
380
+ }
381
+
382
+ /**
383
+ * Downloads a track URL to local file.
384
+ *
385
+ * @async
386
+ * @function downloadTrack
387
+ * @param {string} url - Track download URL from check result
388
+ * @param {string} [destDir] - Destination directory (defaults to TMP_DIR)
389
+ * @returns {Promise<string>} Local path
390
+ */
391
+ async function downloadTrack(url, destDir = TMP_DIR) {
392
+ await ensureTmpDir();
393
+ const response = await fetch(url);
394
+ if (!response.ok) throw new Error(`Download failed: ${response.status}`);
395
+
396
+ const cdHeader = response.headers.get('content-disposition') || '';
397
+ let filename = `track-${Date.now()}.mp3`;
398
+ // Simple filename extraction (improve with email-message parser if needed)
399
+ const match = cdHeader.match(/filename="?([^"]+)"?/);
400
+ if (match) filename = match[1];
401
+
402
+ const localPath = path.join(destDir, filename);
403
+ const buffer = Buffer.from(await response.arrayBuffer());
404
+ await fs.writeFile(localPath, buffer);
405
+ return localPath;
406
+ }
407
+
408
+ /* ============================================================
409
+ EXPORTS
410
+ ============================================================ */
411
+
412
+ export {
413
+ // Auth & utils
414
+ getHeaders,
415
+ uploadFile,
416
+ prepareUploadPayload,
417
+ downloadRemoteToTemp,
418
+ downloadTrack,
419
+
420
+ // Core
421
+ checkTasks,
422
+ pollForTask,
423
+
424
+ // Splits
425
+ splitStemSeparator,
426
+ splitDemuser,
427
+ splitVoiceClean,
428
+ splitMultistem,
429
+
430
+ // Batch
431
+ batchSplitStemSeparator,
432
+
433
+ // Voice
434
+ changeVoice,
435
+ listVoicePacks,
436
+
437
+ // Management
438
+ cancelTasks,
439
+ cancelAllTasks,
440
+ deleteSource,
441
+ getMinutesLeft,
442
+
443
+ // High-level
444
+ processSplit
445
+ };