libretto 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -7
- package/dist/cli/commands/ai.js +3 -5
- package/dist/cli/commands/browser.js +23 -2
- package/dist/cli/commands/init.js +157 -114
- package/dist/cli/commands/snapshot.js +147 -26
- package/dist/cli/core/ai-config.js +38 -46
- package/dist/cli/core/api-snapshot-analyzer.js +74 -0
- package/dist/cli/core/browser.js +21 -4
- package/dist/cli/core/context.js +1 -1
- package/dist/cli/core/snapshot-analyzer.js +295 -104
- package/dist/cli/core/snapshot-api-config.js +137 -0
- package/dist/cli/index.js +1 -0
- package/dist/shared/condense-dom/condense-dom.cjs +462 -0
- package/dist/shared/condense-dom/condense-dom.d.cts +34 -0
- package/dist/shared/condense-dom/condense-dom.d.ts +34 -0
- package/dist/shared/condense-dom/condense-dom.js +438 -0
- package/dist/shared/llm/ai-sdk-adapter.cjs +5 -1
- package/dist/shared/llm/ai-sdk-adapter.js +5 -1
- package/dist/shared/llm/client.cjs +106 -27
- package/dist/shared/llm/client.d.cts +8 -1
- package/dist/shared/llm/client.d.ts +8 -1
- package/dist/shared/llm/client.js +89 -23
- package/dist/shared/llm/types.d.cts +4 -3
- package/dist/shared/llm/types.d.ts +4 -3
- package/dist/shared/state/session-state.cjs +8 -1
- package/dist/shared/state/session-state.d.cts +24 -18
- package/dist/shared/state/session-state.d.ts +24 -18
- package/dist/shared/state/session-state.js +7 -1
- package/package.json +39 -33
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
const TEST_ATTRS = /* @__PURE__ */ new Set(["data-testid", "data-test", "data-qa", "data-cy"]);
|
|
2
|
+
const TRUSTED_ATTRS = /* @__PURE__ */ new Set([
|
|
3
|
+
"id",
|
|
4
|
+
"name",
|
|
5
|
+
"for",
|
|
6
|
+
"tabindex",
|
|
7
|
+
"contenteditable",
|
|
8
|
+
"role",
|
|
9
|
+
"title",
|
|
10
|
+
"alt",
|
|
11
|
+
"type",
|
|
12
|
+
"value",
|
|
13
|
+
"placeholder",
|
|
14
|
+
"autocomplete",
|
|
15
|
+
"href",
|
|
16
|
+
"action",
|
|
17
|
+
"method",
|
|
18
|
+
"src"
|
|
19
|
+
]);
|
|
20
|
+
const STATE_ATTRS = /* @__PURE__ */ new Set([
|
|
21
|
+
"disabled",
|
|
22
|
+
"hidden",
|
|
23
|
+
"inert",
|
|
24
|
+
"readonly",
|
|
25
|
+
"required",
|
|
26
|
+
"checked",
|
|
27
|
+
"selected",
|
|
28
|
+
"open",
|
|
29
|
+
"multiple"
|
|
30
|
+
]);
|
|
31
|
+
const BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
|
|
32
|
+
...STATE_ATTRS,
|
|
33
|
+
"async",
|
|
34
|
+
"defer",
|
|
35
|
+
"nomodule"
|
|
36
|
+
]);
|
|
37
|
+
const EMPTY_VALUE_DROP_ATTRS = /* @__PURE__ */ new Set([
|
|
38
|
+
"alt",
|
|
39
|
+
"autocomplete",
|
|
40
|
+
"href",
|
|
41
|
+
"action",
|
|
42
|
+
"method",
|
|
43
|
+
"name",
|
|
44
|
+
"placeholder",
|
|
45
|
+
"src",
|
|
46
|
+
"tabindex",
|
|
47
|
+
"title",
|
|
48
|
+
"type"
|
|
49
|
+
]);
|
|
50
|
+
const URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action"]);
|
|
51
|
+
const SCRIPT_ATTRS = /* @__PURE__ */ new Set([
|
|
52
|
+
"src",
|
|
53
|
+
"type",
|
|
54
|
+
"id",
|
|
55
|
+
"defer",
|
|
56
|
+
"async",
|
|
57
|
+
"crossorigin",
|
|
58
|
+
"integrity",
|
|
59
|
+
"nomodule",
|
|
60
|
+
"referrerpolicy"
|
|
61
|
+
]);
|
|
62
|
+
const STYLE_TAG_ATTRS = /* @__PURE__ */ new Set(["media", "type", "nonce", "title"]);
|
|
63
|
+
const INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
|
|
64
|
+
"a",
|
|
65
|
+
"button",
|
|
66
|
+
"input",
|
|
67
|
+
"select",
|
|
68
|
+
"textarea",
|
|
69
|
+
"form",
|
|
70
|
+
"details",
|
|
71
|
+
"dialog",
|
|
72
|
+
"label"
|
|
73
|
+
]);
|
|
74
|
+
const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
75
|
+
"button",
|
|
76
|
+
"link",
|
|
77
|
+
"tab",
|
|
78
|
+
"menuitem",
|
|
79
|
+
"checkbox",
|
|
80
|
+
"radio",
|
|
81
|
+
"switch",
|
|
82
|
+
"slider",
|
|
83
|
+
"combobox"
|
|
84
|
+
]);
|
|
85
|
+
const OPEN_TAG_PATTERN = /<([a-zA-Z][\w:-]*)(\s(?:[^"'<>/]|"[^"]*"|'[^']*')*)?\s*(\/?)>/g;
|
|
86
|
+
function condenseDom(html) {
|
|
87
|
+
const originalLength = html.length;
|
|
88
|
+
const reductions = {};
|
|
89
|
+
function track(label, before, after) {
|
|
90
|
+
const diff = before.length - after.length;
|
|
91
|
+
if (diff > 0) {
|
|
92
|
+
reductions[label] = (reductions[label] ?? 0) + diff;
|
|
93
|
+
}
|
|
94
|
+
return after;
|
|
95
|
+
}
|
|
96
|
+
let result = html;
|
|
97
|
+
result = track(
|
|
98
|
+
"noscript",
|
|
99
|
+
result,
|
|
100
|
+
result.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, "")
|
|
101
|
+
);
|
|
102
|
+
result = track(
|
|
103
|
+
"comments",
|
|
104
|
+
result,
|
|
105
|
+
result.replace(/<!--[\s\S]*?(?:-->|$)/g, "")
|
|
106
|
+
);
|
|
107
|
+
result = track(
|
|
108
|
+
"scripts",
|
|
109
|
+
result,
|
|
110
|
+
result.replace(
|
|
111
|
+
/(<script\b[^>]*>)([\s\S]*?)(<\/script(?:\s[^>]*)?>)/gi,
|
|
112
|
+
(_match, open, content, close) => {
|
|
113
|
+
if (!content.trim()) return `${open}${close}`;
|
|
114
|
+
const isDataScript = /type\s*=\s*["']application\/(json|ld\+json)["']/i.test(open);
|
|
115
|
+
if (isDataScript) {
|
|
116
|
+
return `${open}[JSON data, ${content.length} chars]${close}`;
|
|
117
|
+
}
|
|
118
|
+
return `${open}[script, ${content.length} chars]${close}`;
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
result = track(
|
|
123
|
+
"styles",
|
|
124
|
+
result,
|
|
125
|
+
result.replace(
|
|
126
|
+
/(<style\b[^>]*>)([\s\S]*?)(<\/style(?:\s[^>]*)?>)/gi,
|
|
127
|
+
(_match, open, content, close) => {
|
|
128
|
+
if (!content.trim()) return `${open}${close}`;
|
|
129
|
+
return `${open}[CSS, ${content.length} chars]${close}`;
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
result = track(
|
|
134
|
+
"base64",
|
|
135
|
+
result,
|
|
136
|
+
result.replace(
|
|
137
|
+
/(src|href)\s*=\s*["'](data:[^;]+;base64,)[A-Za-z0-9+/=]{100,}["']/gi,
|
|
138
|
+
(_match, attr, prefix) => {
|
|
139
|
+
const mime = prefix.replace("data:", "").replace(";base64,", "");
|
|
140
|
+
return `${attr}="[base64 ${mime}]"`;
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
result = track("attribute-allowlist", result, rewriteTagAttributes(result));
|
|
145
|
+
const svgPattern = /<svg\b([^>]*)>((?:(?!<svg\b)[\s\S])*?)<\/svg>/gi;
|
|
146
|
+
result = track(
|
|
147
|
+
"svg-collapse",
|
|
148
|
+
result,
|
|
149
|
+
(() => {
|
|
150
|
+
let prev;
|
|
151
|
+
let current = result;
|
|
152
|
+
do {
|
|
153
|
+
prev = current;
|
|
154
|
+
current = current.replace(
|
|
155
|
+
svgPattern,
|
|
156
|
+
(_match, attrs, inner) => {
|
|
157
|
+
const keepAttrs = [];
|
|
158
|
+
const attrPatterns = [
|
|
159
|
+
"id",
|
|
160
|
+
"class",
|
|
161
|
+
"role",
|
|
162
|
+
"aria-label",
|
|
163
|
+
"aria-hidden",
|
|
164
|
+
"title",
|
|
165
|
+
"data-testid"
|
|
166
|
+
];
|
|
167
|
+
for (const name of attrPatterns) {
|
|
168
|
+
const attrToken = findAttributeToken(attrs, name);
|
|
169
|
+
if (attrToken) keepAttrs.push(attrToken);
|
|
170
|
+
}
|
|
171
|
+
const hasAriaLabel = /aria-label\s*=/i.test(attrs);
|
|
172
|
+
if (!hasAriaLabel) {
|
|
173
|
+
const titleMatch = inner.match(
|
|
174
|
+
/<title[^>]*>([^<]+)<\/title>/i
|
|
175
|
+
);
|
|
176
|
+
const descMatch = inner.match(
|
|
177
|
+
/<desc[^>]*>([^<]+)<\/desc>/i
|
|
178
|
+
);
|
|
179
|
+
const labelText = titleMatch?.[1]?.trim() || descMatch?.[1]?.trim();
|
|
180
|
+
if (labelText) {
|
|
181
|
+
keepAttrs.push(
|
|
182
|
+
`aria-label="${escapeHtmlAttribute(labelText)}"`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const attrStr = keepAttrs.length > 0 ? ` ${keepAttrs.join(" ")}` : "";
|
|
187
|
+
return `<svg${attrStr}><!-- [icon] --></svg>`;
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
svgPattern.lastIndex = 0;
|
|
191
|
+
} while (current !== prev);
|
|
192
|
+
return current;
|
|
193
|
+
})()
|
|
194
|
+
);
|
|
195
|
+
const layoutProps = /(?:^|;)\s*(?:display|visibility|opacity|pointer-events|position|z-index|overflow)(?:-[a-z]+)?\s*:[^;"]*/gi;
|
|
196
|
+
result = track(
|
|
197
|
+
"inline-styles",
|
|
198
|
+
result,
|
|
199
|
+
result.replace(
|
|
200
|
+
/\sstyle\s*=\s*["']([^"']*)["']/gi,
|
|
201
|
+
(_match, value) => {
|
|
202
|
+
const kept = [];
|
|
203
|
+
let propMatch;
|
|
204
|
+
layoutProps.lastIndex = 0;
|
|
205
|
+
while ((propMatch = layoutProps.exec(value)) !== null) {
|
|
206
|
+
kept.push(propMatch[0].replace(/^[;\s]+/, "").trim());
|
|
207
|
+
}
|
|
208
|
+
if (kept.length === 0) return "";
|
|
209
|
+
return ` style="${kept.join("; ")}"`;
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
);
|
|
213
|
+
result = track(
|
|
214
|
+
"obfuscated-classes",
|
|
215
|
+
result,
|
|
216
|
+
result.replace(
|
|
217
|
+
/\sclass\s*=\s*["']([^"']*)["']/gi,
|
|
218
|
+
(_match, value) => {
|
|
219
|
+
const filtered = filterSemanticClasses(value);
|
|
220
|
+
if (!filtered) return "";
|
|
221
|
+
return ` class="${filtered}"`;
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
const removableAttrs = /\s(?:xmlns(?::[a-z]+)?|xml:space|xml:lang|fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|stroke-miterlimit|stroke-dasharray|stroke-dashoffset|stroke-opacity|fill-opacity|clip-rule|fill-rule|focusable)\s*=\s*["'][^"']*["']/gi;
|
|
226
|
+
result = track(
|
|
227
|
+
"framework-svg-attrs",
|
|
228
|
+
result,
|
|
229
|
+
result.replace(removableAttrs, "")
|
|
230
|
+
);
|
|
231
|
+
const preBlocks = [];
|
|
232
|
+
result = result.replace(
|
|
233
|
+
/(<pre\b[^>]*>)([\s\S]*?)(<\/pre>)/gi,
|
|
234
|
+
(_match, open, content, close) => {
|
|
235
|
+
const idx = preBlocks.length;
|
|
236
|
+
preBlocks.push(`${open}${content}${close}`);
|
|
237
|
+
return `__PRE_PLACEHOLDER_${idx}__`;
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
result = track(
|
|
241
|
+
"whitespace",
|
|
242
|
+
result,
|
|
243
|
+
result.replace(/[ \t]+/g, " ").replace(/\n\s*\n/g, "\n")
|
|
244
|
+
);
|
|
245
|
+
for (let i = 0; i < preBlocks.length; i++) {
|
|
246
|
+
const placeholder = `__PRE_PLACEHOLDER_${i}__`;
|
|
247
|
+
const preBlock = preBlocks[i];
|
|
248
|
+
result = result.replace(placeholder, () => preBlock);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
html: result,
|
|
252
|
+
originalLength,
|
|
253
|
+
condensedLength: result.length,
|
|
254
|
+
reductions
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function rewriteTagAttributes(html) {
|
|
258
|
+
return html.replace(
|
|
259
|
+
OPEN_TAG_PATTERN,
|
|
260
|
+
(match, rawTagName, rawAttrs, selfClosing) => {
|
|
261
|
+
const tagName = rawTagName.toLowerCase();
|
|
262
|
+
if (!rawAttrs?.trim()) return match;
|
|
263
|
+
const attrs = parseAttributes(rawAttrs);
|
|
264
|
+
if (attrs.length === 0) return match;
|
|
265
|
+
const interactive = isInteractiveElement(tagName, attrs);
|
|
266
|
+
const kept = attrs.map((attr) => keepAttribute(tagName, attr, interactive)).filter((value) => value !== null);
|
|
267
|
+
const attrStr = kept.length > 0 ? ` ${kept.join(" ")}` : "";
|
|
268
|
+
const closing = selfClosing ? " /" : "";
|
|
269
|
+
return `<${rawTagName}${attrStr}${closing}>`;
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
function keepAttribute(tagName, attr, interactive) {
|
|
274
|
+
const name = attr.name.toLowerCase();
|
|
275
|
+
const value = attr.value;
|
|
276
|
+
if (name === "class") {
|
|
277
|
+
if (!value?.trim()) return null;
|
|
278
|
+
const filtered = filterSemanticClasses(value);
|
|
279
|
+
if (!filtered) return null;
|
|
280
|
+
return serializeAttribute(attr.name, filtered);
|
|
281
|
+
}
|
|
282
|
+
if (name === "style") {
|
|
283
|
+
if (!value?.trim()) return null;
|
|
284
|
+
return serializeAttribute(attr.name, value);
|
|
285
|
+
}
|
|
286
|
+
if (name.startsWith("aria-")) {
|
|
287
|
+
if (!value?.trim()) return null;
|
|
288
|
+
return attr.rawToken;
|
|
289
|
+
}
|
|
290
|
+
if (TEST_ATTRS.has(name)) {
|
|
291
|
+
if (!value?.trim()) return null;
|
|
292
|
+
return attr.rawToken;
|
|
293
|
+
}
|
|
294
|
+
if (tagName === "script" && SCRIPT_ATTRS.has(name)) {
|
|
295
|
+
return serializePreservedAttribute(attr);
|
|
296
|
+
}
|
|
297
|
+
if (tagName === "style" && STYLE_TAG_ATTRS.has(name)) {
|
|
298
|
+
if (!value?.trim()) return null;
|
|
299
|
+
return attr.rawToken;
|
|
300
|
+
}
|
|
301
|
+
if (STATE_ATTRS.has(name)) {
|
|
302
|
+
return serializePreservedAttribute(attr);
|
|
303
|
+
}
|
|
304
|
+
if (URL_ATTRS.has(name)) {
|
|
305
|
+
if (!value?.trim()) return null;
|
|
306
|
+
const normalized = normalizeUrlValue(value);
|
|
307
|
+
if (normalized === value) return attr.rawToken;
|
|
308
|
+
return serializeAttribute(attr.name, normalized);
|
|
309
|
+
}
|
|
310
|
+
if (TRUSTED_ATTRS.has(name)) {
|
|
311
|
+
if (shouldDropEmptyValue(name, value)) return null;
|
|
312
|
+
return serializePreservedAttribute(attr);
|
|
313
|
+
}
|
|
314
|
+
if (shouldKeepCustomDataAttribute(tagName, name, value, interactive)) {
|
|
315
|
+
return attr.rawToken;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
function serializePreservedAttribute(attr) {
|
|
320
|
+
if (BOOLEAN_ATTRS.has(attr.name.toLowerCase())) {
|
|
321
|
+
return attr.rawToken;
|
|
322
|
+
}
|
|
323
|
+
if (attr.value === null) return attr.rawToken;
|
|
324
|
+
return attr.rawToken;
|
|
325
|
+
}
|
|
326
|
+
function shouldDropEmptyValue(name, value) {
|
|
327
|
+
if (value === null) return false;
|
|
328
|
+
if (value.trim()) return false;
|
|
329
|
+
if (name.startsWith("aria-")) return true;
|
|
330
|
+
return EMPTY_VALUE_DROP_ATTRS.has(name);
|
|
331
|
+
}
|
|
332
|
+
function normalizeUrlValue(value) {
|
|
333
|
+
const loweredValue = value.trim().toLowerCase();
|
|
334
|
+
if (loweredValue.startsWith("blob:")) return "blob:[omitted]";
|
|
335
|
+
if (loweredValue.startsWith("javascript:")) return "javascript:[omitted]";
|
|
336
|
+
if (loweredValue.startsWith("vbscript:")) return "vbscript:[omitted]";
|
|
337
|
+
if (loweredValue.startsWith("data:")) return "data:[omitted]";
|
|
338
|
+
if (value.length <= 160) return value;
|
|
339
|
+
try {
|
|
340
|
+
const isAbsolute = /^[a-z][a-z0-9+.-]*:/i.test(value);
|
|
341
|
+
const parsed = isAbsolute ? new URL(value) : new URL(value, "https://condensed.local");
|
|
342
|
+
const prefix = isAbsolute ? `${parsed.protocol}//${parsed.host}${parsed.pathname}` : `${parsed.pathname}${parsed.hash}`;
|
|
343
|
+
const query = parsed.search ? "?[query omitted]" : "";
|
|
344
|
+
return `${prefix}${query}`;
|
|
345
|
+
} catch {
|
|
346
|
+
return `${value.slice(0, 96)}[omitted]`;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function filterSemanticClasses(value) {
|
|
350
|
+
const classes = value.split(/\s+/).filter(Boolean);
|
|
351
|
+
const kept = classes.filter((cls) => !isObfuscatedClass(cls));
|
|
352
|
+
return kept.join(" ");
|
|
353
|
+
}
|
|
354
|
+
function isObfuscatedClass(cls) {
|
|
355
|
+
if (cls.length > 80) return true;
|
|
356
|
+
if (/^_?[0-9a-f]{6,}$/i.test(cls)) return true;
|
|
357
|
+
if (/^[a-z]+_[0-9a-f]{4,}$/i.test(cls)) return true;
|
|
358
|
+
if (/^[a-z]{1,2}[0-9]{2,}$/i.test(cls)) return true;
|
|
359
|
+
const digits = (cls.match(/[0-9]/g) || []).length;
|
|
360
|
+
const letters = (cls.match(/[a-zA-Z]/g) || []).length;
|
|
361
|
+
if (cls.length >= 6 && digits >= letters * 0.5 && digits >= 2) return true;
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
function parseAttributes(rawAttrs) {
|
|
365
|
+
const attrs = [];
|
|
366
|
+
const attrPattern = /([^\s"'<>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
367
|
+
let match;
|
|
368
|
+
while ((match = attrPattern.exec(rawAttrs)) !== null) {
|
|
369
|
+
const name = match[1];
|
|
370
|
+
if (!name) continue;
|
|
371
|
+
attrs.push({
|
|
372
|
+
name,
|
|
373
|
+
rawToken: match[0].trim(),
|
|
374
|
+
value: match[2] ?? match[3] ?? match[4] ?? null
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return attrs;
|
|
378
|
+
}
|
|
379
|
+
function isInteractiveElement(tagName, attrs) {
|
|
380
|
+
if (INTERACTIVE_TAGS.has(tagName)) return true;
|
|
381
|
+
for (const attr of attrs) {
|
|
382
|
+
const name = attr.name.toLowerCase();
|
|
383
|
+
if (name === "tabindex" || name === "contenteditable") return true;
|
|
384
|
+
if (name !== "role") continue;
|
|
385
|
+
const role = attr.value?.trim().toLowerCase();
|
|
386
|
+
if (role && INTERACTIVE_ROLES.has(role)) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
function shouldKeepCustomDataAttribute(tagName, attrName, value, interactive) {
|
|
393
|
+
if (!interactive) return false;
|
|
394
|
+
if (!attrName.startsWith("data-")) return false;
|
|
395
|
+
if (TEST_ATTRS.has(attrName)) return false;
|
|
396
|
+
if (!value?.trim()) return false;
|
|
397
|
+
if (value.length > 80) return false;
|
|
398
|
+
if (tagName === "script" || tagName === "style") return false;
|
|
399
|
+
const key = attrName.slice("data-".length);
|
|
400
|
+
if (!looksMeaningfulToken(key)) return false;
|
|
401
|
+
if (!looksMeaningfulDataValue(value)) return false;
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
function looksMeaningfulToken(value) {
|
|
405
|
+
if (!/^[a-z][a-z0-9-]{1,40}$/i.test(value)) return false;
|
|
406
|
+
if (!/[a-z]{3}/i.test(value)) return false;
|
|
407
|
+
if (/(track|metric|telemetry|analytics|component|display|loaded|token|dps|color|screen|strict|rehydr|fetch)/i.test(value)) {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
function looksMeaningfulDataValue(value) {
|
|
413
|
+
if (value.length > 80) return false;
|
|
414
|
+
if (/[<>]/.test(value)) return false;
|
|
415
|
+
if (/https?:\/\//i.test(value)) return false;
|
|
416
|
+
return /^[a-z0-9:_./ -]+$/i.test(value);
|
|
417
|
+
}
|
|
418
|
+
function findAttributeToken(attrs, name) {
|
|
419
|
+
const match = attrs.match(
|
|
420
|
+
new RegExp(
|
|
421
|
+
`(?:^|\\s)(${escapeRegExp(name)}(?:\\s*=\\s*(?:"[^"]*"|'[^']*'|[^\\s"'=<>\\x60]+))?)`,
|
|
422
|
+
"i"
|
|
423
|
+
)
|
|
424
|
+
);
|
|
425
|
+
return match?.[1] ?? null;
|
|
426
|
+
}
|
|
427
|
+
function escapeRegExp(value) {
|
|
428
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
429
|
+
}
|
|
430
|
+
function serializeAttribute(name, value) {
|
|
431
|
+
return `${name}="${escapeHtmlAttribute(value)}"`;
|
|
432
|
+
}
|
|
433
|
+
function escapeHtmlAttribute(value) {
|
|
434
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
435
|
+
}
|
|
436
|
+
export {
|
|
437
|
+
condenseDom
|
|
438
|
+
};
|
|
@@ -47,7 +47,11 @@ function createLLMClientFromModel(model) {
|
|
|
47
47
|
return {
|
|
48
48
|
role: "user",
|
|
49
49
|
content: msg.content.map(
|
|
50
|
-
(part) => part.type === "text" ? { type: "text", text: part.text } : {
|
|
50
|
+
(part) => part.type === "text" ? { type: "text", text: part.text } : {
|
|
51
|
+
type: "image",
|
|
52
|
+
image: part.image,
|
|
53
|
+
...part.mediaType ? { mediaType: part.mediaType } : {}
|
|
54
|
+
}
|
|
51
55
|
)
|
|
52
56
|
};
|
|
53
57
|
});
|
|
@@ -24,7 +24,11 @@ function createLLMClientFromModel(model) {
|
|
|
24
24
|
return {
|
|
25
25
|
role: "user",
|
|
26
26
|
content: msg.content.map(
|
|
27
|
-
(part) => part.type === "text" ? { type: "text", text: part.text } : {
|
|
27
|
+
(part) => part.type === "text" ? { type: "text", text: part.text } : {
|
|
28
|
+
type: "image",
|
|
29
|
+
image: part.image,
|
|
30
|
+
...part.mediaType ? { mediaType: part.mediaType } : {}
|
|
31
|
+
}
|
|
28
32
|
)
|
|
29
33
|
};
|
|
30
34
|
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,65 +17,129 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
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
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
var client_exports = {};
|
|
20
30
|
__export(client_exports, {
|
|
21
|
-
createLLMClient: () => createLLMClient
|
|
31
|
+
createLLMClient: () => createLLMClient,
|
|
32
|
+
hasProviderCredentials: () => hasProviderCredentials,
|
|
33
|
+
missingProviderCredentialsMessage: () => missingProviderCredentialsMessage,
|
|
34
|
+
parseModel: () => parseModel
|
|
22
35
|
});
|
|
23
36
|
module.exports = __toCommonJS(client_exports);
|
|
24
|
-
var import_google_vertex = require("@ai-sdk/google-vertex");
|
|
25
|
-
var import_anthropic = require("@ai-sdk/anthropic");
|
|
26
|
-
var import_openai = require("@ai-sdk/openai");
|
|
27
37
|
var import_ai = require("ai");
|
|
38
|
+
const GEMINI_API_KEY_ENV_VARS = [
|
|
39
|
+
"GEMINI_API_KEY",
|
|
40
|
+
"GOOGLE_GENERATIVE_AI_API_KEY"
|
|
41
|
+
];
|
|
42
|
+
const VERTEX_PROJECT_ENV_VARS = [
|
|
43
|
+
"GOOGLE_CLOUD_PROJECT",
|
|
44
|
+
"GCLOUD_PROJECT"
|
|
45
|
+
];
|
|
46
|
+
const SUPPORTED_PROVIDER_ALIASES = {
|
|
47
|
+
google: "google",
|
|
48
|
+
gemini: "google",
|
|
49
|
+
vertex: "vertex",
|
|
50
|
+
anthropic: "anthropic",
|
|
51
|
+
codex: "openai",
|
|
52
|
+
openai: "openai"
|
|
53
|
+
};
|
|
54
|
+
function readFirstEnvValue(env, names) {
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
const value = env[name]?.trim();
|
|
57
|
+
if (value) return value;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
28
61
|
function parseModel(model) {
|
|
29
62
|
const slashIndex = model.indexOf("/");
|
|
30
63
|
if (slashIndex === -1) {
|
|
31
64
|
throw new Error(
|
|
32
|
-
`Invalid model string "${model}". Expected format: "provider/model-id" (
|
|
65
|
+
`Invalid model string "${model}". Expected format: "provider/model-id" (for example "openai/gpt-5.4", "anthropic/claude-sonnet-4-6", "google/gemini-2.5-pro", or "vertex/gemini-2.5-pro").`
|
|
33
66
|
);
|
|
34
67
|
}
|
|
35
|
-
const
|
|
68
|
+
const providerInput = model.slice(0, slashIndex).toLowerCase();
|
|
69
|
+
const provider = SUPPORTED_PROVIDER_ALIASES[providerInput];
|
|
36
70
|
const modelId = model.slice(slashIndex + 1);
|
|
37
|
-
if (!
|
|
71
|
+
if (!provider) {
|
|
38
72
|
throw new Error(
|
|
39
|
-
`Unsupported provider "${
|
|
73
|
+
`Unsupported provider "${providerInput}". Supported providers: openai/codex, anthropic, google (Gemini API), and vertex.`
|
|
40
74
|
);
|
|
41
75
|
}
|
|
42
76
|
return { provider, modelId };
|
|
43
77
|
}
|
|
44
|
-
function
|
|
78
|
+
function hasProviderCredentials(provider, env = process.env) {
|
|
79
|
+
switch (provider) {
|
|
80
|
+
case "google":
|
|
81
|
+
return readFirstEnvValue(env, GEMINI_API_KEY_ENV_VARS) !== null;
|
|
82
|
+
case "vertex":
|
|
83
|
+
return readFirstEnvValue(env, VERTEX_PROJECT_ENV_VARS) !== null;
|
|
84
|
+
case "anthropic":
|
|
85
|
+
return Boolean(env.ANTHROPIC_API_KEY?.trim());
|
|
86
|
+
case "openai":
|
|
87
|
+
return Boolean(env.OPENAI_API_KEY?.trim());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function missingProviderCredentialsMessage(provider) {
|
|
91
|
+
switch (provider) {
|
|
92
|
+
case "google":
|
|
93
|
+
return "Missing Gemini API key. Set GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY.";
|
|
94
|
+
case "vertex":
|
|
95
|
+
return "Missing Vertex AI project. Set GOOGLE_CLOUD_PROJECT (or GCLOUD_PROJECT) and ensure application default credentials are configured.";
|
|
96
|
+
case "anthropic": {
|
|
97
|
+
return "Missing Anthropic API key. Set ANTHROPIC_API_KEY.";
|
|
98
|
+
}
|
|
99
|
+
case "openai": {
|
|
100
|
+
return "Missing OpenAI API key. Set OPENAI_API_KEY.";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function getProviderModel(provider, modelId) {
|
|
45
105
|
switch (provider) {
|
|
46
106
|
case "google": {
|
|
47
|
-
const
|
|
107
|
+
const apiKey = readFirstEnvValue(process.env, GEMINI_API_KEY_ENV_VARS);
|
|
108
|
+
if (!apiKey) {
|
|
109
|
+
throw new Error(missingProviderCredentialsMessage(provider));
|
|
110
|
+
}
|
|
111
|
+
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
|
|
112
|
+
const google = createGoogleGenerativeAI({ apiKey });
|
|
113
|
+
return google(modelId);
|
|
114
|
+
}
|
|
115
|
+
case "vertex": {
|
|
116
|
+
const project = readFirstEnvValue(process.env, VERTEX_PROJECT_ENV_VARS);
|
|
48
117
|
if (!project) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
"Missing GCP project for Vertex AI. Set GOOGLE_CLOUD_PROJECT environment variable and ensure application default credentials are configured (gcloud auth application-default login)."
|
|
51
|
-
);
|
|
118
|
+
throw new Error(missingProviderCredentialsMessage(provider));
|
|
52
119
|
}
|
|
53
|
-
const
|
|
120
|
+
const { createVertex } = await import("@ai-sdk/google-vertex");
|
|
121
|
+
const vertex = createVertex({
|
|
54
122
|
project,
|
|
55
123
|
location: process.env.GOOGLE_CLOUD_LOCATION || "global"
|
|
56
124
|
});
|
|
57
125
|
return vertex(modelId);
|
|
58
126
|
}
|
|
59
127
|
case "anthropic": {
|
|
60
|
-
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
128
|
+
const apiKey = process.env.ANTHROPIC_API_KEY?.trim();
|
|
61
129
|
if (!apiKey) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
"Missing API key for Anthropic. Set ANTHROPIC_API_KEY environment variable."
|
|
64
|
-
);
|
|
130
|
+
throw new Error(missingProviderCredentialsMessage(provider));
|
|
65
131
|
}
|
|
66
|
-
const
|
|
132
|
+
const { createAnthropic } = await import("@ai-sdk/anthropic");
|
|
133
|
+
const anthropic = createAnthropic({ apiKey });
|
|
67
134
|
return anthropic(modelId);
|
|
68
135
|
}
|
|
69
136
|
case "openai": {
|
|
70
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
137
|
+
const apiKey = process.env.OPENAI_API_KEY?.trim();
|
|
71
138
|
if (!apiKey) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
"Missing API key for OpenAI. Set OPENAI_API_KEY environment variable."
|
|
74
|
-
);
|
|
139
|
+
throw new Error(missingProviderCredentialsMessage(provider));
|
|
75
140
|
}
|
|
76
|
-
const
|
|
141
|
+
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
142
|
+
const openai = createOpenAI({ apiKey });
|
|
77
143
|
return openai(modelId);
|
|
78
144
|
}
|
|
79
145
|
}
|
|
@@ -83,7 +149,11 @@ function convertUserContentParts(parts) {
|
|
|
83
149
|
if (part.type === "text") {
|
|
84
150
|
return { type: "text", text: part.text };
|
|
85
151
|
}
|
|
86
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
type: "image",
|
|
154
|
+
image: part.image,
|
|
155
|
+
...part.mediaType ? { mediaType: part.mediaType } : {}
|
|
156
|
+
};
|
|
87
157
|
});
|
|
88
158
|
}
|
|
89
159
|
function convertAssistantContentParts(parts) {
|
|
@@ -111,9 +181,14 @@ function convertMessages(messages) {
|
|
|
111
181
|
}
|
|
112
182
|
function createLLMClient(model) {
|
|
113
183
|
const { provider, modelId } = parseModel(model);
|
|
114
|
-
|
|
184
|
+
let modelPromise = null;
|
|
185
|
+
const getModel = () => {
|
|
186
|
+
modelPromise ??= getProviderModel(provider, modelId);
|
|
187
|
+
return modelPromise;
|
|
188
|
+
};
|
|
115
189
|
return {
|
|
116
190
|
async generateObject(opts) {
|
|
191
|
+
const aiModel = await getModel();
|
|
117
192
|
const result = await (0, import_ai.generateObject)({
|
|
118
193
|
model: aiModel,
|
|
119
194
|
prompt: opts.prompt,
|
|
@@ -123,6 +198,7 @@ function createLLMClient(model) {
|
|
|
123
198
|
return result.object;
|
|
124
199
|
},
|
|
125
200
|
async generateObjectFromMessages(opts) {
|
|
201
|
+
const aiModel = await getModel();
|
|
126
202
|
const result = await (0, import_ai.generateObject)({
|
|
127
203
|
model: aiModel,
|
|
128
204
|
messages: convertMessages(opts.messages),
|
|
@@ -135,5 +211,8 @@ function createLLMClient(model) {
|
|
|
135
211
|
}
|
|
136
212
|
// Annotate the CommonJS export names for ESM import in node:
|
|
137
213
|
0 && (module.exports = {
|
|
138
|
-
createLLMClient
|
|
214
|
+
createLLMClient,
|
|
215
|
+
hasProviderCredentials,
|
|
216
|
+
missingProviderCredentialsMessage,
|
|
217
|
+
parseModel
|
|
139
218
|
});
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { LLMClient } from './types.cjs';
|
|
2
2
|
import 'zod';
|
|
3
3
|
|
|
4
|
+
type Provider = "google" | "vertex" | "anthropic" | "openai";
|
|
5
|
+
declare function parseModel(model: string): {
|
|
6
|
+
provider: Provider;
|
|
7
|
+
modelId: string;
|
|
8
|
+
};
|
|
9
|
+
declare function hasProviderCredentials(provider: Provider, env?: NodeJS.ProcessEnv): boolean;
|
|
10
|
+
declare function missingProviderCredentialsMessage(provider: Provider): string;
|
|
4
11
|
declare function createLLMClient(model: string): LLMClient;
|
|
5
12
|
|
|
6
|
-
export { createLLMClient };
|
|
13
|
+
export { type Provider, createLLMClient, hasProviderCredentials, missingProviderCredentialsMessage, parseModel };
|