hazo_images 1.1.0 → 1.2.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/CHANGE_LOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # hazo_images Change Log
2
2
 
3
+ ## 1.2.0 — 2026-05-30
4
+
5
+ ### Added
6
+ - New `hazo_images/runware` sub-export wrapping the Runware REST API.
7
+ - `createRunwareClient(opts?)` factory — `generateImage(params)` and `getBalance()`.
8
+ - `assemblePrompts(input)` helper for the prefix + character + image + suffix pattern.
9
+ - Four `HazoError` subclasses: `RunwareApiError`, `RunwareAuthError`, `RunwareRateLimitError` (carries `retryAfter`), `RunwareValidationError`. All codes are `HAZO_IMAGES_RUNWARE_*`.
10
+ - API key resolution: `opts.apiKey` → `HAZO_IMAGES_RUNWARE_API_KEY` → `RUNWARE_API_KEY` (legacy).
11
+ - Outbound HTTP goes through `fetchWithRequestId` from `hazo_core` (first outbound HTTP in this package).
12
+ - Configurable `baseUrl`, `defaultModel`, `timeoutMs` (default 60s), `fetch` (override for tests), `logger`.
13
+
14
+ ### Migration
15
+ - Extracted verbatim from `14.seo_monitor/lib/runware/{client,resolve-params}.ts`.
16
+ - seo_monitor migrated to consume `hazo_images/runware` in the same release; seo_monitor's old `lib/runware/client.ts` deleted.
17
+
3
18
  ## 1.1.0 (2026-05-29) — Wave 2 standardisation
4
19
 
5
20
  ### Changed
package/README.md CHANGED
@@ -153,6 +153,50 @@ interface ProcessImageOptions {
153
153
  }
154
154
  ```
155
155
 
156
+ ## Runware (AI image generation)
157
+
158
+ `hazo_images/runware` wraps the Runware REST API behind a factory client.
159
+
160
+ ```ts
161
+ import { createRunwareClient } from 'hazo_images/runware';
162
+
163
+ const client = createRunwareClient({
164
+ // apiKey?: defaults to HAZO_IMAGES_RUNWARE_API_KEY then RUNWARE_API_KEY
165
+ defaultModel: 'runware:z-image@turbo',
166
+ timeoutMs: 60_000,
167
+ });
168
+
169
+ const { imageURL, cost, taskUUID } = await client.generateImage({
170
+ positivePrompt: 'a chibi explorer with a stopwatch',
171
+ width: 1024,
172
+ height: 1024,
173
+ });
174
+ ```
175
+
176
+ ### Error classes
177
+
178
+ | Class | Triggered by | Code(s) |
179
+ |---|---|---|
180
+ | `RunwareApiError` | network fail, 5xx, abort, empty result | `HAZO_IMAGES_RUNWARE_HTTP_FAILURE`, `HAZO_IMAGES_RUNWARE_TIMEOUT`, `HAZO_IMAGES_RUNWARE_EMPTY_RESULT` |
181
+ | `RunwareAuthError` | missing key, 401, 403 | `HAZO_IMAGES_RUNWARE_MISSING_KEY`, `HAZO_IMAGES_RUNWARE_AUTH_FAILED` |
182
+ | `RunwareRateLimitError` | 429 — exposes `retryAfter: number \| null` | `HAZO_IMAGES_RUNWARE_RATE_LIMITED` |
183
+ | `RunwareValidationError` | 400 or `errors[]` in body | upstream code or `HAZO_IMAGES_RUNWARE_BAD_REQUEST` |
184
+
185
+ ### `assemblePrompts`
186
+
187
+ Generic helper for the prefix + character + image + suffix pattern:
188
+
189
+ ```ts
190
+ import { assemblePrompts } from 'hazo_images/runware';
191
+
192
+ const { positive, negative } = assemblePrompts({
193
+ imagePrompt: 'astronaut on mars',
194
+ masterStylePrefix: 'cinematic,',
195
+ characterPositive: 'red spacesuit',
196
+ masterNegative: 'low quality, blurry',
197
+ });
198
+ ```
199
+
156
200
  ## Roadmap
157
201
 
158
202
  | Version | Scope |
@@ -58,7 +58,14 @@ const nextConfig = {
58
58
  module.exports = nextConfig;
59
59
  ```
60
60
 
61
- ## 6. (Optional) Drop in `hazo_images_config.ini`
61
+ ## 6. (Optional) Environment variables for hazo_images/runware
62
+
63
+ Only needed if using the `hazo_images/runware` sub-export (Runware REST API client):
64
+
65
+ - `HAZO_IMAGES_RUNWARE_API_KEY` (preferred) — API key from [runware.ai](https://runware.ai)
66
+ - `RUNWARE_API_KEY` (legacy fallback) — older projects may use this; new code should prefer `HAZO_IMAGES_RUNWARE_API_KEY`
67
+
68
+ ## 7. (Optional) Drop in `hazo_images_config.ini`
62
69
 
63
70
  The package ships `config/hazo_images_config.ini.sample` with sections
64
71
  for `[general]`, `[log.overrides]`, and `[processing]` defaults. Copy
@@ -71,7 +78,7 @@ Per `D-020`, a per-environment overlay file at
71
78
  `config/hazo_images_config.<HAZO_ENV>.ini` is layered on top of the
72
79
  base file when `HAZO_ENV` is set.
73
80
 
74
- ## 7. Verify
81
+ ## 8. Verify
75
82
 
76
83
  ```ts
77
84
  import { processImage } from 'hazo_images/server';
@@ -0,0 +1,83 @@
1
+ import { HazoCoreLogger, HazoError } from 'hazo_core';
2
+
3
+ interface CreateRunwareClientOptions {
4
+ apiKey?: string;
5
+ baseUrl?: string;
6
+ defaultModel?: string;
7
+ timeoutMs?: number;
8
+ fetch?: typeof fetch;
9
+ logger?: HazoCoreLogger;
10
+ }
11
+ interface GenerateImageParams {
12
+ positivePrompt: string;
13
+ negativePrompt?: string;
14
+ model?: string;
15
+ width: number;
16
+ height: number;
17
+ numberResults?: number;
18
+ outputFormat?: 'PNG' | 'JPEG' | 'WEBP';
19
+ outputType?: 'URL' | 'base64Data';
20
+ }
21
+ interface GenerateImageResult {
22
+ imageURL?: string;
23
+ imageBase64Data?: string;
24
+ cost: number | null;
25
+ taskUUID: string;
26
+ }
27
+ interface RunwareClient {
28
+ generateImage(params: GenerateImageParams): Promise<GenerateImageResult>;
29
+ getBalance(): Promise<number>;
30
+ }
31
+ interface AssemblePromptsInput {
32
+ imagePrompt: string;
33
+ masterStylePrefix?: string | null;
34
+ masterStyleSuffix?: string | null;
35
+ characterPositive?: string | null;
36
+ masterNegative?: string | null;
37
+ characterNegative?: string | null;
38
+ callNegative?: string | null;
39
+ }
40
+ interface ResolvedPrompts {
41
+ positive: string;
42
+ negative: string | undefined;
43
+ }
44
+
45
+ declare function createRunwareClient(opts?: CreateRunwareClientOptions): RunwareClient;
46
+
47
+ declare function assemblePrompts(input: AssemblePromptsInput): ResolvedPrompts;
48
+
49
+ declare const RUNWARE_ERROR_CODES: {
50
+ readonly MISSING_KEY: "HAZO_IMAGES_RUNWARE_MISSING_KEY";
51
+ readonly AUTH_FAILED: "HAZO_IMAGES_RUNWARE_AUTH_FAILED";
52
+ readonly RATE_LIMITED: "HAZO_IMAGES_RUNWARE_RATE_LIMITED";
53
+ readonly HTTP_FAILURE: "HAZO_IMAGES_RUNWARE_HTTP_FAILURE";
54
+ readonly TIMEOUT: "HAZO_IMAGES_RUNWARE_TIMEOUT";
55
+ readonly EMPTY_RESULT: "HAZO_IMAGES_RUNWARE_EMPTY_RESULT";
56
+ readonly BAD_REQUEST: "HAZO_IMAGES_RUNWARE_BAD_REQUEST";
57
+ readonly MISSING_MODEL: "HAZO_IMAGES_RUNWARE_MISSING_MODEL";
58
+ };
59
+ type RunwareErrorCode = (typeof RUNWARE_ERROR_CODES)[keyof typeof RUNWARE_ERROR_CODES];
60
+ interface RunwareErrorContext {
61
+ status?: number;
62
+ body?: unknown;
63
+ taskUUID?: string;
64
+ code?: string;
65
+ cause?: unknown;
66
+ }
67
+ declare class RunwareApiError extends HazoError {
68
+ constructor(code: string, message: string, context?: RunwareErrorContext);
69
+ }
70
+ declare class RunwareAuthError extends HazoError {
71
+ constructor(code: string, message: string, context?: RunwareErrorContext);
72
+ }
73
+ declare class RunwareRateLimitError extends HazoError {
74
+ retryAfter: number | null;
75
+ constructor(message: string, context: RunwareErrorContext & {
76
+ retryAfter: number | null;
77
+ });
78
+ }
79
+ declare class RunwareValidationError extends HazoError {
80
+ constructor(code: string, message: string, context?: RunwareErrorContext);
81
+ }
82
+
83
+ export { type AssemblePromptsInput, type CreateRunwareClientOptions, type GenerateImageParams, type GenerateImageResult, RUNWARE_ERROR_CODES, type ResolvedPrompts, RunwareApiError, RunwareAuthError, type RunwareClient, type RunwareErrorCode, RunwareRateLimitError, RunwareValidationError, assemblePrompts, createRunwareClient };
@@ -0,0 +1,371 @@
1
+ // src/runware/client.ts
2
+ import { fetchWithRequestId, generateRequestId } from "hazo_core";
3
+
4
+ // src/utils/logger.ts
5
+ import { createLogger } from "hazo_core";
6
+ var console_logger = {
7
+ info: (m, d) => d ? console.log(`[hazo_images] ${m}`, d) : console.log(`[hazo_images] ${m}`),
8
+ debug: (m, d) => d ? console.debug(`[hazo_images] ${m}`, d) : console.debug(`[hazo_images] ${m}`),
9
+ warn: (m, d) => d ? console.warn(`[hazo_images] ${m}`, d) : console.warn(`[hazo_images] ${m}`),
10
+ error: (m, d) => d ? console.error(`[hazo_images] ${m}`, d) : console.error(`[hazo_images] ${m}`)
11
+ };
12
+ function build_default_logger() {
13
+ try {
14
+ return createLogger("hazo_images");
15
+ } catch {
16
+ return console_logger;
17
+ }
18
+ }
19
+ var current_logger = null;
20
+ function get_logger() {
21
+ if (!current_logger) {
22
+ current_logger = build_default_logger();
23
+ }
24
+ return current_logger;
25
+ }
26
+
27
+ // src/runware/errors.ts
28
+ import { HazoError } from "hazo_core";
29
+ var RUNWARE_ERROR_CODES = {
30
+ MISSING_KEY: "HAZO_IMAGES_RUNWARE_MISSING_KEY",
31
+ AUTH_FAILED: "HAZO_IMAGES_RUNWARE_AUTH_FAILED",
32
+ RATE_LIMITED: "HAZO_IMAGES_RUNWARE_RATE_LIMITED",
33
+ HTTP_FAILURE: "HAZO_IMAGES_RUNWARE_HTTP_FAILURE",
34
+ TIMEOUT: "HAZO_IMAGES_RUNWARE_TIMEOUT",
35
+ EMPTY_RESULT: "HAZO_IMAGES_RUNWARE_EMPTY_RESULT",
36
+ BAD_REQUEST: "HAZO_IMAGES_RUNWARE_BAD_REQUEST",
37
+ MISSING_MODEL: "HAZO_IMAGES_RUNWARE_MISSING_MODEL"
38
+ };
39
+ var RunwareApiError = class extends HazoError {
40
+ constructor(code, message, context = {}) {
41
+ super({ code, pkg: "hazo_images", message, context });
42
+ this.name = "RunwareApiError";
43
+ }
44
+ };
45
+ var RunwareAuthError = class extends HazoError {
46
+ constructor(code, message, context = {}) {
47
+ super({ code, pkg: "hazo_images", message, context });
48
+ this.name = "RunwareAuthError";
49
+ }
50
+ };
51
+ var RunwareRateLimitError = class extends HazoError {
52
+ retryAfter;
53
+ constructor(message, context) {
54
+ const { retryAfter, ...rest } = context;
55
+ super({
56
+ code: RUNWARE_ERROR_CODES.RATE_LIMITED,
57
+ pkg: "hazo_images",
58
+ message,
59
+ context: { ...rest, retryAfter }
60
+ });
61
+ this.name = "RunwareRateLimitError";
62
+ this.retryAfter = retryAfter;
63
+ }
64
+ };
65
+ var RunwareValidationError = class extends HazoError {
66
+ constructor(code, message, context = {}) {
67
+ super({ code, pkg: "hazo_images", message, context });
68
+ this.name = "RunwareValidationError";
69
+ }
70
+ };
71
+
72
+ // src/runware/client.ts
73
+ var DEFAULT_BASE_URL = "https://api.runware.ai/v1";
74
+ var DEFAULT_TIMEOUT_MS = 6e4;
75
+ function resolveApiKey(optsApiKey) {
76
+ if (optsApiKey) return optsApiKey;
77
+ if (process.env.HAZO_IMAGES_RUNWARE_API_KEY) return process.env.HAZO_IMAGES_RUNWARE_API_KEY;
78
+ if (process.env.RUNWARE_API_KEY) return process.env.RUNWARE_API_KEY;
79
+ throw new RunwareAuthError(
80
+ RUNWARE_ERROR_CODES.MISSING_KEY,
81
+ "Runware API key not found. Set HAZO_IMAGES_RUNWARE_API_KEY or RUNWARE_API_KEY, or pass apiKey to createRunwareClient."
82
+ );
83
+ }
84
+ function createRunwareClient(opts = {}) {
85
+ const logger = opts.logger ?? get_logger();
86
+ const cfg = {
87
+ baseUrl: opts.baseUrl ?? DEFAULT_BASE_URL,
88
+ defaultModel: opts.defaultModel,
89
+ timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
90
+ fetchImpl: opts.fetch ?? fetchWithRequestId,
91
+ optsApiKey: opts.apiKey,
92
+ logger
93
+ };
94
+ logger.info("runware.client.created", {
95
+ baseUrl: cfg.baseUrl,
96
+ defaultModel: cfg.defaultModel ?? null,
97
+ timeoutMs: cfg.timeoutMs
98
+ });
99
+ return {
100
+ async generateImage(params) {
101
+ const start = Date.now();
102
+ const apiKey = resolveApiKey(cfg.optsApiKey);
103
+ const model = params.model ?? cfg.defaultModel;
104
+ if (!model) {
105
+ const err = new RunwareValidationError(
106
+ RUNWARE_ERROR_CODES.MISSING_MODEL,
107
+ "Runware generateImage requires a `model` (pass per-call or as defaultModel to createRunwareClient).",
108
+ { taskUUID: "pre-task" }
109
+ );
110
+ cfg.logger.error("runware.generate_image.failed", {
111
+ errorCode: err.code,
112
+ errorClass: err.name,
113
+ durationMs: Date.now() - start
114
+ });
115
+ throw err;
116
+ }
117
+ const taskUUID = generateRequestId();
118
+ cfg.logger.info("runware.generate_image.started", { model, taskUUID });
119
+ const task = {
120
+ taskType: "imageInference",
121
+ taskUUID,
122
+ positivePrompt: params.positivePrompt,
123
+ ...params.negativePrompt ? { negativePrompt: params.negativePrompt } : {},
124
+ model,
125
+ width: params.width,
126
+ height: params.height,
127
+ numberResults: params.numberResults ?? 1,
128
+ outputFormat: params.outputFormat ?? "PNG",
129
+ outputType: params.outputType ?? "URL"
130
+ };
131
+ const controller = new AbortController();
132
+ const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
133
+ let res;
134
+ try {
135
+ res = await cfg.fetchImpl(cfg.baseUrl, {
136
+ method: "POST",
137
+ headers: {
138
+ "Content-Type": "application/json",
139
+ Authorization: `Bearer ${apiKey}`
140
+ },
141
+ body: JSON.stringify([task]),
142
+ signal: controller.signal
143
+ });
144
+ } catch (err) {
145
+ clearTimeout(timer);
146
+ let thrownErr;
147
+ if (err instanceof DOMException && err.name === "AbortError") {
148
+ thrownErr = new RunwareApiError(
149
+ RUNWARE_ERROR_CODES.TIMEOUT,
150
+ `Runware request timed out after ${cfg.timeoutMs}ms`,
151
+ { taskUUID }
152
+ );
153
+ } else {
154
+ thrownErr = new RunwareApiError(
155
+ RUNWARE_ERROR_CODES.HTTP_FAILURE,
156
+ "Runware network error",
157
+ { taskUUID }
158
+ );
159
+ }
160
+ cfg.logger.error("runware.generate_image.failed", {
161
+ taskUUID,
162
+ model,
163
+ errorCode: thrownErr.code,
164
+ errorClass: thrownErr.name,
165
+ durationMs: Date.now() - start
166
+ });
167
+ throw thrownErr;
168
+ }
169
+ clearTimeout(timer);
170
+ if (!res.ok) {
171
+ let bodyText;
172
+ let bodyJson;
173
+ try {
174
+ bodyText = await res.clone().text();
175
+ bodyJson = JSON.parse(bodyText);
176
+ } catch {
177
+ bodyJson = bodyText;
178
+ }
179
+ const errorRecord = bodyJson?.errors?.[0];
180
+ let thrownErr;
181
+ if (res.status === 401 || res.status === 403) {
182
+ thrownErr = new RunwareAuthError(
183
+ RUNWARE_ERROR_CODES.AUTH_FAILED,
184
+ `Runware auth failed (HTTP ${res.status})`,
185
+ { status: res.status, body: bodyJson, taskUUID }
186
+ );
187
+ } else if (res.status === 429) {
188
+ const ra = res.headers.get("retry-after");
189
+ const retryAfter = ra && /^\d+$/.test(ra) ? parseInt(ra, 10) : null;
190
+ thrownErr = new RunwareRateLimitError(`Runware rate limited (HTTP 429)`, {
191
+ status: 429,
192
+ body: bodyJson,
193
+ taskUUID,
194
+ retryAfter
195
+ });
196
+ } else if (res.status === 400 || errorRecord) {
197
+ thrownErr = new RunwareValidationError(
198
+ errorRecord?.code ? `HAZO_IMAGES_RUNWARE_${errorRecord.code.toUpperCase()}` : RUNWARE_ERROR_CODES.BAD_REQUEST,
199
+ errorRecord?.message ?? `Runware validation failed (HTTP ${res.status})`,
200
+ { status: res.status, body: bodyJson, taskUUID, code: errorRecord?.code }
201
+ );
202
+ } else {
203
+ thrownErr = new RunwareApiError(
204
+ RUNWARE_ERROR_CODES.HTTP_FAILURE,
205
+ `Runware HTTP ${res.status}`,
206
+ { status: res.status, body: bodyJson, taskUUID }
207
+ );
208
+ }
209
+ cfg.logger.error("runware.generate_image.failed", {
210
+ taskUUID,
211
+ model,
212
+ errorCode: thrownErr.code,
213
+ errorClass: thrownErr.name,
214
+ durationMs: Date.now() - start
215
+ });
216
+ throw thrownErr;
217
+ }
218
+ const body = await res.json();
219
+ const apiErrors = body.errors;
220
+ if (apiErrors && apiErrors.length > 0) {
221
+ const firstErr = apiErrors[0];
222
+ const thrownErr = new RunwareValidationError(
223
+ firstErr.code ? `HAZO_IMAGES_RUNWARE_${firstErr.code.toUpperCase()}` : RUNWARE_ERROR_CODES.BAD_REQUEST,
224
+ firstErr.message ?? "Runware returned an API error",
225
+ { body, taskUUID, code: firstErr.code }
226
+ );
227
+ cfg.logger.error("runware.generate_image.failed", {
228
+ taskUUID,
229
+ model,
230
+ errorCode: thrownErr.code,
231
+ errorClass: thrownErr.name,
232
+ durationMs: Date.now() - start
233
+ });
234
+ throw thrownErr;
235
+ }
236
+ const first = body.data?.[0];
237
+ if (!first?.imageURL && !first?.imageBase64Data) {
238
+ const thrownErr = new RunwareApiError(
239
+ RUNWARE_ERROR_CODES.EMPTY_RESULT,
240
+ "Runware returned no image data",
241
+ { body, taskUUID }
242
+ );
243
+ cfg.logger.error("runware.generate_image.failed", {
244
+ taskUUID,
245
+ model,
246
+ errorCode: thrownErr.code,
247
+ errorClass: thrownErr.name,
248
+ durationMs: Date.now() - start
249
+ });
250
+ throw thrownErr;
251
+ }
252
+ const result = {
253
+ imageURL: first.imageURL,
254
+ imageBase64Data: first.imageBase64Data,
255
+ cost: typeof first.cost === "number" ? first.cost : null,
256
+ taskUUID
257
+ };
258
+ cfg.logger.info("runware.generate_image.completed", {
259
+ taskUUID,
260
+ model,
261
+ cost: result.cost,
262
+ durationMs: Date.now() - start
263
+ });
264
+ return result;
265
+ },
266
+ async getBalance() {
267
+ const start = Date.now();
268
+ const apiKey = resolveApiKey(cfg.optsApiKey);
269
+ const taskUUID = generateRequestId();
270
+ const controller = new AbortController();
271
+ const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
272
+ let res;
273
+ try {
274
+ res = await cfg.fetchImpl(cfg.baseUrl, {
275
+ method: "POST",
276
+ headers: {
277
+ "Content-Type": "application/json",
278
+ Authorization: `Bearer ${apiKey}`
279
+ },
280
+ body: JSON.stringify([{ taskType: "getUser", taskUUID }]),
281
+ signal: controller.signal
282
+ });
283
+ } catch (err) {
284
+ clearTimeout(timer);
285
+ if (err instanceof DOMException && err.name === "AbortError") {
286
+ const thrownErr2 = new RunwareApiError(
287
+ RUNWARE_ERROR_CODES.TIMEOUT,
288
+ `Runware getBalance timed out`,
289
+ { taskUUID }
290
+ );
291
+ cfg.logger.error("runware.get_balance.failed", {
292
+ taskUUID,
293
+ errorCode: thrownErr2.code,
294
+ errorClass: thrownErr2.name,
295
+ durationMs: Date.now() - start
296
+ });
297
+ throw thrownErr2;
298
+ }
299
+ const thrownErr = new RunwareApiError(
300
+ RUNWARE_ERROR_CODES.HTTP_FAILURE,
301
+ "Runware getBalance network error",
302
+ { taskUUID }
303
+ );
304
+ cfg.logger.error("runware.get_balance.failed", {
305
+ taskUUID,
306
+ errorCode: thrownErr.code,
307
+ errorClass: thrownErr.name,
308
+ durationMs: Date.now() - start
309
+ });
310
+ throw thrownErr;
311
+ }
312
+ clearTimeout(timer);
313
+ if (!res.ok) {
314
+ const thrownErr = new RunwareApiError(
315
+ RUNWARE_ERROR_CODES.HTTP_FAILURE,
316
+ `Runware getBalance HTTP ${res.status}`,
317
+ { status: res.status, taskUUID }
318
+ );
319
+ cfg.logger.error("runware.get_balance.failed", {
320
+ taskUUID,
321
+ errorCode: thrownErr.code,
322
+ errorClass: thrownErr.name,
323
+ durationMs: Date.now() - start
324
+ });
325
+ throw thrownErr;
326
+ }
327
+ const body = await res.json();
328
+ const balance = body.data?.[0]?.balance ?? 0;
329
+ cfg.logger.info("runware.get_balance.completed", {
330
+ taskUUID,
331
+ balance,
332
+ durationMs: Date.now() - start
333
+ });
334
+ return balance;
335
+ }
336
+ };
337
+ }
338
+
339
+ // src/runware/prompt.ts
340
+ function assemblePrompts(input) {
341
+ const {
342
+ imagePrompt,
343
+ masterStylePrefix,
344
+ masterStyleSuffix,
345
+ characterPositive,
346
+ masterNegative,
347
+ characterNegative,
348
+ callNegative
349
+ } = input;
350
+ const positiveParts = [];
351
+ if (masterStylePrefix) positiveParts.push(masterStylePrefix);
352
+ if (characterPositive) positiveParts.push(characterPositive);
353
+ positiveParts.push(imagePrompt);
354
+ if (masterStyleSuffix) positiveParts.push(masterStyleSuffix);
355
+ const positive = positiveParts.join(" ");
356
+ const negativeParts = [];
357
+ if (masterNegative) negativeParts.push(masterNegative);
358
+ if (characterNegative) negativeParts.push(characterNegative);
359
+ if (callNegative) negativeParts.push(callNegative);
360
+ const negative = negativeParts.length > 0 ? negativeParts.join(", ") : void 0;
361
+ return { positive, negative };
362
+ }
363
+ export {
364
+ RUNWARE_ERROR_CODES,
365
+ RunwareApiError,
366
+ RunwareAuthError,
367
+ RunwareRateLimitError,
368
+ RunwareValidationError,
369
+ assemblePrompts,
370
+ createRunwareClient
371
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_images",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Image processing pipeline for the hazo ecosystem — Sharp wrapper, thumbnail generation, EXIF handling, and hazo_files integration helper",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,6 +22,11 @@
22
22
  "types": "./dist/ui/index.d.ts",
23
23
  "import": "./dist/ui/index.js",
24
24
  "default": "./dist/ui/index.js"
25
+ },
26
+ "./runware": {
27
+ "types": "./dist/runware/index.d.ts",
28
+ "import": "./dist/runware/index.js",
29
+ "default": "./dist/runware/index.js"
25
30
  }
26
31
  },
27
32
  "files": [
@@ -35,7 +40,9 @@
35
40
  "build": "tsup",
36
41
  "dev": "tsup --watch",
37
42
  "typecheck": "tsc --noEmit",
38
- "lint": "tsc --noEmit"
43
+ "lint": "tsc --noEmit",
44
+ "test": "node --experimental-vm-modules ../node_modules/jest/bin/jest.js --config jest.config.cjs",
45
+ "test:watch": "node --experimental-vm-modules ../node_modules/jest/bin/jest.js --config jest.config.cjs --watch"
39
46
  },
40
47
  "dependencies": {
41
48
  "ini": "^4.1.0",
@@ -63,9 +70,13 @@
63
70
  }
64
71
  },
65
72
  "devDependencies": {
73
+ "@types/jest": "^30.0.0",
66
74
  "@types/node": "^22.10.0",
67
75
  "@types/react": "^19.0.0",
68
76
  "hazo_core": "^1.0.0",
77
+ "jest": "^30.2.0",
78
+ "jest-environment-node": "^30.2.0",
79
+ "ts-jest": "^29.4.5",
69
80
  "tsup": "^8.0.0",
70
81
  "typescript": "^5.7.2"
71
82
  },