preflight-mcp 0.1.0 → 0.1.1
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 +195 -27
- package/README.zh-CN.md +277 -308
- 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 +701 -25
- 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 +455 -279
- package/dist/tools/searchByTags.js +80 -0
- package/package.json +26 -1
package/dist/server.js
CHANGED
|
@@ -3,16 +3,26 @@ 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';
|
|
10
12
|
const CreateRepoInputSchema = z.union([
|
|
11
13
|
z.object({
|
|
12
14
|
kind: z.literal('github'),
|
|
13
15
|
repo: z.string().describe('GitHub repo in owner/repo form (or github.com/owner/repo URL).'),
|
|
14
16
|
ref: z.string().optional().describe('Optional git ref (branch/tag).'),
|
|
15
17
|
}),
|
|
18
|
+
z.object({
|
|
19
|
+
kind: z.literal('local'),
|
|
20
|
+
repo: z
|
|
21
|
+
.string()
|
|
22
|
+
.describe('Logical repo id in owner/repo form (used for storage layout and de-dup).'),
|
|
23
|
+
path: z.string().describe('Local directory path containing the repository files.'),
|
|
24
|
+
ref: z.string().optional().describe('Optional label/ref for the local snapshot.'),
|
|
25
|
+
}),
|
|
16
26
|
z.object({
|
|
17
27
|
kind: z.literal('deepwiki'),
|
|
18
28
|
url: z.string().url().describe('DeepWiki URL (https://deepwiki.com/owner/repo).'),
|
|
@@ -22,6 +32,10 @@ const CreateBundleInputSchema = {
|
|
|
22
32
|
repos: z.array(CreateRepoInputSchema).min(1).describe('Repositories to ingest into the bundle.'),
|
|
23
33
|
libraries: z.array(z.string()).optional().describe('Optional library names for Context7 docs ingestion.'),
|
|
24
34
|
topics: z.array(z.string()).optional().describe('Optional Context7 topic filters (limits fetched docs).'),
|
|
35
|
+
ifExists: z
|
|
36
|
+
.enum(['error', 'returnExisting', 'updateExisting', 'createNew'])
|
|
37
|
+
.default('error')
|
|
38
|
+
.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
39
|
};
|
|
26
40
|
const UpdateBundleInputSchema = {
|
|
27
41
|
bundleId: z.string().describe('Bundle ID returned by preflight_create_bundle.'),
|
|
@@ -39,8 +53,19 @@ const SearchBundleInputSchema = {
|
|
|
39
53
|
query: z.string().describe('Search query. Prefix with fts: to use raw FTS syntax.'),
|
|
40
54
|
scope: z.enum(['docs', 'code', 'all']).default('all').describe('Search scope.'),
|
|
41
55
|
limit: z.number().int().min(1).max(200).default(30).describe('Max number of hits.'),
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
// Deprecated (kept for backward compatibility): this tool is strictly read-only.
|
|
57
|
+
ensureFresh: z
|
|
58
|
+
.boolean()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-update. Use preflight_update_bundle, then call search again.'),
|
|
61
|
+
maxAgeHours: z
|
|
62
|
+
.number()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('DEPRECATED. Only used with ensureFresh (which is deprecated).'),
|
|
65
|
+
autoRepairIndex: z
|
|
66
|
+
.boolean()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-repair. Use preflight_repair_bundle, then call search again.'),
|
|
44
69
|
};
|
|
45
70
|
const SearchByTagsInputSchema = {
|
|
46
71
|
query: z.string().describe('Search query across bundles.'),
|
|
@@ -53,8 +78,19 @@ const VerifyClaimInputSchema = {
|
|
|
53
78
|
claim: z.string().describe('A claim to look for evidence for (best-effort).'),
|
|
54
79
|
scope: z.enum(['docs', 'code', 'all']).default('all').describe('Search scope.'),
|
|
55
80
|
limit: z.number().int().min(1).max(50).default(8).describe('Max number of evidence hits.'),
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
// Deprecated (kept for backward compatibility): this tool is strictly read-only.
|
|
82
|
+
ensureFresh: z
|
|
83
|
+
.boolean()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-update. Use preflight_update_bundle, then call verify again.'),
|
|
86
|
+
maxAgeHours: z
|
|
87
|
+
.number()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe('DEPRECATED. Only used with ensureFresh (which is deprecated).'),
|
|
90
|
+
autoRepairIndex: z
|
|
91
|
+
.boolean()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-repair. Use preflight_repair_bundle, then call verify again.'),
|
|
58
94
|
};
|
|
59
95
|
const ListBundlesInputSchema = {
|
|
60
96
|
// keep open for future filters
|
|
@@ -62,6 +98,13 @@ const ListBundlesInputSchema = {
|
|
|
62
98
|
const DeleteBundleInputSchema = {
|
|
63
99
|
bundleId: z.string().describe('Bundle ID to delete.'),
|
|
64
100
|
};
|
|
101
|
+
const RepairBundleInputSchema = {
|
|
102
|
+
bundleId: z.string().describe('Bundle ID to repair.'),
|
|
103
|
+
mode: z.enum(['validate', 'repair']).default('repair').describe('validate=report missing components only; repair=fix missing derived artifacts.'),
|
|
104
|
+
rebuildIndex: z.boolean().optional().describe('If true, rebuild search index when missing/empty.'),
|
|
105
|
+
rebuildGuides: z.boolean().optional().describe('If true, rebuild START_HERE.md and AGENTS.md when missing/empty.'),
|
|
106
|
+
rebuildOverview: z.boolean().optional().describe('If true, rebuild OVERVIEW.md when missing/empty.'),
|
|
107
|
+
};
|
|
65
108
|
const BundleInfoInputSchema = {
|
|
66
109
|
bundleId: z.string().describe('Bundle ID to get info for.'),
|
|
67
110
|
};
|
|
@@ -73,7 +116,7 @@ export async function startServer() {
|
|
|
73
116
|
const cfg = getConfig();
|
|
74
117
|
const server = new McpServer({
|
|
75
118
|
name: 'preflight-mcp',
|
|
76
|
-
version: '0.1.
|
|
119
|
+
version: '0.1.1',
|
|
77
120
|
description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
|
|
78
121
|
}, {
|
|
79
122
|
capabilities: {
|
|
@@ -144,74 +187,90 @@ export async function startServer() {
|
|
|
144
187
|
});
|
|
145
188
|
server.registerTool('preflight_list_bundles', {
|
|
146
189
|
title: 'List bundles',
|
|
147
|
-
description: 'List
|
|
190
|
+
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
191
|
inputSchema: {
|
|
149
|
-
filterByTag: z
|
|
150
|
-
|
|
192
|
+
filterByTag: z
|
|
193
|
+
.string()
|
|
194
|
+
.optional()
|
|
195
|
+
.describe('Filter by tag (e.g., "mcp", "agents", "web-scraping").'),
|
|
196
|
+
limit: z
|
|
197
|
+
.number()
|
|
198
|
+
.int()
|
|
199
|
+
.min(1)
|
|
200
|
+
.max(200)
|
|
201
|
+
.default(50)
|
|
202
|
+
.describe('Max number of bundles to return.'),
|
|
203
|
+
maxItemsPerList: z
|
|
204
|
+
.number()
|
|
205
|
+
.int()
|
|
206
|
+
.min(1)
|
|
207
|
+
.max(50)
|
|
208
|
+
.default(10)
|
|
209
|
+
.describe('Max repos/tags to include per bundle to keep output compact.'),
|
|
151
210
|
},
|
|
152
211
|
outputSchema: {
|
|
153
212
|
bundles: z.array(z.object({
|
|
154
213
|
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(),
|
|
214
|
+
displayName: z.string(),
|
|
215
|
+
repos: z.array(z.string()),
|
|
216
|
+
tags: z.array(z.string()),
|
|
161
217
|
})),
|
|
162
|
-
grouped: z.record(z.string(), z.array(z.string())).optional(),
|
|
163
218
|
},
|
|
164
219
|
annotations: {
|
|
165
220
|
readOnlyHint: true,
|
|
166
221
|
},
|
|
167
222
|
}, async (args) => {
|
|
168
223
|
const effectiveDir = await getEffectiveStorageDir(cfg);
|
|
169
|
-
const ids = await listBundles(effectiveDir);
|
|
170
|
-
|
|
171
|
-
|
|
224
|
+
const ids = (await listBundles(effectiveDir)).slice(0, args.limit);
|
|
225
|
+
const capList = (items, max) => {
|
|
226
|
+
if (items.length <= max)
|
|
227
|
+
return items;
|
|
228
|
+
const keep = items.slice(0, max);
|
|
229
|
+
keep.push(`...(+${items.length - max})`);
|
|
230
|
+
return keep;
|
|
231
|
+
};
|
|
232
|
+
const bundlesInternal = [];
|
|
172
233
|
for (const id of ids) {
|
|
173
234
|
try {
|
|
174
235
|
const paths = getBundlePathsForId(effectiveDir, id);
|
|
175
236
|
const manifest = await readManifest(paths.manifestPath);
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
237
|
+
const reposRaw = (manifest.repos ?? []).map((r) => r.id).filter(Boolean);
|
|
238
|
+
const tagsFull = (manifest.tags ?? []).map(String);
|
|
239
|
+
const displayName = (manifest.displayName && manifest.displayName.trim()) ||
|
|
240
|
+
(reposRaw[0] && reposRaw[0].trim()) ||
|
|
241
|
+
'(unnamed)';
|
|
242
|
+
bundlesInternal.push({
|
|
180
243
|
bundleId: id,
|
|
181
|
-
displayName
|
|
182
|
-
|
|
183
|
-
tags:
|
|
184
|
-
|
|
185
|
-
category,
|
|
186
|
-
repoCount: manifest.repos.length,
|
|
244
|
+
displayName,
|
|
245
|
+
repos: capList(reposRaw, args.maxItemsPerList),
|
|
246
|
+
tags: capList(tagsFull, args.maxItemsPerList),
|
|
247
|
+
tagsFull,
|
|
187
248
|
});
|
|
188
249
|
}
|
|
189
250
|
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);
|
|
251
|
+
// Keep the bundleId visible even if the manifest is missing/corrupt.
|
|
252
|
+
bundlesInternal.push({
|
|
253
|
+
bundleId: id,
|
|
254
|
+
displayName: '(unreadable manifest)',
|
|
255
|
+
repos: [],
|
|
256
|
+
tags: [],
|
|
257
|
+
tagsFull: [],
|
|
258
|
+
});
|
|
207
259
|
}
|
|
208
260
|
}
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
};
|
|
261
|
+
const filteredInternal = args.filterByTag
|
|
262
|
+
? bundlesInternal.filter((b) => b.tagsFull.includes(args.filterByTag))
|
|
263
|
+
: bundlesInternal;
|
|
264
|
+
const filtered = filteredInternal.map(({ tagsFull: _tagsFull, ...b }) => b);
|
|
265
|
+
const out = { bundles: filtered };
|
|
266
|
+
// Stable human-readable format for UI logs.
|
|
267
|
+
const lines = filtered.map((b) => {
|
|
268
|
+
const repos = b.repos.length ? b.repos.join(', ') : '(none)';
|
|
269
|
+
const tags = b.tags.length ? b.tags.join(', ') : '(none)';
|
|
270
|
+
return `${b.bundleId} | ${b.displayName} | repos: ${repos} | tags: ${tags}`;
|
|
271
|
+
});
|
|
213
272
|
return {
|
|
214
|
-
content: [{ type: 'text', text:
|
|
273
|
+
content: [{ type: 'text', text: lines.join('\n') || '(no bundles)' }],
|
|
215
274
|
structuredContent: out,
|
|
216
275
|
};
|
|
217
276
|
});
|
|
@@ -228,23 +287,27 @@ export async function startServer() {
|
|
|
228
287
|
readOnlyHint: true,
|
|
229
288
|
},
|
|
230
289
|
}, async (args) => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
290
|
+
try {
|
|
291
|
+
const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
|
|
292
|
+
if (!storageDir) {
|
|
293
|
+
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
294
|
+
}
|
|
295
|
+
const bundleRoot = getBundlePathsForId(storageDir, args.bundleId).rootDir;
|
|
296
|
+
const absPath = safeJoin(bundleRoot, args.file);
|
|
297
|
+
const content = await fs.readFile(absPath, 'utf8');
|
|
298
|
+
const out = {
|
|
299
|
+
bundleId: args.bundleId,
|
|
300
|
+
file: args.file,
|
|
301
|
+
content,
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
content: [{ type: 'text', text: content }],
|
|
305
|
+
structuredContent: out,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
throw wrapPreflightError(err);
|
|
235
310
|
}
|
|
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
311
|
});
|
|
249
312
|
server.registerTool('preflight_delete_bundle', {
|
|
250
313
|
title: 'Delete bundle',
|
|
@@ -258,16 +321,21 @@ export async function startServer() {
|
|
|
258
321
|
destructiveHint: true,
|
|
259
322
|
},
|
|
260
323
|
}, async (args) => {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
324
|
+
try {
|
|
325
|
+
const deleted = await clearBundleMulti(cfg.storageDirs, args.bundleId);
|
|
326
|
+
if (!deleted) {
|
|
327
|
+
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
328
|
+
}
|
|
329
|
+
server.sendResourceListChanged();
|
|
330
|
+
const out = { deleted: true, bundleId: args.bundleId };
|
|
331
|
+
return {
|
|
332
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
333
|
+
structuredContent: out,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
throw wrapPreflightError(err);
|
|
264
338
|
}
|
|
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
339
|
});
|
|
272
340
|
server.registerTool('preflight_bundle_info', {
|
|
273
341
|
title: 'Bundle info',
|
|
@@ -278,8 +346,9 @@ export async function startServer() {
|
|
|
278
346
|
createdAt: z.string(),
|
|
279
347
|
updatedAt: z.string(),
|
|
280
348
|
repos: z.array(z.object({
|
|
281
|
-
kind: z.enum(['github', 'deepwiki']),
|
|
349
|
+
kind: z.enum(['github', 'local', 'deepwiki']),
|
|
282
350
|
id: z.string(),
|
|
351
|
+
source: z.enum(['git', 'archive', 'local', 'deepwiki']).optional(),
|
|
283
352
|
headSha: z.string().optional(),
|
|
284
353
|
fetchedAt: z.string().optional(),
|
|
285
354
|
notes: z.array(z.string()).optional(),
|
|
@@ -310,36 +379,89 @@ export async function startServer() {
|
|
|
310
379
|
readOnlyHint: true,
|
|
311
380
|
},
|
|
312
381
|
}, async (args) => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
382
|
+
try {
|
|
383
|
+
const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
|
|
384
|
+
if (!storageDir) {
|
|
385
|
+
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
386
|
+
}
|
|
387
|
+
const paths = getBundlePathsForId(storageDir, args.bundleId);
|
|
388
|
+
const manifest = await readManifest(paths.manifestPath);
|
|
389
|
+
const resources = {
|
|
390
|
+
startHere: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'START_HERE.md' }),
|
|
391
|
+
agents: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'AGENTS.md' }),
|
|
392
|
+
overview: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'OVERVIEW.md' }),
|
|
393
|
+
manifest: toBundleFileUri({ bundleId: args.bundleId, relativePath: 'manifest.json' }),
|
|
394
|
+
};
|
|
395
|
+
const out = {
|
|
396
|
+
bundleId: manifest.bundleId,
|
|
397
|
+
createdAt: manifest.createdAt,
|
|
398
|
+
updatedAt: manifest.updatedAt,
|
|
399
|
+
repos: manifest.repos,
|
|
400
|
+
libraries: manifest.libraries,
|
|
401
|
+
index: manifest.index,
|
|
402
|
+
resources,
|
|
403
|
+
};
|
|
404
|
+
return {
|
|
405
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
406
|
+
structuredContent: out,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
throw wrapPreflightError(err);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
server.registerTool('preflight_find_bundle', {
|
|
414
|
+
title: 'Find existing bundle',
|
|
415
|
+
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", "这个项目是否已索引".',
|
|
416
|
+
inputSchema: {
|
|
417
|
+
repos: z.array(CreateRepoInputSchema).min(1),
|
|
418
|
+
libraries: z.array(z.string()).optional(),
|
|
419
|
+
topics: z.array(z.string()).optional(),
|
|
420
|
+
},
|
|
421
|
+
outputSchema: {
|
|
422
|
+
found: z.boolean(),
|
|
423
|
+
bundleId: z.string().optional(),
|
|
424
|
+
fingerprint: z.string(),
|
|
425
|
+
},
|
|
426
|
+
annotations: {
|
|
427
|
+
readOnlyHint: true,
|
|
428
|
+
},
|
|
429
|
+
}, async (args) => {
|
|
430
|
+
try {
|
|
431
|
+
const fingerprint = computeCreateInputFingerprint({
|
|
432
|
+
repos: args.repos,
|
|
433
|
+
libraries: args.libraries,
|
|
434
|
+
topics: args.topics,
|
|
435
|
+
});
|
|
436
|
+
const bundleId = await findBundleByInputs(cfg, {
|
|
437
|
+
repos: args.repos,
|
|
438
|
+
libraries: args.libraries,
|
|
439
|
+
topics: args.topics,
|
|
440
|
+
});
|
|
441
|
+
const out = {
|
|
442
|
+
found: !!bundleId,
|
|
443
|
+
bundleId: bundleId ?? undefined,
|
|
444
|
+
fingerprint,
|
|
445
|
+
};
|
|
446
|
+
return {
|
|
447
|
+
content: [
|
|
448
|
+
{
|
|
449
|
+
type: 'text',
|
|
450
|
+
text: out.found
|
|
451
|
+
? `FOUND ${out.bundleId} (fingerprint=${out.fingerprint})`
|
|
452
|
+
: `NOT_FOUND (fingerprint=${out.fingerprint})`,
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
structuredContent: out,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
throw wrapPreflightError(err);
|
|
317
460
|
}
|
|
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
461
|
});
|
|
340
462
|
server.registerTool('preflight_create_bundle', {
|
|
341
463
|
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项目", "学习这个项目".',
|
|
464
|
+
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
465
|
inputSchema: CreateBundleInputSchema,
|
|
344
466
|
outputSchema: {
|
|
345
467
|
bundleId: z.string(),
|
|
@@ -352,8 +474,9 @@ export async function startServer() {
|
|
|
352
474
|
manifest: z.string(),
|
|
353
475
|
}),
|
|
354
476
|
repos: z.array(z.object({
|
|
355
|
-
kind: z.enum(['github', 'deepwiki']),
|
|
477
|
+
kind: z.enum(['github', 'local', 'deepwiki']),
|
|
356
478
|
id: z.string(),
|
|
479
|
+
source: z.enum(['git', 'archive', 'local', 'deepwiki']).optional(),
|
|
357
480
|
headSha: z.string().optional(),
|
|
358
481
|
notes: z.array(z.string()).optional(),
|
|
359
482
|
})),
|
|
@@ -372,27 +495,76 @@ export async function startServer() {
|
|
|
372
495
|
openWorldHint: true,
|
|
373
496
|
},
|
|
374
497
|
}, async (args) => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
498
|
+
try {
|
|
499
|
+
const summary = await createBundle(cfg, {
|
|
500
|
+
repos: args.repos,
|
|
501
|
+
libraries: args.libraries,
|
|
502
|
+
topics: args.topics,
|
|
503
|
+
}, { ifExists: args.ifExists });
|
|
504
|
+
const resources = {
|
|
505
|
+
startHere: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'START_HERE.md' }),
|
|
506
|
+
agents: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'AGENTS.md' }),
|
|
507
|
+
overview: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'OVERVIEW.md' }),
|
|
508
|
+
manifest: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'manifest.json' }),
|
|
509
|
+
};
|
|
510
|
+
// Let clients know resources list may have changed.
|
|
511
|
+
server.sendResourceListChanged();
|
|
512
|
+
const out = {
|
|
513
|
+
...summary,
|
|
514
|
+
resources,
|
|
515
|
+
};
|
|
516
|
+
return {
|
|
517
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
518
|
+
structuredContent: out,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
throw wrapPreflightError(err);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
server.registerTool('preflight_repair_bundle', {
|
|
526
|
+
title: 'Repair bundle (offline)',
|
|
527
|
+
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", "重建索引", "修复概览".',
|
|
528
|
+
inputSchema: RepairBundleInputSchema,
|
|
529
|
+
outputSchema: {
|
|
530
|
+
bundleId: z.string(),
|
|
531
|
+
mode: z.enum(['validate', 'repair']),
|
|
532
|
+
repaired: z.boolean(),
|
|
533
|
+
actionsTaken: z.array(z.string()),
|
|
534
|
+
before: z.object({
|
|
535
|
+
isValid: z.boolean(),
|
|
536
|
+
missingComponents: z.array(z.string()),
|
|
537
|
+
}),
|
|
538
|
+
after: z.object({
|
|
539
|
+
isValid: z.boolean(),
|
|
540
|
+
missingComponents: z.array(z.string()),
|
|
541
|
+
}),
|
|
542
|
+
updatedAt: z.string().optional(),
|
|
543
|
+
},
|
|
544
|
+
annotations: {
|
|
545
|
+
openWorldHint: true,
|
|
546
|
+
},
|
|
547
|
+
}, async (args) => {
|
|
548
|
+
try {
|
|
549
|
+
const out = await repairBundle(cfg, args.bundleId, {
|
|
550
|
+
mode: args.mode,
|
|
551
|
+
rebuildIndex: args.rebuildIndex,
|
|
552
|
+
rebuildGuides: args.rebuildGuides,
|
|
553
|
+
rebuildOverview: args.rebuildOverview,
|
|
554
|
+
});
|
|
555
|
+
const summaryLine = out.mode === 'validate'
|
|
556
|
+
? `VALIDATE ${out.bundleId}: ${out.before.isValid ? 'OK' : 'MISSING'} (${out.before.missingComponents.length} issue(s))`
|
|
557
|
+
: out.repaired
|
|
558
|
+
? `REPAIRED ${out.bundleId}: ${out.actionsTaken.length} action(s), now ${out.after.isValid ? 'OK' : 'STILL_MISSING'} (${out.after.missingComponents.length} issue(s))`
|
|
559
|
+
: `NOOP ${out.bundleId}: nothing to repair (already OK)`;
|
|
560
|
+
return {
|
|
561
|
+
content: [{ type: 'text', text: summaryLine }],
|
|
562
|
+
structuredContent: out,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
catch (err) {
|
|
566
|
+
throw wrapPreflightError(err);
|
|
567
|
+
}
|
|
396
568
|
});
|
|
397
569
|
server.registerTool('preflight_update_bundle', {
|
|
398
570
|
title: 'Update bundle',
|
|
@@ -417,8 +589,9 @@ export async function startServer() {
|
|
|
417
589
|
manifest: z.string(),
|
|
418
590
|
}).optional(),
|
|
419
591
|
repos: z.array(z.object({
|
|
420
|
-
kind: z.enum(['github', 'deepwiki']),
|
|
592
|
+
kind: z.enum(['github', 'local', 'deepwiki']),
|
|
421
593
|
id: z.string(),
|
|
594
|
+
source: z.enum(['git', 'archive', 'local', 'deepwiki']).optional(),
|
|
422
595
|
headSha: z.string().optional(),
|
|
423
596
|
notes: z.array(z.string()).optional(),
|
|
424
597
|
})).optional(),
|
|
@@ -437,41 +610,45 @@ export async function startServer() {
|
|
|
437
610
|
openWorldHint: true,
|
|
438
611
|
},
|
|
439
612
|
}, async (args) => {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
613
|
+
try {
|
|
614
|
+
const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
|
|
615
|
+
if (!storageDir) {
|
|
616
|
+
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
617
|
+
}
|
|
618
|
+
// checkOnly mode: just check for updates without applying
|
|
619
|
+
if (args.checkOnly) {
|
|
620
|
+
const { hasUpdates, details } = await checkForUpdates(cfg, args.bundleId);
|
|
621
|
+
const out = {
|
|
622
|
+
bundleId: args.bundleId,
|
|
623
|
+
changed: hasUpdates,
|
|
624
|
+
checkOnly: true,
|
|
625
|
+
updateDetails: details,
|
|
626
|
+
};
|
|
627
|
+
return {
|
|
628
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
629
|
+
structuredContent: out,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
const { summary, changed } = await updateBundle(cfg, args.bundleId, { force: args.force });
|
|
633
|
+
const resources = {
|
|
634
|
+
startHere: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'START_HERE.md' }),
|
|
635
|
+
agents: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'AGENTS.md' }),
|
|
636
|
+
overview: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'OVERVIEW.md' }),
|
|
637
|
+
manifest: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'manifest.json' }),
|
|
638
|
+
};
|
|
448
639
|
const out = {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
updateDetails: details,
|
|
640
|
+
changed: args.force ? true : changed,
|
|
641
|
+
...summary,
|
|
642
|
+
resources,
|
|
453
643
|
};
|
|
454
644
|
return {
|
|
455
645
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
456
646
|
structuredContent: out,
|
|
457
647
|
};
|
|
458
648
|
}
|
|
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
|
-
};
|
|
649
|
+
catch (err) {
|
|
650
|
+
throw wrapPreflightError(err);
|
|
651
|
+
}
|
|
475
652
|
});
|
|
476
653
|
server.registerTool('preflight_update_all_bundles', {
|
|
477
654
|
title: 'Update all bundles',
|
|
@@ -506,7 +683,7 @@ export async function startServer() {
|
|
|
506
683
|
results.push({ bundleId, changed, updatedAt: summary.updatedAt });
|
|
507
684
|
}
|
|
508
685
|
catch (err) {
|
|
509
|
-
results.push({ bundleId, error: err
|
|
686
|
+
results.push({ bundleId, error: wrapPreflightError(err).message });
|
|
510
687
|
}
|
|
511
688
|
}
|
|
512
689
|
const out = {
|
|
@@ -538,6 +715,15 @@ export async function startServer() {
|
|
|
538
715
|
snippet: z.string(),
|
|
539
716
|
uri: z.string(),
|
|
540
717
|
})),
|
|
718
|
+
warnings: z
|
|
719
|
+
.array(z.object({
|
|
720
|
+
bundleId: z.string(),
|
|
721
|
+
kind: z.string(),
|
|
722
|
+
message: z.string(),
|
|
723
|
+
}))
|
|
724
|
+
.optional()
|
|
725
|
+
.describe('Non-fatal per-bundle errors. Use kind to decide whether to repair/update.'),
|
|
726
|
+
warningsTruncated: z.boolean().optional().describe('True if warnings were capped.'),
|
|
541
727
|
},
|
|
542
728
|
annotations: {
|
|
543
729
|
readOnlyHint: true,
|
|
@@ -545,57 +731,31 @@ export async function startServer() {
|
|
|
545
731
|
}, async (args) => {
|
|
546
732
|
const effectiveDir = await getEffectiveStorageDir(cfg);
|
|
547
733
|
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 {
|
|
734
|
+
const result = await runSearchByTags({
|
|
735
|
+
bundleIds: allBundleIds,
|
|
736
|
+
query: args.query,
|
|
737
|
+
tags: args.tags,
|
|
738
|
+
scope: args.scope,
|
|
739
|
+
limit: args.limit,
|
|
740
|
+
readManifestForBundleId: async (bundleId) => {
|
|
569
741
|
const paths = getBundlePathsForId(effectiveDir, bundleId);
|
|
570
742
|
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
|
-
}
|
|
743
|
+
return { displayName: manifest.displayName, tags: manifest.tags };
|
|
744
|
+
},
|
|
745
|
+
searchIndexForBundleId: (bundleId, query, scope, limit) => {
|
|
746
|
+
const paths = getBundlePathsForId(effectiveDir, bundleId);
|
|
747
|
+
return searchIndex(paths.searchDbPath, query, scope, limit);
|
|
748
|
+
},
|
|
749
|
+
toUri: (bundleId, p) => toBundleFileUri({ bundleId, relativePath: p }),
|
|
750
|
+
});
|
|
593
751
|
const out = {
|
|
594
752
|
query: args.query,
|
|
595
753
|
tags: args.tags,
|
|
596
754
|
scope: args.scope,
|
|
597
|
-
totalBundlesSearched:
|
|
598
|
-
hits:
|
|
755
|
+
totalBundlesSearched: result.totalBundlesSearched,
|
|
756
|
+
hits: result.hits,
|
|
757
|
+
warnings: result.warnings,
|
|
758
|
+
warningsTruncated: result.warningsTruncated,
|
|
599
759
|
};
|
|
600
760
|
return {
|
|
601
761
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
@@ -604,7 +764,7 @@ export async function startServer() {
|
|
|
604
764
|
});
|
|
605
765
|
server.registerTool('preflight_search_bundle', {
|
|
606
766
|
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", "在仓库中查找", "搜代码", "搜文档".',
|
|
767
|
+
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
768
|
inputSchema: SearchBundleInputSchema,
|
|
609
769
|
outputSchema: {
|
|
610
770
|
bundleId: z.string(),
|
|
@@ -618,52 +778,61 @@ export async function startServer() {
|
|
|
618
778
|
snippet: z.string(),
|
|
619
779
|
uri: z.string(),
|
|
620
780
|
})),
|
|
621
|
-
autoUpdated: z
|
|
781
|
+
autoUpdated: z
|
|
782
|
+
.boolean()
|
|
783
|
+
.optional()
|
|
784
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-update.'),
|
|
785
|
+
autoRepaired: z
|
|
786
|
+
.boolean()
|
|
787
|
+
.optional()
|
|
788
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-repair.'),
|
|
789
|
+
repairActions: z
|
|
790
|
+
.array(z.string())
|
|
791
|
+
.optional()
|
|
792
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-repair.'),
|
|
622
793
|
},
|
|
623
794
|
annotations: {
|
|
624
795
|
readOnlyHint: true,
|
|
625
796
|
},
|
|
626
797
|
}, 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;
|
|
798
|
+
try {
|
|
799
|
+
// Resolve bundle location across storageDirs (more robust than a single effectiveDir).
|
|
800
|
+
const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
|
|
801
|
+
if (!storageDir) {
|
|
802
|
+
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
803
|
+
}
|
|
804
|
+
if (args.ensureFresh) {
|
|
805
|
+
throw new Error('ensureFresh is deprecated and not supported in this tool. This tool is strictly read-only. ' +
|
|
806
|
+
'Call preflight_update_bundle explicitly, then call preflight_search_bundle again.');
|
|
643
807
|
}
|
|
644
|
-
|
|
645
|
-
|
|
808
|
+
if (args.autoRepairIndex) {
|
|
809
|
+
throw new Error('autoRepairIndex is deprecated and not supported in this tool. This tool is strictly read-only. ' +
|
|
810
|
+
'Call preflight_repair_bundle explicitly, then call preflight_search_bundle again.');
|
|
646
811
|
}
|
|
812
|
+
const paths = getBundlePathsForId(storageDir, args.bundleId);
|
|
813
|
+
const rawHits = searchIndex(paths.searchDbPath, args.query, args.scope, args.limit);
|
|
814
|
+
const hits = rawHits.map((h) => ({
|
|
815
|
+
...h,
|
|
816
|
+
uri: toBundleFileUri({ bundleId: args.bundleId, relativePath: h.path }),
|
|
817
|
+
}));
|
|
818
|
+
const out = {
|
|
819
|
+
bundleId: args.bundleId,
|
|
820
|
+
query: args.query,
|
|
821
|
+
scope: args.scope,
|
|
822
|
+
hits,
|
|
823
|
+
};
|
|
824
|
+
return {
|
|
825
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
826
|
+
structuredContent: out,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
catch (err) {
|
|
830
|
+
throw wrapPreflightError(err);
|
|
647
831
|
}
|
|
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
832
|
});
|
|
664
833
|
server.registerTool('preflight_verify_claim', {
|
|
665
834
|
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", "验证说法", "找证据", "这个对吗", "有没有依据".',
|
|
835
|
+
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
836
|
inputSchema: VerifyClaimInputSchema,
|
|
668
837
|
outputSchema: {
|
|
669
838
|
bundleId: z.string(),
|
|
@@ -703,57 +872,64 @@ export async function startServer() {
|
|
|
703
872
|
evidenceType: z.enum(['supporting', 'contradicting', 'related']),
|
|
704
873
|
relevanceScore: z.number(),
|
|
705
874
|
})).describe('Related but inconclusive evidence'),
|
|
706
|
-
autoUpdated: z
|
|
875
|
+
autoUpdated: z
|
|
876
|
+
.boolean()
|
|
877
|
+
.optional()
|
|
878
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-update.'),
|
|
879
|
+
autoRepaired: z
|
|
880
|
+
.boolean()
|
|
881
|
+
.optional()
|
|
882
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-repair.'),
|
|
883
|
+
repairActions: z
|
|
884
|
+
.array(z.string())
|
|
885
|
+
.optional()
|
|
886
|
+
.describe('DEPRECATED. This tool is strictly read-only and will not auto-repair.'),
|
|
707
887
|
},
|
|
708
888
|
annotations: {
|
|
709
889
|
readOnlyHint: true,
|
|
710
890
|
},
|
|
711
891
|
}, 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;
|
|
892
|
+
try {
|
|
893
|
+
// Resolve bundle location across storageDirs (more robust than a single effectiveDir).
|
|
894
|
+
const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
|
|
895
|
+
if (!storageDir) {
|
|
896
|
+
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
728
897
|
}
|
|
729
|
-
|
|
730
|
-
|
|
898
|
+
if (args.ensureFresh) {
|
|
899
|
+
throw new Error('ensureFresh is deprecated and not supported in this tool. This tool is strictly read-only. ' +
|
|
900
|
+
'Call preflight_update_bundle explicitly, then call preflight_verify_claim again.');
|
|
731
901
|
}
|
|
902
|
+
if (args.autoRepairIndex) {
|
|
903
|
+
throw new Error('autoRepairIndex is deprecated and not supported in this tool. This tool is strictly read-only. ' +
|
|
904
|
+
'Call preflight_repair_bundle explicitly, then call preflight_verify_claim again.');
|
|
905
|
+
}
|
|
906
|
+
const paths = getBundlePathsForId(storageDir, args.bundleId);
|
|
907
|
+
const verification = verifyClaimInIndex(paths.searchDbPath, args.claim, args.scope, args.limit);
|
|
908
|
+
// Add URIs to evidence hits
|
|
909
|
+
const addUri = (hit) => ({
|
|
910
|
+
...hit,
|
|
911
|
+
uri: toBundleFileUri({ bundleId: args.bundleId, relativePath: hit.path }),
|
|
912
|
+
});
|
|
913
|
+
const out = {
|
|
914
|
+
bundleId: args.bundleId,
|
|
915
|
+
claim: args.claim,
|
|
916
|
+
scope: args.scope,
|
|
917
|
+
found: verification.found,
|
|
918
|
+
confidence: verification.confidence,
|
|
919
|
+
confidenceLabel: verification.confidenceLabel,
|
|
920
|
+
summary: verification.summary,
|
|
921
|
+
supporting: verification.supporting.map(addUri),
|
|
922
|
+
contradicting: verification.contradicting.map(addUri),
|
|
923
|
+
related: verification.related.map(addUri),
|
|
924
|
+
};
|
|
925
|
+
return {
|
|
926
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
927
|
+
structuredContent: out,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
throw wrapPreflightError(err);
|
|
732
932
|
}
|
|
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
933
|
});
|
|
758
934
|
// Provide backward-compatible parsing of the same URI via resources/read for clients that bypass templates.
|
|
759
935
|
// This is a safety net: if a client gives us a fully-specified URI, we can still serve it.
|