preflight-mcp 0.4.0 → 0.4.2
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.
- package/dist/bundle/service.js +63 -2
- package/dist/server.js +102 -4
- package/package.json +1 -1
package/dist/bundle/service.js
CHANGED
|
@@ -20,6 +20,50 @@ const DEDUP_INDEX_FILE = '.preflight-dedup-index.json';
|
|
|
20
20
|
function sha256Hex(text) {
|
|
21
21
|
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse a skipped file string (from ingest) into structured SkippedFileEntry.
|
|
25
|
+
* Formats:
|
|
26
|
+
* - "path/to/file (too large: 12345 bytes)"
|
|
27
|
+
* - "path/to/file (binary)"
|
|
28
|
+
* - "path/to/file (non-utf8)"
|
|
29
|
+
* - "(bundle maxTotalBytes reached) stopped before: path/to/file"
|
|
30
|
+
*/
|
|
31
|
+
function parseSkippedString(s, repoId) {
|
|
32
|
+
// Pattern: "path (too large: 12345 bytes)"
|
|
33
|
+
const tooLargeMatch = s.match(/^(.+?) \(too large: (\d+) bytes\)$/);
|
|
34
|
+
if (tooLargeMatch) {
|
|
35
|
+
return {
|
|
36
|
+
path: `${repoId}/${tooLargeMatch[1]}`,
|
|
37
|
+
reason: 'too_large',
|
|
38
|
+
size: parseInt(tooLargeMatch[2], 10),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Pattern: "path (binary)"
|
|
42
|
+
const binaryMatch = s.match(/^(.+?) \(binary\)$/);
|
|
43
|
+
if (binaryMatch) {
|
|
44
|
+
return {
|
|
45
|
+
path: `${repoId}/${binaryMatch[1]}`,
|
|
46
|
+
reason: 'binary',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Pattern: "path (non-utf8)"
|
|
50
|
+
const nonUtf8Match = s.match(/^(.+?) \(non-utf8\)$/);
|
|
51
|
+
if (nonUtf8Match) {
|
|
52
|
+
return {
|
|
53
|
+
path: `${repoId}/${nonUtf8Match[1]}`,
|
|
54
|
+
reason: 'non_utf8',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// Pattern: "(bundle maxTotalBytes reached) stopped before: path"
|
|
58
|
+
const maxTotalMatch = s.match(/^\(bundle maxTotalBytes reached\) stopped before: (.+)$/);
|
|
59
|
+
if (maxTotalMatch) {
|
|
60
|
+
return {
|
|
61
|
+
path: `${repoId}/${maxTotalMatch[1]}`,
|
|
62
|
+
reason: 'max_total_reached',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
23
67
|
function normalizeList(values) {
|
|
24
68
|
return (values ?? [])
|
|
25
69
|
.map((s) => s.trim())
|
|
@@ -939,6 +983,7 @@ async function createBundleInternal(cfg, input, options) {
|
|
|
939
983
|
await ensureDir(tmpPaths.rootDir);
|
|
940
984
|
const finalPaths = getBundlePaths(effectiveStorageDir, bundleId);
|
|
941
985
|
const allIngestedFiles = [];
|
|
986
|
+
const allSkippedFiles = [];
|
|
942
987
|
const reposSummary = [];
|
|
943
988
|
const allWarnings = [];
|
|
944
989
|
// Track temp checkout directory for cleanup
|
|
@@ -970,9 +1015,16 @@ async function createBundleInternal(cfg, input, options) {
|
|
|
970
1015
|
});
|
|
971
1016
|
allIngestedFiles.push(...files);
|
|
972
1017
|
allWarnings.push(...warnings);
|
|
1018
|
+
// Parse and collect skipped files
|
|
1019
|
+
const repoId = `${owner}/${repo}`;
|
|
1020
|
+
for (const s of skipped) {
|
|
1021
|
+
const entry = parseSkippedString(s, repoId);
|
|
1022
|
+
if (entry)
|
|
1023
|
+
allSkippedFiles.push(entry);
|
|
1024
|
+
}
|
|
973
1025
|
reposSummary.push({
|
|
974
1026
|
kind: 'github',
|
|
975
|
-
id:
|
|
1027
|
+
id: repoId,
|
|
976
1028
|
source,
|
|
977
1029
|
headSha,
|
|
978
1030
|
notes: [...notes, ...skipped].slice(0, 50),
|
|
@@ -993,7 +1045,14 @@ async function createBundleInternal(cfg, input, options) {
|
|
|
993
1045
|
ref: repoInput.ref,
|
|
994
1046
|
});
|
|
995
1047
|
allIngestedFiles.push(...files);
|
|
996
|
-
|
|
1048
|
+
// Parse and collect skipped files
|
|
1049
|
+
const repoId = `${owner}/${repo}`;
|
|
1050
|
+
for (const s of skipped) {
|
|
1051
|
+
const entry = parseSkippedString(s, repoId);
|
|
1052
|
+
if (entry)
|
|
1053
|
+
allSkippedFiles.push(entry);
|
|
1054
|
+
}
|
|
1055
|
+
reposSummary.push({ kind: 'local', id: repoId, source: 'local', notes: skipped.slice(0, 50) });
|
|
997
1056
|
}
|
|
998
1057
|
}
|
|
999
1058
|
// Context7 libraries (best-effort).
|
|
@@ -1061,6 +1120,8 @@ async function createBundleInternal(cfg, input, options) {
|
|
|
1061
1120
|
includeDocs: true,
|
|
1062
1121
|
includeCode: true,
|
|
1063
1122
|
},
|
|
1123
|
+
// Store skipped files for transparency (limit to 200 entries to avoid bloat)
|
|
1124
|
+
skippedFiles: allSkippedFiles.length > 0 ? allSkippedFiles.slice(0, 200) : undefined,
|
|
1064
1125
|
};
|
|
1065
1126
|
await writeManifest(tmpPaths.manifestPath, manifest);
|
|
1066
1127
|
// Guides.
|
package/dist/server.js
CHANGED
|
@@ -134,7 +134,7 @@ export async function startServer() {
|
|
|
134
134
|
startHttpServer(cfg);
|
|
135
135
|
const server = new McpServer({
|
|
136
136
|
name: 'preflight-mcp',
|
|
137
|
-
version: '0.4.
|
|
137
|
+
version: '0.4.2',
|
|
138
138
|
description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
|
|
139
139
|
}, {
|
|
140
140
|
capabilities: {
|
|
@@ -547,6 +547,7 @@ export async function startServer() {
|
|
|
547
547
|
focusDir: z.string().optional().describe('Focus directory path - expand deeper within this path (e.g., "owner/repo/norm/src"). Gets +3 extra depth levels.'),
|
|
548
548
|
focusDepthBonus: z.number().int().min(1).max(6).optional().describe('Extra depth levels for focusDir. Default 3.'),
|
|
549
549
|
showFileCountPerDir: z.boolean().optional().describe('If true, include file count per directory in stats.byDir.'),
|
|
550
|
+
showSkippedFiles: z.boolean().optional().describe('If true, include list of files that were skipped during indexing (too large, binary, etc.). Helps understand what content is NOT searchable.'),
|
|
550
551
|
},
|
|
551
552
|
outputSchema: {
|
|
552
553
|
bundleId: z.string(),
|
|
@@ -563,6 +564,11 @@ export async function startServer() {
|
|
|
563
564
|
type: z.enum(['readme', 'main', 'index', 'cli', 'server', 'app', 'test', 'config']),
|
|
564
565
|
priority: z.number(),
|
|
565
566
|
})),
|
|
567
|
+
skippedFiles: z.array(z.object({
|
|
568
|
+
path: z.string(),
|
|
569
|
+
reason: z.string(),
|
|
570
|
+
size: z.number().optional(),
|
|
571
|
+
})).optional().describe('Files skipped during indexing (only when showSkippedFiles=true). These files are NOT searchable.'),
|
|
566
572
|
},
|
|
567
573
|
annotations: {
|
|
568
574
|
readOnlyHint: true,
|
|
@@ -582,10 +588,45 @@ export async function startServer() {
|
|
|
582
588
|
focusDepthBonus: args.focusDepthBonus,
|
|
583
589
|
showFileCountPerDir: args.showFileCountPerDir,
|
|
584
590
|
});
|
|
591
|
+
// Add skipped files if requested
|
|
592
|
+
let skippedFiles;
|
|
593
|
+
if (args.showSkippedFiles) {
|
|
594
|
+
try {
|
|
595
|
+
const manifest = await readManifest(paths.manifestPath);
|
|
596
|
+
if (manifest.skippedFiles && manifest.skippedFiles.length > 0) {
|
|
597
|
+
const reasonLabels = {
|
|
598
|
+
too_large: 'too large',
|
|
599
|
+
binary: 'binary file',
|
|
600
|
+
non_utf8: 'non-UTF8 encoding',
|
|
601
|
+
max_total_reached: 'bundle size limit reached',
|
|
602
|
+
};
|
|
603
|
+
skippedFiles = manifest.skippedFiles.map(s => ({
|
|
604
|
+
path: s.path,
|
|
605
|
+
reason: reasonLabels[s.reason] ?? s.reason,
|
|
606
|
+
size: s.size,
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
// Ignore manifest read errors
|
|
612
|
+
}
|
|
613
|
+
}
|
|
585
614
|
const textOutput = formatTreeResult(result);
|
|
615
|
+
let fullTextOutput = textOutput;
|
|
616
|
+
if (skippedFiles && skippedFiles.length > 0) {
|
|
617
|
+
fullTextOutput += `\n\n## Skipped Files (${skippedFiles.length} files not searchable)\n`;
|
|
618
|
+
for (const sf of skippedFiles.slice(0, 20)) {
|
|
619
|
+
const sizeStr = sf.size ? ` (${(sf.size / 1024).toFixed(0)}KB)` : '';
|
|
620
|
+
fullTextOutput += `- ${sf.path}: ${sf.reason}${sizeStr}\n`;
|
|
621
|
+
}
|
|
622
|
+
if (skippedFiles.length > 20) {
|
|
623
|
+
fullTextOutput += `... and ${skippedFiles.length - 20} more\n`;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const structuredResult = { ...result, skippedFiles };
|
|
586
627
|
return {
|
|
587
|
-
content: [{ type: 'text', text:
|
|
588
|
-
structuredContent:
|
|
628
|
+
content: [{ type: 'text', text: fullTextOutput }],
|
|
629
|
+
structuredContent: structuredResult,
|
|
589
630
|
};
|
|
590
631
|
}
|
|
591
632
|
catch (err) {
|
|
@@ -708,7 +749,11 @@ export async function startServer() {
|
|
|
708
749
|
});
|
|
709
750
|
server.registerTool('preflight_create_bundle', {
|
|
710
751
|
title: 'Create bundle',
|
|
711
|
-
description: 'Create a new bundle from GitHub repos or local directories.
|
|
752
|
+
description: 'Create a new bundle from GitHub repos or local directories. ' +
|
|
753
|
+
'**Safe to call proactively** - use `ifExists: "returnExisting"` to avoid duplicates. ' +
|
|
754
|
+
'Bundle creation is a **read-only collection** operation (clones repo, builds index, generates guides). ' +
|
|
755
|
+
'When user asks to analyze/understand a project, create the bundle first if it does not exist. ' +
|
|
756
|
+
'Use when: "analyze this repo", "understand this codebase", "index project", "分析项目", "理解代码".',
|
|
712
757
|
inputSchema: CreateBundleInputSchema,
|
|
713
758
|
outputSchema: {
|
|
714
759
|
// Normal completion fields
|
|
@@ -1084,6 +1129,16 @@ export async function startServer() {
|
|
|
1084
1129
|
meta: z.object({
|
|
1085
1130
|
tokenBudgetHint: z.string().optional().describe('Hint about token savings from groupByFile.'),
|
|
1086
1131
|
}).optional().describe('Search metadata for EDDA optimization.'),
|
|
1132
|
+
// Skipped files hint for transparency
|
|
1133
|
+
skippedFilesHint: z.object({
|
|
1134
|
+
message: z.string().describe('Human-readable hint explaining why some content might be missing.'),
|
|
1135
|
+
skippedCount: z.number().describe('Total number of files skipped during indexing.'),
|
|
1136
|
+
examples: z.array(z.object({
|
|
1137
|
+
path: z.string(),
|
|
1138
|
+
reason: z.string(),
|
|
1139
|
+
size: z.number().optional(),
|
|
1140
|
+
})).describe('Example skipped files (limited to 5).'),
|
|
1141
|
+
}).optional().describe('Present when search returns 0 results and files were skipped during indexing. Helps explain why expected content might be missing.'),
|
|
1087
1142
|
autoUpdated: z
|
|
1088
1143
|
.boolean()
|
|
1089
1144
|
.optional()
|
|
@@ -1130,6 +1185,34 @@ export async function startServer() {
|
|
|
1130
1185
|
});
|
|
1131
1186
|
}
|
|
1132
1187
|
const paths = getBundlePathsForId(storageDir, args.bundleId);
|
|
1188
|
+
// Helper to build skippedFilesHint when search returns 0 results
|
|
1189
|
+
const buildSkippedFilesHint = async () => {
|
|
1190
|
+
try {
|
|
1191
|
+
const manifest = await readManifest(paths.manifestPath);
|
|
1192
|
+
if (!manifest.skippedFiles || manifest.skippedFiles.length === 0) {
|
|
1193
|
+
return undefined;
|
|
1194
|
+
}
|
|
1195
|
+
const skipped = manifest.skippedFiles;
|
|
1196
|
+
const reasonLabels = {
|
|
1197
|
+
too_large: 'too large',
|
|
1198
|
+
binary: 'binary file',
|
|
1199
|
+
non_utf8: 'non-UTF8 encoding',
|
|
1200
|
+
max_total_reached: 'bundle size limit reached',
|
|
1201
|
+
};
|
|
1202
|
+
return {
|
|
1203
|
+
message: `Search returned 0 results. Note: ${skipped.length} file(s) were skipped during indexing and are not searchable. Check if your target content might be in a skipped file.`,
|
|
1204
|
+
skippedCount: skipped.length,
|
|
1205
|
+
examples: skipped.slice(0, 5).map(s => ({
|
|
1206
|
+
path: s.path,
|
|
1207
|
+
reason: reasonLabels[s.reason] ?? s.reason,
|
|
1208
|
+
size: s.size,
|
|
1209
|
+
})),
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
catch {
|
|
1213
|
+
return undefined;
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1133
1216
|
// Use advanced search if EDDA features are requested
|
|
1134
1217
|
const useAdvanced = args.groupByFile || args.fileTypeFilters?.length || args.includeScore;
|
|
1135
1218
|
if (useAdvanced) {
|
|
@@ -1178,6 +1261,14 @@ export async function startServer() {
|
|
|
1178
1261
|
if (warnings.length > 0) {
|
|
1179
1262
|
out.warnings = warnings;
|
|
1180
1263
|
}
|
|
1264
|
+
// Add skippedFilesHint when 0 results
|
|
1265
|
+
const hasNoResults = result.hits.length === 0 && (!grouped || grouped.length === 0);
|
|
1266
|
+
if (hasNoResults) {
|
|
1267
|
+
const hint = await buildSkippedFilesHint();
|
|
1268
|
+
if (hint) {
|
|
1269
|
+
out.skippedFilesHint = hint;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1181
1272
|
return {
|
|
1182
1273
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
1183
1274
|
structuredContent: out,
|
|
@@ -1228,6 +1319,13 @@ export async function startServer() {
|
|
|
1228
1319
|
if (warnings.length > 0) {
|
|
1229
1320
|
out.warnings = warnings;
|
|
1230
1321
|
}
|
|
1322
|
+
// Add skippedFilesHint when 0 results
|
|
1323
|
+
if (hits.length === 0) {
|
|
1324
|
+
const hint = await buildSkippedFilesHint();
|
|
1325
|
+
if (hint) {
|
|
1326
|
+
out.skippedFilesHint = hint;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1231
1329
|
return {
|
|
1232
1330
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
1233
1331
|
structuredContent: out,
|