@vibiz/sdk 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1015 @@
1
+ // packages/sdk/src/client.ts
2
+ var VibizApiError = class extends Error {
3
+ constructor(status, code, message, details) {
4
+ super(message);
5
+ this.status = status;
6
+ this.code = code;
7
+ this.details = details;
8
+ this.name = "VibizApiError";
9
+ }
10
+ status;
11
+ code;
12
+ details;
13
+ };
14
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
15
+ var Vibiz = class {
16
+ constructor(options) {
17
+ this.options = options;
18
+ if (!options.baseUrl) {
19
+ throw new Error("No Vibiz API URL: pass baseUrl (CLI: --api, or VIBIZ_API_URL).");
20
+ }
21
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
22
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
23
+ }
24
+ options;
25
+ baseUrl;
26
+ fetchImpl;
27
+ async send(path, init = {}) {
28
+ const headers = { authorization: `Bearer ${this.options.apiKey}` };
29
+ if (init.body !== void 0) headers["content-type"] = "application/json";
30
+ if (init.raw) headers["content-type"] = init.contentType ?? "application/octet-stream";
31
+ if (init.idempotencyKey) headers["idempotency-key"] = init.idempotencyKey;
32
+ if (this.options.user) headers["x-vibiz-user"] = endUserHeader(this.options.user);
33
+ return this.fetchImpl(`${this.baseUrl}/v1${path}`, {
34
+ method: init.method ?? "GET",
35
+ headers,
36
+ body: init.form ?? init.raw ?? (init.body !== void 0 ? JSON.stringify(init.body) : void 0)
37
+ });
38
+ }
39
+ async fail(res) {
40
+ const json = await res.json().catch(() => null);
41
+ const e = json?.error;
42
+ throw new VibizApiError(
43
+ res.status,
44
+ e?.code ?? "http_error",
45
+ e?.message ?? `HTTP ${res.status}`,
46
+ e?.details
47
+ );
48
+ }
49
+ /** A raw response (audio, a stream), failing like `request` on an error status. */
50
+ async stream(path, init = {}) {
51
+ const res = await this.send(path, init);
52
+ if (!res.ok) return this.fail(res);
53
+ return res;
54
+ }
55
+ async request(path, init = {}) {
56
+ const res = await this.send(path, init);
57
+ if (res.status === 204) return void 0;
58
+ const json = await res.json().catch(() => null);
59
+ if (!res.ok) {
60
+ const e = json?.error;
61
+ throw new VibizApiError(
62
+ res.status,
63
+ e?.code ?? "http_error",
64
+ e?.message ?? `HTTP ${res.status}`,
65
+ e?.details
66
+ );
67
+ }
68
+ return json;
69
+ }
70
+ postAction(id, postId, action, body, env) {
71
+ return this.request(
72
+ `/projects/${id}/social/posts/${encodeURIComponent(postId)}/${action}${envQuery(env)}`,
73
+ { method: "POST", body }
74
+ );
75
+ }
76
+ projects = {
77
+ create: (input, opts = {}) => this.request("/projects", {
78
+ method: "POST",
79
+ body: input,
80
+ ...opts
81
+ }),
82
+ list: () => this.request("/projects").then((r) => r.data),
83
+ get: (id) => this.request(`/projects/${id}`),
84
+ delete: (id) => this.request(`/projects/${id}`, {
85
+ method: "DELETE"
86
+ }),
87
+ /** Undo a delete within 7 days: data was kept, the site is provisioned again. */
88
+ restore: (id) => this.request(`/projects/${id}/restore`, {
89
+ method: "POST"
90
+ }),
91
+ addResource: (id, type) => this.request(`/projects/${id}/resources`, {
92
+ method: "POST",
93
+ body: { type }
94
+ }),
95
+ /** Change a resource's settings (merged), e.g. `('replay', { sample_rate: 0.1 })`. */
96
+ configureResource: (id, type, config) => this.request(`/projects/${id}/resources/${type}`, {
97
+ method: "PATCH",
98
+ body: { config }
99
+ }),
100
+ /** Run a project-scoped action, such as `email.send`. */
101
+ runAction: (id, name, input, opts = {}) => this.request(
102
+ `/projects/${id}/actions/${name}`,
103
+ { method: "POST", body: { input }, ...opts }
104
+ ),
105
+ /** The production env. Owner keys only: an agent key gets `forbidden` (use `dev`). */
106
+ env: (id) => this.request(`/projects/${id}/env`).then((r) => r.env),
107
+ /** The production env's variable names, without values. Any key. */
108
+ envNames: (id) => this.request(`/projects/${id}/env?names=1`).then((r) => r.names),
109
+ /**
110
+ * The development environment: its own database branch (schema, no
111
+ * production rows), its own sign-in and AI key. What agents and laptops use.
112
+ */
113
+ dev: {
114
+ get: (id) => this.request(`/projects/${id}/dev`),
115
+ start: (id) => this.request(`/projects/${id}/dev`, { method: "POST", body: {} }),
116
+ env: (id) => this.request(`/projects/${id}/dev/env`).then((r) => r.env),
117
+ remove: (id) => this.request(`/projects/${id}/dev`, { method: "DELETE" }),
118
+ /** Start it if needed and wait until ready, then return its env. */
119
+ ensure: async (id, opts = {}) => {
120
+ const deadline = Date.now() + (opts.timeoutMs ?? 5 * 6e4);
121
+ let state = await this.projects.dev.get(id);
122
+ if (state.status === "none" || state.status === "failed") {
123
+ state = await this.projects.dev.start(id);
124
+ }
125
+ while (state.status !== "ready") {
126
+ if (state.status === "failed") {
127
+ const bad = state.resources.find((r) => r.status === "failed");
128
+ throw new VibizApiError(
129
+ 409,
130
+ "dependency_not_ready",
131
+ bad?.error ?? "The development environment failed."
132
+ );
133
+ }
134
+ if (Date.now() > deadline) {
135
+ throw new VibizApiError(
136
+ 408,
137
+ "timeout",
138
+ "Timed out waiting for the development environment."
139
+ );
140
+ }
141
+ await sleep(opts.intervalMs ?? 2e3);
142
+ state = await this.projects.dev.get(id);
143
+ }
144
+ return this.projects.dev.env(id);
145
+ }
146
+ },
147
+ /** The app key (`vz_app_…`), per environment. The secret itself only ever travels in the env. */
148
+ appKeys: {
149
+ list: (id) => this.request(`/projects/${id}/app-keys`).then((r) => r.data),
150
+ /** Production's needs an owner key. The old key works for `grace_seconds` (default 3600). */
151
+ rotate: (id, opts = {}) => this.request(`/projects/${id}/app-keys/rotate`, { method: "POST", body: opts })
152
+ },
153
+ /** Hosts outside Vibiz that receive the env server to server. Connecting needs an owner key. */
154
+ hosts: {
155
+ list: (id) => this.request(`/projects/${id}/hosts`).then((r) => r.data),
156
+ add: (id, input) => this.request(`/projects/${id}/hosts`, { method: "POST", body: input }),
157
+ sync: (id) => this.request(`/projects/${id}/hosts/sync`, {
158
+ method: "POST",
159
+ body: {}
160
+ }).then((r) => r.data),
161
+ remove: (id, targetId, opts = {}) => this.request(
162
+ `/projects/${id}/hosts/${targetId}${opts.keepValues ? "?keep_values=1" : ""}`,
163
+ { method: "DELETE" }
164
+ )
165
+ },
166
+ /**
167
+ * The app's own database. `production` needs an owner key (it holds the
168
+ * app's users' data); agents read `development`, the default for them.
169
+ */
170
+ database: {
171
+ tables: (id, env) => this.request(
172
+ `/projects/${id}/database${env ? `?env=${env}` : ""}`
173
+ ),
174
+ /** Read-only: one SELECT / WITH / EXPLAIN / SHOW statement, 5 s, 500 rows. */
175
+ query: (id, sql, env) => this.request(`/projects/${id}/database/query`, {
176
+ method: "POST",
177
+ body: { sql, ...env ? { env } : {} }
178
+ })
179
+ },
180
+ /**
181
+ * How people sign in to the app (docs/SIGN-IN.md). `production` needs an
182
+ * owner key; agents work on `development`, the default for them.
183
+ */
184
+ auth: {
185
+ get: (id, env) => this.request(`/projects/${id}/auth/settings${env ? `?env=${env}` : ""}`),
186
+ update: (id, patch, env) => this.request(`/projects/${id}/auth/settings${env ? `?env=${env}` : ""}`, {
187
+ method: "PATCH",
188
+ body: patch
189
+ }),
190
+ /** Turn a social provider on, with shared keys or the customer's own OAuth app. */
191
+ setProvider: (id, provider, input, env) => this.request(
192
+ `/projects/${id}/auth/providers/${provider}${env ? `?env=${env}` : ""}`,
193
+ { method: "PUT", body: input }
194
+ ),
195
+ removeProvider: (id, provider, env) => this.request(
196
+ `/projects/${id}/auth/providers/${provider}${env ? `?env=${env}` : ""}`,
197
+ { method: "DELETE" }
198
+ ),
199
+ /** Owner only: a test message through the SMTP sender already set. */
200
+ testEmail: (id, to, env) => this.request(
201
+ `/projects/${id}/auth/test-email${env ? `?env=${env}` : ""}`,
202
+ { method: "POST", body: { to } }
203
+ )
204
+ },
205
+ /**
206
+ * The business behind the project (docs/COMPANY.md). `env` defaults like
207
+ * the API: production for owners, development for agents. Production
208
+ * submit, signing, actions and documents need an owner key.
209
+ */
210
+ company: {
211
+ get: (id, env) => this.request(`/projects/${id}/company${envQuery(env)}`),
212
+ /** Create or replace the draft. */
213
+ draft: (id, draft, env) => this.request(`/projects/${id}/company${envQuery(env)}`, {
214
+ method: "PUT",
215
+ body: draft
216
+ }),
217
+ /** Change part of the draft: top-level keys replace, `null` clears one. */
218
+ update: (id, patch, env) => this.request(`/projects/${id}/company${envQuery(env)}`, {
219
+ method: "PATCH",
220
+ body: patch
221
+ }),
222
+ remove: (id, env) => this.request(
223
+ `/projects/${id}/company${envQuery(env)}`,
224
+ { method: "DELETE" }
225
+ ),
226
+ /**
227
+ * Production files for real and charges the price: an owner key, `confirm: true`,
228
+ * and the `quote.total_cents` and `updated_at` of the draft that was reviewed
229
+ * (a draft or price changed since is refused with a 409, not filed).
230
+ */
231
+ submit: (id, input, env) => this.request(`/projects/${id}/company/submit${envQuery(env)}`, {
232
+ method: "POST",
233
+ body: input
234
+ }),
235
+ /**
236
+ * Pay a live filing by card instead of from the balance (owner keys): the same
237
+ * body as `submit`; it is filed when the payment arrives (docs/BILLING.md "Company").
238
+ */
239
+ checkout: (id, input) => this.request(`/projects/${id}/company/checkout?env=production`, {
240
+ method: "POST",
241
+ body: input
242
+ }),
243
+ /** A fresh signing link; show it to the signer right away, it voids the previous one. */
244
+ sign: (id, document, env) => this.request(
245
+ `/projects/${id}/company/signatures/${document}${envQuery(env)}`,
246
+ { method: "POST", body: {} }
247
+ ),
248
+ /** Answer a required action: new names, or the founder's own US address. */
249
+ resolve: (id, actionId, input, env) => this.request(
250
+ `/projects/${id}/company/actions/${encodeURIComponent(actionId)}${envQuery(env)}`,
251
+ { method: "POST", body: input }
252
+ ),
253
+ /** A short-lived download link. */
254
+ document: (id, documentId, env) => this.request(
255
+ `/projects/${id}/company/documents/${encodeURIComponent(documentId)}${envQuery(env)}`
256
+ ),
257
+ refresh: (id, env) => this.request(`/projects/${id}/company/refresh${envQuery(env)}`, {
258
+ method: "POST",
259
+ body: {}
260
+ }),
261
+ /** Development only: complete formation or the EIN at the sandbox. */
262
+ simulate: (id, step) => this.request(`/projects/${id}/company/simulate?env=development`, {
263
+ method: "POST",
264
+ body: { step }
265
+ })
266
+ },
267
+ /**
268
+ * Social (docs/SOCIAL.md): the project's own social and ad accounts. `env`
269
+ * defaults like the API: production for owners, development (a sandbox:
270
+ * nothing is posted or spent) for agents. On production an agent's posts
271
+ * and ad changes wait for the owner (`pending_approval`); an owner sends
272
+ * `confirm: true`.
273
+ */
274
+ social: {
275
+ get: (id, env) => this.request(`/projects/${id}/social${envQuery(env)}`),
276
+ accounts: (id, opts = {}) => this.request(
277
+ `/projects/${id}/social/accounts${qs({ env: opts.env, kind: opts.kind, platform: opts.platform })}`
278
+ ),
279
+ /** A link (1 h) for the owner to open and sign in to the platform. */
280
+ connect: (id, platform, opts = {}) => this.request(`/projects/${id}/social/connect${envQuery(opts.env)}`, {
281
+ method: "POST",
282
+ body: { platform, ...opts.ads ? { ads: true } : {} }
283
+ }),
284
+ refreshAccounts: (id, env) => this.request(
285
+ `/projects/${id}/social/accounts/refresh${envQuery(env)}`,
286
+ { method: "POST", body: {} }
287
+ ),
288
+ disconnect: (id, accountId, env) => this.request(
289
+ `/projects/${id}/social/accounts/${encodeURIComponent(accountId)}${envQuery(env)}`,
290
+ { method: "DELETE" }
291
+ ),
292
+ accountAnalytics: (id, accountId, opts = {}) => this.request(
293
+ `/projects/${id}/social/accounts/${encodeURIComponent(accountId)}/analytics${qs(opts)}`
294
+ ),
295
+ /** Upload a file (bytes and their content type) → a URL to post with. */
296
+ upload: (id, file, env) => this.request(
297
+ `/projects/${id}/social/media${qs({ env, filename: file.filename })}`,
298
+ { method: "POST", raw: file.bytes, contentType: file.contentType }
299
+ ),
300
+ posts: (id, opts = {}) => this.request(`/projects/${id}/social/posts${qs(opts)}`),
301
+ post: (id, postId, env) => this.request(
302
+ `/projects/${id}/social/posts/${encodeURIComponent(postId)}${envQuery(env)}`
303
+ ),
304
+ create: (id, input, env) => this.request(`/projects/${id}/social/posts${envQuery(env)}`, {
305
+ method: "POST",
306
+ body: input
307
+ }),
308
+ publish: (id, postId, input = {}, env) => this.postAction(id, postId, "publish", input, env),
309
+ approve: (id, postId, input = {}, env) => this.postAction(id, postId, "approve", input, env),
310
+ reject: (id, postId, env) => this.postAction(id, postId, "reject", {}, env),
311
+ retry: (id, postId, input = {}, env) => this.postAction(id, postId, "retry", input, env),
312
+ refresh: (id, postId, env) => this.postAction(id, postId, "refresh", {}, env),
313
+ /** Cancel a draft, a waiting or a scheduled post. */
314
+ cancel: (id, postId, env) => this.request(
315
+ `/projects/${id}/social/posts/${encodeURIComponent(postId)}${envQuery(env)}`,
316
+ { method: "DELETE" }
317
+ ),
318
+ postAnalytics: (id, postId, env) => this.request(
319
+ `/projects/${id}/social/posts/${encodeURIComponent(postId)}/analytics${envQuery(env)}`
320
+ ),
321
+ campaigns: (id, opts = {}) => this.request(
322
+ `/projects/${id}/social/ads/campaigns${qs(opts)}`
323
+ ),
324
+ adChanges: (id, opts = {}) => this.request(
325
+ `/projects/${id}/social/ads/changes${qs(opts)}`
326
+ ),
327
+ adChange: (id, input, env) => this.request(`/projects/${id}/social/ads/changes${envQuery(env)}`, {
328
+ method: "POST",
329
+ body: input
330
+ }),
331
+ approveAdChange: (id, changeId, env) => this.request(
332
+ `/projects/${id}/social/ads/changes/${encodeURIComponent(changeId)}/approve${envQuery(env)}`,
333
+ { method: "POST", body: {} }
334
+ ),
335
+ rejectAdChange: (id, changeId, env) => this.request(
336
+ `/projects/${id}/social/ads/changes/${encodeURIComponent(changeId)}/reject${envQuery(env)}`,
337
+ { method: "POST", body: {} }
338
+ )
339
+ },
340
+ /**
341
+ * Payments (docs/PAYMENTS.md): Vibiz is the merchant, so an app sells from
342
+ * its first call. `env` defaults like the API: production for owners,
343
+ * development for agents. On production, refunds, cancels, withdrawing,
344
+ * payout setup and webhook settings need an owner key.
345
+ */
346
+ payments: {
347
+ get: (id, env) => this.request(`/projects/${id}/payments${envQuery(env)}`),
348
+ update: (id, patch, env) => this.request(`/projects/${id}/payments${envQuery(env)}`, {
349
+ method: "PATCH",
350
+ body: patch
351
+ }),
352
+ /** A new signing secret, shown once (also put in the app's env as VIBIZ_WEBHOOK_SECRET). */
353
+ rotateWebhookSecret: (id, env) => this.request(
354
+ `/projects/${id}/payments/webhook-secret${envQuery(env)}`,
355
+ { method: "POST", body: {} }
356
+ ),
357
+ deliveries: (id, env) => this.request(
358
+ `/projects/${id}/payments/deliveries${envQuery(env)}`
359
+ ),
360
+ products: {
361
+ list: (id, env) => this.request(
362
+ `/projects/${id}/payments/products${envQuery(env)}`
363
+ ),
364
+ create: (id, input, env) => this.request(`/projects/${id}/payments/products${envQuery(env)}`, {
365
+ method: "POST",
366
+ body: input
367
+ }),
368
+ get: (id, productId, env) => this.request(
369
+ `/projects/${id}/payments/products/${encodeURIComponent(productId)}${envQuery(env)}`
370
+ ),
371
+ update: (id, productId, patch, env) => this.request(
372
+ `/projects/${id}/payments/products/${encodeURIComponent(productId)}${envQuery(env)}`,
373
+ { method: "PATCH", body: patch }
374
+ ),
375
+ /** Archive: kept on record, no longer sold. */
376
+ remove: (id, productId, env) => this.request(
377
+ `/projects/${id}/payments/products/${encodeURIComponent(productId)}${envQuery(env)}`,
378
+ { method: "DELETE" }
379
+ ),
380
+ addPrice: (id, productId, price, env) => this.request(
381
+ `/projects/${id}/payments/products/${encodeURIComponent(productId)}/prices${envQuery(env)}`,
382
+ { method: "POST", body: price }
383
+ ),
384
+ updatePrice: (id, priceId, patch, env) => this.request(
385
+ `/projects/${id}/payments/prices/${encodeURIComponent(priceId)}${envQuery(env)}`,
386
+ { method: "PATCH", body: patch }
387
+ )
388
+ },
389
+ checkout: (id, input, env) => this.request(`/projects/${id}/payments/checkout${envQuery(env)}`, {
390
+ method: "POST",
391
+ body: input
392
+ }),
393
+ list: (id, opts = {}) => this.request(
394
+ `/projects/${id}/payments/payments${paymentsQuery(opts)}`
395
+ ),
396
+ payment: (id, paymentId, env) => this.request(
397
+ `/projects/${id}/payments/payments/${encodeURIComponent(paymentId)}${envQuery(env)}`
398
+ ),
399
+ /** The balance (available + pending) must cover it: `409 insufficient_balance` otherwise. */
400
+ refund: (id, paymentId, input = {}, env) => this.request(
401
+ `/projects/${id}/payments/payments/${encodeURIComponent(paymentId)}/refund${envQuery(env)}`,
402
+ { method: "POST", body: input }
403
+ ),
404
+ subscriptions: (id, opts = {}) => this.request(
405
+ `/projects/${id}/payments/subscriptions${paymentsQuery(opts)}`
406
+ ),
407
+ cancel: (id, subscriptionId, opts = {}, env) => this.request(
408
+ `/projects/${id}/payments/subscriptions/${encodeURIComponent(subscriptionId)}/cancel${envQuery(env)}`,
409
+ { method: "POST", body: opts }
410
+ ),
411
+ ledger: (id, opts = {}) => this.request(
412
+ `/projects/${id}/payments/ledger${paymentsQuery(opts)}`
413
+ ),
414
+ /** For the embedded verification, bank and payout UI (Connect.js). Short-lived. */
415
+ payoutsSession: (id, input = {}, env) => this.request(`/projects/${id}/payments/payouts/session${envQuery(env)}`, {
416
+ method: "POST",
417
+ body: input
418
+ }),
419
+ /** Default: everything available. */
420
+ withdraw: (id, input = {}, env) => this.request(`/projects/${id}/payments/withdraw${envQuery(env)}`, {
421
+ method: "POST",
422
+ body: input
423
+ }),
424
+ withdrawals: (id, env) => this.request(
425
+ `/projects/${id}/payments/withdrawals${envQuery(env)}`
426
+ ),
427
+ refresh: (id, env) => this.request(`/projects/${id}/payments/refresh${envQuery(env)}`, {
428
+ method: "POST",
429
+ body: {}
430
+ }),
431
+ /** Development only: a real test-mode payment, dispute or refund, or release the hold. */
432
+ simulate: (id, input) => this.request(
433
+ `/projects/${id}/payments/simulate?env=development`,
434
+ { method: "POST", body: input }
435
+ )
436
+ },
437
+ /**
438
+ * Custom connectors (docs/CONNECTORS.md): any HTTP API from one manifest,
439
+ * called through Vibiz, which holds the key. `env` defaults like the API:
440
+ * production for owners, development for agents. Production keys, and
441
+ * approving an agent's dangerous call, need an owner key.
442
+ */
443
+ connectors: {
444
+ list: (id) => this.request(`/projects/${id}/connectors`).then((r) => r.data),
445
+ get: (id, name) => this.request(`/projects/${id}/connectors/${encodeURIComponent(name)}`),
446
+ /** From a manifest, a short form (`operations: ["GET /posts/{id}"]`) or an import's draft. */
447
+ create: (id, manifest) => this.request(`/projects/${id}/connectors`, { method: "POST", body: manifest }),
448
+ /** Preview an OpenAPI 3 spec (url or the document): nothing is saved. */
449
+ import: (id, input) => this.request(`/projects/${id}/connectors/import`, {
450
+ method: "POST",
451
+ body: input
452
+ }),
453
+ update: (id, name, patch) => this.request(`/projects/${id}/connectors/${encodeURIComponent(name)}`, {
454
+ method: "PATCH",
455
+ body: patch
456
+ }),
457
+ delete: (id, name) => this.request(`/projects/${id}/connectors/${encodeURIComponent(name)}`, {
458
+ method: "DELETE"
459
+ }),
460
+ /** Set values (`null` removes). Production: owner keys. Values are never read back. */
461
+ setSecrets: (id, name, values, env) => this.request(
462
+ `/projects/${id}/connectors/${encodeURIComponent(name)}/secrets${envQuery(env)}`,
463
+ { method: "PUT", body: { values } }
464
+ ),
465
+ check: (id, name, env) => this.request(
466
+ `/projects/${id}/connectors/${encodeURIComponent(name)}/check${envQuery(env)}`,
467
+ { method: "POST", body: {} }
468
+ ),
469
+ /** One operation. A dangerous one (or a production write) by an agent key answers 403 approval_required. */
470
+ call: (id, name, input, opts = {}) => this.request(
471
+ `/projects/${id}/connectors/${encodeURIComponent(name)}/call${envQuery(opts.env)}`,
472
+ {
473
+ method: "POST",
474
+ body: input,
475
+ ...opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}
476
+ }
477
+ ),
478
+ calls: (id, name, opts = {}) => this.request(
479
+ `/projects/${id}/connectors/${encodeURIComponent(name)}/calls${paymentsQuery(opts)}`
480
+ ),
481
+ approvals: (id, opts = {}) => this.request(
482
+ `/projects/${id}/connectors/approvals${paymentsQuery(opts)}`
483
+ ).then((r) => r.data),
484
+ approval: (id, approvalId) => this.request(
485
+ `/projects/${id}/connectors/approvals/${encodeURIComponent(approvalId)}`
486
+ ),
487
+ /** Owner keys. */
488
+ approve: (id, approvalId) => this.request(
489
+ `/projects/${id}/connectors/approvals/${encodeURIComponent(approvalId)}/approve`,
490
+ { method: "POST", body: {} }
491
+ ),
492
+ /** Owner keys. */
493
+ deny: (id, approvalId) => this.request(
494
+ `/projects/${id}/connectors/approvals/${encodeURIComponent(approvalId)}/deny`,
495
+ { method: "POST", body: {} }
496
+ )
497
+ },
498
+ /** Where the app also runs outside Vibiz hosting. Adding and removing need an owner key. */
499
+ domains: {
500
+ list: (id) => this.request(`/projects/${id}/domains`).then((r) => r.domains),
501
+ add: (id, domain) => this.request(`/projects/${id}/domains`, {
502
+ method: "POST",
503
+ body: { domain }
504
+ }).then((r) => r.domains),
505
+ remove: (id, domain) => this.request(
506
+ `/projects/${id}/domains/${encodeURIComponent(domain)}`,
507
+ { method: "DELETE" }
508
+ ).then((r) => r.domains)
509
+ },
510
+ /** Poll until every resource is ready, or throw on the first failure. */
511
+ waitUntilReady: async (id, opts = {}) => {
512
+ const deadline = Date.now() + (opts.timeoutMs ?? 10 * 6e4);
513
+ for (; ; ) {
514
+ const p = await this.projects.get(id);
515
+ const failed = p.resources.find((r) => r.status === "failed");
516
+ if (failed) {
517
+ throw new VibizApiError(
518
+ 409,
519
+ failed.error?.code ?? "resource_failed",
520
+ failed.error?.message ?? `${failed.type} failed`
521
+ );
522
+ }
523
+ if (p.resources.every((r) => r.status === "ready")) return p;
524
+ if (Date.now() > deadline)
525
+ throw new VibizApiError(408, "timeout", "Timed out waiting for the project.");
526
+ await sleep(opts.intervalMs ?? 2e3);
527
+ }
528
+ }
529
+ };
530
+ files = {
531
+ missing: (shas) => this.request("/files/missing", {
532
+ method: "POST",
533
+ body: { shas }
534
+ }).then((r) => r.missing),
535
+ upload: (sha, bytes) => this.request(`/files/${sha}`, { method: "PUT", raw: bytes })
536
+ };
537
+ deployments = {
538
+ create: (projectId, input, opts = {}) => this.request(
539
+ `/projects/${projectId}/deployments`,
540
+ {
541
+ method: "POST",
542
+ body: input,
543
+ ...opts
544
+ }
545
+ ),
546
+ list: (projectId) => this.request(`/projects/${projectId}/deployments`).then(
547
+ (r) => r.data
548
+ ),
549
+ get: (id) => this.request(`/deployments/${id}`),
550
+ promote: (id) => this.request(
551
+ `/deployments/${id}/promote`,
552
+ { method: "POST" }
553
+ ),
554
+ waitUntilDone: async (id, opts = {}) => {
555
+ const deadline = Date.now() + (opts.timeoutMs ?? 20 * 6e4);
556
+ for (; ; ) {
557
+ const d = await this.deployments.get(id);
558
+ if (d.status === "ready" || d.status === "failed" || d.status === "canceled") return d;
559
+ if (Date.now() > deadline)
560
+ throw new VibizApiError(408, "timeout", "Timed out waiting for the deployment.");
561
+ await sleep(opts.intervalMs ?? 3e3);
562
+ }
563
+ }
564
+ };
565
+ analytics = {
566
+ /** Visitors, pageviews, top pages, sources, countries, events and a timeseries. */
567
+ get: (projectId, opts = {}) => this.request(
568
+ `/projects/${projectId}/analytics?period=${opts.period ?? "7d"}`
569
+ )
570
+ };
571
+ /** Session replay. Start from `insights`; open single sessions with `get`. */
572
+ replays = {
573
+ /** Rule-based findings over recorded sessions: what broke and where people got stuck. */
574
+ insights: (projectId, opts = {}) => this.request(
575
+ `/projects/${projectId}/replays/insights?period=${opts.period ?? "7d"}`
576
+ ),
577
+ /** Recorded sessions, most worth watching first. */
578
+ list: (projectId, opts = {}) => {
579
+ const q = new URLSearchParams();
580
+ for (const [k, v] of Object.entries(opts))
581
+ if (v !== void 0 && v !== false) q.set(k, String(v));
582
+ const qs2 = q.toString();
583
+ return this.request(`/projects/${projectId}/replays${qs2 ? `?${qs2}` : ""}`);
584
+ },
585
+ /** Metadata, signals, completeness and the timeline of notable moments. */
586
+ get: (id) => this.request(`/replays/${id}`),
587
+ /** The raw rrweb events (masked at the source). Pages with `from_seq` / `max_chunks`. */
588
+ events: (id, opts = {}) => {
589
+ const q = new URLSearchParams();
590
+ if (opts.time) q.set("time", opts.time);
591
+ if (opts.from_seq !== void 0) q.set("from_seq", String(opts.from_seq));
592
+ if (opts.max_chunks !== void 0) q.set("max_chunks", String(opts.max_chunks));
593
+ const qs2 = q.toString();
594
+ return this.request(
595
+ `/replays/${id}/events${qs2 ? `?${qs2}` : ""}`
596
+ );
597
+ },
598
+ /** A link to the hosted player for a person, valid for `expires_in` seconds (default 1 h, max 24 h). */
599
+ share: (id, opts = {}) => this.request(`/replays/${id}/share`, {
600
+ method: "POST",
601
+ body: opts
602
+ }),
603
+ delete: (id) => this.request(`/replays/${id}`, { method: "DELETE" })
604
+ };
605
+ operations = {
606
+ get: (id) => this.request(`/operations/${id}`)
607
+ };
608
+ actions = {
609
+ run: (name, input) => this.request(`/actions/${name}`, {
610
+ method: "POST",
611
+ body: { input }
612
+ })
613
+ };
614
+ email = {
615
+ /** Send from the project's address (`info@<slug>.vibiz.app` by default). */
616
+ send: (projectId, input, opts = {}) => this.projects.runAction(projectId, "email.send", input, opts)
617
+ };
618
+ /**
619
+ * The AI gateway: chat, images, speech and transcription, metered against
620
+ * the account's (or the project's) spend. Works with the account key or a
621
+ * project AI key (`VIBIZ_AI_API_KEY`). OpenAI's SDK works too: point it at
622
+ * `<api>/v1/ai`.
623
+ */
624
+ ai = {
625
+ /** One completion. `model` defaults to `vibiz/fast`. */
626
+ chat: (input, opts = {}) => this.request("/ai/chat/completions", {
627
+ method: "POST",
628
+ body: { ...input, stream: false },
629
+ ...opts
630
+ }),
631
+ /** A streamed completion, chunk by chunk. Breaking out of the loop stops the model. */
632
+ chatStream: (input) => {
633
+ const open = () => this.stream("/ai/chat/completions", { method: "POST", body: { ...input, stream: true } });
634
+ return {
635
+ async *[Symbol.asyncIterator]() {
636
+ const res = await open();
637
+ const reader = res.body.getReader();
638
+ const decoder = new TextDecoder();
639
+ let buffer = "";
640
+ try {
641
+ for (; ; ) {
642
+ const { done, value } = await reader.read();
643
+ if (done) return;
644
+ buffer += decoder.decode(value, { stream: true });
645
+ for (let end = buffer.indexOf("\n\n"); end >= 0; end = buffer.indexOf("\n\n")) {
646
+ const block = buffer.slice(0, end);
647
+ buffer = buffer.slice(end + 2);
648
+ const data = block.startsWith("data: ") ? block.slice(6) : "";
649
+ if (!data) continue;
650
+ if (data === "[DONE]") return;
651
+ const parsed = JSON.parse(data);
652
+ if (parsed.error) {
653
+ throw new VibizApiError(
654
+ 502,
655
+ parsed.error.code,
656
+ parsed.error.message,
657
+ parsed.error.details
658
+ );
659
+ }
660
+ yield parsed;
661
+ }
662
+ }
663
+ } finally {
664
+ await reader.cancel().catch(() => {
665
+ });
666
+ }
667
+ }
668
+ };
669
+ },
670
+ embed: (input) => this.request(
671
+ "/ai/embeddings",
672
+ { method: "POST", body: input }
673
+ ),
674
+ image: (input, opts = {}) => this.request("/ai/images/generations", { method: "POST", body: input, ...opts }),
675
+ /** Text to speech: the audio bytes (mp3 by default). */
676
+ speech: async (input) => {
677
+ const res = await this.stream("/ai/audio/speech", { method: "POST", body: input });
678
+ return new Uint8Array(await res.arrayBuffer());
679
+ },
680
+ /** Speech to text. */
681
+ transcribe: (input) => {
682
+ const form = new FormData();
683
+ const blob = input.file instanceof Blob ? input.file : new Blob([input.file]);
684
+ form.set("file", blob, input.filename ?? "audio.mp3");
685
+ if (input.model) form.set("model", input.model);
686
+ if (input.language) form.set("language", input.language);
687
+ return this.request("/ai/audio/transcriptions", { method: "POST", form });
688
+ },
689
+ /**
690
+ * The Vibiz models and the lab models, with prices. `all` adds every
691
+ * language model you can call by id; `q` searches id, name and maker.
692
+ */
693
+ models: (opts = {}) => {
694
+ const qs2 = new URLSearchParams();
695
+ if (opts.all) qs2.set("all", "1");
696
+ if (opts.q) qs2.set("q", opts.q);
697
+ const tail = qs2.size ? `?${qs2}` : "";
698
+ return this.request(`/ai/models${tail}`).then((r) => r.data);
699
+ },
700
+ jobs: {
701
+ create: (input, opts = {}) => this.request("/ai/jobs", { method: "POST", body: input, ...opts }),
702
+ get: (id) => this.request(`/ai/jobs/${id}`),
703
+ cancel: (id) => this.request(`/ai/jobs/${id}/cancel`, { method: "POST" }),
704
+ wait: async (id, opts = {}) => {
705
+ const deadline = Date.now() + (opts.timeoutMs ?? 20 * 6e4);
706
+ for (; ; ) {
707
+ const job = await this.ai.jobs.get(id);
708
+ if (job.status !== "queued" && job.status !== "running") return job;
709
+ if (Date.now() > deadline)
710
+ throw new VibizApiError(408, "timeout", "Timed out waiting for the job.");
711
+ await sleep(opts.intervalMs ?? 2e3);
712
+ }
713
+ }
714
+ }
715
+ };
716
+ /** A project's AI key and settings (the `ai` resource). Account key only. */
717
+ projectAi = {
718
+ get: (projectId) => this.request(`/projects/${projectId}/ai`),
719
+ update: (projectId, patch) => this.request(`/projects/${projectId}/ai`, { method: "PATCH", body: patch }),
720
+ /** A new key; the old one keeps working for `graceSeconds` (default 3600). */
721
+ rotateKey: (projectId, opts = {}) => this.request(`/projects/${projectId}/ai/keys/rotate`, {
722
+ method: "POST",
723
+ body: opts.graceSeconds === void 0 ? {} : { grace_seconds: opts.graceSeconds }
724
+ }),
725
+ revokeKey: (projectId) => this.request(`/projects/${projectId}/ai/keys/revoke`, { method: "POST" })
726
+ };
727
+ /**
728
+ * Your own AI provider keys (docs/AI.md "Your own keys"): calls they can
729
+ * serve run only on them (never falling back to Vibiz) and cost 0 here.
730
+ * Pass `projectId` for a project's override. Setting and removing need an
731
+ * owner key. Secrets are write-only: no method returns one.
732
+ */
733
+ aiKeys = {
734
+ list: (opts = {}) => this.request(`${aiKeysBase(opts.projectId)}`),
735
+ get: (provider, opts = {}) => this.request(`${aiKeysBase(opts.projectId)}/${provider}`),
736
+ /** Checked with a cheap call to the provider first; refused with the reason if it fails. */
737
+ set: (provider, input, opts = {}) => this.request(`${aiKeysBase(opts.projectId)}/${provider}`, {
738
+ method: "PUT",
739
+ body: input
740
+ }),
741
+ /** Which capabilities the key serves; the rest runs on Vibiz. */
742
+ update: (provider, patch, opts = {}) => this.request(`${aiKeysBase(opts.projectId)}/${provider}`, {
743
+ method: "PATCH",
744
+ body: patch
745
+ }),
746
+ test: (provider, opts = {}) => this.request(`${aiKeysBase(opts.projectId)}/${provider}/test`, {
747
+ method: "POST",
748
+ body: {}
749
+ }),
750
+ remove: (provider, opts = {}) => this.request(`${aiKeysBase(opts.projectId)}/${provider}`, {
751
+ method: "DELETE"
752
+ })
753
+ };
754
+ /** What the account used, what it cost, and how close it is to its limits. */
755
+ usage = {
756
+ get: (opts = {}) => this.request(`/usage${usageQuery(opts)}`),
757
+ project: (projectId, opts = {}) => this.request(`/projects/${projectId}/usage${usageQuery(opts)}`)
758
+ };
759
+ /**
760
+ * The account's own credits (docs/BILLING.md): the balance, the card,
761
+ * auto-recharge and history. A top-up is a hosted page a person pays on.
762
+ */
763
+ billing = {
764
+ get: () => this.request("/billing"),
765
+ /** Owner keys: auto-recharge and the daily safety cap. */
766
+ update: (input) => this.request("/billing", { method: "PATCH", body: input }),
767
+ /** A payment page for `amount_cents` (500–100000); hand its `url` to a person. */
768
+ checkout: (input) => this.request("/billing/checkout", { method: "POST", body: input }),
769
+ checkoutStatus: (id) => this.request(`/billing/checkout/${encodeURIComponent(id)}`),
770
+ /** Owner keys: a page to save or replace the card. */
771
+ saveCard: () => this.request("/billing/card", { method: "POST", body: {} }),
772
+ removeCard: () => this.request("/billing/card", { method: "DELETE" }),
773
+ ledger: (opts = {}) => this.request(
774
+ `/billing/ledger${qs({ kind: opts.kind?.join(","), cursor: opts.cursor, limit: opts.limit })}`
775
+ ),
776
+ charges: (opts = {}) => this.request(`/billing/charges${qs(opts)}`)
777
+ };
778
+ /** Request counts, error rate and latency by capability. */
779
+ metrics = {
780
+ get: (opts = {}) => this.request(`/metrics?period=${opts.period ?? "30d"}`)
781
+ };
782
+ /** Named keys, one per teammate or agent, so spend shows per person. */
783
+ apiKeys = {
784
+ list: () => this.request("/api-keys").then((r) => r.data),
785
+ /** The secret is returned once, here. */
786
+ /** `agent` by default: builds and runs, never reads production secrets. Needs an owner key. */
787
+ create: (name, opts = {}) => this.request("/api-keys", {
788
+ method: "POST",
789
+ body: { name, ...opts.access ? { access: opts.access } : {} }
790
+ }),
791
+ revoke: (id) => this.request(`/api-keys/${id}`, { method: "DELETE" })
792
+ };
793
+ /** A short-lived link to the hosted usage dashboard (admin scope needs an admin key). */
794
+ dashboard = {
795
+ link: (opts = {}) => this.request("/dashboard/link", { method: "POST", body: opts })
796
+ };
797
+ /** The platform view. Admin keys only. */
798
+ admin = {
799
+ metrics: (opts = {}) => this.request(`/admin/metrics?period=${opts.period ?? "30d"}`),
800
+ usage: (opts = {}) => this.request(`/admin/usage${usageQuery(opts)}`),
801
+ tenantUsage: (tenantId, opts = {}) => this.request(`/admin/tenants/${tenantId}/usage${usageQuery(opts)}`)
802
+ };
803
+ /** Company formation: what can be formed where, fees, industries, the disclaimer. */
804
+ company = {
805
+ catalog: (env) => this.request(`/company/catalog${envQuery(env)}`)
806
+ };
807
+ /** The hosts Vibiz can connect to, and the fields each asks for. */
808
+ hostKinds() {
809
+ return this.request("/hosts").then((r) => r.data);
810
+ }
811
+ capabilities() {
812
+ return this.request(
813
+ "/capabilities"
814
+ );
815
+ }
816
+ /** The account and key this client acts as. */
817
+ whoami() {
818
+ return this.request("/whoami");
819
+ }
820
+ /** Revoke the key this client uses (`vibiz logout` for a key made by a browser login). */
821
+ logout() {
822
+ return this.request("/cli/logout", {
823
+ method: "POST"
824
+ });
825
+ }
826
+ };
827
+ var envQuery = (env) => env ? `?env=${env}` : "";
828
+ var qs = (q) => {
829
+ const p = new URLSearchParams();
830
+ for (const [k, v] of Object.entries(q))
831
+ if (v !== void 0 && v !== null && v !== "") p.set(k, String(v));
832
+ const s = p.toString();
833
+ return s ? `?${s}` : "";
834
+ };
835
+ function paymentsQuery(opts) {
836
+ const q = new URLSearchParams();
837
+ for (const [k, v] of Object.entries(opts)) if (v !== void 0 && v !== "") q.set(k, String(v));
838
+ const qs2 = q.toString();
839
+ return qs2 ? `?${qs2}` : "";
840
+ }
841
+ function usageQuery(opts) {
842
+ const q = new URLSearchParams();
843
+ for (const [k, v] of Object.entries(opts)) {
844
+ if (v === void 0 || v === null || v === "") continue;
845
+ q.set(k, Array.isArray(v) ? v.join(",") : String(v));
846
+ }
847
+ const qs2 = q.toString();
848
+ return qs2 ? `?${qs2}` : "";
849
+ }
850
+ function endUserHeader(user) {
851
+ if (/^[\x20-\x7e]*$/.test(user) && !user.startsWith("b64u:")) return user;
852
+ const bytes = new TextEncoder().encode(user);
853
+ let bin = "";
854
+ for (const b of bytes) bin += String.fromCharCode(b);
855
+ return `b64u:${btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
856
+ }
857
+ var aiKeysBase = (projectId) => projectId ? `/projects/${encodeURIComponent(projectId)}/ai-keys` : "/ai-keys";
858
+
859
+ // packages/sdk/src/deploy.ts
860
+ import { createHash } from "node:crypto";
861
+ import { readdir, readFile } from "node:fs/promises";
862
+ import { homedir } from "node:os";
863
+ import { join, parse, relative, resolve, sep } from "node:path";
864
+ var IGNORED = /* @__PURE__ */ new Set([
865
+ "vibiz.json",
866
+ "node_modules",
867
+ ".git",
868
+ ".next",
869
+ ".vercel",
870
+ ".turbo",
871
+ ".vibiz",
872
+ "dist",
873
+ ".DS_Store"
874
+ ]);
875
+ var isEnvFile = (name) => name === ".env" || name.startsWith(".env.");
876
+ var isSecretFile = (name) => /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/.test(name) || /\.(pem|key|p12|pfx|keystore|jks)$/i.test(name) || [".npmrc", ".pypirc", ".netrc", ".git-credentials", "credentials.json", ".htpasswd"].includes(
877
+ name
878
+ );
879
+ var PRIVATE_DIRS = [".ssh", ".aws", ".gnupg", ".kube", ".docker", ".config", "Library"];
880
+ async function assertDeployableRoot(root) {
881
+ const abs = resolve(root);
882
+ const home = homedir();
883
+ if (abs === parse(abs).root || abs === home || home.startsWith(`${abs}${sep}`)) {
884
+ throw new Error(`Refusing to deploy ${abs}: point at the app's own folder.`);
885
+ }
886
+ const names = new Set(await readdir(abs));
887
+ const found = PRIVATE_DIRS.filter((d) => names.has(d));
888
+ if (found.length) {
889
+ throw new Error(
890
+ `Refusing to deploy ${abs}: it contains ${found.join(", ")}, which looks like a home or config folder.`
891
+ );
892
+ }
893
+ }
894
+ async function collectFiles(root) {
895
+ const out = [];
896
+ async function walk(dir) {
897
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
898
+ if (IGNORED.has(entry.name) || isEnvFile(entry.name) || isSecretFile(entry.name)) continue;
899
+ const absolute = join(dir, entry.name);
900
+ if (entry.isDirectory()) await walk(absolute);
901
+ else if (entry.isFile()) {
902
+ const bytes = await readFile(absolute);
903
+ out.push({
904
+ absolute,
905
+ path: relative(root, absolute).split(sep).join("/"),
906
+ sha: createHash("sha1").update(bytes).digest("hex"),
907
+ size: bytes.byteLength
908
+ });
909
+ }
910
+ }
911
+ }
912
+ await walk(root);
913
+ return out.sort((a, b) => a.path.localeCompare(b.path));
914
+ }
915
+ async function deployDirectory(vibiz, projectId, root, opts = {}) {
916
+ await assertDeployableRoot(root);
917
+ const files = await collectFiles(root);
918
+ if (files.length === 0) throw new Error(`No files to deploy in ${root}`);
919
+ const missing = new Set(await vibiz.files.missing(files.map((f) => f.sha)));
920
+ const toUpload = files.filter((f) => missing.has(f.sha));
921
+ opts.onProgress?.(`${files.length} files, ${toUpload.length} to upload`);
922
+ const uploadedShas = /* @__PURE__ */ new Set();
923
+ for (const f of toUpload) {
924
+ if (uploadedShas.has(f.sha)) continue;
925
+ await vibiz.files.upload(f.sha, await readFile(f.absolute));
926
+ uploadedShas.add(f.sha);
927
+ }
928
+ const { deployment } = await vibiz.deployments.create(projectId, {
929
+ files: files.map(({ path, sha, size }) => ({ path, sha, size })),
930
+ target: opts.target ?? "production"
931
+ });
932
+ return { deployment, uploaded: uploadedShas.size, total: files.length };
933
+ }
934
+
935
+ // packages/sdk/src/env.ts
936
+ import { chmod, readFile as readFile2, writeFile } from "node:fs/promises";
937
+ function dotenvValue(value) {
938
+ if (!/[\r\n]/.test(value)) {
939
+ if (!/["\\]/.test(value)) return `"${value}"`;
940
+ for (const q of ["'", "`"]) if (!value.includes(q)) return `${q}${value}${q}`;
941
+ } else if (!value.includes('"') && !value.includes("\\n")) {
942
+ return `"${value.replace(/\r?\n/g, "\\n")}"`;
943
+ }
944
+ throw new Error("This value has every kind of quote and cannot be written to a .env file.");
945
+ }
946
+ function mergeEnvFile(existing, env) {
947
+ const pending = new Map(Object.entries(env));
948
+ const lines = existing === "" ? [] : existing.replace(/\n$/, "").split("\n");
949
+ const out = lines.map((line) => {
950
+ const key = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
951
+ if (!key || !pending.has(key)) return line;
952
+ const value = pending.get(key);
953
+ pending.delete(key);
954
+ return `${key}=${dotenvValue(value)}`;
955
+ });
956
+ for (const [k, v] of pending) out.push(`${k}=${dotenvValue(v)}`);
957
+ return `${out.join("\n")}
958
+ `;
959
+ }
960
+ async function writeEnvFile(path, env) {
961
+ const existing = await readFile2(path, "utf8").catch(() => "");
962
+ await writeFile(path, mergeEnvFile(existing, env), { mode: 384 });
963
+ await chmod(path, 384);
964
+ }
965
+
966
+ // packages/sdk/src/format.ts
967
+ function formatUsd(usd) {
968
+ if (usd === 0) return "$0.00";
969
+ const sign = usd < 0 ? "-" : "";
970
+ const a = Math.abs(usd);
971
+ if (a < 1e-6) return `${sign}<$0.000001`;
972
+ if (a < 0.01) return `${sign}$${String(Number(a.toPrecision(2)))}`;
973
+ return `${sign}$${a.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
974
+ }
975
+ var compact = (n) => n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e4 ? `${(n / 1e3).toFixed(1)}k` : `${Math.round(n * 100) / 100}`;
976
+ var SHORT = {
977
+ image: ["img", "img"],
978
+ video: ["video", "videos"],
979
+ character: ["char", "chars"],
980
+ audio_second: ["s audio", "s audio"],
981
+ call: ["call", "calls"],
982
+ email: ["email", "emails"],
983
+ build: ["build", "builds"],
984
+ deploy: ["deploy", "deploys"],
985
+ stack_day: ["stack-day", "stack-days"],
986
+ event: ["event", "events"],
987
+ session: ["session", "sessions"],
988
+ gb_day: ["GB-day", "GB-days"]
989
+ };
990
+ function formatUnits(units) {
991
+ const parts = [];
992
+ const tin = units.input_tokens ?? 0;
993
+ const tout = units.output_tokens ?? 0;
994
+ if (tin || tout) {
995
+ parts.push(tout ? `${compact(tin)} in \xB7 ${compact(tout)} out tok` : `${compact(tin)} tok`);
996
+ }
997
+ for (const [u, n] of Object.entries(units)) {
998
+ if (!n || u === "input_tokens" || u === "output_tokens") continue;
999
+ const [one, many] = SHORT[u] ?? [u.replace(/_/g, " "), u.replace(/_/g, " ")];
1000
+ parts.push(`${compact(n)} ${n === 1 ? one : many}`);
1001
+ }
1002
+ return parts.join(" \xB7 ") || "\u2013";
1003
+ }
1004
+ export {
1005
+ Vibiz,
1006
+ VibizApiError,
1007
+ assertDeployableRoot,
1008
+ collectFiles,
1009
+ deployDirectory,
1010
+ endUserHeader,
1011
+ formatUnits,
1012
+ formatUsd,
1013
+ mergeEnvFile,
1014
+ writeEnvFile
1015
+ };