preflight-mcp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +282 -27
- package/README.zh-CN.md +277 -308
- package/dist/bundle/cleanup.js +155 -0
- package/dist/bundle/deepwiki.js +1 -1
- package/dist/bundle/github.js +100 -15
- package/dist/bundle/githubArchive.js +82 -0
- package/dist/bundle/ingest.js +2 -2
- package/dist/bundle/paths.js +23 -0
- package/dist/bundle/service.js +800 -57
- package/dist/config.js +1 -0
- package/dist/context7/client.js +1 -1
- package/dist/core/concurrency-limiter.js +100 -0
- package/dist/core/scheduler.js +4 -1
- package/dist/jobs/tmp-cleanup-job.js +71 -0
- package/dist/mcp/errorKinds.js +54 -0
- package/dist/mcp/uris.js +28 -8
- package/dist/search/sqliteFts.js +68 -36
- package/dist/server/optimized-server.js +4 -0
- package/dist/server.js +498 -279
- package/dist/tools/searchByTags.js +80 -0
- package/package.json +26 -1
package/dist/server.js
CHANGED
|
@@ -3,16 +3,27 @@ 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, getBundlePathsForId, getEffectiveStorageDir, listBundles, updateBundle, } from './bundle/service.js';
|
|
6
|
+
import { bundleExists, checkForUpdates, clearBundleMulti, createBundle, findBundleByInputs, computeCreateInputFingerprint, 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
|
+
import { wrapPreflightError } from './mcp/errorKinds.js';
|
|
9
10
|
import { searchIndex, verifyClaimInIndex } from './search/sqliteFts.js';
|
|
11
|
+
import { runSearchByTags } from './tools/searchByTags.js';
|
|
12
|
+
import { cleanupOnStartup, cleanupOrphanBundles } from './bundle/cleanup.js';
|
|
10
13
|
const CreateRepoInputSchema = z.union([
|
|
11
14
|
z.object({
|
|
12
15
|
kind: z.literal('github'),
|
|
13
16
|
repo: z.string().describe('GitHub repo in owner/repo form (or github.com/owner/repo URL).'),
|
|
14
17
|
ref: z.string().optional().describe('Optional git ref (branch/tag).'),
|
|
15
18
|
}),
|
|
19
|
+
z.object({
|
|
20
|
+
kind: z.literal('local'),
|
|
21
|
+
repo: z
|
|
22
|
+
.string()
|
|
23
|
+
.describe('Logical repo id in owner/repo form (used for storage layout and de-dup).'),
|
|
24
|
+
path: z.string().describe('Local directory path containing the repository files.'),
|
|
25
|
+
ref: z.string().optional().describe('Optional label/ref for the local snapshot.'),
|
|
26
|
+
}),
|
|
16
27
|
z.object({
|
|
17
28
|
kind: z.literal('deepwiki'),
|
|
18
29
|
url: z.string().url().describe('DeepWiki URL (https://deepwiki.com/owner/repo).'),
|
|
@@ -22,6 +33,10 @@ const CreateBundleInputSchema = {
|
|
|
22
33
|
repos: z.array(CreateRepoInputSchema).min(1).describe('Repositories to ingest into the bundle.'),
|
|
23
34
|
libraries: z.array(z.string()).optional().describe('Optional library names for Context7 docs ingestion.'),
|
|
24
35
|
topics: z.array(z.string()).optional().describe('Optional Context7 topic filters (limits fetched docs).'),
|
|
36
|
+
ifExists: z
|
|
37
|
+
.enum(['error', 'returnExisting', 'updateExisting', 'createNew'])
|
|
38
|
+
.default('error')
|
|
39
|
+
.describe('What to do if a bundle with the same normalized inputs already exists. error=reject (default), returnExisting=return existing without fetching, updateExisting=update existing bundle then return it, createNew=bypass de-duplication.'),
|
|
25
40
|
};
|
|
26
41
|
const UpdateBundleInputSchema = {
|
|
27
42
|
bundleId: z.string().describe('Bundle ID returned by preflight_create_bundle.'),
|
|
@@ -39,8 +54,19 @@ const SearchBundleInputSchema = {
|
|
|
39
54
|
query: z.string().describe('Search query. Prefix with fts: to use raw FTS syntax.'),
|
|
40
55
|
scope: z.enum(['docs', 'code', 'all']).default('all').describe('Search scope.'),
|
|
41
56
|
limit: z.number().int().min(1).max(200).default(30).describe('Max number of hits.'),
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
// Deprecated (kept for backward compatibility): this tool is strictly read-only.
|
|
58
|
+
ensureFresh: z
|
|
59
|
+
.boolean()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-update. Use preflight_update_bundle, then call search again.'),
|
|
62
|
+
maxAgeHours: z
|
|
63
|
+
.number()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('DEPRECATED. Only used with ensureFresh (which is deprecated).'),
|
|
66
|
+
autoRepairIndex: z
|
|
67
|
+
.boolean()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-repair. Use preflight_repair_bundle, then call search again.'),
|
|
44
70
|
};
|
|
45
71
|
const SearchByTagsInputSchema = {
|
|
46
72
|
query: z.string().describe('Search query across bundles.'),
|
|
@@ -53,8 +79,19 @@ const VerifyClaimInputSchema = {
|
|
|
53
79
|
claim: z.string().describe('A claim to look for evidence for (best-effort).'),
|
|
54
80
|
scope: z.enum(['docs', 'code', 'all']).default('all').describe('Search scope.'),
|
|
55
81
|
limit: z.number().int().min(1).max(50).default(8).describe('Max number of evidence hits.'),
|
|
56
|
-
|
|
57
|
-
|
|
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.'),
|
|
58
95
|
};
|
|
59
96
|
const ListBundlesInputSchema = {
|
|
60
97
|
// keep open for future filters
|
|
@@ -62,6 +99,13 @@ const ListBundlesInputSchema = {
|
|
|
62
99
|
const DeleteBundleInputSchema = {
|
|
63
100
|
bundleId: z.string().describe('Bundle ID to delete.'),
|
|
64
101
|
};
|
|
102
|
+
const RepairBundleInputSchema = {
|
|
103
|
+
bundleId: z.string().describe('Bundle ID to repair.'),
|
|
104
|
+
mode: z.enum(['validate', 'repair']).default('repair').describe('validate=report missing components only; repair=fix missing derived artifacts.'),
|
|
105
|
+
rebuildIndex: z.boolean().optional().describe('If true, rebuild search index when missing/empty.'),
|
|
106
|
+
rebuildGuides: z.boolean().optional().describe('If true, rebuild START_HERE.md and AGENTS.md when missing/empty.'),
|
|
107
|
+
rebuildOverview: z.boolean().optional().describe('If true, rebuild OVERVIEW.md when missing/empty.'),
|
|
108
|
+
};
|
|
65
109
|
const BundleInfoInputSchema = {
|
|
66
110
|
bundleId: z.string().describe('Bundle ID to get info for.'),
|
|
67
111
|
};
|
|
@@ -71,9 +115,13 @@ const ReadFileInputSchema = {
|
|
|
71
115
|
};
|
|
72
116
|
export async function startServer() {
|
|
73
117
|
const cfg = getConfig();
|
|
118
|
+
// Run orphan bundle cleanup on startup (non-blocking, best-effort)
|
|
119
|
+
cleanupOnStartup(cfg).catch(() => {
|
|
120
|
+
// Errors already logged, don't block server startup
|
|
121
|
+
});
|
|
74
122
|
const server = new McpServer({
|
|
75
123
|
name: 'preflight-mcp',
|
|
76
|
-
version: '0.1.
|
|
124
|
+
version: '0.1.2',
|
|
77
125
|
description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
|
|
78
126
|
}, {
|
|
79
127
|
capabilities: {
|
|
@@ -144,74 +192,90 @@ export async function startServer() {
|
|
|
144
192
|
});
|
|
145
193
|
server.registerTool('preflight_list_bundles', {
|
|
146
194
|
title: 'List bundles',
|
|
147
|
-
description: 'List
|
|
195
|
+
description: 'List available preflight bundles in a stable, minimal format. Use when: "show bundles", "what bundles exist", "list repos", "show my knowledge bases", "what have I indexed", "查看bundle", "有哪些bundle", "列出仓库".',
|
|
148
196
|
inputSchema: {
|
|
149
|
-
filterByTag: z
|
|
150
|
-
|
|
197
|
+
filterByTag: z
|
|
198
|
+
.string()
|
|
199
|
+
.optional()
|
|
200
|
+
.describe('Filter by tag (e.g., "mcp", "agents", "web-scraping").'),
|
|
201
|
+
limit: z
|
|
202
|
+
.number()
|
|
203
|
+
.int()
|
|
204
|
+
.min(1)
|
|
205
|
+
.max(200)
|
|
206
|
+
.default(50)
|
|
207
|
+
.describe('Max number of bundles to return.'),
|
|
208
|
+
maxItemsPerList: z
|
|
209
|
+
.number()
|
|
210
|
+
.int()
|
|
211
|
+
.min(1)
|
|
212
|
+
.max(50)
|
|
213
|
+
.default(10)
|
|
214
|
+
.describe('Max repos/tags to include per bundle to keep output compact.'),
|
|
151
215
|
},
|
|
152
216
|
outputSchema: {
|
|
153
217
|
bundles: z.array(z.object({
|
|
154
218
|
bundleId: z.string(),
|
|
155
|
-
displayName: z.string()
|
|
156
|
-
|
|
157
|
-
tags: z.array(z.string())
|
|
158
|
-
primaryLanguage: z.string().optional(),
|
|
159
|
-
category: z.string().optional(),
|
|
160
|
-
repoCount: z.number(),
|
|
219
|
+
displayName: z.string(),
|
|
220
|
+
repos: z.array(z.string()),
|
|
221
|
+
tags: z.array(z.string()),
|
|
161
222
|
})),
|
|
162
|
-
grouped: z.record(z.string(), z.array(z.string())).optional(),
|
|
163
223
|
},
|
|
164
224
|
annotations: {
|
|
165
225
|
readOnlyHint: true,
|
|
166
226
|
},
|
|
167
227
|
}, async (args) => {
|
|
168
228
|
const effectiveDir = await getEffectiveStorageDir(cfg);
|
|
169
|
-
const ids = await listBundles(effectiveDir);
|
|
170
|
-
|
|
171
|
-
|
|
229
|
+
const ids = (await listBundles(effectiveDir)).slice(0, args.limit);
|
|
230
|
+
const capList = (items, max) => {
|
|
231
|
+
if (items.length <= max)
|
|
232
|
+
return items;
|
|
233
|
+
const keep = items.slice(0, max);
|
|
234
|
+
keep.push(`...(+${items.length - max})`);
|
|
235
|
+
return keep;
|
|
236
|
+
};
|
|
237
|
+
const bundlesInternal = [];
|
|
172
238
|
for (const id of ids) {
|
|
173
239
|
try {
|
|
174
240
|
const paths = getBundlePathsForId(effectiveDir, id);
|
|
175
241
|
const manifest = await readManifest(paths.manifestPath);
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
242
|
+
const reposRaw = (manifest.repos ?? []).map((r) => r.id).filter(Boolean);
|
|
243
|
+
const tagsFull = (manifest.tags ?? []).map(String);
|
|
244
|
+
const displayName = (manifest.displayName && manifest.displayName.trim()) ||
|
|
245
|
+
(reposRaw[0] && reposRaw[0].trim()) ||
|
|
246
|
+
'(unnamed)';
|
|
247
|
+
bundlesInternal.push({
|
|
180
248
|
bundleId: id,
|
|
181
|
-
displayName
|
|
182
|
-
|
|
183
|
-
tags:
|
|
184
|
-
|
|
185
|
-
category,
|
|
186
|
-
repoCount: manifest.repos.length,
|
|
249
|
+
displayName,
|
|
250
|
+
repos: capList(reposRaw, args.maxItemsPerList),
|
|
251
|
+
tags: capList(tagsFull, args.maxItemsPerList),
|
|
252
|
+
tagsFull,
|
|
187
253
|
});
|
|
188
254
|
}
|
|
189
255
|
catch {
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// Group by category if requested
|
|
199
|
-
let grouped;
|
|
200
|
-
if (args.groupByCategory) {
|
|
201
|
-
grouped = {};
|
|
202
|
-
for (const bundle of filtered) {
|
|
203
|
-
const cat = bundle.category || 'uncategorized';
|
|
204
|
-
if (!grouped[cat])
|
|
205
|
-
grouped[cat] = [];
|
|
206
|
-
grouped[cat].push(bundle.bundleId);
|
|
256
|
+
// Keep the bundleId visible even if the manifest is missing/corrupt.
|
|
257
|
+
bundlesInternal.push({
|
|
258
|
+
bundleId: id,
|
|
259
|
+
displayName: '(unreadable manifest)',
|
|
260
|
+
repos: [],
|
|
261
|
+
tags: [],
|
|
262
|
+
tagsFull: [],
|
|
263
|
+
});
|
|
207
264
|
}
|
|
208
265
|
}
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
};
|
|
266
|
+
const filteredInternal = args.filterByTag
|
|
267
|
+
? bundlesInternal.filter((b) => b.tagsFull.includes(args.filterByTag))
|
|
268
|
+
: bundlesInternal;
|
|
269
|
+
const filtered = filteredInternal.map(({ tagsFull: _tagsFull, ...b }) => b);
|
|
270
|
+
const out = { bundles: filtered };
|
|
271
|
+
// Stable human-readable format for UI logs.
|
|
272
|
+
const lines = filtered.map((b) => {
|
|
273
|
+
const repos = b.repos.length ? b.repos.join(', ') : '(none)';
|
|
274
|
+
const tags = b.tags.length ? b.tags.join(', ') : '(none)';
|
|
275
|
+
return `${b.bundleId} | ${b.displayName} | repos: ${repos} | tags: ${tags}`;
|
|
276
|
+
});
|
|
213
277
|
return {
|
|
214
|
-
content: [{ type: 'text', text:
|
|
278
|
+
content: [{ type: 'text', text: lines.join('\n') || '(no bundles)' }],
|
|
215
279
|
structuredContent: out,
|
|
216
280
|
};
|
|
217
281
|
});
|
|
@@ -228,23 +292,27 @@ export async function startServer() {
|
|
|
228
292
|
readOnlyHint: true,
|
|
229
293
|
},
|
|
230
294
|
}, async (args) => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
295
|
+
try {
|
|
296
|
+
const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
|
|
297
|
+
if (!storageDir) {
|
|
298
|
+
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
299
|
+
}
|
|
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
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: 'text', text: content }],
|
|
310
|
+
structuredContent: out,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
throw wrapPreflightError(err);
|
|
235
315
|
}
|
|
236
|
-
const bundleRoot = getBundlePathsForId(effectiveDir, args.bundleId).rootDir;
|
|
237
|
-
const absPath = safeJoin(bundleRoot, args.file);
|
|
238
|
-
const content = await fs.readFile(absPath, 'utf8');
|
|
239
|
-
const out = {
|
|
240
|
-
bundleId: args.bundleId,
|
|
241
|
-
file: args.file,
|
|
242
|
-
content,
|
|
243
|
-
};
|
|
244
|
-
return {
|
|
245
|
-
content: [{ type: 'text', text: content }],
|
|
246
|
-
structuredContent: out,
|
|
247
|
-
};
|
|
248
316
|
});
|
|
249
317
|
server.registerTool('preflight_delete_bundle', {
|
|
250
318
|
title: 'Delete bundle',
|
|
@@ -258,16 +326,21 @@ export async function startServer() {
|
|
|
258
326
|
destructiveHint: true,
|
|
259
327
|
},
|
|
260
328
|
}, async (args) => {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
329
|
+
try {
|
|
330
|
+
const deleted = await clearBundleMulti(cfg.storageDirs, args.bundleId);
|
|
331
|
+
if (!deleted) {
|
|
332
|
+
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
333
|
+
}
|
|
334
|
+
server.sendResourceListChanged();
|
|
335
|
+
const out = { deleted: true, bundleId: args.bundleId };
|
|
336
|
+
return {
|
|
337
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
338
|
+
structuredContent: out,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
throw wrapPreflightError(err);
|
|
264
343
|
}
|
|
265
|
-
server.sendResourceListChanged();
|
|
266
|
-
const out = { deleted: true, bundleId: args.bundleId };
|
|
267
|
-
return {
|
|
268
|
-
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
269
|
-
structuredContent: out,
|
|
270
|
-
};
|
|
271
344
|
});
|
|
272
345
|
server.registerTool('preflight_bundle_info', {
|
|
273
346
|
title: 'Bundle info',
|
|
@@ -278,8 +351,9 @@ export async function startServer() {
|
|
|
278
351
|
createdAt: z.string(),
|
|
279
352
|
updatedAt: z.string(),
|
|
280
353
|
repos: z.array(z.object({
|
|
281
|
-
kind: z.enum(['github', 'deepwiki']),
|
|
354
|
+
kind: z.enum(['github', 'local', 'deepwiki']),
|
|
282
355
|
id: z.string(),
|
|
356
|
+
source: z.enum(['git', 'archive', 'local', 'deepwiki']).optional(),
|
|
283
357
|
headSha: z.string().optional(),
|
|
284
358
|
fetchedAt: z.string().optional(),
|
|
285
359
|
notes: z.array(z.string()).optional(),
|
|
@@ -310,36 +384,89 @@ export async function startServer() {
|
|
|
310
384
|
readOnlyHint: true,
|
|
311
385
|
},
|
|
312
386
|
}, async (args) => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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);
|
|
317
465
|
}
|
|
318
|
-
const paths = getBundlePathsForId(effectiveDir, args.bundleId);
|
|
319
|
-
const manifest = await readManifest(paths.manifestPath);
|
|
320
|
-
const resources = {
|
|
321
|
-
startHere: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'START_HERE.md' }),
|
|
322
|
-
agents: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'AGENTS.md' }),
|
|
323
|
-
overview: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'OVERVIEW.md' }),
|
|
324
|
-
manifest: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'manifest.json' }),
|
|
325
|
-
};
|
|
326
|
-
const out = {
|
|
327
|
-
bundleId: manifest.bundleId,
|
|
328
|
-
createdAt: manifest.createdAt,
|
|
329
|
-
updatedAt: manifest.updatedAt,
|
|
330
|
-
repos: manifest.repos,
|
|
331
|
-
libraries: manifest.libraries,
|
|
332
|
-
index: manifest.index,
|
|
333
|
-
resources,
|
|
334
|
-
};
|
|
335
|
-
return {
|
|
336
|
-
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
337
|
-
structuredContent: out,
|
|
338
|
-
};
|
|
339
466
|
});
|
|
340
467
|
server.registerTool('preflight_create_bundle', {
|
|
341
468
|
title: 'Create bundle',
|
|
342
|
-
description: 'Create a new bundle from GitHub repos or DeepWiki. Use when: "index this repo", "create bundle for", "add repo to preflight", "索引这个仓库", "创建bundle", "添加GitHub项目", "学习这个项目".',
|
|
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项目", "学习这个项目".',
|
|
343
470
|
inputSchema: CreateBundleInputSchema,
|
|
344
471
|
outputSchema: {
|
|
345
472
|
bundleId: z.string(),
|
|
@@ -352,8 +479,9 @@ export async function startServer() {
|
|
|
352
479
|
manifest: z.string(),
|
|
353
480
|
}),
|
|
354
481
|
repos: z.array(z.object({
|
|
355
|
-
kind: z.enum(['github', 'deepwiki']),
|
|
482
|
+
kind: z.enum(['github', 'local', 'deepwiki']),
|
|
356
483
|
id: z.string(),
|
|
484
|
+
source: z.enum(['git', 'archive', 'local', 'deepwiki']).optional(),
|
|
357
485
|
headSha: z.string().optional(),
|
|
358
486
|
notes: z.array(z.string()).optional(),
|
|
359
487
|
})),
|
|
@@ -372,27 +500,76 @@ export async function startServer() {
|
|
|
372
500
|
openWorldHint: true,
|
|
373
501
|
},
|
|
374
502
|
}, async (args) => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
503
|
+
try {
|
|
504
|
+
const summary = await createBundle(cfg, {
|
|
505
|
+
repos: args.repos,
|
|
506
|
+
libraries: args.libraries,
|
|
507
|
+
topics: args.topics,
|
|
508
|
+
}, { ifExists: args.ifExists });
|
|
509
|
+
const resources = {
|
|
510
|
+
startHere: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'START_HERE.md' }),
|
|
511
|
+
agents: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'AGENTS.md' }),
|
|
512
|
+
overview: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'OVERVIEW.md' }),
|
|
513
|
+
manifest: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'manifest.json' }),
|
|
514
|
+
};
|
|
515
|
+
// Let clients know resources list may have changed.
|
|
516
|
+
server.sendResourceListChanged();
|
|
517
|
+
const out = {
|
|
518
|
+
...summary,
|
|
519
|
+
resources,
|
|
520
|
+
};
|
|
521
|
+
return {
|
|
522
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
523
|
+
structuredContent: out,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
throw wrapPreflightError(err);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
server.registerTool('preflight_repair_bundle', {
|
|
531
|
+
title: 'Repair bundle (offline)',
|
|
532
|
+
description: 'Validate and repair missing/empty derived bundle artifacts (offline, no fetching): search index, START_HERE.md, AGENTS.md, OVERVIEW.md. Use when: "bundle is broken", "search fails", "index missing", "修复bundle", "重建索引", "修复概览".',
|
|
533
|
+
inputSchema: RepairBundleInputSchema,
|
|
534
|
+
outputSchema: {
|
|
535
|
+
bundleId: z.string(),
|
|
536
|
+
mode: z.enum(['validate', 'repair']),
|
|
537
|
+
repaired: z.boolean(),
|
|
538
|
+
actionsTaken: z.array(z.string()),
|
|
539
|
+
before: z.object({
|
|
540
|
+
isValid: z.boolean(),
|
|
541
|
+
missingComponents: z.array(z.string()),
|
|
542
|
+
}),
|
|
543
|
+
after: z.object({
|
|
544
|
+
isValid: z.boolean(),
|
|
545
|
+
missingComponents: z.array(z.string()),
|
|
546
|
+
}),
|
|
547
|
+
updatedAt: z.string().optional(),
|
|
548
|
+
},
|
|
549
|
+
annotations: {
|
|
550
|
+
openWorldHint: true,
|
|
551
|
+
},
|
|
552
|
+
}, async (args) => {
|
|
553
|
+
try {
|
|
554
|
+
const out = await repairBundle(cfg, args.bundleId, {
|
|
555
|
+
mode: args.mode,
|
|
556
|
+
rebuildIndex: args.rebuildIndex,
|
|
557
|
+
rebuildGuides: args.rebuildGuides,
|
|
558
|
+
rebuildOverview: args.rebuildOverview,
|
|
559
|
+
});
|
|
560
|
+
const summaryLine = out.mode === 'validate'
|
|
561
|
+
? `VALIDATE ${out.bundleId}: ${out.before.isValid ? 'OK' : 'MISSING'} (${out.before.missingComponents.length} issue(s))`
|
|
562
|
+
: out.repaired
|
|
563
|
+
? `REPAIRED ${out.bundleId}: ${out.actionsTaken.length} action(s), now ${out.after.isValid ? 'OK' : 'STILL_MISSING'} (${out.after.missingComponents.length} issue(s))`
|
|
564
|
+
: `NOOP ${out.bundleId}: nothing to repair (already OK)`;
|
|
565
|
+
return {
|
|
566
|
+
content: [{ type: 'text', text: summaryLine }],
|
|
567
|
+
structuredContent: out,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
throw wrapPreflightError(err);
|
|
572
|
+
}
|
|
396
573
|
});
|
|
397
574
|
server.registerTool('preflight_update_bundle', {
|
|
398
575
|
title: 'Update bundle',
|
|
@@ -417,8 +594,9 @@ export async function startServer() {
|
|
|
417
594
|
manifest: z.string(),
|
|
418
595
|
}).optional(),
|
|
419
596
|
repos: z.array(z.object({
|
|
420
|
-
kind: z.enum(['github', 'deepwiki']),
|
|
597
|
+
kind: z.enum(['github', 'local', 'deepwiki']),
|
|
421
598
|
id: z.string(),
|
|
599
|
+
source: z.enum(['git', 'archive', 'local', 'deepwiki']).optional(),
|
|
422
600
|
headSha: z.string().optional(),
|
|
423
601
|
notes: z.array(z.string()).optional(),
|
|
424
602
|
})).optional(),
|
|
@@ -437,41 +615,45 @@ export async function startServer() {
|
|
|
437
615
|
openWorldHint: true,
|
|
438
616
|
},
|
|
439
617
|
}, async (args) => {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
618
|
+
try {
|
|
619
|
+
const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
|
|
620
|
+
if (!storageDir) {
|
|
621
|
+
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
622
|
+
}
|
|
623
|
+
// checkOnly mode: just check for updates without applying
|
|
624
|
+
if (args.checkOnly) {
|
|
625
|
+
const { hasUpdates, details } = await checkForUpdates(cfg, args.bundleId);
|
|
626
|
+
const out = {
|
|
627
|
+
bundleId: args.bundleId,
|
|
628
|
+
changed: hasUpdates,
|
|
629
|
+
checkOnly: true,
|
|
630
|
+
updateDetails: details,
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
634
|
+
structuredContent: out,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
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
|
+
};
|
|
448
644
|
const out = {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
updateDetails: details,
|
|
645
|
+
changed: args.force ? true : changed,
|
|
646
|
+
...summary,
|
|
647
|
+
resources,
|
|
453
648
|
};
|
|
454
649
|
return {
|
|
455
650
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
456
651
|
structuredContent: out,
|
|
457
652
|
};
|
|
458
653
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
agents: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'AGENTS.md' }),
|
|
463
|
-
overview: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'OVERVIEW.md' }),
|
|
464
|
-
manifest: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'manifest.json' }),
|
|
465
|
-
};
|
|
466
|
-
const out = {
|
|
467
|
-
changed: args.force ? true : changed,
|
|
468
|
-
...summary,
|
|
469
|
-
resources,
|
|
470
|
-
};
|
|
471
|
-
return {
|
|
472
|
-
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
473
|
-
structuredContent: out,
|
|
474
|
-
};
|
|
654
|
+
catch (err) {
|
|
655
|
+
throw wrapPreflightError(err);
|
|
656
|
+
}
|
|
475
657
|
});
|
|
476
658
|
server.registerTool('preflight_update_all_bundles', {
|
|
477
659
|
title: 'Update all bundles',
|
|
@@ -506,7 +688,7 @@ export async function startServer() {
|
|
|
506
688
|
results.push({ bundleId, changed, updatedAt: summary.updatedAt });
|
|
507
689
|
}
|
|
508
690
|
catch (err) {
|
|
509
|
-
results.push({ bundleId, error: err
|
|
691
|
+
results.push({ bundleId, error: wrapPreflightError(err).message });
|
|
510
692
|
}
|
|
511
693
|
}
|
|
512
694
|
const out = {
|
|
@@ -538,6 +720,15 @@ export async function startServer() {
|
|
|
538
720
|
snippet: z.string(),
|
|
539
721
|
uri: z.string(),
|
|
540
722
|
})),
|
|
723
|
+
warnings: z
|
|
724
|
+
.array(z.object({
|
|
725
|
+
bundleId: z.string(),
|
|
726
|
+
kind: z.string(),
|
|
727
|
+
message: z.string(),
|
|
728
|
+
}))
|
|
729
|
+
.optional()
|
|
730
|
+
.describe('Non-fatal per-bundle errors. Use kind to decide whether to repair/update.'),
|
|
731
|
+
warningsTruncated: z.boolean().optional().describe('True if warnings were capped.'),
|
|
541
732
|
},
|
|
542
733
|
annotations: {
|
|
543
734
|
readOnlyHint: true,
|
|
@@ -545,57 +736,31 @@ export async function startServer() {
|
|
|
545
736
|
}, async (args) => {
|
|
546
737
|
const effectiveDir = await getEffectiveStorageDir(cfg);
|
|
547
738
|
const allBundleIds = await listBundles(effectiveDir);
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const manifest = await readManifest(paths.manifestPath);
|
|
556
|
-
if (manifest.tags && args.tags.some((t) => manifest.tags.includes(t))) {
|
|
557
|
-
targetBundleIds.push(id);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
catch {
|
|
561
|
-
// Skip bundles with errors
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
// Search each bundle and collect results
|
|
566
|
-
const allHits = [];
|
|
567
|
-
for (const bundleId of targetBundleIds) {
|
|
568
|
-
try {
|
|
739
|
+
const result = await runSearchByTags({
|
|
740
|
+
bundleIds: allBundleIds,
|
|
741
|
+
query: args.query,
|
|
742
|
+
tags: args.tags,
|
|
743
|
+
scope: args.scope,
|
|
744
|
+
limit: args.limit,
|
|
745
|
+
readManifestForBundleId: async (bundleId) => {
|
|
569
746
|
const paths = getBundlePathsForId(effectiveDir, bundleId);
|
|
570
747
|
const manifest = await readManifest(paths.manifestPath);
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
lineNo: hit.lineNo,
|
|
580
|
-
snippet: hit.snippet,
|
|
581
|
-
uri: toBundleFileUri({ bundleId, relativePath: hit.path }),
|
|
582
|
-
});
|
|
583
|
-
if (allHits.length >= args.limit)
|
|
584
|
-
break;
|
|
585
|
-
}
|
|
586
|
-
if (allHits.length >= args.limit)
|
|
587
|
-
break;
|
|
588
|
-
}
|
|
589
|
-
catch {
|
|
590
|
-
// Skip bundles with search errors
|
|
591
|
-
}
|
|
592
|
-
}
|
|
748
|
+
return { displayName: manifest.displayName, tags: manifest.tags };
|
|
749
|
+
},
|
|
750
|
+
searchIndexForBundleId: (bundleId, query, scope, limit) => {
|
|
751
|
+
const paths = getBundlePathsForId(effectiveDir, bundleId);
|
|
752
|
+
return searchIndex(paths.searchDbPath, query, scope, limit);
|
|
753
|
+
},
|
|
754
|
+
toUri: (bundleId, p) => toBundleFileUri({ bundleId, relativePath: p }),
|
|
755
|
+
});
|
|
593
756
|
const out = {
|
|
594
757
|
query: args.query,
|
|
595
758
|
tags: args.tags,
|
|
596
759
|
scope: args.scope,
|
|
597
|
-
totalBundlesSearched:
|
|
598
|
-
hits:
|
|
760
|
+
totalBundlesSearched: result.totalBundlesSearched,
|
|
761
|
+
hits: result.hits,
|
|
762
|
+
warnings: result.warnings,
|
|
763
|
+
warningsTruncated: result.warningsTruncated,
|
|
599
764
|
};
|
|
600
765
|
return {
|
|
601
766
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
@@ -604,7 +769,7 @@ export async function startServer() {
|
|
|
604
769
|
});
|
|
605
770
|
server.registerTool('preflight_search_bundle', {
|
|
606
771
|
title: 'Search bundle',
|
|
607
|
-
description: 'Full-text search in bundle docs and code. Use when: "search in bundle", "find in repo", "look for X in bundle", "搜索bundle", "在仓库中查找", "搜代码", "搜文档".',
|
|
772
|
+
description: 'Full-text search in bundle docs and code (strictly read-only). If you need to update or repair, call preflight_update_bundle or preflight_repair_bundle explicitly, then search again. Use when: "search in bundle", "find in repo", "look for X in bundle", "搜索bundle", "在仓库中查找", "搜代码", "搜文档".',
|
|
608
773
|
inputSchema: SearchBundleInputSchema,
|
|
609
774
|
outputSchema: {
|
|
610
775
|
bundleId: z.string(),
|
|
@@ -618,52 +783,61 @@ export async function startServer() {
|
|
|
618
783
|
snippet: z.string(),
|
|
619
784
|
uri: z.string(),
|
|
620
785
|
})),
|
|
621
|
-
autoUpdated: z
|
|
786
|
+
autoUpdated: z
|
|
787
|
+
.boolean()
|
|
788
|
+
.optional()
|
|
789
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-update.'),
|
|
790
|
+
autoRepaired: z
|
|
791
|
+
.boolean()
|
|
792
|
+
.optional()
|
|
793
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-repair.'),
|
|
794
|
+
repairActions: z
|
|
795
|
+
.array(z.string())
|
|
796
|
+
.optional()
|
|
797
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-repair.'),
|
|
622
798
|
},
|
|
623
799
|
annotations: {
|
|
624
800
|
readOnlyHint: true,
|
|
625
801
|
},
|
|
626
802
|
}, async (args) => {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const manifest = await readManifest(paths.manifestPath);
|
|
637
|
-
const updatedAt = new Date(manifest.updatedAt).getTime();
|
|
638
|
-
const ageMs = Date.now() - updatedAt;
|
|
639
|
-
const maxAgeMs = (args.maxAgeHours ?? 24) * 60 * 60 * 1000;
|
|
640
|
-
if (ageMs > maxAgeMs) {
|
|
641
|
-
await updateBundle(cfg, args.bundleId);
|
|
642
|
-
autoUpdated = true;
|
|
803
|
+
try {
|
|
804
|
+
// Resolve bundle location across storageDirs (more robust than a single effectiveDir).
|
|
805
|
+
const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
|
|
806
|
+
if (!storageDir) {
|
|
807
|
+
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
808
|
+
}
|
|
809
|
+
if (args.ensureFresh) {
|
|
810
|
+
throw new Error('ensureFresh is deprecated and not supported in this tool. This tool is strictly read-only. ' +
|
|
811
|
+
'Call preflight_update_bundle explicitly, then call preflight_search_bundle again.');
|
|
643
812
|
}
|
|
644
|
-
|
|
645
|
-
|
|
813
|
+
if (args.autoRepairIndex) {
|
|
814
|
+
throw new Error('autoRepairIndex is deprecated and not supported in this tool. This tool is strictly read-only. ' +
|
|
815
|
+
'Call preflight_repair_bundle explicitly, then call preflight_search_bundle again.');
|
|
646
816
|
}
|
|
817
|
+
const paths = getBundlePathsForId(storageDir, args.bundleId);
|
|
818
|
+
const rawHits = searchIndex(paths.searchDbPath, args.query, args.scope, args.limit);
|
|
819
|
+
const hits = rawHits.map((h) => ({
|
|
820
|
+
...h,
|
|
821
|
+
uri: toBundleFileUri({ bundleId: args.bundleId, relativePath: h.path }),
|
|
822
|
+
}));
|
|
823
|
+
const out = {
|
|
824
|
+
bundleId: args.bundleId,
|
|
825
|
+
query: args.query,
|
|
826
|
+
scope: args.scope,
|
|
827
|
+
hits,
|
|
828
|
+
};
|
|
829
|
+
return {
|
|
830
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
831
|
+
structuredContent: out,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
throw wrapPreflightError(err);
|
|
647
836
|
}
|
|
648
|
-
const hits = searchIndex(paths.searchDbPath, args.query, args.scope, args.limit).map((h) => ({
|
|
649
|
-
...h,
|
|
650
|
-
uri: toBundleFileUri({ bundleId: args.bundleId, relativePath: h.path }),
|
|
651
|
-
}));
|
|
652
|
-
const out = {
|
|
653
|
-
bundleId: args.bundleId,
|
|
654
|
-
query: args.query,
|
|
655
|
-
scope: args.scope,
|
|
656
|
-
hits,
|
|
657
|
-
autoUpdated,
|
|
658
|
-
};
|
|
659
|
-
return {
|
|
660
|
-
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
661
|
-
structuredContent: out,
|
|
662
|
-
};
|
|
663
837
|
});
|
|
664
838
|
server.registerTool('preflight_verify_claim', {
|
|
665
839
|
title: 'Verify claim',
|
|
666
|
-
description: 'Verify a claim with evidence classification and confidence scoring. Returns supporting/contradicting/related evidence. Use when: "verify this claim", "is this true", "find evidence for", "check if", "验证说法", "找证据", "这个对吗", "有没有依据".',
|
|
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", "验证说法", "找证据", "这个对吗", "有没有依据".',
|
|
667
841
|
inputSchema: VerifyClaimInputSchema,
|
|
668
842
|
outputSchema: {
|
|
669
843
|
bundleId: z.string(),
|
|
@@ -703,57 +877,102 @@ export async function startServer() {
|
|
|
703
877
|
evidenceType: z.enum(['supporting', 'contradicting', 'related']),
|
|
704
878
|
relevanceScore: z.number(),
|
|
705
879
|
})).describe('Related but inconclusive evidence'),
|
|
706
|
-
autoUpdated: z
|
|
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.'),
|
|
707
892
|
},
|
|
708
893
|
annotations: {
|
|
709
894
|
readOnlyHint: true,
|
|
710
895
|
},
|
|
711
896
|
}, async (args) => {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
let autoUpdated;
|
|
718
|
-
const paths = getBundlePathsForId(effectiveDir, args.bundleId);
|
|
719
|
-
// Lazy update: check if bundle is stale when ensureFresh is true.
|
|
720
|
-
if (args.ensureFresh) {
|
|
721
|
-
const manifest = await readManifest(paths.manifestPath);
|
|
722
|
-
const updatedAt = new Date(manifest.updatedAt).getTime();
|
|
723
|
-
const ageMs = Date.now() - updatedAt;
|
|
724
|
-
const maxAgeMs = (args.maxAgeHours ?? 24) * 60 * 60 * 1000;
|
|
725
|
-
if (ageMs > maxAgeMs) {
|
|
726
|
-
await updateBundle(cfg, args.bundleId);
|
|
727
|
-
autoUpdated = true;
|
|
897
|
+
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}`);
|
|
728
902
|
}
|
|
729
|
-
|
|
730
|
-
|
|
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.');
|
|
731
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),
|
|
929
|
+
};
|
|
930
|
+
return {
|
|
931
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
932
|
+
structuredContent: out,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
catch (err) {
|
|
936
|
+
throw wrapPreflightError(err);
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
server.registerTool('preflight_cleanup_orphans', {
|
|
940
|
+
title: 'Cleanup orphan bundles',
|
|
941
|
+
description: 'Remove incomplete or corrupted bundles (bundles without valid manifest.json). Safe to run anytime. Use when: "clean up broken bundles", "remove orphans", "清理孤儿bundle", "清除损坏的bundle".',
|
|
942
|
+
inputSchema: {
|
|
943
|
+
dryRun: z.boolean().default(true).describe('If true, only report orphans without deleting. Set to false to actually delete.'),
|
|
944
|
+
minAgeHours: z.number().default(1).describe('Only clean bundles older than N hours (safety margin to avoid race conditions).'),
|
|
945
|
+
},
|
|
946
|
+
outputSchema: {
|
|
947
|
+
totalFound: z.number(),
|
|
948
|
+
totalCleaned: z.number(),
|
|
949
|
+
details: z.array(z.object({
|
|
950
|
+
storageDir: z.string(),
|
|
951
|
+
found: z.array(z.string()),
|
|
952
|
+
cleaned: z.array(z.string()),
|
|
953
|
+
skipped: z.array(z.object({ bundleId: z.string(), reason: z.string() })),
|
|
954
|
+
})),
|
|
955
|
+
},
|
|
956
|
+
annotations: {
|
|
957
|
+
destructiveHint: true,
|
|
958
|
+
},
|
|
959
|
+
}, async (args) => {
|
|
960
|
+
try {
|
|
961
|
+
const result = await cleanupOrphanBundles(cfg, {
|
|
962
|
+
minAgeHours: args.minAgeHours,
|
|
963
|
+
dryRun: args.dryRun,
|
|
964
|
+
});
|
|
965
|
+
const summary = args.dryRun
|
|
966
|
+
? `Found ${result.totalFound} orphan bundle(s) (DRY RUN - not deleted)`
|
|
967
|
+
: `Cleaned ${result.totalCleaned} of ${result.totalFound} orphan bundle(s)`;
|
|
968
|
+
return {
|
|
969
|
+
content: [{ type: 'text', text: summary }],
|
|
970
|
+
structuredContent: result,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
catch (err) {
|
|
974
|
+
throw wrapPreflightError(err);
|
|
732
975
|
}
|
|
733
|
-
// Use differentiated verification with confidence scoring
|
|
734
|
-
const verification = verifyClaimInIndex(paths.searchDbPath, args.claim, args.scope, args.limit);
|
|
735
|
-
// Add URIs to evidence hits
|
|
736
|
-
const addUri = (hit) => ({
|
|
737
|
-
...hit,
|
|
738
|
-
uri: toBundleFileUri({ bundleId: args.bundleId, relativePath: hit.path }),
|
|
739
|
-
});
|
|
740
|
-
const out = {
|
|
741
|
-
bundleId: args.bundleId,
|
|
742
|
-
claim: args.claim,
|
|
743
|
-
scope: args.scope,
|
|
744
|
-
found: verification.found,
|
|
745
|
-
confidence: verification.confidence,
|
|
746
|
-
confidenceLabel: verification.confidenceLabel,
|
|
747
|
-
summary: verification.summary,
|
|
748
|
-
supporting: verification.supporting.map(addUri),
|
|
749
|
-
contradicting: verification.contradicting.map(addUri),
|
|
750
|
-
related: verification.related.map(addUri),
|
|
751
|
-
autoUpdated,
|
|
752
|
-
};
|
|
753
|
-
return {
|
|
754
|
-
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
755
|
-
structuredContent: out,
|
|
756
|
-
};
|
|
757
976
|
});
|
|
758
977
|
// Provide backward-compatible parsing of the same URI via resources/read for clients that bypass templates.
|
|
759
978
|
// This is a safety net: if a client gives us a fully-specified URI, we can still serve it.
|