preflight-mcp 0.1.2 → 0.1.4

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/server.js CHANGED
@@ -3,13 +3,18 @@ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mc
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import * as z from 'zod';
5
5
  import { getConfig } from './config.js';
6
- import { bundleExists, checkForUpdates, clearBundleMulti, createBundle, findBundleByInputs, computeCreateInputFingerprint, findBundleStorageDir, getBundlePathsForId, getEffectiveStorageDir, listBundles, repairBundle, updateBundle, } from './bundle/service.js';
6
+ import { assertBundleComplete, checkForUpdates, checkInProgressLock, clearBundleMulti, computeCreateInputFingerprint, createBundle, findBundleStorageDir, getBundlePathsForId, getEffectiveStorageDir, listBundles, repairBundle, updateBundle, } from './bundle/service.js';
7
+ import { getProgressTracker } from './jobs/progressTracker.js';
7
8
  import { readManifest } from './bundle/manifest.js';
8
9
  import { safeJoin, toBundleFileUri } from './mcp/uris.js';
9
10
  import { wrapPreflightError } from './mcp/errorKinds.js';
10
- import { searchIndex, verifyClaimInIndex } from './search/sqliteFts.js';
11
+ import { searchIndex } from './search/sqliteFts.js';
12
+ import { logger } from './logging/logger.js';
11
13
  import { runSearchByTags } from './tools/searchByTags.js';
12
14
  import { cleanupOnStartup, cleanupOrphanBundles } from './bundle/cleanup.js';
15
+ import { startHttpServer } from './http/server.js';
16
+ import { DependencyGraphInputSchema, generateDependencyGraph } from './evidence/dependencyGraph.js';
17
+ import { TraceQueryInputSchema, TraceUpsertInputSchema, traceQuery, traceUpsert } from './trace/service.js';
13
18
  const CreateRepoInputSchema = z.union([
14
19
  z.object({
15
20
  kind: z.literal('github'),
@@ -24,10 +29,6 @@ const CreateRepoInputSchema = z.union([
24
29
  path: z.string().describe('Local directory path containing the repository files.'),
25
30
  ref: z.string().optional().describe('Optional label/ref for the local snapshot.'),
26
31
  }),
27
- z.object({
28
- kind: z.literal('deepwiki'),
29
- url: z.string().url().describe('DeepWiki URL (https://deepwiki.com/owner/repo).'),
30
- }),
31
32
  ]);
32
33
  const CreateBundleInputSchema = {
33
34
  repos: z.array(CreateRepoInputSchema).min(1).describe('Repositories to ingest into the bundle.'),
@@ -43,12 +44,6 @@ const UpdateBundleInputSchema = {
43
44
  checkOnly: z.boolean().optional().describe('If true, only check if updates are available without applying them.'),
44
45
  force: z.boolean().optional().describe('If true, force rebuild index even if no changes detected.'),
45
46
  };
46
- const UpdateAllBundlesInputSchema = {
47
- bundleIds: z
48
- .array(z.string())
49
- .optional()
50
- .describe('Optional bundle IDs to update. If omitted, updates all bundles in storage.'),
51
- };
52
47
  const SearchBundleInputSchema = {
53
48
  bundleId: z.string().describe('Bundle ID to search.'),
54
49
  query: z.string().describe('Search query. Prefix with fts: to use raw FTS syntax.'),
@@ -74,25 +69,6 @@ const SearchByTagsInputSchema = {
74
69
  scope: z.enum(['docs', 'code', 'all']).default('all').describe('Search scope.'),
75
70
  limit: z.number().int().min(1).max(200).default(50).describe('Max total hits across all bundles.'),
76
71
  };
77
- const VerifyClaimInputSchema = {
78
- bundleId: z.string().describe('Bundle ID to verify against.'),
79
- claim: z.string().describe('A claim to look for evidence for (best-effort).'),
80
- scope: z.enum(['docs', 'code', 'all']).default('all').describe('Search scope.'),
81
- limit: z.number().int().min(1).max(50).default(8).describe('Max number of evidence hits.'),
82
- // Deprecated (kept for backward compatibility): this tool is strictly read-only.
83
- ensureFresh: z
84
- .boolean()
85
- .optional()
86
- .describe('DEPRECATED. This tool is strictly read-only and will not auto-update. Use preflight_update_bundle, then call verify again.'),
87
- maxAgeHours: z
88
- .number()
89
- .optional()
90
- .describe('DEPRECATED. Only used with ensureFresh (which is deprecated).'),
91
- autoRepairIndex: z
92
- .boolean()
93
- .optional()
94
- .describe('DEPRECATED. This tool is strictly read-only and will not auto-repair. Use preflight_repair_bundle, then call verify again.'),
95
- };
96
72
  const ListBundlesInputSchema = {
97
73
  // keep open for future filters
98
74
  };
@@ -106,22 +82,24 @@ const RepairBundleInputSchema = {
106
82
  rebuildGuides: z.boolean().optional().describe('If true, rebuild START_HERE.md and AGENTS.md when missing/empty.'),
107
83
  rebuildOverview: z.boolean().optional().describe('If true, rebuild OVERVIEW.md when missing/empty.'),
108
84
  };
109
- const BundleInfoInputSchema = {
110
- bundleId: z.string().describe('Bundle ID to get info for.'),
111
- };
112
- const ReadFileInputSchema = {
113
- bundleId: z.string().describe('Bundle ID.'),
114
- file: z.string().default('OVERVIEW.md').describe('File path relative to bundle root. Common files: OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json, or any repo file like repos/owner/repo/norm/README.md'),
85
+ const GetTaskStatusInputSchema = {
86
+ taskId: z.string().optional().describe('Task ID to query (from BUNDLE_IN_PROGRESS error).'),
87
+ fingerprint: z.string().optional().describe('Fingerprint to query (computed from repos/libraries/topics).'),
88
+ repos: z.array(CreateRepoInputSchema).optional().describe('Repos to compute fingerprint from (alternative to fingerprint).'),
89
+ libraries: z.array(z.string()).optional().describe('Libraries for fingerprint computation.'),
90
+ topics: z.array(z.string()).optional().describe('Topics for fingerprint computation.'),
115
91
  };
116
92
  export async function startServer() {
117
93
  const cfg = getConfig();
118
94
  // Run orphan bundle cleanup on startup (non-blocking, best-effort)
119
- cleanupOnStartup(cfg).catch(() => {
120
- // Errors already logged, don't block server startup
95
+ cleanupOnStartup(cfg).catch((err) => {
96
+ logger.debug('Startup cleanup failed (non-critical)', err instanceof Error ? err : undefined);
121
97
  });
98
+ // Start built-in REST API (best-effort). This must not interfere with MCP stdio transport.
99
+ startHttpServer(cfg);
122
100
  const server = new McpServer({
123
101
  name: 'preflight-mcp',
124
- version: '0.1.2',
102
+ version: '0.1.4',
125
103
  description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
126
104
  }, {
127
105
  capabilities: {
@@ -280,13 +258,20 @@ export async function startServer() {
280
258
  };
281
259
  });
282
260
  server.registerTool('preflight_read_file', {
283
- title: 'Read bundle file',
284
- description: 'Read a file from bundle. Use when: "show overview", "read file", "查看概览", "项目概览", "看README", "查看文档". Common files: OVERVIEW.md, START_HERE.md, AGENTS.md.',
285
- inputSchema: ReadFileInputSchema,
261
+ title: 'Read bundle file(s)',
262
+ description: 'Read file(s) from bundle. Two modes: ' +
263
+ '(1) Omit "file" param → returns ALL key files (OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json, repo READMEs) in one call. ' +
264
+ '(2) Provide "file" param → returns that specific file. ' +
265
+ 'Use when: "查看bundle", "show bundle", "read overview", "bundle概览", "项目信息".',
266
+ inputSchema: {
267
+ bundleId: z.string().describe('Bundle ID to read.'),
268
+ file: z.string().optional().describe('Specific file to read. If omitted, returns all key files (OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json, repo READMEs).'),
269
+ },
286
270
  outputSchema: {
287
271
  bundleId: z.string(),
288
- file: z.string(),
289
- content: z.string(),
272
+ file: z.string().optional(),
273
+ content: z.string().optional(),
274
+ files: z.record(z.string(), z.string().nullable()).optional(),
290
275
  },
291
276
  annotations: {
292
277
  readOnlyHint: true,
@@ -297,16 +282,66 @@ export async function startServer() {
297
282
  if (!storageDir) {
298
283
  throw new Error(`Bundle not found: ${args.bundleId}`);
299
284
  }
300
- const bundleRoot = getBundlePathsForId(storageDir, args.bundleId).rootDir;
301
- const absPath = safeJoin(bundleRoot, args.file);
302
- const content = await fs.readFile(absPath, 'utf8');
303
- const out = {
304
- bundleId: args.bundleId,
305
- file: args.file,
306
- content,
307
- };
285
+ const paths = getBundlePathsForId(storageDir, args.bundleId);
286
+ const bundleRoot = paths.rootDir;
287
+ // Single file mode
288
+ if (args.file) {
289
+ const absPath = safeJoin(bundleRoot, args.file);
290
+ const content = await fs.readFile(absPath, 'utf8');
291
+ const out = { bundleId: args.bundleId, file: args.file, content };
292
+ return {
293
+ content: [{ type: 'text', text: content }],
294
+ structuredContent: out,
295
+ };
296
+ }
297
+ // Batch mode: read all key files
298
+ const keyFiles = ['OVERVIEW.md', 'START_HERE.md', 'AGENTS.md', 'manifest.json'];
299
+ const files = {};
300
+ for (const file of keyFiles) {
301
+ try {
302
+ const absPath = safeJoin(bundleRoot, file);
303
+ files[file] = await fs.readFile(absPath, 'utf8');
304
+ }
305
+ catch {
306
+ files[file] = null;
307
+ }
308
+ }
309
+ // Try to find and read repo README files
310
+ try {
311
+ const manifest = await readManifest(paths.manifestPath);
312
+ for (const repo of manifest.repos ?? []) {
313
+ if (!repo.id)
314
+ continue;
315
+ const [owner, repoName] = repo.id.split('/');
316
+ if (!owner || !repoName)
317
+ continue;
318
+ const readmeNames = ['README.md', 'readme.md', 'Readme.md', 'README.MD'];
319
+ for (const readmeName of readmeNames) {
320
+ const readmePath = `repos/${owner}/${repoName}/norm/${readmeName}`;
321
+ try {
322
+ const absPath = safeJoin(bundleRoot, readmePath);
323
+ files[readmePath] = await fs.readFile(absPath, 'utf8');
324
+ break;
325
+ }
326
+ catch {
327
+ // Try next
328
+ }
329
+ }
330
+ }
331
+ }
332
+ catch {
333
+ // Ignore manifest read errors
334
+ }
335
+ // Build combined text output
336
+ const textParts = [];
337
+ for (const [filePath, content] of Object.entries(files)) {
338
+ if (content) {
339
+ textParts.push(`=== ${filePath} ===\n${content}`);
340
+ }
341
+ }
342
+ const out = { bundleId: args.bundleId, files };
308
343
  return {
309
- content: [{ type: 'text', text: content }],
344
+ content: [{ type: 'text', text: textParts.join('\n\n') || '(no files found)' }],
310
345
  structuredContent: out,
311
346
  };
312
347
  }
@@ -342,131 +377,9 @@ export async function startServer() {
342
377
  throw wrapPreflightError(err);
343
378
  }
344
379
  });
345
- server.registerTool('preflight_bundle_info', {
346
- title: 'Bundle info',
347
- description: 'Get bundle details: repos, update time, stats. Use when: "bundle info", "show bundle details", "what\'s in this bundle", "bundle状态", "查看bundle详情", "仓库信息".',
348
- inputSchema: BundleInfoInputSchema,
349
- outputSchema: {
350
- bundleId: z.string(),
351
- createdAt: z.string(),
352
- updatedAt: z.string(),
353
- repos: z.array(z.object({
354
- kind: z.enum(['github', 'local', 'deepwiki']),
355
- id: z.string(),
356
- source: z.enum(['git', 'archive', 'local', 'deepwiki']).optional(),
357
- headSha: z.string().optional(),
358
- fetchedAt: z.string().optional(),
359
- notes: z.array(z.string()).optional(),
360
- })),
361
- libraries: z
362
- .array(z.object({
363
- kind: z.literal('context7'),
364
- input: z.string(),
365
- id: z.string().optional(),
366
- fetchedAt: z.string(),
367
- notes: z.array(z.string()).optional(),
368
- files: z.array(z.string()).optional(),
369
- }))
370
- .optional(),
371
- index: z.object({
372
- backend: z.string(),
373
- includeDocs: z.boolean(),
374
- includeCode: z.boolean(),
375
- }),
376
- resources: z.object({
377
- startHere: z.string(),
378
- agents: z.string(),
379
- overview: z.string(),
380
- manifest: z.string(),
381
- }),
382
- },
383
- annotations: {
384
- readOnlyHint: true,
385
- },
386
- }, async (args) => {
387
- try {
388
- const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
389
- if (!storageDir) {
390
- throw new Error(`Bundle not found: ${args.bundleId}`);
391
- }
392
- const paths = getBundlePathsForId(storageDir, args.bundleId);
393
- const manifest = await readManifest(paths.manifestPath);
394
- const resources = {
395
- startHere: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'START_HERE.md' }),
396
- agents: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'AGENTS.md' }),
397
- overview: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'OVERVIEW.md' }),
398
- manifest: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'manifest.json' }),
399
- };
400
- const out = {
401
- bundleId: manifest.bundleId,
402
- createdAt: manifest.createdAt,
403
- updatedAt: manifest.updatedAt,
404
- repos: manifest.repos,
405
- libraries: manifest.libraries,
406
- index: manifest.index,
407
- resources,
408
- };
409
- return {
410
- content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
411
- structuredContent: out,
412
- };
413
- }
414
- catch (err) {
415
- throw wrapPreflightError(err);
416
- }
417
- });
418
- server.registerTool('preflight_find_bundle', {
419
- title: 'Find existing bundle',
420
- description: 'Check whether a bundle already exists for the given inputs (no fetching, no changes). Use when: "does this repo already exist", "have I indexed this", "find bundle for", "这个项目是否已索引".',
421
- inputSchema: {
422
- repos: z.array(CreateRepoInputSchema).min(1),
423
- libraries: z.array(z.string()).optional(),
424
- topics: z.array(z.string()).optional(),
425
- },
426
- outputSchema: {
427
- found: z.boolean(),
428
- bundleId: z.string().optional(),
429
- fingerprint: z.string(),
430
- },
431
- annotations: {
432
- readOnlyHint: true,
433
- },
434
- }, async (args) => {
435
- try {
436
- const fingerprint = computeCreateInputFingerprint({
437
- repos: args.repos,
438
- libraries: args.libraries,
439
- topics: args.topics,
440
- });
441
- const bundleId = await findBundleByInputs(cfg, {
442
- repos: args.repos,
443
- libraries: args.libraries,
444
- topics: args.topics,
445
- });
446
- const out = {
447
- found: !!bundleId,
448
- bundleId: bundleId ?? undefined,
449
- fingerprint,
450
- };
451
- return {
452
- content: [
453
- {
454
- type: 'text',
455
- text: out.found
456
- ? `FOUND ${out.bundleId} (fingerprint=${out.fingerprint})`
457
- : `NOT_FOUND (fingerprint=${out.fingerprint})`,
458
- },
459
- ],
460
- structuredContent: out,
461
- };
462
- }
463
- catch (err) {
464
- throw wrapPreflightError(err);
465
- }
466
- });
467
380
  server.registerTool('preflight_create_bundle', {
468
381
  title: 'Create bundle',
469
- description: 'Create a new bundle from GitHub repos or DeepWiki (or update an existing one if ifExists=updateExisting). Use when: "index this repo", "create bundle for", "add repo to preflight", "索引这个仓库", "创建bundle", "添加GitHub项目", "学习这个项目".',
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).',
470
383
  inputSchema: CreateBundleInputSchema,
471
384
  outputSchema: {
472
385
  bundleId: z.string(),
@@ -479,9 +392,9 @@ export async function startServer() {
479
392
  manifest: z.string(),
480
393
  }),
481
394
  repos: z.array(z.object({
482
- kind: z.enum(['github', 'local', 'deepwiki']),
395
+ kind: z.enum(['github', 'local']),
483
396
  id: z.string(),
484
- source: z.enum(['git', 'archive', 'local', 'deepwiki']).optional(),
397
+ source: z.enum(['git', 'archive', 'local']).optional(),
485
398
  headSha: z.string().optional(),
486
399
  notes: z.array(z.string()).optional(),
487
400
  })),
@@ -524,6 +437,31 @@ export async function startServer() {
524
437
  };
525
438
  }
526
439
  catch (err) {
440
+ // Handle BUNDLE_IN_PROGRESS error specially - provide useful info instead of just error
441
+ if (err?.code === 'BUNDLE_IN_PROGRESS') {
442
+ const elapsedSec = err.startedAt
443
+ ? Math.round((Date.now() - new Date(err.startedAt).getTime()) / 1000)
444
+ : 0;
445
+ // Check current progress from tracker
446
+ const tracker = getProgressTracker();
447
+ const task = err.taskId ? tracker.getTask(err.taskId) : undefined;
448
+ const out = {
449
+ status: 'in-progress',
450
+ message: `Bundle creation already in progress. Use preflight_get_task_status to check progress.`,
451
+ taskId: err.taskId,
452
+ fingerprint: err.fingerprint,
453
+ repos: err.repos,
454
+ startedAt: err.startedAt,
455
+ elapsedSeconds: elapsedSec,
456
+ currentPhase: task?.phase,
457
+ currentProgress: task?.progress,
458
+ currentMessage: task?.message,
459
+ };
460
+ return {
461
+ content: [{ type: 'text', text: `⚠️ Bundle creation in progress (${elapsedSec}s elapsed). ${task ? `Current: ${task.phase} (${task.progress}%) - ${task.message}` : 'Use preflight_get_task_status to check progress.'}` }],
462
+ structuredContent: out,
463
+ };
464
+ }
527
465
  throw wrapPreflightError(err);
528
466
  }
529
467
  });
@@ -634,72 +572,42 @@ export async function startServer() {
634
572
  structuredContent: out,
635
573
  };
636
574
  }
637
- const { summary, changed } = await updateBundle(cfg, args.bundleId, { force: args.force });
638
- const resources = {
639
- startHere: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'START_HERE.md' }),
640
- agents: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'AGENTS.md' }),
641
- overview: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'OVERVIEW.md' }),
642
- manifest: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'manifest.json' }),
643
- };
644
- const out = {
645
- changed: args.force ? true : changed,
646
- ...summary,
647
- resources,
648
- };
649
- return {
650
- content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
651
- structuredContent: out,
652
- };
653
- }
654
- catch (err) {
655
- throw wrapPreflightError(err);
656
- }
657
- });
658
- server.registerTool('preflight_update_all_bundles', {
659
- title: 'Update all bundles',
660
- description: 'Batch update all bundles at once. Use when: "update all bundles", "refresh everything", "sync all", "批量更新", "全部刷新", "更新所有bundle".',
661
- inputSchema: UpdateAllBundlesInputSchema,
662
- outputSchema: {
663
- total: z.number().int(),
664
- ok: z.number().int(),
665
- results: z.array(z.object({
666
- bundleId: z.string(),
667
- changed: z.boolean().optional(),
668
- updatedAt: z.string().optional(),
669
- error: z.string().optional(),
670
- })),
671
- },
672
- annotations: {
673
- openWorldHint: true,
674
- },
675
- }, async (args) => {
676
- const effectiveDir = await getEffectiveStorageDir(cfg);
677
- const ids = args.bundleIds && args.bundleIds.length > 0
678
- ? args.bundleIds
679
- : await listBundles(effectiveDir);
680
- const results = [];
681
- for (const bundleId of ids) {
575
+ // Create task for progress tracking
576
+ const tracker = getProgressTracker();
577
+ const fingerprint = `update-${args.bundleId}`;
578
+ const taskId = tracker.startTask(fingerprint, [args.bundleId]);
682
579
  try {
683
- const exists = await bundleExists(effectiveDir, bundleId);
684
- if (!exists) {
685
- throw new Error(`Bundle not found: ${bundleId}`);
686
- }
687
- const { summary, changed } = await updateBundle(cfg, bundleId);
688
- results.push({ bundleId, changed, updatedAt: summary.updatedAt });
580
+ const { summary, changed } = await updateBundle(cfg, args.bundleId, {
581
+ force: args.force,
582
+ onProgress: (phase, progress, message, total) => {
583
+ tracker.updateProgress(taskId, phase, progress, message, total);
584
+ },
585
+ });
586
+ tracker.completeTask(taskId, args.bundleId);
587
+ const resources = {
588
+ startHere: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'START_HERE.md' }),
589
+ agents: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'AGENTS.md' }),
590
+ overview: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'OVERVIEW.md' }),
591
+ manifest: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'manifest.json' }),
592
+ };
593
+ const out = {
594
+ changed: args.force ? true : changed,
595
+ ...summary,
596
+ resources,
597
+ };
598
+ return {
599
+ content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
600
+ structuredContent: out,
601
+ };
689
602
  }
690
- catch (err) {
691
- results.push({ bundleId, error: wrapPreflightError(err).message });
603
+ catch (updateErr) {
604
+ tracker.failTask(taskId, updateErr instanceof Error ? updateErr.message : String(updateErr));
605
+ throw updateErr;
692
606
  }
693
607
  }
694
- const out = {
695
- total: ids.length,
696
- ok: results.filter((r) => !r.error).length,
697
- results,
698
- };
699
- return {
700
- content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
701
- structuredContent: out,
702
- };
608
+ catch (err) {
609
+ throw wrapPreflightError(err);
610
+ }
703
611
  });
704
612
  server.registerTool('preflight_search_by_tags', {
705
613
  title: 'Search by tags',
@@ -749,7 +657,7 @@ export async function startServer() {
749
657
  },
750
658
  searchIndexForBundleId: (bundleId, query, scope, limit) => {
751
659
  const paths = getBundlePathsForId(effectiveDir, bundleId);
752
- return searchIndex(paths.searchDbPath, query, scope, limit);
660
+ return searchIndex(paths.searchDbPath, query, scope, limit, paths.rootDir);
753
661
  },
754
662
  toUri: (bundleId, p) => toBundleFileUri({ bundleId, relativePath: p }),
755
663
  });
@@ -801,6 +709,8 @@ export async function startServer() {
801
709
  },
802
710
  }, async (args) => {
803
711
  try {
712
+ // Check bundle completeness before any operation
713
+ await assertBundleComplete(cfg, args.bundleId);
804
714
  // Resolve bundle location across storageDirs (more robust than a single effectiveDir).
805
715
  const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
806
716
  if (!storageDir) {
@@ -815,7 +725,7 @@ export async function startServer() {
815
725
  'Call preflight_repair_bundle explicitly, then call preflight_search_bundle again.');
816
726
  }
817
727
  const paths = getBundlePathsForId(storageDir, args.bundleId);
818
- const rawHits = searchIndex(paths.searchDbPath, args.query, args.scope, args.limit);
728
+ const rawHits = searchIndex(paths.searchDbPath, args.query, args.scope, args.limit, paths.rootDir);
819
729
  const hits = rawHits.map((h) => ({
820
730
  ...h,
821
731
  uri: toBundleFileUri({ bundleId: args.bundleId, relativePath: h.path }),
@@ -835,98 +745,93 @@ export async function startServer() {
835
745
  throw wrapPreflightError(err);
836
746
  }
837
747
  });
838
- server.registerTool('preflight_verify_claim', {
839
- title: 'Verify claim',
840
- description: 'Verify a claim with evidence classification and confidence scoring (strictly read-only). If you need to update or repair, call preflight_update_bundle or preflight_repair_bundle explicitly, then verify again. Returns supporting/contradicting/related evidence. Use when: "verify this claim", "is this true", "find evidence for", "check if", "验证说法", "找证据", "这个对吗", "有没有依据".',
841
- inputSchema: VerifyClaimInputSchema,
748
+ server.registerTool('preflight_evidence_dependency_graph', {
749
+ title: 'Evidence: dependency graph (callers + imports)',
750
+ description: 'Generate an evidence-based dependency graph. Two modes: ' +
751
+ '(1) TARGET MODE: provide target.file to analyze a specific file\'s imports and callers. ' +
752
+ '(2) GLOBAL MODE: omit target to generate a project-wide import graph of all code files. ' +
753
+ 'For target mode, file path must be bundle-relative: repos/{owner}/{repo}/norm/{path}. ' +
754
+ 'Use preflight_search_bundle to find file paths, or check OVERVIEW.md.',
755
+ inputSchema: DependencyGraphInputSchema,
756
+ outputSchema: {
757
+ meta: z.any(),
758
+ facts: z.any(),
759
+ signals: z.any(),
760
+ },
761
+ annotations: {
762
+ readOnlyHint: true,
763
+ },
764
+ }, async (args) => {
765
+ try {
766
+ // Check bundle completeness before generating dependency graph
767
+ await assertBundleComplete(cfg, args.bundleId);
768
+ const out = await generateDependencyGraph(cfg, args);
769
+ return {
770
+ content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
771
+ structuredContent: out,
772
+ };
773
+ }
774
+ catch (err) {
775
+ throw wrapPreflightError(err);
776
+ }
777
+ });
778
+ server.registerTool('preflight_trace_upsert', {
779
+ title: 'Trace: upsert links',
780
+ description: 'Upsert traceability links (commit↔ticket, symbol↔test, code↔doc, etc.) for a bundle. Stores trace edges in a per-bundle SQLite database.',
781
+ inputSchema: TraceUpsertInputSchema,
842
782
  outputSchema: {
843
783
  bundleId: z.string(),
844
- claim: z.string(),
845
- scope: z.enum(['docs', 'code', 'all']),
846
- found: z.boolean(),
847
- confidence: z.number().describe('Confidence score 0-1'),
848
- confidenceLabel: z.enum(['high', 'medium', 'low', 'none']),
849
- summary: z.string().describe('Human-readable summary of verification'),
850
- supporting: z.array(z.object({
851
- kind: z.enum(['doc', 'code']),
852
- repo: z.string(),
853
- path: z.string(),
854
- lineNo: z.number(),
855
- snippet: z.string(),
856
- uri: z.string(),
857
- evidenceType: z.enum(['supporting', 'contradicting', 'related']),
858
- relevanceScore: z.number(),
859
- })).describe('Evidence supporting the claim'),
860
- contradicting: z.array(z.object({
861
- kind: z.enum(['doc', 'code']),
862
- repo: z.string(),
863
- path: z.string(),
864
- lineNo: z.number(),
865
- snippet: z.string(),
866
- uri: z.string(),
867
- evidenceType: z.enum(['supporting', 'contradicting', 'related']),
868
- relevanceScore: z.number(),
869
- })).describe('Evidence contradicting the claim'),
870
- related: z.array(z.object({
871
- kind: z.enum(['doc', 'code']),
872
- repo: z.string(),
873
- path: z.string(),
874
- lineNo: z.number(),
875
- snippet: z.string(),
876
- uri: z.string(),
877
- evidenceType: z.enum(['supporting', 'contradicting', 'related']),
878
- relevanceScore: z.number(),
879
- })).describe('Related but inconclusive evidence'),
880
- autoUpdated: z
881
- .boolean()
882
- .optional()
883
- .describe('DEPRECATED. This tool is strictly read-only and will not auto-update.'),
884
- autoRepaired: z
885
- .boolean()
886
- .optional()
887
- .describe('DEPRECATED. This tool is strictly read-only and will not auto-repair.'),
888
- repairActions: z
889
- .array(z.string())
890
- .optional()
891
- .describe('DEPRECATED. This tool is strictly read-only and will not auto-repair.'),
784
+ upserted: z.number().int(),
785
+ ids: z.array(z.string()),
786
+ },
787
+ annotations: {
788
+ openWorldHint: true,
789
+ },
790
+ }, async (args) => {
791
+ try {
792
+ // Check bundle completeness before trace upsert
793
+ await assertBundleComplete(cfg, args.bundleId);
794
+ const out = await traceUpsert(cfg, args);
795
+ return {
796
+ content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
797
+ structuredContent: out,
798
+ };
799
+ }
800
+ catch (err) {
801
+ throw wrapPreflightError(err);
802
+ }
803
+ });
804
+ server.registerTool('preflight_trace_query', {
805
+ title: 'Trace: query links',
806
+ description: 'Query traceability links. Provide bundleId for fast queries; if omitted, scans across bundles (capped). This tool is read-only.',
807
+ inputSchema: TraceQueryInputSchema,
808
+ outputSchema: {
809
+ bundleId: z.string().optional(),
810
+ scannedBundles: z.number().int().optional(),
811
+ truncated: z.boolean().optional(),
812
+ edges: z.array(z.object({
813
+ id: z.string(),
814
+ source: z.object({ type: z.string(), id: z.string() }),
815
+ target: z.object({ type: z.string(), id: z.string() }),
816
+ type: z.string(),
817
+ confidence: z.number(),
818
+ method: z.enum(['exact', 'heuristic']),
819
+ sources: z.array(z.any()),
820
+ createdAt: z.string(),
821
+ updatedAt: z.string(),
822
+ bundleId: z.string().optional(),
823
+ })),
892
824
  },
893
825
  annotations: {
894
826
  readOnlyHint: true,
895
827
  },
896
828
  }, async (args) => {
897
829
  try {
898
- // Resolve bundle location across storageDirs (more robust than a single effectiveDir).
899
- const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
900
- if (!storageDir) {
901
- throw new Error(`Bundle not found: ${args.bundleId}`);
902
- }
903
- if (args.ensureFresh) {
904
- throw new Error('ensureFresh is deprecated and not supported in this tool. This tool is strictly read-only. ' +
905
- 'Call preflight_update_bundle explicitly, then call preflight_verify_claim again.');
906
- }
907
- if (args.autoRepairIndex) {
908
- throw new Error('autoRepairIndex is deprecated and not supported in this tool. This tool is strictly read-only. ' +
909
- 'Call preflight_repair_bundle explicitly, then call preflight_verify_claim again.');
830
+ // Check bundle completeness if bundleId is provided
831
+ if (args.bundleId) {
832
+ await assertBundleComplete(cfg, args.bundleId);
910
833
  }
911
- const paths = getBundlePathsForId(storageDir, args.bundleId);
912
- const verification = verifyClaimInIndex(paths.searchDbPath, args.claim, args.scope, args.limit);
913
- // Add URIs to evidence hits
914
- const addUri = (hit) => ({
915
- ...hit,
916
- uri: toBundleFileUri({ bundleId: args.bundleId, relativePath: hit.path }),
917
- });
918
- const out = {
919
- bundleId: args.bundleId,
920
- claim: args.claim,
921
- scope: args.scope,
922
- found: verification.found,
923
- confidence: verification.confidence,
924
- confidenceLabel: verification.confidenceLabel,
925
- summary: verification.summary,
926
- supporting: verification.supporting.map(addUri),
927
- contradicting: verification.contradicting.map(addUri),
928
- related: verification.related.map(addUri),
929
- };
834
+ const out = await traceQuery(cfg, args);
930
835
  return {
931
836
  content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
932
837
  structuredContent: out,
@@ -974,6 +879,115 @@ export async function startServer() {
974
879
  throw wrapPreflightError(err);
975
880
  }
976
881
  });
882
+ // Get task status - for checking progress of in-progress bundle creations
883
+ server.registerTool('preflight_get_task_status', {
884
+ title: 'Get task status',
885
+ description: 'Check status of bundle creation tasks (especially in-progress ones). Use when: "check bundle creation progress", "what is the status", "查看任务状态", "下载进度". Can query by taskId (from error), fingerprint, or repos.',
886
+ inputSchema: GetTaskStatusInputSchema,
887
+ outputSchema: {
888
+ found: z.boolean(),
889
+ task: z.object({
890
+ taskId: z.string(),
891
+ fingerprint: z.string(),
892
+ phase: z.string(),
893
+ progress: z.number(),
894
+ total: z.number().optional(),
895
+ message: z.string(),
896
+ startedAt: z.string(),
897
+ updatedAt: z.string(),
898
+ repos: z.array(z.string()),
899
+ bundleId: z.string().optional(),
900
+ error: z.string().optional(),
901
+ }).optional(),
902
+ inProgressLock: z.object({
903
+ bundleId: z.string(),
904
+ status: z.string(),
905
+ startedAt: z.string().optional(),
906
+ taskId: z.string().optional(),
907
+ repos: z.array(z.string()).optional(),
908
+ elapsedSeconds: z.number().optional(),
909
+ }).optional(),
910
+ activeTasks: z.array(z.object({
911
+ taskId: z.string(),
912
+ fingerprint: z.string(),
913
+ phase: z.string(),
914
+ progress: z.number(),
915
+ message: z.string(),
916
+ repos: z.array(z.string()),
917
+ startedAt: z.string(),
918
+ })).optional(),
919
+ },
920
+ annotations: {
921
+ readOnlyHint: true,
922
+ },
923
+ }, async (args) => {
924
+ try {
925
+ const tracker = getProgressTracker();
926
+ let result = { found: false };
927
+ // Compute fingerprint if repos provided
928
+ let fingerprint = args.fingerprint;
929
+ if (!fingerprint && args.repos?.length) {
930
+ fingerprint = computeCreateInputFingerprint({
931
+ repos: args.repos,
932
+ libraries: args.libraries,
933
+ topics: args.topics,
934
+ });
935
+ }
936
+ // Query by taskId
937
+ if (args.taskId) {
938
+ const task = tracker.getTask(args.taskId);
939
+ if (task) {
940
+ result = { found: true, task };
941
+ }
942
+ }
943
+ // Query by fingerprint
944
+ else if (fingerprint) {
945
+ const task = tracker.getTaskByFingerprint(fingerprint);
946
+ if (task) {
947
+ result = { found: true, task };
948
+ }
949
+ // Also check persistent in-progress lock
950
+ const lock = await checkInProgressLock(cfg, fingerprint);
951
+ if (lock) {
952
+ const elapsedSeconds = lock.startedAt
953
+ ? Math.round((Date.now() - new Date(lock.startedAt).getTime()) / 1000)
954
+ : undefined;
955
+ result.inProgressLock = {
956
+ bundleId: lock.bundleId,
957
+ status: lock.status ?? 'unknown',
958
+ startedAt: lock.startedAt,
959
+ taskId: lock.taskId,
960
+ repos: lock.repos,
961
+ elapsedSeconds,
962
+ };
963
+ result.found = true;
964
+ }
965
+ }
966
+ // If no specific query, return all active tasks
967
+ else {
968
+ const activeTasks = tracker.listActiveTasks();
969
+ if (activeTasks.length > 0) {
970
+ result = { found: true, activeTasks };
971
+ }
972
+ }
973
+ const summary = result.found
974
+ ? result.task
975
+ ? `Task ${result.task.taskId}: ${result.task.phase} (${result.task.progress}%) - ${result.task.message}`
976
+ : result.activeTasks
977
+ ? `${result.activeTasks.length} active task(s)`
978
+ : result.inProgressLock
979
+ ? `In-progress lock found (started ${result.inProgressLock.elapsedSeconds}s ago)`
980
+ : 'Status found'
981
+ : 'No matching task found';
982
+ return {
983
+ content: [{ type: 'text', text: summary }],
984
+ structuredContent: result,
985
+ };
986
+ }
987
+ catch (err) {
988
+ throw wrapPreflightError(err);
989
+ }
990
+ });
977
991
  // Provide backward-compatible parsing of the same URI via resources/read for clients that bypass templates.
978
992
  // This is a safety net: if a client gives us a fully-specified URI, we can still serve it.
979
993
  server.registerResource('bundle-file-compat', 'preflight://bundle-file', {