@nodaro/sdk 1.1.1

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,1977 @@
1
+ import { buildPersonHints } from '@nodaro/prompts';
2
+ export { PEOPLE, PERSON_DIMENSION_LABELS, PERSON_DIMENSION_ORDER, buildPersonHints } from '@nodaro/prompts';
3
+ export { CHARACTER_ASPECT_DEFAULTS, CHARACTER_ASPECT_OPTIONS, CHARACTER_STYLES, OBJECT_ASPECT_DEFAULTS as CREATURE_ASPECT_DEFAULTS, OBJECT_ASPECT_OPTIONS as CREATURE_ASPECT_OPTIONS, CREATURE_ATTACH_COLUMNS, LOCATION_ASSET_TYPES, LOCATION_ATTACH_COLUMNS, OBJECT_ASPECT_DEFAULTS, OBJECT_ASPECT_OPTIONS, OBJECT_ASSET_TYPES, OBJECT_ATTACH_COLUMNS, SURROUND_DIRECTIONS } from '@nodaro/shared';
4
+
5
+ // src/errors.ts
6
+ var NodaroError = class extends Error {
7
+ constructor(message, code, status) {
8
+ super(message);
9
+ this.code = code;
10
+ this.status = status;
11
+ this.name = "NodaroError";
12
+ }
13
+ code;
14
+ status;
15
+ };
16
+ var UnauthorizedError = class extends NodaroError {
17
+ constructor(message = "Authentication required") {
18
+ super(message, "unauthorized", 401);
19
+ this.name = "UnauthorizedError";
20
+ }
21
+ };
22
+ var ForbiddenError = class extends NodaroError {
23
+ constructor(message = "Forbidden", missingScope) {
24
+ super(message, "forbidden", 403);
25
+ this.missingScope = missingScope;
26
+ this.name = "ForbiddenError";
27
+ }
28
+ missingScope;
29
+ };
30
+ var NotFoundError = class extends NodaroError {
31
+ constructor(message = "Not found") {
32
+ super(message, "not_found", 404);
33
+ this.name = "NotFoundError";
34
+ }
35
+ };
36
+ var RateLimitedError = class extends NodaroError {
37
+ constructor(message = "Rate limited") {
38
+ super(message, "rate_limited", 429);
39
+ this.name = "RateLimitedError";
40
+ }
41
+ };
42
+ var InsufficientCreditsError = class extends NodaroError {
43
+ constructor(message = "Insufficient credits", required, available) {
44
+ super(message, "insufficient_credits", 402);
45
+ this.required = required;
46
+ this.available = available;
47
+ this.name = "InsufficientCreditsError";
48
+ }
49
+ required;
50
+ available;
51
+ };
52
+ var StorageExceededError = class extends NodaroError {
53
+ constructor(message = "Storage exceeded", limitBytes) {
54
+ super(message, "storage_exceeded", 413);
55
+ this.limitBytes = limitBytes;
56
+ this.name = "StorageExceededError";
57
+ }
58
+ limitBytes;
59
+ };
60
+ var JobFailedError = class extends NodaroError {
61
+ constructor(message, jobId, jobStatus = "failed") {
62
+ super(message, "job_failed", 0);
63
+ this.jobId = jobId;
64
+ this.jobStatus = jobStatus;
65
+ this.name = "JobFailedError";
66
+ }
67
+ jobId;
68
+ jobStatus;
69
+ };
70
+ var JobTimeoutError = class extends NodaroError {
71
+ constructor(message, jobId, timeoutMs) {
72
+ super(message, "job_timeout", 0);
73
+ this.jobId = jobId;
74
+ this.timeoutMs = timeoutMs;
75
+ this.name = "JobTimeoutError";
76
+ }
77
+ jobId;
78
+ timeoutMs;
79
+ };
80
+ var JobAbortedError = class extends NodaroError {
81
+ constructor(message = "Aborted", jobId) {
82
+ super(message, "job_aborted", 0);
83
+ this.jobId = jobId;
84
+ this.name = "JobAbortedError";
85
+ }
86
+ jobId;
87
+ };
88
+ function throwFromResponse(status, body) {
89
+ const code = body.error?.code ?? "internal_error";
90
+ const message = body.error?.message ?? "Request failed";
91
+ if (status === 401) throw new UnauthorizedError(message);
92
+ if (status === 403 && code === "insufficient_scope") {
93
+ throw new ForbiddenError(message, body.error?.missingScope);
94
+ }
95
+ if (status === 403) throw new ForbiddenError(message);
96
+ if (status === 404) throw new NotFoundError(message);
97
+ if (status === 429) throw new RateLimitedError(message);
98
+ if (status === 402) {
99
+ throw new InsufficientCreditsError(message, body.error?.required, body.error?.available);
100
+ }
101
+ if (status === 413) throw new StorageExceededError(message, body.error?.limitBytes);
102
+ throw new NodaroError(message, code, status);
103
+ }
104
+
105
+ // src/resources/workflows.ts
106
+ var WorkflowsResource = class {
107
+ constructor(client) {
108
+ this.client = client;
109
+ }
110
+ client;
111
+ /** List workflows for a project. Returns metadata only — `nodes`/`edges` are not included. */
112
+ list(params) {
113
+ return this.client.request(
114
+ "GET",
115
+ `/v1/projects/${encodeURIComponent(params.projectId)}/workflows`
116
+ );
117
+ }
118
+ /** Get a workflow including its full nodes/edges/settings. */
119
+ get(id) {
120
+ return this.client.request("GET", `/v1/workflows/${encodeURIComponent(id)}`);
121
+ }
122
+ /**
123
+ * Get a PUBLICLY-SHARED workflow by id (`GET /v1/public/workflows/:id`) — the
124
+ * unauthenticated share-by-link read. Returns the workflow's nodes/edges/
125
+ * settings ONLY when it's opted into sharing server-side (`settings.studio.shared
126
+ * === true`); otherwise the route 404s (→ `NotFoundError`). No auth required —
127
+ * a share viewer has no session; the SDK omits the bearer when no token exists.
128
+ */
129
+ getPublic(id) {
130
+ return this.client.request("GET", `/v1/public/workflows/${encodeURIComponent(id)}`);
131
+ }
132
+ /**
133
+ * Create a workflow under a project. Returns the full record.
134
+ * NOTE: server route is `POST /v1/projects/:projectId/workflows`.
135
+ */
136
+ create(input) {
137
+ const { projectId, ...body } = input;
138
+ return this.client.request(
139
+ "POST",
140
+ `/v1/projects/${encodeURIComponent(projectId)}/workflows`,
141
+ { body }
142
+ );
143
+ }
144
+ /** Patch a workflow. Returns the full updated record. */
145
+ update(id, input) {
146
+ return this.client.request(
147
+ "PATCH",
148
+ `/v1/workflows/${encodeURIComponent(id)}`,
149
+ { body: input }
150
+ );
151
+ }
152
+ /** Delete a workflow. Returns `{ success: true }`. */
153
+ delete(id) {
154
+ return this.client.request("DELETE", `/v1/workflows/${encodeURIComponent(id)}`);
155
+ }
156
+ /**
157
+ * Run a workflow. Returns the executionId for polling via
158
+ * `client.executions.get(executionId)`.
159
+ */
160
+ run(id, params = {}) {
161
+ return this.client.request(
162
+ "POST",
163
+ `/v1/workflows/${encodeURIComponent(id)}/run`,
164
+ { body: params }
165
+ );
166
+ }
167
+ /**
168
+ * Export a workflow as a portable JSON bundle.
169
+ * Pass `opts.assets = true` to include character/object/location entity data.
170
+ */
171
+ export(workflowId, opts) {
172
+ return this.client.request(
173
+ "GET",
174
+ `/v1/workflows/${encodeURIComponent(workflowId)}/export`,
175
+ { query: { assets: opts?.assets ?? false } }
176
+ );
177
+ }
178
+ /**
179
+ * Import a `WorkflowExport` bundle into the specified project.
180
+ * Re-creates any bundled assets (characters, objects, locations) under your account.
181
+ */
182
+ import(input) {
183
+ const { projectId, ...workflowJson } = input;
184
+ return this.client.request("POST", "/v1/workflows/import", {
185
+ body: { projectId, workflow_json: workflowJson }
186
+ });
187
+ }
188
+ };
189
+
190
+ // src/resources/projects.ts
191
+ var ProjectsResource = class {
192
+ constructor(client) {
193
+ this.client = client;
194
+ }
195
+ client;
196
+ /** List the authenticated user's projects. */
197
+ list() {
198
+ return this.client.request("GET", "/v1/projects");
199
+ }
200
+ /** Get a project by ID. */
201
+ get(id) {
202
+ return this.client.request("GET", `/v1/projects/${encodeURIComponent(id)}`);
203
+ }
204
+ /** Create a new project. */
205
+ create(input) {
206
+ return this.client.request("POST", "/v1/projects", { body: input });
207
+ }
208
+ /** Update a project. At least one field must be provided. */
209
+ update(id, input) {
210
+ return this.client.request(
211
+ "PATCH",
212
+ `/v1/projects/${encodeURIComponent(id)}`,
213
+ { body: input }
214
+ );
215
+ }
216
+ /** Delete a project. Returns `{ success: true }`. */
217
+ delete(id) {
218
+ return this.client.request("DELETE", `/v1/projects/${encodeURIComponent(id)}`);
219
+ }
220
+ };
221
+
222
+ // src/resources/jobs.ts
223
+ var JobsResource = class {
224
+ constructor(client) {
225
+ this.client = client;
226
+ }
227
+ client;
228
+ /** Get a single job by ID. */
229
+ get(id) {
230
+ return this.client.request("GET", `/v1/jobs/${encodeURIComponent(id)}`);
231
+ }
232
+ /**
233
+ * Get the lean status of a single job (poll-loop friendly).
234
+ * Hits `GET /v1/jobs/:id/status` — returns only id/status/progress/
235
+ * output_data/error_message, with far less wire/CPU cost than `get()`.
236
+ * Same auth + ownership semantics as {@link get}.
237
+ */
238
+ getStatus(id) {
239
+ return this.client.request(
240
+ "GET",
241
+ `/v1/jobs/${encodeURIComponent(id)}/status`
242
+ );
243
+ }
244
+ /**
245
+ * Cancel a job. Server route is `POST /v1/jobs/:jobId/cancel`.
246
+ * Refunds any reserved credit holds.
247
+ */
248
+ cancel(id) {
249
+ return this.client.request(
250
+ "POST",
251
+ `/v1/jobs/${encodeURIComponent(id)}/cancel`
252
+ );
253
+ }
254
+ };
255
+
256
+ // src/resources/executions.ts
257
+ var ExecutionsResource = class {
258
+ constructor(client) {
259
+ this.client = client;
260
+ }
261
+ client;
262
+ /** Get an execution by ID. Falls back to standalone single-node jobs server-side. */
263
+ get(id) {
264
+ return this.client.request(
265
+ "GET",
266
+ `/v1/workflow-executions/${encodeURIComponent(id)}`
267
+ );
268
+ }
269
+ /** List executions for a workflow. Merges workflow_executions + standalone single-node jobs. */
270
+ listForWorkflow(workflowId, params = {}) {
271
+ return this.client.request(
272
+ "GET",
273
+ `/v1/workflows/${encodeURIComponent(workflowId)}/executions`,
274
+ {
275
+ query: {
276
+ limit: params.limit,
277
+ cursor: params.cursor,
278
+ status: params.status,
279
+ source: params.source
280
+ }
281
+ }
282
+ );
283
+ }
284
+ /** Cancel an execution. Returns `{ success: true }`. */
285
+ cancel(id, params = {}) {
286
+ return this.client.request(
287
+ "POST",
288
+ `/v1/workflow-executions/${encodeURIComponent(id)}/cancel`,
289
+ { body: params }
290
+ );
291
+ }
292
+ };
293
+
294
+ // src/resources/nodes.ts
295
+ var DEFAULT_POLL_MS = 2e3;
296
+ var DEFAULT_MAX_MS = 15 * 60 * 1e3;
297
+ var sleep = (ms, signal) => new Promise((resolve, reject) => {
298
+ if (signal?.aborted) {
299
+ reject(new JobAbortedError());
300
+ return;
301
+ }
302
+ const timer = setTimeout(() => {
303
+ signal?.removeEventListener("abort", onAbort);
304
+ resolve();
305
+ }, ms);
306
+ function onAbort() {
307
+ clearTimeout(timer);
308
+ reject(new JobAbortedError());
309
+ }
310
+ signal?.addEventListener("abort", onAbort, { once: true });
311
+ });
312
+ function extractJobId(result, label) {
313
+ if (result && typeof result === "object" && "jobId" in result) {
314
+ const jobId = result.jobId;
315
+ if (typeof jobId === "string") return jobId;
316
+ }
317
+ throw new JobFailedError(`${label} did not return a jobId`, "");
318
+ }
319
+ var NodesResource = class {
320
+ constructor(client) {
321
+ this.client = client;
322
+ }
323
+ client;
324
+ /** List all known node descriptors. Server caches publicly for 5 minutes. */
325
+ list() {
326
+ return this.client.request("GET", "/v1/nodes");
327
+ }
328
+ /** Get a single node descriptor by type slug (e.g. "generate-image"). */
329
+ get(type) {
330
+ return this.client.request("GET", `/v1/nodes/${encodeURIComponent(type)}`);
331
+ }
332
+ run(type, params = {}) {
333
+ return this.client.request("POST", `/v1/${encodeURIComponent(type)}`, { body: params });
334
+ }
335
+ async runAndWait(type, params = {}, opts = {}) {
336
+ if (opts.signal?.aborted) throw new JobAbortedError();
337
+ const result = await this.run(type, params);
338
+ const jobId = extractJobId(result, type);
339
+ return this.pollJob(jobId, type, opts);
340
+ }
341
+ /**
342
+ * Fan out N async runs of the same node `type` to completion concurrently —
343
+ * the candidate-grid path (generate N stills/clips in parallel). Each runs
344
+ * via {@link runAndWait}; resolves once ALL settle, to an array of
345
+ * `{ jobId, output }` in input order. Rejects (and the rejection wins) if any
346
+ * single run rejects — same typed errors as {@link runAndWait}. A shared
347
+ * `signal` aborts the whole batch.
348
+ *
349
+ * @param type Node type slug, applied to every entry.
350
+ * @param paramsList One request body per candidate.
351
+ * @param opts Shared `signal` / `onProgress` / `pollMs` / `maxMs`.
352
+ */
353
+ async runMany(type, paramsList, opts = {}) {
354
+ if (opts.signal?.aborted) throw new JobAbortedError();
355
+ return Promise.all(
356
+ paramsList.map(async (params) => {
357
+ if (opts.signal?.aborted) throw new JobAbortedError();
358
+ const result = await this.run(type, params);
359
+ const jobId = extractJobId(result, type);
360
+ const output = await this.pollJob(jobId, type, opts);
361
+ return { jobId, output };
362
+ })
363
+ );
364
+ }
365
+ /** Poll an already-kicked job id until terminal; resolve output_data or throw. */
366
+ async pollJob(jobId, label, opts) {
367
+ const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
368
+ const maxMs = opts.maxMs ?? DEFAULT_MAX_MS;
369
+ const deadline = Date.now() + maxMs;
370
+ for (; ; ) {
371
+ if (opts.signal?.aborted) throw new JobAbortedError(void 0, jobId);
372
+ const { data } = await this.client.jobs.getStatus(jobId);
373
+ opts.onProgress?.(data);
374
+ if (data.status === "completed") {
375
+ return data.output_data ?? {};
376
+ }
377
+ if (data.status === "failed" || data.status === "cancelled") {
378
+ throw new JobFailedError(
379
+ data.error_message ?? `${label} ${data.status}`,
380
+ jobId,
381
+ data.status
382
+ );
383
+ }
384
+ if (Date.now() > deadline) {
385
+ throw new JobTimeoutError(`${label} timed out`, jobId, maxMs);
386
+ }
387
+ await sleep(pollMs, opts.signal);
388
+ }
389
+ }
390
+ };
391
+
392
+ // src/resources/developer-apps.ts
393
+ var DeveloperAppsResource = class {
394
+ constructor(client) {
395
+ this.client = client;
396
+ }
397
+ client;
398
+ /** List the authenticated user's developer apps. */
399
+ list() {
400
+ return this.client.request("GET", "/v1/developer-apps");
401
+ }
402
+ /** Get a developer app by ID. */
403
+ get(id) {
404
+ return this.client.request("GET", `/v1/developer-apps/${encodeURIComponent(id)}`);
405
+ }
406
+ /**
407
+ * Create a new developer app. Returns the app PLUS a one-time `clientSecret`
408
+ * — store it now, the secret hash is the only copy kept server-side.
409
+ */
410
+ create(input) {
411
+ return this.client.request("POST", "/v1/developer-apps", { body: input });
412
+ }
413
+ /** Update a developer app's metadata, redirect URIs, origins, or requested scopes. */
414
+ update(id, input) {
415
+ return this.client.request(
416
+ "PATCH",
417
+ `/v1/developer-apps/${encodeURIComponent(id)}`,
418
+ { body: input }
419
+ );
420
+ }
421
+ /** Delete a developer app. Returns `{ success: true }`. */
422
+ delete(id) {
423
+ return this.client.request(
424
+ "DELETE",
425
+ `/v1/developer-apps/${encodeURIComponent(id)}`
426
+ );
427
+ }
428
+ /**
429
+ * Generate a new `clientSecret`. The previous secret is invalidated.
430
+ * Server returns ONLY the new secret, not the full app record.
431
+ */
432
+ rotateSecret(id) {
433
+ return this.client.request(
434
+ "POST",
435
+ `/v1/developer-apps/${encodeURIComponent(id)}/rotate-secret`
436
+ );
437
+ }
438
+ };
439
+
440
+ // src/resources/oauth.ts
441
+ var OAuthResource = class {
442
+ constructor(client) {
443
+ this.client = client;
444
+ }
445
+ client;
446
+ /**
447
+ * Server-side authorization-code exchange. Sends the standard OAuth 2.0
448
+ * `application/json` body to `POST /v1/oauth/token`.
449
+ *
450
+ * NEVER call this from a browser — `client_secret` must stay on the server.
451
+ */
452
+ exchangeCode(input) {
453
+ return this.client.request("POST", "/v1/oauth/token", {
454
+ body: { grant_type: "authorization_code", ...input }
455
+ });
456
+ }
457
+ /**
458
+ * Revoke an access token (RFC 7009). Always returns `{ success: true }`,
459
+ * even for unknown tokens — the spec forbids leaking token validity.
460
+ */
461
+ revoke(token) {
462
+ return this.client.request("POST", "/v1/oauth/revoke", { body: { token } });
463
+ }
464
+ /**
465
+ * Get public app metadata for a consent screen.
466
+ * `GET /v1/oauth/app-info?client_id=<id>`. Public route — no auth needed.
467
+ */
468
+ getAppInfo(clientId) {
469
+ return this.client.request("GET", "/v1/oauth/app-info", {
470
+ query: { client_id: clientId }
471
+ });
472
+ }
473
+ };
474
+
475
+ // src/resources/apps.ts
476
+ var AppsResource = class {
477
+ constructor(client) {
478
+ this.client = client;
479
+ }
480
+ client;
481
+ /** List published apps. Public — no auth required for community apps. */
482
+ list(params = {}) {
483
+ const qs = new URLSearchParams();
484
+ if (params.search) qs.set("search", params.search);
485
+ if (params.limit !== void 0) qs.set("limit", String(params.limit));
486
+ if (params.cursor) qs.set("cursor", params.cursor);
487
+ if (params.category) qs.set("category", params.category);
488
+ const query = qs.toString();
489
+ return this.client.request("GET", `/v1/apps/browse${query ? `?${query}` : ""}`);
490
+ }
491
+ /** Get one app's metadata + input schema by slug. */
492
+ get(slug) {
493
+ return this.client.request("GET", `/v1/app/${encodeURIComponent(slug)}`);
494
+ }
495
+ /**
496
+ * Trigger an app run with the given input values. The keys in `inputs` must
497
+ * match the app's input-schema field names (see `get(slug).inputSchema`).
498
+ * Returns the execution-id for status polling via client.executions.get().
499
+ */
500
+ run(slug, inputs = {}) {
501
+ return this.client.request("POST", `/v1/app/${encodeURIComponent(slug)}/run`, {
502
+ body: { inputs }
503
+ });
504
+ }
505
+ /** List past runs for an app (the caller must own the app or the runs). */
506
+ listRuns(slug, params = {}) {
507
+ const qs = new URLSearchParams();
508
+ if (params.limit !== void 0) qs.set("limit", String(params.limit));
509
+ if (params.cursor) qs.set("cursor", params.cursor);
510
+ const query = qs.toString();
511
+ return this.client.request(
512
+ "GET",
513
+ `/v1/app/${encodeURIComponent(slug)}/runs${query ? `?${query}` : ""}`
514
+ );
515
+ }
516
+ /** Get one app-run by id. */
517
+ getRun(slug, runId) {
518
+ return this.client.request(
519
+ "GET",
520
+ `/v1/app/${encodeURIComponent(slug)}/runs/${encodeURIComponent(runId)}`
521
+ );
522
+ }
523
+ /**
524
+ * Archive (soft-delete) a published-app run. The run is hidden from the
525
+ * default run list and can be restored or permanently deleted from the
526
+ * archive view at https://app.nodaro.ai/archived-runs.
527
+ *
528
+ * @param slug The published app's slug (the last path segment of its URL).
529
+ * @param runId The run's UUID.
530
+ */
531
+ deleteRun(slug, runId) {
532
+ return this.client.request(
533
+ "DELETE",
534
+ `/v1/app/${encodeURIComponent(slug)}/runs/${encodeURIComponent(runId)}`
535
+ );
536
+ }
537
+ };
538
+ var CharactersResource = class {
539
+ constructor(client) {
540
+ this.client = client;
541
+ }
542
+ client;
543
+ /**
544
+ * List the caller's characters. By default returns active characters only;
545
+ * pass `archived: true` to fetch soft-deleted rows for an "archive" view.
546
+ * When `projectId` is set, only characters belonging to that project are
547
+ * returned.
548
+ */
549
+ list(params = {}) {
550
+ const query = {};
551
+ if (params.projectId) query.projectId = params.projectId;
552
+ if (params.archived) query.archived = "true";
553
+ if (params.limit !== void 0) query.limit = String(params.limit);
554
+ return this.client.request("GET", "/v1/characters", { query });
555
+ }
556
+ /**
557
+ * Fetch a single character including in-flight portrait / asset job state.
558
+ * Soft-deleted (archived) rows are returned by id intentionally so canvas
559
+ * nodes that hold a stale `characterDbId` keep loading.
560
+ */
561
+ get(id) {
562
+ return this.client.request("GET", `/v1/characters/${encodeURIComponent(id)}`);
563
+ }
564
+ /**
565
+ * Create or update a character. Omit `id` to create; supply it to update
566
+ * (only the fields you pass get written — undefined keys are untouched).
567
+ *
568
+ * If the caller-supplied `name` collides with an existing active character
569
+ * for this user, the request returns 409 `name_taken`. To auto-number a
570
+ * placeholder, pass the placeholder name from `@nodaro/shared` and the
571
+ * server will derive "Untitled character 2", "Untitled character 3", etc.
572
+ */
573
+ upsert(input) {
574
+ return this.client.request("POST", "/v1/characters", { body: input });
575
+ }
576
+ /**
577
+ * Convenience wrapper around `upsert()` for creating new characters.
578
+ * Equivalent to `upsert({ ...input, id: undefined })`. `name` is REQUIRED
579
+ * on create — the route 400s on INSERT-without-name; we narrow the type
580
+ * here so callers fail at compile-time rather than runtime.
581
+ */
582
+ create(input) {
583
+ return this.upsert(input);
584
+ }
585
+ /**
586
+ * Convenience wrapper around `upsert()` for updating an existing character.
587
+ * Equivalent to `upsert({ ...input, id })`.
588
+ */
589
+ update(id, input) {
590
+ return this.upsert({ ...input, id });
591
+ }
592
+ /**
593
+ * Soft-delete (archive) a character. The row is hidden from `list()` by
594
+ * default but still loadable via `get(id)` so canvas nodes pointing at it
595
+ * keep working. Restore with `restore(id)`.
596
+ */
597
+ delete(id) {
598
+ return this.client.request("DELETE", `/v1/characters/${encodeURIComponent(id)}`);
599
+ }
600
+ /**
601
+ * Un-archive a character. If the original name now collides with an
602
+ * active row, the server auto-suffixes "(restored)" and returns the
603
+ * effective name.
604
+ */
605
+ restore(id) {
606
+ return this.client.request("POST", `/v1/characters/${encodeURIComponent(id)}/restore`);
607
+ }
608
+ /**
609
+ * Duplicate (fork) a character to a new row with a `"(copy)"` suffix.
610
+ * Asset URLs are shared by reference — the new row can diverge by
611
+ * regenerating any of them.
612
+ */
613
+ duplicate(id, input = {}) {
614
+ return this.client.request(
615
+ "POST",
616
+ `/v1/characters/${encodeURIComponent(id)}/duplicate`,
617
+ { body: input }
618
+ );
619
+ }
620
+ /**
621
+ * Count of the caller's workflows that reference this character. Powers the
622
+ * library "Archive" confirmation modal in the editor.
623
+ */
624
+ usage(id) {
625
+ return this.client.request("GET", `/v1/characters/${encodeURIComponent(id)}/usage`);
626
+ }
627
+ /**
628
+ * Fire `POST /v1/generate-character` to produce one or more portrait
629
+ * candidates. With `count > 1`, all jobs are reserved up-front before any
630
+ * is enqueued — mid-batch failures roll back atomically.
631
+ *
632
+ * When `attachToCharacterId` is set, the worker writes the result directly
633
+ * to the row's `source_image_url`; otherwise you must call
634
+ * `approvePortrait()` after picking a candidate.
635
+ */
636
+ generate(input) {
637
+ return this.client.request("POST", "/v1/generate-character", { body: input });
638
+ }
639
+ /**
640
+ * Fire `POST /v1/generate-character-asset` to produce a single
641
+ * expression / pose / angle / lighting variant. When the studio path is
642
+ * set (`attachToCharacterId` + `attachToColumn` + `attachName`), the
643
+ * worker appends `{ name: attachName, url: <result> }` to the named
644
+ * JSONB array column on completion.
645
+ */
646
+ generateAsset(input) {
647
+ return this.client.request("POST", "/v1/generate-character-asset", { body: input });
648
+ }
649
+ /**
650
+ * Fire `POST /v1/generate-character-motion` to animate the character's
651
+ * portrait into a motion clip. The result is appended to the character's
652
+ * `motions[]` bucket when `attachToCharacterId` is set.
653
+ */
654
+ generateMotion(input) {
655
+ return this.client.request("POST", "/v1/generate-character-motion", { body: input });
656
+ }
657
+ /**
658
+ * Approve a completed `generate-character` job as the character's portrait.
659
+ * Sets `source_image_url` and fires the LLM caption (Claude Sonnet vision)
660
+ * inline. Returns the new portrait URL plus the caption — `canonicalDescription`
661
+ * is `null` if the LLM call sub-failed (portrait still set; retry via `recaption()`).
662
+ */
663
+ approvePortrait(id, candidateJobId) {
664
+ return this.client.request(
665
+ "POST",
666
+ `/v1/characters/${encodeURIComponent(id)}/approve-portrait`,
667
+ { body: { candidateJobId } }
668
+ );
669
+ }
670
+ /**
671
+ * Re-fire the LLM caption against the character's current portrait. 502s on
672
+ * LLM failure; returns 400 `no_portrait` if no portrait is set yet.
673
+ */
674
+ recaption(id) {
675
+ return this.client.request(
676
+ "POST",
677
+ `/v1/characters/${encodeURIComponent(id)}/llm-caption`
678
+ );
679
+ }
680
+ };
681
+ function buildPersonSeedPrompt(value) {
682
+ return buildPersonHints(value).join(", ");
683
+ }
684
+ var LocationsResource = class {
685
+ constructor(client) {
686
+ this.client = client;
687
+ }
688
+ client;
689
+ /**
690
+ * List the caller's locations. By default returns active locations only;
691
+ * pass `archived: true` to fetch soft-deleted rows for an "archive" view.
692
+ */
693
+ list(params = {}) {
694
+ const query = {};
695
+ if (params.archived) query.archived = "true";
696
+ return this.client.request("GET", "/v1/locations", { query });
697
+ }
698
+ /**
699
+ * Convenience wrapper for `list({ archived: true })`. Returns soft-deleted
700
+ * rows so callers can drive a UI "Archived" tab without re-encoding the
701
+ * query param. Mirrors `ObjectsResource.listArchived`.
702
+ *
703
+ * `archived` is omitted from the param type — it's always set to `true` here.
704
+ */
705
+ listArchived(params = {}) {
706
+ return this.list({ ...params, archived: true });
707
+ }
708
+ /**
709
+ * Fetch a single location including in-flight asset job state. Soft-deleted
710
+ * (archived) rows are returned by id intentionally so canvas nodes that
711
+ * hold a stale `locationDbId` keep loading.
712
+ */
713
+ async get(id) {
714
+ const res = await this.client.request(
715
+ "GET",
716
+ `/v1/locations/${encodeURIComponent(id)}`
717
+ );
718
+ return { ...res, canonicalDescription: res.canonicalDescription || null };
719
+ }
720
+ /**
721
+ * Create a new location. `name` + `nodeId` are required — the route 400s
722
+ * otherwise. Returns the new row's id.
723
+ *
724
+ * Note: the underlying route is the same `POST /v1/locations` upsert that
725
+ * powers `update()`. This convenience wrapper enforces the INSERT-required
726
+ * fields at the type level and never sends an `id`.
727
+ */
728
+ create(data) {
729
+ return this.client.request("POST", "/v1/locations", { body: data });
730
+ }
731
+ /**
732
+ * Update a location. Only the fields you pass are written — undefined keys
733
+ * are NOT touched on the row. Worker-owned asset buckets are intentionally
734
+ * not exposed on this surface (see `UpdateLocationInput` for the rationale).
735
+ *
736
+ * Optimistic-concurrency: pass `expectedUpdatedAt` to require the row's
737
+ * `updated_at` still matches; on mismatch the route returns 409
738
+ * `concurrent_modification`. The SDK surfaces that as a generic
739
+ * `NodaroError` with the same code.
740
+ */
741
+ update(id, data) {
742
+ return this.client.request("POST", "/v1/locations", {
743
+ body: { id, ...data }
744
+ });
745
+ }
746
+ /**
747
+ * Soft-delete (archive) a location. The row is hidden from `list()` by
748
+ * default but still loadable via `get(id)` so canvas nodes pointing at it
749
+ * keep working. Restore with `restore(id)`.
750
+ */
751
+ delete(id) {
752
+ return this.client.request("DELETE", `/v1/locations/${encodeURIComponent(id)}`);
753
+ }
754
+ /**
755
+ * Un-archive a location. If the original name now collides (case-
756
+ * insensitive) with an active row, the server auto-suffixes "(restored)"
757
+ * and returns the effective name.
758
+ */
759
+ restore(id) {
760
+ return this.client.request("POST", `/v1/locations/${encodeURIComponent(id)}/restore`);
761
+ }
762
+ /**
763
+ * Fire `POST /v1/generate-location` to produce one or more candidate main
764
+ * images. With `count > 1`, all jobs are reserved up-front before any
765
+ * is enqueued — mid-batch failures roll back atomically.
766
+ *
767
+ * When `attachToLocationId` is set AND `count === 1`, the worker writes
768
+ * the result directly to the row's `source_image_url`; otherwise you must
769
+ * call `approveMainImage()` after picking a candidate.
770
+ */
771
+ async generate(data) {
772
+ const res = await this.client.request(
773
+ "POST",
774
+ "/v1/generate-location",
775
+ { body: data }
776
+ );
777
+ const jobIds = res.jobIds ?? (res.jobId ? [res.jobId] : []);
778
+ return res.jobId ? { jobIds, jobId: res.jobId } : { jobIds };
779
+ }
780
+ /**
781
+ * Fire `POST /v1/generate-location-asset` to produce a single variant.
782
+ * When the studio path is set (`attachToLocationId` + `attachToColumn` +
783
+ * `attachName`), the worker appends `{ name: attachName, url: <result> }`
784
+ * to the named JSONB array column on completion.
785
+ */
786
+ generateAsset(data) {
787
+ return this.client.request("POST", "/v1/generate-location-asset", { body: data });
788
+ }
789
+ /**
790
+ * Fire `POST /v1/generate-surround-continuation` to produce one seamless 360°
791
+ * ring view as an i2i continuation of `referenceImageUrl`. The platform builds
792
+ * the half-carry composite, paints the missing half, and color-harmonizes it
793
+ * to the carried half (no tonal seam; carried half stays pixel-exact). When the
794
+ * studio path is set, the worker appends the result to the location's bucket.
795
+ */
796
+ generateSurroundContinuation(data) {
797
+ return this.client.request("POST", "/v1/generate-surround-continuation", { body: data });
798
+ }
799
+ /**
800
+ * Fire `POST /v1/generate-location-motion` to animate the location's
801
+ * establishing shot into an atmospheric motion clip. Image-to-video, single
802
+ * clip per call; the attach column is hardcoded to `atmosphere_motions`
803
+ * server-side (locations have a single motion bucket so the caller doesn't
804
+ * supply `attachToColumn`). When the studio path is set
805
+ * (`attachToLocationId` + `attachName`), the worker appends
806
+ * `{ name: attachName, url: <result> }` to the row's `atmosphere_motions`
807
+ * column on completion.
808
+ */
809
+ generateMotion(data) {
810
+ return this.client.request("POST", "/v1/generate-location-motion", { body: data });
811
+ }
812
+ /**
813
+ * Atomically remove ONE asset take (every entry matching `url`) from a
814
+ * worker-owned bucket column — `POST /v1/locations/:id/remove-asset`. The
815
+ * worker-owned buckets are deliberately not writable through `update()`
816
+ * (a stale snapshot would race concurrent worker appends), so deleting a
817
+ * take — e.g. a 360° surround view being regenerated — goes through this
818
+ * single-statement server-side filter instead. 404s (`NotFoundError`) when
819
+ * the url isn't in that bucket or the location isn't yours.
820
+ */
821
+ removeAsset(id, data) {
822
+ return this.client.request(
823
+ "POST",
824
+ `/v1/locations/${encodeURIComponent(id)}/remove-asset`,
825
+ { body: data }
826
+ );
827
+ }
828
+ /**
829
+ * Approve a completed `generate-location` job as the location's main image.
830
+ * Sets `source_image_url` and fires the LLM caption (Claude Sonnet vision)
831
+ * inline. Returns the new main-image URL plus the caption.
832
+ *
833
+ * Caption-failure semantics: the route still sends `""` on LLM sub-failure,
834
+ * but the SDK normalizes `""` → `null` here so `canonicalDescription` carries
835
+ * the same `string | null` semantics as characters. The main image is still
836
+ * set; call `recaption()` to retry.
837
+ */
838
+ async approveMainImage(id, candidateJobId) {
839
+ const res = await this.client.request(
840
+ "POST",
841
+ `/v1/locations/${encodeURIComponent(id)}/approve-main-image`,
842
+ { body: { candidateJobId } }
843
+ );
844
+ return { sourceImageUrl: res.sourceImageUrl, canonicalDescription: res.canonicalDescription || null };
845
+ }
846
+ /**
847
+ * Re-fire the LLM caption against the location's current main image. 502s
848
+ * on LLM failure (unlike `approveMainImage` which preserves the side-effect
849
+ * and returns ""); returns 400 `no_source_image` if no main image is set
850
+ * yet.
851
+ */
852
+ recaption(id) {
853
+ return this.client.request(
854
+ "POST",
855
+ `/v1/locations/${encodeURIComponent(id)}/llm-caption`
856
+ );
857
+ }
858
+ };
859
+ var ObjectsResource = class {
860
+ constructor(client) {
861
+ this.client = client;
862
+ }
863
+ client;
864
+ /**
865
+ * List the caller's objects. By default returns active objects only;
866
+ * pass `archived: true` to fetch soft-deleted rows for an "archive" view.
867
+ * Optional `projectId` scopes the result to a single project.
868
+ */
869
+ list(params = {}) {
870
+ const query = {};
871
+ if (params.archived) query.archived = "true";
872
+ if (params.projectId) query.projectId = params.projectId;
873
+ return this.client.request("GET", "/v1/objects", { query });
874
+ }
875
+ /**
876
+ * Convenience wrapper for `list({ archived: true })`. Returns soft-deleted
877
+ * rows so callers can drive a UI "Archived" tab without re-encoding the
878
+ * query param.
879
+ *
880
+ * `archived` is omitted from the param type — it's always set to `true` here.
881
+ */
882
+ listArchived(params = {}) {
883
+ return this.list({ ...params, archived: true });
884
+ }
885
+ /**
886
+ * Fetch a single object including in-flight asset job state. Soft-deleted
887
+ * (archived) rows are NOT returned by id — the route enforces
888
+ * `deleted_at IS NULL` so archived objects 404 (uniform Pass 10 F-90b
889
+ * "not_found" — does not leak the deleted vs non-existent distinction).
890
+ */
891
+ async get(id) {
892
+ const res = await this.client.request(
893
+ "GET",
894
+ `/v1/objects/${encodeURIComponent(id)}`
895
+ );
896
+ return { ...res, canonicalDescription: res.canonicalDescription || null };
897
+ }
898
+ /**
899
+ * Create a new object. `name` + `nodeId` are required — the route 400s
900
+ * otherwise. Returns the new row's id.
901
+ *
902
+ * Note: the underlying route is the same `POST /v1/objects` upsert that
903
+ * powers `update()`. This convenience wrapper enforces the INSERT-required
904
+ * fields at the type level and never sends an `id`.
905
+ */
906
+ create(data) {
907
+ return this.client.request("POST", "/v1/objects", { body: data });
908
+ }
909
+ /**
910
+ * Update an object. Only the fields you pass are written — undefined keys
911
+ * are NOT touched on the row. Worker-owned asset buckets are intentionally
912
+ * not exposed on this surface (see `UpdateObjectInput` for the rationale).
913
+ *
914
+ * Optimistic-concurrency: pass `expectedUpdatedAt` to require the row's
915
+ * `updated_at` still matches; on mismatch the route returns 409
916
+ * `concurrent_modification` carrying the fresh `updatedAt`. The SDK
917
+ * surfaces that as a generic `NodaroError` with the same code (per Phase
918
+ * E1 calibration finding — error centralization in `throwApiError`).
919
+ */
920
+ update(id, data) {
921
+ return this.client.request("POST", "/v1/objects", {
922
+ body: { id, ...data }
923
+ });
924
+ }
925
+ /**
926
+ * Soft-delete (archive) an object. The row is hidden from `list()` by
927
+ * default but recoverable via `restore(id)` or visible under
928
+ * `listArchived()`. Idempotent — repeating a delete on an already-archived
929
+ * row is a no-op.
930
+ */
931
+ delete(id) {
932
+ return this.client.request("DELETE", `/v1/objects/${encodeURIComponent(id)}`);
933
+ }
934
+ /**
935
+ * Hard-delete (permanent) an object — the row + every R2 asset it
936
+ * references. Archived rows ONLY: active objects return 400 `not_archived`.
937
+ * Call `delete()` first to archive, then `permanentDelete()` to destroy.
938
+ *
939
+ * Mirrors the `app_runs` permanent-delete pattern (archive-first) so a
940
+ * stray SDK / curl caller cannot bypass the studio's archive-first UI
941
+ * flow.
942
+ */
943
+ permanentDelete(id) {
944
+ return this.client.request("DELETE", `/v1/objects/${encodeURIComponent(id)}`, {
945
+ query: { permanent: "true" }
946
+ });
947
+ }
948
+ /**
949
+ * Un-archive an object. If the original name now collides (case-
950
+ * insensitive) with an active row, the server auto-suffixes "(restored)"
951
+ * and returns the effective name.
952
+ */
953
+ restore(id) {
954
+ return this.client.request("POST", `/v1/objects/${encodeURIComponent(id)}/restore`);
955
+ }
956
+ /**
957
+ * Fire `POST /v1/generate-object` to produce one or more candidate main
958
+ * images. With `count > 1`, all jobs are reserved up-front before any
959
+ * is enqueued — mid-batch failures roll back atomically.
960
+ *
961
+ * When `attachToObjectId` is set AND `count === 1`, the worker writes
962
+ * the result directly to the row's `source_image_url`; otherwise you must
963
+ * call `approveMainImage()` after picking a candidate.
964
+ */
965
+ async generate(data) {
966
+ const res = await this.client.request(
967
+ "POST",
968
+ "/v1/generate-object",
969
+ { body: data }
970
+ );
971
+ const jobIds = res.jobIds ?? (res.jobId ? [res.jobId] : []);
972
+ return res.jobId ? { jobIds, jobId: res.jobId } : { jobIds };
973
+ }
974
+ /**
975
+ * Fire `POST /v1/generate-object-asset` to produce a single variant.
976
+ * When the studio path is set (`attachToObjectId` + `attachToColumn` +
977
+ * `attachName`), the worker appends `{ name: attachName, url: <result> }`
978
+ * to the named JSONB array column on completion.
979
+ *
980
+ * Note: `attachToColumn` is REQUIRED for `assetType === "custom"` — the
981
+ * worker can't infer the bucket from the asset type. For canonical asset
982
+ * types (`angles` / `materials` / `variations` / `motion`), the column is
983
+ * derived automatically by the route.
984
+ */
985
+ generateAsset(data) {
986
+ return this.client.request("POST", "/v1/generate-object-asset", { body: data });
987
+ }
988
+ /**
989
+ * Fire `POST /v1/generate-object-motion` to animate the object's main
990
+ * image into a motion clip. Image-to-video, single clip per call; the
991
+ * attach column is hardcoded to `motion_clips` server-side (objects have a
992
+ * single motion bucket so the caller doesn't supply `attachToColumn`).
993
+ * When the studio path is set (`attachToObjectId` + `attachName`), the
994
+ * worker appends `{ name: attachName, url: <result> }` to the row's
995
+ * `motion_clips` column on completion.
996
+ *
997
+ * Defaults: `provider` → `"kling-turbo"`, `aspectRatio` → `"1:1"` (set
998
+ * server-side via `resolveObjectAspectRatio({ assetType: "motion" })`).
999
+ */
1000
+ generateMotion(data) {
1001
+ return this.client.request("POST", "/v1/generate-object-motion", { body: data });
1002
+ }
1003
+ /**
1004
+ * Approve a completed `generate-object` job as the object's main image.
1005
+ * Sets `source_image_url` and fires the LLM caption (Claude Sonnet vision)
1006
+ * inline. Returns the new main-image URL plus the caption.
1007
+ *
1008
+ * Caption-failure semantics: the route still sends `""` on LLM sub-failure,
1009
+ * but the SDK normalizes `""` → `null` here so `canonicalDescription` carries
1010
+ * the same `string | null` semantics as characters. The main image is still
1011
+ * set; call `recaption()` to retry.
1012
+ *
1013
+ * Optimistic-concurrency: pass `expectedUpdatedAt` to gate the update on
1014
+ * the row's current `updated_at`; on mismatch the route returns 409
1015
+ * `concurrent_modification` carrying the fresh token.
1016
+ */
1017
+ async approveMainImage(id, candidateJobId, expectedUpdatedAt) {
1018
+ const res = await this.client.request(
1019
+ "POST",
1020
+ `/v1/objects/${encodeURIComponent(id)}/approve-main-image`,
1021
+ { body: { candidateJobId, expectedUpdatedAt } }
1022
+ );
1023
+ return { sourceImageUrl: res.sourceImageUrl, canonicalDescription: res.canonicalDescription || null };
1024
+ }
1025
+ /**
1026
+ * Re-fire the LLM caption against the object's current main image. 502s
1027
+ * on LLM failure (unlike `approveMainImage` which preserves the side-effect
1028
+ * and returns ""); returns 400 `main_image_required` if no main image is
1029
+ * set yet.
1030
+ *
1031
+ * The route is a pure idempotent retry — it does NOT accept an
1032
+ * `expectedUpdatedAt` token (per Phase E1 calibration finding: backend
1033
+ * route is idempotent retry, not gated on optimistic-concurrency).
1034
+ */
1035
+ recaption(id) {
1036
+ return this.client.request(
1037
+ "POST",
1038
+ `/v1/objects/${encodeURIComponent(id)}/llm-caption`
1039
+ );
1040
+ }
1041
+ };
1042
+ var CREATURE_ASSET_TYPES = ["angles", "poses", "variations", "custom"];
1043
+ var CreaturesResource = class {
1044
+ constructor(client) {
1045
+ this.client = client;
1046
+ }
1047
+ client;
1048
+ /**
1049
+ * List the caller's creatures. By default returns active creatures only;
1050
+ * pass `archived: true` to fetch soft-deleted rows for an "archive" view.
1051
+ * Optional `projectId` scopes the result to a single project.
1052
+ */
1053
+ list(params = {}) {
1054
+ const query = {};
1055
+ if (params.archived) query.archived = "true";
1056
+ if (params.projectId) query.projectId = params.projectId;
1057
+ return this.client.request("GET", "/v1/creatures", { query });
1058
+ }
1059
+ /**
1060
+ * Convenience wrapper for `list({ archived: true })`. Returns soft-deleted
1061
+ * rows so callers can drive a UI "Archived" tab without re-encoding the
1062
+ * query param.
1063
+ *
1064
+ * `archived` is omitted from the param type — it's always set to `true` here.
1065
+ */
1066
+ listArchived(params = {}) {
1067
+ return this.list({ ...params, archived: true });
1068
+ }
1069
+ /**
1070
+ * Fetch a single creature including in-flight asset job state. Soft-deleted
1071
+ * (archived) rows are NOT returned by id — the route enforces
1072
+ * `deleted_at IS NULL` so archived creatures 404 (uniform "not_found" — does
1073
+ * not leak the deleted vs non-existent distinction).
1074
+ */
1075
+ async get(id) {
1076
+ const res = await this.client.request(
1077
+ "GET",
1078
+ `/v1/creatures/${encodeURIComponent(id)}`
1079
+ );
1080
+ return { ...res, canonicalDescription: res.canonicalDescription || null };
1081
+ }
1082
+ /**
1083
+ * Create a new creature. `name` + `nodeId` are required — the route 400s
1084
+ * otherwise. Returns the new row's id.
1085
+ *
1086
+ * Note: the underlying route is the same `POST /v1/creatures` upsert that
1087
+ * powers `update()`. This convenience wrapper enforces the INSERT-required
1088
+ * fields at the type level and never sends an `id`.
1089
+ */
1090
+ create(data) {
1091
+ return this.client.request("POST", "/v1/creatures", { body: data });
1092
+ }
1093
+ /**
1094
+ * Update a creature. Only the fields you pass are written — undefined keys
1095
+ * are NOT touched on the row. Worker-owned asset buckets are intentionally
1096
+ * not exposed on this surface (see `UpdateCreatureInput` for the rationale).
1097
+ *
1098
+ * Optimistic-concurrency: pass `expectedUpdatedAt` to require the row's
1099
+ * `updated_at` still matches; on mismatch the route returns 409
1100
+ * `concurrent_modification` carrying the fresh `updatedAt`. The SDK
1101
+ * surfaces that as a generic `NodaroError` with the same code.
1102
+ */
1103
+ update(id, data) {
1104
+ return this.client.request("POST", "/v1/creatures", {
1105
+ body: { id, ...data }
1106
+ });
1107
+ }
1108
+ /**
1109
+ * Soft-delete (archive) a creature. The row is hidden from `list()` by
1110
+ * default but recoverable via `restore(id)` or visible under
1111
+ * `listArchived()`. Idempotent — repeating a delete on an already-archived
1112
+ * row is a no-op.
1113
+ */
1114
+ delete(id) {
1115
+ return this.client.request("DELETE", `/v1/creatures/${encodeURIComponent(id)}`);
1116
+ }
1117
+ /**
1118
+ * Hard-delete (permanent) a creature — the row + every R2 asset it
1119
+ * references. Archived rows ONLY: active creatures return 400 `not_archived`.
1120
+ * Call `delete()` first to archive, then `permanentDelete()` to destroy.
1121
+ *
1122
+ * Mirrors the `app_runs` permanent-delete pattern (archive-first) so a
1123
+ * stray SDK / curl caller cannot bypass the studio's archive-first UI
1124
+ * flow.
1125
+ */
1126
+ permanentDelete(id) {
1127
+ return this.client.request("DELETE", `/v1/creatures/${encodeURIComponent(id)}`, {
1128
+ query: { permanent: "true" }
1129
+ });
1130
+ }
1131
+ /**
1132
+ * Un-archive a creature. If the original name now collides (case-
1133
+ * insensitive) with an active row, the server auto-suffixes "(restored)"
1134
+ * and returns the effective name.
1135
+ */
1136
+ restore(id) {
1137
+ return this.client.request("POST", `/v1/creatures/${encodeURIComponent(id)}/restore`);
1138
+ }
1139
+ /**
1140
+ * Fire `POST /v1/generate-creature` to produce one or more candidate main
1141
+ * images. With `count > 1`, all jobs are reserved up-front before any
1142
+ * is enqueued — mid-batch failures roll back atomically.
1143
+ *
1144
+ * When `attachToCreatureId` is set AND `count === 1`, the worker writes
1145
+ * the result directly to the row's `source_image_url`; otherwise you must
1146
+ * call `approveMainImage()` after picking a candidate.
1147
+ */
1148
+ async generate(data) {
1149
+ const res = await this.client.request(
1150
+ "POST",
1151
+ "/v1/generate-creature",
1152
+ { body: data }
1153
+ );
1154
+ const jobIds = res.jobIds ?? (res.jobId ? [res.jobId] : []);
1155
+ return res.jobId ? { jobIds, jobId: res.jobId } : { jobIds };
1156
+ }
1157
+ /**
1158
+ * Fire `POST /v1/generate-creature-asset` to produce a single variant.
1159
+ * When the studio path is set (`attachToCreatureId` + `attachToColumn` +
1160
+ * `attachName`), the worker appends `{ name: attachName, url: <result> }`
1161
+ * to the named JSONB array column on completion.
1162
+ *
1163
+ * Note: `attachToColumn` is REQUIRED for `assetType === "custom"` — the
1164
+ * worker can't infer the bucket from the asset type. For canonical asset
1165
+ * types (`angles` / `poses` / `variations`), the column is derived
1166
+ * automatically by the route.
1167
+ */
1168
+ generateAsset(data) {
1169
+ return this.client.request("POST", "/v1/generate-creature-asset", { body: data });
1170
+ }
1171
+ /**
1172
+ * Fire `POST /v1/generate-creature-motion` to animate the creature's main
1173
+ * image into a motion clip. Image-to-video, single clip per call; the
1174
+ * attach column is hardcoded to `motion_clips` server-side (creatures have a
1175
+ * single motion bucket so the caller doesn't supply `attachToColumn`).
1176
+ * When the studio path is set (`attachToCreatureId` + `attachName`), the
1177
+ * worker appends `{ name: attachName, url: <result> }` to the row's
1178
+ * `motion_clips` column on completion.
1179
+ *
1180
+ * Defaults: `provider` → `"kling-turbo"`, `aspectRatio` → `"1:1"` (set
1181
+ * server-side via `resolveObjectAspectRatio({ assetType: "motion" })`).
1182
+ */
1183
+ generateMotion(data) {
1184
+ return this.client.request("POST", "/v1/generate-creature-motion", { body: data });
1185
+ }
1186
+ /**
1187
+ * Approve a completed `generate-creature` job as the creature's main image.
1188
+ * Sets `source_image_url` and fires the LLM caption (Claude Sonnet vision)
1189
+ * inline. Returns the new main-image URL plus the caption.
1190
+ *
1191
+ * Caption-failure semantics: the route still sends `""` on LLM sub-failure,
1192
+ * but the SDK normalizes `""` → `null` here so `canonicalDescription` carries
1193
+ * the same `string | null` semantics as characters. The main image is still
1194
+ * set; call `recaption()` to retry.
1195
+ *
1196
+ * Optimistic-concurrency: pass `expectedUpdatedAt` to gate the update on
1197
+ * the row's current `updated_at`; on mismatch the route returns 409
1198
+ * `concurrent_modification` carrying the fresh token.
1199
+ */
1200
+ async approveMainImage(id, candidateJobId, expectedUpdatedAt) {
1201
+ const res = await this.client.request(
1202
+ "POST",
1203
+ `/v1/creatures/${encodeURIComponent(id)}/approve-main-image`,
1204
+ { body: { candidateJobId, expectedUpdatedAt } }
1205
+ );
1206
+ return { sourceImageUrl: res.sourceImageUrl, canonicalDescription: res.canonicalDescription || null };
1207
+ }
1208
+ /**
1209
+ * Re-fire the LLM caption against the creature's current main image. 502s
1210
+ * on LLM failure (unlike `approveMainImage` which preserves the side-effect
1211
+ * and returns ""); returns 400 `main_image_required` if no main image is
1212
+ * set yet.
1213
+ *
1214
+ * The route is a pure idempotent retry — it does NOT accept an
1215
+ * `expectedUpdatedAt` token (backend route is idempotent retry, not gated on
1216
+ * optimistic-concurrency).
1217
+ */
1218
+ recaption(id) {
1219
+ return this.client.request(
1220
+ "POST",
1221
+ `/v1/creatures/${encodeURIComponent(id)}/llm-caption`
1222
+ );
1223
+ }
1224
+ };
1225
+
1226
+ // src/resources/pipelines.ts
1227
+ var PipelinesResource = class {
1228
+ constructor(client) {
1229
+ this.client = client;
1230
+ }
1231
+ client;
1232
+ /**
1233
+ * Start a new pipeline (headless film generation) — the programmatic
1234
+ * equivalent of the studio's "Create film". In Auto mode the engine
1235
+ * self-advances to completion; poll {@link get} for status and
1236
+ * {@link getTimeline} for the assembled output. In manual/guided mode, drive
1237
+ * it with {@link pendingApprovals} + {@link approveStage} /
1238
+ * {@link approveSubGate}.
1239
+ *
1240
+ * Requires `pipelines:execute` scope. Returns the new pipeline id.
1241
+ */
1242
+ create(input) {
1243
+ return this.client.request("POST", "/v1/pipelines", { body: input });
1244
+ }
1245
+ /**
1246
+ * Fetch current pipeline state: `status`, `current_stage`, credit counters,
1247
+ * `mode`, and `failure_reason` (set when `status='failed'`). Poll this to
1248
+ * track a headless Auto run to completion. Requires `pipelines:read`.
1249
+ */
1250
+ get(id) {
1251
+ return this.client.request(
1252
+ "GET",
1253
+ `/v1/pipelines/${encodeURIComponent(id)}`
1254
+ );
1255
+ }
1256
+ /** List the caller's pipelines (most recent first). Requires `pipelines:read`. */
1257
+ list() {
1258
+ return this.client.request("GET", "/v1/pipelines");
1259
+ }
1260
+ /**
1261
+ * Cancel a running pipeline. Unspent reserved credits refund. Idempotent on
1262
+ * an already-terminal pipeline. Requires `pipelines:execute`.
1263
+ */
1264
+ cancel(id) {
1265
+ return this.client.request(
1266
+ "POST",
1267
+ `/v1/pipelines/${encodeURIComponent(id)}/cancel`,
1268
+ { body: {} }
1269
+ );
1270
+ }
1271
+ /**
1272
+ * Stages currently `awaiting_approval`. Empty in a clean Auto run (the engine
1273
+ * self-approves); populated in manual/guided mode at each gate. Requires
1274
+ * `pipelines:read`.
1275
+ */
1276
+ pendingApprovals(id) {
1277
+ return this.client.request(
1278
+ "GET",
1279
+ `/v1/pipelines/${encodeURIComponent(id)}/pending-approvals`
1280
+ );
1281
+ }
1282
+ /**
1283
+ * Approve a stage so the engine advances to the next one. An optional `edits`
1284
+ * JSON-Patch is applied to the stage output before approval. Requires
1285
+ * `pipelines:approve`.
1286
+ */
1287
+ approveStage(id, stage, edits) {
1288
+ return this.client.request(
1289
+ "POST",
1290
+ `/v1/pipelines/${encodeURIComponent(id)}/stages/${encodeURIComponent(stage)}/approve`,
1291
+ { body: edits ? { edits } : {} }
1292
+ );
1293
+ }
1294
+ /**
1295
+ * Reject a stage with feedback; the engine re-runs it incorporating the note.
1296
+ * Requires `pipelines:approve`.
1297
+ */
1298
+ rejectStage(id, stage, feedback) {
1299
+ return this.client.request(
1300
+ "POST",
1301
+ `/v1/pipelines/${encodeURIComponent(id)}/stages/${encodeURIComponent(stage)}/reject`,
1302
+ { body: { feedback } }
1303
+ );
1304
+ }
1305
+ /**
1306
+ * Approve a Stage-7 sub-gate (`dialogue_recheck` / `silent_cut`) so the
1307
+ * orchestrator resumes from the next sub-step. Requires `pipelines:approve`.
1308
+ */
1309
+ approveSubGate(id, gate) {
1310
+ return this.client.request(
1311
+ "POST",
1312
+ `/v1/pipelines/${encodeURIComponent(id)}/sub-gates/${encodeURIComponent(gate)}/approve`,
1313
+ { body: {} }
1314
+ );
1315
+ }
1316
+ /**
1317
+ * Read a single stage's `status`, `output`, and `critic_feedback`. Useful for
1318
+ * inspecting the script/plan before approving. Requires `pipelines:read`.
1319
+ */
1320
+ getStage(id, stage) {
1321
+ return this.client.request(
1322
+ "GET",
1323
+ `/v1/pipelines/${encodeURIComponent(id)}/stages/${encodeURIComponent(stage)}`
1324
+ );
1325
+ }
1326
+ /**
1327
+ * Assembled timeline — ordered scene composites + durations + audio URLs +
1328
+ * live animate progress. The output a headless caller renders or hands to a
1329
+ * downstream editor. Requires `pipelines:read`.
1330
+ */
1331
+ getTimeline(id) {
1332
+ return this.client.request(
1333
+ "GET",
1334
+ `/v1/pipelines/${encodeURIComponent(id)}/timeline`
1335
+ );
1336
+ }
1337
+ /**
1338
+ * Branch a completed pipeline into a new pipeline that re-runs from the
1339
+ * given stage. The original pipeline's upstream stages and entities are
1340
+ * cloned into the new pipeline; downstream stages are created by the
1341
+ * orchestrator as it advances.
1342
+ *
1343
+ * Requires `pipelines:execute` scope.
1344
+ * The source pipeline must have `status='completed'`.
1345
+ *
1346
+ * @returns 201 with `{ pipelineId, clonedStages, clonedEntities }`.
1347
+ */
1348
+ branch(id, input) {
1349
+ return this.client.request(
1350
+ "POST",
1351
+ `/v1/pipelines/${encodeURIComponent(id)}/branch`,
1352
+ { body: input }
1353
+ );
1354
+ }
1355
+ /**
1356
+ * Send a chat message to the Showrunner Refinement Director (Guided Mode).
1357
+ * Persists user + assistant turns; returns the assistant's reply and an
1358
+ * optional `proposed_change` the user can `applyChatProposal` to commit.
1359
+ *
1360
+ * Requires `pipelines:approve` scope. The pipeline must have
1361
+ * `mode='guided'` and the stage must be `awaiting_approval`.
1362
+ *
1363
+ * Only the Script stage ships a wired specialist in Phase 1D.2b — the other
1364
+ * chat-enabled stages (`shot_list`, `post_merge`) return 501 until 1D.2d.
1365
+ */
1366
+ chatStage(pipelineId, stage, message) {
1367
+ return this.client.request(
1368
+ "POST",
1369
+ `/v1/pipelines/${encodeURIComponent(pipelineId)}/stages/${encodeURIComponent(stage)}/chat`,
1370
+ { body: { message } }
1371
+ );
1372
+ }
1373
+ /**
1374
+ * Accept a proposed change from a prior assistant turn. Routes through
1375
+ * `applyStageEdit` (validates JSON Patch + per-stage schema +
1376
+ * reference-integrity, inserts a new pipeline_stage_attempts row, CAS-flips
1377
+ * the stage to approved, emits `chat:proposal_applied` SSE).
1378
+ *
1379
+ * Requires `pipelines:approve` scope.
1380
+ *
1381
+ * Returns `{ applied: true, attemptId, newOutput }` on success, or
1382
+ * `{ applied: false, error }` on recoverable failures (the backend already
1383
+ * inserted a follow-up assistant turn with a hint). Hard failures
1384
+ * (`patch_invalid`, `stage_not_awaiting`) throw via the standard error
1385
+ * pipeline (HTTP 409).
1386
+ */
1387
+ applyChatProposal(pipelineId, stage, turnId) {
1388
+ return this.client.request(
1389
+ "POST",
1390
+ `/v1/pipelines/${encodeURIComponent(pipelineId)}/stages/${encodeURIComponent(stage)}/chat/turns/${encodeURIComponent(turnId)}/apply`,
1391
+ { body: {} }
1392
+ );
1393
+ }
1394
+ /**
1395
+ * Fetch the chat history for a stage. Returns an empty array when no turns
1396
+ * exist yet (e.g., stage has not been started or the user hasn't sent any
1397
+ * messages). Used by the frontend chat panel on initial mount; subsequent
1398
+ * updates arrive via SSE (`chat:turn` events).
1399
+ *
1400
+ * Requires `pipelines:read` scope.
1401
+ */
1402
+ getStageChat(pipelineId, stage) {
1403
+ return this.client.request(
1404
+ "GET",
1405
+ `/v1/pipelines/${encodeURIComponent(pipelineId)}/stages/${encodeURIComponent(stage)}/chat`
1406
+ );
1407
+ }
1408
+ };
1409
+
1410
+ // src/resources/reduce.ts
1411
+ var ReduceResource = class {
1412
+ constructor(client) {
1413
+ this.client = client;
1414
+ }
1415
+ client;
1416
+ /**
1417
+ * Run the Reduce (fan-in) node directly — useful for scripted batch
1418
+ * scoring, picking the best of N generations outside a workflow, or
1419
+ * one-shot programmatic merges.
1420
+ *
1421
+ * Throws `NodaroError` on 4xx/5xx responses (e.g. `code: "no_valid_inputs"`
1422
+ * with status 400 when every input is empty / whitespace; the underlying
1423
+ * `EmptyInputError` is mapped to a 400 server-side).
1424
+ */
1425
+ run(input) {
1426
+ return this.client.request("POST", "/v1/reduce", {
1427
+ body: {
1428
+ strategyId: input.strategyId,
1429
+ strategyConfig: input.strategyConfig ?? {},
1430
+ inputs: input.inputs,
1431
+ ...input.workflowId !== void 0 ? { workflowId: input.workflowId } : {}
1432
+ }
1433
+ });
1434
+ }
1435
+ };
1436
+
1437
+ // src/resources/prompt-helper.ts
1438
+ var PromptHelperResource = class {
1439
+ constructor(client) {
1440
+ this.client = client;
1441
+ }
1442
+ client;
1443
+ analyze(input) {
1444
+ return this.client.request("POST", "/v1/prompt-helper/wizard", { body: { action: "analyze", ...input } });
1445
+ }
1446
+ generate(input) {
1447
+ return this.client.request("POST", "/v1/prompt-helper/wizard", { body: { action: "generate", ...input } });
1448
+ }
1449
+ enhance(input) {
1450
+ return this.client.request("POST", "/v1/prompt-helper/wizard", { body: { action: "enhance", ...input } });
1451
+ }
1452
+ };
1453
+
1454
+ // src/resources/voices.ts
1455
+ var VoicesResource = class {
1456
+ constructor(client) {
1457
+ this.client = client;
1458
+ }
1459
+ client;
1460
+ /**
1461
+ * List the premade ElevenLabs voices (`GET /v1/voices`). Falls back to a
1462
+ * curated set server-side when no ElevenLabs API key is configured.
1463
+ */
1464
+ async list() {
1465
+ const res = await this.client.request("GET", "/v1/voices");
1466
+ return res.voices;
1467
+ }
1468
+ /**
1469
+ * Search the shared/community Voice Library (`GET /v1/voices/library`). All
1470
+ * params are optional and forwarded as a querystring; `undefined` / `null` /
1471
+ * empty-string values are omitted so the server defaults apply. `hasMore`
1472
+ * drives "load more" pagination.
1473
+ */
1474
+ searchLibrary(params = {}) {
1475
+ const query = {};
1476
+ for (const [k, v] of Object.entries(params)) {
1477
+ if (v !== void 0 && v !== null && v !== "") query[k] = v;
1478
+ }
1479
+ return this.client.request("GET", "/v1/voices/library", { query });
1480
+ }
1481
+ /**
1482
+ * List the signed-in user's voice clones (`GET /v1/voice-clones`). The route
1483
+ * wraps the rows in `{ voiceClones }`; we unwrap to the bare array.
1484
+ */
1485
+ async listClones() {
1486
+ const res = await this.client.request("GET", "/v1/voice-clones");
1487
+ return res.voiceClones;
1488
+ }
1489
+ /**
1490
+ * Clone a voice from an already-uploaded audio URL
1491
+ * (`POST /v1/voice-clones/from-url`). Costs credits. Returns the create
1492
+ * subset of `VoiceClone` (`elevenlabsVoiceId` is the id to use at
1493
+ * text-to-speech time).
1494
+ */
1495
+ createClone(input) {
1496
+ return this.client.request("POST", "/v1/voice-clones/from-url", { body: input });
1497
+ }
1498
+ /** Delete one of the user's voice clones (`DELETE /v1/voice-clones/:id`). */
1499
+ async deleteClone(id) {
1500
+ await this.client.request("DELETE", `/v1/voice-clones/${encodeURIComponent(id)}`);
1501
+ }
1502
+ /**
1503
+ * Replace the voice in a recording — or in a whole talking video — with a
1504
+ * different voice (`POST /v1/voice-changer`). Pass `audioUrl` to revoice
1505
+ * audio→audio, or `videoUrl` to revoice an entire clip (the server demuxes
1506
+ * the audio, runs speech-to-speech, and remuxes onto the original video,
1507
+ * returning the video plus the new audio track). Exactly one of `audioUrl` /
1508
+ * `videoUrl` is required; when both are sent, video wins. `removeBackgroundNoise`
1509
+ * off keeps the music/SFX bed under the new voice; on yields a clean voice-only
1510
+ * result. Costs credits and runs async — poll `jobs.get(jobId)` for the result
1511
+ * (`output_data.videoUrl` + `output_data.audioUrl` in video mode).
1512
+ */
1513
+ change(input) {
1514
+ return this.client.request("POST", "/v1/voice-changer", { body: input });
1515
+ }
1516
+ /**
1517
+ * Recast each detected speaker in a multi-speaker recording to a different
1518
+ * voice (`POST /v1/voice-changer-pro`). `orderedVoices` maps speaker positions to
1519
+ * voices in detection order — speaker 0 → `orderedVoices[0]`, speaker 1 →
1520
+ * `orderedVoices[1]`, etc. Speakers beyond the end of `orderedVoices` keep
1521
+ * their original voice. Each entry is EITHER a bare voice id (premade name or
1522
+ * ElevenLabs UUID) OR a {@link VoiceChangerProVoice} object carrying per-voice
1523
+ * ElevenLabs speech-to-speech settings (stability / similarityBoost / style /
1524
+ * useSpeakerBoost / `seed`) plus a loudness `volumeMode` (and a manual
1525
+ * `volume`). A per-voice `seed` makes that speaker's recast reproducible.
1526
+ *
1527
+ * Pass `audioUrl` for audio-only recast or `videoUrl` to recast the audio
1528
+ * track of a video clip (the server demuxes, recasts, and remuxes).
1529
+ *
1530
+ * Voice and music are ALWAYS separated first — ElevenLabs only ever sees the
1531
+ * isolated vocal stem, never the music bed. `preserveBackground` (default
1532
+ * `true`) only controls whether that music/instrumental stem is mixed back
1533
+ * under the new voices; set it `false` for a clean voice-only result.
1534
+ * `separationQuality` selects the demucs model used for that split: `"fast"`
1535
+ * (default, htdemucs — preserves more of the voice) or `"best"` (htdemucs_ft —
1536
+ * finer separation). `removeBackgroundNoise` additionally denoises the result.
1537
+ * `musicVolumeMode` sets the level of that preserved background (only relevant
1538
+ * when `preserveBackground` is on): `"match"` (default) keeps the original
1539
+ * level, `"normalize"` loudnorms it, `"manual"` uses `musicVolume`%.
1540
+ * `voiceFx` applies a reverb/echo to the COMBINED recast voices BEFORE the
1541
+ * background is mixed back in (effect sits on the voices, not the music bed).
1542
+ *
1543
+ * Cloud-only — costs credits and runs async; poll `jobs.get(jobId)` for the
1544
+ * result (`output_data.videoUrl` + `output_data.audioUrl` in video mode).
1545
+ */
1546
+ recast(input) {
1547
+ return this.client.request("POST", "/v1/voice-changer-pro", { body: input });
1548
+ }
1549
+ };
1550
+
1551
+ // src/resources/credits.ts
1552
+ var MODEL_COSTS_LIMIT = 50;
1553
+ var CreditsResource = class {
1554
+ constructor(client) {
1555
+ this.client = client;
1556
+ }
1557
+ client;
1558
+ /**
1559
+ * `GET /v1/user/credits` → the authenticated user's credit balance and tier
1560
+ * info. Throws `UnauthorizedError` (401) when signed out, and the SDK's
1561
+ * other typed errors on the usual statuses.
1562
+ */
1563
+ async balance() {
1564
+ const res = await this.client.request(
1565
+ "GET",
1566
+ "/v1/user/credits"
1567
+ );
1568
+ return res.data;
1569
+ }
1570
+ /**
1571
+ * `POST /v1/credits/model-costs` → per-identifier credit cost, for editor
1572
+ * cost previews. Capped at the first {@link MODEL_COSTS_LIMIT} identifiers
1573
+ * (the route's request limit). Preserves the `{ data, missing, errors }`
1574
+ * fault-isolation shape verbatim (see {@link ModelCostsResult}).
1575
+ */
1576
+ modelCosts(ids) {
1577
+ return this.client.request("POST", "/v1/credits/model-costs", {
1578
+ body: { models: ids.slice(0, MODEL_COSTS_LIMIT) }
1579
+ });
1580
+ }
1581
+ // NOTE: no `estimate(...)` helper. The backend exposes
1582
+ // `POST /v1/credits/estimate-workflow` (body `{ nodes }` → `{ data: {
1583
+ // totalCredits, nodeCount } }`), but no consumer has a settled shape for it
1584
+ // yet (studio pre-checks `balance >= Σ modelCosts` client-side rather than
1585
+ // calling an estimate endpoint). Adding it now would be inventing an API
1586
+ // surface ahead of a real caller, so it's deliberately omitted — add it when
1587
+ // a consumer needs it, shaped to that need.
1588
+ };
1589
+
1590
+ // src/resources/uploads.ts
1591
+ var UploadsResource = class {
1592
+ constructor(client) {
1593
+ this.client = client;
1594
+ }
1595
+ client;
1596
+ /**
1597
+ * Upload one file (`POST /v1/upload`, multipart — the file rides the `file`
1598
+ * field). The SDK's `request` detects the `FormData` body and lets the
1599
+ * runtime set the multipart boundary. Returns the persisted asset's public
1600
+ * URL + storage metadata (unwraps the `{ data }` envelope). Throws
1601
+ * `StorageExceededError` (413) over the storage cap and the SDK's other typed
1602
+ * errors on the usual statuses.
1603
+ */
1604
+ async upload(file) {
1605
+ const form = new FormData();
1606
+ form.append("file", file);
1607
+ const res = await this.client.request(
1608
+ "POST",
1609
+ "/v1/upload",
1610
+ { body: form }
1611
+ );
1612
+ return res.data;
1613
+ }
1614
+ };
1615
+
1616
+ // src/resources/library.ts
1617
+ var LibraryResource = class {
1618
+ constructor(client) {
1619
+ this.client = client;
1620
+ }
1621
+ client;
1622
+ /**
1623
+ * `GET /v1/library` → a page of the caller's media assets (newest first) plus
1624
+ * a `nextCursor`. Pass the returned `nextCursor` back as `cursor` for the next
1625
+ * page. Filter by `type` and a filename `search`; `owned: true` returns the
1626
+ * full Storage set (uploads + generations), the default only library-saved +
1627
+ * shared items.
1628
+ */
1629
+ list(params = {}) {
1630
+ const qs = new URLSearchParams();
1631
+ if (params.type) qs.set("type", params.type);
1632
+ if (params.search) qs.set("search", params.search);
1633
+ if (params.limit !== void 0) qs.set("limit", String(params.limit));
1634
+ if (params.cursor) qs.set("cursor", params.cursor);
1635
+ if (params.owned !== void 0) qs.set("owned", String(params.owned));
1636
+ const query = qs.toString();
1637
+ return this.client.request("GET", `/v1/library${query ? `?${query}` : ""}`);
1638
+ }
1639
+ };
1640
+
1641
+ // src/resources/node-presets.ts
1642
+ var PresetsResource = class {
1643
+ constructor(client) {
1644
+ this.client = client;
1645
+ }
1646
+ client;
1647
+ /**
1648
+ * `GET /v1/node-presets` → your custom presets, newest first. Pass `nodeType`
1649
+ * (e.g. `"generate-image"`) to filter to one node type.
1650
+ */
1651
+ async list(nodeType) {
1652
+ const qs = nodeType ? `?nodeType=${encodeURIComponent(nodeType)}` : "";
1653
+ const res = await this.client.request("GET", `/v1/node-presets${qs}`);
1654
+ return res.data;
1655
+ }
1656
+ /**
1657
+ * `GET /v1/node-preset-groups` → your preset folders/sections, in display
1658
+ * order. Pass `nodeType` to filter to one node type.
1659
+ */
1660
+ async listGroups(nodeType) {
1661
+ const qs = nodeType ? `?nodeType=${encodeURIComponent(nodeType)}` : "";
1662
+ const res = await this.client.request("GET", `/v1/node-preset-groups${qs}`);
1663
+ return res.data;
1664
+ }
1665
+ /**
1666
+ * `GET /v1/node-presets/factory` → the built-in catalog for `nodeType`. These
1667
+ * ship with the app (no account needed to exist), so they're a good starting
1668
+ * point for "what configs are available".
1669
+ */
1670
+ listFactory(nodeType) {
1671
+ return this.client.request(
1672
+ "GET",
1673
+ `/v1/node-presets/factory?nodeType=${encodeURIComponent(nodeType)}`
1674
+ );
1675
+ }
1676
+ };
1677
+
1678
+ // src/resources/picker-catalogs.ts
1679
+ var PickerCatalogsResource = class {
1680
+ constructor(client) {
1681
+ this.client = client;
1682
+ }
1683
+ client;
1684
+ /** List every parameter-picker node type + its option count. Cached publicly 5 min. */
1685
+ list() {
1686
+ return this.client.request("GET", "/v1/picker-catalogs");
1687
+ }
1688
+ /** Get one picker's catalog of valid values. */
1689
+ get(nodeType, opts = {}) {
1690
+ const qs = new URLSearchParams();
1691
+ if (opts.detail) qs.set("detail", opts.detail);
1692
+ if (opts.category) qs.set("category", opts.category);
1693
+ if (opts.field) qs.set("field", opts.field);
1694
+ const query = qs.toString();
1695
+ return this.client.request(
1696
+ "GET",
1697
+ `/v1/picker-catalogs/${encodeURIComponent(nodeType)}${query ? `?${query}` : ""}`
1698
+ );
1699
+ }
1700
+ };
1701
+
1702
+ // src/resources/community.ts
1703
+ var CommunityResource = class {
1704
+ constructor(client) {
1705
+ this.client = client;
1706
+ }
1707
+ client;
1708
+ /**
1709
+ * `GET /v1/community/browse` → a page of public listings plus a `nextCursor`.
1710
+ * Pass the returned `nextCursor` back as `cursor` to fetch the next page.
1711
+ */
1712
+ browse(params = {}) {
1713
+ const qs = new URLSearchParams();
1714
+ if (params.entityType) qs.set("entityType", params.entityType);
1715
+ if (params.q) qs.set("q", params.q);
1716
+ if (params.category) qs.set("category", params.category);
1717
+ if (params.sort) qs.set("sort", params.sort);
1718
+ if (params.cursor) qs.set("cursor", params.cursor);
1719
+ if (params.limit !== void 0) qs.set("limit", String(params.limit));
1720
+ const query = qs.toString();
1721
+ return this.client.request(
1722
+ "GET",
1723
+ `/v1/community/browse${query ? `?${query}` : ""}`
1724
+ );
1725
+ }
1726
+ /** `GET /v1/community/detail/:slug` → a single listing by its slug. */
1727
+ get(slug) {
1728
+ return this.client.request(
1729
+ "GET",
1730
+ `/v1/community/detail/${encodeURIComponent(slug)}`
1731
+ );
1732
+ }
1733
+ /**
1734
+ * `GET /v1/community/detail/:slug/full` → the full read-only detail (card
1735
+ * identity + the stored public snapshot). Like {@link get}, but includes the
1736
+ * snapshot asset/voice/text blob needed to render the full cross-user view.
1737
+ */
1738
+ getFull(slug) {
1739
+ return this.client.request(
1740
+ "GET",
1741
+ `/v1/community/detail/${encodeURIComponent(slug)}/full`
1742
+ );
1743
+ }
1744
+ /** `GET /v1/community/favorites` → the listings you've favorited. */
1745
+ favorites() {
1746
+ return this.client.request("GET", "/v1/community/favorites");
1747
+ }
1748
+ /**
1749
+ * `POST /v1/community/listings/:id/clone` → copy a listing into your library.
1750
+ * Returns the new asset's `entityType` and `id`. Requires the `assets:write`
1751
+ * scope when called with an OAuth app token.
1752
+ */
1753
+ clone(id, entityType) {
1754
+ return this.client.request(
1755
+ "POST",
1756
+ `/v1/community/listings/${encodeURIComponent(id)}/clone`,
1757
+ { body: { entityType } }
1758
+ );
1759
+ }
1760
+ /**
1761
+ * `POST /v1/community/listings/:id/favorite` → toggle a favorite. Returns the
1762
+ * resulting `favorited` state (`true` after adding, `false` after removing).
1763
+ */
1764
+ favorite(id) {
1765
+ return this.client.request(
1766
+ "POST",
1767
+ `/v1/community/listings/${encodeURIComponent(id)}/favorite`
1768
+ );
1769
+ }
1770
+ /**
1771
+ * `POST /v1/community/listings/:id/report` → flag a listing for moderation.
1772
+ * `reason` must be one of {@link CommunityReportReason}.
1773
+ */
1774
+ report(id, reason) {
1775
+ return this.client.request(
1776
+ "POST",
1777
+ `/v1/community/listings/${encodeURIComponent(id)}/report`,
1778
+ { body: { reason } }
1779
+ );
1780
+ }
1781
+ /**
1782
+ * `POST /v1/admin/community/:entityType/:id/publish` → share one of YOUR
1783
+ * entities to the community, returning the new listing's `slug` + `id`.
1784
+ *
1785
+ * **Requires an admin token** (the route is `requireAdmin`) AND the caller
1786
+ * must own the source entity. Personal/OAuth tokens without admin role get a
1787
+ * 401. For `character` listings, `params.likenessAttestation` must be `true`.
1788
+ */
1789
+ publish(entityType, entityId, params) {
1790
+ return this.client.request(
1791
+ "POST",
1792
+ `/v1/admin/community/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}/publish`,
1793
+ { body: params }
1794
+ );
1795
+ }
1796
+ /**
1797
+ * `DELETE /v1/admin/community/listings/:id` → unshare (deactivate) a listing
1798
+ * you published. **Requires an admin token** (the route is `requireAdmin`).
1799
+ */
1800
+ unpublish(listingId) {
1801
+ return this.client.request(
1802
+ "DELETE",
1803
+ `/v1/admin/community/listings/${encodeURIComponent(listingId)}`
1804
+ );
1805
+ }
1806
+ /**
1807
+ * `GET /v1/admin/community/by-source/:entityType/:sourceId` → look up YOUR
1808
+ * existing listing (if any) for a source entity. Returns `{ data: null }`
1809
+ * when the entity hasn't been shared. **Requires an admin token** (the route
1810
+ * is `requireAdmin`); only returns listings created by the caller.
1811
+ */
1812
+ sharedListing(entityType, sourceId) {
1813
+ return this.client.request(
1814
+ "GET",
1815
+ `/v1/admin/community/by-source/${encodeURIComponent(entityType)}/${encodeURIComponent(sourceId)}`
1816
+ );
1817
+ }
1818
+ };
1819
+
1820
+ // src/client.ts
1821
+ var NodaroClient = class {
1822
+ baseUrl;
1823
+ auth;
1824
+ timeoutMs;
1825
+ fetchOverride;
1826
+ /**
1827
+ * Resolved lazily so consumers can swap `globalThis.fetch` after the
1828
+ * client has been constructed (e.g. test mocks). Always rebound to the
1829
+ * global object — native fetch throws "Illegal invocation" when its
1830
+ * `this` is anything else.
1831
+ */
1832
+ get fetch() {
1833
+ return this.fetchOverride ?? globalThis.fetch.bind(globalThis);
1834
+ }
1835
+ workflows;
1836
+ projects;
1837
+ jobs;
1838
+ executions;
1839
+ nodes;
1840
+ developerApps;
1841
+ oauth;
1842
+ apps;
1843
+ characters;
1844
+ locations;
1845
+ objects;
1846
+ creatures;
1847
+ pipelines;
1848
+ reduce;
1849
+ promptHelper;
1850
+ voices;
1851
+ credits;
1852
+ uploads;
1853
+ library;
1854
+ presets;
1855
+ pickerCatalogs;
1856
+ community;
1857
+ constructor(opts) {
1858
+ this.baseUrl = opts.baseUrl.replace(/\/$/, "");
1859
+ this.auth = opts.auth;
1860
+ this.fetchOverride = opts.fetch;
1861
+ this.timeoutMs = opts.timeoutMs ?? 6e4;
1862
+ this.workflows = new WorkflowsResource(this);
1863
+ this.projects = new ProjectsResource(this);
1864
+ this.jobs = new JobsResource(this);
1865
+ this.executions = new ExecutionsResource(this);
1866
+ this.nodes = new NodesResource(this);
1867
+ this.developerApps = new DeveloperAppsResource(this);
1868
+ this.oauth = new OAuthResource(this);
1869
+ this.apps = new AppsResource(this);
1870
+ this.characters = new CharactersResource(this);
1871
+ this.locations = new LocationsResource(this);
1872
+ this.objects = new ObjectsResource(this);
1873
+ this.creatures = new CreaturesResource(this);
1874
+ this.pipelines = new PipelinesResource(this);
1875
+ this.reduce = new ReduceResource(this);
1876
+ this.promptHelper = new PromptHelperResource(this);
1877
+ this.voices = new VoicesResource(this);
1878
+ this.credits = new CreditsResource(this);
1879
+ this.uploads = new UploadsResource(this);
1880
+ this.library = new LibraryResource(this);
1881
+ this.presets = new PresetsResource(this);
1882
+ this.pickerCatalogs = new PickerCatalogsResource(this);
1883
+ this.community = new CommunityResource(this);
1884
+ }
1885
+ async request(method, path, options = {}) {
1886
+ const url = this.buildUrl(path, options.query);
1887
+ const token = await this.auth.getToken();
1888
+ const isFormData = typeof FormData !== "undefined" && options.body instanceof FormData;
1889
+ const headers = {
1890
+ ...isFormData ? {} : { "Content-Type": "application/json" },
1891
+ ...options.headers ?? {}
1892
+ };
1893
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1894
+ const ac = new AbortController();
1895
+ const timeoutId = setTimeout(() => ac.abort(), this.timeoutMs);
1896
+ if (options.signal) {
1897
+ options.signal.addEventListener("abort", () => ac.abort(), { once: true });
1898
+ }
1899
+ try {
1900
+ const res = await this.fetch(url, {
1901
+ method,
1902
+ headers,
1903
+ body: options.body === void 0 ? void 0 : isFormData ? options.body : JSON.stringify(options.body),
1904
+ signal: ac.signal
1905
+ });
1906
+ if (!res.ok) {
1907
+ let errBody = {};
1908
+ try {
1909
+ errBody = await res.json();
1910
+ } catch {
1911
+ }
1912
+ throwFromResponse(res.status, errBody);
1913
+ }
1914
+ if (res.status === 204) return void 0;
1915
+ return await res.json();
1916
+ } finally {
1917
+ clearTimeout(timeoutId);
1918
+ }
1919
+ }
1920
+ /**
1921
+ * `GET /v1/me` → the authenticated user's identity (see {@link UserIdentity}).
1922
+ * Unwraps the `{ data }` envelope. Throws `UnauthorizedError` (401) when the
1923
+ * token is missing/invalid, and the SDK's other typed errors as usual.
1924
+ */
1925
+ async me() {
1926
+ const res = await this.request("GET", "/v1/me");
1927
+ return res.data;
1928
+ }
1929
+ buildUrl(path, query) {
1930
+ const base = this.baseUrl || (typeof window !== "undefined" ? window.location.origin : "http://placeholder");
1931
+ const url = new URL(path, base);
1932
+ const fullUrl = this.baseUrl ? url.toString() : url.pathname + url.search;
1933
+ if (query) {
1934
+ const u = new URL(this.baseUrl ? fullUrl : fullUrl, base);
1935
+ for (const [k, v] of Object.entries(query)) {
1936
+ if (v !== void 0) u.searchParams.set(k, String(v));
1937
+ }
1938
+ return this.baseUrl ? u.toString() : u.pathname + u.search;
1939
+ }
1940
+ return fullUrl;
1941
+ }
1942
+ };
1943
+ function createClient(opts) {
1944
+ return new NodaroClient(opts);
1945
+ }
1946
+
1947
+ // src/auth.ts
1948
+ var StaticTokenAuth = class {
1949
+ constructor(token) {
1950
+ this.token = token;
1951
+ }
1952
+ token;
1953
+ async getToken() {
1954
+ return this.token;
1955
+ }
1956
+ };
1957
+ var CallbackAuth = class {
1958
+ constructor(fn) {
1959
+ this.fn = fn;
1960
+ }
1961
+ fn;
1962
+ async getToken() {
1963
+ return this.fn();
1964
+ }
1965
+ };
1966
+ function supabaseAuth(supabase) {
1967
+ return {
1968
+ async getToken() {
1969
+ const { data } = await supabase.auth.getSession();
1970
+ return data.session?.access_token ?? null;
1971
+ }
1972
+ };
1973
+ }
1974
+
1975
+ export { AppsResource, CREATURE_ASSET_TYPES, CallbackAuth, CharactersResource, CommunityResource, CreaturesResource, CreditsResource, DeveloperAppsResource, ExecutionsResource, ForbiddenError, InsufficientCreditsError, JobAbortedError, JobFailedError, JobTimeoutError, JobsResource, LibraryResource, LocationsResource, NodaroClient, NodaroError, NodesResource, NotFoundError, OAuthResource, ObjectsResource, PickerCatalogsResource, PipelinesResource, PresetsResource, ProjectsResource, PromptHelperResource, RateLimitedError, ReduceResource, StaticTokenAuth, StorageExceededError, UnauthorizedError, UploadsResource, VoicesResource, WorkflowsResource, buildPersonSeedPrompt, createClient, supabaseAuth, throwFromResponse };
1976
+ //# sourceMappingURL=index.js.map
1977
+ //# sourceMappingURL=index.js.map