@tangle-network/agent-app 0.26.0 → 0.28.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 (101) hide show
  1. package/dist/DesignCanvas-AOIVN3SJ.js +12 -0
  2. package/dist/DesignCanvasEditor-TN7CWUJW.js +13 -0
  3. package/dist/{TimelineEditor-JDG54OBP.js → TimelineEditor-Z7C6BFP5.js} +2 -2
  4. package/dist/{apply-Cp8c3K9D.d.ts → apply-Gk4raT2j.d.ts} +3 -1
  5. package/dist/brand-extraction/index.d.ts +212 -0
  6. package/dist/brand-extraction/index.js +446 -0
  7. package/dist/brand-extraction/index.js.map +1 -0
  8. package/dist/{chunk-AFDROJ64.js → chunk-2QI7XV2T.js} +1 -1
  9. package/dist/chunk-2QI7XV2T.js.map +1 -0
  10. package/dist/{chunk-LUE4HO5C.js → chunk-4IQMTTNE.js} +10 -10
  11. package/dist/chunk-4IQMTTNE.js.map +1 -0
  12. package/dist/{chunk-FSUR2752.js → chunk-7I37CO5D.js} +274 -388
  13. package/dist/chunk-7I37CO5D.js.map +1 -0
  14. package/dist/{chunk-5RT6KY4G.js → chunk-GQRL6VNU.js} +1 -1
  15. package/dist/chunk-GQRL6VNU.js.map +1 -0
  16. package/dist/{chunk-SCG5JJ4C.js → chunk-HFC4BTWJ.js} +32 -4
  17. package/dist/chunk-HFC4BTWJ.js.map +1 -0
  18. package/dist/{chunk-7QCIYDGC.js → chunk-IOREXWLM.js} +2 -2
  19. package/dist/chunk-J7JNV5BH.js +14 -0
  20. package/dist/chunk-J7JNV5BH.js.map +1 -0
  21. package/dist/{chunk-6UOE5CTA.js → chunk-JT6HHO5P.js} +5 -10
  22. package/dist/chunk-JT6HHO5P.js.map +1 -0
  23. package/dist/{chunk-JZAJE3JL.js → chunk-M2EBUDZ7.js} +19 -5
  24. package/dist/chunk-M2EBUDZ7.js.map +1 -0
  25. package/dist/{chunk-RJ2ZHK5D.js → chunk-S6WS2B32.js} +132 -7
  26. package/dist/chunk-S6WS2B32.js.map +1 -0
  27. package/dist/chunk-SQV7NUEM.js +136 -0
  28. package/dist/chunk-SQV7NUEM.js.map +1 -0
  29. package/dist/chunk-UAKFCMWK.js +1 -0
  30. package/dist/chunk-UAKFCMWK.js.map +1 -0
  31. package/dist/chunk-V5XZKHCL.js +296 -0
  32. package/dist/chunk-V5XZKHCL.js.map +1 -0
  33. package/dist/chunk-XWJXZ6WE.js +604 -0
  34. package/dist/chunk-XWJXZ6WE.js.map +1 -0
  35. package/dist/chunk-Y2SZVW35.js +125 -0
  36. package/dist/chunk-Y2SZVW35.js.map +1 -0
  37. package/dist/{chunk-LPAVBGVY.js → chunk-YEWTVDDT.js} +399 -860
  38. package/dist/chunk-YEWTVDDT.js.map +1 -0
  39. package/dist/{chunk-UDXMR3AD.js → chunk-YNALYIQ3.js} +56 -18
  40. package/dist/chunk-YNALYIQ3.js.map +1 -0
  41. package/dist/command-stack-CkjelfK_.d.ts +267 -0
  42. package/dist/design-canvas/drizzle.d.ts +2 -2
  43. package/dist/design-canvas/index.d.ts +7 -6
  44. package/dist/design-canvas/index.js +3 -2
  45. package/dist/design-canvas-react/engine.d.ts +349 -0
  46. package/dist/design-canvas-react/engine.js +86 -0
  47. package/dist/design-canvas-react/engine.js.map +1 -0
  48. package/dist/design-canvas-react/index.d.ts +80 -618
  49. package/dist/design-canvas-react/index.js +85 -172
  50. package/dist/design-canvas-react/index.js.map +1 -1
  51. package/dist/design-canvas-react/lazy.d.ts +6 -0
  52. package/dist/design-canvas-react/lazy.js +9 -0
  53. package/dist/design-canvas-react/lazy.js.map +1 -0
  54. package/dist/{export-presets-Dl5Aa5xj.d.ts → export-presets-DAsU5Wal.d.ts} +2 -143
  55. package/dist/flow-types-Cb_AblZs.d.ts +25 -0
  56. package/dist/index.d.ts +11 -8
  57. package/dist/index.js +21 -8
  58. package/dist/lazy-BHHqS8-j.d.ts +72 -0
  59. package/dist/missions/index.d.ts +12 -0
  60. package/dist/missions/index.js +1 -1
  61. package/dist/{model-BHLN208Z.d.ts → model-BBwyOuWG.d.ts} +3 -1
  62. package/dist/operations-DLIvvD00.d.ts +144 -0
  63. package/dist/platform/index.js +1 -1
  64. package/dist/runtime/index.d.ts +33 -33
  65. package/dist/runtime/index.js +1 -1
  66. package/dist/sequences/index.d.ts +1 -1
  67. package/dist/sequences/index.js +2 -1
  68. package/dist/sequences-react/index.d.ts +15 -1
  69. package/dist/sequences-react/index.js +2 -2
  70. package/dist/{store-CUStmtdH.d.ts → store-CFyxgTlD.d.ts} +1 -1
  71. package/dist/theme/index.d.ts +80 -0
  72. package/dist/theme/index.js +14 -0
  73. package/dist/theme/index.js.map +1 -0
  74. package/dist/theme/tailwind-preset.d.ts +65 -0
  75. package/dist/theme/tailwind-preset.js +33 -0
  76. package/dist/theme/tailwind-preset.js.map +1 -0
  77. package/dist/theme/tokens.css +95 -0
  78. package/dist/trace/index.d.ts +2 -18
  79. package/dist/trace/index.js +2 -2
  80. package/dist/web/index.d.ts +25 -2
  81. package/dist/web/index.js +3 -1
  82. package/dist/web-react/index.d.ts +71 -31
  83. package/dist/web-react/index.js +625 -486
  84. package/dist/web-react/index.js.map +1 -1
  85. package/package.json +33 -3
  86. package/dist/DesignCanvas-MA5EH324.js +0 -10
  87. package/dist/DesignCanvasEditor-YLCO7I5I.js +0 -9
  88. package/dist/chunk-5RT6KY4G.js.map +0 -1
  89. package/dist/chunk-6UOE5CTA.js.map +0 -1
  90. package/dist/chunk-AFDROJ64.js.map +0 -1
  91. package/dist/chunk-FSUR2752.js.map +0 -1
  92. package/dist/chunk-JZAJE3JL.js.map +0 -1
  93. package/dist/chunk-LPAVBGVY.js.map +0 -1
  94. package/dist/chunk-LUE4HO5C.js.map +0 -1
  95. package/dist/chunk-RJ2ZHK5D.js.map +0 -1
  96. package/dist/chunk-SCG5JJ4C.js.map +0 -1
  97. package/dist/chunk-UDXMR3AD.js.map +0 -1
  98. /package/dist/{DesignCanvas-MA5EH324.js.map → DesignCanvas-AOIVN3SJ.js.map} +0 -0
  99. /package/dist/{DesignCanvasEditor-YLCO7I5I.js.map → DesignCanvasEditor-TN7CWUJW.js.map} +0 -0
  100. /package/dist/{TimelineEditor-JDG54OBP.js.map → TimelineEditor-Z7C6BFP5.js.map} +0 -0
  101. /package/dist/{chunk-7QCIYDGC.js.map → chunk-IOREXWLM.js.map} +0 -0
@@ -0,0 +1,446 @@
1
+ // src/brand-extraction/extract.ts
2
+ var DEFAULT_TIMEOUT_MS = 15e3;
3
+ var DEFAULT_MAX_PER_LIST = 12;
4
+ function normalizeSiteUrl(raw) {
5
+ const trimmed = raw.trim();
6
+ if (!trimmed) return null;
7
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !/^https?:\/\//i.test(trimmed)) return null;
8
+ const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
9
+ try {
10
+ const u = new URL(withScheme);
11
+ if (u.protocol !== "http:" && u.protocol !== "https:") return null;
12
+ return u.toString();
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function absolutize(href, base) {
18
+ const v = href.trim();
19
+ if (!v || v.startsWith("data:") || v.startsWith("javascript:")) return null;
20
+ try {
21
+ const u = new URL(v, base);
22
+ if (u.protocol !== "http:" && u.protocol !== "https:") return null;
23
+ return u.toString();
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function matchTags(html, tag) {
29
+ const re = new RegExp(`<${tag}\\b[^>]*>`, "gi");
30
+ return html.match(re) ?? [];
31
+ }
32
+ function attr(tag, name) {
33
+ const quoted = new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, "i").exec(tag);
34
+ if (quoted) return decodeEntities(quoted[1] ?? "");
35
+ const bare = new RegExp(`\\b${name}\\s*=\\s*([^\\s"'>]+)`, "i").exec(tag);
36
+ return bare ? decodeEntities(bare[1] ?? "") : void 0;
37
+ }
38
+ function decodeEntities(s) {
39
+ return s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x2F;/gi, "/");
40
+ }
41
+ function extractName(html) {
42
+ for (const tag of matchTags(html, "meta")) {
43
+ const prop = (attr(tag, "property") ?? attr(tag, "name"))?.toLowerCase();
44
+ if (prop === "og:site_name") {
45
+ const c = attr(tag, "content")?.trim();
46
+ if (c) return c;
47
+ }
48
+ }
49
+ const title = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1];
50
+ if (title) {
51
+ const head = (decodeEntities(title).split(/\s+[|–—\-:·]\s+/)[0] ?? "").trim();
52
+ if (head) return head;
53
+ }
54
+ return void 0;
55
+ }
56
+ function extractDescription(html) {
57
+ for (const tag of matchTags(html, "meta")) {
58
+ const prop = (attr(tag, "property") ?? attr(tag, "name"))?.toLowerCase();
59
+ if (prop === "description" || prop === "og:description") {
60
+ const c = attr(tag, "content")?.trim();
61
+ if (c) return c;
62
+ }
63
+ }
64
+ return void 0;
65
+ }
66
+ var MIME_BY_EXT = {
67
+ png: "image/png",
68
+ jpg: "image/jpeg",
69
+ jpeg: "image/jpeg",
70
+ gif: "image/gif",
71
+ webp: "image/webp",
72
+ svg: "image/svg+xml",
73
+ ico: "image/x-icon"
74
+ };
75
+ function mimeFromUrl(url) {
76
+ const ext = new URL(url).pathname.split(".").pop()?.toLowerCase();
77
+ return ext ? MIME_BY_EXT[ext] : void 0;
78
+ }
79
+ function parseSizes(sizes) {
80
+ if (!sizes) return {};
81
+ const m = /(\d+)\s*[x×]\s*(\d+)/i.exec(sizes);
82
+ if (!m) return {};
83
+ return { width: Number(m[1]), height: Number(m[2]) };
84
+ }
85
+ function extractLogos(html, base) {
86
+ const out = [];
87
+ const seen = /* @__PURE__ */ new Set();
88
+ const push = (c) => {
89
+ if (seen.has(c.url)) return;
90
+ seen.add(c.url);
91
+ out.push(c);
92
+ };
93
+ for (const tag of matchTags(html, "link")) {
94
+ const rel = attr(tag, "rel")?.toLowerCase() ?? "";
95
+ const href = attr(tag, "href");
96
+ if (!href) continue;
97
+ const url = absolutize(href, base);
98
+ if (!url) continue;
99
+ const { width, height } = parseSizes(attr(tag, "sizes"));
100
+ if (rel.includes("apple-touch-icon")) {
101
+ push({ url, source: "apple-touch-icon", confidence: 0.7, width, height, mimeType: mimeFromUrl(url) });
102
+ } else if (rel.split(/\s+/).includes("icon") || rel.includes("shortcut icon")) {
103
+ push({ url, source: "favicon", confidence: 0.5, width, height, mimeType: mimeFromUrl(url) });
104
+ }
105
+ }
106
+ for (const tag of matchTags(html, "meta")) {
107
+ const prop = (attr(tag, "property") ?? attr(tag, "name"))?.toLowerCase();
108
+ if (prop === "og:image" || prop === "og:image:url") {
109
+ const href = attr(tag, "content");
110
+ const url = href ? absolutize(href, base) : null;
111
+ if (url) push({ url, source: "og:image", confidence: 0.55, mimeType: mimeFromUrl(url) });
112
+ }
113
+ }
114
+ for (const tag of matchTags(html, "img")) {
115
+ const src = attr(tag, "src") ?? attr(tag, "data-src");
116
+ if (!src) continue;
117
+ const alt = attr(tag, "alt");
118
+ const cls = attr(tag, "class") ?? "";
119
+ const id = attr(tag, "id") ?? "";
120
+ const looksLikeLogo = /logo|brand|wordmark/i.test(`${src} ${alt ?? ""} ${cls} ${id}`);
121
+ if (!looksLikeLogo) continue;
122
+ const url = absolutize(src, base);
123
+ if (!url) continue;
124
+ const w = attr(tag, "width");
125
+ const h = attr(tag, "height");
126
+ push({
127
+ url,
128
+ source: "img-logo",
129
+ confidence: 0.85,
130
+ alt,
131
+ width: w ? Number(w) || void 0 : void 0,
132
+ height: h ? Number(h) || void 0 : void 0,
133
+ mimeType: mimeFromUrl(url)
134
+ });
135
+ }
136
+ if (!out.some((l) => l.source === "favicon" || l.source === "apple-touch-icon")) {
137
+ const fav = absolutize("/favicon.ico", base);
138
+ if (fav) push({ url: fav, source: "favicon", confidence: 0.3, mimeType: "image/x-icon" });
139
+ }
140
+ return out.sort((a, b) => b.confidence - a.confidence);
141
+ }
142
+ function normalizeColor(raw) {
143
+ const v = raw.trim().toLowerCase();
144
+ const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(v);
145
+ if (hex) {
146
+ const h = hex[1] ?? "";
147
+ if (h.length === 3 || h.length === 4) {
148
+ return `#${h.split("").map((c) => c + c).join("")}`;
149
+ }
150
+ return `#${h}`;
151
+ }
152
+ const rgb = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.%]+))?\s*\)$/.exec(v);
153
+ if (rgb) {
154
+ const to255 = (n) => Math.max(0, Math.min(255, Math.round(Number(n))));
155
+ const r = to255(rgb[1]).toString(16).padStart(2, "0");
156
+ const g = to255(rgb[2]).toString(16).padStart(2, "0");
157
+ const b = to255(rgb[3]).toString(16).padStart(2, "0");
158
+ let a = "";
159
+ if (rgb[4] !== void 0) {
160
+ const av = rgb[4].endsWith("%") ? Number(rgb[4].slice(0, -1)) / 100 : Number(rgb[4]);
161
+ const ai = Math.max(0, Math.min(255, Math.round(av * 255)));
162
+ if (ai < 255) a = ai.toString(16).padStart(2, "0");
163
+ }
164
+ return `#${r}${g}${b}${a}`;
165
+ }
166
+ return null;
167
+ }
168
+ var COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)/g;
169
+ var ROOT_VAR_RE = /(--[a-z0-9-]*(?:color|colour|bg|background|accent|brand|primary|secondary|surface|text|fg|ink)[a-z0-9-]*)\s*:\s*([^;}{]+)/gi;
170
+ function extractPalette(html) {
171
+ const byHex = /* @__PURE__ */ new Map();
172
+ const bump = (hex, fromToken, tokenName) => {
173
+ const existing = byHex.get(hex);
174
+ if (existing) {
175
+ existing.occurrences += 1;
176
+ if (fromToken && !existing.fromToken) {
177
+ existing.fromToken = true;
178
+ existing.tokenName = tokenName;
179
+ }
180
+ } else {
181
+ byHex.set(hex, { hex, occurrences: 1, fromToken, ...tokenName ? { tokenName } : {} });
182
+ }
183
+ };
184
+ for (const m of html.matchAll(ROOT_VAR_RE)) {
185
+ const tokenName = m[1];
186
+ const value = m[2];
187
+ if (!value) continue;
188
+ const lit = value.match(COLOR_LITERAL_RE)?.[0];
189
+ if (!lit) continue;
190
+ const hex = normalizeColor(lit);
191
+ if (hex) bump(hex, true, tokenName);
192
+ }
193
+ for (const m of html.matchAll(COLOR_LITERAL_RE)) {
194
+ const hex = normalizeColor(m[0]);
195
+ if (hex) bump(hex, false);
196
+ }
197
+ const isNeutral = (h) => /^#(0{6}|f{6})(ff)?$/i.test(h);
198
+ return [...byHex.values()].sort((a, b) => {
199
+ if (a.fromToken !== b.fromToken) return a.fromToken ? -1 : 1;
200
+ const an = isNeutral(a.hex) ? 1 : 0;
201
+ const bn = isNeutral(b.hex) ? 1 : 0;
202
+ if (an !== bn) return an - bn;
203
+ return b.occurrences - a.occurrences;
204
+ });
205
+ }
206
+ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
207
+ "serif",
208
+ "sans-serif",
209
+ "monospace",
210
+ "cursive",
211
+ "fantasy",
212
+ "system-ui",
213
+ "ui-sans-serif",
214
+ "ui-serif",
215
+ "ui-monospace",
216
+ "ui-rounded",
217
+ "inherit",
218
+ "initial",
219
+ "unset"
220
+ ]);
221
+ var FONT_DECL_RE = /([^{}]*)\{[^{}]*font-family\s*:\s*([^;}{]+)[;}]/gi;
222
+ function parseFontStack(raw) {
223
+ if (!raw) return [];
224
+ return raw.split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
225
+ }
226
+ function roleFromSelector(selector) {
227
+ const s = selector.toLowerCase();
228
+ if (/\b(h[1-3]|\.h[1-3]|heading|title|display|hero)\b/.test(s)) return "display";
229
+ if (/\b(body|p|html|\.text|\.content|root|:root|\*)\b/.test(s)) return "body";
230
+ return "unknown";
231
+ }
232
+ function extractFonts(html) {
233
+ const byFamily = /* @__PURE__ */ new Map();
234
+ const consider = (family, stack, role) => {
235
+ const key = family.toLowerCase();
236
+ if (!family || GENERIC_FAMILIES.has(key)) return;
237
+ const existing = byFamily.get(key);
238
+ if (existing) {
239
+ existing.occurrences += 1;
240
+ if (existing.role === "unknown" && role !== "unknown") existing.role = role;
241
+ } else {
242
+ byFamily.set(key, { family, stack, role, occurrences: 1 });
243
+ }
244
+ };
245
+ for (const m of html.matchAll(FONT_DECL_RE)) {
246
+ const selector = m[1] ?? "";
247
+ const stack = parseFontStack(m[2]);
248
+ const primary = stack[0];
249
+ if (!primary) continue;
250
+ consider(primary, stack, roleFromSelector(selector));
251
+ }
252
+ const BARE_RE = /font-family\s*:\s*([^;"'}{]+)/gi;
253
+ for (const m of html.matchAll(BARE_RE)) {
254
+ const stack = parseFontStack(m[1]);
255
+ const primary = stack[0];
256
+ if (!primary) continue;
257
+ consider(primary, stack, "unknown");
258
+ }
259
+ for (const tag of matchTags(html, "link")) {
260
+ const href = attr(tag, "href") ?? "";
261
+ if (!/fonts\.googleapis\.com/i.test(href)) continue;
262
+ for (const fam of href.matchAll(/family=([^:&]+)/gi)) {
263
+ const captured = fam[1];
264
+ if (!captured) continue;
265
+ const family = decodeURIComponent(captured.replace(/\+/g, " ")).trim();
266
+ consider(family, [family], "display");
267
+ }
268
+ }
269
+ return [...byFamily.values()].sort((a, b) => {
270
+ const rank = (r) => r === "display" ? 0 : r === "body" ? 1 : 2;
271
+ const rr = rank(a.role) - rank(b.role);
272
+ if (rr !== 0) return rr;
273
+ return b.occurrences - a.occurrences;
274
+ });
275
+ }
276
+ function extractImages(html, base) {
277
+ const out = [];
278
+ const seen = /* @__PURE__ */ new Set();
279
+ const push = (img) => {
280
+ if (seen.has(img.url)) return;
281
+ seen.add(img.url);
282
+ out.push(img);
283
+ };
284
+ for (const tag of matchTags(html, "meta")) {
285
+ const prop = (attr(tag, "property") ?? attr(tag, "name"))?.toLowerCase();
286
+ const href = attr(tag, "content");
287
+ const url = href ? absolutize(href, base) : null;
288
+ if (!url) continue;
289
+ if (prop === "og:image" || prop === "og:image:url") push({ url, source: "og:image" });
290
+ else if (prop === "twitter:image" || prop === "twitter:image:src") push({ url, source: "twitter:image" });
291
+ }
292
+ for (const tag of matchTags(html, "img")) {
293
+ const src = attr(tag, "src") ?? attr(tag, "data-src");
294
+ if (!src) continue;
295
+ const url = absolutize(src, base);
296
+ if (!url) continue;
297
+ const cls = `${attr(tag, "class") ?? ""} ${attr(tag, "id") ?? ""}`;
298
+ if (/logo|icon|favicon|wordmark/i.test(`${url} ${cls}`)) continue;
299
+ const isHero = /hero|banner|cover|feature|splash/i.test(cls);
300
+ const w = attr(tag, "width");
301
+ const h = attr(tag, "height");
302
+ push({
303
+ url,
304
+ source: isHero ? "img-hero" : "img-content",
305
+ alt: attr(tag, "alt"),
306
+ width: w ? Number(w) || void 0 : void 0,
307
+ height: h ? Number(h) || void 0 : void 0
308
+ });
309
+ }
310
+ const rank = (s) => s === "og:image" ? 0 : s === "twitter:image" ? 1 : s === "img-hero" ? 2 : 3;
311
+ return out.sort((a, b) => rank(a.source) - rank(b.source));
312
+ }
313
+ function parseBrandKit(html, sourceUrl, maxPerList = DEFAULT_MAX_PER_LIST) {
314
+ const logos = extractLogos(html, sourceUrl).slice(0, maxPerList);
315
+ const palette = extractPalette(html).slice(0, maxPerList);
316
+ const fonts = extractFonts(html).slice(0, maxPerList);
317
+ const images = extractImages(html, sourceUrl).slice(0, maxPerList);
318
+ const name = extractName(html);
319
+ const description = extractDescription(html);
320
+ return {
321
+ sourceUrl,
322
+ ...name ? { name } : {},
323
+ ...description ? { description } : {},
324
+ logos,
325
+ palette,
326
+ fonts,
327
+ images,
328
+ extractedFrom: [sourceUrl]
329
+ };
330
+ }
331
+ async function extractBrandKit(url, options = {}) {
332
+ const { html: providedHtml, fetchImpl, timeoutMs = DEFAULT_TIMEOUT_MS, maxPerList = DEFAULT_MAX_PER_LIST } = options;
333
+ const warnings = [];
334
+ const sourceUrl = normalizeSiteUrl(url);
335
+ if (!sourceUrl) {
336
+ return { succeeded: false, error: `Not a valid http(s) URL: ${JSON.stringify(url)}`, stage: "input" };
337
+ }
338
+ let html = providedHtml;
339
+ if (html === void 0) {
340
+ const doFetch = fetchImpl ?? (typeof globalThis.fetch === "function" ? (u, init) => globalThis.fetch(u, init) : void 0);
341
+ if (!doFetch) {
342
+ return { succeeded: false, error: "No html provided and no fetch implementation available", stage: "input" };
343
+ }
344
+ const controller = new AbortController();
345
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
346
+ try {
347
+ const res = await doFetch(sourceUrl, {
348
+ signal: controller.signal,
349
+ headers: { "user-agent": "TangleBrandExtractor/1.0 (+https://tangle.tools)" }
350
+ });
351
+ if (!res.ok) {
352
+ return { succeeded: false, error: `Fetch failed: HTTP ${res.status} for ${sourceUrl}`, stage: "fetch" };
353
+ }
354
+ html = await res.text();
355
+ } catch (err) {
356
+ const reason = err instanceof Error ? err.message : String(err);
357
+ return { succeeded: false, error: `Fetch error for ${sourceUrl}: ${reason}`, stage: "fetch" };
358
+ } finally {
359
+ clearTimeout(timer);
360
+ }
361
+ }
362
+ if (!html.trim()) {
363
+ return { succeeded: false, error: `Empty document for ${sourceUrl}`, stage: "parse" };
364
+ }
365
+ const kit = parseBrandKit(html, sourceUrl, maxPerList);
366
+ if (kit.logos.length === 0) warnings.push("No logo candidates found.");
367
+ if (kit.palette.length === 0) warnings.push("No colors extracted \u2014 page has no inline CSS or design tokens.");
368
+ if (kit.fonts.length === 0) warnings.push("No custom fonts found \u2014 site likely uses system defaults.");
369
+ if (kit.images.length === 0) warnings.push("No prominent images found.");
370
+ return { succeeded: true, kit, warnings };
371
+ }
372
+
373
+ // src/brand-extraction/map.ts
374
+ function luminance(hex) {
375
+ const h = hex.replace("#", "").slice(0, 6);
376
+ const r = parseInt(h.slice(0, 2), 16) / 255;
377
+ const g = parseInt(h.slice(2, 4), 16) / 255;
378
+ const b = parseInt(h.slice(4, 6), 16) / 255;
379
+ const lin = (c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
380
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
381
+ }
382
+ function isGreyish(hex) {
383
+ const h = hex.replace("#", "").slice(0, 6);
384
+ const r = parseInt(h.slice(0, 2), 16);
385
+ const g = parseInt(h.slice(2, 4), 16);
386
+ const b = parseInt(h.slice(4, 6), 16);
387
+ const max = Math.max(r, g, b);
388
+ const min = Math.min(r, g, b);
389
+ return max - min < 18;
390
+ }
391
+ function decidePalette(palette) {
392
+ if (palette.length === 0) return {};
393
+ const hexes = palette.map((c) => c.hex);
394
+ const greys = hexes.filter(isGreyish).sort((a, b) => luminance(b) - luminance(a));
395
+ const colored = hexes.filter((h) => !isGreyish(h));
396
+ const accent = colored[0];
397
+ const sortedLight = [...hexes].sort((a, b) => luminance(b) - luminance(a));
398
+ const background = greys[0] ?? sortedLight[0];
399
+ const surface = greys[1] ?? sortedLight.find((h) => h !== background);
400
+ const textPrimary = [...hexes].sort((a, b) => luminance(a) - luminance(b))[0];
401
+ const textSecondary = greys.find((h) => h !== background && h !== surface && h !== textPrimary);
402
+ const result = {};
403
+ if (background) result.background = background;
404
+ if (surface) result.surface = surface;
405
+ if (textPrimary) result.textPrimary = textPrimary;
406
+ if (textSecondary) result.textSecondary = textSecondary;
407
+ if (accent) {
408
+ result.accent = accent;
409
+ result.accentText = luminance(accent) > 0.45 ? "#000000" : "#ffffff";
410
+ }
411
+ return result;
412
+ }
413
+ function decideFonts(fonts) {
414
+ if (fonts.length === 0) return {};
415
+ const display = fonts.find((f) => f.role === "display") ?? fonts[0];
416
+ const body = fonts.find((f) => f.role === "body") ?? fonts.find((f) => f !== display) ?? display;
417
+ return { display, body };
418
+ }
419
+ function decideBrandKit(kit) {
420
+ const palette = decidePalette(kit.palette);
421
+ const fonts = decideFonts(kit.fonts);
422
+ const logoUrls = kit.logos.map((l) => l.url);
423
+ const result = {
424
+ sourceUrl: kit.sourceUrl,
425
+ palette,
426
+ fonts,
427
+ logoUrls,
428
+ imageUrls: kit.images.map((i) => i.url),
429
+ extractedFrom: kit.extractedFrom
430
+ };
431
+ if (kit.name) result.name = kit.name;
432
+ if (kit.description) result.description = kit.description;
433
+ if (logoUrls[0]) result.primaryLogoUrl = logoUrls[0];
434
+ return result;
435
+ }
436
+ export {
437
+ decideBrandKit,
438
+ decideFonts,
439
+ decidePalette,
440
+ extractBrandKit,
441
+ luminance,
442
+ normalizeColor,
443
+ normalizeSiteUrl,
444
+ parseBrandKit
445
+ };
446
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/brand-extraction/extract.ts","../../src/brand-extraction/map.ts"],"sourcesContent":["/**\n * Brand-kit extraction engine.\n *\n * Given a website URL (or raw HTML), parse the page and pull a typed BrandKit:\n * logo candidates, color palette, fonts, and prominent images. Parsing is\n * regex-based on purpose — this runs on Cloudflare Workers and other edge\n * runtimes where DOM libraries (jsdom/cheerio) are unavailable, and the page\n * structures we read (link tags, meta tags, CSS custom properties, font-family\n * declarations) are flat enough that a DOM buys nothing.\n *\n * Degrades gracefully: a missing favicon, no CSS tokens, or an image-only page\n * each narrow the kit rather than failing it. The ONLY hard failures are a bad\n * input (no html and no fetch) and a fetch error — both returned as a typed\n * `{ succeeded: false }` outcome so callers never mistake an empty kit for a\n * real one.\n */\n\nimport type {\n BrandColor,\n BrandExtractionResult,\n BrandFont,\n BrandImage,\n BrandKit,\n BrandLogoCandidate,\n ExtractBrandKitOptions,\n FetchLike,\n} from './types'\n\nconst DEFAULT_TIMEOUT_MS = 15000\nconst DEFAULT_MAX_PER_LIST = 12\n\n// ── URL helpers ──────────────────────────────────────────────────────────────\n\n/** Normalize a user-supplied site URL: add https:// when scheme-less, validate. */\nexport function normalizeSiteUrl(raw: string): string | null {\n const trimmed = raw.trim()\n if (!trimmed) return null\n // A non-http(s) scheme is an explicit reject — don't paper over it by\n // prepending https:// (that would turn \"ftp://x\" into host \"ftp\").\n if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !/^https?:\\/\\//i.test(trimmed)) return null\n const withScheme = /^https?:\\/\\//i.test(trimmed) ? trimmed : `https://${trimmed}`\n try {\n const u = new URL(withScheme)\n if (u.protocol !== 'http:' && u.protocol !== 'https:') return null\n return u.toString()\n } catch {\n return null\n }\n}\n\n/** Resolve a possibly-relative href against the page URL. Returns null for\n * data: URIs and unresolvable values — we want servable http(s) URLs only. */\nfunction absolutize(href: string, base: string): string | null {\n const v = href.trim()\n if (!v || v.startsWith('data:') || v.startsWith('javascript:')) return null\n try {\n const u = new URL(v, base)\n if (u.protocol !== 'http:' && u.protocol !== 'https:') return null\n return u.toString()\n } catch {\n return null\n }\n}\n\n// ── Attribute parsing ────────────────────────────────────────────────────────\n\n/** Pull all `<tag ...>` open-tags from html (case-insensitive). */\nfunction matchTags(html: string, tag: string): string[] {\n const re = new RegExp(`<${tag}\\\\b[^>]*>`, 'gi')\n return html.match(re) ?? []\n}\n\n/** Read an attribute value from a single tag string. Handles quoted and\n * unquoted values. Returns undefined when absent. */\nfunction attr(tag: string, name: string): string | undefined {\n const quoted = new RegExp(`\\\\b${name}\\\\s*=\\\\s*[\"']([^\"']*)[\"']`, 'i').exec(tag)\n if (quoted) return decodeEntities(quoted[1] ?? '')\n const bare = new RegExp(`\\\\b${name}\\\\s*=\\\\s*([^\\\\s\"'>]+)`, 'i').exec(tag)\n return bare ? decodeEntities(bare[1] ?? '') : undefined\n}\n\nfunction decodeEntities(s: string): string {\n return s\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&#x2F;/gi, '/')\n}\n\n// ── Name / description ───────────────────────────────────────────────────────\n\nfunction extractName(html: string): string | undefined {\n for (const tag of matchTags(html, 'meta')) {\n const prop = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n if (prop === 'og:site_name') {\n const c = attr(tag, 'content')?.trim()\n if (c) return c\n }\n }\n const title = /<title[^>]*>([\\s\\S]*?)<\\/title>/i.exec(html)?.[1]\n if (title) {\n // \"Acme — the best widget\" → \"Acme\". Split on common separators, take head.\n const head = (decodeEntities(title).split(/\\s+[|–—\\-:·]\\s+/)[0] ?? '').trim()\n if (head) return head\n }\n return undefined\n}\n\nfunction extractDescription(html: string): string | undefined {\n for (const tag of matchTags(html, 'meta')) {\n const prop = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n if (prop === 'description' || prop === 'og:description') {\n const c = attr(tag, 'content')?.trim()\n if (c) return c\n }\n }\n return undefined\n}\n\n// ── Logos ────────────────────────────────────────────────────────────────────\n\nconst MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n ico: 'image/x-icon',\n}\n\nfunction mimeFromUrl(url: string): string | undefined {\n const ext = new URL(url).pathname.split('.').pop()?.toLowerCase()\n return ext ? MIME_BY_EXT[ext] : undefined\n}\n\nfunction parseSizes(sizes: string | undefined): { width?: number; height?: number } {\n if (!sizes) return {}\n const m = /(\\d+)\\s*[x×]\\s*(\\d+)/i.exec(sizes)\n if (!m) return {}\n return { width: Number(m[1]), height: Number(m[2]) }\n}\n\nfunction extractLogos(html: string, base: string): BrandLogoCandidate[] {\n const out: BrandLogoCandidate[] = []\n const seen = new Set<string>()\n const push = (c: BrandLogoCandidate) => {\n if (seen.has(c.url)) return\n seen.add(c.url)\n out.push(c)\n }\n\n // link rel icons / apple-touch-icon\n for (const tag of matchTags(html, 'link')) {\n const rel = attr(tag, 'rel')?.toLowerCase() ?? ''\n const href = attr(tag, 'href')\n if (!href) continue\n const url = absolutize(href, base)\n if (!url) continue\n const { width, height } = parseSizes(attr(tag, 'sizes'))\n if (rel.includes('apple-touch-icon')) {\n push({ url, source: 'apple-touch-icon', confidence: 0.7, width, height, mimeType: mimeFromUrl(url) })\n } else if (rel.split(/\\s+/).includes('icon') || rel.includes('shortcut icon')) {\n push({ url, source: 'favicon', confidence: 0.5, width, height, mimeType: mimeFromUrl(url) })\n }\n }\n\n // og:image (often shows the logo in context — moderate confidence)\n for (const tag of matchTags(html, 'meta')) {\n const prop = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n if (prop === 'og:image' || prop === 'og:image:url') {\n const href = attr(tag, 'content')\n const url = href ? absolutize(href, base) : null\n if (url) push({ url, source: 'og:image', confidence: 0.55, mimeType: mimeFromUrl(url) })\n }\n }\n\n // <img> whose src/class/alt/id mentions \"logo\" — the strongest in-page signal\n for (const tag of matchTags(html, 'img')) {\n const src = attr(tag, 'src') ?? attr(tag, 'data-src')\n if (!src) continue\n const alt = attr(tag, 'alt')\n const cls = attr(tag, 'class') ?? ''\n const id = attr(tag, 'id') ?? ''\n const looksLikeLogo = /logo|brand|wordmark/i.test(`${src} ${alt ?? ''} ${cls} ${id}`)\n if (!looksLikeLogo) continue\n const url = absolutize(src, base)\n if (!url) continue\n const w = attr(tag, 'width')\n const h = attr(tag, 'height')\n push({\n url,\n source: 'img-logo',\n confidence: 0.85,\n alt,\n width: w ? Number(w) || undefined : undefined,\n height: h ? Number(h) || undefined : undefined,\n mimeType: mimeFromUrl(url),\n })\n }\n\n // No favicon link tag at all? Fall back to the conventional /favicon.ico —\n // it almost always exists. Lowest confidence.\n if (!out.some((l) => l.source === 'favicon' || l.source === 'apple-touch-icon')) {\n const fav = absolutize('/favicon.ico', base)\n if (fav) push({ url: fav, source: 'favicon', confidence: 0.3, mimeType: 'image/x-icon' })\n }\n\n return out.sort((a, b) => b.confidence - a.confidence)\n}\n\n// ── Colors ───────────────────────────────────────────────────────────────────\n\n/** Normalize any CSS color literal to #rrggbb / #rrggbbaa hex, or null. */\nexport function normalizeColor(raw: string): string | null {\n const v = raw.trim().toLowerCase()\n // #rgb / #rgba / #rrggbb / #rrggbbaa\n const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(v)\n if (hex) {\n const h = hex[1] ?? ''\n // Expand 3/4-digit shorthand by doubling each nibble.\n if (h.length === 3 || h.length === 4) {\n return `#${h.split('').map((c) => c + c).join('')}`\n }\n return `#${h}`\n }\n // rgb()/rgba()\n const rgb = /^rgba?\\(\\s*([\\d.]+)[\\s,]+([\\d.]+)[\\s,]+([\\d.]+)(?:[\\s,/]+([\\d.%]+))?\\s*\\)$/.exec(v)\n if (rgb) {\n const to255 = (n: string | undefined) => Math.max(0, Math.min(255, Math.round(Number(n))))\n const r = to255(rgb[1]).toString(16).padStart(2, '0')\n const g = to255(rgb[2]).toString(16).padStart(2, '0')\n const b = to255(rgb[3]).toString(16).padStart(2, '0')\n let a = ''\n if (rgb[4] !== undefined) {\n const av = rgb[4].endsWith('%') ? Number(rgb[4].slice(0, -1)) / 100 : Number(rgb[4])\n const ai = Math.max(0, Math.min(255, Math.round(av * 255)))\n if (ai < 255) a = ai.toString(16).padStart(2, '0')\n }\n return `#${r}${g}${b}${a}`\n }\n return null\n}\n\nconst COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\\b|rgba?\\([^)]*\\)/g\nconst ROOT_VAR_RE = /(--[a-z0-9-]*(?:color|colour|bg|background|accent|brand|primary|secondary|surface|text|fg|ink)[a-z0-9-]*)\\s*:\\s*([^;}{]+)/gi\n\nfunction extractPalette(html: string): BrandColor[] {\n const byHex = new Map<string, BrandColor>()\n const bump = (hex: string, fromToken: boolean, tokenName?: string) => {\n const existing = byHex.get(hex)\n if (existing) {\n existing.occurrences += 1\n if (fromToken && !existing.fromToken) {\n existing.fromToken = true\n existing.tokenName = tokenName\n }\n } else {\n byHex.set(hex, { hex, occurrences: 1, fromToken, ...(tokenName ? { tokenName } : {}) })\n }\n }\n\n // CSS custom properties — highest-fidelity design tokens.\n for (const m of html.matchAll(ROOT_VAR_RE)) {\n const tokenName = m[1]\n const value = m[2]\n if (!value) continue\n const lit = value.match(COLOR_LITERAL_RE)?.[0]\n if (!lit) continue\n const hex = normalizeColor(lit)\n if (hex) bump(hex, true, tokenName)\n }\n\n // All other color literals across the document (inline styles, <style>).\n for (const m of html.matchAll(COLOR_LITERAL_RE)) {\n const hex = normalizeColor(m[0])\n if (hex) bump(hex, false)\n }\n\n // Rank: token-sourced first, then by popularity. Drop pure white/black noise\n // to the back so a real brand color leads.\n const isNeutral = (h: string) => /^#(0{6}|f{6})(ff)?$/i.test(h)\n return [...byHex.values()].sort((a, b) => {\n if (a.fromToken !== b.fromToken) return a.fromToken ? -1 : 1\n const an = isNeutral(a.hex) ? 1 : 0\n const bn = isNeutral(b.hex) ? 1 : 0\n if (an !== bn) return an - bn\n return b.occurrences - a.occurrences\n })\n}\n\n// ── Fonts ────────────────────────────────────────────────────────────────────\n\nconst GENERIC_FAMILIES = new Set([\n 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',\n 'ui-sans-serif', 'ui-serif', 'ui-monospace', 'ui-rounded', 'inherit', 'initial', 'unset',\n])\n\n/** font-family declarations, with the selector chunk preceding them for role\n * inference. Captures the selector text in group 1, the stack in group 2. */\nconst FONT_DECL_RE = /([^{}]*)\\{[^{}]*font-family\\s*:\\s*([^;}{]+)[;}]/gi\n\nfunction parseFontStack(raw: string | undefined): string[] {\n if (!raw) return []\n return raw\n .split(',')\n .map((s) => s.trim().replace(/^[\"']|[\"']$/g, ''))\n .filter(Boolean)\n}\n\nfunction roleFromSelector(selector: string): 'display' | 'body' | 'unknown' {\n const s = selector.toLowerCase()\n if (/\\b(h[1-3]|\\.h[1-3]|heading|title|display|hero)\\b/.test(s)) return 'display'\n if (/\\b(body|p|html|\\.text|\\.content|root|:root|\\*)\\b/.test(s)) return 'body'\n return 'unknown'\n}\n\nfunction extractFonts(html: string): BrandFont[] {\n // Restrict to <style> blocks + the whole doc; inline style=\"\" attributes are\n // included because the regex matches any `{...font-family...}` chunk, but we\n // also scan style attributes explicitly below.\n const byFamily = new Map<string, BrandFont>()\n const consider = (family: string, stack: string[], role: BrandFont['role']) => {\n const key = family.toLowerCase()\n if (!family || GENERIC_FAMILIES.has(key)) return\n const existing = byFamily.get(key)\n if (existing) {\n existing.occurrences += 1\n if (existing.role === 'unknown' && role !== 'unknown') existing.role = role\n } else {\n byFamily.set(key, { family, stack, role, occurrences: 1 })\n }\n }\n\n for (const m of html.matchAll(FONT_DECL_RE)) {\n const selector = m[1] ?? ''\n const stack = parseFontStack(m[2])\n const primary = stack[0]\n if (!primary) continue\n consider(primary, stack, roleFromSelector(selector))\n }\n\n // Bare `font-family:` declarations inside inline style attributes and tokens.\n const BARE_RE = /font-family\\s*:\\s*([^;\"'}{]+)/gi\n for (const m of html.matchAll(BARE_RE)) {\n const stack = parseFontStack(m[1])\n const primary = stack[0]\n if (!primary) continue\n consider(primary, stack, 'unknown')\n }\n\n // Google Fonts <link href=\"...family=Inter:wght@400...\"> — strong signal.\n for (const tag of matchTags(html, 'link')) {\n const href = attr(tag, 'href') ?? ''\n if (!/fonts\\.googleapis\\.com/i.test(href)) continue\n for (const fam of href.matchAll(/family=([^:&]+)/gi)) {\n const captured = fam[1]\n if (!captured) continue\n const family = decodeURIComponent(captured.replace(/\\+/g, ' ')).trim()\n consider(family, [family], 'display')\n }\n }\n\n return [...byFamily.values()].sort((a, b) => {\n const rank = (r: BrandFont['role']) => (r === 'display' ? 0 : r === 'body' ? 1 : 2)\n const rr = rank(a.role) - rank(b.role)\n if (rr !== 0) return rr\n return b.occurrences - a.occurrences\n })\n}\n\n// ── Images ───────────────────────────────────────────────────────────────────\n\nfunction extractImages(html: string, base: string): BrandImage[] {\n const out: BrandImage[] = []\n const seen = new Set<string>()\n const push = (img: BrandImage) => {\n if (seen.has(img.url)) return\n seen.add(img.url)\n out.push(img)\n }\n\n for (const tag of matchTags(html, 'meta')) {\n const prop = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n const href = attr(tag, 'content')\n const url = href ? absolutize(href, base) : null\n if (!url) continue\n if (prop === 'og:image' || prop === 'og:image:url') push({ url, source: 'og:image' })\n else if (prop === 'twitter:image' || prop === 'twitter:image:src') push({ url, source: 'twitter:image' })\n }\n\n for (const tag of matchTags(html, 'img')) {\n const src = attr(tag, 'src') ?? attr(tag, 'data-src')\n if (!src) continue\n const url = absolutize(src, base)\n if (!url) continue\n const cls = `${attr(tag, 'class') ?? ''} ${attr(tag, 'id') ?? ''}`\n // Skip obvious logos/icons — those go in the logos list, not images.\n if (/logo|icon|favicon|wordmark/i.test(`${url} ${cls}`)) continue\n const isHero = /hero|banner|cover|feature|splash/i.test(cls)\n const w = attr(tag, 'width')\n const h = attr(tag, 'height')\n push({\n url,\n source: isHero ? 'img-hero' : 'img-content',\n alt: attr(tag, 'alt'),\n width: w ? Number(w) || undefined : undefined,\n height: h ? Number(h) || undefined : undefined,\n })\n }\n\n const rank = (s: BrandImage['source']) =>\n s === 'og:image' ? 0 : s === 'twitter:image' ? 1 : s === 'img-hero' ? 2 : 3\n return out.sort((a, b) => rank(a.source) - rank(b.source))\n}\n\n// ── Public API ───────────────────────────────────────────────────────────────\n\n/** Parse already-fetched HTML into a BrandKit. Pure — no network. Exposed so\n * callers that hold the HTML (or want to combine multiple pages) can reuse the\n * parsing without re-fetching. */\nexport function parseBrandKit(html: string, sourceUrl: string, maxPerList = DEFAULT_MAX_PER_LIST): BrandKit {\n const logos = extractLogos(html, sourceUrl).slice(0, maxPerList)\n const palette = extractPalette(html).slice(0, maxPerList)\n const fonts = extractFonts(html).slice(0, maxPerList)\n const images = extractImages(html, sourceUrl).slice(0, maxPerList)\n const name = extractName(html)\n const description = extractDescription(html)\n\n return {\n sourceUrl,\n ...(name ? { name } : {}),\n ...(description ? { description } : {}),\n logos,\n palette,\n fonts,\n images,\n extractedFrom: [sourceUrl],\n }\n}\n\n/**\n * Fetch a website (or use supplied HTML) and extract its BrandKit.\n *\n * Returns a typed outcome — callers MUST check `succeeded`. A fetch failure or\n * empty input is a real, surfaced error, never an empty kit masquerading as a\n * result. Parsing itself never throws: a malformed page yields a sparse kit\n * with warnings, which is information, not failure.\n */\nexport async function extractBrandKit(\n url: string,\n options: ExtractBrandKitOptions = {},\n): Promise<BrandExtractionResult> {\n const { html: providedHtml, fetchImpl, timeoutMs = DEFAULT_TIMEOUT_MS, maxPerList = DEFAULT_MAX_PER_LIST } = options\n const warnings: string[] = []\n\n const sourceUrl = normalizeSiteUrl(url)\n if (!sourceUrl) {\n return { succeeded: false, error: `Not a valid http(s) URL: ${JSON.stringify(url)}`, stage: 'input' }\n }\n\n let html = providedHtml\n if (html === undefined) {\n const doFetch: FetchLike | undefined =\n fetchImpl ?? (typeof globalThis.fetch === 'function'\n ? (u, init) => globalThis.fetch(u, init as RequestInit)\n : undefined)\n if (!doFetch) {\n return { succeeded: false, error: 'No html provided and no fetch implementation available', stage: 'input' }\n }\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await doFetch(sourceUrl, {\n signal: controller.signal,\n headers: { 'user-agent': 'TangleBrandExtractor/1.0 (+https://tangle.tools)' },\n })\n if (!res.ok) {\n return { succeeded: false, error: `Fetch failed: HTTP ${res.status} for ${sourceUrl}`, stage: 'fetch' }\n }\n html = await res.text()\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err)\n return { succeeded: false, error: `Fetch error for ${sourceUrl}: ${reason}`, stage: 'fetch' }\n } finally {\n clearTimeout(timer)\n }\n }\n\n if (!html.trim()) {\n return { succeeded: false, error: `Empty document for ${sourceUrl}`, stage: 'parse' }\n }\n\n const kit = parseBrandKit(html, sourceUrl, maxPerList)\n\n if (kit.logos.length === 0) warnings.push('No logo candidates found.')\n if (kit.palette.length === 0) warnings.push('No colors extracted — page has no inline CSS or design tokens.')\n if (kit.fonts.length === 0) warnings.push('No custom fonts found — site likely uses system defaults.')\n if (kit.images.length === 0) warnings.push('No prominent images found.')\n\n return { succeeded: true, kit, warnings }\n}\n","/**\n * Pure mapping helpers from an extracted BrandKit onto consumable shapes.\n *\n * Extraction yields ranked candidate lists; products need decided roles\n * (background vs accent, display vs body). These helpers make the obvious,\n * defensible default choice and expose the reasoning so a confirmation UI can\n * show \"we picked X — change it?\". They NEVER invent a value: when a role can't\n * be filled from the kit, it is omitted, and the caller decides the fallback.\n */\n\nimport type { BrandColor, BrandFont, BrandKit } from './types'\n\n/** Relative luminance (0..1) of an #rrggbb(aa) hex — for light/dark sorting. */\nexport function luminance(hex: string): number {\n const h = hex.replace('#', '').slice(0, 6)\n const r = parseInt(h.slice(0, 2), 16) / 255\n const g = parseInt(h.slice(2, 4), 16) / 255\n const b = parseInt(h.slice(4, 6), 16) / 255\n const lin = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)\n return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)\n}\n\nfunction isGreyish(hex: string): boolean {\n const h = hex.replace('#', '').slice(0, 6)\n const r = parseInt(h.slice(0, 2), 16)\n const g = parseInt(h.slice(2, 4), 16)\n const b = parseInt(h.slice(4, 6), 16)\n const max = Math.max(r, g, b)\n const min = Math.min(r, g, b)\n return max - min < 18\n}\n\nexport interface DecidedPalette {\n /** Lightest neutral — page background. */\n background?: string\n /** A raised neutral one step from background. */\n surface?: string\n /** Darkest readable color — primary text. */\n textPrimary?: string\n /** Mid-tone neutral — secondary text. */\n textSecondary?: string\n /** Most saturated / popular brand color — accent. */\n accent?: string\n /** Readable text color to sit on the accent (black or white by contrast). */\n accentText?: string\n}\n\n/**\n * Assign palette roles from the ranked colors. Heuristic, not authoritative —\n * a confirmation step should let the user correct it. Roles only appear when\n * the kit actually contained a color that fits.\n */\nexport function decidePalette(palette: BrandColor[]): DecidedPalette {\n if (palette.length === 0) return {}\n const hexes = palette.map((c) => c.hex)\n const greys = hexes.filter(isGreyish).sort((a, b) => luminance(b) - luminance(a))\n const colored = hexes.filter((h) => !isGreyish(h))\n\n // Accent: the top-ranked non-grey (token-first, popularity-first) color.\n const accent = colored[0]\n // Background: lightest grey (or lightest overall when no greys).\n const sortedLight = [...hexes].sort((a, b) => luminance(b) - luminance(a))\n const background = greys[0] ?? sortedLight[0]\n // Surface: next grey under background, else second-lightest.\n const surface = greys[1] ?? sortedLight.find((h) => h !== background)\n // Text primary: darkest color overall.\n const textPrimary = [...hexes].sort((a, b) => luminance(a) - luminance(b))[0]\n // Text secondary: a mid-luminance grey distinct from primary/background.\n const textSecondary = greys.find((h) => h !== background && h !== surface && h !== textPrimary)\n\n const result: DecidedPalette = {}\n if (background) result.background = background\n if (surface) result.surface = surface\n if (textPrimary) result.textPrimary = textPrimary\n if (textSecondary) result.textSecondary = textSecondary\n if (accent) {\n result.accent = accent\n result.accentText = luminance(accent) > 0.45 ? '#000000' : '#ffffff'\n }\n return result\n}\n\nexport interface DecidedFonts {\n display?: BrandFont\n body?: BrandFont\n}\n\n/** Pick a display and body font from the ranked list. When only one usable\n * font exists it fills both roles — a single-typeface brand is valid. */\nexport function decideFonts(fonts: BrandFont[]): DecidedFonts {\n if (fonts.length === 0) return {}\n const display = fonts.find((f) => f.role === 'display') ?? fonts[0]\n const body = fonts.find((f) => f.role === 'body') ?? fonts.find((f) => f !== display) ?? display\n return { display, body }\n}\n\n/** Everything a confirmation step needs from a kit, with roles decided. */\nexport interface DecidedBrandKit {\n name?: string\n description?: string\n sourceUrl: string\n palette: DecidedPalette\n fonts: DecidedFonts\n /** Best logo URL, when any candidate was found. */\n primaryLogoUrl?: string\n /** All logo URLs, ranked. */\n logoUrls: string[]\n /** Prominent image URLs, ranked. */\n imageUrls: string[]\n extractedFrom: string[]\n}\n\n/** Collapse a raw BrandKit into decided roles — the shape a product persists. */\nexport function decideBrandKit(kit: BrandKit): DecidedBrandKit {\n const palette = decidePalette(kit.palette)\n const fonts = decideFonts(kit.fonts)\n const logoUrls = kit.logos.map((l) => l.url)\n const result: DecidedBrandKit = {\n sourceUrl: kit.sourceUrl,\n palette,\n fonts,\n logoUrls,\n imageUrls: kit.images.map((i) => i.url),\n extractedFrom: kit.extractedFrom,\n }\n if (kit.name) result.name = kit.name\n if (kit.description) result.description = kit.description\n if (logoUrls[0]) result.primaryLogoUrl = logoUrls[0]\n return result\n}\n"],"mappings":"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","names":[]}
@@ -215,4 +215,4 @@ export {
215
215
  stepActivityFlowTrace,
216
216
  composeMissionFlowTrace
217
217
  };
218
- //# sourceMappingURL=chunk-AFDROJ64.js.map
218
+ //# sourceMappingURL=chunk-2QI7XV2T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/trace/mission-flow.ts"],"sourcesContent":["/**\n * Delegation → FlowSpan converters — render what a mission's delegated agent\n * runs actually did as ONE FlowTrace, drawable by the existing\n * `renderWaterfall` (or any viewer that consumes FlowSpans).\n *\n * Two fidelities, one tree:\n * - COARSE: `delegationActivityToFlowSpans` draws one 'tool' span per\n * delegation from the StepAgentActivity snapshot (startedAt/durationMs) —\n * available live, from the step's journaled lane.\n * - FINE: `loopTraceEventsToFlowSpans` reconstructs agent-runtime's\n * loop → round → iteration hierarchy from the LoopTraceEvent journal a\n * delegation persists. FlowSpans carry no parent ids, so nesting rides the\n * span NAME (`loop ▸ round 0 ▸ iter 1 (coder)`), matching how the ASCII\n * waterfall reads.\n *\n * `composeMissionFlowTrace` lays a whole mission out: one 'pipeline' span per\n * step, each step's delegations beneath it. Pure data transforms — the\n * structural `LoopTraceEventLike` keeps this module free of the optional\n * agent-runtime peer.\n */\n\nimport type { StepAgentActivity } from '../missions/agent-activity'\nimport type { FlowSpan, FlowTrace } from './flow-types'\n\n/** Structural mirror of agent-runtime's `LoopTraceEvent` — same fields, no\n * import, so journals parsed from JSON feed straight in. */\nexport interface LoopTraceEventLike {\n kind: string\n runId: string\n /** Epoch ms. */\n timestamp: number\n payload: object\n}\n\nconst num = (v: unknown): number | undefined =>\n typeof v === 'number' && Number.isFinite(v) ? v : undefined\nconst str = (v: unknown): string | undefined =>\n typeof v === 'string' && v.length > 0 ? v : undefined\nconst rec = (v: unknown): Record<string, unknown> =>\n v && typeof v === 'object' ? (v as Record<string, unknown>) : {}\n\n/** Statuses that mean the delegation is still producing wall time. */\nconst LIVE_DELEGATION_STATUSES = new Set(['pending', 'running'])\n\n/**\n * One 'tool' FlowSpan per delegation, positioned relative to `turnStartMs`\n * (the epoch-ms origin of the trace — usually the step or mission start).\n * A row whose `startedAt` does not parse cannot be placed on a timeline and\n * is omitted from the WATERFALL (it stays in the lane itself). A run without\n * `durationMs` is still in flight: its span extends to `opts.nowMs` when\n * given, else renders as a point — `approx` flags both.\n */\nexport function delegationActivityToFlowSpans(\n activity: StepAgentActivity[],\n turnStartMs: number,\n opts?: { nowMs?: number },\n): FlowSpan[] {\n const spans: FlowSpan[] = []\n for (const run of activity) {\n const startedAt = Date.parse(run.startedAt)\n if (!Number.isFinite(startedAt)) continue\n const startMs = startedAt - turnStartMs\n const live = LIVE_DELEGATION_STATUSES.has(run.status)\n const endMs =\n run.durationMs !== undefined\n ? startMs + run.durationMs\n : live && opts?.nowMs !== undefined\n ? opts.nowMs - turnStartMs\n : startMs\n spans.push({\n kind: 'tool',\n name: `${run.tool} — ${run.detail}`,\n startMs,\n endMs: Math.max(endMs, startMs),\n ...(run.durationMs === undefined ? { approx: true } : {}),\n meta: {\n taskId: run.taskId,\n status: run.status,\n ...(run.costUsd !== undefined ? { costUsd: run.costUsd } : {}),\n ...(run.iteration !== undefined ? { iteration: run.iteration } : {}),\n ...(run.phase !== undefined ? { phase: run.phase } : {}),\n ...(run.traceId !== undefined ? { traceId: run.traceId } : {}),\n ...(run.spanId !== undefined ? { spanId: run.spanId } : {}),\n },\n })\n }\n return spans\n}\n\n/**\n * Reconstruct one delegation's loop → round → iteration tree from its\n * journaled LoopTraceEvents, as FlowSpans relative to `loop.started` (or the\n * first event). Mirrors agent-runtime's `buildLoopOtelSpans` topology:\n * rounds open on `loop.plan` and flush on the next plan / `loop.decision` /\n * `loop.ended`; iterations span `loop.iteration.started` → `.ended`.\n */\nexport function loopTraceEventsToFlowSpans(events: LoopTraceEventLike[]): FlowSpan[] {\n if (events.length === 0) return []\n const ordered = [...events].sort((a, b) => a.timestamp - b.timestamp)\n const started = ordered.find((e) => e.kind === 'loop.started')\n const ended = ordered.find((e) => e.kind === 'loop.ended')\n const origin = started?.timestamp ?? ordered[0]!.timestamp\n const rootEnd = ended?.timestamp ?? ordered[ordered.length - 1]!.timestamp\n const t = (epochMs: number) => epochMs - origin\n\n const spans: FlowSpan[] = []\n const sp = rec(started?.payload)\n const ep = rec(ended?.payload)\n spans.push({\n kind: 'pipeline',\n name: 'loop',\n startMs: 0,\n endMs: t(rootEnd),\n meta: {\n runId: ordered[0]!.runId,\n ...(str(sp.driver) !== undefined ? { driver: str(sp.driver) } : {}),\n ...(num(ep.totalCostUsd) !== undefined ? { costUsd: num(ep.totalCostUsd) } : {}),\n ...(num(ep.winnerIterationIndex) !== undefined\n ? { winnerIterationIndex: num(ep.winnerIterationIndex) }\n : {}),\n ...(num(ep.iterations) !== undefined ? { iterations: num(ep.iterations) } : {}),\n },\n })\n\n const iterStart = new Map<number, number>()\n let round: { index: number; startMs: number; meta: Record<string, unknown>; moveKind: string } | undefined\n const flushRound = (endEpochMs: number) => {\n if (!round) return\n spans.push({\n kind: 'pipeline',\n name: `loop ▸ round ${round.index} (${round.moveKind})`,\n startMs: round.startMs,\n endMs: t(endEpochMs),\n meta: round.meta,\n })\n round = undefined\n }\n\n for (const e of ordered) {\n const p = rec(e.payload)\n switch (e.kind) {\n case 'loop.plan': {\n flushRound(e.timestamp)\n const index = num(p.roundIndex) ?? 0\n round = {\n index,\n startMs: t(e.timestamp),\n moveKind: str(p.moveKind) ?? 'unknown',\n meta: {\n roundIndex: index,\n moveKind: str(p.moveKind) ?? 'unknown',\n width: num(p.plannedCount) ?? 0,\n ...(str(p.rationale) !== undefined ? { rationale: str(p.rationale) } : {}),\n ...(num(p.parentIndex) !== undefined ? { parentIndex: num(p.parentIndex) } : {}),\n },\n }\n break\n }\n case 'loop.iteration.started': {\n const idx = num(p.iterationIndex)\n if (idx !== undefined) iterStart.set(idx, e.timestamp)\n break\n }\n case 'loop.iteration.ended': {\n const idx = num(p.iterationIndex) ?? 0\n const startEpoch = iterStart.get(idx) ?? e.timestamp\n const error = str(p.error)\n const verdict = rec(p.verdict)\n const tokens = rec(p.tokenUsage)\n const roundLabel = round ? `round ${round.index} ▸ ` : ''\n spans.push({\n kind: 'model',\n name: `loop ▸ ${roundLabel}iter ${idx} (${str(p.agentRunName) ?? 'agent'})`,\n startMs: t(startEpoch),\n endMs: t(e.timestamp),\n meta: {\n iterationIndex: idx,\n ok: error === undefined,\n ...(error !== undefined ? { error } : {}),\n ...(num(p.costUsd) !== undefined ? { costUsd: num(p.costUsd) } : {}),\n ...(typeof verdict.valid === 'boolean' ? { verdictValid: verdict.valid } : {}),\n ...(num(verdict.score) !== undefined ? { verdictScore: num(verdict.score) } : {}),\n ...(num(tokens.input) !== undefined ? { inputTokens: num(tokens.input) } : {}),\n ...(num(tokens.output) !== undefined ? { outputTokens: num(tokens.output) } : {}),\n },\n })\n break\n }\n case 'loop.decision': {\n if (round) {\n const decision = str(p.decision)\n if (decision !== undefined) round.meta.decision = decision\n flushRound(e.timestamp)\n }\n break\n }\n }\n }\n flushRound(rootEnd)\n return spans\n}\n\n/**\n * A single step's activity lane as its own FlowTrace — what a per-step\n * drill-in renders. Origin defaults to the earliest delegation start so the\n * waterfall begins at the lane's first run.\n */\nexport function stepActivityFlowTrace(\n activity: StepAgentActivity[],\n opts?: { startedAt?: number; nowMs?: number },\n): FlowTrace {\n let origin = opts?.startedAt\n if (origin === undefined) {\n for (const run of activity) {\n const parsed = Date.parse(run.startedAt)\n if (Number.isFinite(parsed) && (origin === undefined || parsed < origin)) origin = parsed\n }\n }\n const spans = delegationActivityToFlowSpans(\n activity,\n origin ?? 0,\n opts?.nowMs !== undefined ? { nowMs: opts.nowMs } : undefined,\n )\n let costUsd: number | undefined\n for (const span of spans) {\n const c = num(rec(span.meta).costUsd)\n if (c !== undefined) costUsd = (costUsd ?? 0) + c\n }\n return {\n spans,\n totalMs: spans.reduce((max, s) => Math.max(max, s.endMs), 0),\n promptTokens: 0,\n completionTokens: 0,\n ...(costUsd !== undefined ? { costUsd } : {}),\n toolCalls: spans.length,\n }\n}\n\nexport interface MissionFlowStep {\n id: string\n intent: string\n status?: string\n /** Epoch ms the step attempt started. Absent → laid out sequentially after\n * the previous step (missions run steps in order), `approx` flagged. */\n startedAt?: number\n durationMs?: number\n}\n\n/**\n * Compose a mission-wide FlowTrace: one 'pipeline' span per step, the step's\n * delegated runs ('tool' spans, from `activity[stepId]`) beneath it. A step\n * span always covers its delegations' extent. Cost is the sum of delegation\n * `costUsd`; token counts are not knowable from the activity lane and stay 0.\n */\nexport function composeMissionFlowTrace(input: {\n steps: MissionFlowStep[]\n /** Delegated-run snapshots keyed by step id (the step's `agentActivity`). */\n activity?: Record<string, StepAgentActivity[]>\n /** Epoch-ms origin. Default: the earliest known step/delegation start. */\n startedAt?: number\n}): FlowTrace {\n const activity = input.activity ?? {}\n\n const startCandidates: number[] = []\n if (input.startedAt !== undefined) startCandidates.push(input.startedAt)\n else {\n for (const step of input.steps) {\n if (step.startedAt !== undefined) startCandidates.push(step.startedAt)\n }\n for (const runs of Object.values(activity)) {\n for (const run of runs) {\n const parsed = Date.parse(run.startedAt)\n if (Number.isFinite(parsed)) startCandidates.push(parsed)\n }\n }\n }\n const origin = startCandidates.length > 0 ? Math.min(...startCandidates) : 0\n\n const spans: FlowSpan[] = []\n let costUsd: number | undefined\n let toolCalls = 0\n let cursorMs = 0\n\n for (const step of input.steps) {\n const inferred = step.startedAt === undefined\n const startMs = inferred ? cursorMs : step.startedAt! - origin\n const runSpans = delegationActivityToFlowSpans(activity[step.id] ?? [], origin)\n const runExtent = runSpans.reduce((max, s) => Math.max(max, s.endMs), startMs)\n const endMs = Math.max(step.durationMs !== undefined ? startMs + step.durationMs : startMs, runExtent)\n\n spans.push({\n kind: 'pipeline',\n name: step.intent,\n startMs,\n endMs,\n ...(inferred || step.durationMs === undefined ? { approx: true } : {}),\n meta: { stepId: step.id, ...(step.status !== undefined ? { status: step.status } : {}) },\n })\n for (const span of runSpans) {\n spans.push({ ...span, name: `${step.intent} ▸ ${span.name}` })\n toolCalls++\n const c = num(rec(span.meta).costUsd)\n if (c !== undefined) costUsd = (costUsd ?? 0) + c\n }\n cursorMs = endMs\n }\n\n return {\n spans,\n totalMs: spans.reduce((max, s) => Math.max(max, s.endMs), 0),\n promptTokens: 0,\n completionTokens: 0,\n ...(costUsd !== undefined ? { costUsd } : {}),\n toolCalls,\n }\n}\n"],"mappings":"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","names":[]}
@@ -248,6 +248,12 @@ async function* streamChatCompletions(doFetch, url, apiKey, body) {
248
248
  }
249
249
  }
250
250
 
251
+ // src/runtime/loop.ts
252
+ import {
253
+ runToolLoop,
254
+ streamToolLoop
255
+ } from "@tangle-network/agent-runtime";
256
+
251
257
  // src/runtime/agent.ts
252
258
  function createAgentRuntime(opts) {
253
259
  if (opts.executeOtherTool && !opts.isOtherExecutableTool) {
@@ -477,12 +483,6 @@ function assertSurfaceOverlay(overlay, label) {
477
483
  }
478
484
  }
479
485
 
480
- // src/runtime/index.ts
481
- import {
482
- runToolLoop,
483
- streamToolLoop
484
- } from "@tangle-network/agent-runtime";
485
-
486
486
  export {
487
487
  normalizeModelId,
488
488
  buildCatalog,
@@ -490,12 +490,12 @@ export {
490
490
  __resetCatalogCache,
491
491
  toLoopEvents,
492
492
  createOpenAICompatStreamTurn,
493
+ runToolLoop,
494
+ streamToolLoop,
493
495
  createAgentRuntime,
494
496
  createCertifiedDelivery,
495
497
  defineSurfaceKind,
496
498
  createSurfaceRegistry,
497
- mergeSurfaceOverlay,
498
- runToolLoop,
499
- streamToolLoop
499
+ mergeSurfaceOverlay
500
500
  };
501
- //# sourceMappingURL=chunk-LUE4HO5C.js.map
501
+ //# sourceMappingURL=chunk-4IQMTTNE.js.map