@bike4mind/cli 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{AgentHistoryStore-DdYKfx7C.mjs → AgentHistoryStore-DVxv1kSO.mjs} +7905 -7029
- package/dist/{ConfigStore-BygzEI6H.mjs → ConfigStore-51UaP9pJ.mjs} +10645 -6760
- package/dist/{ImageStore-kVo-oHoS.mjs → ImageStore-DuI3tgwQ.mjs} +8 -6
- package/dist/SandboxRuntimeAdapter-Bt1wPpld.mjs +2 -0
- package/dist/{WorkItemsClient-ClBC49y0.mjs → WorkItemsClient-BMY0Qq1s.mjs} +27 -2
- package/dist/{buildAgent-B7cMwRAy.mjs → buildAgent-CrSRKYU_.mjs} +75 -43
- package/dist/commands/acpCommand.mjs +26 -35
- package/dist/commands/apiCommand.mjs +1 -1
- package/dist/commands/doctorCommand.mjs +3 -3
- package/dist/commands/envCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +15 -24
- package/dist/commands/mcpCommand.mjs +3 -3
- package/dist/commands/pluginCommand.mjs +2 -2
- package/dist/commands/updateCommand.mjs +5 -15
- package/dist/dist-BsG2Hlp-.mjs +739 -0
- package/dist/index.mjs +602 -324
- package/dist/{package-CRNnNXxq.mjs → package-DEsNSP54.mjs} +1 -1
- package/dist/{serve-DM7ld3-o.mjs → serve-BaHzVbcm.mjs} +300 -154
- package/dist/store-BkMur6uk.mjs +2 -0
- package/dist/{store-CvjTpQPs.mjs → store-BxmPEI3v.mjs} +10 -0
- package/dist/types-I45lMh7Q.mjs +2 -0
- package/dist/{updateChecker-CQW8bxo6.mjs → updateChecker-CCLVVdH0.mjs} +36 -13
- package/package.json +24 -23
- package/dist/ApiClient-dIB3YQc2.mjs +0 -316
- package/dist/SandboxRuntimeAdapter-BmHELLuM.mjs +0 -3
- package/dist/store-BG3e54c8.mjs +0 -3
- package/dist/types-CdIKgWWe.mjs +0 -3
- package/dist/{bashExecute-CrdPpBqk-DCATrE-D.mjs → bashExecute-CrdPpBqk-CgK1gZa2.mjs} +1 -1
- package/dist/{grepSearch-BgoOOwGe-DtlV8Gn-.mjs → grepSearch-BgoOOwGe-B388GxRD.mjs} +2 -2
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { _ as logger, f as AuthenticatedApiClient, p as DeviceFlowClient, t as ConfigStore } from "./ConfigStore-51UaP9pJ.mjs";
|
|
3
|
+
import { t as version } from "./package-DEsNSP54.mjs";
|
|
4
|
+
import { isAxiosError } from "axios";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { createParser } from "eventsource-parser";
|
|
7
|
+
//#region src/auth/OAuthClient.ts
|
|
8
|
+
/**
|
|
9
|
+
* The CLI's RFC 8628 `client_id`; the desktop app registers under its own. Typed against the
|
|
10
|
+
* server allowlist so dropping this id there fails this build rather than this client's logins
|
|
11
|
+
* (see OAUTH_DEVICE_CLIENT_IDS).
|
|
12
|
+
*/
|
|
13
|
+
const CLI_OAUTH_CLIENT_ID = "b4m-cli";
|
|
14
|
+
/**
|
|
15
|
+
* The shared device-flow client, pinned to the CLI's registration.
|
|
16
|
+
*/
|
|
17
|
+
var OAuthClient = class extends DeviceFlowClient {
|
|
18
|
+
constructor(baseURL = "http://localhost:3000") {
|
|
19
|
+
super({
|
|
20
|
+
baseUrl: baseURL,
|
|
21
|
+
clientId: CLI_OAUTH_CLIENT_ID
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/auth/providerKeyFailure.ts
|
|
27
|
+
const PROVIDER_KEY_ERROR_CODES = /* @__PURE__ */ new Set(["provider_not_configured", "provider_rejected"]);
|
|
28
|
+
/**
|
|
29
|
+
* A 401 whose body names a missing or rejected AI-provider key (e.g. /api/ai/tts). The
|
|
30
|
+
* Bike4Mind credential was accepted, so refreshing or re-authenticating cannot fix it.
|
|
31
|
+
* Shared by the ApiClient refresh interceptor and mapApiError in mcp/b4mApiClient.ts.
|
|
32
|
+
*/
|
|
33
|
+
function isProviderKeyFailure(data) {
|
|
34
|
+
const body = decodeBinaryBody(data);
|
|
35
|
+
if (!body || typeof body !== "object") return false;
|
|
36
|
+
const { errorCode } = body;
|
|
37
|
+
return typeof errorCode === "string" && PROVIDER_KEY_ERROR_CODES.has(errorCode);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* An arraybuffer-typed request (every @bike4mind/sdk call, through ApiClient.fetch) gets its
|
|
41
|
+
* error body as raw bytes, and the refresh interceptor sees it before any caller can decode it.
|
|
42
|
+
*/
|
|
43
|
+
function decodeBinaryBody(data) {
|
|
44
|
+
const bytes = data instanceof ArrayBuffer ? Buffer.from(data) : ArrayBuffer.isView(data) ? Buffer.from(data.buffer, data.byteOffset, data.byteLength) : void 0;
|
|
45
|
+
if (!bytes) return data;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/auth/ApiClient.ts
|
|
54
|
+
const USER_AGENT = `b4m-cli/${version}`;
|
|
55
|
+
/**
|
|
56
|
+
* Per-request timeout. Generous by design: a waited chat (POST /api/chat with wait:true)
|
|
57
|
+
* runs a full server-side quest, so the bound only exists to stop a hung backend from
|
|
58
|
+
* wedging a caller forever, not to cap a normal long quest. Override with B4M_API_TIMEOUT_MS
|
|
59
|
+
* (0 disables the timeout entirely).
|
|
60
|
+
*/
|
|
61
|
+
const DEFAULT_API_TIMEOUT_MS = 6e5;
|
|
62
|
+
function resolveTimeoutMs() {
|
|
63
|
+
const raw = process.env.B4M_API_TIMEOUT_MS;
|
|
64
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_API_TIMEOUT_MS;
|
|
65
|
+
const parsed = Number(raw);
|
|
66
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_API_TIMEOUT_MS;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The shared authenticated client, wired to the CLI's storage, logger and branding.
|
|
70
|
+
*/
|
|
71
|
+
var ApiClient = class extends AuthenticatedApiClient {
|
|
72
|
+
/**
|
|
73
|
+
* @param apiKey - When set, requests authenticate with this instance API key via
|
|
74
|
+
* the `x-api-key` header and the OAuth-JWT path (Bearer injection + refresh-on-401)
|
|
75
|
+
* is bypassed entirely. Omit to keep the default stored-JWT behavior unchanged.
|
|
76
|
+
*/
|
|
77
|
+
constructor(baseURL = "http://localhost:3000", configStore, apiKey) {
|
|
78
|
+
super({
|
|
79
|
+
baseUrl: baseURL,
|
|
80
|
+
tokenStore: configStore || new ConfigStore(),
|
|
81
|
+
oauthClient: new OAuthClient(baseURL),
|
|
82
|
+
logger,
|
|
83
|
+
apiKey,
|
|
84
|
+
timeoutMs: resolveTimeoutMs(),
|
|
85
|
+
headers: {
|
|
86
|
+
"User-Agent": USER_AGENT,
|
|
87
|
+
"X-B4M-Client": USER_AGENT
|
|
88
|
+
},
|
|
89
|
+
reauthMessages: {
|
|
90
|
+
refreshFailed: "Authentication expired. Please run `b4m login` again.",
|
|
91
|
+
stillUnauthorized: "Authentication failed. Please run /login to authenticate.",
|
|
92
|
+
notLoggedIn: "Authentication failed: not logged in. Please run `b4m login` to authenticate."
|
|
93
|
+
},
|
|
94
|
+
isPassThrough401: isProviderKeyFailure
|
|
95
|
+
});
|
|
96
|
+
this.fetch = async (input, init = {}) => {
|
|
97
|
+
const headers = Object.fromEntries(new Headers(init.headers));
|
|
98
|
+
try {
|
|
99
|
+
return toFetchResponse(await this.getAxiosInstance().request({
|
|
100
|
+
url: input instanceof Request ? input.url : String(input),
|
|
101
|
+
method: init.method ?? "GET",
|
|
102
|
+
headers,
|
|
103
|
+
data: init.body ?? void 0,
|
|
104
|
+
signal: init.signal ?? void 0,
|
|
105
|
+
responseType: /text\/event-stream/i.test(headers.accept ?? "") ? "stream" : "arraybuffer",
|
|
106
|
+
...init.redirect && init.redirect !== "follow" ? { maxRedirects: 0 } : {}
|
|
107
|
+
}));
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (isAxiosError(error) && error.response) return toFetchResponse(error.response);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
get baseURL() {
|
|
115
|
+
return this.getAxiosInstance().defaults.baseURL ?? "";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function toFetchResponse(response) {
|
|
119
|
+
const headers = new Headers();
|
|
120
|
+
for (const [name, value] of Object.entries(response.headers)) if (value !== void 0 && value !== null) headers.set(name, Array.isArray(value) ? value.join(", ") : String(value));
|
|
121
|
+
const body = [
|
|
122
|
+
101,
|
|
123
|
+
204,
|
|
124
|
+
205,
|
|
125
|
+
304
|
|
126
|
+
].includes(response.status) ? null : response.data instanceof Readable ? Readable.toWeb(response.data) : response.data;
|
|
127
|
+
return new Response(body, {
|
|
128
|
+
status: response.status,
|
|
129
|
+
statusText: response.statusText,
|
|
130
|
+
headers
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region ../sdk/dist/index.mjs
|
|
135
|
+
/**
|
|
136
|
+
* A non-2xx response. `message` is the envelope's `error` (`ErrorResponse` in the spec); branch on `status` and
|
|
137
|
+
* `errorCode`, never on the prose. The deprecated `name` field is deliberately not surfaced.
|
|
138
|
+
*/
|
|
139
|
+
var B4mApiError = class B4mApiError extends Error {
|
|
140
|
+
status;
|
|
141
|
+
errorCode;
|
|
142
|
+
requestId;
|
|
143
|
+
retryAfterSeconds;
|
|
144
|
+
body;
|
|
145
|
+
headers;
|
|
146
|
+
constructor(status, body, headers) {
|
|
147
|
+
const record = body && typeof body === "object" ? body : {};
|
|
148
|
+
super(typeof record.error === "string" ? record.error : `HTTP ${status}`);
|
|
149
|
+
this.name = "B4mApiError";
|
|
150
|
+
this.status = status;
|
|
151
|
+
this.body = body;
|
|
152
|
+
this.headers = headers;
|
|
153
|
+
if (typeof record.errorCode === "string") this.errorCode = record.errorCode;
|
|
154
|
+
const requestId = typeof record.request_id === "string" ? record.request_id : headers.get("x-request-id");
|
|
155
|
+
if (requestId) this.requestId = requestId;
|
|
156
|
+
const retryAfterSeconds = parseRetryAfterSeconds(headers.get("retry-after"));
|
|
157
|
+
if (retryAfterSeconds !== void 0) this.retryAfterSeconds = retryAfterSeconds;
|
|
158
|
+
}
|
|
159
|
+
static async fromResponse(response) {
|
|
160
|
+
const text = await response.text().catch(() => "");
|
|
161
|
+
let body = text;
|
|
162
|
+
try {
|
|
163
|
+
body = JSON.parse(text);
|
|
164
|
+
} catch {}
|
|
165
|
+
return new B4mApiError(response.status, body, response.headers);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* A completions stream that closed without `[DONE]` and without an `error` event: the connection was cut
|
|
170
|
+
* mid-response, so nothing about the request itself failed. Safe to retry when no output was used yet.
|
|
171
|
+
*/
|
|
172
|
+
var B4mStreamEndedError = class extends Error {
|
|
173
|
+
constructor() {
|
|
174
|
+
super("completion stream ended before [DONE]");
|
|
175
|
+
this.name = "B4mStreamEndedError";
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* A polled quest (chat turn, image generation or edit) that did not succeed: it carries `type: "error"`
|
|
180
|
+
* (`reason: 'error'`), ended `stopped` (`'stopped'`), or was still running when the poll's `timeoutMs` ran out
|
|
181
|
+
* (`'timeout'`). `quest` is the last state seen; its `reply` carries the server's explanation.
|
|
182
|
+
*/
|
|
183
|
+
var B4mQuestError = class extends Error {
|
|
184
|
+
reason;
|
|
185
|
+
quest;
|
|
186
|
+
constructor(reason, quest) {
|
|
187
|
+
super(reason === "timeout" ? `quest ${quest.id} still ${quest.status ?? "pending"} when the poll timed out` : quest.reply ?? `quest ${quest.id} ${reason === "error" ? "failed" : "was stopped"}`);
|
|
188
|
+
this.name = "B4mQuestError";
|
|
189
|
+
this.reason = reason;
|
|
190
|
+
this.quest = quest;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Normalize a Retry-After header (RFC 7231: delta-seconds or an HTTP-date) to whole, non-negative seconds;
|
|
195
|
+
* undefined when absent or unparseable.
|
|
196
|
+
*/
|
|
197
|
+
function parseRetryAfterSeconds(value) {
|
|
198
|
+
if (value === void 0 || value === null) return void 0;
|
|
199
|
+
const raw = String(value).trim();
|
|
200
|
+
if (/^\d+$/.test(raw)) return Number(raw);
|
|
201
|
+
const dateMs = Date.parse(raw);
|
|
202
|
+
if (Number.isNaN(dateMs)) return void 0;
|
|
203
|
+
return Math.max(0, Math.ceil((dateMs - Date.now()) / 1e3));
|
|
204
|
+
}
|
|
205
|
+
const operations = {
|
|
206
|
+
addDataLakeFile: {
|
|
207
|
+
method: "POST",
|
|
208
|
+
path: "/api/v1/data-lakes/{id}/files/{file_id}"
|
|
209
|
+
},
|
|
210
|
+
cancelMusicGeneration: {
|
|
211
|
+
method: "POST",
|
|
212
|
+
path: "/api/v1/music-generations/{id}/cancel"
|
|
213
|
+
},
|
|
214
|
+
cancelVideoGeneration: {
|
|
215
|
+
method: "POST",
|
|
216
|
+
path: "/api/v1/video-generations/{id}/cancel"
|
|
217
|
+
},
|
|
218
|
+
cloneSession: {
|
|
219
|
+
method: "POST",
|
|
220
|
+
path: "/api/v1/sessions/{id}/clone"
|
|
221
|
+
},
|
|
222
|
+
createAgent: {
|
|
223
|
+
method: "POST",
|
|
224
|
+
path: "/api/v1/agents"
|
|
225
|
+
},
|
|
226
|
+
createArtifact: {
|
|
227
|
+
method: "POST",
|
|
228
|
+
path: "/api/v1/artifacts"
|
|
229
|
+
},
|
|
230
|
+
createCompletion: {
|
|
231
|
+
method: "POST",
|
|
232
|
+
path: "/api/ai/v1/completions"
|
|
233
|
+
},
|
|
234
|
+
createDecision: {
|
|
235
|
+
method: "POST",
|
|
236
|
+
path: "/api/v1/decisions"
|
|
237
|
+
},
|
|
238
|
+
createEmbeddings: {
|
|
239
|
+
method: "POST",
|
|
240
|
+
path: "/api/v1/embeddings"
|
|
241
|
+
},
|
|
242
|
+
createFileUpload: {
|
|
243
|
+
method: "POST",
|
|
244
|
+
path: "/api/v1/files"
|
|
245
|
+
},
|
|
246
|
+
createMusicGeneration: {
|
|
247
|
+
method: "POST",
|
|
248
|
+
path: "/api/v1/music-generations"
|
|
249
|
+
},
|
|
250
|
+
createProject: {
|
|
251
|
+
method: "POST",
|
|
252
|
+
path: "/api/v1/projects"
|
|
253
|
+
},
|
|
254
|
+
createSession: {
|
|
255
|
+
method: "POST",
|
|
256
|
+
path: "/api/v1/sessions"
|
|
257
|
+
},
|
|
258
|
+
createTranscription: {
|
|
259
|
+
method: "POST",
|
|
260
|
+
path: "/api/v1/transcriptions"
|
|
261
|
+
},
|
|
262
|
+
createTranscriptionUpload: {
|
|
263
|
+
method: "POST",
|
|
264
|
+
path: "/api/v1/transcriptions/uploads"
|
|
265
|
+
},
|
|
266
|
+
createVideoGeneration: {
|
|
267
|
+
method: "POST",
|
|
268
|
+
path: "/api/v1/video-generations"
|
|
269
|
+
},
|
|
270
|
+
createVoiceSession: {
|
|
271
|
+
method: "POST",
|
|
272
|
+
path: "/api/v1/voice/sessions"
|
|
273
|
+
},
|
|
274
|
+
deleteAgent: {
|
|
275
|
+
method: "DELETE",
|
|
276
|
+
path: "/api/v1/agents/{id}"
|
|
277
|
+
},
|
|
278
|
+
deleteArtifact: {
|
|
279
|
+
method: "DELETE",
|
|
280
|
+
path: "/api/v1/artifacts/{id}"
|
|
281
|
+
},
|
|
282
|
+
deleteFile: {
|
|
283
|
+
method: "DELETE",
|
|
284
|
+
path: "/api/v1/files/{id}"
|
|
285
|
+
},
|
|
286
|
+
deleteProject: {
|
|
287
|
+
method: "DELETE",
|
|
288
|
+
path: "/api/v1/projects/{id}"
|
|
289
|
+
},
|
|
290
|
+
deleteSession: {
|
|
291
|
+
method: "DELETE",
|
|
292
|
+
path: "/api/sessions/{id}"
|
|
293
|
+
},
|
|
294
|
+
editImage: {
|
|
295
|
+
method: "POST",
|
|
296
|
+
path: "/api/v1/image-edits"
|
|
297
|
+
},
|
|
298
|
+
endVoiceSession: {
|
|
299
|
+
method: "POST",
|
|
300
|
+
path: "/api/v1/voice/sessions/{id}/end"
|
|
301
|
+
},
|
|
302
|
+
executeTool: {
|
|
303
|
+
method: "POST",
|
|
304
|
+
path: "/api/ai/v1/tools"
|
|
305
|
+
},
|
|
306
|
+
generateImage: {
|
|
307
|
+
method: "POST",
|
|
308
|
+
path: "/api/v1/image-generations"
|
|
309
|
+
},
|
|
310
|
+
generateMusic: {
|
|
311
|
+
method: "POST",
|
|
312
|
+
path: "/api/ai/music"
|
|
313
|
+
},
|
|
314
|
+
generateSoundEffect: {
|
|
315
|
+
method: "POST",
|
|
316
|
+
path: "/api/ai/sound-effects"
|
|
317
|
+
},
|
|
318
|
+
getAgent: {
|
|
319
|
+
method: "GET",
|
|
320
|
+
path: "/api/v1/agents/{id}"
|
|
321
|
+
},
|
|
322
|
+
getAgentExecution: {
|
|
323
|
+
method: "GET",
|
|
324
|
+
path: "/api/v1/agent-executions/{id}"
|
|
325
|
+
},
|
|
326
|
+
getArtifact: {
|
|
327
|
+
method: "GET",
|
|
328
|
+
path: "/api/v1/artifacts/{id}"
|
|
329
|
+
},
|
|
330
|
+
getArtifactVersion: {
|
|
331
|
+
method: "GET",
|
|
332
|
+
path: "/api/v1/artifacts/{id}/versions/{version}"
|
|
333
|
+
},
|
|
334
|
+
getCreditBalance: {
|
|
335
|
+
method: "GET",
|
|
336
|
+
path: "/api/v1/credits"
|
|
337
|
+
},
|
|
338
|
+
getDataLake: {
|
|
339
|
+
method: "GET",
|
|
340
|
+
path: "/api/v1/data-lakes/{id}"
|
|
341
|
+
},
|
|
342
|
+
getDataLakeFile: {
|
|
343
|
+
method: "GET",
|
|
344
|
+
path: "/api/v1/data-lakes/{id}/files/{file_id}"
|
|
345
|
+
},
|
|
346
|
+
getFile: {
|
|
347
|
+
method: "GET",
|
|
348
|
+
path: "/api/v1/files/{id}"
|
|
349
|
+
},
|
|
350
|
+
getMe: {
|
|
351
|
+
method: "GET",
|
|
352
|
+
path: "/api/v1/me"
|
|
353
|
+
},
|
|
354
|
+
getMusicGeneration: {
|
|
355
|
+
method: "GET",
|
|
356
|
+
path: "/api/v1/music-generations/{id}"
|
|
357
|
+
},
|
|
358
|
+
getProject: {
|
|
359
|
+
method: "GET",
|
|
360
|
+
path: "/api/v1/projects/{id}"
|
|
361
|
+
},
|
|
362
|
+
getQuest: {
|
|
363
|
+
method: "GET",
|
|
364
|
+
path: "/api/v1/quests/{id}"
|
|
365
|
+
},
|
|
366
|
+
getSession: {
|
|
367
|
+
method: "GET",
|
|
368
|
+
path: "/api/sessions/{id}"
|
|
369
|
+
},
|
|
370
|
+
getVideoGeneration: {
|
|
371
|
+
method: "GET",
|
|
372
|
+
path: "/api/v1/video-generations/{id}"
|
|
373
|
+
},
|
|
374
|
+
listAgents: {
|
|
375
|
+
method: "GET",
|
|
376
|
+
path: "/api/v1/agents"
|
|
377
|
+
},
|
|
378
|
+
listArtifactVersions: {
|
|
379
|
+
method: "GET",
|
|
380
|
+
path: "/api/v1/artifacts/{id}/versions"
|
|
381
|
+
},
|
|
382
|
+
listArtifacts: {
|
|
383
|
+
method: "GET",
|
|
384
|
+
path: "/api/v1/artifacts"
|
|
385
|
+
},
|
|
386
|
+
listDataLakes: {
|
|
387
|
+
method: "GET",
|
|
388
|
+
path: "/api/v1/data-lakes"
|
|
389
|
+
},
|
|
390
|
+
listDecisionModels: {
|
|
391
|
+
method: "GET",
|
|
392
|
+
path: "/api/v1/decision-models"
|
|
393
|
+
},
|
|
394
|
+
listFiles: {
|
|
395
|
+
method: "GET",
|
|
396
|
+
path: "/api/v1/files"
|
|
397
|
+
},
|
|
398
|
+
listModels: {
|
|
399
|
+
method: "GET",
|
|
400
|
+
path: "/api/v1/models"
|
|
401
|
+
},
|
|
402
|
+
listMusicGenerations: {
|
|
403
|
+
method: "GET",
|
|
404
|
+
path: "/api/v1/music-generations"
|
|
405
|
+
},
|
|
406
|
+
listMusicModels: {
|
|
407
|
+
method: "GET",
|
|
408
|
+
path: "/api/v1/music-models"
|
|
409
|
+
},
|
|
410
|
+
listProjects: {
|
|
411
|
+
method: "GET",
|
|
412
|
+
path: "/api/v1/projects"
|
|
413
|
+
},
|
|
414
|
+
listQuestFiles: {
|
|
415
|
+
method: "GET",
|
|
416
|
+
path: "/api/v1/quests/{id}/files"
|
|
417
|
+
},
|
|
418
|
+
listSessions: {
|
|
419
|
+
method: "GET",
|
|
420
|
+
path: "/api/v1/sessions"
|
|
421
|
+
},
|
|
422
|
+
listVideoGenerations: {
|
|
423
|
+
method: "GET",
|
|
424
|
+
path: "/api/v1/video-generations"
|
|
425
|
+
},
|
|
426
|
+
listVideoModels: {
|
|
427
|
+
method: "GET",
|
|
428
|
+
path: "/api/v1/video-models"
|
|
429
|
+
},
|
|
430
|
+
listVoices: {
|
|
431
|
+
method: "GET",
|
|
432
|
+
path: "/api/v1/voice/voices"
|
|
433
|
+
},
|
|
434
|
+
listWhatsNew: {
|
|
435
|
+
method: "GET",
|
|
436
|
+
path: "/api/v1/whats-new"
|
|
437
|
+
},
|
|
438
|
+
removeDataLakeFile: {
|
|
439
|
+
method: "DELETE",
|
|
440
|
+
path: "/api/v1/data-lakes/{id}/files/{file_id}"
|
|
441
|
+
},
|
|
442
|
+
searchDataLake: {
|
|
443
|
+
method: "POST",
|
|
444
|
+
path: "/api/v1/data-lakes/{id}/search"
|
|
445
|
+
},
|
|
446
|
+
sendChatMessage: {
|
|
447
|
+
method: "POST",
|
|
448
|
+
path: "/api/chat"
|
|
449
|
+
},
|
|
450
|
+
startAgentExecution: {
|
|
451
|
+
method: "POST",
|
|
452
|
+
path: "/api/v1/agent-executions"
|
|
453
|
+
},
|
|
454
|
+
synthesizeSpeech: {
|
|
455
|
+
method: "POST",
|
|
456
|
+
path: "/api/ai/tts"
|
|
457
|
+
},
|
|
458
|
+
updateAgent: {
|
|
459
|
+
method: "PATCH",
|
|
460
|
+
path: "/api/v1/agents/{id}"
|
|
461
|
+
},
|
|
462
|
+
updateArtifact: {
|
|
463
|
+
method: "PATCH",
|
|
464
|
+
path: "/api/v1/artifacts/{id}"
|
|
465
|
+
},
|
|
466
|
+
updateFile: {
|
|
467
|
+
method: "PATCH",
|
|
468
|
+
path: "/api/v1/files/{id}"
|
|
469
|
+
},
|
|
470
|
+
updateProject: {
|
|
471
|
+
method: "PATCH",
|
|
472
|
+
path: "/api/v1/projects/{id}"
|
|
473
|
+
},
|
|
474
|
+
updateSession: {
|
|
475
|
+
method: "PUT",
|
|
476
|
+
path: "/api/sessions/{id}"
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
/**
|
|
480
|
+
* Yield each server-sent event's `data` from a fetch body, until `[DONE]`, the end of the stream, or `signal`
|
|
481
|
+
* aborts (which throws the signal's reason). `onDone` is called when `[DONE]` is seen, so a caller can tell it
|
|
482
|
+
* from a truncated stream. Breaking out of the loop early cancels the body.
|
|
483
|
+
*/
|
|
484
|
+
async function* parseSse(body, signal, onDone) {
|
|
485
|
+
const queue = [];
|
|
486
|
+
const parser = createParser({ onEvent: (event) => queue.push(event.data) });
|
|
487
|
+
const reader = body.getReader();
|
|
488
|
+
const decoder = new TextDecoder();
|
|
489
|
+
const onAbort = () => void reader.cancel().catch(() => {});
|
|
490
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
491
|
+
let finished = false;
|
|
492
|
+
try {
|
|
493
|
+
for (;;) {
|
|
494
|
+
signal?.throwIfAborted();
|
|
495
|
+
const { done, value } = await reader.read();
|
|
496
|
+
signal?.throwIfAborted();
|
|
497
|
+
parser.feed(done ? decoder.decode() : decoder.decode(value, { stream: true }));
|
|
498
|
+
while (queue.length > 0) {
|
|
499
|
+
const data = queue.shift();
|
|
500
|
+
if (data === "[DONE]") {
|
|
501
|
+
onDone?.();
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
yield data;
|
|
505
|
+
}
|
|
506
|
+
if (done) {
|
|
507
|
+
finished = true;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} finally {
|
|
512
|
+
signal?.removeEventListener("abort", onAbort);
|
|
513
|
+
if (!finished) await reader.cancel().catch(() => {});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const ABSOLUTE_URL = /^https?:\/\//i;
|
|
517
|
+
function buildUrl(baseUrl, path, params, query) {
|
|
518
|
+
const values = params ?? {};
|
|
519
|
+
const resolved = path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
520
|
+
const value = values[name];
|
|
521
|
+
if (value === void 0 || value === null || value === "" || value === "." || value === "..") throw new Error(`Invalid path parameter ${name}: ${JSON.stringify(value)}`);
|
|
522
|
+
return encodeURIComponent(String(value));
|
|
523
|
+
});
|
|
524
|
+
const search = new URLSearchParams();
|
|
525
|
+
for (const [key, value] of Object.entries(query ?? {})) for (const item of Array.isArray(value) ? value : [value]) if (item !== void 0 && item !== null) search.append(key, String(item));
|
|
526
|
+
const qs = search.toString();
|
|
527
|
+
const relative = resolved.startsWith("/") ? resolved : `/${resolved}`;
|
|
528
|
+
const url = ABSOLUTE_URL.test(resolved) ? resolved : `${baseUrl.replace(/\/+$/, "")}${relative}`;
|
|
529
|
+
return qs ? `${url}${url.includes("?") ? "&" : "?"}${qs}` : url;
|
|
530
|
+
}
|
|
531
|
+
function assertBaseUrl(baseUrl) {
|
|
532
|
+
let parsed;
|
|
533
|
+
try {
|
|
534
|
+
parsed = new URL(baseUrl);
|
|
535
|
+
} catch {
|
|
536
|
+
throw new Error(`createClient: baseUrl must be an absolute http(s) URL, got ${JSON.stringify(baseUrl)}`);
|
|
537
|
+
}
|
|
538
|
+
if (!/^https?:$/.test(parsed.protocol) || parsed.search || parsed.hash) throw new Error(`createClient: baseUrl must be an http(s) URL with no query or fragment, got ${JSON.stringify(baseUrl)}`);
|
|
539
|
+
}
|
|
540
|
+
function createClient(options) {
|
|
541
|
+
if (!options.baseUrl) throw new Error("createClient: baseUrl is required");
|
|
542
|
+
assertBaseUrl(options.baseUrl);
|
|
543
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
544
|
+
async function send(method, path, args = {}) {
|
|
545
|
+
const headers = new Headers(options.headers);
|
|
546
|
+
for (const [name, value] of Object.entries(args.headers ?? {})) headers.set(name, value);
|
|
547
|
+
const token = await options.getAuthToken?.() || options.apiKey;
|
|
548
|
+
if (token) headers.set("authorization", `Bearer ${token}`);
|
|
549
|
+
let body;
|
|
550
|
+
if (args.body !== void 0) {
|
|
551
|
+
headers.set("content-type", "application/json");
|
|
552
|
+
body = JSON.stringify(args.body);
|
|
553
|
+
}
|
|
554
|
+
const response = await fetchImpl(buildUrl(options.baseUrl, path, args.params, args.query), {
|
|
555
|
+
method,
|
|
556
|
+
headers,
|
|
557
|
+
body,
|
|
558
|
+
signal: args.signal,
|
|
559
|
+
...args.redirect ? { redirect: args.redirect } : {}
|
|
560
|
+
});
|
|
561
|
+
if (!response.ok) throw await B4mApiError.fromResponse(response);
|
|
562
|
+
return response;
|
|
563
|
+
}
|
|
564
|
+
/** Any spec operation, returning the raw 2xx `Response` (for binary or streamed bodies). Non-2xx throws. */
|
|
565
|
+
function raw(operationId, ...[args]) {
|
|
566
|
+
const { method, path } = operations[operationId];
|
|
567
|
+
return send(method, path, args);
|
|
568
|
+
}
|
|
569
|
+
/** Any spec operation, typed from the spec; returns the parsed JSON body (`undefined` for an empty one). A non-JSON 2xx body throws. */
|
|
570
|
+
async function call(operationId, ...rest) {
|
|
571
|
+
const response = await raw(operationId, ...rest);
|
|
572
|
+
const text = await response.text();
|
|
573
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
574
|
+
if (text && !/json/i.test(contentType)) throw new Error(`${operationId} returned ${contentType || "a non-JSON body"}; use raw(), completions() or the tts/music/soundEffects helpers`);
|
|
575
|
+
return text ? JSON.parse(text) : void 0;
|
|
576
|
+
}
|
|
577
|
+
async function pollQuest(questId, poll = {}) {
|
|
578
|
+
const { intervalMs = 1e3, maxIntervalMs = 5e3, timeoutMs, signal } = poll;
|
|
579
|
+
const deadline = timeoutMs === void 0 ? void 0 : Date.now() + timeoutMs;
|
|
580
|
+
let delay = intervalMs;
|
|
581
|
+
for (;;) {
|
|
582
|
+
const quest = await call("getQuest", {
|
|
583
|
+
params: { id: questId },
|
|
584
|
+
signal
|
|
585
|
+
});
|
|
586
|
+
if (quest.status === "stopped") throw new B4mQuestError("stopped", quest);
|
|
587
|
+
if (quest.type === "error") throw new B4mQuestError("error", quest);
|
|
588
|
+
if (quest.status === "done") return quest;
|
|
589
|
+
if (deadline !== void 0 && Date.now() + delay > deadline) throw new B4mQuestError("timeout", quest);
|
|
590
|
+
await sleep(delay, signal);
|
|
591
|
+
delay = Math.min(delay * 1.5, maxIntervalMs);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const questHandle = (questId, ack) => ({
|
|
595
|
+
questId,
|
|
596
|
+
ack,
|
|
597
|
+
poll: (poll) => pollQuest(questId, poll)
|
|
598
|
+
});
|
|
599
|
+
async function audio(operationId, body, signal) {
|
|
600
|
+
const { method, path } = operations[operationId];
|
|
601
|
+
const response = await send(method, path, {
|
|
602
|
+
body: {
|
|
603
|
+
...body,
|
|
604
|
+
encoding: "base64"
|
|
605
|
+
},
|
|
606
|
+
signal
|
|
607
|
+
});
|
|
608
|
+
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
609
|
+
if (/json/i.test(contentType)) return await response.json();
|
|
610
|
+
const saved = response.headers.get("x-b4m-audio-saved") === "true";
|
|
611
|
+
const header = (name) => saved ? response.headers.get(name) ?? void 0 : void 0;
|
|
612
|
+
const result = {
|
|
613
|
+
delivery: "inline",
|
|
614
|
+
audio: toBase64(new Uint8Array(await response.arrayBuffer())),
|
|
615
|
+
contentType,
|
|
616
|
+
saved
|
|
617
|
+
};
|
|
618
|
+
const fabFileId = header("x-b4m-audio-fab-file-id");
|
|
619
|
+
const fileName = header("x-b4m-audio-file-name");
|
|
620
|
+
const fileUrl = header("x-b4m-audio-file-url");
|
|
621
|
+
if (fabFileId) result.fabFileId = fabFileId;
|
|
622
|
+
if (fileName) result.fileName = fileName;
|
|
623
|
+
if (fileUrl) result.fileUrl = fileUrl;
|
|
624
|
+
return result;
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
call,
|
|
628
|
+
raw,
|
|
629
|
+
/**
|
|
630
|
+
* Stream `POST /api/ai/v1/completions` as parsed events. A non-2xx before the stream throws `B4mApiError`; a
|
|
631
|
+
* server `{ type: 'error' }` event is yielded, not thrown. A stream that ends without `[DONE]` and without an
|
|
632
|
+
* error event throws. `url` overrides the path: an absolute http(s) URL is used as-is, anything else is joined
|
|
633
|
+
* under `baseUrl`. An absolute `url` receives the same `Authorization` header, so pass only an origin you trust.
|
|
634
|
+
*/
|
|
635
|
+
async *completions(body, opts = {}) {
|
|
636
|
+
const response = await send("POST", opts.url ?? operations.createCompletion.path, {
|
|
637
|
+
body,
|
|
638
|
+
signal: opts.signal,
|
|
639
|
+
headers: {
|
|
640
|
+
accept: "text/event-stream",
|
|
641
|
+
...opts.headers
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
if (!response.body) return;
|
|
645
|
+
let sawDone = false;
|
|
646
|
+
let sawError = false;
|
|
647
|
+
for await (const data of parseSse(response.body, opts.signal, () => {
|
|
648
|
+
sawDone = true;
|
|
649
|
+
})) {
|
|
650
|
+
let event;
|
|
651
|
+
try {
|
|
652
|
+
event = JSON.parse(data);
|
|
653
|
+
} catch {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (event.type === "error") sawError = true;
|
|
657
|
+
yield event;
|
|
658
|
+
}
|
|
659
|
+
if (!sawDone && !sawError) throw new B4mStreamEndedError();
|
|
660
|
+
},
|
|
661
|
+
/** `POST /api/chat`, queued (the default): the turn's outcome is on the returned handle's `poll()`. */
|
|
662
|
+
async chat(body, opts = {}) {
|
|
663
|
+
const ack = await call("sendChatMessage", {
|
|
664
|
+
body,
|
|
665
|
+
signal: opts.signal
|
|
666
|
+
});
|
|
667
|
+
return questHandle(ack.id, ack);
|
|
668
|
+
},
|
|
669
|
+
async generateImage(body, opts = {}) {
|
|
670
|
+
const ack = await call("generateImage", {
|
|
671
|
+
body,
|
|
672
|
+
signal: opts.signal
|
|
673
|
+
});
|
|
674
|
+
return questHandle(ack.quest.id, ack);
|
|
675
|
+
},
|
|
676
|
+
/** Edited images arrive on the finished quest's `files[].url`. */
|
|
677
|
+
async editImage(body, opts = {}) {
|
|
678
|
+
const ack = await call("editImage", {
|
|
679
|
+
body,
|
|
680
|
+
signal: opts.signal
|
|
681
|
+
});
|
|
682
|
+
return questHandle(ack.id, ack);
|
|
683
|
+
},
|
|
684
|
+
pollQuest,
|
|
685
|
+
async tts(body, opts = {}) {
|
|
686
|
+
try {
|
|
687
|
+
return {
|
|
688
|
+
kind: "audio",
|
|
689
|
+
data: await audio("synthesizeSpeech", body, opts.signal)
|
|
690
|
+
};
|
|
691
|
+
} catch (error) {
|
|
692
|
+
const tooLarge = error instanceof B4mApiError && error.status === 413 ? error : void 0;
|
|
693
|
+
const data = tooLarge?.body;
|
|
694
|
+
if (!tooLarge || !data?.saved || typeof data.fabFileId !== "string" || typeof data.provider !== "string") throw error;
|
|
695
|
+
const fallbackFrom = tooLarge.headers.get("x-b4m-tts-provider-fallback-from");
|
|
696
|
+
const result = {
|
|
697
|
+
kind: "saved-too-large",
|
|
698
|
+
data: {
|
|
699
|
+
...data,
|
|
700
|
+
fabFileId: data.fabFileId
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
if (fallbackFrom) result.fallbackFrom = fallbackFrom;
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
/**
|
|
708
|
+
* @deprecated `POST /api/ai/music` sunsets 2027-01-15. Queue a job with
|
|
709
|
+
* `call('createMusicGeneration', ...)` and poll `getMusicGeneration` instead.
|
|
710
|
+
*/
|
|
711
|
+
music(body, opts = {}) {
|
|
712
|
+
return audio("generateMusic", body, opts.signal);
|
|
713
|
+
},
|
|
714
|
+
soundEffects(body, opts = {}) {
|
|
715
|
+
return audio("generateSoundEffect", body, opts.signal);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function sleep(ms, signal) {
|
|
720
|
+
return new Promise((resolve, reject) => {
|
|
721
|
+
if (signal?.aborted) return reject(signal.reason);
|
|
722
|
+
const onAbort = () => {
|
|
723
|
+
clearTimeout(timer);
|
|
724
|
+
reject(signal?.reason);
|
|
725
|
+
};
|
|
726
|
+
const timer = setTimeout(() => {
|
|
727
|
+
signal?.removeEventListener("abort", onAbort);
|
|
728
|
+
resolve();
|
|
729
|
+
}, ms);
|
|
730
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
function toBase64(bytes) {
|
|
734
|
+
let binary = "";
|
|
735
|
+
for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
736
|
+
return btoa(binary);
|
|
737
|
+
}
|
|
738
|
+
//#endregion
|
|
739
|
+
export { ApiClient as a, parseRetryAfterSeconds as i, B4mStreamEndedError as n, isProviderKeyFailure as o, createClient as r, OAuthClient as s, B4mApiError as t };
|