@howells/motif-sdk 0.8.0 → 0.10.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/README.md CHANGED
@@ -123,6 +123,83 @@ The package exports public types for generation, processing, queue, metadata, an
123
123
  - `ModelConfig`, `AspectRatio`, `Resolution`, `ImageSize`, `ImageQuality`, `BackgroundMode`, `ThinkingLevel`
124
124
  - `FalToolConfig`, `FalToolId`, `FalToolRequest`, `FalToolRunOptions`
125
125
 
126
+ ## Image Layer (`@howells/motif-sdk/image`)
127
+
128
+ A provider-agnostic image generation + editing layer, additive to the fal-specific `MotifServer` surface above. It is an ESM-only subpath export, built on the Vercel AI SDK image interface (`ai`'s `generateImage`).
129
+
130
+ ```bash
131
+ npm install @howells/motif-sdk
132
+ ```
133
+
134
+ ```ts
135
+ import { createMotifImage } from "@howells/motif-sdk/image";
136
+
137
+ const img = createMotifImage({ defaultProvider: "google" });
138
+
139
+ // text -> image
140
+ const generated = await img.generate({
141
+ tier: "fast",
142
+ prompt: "a plain room, bare concrete wall",
143
+ aspectRatio: "1:1",
144
+ });
145
+
146
+ // multi-image edit (images + instruction, optional mask -> image out)
147
+ const edited = await img.edit({
148
+ tier: "balanced",
149
+ images: [roomBytes, tileBytes],
150
+ instruction: "Apply the oak texture from image 2 onto the wall in image 1.",
151
+ mask: surfaceMaskBytes,
152
+ });
153
+
154
+ if (edited.isOk()) {
155
+ edited.value.images; // MotifImageFile[]
156
+ edited.value.cost; // { usd, source: "provider-metadata" | "table" | "unknown" }
157
+ edited.value.provider; // resolved ImageProviderId
158
+ edited.value.model; // resolved model id
159
+ }
160
+ ```
161
+
162
+ Both `generate()` and `edit()` return `Result<MotifImageResult, MotifError>`, matching the rest of the SDK — no throws, check `isOk()` / `isErr()`.
163
+
164
+ Four providers are implemented, each reading its own API key from the environment (or a `MotifImageConfig` override):
165
+
166
+ | Provider | Env var | Notes |
167
+ | --- | --- | --- |
168
+ | `google` | `GOOGLE_GENERATIVE_AI_API_KEY` | Default provider; Gemini gen + edit |
169
+ | `openai` | `OPENAI_API_KEY` | gpt-image-1 |
170
+ | `replicate` | `REPLICATE_API_TOKEN` | flux-1.1-pro-ultra |
171
+ | `fal` | `FAL_KEY` | fal-hosted adapter |
172
+
173
+ `generate()` and `edit()` accept `tier` (`"fast" | "balanced" | "quality" | "hero"`) to resolve a model per provider, or an explicit `model` id. Every result carries a normalized per-call `cost: { usd, source }`.
174
+
175
+ ### Best-of-N with an injectable judge
176
+
177
+ `bestOfN()` generates `n` candidates in parallel and picks a winner. It reuses the same options as `generate()` (text→image) or `edit()` (pass `images` for the edit path), plus `n` and an optional `judge`. When a `seed` is given each candidate uses `seed + index`, so the N vary. The judge is a caller-provided function — the layer takes no text-client dependency, so it pairs well with `@howells/ai`'s vision client but does not require it. Omit the judge and candidate 0 wins.
178
+
179
+ ```ts
180
+ const best = await img.bestOfN({
181
+ prompt: "a plain room, bare concrete wall",
182
+ n: 4,
183
+ seed: 100, // candidates get seeds 100, 101, 102, 103
184
+ // Caller-provided judge: receives the successful candidates + context,
185
+ // returns the winning index. Wire in @howells/ai here if you want a vision judge.
186
+ judge: async (candidates, context) => {
187
+ // ...score candidates[i].images[0] against context.prompt...
188
+ return { index: 0, reason: "sharpest wall texture" };
189
+ },
190
+ });
191
+
192
+ if (best.isOk()) {
193
+ best.value.best; // the winning MotifImageResult
194
+ best.value.chosenIndex; // its index within candidates
195
+ best.value.reason; // the judge's rationale, if any
196
+ best.value.candidates; // every successful candidate (generation order)
197
+ best.value.totalCostUsd; // summed USD across all candidates generated
198
+ }
199
+ ```
200
+
201
+ If some candidates fail, the judge sees only the survivors; if all `n` fail, `bestOfN()` returns `Result.err`.
202
+
126
203
  ## Testing
127
204
 
128
205
  ```bash
package/dist/image.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ImageModel, generateImage } from 'ai';
1
+ import { generateImage, ImageModel } from 'ai';
2
2
  import { Result } from 'neverthrow';
3
3
 
4
4
  declare class MotifError extends Error {
@@ -78,25 +78,53 @@ interface GenerateImageOptions {
78
78
  model?: string;
79
79
  /** Provider override for this call. */
80
80
  provider?: ImageProviderId;
81
- /** Aspect ratio, e.g. `"1:1"`. */
81
+ /**
82
+ * Aspect ratio, e.g. `"1:1"`. An alternative to {@link size}; pass one or the
83
+ * other. If both are passed the provider decides which to honor and typically
84
+ * surfaces a warning (see {@link MotifImageResult.warnings}).
85
+ */
82
86
  aspectRatio?: `${number}:${number}`;
83
- /** Explicit pixel size, e.g. `"1024x1024"`. */
87
+ /**
88
+ * Explicit pixel size, e.g. `"1024x1024"`. An alternative to
89
+ * {@link aspectRatio}; pass one or the other. If both are passed the provider
90
+ * decides which to honor and typically surfaces a warning (see
91
+ * {@link MotifImageResult.warnings}).
92
+ */
84
93
  size?: `${number}x${number}`;
85
94
  /** Number of images to generate. */
86
95
  n?: number;
96
+ /** Seed for reproducible generation, where the provider supports it. */
97
+ seed?: number;
98
+ /** Abort signal to cancel the in-flight request. */
99
+ signal?: AbortSignal;
87
100
  /**
88
101
  * Provider-specific options, passed straight through to the underlying model
89
102
  * as body parameters. Outer key = provider name, inner key = option name.
103
+ * Values must be JSON-representable; a non-JSON value (undefined, function,
104
+ * bigint, symbol) makes the call fail with a `MotifError`.
90
105
  */
91
106
  providerOptions?: Record<string, Record<string, unknown>>;
92
107
  }
93
108
  /** Options for a multi-image edit (images in → image out), with an optional mask. */
94
109
  interface EditImageOptions {
95
- /** Input images. Each is raw bytes, a base64 string, or a data URL. */
110
+ /**
111
+ * Input images. Each entry is raw bytes (`Uint8Array`) or a string. A string
112
+ * may be base64, a `data:` URL, OR a remote `http(s)://` URL. Remote URLs are
113
+ * FETCHED by the provider — and on some providers (e.g. OpenAI) that fetch
114
+ * happens from the local process running this SDK. Callers that accept
115
+ * untrusted URLs should fetch and validate the bytes themselves before
116
+ * passing them here (SSRF / local-network exposure otherwise).
117
+ */
96
118
  images: (Uint8Array | string)[];
97
119
  /** Natural-language edit instruction. */
98
120
  instruction: string;
99
- /** Optional mask (bytes, base64, or data URL) constraining the edited region. */
121
+ /**
122
+ * Optional mask constraining the edited region. Same accepted forms as
123
+ * {@link EditImageOptions.images} (bytes, base64, `data:` URL, or a remote
124
+ * `http(s)://` URL that the provider fetches — see the images note on
125
+ * untrusted URLs). When multiple images are passed, the mask applies to
126
+ * `images[0]`.
127
+ */
100
128
  mask?: Uint8Array | string;
101
129
  /** Quality/latency tier. Ignored when `model` is set. */
102
130
  tier?: ImageTier;
@@ -106,6 +134,10 @@ interface EditImageOptions {
106
134
  provider?: ImageProviderId;
107
135
  /** Number of images to generate. */
108
136
  n?: number;
137
+ /** Seed for reproducible generation, where the provider supports it. */
138
+ seed?: number;
139
+ /** Abort signal to cancel the in-flight request. */
140
+ signal?: AbortSignal;
109
141
  /** Provider-specific options (see {@link GenerateImageOptions.providerOptions}). */
110
142
  providerOptions?: Record<string, Record<string, unknown>>;
111
143
  }
@@ -127,6 +159,56 @@ interface MotifImageResult {
127
159
  model: string;
128
160
  /** Provider correlation id, where the provider surfaces one. */
129
161
  requestId?: string;
162
+ /**
163
+ * Degraded-success warnings from the provider (a requested setting was
164
+ * ignored or adjusted — e.g. passing both `size` and `aspectRatio`). Each is a
165
+ * readable string. Omitted entirely when the provider returned none.
166
+ */
167
+ warnings?: readonly string[];
168
+ }
169
+ /**
170
+ * Picks the winning candidate for a {@link MotifImageClient.bestOfN} call.
171
+ *
172
+ * Receives the successful candidates (index-aligned to
173
+ * {@link BestOfNResult.candidates}) and the request context, and returns the
174
+ * chosen index (plus an optional human-readable reason). May be sync or async.
175
+ * The judge is caller-provided so the image layer stays decoupled from any text
176
+ * client — it pairs well with `@howells/ai`'s vision client, but that dependency
177
+ * is not required.
178
+ */
179
+ type ImageJudge = (candidates: readonly MotifImageResult[], context: {
180
+ readonly prompt?: string;
181
+ readonly instruction?: string;
182
+ }) => Promise<{
183
+ index: number;
184
+ reason?: string;
185
+ }> | {
186
+ index: number;
187
+ reason?: string;
188
+ };
189
+ /**
190
+ * Options for a best-of-N generation. Extends either {@link GenerateImageOptions}
191
+ * (text→image) or {@link EditImageOptions} (multi-image edit) — the presence of
192
+ * `images` selects the edit path — with the candidate count and an optional judge.
193
+ */
194
+ type BestOfNOptions = (GenerateImageOptions | EditImageOptions) & {
195
+ /** How many candidates to generate (>= 1). */
196
+ n: number;
197
+ /** Chooses the winner. Omit → candidate 0 wins. */
198
+ judge?: ImageJudge;
199
+ };
200
+ /** Result of a {@link MotifImageClient.bestOfN} call. */
201
+ interface BestOfNResult {
202
+ /** The winning candidate (its single-image {@link MotifImageResult}). */
203
+ best: MotifImageResult;
204
+ /** The index of `best` within `candidates`. */
205
+ chosenIndex: number;
206
+ /** Judge's rationale, if it returned one. */
207
+ reason?: string;
208
+ /** All successful candidates, in generation order. */
209
+ candidates: readonly MotifImageResult[];
210
+ /** Total USD across all candidates that were generated (successes only). */
211
+ totalCostUsd: number;
130
212
  }
131
213
  /** The provider-agnostic image client. Every method returns a Result — no throws. */
132
214
  interface MotifImageClient {
@@ -134,6 +216,30 @@ interface MotifImageClient {
134
216
  generate: (opts: GenerateImageOptions) => Promise<Result<MotifImageResult, MotifError>>;
135
217
  /** Multi-image edit (with optional mask). */
136
218
  edit: (opts: EditImageOptions) => Promise<Result<MotifImageResult, MotifError>>;
219
+ /**
220
+ * Generate N candidates (in parallel) and pick the best via an optional
221
+ * injectable judge. Discriminates generate vs edit by the presence of
222
+ * `images`. Returns the winner plus all successful candidates and total spend.
223
+ */
224
+ bestOfN: (opts: BestOfNOptions) => Promise<Result<BestOfNResult, MotifError>>;
225
+ }
226
+
227
+ /**
228
+ * Internal dependency-injection seam for the image layer.
229
+ *
230
+ * These types are INTERNAL: they are consumed by `createMotifImage`'s `deps`
231
+ * parameter and by the offline tests, but they are deliberately NOT re-exported
232
+ * from the public `@howells/motif-sdk/image` subpath, so they never appear in
233
+ * `dist/image.d.ts`. Import them from `./deps` inside the package (and from
234
+ * `../src/image/deps` in tests).
235
+ */
236
+
237
+ /** A model resolver: builds an AI SDK `ImageModel` for a (provider, model, key). */
238
+ type ResolveImageModel = (provider: ImageProviderId, modelId: string, apiKey?: string) => ImageModel;
239
+ /** Internal dependency-injection seam (default: real `generateImage` + adapters). */
240
+ interface MotifImageDeps {
241
+ generateImage?: typeof generateImage;
242
+ resolveModel?: ResolveImageModel;
137
243
  }
138
244
 
139
245
  /**
@@ -164,7 +270,18 @@ interface ImageProviderAdapter {
164
270
  * network I/O.
165
271
  */
166
272
  readonly resolveModel: (modelId: string, apiKey?: string) => ImageModel;
167
- /** Static per-model USD/image table (best-effort; cited per adapter). */
273
+ /**
274
+ * Static per-model USD/**image** table (best-effort; cited per adapter). This
275
+ * is multiplied by the returned image count to form the call total.
276
+ *
277
+ * Cost contract: if an adapter instead surfaces a cost on
278
+ * `result.providerMetadata` (the preferred source; see
279
+ * {@link costFromProviderMetadata}), that value MUST already be the **call
280
+ * total** across all `n` images — NOT a per-image figure. `priceUsdByModel`
281
+ * is per-image; `providerMetadata.cost` is the whole call. These two paths
282
+ * intentionally differ, so an adapter must not populate a per-image number
283
+ * into `providerMetadata.cost`.
284
+ */
168
285
  readonly priceUsdByModel: Readonly<Record<string, number>>;
169
286
  }
170
287
  /**
@@ -297,13 +414,6 @@ declare function costForImages(provider: ImageProviderId, modelId: string, provi
297
414
  * ```
298
415
  */
299
416
 
300
- /** A model resolver: builds an AI SDK `ImageModel` for a (provider, model, key). */
301
- type ResolveImageModel = (provider: ImageProviderId, modelId: string, apiKey?: string) => ImageModel;
302
- /** Internal dependency-injection seam (default: real `generateImage` + adapters). */
303
- interface MotifImageDeps {
304
- generateImage?: typeof generateImage;
305
- resolveModel?: ResolveImageModel;
306
- }
307
417
  /**
308
418
  * Create a provider-agnostic image client.
309
419
  *
@@ -313,4 +423,4 @@ interface MotifImageDeps {
313
423
  */
314
424
  declare function createMotifImage(config?: MotifImageConfig, deps?: MotifImageDeps): MotifImageClient;
315
425
 
316
- export { type EditImageOptions, FAL_API_KEY_ENV, FAL_TIER_MODELS, GOOGLE_API_KEY_ENV, GOOGLE_TIER_MODELS, type GenerateImageOptions, type ImageCost, type ImageCostSource, type ImageProviderAdapter, type ImageProviderId, type ImageTier, type MotifImageClient, type MotifImageConfig, type MotifImageDeps, type MotifImageFile, type MotifImageResult, OPENAI_API_KEY_ENV, OPENAI_TIER_MODELS, PROVIDERS, REPLICATE_API_KEY_ENV, REPLICATE_TIER_MODELS, type ResolveImageModel, costForImages, costFromProviderMetadata, createMotifImage, getProviderAdapter };
426
+ export { type BestOfNOptions, type BestOfNResult, type EditImageOptions, FAL_API_KEY_ENV, FAL_TIER_MODELS, GOOGLE_API_KEY_ENV, GOOGLE_TIER_MODELS, type GenerateImageOptions, type ImageCost, type ImageCostSource, type ImageJudge, type ImageProviderAdapter, type ImageProviderId, type ImageTier, type MotifImageClient, type MotifImageConfig, type MotifImageFile, type MotifImageResult, OPENAI_API_KEY_ENV, OPENAI_TIER_MODELS, PROVIDERS, REPLICATE_API_KEY_ENV, REPLICATE_TIER_MODELS, costForImages, costFromProviderMetadata, createMotifImage, getProviderAdapter };
package/dist/image.js CHANGED
@@ -1503,6 +1503,8 @@ function createMotifImage(config = {}, deps = {}) {
1503
1503
  ...opts.n === void 0 ? {} : { n: opts.n },
1504
1504
  ...opts.size === void 0 ? {} : { size: opts.size },
1505
1505
  ...opts.aspectRatio === void 0 ? {} : { aspectRatio: opts.aspectRatio },
1506
+ ...opts.seed === void 0 ? {} : { seed: opts.seed },
1507
+ ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
1506
1508
  ...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
1507
1509
  });
1508
1510
  return ok2(toMotifImageResult(result, provider, modelId));
@@ -1523,6 +1525,8 @@ function createMotifImage(config = {}, deps = {}) {
1523
1525
  ...opts.mask === void 0 ? {} : { mask: opts.mask }
1524
1526
  },
1525
1527
  ...opts.n === void 0 ? {} : { n: opts.n },
1528
+ ...opts.seed === void 0 ? {} : { seed: opts.seed },
1529
+ ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
1526
1530
  ...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
1527
1531
  });
1528
1532
  return ok2(toMotifImageResult(result, provider, modelId));
@@ -1530,7 +1534,91 @@ function createMotifImage(config = {}, deps = {}) {
1530
1534
  return err2(toMotifError(error));
1531
1535
  }
1532
1536
  }
1533
- return { generate, edit };
1537
+ async function bestOfN(opts) {
1538
+ return await runBestOfN(opts, generate, edit);
1539
+ }
1540
+ return { generate, edit, bestOfN };
1541
+ }
1542
+ async function runCandidate(opts, index, generate, edit) {
1543
+ const candidateSeed = opts.seed === void 0 ? void 0 : opts.seed + index;
1544
+ const overrides = {
1545
+ n: 1,
1546
+ ...candidateSeed === void 0 ? {} : { seed: candidateSeed }
1547
+ };
1548
+ if ("images" in opts) {
1549
+ return await edit({ ...opts, ...overrides });
1550
+ }
1551
+ return await generate({ ...opts, ...overrides });
1552
+ }
1553
+ async function selectWinner(opts, candidates) {
1554
+ const { judge } = opts;
1555
+ if (judge === void 0) {
1556
+ return ok2({ chosenIndex: 0 });
1557
+ }
1558
+ const context = "images" in opts ? { instruction: opts.instruction } : { prompt: opts.prompt };
1559
+ const decision = await judge(candidates, context);
1560
+ if (!Number.isInteger(decision.index) || decision.index < 0 || decision.index >= candidates.length) {
1561
+ return err2(
1562
+ new MotifError(
1563
+ `bestOfN judge returned an out-of-range index ${decision.index} (expected 0..${candidates.length - 1})`,
1564
+ 0
1565
+ )
1566
+ );
1567
+ }
1568
+ return ok2({
1569
+ chosenIndex: decision.index,
1570
+ ...decision.reason === void 0 ? {} : { reason: decision.reason }
1571
+ });
1572
+ }
1573
+ async function runBestOfN(opts, generate, edit) {
1574
+ try {
1575
+ const { n } = opts;
1576
+ if (!Number.isInteger(n) || n < 1) {
1577
+ return err2(
1578
+ new MotifError(`bestOfN requires an integer n >= 1 (got ${n})`, 0)
1579
+ );
1580
+ }
1581
+ const results = await Promise.all(
1582
+ Array.from({ length: n }, (_unused, index) => index).map(
1583
+ async (index) => await runCandidate(opts, index, generate, edit)
1584
+ )
1585
+ );
1586
+ const candidates = [];
1587
+ let firstError;
1588
+ for (const result of results) {
1589
+ if (result.isOk()) {
1590
+ candidates.push(result.value);
1591
+ } else {
1592
+ firstError ??= result.error;
1593
+ }
1594
+ }
1595
+ if (candidates.length === 0) {
1596
+ return err2(
1597
+ firstError ?? new MotifError(`all ${n} bestOfN candidates failed`, 0)
1598
+ );
1599
+ }
1600
+ const winner = await selectWinner(opts, candidates);
1601
+ if (winner.isErr()) {
1602
+ return err2(winner.error);
1603
+ }
1604
+ const { chosenIndex, reason } = winner.value;
1605
+ const best = candidates[chosenIndex];
1606
+ if (best === void 0) {
1607
+ return err2(new MotifError("bestOfN failed to resolve a winner", 0));
1608
+ }
1609
+ const totalCostUsd = Number(
1610
+ candidates.reduce((sum, candidate) => sum + candidate.cost.usd, 0).toFixed(6)
1611
+ );
1612
+ return ok2({
1613
+ best,
1614
+ chosenIndex,
1615
+ ...reason === void 0 ? {} : { reason },
1616
+ candidates,
1617
+ totalCostUsd
1618
+ });
1619
+ } catch (error) {
1620
+ return err2(toMotifError(error));
1621
+ }
1534
1622
  }
1535
1623
  function defaultResolveModel(provider, modelId, apiKey) {
1536
1624
  return getProviderAdapter(provider).resolveModel(modelId, apiKey);
@@ -1555,14 +1643,26 @@ function toMotifImageResult(result, provider, model) {
1555
1643
  images.length
1556
1644
  );
1557
1645
  const requestId = extractRequestId(result);
1646
+ const warnings = result.warnings.map(renderWarning);
1558
1647
  return {
1559
1648
  images,
1560
1649
  cost,
1561
1650
  provider,
1562
1651
  model,
1563
- ...requestId === void 0 ? {} : { requestId }
1652
+ ...requestId === void 0 ? {} : { requestId },
1653
+ ...warnings.length === 0 ? {} : { warnings }
1564
1654
  };
1565
1655
  }
1656
+ function renderWarning(warning) {
1657
+ if (warning.type === "unsupported" || warning.type === "compatibility") {
1658
+ const label = warning.type === "unsupported" ? "unsupported feature" : "compatibility mode for feature";
1659
+ return warning.details === void 0 ? `${label} "${warning.feature}"` : `${label} "${warning.feature}": ${warning.details}`;
1660
+ }
1661
+ if (warning.type === "deprecated") {
1662
+ return `deprecated setting "${warning.setting}": ${warning.message}`;
1663
+ }
1664
+ return warning.message;
1665
+ }
1566
1666
  function isRecord3(value) {
1567
1667
  return typeof value === "object" && value !== null;
1568
1668
  }
@@ -1601,13 +1701,13 @@ function toProviderOptions(input) {
1601
1701
  for (const [namespace, options] of Object.entries(input)) {
1602
1702
  const inner = {};
1603
1703
  for (const [key, value] of Object.entries(options)) {
1604
- inner[key] = toJsonValue(value);
1704
+ inner[key] = toJsonValue(value, `providerOptions.${namespace}.${key}`);
1605
1705
  }
1606
1706
  out[namespace] = inner;
1607
1707
  }
1608
1708
  return out;
1609
1709
  }
1610
- function toJsonValue(value) {
1710
+ function toJsonValue(value, path) {
1611
1711
  if (value === null) {
1612
1712
  return null;
1613
1713
  }
@@ -1617,18 +1717,21 @@ function toJsonValue(value) {
1617
1717
  if (typeof value === "object") {
1618
1718
  if (Array.isArray(value)) {
1619
1719
  const arr = [];
1620
- for (const item of value) {
1621
- arr.push(toJsonValue(item));
1720
+ for (const [index, item] of value.entries()) {
1721
+ arr.push(toJsonValue(item, `${path}[${index}]`));
1622
1722
  }
1623
1723
  return arr;
1624
1724
  }
1625
1725
  const obj = {};
1626
1726
  for (const [key, entry] of Object.entries(value)) {
1627
- obj[key] = toJsonValue(entry);
1727
+ obj[key] = toJsonValue(entry, `${path}.${key}`);
1628
1728
  }
1629
1729
  return obj;
1630
1730
  }
1631
- return null;
1731
+ throw new MotifError(
1732
+ `providerOptions value at ${path} is not JSON-representable (type: ${typeof value})`,
1733
+ 0
1734
+ );
1632
1735
  }
1633
1736
  function toMotifError(error) {
1634
1737
  if (error instanceof MotifError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howells/motif-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Public Node SDK for Motif fal.ai generation, editing, utility tools, and model metadata.",
5
5
  "keywords": [
6
6
  "fal",
@@ -61,8 +61,8 @@
61
61
  "vitest": "^4.1.10"
62
62
  },
63
63
  "scripts": {
64
- "build": "tsup",
65
- "dev": "tsup --watch",
64
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsup",
65
+ "dev": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsup --watch",
66
66
  "lint": "howells-check .",
67
67
  "lint:fix": "howells-fix .",
68
68
  "typecheck": "tsc --noEmit",