preflight-mcp 0.5.0 → 0.5.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/README.md CHANGED
@@ -310,20 +310,39 @@ Parameters:
310
310
  - `min_confidence`: 0-1 (default: 0.85)
311
311
  - `limit`: Max suggestions (default: 50)
312
312
 
313
- ### `preflight_deep_analyze_bundle` *(NEW v0.4.0)*
314
- One-call deep analysis aggregating tree, search, deps, and traces.
313
+ ### `preflight_deep_analyze_bundle` *(Enhanced v0.5.1)*
314
+ One-call deep analysis aggregating tree, search, deps, traces, **overview content**, and **test detection**.
315
315
  - Returns unified evidence pack with LLM-friendly summary
316
+ - **Now includes OVERVIEW.md, START_HERE.md, AGENTS.md, README content** (v0.5.1)
317
+ - **Auto-detects test frameworks** (jest, vitest, pytest, go, mocha) (v0.5.1)
318
+ - **Generates copyable `nextCommands`** for follow-up actions (v0.5.1)
316
319
  - Auto-generates **claims** with evidence references
317
320
  - Tracks analysis progress via **checklistStatus**
318
321
  - Reports unanswered questions as **openQuestions**
319
- - Triggers: "deep analyze", "comprehensive analysis", "深度分析"
322
+ - Triggers: "deep analyze", "comprehensive analysis", "深度分析", "快速了解项目"
323
+
324
+ **New in v0.5.1 - Aggregated content (reduces round-trips):**
325
+ - `includeOverview` (default: true): Include OVERVIEW.md, START_HERE.md, AGENTS.md
326
+ - `includeReadme` (default: true): Include repo README.md
327
+ - `includeTests` (default: true): Detect test directories and frameworks
320
328
 
321
329
  Output includes:
330
+ - `overviewContent`: `{overview, startHere, agents, readme}` - bundle documentation content
331
+ - `testInfo`: `{detected, framework, testDirs, testFileCount, configFiles, hint}` - test detection result
332
+ - `nextCommands[]`: Copyable tool calls for next steps (can be directly used as arguments)
322
333
  - `claims[]`: Auto-generated findings with evidence
323
334
  - `checklistStatus`: Analysis progress (repo_tree, deps, entrypoints, etc.)
324
335
  - `openQuestions[]`: Questions with `nextEvidenceToFetch` hints
325
336
  - `summary`: Markdown summary with checklist and key findings
326
337
 
338
+ **Example `nextCommands` output:**
339
+ ```json
340
+ [
341
+ { "tool": "preflight_search_bundle", "description": "Search for specific code", "args": { "bundleId": "...", "query": "<填入关键词>" } },
342
+ { "tool": "preflight_read_file", "description": "Read core module", "args": { "bundleId": "...", "file": "src/server.ts" } }
343
+ ]
344
+ ```
345
+
327
346
  ### `preflight_validate_report` *(NEW v0.4.0)*
328
347
  Validate claims and evidence chains for auditability.
329
348
  - Checks: missing evidence, invalid file references, broken snippet hashes
@@ -8,7 +8,7 @@ import { createEmptyCoverageReport, isCoverageSufficient, } from '../types/evide
8
8
  * This is called by the server after gathering data from each source.
9
9
  */
10
10
  export function buildDeepAnalysis(bundleId, components) {
11
- const { tree, search, deps, traces, focusPath, focusQuery, errors = [] } = components;
11
+ const { tree, search, deps, traces, overviewContent, testInfo, focusPath, focusQuery, errors = [] } = components;
12
12
  // Build coverage report
13
13
  const coverageReport = createEmptyCoverageReport();
14
14
  if (tree) {
@@ -75,9 +75,28 @@ export function buildDeepAnalysis(bundleId, components) {
75
75
  }
76
76
  summaryParts.push('');
77
77
  }
78
+ // Test detection summary
79
+ if (testInfo) {
80
+ summaryParts.push(`## Test Detection`);
81
+ if (testInfo.detected) {
82
+ summaryParts.push(`- Framework: ${testInfo.framework ?? 'unknown'}`);
83
+ summaryParts.push(`- Test files: ${testInfo.testFileCount}`);
84
+ if (testInfo.testDirs.length > 0) {
85
+ summaryParts.push(`- Test directories: ${testInfo.testDirs.slice(0, 3).join(', ')}`);
86
+ }
87
+ if (testInfo.configFiles.length > 0) {
88
+ summaryParts.push(`- Config files: ${testInfo.configFiles.join(', ')}`);
89
+ }
90
+ }
91
+ else {
92
+ summaryParts.push(`- No tests detected`);
93
+ }
94
+ summaryParts.push(`- 💡 ${testInfo.hint}`);
95
+ summaryParts.push('');
96
+ }
78
97
  // Build checklist status
79
98
  const checklistStatus = {
80
- read_overview: false, // Would need OVERVIEW.md read - caller should set
99
+ read_overview: !!(overviewContent?.overview || overviewContent?.startHere),
81
100
  repo_tree: !!tree && tree.totalFiles > 0,
82
101
  search_focus: !!search && search.totalHits > 0,
83
102
  dependency_graph_global: !!deps && deps.totalNodes > 0,
@@ -271,6 +290,51 @@ export function buildDeepAnalysis(bundleId, components) {
271
290
  if (isCoverageSufficient(coverageReport) && openQuestions.length === 0 && claims.length > 0) {
272
291
  nextSteps.push('🎉 Analysis complete - all key areas covered. Ready for detailed review.');
273
292
  }
293
+ // Build nextCommands (copyable JSON for LLM)
294
+ const nextCommands = [];
295
+ // Always suggest search as a useful next step
296
+ nextCommands.push({
297
+ tool: 'preflight_search_bundle',
298
+ description: 'Search for specific code or concepts',
299
+ args: { bundleId, query: '<填入关键词>', scope: 'all', limit: 30 },
300
+ });
301
+ // Suggest reading a specific entry point if identified
302
+ if (deps && deps.topImported.length > 0) {
303
+ const coreFile = deps.topImported[0].file;
304
+ nextCommands.push({
305
+ tool: 'preflight_read_file',
306
+ description: `Read core module: ${coreFile}`,
307
+ args: { bundleId, file: coreFile, withLineNumbers: true },
308
+ });
309
+ }
310
+ // Suggest dependency analysis for a specific file if entry point identified
311
+ if (deps && deps.topImporters.length > 0) {
312
+ const entryFile = deps.topImporters[0].file;
313
+ nextCommands.push({
314
+ tool: 'preflight_evidence_dependency_graph',
315
+ description: `Analyze dependencies of entry point: ${entryFile}`,
316
+ args: { bundleId, target: { file: entryFile } },
317
+ });
318
+ }
319
+ // Suggest trace discovery if no traces exist
320
+ if (!traces || traces.totalLinks === 0) {
321
+ nextCommands.push({
322
+ tool: 'preflight_suggest_traces',
323
+ description: 'Auto-discover test↔code relationships',
324
+ args: { bundleId, edge_type: 'tested_by', scope: 'repo' },
325
+ });
326
+ }
327
+ // Suggest focused tree if large directory detected
328
+ if (tree) {
329
+ const largeDir = tree.topDirs.find(d => d.fileCount > 50);
330
+ if (largeDir) {
331
+ nextCommands.push({
332
+ tool: 'preflight_repo_tree',
333
+ description: `Explore large directory: ${largeDir.path}`,
334
+ args: { bundleId, focusDir: largeDir.path, depth: 6 },
335
+ });
336
+ }
337
+ }
274
338
  // Add checklist and claims to summary
275
339
  summaryParts.push(`## Analysis Checklist`);
276
340
  const checklistItems = [
@@ -307,11 +371,104 @@ export function buildDeepAnalysis(bundleId, components) {
307
371
  search,
308
372
  deps,
309
373
  traces,
374
+ overviewContent,
375
+ testInfo,
310
376
  claims,
311
377
  checklistStatus,
312
378
  openQuestions,
313
379
  coverageReport,
314
380
  summary: summaryParts.join('\n'),
315
381
  nextSteps,
382
+ nextCommands,
383
+ };
384
+ }
385
+ /**
386
+ * Detect test setup from file tree statistics.
387
+ * Scans for test directories, test files, and framework config files.
388
+ */
389
+ export function detectTestInfo(stats, filesFound) {
390
+ const testDirs = [];
391
+ let testFileCount = 0;
392
+ const configFiles = [];
393
+ let framework = null;
394
+ // Common test directory patterns
395
+ const testDirPatterns = ['tests', 'test', '__tests__', 'spec', 'specs', 'e2e', 'integration'];
396
+ // Check byTopDir for test directories
397
+ if (stats.byTopDir) {
398
+ for (const [dir, count] of Object.entries(stats.byTopDir)) {
399
+ const dirLower = dir.toLowerCase();
400
+ if (testDirPatterns.some(p => dirLower === p || dirLower.endsWith('/' + p))) {
401
+ testDirs.push(dir);
402
+ testFileCount += count;
403
+ }
404
+ }
405
+ }
406
+ // Framework detection from config files (if filesFound provided)
407
+ const frameworkConfigs = [
408
+ { pattern: /^jest\.config\.(js|ts|mjs|cjs|json)$/i, framework: 'jest' },
409
+ { pattern: /^vitest\.config\.(js|ts|mjs|cjs)$/i, framework: 'vitest' },
410
+ { pattern: /^pytest\.ini$/i, framework: 'pytest' },
411
+ { pattern: /^pyproject\.toml$/i, framework: 'pytest' }, // May contain pytest config
412
+ { pattern: /^setup\.cfg$/i, framework: 'pytest' },
413
+ { pattern: /^\.mocharc\.(js|json|yml|yaml)$/i, framework: 'mocha' },
414
+ { pattern: /^mocha\.opts$/i, framework: 'mocha' },
415
+ ];
416
+ if (filesFound) {
417
+ for (const file of filesFound) {
418
+ for (const cfg of frameworkConfigs) {
419
+ if (cfg.pattern.test(file.name)) {
420
+ configFiles.push(file.path);
421
+ if (!framework) {
422
+ framework = cfg.framework;
423
+ }
424
+ }
425
+ }
426
+ }
427
+ }
428
+ // Infer framework from file extensions if not detected from config
429
+ if (!framework && stats.byExtension) {
430
+ // Check for test file patterns in extensions
431
+ const hasTs = (stats.byExtension['.ts'] ?? 0) > 0 || (stats.byExtension['.tsx'] ?? 0) > 0;
432
+ const hasPy = (stats.byExtension['.py'] ?? 0) > 0;
433
+ const hasGo = (stats.byExtension['.go'] ?? 0) > 0;
434
+ if (testDirs.length > 0 || testFileCount > 0) {
435
+ if (hasGo)
436
+ framework = 'go';
437
+ else if (hasPy)
438
+ framework = 'pytest';
439
+ else if (hasTs)
440
+ framework = 'unknown'; // Could be jest/vitest/mocha
441
+ }
442
+ }
443
+ // Count test files by pattern (approximate from extensions)
444
+ // This is a heuristic - actual test files may vary
445
+ if (testFileCount === 0 && stats.byExtension) {
446
+ // If no test directories found, estimate based on common patterns
447
+ // This is imprecise but gives a hint
448
+ }
449
+ const detected = testDirs.length > 0 || testFileCount > 0 || configFiles.length > 0;
450
+ // Generate hint based on detection results
451
+ let hint;
452
+ if (detected) {
453
+ if (testFileCount > 0) {
454
+ hint = `Found ${testFileCount} test files. Run preflight_suggest_traces to map code↔test relationships.`;
455
+ }
456
+ else if (configFiles.length > 0) {
457
+ hint = `Test config found (${configFiles[0]}). Run preflight_suggest_traces to discover test files.`;
458
+ }
459
+ else {
460
+ hint = `Test directories found. Run preflight_suggest_traces to map code↔test relationships.`;
461
+ }
462
+ }
463
+ else {
464
+ hint = 'No tests detected. Consider adding tests or check if test files use non-standard naming.';
465
+ }
466
+ return {
467
+ detected,
468
+ framework,
469
+ testDirs,
470
+ testFileCount,
471
+ configFiles,
472
+ hint,
316
473
  };
317
474
  }
@@ -72,11 +72,21 @@ async function buildIgnore(repoRoot) {
72
72
  }
73
73
  return ig;
74
74
  }
75
- async function* walkFiles(repoRoot, ig) {
75
+ async function* walkFiles(repoRoot, ig, onSkip) {
76
76
  const stack = [repoRoot];
77
77
  while (stack.length) {
78
78
  const dir = stack.pop();
79
- const entries = await fs.readdir(dir, { withFileTypes: true });
79
+ let entries;
80
+ try {
81
+ entries = await fs.readdir(dir, { withFileTypes: true });
82
+ }
83
+ catch (err) {
84
+ // Skip directories that cannot be read (broken symlinks, permission issues, etc.)
85
+ const rel = path.relative(repoRoot, dir);
86
+ const relPosix = toPosix(rel);
87
+ onSkip?.(relPosix, `cannot read directory: ${err.code ?? 'unknown'}`);
88
+ continue;
89
+ }
80
90
  for (const ent of entries) {
81
91
  const abs = path.join(dir, ent.name);
82
92
  const rel = path.relative(repoRoot, abs);
@@ -85,12 +95,29 @@ async function* walkFiles(repoRoot, ig) {
85
95
  if (ig.ignores(relPosix)) {
86
96
  continue;
87
97
  }
88
- if (ent.isDirectory()) {
89
- stack.push(abs);
90
- continue;
98
+ // Handle symlinks and other special entries
99
+ try {
100
+ if (ent.isSymbolicLink()) {
101
+ // Check if symlink target exists
102
+ try {
103
+ await fs.stat(abs);
104
+ }
105
+ catch {
106
+ onSkip?.(relPosix, 'broken symlink');
107
+ continue;
108
+ }
109
+ }
110
+ if (ent.isDirectory()) {
111
+ stack.push(abs);
112
+ continue;
113
+ }
114
+ if (!ent.isFile())
115
+ continue;
91
116
  }
92
- if (!ent.isFile())
117
+ catch (err) {
118
+ onSkip?.(relPosix, `cannot stat: ${err.code ?? 'unknown'}`);
93
119
  continue;
120
+ }
94
121
  yield { absPath: abs, relPosix };
95
122
  }
96
123
  }
@@ -101,7 +128,9 @@ export async function ingestRepoToBundle(params) {
101
128
  const files = [];
102
129
  const skipped = [];
103
130
  const decoder = new TextDecoder('utf-8', { fatal: true });
104
- for await (const f of walkFiles(params.repoRoot, ig)) {
131
+ for await (const f of walkFiles(params.repoRoot, ig, (relPosix, reason) => {
132
+ skipped.push(`${relPosix} (${reason})`);
133
+ })) {
105
134
  // ignore check already done in walkFiles
106
135
  const st = await fs.stat(f.absPath);
107
136
  if (st.size > params.options.maxFileBytes) {
package/dist/server.js CHANGED
@@ -19,7 +19,7 @@ import { DependencyGraphInputSchema, generateDependencyGraph } from './evidence/
19
19
  import { TraceQueryInputSchema, TraceUpsertInputSchema, traceQuery, traceUpsert } from './trace/service.js';
20
20
  import { suggestTestedByTraces } from './trace/suggest.js';
21
21
  import { generateRepoTree, formatTreeResult } from './bundle/tree.js';
22
- import { buildDeepAnalysis } from './analysis/deep.js';
22
+ import { buildDeepAnalysis, detectTestInfo } from './analysis/deep.js';
23
23
  import { validateReport } from './analysis/validate.js';
24
24
  // RFC v2: New aggregation tools
25
25
  import { ReadFilesInputSchema, createReadFilesHandler, readFilesToolDescription } from './tools/readFiles.js';
@@ -147,7 +147,7 @@ export async function startServer() {
147
147
  startHttpServer(cfg);
148
148
  const server = new McpServer({
149
149
  name: 'preflight-mcp',
150
- version: '0.5.0',
150
+ version: '0.5.2',
151
151
  description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
152
152
  }, {
153
153
  capabilities: {
@@ -2071,6 +2071,9 @@ export async function startServer() {
2071
2071
  includeSearch: z.boolean().optional().default(true).describe('Include search results (requires focus.query).'),
2072
2072
  includeDeps: z.boolean().optional().default(true).describe('Include dependency analysis.'),
2073
2073
  includeTraces: z.boolean().optional().default(true).describe('Include trace link summary.'),
2074
+ includeOverview: z.boolean().optional().default(true).describe('Include OVERVIEW.md, START_HERE.md, AGENTS.md content.'),
2075
+ includeReadme: z.boolean().optional().default(true).describe('Include README.md content from repos.'),
2076
+ includeTests: z.boolean().optional().default(true).describe('Detect test directories and frameworks.'),
2074
2077
  tokenBudget: z.number().int().optional().describe('Soft limit on output tokens (reduces detail if exceeded).'),
2075
2078
  maxFiles: z.number().int().min(10).max(1000).optional().default(500).describe('Max files to scan for tree/deps.'),
2076
2079
  }).optional().describe('Analysis options.'),
@@ -2103,6 +2106,20 @@ export async function startServer() {
2103
2106
  byType: z.record(z.string(), z.number()),
2104
2107
  coverageEstimate: z.number(),
2105
2108
  }).optional(),
2109
+ overviewContent: z.object({
2110
+ overview: z.string().optional(),
2111
+ startHere: z.string().optional(),
2112
+ agents: z.string().optional(),
2113
+ readme: z.string().optional(),
2114
+ }).optional().describe('Overview content from bundle files (OVERVIEW.md, START_HERE.md, AGENTS.md, README.md).'),
2115
+ testInfo: z.object({
2116
+ detected: z.boolean(),
2117
+ framework: z.enum(['jest', 'vitest', 'pytest', 'go', 'mocha', 'unknown']).nullable(),
2118
+ testDirs: z.array(z.string()),
2119
+ testFileCount: z.number(),
2120
+ configFiles: z.array(z.string()),
2121
+ hint: z.string(),
2122
+ }).optional().describe('Test detection result.'),
2106
2123
  claims: z.array(z.object({
2107
2124
  id: z.string(),
2108
2125
  text: z.string(),
@@ -2129,7 +2146,12 @@ export async function startServer() {
2129
2146
  })).describe('Questions that could not be answered.'),
2130
2147
  coverageReport: z.any(),
2131
2148
  summary: z.string().describe('LLM-formatted analysis summary with checklist and claims.'),
2132
- nextSteps: z.array(z.string()),
2149
+ nextSteps: z.array(z.string()).describe('Human-readable next step suggestions.'),
2150
+ nextCommands: z.array(z.object({
2151
+ tool: z.string(),
2152
+ description: z.string(),
2153
+ args: z.record(z.string(), z.unknown()),
2154
+ })).describe('Copyable next commands for LLM/automation - can be directly used as tool call arguments.'),
2133
2155
  // RFC v2: Top-level evidence aggregation
2134
2156
  evidence: z.array(z.object({
2135
2157
  path: z.string(),
@@ -2159,6 +2181,8 @@ export async function startServer() {
2159
2181
  let search;
2160
2182
  let deps;
2161
2183
  let traces;
2184
+ let overviewContent;
2185
+ let testInfo;
2162
2186
  // 1. Tree
2163
2187
  if (opts.includeTree ?? true) {
2164
2188
  try {
@@ -2280,11 +2304,89 @@ export async function startServer() {
2280
2304
  traces = { totalLinks: 0, byType: {}, coverageEstimate: 0 };
2281
2305
  }
2282
2306
  }
2307
+ // 5. Overview content (OVERVIEW.md, START_HERE.md, AGENTS.md)
2308
+ if (opts.includeOverview ?? true) {
2309
+ overviewContent = {};
2310
+ const readFile = async (filename) => {
2311
+ try {
2312
+ const absPath = safeJoin(paths.rootDir, filename);
2313
+ return await fs.readFile(absPath, 'utf8');
2314
+ }
2315
+ catch {
2316
+ return undefined;
2317
+ }
2318
+ };
2319
+ overviewContent.overview = await readFile('OVERVIEW.md');
2320
+ overviewContent.startHere = await readFile('START_HERE.md');
2321
+ overviewContent.agents = await readFile('AGENTS.md');
2322
+ }
2323
+ // 6. README content (from repos)
2324
+ if (opts.includeReadme ?? true) {
2325
+ if (!overviewContent)
2326
+ overviewContent = {};
2327
+ try {
2328
+ const manifest = await readManifest(paths.manifestPath);
2329
+ for (const repo of manifest.repos ?? []) {
2330
+ if (!repo.id)
2331
+ continue;
2332
+ const [owner, repoName] = repo.id.split('/');
2333
+ if (!owner || !repoName)
2334
+ continue;
2335
+ const readmeNames = ['README.md', 'readme.md', 'Readme.md'];
2336
+ for (const readmeName of readmeNames) {
2337
+ const readmePath = `repos/${owner}/${repoName}/norm/${readmeName}`;
2338
+ try {
2339
+ const absPath = safeJoin(paths.rootDir, readmePath);
2340
+ overviewContent.readme = await fs.readFile(absPath, 'utf8');
2341
+ break; // Found README, stop searching
2342
+ }
2343
+ catch {
2344
+ // Try next README name
2345
+ }
2346
+ }
2347
+ if (overviewContent.readme)
2348
+ break; // Only read first repo's README
2349
+ }
2350
+ }
2351
+ catch {
2352
+ // Ignore manifest read errors
2353
+ }
2354
+ }
2355
+ // 7. Test detection
2356
+ if ((opts.includeTests ?? true) && tree) {
2357
+ // Collect files for framework detection
2358
+ const filesFound = [];
2359
+ try {
2360
+ // Scan for config files at bundle root
2361
+ const configPatterns = [
2362
+ 'jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.json',
2363
+ 'vitest.config.js', 'vitest.config.ts',
2364
+ 'pytest.ini', 'pyproject.toml', 'setup.cfg',
2365
+ '.mocharc.js', '.mocharc.json', '.mocharc.yml',
2366
+ ];
2367
+ for (const cfg of configPatterns) {
2368
+ try {
2369
+ const cfgPath = safeJoin(paths.rootDir, cfg);
2370
+ await fs.access(cfgPath);
2371
+ filesFound.push({ path: cfg, name: cfg });
2372
+ }
2373
+ catch {
2374
+ // Config file doesn't exist
2375
+ }
2376
+ }
2377
+ }
2378
+ catch {
2379
+ // Ignore errors during config scanning
2380
+ }
2381
+ testInfo = detectTestInfo({ byExtension: tree.byExtension, byTopDir: tree.topDirs.reduce((acc, d) => ({ ...acc, [d.path]: d.fileCount }), {}) }, filesFound.length > 0 ? filesFound : undefined);
2382
+ }
2283
2383
  const result = buildDeepAnalysis(args.bundleId, {
2284
2384
  tree,
2285
2385
  search,
2286
2386
  deps,
2287
2387
  traces,
2388
+ overviewContent,
2389
+ testInfo,
2288
2390
  focusPath: focus.path,
2289
2391
  focusQuery: focus.query,
2290
2392
  errors,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "preflight-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "MCP server that creates evidence-based preflight bundles for GitHub repositories and library docs.",
5
5
  "type": "module",
6
6
  "license": "MIT",