kiku-agent 1.2.0 → 1.3.1

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 (131) hide show
  1. package/README.md +459 -407
  2. package/package.json +32 -3
  3. package/server/agent/agentCore.js +635 -267
  4. package/server/agent/appPaths.js +2 -0
  5. package/server/agent/catalogEntityResolver.js +637 -0
  6. package/server/agent/citations.js +0 -21
  7. package/server/agent/clarificationAnalytics.js +1 -1
  8. package/server/agent/clarifyingQuestions.js +286 -216
  9. package/server/agent/cli.js +532 -73
  10. package/server/agent/commandFeatures.js +123 -24
  11. package/server/agent/commandRegistry.js +128 -62
  12. package/server/agent/commands.js +1410 -95
  13. package/server/agent/config.js +253 -163
  14. package/server/agent/contextCompactor.js +239 -25
  15. package/server/agent/dynamicCatalog.js +192 -0
  16. package/server/agent/envBootstrap.js +46 -0
  17. package/server/agent/envManager.js +317 -0
  18. package/server/agent/eventBus.js +3 -3
  19. package/server/agent/feedbackLearning.js +0 -8
  20. package/server/agent/gpuMetrics.js +188 -55
  21. package/server/agent/ingestionWorker.js +339 -0
  22. package/server/agent/ink/App.jsx +348 -0
  23. package/server/agent/ink/Panel.jsx +54 -0
  24. package/server/agent/ink/PromptInput.jsx +321 -0
  25. package/server/agent/ink/TaskPlanPanel.jsx +88 -0
  26. package/server/agent/ink/ThinkingPanel.jsx +68 -0
  27. package/server/agent/ink/ToolResult.jsx +133 -0
  28. package/server/agent/ink/main.jsx +195 -0
  29. package/server/agent/ink/useEventBus.js +218 -0
  30. package/server/agent/lanDiscovery.js +369 -0
  31. package/server/agent/llmClient.js +111 -39
  32. package/server/agent/lmStudioWakeup.js +144 -0
  33. package/server/agent/mcpClient.js +2 -2
  34. package/server/agent/memory/contextAllocator.js +214 -0
  35. package/server/agent/memory/hierarchicalStore.js +523 -0
  36. package/server/agent/memory/reflectionWorker.js +110 -0
  37. package/server/agent/memory.js +635 -494
  38. package/server/agent/modelConfig.js +487 -0
  39. package/server/agent/newsIntelligence.js +219 -0
  40. package/server/agent/newsWatch.js +85 -0
  41. package/server/agent/prompts.js +146 -0
  42. package/server/agent/publicTools.js +7 -0
  43. package/server/agent/queryDetails.js +62 -1
  44. package/server/agent/research.js +314 -35
  45. package/server/agent/routingInterceptor.js +77 -0
  46. package/server/agent/rumoredProjects.js +15 -0
  47. package/server/agent/sanitizer.js +117 -17
  48. package/server/agent/subagent.js +52 -9
  49. package/server/agent/suggestionTui.js +33 -13
  50. package/server/agent/suggestions.js +29 -13
  51. package/server/agent/telemetry/hardwareTiers.js +332 -0
  52. package/server/agent/telemetry/runtimeWatchdog.js +66 -0
  53. package/server/agent/telemetry/techPowerUpClient.js +682 -0
  54. package/server/agent/telemetry/techpowerup-mcp.json +22 -0
  55. package/server/agent/telemetry.js +0 -11
  56. package/server/agent/terminalImage.js +0 -1
  57. package/server/agent/textToolProtocol.js +190 -0
  58. package/server/agent/tokenTracker.js +220 -0
  59. package/server/agent/toolRouter.js +362 -216
  60. package/server/agent/tools.js +1155 -194
  61. package/server/agent/ui.js +144 -41
  62. package/server/agent/vramService.js +572 -0
  63. package/server/agent/webSearch.js +2279 -273
  64. package/server/agent/websearch/adapters/duckduckgo.js +97 -0
  65. package/server/agent/websearch/adapters/jina.js +77 -0
  66. package/server/agent/websearch/adapters/searxng.js +57 -0
  67. package/server/agent/websearch/core/canonical.js +89 -0
  68. package/server/agent/websearch/core/fusion.js +81 -0
  69. package/server/agent/websearch/core/types.js +29 -0
  70. package/server/agent/websearch/extract.js +605 -0
  71. package/server/agent/websearch/index.js +43 -0
  72. package/server/agent/websearch/orchestrator.js +266 -0
  73. package/server/agent/websearch/resilience/breaker.js +97 -0
  74. package/server/agent/websearch/resilience/cache.js +59 -0
  75. package/server/agent/websearch/resilience/limiter.js +93 -0
  76. package/server/agent/websearch/security/filter.js +36 -0
  77. package/server/agent/websearch/security/ssrf.js +289 -0
  78. package/server/agent/websearch/telemetry.js +71 -0
  79. package/server/agent/youtubeAuth.js +82 -73
  80. package/server/agent/youtubeTranscript.js +209 -0
  81. package/server/apiRouter.js +390 -190
  82. package/server/mcp/data/brands.js +5 -5
  83. package/server/mcp/data/discography.js +56 -45
  84. package/server/mcp/data/gallery.js +9 -0
  85. package/server/mcp/data/index.js +1 -0
  86. package/server/mcp/data/industryIndex.js +251 -0
  87. package/server/mcp/data/profile.js +5 -5
  88. package/server/mcp/data/timeline.js +2 -2
  89. package/server/mcp/data/works.js +2 -2
  90. package/server/mcp/handler.js +0 -2
  91. package/server/mcp/prompts.js +42 -2
  92. package/server/mcp/resources.js +5 -1
  93. package/server/mcp/security.js +25 -14
  94. package/server/mcp/server.js +92 -3
  95. package/server/mcp/tools.js +20 -11
  96. package/server/routes/agent.js +55 -0
  97. package/server/web-chat/core/connection.js +218 -194
  98. package/server/web-chat/core/conversationContext.js +1 -1
  99. package/server/web-chat/core/directCatalogEngine.js +190 -184
  100. package/server/web-chat/core/followUpEngine.js +88 -11
  101. package/server/web-chat/core/security.js +7 -4
  102. package/server/web-chat/router.js +54 -17
  103. package/server/web-chat/routes/bilibiliRoute.js +34 -0
  104. package/server/web-chat/routes/chatRoute.js +488 -397
  105. package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
  106. package/server/web-chat/routes/exportRoute.js +2 -1
  107. package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
  108. package/server/web-chat/routes/newsRoute.js +36 -6
  109. package/server/web-chat/routes/providerRoute.js +87 -0
  110. package/server/web-chat/routes/staticRoute.js +61 -13
  111. package/server/web-chat/routes/statusRoute.js +23 -2
  112. package/server/web-chat/routes/vramRoute.js +22 -0
  113. package/server/web-chat/server.js +135 -106
  114. package/server/web-chat/ui/htmlBuilder.js +8 -8
  115. package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
  116. package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
  117. package/server/web-chat/ui/scripts/client.js +10526 -8397
  118. package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
  119. package/server/web-chat/ui/styles/main.css +7471 -4246
  120. package/server/web-chat/ui/templates/body.html +305 -179
  121. package/src/data/brandsData.js +29 -6
  122. package/src/data/discographyData.js +918 -900
  123. package/src/data/galleryData.js +66 -0
  124. package/src/data/industryIndexData.js +292 -0
  125. package/src/data/musicVideoData.js +48 -5
  126. package/src/data/profileData.js +15 -1
  127. package/src/data/timelineData.js +4 -3
  128. package/src/data/worksData.js +536 -535
  129. package/src/lib/vram/estimator.js +464 -0
  130. package/src/lib/vram/modelRegistry.js +387 -0
  131. package/tokens.css +33 -2
@@ -0,0 +1,605 @@
1
+ let _cheerioLoad = null;
2
+
3
+ export async function loadCheerio() {
4
+ if (_cheerioLoad) return _cheerioLoad;
5
+ try {
6
+ const mod = await import('cheerio');
7
+ _cheerioLoad = mod.load;
8
+ return _cheerioLoad;
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ const CHARSET_ALIASES = new Map(Object.entries({
15
+ 'utf8': 'utf-8',
16
+ 'utf-8': 'utf-8',
17
+ 'unicode-1-1-utf-8': 'utf-8',
18
+ 'utf-16': 'utf-16le',
19
+ 'utf-16le': 'utf-16le',
20
+ 'unicodefeff': 'utf-16le',
21
+ 'utf-16be': 'utf-16be',
22
+ 'unicodefffe': 'utf-16be',
23
+ 'latin1': 'windows-1252',
24
+ 'iso-8859-1': 'windows-1252',
25
+ 'iso8859-1': 'windows-1252',
26
+ 'windows-1252': 'windows-1252',
27
+ 'cp1252': 'windows-1252',
28
+ 'ascii': 'windows-1252',
29
+ 'us-ascii': 'windows-1252',
30
+ 'tis-620': 'windows-874',
31
+ 'iso-8859-11': 'windows-874',
32
+ 'windows-874': 'windows-874',
33
+ 'dos-874': 'windows-874',
34
+ 'gb2312': 'gbk',
35
+ 'gb_2312': 'gbk',
36
+ 'gbk': 'gbk',
37
+ 'gb18030': 'gb18030',
38
+ 'big5': 'big5',
39
+ 'big5-hkscs': 'big5',
40
+ 'shift_jis': 'shift_jis',
41
+ 'shift-jis': 'shift_jis',
42
+ 'sjis': 'shift_jis',
43
+ 'cp932': 'shift_jis',
44
+ 'euc-kr': 'euc-kr',
45
+ 'ksc5601': 'euc-kr',
46
+ 'windows-1251': 'windows-1251',
47
+ 'koi8-r': 'koi8-r',
48
+ }));
49
+
50
+ export function normalizeCharsetLabel(raw) {
51
+ if (!raw) return null;
52
+ const key = String(raw).trim().toLowerCase().replace(/["'\s]/g, '');
53
+ return CHARSET_ALIASES.get(key) || null;
54
+ }
55
+
56
+ export function decodeHtmlBytes(bytes, contentTypeHeader = '') {
57
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes || []);
58
+ if (buf.length >= 3 && buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
59
+ return { text: new TextDecoder('utf-8').decode(buf.subarray(3)), charset: 'utf-8' };
60
+ }
61
+ if (buf.length >= 2 && buf[0] === 0xFF && buf[1] === 0xFE) {
62
+ return { text: new TextDecoder('utf-16le').decode(buf.subarray(2)), charset: 'utf-16le' };
63
+ }
64
+ if (buf.length >= 2 && buf[0] === 0xFE && buf[1] === 0xFF) {
65
+ return { text: new TextDecoder('utf-16be').decode(buf.subarray(2)), charset: 'utf-16be' };
66
+ }
67
+
68
+ let charset = null;
69
+ const headerMatch = /charset\s*=\s*"?([A-Za-z0-9_\-.:]+)"?/i.exec(String(contentTypeHeader || ''));
70
+ if (headerMatch) charset = headerMatch[1];
71
+ if (!charset) {
72
+ const head = buf.subarray(0, Math.min(buf.length, 4096)).toString('latin1');
73
+ const metaMatch = /charset\s*=\s*["']?\s*([A-Za-z0-9_\-.:]+)/i.exec(head);
74
+ if (metaMatch) charset = metaMatch[1];
75
+ }
76
+
77
+ const label = normalizeCharsetLabel(charset);
78
+ if (!label) {
79
+ return { text: new TextDecoder('utf-8').decode(buf), charset: 'utf-8' };
80
+ }
81
+ try {
82
+ return { text: new TextDecoder(label).decode(buf), charset: label };
83
+ } catch {
84
+ return { text: new TextDecoder('utf-8').decode(buf), charset: 'utf-8' };
85
+ }
86
+ }
87
+
88
+ const NAMED_ENTITIES = {
89
+ nbsp: ' ', amp: '&', lt: '<', gt: '>', quot: '"', apos: "'",
90
+ mdash: '—', ndash: '–', hellip: '...', rsquo: '’', lsquo: '‘',
91
+ rdquo: '”', ldquo: '“', copy: '\u00A9', reg: '\u00AE', trade: '\u2122',
92
+ laquo: '«', raquo: '»', times: '×', divide: '÷', cent: '¢',
93
+ pound: '£', euro: '€', yen: '¥', sect: '§', middot: '·',
94
+ bull: '•', deg: '°', plusmn: '±', frac12: '½', sup2: '²',
95
+ sup3: '³', micro: 'µ', para: '¶', eacute: 'é', egrave: 'è',
96
+ agrave: 'à', ccedil: 'ç', uuml: 'ü', ouml: 'ö', auml: 'ä', ntilde: 'ñ',
97
+ };
98
+
99
+ export function decodeEntities(str) {
100
+ if (!str || typeof str !== 'string' || !str.includes('&')) return str || '';
101
+ return str
102
+ .replace(/&#x([0-9a-f]+);/gi, (_, hex) => {
103
+ try { return String.fromCodePoint(parseInt(hex, 16)); } catch { return ''; }
104
+ })
105
+ .replace(/&#(\d+);/g, (_, dec) => {
106
+ try { return String.fromCodePoint(Number(dec)); } catch { return ''; }
107
+ })
108
+ .replace(/&([a-z][a-z0-9]*);/gi, (match, name) => {
109
+ const value = NAMED_ENTITIES[name.toLowerCase()];
110
+ return value !== undefined ? value : match;
111
+ });
112
+ }
113
+
114
+ const ZERO_WIDTH_RE = /[\u200B-\u200D\uFEFF\u2060]/g;
115
+
116
+ export function normalizeText(str) {
117
+ if (!str || typeof str !== 'string') return '';
118
+ return str
119
+ .normalize('NFC')
120
+ .replace(ZERO_WIDTH_RE, '')
121
+ .replace(/\s+/g, ' ')
122
+ .trim();
123
+ }
124
+
125
+ const CONTENT_HINT_RE = /(article|content|post|entry|story|body|main|blog|news|wiki|markdown|rich-?text|editor|mp-editor|prose|detail)/i;
126
+ const NOISE_HINT_RE = /(nav|menu|header|footer|sidebar|aside|comment|promo|banner|ads?-|advert|social|share|related|recommend|breadcrumb|widget|tag-|category-list|pagination|subscribe|newsletter|cookie|popup|modal|searchbox|form|button|toolbar|copyright|disclaimer)/i;
127
+
128
+ function extractAuthor(author) {
129
+ if (!author) return '';
130
+ if (typeof author === 'string') return author;
131
+ if (Array.isArray(author)) return author.map(extractAuthor).filter(Boolean).join(', ');
132
+ if (typeof author === 'object' && author.name) return String(author.name);
133
+ return '';
134
+ }
135
+
136
+ export function parseJsonLd($) {
137
+ const found = [];
138
+ $('script[type="application/ld+json"]').each((_, el) => {
139
+ try {
140
+ collectJsonLd(JSON.parse($(el).text() || '{}'), found);
141
+ } catch {}
142
+ });
143
+
144
+ let best = null;
145
+ for (const item of found) {
146
+ const body = typeof item.articleBody === 'string'
147
+ ? item.articleBody
148
+ : (Array.isArray(item.articleBody) ? item.articleBody.join('\n\n') : '');
149
+ const headline = typeof item.headline === 'string' ? item.headline : '';
150
+ if (!body && !headline) continue;
151
+ if (!best || body.length >= best.body.length) {
152
+ best = {
153
+ body,
154
+ headline,
155
+ datePublished: item.datePublished || null,
156
+ dateModified: item.dateModified || null,
157
+ author: extractAuthor(item.author),
158
+ };
159
+ }
160
+ }
161
+ return best;
162
+ }
163
+
164
+ function collectJsonLd(node, out) {
165
+ if (Array.isArray(node)) {
166
+ node.forEach(entry => collectJsonLd(entry, out));
167
+ return;
168
+ }
169
+ if (!node || typeof node !== 'object') return;
170
+ if (node['@graph']) {
171
+ collectJsonLd(node['@graph'], out);
172
+ return;
173
+ }
174
+ const type = String(node['@type'] || '').toLowerCase();
175
+ if (/article|blogposting|newsarticle|webpage|discussion|report|answer/.test(type) || node.articleBody) {
176
+ out.push(node);
177
+ }
178
+ }
179
+
180
+ const MAX_BLOCKS = 500;
181
+ const MAX_MARKDOWN_CHARS = 120_000;
182
+
183
+ export async function extractMainContent(html, { url = '' } = {}) {
184
+ const htmlChars = String(html || '').length;
185
+ const cheerioLoad = await loadCheerio();
186
+
187
+ if (!cheerioLoad) {
188
+ const text = normalizeText(decodeEntities(
189
+ String(html || '')
190
+ .replace(/<script\b[\s\S]*?<\/script>/gi, ' ')
191
+ .replace(/<style\b[\s\S]*?<\/style>/gi, ' ')
192
+ .replace(/<[^>]+>/g, ' ')
193
+ ));
194
+ return buildExtractionResult({ markdown: text, meta: {}, headings: [], paragraphs: [], links: [], images: [], jsonLd: null, url, htmlChars });
195
+ }
196
+
197
+ const $ = cheerioLoad(html);
198
+ $('script:not([type="application/ld+json"]), style, noscript, iframe, svg, template, form, button').remove();
199
+
200
+ const meta = {};
201
+ meta.title = $('title').first().text().trim() || '';
202
+ meta.description = $('meta[name="description"]').attr('content') || $('meta[property="og:description"]').attr('content') || '';
203
+ meta.author = $('meta[name="author"]').attr('content') || $('meta[property="article:author"]').attr('content') || '';
204
+ meta.publishedAt = $('meta[itemprop="datePublished"]').attr('content') || $('meta[property="article:published_time"]').attr('content') || $('meta[name="date"]').attr('content') || '';
205
+ meta.keywords = $('meta[name="keywords"]').attr('content') || '';
206
+ meta.ogTitle = $('meta[property="og:title"]').attr('content') || '';
207
+ meta.ogImage = $('meta[property="og:image"]').attr('content') || '';
208
+ meta.ogType = $('meta[property="og:type"]').attr('content') || '';
209
+ meta.canonical = $('link[rel="canonical"]').attr('href') || '';
210
+ meta.favicon = $('link[rel="icon"], link[rel="shortcut icon"]').attr('href') || '';
211
+ meta.lang = $('html').attr('lang') || '';
212
+
213
+ const jsonLd = parseJsonLd($);
214
+ const container = pickMainContainer($);
215
+
216
+ const blocks = collectStructuredBlocks($, container);
217
+ let markdown = joinBlocks(blocks);
218
+ let paragraphs = blocks.filter(b => b.type === 'p' && b.text.length > 20).map(b => b.text).slice(0, 30);
219
+ let headings = blocks
220
+ .filter(b => b.type === 'heading')
221
+ .slice(0, 12)
222
+ .map(b => ({ level: b.level, text: b.text }));
223
+
224
+ if (paragraphs.length === 0) {
225
+ $('p').each((i, el) => {
226
+ if (paragraphs.length >= 15) return;
227
+ const txt = normalizeText(decodeEntities($(el).text()));
228
+ if (txt && txt.length > 30) paragraphs.push(txt);
229
+ });
230
+ if (markdown.length < 100 && paragraphs.length > 0) {
231
+ markdown = paragraphs.join('\n\n');
232
+ }
233
+ }
234
+ if (headings.length === 0) {
235
+ $('h1, h2, h3').each((i, el) => {
236
+ if (headings.length >= 10) return;
237
+ const txt = normalizeText($(el).text());
238
+ if (txt) headings.push({ level: el.tagName, text: txt });
239
+ });
240
+ }
241
+
242
+ const scope = container ? $(container) : $.root();
243
+ const links = [];
244
+ scope.find('a').each((i, el) => {
245
+ if (links.length >= 15) return;
246
+ const href = $(el).attr('href');
247
+ const text = normalizeText($(el).text());
248
+ if (href && text && text.length > 2) {
249
+ let absoluteUrl = href;
250
+ try { absoluteUrl = new URL(href, url || undefined).href; } catch {}
251
+ links.push({ text: text.slice(0, 80), url: absoluteUrl });
252
+ }
253
+ });
254
+
255
+ const images = [];
256
+ scope.find('img').each((i, el) => {
257
+ if (images.length >= 8) return;
258
+ let rawSrc = $(el).attr('data-src') || $(el).attr('src') || $(el).attr('data-original') || '';
259
+ const alt = $(el).attr('alt') || $(el).attr('title') || '';
260
+ if (rawSrc) {
261
+ if (rawSrc.startsWith('//')) rawSrc = `https:${rawSrc}`;
262
+ let absoluteUrl = rawSrc;
263
+ try { absoluteUrl = new URL(rawSrc, url || undefined).href; } catch {}
264
+ images.push({ src: absoluteUrl, alt: alt.slice(0, 100) });
265
+ }
266
+ });
267
+
268
+ return buildExtractionResult({ markdown, meta, headings, paragraphs, links, images, jsonLd, url, htmlChars });
269
+ }
270
+
271
+ function buildExtractionResult({ markdown, meta, headings, paragraphs, links, images, jsonLd, url, htmlChars }) {
272
+ const textChars = markdown.length;
273
+ const density = htmlChars > 0 ? Number((textChars / htmlChars).toFixed(4)) : 0;
274
+ const isThin = (textChars < 400 && htmlChars > 15_000) || textChars < 200;
275
+ return {
276
+ markdown,
277
+ meta: Object.fromEntries(Object.entries(meta || {}).filter(([, v]) => v)),
278
+ headings: headings || [],
279
+ paragraphs: paragraphs || [],
280
+ links: links || [],
281
+ images: images || [],
282
+ jsonLd: jsonLd || null,
283
+ quality: { textChars, htmlChars, density, isThin },
284
+ };
285
+ }
286
+
287
+ function pickMainContainer($) {
288
+ const scored = [];
289
+ $('article, main, [role="main"], div, section').each((i, el) => {
290
+ if (scored.length >= 150) return;
291
+ const $el = $(el);
292
+ const attr = `${$el.attr('class') || ''} ${$el.attr('id') || ''} ${$el.attr('role') || ''}`;
293
+ if (NOISE_HINT_RE.test(attr)) return;
294
+ const text = $el.text().replace(/\s+/g, ' ').trim();
295
+ if (text.length < 120) return;
296
+
297
+ const anchorTextLen = $el.find('a').text().replace(/\s+/g, ' ').trim().length;
298
+ const linkDensity = text.length ? anchorTextLen / text.length : 0;
299
+ const paragraphCount = $el.find('p').filter((_, p) => $(p).text().trim().length > 40).length;
300
+ const punctuationPer100 = (text.match(/[.!?。!?,、;:]/g) || []).length / Math.max(1, text.length / 100);
301
+
302
+ let score = Math.min(text.length, 6000) / 100;
303
+ score += Math.min(paragraphCount, 30) * 8;
304
+ score += Math.min(60, punctuationPer100 * 2);
305
+ score *= 1 - Math.min(1, linkDensity) * 0.9;
306
+ if (CONTENT_HINT_RE.test(attr)) score *= 1.35;
307
+ if (el.tagName === 'article' || el.tagName === 'main') score *= 1.5;
308
+ scored.push({ el, score, textLen: text.length });
309
+ });
310
+
311
+ scored.sort((a, b) => b.score - a.score);
312
+ if (scored.length > 0 && scored[0].textLen >= 200) {
313
+ return scored[0].el;
314
+ }
315
+ return $('article').first().get(0) || $('main').first().get(0) || $('body').first().get(0) || null;
316
+ }
317
+
318
+ const HANDLED_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'li', 'table', 'pre', 'blockquote']);
319
+
320
+ function collectStructuredBlocks($, container) {
321
+ const blocks = [];
322
+ if (!container) return blocks;
323
+ const $container = $(container);
324
+
325
+ const isInsideHandledTag = $el => {
326
+ const parents = $el.parents().toArray();
327
+ for (const ancestor of parents) {
328
+ if (ancestor === container) return false;
329
+ const tag = String(ancestor.tagName || '').toLowerCase();
330
+ if (HANDLED_TAGS.has(tag)) return true;
331
+ }
332
+ return true;
333
+ };
334
+
335
+ $container.find('h1,h2,h3,h4,h5,h6,p,li,table,pre,blockquote').each((_, el) => {
336
+ if (blocks.length >= MAX_BLOCKS) return;
337
+ const tag = String(el.tagName || '').toLowerCase();
338
+ if (!HANDLED_TAGS.has(tag)) return;
339
+ const $el = $(el);
340
+ if (isInsideHandledTag($el)) return;
341
+
342
+ if (tag === 'table') {
343
+ const rows = [];
344
+ $el.find('tr').each((__, tr) => {
345
+ if (rows.length >= 40) return;
346
+ const cells = [];
347
+ $(tr).find('th,td').each((___, cell) => {
348
+ if (cells.length >= 12) return;
349
+ const text = normalizeText(decodeEntities($(cell).text())).slice(0, 80);
350
+ cells.push((text || ' ').replace(/\|/g, '\\|'));
351
+ });
352
+ if (cells.length > 0) rows.push(`| ${cells.join(' | ')} |`);
353
+ });
354
+ if (rows.length >= 2) blocks.push({ type: 'table', text: rows.join('\n') });
355
+ return;
356
+ }
357
+
358
+ if (tag === 'pre') {
359
+ const raw = decodeEntities($el.text()).replace(/\s+$/, '').slice(0, 2000);
360
+ if (raw) blocks.push({ type: 'pre', text: `---\n${raw}\n---` });
361
+ return;
362
+ }
363
+
364
+ const text = normalizeText(decodeEntities($el.text()));
365
+ if (!text) return;
366
+
367
+ if (tag !== 'table' && tag !== 'pre') {
368
+ const anchorTextLen = $el.find('a').text().replace(/\s+/g, ' ').trim().length;
369
+ if (text.length < 120 && anchorTextLen > text.length * 0.5) return;
370
+ }
371
+
372
+ if (tag === 'li') {
373
+ blocks.push({ type: 'li', text: `- ${text}` });
374
+ } else if (tag === 'blockquote') {
375
+ blocks.push({ type: 'p', text: `> ${text}` });
376
+ } else if (tag.startsWith('h')) {
377
+ const level = Number(tag[1]) || 2;
378
+ const hashes = '#'.repeat(level <= 2 ? 2 : Math.min(level + 0, 4));
379
+ blocks.push({ type: 'heading', level: tag, text: `${hashes} ${text}` });
380
+ } else {
381
+ blocks.push({ type: 'p', text });
382
+ }
383
+ });
384
+
385
+ return blocks;
386
+ }
387
+
388
+ function joinBlocks(blocks) {
389
+ let markdown = '';
390
+ let prevType = null;
391
+ for (const block of blocks) {
392
+ const separator = !markdown ? '' : (block.type === 'li' && prevType === 'li' ? '\n' : '\n\n');
393
+ markdown += separator + block.text;
394
+ prevType = block.type;
395
+ if (markdown.length >= MAX_MARKDOWN_CHARS) break;
396
+ }
397
+ return markdown;
398
+ }
399
+
400
+ const _segmenters = new Map();
401
+
402
+ function getSegmenter(lang) {
403
+ if (typeof Intl === 'undefined' || !Intl.Segmenter) return null;
404
+ const key = lang || 'en';
405
+ if (!_segmenters.has(key)) {
406
+ try {
407
+ _segmenters.set(key, new Intl.Segmenter(key, { granularity: 'word' }));
408
+ } catch {
409
+ try {
410
+ _segmenters.set(key, new Intl.Segmenter('en', { granularity: 'word' }));
411
+ } catch {
412
+ _segmenters.set(key, null);
413
+ }
414
+ }
415
+ }
416
+ return _segmenters.get(key);
417
+ }
418
+
419
+ const THAI_RUN_RE = /[\u0E00-\u0E7F]+/g;
420
+ const CJK_RUN_RE = /[\u3040-\u30FF\u3400-\u9FFF\uAC00-\uD7AF]+/g;
421
+ const LATIN_WORD_RE = /[a-z0-9à-öø-ÿāáǎàēéěèīíǐìōóǒòūúǔùǖǘǚǜ]{2,}/g;
422
+
423
+ export function truncateByWords(text, maxChars = 8000, langHint = 'en') {
424
+ const str = String(text || '');
425
+ if (str.length <= maxChars) return str;
426
+ const slice = str.slice(0, maxChars);
427
+ const segmenter = getSegmenter(langHint);
428
+ if (!segmenter) return slice;
429
+ try {
430
+ let cut = -1;
431
+ const boundaryTarget = Math.floor(maxChars * 0.85);
432
+ for (const segment of segmenter.segment(slice)) {
433
+ if (segment.index > boundaryTarget) break;
434
+ cut = segment.index + segment.segment.length;
435
+ }
436
+ return cut > 0 ? slice.slice(0, cut) : slice;
437
+ } catch {
438
+ return slice;
439
+ }
440
+ }
441
+
442
+ export function tokenizeForMatch(text, langHint) {
443
+ const normalized = normalizeText(String(text || '')).toLowerCase();
444
+ if (!normalized) return [];
445
+
446
+ const thaiRuns = normalized.match(THAI_RUN_RE);
447
+ if (thaiRuns) {
448
+ const segmenter = getSegmenter('th');
449
+ if (segmenter) {
450
+ const words = [];
451
+ try {
452
+ for (const segment of segmenter.segment(normalized)) {
453
+ if (segment.isWordLike && segment.segment.trim()) words.push(segment.segment);
454
+ }
455
+ } catch {}
456
+ if (words.length > 0) return words;
457
+ }
458
+ return thaiRuns;
459
+ }
460
+
461
+ const cjkRuns = normalized.match(CJK_RUN_RE);
462
+ if (cjkRuns) {
463
+ const grams = [];
464
+ for (const run of cjkRuns) {
465
+ if (run.length === 1) {
466
+ grams.push(run);
467
+ continue;
468
+ }
469
+ for (let i = 0; i < run.length - 1; i++) grams.push(run.slice(i, i + 2));
470
+ }
471
+ const latin = normalized.match(/[a-z0-9]{2,}/g) || [];
472
+ return [...grams, ...latin];
473
+ }
474
+
475
+ return normalized.match(LATIN_WORD_RE) || [];
476
+ }
477
+
478
+ export function scorePassages(passages, query) {
479
+ const queryTokens = [...new Set(tokenizeForMatch(query || ''))];
480
+ return (Array.isArray(passages) ? passages : []).map(passage => {
481
+ if (!queryTokens.length) return { passage, score: 0 };
482
+ const tokens = tokenizeForMatch(passage.text || '');
483
+ if (!tokens.length) return { passage, score: 0 };
484
+ const tokenSet = new Set(tokens);
485
+ let covered = 0;
486
+ for (const token of queryTokens) {
487
+ if (tokenSet.has(token)) covered++;
488
+ }
489
+ const coverage = covered / queryTokens.length;
490
+ const density = covered / Math.max(8, Math.min(400, tokens.length));
491
+ const headingTokens = passage.heading ? new Set(tokenizeForMatch(passage.heading)) : null;
492
+ const headingBonus = headingTokens && queryTokens.some(t => headingTokens.has(t)) ? 0.15 : 0;
493
+ return { passage, score: Number((coverage * 0.7 + Math.min(1, density) * 0.2 + headingBonus).toFixed(4)) };
494
+ });
495
+ }
496
+
497
+ export function splitIntoPassages(markdown, { maxChars = 1600 } = {}) {
498
+ const limit = Math.max(200, maxChars);
499
+ const lines = String(markdown || '').split('\n');
500
+ const passages = [];
501
+ let heading = '';
502
+ let buffer = [];
503
+
504
+ const flush = () => {
505
+ const body = buffer.join('\n').trim();
506
+ if (body) {
507
+ passages.push({ heading, text: heading ? `${heading}\n${body}` : body });
508
+ }
509
+ buffer = [];
510
+ };
511
+
512
+ for (const line of lines) {
513
+ if (/^#{1,6}\s+\S/.test(line)) {
514
+ flush();
515
+ heading = line.trim();
516
+ continue;
517
+ }
518
+ buffer.push(line);
519
+ if (buffer.join('\n').length >= limit) flush();
520
+ }
521
+ flush();
522
+
523
+ if (passages.length === 0 && String(markdown || '').trim()) {
524
+ passages.push({ heading: '', text: String(markdown).trim() });
525
+ }
526
+ return passages.map((p, index) => ({ ...p, index }));
527
+ }
528
+
529
+ function fnv1a64(str) {
530
+ let hash = 0xcbf29ce484222325n;
531
+ for (let i = 0; i < str.length; i++) {
532
+ hash ^= BigInt(str.charCodeAt(i));
533
+ hash = (hash * 0x100000001b3n) & 0xffffffffffffffffn;
534
+ }
535
+ return hash;
536
+ }
537
+
538
+ export function simhashText(text) {
539
+ const tokens = tokenizeForMatch(text);
540
+ if (tokens.length < 3) {
541
+ return tokens.length ? fnv1a64(tokens.join(' ')) : 0n;
542
+ }
543
+ const bitWeights = new Array(64).fill(0);
544
+ for (let i = 0; i + 3 <= tokens.length; i++) {
545
+ const hash = fnv1a64(tokens.slice(i, i + 3).join(' '));
546
+ for (let bit = 0; bit < 64; bit++) {
547
+ bitWeights[bit] += (hash >> BigInt(bit)) & 1n ? 1 : -1;
548
+ }
549
+ }
550
+ let fingerprint = 0n;
551
+ for (let bit = 0; bit < 64; bit++) {
552
+ if (bitWeights[bit] > 0) fingerprint |= 1n << BigInt(bit);
553
+ }
554
+ return fingerprint;
555
+ }
556
+
557
+ export function hamming64(a, b) {
558
+ let xor = (a ^ b) & 0xffffffffffffffffn;
559
+ let count = 0;
560
+ while (xor) {
561
+ if (xor & 1n) count++;
562
+ xor >>= 1n;
563
+ }
564
+ return count;
565
+ }
566
+
567
+ export function filterUniquePassages(passages, state = { hashes: [] }, { maxHamming = 6 } = {}) {
568
+ const hashes = Array.isArray(state?.hashes) ? state.hashes : [];
569
+ return (Array.isArray(passages) ? passages : []).filter(passage => {
570
+ const fingerprint = simhashText(passage.text || '');
571
+ for (const kept of hashes) {
572
+ if (hamming64(kept, fingerprint) <= maxHamming) return false;
573
+ }
574
+ hashes.push(fingerprint);
575
+ return true;
576
+ });
577
+ }
578
+
579
+ export function dedupePassages(passages, { maxHamming = 6 } = {}) {
580
+ return filterUniquePassages(passages, { hashes: [] }, { maxHamming });
581
+ }
582
+
583
+ export function selectPassages(passages, query, { budgetChars = 8000, maxHamming = 6 } = {}) {
584
+ const total = Array.isArray(passages) ? passages.length : 0;
585
+ if (total === 0) return { text: '', selected: 0, total: 0 };
586
+ const budget = Number.isFinite(Number(budgetChars)) && Number(budgetChars) > 0 ? Number(budgetChars) : 8000;
587
+ const scored = scorePassages(dedupePassages(passages, { maxHamming }), query || '');
588
+ const ranked = [...scored].sort((a, b) => b.score - a.score);
589
+
590
+ const kept = [];
591
+ let used = 0;
592
+ for (const entry of ranked) {
593
+ const cost = entry.passage.text.length + 2;
594
+ if (kept.length > 0 && used + cost > budget) continue;
595
+ kept.push(entry);
596
+ used += cost;
597
+ if (used >= budget) break;
598
+ }
599
+ kept.sort((a, b) => a.passage.index - b.passage.index);
600
+ return {
601
+ text: kept.map(entry => entry.passage.text).join('\n\n'),
602
+ selected: kept.length,
603
+ total,
604
+ };
605
+ }
@@ -0,0 +1,43 @@
1
+ import { MetaSearchOrchestrator, defaultMetaSearch } from './orchestrator.js';
2
+ import { fuseSearchResults } from './core/fusion.js';
3
+ import { canonicalizeUrl } from './core/canonical.js';
4
+ import { createSearchResult } from './core/types.js';
5
+ import { searchDuckDuckGo } from './adapters/duckduckgo.js';
6
+ import { searchSearXNG } from './adapters/searxng.js';
7
+ import { searchJina } from './adapters/jina.js';
8
+ import { TokenBucketLimiter, defaultLimiters } from './resilience/limiter.js';
9
+ import { CircuitBreaker, defaultBreakers } from './resilience/breaker.js';
10
+ import { SearchResponseCache, defaultSearchCache } from './resilience/cache.js';
11
+ import { safeFetchWithPinning, assertSafeUrlHostname, isPrivateIPv4, isPrivateIPv6 } from './security/ssrf.js';
12
+ import { DomainFilter, defaultDomainFilter } from './security/filter.js';
13
+ import { SearchTelemetry, defaultTelemetry } from './telemetry.js';
14
+
15
+
16
+ export async function hybridWebSearch(query, options = {}) {
17
+ return defaultMetaSearch.executeSearch(query, options);
18
+ }
19
+
20
+ export {
21
+ MetaSearchOrchestrator,
22
+ defaultMetaSearch,
23
+ fuseSearchResults,
24
+ canonicalizeUrl,
25
+ createSearchResult,
26
+ searchDuckDuckGo,
27
+ searchSearXNG,
28
+ searchJina,
29
+ TokenBucketLimiter,
30
+ defaultLimiters,
31
+ CircuitBreaker,
32
+ defaultBreakers,
33
+ SearchResponseCache,
34
+ defaultSearchCache,
35
+ safeFetchWithPinning,
36
+ assertSafeUrlHostname,
37
+ isPrivateIPv4,
38
+ isPrivateIPv6,
39
+ DomainFilter,
40
+ defaultDomainFilter,
41
+ SearchTelemetry,
42
+ defaultTelemetry
43
+ };