@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,412 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/images/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ CACHE_TTL_MS: () => CACHE_TTL_MS,
34
+ DEFAULT_CLOUDINARY_FOLDER: () => DEFAULT_CLOUDINARY_FOLDER,
35
+ DEFAULT_IMAGE_MODEL: () => DEFAULT_IMAGE_MODEL,
36
+ DEFAULT_IMAGE_QUALITY: () => DEFAULT_IMAGE_QUALITY,
37
+ DEFAULT_IMAGE_SIZE: () => DEFAULT_IMAGE_SIZE,
38
+ ImageGenerationError: () => ImageGenerationError,
39
+ createCaptionComposer: () => createCaptionComposer,
40
+ createCloudinaryUploadClient: () => createCloudinaryUploadClient,
41
+ createImageGenerator: () => createImageGenerator,
42
+ createImageUploader: () => createImageUploader,
43
+ createOpenAIImageClient: () => createOpenAIImageClient,
44
+ createTursoImageCacheStore: () => createTursoImageCacheStore,
45
+ isCacheEntryFresh: () => isCacheEntryFresh,
46
+ isCloudinaryConfigured: () => isCloudinaryConfigured,
47
+ isImageConfigured: () => isImageConfigured,
48
+ publicIdForRequest: () => publicIdForRequest,
49
+ resolveCloudinaryConfig: () => resolveCloudinaryConfig,
50
+ resolveImageConfig: () => resolveImageConfig,
51
+ signCloudinaryParams: () => signCloudinaryParams
52
+ });
53
+ module.exports = __toCommonJS(index_exports);
54
+
55
+ // src/images/cache.ts
56
+ var import_node_crypto = require("node:crypto");
57
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
58
+ function publicIdForRequest(prompt, images) {
59
+ const hash = (0, import_node_crypto.createHash)("sha256").update(prompt);
60
+ if (images?.baseImage) hash.update(images.baseImage.bytes);
61
+ for (const image of images?.inputImages ?? []) {
62
+ hash.update(image.bytes);
63
+ }
64
+ return hash.digest("hex");
65
+ }
66
+ function isCacheEntryFresh(entry, now) {
67
+ return now - entry.createdAt < CACHE_TTL_MS;
68
+ }
69
+
70
+ // src/images/cacheStore.ts
71
+ var schemaReady = /* @__PURE__ */ new WeakMap();
72
+ function ensureCacheSchema(client, tableName) {
73
+ let perTable = schemaReady.get(client);
74
+ if (!perTable) {
75
+ perTable = /* @__PURE__ */ new Map();
76
+ schemaReady.set(client, perTable);
77
+ }
78
+ const tables = perTable;
79
+ let ready = tables.get(tableName);
80
+ if (!ready) {
81
+ ready = client.execute(
82
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
83
+ public_id TEXT PRIMARY KEY,
84
+ url TEXT NOT NULL,
85
+ created_at INTEGER NOT NULL
86
+ )`
87
+ ).catch((error) => {
88
+ tables.delete(tableName);
89
+ throw error;
90
+ });
91
+ tables.set(tableName, ready);
92
+ }
93
+ return ready;
94
+ }
95
+ var VALID_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
96
+ function createTursoImageCacheStore(client, tableName) {
97
+ if (!VALID_TABLE_NAME.test(tableName)) {
98
+ throw new Error(`Invalid image cache table name: ${tableName}`);
99
+ }
100
+ return {
101
+ async get(key) {
102
+ await ensureCacheSchema(client, tableName);
103
+ const result = await client.execute({
104
+ sql: `SELECT url, created_at FROM ${tableName} WHERE public_id = ?`,
105
+ args: [key]
106
+ });
107
+ const row = result.rows[0];
108
+ if (!row) return null;
109
+ return { url: String(row.url), createdAt: Number(row.created_at) };
110
+ },
111
+ async set(key, entry) {
112
+ await ensureCacheSchema(client, tableName);
113
+ await client.execute({
114
+ sql: `INSERT INTO ${tableName} (public_id, url, created_at) VALUES (?, ?, ?)
115
+ ON CONFLICT(public_id) DO UPDATE SET url = excluded.url, created_at = excluded.created_at`,
116
+ args: [key, entry.url, entry.createdAt]
117
+ });
118
+ }
119
+ };
120
+ }
121
+
122
+ // src/images/captionComposer.ts
123
+ var DEFAULT_TIMEOUT_MS = 8e3;
124
+ var MAX_COMPOSED_LENGTH = 300;
125
+ function createCaptionComposer(options) {
126
+ const { pool } = options;
127
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
128
+ const random = options.random ?? Math.random;
129
+ function randomCaption(subject) {
130
+ if (pool.length === 0) return "";
131
+ const eligible = pool.filter((template2) => subject || !template2.includes("{subject}"));
132
+ const candidates = eligible.length > 0 ? eligible : pool;
133
+ const template = candidates[Math.floor(random() * candidates.length)] ?? candidates[0] ?? "";
134
+ return template.replaceAll("{subject}", subject ?? "");
135
+ }
136
+ return {
137
+ randomCaption,
138
+ async composeCaption(request) {
139
+ if (!options.compose) return randomCaption(request.subject);
140
+ let timer;
141
+ try {
142
+ const text = await Promise.race([
143
+ options.compose(request),
144
+ new Promise((_, reject) => {
145
+ timer = setTimeout(() => reject(new Error("caption compose timed out")), timeoutMs);
146
+ })
147
+ ]);
148
+ const trimmed = text.trim();
149
+ return trimmed.length > 0 && trimmed.length <= MAX_COMPOSED_LENGTH ? trimmed : randomCaption(request.subject);
150
+ } catch {
151
+ return randomCaption(request.subject);
152
+ } finally {
153
+ clearTimeout(timer);
154
+ }
155
+ }
156
+ };
157
+ }
158
+
159
+ // src/images/cloudinaryConfig.ts
160
+ var DEFAULT_CLOUDINARY_FOLDER = "generated";
161
+ function pick(...values) {
162
+ return values.find((value) => typeof value === "string" && value.length > 0);
163
+ }
164
+ function resolveCloudinaryConfig(env = {}, overrides = {}) {
165
+ return {
166
+ cloudName: pick(overrides.cloudName, env.CLOUDINARY_CLOUD_NAME),
167
+ apiKey: pick(overrides.apiKey, env.CLOUDINARY_API_KEY),
168
+ apiSecret: pick(overrides.apiSecret, env.CLOUDINARY_API_SECRET),
169
+ folder: pick(overrides.folder, env.CLOUDINARY_FOLDER, DEFAULT_CLOUDINARY_FOLDER) ?? DEFAULT_CLOUDINARY_FOLDER
170
+ };
171
+ }
172
+ function isCloudinaryConfigured(config) {
173
+ return Boolean(config.cloudName && config.apiKey && config.apiSecret);
174
+ }
175
+
176
+ // src/images/cloudinarySignature.ts
177
+ var import_node_crypto2 = require("node:crypto");
178
+ function signCloudinaryParams(params, apiSecret) {
179
+ const toSign = Object.keys(params).sort().map((key) => `${key}=${params[key]}`).join("&");
180
+ return (0, import_node_crypto2.createHash)("sha1").update(`${toSign}${apiSecret}`).digest("hex");
181
+ }
182
+
183
+ // src/images/types.ts
184
+ var ImageGenerationError = class extends Error {
185
+ code;
186
+ constructor(code, message, options) {
187
+ super(message, options);
188
+ this.name = "ImageGenerationError";
189
+ this.code = code;
190
+ }
191
+ };
192
+
193
+ // src/images/cloudinaryClient.ts
194
+ function createCloudinaryUploadClient(config, fetchImpl = fetch) {
195
+ return {
196
+ async upload({ imageBytes, publicId, folder }) {
197
+ if (!isCloudinaryConfigured(config)) {
198
+ throw new ImageGenerationError("not_configured", "Image upload is not configured");
199
+ }
200
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
201
+ const signature = signCloudinaryParams(
202
+ { folder, public_id: publicId, timestamp },
203
+ config.apiSecret
204
+ );
205
+ const form = new FormData();
206
+ form.set(
207
+ "file",
208
+ new Blob([Buffer.from(imageBytes)], { type: "image/png" }),
209
+ `${publicId}.png`
210
+ );
211
+ form.set("public_id", publicId);
212
+ form.set("folder", folder);
213
+ form.set("timestamp", timestamp);
214
+ form.set("api_key", config.apiKey);
215
+ form.set("signature", signature);
216
+ const response = await fetchImpl(
217
+ `https://api.cloudinary.com/v1_1/${config.cloudName}/image/upload`,
218
+ { method: "POST", body: form }
219
+ );
220
+ if (!response.ok) {
221
+ const body = await response.text().catch(() => "");
222
+ throw new ImageGenerationError(
223
+ "api_error",
224
+ `Cloudinary upload failed: ${response.status} ${body}`
225
+ );
226
+ }
227
+ const data = await response.json();
228
+ if (!data.secure_url) {
229
+ throw new ImageGenerationError("api_error", "Cloudinary upload returned no secure_url");
230
+ }
231
+ return { secureUrl: data.secure_url };
232
+ }
233
+ };
234
+ }
235
+
236
+ // src/images/config.ts
237
+ var DEFAULT_IMAGE_MODEL = "gpt-image-1";
238
+ var DEFAULT_IMAGE_SIZE = "1024x1024";
239
+ var DEFAULT_IMAGE_QUALITY = "medium";
240
+ function pick2(...values) {
241
+ return values.find((value) => typeof value === "string" && value.length > 0) ?? "";
242
+ }
243
+ function resolveImageConfig(env = {}, overrides = {}) {
244
+ return {
245
+ apiKey: overrides.apiKey ?? env.OPENAI_API_KEY,
246
+ model: pick2(overrides.model, env.IMAGE_MODEL, DEFAULT_IMAGE_MODEL),
247
+ size: pick2(overrides.size, env.IMAGE_SIZE, DEFAULT_IMAGE_SIZE),
248
+ quality: pick2(overrides.quality, env.IMAGE_QUALITY, DEFAULT_IMAGE_QUALITY)
249
+ };
250
+ }
251
+ function isImageConfigured(config) {
252
+ return typeof config.apiKey === "string" && config.apiKey.length > 0;
253
+ }
254
+
255
+ // src/images/generator.ts
256
+ var MODERATION_PATTERN = /moderation|safety system|content policy|rejected as a result/i;
257
+ var DEFAULT_MIME_TYPE = "image/png";
258
+ function extensionForMimeType(mimeType) {
259
+ const subtype = mimeType.split("/")[1];
260
+ return subtype === "jpeg" ? "jpg" : subtype || "png";
261
+ }
262
+ function toImageError(error) {
263
+ if (error instanceof ImageGenerationError) return error;
264
+ const code = error?.code;
265
+ const message = error instanceof Error ? error.message : String(error);
266
+ if (code === "moderation_blocked" || MODERATION_PATTERN.test(message)) {
267
+ return new ImageGenerationError("moderation", message, { cause: error });
268
+ }
269
+ return new ImageGenerationError("api_error", message, { cause: error });
270
+ }
271
+ function createImageGenerator(config, deps) {
272
+ return {
273
+ isConfigured: () => isImageConfigured(config),
274
+ async generateImage(request) {
275
+ if (!isImageConfigured(config)) {
276
+ throw new ImageGenerationError("not_configured", "Image generation is not configured");
277
+ }
278
+ if (request.prompt.trim().length === 0) {
279
+ throw new ImageGenerationError("invalid_request", "Image request has an empty prompt");
280
+ }
281
+ let response;
282
+ try {
283
+ response = await deps.client.createImage({
284
+ model: config.model,
285
+ prompt: request.prompt,
286
+ size: config.size,
287
+ quality: config.quality,
288
+ n: 1,
289
+ baseImage: request.baseImage ? {
290
+ bytes: request.baseImage.bytes,
291
+ mimeType: request.baseImage.mimeType ?? DEFAULT_MIME_TYPE,
292
+ fileName: `base.${extensionForMimeType(request.baseImage.mimeType ?? DEFAULT_MIME_TYPE)}`
293
+ } : void 0,
294
+ inputImages: request.inputImages?.map((image, index) => ({
295
+ bytes: image.bytes,
296
+ mimeType: image.mimeType ?? DEFAULT_MIME_TYPE,
297
+ fileName: `photo-${index + 1}.${extensionForMimeType(image.mimeType ?? DEFAULT_MIME_TYPE)}`
298
+ }))
299
+ });
300
+ } catch (error) {
301
+ throw toImageError(error);
302
+ }
303
+ const b64 = response?.data?.[0]?.b64_json;
304
+ if (!b64) {
305
+ throw new ImageGenerationError("api_error", "Image API returned no image data");
306
+ }
307
+ return { b64, imageBytes: Uint8Array.from(Buffer.from(b64, "base64")) };
308
+ }
309
+ };
310
+ }
311
+
312
+ // src/images/openaiClient.ts
313
+ var import_openai = __toESM(require("openai"));
314
+ function isGptImageModel(model) {
315
+ return model.startsWith("gpt-image");
316
+ }
317
+ function createOpenAIImageClient(config, client = new import_openai.default({ apiKey: config.apiKey })) {
318
+ return {
319
+ async createImage(params) {
320
+ const responseFormat = isGptImageModel(params.model) ? {} : { response_format: "b64_json" };
321
+ if (!params.baseImage && (!params.inputImages || params.inputImages.length === 0)) {
322
+ const response2 = await client.images.generate({
323
+ model: params.model,
324
+ prompt: params.prompt,
325
+ size: params.size,
326
+ quality: params.quality,
327
+ n: params.n,
328
+ ...responseFormat
329
+ });
330
+ return { data: response2.data };
331
+ }
332
+ const baseFile = params.baseImage ? await (0, import_openai.toFile)(Buffer.from(params.baseImage.bytes), params.baseImage.fileName, {
333
+ type: params.baseImage.mimeType
334
+ }) : void 0;
335
+ const photoFiles = await Promise.all(
336
+ (params.inputImages ?? []).map(
337
+ (photo) => (0, import_openai.toFile)(Buffer.from(photo.bytes), photo.fileName, { type: photo.mimeType })
338
+ )
339
+ );
340
+ const image = baseFile ? [...photoFiles, baseFile] : photoFiles;
341
+ const response = await client.images.edit({
342
+ model: params.model,
343
+ image,
344
+ prompt: params.prompt,
345
+ size: params.size,
346
+ quality: params.quality,
347
+ n: params.n,
348
+ ...responseFormat
349
+ });
350
+ return { data: response.data };
351
+ }
352
+ };
353
+ }
354
+
355
+ // src/images/uploader.ts
356
+ function createImageUploader(config, deps) {
357
+ const now = deps.now ?? Date.now;
358
+ function cacheKey(request) {
359
+ if (!isCloudinaryConfigured(config)) {
360
+ throw new ImageGenerationError("not_configured", "Image upload is not configured");
361
+ }
362
+ return publicIdForRequest(request.prompt, request);
363
+ }
364
+ return {
365
+ // A cache hit never needs the generator, but a fresh upload does — both
366
+ // must be configured for this uploader to actually work end to end.
367
+ isConfigured: () => isCloudinaryConfigured(config) && deps.generator.isConfigured(),
368
+ async peekCached(request) {
369
+ const publicId = cacheKey(request);
370
+ const cached = await deps.cacheStore.get(publicId);
371
+ return !!cached && isCacheEntryFresh(cached, now());
372
+ },
373
+ async getOrCreateImage(request) {
374
+ const publicId = cacheKey(request);
375
+ const nowMs = now();
376
+ const cached = await deps.cacheStore.get(publicId);
377
+ if (cached && isCacheEntryFresh(cached, nowMs)) {
378
+ return { url: cached.url, cached: true };
379
+ }
380
+ const generated = await deps.generator.generateImage(request);
381
+ const uploaded = await deps.uploadClient.upload({
382
+ imageBytes: generated.imageBytes,
383
+ publicId,
384
+ folder: config.folder
385
+ });
386
+ await deps.cacheStore.set(publicId, { url: uploaded.secureUrl, createdAt: nowMs });
387
+ return { url: uploaded.secureUrl, cached: false };
388
+ }
389
+ };
390
+ }
391
+ // Annotate the CommonJS export names for ESM import in node:
392
+ 0 && (module.exports = {
393
+ CACHE_TTL_MS,
394
+ DEFAULT_CLOUDINARY_FOLDER,
395
+ DEFAULT_IMAGE_MODEL,
396
+ DEFAULT_IMAGE_QUALITY,
397
+ DEFAULT_IMAGE_SIZE,
398
+ ImageGenerationError,
399
+ createCaptionComposer,
400
+ createCloudinaryUploadClient,
401
+ createImageGenerator,
402
+ createImageUploader,
403
+ createOpenAIImageClient,
404
+ createTursoImageCacheStore,
405
+ isCacheEntryFresh,
406
+ isCloudinaryConfigured,
407
+ isImageConfigured,
408
+ publicIdForRequest,
409
+ resolveCloudinaryConfig,
410
+ resolveImageConfig,
411
+ signCloudinaryParams
412
+ });