preflight-mcp 0.1.4 → 0.1.6

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.
@@ -7,7 +7,7 @@ function nowIso() {
7
7
  }
8
8
  function githubHeaders(cfg) {
9
9
  const headers = {
10
- 'User-Agent': 'preflight-mcp/0.1.3',
10
+ 'User-Agent': 'preflight-mcp/0.1.6',
11
11
  Accept: 'application/vnd.github+json',
12
12
  };
13
13
  if (cfg.githubToken) {
@@ -746,6 +746,7 @@ async function cloneAndIngestGitHubRepo(params) {
746
746
  let repoRootForIngest = tmpCheckoutGit;
747
747
  let headSha;
748
748
  const notes = [];
749
+ const warnings = [];
749
750
  let source = 'git';
750
751
  let fetchedAt = nowIso();
751
752
  let refUsed = params.ref;
@@ -763,23 +764,49 @@ async function cloneAndIngestGitHubRepo(params) {
763
764
  catch (err) {
764
765
  // Fallback: GitHub archive download (zipball) + extract.
765
766
  source = 'archive';
766
- const msg = err instanceof Error ? err.message : String(err);
767
- notes.push(`git clone failed; used GitHub archive fallback: ${msg}`);
767
+ const errMsg = err instanceof Error ? err.message : String(err);
768
+ notes.push(`git clone failed; used GitHub archive fallback: ${errMsg}`);
769
+ // User-facing warning: communicate the network issue clearly
770
+ warnings.push(`⚠️ [${repoId}] Git clone failed (network issue), switched to ZIP download.\n` +
771
+ ` Reason: ${errMsg.slice(0, 200)}${errMsg.length > 200 ? '...' : ''}`);
768
772
  params.onProgress?.('downloading', 0, `Downloading ${repoId} archive...`);
769
- const archive = await downloadAndExtractGitHubArchive({
770
- cfg: params.cfg,
771
- owner: params.owner,
772
- repo: params.repo,
773
- ref: params.ref,
774
- destDir: tmpArchiveDir,
775
- onProgress: (downloaded, total, msg) => {
776
- const percent = total ? Math.round((downloaded / total) * 100) : 0;
777
- params.onProgress?.('downloading', percent, `${repoId}: ${msg}`);
778
- },
779
- });
780
- repoRootForIngest = archive.repoRoot;
781
- fetchedAt = archive.fetchedAt;
782
- refUsed = archive.refUsed;
773
+ // Track zip path for error message
774
+ const zipPath = path.join(tmpArchiveDir, `github-zipball-${params.owner}-${params.repo}-partial.zip`);
775
+ try {
776
+ const archive = await downloadAndExtractGitHubArchive({
777
+ cfg: params.cfg,
778
+ owner: params.owner,
779
+ repo: params.repo,
780
+ ref: params.ref,
781
+ destDir: tmpArchiveDir,
782
+ onProgress: (downloaded, total, msg) => {
783
+ const percent = total ? Math.round((downloaded / total) * 100) : 0;
784
+ params.onProgress?.('downloading', percent, `${repoId}: ${msg}`);
785
+ },
786
+ });
787
+ repoRootForIngest = archive.repoRoot;
788
+ fetchedAt = archive.fetchedAt;
789
+ refUsed = archive.refUsed;
790
+ // Success: ZIP download completed
791
+ warnings.push(`✅ [${repoId}] ZIP download completed successfully as fallback.`);
792
+ }
793
+ catch (zipErr) {
794
+ // ZIP download also failed - provide helpful error with temp path
795
+ const zipErrMsg = zipErr instanceof Error ? zipErr.message : String(zipErr);
796
+ // Check if partial file exists
797
+ const partialExists = await statOrNull(tmpArchiveDir);
798
+ const tempPathMsg = partialExists
799
+ ? `\n Partial files may exist in: ${tmpArchiveDir}`
800
+ : '';
801
+ throw new Error(`Both git clone and ZIP download failed for ${repoId}.\n\n` +
802
+ `Git error: ${errMsg.slice(0, 150)}\n` +
803
+ `ZIP error: ${zipErrMsg.slice(0, 150)}${tempPathMsg}\n\n` +
804
+ `Suggestions:\n` +
805
+ `1. Check your network connection\n` +
806
+ `2. Verify the repository exists: https://github.com/${repoId}\n` +
807
+ `3. If you have the repo locally, use 'kind: local' with 'path: /your/local/path'\n` +
808
+ `4. If behind a proxy, configure GITHUB_TOKEN environment variable`);
809
+ }
783
810
  }
784
811
  const bundlePaths = getBundlePaths(params.storageDir, params.bundleId);
785
812
  const rawDest = repoRawDir(bundlePaths, params.owner, params.repo);
@@ -813,7 +840,7 @@ async function cloneAndIngestGitHubRepo(params) {
813
840
  });
814
841
  await rmIfExists(tmpCheckoutGit);
815
842
  await rmIfExists(tmpArchiveDir);
816
- return { headSha, files: ingested.files, skipped: ingested.skipped, notes, source };
843
+ return { headSha, files: ingested.files, skipped: ingested.skipped, notes, warnings, source };
817
844
  }
818
845
  function groupFilesByRepoId(files) {
819
846
  const byRepo = new Map();
@@ -912,6 +939,9 @@ async function createBundleInternal(cfg, input, options) {
912
939
  const finalPaths = getBundlePaths(effectiveStorageDir, bundleId);
913
940
  const allIngestedFiles = [];
914
941
  const reposSummary = [];
942
+ const allWarnings = [];
943
+ // Track temp checkout directory for cleanup
944
+ const tmpCheckoutsDir = path.join(cfg.tmpDir, 'checkouts', bundleId);
915
945
  try {
916
946
  // All operations happen in tmpPaths (temporary directory)
917
947
  const totalRepos = input.repos.length;
@@ -923,7 +953,7 @@ async function createBundleInternal(cfg, input, options) {
923
953
  const { owner, repo } = parseOwnerRepo(repoInput.repo);
924
954
  reportProgress('cloning', repoProgress, `[${repoIndex}/${totalRepos}] Fetching ${owner}/${repo}...`);
925
955
  tracker.updateProgress(taskId, 'cloning', repoProgress, `Fetching ${owner}/${repo}...`);
926
- const { headSha, files, skipped, notes, source } = await cloneAndIngestGitHubRepo({
956
+ const { headSha, files, skipped, notes, warnings, source } = await cloneAndIngestGitHubRepo({
927
957
  cfg,
928
958
  bundleId,
929
959
  storageDir: tmpBundlesDir,
@@ -938,6 +968,7 @@ async function createBundleInternal(cfg, input, options) {
938
968
  },
939
969
  });
940
970
  allIngestedFiles.push(...files);
971
+ allWarnings.push(...warnings);
941
972
  reposSummary.push({
942
973
  kind: 'github',
943
974
  id: `${owner}/${repo}`,
@@ -1112,6 +1143,7 @@ async function createBundleInternal(cfg, input, options) {
1112
1143
  updatedAt: createdAt,
1113
1144
  repos: reposSummary,
1114
1145
  libraries: librariesSummary,
1146
+ warnings: allWarnings.length > 0 ? allWarnings : undefined,
1115
1147
  };
1116
1148
  return summary;
1117
1149
  }
@@ -1135,6 +1167,10 @@ async function createBundleInternal(cfg, input, options) {
1135
1167
  await rmIfExists(tmpPaths.rootDir).catch((err) => {
1136
1168
  logger.debug('Failed to cleanup temp bundle directory in finally block (non-critical)', err instanceof Error ? err : undefined);
1137
1169
  });
1170
+ // Clean up temp checkouts directory (git clones, zip extracts)
1171
+ await rmIfExists(tmpCheckoutsDir).catch((err) => {
1172
+ logger.debug('Failed to cleanup temp checkouts directory in finally block (non-critical)', err instanceof Error ? err : undefined);
1173
+ });
1138
1174
  }
1139
1175
  }
1140
1176
  /** Check if a bundle has upstream changes without applying updates. */
@@ -1297,6 +1333,14 @@ export async function repairBundle(cfg, bundleId, options) {
1297
1333
  // Manifest is required for safe repairs (no fetching/re-ingest).
1298
1334
  const manifest = await readManifest(paths.manifestPath);
1299
1335
  const actionsTaken = [];
1336
+ const unfixableIssues = [];
1337
+ // Check for unfixable issues (require re-download, can't be repaired offline)
1338
+ const reposHasContent = before.missingComponents.every(c => !c.includes('repos/'));
1339
+ if (!reposHasContent) {
1340
+ unfixableIssues.push('repos/ directory is empty or missing - this requires re-downloading the repository. ' +
1341
+ 'Use preflight_delete_bundle and preflight_create_bundle to start fresh, ' +
1342
+ 'or use preflight_update_bundle with force:true to re-fetch.');
1343
+ }
1300
1344
  // Determine what needs repair.
1301
1345
  const stAgents = await statOrNull(paths.agentsPath);
1302
1346
  const stStartHere = await statOrNull(paths.startHerePath);
@@ -1384,6 +1428,7 @@ export async function repairBundle(cfg, bundleId, options) {
1384
1428
  mode,
1385
1429
  repaired: actionsTaken.length > 0,
1386
1430
  actionsTaken,
1431
+ unfixableIssues: unfixableIssues.length > 0 ? unfixableIssues : undefined,
1387
1432
  before,
1388
1433
  after,
1389
1434
  updatedAt,
@@ -20,7 +20,7 @@ export async function connectContext7(cfg) {
20
20
  maxRetries: 1,
21
21
  },
22
22
  });
23
- const client = new Client({ name: 'preflight-context7', version: '0.1.3' });
23
+ const client = new Client({ name: 'preflight-context7', version: '0.1.6' });
24
24
  await client.connect(transport);
25
25
  return {
26
26
  client,
package/dist/server.js CHANGED
@@ -99,7 +99,7 @@ export async function startServer() {
99
99
  startHttpServer(cfg);
100
100
  const server = new McpServer({
101
101
  name: 'preflight-mcp',
102
- version: '0.1.4',
102
+ version: '0.1.6',
103
103
  description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
104
104
  }, {
105
105
  capabilities: {
@@ -382,22 +382,23 @@ export async function startServer() {
382
382
  description: 'Create a new bundle from GitHub repos or local directories (or update an existing one if ifExists=updateExisting). Use when: "index this repo", "create bundle for", "add repo to preflight", "索引这个仓库", "创建bundle", "添加GitHub项目", "学习这个项目". NOTE: If the bundle contains code files, consider asking user if they want to generate dependency graph (preflight_evidence_dependency_graph) or establish trace links (preflight_trace_upsert).',
383
383
  inputSchema: CreateBundleInputSchema,
384
384
  outputSchema: {
385
- bundleId: z.string(),
386
- createdAt: z.string(),
387
- updatedAt: z.string(),
385
+ // Normal completion fields
386
+ bundleId: z.string().optional(),
387
+ createdAt: z.string().optional(),
388
+ updatedAt: z.string().optional(),
388
389
  resources: z.object({
389
390
  startHere: z.string(),
390
391
  agents: z.string(),
391
392
  overview: z.string(),
392
393
  manifest: z.string(),
393
- }),
394
+ }).optional(),
394
395
  repos: z.array(z.object({
395
396
  kind: z.enum(['github', 'local']),
396
397
  id: z.string(),
397
398
  source: z.enum(['git', 'archive', 'local']).optional(),
398
399
  headSha: z.string().optional(),
399
400
  notes: z.array(z.string()).optional(),
400
- })),
401
+ })).optional(),
401
402
  libraries: z
402
403
  .array(z.object({
403
404
  kind: z.literal('context7'),
@@ -408,6 +409,20 @@ export async function startServer() {
408
409
  files: z.array(z.string()).optional(),
409
410
  }))
410
411
  .optional(),
412
+ // User-facing warnings (e.g., git clone failed, used zip fallback)
413
+ warnings: z.array(z.string()).optional(),
414
+ // In-progress status fields
415
+ status: z.enum(['in-progress', 'complete']).optional(),
416
+ message: z.string().optional(),
417
+ taskId: z.string().optional(),
418
+ fingerprint: z.string().optional(),
419
+ /** Repo IDs requested (for in-progress status only, different from repos array) */
420
+ requestedRepos: z.array(z.string()).optional(),
421
+ startedAt: z.string().optional(),
422
+ elapsedSeconds: z.number().optional(),
423
+ currentPhase: z.string().optional(),
424
+ currentProgress: z.number().optional(),
425
+ currentMessage: z.string().optional(),
411
426
  },
412
427
  annotations: {
413
428
  openWorldHint: true,
@@ -431,8 +446,19 @@ export async function startServer() {
431
446
  ...summary,
432
447
  resources,
433
448
  };
449
+ // Build text response - prominently show warnings if any
450
+ let textResponse = '';
451
+ if (summary.warnings && summary.warnings.length > 0) {
452
+ textResponse += '📢 **Network Issues Encountered:**\n';
453
+ for (const warn of summary.warnings) {
454
+ textResponse += `${warn}\n`;
455
+ }
456
+ textResponse += '\n';
457
+ }
458
+ textResponse += `✅ Bundle created: ${summary.bundleId}\n`;
459
+ textResponse += `Repos: ${summary.repos.map(r => `${r.id} (${r.source})`).join(', ')}`;
434
460
  return {
435
- content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
461
+ content: [{ type: 'text', text: textResponse }],
436
462
  structuredContent: out,
437
463
  };
438
464
  }
@@ -450,7 +476,7 @@ export async function startServer() {
450
476
  message: `Bundle creation already in progress. Use preflight_get_task_status to check progress.`,
451
477
  taskId: err.taskId,
452
478
  fingerprint: err.fingerprint,
453
- repos: err.repos,
479
+ requestedRepos: err.repos,
454
480
  startedAt: err.startedAt,
455
481
  elapsedSeconds: elapsedSec,
456
482
  currentPhase: task?.phase,
@@ -474,6 +500,8 @@ export async function startServer() {
474
500
  mode: z.enum(['validate', 'repair']),
475
501
  repaired: z.boolean(),
476
502
  actionsTaken: z.array(z.string()),
503
+ /** Issues that cannot be fixed by repair (require re-download) */
504
+ unfixableIssues: z.array(z.string()).optional(),
477
505
  before: z.object({
478
506
  isValid: z.boolean(),
479
507
  missingComponents: z.array(z.string()),
@@ -495,11 +523,21 @@ export async function startServer() {
495
523
  rebuildGuides: args.rebuildGuides,
496
524
  rebuildOverview: args.rebuildOverview,
497
525
  });
498
- const summaryLine = out.mode === 'validate'
499
- ? `VALIDATE ${out.bundleId}: ${out.before.isValid ? 'OK' : 'MISSING'} (${out.before.missingComponents.length} issue(s))`
500
- : out.repaired
501
- ? `REPAIRED ${out.bundleId}: ${out.actionsTaken.length} action(s), now ${out.after.isValid ? 'OK' : 'STILL_MISSING'} (${out.after.missingComponents.length} issue(s))`
502
- : `NOOP ${out.bundleId}: nothing to repair (already OK)`;
526
+ let summaryLine;
527
+ if (out.mode === 'validate') {
528
+ summaryLine = `VALIDATE ${out.bundleId}: ${out.before.isValid ? 'OK' : 'INVALID'} (${out.before.missingComponents.length} issue(s))`;
529
+ }
530
+ else if (out.unfixableIssues && out.unfixableIssues.length > 0) {
531
+ // Has unfixable issues - clearly communicate this
532
+ summaryLine = `⚠️ UNFIXABLE ${out.bundleId}: ${out.unfixableIssues.length} issue(s) cannot be repaired offline.\n` +
533
+ out.unfixableIssues.map(i => ` - ${i}`).join('\n');
534
+ }
535
+ else if (out.repaired) {
536
+ summaryLine = `REPAIRED ${out.bundleId}: ${out.actionsTaken.length} action(s), now ${out.after.isValid ? 'OK' : 'STILL_INVALID'} (${out.after.missingComponents.length} issue(s))`;
537
+ }
538
+ else {
539
+ summaryLine = `NOOP ${out.bundleId}: nothing to repair (already OK)`;
540
+ }
503
541
  return {
504
542
  content: [{ type: 'text', text: summaryLine }],
505
543
  structuredContent: out,
@@ -690,6 +728,13 @@ export async function startServer() {
690
728
  lineNo: z.number(),
691
729
  snippet: z.string(),
692
730
  uri: z.string(),
731
+ context: z.object({
732
+ functionName: z.string().optional(),
733
+ className: z.string().optional(),
734
+ startLine: z.number(),
735
+ endLine: z.number(),
736
+ surroundingLines: z.array(z.string()),
737
+ }).optional(),
693
738
  })),
694
739
  autoUpdated: z
695
740
  .boolean()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "preflight-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",