md-semantic-search 0.3.0 → 0.5.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 (59) hide show
  1. package/README.md +438 -20
  2. package/RESEARCH.md +117 -37
  3. package/bin/cli.mjs +931 -36
  4. package/dist/collapse.d.ts +6 -0
  5. package/dist/collapse.js +20 -0
  6. package/dist/core.d.ts +84 -0
  7. package/dist/core.js +479 -0
  8. package/dist/frontmatter.d.ts +20 -0
  9. package/dist/frontmatter.js +150 -0
  10. package/dist/index-format.d.ts +23 -0
  11. package/dist/index-format.js +125 -0
  12. package/dist/index.d.ts +29 -0
  13. package/dist/index.js +29 -0
  14. package/dist/indexer.d.ts +70 -0
  15. package/dist/indexer.js +452 -0
  16. package/dist/ivf.d.ts +33 -0
  17. package/dist/ivf.js +144 -0
  18. package/dist/lexical.d.ts +44 -0
  19. package/dist/lexical.js +226 -0
  20. package/dist/markdown-parser.d.ts +27 -0
  21. package/dist/markdown-parser.js +252 -0
  22. package/dist/mcp.d.ts +111 -0
  23. package/dist/mcp.js +216 -0
  24. package/dist/metrics.d.ts +43 -0
  25. package/dist/metrics.js +97 -0
  26. package/dist/models.d.ts +38 -0
  27. package/dist/models.js +181 -0
  28. package/dist/rerank.d.ts +10 -0
  29. package/dist/rerank.js +41 -0
  30. package/dist/search.d.ts +135 -0
  31. package/dist/search.js +492 -0
  32. package/dist/serve.d.ts +49 -0
  33. package/dist/serve.js +321 -0
  34. package/dist/tui.d.ts +17 -0
  35. package/dist/tui.js +196 -0
  36. package/dist/wikilinks.d.ts +47 -0
  37. package/dist/wikilinks.js +171 -0
  38. package/package.json +25 -2
  39. package/src/collapse.ts +26 -0
  40. package/src/core.ts +546 -0
  41. package/src/frontmatter.ts +190 -0
  42. package/src/index-format.ts +151 -0
  43. package/src/index.ts +55 -0
  44. package/src/indexer.ts +585 -0
  45. package/src/ivf.ts +190 -0
  46. package/src/lexical.ts +257 -0
  47. package/src/markdown-parser.ts +303 -0
  48. package/src/mcp.ts +243 -0
  49. package/src/metrics.ts +135 -0
  50. package/src/models.ts +219 -0
  51. package/src/rerank.ts +54 -0
  52. package/src/search.ts +628 -0
  53. package/src/serve.ts +372 -0
  54. package/src/tui.ts +227 -0
  55. package/src/wikilinks.ts +244 -0
  56. package/src/core.mjs +0 -171
  57. package/src/indexer.mjs +0 -197
  58. package/src/models.mjs +0 -67
  59. package/src/search.mjs +0 -107
@@ -0,0 +1,226 @@
1
+ import crypto from 'node:crypto';
2
+ const STOP = new Set([
3
+ 'the', 'and', 'for', 'are', 'was', 'has', 'with', 'this', 'that', 'from',
4
+ 'not', 'but', 'you', 'your', 'can', 'all', 'any', 'its',
5
+ 'все', 'как', 'что', 'это', 'при', 'для', 'или', 'был', 'без', 'над',
6
+ 'под', 'так', 'его', 'нет', 'есть',
7
+ 'of', 'to', 'in', 'on', 'it', 'is', 'at', 'by', 'be', 'we', 'us', 'he',
8
+ 'as', 'or', 'an', 'do', 'so', 'no', 'if', 'up', 'my', 'me', 'am',
9
+ 'по', 'от', 'из', 'на', 'за', 'во', 'со', 'не', 'ни', 'же', 'ли', 'бы',
10
+ 'уж', 'мы', 'вы', 'он', 'то', 'но', 'до', 'ко',
11
+ ]);
12
+ export const _lexicalStats = { documentsAnalyzed: 0 };
13
+ function normalize(text) {
14
+ return (text ?? '').replace(/\r\n?/g, '\n').trim();
15
+ }
16
+ function equivalent(left, right) {
17
+ return left.toLowerCase() === right.toLowerCase();
18
+ }
19
+ export function lexicalDocument(chunk) {
20
+ const title = normalize(chunk.title);
21
+ const heading = normalize(chunk.heading);
22
+ const ancestors = (chunk.headingPath ?? (heading ? [chunk.heading] : [])).map(normalize);
23
+ if (ancestors.length > 0 && equivalent(ancestors[0], title))
24
+ ancestors.shift();
25
+ if (ancestors.length > 0 && equivalent(ancestors[ancestors.length - 1], heading))
26
+ ancestors.pop();
27
+ return [title, ...ancestors, heading, normalize(chunk.text)].join('\n');
28
+ }
29
+ export function lexicalIdentity(chunk) {
30
+ return crypto.createHash('sha256').update(lexicalDocument(chunk)).digest('hex');
31
+ }
32
+ export function tokenize(text) {
33
+ const matches = text.toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}#+-]*/gu);
34
+ if (!matches)
35
+ return [];
36
+ return matches.filter((term) => term.length > 1 && !STOP.has(term));
37
+ }
38
+ export const DEFAULT_FIELD_WEIGHTS = {
39
+ title: 3.0,
40
+ aliases: 3.0,
41
+ headingPath: 1.8,
42
+ body: 1.0,
43
+ };
44
+ export function analyzeLexicalDocument(chunk, weights = DEFAULT_FIELD_WEIGHTS) {
45
+ _lexicalStats.documentsAnalyzed++;
46
+ const frequencies = Object.create(null);
47
+ const titleText = chunk.title || '';
48
+ const aliasesText = (chunk.meta?.aliases || []).join(' ');
49
+ const headingText = (chunk.headingPath || [chunk.heading]).join(' ');
50
+ const bodyText = chunk.text || '';
51
+ const addTokens = (text, weight) => {
52
+ for (const term of tokenize(text)) {
53
+ frequencies[term] = (frequencies[term] ?? 0) + weight;
54
+ }
55
+ };
56
+ addTokens(titleText, weights.title);
57
+ if (aliasesText)
58
+ addTokens(aliasesText, weights.aliases);
59
+ if (headingText)
60
+ addTokens(headingText, weights.headingPath);
61
+ addTokens(bodyText, weights.body);
62
+ return frequencies;
63
+ }
64
+ export function buildLexicalIndex(records) {
65
+ const postings = Object.create(null);
66
+ const documentLengths = records.map((record, docId) => {
67
+ let length = 0;
68
+ for (const [term, tf] of Object.entries(record)) {
69
+ length += tf;
70
+ const posting = postings[term] ?? [];
71
+ posting.push([docId, tf]);
72
+ postings[term] = posting;
73
+ }
74
+ return Number(length.toFixed(3));
75
+ });
76
+ return { format: 'bm25-v2', documentLengths, postings };
77
+ }
78
+ export function editDistance(a, b) {
79
+ if (a === b)
80
+ return 0;
81
+ const alen = a.length;
82
+ const blen = b.length;
83
+ if (alen === 0)
84
+ return blen;
85
+ if (blen === 0)
86
+ return alen;
87
+ const matrix = Array.from({ length: alen + 1 }, () => new Array(blen + 1).fill(0));
88
+ for (let i = 0; i <= alen; i++)
89
+ matrix[i][0] = i;
90
+ for (let j = 0; j <= blen; j++)
91
+ matrix[0][j] = j;
92
+ for (let i = 1; i <= alen; i++) {
93
+ for (let j = 1; j <= blen; j++) {
94
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
95
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
96
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
97
+ matrix[i][j] = Math.min(matrix[i][j], matrix[i - 2][j - 2] + cost);
98
+ }
99
+ }
100
+ }
101
+ return matrix[alen][blen];
102
+ }
103
+ export function fuzzyTitleAliasScores(chunks, query) {
104
+ const scores = new Map();
105
+ const qTerms = tokenize(query);
106
+ if (qTerms.length === 0)
107
+ return scores;
108
+ for (let docId = 0; docId < chunks.length; docId++) {
109
+ const c = chunks[docId];
110
+ const targets = [c.title, ...(c.meta?.aliases || [])].filter(Boolean);
111
+ let maxScore = 0;
112
+ for (const target of targets) {
113
+ const tTerms = tokenize(target);
114
+ for (const qt of qTerms) {
115
+ if (qt.length < 3)
116
+ continue;
117
+ for (const tt of tTerms) {
118
+ if (tt.length < 3)
119
+ continue;
120
+ const maxDist = Math.min(2, Math.floor(qt.length / 3));
121
+ const dist = editDistance(qt, tt);
122
+ if (dist <= maxDist) {
123
+ const similarity = 1 - dist / Math.max(qt.length, tt.length);
124
+ if (similarity > maxScore)
125
+ maxScore = similarity;
126
+ }
127
+ }
128
+ }
129
+ }
130
+ if (maxScore > 0) {
131
+ scores.set(docId, maxScore * 2.0);
132
+ }
133
+ }
134
+ return scores;
135
+ }
136
+ export function validateLexicalIndex(value, chunkCount) {
137
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
138
+ return 'lexical must be an object';
139
+ }
140
+ const lexical = value;
141
+ if (lexical.format !== 'bm25-v1' && lexical.format !== 'bm25-v2') {
142
+ return 'unknown lexical format';
143
+ }
144
+ if (!Array.isArray(lexical.documentLengths) ||
145
+ lexical.documentLengths.length !== chunkCount ||
146
+ !lexical.documentLengths.every((length) => Number.isFinite(length) && length >= 0)) {
147
+ return `documentLengths must contain ${chunkCount} non-negative numbers`;
148
+ }
149
+ if (lexical.postings === null || typeof lexical.postings !== 'object' || Array.isArray(lexical.postings)) {
150
+ return 'postings must be an object';
151
+ }
152
+ const sums = new Array(chunkCount).fill(0);
153
+ for (const [term, rawPosting] of Object.entries(lexical.postings)) {
154
+ if (!Array.isArray(rawPosting))
155
+ return `posting for ${term} must be an array`;
156
+ let previous = -1;
157
+ for (const entry of rawPosting) {
158
+ if (!Array.isArray(entry) || entry.length !== 2)
159
+ return `posting for ${term} has an invalid entry`;
160
+ const [docId, tf] = entry;
161
+ if (!Number.isSafeInteger(docId)) {
162
+ return `posting for ${term} must have a safe integer document ID`;
163
+ }
164
+ if (docId < 0 || docId >= chunkCount) {
165
+ return `posting for ${term} has a document ID out of range`;
166
+ }
167
+ if (docId <= previous)
168
+ return `posting for ${term} document IDs must be strictly increasing`;
169
+ if (!Number.isFinite(tf) || tf <= 0)
170
+ return `posting for ${term} must have positive TF`;
171
+ sums[docId] += tf;
172
+ previous = docId;
173
+ }
174
+ }
175
+ for (let docId = 0; docId < chunkCount; docId++) {
176
+ if (Math.abs(sums[docId] - lexical.documentLengths[docId]) > 1e-3) {
177
+ return `document ${docId} TF sum does not equal documentLengths`;
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+ export function reverseLexicalIndex(lexical) {
183
+ const records = lexical.documentLengths.map(() => Object.create(null));
184
+ for (const [term, posting] of Object.entries(lexical.postings)) {
185
+ for (const [docId, tf] of posting)
186
+ records[docId][term] = tf;
187
+ }
188
+ return records;
189
+ }
190
+ export function bm25Scores(lexical, queryTerms, eligible) {
191
+ const scores = new Map();
192
+ const documentCount = lexical.documentLengths.length;
193
+ if (documentCount === 0)
194
+ return scores;
195
+ const averageLength = lexical.documentLengths.reduce((sum, length) => sum + length, 0) / documentCount || 1;
196
+ if (!Number.isFinite(averageLength))
197
+ throw new Error('non-finite BM25 average document length');
198
+ for (const term of new Set(queryTerms)) {
199
+ const posting = Object.prototype.hasOwnProperty.call(lexical.postings, term) ? lexical.postings[term] : undefined;
200
+ if (!posting)
201
+ continue;
202
+ const idf = Math.log(1 + (documentCount - posting.length + 0.5) / (posting.length + 0.5));
203
+ if (!Number.isFinite(idf))
204
+ throw new Error(`non-finite BM25 IDF for ${term}`);
205
+ for (const [docId, tf] of posting) {
206
+ if (eligible && !eligible.has(docId))
207
+ continue;
208
+ const lengthRatio = lexical.documentLengths[docId] / averageLength;
209
+ const score = (idf * (tf * 2.2)) / (tf + 1.2 * (0.25 + 0.75 * lengthRatio));
210
+ if (!Number.isFinite(lengthRatio) || !Number.isFinite(score)) {
211
+ throw new Error(`non-finite BM25 score for ${term}`);
212
+ }
213
+ scores.set(docId, (scores.get(docId) ?? 0) + score);
214
+ }
215
+ }
216
+ return scores;
217
+ }
218
+ export function matchingTerms(lexical, queryTerms, docId) {
219
+ const matches = [];
220
+ for (const term of new Set(queryTerms)) {
221
+ const posting = Object.prototype.hasOwnProperty.call(lexical.postings, term) ? lexical.postings[term] : undefined;
222
+ if (posting?.some(([candidate]) => candidate === docId))
223
+ matches.push(term);
224
+ }
225
+ return matches;
226
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Structural Markdown parser and block chunker (issue #57).
3
+ * Parses Markdown source into structured blocks (headings, fenced code,
4
+ * tables, blockquotes, lists, paragraphs) and emits chunk records.
5
+ */
6
+ export declare const PARSER_VERSION = "v1-ast";
7
+ export interface MarkdownBlock {
8
+ type: 'atx_heading' | 'setext_heading' | 'code' | 'table' | 'blockquote' | 'list' | 'paragraph';
9
+ level?: number;
10
+ headingText?: string;
11
+ text: string;
12
+ startLine: number;
13
+ endLine: number;
14
+ }
15
+ export interface StructuralChunk {
16
+ heading: string;
17
+ headingPath: string[];
18
+ text: string;
19
+ startLine: number;
20
+ endLine: number;
21
+ }
22
+ export declare function parseMarkdownBlocks(body: string): MarkdownBlock[];
23
+ export declare function resolveChunkBudget(tokens?: number, defaultMax?: number): number;
24
+ export declare function chunkMarkdownStructural(input: string | MarkdownBlock[], opts?: number | {
25
+ maxChunk?: number;
26
+ targetTokens?: number;
27
+ }): StructuralChunk[];
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Structural Markdown parser and block chunker (issue #57).
3
+ * Parses Markdown source into structured blocks (headings, fenced code,
4
+ * tables, blockquotes, lists, paragraphs) and emits chunk records.
5
+ */
6
+ export const PARSER_VERSION = 'v1-ast';
7
+ export function parseMarkdownBlocks(body) {
8
+ const lines = body.split(/\r?\n/);
9
+ const blocks = [];
10
+ let i = 0;
11
+ while (i < lines.length) {
12
+ const line = lines[i];
13
+ const lineNum = i + 1;
14
+ const fenceMatch = line.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
15
+ if (fenceMatch) {
16
+ const fenceChar = fenceMatch[2][0];
17
+ const fenceLen = fenceMatch[2].length;
18
+ const startLine = lineNum;
19
+ const codeLines = [line];
20
+ i++;
21
+ while (i < lines.length) {
22
+ const curLine = lines[i];
23
+ codeLines.push(curLine);
24
+ const closeMatch = curLine.match(/^(\s*)(`{3,}|~{3,})\s*$/);
25
+ if (closeMatch && closeMatch[2][0] === fenceChar && closeMatch[2].length >= fenceLen) {
26
+ i++;
27
+ break;
28
+ }
29
+ i++;
30
+ }
31
+ blocks.push({
32
+ type: 'code',
33
+ text: codeLines.join('\n'),
34
+ startLine,
35
+ endLine: i,
36
+ });
37
+ continue;
38
+ }
39
+ const atxMatch = line.match(/^(\s{0,3})(#{1,6})\s+(.+?)\s*#*\s*$/);
40
+ if (atxMatch) {
41
+ const level = atxMatch[2].length;
42
+ const headingText = atxMatch[3].trim();
43
+ blocks.push({
44
+ type: 'atx_heading',
45
+ level,
46
+ headingText,
47
+ text: line,
48
+ startLine: lineNum,
49
+ endLine: lineNum,
50
+ });
51
+ i++;
52
+ continue;
53
+ }
54
+ if (i + 1 < lines.length &&
55
+ line.trim().length > 0 &&
56
+ !/^\s*(?:[#>]|```|~~~|[-*+]\s|\d+\.\s)/.test(line)) {
57
+ const nextLine = lines[i + 1];
58
+ const setextMatch = nextLine.match(/^(\s{0,3})(=+|-+)\s*$/);
59
+ if (setextMatch) {
60
+ const level = setextMatch[2][0] === '=' ? 1 : 2;
61
+ const headingText = line.trim();
62
+ blocks.push({
63
+ type: 'setext_heading',
64
+ level,
65
+ headingText,
66
+ text: `${line}\n${nextLine}`,
67
+ startLine: lineNum,
68
+ endLine: lineNum + 1,
69
+ });
70
+ i += 2;
71
+ continue;
72
+ }
73
+ }
74
+ if (line.trim() === '') {
75
+ i++;
76
+ continue;
77
+ }
78
+ if (/^\s*\|/.test(line) || (/\|/.test(line) && i + 1 < lines.length && /^\s*\|?\s*[-:]+[-|\s:]*$/.test(lines[i + 1]))) {
79
+ const startLine = lineNum;
80
+ const tableLines = [];
81
+ while (i < lines.length && (lines[i].includes('|') || /^\s*\|?\s*[-:]+[-|\s:]*$/.test(lines[i]))) {
82
+ tableLines.push(lines[i]);
83
+ i++;
84
+ }
85
+ blocks.push({
86
+ type: 'table',
87
+ text: tableLines.join('\n'),
88
+ startLine,
89
+ endLine: i,
90
+ });
91
+ continue;
92
+ }
93
+ if (/^\s*>/.test(line)) {
94
+ const startLine = lineNum;
95
+ const bqLines = [];
96
+ while (i < lines.length && (/^\s*>/.test(lines[i]) || (lines[i].trim() !== '' && !/^\s*(?:#{1,6}\s|```|~~~|[-*+]\s|\d+\.\s)/.test(lines[i])))) {
97
+ bqLines.push(lines[i]);
98
+ i++;
99
+ }
100
+ blocks.push({
101
+ type: 'blockquote',
102
+ text: bqLines.join('\n'),
103
+ startLine,
104
+ endLine: i,
105
+ });
106
+ continue;
107
+ }
108
+ const startLine = lineNum;
109
+ const paraLines = [line];
110
+ i++;
111
+ while (i < lines.length) {
112
+ const l = lines[i];
113
+ if (l.trim() === '' ||
114
+ /^\s{0,3}#{1,6}\s/.test(l) ||
115
+ /^\s*(?:`{3,}|~{3,})/.test(l) ||
116
+ /^\s*>/.test(l) ||
117
+ /^\s*\|/.test(l)) {
118
+ break;
119
+ }
120
+ if (i + 1 < lines.length &&
121
+ /^\s{0,3}(?:=+|-+)\s*$/.test(lines[i + 1])) {
122
+ break;
123
+ }
124
+ paraLines.push(l);
125
+ i++;
126
+ }
127
+ blocks.push({
128
+ type: 'paragraph',
129
+ text: paraLines.join('\n'),
130
+ startLine,
131
+ endLine: i,
132
+ });
133
+ }
134
+ return blocks;
135
+ }
136
+ export function resolveChunkBudget(tokens, defaultMax = 1200) {
137
+ if (tokens && Number.isFinite(tokens) && tokens > 0) {
138
+ return Math.max(100, Math.min(8192, Math.round(tokens * 4)));
139
+ }
140
+ return defaultMax;
141
+ }
142
+ export function chunkMarkdownStructural(input, opts) {
143
+ const options = typeof opts === 'number' ? { maxChunk: opts } : (opts || {});
144
+ const maxChunk = resolveChunkBudget(options.targetTokens, options.maxChunk ?? 1200);
145
+ const blocks = typeof input === 'string' ? parseMarkdownBlocks(input) : input;
146
+ const chunks = [];
147
+ const headingStack = [];
148
+ let currentGroup = [];
149
+ let currentLen = 0;
150
+ let groupStartLine = 0;
151
+ let groupEndLine = 0;
152
+ const flushGroup = () => {
153
+ if (currentGroup.length === 0)
154
+ return;
155
+ const text = currentGroup.join('\n\n').trim();
156
+ if (text) {
157
+ const headingPath = headingStack.map(h => h.heading);
158
+ chunks.push({
159
+ heading: headingPath.at(-1) ?? '',
160
+ headingPath,
161
+ text,
162
+ startLine: groupStartLine,
163
+ endLine: groupEndLine,
164
+ });
165
+ }
166
+ currentGroup = [];
167
+ currentLen = 0;
168
+ };
169
+ for (const block of blocks) {
170
+ if (block.type === 'atx_heading' || block.type === 'setext_heading') {
171
+ flushGroup();
172
+ const level = block.level || 1;
173
+ const heading = block.headingText || '';
174
+ while (headingStack.length > 0 && headingStack[headingStack.length - 1].level >= level) {
175
+ headingStack.pop();
176
+ }
177
+ headingStack.push({ level, heading });
178
+ continue;
179
+ }
180
+ const blockText = block.text.trim();
181
+ if (!blockText)
182
+ continue;
183
+ if (blockText.length > maxChunk) {
184
+ flushGroup();
185
+ const headingPath = headingStack.map(h => h.heading);
186
+ const subPieces = splitOversizedBlock(blockText, maxChunk);
187
+ let lineCursor = block.startLine;
188
+ for (let idx = 0; idx < subPieces.length; idx++) {
189
+ const piece = subPieces[idx];
190
+ const pieceLineCount = piece.split('\n').length;
191
+ chunks.push({
192
+ heading: headingPath.at(-1) ?? '',
193
+ headingPath: [...headingPath],
194
+ text: piece,
195
+ startLine: lineCursor,
196
+ endLine: Math.min(block.endLine, lineCursor + pieceLineCount - 1),
197
+ });
198
+ lineCursor += pieceLineCount;
199
+ }
200
+ continue;
201
+ }
202
+ const addedLen = currentLen === 0 ? blockText.length : currentLen + 2 + blockText.length;
203
+ if (addedLen > maxChunk && currentGroup.length > 0) {
204
+ flushGroup();
205
+ }
206
+ if (currentGroup.length === 0) {
207
+ groupStartLine = block.startLine;
208
+ }
209
+ currentGroup.push(blockText);
210
+ currentLen += (currentGroup.length === 1 ? 0 : 2) + blockText.length;
211
+ groupEndLine = block.endLine;
212
+ }
213
+ flushGroup();
214
+ return chunks.filter(c => c.text.replace(/\s/g, '').length >= 24);
215
+ }
216
+ function splitOversizedBlock(text, maxChunk) {
217
+ const lines = text.split('\n');
218
+ const pieces = [];
219
+ let currentLines = [];
220
+ let currentLen = 0;
221
+ for (const line of lines) {
222
+ if (currentLen + line.length + 1 > maxChunk && currentLines.length > 0) {
223
+ pieces.push(currentLines.join('\n').trim());
224
+ currentLines = [];
225
+ currentLen = 0;
226
+ }
227
+ if (line.length > maxChunk) {
228
+ if (currentLines.length > 0) {
229
+ pieces.push(currentLines.join('\n').trim());
230
+ currentLines = [];
231
+ }
232
+ let rest = line;
233
+ while (rest.length > maxChunk) {
234
+ let cut = rest.lastIndexOf(' ', maxChunk);
235
+ if (cut <= 0)
236
+ cut = maxChunk;
237
+ pieces.push(rest.slice(0, cut).trim());
238
+ rest = rest.slice(cut).trim();
239
+ }
240
+ if (rest)
241
+ currentLines.push(rest);
242
+ currentLen = rest.length;
243
+ continue;
244
+ }
245
+ currentLines.push(line);
246
+ currentLen += line.length + 1;
247
+ }
248
+ if (currentLines.length > 0) {
249
+ pieces.push(currentLines.join('\n').trim());
250
+ }
251
+ return pieces;
252
+ }
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * MCP (Model Context Protocol) server mode (issue #62).
3
+ * Exposes local semantic search to IDEs and LLM agents (Cursor, Claude Desktop, Copilot)
4
+ * via a zero-dependency JSON-RPC 2.0 stdio transport per the MCP specification.
5
+ */
6
+ import readline from 'node:readline';
7
+ export declare const MCP_TOOLS: ({
8
+ name: string;
9
+ description: string;
10
+ inputSchema: {
11
+ type: string;
12
+ properties: {
13
+ query: {
14
+ type: string;
15
+ description: string;
16
+ };
17
+ k: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ path: {
22
+ type: string;
23
+ description: string;
24
+ };
25
+ tag: {
26
+ type: string;
27
+ items: {
28
+ type: string;
29
+ };
30
+ description: string;
31
+ };
32
+ file?: undefined;
33
+ heading?: undefined;
34
+ };
35
+ required: string[];
36
+ };
37
+ } | {
38
+ name: string;
39
+ description: string;
40
+ inputSchema: {
41
+ type: string;
42
+ properties: {
43
+ query?: undefined;
44
+ k?: undefined;
45
+ tag?: undefined;
46
+ file: {
47
+ type: string;
48
+ description: string;
49
+ };
50
+ heading: {
51
+ type: string;
52
+ description: string;
53
+ };
54
+ path?: undefined;
55
+ };
56
+ required: string[];
57
+ };
58
+ } | {
59
+ name: string;
60
+ description: string;
61
+ inputSchema: {
62
+ required?: undefined;
63
+ type: string;
64
+ properties: {
65
+ query?: undefined;
66
+ k?: undefined;
67
+ tag?: undefined;
68
+ file?: undefined;
69
+ heading?: undefined;
70
+ path: {
71
+ type: string;
72
+ description: string;
73
+ };
74
+ };
75
+ };
76
+ } | {
77
+ name: string;
78
+ description: string;
79
+ inputSchema: {
80
+ required?: undefined;
81
+ type: string;
82
+ properties: {
83
+ query?: undefined;
84
+ k?: undefined;
85
+ tag?: undefined;
86
+ file?: undefined;
87
+ heading?: undefined;
88
+ path?: undefined;
89
+ };
90
+ };
91
+ })[];
92
+ export declare function handleMcpRequest(req: any, state: {
93
+ loaded: any;
94
+ cacheDir: string;
95
+ offline: boolean;
96
+ embedFn?: any;
97
+ }): Promise<any | null>;
98
+ export interface StartMcpServerOptions {
99
+ indexDir: string;
100
+ cacheDir: string;
101
+ db?: string;
102
+ modelName?: string;
103
+ ignore?: string[];
104
+ offline?: boolean;
105
+ embedFn?: any;
106
+ log?: (msg: string) => void;
107
+ }
108
+ export declare function startMcpServer(opts: StartMcpServerOptions): Promise<{
109
+ state: any;
110
+ rl: readline.Interface;
111
+ }>;