@curvet/sdk 0.1.0 → 0.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@curvet/sdk` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0] - 2026-06-27
9
+
10
+ ### Added
11
+ - **Pollable workflow runs** for long workflows (video/audio/3D nodes) — no more
12
+ one long-lived HTTP call:
13
+ - `workflows.submit(id, params)` — start a run, returns a `runId` immediately.
14
+ - `workflows.runs.retrieve(runId)` — live status: `currentNode`, `progress`,
15
+ per-node history, and the final `result`.
16
+ - `workflows.runAndPoll(id, params, { onProgress })` — submit + auto-poll to
17
+ completion (mirrors `video.generate`).
18
+ - `WorkflowRunFailedError` and `WorkflowRunTimeoutError` (both carry `runId`).
19
+ - `examples/pollable-workflow.ts`.
20
+
21
+ ### Notes
22
+ - `workflows.run()` (synchronous) is unchanged.
23
+ - Requires the matching backend (media-node execution + pollable run endpoints).
24
+
25
+ ## [0.2.1] - 2026-06-20
26
+
27
+ ### Added
28
+ - Additional resources: `audio.generate`/`submit`, `threeD.generate`/`submit`,
29
+ `analytics.get`, `workflows.run` (JSON or multipart file inputs), `food.*`
30
+ (list/search/recommendations), and `voice.stt`.
31
+ - `FormData` (multipart) support in the HTTP layer for file uploads.
32
+
33
+ ## [0.1.0] - 2026-06-19
34
+
35
+ ### Added
36
+ - Initial release. One typed client over the Curvet Playground API:
37
+ `chat.create`, `image.generate`, `video.generate`/`submit` with **async
38
+ auto-polling**, `jobs.retrieve`, `models.list`, `balance.get`.
39
+ - Typed error taxonomy (`AuthError`, `InsufficientBalanceError`, `RateLimitError`,
40
+ `JobFailedError`, …) and automatic retry/backoff on 429/5xx.
41
+ - Live model catalog (never hardcoded). Ships ESM + CJS + type declarations.
42
+
43
+ [0.3.0]: https://github.com/Curvet-in/curvet-sdk/releases/tag/v0.3.0
44
+ [0.2.1]: https://github.com/Curvet-in/curvet-sdk/releases/tag/v0.2.1
45
+ [0.1.0]: https://github.com/Curvet-in/curvet-sdk/releases/tag/v0.1.0
package/README.md CHANGED
@@ -83,11 +83,57 @@ if (job.status !== "completed") {
83
83
  const status = await curvet.jobs.retrieve(job.jobId!);
84
84
  ```
85
85
 
86
- ### Models & balance
86
+ ### Audio & 3D (async, same as video)
87
+
88
+ ```ts
89
+ const audio = await curvet.audio.generate({ model: "fish-audio", prompt: "Hello there" });
90
+ const mesh = await curvet.threeD.generate({ model: "meshy-3d", prompt: "a ceramic mug" });
91
+ console.log(audio.mediaUrl, mesh.mediaUrl);
92
+ ```
93
+
94
+ ### Models, balance & analytics
87
95
 
88
96
  ```ts
89
97
  const chatModels = await curvet.models.list({ type: "chat" });
90
98
  const balance = await curvet.balance.get();
99
+ const analytics = await curvet.analytics.get({ startDate: "2026-01-01", endDate: "2026-02-01" });
100
+ ```
101
+
102
+ ### Workflows
103
+
104
+ ```ts
105
+ // JSON inputs:
106
+ const out = await curvet.workflows.run("workflowId", { inputs: { topic: "ai" } });
107
+
108
+ // With file inputs (multipart, handled for you):
109
+ await curvet.workflows.run("workflowId", {
110
+ inputs: { caption: "hello" },
111
+ files: { image: new Blob([bytes], { type: "image/png" }) },
112
+ });
113
+ ```
114
+
115
+ For long workflows (video/audio/3D nodes), use the **pollable** API — submit
116
+ and auto-poll to completion with live progress, instead of one long HTTP call:
117
+
118
+ ```ts
119
+ const run = await curvet.workflows.runAndPoll(
120
+ "workflowId",
121
+ { inputs: { topic: "ai" } },
122
+ { onProgress: (r) => console.log(r.status, r.progress + "%", r.currentNode?.label) },
123
+ );
124
+ console.log(run.result);
125
+
126
+ // Or fire-and-forget + poll yourself:
127
+ const { runId } = await curvet.workflows.submit("workflowId", { inputs: {} });
128
+ const status = await curvet.workflows.runs.retrieve(runId);
129
+ ```
130
+
131
+ ### Food & speech-to-text
132
+
133
+ ```ts
134
+ const dishes = await curvet.food.search("paneer", { limit: 5 });
135
+ const stt = await curvet.voice.stt({ audio: audioBytes, filename: "clip.wav" });
136
+ console.log(stt.text);
91
137
  ```
92
138
 
93
139
  ## Errors
@@ -155,6 +201,32 @@ npm run build # ESM + CJS + d.ts via tsup
155
201
  CURVET_TEST_APP_KEY=cvt_app_xxx npm test
156
202
  ```
157
203
 
204
+ ## Releasing
205
+
206
+ Publishing happens locally with the release script — no npm token, no CI.
207
+ `npm publish` prompts for your passkey/2FA, which you approve in the browser.
208
+
209
+ ```bash
210
+ ./scripts/release.sh # publish the current package.json version
211
+ ./scripts/release.sh patch # bump patch, then publish (0.2.0 -> 0.2.1)
212
+ ./scripts/release.sh minor # bump minor, then publish
213
+ ./scripts/release.sh major # bump major, then publish
214
+ ```
215
+
216
+ The script validates (typecheck + tests + build) before bumping, tags the
217
+ version, publishes (you approve the passkey prompt), then pushes the tag and
218
+ commit to GitHub. The order is deliberate — if the publish is cancelled,
219
+ nothing is pushed; just re-run `npm publish && git push --follow-tags origin main`
220
+ to finish.
221
+
222
+ > A tokenless CI alternative using npm Trusted Publishing (OIDC) is also included
223
+ > at [`.github/workflows/publish.yml`](.github/workflows/publish.yml) for when
224
+ > GitHub Actions is available.
225
+
226
+ ## Changelog
227
+
228
+ See [CHANGELOG.md](CHANGELOG.md) for release notes.
229
+
158
230
  ## License
159
231
 
160
232
  MIT
package/dist/index.cjs CHANGED
@@ -21,6 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  APIError: () => APIError,
24
+ Analytics: () => Analytics,
25
+ Audio: () => Audio,
24
26
  AuthError: () => AuthError,
25
27
  BadRequestError: () => BadRequestError,
26
28
  Balance: () => Balance,
@@ -29,17 +31,25 @@ __export(index_exports, {
29
31
  Curvet: () => Curvet,
30
32
  CurvetError: () => CurvetError,
31
33
  DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
34
+ Food: () => Food,
32
35
  Images: () => Images,
33
36
  InsufficientBalanceError: () => InsufficientBalanceError,
34
37
  Job: () => Job,
35
38
  JobFailedError: () => JobFailedError,
36
39
  JobTimeoutError: () => JobTimeoutError,
37
40
  Jobs: () => Jobs,
41
+ MediaResource: () => MediaResource,
38
42
  Models: () => Models,
39
43
  NotFoundError: () => NotFoundError,
40
44
  PermissionError: () => PermissionError,
41
45
  RateLimitError: () => RateLimitError,
42
- Video: () => Video
46
+ ThreeD: () => ThreeD,
47
+ Video: () => Video,
48
+ Voice: () => Voice,
49
+ WorkflowRunFailedError: () => WorkflowRunFailedError,
50
+ WorkflowRunTimeoutError: () => WorkflowRunTimeoutError,
51
+ WorkflowRuns: () => WorkflowRuns,
52
+ Workflows: () => Workflows
43
53
  });
44
54
  module.exports = __toCommonJS(index_exports);
45
55
 
@@ -82,6 +92,18 @@ var JobTimeoutError = class extends CurvetError {
82
92
  this.jobId = jobId;
83
93
  }
84
94
  };
95
+ var WorkflowRunFailedError = class extends CurvetError {
96
+ constructor(message, runId, opts = {}) {
97
+ super(message, opts);
98
+ this.runId = runId;
99
+ }
100
+ };
101
+ var WorkflowRunTimeoutError = class extends CurvetError {
102
+ constructor(message, runId, opts = {}) {
103
+ super(message, opts);
104
+ this.runId = runId;
105
+ }
106
+ };
85
107
  function errorFromResponse(status, body, requestId, headers) {
86
108
  const b = body ?? {};
87
109
  const message = typeof b.error === "string" ? b.error : `HTTP ${status}`;
@@ -170,8 +192,12 @@ var HttpClient = class {
170
192
  };
171
193
  let payload;
172
194
  if (body !== void 0) {
173
- headers["content-type"] = "application/json";
174
- payload = JSON.stringify(body);
195
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
196
+ payload = body;
197
+ } else {
198
+ headers["content-type"] = "application/json";
199
+ payload = JSON.stringify(body);
200
+ }
175
201
  }
176
202
  let attempt = 0;
177
203
  for (; ; ) {
@@ -405,20 +431,17 @@ var Job = class {
405
431
  }
406
432
  };
407
433
 
408
- // src/resources/video.ts
409
- var Video = class {
410
- constructor(client, defaults, path = "/video") {
434
+ // src/resources/media.ts
435
+ var MediaResource = class {
436
+ constructor(client, defaults, path) {
411
437
  this.client = client;
412
438
  this.defaults = defaults;
413
439
  this.path = path;
414
440
  }
415
441
  /**
416
- * Submit a job WITHOUT polling. Returns once the server responds either the
417
- * 200 fast-path (already done) or a 202 with a jobId.
418
- *
419
- * The media POST long-polls server-side and can block well past a normal
420
- * request timeout, so we default its timeout to the poll budget and disable
421
- * auto-retry (a retried POST would enqueue a duplicate, double-charged job).
442
+ * Submit WITHOUT polling. The media POST long-polls server-side and can block
443
+ * well past a normal request timeout, so we default its timeout to the poll
444
+ * budget and disable auto-retry (a retried POST would enqueue a duplicate job).
422
445
  */
423
446
  async submit(params, options) {
424
447
  const reqOptions = {
@@ -434,10 +457,7 @@ var Video = class {
434
457
  });
435
458
  return normalizeMediaPost(body);
436
459
  }
437
- /**
438
- * Submit and resolve to the finished media. Handles the 200-vs-202 split and
439
- * polls `/jobs/:id` internally. Throws JobFailedError / JobTimeoutError.
440
- */
460
+ /** Submit and resolve to the finished media (auto-polls /jobs/:id). */
441
461
  async generate(params, options) {
442
462
  const submitted = await this.submit(params, options);
443
463
  if (submitted.status === "completed" || submitted.status === "failed") {
@@ -458,6 +478,27 @@ var Video = class {
458
478
  }
459
479
  };
460
480
 
481
+ // src/resources/video.ts
482
+ var Video = class extends MediaResource {
483
+ constructor(client, defaults) {
484
+ super(client, defaults, "/video");
485
+ }
486
+ };
487
+
488
+ // src/resources/audio.ts
489
+ var Audio = class extends MediaResource {
490
+ constructor(client, defaults) {
491
+ super(client, defaults, "/audio");
492
+ }
493
+ };
494
+
495
+ // src/resources/threeD.ts
496
+ var ThreeD = class extends MediaResource {
497
+ constructor(client, defaults) {
498
+ super(client, defaults, "/3d");
499
+ }
500
+ };
501
+
461
502
  // src/resources/models.ts
462
503
  var Models = class {
463
504
  constructor(client, cacheTtlMs = 6e4) {
@@ -510,6 +551,227 @@ var Balance = class {
510
551
  }
511
552
  };
512
553
 
554
+ // src/resources/analytics.ts
555
+ var Analytics = class {
556
+ constructor(client) {
557
+ this.client = client;
558
+ }
559
+ /** Usage analytics for the app, optionally bounded by a date range. */
560
+ async get(params = {}) {
561
+ const { startDate, endDate, ...options } = params;
562
+ const body = await this.client.request({
563
+ method: "GET",
564
+ path: "/analytics",
565
+ query: { startDate, endDate },
566
+ options
567
+ });
568
+ return body.analytics;
569
+ }
570
+ };
571
+
572
+ // src/resources/workflows.ts
573
+ function normalizeRun(body) {
574
+ const b = body ?? {};
575
+ return {
576
+ runId: b.runId,
577
+ status: b.status,
578
+ progress: b.progress,
579
+ totalNodes: b.totalNodes,
580
+ completedNodeCount: b.completedNodeCount,
581
+ currentNode: b.currentNode ?? null,
582
+ nodesExecuted: b.nodesExecuted,
583
+ result: b.result,
584
+ error: b.error ?? null,
585
+ startTime: b.startTime,
586
+ endTime: b.endTime,
587
+ raw: body
588
+ };
589
+ }
590
+ function buildBody(params, extra = {}) {
591
+ const hasFiles = params.files && Object.keys(params.files).length > 0;
592
+ if (hasFiles) {
593
+ const form = new FormData();
594
+ form.append("inputs", JSON.stringify(params.inputs ?? {}));
595
+ if (params.includeFullState !== void 0) {
596
+ form.append("includeFullState", String(params.includeFullState));
597
+ }
598
+ for (const [k, v] of Object.entries(extra)) form.append(k, String(v));
599
+ for (const [field, file] of Object.entries(params.files)) {
600
+ form.append(field, file);
601
+ }
602
+ return form;
603
+ }
604
+ return {
605
+ inputs: params.inputs ?? {},
606
+ includeFullState: params.includeFullState,
607
+ ...extra
608
+ };
609
+ }
610
+ var WorkflowRuns = class {
611
+ constructor(client) {
612
+ this.client = client;
613
+ }
614
+ /** Fetch the current status of an async run once (no polling). */
615
+ async retrieve(runId, options) {
616
+ const body = await this.client.request({
617
+ method: "GET",
618
+ path: `/workflows/runs/${encodeURIComponent(runId)}`,
619
+ options
620
+ });
621
+ return normalizeRun(body);
622
+ }
623
+ };
624
+ var Workflows = class {
625
+ constructor(client) {
626
+ this.client = client;
627
+ this.runs = new WorkflowRuns(client);
628
+ }
629
+ /**
630
+ * Execute a workflow synchronously (blocks until it finishes). Best for short
631
+ * workflows; for long ones (video/audio/3D nodes) prefer `runAndPoll`.
632
+ * Sends JSON, or multipart/form-data when file inputs are provided.
633
+ */
634
+ async run(id, params = {}, options) {
635
+ const reqOptions = { ...options, maxRetries: options?.maxRetries ?? 0 };
636
+ return this.client.request({
637
+ method: "POST",
638
+ path: `/workflows/${encodeURIComponent(id)}/run`,
639
+ body: buildBody(params),
640
+ options: reqOptions
641
+ });
642
+ }
643
+ /**
644
+ * Submit a workflow in async (pollable) mode — returns immediately with a
645
+ * runId. Poll `runs.retrieve(runId)` for status, or use `runAndPoll`.
646
+ */
647
+ async submit(id, params = {}, options) {
648
+ const reqOptions = { ...options, maxRetries: options?.maxRetries ?? 0 };
649
+ const res = await this.client.request({
650
+ method: "POST",
651
+ path: `/workflows/${encodeURIComponent(id)}/run`,
652
+ body: buildBody(params, { async: true }),
653
+ options: reqOptions
654
+ });
655
+ return { runId: res?.runId, status: res?.status ?? "running", raw: res };
656
+ }
657
+ /**
658
+ * Submit and poll to completion. Resolves with the completed run (including
659
+ * `result`), reporting progress via `onProgress`. Throws WorkflowRunFailedError
660
+ * on failure, WorkflowRunTimeoutError on timeout.
661
+ */
662
+ async runAndPoll(id, params = {}, opts = {}) {
663
+ const submitted = await this.submit(id, params, opts);
664
+ if (!submitted.runId) {
665
+ throw new CurvetError("Workflow submit did not return a runId", {
666
+ raw: submitted.raw
667
+ });
668
+ }
669
+ const intervalMs = opts.pollIntervalMs ?? 2500;
670
+ const timeoutMs = opts.pollTimeoutMs ?? 3e5;
671
+ let run;
672
+ try {
673
+ run = await pollUntil(
674
+ () => this.runs.retrieve(submitted.runId, { signal: opts.signal }),
675
+ {
676
+ intervalMs,
677
+ timeoutMs,
678
+ signal: opts.signal,
679
+ isTerminal: (r) => r.status === "completed" || r.status === "failed" || r.status === "stopped",
680
+ onTick: (r) => opts.onProgress?.(r)
681
+ }
682
+ );
683
+ } catch (e) {
684
+ if (e instanceof PollTimeoutError) {
685
+ throw new WorkflowRunTimeoutError(
686
+ `Workflow run ${submitted.runId} did not finish within ${timeoutMs}ms`,
687
+ submitted.runId
688
+ );
689
+ }
690
+ throw e;
691
+ }
692
+ if (run.status === "failed" || run.status === "stopped") {
693
+ throw new WorkflowRunFailedError(
694
+ run.error || `Workflow run ${submitted.runId} ${run.status}`,
695
+ submitted.runId,
696
+ { raw: run.raw }
697
+ );
698
+ }
699
+ return run;
700
+ }
701
+ };
702
+
703
+ // src/resources/food.ts
704
+ var Food = class {
705
+ constructor(client) {
706
+ this.client = client;
707
+ }
708
+ /** List dishes (default limit 20). */
709
+ async list(opts) {
710
+ const { limit, ...options } = opts ?? {};
711
+ const body = await this.client.request({
712
+ method: "GET",
713
+ path: "/food",
714
+ query: { limit },
715
+ options
716
+ });
717
+ return body.data;
718
+ }
719
+ /** Full-text search for dishes. */
720
+ async search(query, opts) {
721
+ const { limit, ...options } = opts ?? {};
722
+ const body = await this.client.request({
723
+ method: "GET",
724
+ path: "/food/search",
725
+ query: { q: query, limit },
726
+ options
727
+ });
728
+ return body.data;
729
+ }
730
+ /** Natural-language dish recommendations. */
731
+ async recommendations(prompt, options) {
732
+ const body = await this.client.request({
733
+ method: "POST",
734
+ path: "/food/recommendations",
735
+ body: { prompt },
736
+ options
737
+ });
738
+ return body.data;
739
+ }
740
+ };
741
+
742
+ // src/resources/voice.ts
743
+ var Voice = class {
744
+ constructor(client) {
745
+ this.client = client;
746
+ }
747
+ async stt(params, options) {
748
+ const form = new FormData();
749
+ form.append("audio", toBlob(params.audio), params.filename ?? "audio");
750
+ if (params.provider) form.append("provider", params.provider);
751
+ if (params.model) form.append("model", params.model);
752
+ if (params.prompt) form.append("prompt", params.prompt);
753
+ if (params.languageCode) form.append("languageCode", params.languageCode);
754
+ if (params.allowFallback !== void 0) {
755
+ form.append("allowFallback", String(params.allowFallback));
756
+ }
757
+ const reqOptions = {
758
+ ...options,
759
+ timeout: options?.timeout ?? 12e4,
760
+ maxRetries: options?.maxRetries ?? 0
761
+ };
762
+ return this.client.request({
763
+ method: "POST",
764
+ path: "/voice/stt/public",
765
+ body: form,
766
+ options: reqOptions
767
+ });
768
+ }
769
+ };
770
+ function toBlob(audio) {
771
+ if (typeof Blob !== "undefined" && audio instanceof Blob) return audio;
772
+ return new Blob([audio]);
773
+ }
774
+
513
775
  // src/client.ts
514
776
  var DEFAULT_BASE_URL = "https://curvet.ai/api/v1/playground";
515
777
  var Curvet = class {
@@ -526,13 +788,16 @@ var Curvet = class {
526
788
  "No fetch implementation available. Use Node 18+ or pass { fetch }."
527
789
  );
528
790
  }
529
- const client = new HttpClient({
791
+ const playgroundBase = options.baseURL ?? DEFAULT_BASE_URL;
792
+ const v1Base = playgroundBase.replace(/\/playground\/?$/, "");
793
+ const shared = {
530
794
  appKey,
531
- baseURL: options.baseURL ?? DEFAULT_BASE_URL,
532
795
  timeout: options.timeout ?? 6e4,
533
796
  maxRetries: options.maxRetries ?? 2,
534
797
  fetch: fetchImpl
535
- });
798
+ };
799
+ const client = new HttpClient({ ...shared, baseURL: playgroundBase });
800
+ const v1Client = new HttpClient({ ...shared, baseURL: v1Base });
536
801
  const jobDefaults = {
537
802
  pollIntervalMs: options.defaultPollIntervalMs ?? 2500,
538
803
  pollTimeoutMs: options.defaultPollTimeoutMs ?? 18e4
@@ -541,8 +806,14 @@ var Curvet = class {
541
806
  this.image = new Images(client);
542
807
  this.jobs = new Jobs(client, jobDefaults);
543
808
  this.video = new Video(client, jobDefaults);
809
+ this.audio = new Audio(client, jobDefaults);
810
+ this.threeD = new ThreeD(client, jobDefaults);
544
811
  this.models = new Models(client);
545
812
  this.balance = new Balance(client);
813
+ this.analytics = new Analytics(client);
814
+ this.workflows = new Workflows(client);
815
+ this.food = new Food(v1Client);
816
+ this.voice = new Voice(v1Client);
546
817
  }
547
818
  };
548
819
  function envKey() {
@@ -555,6 +826,8 @@ function defaultFetch() {
555
826
  // Annotate the CommonJS export names for ESM import in node:
556
827
  0 && (module.exports = {
557
828
  APIError,
829
+ Analytics,
830
+ Audio,
558
831
  AuthError,
559
832
  BadRequestError,
560
833
  Balance,
@@ -563,16 +836,24 @@ function defaultFetch() {
563
836
  Curvet,
564
837
  CurvetError,
565
838
  DEFAULT_BASE_URL,
839
+ Food,
566
840
  Images,
567
841
  InsufficientBalanceError,
568
842
  Job,
569
843
  JobFailedError,
570
844
  JobTimeoutError,
571
845
  Jobs,
846
+ MediaResource,
572
847
  Models,
573
848
  NotFoundError,
574
849
  PermissionError,
575
850
  RateLimitError,
576
- Video
851
+ ThreeD,
852
+ Video,
853
+ Voice,
854
+ WorkflowRunFailedError,
855
+ WorkflowRunTimeoutError,
856
+ WorkflowRuns,
857
+ Workflows
577
858
  });
578
859
  //# sourceMappingURL=index.cjs.map