@comfyorg/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +236 -0
  2. package/dist/index.d.ts +27 -0
  3. package/dist/index.js +27 -0
  4. package/dist/low/errors.d.ts +73 -0
  5. package/dist/low/errors.js +104 -0
  6. package/dist/low/generated/index.d.ts +1 -0
  7. package/dist/low/generated/index.js +2 -0
  8. package/dist/low/generated/types.gen.d.ts +528 -0
  9. package/dist/low/generated/types.gen.js +2 -0
  10. package/dist/low/generated/zod.gen.d.ts +455 -0
  11. package/dist/low/generated/zod.gen.js +204 -0
  12. package/dist/low/index.d.ts +21 -0
  13. package/dist/low/index.js +21 -0
  14. package/dist/low/models.d.ts +47 -0
  15. package/dist/low/models.js +16 -0
  16. package/dist/low/sse.d.ts +24 -0
  17. package/dist/low/sse.js +44 -0
  18. package/dist/low/transport.d.ts +128 -0
  19. package/dist/low/transport.js +285 -0
  20. package/dist/sdk/abortable-sleep.d.ts +13 -0
  21. package/dist/sdk/abortable-sleep.js +31 -0
  22. package/dist/sdk/assets.d.ts +74 -0
  23. package/dist/sdk/assets.js +200 -0
  24. package/dist/sdk/client.d.ts +70 -0
  25. package/dist/sdk/client.js +120 -0
  26. package/dist/sdk/core.d.ts +33 -0
  27. package/dist/sdk/core.js +89 -0
  28. package/dist/sdk/events.d.ts +60 -0
  29. package/dist/sdk/events.js +84 -0
  30. package/dist/sdk/exceptions.d.ts +79 -0
  31. package/dist/sdk/exceptions.js +114 -0
  32. package/dist/sdk/hashing.d.ts +14 -0
  33. package/dist/sdk/hashing.js +28 -0
  34. package/dist/sdk/index.d.ts +13 -0
  35. package/dist/sdk/index.js +12 -0
  36. package/dist/sdk/jobs.d.ts +47 -0
  37. package/dist/sdk/jobs.js +145 -0
  38. package/dist/sdk/outputs.d.ts +26 -0
  39. package/dist/sdk/outputs.js +48 -0
  40. package/dist/sdk/workflows.d.ts +32 -0
  41. package/dist/sdk/workflows.js +45 -0
  42. package/package.json +52 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Idiomatic `sdk` exceptions.
3
+ *
4
+ * These wrap the protocol-level `low.ApiError` codes with names an
5
+ * integrator catches directly (`JobFailed`, `QueueFull`, ...). `toSdkError`
6
+ * maps a raised `ApiError` to the right subclass; anything unmapped stays a
7
+ * `ComfyError` carrying the original code. Mirrors `comfy_sdk.exceptions`
8
+ * in the Python SDK.
9
+ */
10
+ import { ApiError } from "../low/index.js";
11
+ import type { JobError } from "../low/index.js";
12
+ export interface ComfyErrorOptions {
13
+ code?: string;
14
+ httpStatus?: number;
15
+ details?: Record<string, unknown> | null;
16
+ }
17
+ export declare class ComfyError extends Error {
18
+ readonly code?: string;
19
+ readonly httpStatus?: number;
20
+ readonly details: Record<string, unknown> | null;
21
+ constructor(message: string, options?: ComfyErrorOptions);
22
+ }
23
+ /** The surface rejected the request for lack of a valid key. Comfy Cloud
24
+ * and serverless require a key; a self-hosted proxy needs none. */
25
+ export declare class Unauthorized extends ComfyError {
26
+ }
27
+ export declare class Forbidden extends ComfyError {
28
+ }
29
+ export declare class NotFound extends ComfyError {
30
+ }
31
+ /** Structural/validation failure; `details` carries per-node errors. */
32
+ export declare class InvalidWorkflow extends ComfyError {
33
+ }
34
+ /** UI-export JSON was submitted instead of the API-format graph. */
35
+ export declare class WorkflowFormatUi extends InvalidWorkflow {
36
+ }
37
+ /** A `core/ASSET` reference was not usable (unknown/unscanned/not owned). */
38
+ export declare class MissingAsset extends ComfyError {
39
+ }
40
+ /** Uploaded bytes did not match the declared `expectedHash`. */
41
+ export declare class HashMismatch extends ComfyError {
42
+ }
43
+ /** from-hash / existence probe found no blob the caller can mint from. */
44
+ export declare class BlobNotFound extends ComfyError {
45
+ }
46
+ /** The idempotency key was reused. Keys are single-use (reject-on-duplicate,
47
+ * no replay): any second request with the same key — a retry, a concurrent
48
+ * duplicate, or the same key with a different body — is rejected. */
49
+ export declare class IdempotencyKeyReuse extends ComfyError {
50
+ }
51
+ export declare class InsufficientCredits extends ComfyError {
52
+ }
53
+ /** Backpressure: the queue is full. `retryAfter` is seconds to wait. */
54
+ export declare class QueueFull extends ComfyError {
55
+ readonly retryAfter: number;
56
+ constructor(message: string, options: ComfyErrorOptions & {
57
+ retryAfter: number;
58
+ });
59
+ }
60
+ /**
61
+ * A job reached a non-success terminal state. `error` carries the
62
+ * node-level detail (`code`, `nodeId`, `message`, `traceback`) when the
63
+ * platform provided one.
64
+ */
65
+ export declare class JobFailed extends ComfyError {
66
+ readonly error: JobError | null;
67
+ constructor(message: string, options?: {
68
+ error?: JobError | null;
69
+ });
70
+ }
71
+ /** Translate a protocol `ApiError` into the idiomatic SDK exception. */
72
+ export declare function toSdkError(exc: ApiError): ComfyError;
73
+ /**
74
+ * Run `fn`, re-raising any protocol `ApiError` as its idiomatic SDK
75
+ * exception. Wrap every `sdk`-level operation that calls into `low` with
76
+ * this so integrators only ever catch `sdk` exceptions (`MissingAsset`,
77
+ * `HashMismatch`, `NotFound`, ...), never the raw protocol error.
78
+ */
79
+ export declare function translate<T>(fn: () => Promise<T>): Promise<T>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Idiomatic `sdk` exceptions.
3
+ *
4
+ * These wrap the protocol-level `low.ApiError` codes with names an
5
+ * integrator catches directly (`JobFailed`, `QueueFull`, ...). `toSdkError`
6
+ * maps a raised `ApiError` to the right subclass; anything unmapped stays a
7
+ * `ComfyError` carrying the original code. Mirrors `comfy_sdk.exceptions`
8
+ * in the Python SDK.
9
+ */
10
+ import { ApiError } from "../low/index.js";
11
+ export class ComfyError extends Error {
12
+ code;
13
+ httpStatus;
14
+ details;
15
+ constructor(message, options = {}) {
16
+ super(message);
17
+ this.name = new.target.name;
18
+ this.code = options.code;
19
+ this.httpStatus = options.httpStatus;
20
+ this.details = options.details ?? null;
21
+ }
22
+ }
23
+ /** The surface rejected the request for lack of a valid key. Comfy Cloud
24
+ * and serverless require a key; a self-hosted proxy needs none. */
25
+ export class Unauthorized extends ComfyError {
26
+ }
27
+ export class Forbidden extends ComfyError {
28
+ }
29
+ export class NotFound extends ComfyError {
30
+ }
31
+ /** Structural/validation failure; `details` carries per-node errors. */
32
+ export class InvalidWorkflow extends ComfyError {
33
+ }
34
+ /** UI-export JSON was submitted instead of the API-format graph. */
35
+ export class WorkflowFormatUi extends InvalidWorkflow {
36
+ }
37
+ /** A `core/ASSET` reference was not usable (unknown/unscanned/not owned). */
38
+ export class MissingAsset extends ComfyError {
39
+ }
40
+ /** Uploaded bytes did not match the declared `expectedHash`. */
41
+ export class HashMismatch extends ComfyError {
42
+ }
43
+ /** from-hash / existence probe found no blob the caller can mint from. */
44
+ export class BlobNotFound extends ComfyError {
45
+ }
46
+ /** The idempotency key was reused. Keys are single-use (reject-on-duplicate,
47
+ * no replay): any second request with the same key — a retry, a concurrent
48
+ * duplicate, or the same key with a different body — is rejected. */
49
+ export class IdempotencyKeyReuse extends ComfyError {
50
+ }
51
+ export class InsufficientCredits extends ComfyError {
52
+ }
53
+ /** Backpressure: the queue is full. `retryAfter` is seconds to wait. */
54
+ export class QueueFull extends ComfyError {
55
+ retryAfter;
56
+ constructor(message, options) {
57
+ super(message, options);
58
+ this.retryAfter = options.retryAfter;
59
+ }
60
+ }
61
+ /**
62
+ * A job reached a non-success terminal state. `error` carries the
63
+ * node-level detail (`code`, `nodeId`, `message`, `traceback`) when the
64
+ * platform provided one.
65
+ */
66
+ export class JobFailed extends ComfyError {
67
+ error;
68
+ constructor(message, options = {}) {
69
+ super(message, { code: options.error?.code ?? "job_failed" });
70
+ this.error = options.error ?? null;
71
+ }
72
+ }
73
+ const BY_CODE = {
74
+ invalid_workflow: InvalidWorkflow,
75
+ workflow_format_ui: WorkflowFormatUi,
76
+ missing_asset: MissingAsset,
77
+ hash_mismatch: HashMismatch,
78
+ blob_not_found: BlobNotFound,
79
+ idempotency_key_reuse: IdempotencyKeyReuse,
80
+ insufficient_credits: InsufficientCredits,
81
+ not_found: NotFound,
82
+ unauthorized: Unauthorized,
83
+ forbidden: Forbidden,
84
+ };
85
+ /** Translate a protocol `ApiError` into the idiomatic SDK exception. */
86
+ export function toSdkError(exc) {
87
+ if (exc.code === "queue_full") {
88
+ return new QueueFull(exc.message, {
89
+ retryAfter: exc.retryAfter ?? 0,
90
+ code: exc.code,
91
+ httpStatus: exc.httpStatus,
92
+ details: exc.details,
93
+ });
94
+ }
95
+ const cls = BY_CODE[exc.code] ?? ComfyError;
96
+ return new cls(exc.message, { code: exc.code, httpStatus: exc.httpStatus, details: exc.details });
97
+ }
98
+ /**
99
+ * Run `fn`, re-raising any protocol `ApiError` as its idiomatic SDK
100
+ * exception. Wrap every `sdk`-level operation that calls into `low` with
101
+ * this so integrators only ever catch `sdk` exceptions (`MissingAsset`,
102
+ * `HashMismatch`, `NotFound`, ...), never the raw protocol error.
103
+ */
104
+ export async function translate(fn) {
105
+ try {
106
+ return await fn();
107
+ }
108
+ catch (exc) {
109
+ if (exc instanceof ApiError) {
110
+ throw toSdkError(exc);
111
+ }
112
+ throw exc;
113
+ }
114
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Local blake3 content hashing (via `hash-wasm`, which runs anywhere Node
3
+ * runs — no native addon).
4
+ *
5
+ * The hash is computed client-side purely as a dedup hint — the server
6
+ * always recomputes it from the received bytes and that value is
7
+ * authoritative. Files are hashed by streaming in chunks so a multi-GB
8
+ * input is never buffered whole. Mirrors `comfy_sdk._hashing` in the
9
+ * Python SDK.
10
+ */
11
+ /** `blake3:<hex>` of an in-memory buffer. */
12
+ export declare function hashBytes(data: Uint8Array): Promise<string>;
13
+ /** `blake3:<hex>` of a file on disk, streamed in chunks. */
14
+ export declare function hashFile(path: string): Promise<string>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Local blake3 content hashing (via `hash-wasm`, which runs anywhere Node
3
+ * runs — no native addon).
4
+ *
5
+ * The hash is computed client-side purely as a dedup hint — the server
6
+ * always recomputes it from the received bytes and that value is
7
+ * authoritative. Files are hashed by streaming in chunks so a multi-GB
8
+ * input is never buffered whole. Mirrors `comfy_sdk._hashing` in the
9
+ * Python SDK.
10
+ */
11
+ import { createReadStream } from "node:fs";
12
+ import { createBLAKE3 } from "hash-wasm";
13
+ /** `blake3:<hex>` of an in-memory buffer. */
14
+ export async function hashBytes(data) {
15
+ const hasher = await createBLAKE3();
16
+ hasher.init();
17
+ hasher.update(data);
18
+ return `blake3:${hasher.digest()}`;
19
+ }
20
+ /** `blake3:<hex>` of a file on disk, streamed in chunks. */
21
+ export async function hashFile(path) {
22
+ const hasher = await createBLAKE3();
23
+ hasher.init();
24
+ for await (const chunk of createReadStream(path)) {
25
+ hasher.update(chunk);
26
+ }
27
+ return `blake3:${hasher.digest()}`;
28
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `sdk` — the idiomatic, hand-written layer integrators import (the
3
+ * package's default export surface, re-exported from the package root).
4
+ * Mirrors `comfy_sdk`'s `__init__.py` in the Python SDK, minus the
5
+ * sync/async duplication (JS is async-native).
6
+ */
7
+ export { Comfy, type ComfyOptions } from "./client.js";
8
+ export { Asset, AssetFactory } from "./assets.js";
9
+ export { Workflow, WorkflowFactory, type WorkflowGraph } from "./workflows.js";
10
+ export { Job, JobFactory } from "./jobs.js";
11
+ export { Output } from "./outputs.js";
12
+ export type { ComfyEvent, Log, OutputReady, Preview, Progress, StatusChange } from "./events.js";
13
+ export { BlobNotFound, ComfyError, Forbidden, HashMismatch, IdempotencyKeyReuse, InsufficientCredits, InvalidWorkflow, JobFailed, MissingAsset, NotFound, QueueFull, Unauthorized, WorkflowFormatUi, } from "./exceptions.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `sdk` — the idiomatic, hand-written layer integrators import (the
3
+ * package's default export surface, re-exported from the package root).
4
+ * Mirrors `comfy_sdk`'s `__init__.py` in the Python SDK, minus the
5
+ * sync/async duplication (JS is async-native).
6
+ */
7
+ export { Comfy } from "./client.js";
8
+ export { Asset, AssetFactory } from "./assets.js";
9
+ export { Workflow, WorkflowFactory } from "./workflows.js";
10
+ export { Job, JobFactory } from "./jobs.js";
11
+ export { Output } from "./outputs.js";
12
+ export { BlobNotFound, ComfyError, Forbidden, HashMismatch, IdempotencyKeyReuse, InsufficientCredits, InvalidWorkflow, JobFailed, MissingAsset, NotFound, QueueFull, Unauthorized, WorkflowFormatUi, } from "./exceptions.js";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Job handles — the resumable, poll-authoritative core of the SDK.
3
+ *
4
+ * A {@link Job} is rehydratable purely from its ID. `wait` polls
5
+ * `GET /api/v2/jobs/{id}` with adaptive backoff as the source of truth for
6
+ * terminal status and outputs, so a stream that is throttled, dropped, or
7
+ * permanently unavailable never stalls completion. `events` is the live SSE
8
+ * stream on top: typed, auto-reconnecting (no replay — the stream carries
9
+ * no cursor), with the poll path as its backstop. Mirrors `comfy_sdk.jobs`
10
+ * in the Python SDK — one async class since JS is async-native.
11
+ */
12
+ import type { ComfyLow, Job as LowJob } from "../low/index.js";
13
+ import { type ComfyEvent } from "./events.js";
14
+ import { Output } from "./outputs.js";
15
+ export declare class Job {
16
+ private readonly low;
17
+ private model;
18
+ constructor(low: ComfyLow, model: LowJob);
19
+ get id(): string;
20
+ get status(): string;
21
+ get outputs(): Output[];
22
+ get error(): LowJob["error"];
23
+ getOutputs(nodeId: string): Output[];
24
+ private bindOutput;
25
+ /** Poll `GET /api/v2/jobs/{id}` once and adopt the fresh state. */
26
+ refresh(signal?: AbortSignal): Promise<this>;
27
+ /** Poll to a terminal state (adaptive backoff). Rejects with a
28
+ * `TimeoutError` if `timeoutMs` elapses first, or immediately if `signal`
29
+ * aborts — the abort interrupts the backoff wait itself, not just the
30
+ * in-flight poll request. */
31
+ wait(timeoutMs?: number, signal?: AbortSignal): Promise<this>;
32
+ /** Wait for terminal, then throw `JobFailed` unless it succeeded. */
33
+ result(signal?: AbortSignal): Promise<this>;
34
+ cancel(signal?: AbortSignal): Promise<this>;
35
+ /**
36
+ * Typed live event iterator. Auto-reconnects with no replay; falls back
37
+ * to polling to detect terminal status if the stream ends early. An
38
+ * aborted `signal` stops both the current SSE connection/poll and the
39
+ * pause between reconnect attempts.
40
+ */
41
+ events(signal?: AbortSignal): AsyncGenerator<ComfyEvent, void, void>;
42
+ }
43
+ export declare class JobFactory {
44
+ private readonly low;
45
+ constructor(low: ComfyLow);
46
+ get(jobId: string): Promise<Job>;
47
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Job handles — the resumable, poll-authoritative core of the SDK.
3
+ *
4
+ * A {@link Job} is rehydratable purely from its ID. `wait` polls
5
+ * `GET /api/v2/jobs/{id}` with adaptive backoff as the source of truth for
6
+ * terminal status and outputs, so a stream that is throttled, dropped, or
7
+ * permanently unavailable never stalls completion. `events` is the live SSE
8
+ * stream on top: typed, auto-reconnecting (no replay — the stream carries
9
+ * no cursor), with the poll path as its backstop. Mirrors `comfy_sdk.jobs`
10
+ * in the Python SDK — one async class since JS is async-native.
11
+ */
12
+ import { abortableSleep } from "./abortable-sleep.js";
13
+ import { backoffSchedule, isTerminal, SUCCESS } from "./core.js";
14
+ import { eventFromRaw } from "./events.js";
15
+ import { JobFailed, translate } from "./exceptions.js";
16
+ import { Output } from "./outputs.js";
17
+ // Pause before reconnecting an SSE stream that dropped mid-job, without a
18
+ // terminal frame having been seen.
19
+ const RECONNECT_PAUSE_MS = 100;
20
+ export class Job {
21
+ low;
22
+ model;
23
+ constructor(low, model) {
24
+ this.low = low;
25
+ this.model = model;
26
+ }
27
+ get id() {
28
+ return this.model.id;
29
+ }
30
+ get status() {
31
+ return this.model.status;
32
+ }
33
+ get outputs() {
34
+ return this.model.outputs.map((o) => this.bindOutput(o));
35
+ }
36
+ get error() {
37
+ return this.model.error;
38
+ }
39
+ getOutputs(nodeId) {
40
+ return this.model.outputs.filter((o) => o.node_id === nodeId).map((o) => this.bindOutput(o));
41
+ }
42
+ bindOutput(model) {
43
+ return new Output(model, this.low);
44
+ }
45
+ /** Poll `GET /api/v2/jobs/{id}` once and adopt the fresh state. */
46
+ async refresh(signal) {
47
+ this.model = await translate(() => this.low.getJob(this.model.urls.self || this.model.id, { signal }));
48
+ return this;
49
+ }
50
+ /** Poll to a terminal state (adaptive backoff). Rejects with a
51
+ * `TimeoutError` if `timeoutMs` elapses first, or immediately if `signal`
52
+ * aborts — the abort interrupts the backoff wait itself, not just the
53
+ * in-flight poll request. */
54
+ async wait(timeoutMs, signal) {
55
+ const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
56
+ const backoff = backoffSchedule();
57
+ for (;;) {
58
+ await this.refresh(signal);
59
+ if (isTerminal(this.status))
60
+ return this;
61
+ if (deadline !== undefined && Date.now() >= deadline) {
62
+ throw new Error(`job ${this.id} not terminal after ${timeoutMs}ms (status=${this.status})`);
63
+ }
64
+ await abortableSleep(backoff.next().value, signal);
65
+ }
66
+ }
67
+ /** Wait for terminal, then throw `JobFailed` unless it succeeded. */
68
+ async result(signal) {
69
+ await this.wait(undefined, signal);
70
+ if (this.status !== SUCCESS) {
71
+ throw new JobFailed(`job ${this.id} ended ${this.status}`, { error: this.model.error });
72
+ }
73
+ return this;
74
+ }
75
+ async cancel(signal) {
76
+ this.model = await translate(() => this.low.cancelJob(this.model.urls.cancel || this.model.id, { signal }));
77
+ return this;
78
+ }
79
+ /**
80
+ * Typed live event iterator. Auto-reconnects with no replay; falls back
81
+ * to polling to detect terminal status if the stream ends early. An
82
+ * aborted `signal` stops both the current SSE connection/poll and the
83
+ * pause between reconnect attempts.
84
+ */
85
+ async *events(signal) {
86
+ const eventsUrl = this.model.urls.events || this.model.id;
87
+ // Progress is monotonic across the whole stream, reconnects included: a
88
+ // frame whose value regresses (e.g. a lower value replayed by the server
89
+ // after a mid-stream drop) is suppressed so a consumer's progress never
90
+ // goes backwards.
91
+ let lastProgress = Number.NEGATIVE_INFINITY;
92
+ for (;;) {
93
+ let terminalSeen = false;
94
+ try {
95
+ for await (const raw of this.low.getJobEvents(eventsUrl, { signal })) {
96
+ const event = eventFromRaw(raw, (data) => this.bindOutput(data));
97
+ if (event === null)
98
+ continue;
99
+ if (event.kind === "progress") {
100
+ if (event.value < lastProgress)
101
+ continue;
102
+ lastProgress = event.value;
103
+ }
104
+ if (event.kind === "statusChange" && isTerminal(event.status)) {
105
+ terminalSeen = true;
106
+ yield event;
107
+ return;
108
+ }
109
+ yield event;
110
+ }
111
+ }
112
+ catch (exc) {
113
+ // A caller abort must propagate (and stop the loop), not be
114
+ // swallowed as an ordinary mid-stream drop.
115
+ if (signal?.aborted)
116
+ throw exc;
117
+ // Connection dropped mid-stream — reconnect below.
118
+ }
119
+ if (terminalSeen)
120
+ return;
121
+ // Stream ended without a terminal frame. Poll the authoritative
122
+ // state: stop if already terminal, else reconnect for fresh frames.
123
+ await this.refresh(signal);
124
+ if (isTerminal(this.status)) {
125
+ const statusChange = {
126
+ kind: "statusChange",
127
+ status: this.status,
128
+ queuePosition: null,
129
+ };
130
+ yield statusChange;
131
+ return;
132
+ }
133
+ await abortableSleep(RECONNECT_PAUSE_MS, signal);
134
+ }
135
+ }
136
+ }
137
+ export class JobFactory {
138
+ low;
139
+ constructor(low) {
140
+ this.low = low;
141
+ }
142
+ async get(jobId) {
143
+ return new Job(this.low, await translate(() => this.low.getJob(jobId)));
144
+ }
145
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Output handles — typed, range-aware download over an asset id.
3
+ *
4
+ * An output is an asset: the bytes are retrievable via `getAssetContent`
5
+ * (which serves directly or redirects to a signed URL) for as long as the
6
+ * job is retained. `toFile` streams to disk; `toBytes` buffers. Mirrors
7
+ * `comfy_sdk.outputs` in the Python SDK.
8
+ */
9
+ import type { ComfyLow, Output as LowOutput } from "../low/index.js";
10
+ export declare class Output {
11
+ private readonly model;
12
+ private readonly low;
13
+ constructor(model: LowOutput, low: ComfyLow);
14
+ get nodeId(): string;
15
+ get name(): string;
16
+ get type(): LowOutput["type"];
17
+ get id(): string;
18
+ get sizeBytes(): number;
19
+ get contentType(): string;
20
+ toFile(path: string, options?: {
21
+ range?: readonly [number, number];
22
+ }): Promise<string>;
23
+ toBytes(options?: {
24
+ range?: readonly [number, number];
25
+ }): Promise<Uint8Array>;
26
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Output handles — typed, range-aware download over an asset id.
3
+ *
4
+ * An output is an asset: the bytes are retrievable via `getAssetContent`
5
+ * (which serves directly or redirects to a signed URL) for as long as the
6
+ * job is retained. `toFile` streams to disk; `toBytes` buffers. Mirrors
7
+ * `comfy_sdk.outputs` in the Python SDK.
8
+ */
9
+ import { createWriteStream } from "node:fs";
10
+ import { Readable } from "node:stream";
11
+ import { pipeline } from "node:stream/promises";
12
+ export class Output {
13
+ model;
14
+ low;
15
+ constructor(model, low) {
16
+ this.model = model;
17
+ this.low = low;
18
+ }
19
+ get nodeId() {
20
+ return this.model.node_id;
21
+ }
22
+ get name() {
23
+ return this.model.name;
24
+ }
25
+ get type() {
26
+ return this.model.type;
27
+ }
28
+ get id() {
29
+ return this.model.id;
30
+ }
31
+ get sizeBytes() {
32
+ return this.model.size_bytes;
33
+ }
34
+ get contentType() {
35
+ return this.model.content_type;
36
+ }
37
+ async toFile(path, options = {}) {
38
+ const response = await this.low.getAssetContent(this.model.id, { range: options.range });
39
+ if (!response.body)
40
+ throw new Error(`empty response body for asset ${this.model.id}`);
41
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(path));
42
+ return path;
43
+ }
44
+ async toBytes(options = {}) {
45
+ const response = await this.low.getAssetContent(this.model.id, { range: options.range });
46
+ return new Uint8Array(await response.arrayBuffer());
47
+ }
48
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Workflow construction and mutation.
3
+ *
4
+ * A {@link Workflow} is a thin, local wrapper over the raw API-format
5
+ * graph. The graph stays a freely-mutable object (`wf.json`); `setInput` is
6
+ * sugar for `wf.json[node].inputs[field] = value` that also accepts an
7
+ * asset handle (substituted into a `core/ASSET` object at submit time).
8
+ * Construction does no network I/O in v1. Mirrors `comfy_sdk.workflows` in
9
+ * the Python SDK.
10
+ */
11
+ export type WorkflowGraph = Record<string, unknown>;
12
+ export declare class Workflow {
13
+ json: WorkflowGraph;
14
+ constructor(graph: WorkflowGraph);
15
+ /**
16
+ * Set `node.inputs.field`. `value` may be a plain JSON value or an asset
17
+ * handle; handles are substituted into `core/ASSET` objects when the
18
+ * workflow is submitted.
19
+ */
20
+ setInput(nodeId: string, field: string, value: unknown): void;
21
+ }
22
+ /**
23
+ * `client.workflows` — alternative constructors for {@link Workflow}.
24
+ * Namespaced on the client (rather than free-standing) because
25
+ * construction is expected to become client-bound once server-side
26
+ * subgraphs land; in v1 it is purely local.
27
+ */
28
+ export declare class WorkflowFactory {
29
+ fromFile(path: string): Promise<Workflow>;
30
+ fromJson(graph: WorkflowGraph): Workflow;
31
+ fromString(text: string): Workflow;
32
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Workflow construction and mutation.
3
+ *
4
+ * A {@link Workflow} is a thin, local wrapper over the raw API-format
5
+ * graph. The graph stays a freely-mutable object (`wf.json`); `setInput` is
6
+ * sugar for `wf.json[node].inputs[field] = value` that also accepts an
7
+ * asset handle (substituted into a `core/ASSET` object at submit time).
8
+ * Construction does no network I/O in v1. Mirrors `comfy_sdk.workflows` in
9
+ * the Python SDK.
10
+ */
11
+ import { readFile } from "node:fs/promises";
12
+ export class Workflow {
13
+ json;
14
+ constructor(graph) {
15
+ this.json = graph;
16
+ }
17
+ /**
18
+ * Set `node.inputs.field`. `value` may be a plain JSON value or an asset
19
+ * handle; handles are substituted into `core/ASSET` objects when the
20
+ * workflow is submitted.
21
+ */
22
+ setInput(nodeId, field, value) {
23
+ const node = (this.json[nodeId] ??= {});
24
+ const inputs = (node.inputs ??= {});
25
+ inputs[field] = value;
26
+ }
27
+ }
28
+ /**
29
+ * `client.workflows` — alternative constructors for {@link Workflow}.
30
+ * Namespaced on the client (rather than free-standing) because
31
+ * construction is expected to become client-bound once server-side
32
+ * subgraphs land; in v1 it is purely local.
33
+ */
34
+ export class WorkflowFactory {
35
+ async fromFile(path) {
36
+ const text = await readFile(path, "utf-8");
37
+ return new Workflow(JSON.parse(text));
38
+ }
39
+ fromJson(graph) {
40
+ return new Workflow(graph);
41
+ }
42
+ fromString(text) {
43
+ return new Workflow(JSON.parse(text));
44
+ }
45
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@comfyorg/sdk",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "TypeScript SDK for running ComfyUI workflows via the Comfy API v2 (self-hosted, Comfy Cloud, serverless).",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./low": {
18
+ "types": "./dist/low/index.d.ts",
19
+ "default": "./dist/low/index.js"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "generate": "openapi-ts",
25
+ "lint": "oxlint .",
26
+ "format": "oxfmt --write .",
27
+ "format:check": "oxfmt --check .",
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "vitest run",
30
+ "test:coverage": "vitest run --coverage",
31
+ "check:spec-drift": "node scripts/check-spec-drift.mjs"
32
+ },
33
+ "dependencies": {
34
+ "eventsource-parser": "^3.0.0",
35
+ "hash-wasm": "^4.11.0",
36
+ "zod": "^4.4.3"
37
+ },
38
+ "devDependencies": {
39
+ "@hey-api/openapi-ts": "0.99.0",
40
+ "@types/node": "^22.10.0",
41
+ "@vitest/coverage-v8": "^4.1.0",
42
+ "oxfmt": "^0.45.0",
43
+ "oxlint": "^1.62.0",
44
+ "typescript": "^5.8.3",
45
+ "vitest": "^4.1.0",
46
+ "yaml": "^2.9.0"
47
+ },
48
+ "engines": {
49
+ "node": ">=22"
50
+ },
51
+ "packageManager": "pnpm@11.13.0"
52
+ }