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/README.md +49 -142
- package/README.zh-CN.md +141 -124
- package/dist/ast/treeSitter.js +588 -0
- package/dist/bundle/analysis.js +47 -0
- package/dist/bundle/context7.js +65 -36
- package/dist/bundle/facts.js +829 -0
- package/dist/bundle/github.js +34 -3
- package/dist/bundle/githubArchive.js +102 -29
- package/dist/bundle/overview.js +226 -48
- package/dist/bundle/service.js +250 -130
- package/dist/config.js +30 -3
- package/dist/context7/client.js +5 -2
- package/dist/evidence/dependencyGraph.js +1136 -0
- package/dist/http/server.js +109 -0
- package/dist/jobs/progressTracker.js +191 -0
- package/dist/search/sqliteFts.js +150 -10
- package/dist/server.js +340 -326
- package/dist/trace/service.js +108 -0
- package/dist/trace/store.js +170 -0
- package/package.json +4 -2
- package/dist/bundle/deepwiki.js +0 -206
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 {
|
|
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
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
285
|
-
|
|
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
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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:
|
|
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
|
|
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'
|
|
395
|
+
kind: z.enum(['github', 'local']),
|
|
483
396
|
id: z.string(),
|
|
484
|
-
source: z.enum(['git', 'archive', 'local'
|
|
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
|
-
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
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
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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 (
|
|
691
|
-
|
|
603
|
+
catch (updateErr) {
|
|
604
|
+
tracker.failTask(taskId, updateErr instanceof Error ? updateErr.message : String(updateErr));
|
|
605
|
+
throw updateErr;
|
|
692
606
|
}
|
|
693
607
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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('
|
|
839
|
-
title: '
|
|
840
|
-
description: '
|
|
841
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
.
|
|
882
|
-
.optional()
|
|
883
|
-
|
|
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
|
-
//
|
|
899
|
-
|
|
900
|
-
|
|
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
|
|
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', {
|