@diegoaltoworks/chatter 0.32.0 → 0.33.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 (47) hide show
  1. package/dist/images/cache.d.ts +32 -0
  2. package/dist/images/cache.d.ts.map +1 -0
  3. package/dist/images/cache.test.d.ts +2 -0
  4. package/dist/images/cache.test.d.ts.map +1 -0
  5. package/dist/images/cacheStore.d.ts +20 -0
  6. package/dist/images/cacheStore.d.ts.map +1 -0
  7. package/dist/images/cacheStore.test.d.ts +8 -0
  8. package/dist/images/cacheStore.test.d.ts.map +1 -0
  9. package/dist/images/captionComposer.d.ts +33 -0
  10. package/dist/images/captionComposer.d.ts.map +1 -0
  11. package/dist/images/captionComposer.test.d.ts +2 -0
  12. package/dist/images/captionComposer.test.d.ts.map +1 -0
  13. package/dist/images/cloudinaryClient.d.ts +12 -0
  14. package/dist/images/cloudinaryClient.d.ts.map +1 -0
  15. package/dist/images/cloudinaryClient.test.d.ts +2 -0
  16. package/dist/images/cloudinaryClient.test.d.ts.map +1 -0
  17. package/dist/images/cloudinaryConfig.d.ts +29 -0
  18. package/dist/images/cloudinaryConfig.d.ts.map +1 -0
  19. package/dist/images/cloudinaryConfig.test.d.ts +2 -0
  20. package/dist/images/cloudinaryConfig.test.d.ts.map +1 -0
  21. package/dist/images/cloudinarySignature.d.ts +9 -0
  22. package/dist/images/cloudinarySignature.d.ts.map +1 -0
  23. package/dist/images/cloudinarySignature.test.d.ts +2 -0
  24. package/dist/images/cloudinarySignature.test.d.ts.map +1 -0
  25. package/dist/images/config.d.ts +28 -0
  26. package/dist/images/config.d.ts.map +1 -0
  27. package/dist/images/config.test.d.ts +2 -0
  28. package/dist/images/config.test.d.ts.map +1 -0
  29. package/dist/images/generator.d.ts +19 -0
  30. package/dist/images/generator.d.ts.map +1 -0
  31. package/dist/images/generator.test.d.ts +2 -0
  32. package/dist/images/generator.test.d.ts.map +1 -0
  33. package/dist/images/index.d.ts +34 -0
  34. package/dist/images/index.d.ts.map +1 -0
  35. package/dist/images/index.js +412 -0
  36. package/dist/images/index.mjs +357 -0
  37. package/dist/images/openaiClient.d.ts +14 -0
  38. package/dist/images/openaiClient.d.ts.map +1 -0
  39. package/dist/images/openaiClient.test.d.ts +2 -0
  40. package/dist/images/openaiClient.test.d.ts.map +1 -0
  41. package/dist/images/types.d.ts +96 -0
  42. package/dist/images/types.d.ts.map +1 -0
  43. package/dist/images/uploader.d.ts +39 -0
  44. package/dist/images/uploader.d.ts.map +1 -0
  45. package/dist/images/uploader.test.d.ts +2 -0
  46. package/dist/images/uploader.test.d.ts.map +1 -0
  47. package/package.json +8 -2
@@ -0,0 +1,357 @@
1
+ // src/images/cache.ts
2
+ import { createHash } from "node:crypto";
3
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
4
+ function publicIdForRequest(prompt, images) {
5
+ const hash = createHash("sha256").update(prompt);
6
+ if (images?.baseImage) hash.update(images.baseImage.bytes);
7
+ for (const image of images?.inputImages ?? []) {
8
+ hash.update(image.bytes);
9
+ }
10
+ return hash.digest("hex");
11
+ }
12
+ function isCacheEntryFresh(entry, now) {
13
+ return now - entry.createdAt < CACHE_TTL_MS;
14
+ }
15
+
16
+ // src/images/cacheStore.ts
17
+ var schemaReady = /* @__PURE__ */ new WeakMap();
18
+ function ensureCacheSchema(client, tableName) {
19
+ let perTable = schemaReady.get(client);
20
+ if (!perTable) {
21
+ perTable = /* @__PURE__ */ new Map();
22
+ schemaReady.set(client, perTable);
23
+ }
24
+ const tables = perTable;
25
+ let ready = tables.get(tableName);
26
+ if (!ready) {
27
+ ready = client.execute(
28
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
29
+ public_id TEXT PRIMARY KEY,
30
+ url TEXT NOT NULL,
31
+ created_at INTEGER NOT NULL
32
+ )`
33
+ ).catch((error) => {
34
+ tables.delete(tableName);
35
+ throw error;
36
+ });
37
+ tables.set(tableName, ready);
38
+ }
39
+ return ready;
40
+ }
41
+ var VALID_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
42
+ function createTursoImageCacheStore(client, tableName) {
43
+ if (!VALID_TABLE_NAME.test(tableName)) {
44
+ throw new Error(`Invalid image cache table name: ${tableName}`);
45
+ }
46
+ return {
47
+ async get(key) {
48
+ await ensureCacheSchema(client, tableName);
49
+ const result = await client.execute({
50
+ sql: `SELECT url, created_at FROM ${tableName} WHERE public_id = ?`,
51
+ args: [key]
52
+ });
53
+ const row = result.rows[0];
54
+ if (!row) return null;
55
+ return { url: String(row.url), createdAt: Number(row.created_at) };
56
+ },
57
+ async set(key, entry) {
58
+ await ensureCacheSchema(client, tableName);
59
+ await client.execute({
60
+ sql: `INSERT INTO ${tableName} (public_id, url, created_at) VALUES (?, ?, ?)
61
+ ON CONFLICT(public_id) DO UPDATE SET url = excluded.url, created_at = excluded.created_at`,
62
+ args: [key, entry.url, entry.createdAt]
63
+ });
64
+ }
65
+ };
66
+ }
67
+
68
+ // src/images/captionComposer.ts
69
+ var DEFAULT_TIMEOUT_MS = 8e3;
70
+ var MAX_COMPOSED_LENGTH = 300;
71
+ function createCaptionComposer(options) {
72
+ const { pool } = options;
73
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
74
+ const random = options.random ?? Math.random;
75
+ function randomCaption(subject) {
76
+ if (pool.length === 0) return "";
77
+ const eligible = pool.filter((template2) => subject || !template2.includes("{subject}"));
78
+ const candidates = eligible.length > 0 ? eligible : pool;
79
+ const template = candidates[Math.floor(random() * candidates.length)] ?? candidates[0] ?? "";
80
+ return template.replaceAll("{subject}", subject ?? "");
81
+ }
82
+ return {
83
+ randomCaption,
84
+ async composeCaption(request) {
85
+ if (!options.compose) return randomCaption(request.subject);
86
+ let timer;
87
+ try {
88
+ const text = await Promise.race([
89
+ options.compose(request),
90
+ new Promise((_, reject) => {
91
+ timer = setTimeout(() => reject(new Error("caption compose timed out")), timeoutMs);
92
+ })
93
+ ]);
94
+ const trimmed = text.trim();
95
+ return trimmed.length > 0 && trimmed.length <= MAX_COMPOSED_LENGTH ? trimmed : randomCaption(request.subject);
96
+ } catch {
97
+ return randomCaption(request.subject);
98
+ } finally {
99
+ clearTimeout(timer);
100
+ }
101
+ }
102
+ };
103
+ }
104
+
105
+ // src/images/cloudinaryConfig.ts
106
+ var DEFAULT_CLOUDINARY_FOLDER = "generated";
107
+ function pick(...values) {
108
+ return values.find((value) => typeof value === "string" && value.length > 0);
109
+ }
110
+ function resolveCloudinaryConfig(env = {}, overrides = {}) {
111
+ return {
112
+ cloudName: pick(overrides.cloudName, env.CLOUDINARY_CLOUD_NAME),
113
+ apiKey: pick(overrides.apiKey, env.CLOUDINARY_API_KEY),
114
+ apiSecret: pick(overrides.apiSecret, env.CLOUDINARY_API_SECRET),
115
+ folder: pick(overrides.folder, env.CLOUDINARY_FOLDER, DEFAULT_CLOUDINARY_FOLDER) ?? DEFAULT_CLOUDINARY_FOLDER
116
+ };
117
+ }
118
+ function isCloudinaryConfigured(config) {
119
+ return Boolean(config.cloudName && config.apiKey && config.apiSecret);
120
+ }
121
+
122
+ // src/images/cloudinarySignature.ts
123
+ import { createHash as createHash2 } from "node:crypto";
124
+ function signCloudinaryParams(params, apiSecret) {
125
+ const toSign = Object.keys(params).sort().map((key) => `${key}=${params[key]}`).join("&");
126
+ return createHash2("sha1").update(`${toSign}${apiSecret}`).digest("hex");
127
+ }
128
+
129
+ // src/images/types.ts
130
+ var ImageGenerationError = class extends Error {
131
+ code;
132
+ constructor(code, message, options) {
133
+ super(message, options);
134
+ this.name = "ImageGenerationError";
135
+ this.code = code;
136
+ }
137
+ };
138
+
139
+ // src/images/cloudinaryClient.ts
140
+ function createCloudinaryUploadClient(config, fetchImpl = fetch) {
141
+ return {
142
+ async upload({ imageBytes, publicId, folder }) {
143
+ if (!isCloudinaryConfigured(config)) {
144
+ throw new ImageGenerationError("not_configured", "Image upload is not configured");
145
+ }
146
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
147
+ const signature = signCloudinaryParams(
148
+ { folder, public_id: publicId, timestamp },
149
+ config.apiSecret
150
+ );
151
+ const form = new FormData();
152
+ form.set(
153
+ "file",
154
+ new Blob([Buffer.from(imageBytes)], { type: "image/png" }),
155
+ `${publicId}.png`
156
+ );
157
+ form.set("public_id", publicId);
158
+ form.set("folder", folder);
159
+ form.set("timestamp", timestamp);
160
+ form.set("api_key", config.apiKey);
161
+ form.set("signature", signature);
162
+ const response = await fetchImpl(
163
+ `https://api.cloudinary.com/v1_1/${config.cloudName}/image/upload`,
164
+ { method: "POST", body: form }
165
+ );
166
+ if (!response.ok) {
167
+ const body = await response.text().catch(() => "");
168
+ throw new ImageGenerationError(
169
+ "api_error",
170
+ `Cloudinary upload failed: ${response.status} ${body}`
171
+ );
172
+ }
173
+ const data = await response.json();
174
+ if (!data.secure_url) {
175
+ throw new ImageGenerationError("api_error", "Cloudinary upload returned no secure_url");
176
+ }
177
+ return { secureUrl: data.secure_url };
178
+ }
179
+ };
180
+ }
181
+
182
+ // src/images/config.ts
183
+ var DEFAULT_IMAGE_MODEL = "gpt-image-1";
184
+ var DEFAULT_IMAGE_SIZE = "1024x1024";
185
+ var DEFAULT_IMAGE_QUALITY = "medium";
186
+ function pick2(...values) {
187
+ return values.find((value) => typeof value === "string" && value.length > 0) ?? "";
188
+ }
189
+ function resolveImageConfig(env = {}, overrides = {}) {
190
+ return {
191
+ apiKey: overrides.apiKey ?? env.OPENAI_API_KEY,
192
+ model: pick2(overrides.model, env.IMAGE_MODEL, DEFAULT_IMAGE_MODEL),
193
+ size: pick2(overrides.size, env.IMAGE_SIZE, DEFAULT_IMAGE_SIZE),
194
+ quality: pick2(overrides.quality, env.IMAGE_QUALITY, DEFAULT_IMAGE_QUALITY)
195
+ };
196
+ }
197
+ function isImageConfigured(config) {
198
+ return typeof config.apiKey === "string" && config.apiKey.length > 0;
199
+ }
200
+
201
+ // src/images/generator.ts
202
+ var MODERATION_PATTERN = /moderation|safety system|content policy|rejected as a result/i;
203
+ var DEFAULT_MIME_TYPE = "image/png";
204
+ function extensionForMimeType(mimeType) {
205
+ const subtype = mimeType.split("/")[1];
206
+ return subtype === "jpeg" ? "jpg" : subtype || "png";
207
+ }
208
+ function toImageError(error) {
209
+ if (error instanceof ImageGenerationError) return error;
210
+ const code = error?.code;
211
+ const message = error instanceof Error ? error.message : String(error);
212
+ if (code === "moderation_blocked" || MODERATION_PATTERN.test(message)) {
213
+ return new ImageGenerationError("moderation", message, { cause: error });
214
+ }
215
+ return new ImageGenerationError("api_error", message, { cause: error });
216
+ }
217
+ function createImageGenerator(config, deps) {
218
+ return {
219
+ isConfigured: () => isImageConfigured(config),
220
+ async generateImage(request) {
221
+ if (!isImageConfigured(config)) {
222
+ throw new ImageGenerationError("not_configured", "Image generation is not configured");
223
+ }
224
+ if (request.prompt.trim().length === 0) {
225
+ throw new ImageGenerationError("invalid_request", "Image request has an empty prompt");
226
+ }
227
+ let response;
228
+ try {
229
+ response = await deps.client.createImage({
230
+ model: config.model,
231
+ prompt: request.prompt,
232
+ size: config.size,
233
+ quality: config.quality,
234
+ n: 1,
235
+ baseImage: request.baseImage ? {
236
+ bytes: request.baseImage.bytes,
237
+ mimeType: request.baseImage.mimeType ?? DEFAULT_MIME_TYPE,
238
+ fileName: `base.${extensionForMimeType(request.baseImage.mimeType ?? DEFAULT_MIME_TYPE)}`
239
+ } : void 0,
240
+ inputImages: request.inputImages?.map((image, index) => ({
241
+ bytes: image.bytes,
242
+ mimeType: image.mimeType ?? DEFAULT_MIME_TYPE,
243
+ fileName: `photo-${index + 1}.${extensionForMimeType(image.mimeType ?? DEFAULT_MIME_TYPE)}`
244
+ }))
245
+ });
246
+ } catch (error) {
247
+ throw toImageError(error);
248
+ }
249
+ const b64 = response?.data?.[0]?.b64_json;
250
+ if (!b64) {
251
+ throw new ImageGenerationError("api_error", "Image API returned no image data");
252
+ }
253
+ return { b64, imageBytes: Uint8Array.from(Buffer.from(b64, "base64")) };
254
+ }
255
+ };
256
+ }
257
+
258
+ // src/images/openaiClient.ts
259
+ import OpenAI, { toFile } from "openai";
260
+ function isGptImageModel(model) {
261
+ return model.startsWith("gpt-image");
262
+ }
263
+ function createOpenAIImageClient(config, client = new OpenAI({ apiKey: config.apiKey })) {
264
+ return {
265
+ async createImage(params) {
266
+ const responseFormat = isGptImageModel(params.model) ? {} : { response_format: "b64_json" };
267
+ if (!params.baseImage && (!params.inputImages || params.inputImages.length === 0)) {
268
+ const response2 = await client.images.generate({
269
+ model: params.model,
270
+ prompt: params.prompt,
271
+ size: params.size,
272
+ quality: params.quality,
273
+ n: params.n,
274
+ ...responseFormat
275
+ });
276
+ return { data: response2.data };
277
+ }
278
+ const baseFile = params.baseImage ? await toFile(Buffer.from(params.baseImage.bytes), params.baseImage.fileName, {
279
+ type: params.baseImage.mimeType
280
+ }) : void 0;
281
+ const photoFiles = await Promise.all(
282
+ (params.inputImages ?? []).map(
283
+ (photo) => toFile(Buffer.from(photo.bytes), photo.fileName, { type: photo.mimeType })
284
+ )
285
+ );
286
+ const image = baseFile ? [...photoFiles, baseFile] : photoFiles;
287
+ const response = await client.images.edit({
288
+ model: params.model,
289
+ image,
290
+ prompt: params.prompt,
291
+ size: params.size,
292
+ quality: params.quality,
293
+ n: params.n,
294
+ ...responseFormat
295
+ });
296
+ return { data: response.data };
297
+ }
298
+ };
299
+ }
300
+
301
+ // src/images/uploader.ts
302
+ function createImageUploader(config, deps) {
303
+ const now = deps.now ?? Date.now;
304
+ function cacheKey(request) {
305
+ if (!isCloudinaryConfigured(config)) {
306
+ throw new ImageGenerationError("not_configured", "Image upload is not configured");
307
+ }
308
+ return publicIdForRequest(request.prompt, request);
309
+ }
310
+ return {
311
+ // A cache hit never needs the generator, but a fresh upload does — both
312
+ // must be configured for this uploader to actually work end to end.
313
+ isConfigured: () => isCloudinaryConfigured(config) && deps.generator.isConfigured(),
314
+ async peekCached(request) {
315
+ const publicId = cacheKey(request);
316
+ const cached = await deps.cacheStore.get(publicId);
317
+ return !!cached && isCacheEntryFresh(cached, now());
318
+ },
319
+ async getOrCreateImage(request) {
320
+ const publicId = cacheKey(request);
321
+ const nowMs = now();
322
+ const cached = await deps.cacheStore.get(publicId);
323
+ if (cached && isCacheEntryFresh(cached, nowMs)) {
324
+ return { url: cached.url, cached: true };
325
+ }
326
+ const generated = await deps.generator.generateImage(request);
327
+ const uploaded = await deps.uploadClient.upload({
328
+ imageBytes: generated.imageBytes,
329
+ publicId,
330
+ folder: config.folder
331
+ });
332
+ await deps.cacheStore.set(publicId, { url: uploaded.secureUrl, createdAt: nowMs });
333
+ return { url: uploaded.secureUrl, cached: false };
334
+ }
335
+ };
336
+ }
337
+ export {
338
+ CACHE_TTL_MS,
339
+ DEFAULT_CLOUDINARY_FOLDER,
340
+ DEFAULT_IMAGE_MODEL,
341
+ DEFAULT_IMAGE_QUALITY,
342
+ DEFAULT_IMAGE_SIZE,
343
+ ImageGenerationError,
344
+ createCaptionComposer,
345
+ createCloudinaryUploadClient,
346
+ createImageGenerator,
347
+ createImageUploader,
348
+ createOpenAIImageClient,
349
+ createTursoImageCacheStore,
350
+ isCacheEntryFresh,
351
+ isCloudinaryConfigured,
352
+ isImageConfigured,
353
+ publicIdForRequest,
354
+ resolveCloudinaryConfig,
355
+ resolveImageConfig,
356
+ signCloudinaryParams
357
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The real image client, backed by the OpenAI Images API.
3
+ *
4
+ * This is the only file in the module that talks to a paid API. `client` is
5
+ * injectable (defaulting to a real `OpenAI` instance), so tests substitute a
6
+ * mock and never reach it. A request with no base/input images calls
7
+ * `images.generate` (pure text-to-image); a request with either calls
8
+ * `images.edit`.
9
+ */
10
+ import OpenAI from "openai";
11
+ import type { ImageModuleConfig } from "./config";
12
+ import type { ImageClient } from "./types";
13
+ export declare function createOpenAIImageClient(config: ImageModuleConfig, client?: OpenAI): ImageClient;
14
+ //# sourceMappingURL=openaiClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openaiClient.d.ts","sourceRoot":"","sources":["../../src/images/openaiClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,MAAkB,MAAM,QAAQ,CAAC;AACxC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAA2C,MAAM,SAAS,CAAC;AAcpF,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,iBAAiB,EACzB,MAAM,GAAE,MAA8C,GACrD,WAAW,CA+Cb"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=openaiClient.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openaiClient.test.d.ts","sourceRoot":"","sources":["../../src/images/openaiClient.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Image generation - public types.
3
+ *
4
+ * Framework-free: this file (and everything else in src/images/) must not import
5
+ * from chatter's core, routes or channels. The module is self-contained and
6
+ * carries no persona or bot-specific content - prompts arrive fully composed
7
+ * from the caller.
8
+ */
9
+ /** Every failure mode the module reports, so callers can pick a reply per case. */
10
+ export type ImageErrorCode = "not_configured" | "invalid_request" | "moderation" | "api_error";
11
+ /** Typed error - callers switch on `code`, never on message text. */
12
+ export declare class ImageGenerationError extends Error {
13
+ readonly code: ImageErrorCode;
14
+ constructor(code: ImageErrorCode, message: string, options?: {
15
+ cause?: unknown;
16
+ });
17
+ }
18
+ /** Image bytes plus the MIME type they were encoded with. Defaults to PNG when omitted. */
19
+ export interface ImageInput {
20
+ bytes: Uint8Array;
21
+ mimeType?: string;
22
+ }
23
+ /**
24
+ * A request for a generated image.
25
+ *
26
+ * `prompt` is fully composed by the caller - this module has no prompt
27
+ * templates or character content of its own. `baseImage` and `inputImages`
28
+ * are both optional: omit both for a pure text-to-image generate call, or
29
+ * supply them to edit/composite against existing pictures.
30
+ */
31
+ export interface ImageRequest {
32
+ prompt: string;
33
+ /** Image to edit. Omit for a pure generate call. */
34
+ baseImage?: ImageInput;
35
+ /** Additional reference photos, sent alongside `baseImage`. */
36
+ inputImages?: ImageInput[];
37
+ }
38
+ /** A generated picture, ready to be uploaded or attached. */
39
+ export interface GeneratedImage {
40
+ /** Raw PNG bytes. */
41
+ imageBytes: Uint8Array;
42
+ /** The same bytes, base64-encoded, as returned by the image API. */
43
+ b64: string;
44
+ }
45
+ /**
46
+ * The slice of the image API this module depends on.
47
+ *
48
+ * Declared structurally so tests can inject a mock and no real (paid) call is
49
+ * ever made from the suite, and so the module does not hard-depend on one SDK
50
+ * version.
51
+ */
52
+ export interface ImageClient {
53
+ createImage(params: ImageClientRequest): Promise<ImageClientResponse>;
54
+ }
55
+ export interface ImageClientRequest {
56
+ model: string;
57
+ prompt: string;
58
+ size: string;
59
+ quality: string;
60
+ n: number;
61
+ baseImage?: {
62
+ bytes: Uint8Array;
63
+ fileName: string;
64
+ mimeType: string;
65
+ };
66
+ inputImages?: Array<{
67
+ bytes: Uint8Array;
68
+ fileName: string;
69
+ mimeType: string;
70
+ }>;
71
+ }
72
+ export interface ImageClientResponse {
73
+ data?: Array<{
74
+ b64_json?: string | null;
75
+ }> | null;
76
+ }
77
+ /**
78
+ * The slice of the Cloudinary upload API this module depends on.
79
+ *
80
+ * Declared structurally, same reasoning as {@link ImageClient}: tests inject a
81
+ * mock and no real (paid) upload is ever made from the suite.
82
+ */
83
+ export interface CloudinaryUploadClient {
84
+ upload(request: CloudinaryUploadRequest): Promise<CloudinaryUploadResponse>;
85
+ }
86
+ export interface CloudinaryUploadRequest {
87
+ /** PNG bytes to upload. */
88
+ imageBytes: Uint8Array;
89
+ /** Deterministic id derived from the request - see `publicIdForRequest`. */
90
+ publicId: string;
91
+ folder: string;
92
+ }
93
+ export interface CloudinaryUploadResponse {
94
+ secureUrl: string;
95
+ }
96
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/images/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,mFAAmF;AACnF,MAAM,MAAM,cAAc,GAAG,gBAAgB,GAAG,iBAAiB,GAAG,YAAY,GAAG,WAAW,CAAC;AAE/F,qEAAqE;AACrE,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAE9B,YAAY,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAI/E;CACF;AAED,2FAA2F;AAC3F,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC7B,qBAAqB;IACrB,UAAU,EAAE,UAAU,CAAC;IACvB,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,MAAM,CAAC;IACV,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChF;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC;CACnD;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;CAC7E;AAED,MAAM,WAAW,uBAAuB;IACtC,2BAA2B;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;CACnB"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Generate-or-serve-from-cache orchestration.
3
+ *
4
+ * The request is hashed into a deterministic key before anything paid
5
+ * happens, and checked against the cache first - only a miss or an expired
6
+ * entry reaches the image generator and Cloudinary.
7
+ */
8
+ import { type ImageCacheStore } from "./cache";
9
+ import type { CloudinaryModuleConfig } from "./cloudinaryConfig";
10
+ import type { ImageGenerator } from "./generator";
11
+ import { type CloudinaryUploadClient, type ImageRequest } from "./types";
12
+ export interface ImageUploaderDeps {
13
+ generator: ImageGenerator;
14
+ uploadClient: CloudinaryUploadClient;
15
+ cacheStore: ImageCacheStore;
16
+ /** Clock, injectable so cache-freshness tests don't depend on real time. */
17
+ now?: () => number;
18
+ }
19
+ export interface UploadedImage {
20
+ url: string;
21
+ /** True when served from cache without generating or uploading anything new. */
22
+ cached: boolean;
23
+ }
24
+ export interface ImageUploader {
25
+ isConfigured(): boolean;
26
+ /**
27
+ * True when `request` already has a fresh cached upload.
28
+ *
29
+ * Lets a caller decide whether to spend a spend-guard unit *before* calling
30
+ * `getOrCreateImage` - a cache hit costs nothing, so it must not touch a
31
+ * daily cap. Throws the same `not_configured` error `getOrCreateImage`
32
+ * would, so a caller that checks this first sees a consistent error no
33
+ * matter which method it called.
34
+ */
35
+ peekCached(request: ImageRequest): Promise<boolean>;
36
+ getOrCreateImage(request: ImageRequest): Promise<UploadedImage>;
37
+ }
38
+ export declare function createImageUploader(config: CloudinaryModuleConfig, deps: ImageUploaderDeps): ImageUploader;
39
+ //# sourceMappingURL=uploader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/images/uploader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,eAAe,EAAyC,MAAM,SAAS,CAAC;AACtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAEjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,KAAK,sBAAsB,EAAwB,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AAE/F,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,cAAc,CAAC;IAC1B,YAAY,EAAE,sBAAsB,CAAC;IACrC,UAAU,EAAE,eAAe,CAAC;IAC5B,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,IAAI,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACjE;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,iBAAiB,GACtB,aAAa,CA0Cf"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=uploader.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploader.test.d.ts","sourceRoot":"","sources":["../../src/images/uploader.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegoaltoworks/chatter",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Embeddable AI chatbot framework with RAG, authentication, and customizable widgets",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -54,6 +54,11 @@
54
54
  "types": "./dist/channels/whatsapp/index.d.ts",
55
55
  "import": "./dist/channels/whatsapp/index.mjs",
56
56
  "require": "./dist/channels/whatsapp/index.js"
57
+ },
58
+ "./images": {
59
+ "types": "./dist/images/index.d.ts",
60
+ "import": "./dist/images/index.mjs",
61
+ "require": "./dist/images/index.js"
57
62
  }
58
63
  },
59
64
  "files": [
@@ -63,7 +68,7 @@
63
68
  ],
64
69
  "scripts": {
65
70
  "dev": "bun --watch src/index.ts",
66
- "build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:mcp && bun run build:usage && bun run build:personas && bun run build:channels && bun run build:whatsapp && bun run build:flows && bun run build:bin && bun run build:client && bun run build:copy-client",
71
+ "build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:mcp && bun run build:usage && bun run build:personas && bun run build:channels && bun run build:whatsapp && bun run build:flows && bun run build:images && bun run build:bin && bun run build:client && bun run build:copy-client",
67
72
  "build:clean": "rm -rf dist",
68
73
  "build:types": "tsc --emitDeclarationOnly --outDir dist",
69
74
  "build:esm": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.mjs --external:hono --external:openai --external:@libsql/client --external:jose --platform=node --target=node24",
@@ -74,6 +79,7 @@
74
79
  "build:channels": "esbuild src/channels/index.ts --bundle --format=esm --outfile=dist/channels/index.mjs --external:hono --external:openai --external:@libsql/client --platform=node --target=node24 && esbuild src/channels/index.ts --bundle --format=cjs --outfile=dist/channels/index.js --external:hono --external:openai --external:@libsql/client --platform=node --target=node24",
75
80
  "build:whatsapp": "esbuild src/channels/whatsapp/index.ts --bundle --format=esm --outfile=dist/channels/whatsapp/index.mjs --external:@libsql/client --external:@whiskeysockets/baileys --platform=node --target=node24 && esbuild src/channels/whatsapp/index.ts --bundle --format=cjs --outfile=dist/channels/whatsapp/index.js --external:@libsql/client --external:@whiskeysockets/baileys --platform=node --target=node24",
76
81
  "build:flows": "esbuild src/flows/index.ts --bundle --format=esm --outfile=dist/flows/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/flows/index.ts --bundle --format=cjs --outfile=dist/flows/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
82
+ "build:images": "esbuild src/images/index.ts --bundle --format=esm --outfile=dist/images/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/images/index.ts --bundle --format=cjs --outfile=dist/images/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
77
83
  "build:bin": "esbuild bin/create-apikey.ts --bundle --format=cjs --outfile=dist/bin/create-apikey.js --external:jose --platform=node --target=node18 --banner:js='#!/usr/bin/env node' && esbuild bin/wa-pair.ts --bundle --format=cjs --outfile=dist/bin/wa-pair.js --external:@libsql/client --external:@whiskeysockets/baileys --external:qrcode-terminal --platform=node --target=node18 --banner:js='#!/usr/bin/env node'",
78
84
  "build:client": "cd src/client && bun run build",
79
85
  "build:copy-client": "mkdir -p dist/static && cp src/client/dist/chatter.js src/client/dist/chatter.css dist/static/",