projscan 0.6.0 → 0.8.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.
@@ -0,0 +1,268 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ /**
4
+ * Lightweight BM25-ranked inverted index over source files.
5
+ *
6
+ * We index three fields per file with different weights:
7
+ * - content (body tokens, BM25 baseline)
8
+ * - symbols (export names — most informative for code search)
9
+ * - path (file path tokens)
10
+ *
11
+ * Scoring:
12
+ * score(file, query) = BM25(content) + 2.0 * hits(symbols) + 0.5 * hits(path)
13
+ *
14
+ * This intentionally beats pure substring matching while staying
15
+ * zero-dependency and fast enough for sub-second queries on 10k-file repos.
16
+ */
17
+ const MAX_FILE_SIZE = 512 * 1024;
18
+ const STOPWORDS = new Set([
19
+ 'the', 'a', 'an', 'and', 'or', 'not', 'of', 'to', 'in', 'is', 'it', 'for', 'on', 'with',
20
+ 'this', 'that', 'by', 'as', 'at', 'be', 'are', 'was', 'were', 'has', 'have', 'had',
21
+ ]);
22
+ const TS_KEYWORDS = new Set([
23
+ 'const', 'let', 'var', 'function', 'return', 'if', 'else', 'while', 'for', 'do', 'break',
24
+ 'continue', 'switch', 'case', 'default', 'new', 'class', 'extends', 'implements',
25
+ 'interface', 'type', 'enum', 'public', 'private', 'protected', 'static', 'readonly',
26
+ 'async', 'await', 'try', 'catch', 'finally', 'throw', 'import', 'export', 'from', 'as',
27
+ 'typeof', 'instanceof', 'void', 'null', 'undefined', 'true', 'false', 'this', 'super',
28
+ 'yield', 'delete', 'in', 'of', 'any', 'never', 'unknown', 'string', 'number', 'boolean',
29
+ 'object', 'symbol', 'bigint',
30
+ ]);
31
+ export async function buildSearchIndex(rootPath, files, graph) {
32
+ const indexed = new Map();
33
+ const postings = new Map();
34
+ const parseable = files.filter((f) => f.sizeBytes <= MAX_FILE_SIZE && isIndexable(f.relativePath));
35
+ await Promise.all(parseable.map(async (file) => {
36
+ const abs = path.isAbsolute(file.absolutePath)
37
+ ? file.absolutePath
38
+ : path.resolve(rootPath, file.relativePath);
39
+ let content;
40
+ try {
41
+ content = await fs.readFile(abs, 'utf-8');
42
+ }
43
+ catch {
44
+ return;
45
+ }
46
+ const contentTokens = tokenize(content);
47
+ const pathTokens = tokenize(file.relativePath);
48
+ const symbols = (graph?.files.get(file.relativePath)?.exports ?? []).map((e) => e.name.toLowerCase());
49
+ const entry = {
50
+ relativePath: file.relativePath,
51
+ content: contentTokens,
52
+ symbols: symbols.flatMap((s) => tokenize(s)),
53
+ pathTokens,
54
+ length: contentTokens.length,
55
+ };
56
+ indexed.set(file.relativePath, entry);
57
+ // Build postings from content tokens
58
+ const termCounts = new Map();
59
+ for (const tok of contentTokens) {
60
+ termCounts.set(tok, (termCounts.get(tok) ?? 0) + 1);
61
+ }
62
+ for (const [tok, count] of termCounts) {
63
+ if (!postings.has(tok))
64
+ postings.set(tok, new Map());
65
+ postings.get(tok).set(file.relativePath, count);
66
+ }
67
+ }));
68
+ const totalLength = [...indexed.values()].reduce((sum, f) => sum + f.length, 0);
69
+ const avgDocLength = indexed.size > 0 ? totalLength / indexed.size : 1;
70
+ return {
71
+ files: indexed,
72
+ postings,
73
+ avgDocLength,
74
+ docCount: indexed.size,
75
+ };
76
+ }
77
+ export function search(index, query, options = {}) {
78
+ const limit = Math.max(1, Math.min(500, options.limit ?? 30));
79
+ const symbolWeight = options.symbolWeight ?? 2.0;
80
+ const pathWeight = options.pathWeight ?? 0.5;
81
+ const queryTokens = expandQuery(query);
82
+ if (queryTokens.length === 0)
83
+ return [];
84
+ // BM25 parameters
85
+ const k1 = 1.5;
86
+ const b = 0.75;
87
+ const scores = new Map();
88
+ for (const qTok of queryTokens) {
89
+ const postings = index.postings.get(qTok);
90
+ if (!postings)
91
+ continue;
92
+ const df = postings.size;
93
+ const idf = Math.log(1 + (index.docCount - df + 0.5) / (df + 0.5));
94
+ for (const [file, tf] of postings) {
95
+ const doc = index.files.get(file);
96
+ if (!doc)
97
+ continue;
98
+ const dl = doc.length;
99
+ const norm = tf * (k1 + 1) / (tf + k1 * (1 - b + b * (dl / index.avgDocLength)));
100
+ const bm25 = idf * norm;
101
+ const existing = scores.get(file);
102
+ if (existing) {
103
+ existing.score += bm25;
104
+ existing.matched.add(qTok);
105
+ }
106
+ else {
107
+ scores.set(file, { score: bm25, matched: new Set([qTok]) });
108
+ }
109
+ }
110
+ }
111
+ // Apply symbol + path boosts
112
+ for (const [file, entry] of index.files) {
113
+ const symbolHits = countHits(entry.symbols, queryTokens);
114
+ const pathHits = countHits(entry.pathTokens, queryTokens);
115
+ if (symbolHits > 0 || pathHits > 0) {
116
+ const current = scores.get(file) ?? { score: 0, matched: new Set() };
117
+ current.score += symbolHits * symbolWeight + pathHits * pathWeight;
118
+ for (const qt of queryTokens) {
119
+ if (entry.symbols.includes(qt) || entry.pathTokens.includes(qt)) {
120
+ current.matched.add(qt);
121
+ }
122
+ }
123
+ scores.set(file, current);
124
+ }
125
+ }
126
+ if (scores.size === 0)
127
+ return [];
128
+ // Sort by score, take top limit
129
+ const ranked = [...scores.entries()]
130
+ .sort((a, b) => b[1].score - a[1].score)
131
+ .slice(0, limit);
132
+ return ranked.map(([file, info]) => {
133
+ const entry = index.files.get(file);
134
+ const symbolMatch = queryTokens.some((t) => entry.symbols.includes(t));
135
+ const pathMatch = queryTokens.some((t) => entry.pathTokens.includes(t));
136
+ return {
137
+ file,
138
+ score: Math.round(info.score * 100) / 100,
139
+ matched: [...info.matched],
140
+ symbolMatch,
141
+ pathMatch,
142
+ excerpt: '',
143
+ line: 0,
144
+ };
145
+ });
146
+ }
147
+ /**
148
+ * Attach a one-line excerpt to each hit, reading the file to find the first
149
+ * matching line. This is a separate pass to avoid paying the I/O cost when
150
+ * the caller only wants paths (e.g., an agent filtering before fetching).
151
+ */
152
+ export async function attachExcerpts(rootPath, hits, queryTokens) {
153
+ const qLower = queryTokens.map((t) => t.toLowerCase());
154
+ return Promise.all(hits.map(async (hit) => {
155
+ const abs = path.resolve(rootPath, hit.file);
156
+ try {
157
+ const content = await fs.readFile(abs, 'utf-8');
158
+ const lines = content.split('\n');
159
+ for (let i = 0; i < lines.length; i++) {
160
+ const lower = lines[i].toLowerCase();
161
+ if (qLower.some((t) => lower.includes(t))) {
162
+ return { ...hit, line: i + 1, excerpt: lines[i].trim().slice(0, 200) };
163
+ }
164
+ }
165
+ }
166
+ catch {
167
+ // ignore
168
+ }
169
+ return hit;
170
+ }));
171
+ }
172
+ /**
173
+ * Tokenize a string for indexing/querying:
174
+ * - lowercase
175
+ * - split on non-identifier chars
176
+ * - split camelCase and snake_case
177
+ * - drop tokens shorter than 2 chars, stopwords, TS keywords
178
+ * - apply basic stem (drop trailing s / ing / ed)
179
+ */
180
+ export function tokenize(input) {
181
+ const out = [];
182
+ // Split on non-identifier boundaries. Keep original case so we can also
183
+ // split on camelCase boundaries below.
184
+ const rawTokens = input.match(/[A-Za-z0-9_]+/g) ?? [];
185
+ for (const raw of rawTokens) {
186
+ // Split on underscore and camelCase. camelCase: insert a boundary before
187
+ // each uppercase that follows a lowercase or digit (OR before runs of
188
+ // uppercase followed by lowercase to handle acronyms like "XMLParser").
189
+ const camelSplit = raw
190
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
191
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
192
+ const parts = camelSplit.split(/[_\s]+/).filter(Boolean);
193
+ for (const part of parts) {
194
+ // Split embedded digits from letters — e.g. "v1api" → "v", "1", "api"
195
+ const subparts = part.split(/(\d+)/).filter(Boolean);
196
+ for (const sp of subparts) {
197
+ const lower = sp.toLowerCase();
198
+ const stemmed = stem(lower);
199
+ if (!keepToken(stemmed))
200
+ continue;
201
+ out.push(stemmed);
202
+ }
203
+ }
204
+ }
205
+ return out;
206
+ }
207
+ /**
208
+ * Expand a user query into a set of candidate tokens. Same rules as tokenize
209
+ * plus: if the raw query has no hits, try progressively looser tokenization.
210
+ */
211
+ export function expandQuery(query) {
212
+ const tokens = tokenize(query);
213
+ return [...new Set(tokens)];
214
+ }
215
+ function stem(token) {
216
+ if (token.length <= 3)
217
+ return token;
218
+ if (token.endsWith('ing'))
219
+ return token.slice(0, -3);
220
+ if (token.endsWith('ed') && token.length > 4)
221
+ return token.slice(0, -2);
222
+ if (token.endsWith('es') && token.length > 4)
223
+ return token.slice(0, -2);
224
+ if (token.endsWith('s') && !token.endsWith('ss'))
225
+ return token.slice(0, -1);
226
+ return token;
227
+ }
228
+ function keepToken(token) {
229
+ if (token.length < 2)
230
+ return false;
231
+ if (STOPWORDS.has(token))
232
+ return false;
233
+ if (TS_KEYWORDS.has(token))
234
+ return false;
235
+ return true;
236
+ }
237
+ function countHits(tokens, query) {
238
+ let count = 0;
239
+ const set = new Set(tokens);
240
+ for (const q of query)
241
+ if (set.has(q))
242
+ count++;
243
+ return count;
244
+ }
245
+ function isIndexable(relativePath) {
246
+ const ext = path.extname(relativePath).toLowerCase();
247
+ // Index source and markup/docs where it's likely useful
248
+ return (ext === '.ts' ||
249
+ ext === '.tsx' ||
250
+ ext === '.js' ||
251
+ ext === '.jsx' ||
252
+ ext === '.mjs' ||
253
+ ext === '.cjs' ||
254
+ ext === '.mts' ||
255
+ ext === '.cts' ||
256
+ ext === '.py' ||
257
+ ext === '.go' ||
258
+ ext === '.rb' ||
259
+ ext === '.java' ||
260
+ ext === '.rs' ||
261
+ ext === '.php' ||
262
+ ext === '.cs' ||
263
+ ext === '.swift' ||
264
+ ext === '.kt' ||
265
+ ext === '.md' ||
266
+ ext === '.mdx');
267
+ }
268
+ //# sourceMappingURL=searchIndex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"searchIndex.js","sourceRoot":"","sources":["../../src/core/searchIndex.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -15,6 +15,10 @@ export { parseSource, isParseable } from './core/ast.js';
15
15
  export { buildCodeGraph, filesImportingFile, filesImportingPackage, filesDefiningSymbol, exportsOf, importsOf, importersOf, } from './core/codeGraph.js';
16
16
  export { loadCachedGraph, saveCachedGraph, invalidateCache } from './core/indexCache.js';
17
17
  export { applyBudget, estimateTokens } from './mcp/tokenBudget.js';
18
+ export { paginate, encodeCursor, decodeCursor, listChecksum, readPageParams } from './mcp/pagination.js';
19
+ export { toContentBlocks } from './mcp/chunker.js';
20
+ export { emitProgress, withProgress } from './mcp/progress.js';
21
+ export { buildSearchIndex, search, tokenize, expandQuery, attachExcerpts, } from './core/searchIndex.js';
18
22
  export { findDependencyLines } from './utils/packageJsonLocator.js';
19
23
  export { parse as parseSemver, compare as compareSemver, drift as semverDrift } from './utils/semver.js';
20
24
  export { walkFiles } from './utils/fileWalker.js';
package/dist/index.js CHANGED
@@ -15,6 +15,10 @@ export { parseSource, isParseable } from './core/ast.js';
15
15
  export { buildCodeGraph, filesImportingFile, filesImportingPackage, filesDefiningSymbol, exportsOf, importsOf, importersOf, } from './core/codeGraph.js';
16
16
  export { loadCachedGraph, saveCachedGraph, invalidateCache } from './core/indexCache.js';
17
17
  export { applyBudget, estimateTokens } from './mcp/tokenBudget.js';
18
+ export { paginate, encodeCursor, decodeCursor, listChecksum, readPageParams } from './mcp/pagination.js';
19
+ export { toContentBlocks } from './mcp/chunker.js';
20
+ export { emitProgress, withProgress } from './mcp/progress.js';
21
+ export { buildSearchIndex, search, tokenize, expandQuery, attachExcerpts, } from './core/searchIndex.js';
18
22
  export { findDependencyLines } from './utils/packageJsonLocator.js';
19
23
  export { parse as parseSemver, compare as compareSemver, drift as semverDrift } from './utils/semver.js';
20
24
  export { walkFiles } from './utils/fileWalker.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvG,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,SAAS,EACT,SAAS,EACT,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,KAAK,IAAI,WAAW,EAAE,OAAO,IAAI,aAAa,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACzG,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvG,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,SAAS,EACT,SAAS,EACT,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACzG,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EACL,gBAAgB,EAChB,MAAM,EACN,QAAQ,EACR,WAAW,EACX,cAAc,GACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,KAAK,IAAI,WAAW,EAAE,OAAO,IAAI,aAAa,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACzG,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Split large tool responses into multiple MCP `content` blocks so agents
3
+ * that consume them streaming-style can process results incrementally.
4
+ *
5
+ * MCP content is `[{ type: "text", text: string }, ...]`. Current strategy:
6
+ * - If the payload has a primary array field (hotspots, entries, findings,
7
+ * packages, matches), emit one header block with scalar fields, then
8
+ * emit chunk blocks each containing a slice of the array.
9
+ * - Otherwise, emit a single block.
10
+ *
11
+ * Chunk size defaults to 20 records per block — small enough to be a
12
+ * meaningful streaming unit, big enough to avoid pathological block counts.
13
+ */
14
+ export interface ContentBlock {
15
+ type: 'text';
16
+ text: string;
17
+ }
18
+ export interface ChunkOptions {
19
+ chunkSize?: number;
20
+ /** Only chunk when the primary array has at least this many items. */
21
+ minItemsToChunk?: number;
22
+ }
23
+ export declare function toContentBlocks(value: unknown, options?: ChunkOptions): ContentBlock[];
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Split large tool responses into multiple MCP `content` blocks so agents
3
+ * that consume them streaming-style can process results incrementally.
4
+ *
5
+ * MCP content is `[{ type: "text", text: string }, ...]`. Current strategy:
6
+ * - If the payload has a primary array field (hotspots, entries, findings,
7
+ * packages, matches), emit one header block with scalar fields, then
8
+ * emit chunk blocks each containing a slice of the array.
9
+ * - Otherwise, emit a single block.
10
+ *
11
+ * Chunk size defaults to 20 records per block — small enough to be a
12
+ * meaningful streaming unit, big enough to avoid pathological block counts.
13
+ */
14
+ const DEFAULT_CHUNK_SIZE = 20;
15
+ const PRIMARY_ARRAY_KEYS = [
16
+ 'hotspots',
17
+ 'entries',
18
+ 'findings',
19
+ 'packages',
20
+ 'matches',
21
+ 'importers',
22
+ 'files',
23
+ 'definedIn',
24
+ ];
25
+ export function toContentBlocks(value, options = {}) {
26
+ const chunkSize = Math.max(1, options.chunkSize ?? DEFAULT_CHUNK_SIZE);
27
+ const minItemsToChunk = options.minItemsToChunk ?? chunkSize;
28
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
29
+ return [{ type: 'text', text: safeStringify(value) }];
30
+ }
31
+ const obj = value;
32
+ const primaryKey = PRIMARY_ARRAY_KEYS.find((k) => Array.isArray(obj[k]) && obj[k].length >= minItemsToChunk);
33
+ if (!primaryKey) {
34
+ return [{ type: 'text', text: safeStringify(value) }];
35
+ }
36
+ const items = obj[primaryKey];
37
+ const header = {};
38
+ for (const [k, v] of Object.entries(obj)) {
39
+ if (k === primaryKey)
40
+ continue;
41
+ header[k] = v;
42
+ }
43
+ header[`${primaryKey}Preview`] = {
44
+ totalItems: items.length,
45
+ chunkedInto: Math.ceil(items.length / chunkSize),
46
+ chunkSize,
47
+ };
48
+ const blocks = [{ type: 'text', text: safeStringify(header) }];
49
+ for (let i = 0; i < items.length; i += chunkSize) {
50
+ const chunkIndex = Math.floor(i / chunkSize);
51
+ const chunkPayload = {
52
+ _chunk: { index: chunkIndex, offset: i, size: items.slice(i, i + chunkSize).length },
53
+ [primaryKey]: items.slice(i, i + chunkSize),
54
+ };
55
+ blocks.push({ type: 'text', text: safeStringify(chunkPayload) });
56
+ }
57
+ return blocks;
58
+ }
59
+ function safeStringify(value) {
60
+ try {
61
+ return JSON.stringify(value, null, 2);
62
+ }
63
+ catch {
64
+ return String(value);
65
+ }
66
+ }
67
+ //# sourceMappingURL=chunker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunker.js","sourceRoot":"","sources":["../../src/mcp/chunker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Cursor-based pagination for MCP tool responses.
3
+ *
4
+ * Cursors are opaque base64-encoded JSON objects containing an integer
5
+ * offset plus an optional checksum of the result-set shape so we can
6
+ * detect when the underlying data has shifted between paginated calls.
7
+ *
8
+ * Agents pass `cursor` back to the same tool to fetch the next page.
9
+ * When no more results exist, the response omits `nextCursor`.
10
+ */
11
+ export interface PageRequest {
12
+ cursor?: string;
13
+ pageSize?: number;
14
+ }
15
+ export interface Page<T> {
16
+ items: T[];
17
+ nextCursor?: string;
18
+ total: number;
19
+ }
20
+ /**
21
+ * Slice an array into a page. `checksum` should be a cheap identifier of
22
+ * the result-set shape (e.g., `items.length`) — if it mismatches a cursor's
23
+ * captured checksum we treat the page as fresh (offset=0) rather than risk
24
+ * returning stale offsets.
25
+ */
26
+ export declare function paginate<T>(items: T[], request: PageRequest, checksum: string): Page<T>;
27
+ interface DecodedCursor {
28
+ offset: number;
29
+ checksum: string;
30
+ }
31
+ export declare function encodeCursor(cursor: DecodedCursor): string;
32
+ export declare function decodeCursor(cursor?: string): DecodedCursor | null;
33
+ /**
34
+ * Compute a lightweight checksum for a list. Deliberately weak — we want
35
+ * cursor invalidation on shape changes (length) but not on micro-changes
36
+ * within items (scores that shift slightly between runs). Agents already
37
+ * handle eventual consistency.
38
+ */
39
+ export declare function listChecksum(items: unknown[]): string;
40
+ /** Extract pageSize + cursor from MCP args, defaulting conservatively. */
41
+ export declare function readPageParams(args: Record<string, unknown>): PageRequest;
42
+ export {};
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Cursor-based pagination for MCP tool responses.
3
+ *
4
+ * Cursors are opaque base64-encoded JSON objects containing an integer
5
+ * offset plus an optional checksum of the result-set shape so we can
6
+ * detect when the underlying data has shifted between paginated calls.
7
+ *
8
+ * Agents pass `cursor` back to the same tool to fetch the next page.
9
+ * When no more results exist, the response omits `nextCursor`.
10
+ */
11
+ const DEFAULT_PAGE_SIZE = 50;
12
+ const MAX_PAGE_SIZE = 500;
13
+ /**
14
+ * Slice an array into a page. `checksum` should be a cheap identifier of
15
+ * the result-set shape (e.g., `items.length`) — if it mismatches a cursor's
16
+ * captured checksum we treat the page as fresh (offset=0) rather than risk
17
+ * returning stale offsets.
18
+ */
19
+ export function paginate(items, request, checksum) {
20
+ const size = Math.max(1, Math.min(MAX_PAGE_SIZE, request.pageSize ?? DEFAULT_PAGE_SIZE));
21
+ const decoded = decodeCursor(request.cursor);
22
+ const offset = decoded && decoded.checksum === checksum ? decoded.offset : 0;
23
+ const slice = items.slice(offset, offset + size);
24
+ const nextOffset = offset + slice.length;
25
+ const hasMore = nextOffset < items.length;
26
+ return {
27
+ items: slice,
28
+ nextCursor: hasMore ? encodeCursor({ offset: nextOffset, checksum }) : undefined,
29
+ total: items.length,
30
+ };
31
+ }
32
+ export function encodeCursor(cursor) {
33
+ const json = JSON.stringify(cursor);
34
+ return Buffer.from(json, 'utf-8').toString('base64');
35
+ }
36
+ export function decodeCursor(cursor) {
37
+ if (!cursor || typeof cursor !== 'string')
38
+ return null;
39
+ try {
40
+ const json = Buffer.from(cursor, 'base64').toString('utf-8');
41
+ const parsed = JSON.parse(json);
42
+ if (typeof parsed.offset !== 'number' || typeof parsed.checksum !== 'string') {
43
+ return null;
44
+ }
45
+ return parsed;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ /**
52
+ * Compute a lightweight checksum for a list. Deliberately weak — we want
53
+ * cursor invalidation on shape changes (length) but not on micro-changes
54
+ * within items (scores that shift slightly between runs). Agents already
55
+ * handle eventual consistency.
56
+ */
57
+ export function listChecksum(items) {
58
+ return `len:${items.length}`;
59
+ }
60
+ /** Extract pageSize + cursor from MCP args, defaulting conservatively. */
61
+ export function readPageParams(args) {
62
+ const cursor = typeof args.cursor === 'string' ? args.cursor : undefined;
63
+ const pageSize = typeof args.page_size === 'number' ? args.page_size : undefined;
64
+ return { cursor, pageSize };
65
+ }
66
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.js","sourceRoot":"","sources":["../../src/mcp/pagination.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAaH,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAU,EACV,OAAoB,EACpB,QAAgB;IAEhB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC,CAAC,CAAC;IACzF,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,OAAO,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7E,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IACzC,MAAM,OAAO,GAAG,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC;IAE1C,OAAO;QACL,KAAK,EAAE,KAAK;QACZ,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS;QAChF,KAAK,EAAE,KAAK,CAAC,MAAM;KACpB,CAAC;AACJ,CAAC;AAOD,MAAM,UAAU,YAAY,CAAC,MAAqB;IAChD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACpC,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAe;IAC1C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAkB,CAAC;QACjD,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC7E,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,KAAgB;IAC3C,OAAO,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC;AAC/B,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,cAAc,CAAC,IAA6B;IAC1D,MAAM,MAAM,GAAG,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IACzE,MAAM,QAAQ,GAAG,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IACjF,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Progress notification plumbing for long-running MCP tools.
3
+ *
4
+ * Per MCP spec, a client that wants progress sets `_meta.progressToken` on
5
+ * the tool-call request. We capture it at dispatch time and expose a
6
+ * `notify(progress, total?)` callback to the tool handler via the rootPath
7
+ * extension — which is awkward but keeps the handler signature unchanged
8
+ * for tools that don't care.
9
+ *
10
+ * The notification wire format (MCP 2024-11-05 + 2025-03-26):
11
+ * { "jsonrpc": "2.0", "method": "notifications/progress",
12
+ * "params": { "progressToken": <token>, "progress": <n>, "total"?: <n>, "message"?: "..." } }
13
+ */
14
+ export type ProgressEmitter = (progress: number, total?: number, message?: string) => void;
15
+ declare const NOOP: ProgressEmitter;
16
+ export declare function withProgress<T>(emit: ProgressEmitter | undefined, fn: () => Promise<T>): Promise<T>;
17
+ export declare function emitProgress(progress: number, total?: number, message?: string): void;
18
+ export { NOOP };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Progress notification plumbing for long-running MCP tools.
3
+ *
4
+ * Per MCP spec, a client that wants progress sets `_meta.progressToken` on
5
+ * the tool-call request. We capture it at dispatch time and expose a
6
+ * `notify(progress, total?)` callback to the tool handler via the rootPath
7
+ * extension — which is awkward but keeps the handler signature unchanged
8
+ * for tools that don't care.
9
+ *
10
+ * The notification wire format (MCP 2024-11-05 + 2025-03-26):
11
+ * { "jsonrpc": "2.0", "method": "notifications/progress",
12
+ * "params": { "progressToken": <token>, "progress": <n>, "total"?: <n>, "message"?: "..." } }
13
+ */
14
+ const NOOP = () => { };
15
+ /**
16
+ * AsyncLocalStorage-style context. We stash the current emitter on a
17
+ * module-level WeakMap keyed by a symbol to survive `await` points.
18
+ * Simpler than pulling in `node:async_hooks`.
19
+ */
20
+ const emitters = new Map();
21
+ export function withProgress(emit, fn) {
22
+ if (!emit)
23
+ return fn();
24
+ const key = Symbol('progress');
25
+ emitters.set(key, emit);
26
+ currentKey = key;
27
+ return fn().finally(() => {
28
+ emitters.delete(key);
29
+ currentKey = null;
30
+ });
31
+ }
32
+ let currentKey = null;
33
+ export function emitProgress(progress, total, message) {
34
+ if (!currentKey)
35
+ return;
36
+ const emit = emitters.get(currentKey);
37
+ if (!emit)
38
+ return;
39
+ try {
40
+ emit(progress, total, message);
41
+ }
42
+ catch {
43
+ // progress is best-effort; never throw back into user code
44
+ }
45
+ }
46
+ export { NOOP };
47
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.js","sourceRoot":"","sources":["../../src/mcp/progress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,MAAM,IAAI,GAAoB,GAAG,EAAE,GAAE,CAAC,CAAC;AAEvC;;;;GAIG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEpD,MAAM,UAAU,YAAY,CAC1B,IAAiC,EACjC,EAAoB;IAEpB,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,EAAE,CAAC;IACvB,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IAC/B,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACxB,UAAU,GAAG,GAAG,CAAC;IACjB,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;QACvB,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACrB,UAAU,GAAG,IAAI,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,UAAU,GAAkB,IAAI,CAAC;AAErC,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,KAAc,EAAE,OAAgB;IAC7E,IAAI,CAAC,UAAU;QAAE,OAAO;IACxB,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACtC,IAAI,CAAC,IAAI;QAAE,OAAO;IAClB,IAAI,CAAC;QACH,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,2DAA2D;IAC7D,CAAC;AACH,CAAC;AAED,OAAO,EAAE,IAAI,EAAE,CAAC"}
@@ -1,5 +1,13 @@
1
1
  export interface McpServerHandle {
2
2
  handleMessage(line: string): Promise<string | null>;
3
3
  }
4
- export declare function createMcpServer(rootPath: string): McpServerHandle;
4
+ export interface McpServerOptions {
5
+ /**
6
+ * Called when the server wants to emit a JSON-RPC notification (e.g.,
7
+ * `notifications/progress`) out of band from the normal request/response
8
+ * cycle. The transport layer is responsible for writing the payload.
9
+ */
10
+ notify?: (payload: string) => void;
11
+ }
12
+ export declare function createMcpServer(rootPath: string, options?: McpServerOptions): McpServerHandle;
5
13
  export declare function runMcpServer(rootPath: string): Promise<void>;