best-email-builder 0.6.0 → 0.7.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/CHANGELOG.md +37 -0
- package/README.md +41 -4
- package/dist/ai-server.js +114 -1
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.js +504 -169
- package/dist/ai.js.map +1 -1
- package/dist/builder.js +4 -4
- package/dist/{chunk-5W4TUUPD.js → chunk-3RPWH5PC.js} +3 -3
- package/dist/{chunk-QPAZLCPO.js → chunk-C6WHIBGH.js} +2 -2
- package/dist/{chunk-QPAZLCPO.js.map → chunk-C6WHIBGH.js.map} +1 -1
- package/dist/{chunk-NZIQF7IW.js → chunk-CTZ56YJG.js} +192 -23
- package/dist/chunk-CTZ56YJG.js.map +1 -0
- package/dist/{chunk-NC2YDZSP.js → chunk-QH5OI43L.js} +4 -4
- package/dist/{chunk-JU3OUO6X.js → chunk-QRUMXCGW.js} +2 -2
- package/dist/{chunk-XEFBPP5Y.js → chunk-UMVBT7NW.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/library.js +4 -4
- package/dist/server.js +4 -4
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/ai/ConnectAi.d.ts +2 -1
- package/dist/types/ai/ModelPicker.d.ts +34 -0
- package/dist/types/ai/agent.d.ts +31 -0
- package/dist/types/ai/agentEndpoint.d.ts +69 -0
- package/dist/types/ai/claude.d.ts +2 -1
- package/dist/types/ai/draft.d.ts +10 -4
- package/dist/types/ai/generator.d.ts +10 -4
- package/dist/types/ai/index.d.ts +2 -1
- package/dist/types/ai/keys.d.ts +14 -1
- package/dist/types/ai/providers.d.ts +48 -5
- package/dist/types/ai/rewrite.d.ts +2 -1
- package/dist/types/ai/server.d.ts +3 -1
- package/dist/types/ai/subjects.d.ts +2 -1
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/engine/NativeAdapter.d.ts +1 -1
- package/dist/types/server/handler.d.ts +3 -0
- package/package.json +1 -1
- package/dist/chunk-NZIQF7IW.js.map +0 -1
- /package/dist/{chunk-5W4TUUPD.js.map → chunk-3RPWH5PC.js.map} +0 -0
- /package/dist/{chunk-NC2YDZSP.js.map → chunk-QH5OI43L.js.map} +0 -0
- /package/dist/{chunk-JU3OUO6X.js.map → chunk-QRUMXCGW.js.map} +0 -0
- /package/dist/{chunk-XEFBPP5Y.js.map → chunk-UMVBT7NW.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/constants.js","../src/core/document.js","../src/validation/images.js"],"sourcesContent":["// @ts-check\n/**\n * Email builder constants.\n *\n * `EMAIL_BUILDER_VERSION` versions THIS package (UI, blocks, rendering);\n * `EMAIL_DOCUMENT_SCHEMA_VERSION` versions the stored JSON envelope (the\n * project JSON). Both are written with every template, so a document read by\n * a newer version knows whether it must be migrated (see `core/document.js`).\n */\n\nexport const EMAIL_BUILDER_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../src/core/constants.js","../src/core/document.js","../src/validation/images.js"],"sourcesContent":["// @ts-check\n/**\n * Email builder constants.\n *\n * `EMAIL_BUILDER_VERSION` versions THIS package (UI, blocks, rendering);\n * `EMAIL_DOCUMENT_SCHEMA_VERSION` versions the stored JSON envelope (the\n * project JSON). Both are written with every template, so a document read by\n * a newer version knows whether it must be migrated (see `core/document.js`).\n */\n\nexport const EMAIL_BUILDER_VERSION = '0.7.0';\n\nexport const EMAIL_DOCUMENT_SCHEMA_VERSION = 1;\n\n/** @typedef {'desktop'|'mobile'} EmailDevice */\n\n/** @type {EmailDevice[]} */\nexport const EMAIL_DEVICES = ['desktop', 'mobile'];\n\n/** Content column width — the email client standard. */\nexport const EMAIL_CONTENT_WIDTH = 600;\n\n/** Mobile preview frame width (standard iPhone, the common worst case). */\nexport const EMAIL_MOBILE_WIDTH = 375;\n","// @ts-check\n/**\n * DOCUMENT envelope — what gets stored (the project JSON).\n *\n * HTML is only a compilation output; what gets edited is the engine's\n * project. It is not stored bare but wrapped with:\n *\n * schemaVersion the shape of THIS envelope (migrations, see MIGRATIONS);\n * engine the engine that can read `project` back;\n * engineVersion its version at write time;\n * builderVersion the builder version that wrote it;\n * project the engine data, opaque to the rest of the code.\n *\n * A document from another engine, a future schema or a malformed one is\n * REJECTED on read rather than half-loaded: a template silently emptied and\n * then saved again would be lost.\n *\n * Dependency-free module: testable outside the browser.\n */\n\nimport { EMAIL_BUILDER_VERSION, EMAIL_DOCUMENT_SCHEMA_VERSION } from './constants.js';\n\n/**\n * @typedef {{\n * schemaVersion: number,\n * engine: string,\n * engineVersion: string,\n * builderVersion: string,\n * project: Record<string, any>,\n * }} EmailDocument\n */\n\n/**\n * Envelope migrations, from N to N+1. Empty while only one schema exists; the\n * first entry will be `1: (doc) => ({ ...doc, schemaVersion: 2 })`.\n *\n * @type {Record<number, (document: any) => any>}\n */\nconst MIGRATIONS = {};\n\nexport class EmailDocumentError extends Error {\n /** @param {string} message */\n constructor(message) {\n super(message);\n this.name = 'EmailDocumentError';\n this.status = 422;\n }\n}\n\nfunction isPlainObject(value) {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * @param {{engine: string, engineVersion: string, project: object}} parts\n * @returns {EmailDocument}\n */\nexport function createDocument({ engine, engineVersion, project }) {\n if (!engine) throw new EmailDocumentError('Document: missing engine');\n if (!isPlainObject(project)) throw new EmailDocumentError('Document: invalid engine project');\n return {\n schemaVersion: EMAIL_DOCUMENT_SCHEMA_VERSION,\n engine,\n engineVersion: String(engineVersion || ''),\n builderVersion: EMAIL_BUILDER_VERSION,\n project: /** @type {Record<string, any>} */ (project),\n };\n}\n\n/**\n * Reads a document (object or JSON string), migrates it to the current schema\n * and validates it.\n *\n * @param {unknown} raw\n * @returns {EmailDocument}\n * @throws {EmailDocumentError}\n */\nexport function readDocument(raw) {\n let value = raw;\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value);\n } catch {\n throw new EmailDocumentError('Unreadable document: invalid JSON');\n }\n }\n if (!isPlainObject(value)) throw new EmailDocumentError('Unreadable document: object expected');\n\n /** @type {any} */\n let document = { ...(/** @type {object} */ (value)) };\n const version = Number(document.schemaVersion);\n if (!Number.isInteger(version) || version < 1) {\n throw new EmailDocumentError('Unreadable document: missing schema version');\n }\n if (version > EMAIL_DOCUMENT_SCHEMA_VERSION) {\n throw new EmailDocumentError(\n `Document written by a newer version of the builder (schema ${version}) — ` +\n 'reload the page'\n );\n }\n for (let v = version; v < EMAIL_DOCUMENT_SCHEMA_VERSION; v += 1) {\n const migrate = MIGRATIONS[v];\n if (!migrate) throw new EmailDocumentError(`Document: no migration from schema ${v}`);\n document = migrate(document);\n }\n\n if (typeof document.engine !== 'string' || !document.engine) {\n throw new EmailDocumentError('Unreadable document: missing engine');\n }\n if (!isPlainObject(document.project)) {\n throw new EmailDocumentError('Unreadable document: missing engine project');\n }\n\n return {\n schemaVersion: EMAIL_DOCUMENT_SCHEMA_VERSION,\n engine: document.engine,\n engineVersion: String(document.engineVersion || ''),\n builderVersion: String(document.builderVersion || ''),\n project: document.project,\n };\n}\n\n/** @param {EmailDocument} document */\nexport function serializeDocument(document) {\n return JSON.stringify(readDocument(document));\n}\n","// @ts-check\n/**\n * Images UPLOADED in the builder — one rule shared by the browser (immediate\n * rejection, storage/client.js) and the server (storage/server/assetFile.js).\n *\n * PNG, JPEG, GIF: the three formats every email client displays. WebP and SVG\n * do not display in Outlook, and an SVG can carry script.\n * The server reads the type from the file BYTES; the browser, from `file.type`.\n */\n\nimport { identity } from '../i18n/index.js';\n\n/** Maximum image size: beyond it, the email becomes slow to open. */\nexport const MAX_IMAGE_BYTES = 2 * 1024 * 1024;\n\n/** Accepted types → extension of the public key. */\nexport const IMAGE_TYPES = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif' };\n\n/** Public key: 32 hex characters of the hash + extension. */\nexport const ASSET_KEY_PATTERN = /^[a-f0-9]{32}\\.(png|jpg|gif)$/;\n\nconst SIGNATURES = [\n { type: 'image/png', bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },\n { type: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },\n { type: 'image/gif', bytes: [0x47, 0x49, 0x46, 0x38] },\n];\n\n/**\n * Image type detected from the file signature, or `null`.\n * @param {Uint8Array} bytes\n * @returns {{type: string, ext: string}|null}\n */\nexport function detectImage(bytes) {\n if (!bytes || bytes.length < 8) return null;\n const found = SIGNATURES.find((s) => s.bytes.every((b, i) => bytes[i] === b));\n return found ? { type: found.type, ext: IMAGE_TYPES[found.type] } : null;\n}\n\n/**\n * Immediate rejection of a file the server would refuse, or `null`.\n * @param {{type?: string, size?: number}} file\n * @param {{t?: import('../i18n/index.js').Translate}} [options]\n */\nexport function imageUploadError(file, { t = identity } = {}) {\n if (!(String(file?.type || '') in IMAGE_TYPES)) return t('Format rejected: PNG, JPEG or GIF expected');\n if (Number(file?.size || 0) > MAX_IMAGE_BYTES) {\n return t('Image too large: {max} MB maximum', { max: MAX_IMAGE_BYTES / (1024 * 1024) });\n }\n return null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -40,6 +40,7 @@ var GENERATION_ERRORS = {
|
|
|
40
40
|
// The provider's side (providers.js). Never 401 to the browser: the key is the AI's, not the user's session.
|
|
41
41
|
KEY_MISSING: { status: 502, message: "Connect an AI provider first: it needs a key." },
|
|
42
42
|
KEY_REFUSED: { status: 502, message: "The AI provider refused the key. Check it, or use another one." },
|
|
43
|
+
MODEL_UNAVAILABLE: { status: 502, message: "The AI provider does not have this model. Choose another one." },
|
|
43
44
|
MODEL_CREDITS: { status: 502, message: "The AI provider account has no credits left." },
|
|
44
45
|
MODEL_BUSY: { status: 503, message: "The AI is busy right now. Try again in a minute." },
|
|
45
46
|
MODEL_ERROR: { status: 502, message: "The AI provider could not answer. Try again." }
|
|
@@ -963,7 +964,7 @@ function reviewRewrite(original, rewritten, format) {
|
|
|
963
964
|
return problems;
|
|
964
965
|
}
|
|
965
966
|
function createRewriter({ model, maxAttempts = 2 }) {
|
|
966
|
-
return async function rewrite(input, { signal } = {}) {
|
|
967
|
+
return async function rewrite(input, { signal, user } = {}) {
|
|
967
968
|
const request = cleanRewriteInput(input);
|
|
968
969
|
const messages = [{ role: "user", content: buildRewritePrompt(request) }];
|
|
969
970
|
let lastProblem = "";
|
|
@@ -971,7 +972,7 @@ function createRewriter({ model, maxAttempts = 2 }) {
|
|
|
971
972
|
signal?.throwIfAborted();
|
|
972
973
|
let answer;
|
|
973
974
|
try {
|
|
974
|
-
answer = await model({ system: REWRITE_SYSTEM_PROMPT, messages, schema: REWRITE_SCHEMA, signal, task: "rewrite" });
|
|
975
|
+
answer = await model({ system: REWRITE_SYSTEM_PROMPT, messages, schema: REWRITE_SCHEMA, signal, task: "rewrite", user });
|
|
975
976
|
} catch (error) {
|
|
976
977
|
if (error instanceof EmailGenerationError && error.code === "REFUSED") throw new EmailGenerationError("REWRITE_REFUSED");
|
|
977
978
|
throw error;
|
|
@@ -1213,7 +1214,7 @@ ${request.text}`;
|
|
|
1213
1214
|
return { usable: usable.slice(0, SUBJECT_COUNT), problems: usable.length >= SUBJECT_COUNT ? [] : problems };
|
|
1214
1215
|
}
|
|
1215
1216
|
function createSubjectSuggester({ model, maxAttempts = 2 }) {
|
|
1216
|
-
return async function suggestSubjects(input, { signal } = {}) {
|
|
1217
|
+
return async function suggestSubjects(input, { signal, user } = {}) {
|
|
1217
1218
|
const request = cleanSubjectsInput(input);
|
|
1218
1219
|
const messages = [{ role: "user", content: buildSubjectsPrompt(request) }];
|
|
1219
1220
|
let best = [];
|
|
@@ -1221,7 +1222,7 @@ function createSubjectSuggester({ model, maxAttempts = 2 }) {
|
|
|
1221
1222
|
signal?.throwIfAborted();
|
|
1222
1223
|
let answer;
|
|
1223
1224
|
try {
|
|
1224
|
-
answer = await model({ system: SUBJECTS_SYSTEM_PROMPT, messages, schema: SUBJECTS_SCHEMA, signal, task: "subjects" });
|
|
1225
|
+
answer = await model({ system: SUBJECTS_SYSTEM_PROMPT, messages, schema: SUBJECTS_SCHEMA, signal, task: "subjects", user });
|
|
1225
1226
|
} catch (error) {
|
|
1226
1227
|
if (error instanceof EmailGenerationError && error.code === "REFUSED") throw new EmailGenerationError("SUBJECTS_REFUSED");
|
|
1227
1228
|
throw error;
|
|
@@ -1360,6 +1361,7 @@ var fr = {
|
|
|
1360
1361
|
"No usable subject came back. Try again.": "Aucun objet utilisable n'a \xE9t\xE9 propos\xE9. R\xE9essayez.",
|
|
1361
1362
|
"Connect an AI provider first: it needs a key.": "Connectez d'abord un fournisseur d'IA : il faut une cl\xE9.",
|
|
1362
1363
|
"The AI provider refused the key. Check it, or use another one.": "Le fournisseur d'IA a refus\xE9 la cl\xE9. V\xE9rifiez-la ou utilisez-en une autre.",
|
|
1364
|
+
"The AI provider does not have this model. Choose another one.": "Le fournisseur d'IA n'a pas ce mod\xE8le. Choisissez-en un autre.",
|
|
1363
1365
|
"The AI provider account has no credits left.": "Le compte du fournisseur d'IA n'a plus de cr\xE9dits.",
|
|
1364
1366
|
"The AI is busy right now. Try again in a minute.": "L'IA est occup\xE9e pour le moment. R\xE9essayez dans une minute.",
|
|
1365
1367
|
"The AI provider could not answer. Try again.": "Le fournisseur d'IA n'a pas pu r\xE9pondre. R\xE9essayez.",
|
|
@@ -1387,7 +1389,22 @@ var fr = {
|
|
|
1387
1389
|
"Runs on your own machine": "Tourne sur votre propre machine",
|
|
1388
1390
|
"AI provider": "Fournisseur d'IA",
|
|
1389
1391
|
"Key saved": "Cl\xE9 enregistr\xE9e",
|
|
1390
|
-
"A key for {provider} is saved in this browser: Connect uses it, or paste another one.": "Une cl\xE9 {provider} est enregistr\xE9e dans ce navigateur : Connecter l'utilise, ou collez-en une autre."
|
|
1392
|
+
"A key for {provider} is saved in this browser: Connect uses it, or paste another one.": "Une cl\xE9 {provider} est enregistr\xE9e dans ce navigateur : Connecter l'utilise, ou collez-en une autre.",
|
|
1393
|
+
"Model": "Mod\xE8le",
|
|
1394
|
+
"Choose a model": "Choisir un mod\xE8le",
|
|
1395
|
+
"Free": "Gratuit",
|
|
1396
|
+
"Free only": "Gratuits",
|
|
1397
|
+
"Search the models": "Rechercher un mod\xE8le",
|
|
1398
|
+
"Search the models\u2026": "Rechercher un mod\xE8le\u2026",
|
|
1399
|
+
"Use \u201C{model}\u201D": "Utiliser \xAB {model} \xBB",
|
|
1400
|
+
"No model matches.": "Aucun mod\xE8le ne correspond.",
|
|
1401
|
+
"Loading the models of {provider}\u2026": "Chargement des mod\xE8les de {provider}\u2026",
|
|
1402
|
+
"Paste the key to see all the models of {provider}.": "Collez la cl\xE9 pour voir tous les mod\xE8les de {provider}.",
|
|
1403
|
+
"{provider} refused this key: its models could not be listed.": "{provider} a refus\xE9 cette cl\xE9 : ses mod\xE8les n'ont pas pu \xEAtre list\xE9s.",
|
|
1404
|
+
"The list of models could not be loaded. The default model works, or type the id of another.": "La liste des mod\xE8les n'a pas pu \xEAtre charg\xE9e. Le mod\xE8le par d\xE9faut fonctionne, ou tapez l'identifiant d'un autre.",
|
|
1405
|
+
"{count} models, the free ones first.": "{count} mod\xE8les, les gratuits en premier.",
|
|
1406
|
+
"{count} models.": "{count} mod\xE8les.",
|
|
1407
|
+
"{provider} lists no models here: type the id of the one you want.": "{provider} ne liste aucun mod\xE8le ici : tapez l'identifiant de celui que vous voulez."
|
|
1391
1408
|
};
|
|
1392
1409
|
|
|
1393
1410
|
// src/ai/i18n/index.js
|
|
@@ -1424,7 +1441,7 @@ function createEmailGenerator({ model, maxAttempts = 2, compile } = (
|
|
|
1424
1441
|
{}
|
|
1425
1442
|
)) {
|
|
1426
1443
|
if (typeof model !== "function") throw new Error("createEmailGenerator: `model` must be a function");
|
|
1427
|
-
async function generate(input, { signal } = {}) {
|
|
1444
|
+
async function generate(input, { signal, user } = {}) {
|
|
1428
1445
|
const request = cleanGenerateInput(input);
|
|
1429
1446
|
if (!request.purpose && !request.details) {
|
|
1430
1447
|
throw new EmailGenerationError("NOTHING_TO_GENERATE");
|
|
@@ -1436,7 +1453,7 @@ function createEmailGenerator({ model, maxAttempts = 2, compile } = (
|
|
|
1436
1453
|
let lastProblem = "";
|
|
1437
1454
|
for (let attempt = 1; attempt <= Math.max(1, maxAttempts); attempt += 1) {
|
|
1438
1455
|
signal?.throwIfAborted();
|
|
1439
|
-
const answer = await model({ system: SYSTEM_PROMPT, messages, schema: EMAIL_DRAFT_SCHEMA, signal, task: "generate" });
|
|
1456
|
+
const answer = await model({ system: SYSTEM_PROMPT, messages, schema: EMAIL_DRAFT_SCHEMA, signal, task: "generate", user });
|
|
1440
1457
|
let problems;
|
|
1441
1458
|
try {
|
|
1442
1459
|
const draft = parseDraft(answer);
|
|
@@ -1486,6 +1503,7 @@ var AI_PROVIDERS = Object.freeze({
|
|
|
1486
1503
|
keyUrl: "https://openrouter.ai/keys",
|
|
1487
1504
|
keyHint: "sk-or-",
|
|
1488
1505
|
free: "Free models, with a free account",
|
|
1506
|
+
publicModels: true,
|
|
1489
1507
|
headers: { "X-Title": "Best Email Builder" },
|
|
1490
1508
|
// Most free models reason before they answer: 5 000 to 12 000 tokens
|
|
1491
1509
|
// before the email itself, which a limit of 16 000 cut short. Less
|
|
@@ -1588,6 +1606,66 @@ function resolveProvider(provider) {
|
|
|
1588
1606
|
] : AI_PROVIDERS["openai-compatible"];
|
|
1589
1607
|
return define({ ...base, ...provider });
|
|
1590
1608
|
}
|
|
1609
|
+
var NOT_A_WRITER = /embed|tts|audio|transcri|whisper|speech|voxtral|image|imagen|dall-e|sora|veo|lyria|music|moderation|realtime|(^|[-_/.])live($|[-_.])|search|computer-use|robotics|\baqa\b|nano-banana|deep-research|antigravity|guard|safety|ocr|rerank|translate|customtools|codex|^(davinci|babbage)|turbo-instruct/i;
|
|
1610
|
+
function writes(entry) {
|
|
1611
|
+
const id = typeof entry?.id === "string" ? entry.id : "";
|
|
1612
|
+
if (!id || NOT_A_WRITER.test(id)) return false;
|
|
1613
|
+
const outputs = entry.architecture?.output_modalities;
|
|
1614
|
+
if (Array.isArray(outputs) && !outputs.every((output) => output === "text")) return false;
|
|
1615
|
+
return entry.capabilities?.completion_chat !== false;
|
|
1616
|
+
}
|
|
1617
|
+
var entryOf = (entry) => {
|
|
1618
|
+
const id = String(entry.id).replace(/^models\//, "");
|
|
1619
|
+
const price = entry.pricing;
|
|
1620
|
+
return {
|
|
1621
|
+
id,
|
|
1622
|
+
name: String(entry.display_name || entry.name || id),
|
|
1623
|
+
free: /:free$/.test(id) || price != null && Number(price.prompt) === 0 && Number(price.completion) === 0,
|
|
1624
|
+
created: Number(entry.created) || (entry.created_at ? Date.parse(entry.created_at) / 1e3 : 0) || 0
|
|
1625
|
+
};
|
|
1626
|
+
};
|
|
1627
|
+
var versionOf = (id) => Number(/(\d+(?:\.\d+)?)/.exec(id)?.[1] || 0);
|
|
1628
|
+
function orderModels(entries, defaultModel) {
|
|
1629
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1630
|
+
const models = entries.filter((entry) => entry?.id && !seen.has(entry.id) && seen.add(entry.id));
|
|
1631
|
+
const rank = (entry) => entry.id === defaultModel ? 0 : entry.free ? 1 : 2;
|
|
1632
|
+
const dated = models.some((entry) => entry.created);
|
|
1633
|
+
return models.map((entry, at) => ({ entry, at })).sort(
|
|
1634
|
+
(a, b) => rank(a.entry) - rank(b.entry) || (dated ? (b.entry.created || 0) - (a.entry.created || 0) : versionOf(b.entry.id) - versionOf(a.entry.id)) || a.at - b.at
|
|
1635
|
+
).map(({ entry }) => Object.freeze({ id: entry.id, name: entry.name || entry.id, free: Boolean(entry.free) }));
|
|
1636
|
+
}
|
|
1637
|
+
async function listProviderModels(options = {}) {
|
|
1638
|
+
const { apiKey = "", fetch: fetchImpl, signal } = options;
|
|
1639
|
+
const provider = resolveProvider(options.provider ?? "openai-compatible");
|
|
1640
|
+
if (provider.listModels) return orderModels(await provider.listModels({ apiKey, fetch: fetchImpl, signal }), provider.model);
|
|
1641
|
+
const baseUrl = String(provider.baseUrl).replace(/\/+$/, "");
|
|
1642
|
+
if (!baseUrl) return [];
|
|
1643
|
+
if (!apiKey && !provider.keyless && !provider.publicModels) throw new EmailGenerationError("KEY_MISSING", `${provider.name}: no key`);
|
|
1644
|
+
let response;
|
|
1645
|
+
try {
|
|
1646
|
+
response = await (fetchImpl || globalThis.fetch)(`${baseUrl}/models`, {
|
|
1647
|
+
headers: { ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, ...provider.headers },
|
|
1648
|
+
signal
|
|
1649
|
+
});
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
if (signal?.aborted || /** @type {any} */
|
|
1652
|
+
error?.name === "AbortError") throw error;
|
|
1653
|
+
throw new EmailGenerationError("MODEL_ERROR", `${provider.name}: ${/** @type {any} */
|
|
1654
|
+
error?.message || error}`);
|
|
1655
|
+
}
|
|
1656
|
+
const text2 = await response.text();
|
|
1657
|
+
if (!response.ok) {
|
|
1658
|
+
throw new EmailGenerationError(failureCode(response.status, errorMessage(text2)), `${provider.name} ${response.status}: ${text2.slice(0, 300)}`);
|
|
1659
|
+
}
|
|
1660
|
+
let data;
|
|
1661
|
+
try {
|
|
1662
|
+
data = JSON.parse(text2);
|
|
1663
|
+
} catch {
|
|
1664
|
+
throw new EmailGenerationError("MODEL_ERROR", `${provider.name}: not JSON \u2014 ${text2.slice(0, 200)}`);
|
|
1665
|
+
}
|
|
1666
|
+
const entries = Array.isArray(data?.data) ? data.data : Array.isArray(data?.models) ? data.models : [];
|
|
1667
|
+
return orderModels(entries.filter(writes).map(entryOf), provider.model);
|
|
1668
|
+
}
|
|
1591
1669
|
var answerInstruction = (schema) => `
|
|
1592
1670
|
|
|
1593
1671
|
# Answer format
|
|
@@ -1617,7 +1695,9 @@ function refusal(status, message) {
|
|
|
1617
1695
|
if (/\bmodels?\b|harness|region|country|territory|not available|not allowed to (sample|access|use)/i.test(message)) return "MODEL_ERROR";
|
|
1618
1696
|
return "KEY_REFUSED";
|
|
1619
1697
|
}
|
|
1698
|
+
var missingModel = (status, message) => status === 404 && /\bmodel/i.test(message) || (status === 400 || status === 404) && /not a valid model|(unknown|invalid) model|model\b.*\b(not found|does not exist|no longer available)/i.test(message);
|
|
1620
1699
|
function failureCode(status, message) {
|
|
1700
|
+
if (missingModel(status, message)) return "MODEL_UNAVAILABLE";
|
|
1621
1701
|
const refused = refusal(status, message);
|
|
1622
1702
|
if (refused) return refused;
|
|
1623
1703
|
if (status === 402 || /credit balance|insufficient[ _](quota|credits|balance)|exceeded your current quota/i.test(message)) return "MODEL_CREDITS";
|
|
@@ -1735,18 +1815,7 @@ function claudeEmailModel({
|
|
|
1735
1815
|
{ signal }
|
|
1736
1816
|
).finalMessage();
|
|
1737
1817
|
} catch (error) {
|
|
1738
|
-
|
|
1739
|
-
/** @type {any} */
|
|
1740
|
-
error || {}
|
|
1741
|
-
);
|
|
1742
|
-
if (signal?.aborted || name === "AbortError" || name === "APIUserAbortError" || error instanceof EmailGenerationError) throw error;
|
|
1743
|
-
const text2 = String(
|
|
1744
|
-
/** @type {any} */
|
|
1745
|
-
error?.error?.error?.message || /** @type {any} */
|
|
1746
|
-
error?.message || error
|
|
1747
|
-
);
|
|
1748
|
-
if (typeof status !== "number") throw new EmailGenerationError("MODEL_ERROR", `Claude: ${text2.slice(0, 300)}`);
|
|
1749
|
-
throw new EmailGenerationError(failureCode(status, text2), `Claude ${status}: ${text2.slice(0, 300)}`);
|
|
1818
|
+
throw sdkFailure(error, signal);
|
|
1750
1819
|
}
|
|
1751
1820
|
if (message.stop_reason === "refusal") {
|
|
1752
1821
|
throw new EmailGenerationError("REFUSED");
|
|
@@ -1757,6 +1826,20 @@ function claudeEmailModel({
|
|
|
1757
1826
|
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1758
1827
|
};
|
|
1759
1828
|
}
|
|
1829
|
+
function sdkFailure(error, signal) {
|
|
1830
|
+
const { status, name } = (
|
|
1831
|
+
/** @type {any} */
|
|
1832
|
+
error || {}
|
|
1833
|
+
);
|
|
1834
|
+
if (signal?.aborted || name === "AbortError" || name === "APIUserAbortError" || error instanceof EmailGenerationError) return error;
|
|
1835
|
+
const text2 = String(
|
|
1836
|
+
/** @type {any} */
|
|
1837
|
+
error?.error?.error?.message || /** @type {any} */
|
|
1838
|
+
error?.message || error
|
|
1839
|
+
);
|
|
1840
|
+
if (typeof status !== "number") return new EmailGenerationError("MODEL_ERROR", `Claude: ${text2.slice(0, 300)}`);
|
|
1841
|
+
return new EmailGenerationError(failureCode(status, text2), `Claude ${status}: ${text2.slice(0, 300)}`);
|
|
1842
|
+
}
|
|
1760
1843
|
function claudeProvider(options) {
|
|
1761
1844
|
const { client, ...rest } = options || /** @type {any} */
|
|
1762
1845
|
{};
|
|
@@ -1780,18 +1863,100 @@ function claudeProvider(options) {
|
|
|
1780
1863
|
"max_tokens"
|
|
1781
1864
|
),
|
|
1782
1865
|
maxTokens: rest.maxTokens ?? 32e3,
|
|
1783
|
-
createModel: ({ apiKey }) => {
|
|
1866
|
+
createModel: ({ apiKey, model }) => {
|
|
1784
1867
|
let current = null;
|
|
1785
1868
|
return async (request) => {
|
|
1786
1869
|
const key = String(await apiKey() || "").trim();
|
|
1787
1870
|
if (!key) throw new EmailGenerationError("KEY_MISSING", "Claude: no key");
|
|
1788
|
-
if (!current || current.key !== key)
|
|
1871
|
+
if (!current || current.key !== key) {
|
|
1872
|
+
current = { key, model: claudeEmailModel({ ...rest, ...model ? { model } : {}, client: await client(key) }) };
|
|
1873
|
+
}
|
|
1789
1874
|
return current.model(request);
|
|
1790
1875
|
};
|
|
1876
|
+
},
|
|
1877
|
+
listModels: async ({ apiKey, signal }) => {
|
|
1878
|
+
const key = String(apiKey || "").trim();
|
|
1879
|
+
if (!key) throw new EmailGenerationError("KEY_MISSING", "Claude: no key");
|
|
1880
|
+
try {
|
|
1881
|
+
const page = await (await client(key)).models.list({ limit: 100 }, { signal });
|
|
1882
|
+
return (page?.data || []).map((entry) => ({
|
|
1883
|
+
id: String(entry.id),
|
|
1884
|
+
name: String(entry.display_name || entry.id),
|
|
1885
|
+
created: entry.created_at ? Date.parse(entry.created_at) / 1e3 : 0
|
|
1886
|
+
}));
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
throw sdkFailure(error, signal);
|
|
1889
|
+
}
|
|
1791
1890
|
}
|
|
1792
1891
|
});
|
|
1793
1892
|
}
|
|
1794
1893
|
|
|
1894
|
+
// src/ai/agent.js
|
|
1895
|
+
var AGENT_PROTOCOL = "best-email-builder/agent@1";
|
|
1896
|
+
var AGENT_ERROR_CODES = Object.freeze(["REFUSED", "TRUNCATED", "MODEL_UNAVAILABLE", "MODEL_CREDITS", "MODEL_BUSY", "MODEL_ERROR"]);
|
|
1897
|
+
var DEFAULT_TIMEOUT = 18e4;
|
|
1898
|
+
var statusCode = (status) => status === 429 || status === 503 || status === 504 || status === 529 ? "MODEL_BUSY" : "MODEL_ERROR";
|
|
1899
|
+
var isAgentCode = (code) => typeof code === "string" && AGENT_ERROR_CODES.includes(
|
|
1900
|
+
/** @type {any} */
|
|
1901
|
+
code
|
|
1902
|
+
);
|
|
1903
|
+
var errorText = (data) => {
|
|
1904
|
+
const error = data?.error ?? data?.detail;
|
|
1905
|
+
if (typeof error === "string") return error;
|
|
1906
|
+
return String(error?.message || (error ? JSON.stringify(error) : ""));
|
|
1907
|
+
};
|
|
1908
|
+
function agentEmailModel(options) {
|
|
1909
|
+
const { url, headers, fetch: fetchImpl, timeout = DEFAULT_TIMEOUT, name = "Agent" } = options || /** @type {any} */
|
|
1910
|
+
{};
|
|
1911
|
+
if (typeof url !== "string" || !url) throw new TypeError("agentEmailModel: `url` is required \u2014 the endpoint of your agent");
|
|
1912
|
+
if (headers !== void 0 && typeof headers !== "function" && (typeof headers !== "object" || headers === null)) {
|
|
1913
|
+
throw new TypeError("agentEmailModel: `headers` is an object, or a function of { task, user } that returns one");
|
|
1914
|
+
}
|
|
1915
|
+
const emailModel = async ({ system, messages, schema, signal, task = "generate", user }) => {
|
|
1916
|
+
const call = fetchImpl || globalThis.fetch;
|
|
1917
|
+
const own = typeof headers === "function" ? await headers({ task, user }) : headers;
|
|
1918
|
+
signal?.throwIfAborted();
|
|
1919
|
+
const controller = new AbortController();
|
|
1920
|
+
const stop = () => controller.abort(signal?.reason);
|
|
1921
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
1922
|
+
let late = false;
|
|
1923
|
+
const timer = timeout > 0 ? setTimeout(() => (late = true, controller.abort()), timeout) : null;
|
|
1924
|
+
let response;
|
|
1925
|
+
let text2;
|
|
1926
|
+
try {
|
|
1927
|
+
response = await call(url, {
|
|
1928
|
+
method: "POST",
|
|
1929
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", ...own },
|
|
1930
|
+
body: JSON.stringify({ protocol: AGENT_PROTOCOL, task, system: system + answerInstruction(schema), messages, schema }),
|
|
1931
|
+
signal: controller.signal
|
|
1932
|
+
});
|
|
1933
|
+
text2 = await response.text();
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
if (signal?.aborted) throw signal.reason ?? error;
|
|
1936
|
+
if (late) throw new EmailGenerationError("MODEL_BUSY", `${name}: no answer after ${Math.round(timeout / 1e3)} s`);
|
|
1937
|
+
throw new EmailGenerationError("MODEL_ERROR", `${name}: ${/** @type {any} */
|
|
1938
|
+
error?.message || error}`);
|
|
1939
|
+
} finally {
|
|
1940
|
+
if (timer) clearTimeout(timer);
|
|
1941
|
+
signal?.removeEventListener("abort", stop);
|
|
1942
|
+
}
|
|
1943
|
+
let data = null;
|
|
1944
|
+
try {
|
|
1945
|
+
data = JSON.parse(text2);
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
if (!response.ok || data && typeof data === "object" && "error" in data && !("text" in data) && !("output" in data)) {
|
|
1949
|
+
const said = data ? errorText(data) : text2;
|
|
1950
|
+
const detail = `${name} ${response.status}: ${said.slice(0, 300)}`;
|
|
1951
|
+
throw new EmailGenerationError(isAgentCode(data?.code) ? data.code : statusCode(response.ok ? 502 : response.status), detail);
|
|
1952
|
+
}
|
|
1953
|
+
if (typeof data?.text === "string") return data.text;
|
|
1954
|
+
if (data?.output && typeof data.output === "object") return JSON.stringify(data.output);
|
|
1955
|
+
return text2;
|
|
1956
|
+
};
|
|
1957
|
+
return Object.assign(emailModel, { url });
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1795
1960
|
export {
|
|
1796
1961
|
GENERATION_ERRORS,
|
|
1797
1962
|
EmailGenerationError,
|
|
@@ -1821,10 +1986,14 @@ export {
|
|
|
1821
1986
|
createEmailGenerator,
|
|
1822
1987
|
AI_PROVIDERS,
|
|
1823
1988
|
resolveProvider,
|
|
1989
|
+
listProviderModels,
|
|
1824
1990
|
failureCode,
|
|
1825
1991
|
providerEmailModel,
|
|
1826
1992
|
DEFAULT_CLAUDE_MODEL,
|
|
1827
1993
|
claudeEmailModel,
|
|
1828
|
-
claudeProvider
|
|
1994
|
+
claudeProvider,
|
|
1995
|
+
AGENT_PROTOCOL,
|
|
1996
|
+
AGENT_ERROR_CODES,
|
|
1997
|
+
agentEmailModel
|
|
1829
1998
|
};
|
|
1830
|
-
//# sourceMappingURL=chunk-
|
|
1999
|
+
//# sourceMappingURL=chunk-CTZ56YJG.js.map
|