preflight-mcp 0.1.2 → 0.1.3
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 +35 -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/githubArchive.js +49 -28
- package/dist/bundle/overview.js +226 -48
- package/dist/bundle/service.js +27 -126
- package/dist/config.js +29 -3
- package/dist/context7/client.js +5 -2
- package/dist/evidence/dependencyGraph.js +826 -0
- package/dist/http/server.js +109 -0
- package/dist/search/sqliteFts.js +150 -10
- package/dist/server.js +84 -295
- 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,17 @@ 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 { checkForUpdates, clearBundleMulti, createBundle, findBundleStorageDir, getBundlePathsForId, getEffectiveStorageDir, listBundles, repairBundle, updateBundle, } from './bundle/service.js';
|
|
7
7
|
import { readManifest } from './bundle/manifest.js';
|
|
8
8
|
import { safeJoin, toBundleFileUri } from './mcp/uris.js';
|
|
9
9
|
import { wrapPreflightError } from './mcp/errorKinds.js';
|
|
10
|
-
import { searchIndex
|
|
10
|
+
import { searchIndex } from './search/sqliteFts.js';
|
|
11
|
+
import { logger } from './logging/logger.js';
|
|
11
12
|
import { runSearchByTags } from './tools/searchByTags.js';
|
|
12
13
|
import { cleanupOnStartup, cleanupOrphanBundles } from './bundle/cleanup.js';
|
|
14
|
+
import { startHttpServer } from './http/server.js';
|
|
15
|
+
import { DependencyGraphInputSchema, generateDependencyGraph } from './evidence/dependencyGraph.js';
|
|
16
|
+
import { TraceQueryInputSchema, TraceUpsertInputSchema, traceQuery, traceUpsert } from './trace/service.js';
|
|
13
17
|
const CreateRepoInputSchema = z.union([
|
|
14
18
|
z.object({
|
|
15
19
|
kind: z.literal('github'),
|
|
@@ -24,10 +28,6 @@ const CreateRepoInputSchema = z.union([
|
|
|
24
28
|
path: z.string().describe('Local directory path containing the repository files.'),
|
|
25
29
|
ref: z.string().optional().describe('Optional label/ref for the local snapshot.'),
|
|
26
30
|
}),
|
|
27
|
-
z.object({
|
|
28
|
-
kind: z.literal('deepwiki'),
|
|
29
|
-
url: z.string().url().describe('DeepWiki URL (https://deepwiki.com/owner/repo).'),
|
|
30
|
-
}),
|
|
31
31
|
]);
|
|
32
32
|
const CreateBundleInputSchema = {
|
|
33
33
|
repos: z.array(CreateRepoInputSchema).min(1).describe('Repositories to ingest into the bundle.'),
|
|
@@ -43,12 +43,6 @@ const UpdateBundleInputSchema = {
|
|
|
43
43
|
checkOnly: z.boolean().optional().describe('If true, only check if updates are available without applying them.'),
|
|
44
44
|
force: z.boolean().optional().describe('If true, force rebuild index even if no changes detected.'),
|
|
45
45
|
};
|
|
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
46
|
const SearchBundleInputSchema = {
|
|
53
47
|
bundleId: z.string().describe('Bundle ID to search.'),
|
|
54
48
|
query: z.string().describe('Search query. Prefix with fts: to use raw FTS syntax.'),
|
|
@@ -74,25 +68,6 @@ const SearchByTagsInputSchema = {
|
|
|
74
68
|
scope: z.enum(['docs', 'code', 'all']).default('all').describe('Search scope.'),
|
|
75
69
|
limit: z.number().int().min(1).max(200).default(50).describe('Max total hits across all bundles.'),
|
|
76
70
|
};
|
|
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
71
|
const ListBundlesInputSchema = {
|
|
97
72
|
// keep open for future filters
|
|
98
73
|
};
|
|
@@ -106,9 +81,6 @@ const RepairBundleInputSchema = {
|
|
|
106
81
|
rebuildGuides: z.boolean().optional().describe('If true, rebuild START_HERE.md and AGENTS.md when missing/empty.'),
|
|
107
82
|
rebuildOverview: z.boolean().optional().describe('If true, rebuild OVERVIEW.md when missing/empty.'),
|
|
108
83
|
};
|
|
109
|
-
const BundleInfoInputSchema = {
|
|
110
|
-
bundleId: z.string().describe('Bundle ID to get info for.'),
|
|
111
|
-
};
|
|
112
84
|
const ReadFileInputSchema = {
|
|
113
85
|
bundleId: z.string().describe('Bundle ID.'),
|
|
114
86
|
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'),
|
|
@@ -116,12 +88,14 @@ const ReadFileInputSchema = {
|
|
|
116
88
|
export async function startServer() {
|
|
117
89
|
const cfg = getConfig();
|
|
118
90
|
// Run orphan bundle cleanup on startup (non-blocking, best-effort)
|
|
119
|
-
cleanupOnStartup(cfg).catch(() => {
|
|
120
|
-
|
|
91
|
+
cleanupOnStartup(cfg).catch((err) => {
|
|
92
|
+
logger.debug('Startup cleanup failed (non-critical)', err instanceof Error ? err : undefined);
|
|
121
93
|
});
|
|
94
|
+
// Start built-in REST API (best-effort). This must not interfere with MCP stdio transport.
|
|
95
|
+
startHttpServer(cfg);
|
|
122
96
|
const server = new McpServer({
|
|
123
97
|
name: 'preflight-mcp',
|
|
124
|
-
version: '0.1.
|
|
98
|
+
version: '0.1.3',
|
|
125
99
|
description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
|
|
126
100
|
}, {
|
|
127
101
|
capabilities: {
|
|
@@ -281,7 +255,7 @@ export async function startServer() {
|
|
|
281
255
|
});
|
|
282
256
|
server.registerTool('preflight_read_file', {
|
|
283
257
|
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.',
|
|
258
|
+
description: 'Read a file from bundle. Use when: "show overview", "read file", "查看概览", "项目概览", "看README", "查看文档", "bundle详情", "bundle状态", "仓库信息". Common files: OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json (for bundle metadata/status).',
|
|
285
259
|
inputSchema: ReadFileInputSchema,
|
|
286
260
|
outputSchema: {
|
|
287
261
|
bundleId: z.string(),
|
|
@@ -342,131 +316,9 @@ export async function startServer() {
|
|
|
342
316
|
throw wrapPreflightError(err);
|
|
343
317
|
}
|
|
344
318
|
});
|
|
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
319
|
server.registerTool('preflight_create_bundle', {
|
|
468
320
|
title: 'Create bundle',
|
|
469
|
-
description: 'Create a new bundle from GitHub repos or
|
|
321
|
+
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
322
|
inputSchema: CreateBundleInputSchema,
|
|
471
323
|
outputSchema: {
|
|
472
324
|
bundleId: z.string(),
|
|
@@ -479,9 +331,9 @@ export async function startServer() {
|
|
|
479
331
|
manifest: z.string(),
|
|
480
332
|
}),
|
|
481
333
|
repos: z.array(z.object({
|
|
482
|
-
kind: z.enum(['github', 'local'
|
|
334
|
+
kind: z.enum(['github', 'local']),
|
|
483
335
|
id: z.string(),
|
|
484
|
-
source: z.enum(['git', 'archive', 'local'
|
|
336
|
+
source: z.enum(['git', 'archive', 'local']).optional(),
|
|
485
337
|
headSha: z.string().optional(),
|
|
486
338
|
notes: z.array(z.string()).optional(),
|
|
487
339
|
})),
|
|
@@ -655,52 +507,6 @@ export async function startServer() {
|
|
|
655
507
|
throw wrapPreflightError(err);
|
|
656
508
|
}
|
|
657
509
|
});
|
|
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) {
|
|
682
|
-
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 });
|
|
689
|
-
}
|
|
690
|
-
catch (err) {
|
|
691
|
-
results.push({ bundleId, error: wrapPreflightError(err).message });
|
|
692
|
-
}
|
|
693
|
-
}
|
|
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
|
-
};
|
|
703
|
-
});
|
|
704
510
|
server.registerTool('preflight_search_by_tags', {
|
|
705
511
|
title: 'Search by tags',
|
|
706
512
|
description: 'Search across multiple bundles filtered by tags. Use when: "search in MCP bundles", "find in all agent repos", "search web-scraping tools", "在MCP项目中搜索", "搜索所有agent".',
|
|
@@ -749,7 +555,7 @@ export async function startServer() {
|
|
|
749
555
|
},
|
|
750
556
|
searchIndexForBundleId: (bundleId, query, scope, limit) => {
|
|
751
557
|
const paths = getBundlePathsForId(effectiveDir, bundleId);
|
|
752
|
-
return searchIndex(paths.searchDbPath, query, scope, limit);
|
|
558
|
+
return searchIndex(paths.searchDbPath, query, scope, limit, paths.rootDir);
|
|
753
559
|
},
|
|
754
560
|
toUri: (bundleId, p) => toBundleFileUri({ bundleId, relativePath: p }),
|
|
755
561
|
});
|
|
@@ -815,7 +621,7 @@ export async function startServer() {
|
|
|
815
621
|
'Call preflight_repair_bundle explicitly, then call preflight_search_bundle again.');
|
|
816
622
|
}
|
|
817
623
|
const paths = getBundlePathsForId(storageDir, args.bundleId);
|
|
818
|
-
const rawHits = searchIndex(paths.searchDbPath, args.query, args.scope, args.limit);
|
|
624
|
+
const rawHits = searchIndex(paths.searchDbPath, args.query, args.scope, args.limit, paths.rootDir);
|
|
819
625
|
const hits = rawHits.map((h) => ({
|
|
820
626
|
...h,
|
|
821
627
|
uri: toBundleFileUri({ bundleId: args.bundleId, relativePath: h.path }),
|
|
@@ -835,98 +641,81 @@ export async function startServer() {
|
|
|
835
641
|
throw wrapPreflightError(err);
|
|
836
642
|
}
|
|
837
643
|
});
|
|
838
|
-
server.registerTool('
|
|
839
|
-
title: '
|
|
840
|
-
description: '
|
|
841
|
-
inputSchema:
|
|
644
|
+
server.registerTool('preflight_evidence_dependency_graph', {
|
|
645
|
+
title: 'Evidence: dependency graph (callers + imports)',
|
|
646
|
+
description: 'Generate an evidence-based dependency graph for a target file/symbol inside a bundle. Output is deterministic (FTS + regex) and every edge includes traceable sources (file + range). This tool is read-only.',
|
|
647
|
+
inputSchema: DependencyGraphInputSchema,
|
|
842
648
|
outputSchema: {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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.'),
|
|
649
|
+
meta: z.any(),
|
|
650
|
+
facts: z.any(),
|
|
651
|
+
signals: z.any(),
|
|
892
652
|
},
|
|
893
653
|
annotations: {
|
|
894
654
|
readOnlyHint: true,
|
|
895
655
|
},
|
|
896
656
|
}, async (args) => {
|
|
897
657
|
try {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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.');
|
|
910
|
-
}
|
|
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),
|
|
658
|
+
const out = await generateDependencyGraph(cfg, args);
|
|
659
|
+
return {
|
|
660
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
661
|
+
structuredContent: out,
|
|
929
662
|
};
|
|
663
|
+
}
|
|
664
|
+
catch (err) {
|
|
665
|
+
throw wrapPreflightError(err);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
server.registerTool('preflight_trace_upsert', {
|
|
669
|
+
title: 'Trace: upsert links',
|
|
670
|
+
description: 'Upsert traceability links (commit↔ticket, symbol↔test, code↔doc, etc.) for a bundle. Stores trace edges in a per-bundle SQLite database.',
|
|
671
|
+
inputSchema: TraceUpsertInputSchema,
|
|
672
|
+
outputSchema: {
|
|
673
|
+
bundleId: z.string(),
|
|
674
|
+
upserted: z.number().int(),
|
|
675
|
+
ids: z.array(z.string()),
|
|
676
|
+
},
|
|
677
|
+
annotations: {
|
|
678
|
+
openWorldHint: true,
|
|
679
|
+
},
|
|
680
|
+
}, async (args) => {
|
|
681
|
+
try {
|
|
682
|
+
const out = await traceUpsert(cfg, args);
|
|
683
|
+
return {
|
|
684
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
685
|
+
structuredContent: out,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
catch (err) {
|
|
689
|
+
throw wrapPreflightError(err);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
server.registerTool('preflight_trace_query', {
|
|
693
|
+
title: 'Trace: query links',
|
|
694
|
+
description: 'Query traceability links. Provide bundleId for fast queries; if omitted, scans across bundles (capped). This tool is read-only.',
|
|
695
|
+
inputSchema: TraceQueryInputSchema,
|
|
696
|
+
outputSchema: {
|
|
697
|
+
bundleId: z.string().optional(),
|
|
698
|
+
scannedBundles: z.number().int().optional(),
|
|
699
|
+
truncated: z.boolean().optional(),
|
|
700
|
+
edges: z.array(z.object({
|
|
701
|
+
id: z.string(),
|
|
702
|
+
source: z.object({ type: z.string(), id: z.string() }),
|
|
703
|
+
target: z.object({ type: z.string(), id: z.string() }),
|
|
704
|
+
type: z.string(),
|
|
705
|
+
confidence: z.number(),
|
|
706
|
+
method: z.enum(['exact', 'heuristic']),
|
|
707
|
+
sources: z.array(z.any()),
|
|
708
|
+
createdAt: z.string(),
|
|
709
|
+
updatedAt: z.string(),
|
|
710
|
+
bundleId: z.string().optional(),
|
|
711
|
+
})),
|
|
712
|
+
},
|
|
713
|
+
annotations: {
|
|
714
|
+
readOnlyHint: true,
|
|
715
|
+
},
|
|
716
|
+
}, async (args) => {
|
|
717
|
+
try {
|
|
718
|
+
const out = await traceQuery(cfg, args);
|
|
930
719
|
return {
|
|
931
720
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
932
721
|
structuredContent: out,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
import { findBundleStorageDir, getBundlePathsForId, listBundlesMulti } from '../bundle/service.js';
|
|
4
|
+
import { ensureTraceDb, queryTraceEdges, upsertTraceEdges } from './store.js';
|
|
5
|
+
export const TraceUpsertInputSchema = {
|
|
6
|
+
bundleId: z.string().describe('Bundle ID to attach trace links to.'),
|
|
7
|
+
edges: z
|
|
8
|
+
.array(z.object({
|
|
9
|
+
id: z.string().optional(),
|
|
10
|
+
source: z.object({ type: z.string(), id: z.string() }),
|
|
11
|
+
target: z.object({ type: z.string(), id: z.string() }),
|
|
12
|
+
type: z.string().describe('Edge type, e.g. mentions/tests/implements/relates_to'),
|
|
13
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
14
|
+
method: z.enum(['exact', 'heuristic']).optional(),
|
|
15
|
+
sources: z
|
|
16
|
+
.array(z.object({
|
|
17
|
+
file: z.string().optional(),
|
|
18
|
+
range: z
|
|
19
|
+
.object({
|
|
20
|
+
startLine: z.number().int().min(1),
|
|
21
|
+
startCol: z.number().int().min(1),
|
|
22
|
+
endLine: z.number().int().min(1),
|
|
23
|
+
endCol: z.number().int().min(1),
|
|
24
|
+
})
|
|
25
|
+
.optional(),
|
|
26
|
+
externalUrl: z.string().optional(),
|
|
27
|
+
note: z.string().optional(),
|
|
28
|
+
}))
|
|
29
|
+
.optional(),
|
|
30
|
+
}))
|
|
31
|
+
.min(1)
|
|
32
|
+
.describe('Trace edges to upsert.'),
|
|
33
|
+
};
|
|
34
|
+
export const TraceQueryInputSchema = {
|
|
35
|
+
// When omitted, the query may scan across bundles (best-effort, capped).
|
|
36
|
+
bundleId: z.string().optional().describe('Optional bundleId. If omitted, scans across bundles (capped).'),
|
|
37
|
+
source_type: z.string(),
|
|
38
|
+
source_id: z.string(),
|
|
39
|
+
target_type: z.string().optional(),
|
|
40
|
+
target_id: z.string().optional(),
|
|
41
|
+
edge_type: z.string().optional(),
|
|
42
|
+
limit: z.number().int().min(1).max(500).default(50),
|
|
43
|
+
timeBudgetMs: z.number().int().min(500).max(30_000).default(5_000),
|
|
44
|
+
maxBundles: z.number().int().min(1).max(200).default(50),
|
|
45
|
+
};
|
|
46
|
+
function traceDbPathForBundleRoot(bundleRoot) {
|
|
47
|
+
return path.join(bundleRoot, 'trace', 'trace.sqlite3');
|
|
48
|
+
}
|
|
49
|
+
async function getTraceDbPathForBundleId(cfg, bundleId) {
|
|
50
|
+
const storageDir = await findBundleStorageDir(cfg.storageDirs, bundleId);
|
|
51
|
+
if (!storageDir) {
|
|
52
|
+
throw new Error(`Bundle not found: ${bundleId}`);
|
|
53
|
+
}
|
|
54
|
+
const paths = getBundlePathsForId(storageDir, bundleId);
|
|
55
|
+
const traceDbPath = traceDbPathForBundleRoot(paths.rootDir);
|
|
56
|
+
await ensureTraceDb(traceDbPath);
|
|
57
|
+
return traceDbPath;
|
|
58
|
+
}
|
|
59
|
+
export async function traceUpsert(cfg, rawArgs) {
|
|
60
|
+
const args = z.object(TraceUpsertInputSchema).parse(rawArgs);
|
|
61
|
+
const traceDbPath = await getTraceDbPathForBundleId(cfg, args.bundleId);
|
|
62
|
+
const res = await upsertTraceEdges(traceDbPath, args.edges);
|
|
63
|
+
return { bundleId: args.bundleId, ...res };
|
|
64
|
+
}
|
|
65
|
+
export async function traceQuery(cfg, rawArgs) {
|
|
66
|
+
const args = z.object(TraceQueryInputSchema).parse(rawArgs);
|
|
67
|
+
const source = { type: args.source_type, id: args.source_id };
|
|
68
|
+
const target = args.target_type && args.target_id ? { type: args.target_type, id: args.target_id } : undefined;
|
|
69
|
+
// Fast path: single bundle
|
|
70
|
+
if (args.bundleId) {
|
|
71
|
+
const dbPath = await getTraceDbPathForBundleId(cfg, args.bundleId);
|
|
72
|
+
const rows = queryTraceEdges(dbPath, { source, target, edgeType: args.edge_type, limit: args.limit });
|
|
73
|
+
return { bundleId: args.bundleId, edges: rows };
|
|
74
|
+
}
|
|
75
|
+
// Slow path: scan across bundles (best-effort, capped)
|
|
76
|
+
const startedAt = Date.now();
|
|
77
|
+
const timeLeft = () => args.timeBudgetMs - (Date.now() - startedAt);
|
|
78
|
+
let truncated = false;
|
|
79
|
+
const bundleIds = (await listBundlesMulti(cfg.storageDirs)).slice(0, args.maxBundles);
|
|
80
|
+
const collected = [];
|
|
81
|
+
for (const bundleId of bundleIds) {
|
|
82
|
+
if (timeLeft() <= 0) {
|
|
83
|
+
truncated = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const dbPath = await getTraceDbPathForBundleId(cfg, bundleId);
|
|
88
|
+
const rows = queryTraceEdges(dbPath, { source, target, edgeType: args.edge_type, limit: Math.min(50, args.limit) });
|
|
89
|
+
for (const r of rows) {
|
|
90
|
+
collected.push({ ...r, bundleId });
|
|
91
|
+
if (collected.length >= args.limit)
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// ignore bundles without trace
|
|
97
|
+
}
|
|
98
|
+
if (collected.length >= args.limit)
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
// Sort by updatedAt desc across bundles
|
|
102
|
+
collected.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
103
|
+
return {
|
|
104
|
+
scannedBundles: bundleIds.length,
|
|
105
|
+
truncated: truncated ? true : undefined,
|
|
106
|
+
edges: collected.slice(0, args.limit),
|
|
107
|
+
};
|
|
108
|
+
}
|