@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.
Files changed (30) hide show
  1. package/README.md +1 -1
  2. package/dist/{AgentHistoryStore-DdYKfx7C.mjs → AgentHistoryStore-DVxv1kSO.mjs} +7905 -7029
  3. package/dist/{ConfigStore-BygzEI6H.mjs → ConfigStore-51UaP9pJ.mjs} +10645 -6760
  4. package/dist/{ImageStore-kVo-oHoS.mjs → ImageStore-DuI3tgwQ.mjs} +8 -6
  5. package/dist/SandboxRuntimeAdapter-Bt1wPpld.mjs +2 -0
  6. package/dist/{WorkItemsClient-ClBC49y0.mjs → WorkItemsClient-BMY0Qq1s.mjs} +27 -2
  7. package/dist/{buildAgent-B7cMwRAy.mjs → buildAgent-CrSRKYU_.mjs} +75 -43
  8. package/dist/commands/acpCommand.mjs +26 -35
  9. package/dist/commands/apiCommand.mjs +1 -1
  10. package/dist/commands/doctorCommand.mjs +3 -3
  11. package/dist/commands/envCommand.mjs +1 -1
  12. package/dist/commands/headlessCommand.mjs +15 -24
  13. package/dist/commands/mcpCommand.mjs +3 -3
  14. package/dist/commands/pluginCommand.mjs +2 -2
  15. package/dist/commands/updateCommand.mjs +5 -15
  16. package/dist/dist-BsG2Hlp-.mjs +739 -0
  17. package/dist/index.mjs +602 -324
  18. package/dist/{package-CRNnNXxq.mjs → package-DEsNSP54.mjs} +1 -1
  19. package/dist/{serve-DM7ld3-o.mjs → serve-BaHzVbcm.mjs} +300 -154
  20. package/dist/store-BkMur6uk.mjs +2 -0
  21. package/dist/{store-CvjTpQPs.mjs → store-BxmPEI3v.mjs} +10 -0
  22. package/dist/types-I45lMh7Q.mjs +2 -0
  23. package/dist/{updateChecker-CQW8bxo6.mjs → updateChecker-CCLVVdH0.mjs} +36 -13
  24. package/package.json +24 -23
  25. package/dist/ApiClient-dIB3YQc2.mjs +0 -316
  26. package/dist/SandboxRuntimeAdapter-BmHELLuM.mjs +0 -3
  27. package/dist/store-BG3e54c8.mjs +0 -3
  28. package/dist/types-CdIKgWWe.mjs +0 -3
  29. package/dist/{bashExecute-CrdPpBqk-DCATrE-D.mjs → bashExecute-CrdPpBqk-CgK1gZa2.mjs} +1 -1
  30. 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 };