webpeel 0.10.0 → 0.12.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 (43) hide show
  1. package/dist/cli.js +86 -3
  2. package/dist/cli.js.map +1 -1
  3. package/dist/core/bm25-filter.d.ts +57 -0
  4. package/dist/core/bm25-filter.d.ts.map +1 -0
  5. package/dist/core/bm25-filter.js +249 -0
  6. package/dist/core/bm25-filter.js.map +1 -0
  7. package/dist/core/chunking.d.ts +43 -0
  8. package/dist/core/chunking.d.ts.map +1 -0
  9. package/dist/core/chunking.js +182 -0
  10. package/dist/core/chunking.js.map +1 -0
  11. package/dist/core/content-pruner.d.ts +33 -0
  12. package/dist/core/content-pruner.d.ts.map +1 -0
  13. package/dist/core/content-pruner.js +249 -0
  14. package/dist/core/content-pruner.js.map +1 -0
  15. package/dist/core/fetcher.d.ts +8 -1
  16. package/dist/core/fetcher.d.ts.map +1 -1
  17. package/dist/core/fetcher.js +33 -7
  18. package/dist/core/fetcher.js.map +1 -1
  19. package/dist/core/hotel-search.d.ts +2 -0
  20. package/dist/core/hotel-search.d.ts.map +1 -1
  21. package/dist/core/hotel-search.js +2 -0
  22. package/dist/core/hotel-search.js.map +1 -1
  23. package/dist/core/llm-extract.d.ts +15 -1
  24. package/dist/core/llm-extract.d.ts.map +1 -1
  25. package/dist/core/llm-extract.js +127 -7
  26. package/dist/core/llm-extract.js.map +1 -1
  27. package/dist/core/markdown.d.ts +4 -1
  28. package/dist/core/markdown.d.ts.map +1 -1
  29. package/dist/core/markdown.js +11 -2
  30. package/dist/core/markdown.js.map +1 -1
  31. package/dist/core/strategies.d.ts +6 -0
  32. package/dist/core/strategies.d.ts.map +1 -1
  33. package/dist/core/strategies.js +10 -5
  34. package/dist/core/strategies.js.map +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +17 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp/server.js +47 -3
  39. package/dist/mcp/server.js.map +1 -1
  40. package/dist/types.d.ts +19 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js.map +1 -1
  43. package/package.json +1 -1
@@ -0,0 +1,57 @@
1
+ /**
2
+ * BM25 Query-Focused Content Filter
3
+ *
4
+ * Filters markdown content by BM25 relevance to a query, keeping only the
5
+ * blocks that are most relevant. This can reduce token usage by 70-90% for
6
+ * focused tasks (e.g., "find hotel prices" should not return navigation menus,
7
+ * footer text, or unrelated article sections).
8
+ *
9
+ * Algorithm: BM25 (Best Matching 25) — Okapi BM25
10
+ * score(D, Q) = Σ IDF(qi) * tf(qi,D)*(k1+1) / (tf(qi,D) + k1*(1 - b + b*|D|/avgdl))
11
+ */
12
+ export interface BM25FilterOptions {
13
+ /** Query to rank content against */
14
+ query: string;
15
+ /** BM25 threshold score. Blocks below this are removed. Default: auto-calculated */
16
+ threshold?: number;
17
+ /** Whether to return scores in output. Default: false */
18
+ includeScores?: boolean;
19
+ }
20
+ export interface BM25FilterResult {
21
+ /** Filtered content (relevant paragraphs only) */
22
+ content: string;
23
+ /** Number of blocks kept */
24
+ kept: number;
25
+ /** Total number of blocks */
26
+ total: number;
27
+ /** Percentage of content removed */
28
+ reductionPercent: number;
29
+ }
30
+ interface ContentBlock {
31
+ /** Original markdown text (preserved verbatim in output) */
32
+ raw: string;
33
+ /** Index in the original block list (for order preservation) */
34
+ index: number;
35
+ }
36
+ /**
37
+ * Split markdown content into logical blocks for scoring:
38
+ * - Code fences (``` ... ```) → single block
39
+ * - Heading + immediately following paragraph → single block
40
+ * - Lists (contiguous lines starting with - / * / + / number.) → single block
41
+ * - Tables → single block
42
+ * - Paragraphs → one block each
43
+ */
44
+ export declare function splitIntoBlocks(content: string): ContentBlock[];
45
+ /**
46
+ * Calculate BM25 scores for all blocks against a query.
47
+ * Returns array of scores in same order as blocks.
48
+ */
49
+ export declare function scoreBM25(blocks: ContentBlock[], queryTerms: string[]): number[];
50
+ /**
51
+ * Filter markdown content by BM25 relevance to a query.
52
+ * Splits content into blocks (paragraphs, headings+body, list items),
53
+ * scores each by BM25, and returns only blocks above threshold.
54
+ */
55
+ export declare function filterByRelevance(content: string, options: BM25FilterOptions): BM25FilterResult;
56
+ export {};
57
+ //# sourceMappingURL=bm25-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bm25-filter.d.ts","sourceRoot":"","sources":["../../src/core/bm25-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,iBAAiB;IAChC,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAiDD,UAAU,YAAY;IACpB,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,CA2E/D;AAMD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CA8ChF;AAMD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAqF/F"}
@@ -0,0 +1,249 @@
1
+ /**
2
+ * BM25 Query-Focused Content Filter
3
+ *
4
+ * Filters markdown content by BM25 relevance to a query, keeping only the
5
+ * blocks that are most relevant. This can reduce token usage by 70-90% for
6
+ * focused tasks (e.g., "find hotel prices" should not return navigation menus,
7
+ * footer text, or unrelated article sections).
8
+ *
9
+ * Algorithm: BM25 (Best Matching 25) — Okapi BM25
10
+ * score(D, Q) = Σ IDF(qi) * tf(qi,D)*(k1+1) / (tf(qi,D) + k1*(1 - b + b*|D|/avgdl))
11
+ */
12
+ // BM25 tuning parameters
13
+ const K1 = 1.5; // term frequency saturation
14
+ const B = 0.75; // length normalization
15
+ // ---------------------------------------------------------------------------
16
+ // Tokenization
17
+ // ---------------------------------------------------------------------------
18
+ /**
19
+ * Tokenize text into lowercase terms, stripping punctuation.
20
+ * Markdown formatting characters are also stripped.
21
+ */
22
+ function tokenize(text) {
23
+ return text
24
+ .toLowerCase()
25
+ // Strip markdown formatting (bold, italic, code, links, images, headings)
26
+ .replace(/!\[.*?\]\(.*?\)/g, ' ') // images
27
+ .replace(/\[.*?\]\(.*?\)/g, ' ') // links
28
+ .replace(/`{1,3}[^`]*`{1,3}/g, ' ') // inline code
29
+ .replace(/[#*_~`>|\\]/g, ' ') // formatting chars
30
+ .replace(/[^\w\s]/g, ' ') // remaining punctuation
31
+ .split(/\s+/)
32
+ .filter(t => t.length > 0);
33
+ }
34
+ /**
35
+ * Strip markdown formatting from text for scoring purposes.
36
+ * Preserves words but removes symbols.
37
+ */
38
+ function stripMarkdown(text) {
39
+ return text
40
+ .replace(/```[\s\S]*?```/g, ' ') // fenced code blocks
41
+ .replace(/`[^`]+`/g, ' ') // inline code
42
+ .replace(/!\[.*?\]\(.*?\)/g, ' ') // images
43
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // links → text
44
+ .replace(/^#{1,6}\s+/gm, '') // headings
45
+ .replace(/[*_~`>|\\]/g, ' ') // formatting
46
+ .replace(/^\s*[-*+]\s+/gm, ' ') // list bullets
47
+ .replace(/^\s*\d+\.\s+/gm, ' ') // numbered list
48
+ .replace(/\s+/g, ' ')
49
+ .trim();
50
+ }
51
+ /**
52
+ * Split markdown content into logical blocks for scoring:
53
+ * - Code fences (``` ... ```) → single block
54
+ * - Heading + immediately following paragraph → single block
55
+ * - Lists (contiguous lines starting with - / * / + / number.) → single block
56
+ * - Tables → single block
57
+ * - Paragraphs → one block each
58
+ */
59
+ export function splitIntoBlocks(content) {
60
+ // Normalise line endings
61
+ const text = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
62
+ // First, extract fenced code blocks so they aren't broken apart
63
+ // We'll replace them with placeholders, split, then restore.
64
+ const codeBlocks = [];
65
+ const withPlaceholders = text.replace(/```[\s\S]*?```/g, (match) => {
66
+ const id = codeBlocks.length;
67
+ codeBlocks.push(match);
68
+ return `\x00CODE_BLOCK_${id}\x00`;
69
+ });
70
+ // Split on double newlines
71
+ const rawChunks = withPlaceholders.split(/\n{2,}/);
72
+ // Re-join heading with its following paragraph
73
+ const merged = [];
74
+ for (let i = 0; i < rawChunks.length; i++) {
75
+ const chunk = rawChunks[i].trim();
76
+ if (!chunk)
77
+ continue;
78
+ const isHeading = /^#{1,6}\s/.test(chunk);
79
+ const nextChunk = rawChunks[i + 1]?.trim();
80
+ if (isHeading && nextChunk && !/^#{1,6}\s/.test(nextChunk)) {
81
+ // Merge heading + following paragraph
82
+ merged.push(chunk + '\n\n' + nextChunk);
83
+ i++; // skip next
84
+ }
85
+ else {
86
+ merged.push(chunk);
87
+ }
88
+ }
89
+ // Now merge contiguous list lines that got split
90
+ const regrouped = [];
91
+ for (const chunk of merged) {
92
+ const lines = chunk.split('\n');
93
+ const isListBlock = lines.every(l => l.trim() === '' || /^\s*[-*+]\s/.test(l) || /^\s*\d+\.\s/.test(l) || /^\s*\d+\)\s/.test(l)) && lines.some(l => /^\s*[-*+]\s/.test(l) || /^\s*\d+[.)]\s/.test(l));
94
+ const isTableBlock = lines.some(l => /^\|/.test(l.trim()));
95
+ if (isListBlock || isTableBlock) {
96
+ // Check if previous block was the same type (adjacent lists should merge)
97
+ const prev = regrouped[regrouped.length - 1];
98
+ if (prev) {
99
+ const prevLines = prev.split('\n');
100
+ const prevIsListOrTable = prevLines.some(l => /^\s*[-*+]\s/.test(l) || /^\s*\d+[.)]\s/.test(l) || /^\|/.test(l.trim()));
101
+ if (prevIsListOrTable && isListBlock === prevIsListOrTable) {
102
+ regrouped[regrouped.length - 1] = prev + '\n' + chunk;
103
+ continue;
104
+ }
105
+ }
106
+ }
107
+ regrouped.push(chunk);
108
+ }
109
+ // Restore code blocks and build final ContentBlock array
110
+ const blocks = [];
111
+ for (let i = 0; i < regrouped.length; i++) {
112
+ let raw = regrouped[i];
113
+ // Restore code block placeholders
114
+ raw = raw.replace(/\x00CODE_BLOCK_(\d+)\x00/g, (_m, idx) => codeBlocks[Number(idx)]);
115
+ if (raw.trim()) {
116
+ blocks.push({ raw: raw.trim(), index: i });
117
+ }
118
+ }
119
+ return blocks;
120
+ }
121
+ // ---------------------------------------------------------------------------
122
+ // BM25 Scoring
123
+ // ---------------------------------------------------------------------------
124
+ /**
125
+ * Calculate BM25 scores for all blocks against a query.
126
+ * Returns array of scores in same order as blocks.
127
+ */
128
+ export function scoreBM25(blocks, queryTerms) {
129
+ if (blocks.length === 0 || queryTerms.length === 0) {
130
+ return blocks.map(() => 0);
131
+ }
132
+ const N = blocks.length;
133
+ // Tokenize each block (strip markdown for scoring)
134
+ const blockTokens = blocks.map(b => tokenize(stripMarkdown(b.raw)));
135
+ const blockLengths = blockTokens.map(t => t.length);
136
+ const avgdl = blockLengths.reduce((s, l) => s + l, 0) / N || 1;
137
+ // Build term frequency maps for each block
138
+ const tfMaps = blockTokens.map(tokens => {
139
+ const tf = new Map();
140
+ for (const t of tokens) {
141
+ tf.set(t, (tf.get(t) ?? 0) + 1);
142
+ }
143
+ return tf;
144
+ });
145
+ // For each query term, compute IDF and score contribution
146
+ const scores = new Array(N).fill(0);
147
+ for (const term of queryTerms) {
148
+ // n(qi) = number of documents containing the term
149
+ let nqi = 0;
150
+ for (const tf of tfMaps) {
151
+ if (tf.has(term))
152
+ nqi++;
153
+ }
154
+ // IDF(qi) = log((N - n(qi) + 0.5) / (n(qi) + 0.5) + 1)
155
+ const idf = Math.log((N - nqi + 0.5) / (nqi + 0.5) + 1);
156
+ for (let d = 0; d < N; d++) {
157
+ const tf = tfMaps[d].get(term) ?? 0;
158
+ if (tf === 0)
159
+ continue;
160
+ const dl = blockLengths[d];
161
+ // BM25 term score
162
+ const termScore = idf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * dl / avgdl));
163
+ scores[d] += termScore;
164
+ }
165
+ }
166
+ return scores;
167
+ }
168
+ // ---------------------------------------------------------------------------
169
+ // Main filter function
170
+ // ---------------------------------------------------------------------------
171
+ /**
172
+ * Filter markdown content by BM25 relevance to a query.
173
+ * Splits content into blocks (paragraphs, headings+body, list items),
174
+ * scores each by BM25, and returns only blocks above threshold.
175
+ */
176
+ export function filterByRelevance(content, options) {
177
+ const { query, threshold, includeScores = false } = options;
178
+ // Empty query → return full content
179
+ if (!query || !query.trim()) {
180
+ return {
181
+ content,
182
+ kept: 0,
183
+ total: 0,
184
+ reductionPercent: 0,
185
+ };
186
+ }
187
+ const blocks = splitIntoBlocks(content);
188
+ const total = blocks.length;
189
+ if (total === 0) {
190
+ return { content, kept: 0, total: 0, reductionPercent: 0 };
191
+ }
192
+ const queryTerms = tokenize(query);
193
+ if (queryTerms.length === 0) {
194
+ return { content, kept: total, total, reductionPercent: 0 };
195
+ }
196
+ const scores = scoreBM25(blocks, queryTerms);
197
+ // Determine threshold
198
+ let effectiveThreshold;
199
+ if (threshold !== undefined) {
200
+ effectiveThreshold = threshold;
201
+ }
202
+ else {
203
+ const meanScore = scores.reduce((s, v) => s + v, 0) / scores.length;
204
+ effectiveThreshold = meanScore * 0.5;
205
+ }
206
+ // Select blocks above threshold
207
+ let keptIndices = scores
208
+ .map((score, i) => ({ score, i }))
209
+ .filter(({ score }) => score >= effectiveThreshold)
210
+ .map(({ i }) => i);
211
+ // Fallback: never return empty — keep top 3
212
+ if (keptIndices.length === 0) {
213
+ keptIndices = scores
214
+ .map((score, i) => ({ score, i }))
215
+ .sort((a, b) => b.score - a.score)
216
+ .slice(0, 3)
217
+ .map(({ i }) => i)
218
+ .sort((a, b) => a - b); // restore document order
219
+ }
220
+ // Preserve original document order
221
+ keptIndices.sort((a, b) => a - b);
222
+ const keptBlocks = keptIndices.map(i => blocks[i]);
223
+ const kept = keptBlocks.length;
224
+ // Build output content
225
+ let outputParts;
226
+ if (includeScores) {
227
+ outputParts = keptBlocks.map(b => {
228
+ const score = scores[b.index];
229
+ return `<!-- BM25: ${score.toFixed(4)} -->\n${b.raw}`;
230
+ });
231
+ }
232
+ else {
233
+ outputParts = keptBlocks.map(b => b.raw);
234
+ }
235
+ const filteredContent = outputParts.join('\n\n');
236
+ // Calculate reduction percent based on character count
237
+ const originalLen = content.length;
238
+ const filteredLen = filteredContent.length;
239
+ const reductionPercent = originalLen > 0
240
+ ? Math.round(((originalLen - filteredLen) / originalLen) * 100)
241
+ : 0;
242
+ return {
243
+ content: filteredContent,
244
+ kept,
245
+ total,
246
+ reductionPercent,
247
+ };
248
+ }
249
+ //# sourceMappingURL=bm25-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bm25-filter.js","sourceRoot":"","sources":["../../src/core/bm25-filter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Smart content chunking for LLM processing.
3
+ *
4
+ * Splits content into manageable pieces with configurable overlap and strategy.
5
+ * Zero external dependencies; target <1ms for typical page content.
6
+ */
7
+ export interface ChunkOptions {
8
+ /** Target tokens per chunk. Default: 4000 */
9
+ chunkSize?: number;
10
+ /** Overlap tokens between chunks. Default: 200 */
11
+ overlap?: number;
12
+ /** Chunking strategy. Default: 'semantic' */
13
+ strategy?: 'fixed' | 'semantic' | 'paragraph';
14
+ }
15
+ export interface Chunk {
16
+ /** Chunk index (0-based) */
17
+ index: number;
18
+ /** Chunk content */
19
+ content: string;
20
+ /** Estimated tokens in this chunk */
21
+ tokens: number;
22
+ /** Character offset in original content */
23
+ startOffset: number;
24
+ /** Whether this is the last chunk */
25
+ isLast: boolean;
26
+ }
27
+ export interface ChunkResult {
28
+ /** Array of content chunks */
29
+ chunks: Chunk[];
30
+ /** Total chunks */
31
+ totalChunks: number;
32
+ /** Total tokens across all chunks */
33
+ totalTokens: number;
34
+ /** Original content tokens */
35
+ originalTokens: number;
36
+ }
37
+ /** Estimate token count using chars/4 heuristic. Accurate within ±10%. */
38
+ export declare function estimateTokens(text: string): number;
39
+ /**
40
+ * Split content into chunks suitable for LLM processing.
41
+ */
42
+ export declare function chunkContent(content: string, options?: ChunkOptions): ChunkResult;
43
+ //# sourceMappingURL=chunking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunking.d.ts","sourceRoot":"","sources":["../../src/core/chunking.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,YAAY;IAC3B,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,WAAW,CAAC;CAC/C;AAED,MAAM,WAAW,KAAK;IACpB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,8BAA8B;IAC9B,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,mBAAmB;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,WAAW,CA0CjF"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Smart content chunking for LLM processing.
3
+ *
4
+ * Splits content into manageable pieces with configurable overlap and strategy.
5
+ * Zero external dependencies; target <1ms for typical page content.
6
+ */
7
+ /** Estimate token count using chars/4 heuristic. Accurate within ±10%. */
8
+ export function estimateTokens(text) {
9
+ return Math.ceil(text.length / 4);
10
+ }
11
+ /**
12
+ * Split content into chunks suitable for LLM processing.
13
+ */
14
+ export function chunkContent(content, options) {
15
+ const chunkSize = options?.chunkSize ?? 4000;
16
+ const overlap = options?.overlap ?? 200;
17
+ const strategy = options?.strategy ?? 'semantic';
18
+ const originalTokens = estimateTokens(content);
19
+ if (content.length === 0) {
20
+ return { chunks: [], totalChunks: 0, totalTokens: 0, originalTokens: 0 };
21
+ }
22
+ let rawChunks;
23
+ switch (strategy) {
24
+ case 'fixed':
25
+ rawChunks = chunkFixed(content, chunkSize, overlap);
26
+ break;
27
+ case 'paragraph':
28
+ rawChunks = chunkParagraph(content, chunkSize);
29
+ break;
30
+ case 'semantic':
31
+ default:
32
+ rawChunks = chunkSemantic(content, chunkSize, overlap);
33
+ break;
34
+ }
35
+ const chunks = rawChunks.map((raw, i) => ({
36
+ index: i,
37
+ content: raw.content,
38
+ tokens: estimateTokens(raw.content),
39
+ startOffset: raw.startOffset,
40
+ isLast: i === rawChunks.length - 1,
41
+ }));
42
+ const totalTokens = chunks.reduce((sum, c) => sum + c.tokens, 0);
43
+ return {
44
+ chunks,
45
+ totalChunks: chunks.length,
46
+ totalTokens,
47
+ originalTokens,
48
+ };
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Fixed strategy
52
+ // ---------------------------------------------------------------------------
53
+ function chunkFixed(content, chunkSize, overlap) {
54
+ const chunkChars = chunkSize * 4;
55
+ const overlapChars = overlap * 4;
56
+ const result = [];
57
+ let start = 0;
58
+ while (start < content.length) {
59
+ const end = Math.min(start + chunkChars, content.length);
60
+ result.push({ content: content.slice(start, end), startOffset: start });
61
+ if (end >= content.length)
62
+ break;
63
+ start = end - overlapChars;
64
+ }
65
+ return result;
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // Semantic strategy
69
+ // ---------------------------------------------------------------------------
70
+ function findNaturalBreak(content, target, tolerance) {
71
+ const min = target - tolerance;
72
+ const max = Math.min(target + tolerance, content.length);
73
+ // 1. Heading break: newline immediately before ##
74
+ const headingRe = /\n(?=#{1,6} )/g;
75
+ let best = null;
76
+ headingRe.lastIndex = min;
77
+ let m = headingRe.exec(content);
78
+ while (m && m.index <= max) {
79
+ const dist = Math.abs(m.index - target);
80
+ if (!best || best.priority < 4 || dist < Math.abs(best.pos - target)) {
81
+ best = { pos: m.index, priority: 4 };
82
+ }
83
+ m = headingRe.exec(content);
84
+ }
85
+ // 2. Paragraph break: \n\n
86
+ const paraRe = /\n\n/g;
87
+ paraRe.lastIndex = min;
88
+ m = paraRe.exec(content);
89
+ while (m && m.index <= max) {
90
+ const dist = Math.abs(m.index - target);
91
+ if (!best || best.priority < 3 || (best.priority === 3 && dist < Math.abs(best.pos - target))) {
92
+ best = { pos: m.index + 2, priority: 3 };
93
+ }
94
+ m = paraRe.exec(content);
95
+ }
96
+ // 3. Sentence end: '. ', '! ', '? ' followed by capital or newline
97
+ const sentRe = /[.!?](?:\s+(?=[A-Z\n])|(?=\n))/g;
98
+ sentRe.lastIndex = min;
99
+ m = sentRe.exec(content);
100
+ while (m && m.index <= max) {
101
+ const pos = m.index + m[0].length;
102
+ const dist = Math.abs(pos - target);
103
+ if (!best || best.priority < 2 || (best.priority === 2 && dist < Math.abs(best.pos - target))) {
104
+ best = { pos, priority: 2 };
105
+ }
106
+ m = sentRe.exec(content);
107
+ }
108
+ // 4. Word boundary (space)
109
+ if (!best || best.priority < 1) {
110
+ const spaceRe = / /g;
111
+ spaceRe.lastIndex = min;
112
+ m = spaceRe.exec(content);
113
+ while (m && m.index <= max) {
114
+ const pos = m.index + 1;
115
+ const dist = Math.abs(pos - target);
116
+ if (!best || (best.priority < 2 && dist < Math.abs(best.pos - target))) {
117
+ best = { pos, priority: 1 };
118
+ }
119
+ m = spaceRe.exec(content);
120
+ }
121
+ }
122
+ return best ? best.pos : Math.min(target, content.length);
123
+ }
124
+ function chunkSemantic(content, chunkSize, overlap) {
125
+ const chunkChars = chunkSize * 4;
126
+ const overlapChars = overlap * 4;
127
+ const tolerance = Math.floor(chunkChars * 0.2);
128
+ const result = [];
129
+ let start = 0;
130
+ while (start < content.length) {
131
+ const remaining = content.length - start;
132
+ // If the rest fits, take it all
133
+ if (remaining <= chunkChars + tolerance) {
134
+ result.push({ content: content.slice(start), startOffset: start });
135
+ break;
136
+ }
137
+ const breakPos = findNaturalBreak(content, start + chunkChars, tolerance);
138
+ const end = Math.max(breakPos, start + 1); // always advance
139
+ result.push({ content: content.slice(start, end), startOffset: start });
140
+ // Next chunk starts with overlap from end of this chunk
141
+ const nextStart = Math.max(start + 1, end - overlapChars);
142
+ start = nextStart;
143
+ }
144
+ return result;
145
+ }
146
+ // ---------------------------------------------------------------------------
147
+ // Paragraph strategy
148
+ // ---------------------------------------------------------------------------
149
+ function chunkParagraph(content, chunkSize) {
150
+ const chunkChars = chunkSize * 4;
151
+ const paragraphs = content.split(/\n\n/);
152
+ const result = [];
153
+ let currentParts = [];
154
+ let currentLen = 0;
155
+ let currentOffset = 0;
156
+ let offsetTracker = 0;
157
+ for (let i = 0; i < paragraphs.length; i++) {
158
+ const para = paragraphs[i];
159
+ // +2 for the \n\n separator (except for first paragraph)
160
+ const paraLen = para.length + (currentParts.length > 0 ? 2 : 0);
161
+ if (currentParts.length > 0 && currentLen + paraLen > chunkChars) {
162
+ // Flush current group
163
+ result.push({ content: currentParts.join('\n\n'), startOffset: currentOffset });
164
+ currentOffset = offsetTracker;
165
+ currentParts = [para];
166
+ currentLen = para.length;
167
+ }
168
+ else {
169
+ if (currentParts.length === 0) {
170
+ currentOffset = offsetTracker;
171
+ }
172
+ currentParts.push(para);
173
+ currentLen += paraLen;
174
+ }
175
+ offsetTracker += para.length + 2; // account for \n\n
176
+ }
177
+ if (currentParts.length > 0) {
178
+ result.push({ content: currentParts.join('\n\n'), startOffset: currentOffset });
179
+ }
180
+ return result;
181
+ }
182
+ //# sourceMappingURL=chunking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunking.js","sourceRoot":"","sources":["../../src/core/chunking.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Content Density Pruner
3
+ *
4
+ * Scores HTML block elements by text quality and removes low-value blocks
5
+ * (sidebars, footers, navigation, ads) that CSS selectors miss.
6
+ *
7
+ * Inspired by Crawl4AI's fit_markdown approach — typical 40-60% token savings.
8
+ */
9
+ export interface PruneOptions {
10
+ /** Score threshold (0-1). Blocks below this are removed. Default: 0.4 */
11
+ threshold?: number;
12
+ /** Minimum word count for a block to be considered. Default: 3 */
13
+ minWords?: number;
14
+ /** Whether threshold adapts to content distribution. Default: true */
15
+ dynamic?: boolean;
16
+ }
17
+ export interface PruneResult {
18
+ /** Pruned HTML */
19
+ html: string;
20
+ /** Number of nodes removed */
21
+ nodesRemoved: number;
22
+ /** Percentage of content removed (by character count) */
23
+ reductionPercent: number;
24
+ }
25
+ /**
26
+ * Prune low-value HTML blocks using content density scoring.
27
+ *
28
+ * @param html - Raw HTML to prune
29
+ * @param options - Pruning configuration
30
+ * @returns Pruned HTML with stats
31
+ */
32
+ export declare function pruneContent(html: string, options?: PruneOptions): PruneResult;
33
+ //# sourceMappingURL=content-pruner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-pruner.d.ts","sourceRoot":"","sources":["../../src/core/content-pruner.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,MAAM,WAAW,YAAY;IAC3B,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AA2LD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,WAAW,CAoGlF"}