preflight-mcp 0.1.3 → 0.1.5
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 +21 -7
- package/dist/bundle/github.js +34 -3
- package/dist/bundle/githubArchive.js +59 -7
- package/dist/bundle/service.js +283 -19
- package/dist/config.js +1 -0
- package/dist/context7/client.js +1 -1
- package/dist/evidence/dependencyGraph.js +312 -2
- package/dist/jobs/progressTracker.js +191 -0
- package/dist/server.js +310 -47
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -3,7 +3,8 @@ 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 { checkForUpdates, clearBundleMulti, createBundle, findBundleStorageDir, getBundlePathsForId, getEffectiveStorageDir, listBundles, repairBundle, updateBundle, } from './bundle/service.js';
|
|
6
|
+
import { assertBundleComplete, checkForUpdates, checkInProgressLock, clearBundleMulti, computeCreateInputFingerprint, createBundle, findBundleStorageDir, getBundlePathsForId, getEffectiveStorageDir, listBundles, repairBundle, updateBundle, } from './bundle/service.js';
|
|
7
|
+
import { getProgressTracker } from './jobs/progressTracker.js';
|
|
7
8
|
import { readManifest } from './bundle/manifest.js';
|
|
8
9
|
import { safeJoin, toBundleFileUri } from './mcp/uris.js';
|
|
9
10
|
import { wrapPreflightError } from './mcp/errorKinds.js';
|
|
@@ -81,9 +82,12 @@ const RepairBundleInputSchema = {
|
|
|
81
82
|
rebuildGuides: z.boolean().optional().describe('If true, rebuild START_HERE.md and AGENTS.md when missing/empty.'),
|
|
82
83
|
rebuildOverview: z.boolean().optional().describe('If true, rebuild OVERVIEW.md when missing/empty.'),
|
|
83
84
|
};
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
const GetTaskStatusInputSchema = {
|
|
86
|
+
taskId: z.string().optional().describe('Task ID to query (from BUNDLE_IN_PROGRESS error).'),
|
|
87
|
+
fingerprint: z.string().optional().describe('Fingerprint to query (computed from repos/libraries/topics).'),
|
|
88
|
+
repos: z.array(CreateRepoInputSchema).optional().describe('Repos to compute fingerprint from (alternative to fingerprint).'),
|
|
89
|
+
libraries: z.array(z.string()).optional().describe('Libraries for fingerprint computation.'),
|
|
90
|
+
topics: z.array(z.string()).optional().describe('Topics for fingerprint computation.'),
|
|
87
91
|
};
|
|
88
92
|
export async function startServer() {
|
|
89
93
|
const cfg = getConfig();
|
|
@@ -95,7 +99,7 @@ export async function startServer() {
|
|
|
95
99
|
startHttpServer(cfg);
|
|
96
100
|
const server = new McpServer({
|
|
97
101
|
name: 'preflight-mcp',
|
|
98
|
-
version: '0.1.
|
|
102
|
+
version: '0.1.5',
|
|
99
103
|
description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
|
|
100
104
|
}, {
|
|
101
105
|
capabilities: {
|
|
@@ -254,13 +258,20 @@ export async function startServer() {
|
|
|
254
258
|
};
|
|
255
259
|
});
|
|
256
260
|
server.registerTool('preflight_read_file', {
|
|
257
|
-
title: 'Read bundle file',
|
|
258
|
-
description: 'Read
|
|
259
|
-
|
|
261
|
+
title: 'Read bundle file(s)',
|
|
262
|
+
description: 'Read file(s) from bundle. Two modes: ' +
|
|
263
|
+
'(1) Omit "file" param → returns ALL key files (OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json, repo READMEs) in one call. ' +
|
|
264
|
+
'(2) Provide "file" param → returns that specific file. ' +
|
|
265
|
+
'Use when: "查看bundle", "show bundle", "read overview", "bundle概览", "项目信息".',
|
|
266
|
+
inputSchema: {
|
|
267
|
+
bundleId: z.string().describe('Bundle ID to read.'),
|
|
268
|
+
file: z.string().optional().describe('Specific file to read. If omitted, returns all key files (OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json, repo READMEs).'),
|
|
269
|
+
},
|
|
260
270
|
outputSchema: {
|
|
261
271
|
bundleId: z.string(),
|
|
262
|
-
file: z.string(),
|
|
263
|
-
content: z.string(),
|
|
272
|
+
file: z.string().optional(),
|
|
273
|
+
content: z.string().optional(),
|
|
274
|
+
files: z.record(z.string(), z.string().nullable()).optional(),
|
|
264
275
|
},
|
|
265
276
|
annotations: {
|
|
266
277
|
readOnlyHint: true,
|
|
@@ -271,16 +282,66 @@ export async function startServer() {
|
|
|
271
282
|
if (!storageDir) {
|
|
272
283
|
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
273
284
|
}
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
content
|
|
281
|
-
|
|
285
|
+
const paths = getBundlePathsForId(storageDir, args.bundleId);
|
|
286
|
+
const bundleRoot = paths.rootDir;
|
|
287
|
+
// Single file mode
|
|
288
|
+
if (args.file) {
|
|
289
|
+
const absPath = safeJoin(bundleRoot, args.file);
|
|
290
|
+
const content = await fs.readFile(absPath, 'utf8');
|
|
291
|
+
const out = { bundleId: args.bundleId, file: args.file, content };
|
|
292
|
+
return {
|
|
293
|
+
content: [{ type: 'text', text: content }],
|
|
294
|
+
structuredContent: out,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// Batch mode: read all key files
|
|
298
|
+
const keyFiles = ['OVERVIEW.md', 'START_HERE.md', 'AGENTS.md', 'manifest.json'];
|
|
299
|
+
const files = {};
|
|
300
|
+
for (const file of keyFiles) {
|
|
301
|
+
try {
|
|
302
|
+
const absPath = safeJoin(bundleRoot, file);
|
|
303
|
+
files[file] = await fs.readFile(absPath, 'utf8');
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
files[file] = null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Try to find and read repo README files
|
|
310
|
+
try {
|
|
311
|
+
const manifest = await readManifest(paths.manifestPath);
|
|
312
|
+
for (const repo of manifest.repos ?? []) {
|
|
313
|
+
if (!repo.id)
|
|
314
|
+
continue;
|
|
315
|
+
const [owner, repoName] = repo.id.split('/');
|
|
316
|
+
if (!owner || !repoName)
|
|
317
|
+
continue;
|
|
318
|
+
const readmeNames = ['README.md', 'readme.md', 'Readme.md', 'README.MD'];
|
|
319
|
+
for (const readmeName of readmeNames) {
|
|
320
|
+
const readmePath = `repos/${owner}/${repoName}/norm/${readmeName}`;
|
|
321
|
+
try {
|
|
322
|
+
const absPath = safeJoin(bundleRoot, readmePath);
|
|
323
|
+
files[readmePath] = await fs.readFile(absPath, 'utf8');
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Try next
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// Ignore manifest read errors
|
|
334
|
+
}
|
|
335
|
+
// Build combined text output
|
|
336
|
+
const textParts = [];
|
|
337
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
338
|
+
if (content) {
|
|
339
|
+
textParts.push(`=== ${filePath} ===\n${content}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const out = { bundleId: args.bundleId, files };
|
|
282
343
|
return {
|
|
283
|
-
content: [{ type: 'text', text:
|
|
344
|
+
content: [{ type: 'text', text: textParts.join('\n\n') || '(no files found)' }],
|
|
284
345
|
structuredContent: out,
|
|
285
346
|
};
|
|
286
347
|
}
|
|
@@ -321,22 +382,23 @@ export async function startServer() {
|
|
|
321
382
|
description: 'Create a new bundle from GitHub repos or local directories (or update an existing one if ifExists=updateExisting). Use when: "index this repo", "create bundle for", "add repo to preflight", "索引这个仓库", "创建bundle", "添加GitHub项目", "学习这个项目". NOTE: If the bundle contains code files, consider asking user if they want to generate dependency graph (preflight_evidence_dependency_graph) or establish trace links (preflight_trace_upsert).',
|
|
322
383
|
inputSchema: CreateBundleInputSchema,
|
|
323
384
|
outputSchema: {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
385
|
+
// Normal completion fields
|
|
386
|
+
bundleId: z.string().optional(),
|
|
387
|
+
createdAt: z.string().optional(),
|
|
388
|
+
updatedAt: z.string().optional(),
|
|
327
389
|
resources: z.object({
|
|
328
390
|
startHere: z.string(),
|
|
329
391
|
agents: z.string(),
|
|
330
392
|
overview: z.string(),
|
|
331
393
|
manifest: z.string(),
|
|
332
|
-
}),
|
|
394
|
+
}).optional(),
|
|
333
395
|
repos: z.array(z.object({
|
|
334
396
|
kind: z.enum(['github', 'local']),
|
|
335
397
|
id: z.string(),
|
|
336
398
|
source: z.enum(['git', 'archive', 'local']).optional(),
|
|
337
399
|
headSha: z.string().optional(),
|
|
338
400
|
notes: z.array(z.string()).optional(),
|
|
339
|
-
})),
|
|
401
|
+
})).optional(),
|
|
340
402
|
libraries: z
|
|
341
403
|
.array(z.object({
|
|
342
404
|
kind: z.literal('context7'),
|
|
@@ -347,6 +409,20 @@ export async function startServer() {
|
|
|
347
409
|
files: z.array(z.string()).optional(),
|
|
348
410
|
}))
|
|
349
411
|
.optional(),
|
|
412
|
+
// User-facing warnings (e.g., git clone failed, used zip fallback)
|
|
413
|
+
warnings: z.array(z.string()).optional(),
|
|
414
|
+
// In-progress status fields
|
|
415
|
+
status: z.enum(['in-progress', 'complete']).optional(),
|
|
416
|
+
message: z.string().optional(),
|
|
417
|
+
taskId: z.string().optional(),
|
|
418
|
+
fingerprint: z.string().optional(),
|
|
419
|
+
/** Repo IDs requested (for in-progress status only, different from repos array) */
|
|
420
|
+
requestedRepos: z.array(z.string()).optional(),
|
|
421
|
+
startedAt: z.string().optional(),
|
|
422
|
+
elapsedSeconds: z.number().optional(),
|
|
423
|
+
currentPhase: z.string().optional(),
|
|
424
|
+
currentProgress: z.number().optional(),
|
|
425
|
+
currentMessage: z.string().optional(),
|
|
350
426
|
},
|
|
351
427
|
annotations: {
|
|
352
428
|
openWorldHint: true,
|
|
@@ -370,12 +446,48 @@ export async function startServer() {
|
|
|
370
446
|
...summary,
|
|
371
447
|
resources,
|
|
372
448
|
};
|
|
449
|
+
// Build text response - prominently show warnings if any
|
|
450
|
+
let textResponse = '';
|
|
451
|
+
if (summary.warnings && summary.warnings.length > 0) {
|
|
452
|
+
textResponse += '📢 **Network Issues Encountered:**\n';
|
|
453
|
+
for (const warn of summary.warnings) {
|
|
454
|
+
textResponse += `${warn}\n`;
|
|
455
|
+
}
|
|
456
|
+
textResponse += '\n';
|
|
457
|
+
}
|
|
458
|
+
textResponse += `✅ Bundle created: ${summary.bundleId}\n`;
|
|
459
|
+
textResponse += `Repos: ${summary.repos.map(r => `${r.id} (${r.source})`).join(', ')}`;
|
|
373
460
|
return {
|
|
374
|
-
content: [{ type: 'text', text:
|
|
461
|
+
content: [{ type: 'text', text: textResponse }],
|
|
375
462
|
structuredContent: out,
|
|
376
463
|
};
|
|
377
464
|
}
|
|
378
465
|
catch (err) {
|
|
466
|
+
// Handle BUNDLE_IN_PROGRESS error specially - provide useful info instead of just error
|
|
467
|
+
if (err?.code === 'BUNDLE_IN_PROGRESS') {
|
|
468
|
+
const elapsedSec = err.startedAt
|
|
469
|
+
? Math.round((Date.now() - new Date(err.startedAt).getTime()) / 1000)
|
|
470
|
+
: 0;
|
|
471
|
+
// Check current progress from tracker
|
|
472
|
+
const tracker = getProgressTracker();
|
|
473
|
+
const task = err.taskId ? tracker.getTask(err.taskId) : undefined;
|
|
474
|
+
const out = {
|
|
475
|
+
status: 'in-progress',
|
|
476
|
+
message: `Bundle creation already in progress. Use preflight_get_task_status to check progress.`,
|
|
477
|
+
taskId: err.taskId,
|
|
478
|
+
fingerprint: err.fingerprint,
|
|
479
|
+
requestedRepos: err.repos,
|
|
480
|
+
startedAt: err.startedAt,
|
|
481
|
+
elapsedSeconds: elapsedSec,
|
|
482
|
+
currentPhase: task?.phase,
|
|
483
|
+
currentProgress: task?.progress,
|
|
484
|
+
currentMessage: task?.message,
|
|
485
|
+
};
|
|
486
|
+
return {
|
|
487
|
+
content: [{ type: 'text', text: `⚠️ Bundle creation in progress (${elapsedSec}s elapsed). ${task ? `Current: ${task.phase} (${task.progress}%) - ${task.message}` : 'Use preflight_get_task_status to check progress.'}` }],
|
|
488
|
+
structuredContent: out,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
379
491
|
throw wrapPreflightError(err);
|
|
380
492
|
}
|
|
381
493
|
});
|
|
@@ -388,6 +500,8 @@ export async function startServer() {
|
|
|
388
500
|
mode: z.enum(['validate', 'repair']),
|
|
389
501
|
repaired: z.boolean(),
|
|
390
502
|
actionsTaken: z.array(z.string()),
|
|
503
|
+
/** Issues that cannot be fixed by repair (require re-download) */
|
|
504
|
+
unfixableIssues: z.array(z.string()).optional(),
|
|
391
505
|
before: z.object({
|
|
392
506
|
isValid: z.boolean(),
|
|
393
507
|
missingComponents: z.array(z.string()),
|
|
@@ -409,11 +523,21 @@ export async function startServer() {
|
|
|
409
523
|
rebuildGuides: args.rebuildGuides,
|
|
410
524
|
rebuildOverview: args.rebuildOverview,
|
|
411
525
|
});
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
: out.
|
|
415
|
-
|
|
416
|
-
|
|
526
|
+
let summaryLine;
|
|
527
|
+
if (out.mode === 'validate') {
|
|
528
|
+
summaryLine = `VALIDATE ${out.bundleId}: ${out.before.isValid ? 'OK' : 'INVALID'} (${out.before.missingComponents.length} issue(s))`;
|
|
529
|
+
}
|
|
530
|
+
else if (out.unfixableIssues && out.unfixableIssues.length > 0) {
|
|
531
|
+
// Has unfixable issues - clearly communicate this
|
|
532
|
+
summaryLine = `⚠️ UNFIXABLE ${out.bundleId}: ${out.unfixableIssues.length} issue(s) cannot be repaired offline.\n` +
|
|
533
|
+
out.unfixableIssues.map(i => ` - ${i}`).join('\n');
|
|
534
|
+
}
|
|
535
|
+
else if (out.repaired) {
|
|
536
|
+
summaryLine = `REPAIRED ${out.bundleId}: ${out.actionsTaken.length} action(s), now ${out.after.isValid ? 'OK' : 'STILL_INVALID'} (${out.after.missingComponents.length} issue(s))`;
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
summaryLine = `NOOP ${out.bundleId}: nothing to repair (already OK)`;
|
|
540
|
+
}
|
|
417
541
|
return {
|
|
418
542
|
content: [{ type: 'text', text: summaryLine }],
|
|
419
543
|
structuredContent: out,
|
|
@@ -486,22 +610,38 @@ export async function startServer() {
|
|
|
486
610
|
structuredContent: out,
|
|
487
611
|
};
|
|
488
612
|
}
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
613
|
+
// Create task for progress tracking
|
|
614
|
+
const tracker = getProgressTracker();
|
|
615
|
+
const fingerprint = `update-${args.bundleId}`;
|
|
616
|
+
const taskId = tracker.startTask(fingerprint, [args.bundleId]);
|
|
617
|
+
try {
|
|
618
|
+
const { summary, changed } = await updateBundle(cfg, args.bundleId, {
|
|
619
|
+
force: args.force,
|
|
620
|
+
onProgress: (phase, progress, message, total) => {
|
|
621
|
+
tracker.updateProgress(taskId, phase, progress, message, total);
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
tracker.completeTask(taskId, args.bundleId);
|
|
625
|
+
const resources = {
|
|
626
|
+
startHere: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'START_HERE.md' }),
|
|
627
|
+
agents: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'AGENTS.md' }),
|
|
628
|
+
overview: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'OVERVIEW.md' }),
|
|
629
|
+
manifest: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'manifest.json' }),
|
|
630
|
+
};
|
|
631
|
+
const out = {
|
|
632
|
+
changed: args.force ? true : changed,
|
|
633
|
+
...summary,
|
|
634
|
+
resources,
|
|
635
|
+
};
|
|
636
|
+
return {
|
|
637
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
638
|
+
structuredContent: out,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
catch (updateErr) {
|
|
642
|
+
tracker.failTask(taskId, updateErr instanceof Error ? updateErr.message : String(updateErr));
|
|
643
|
+
throw updateErr;
|
|
644
|
+
}
|
|
505
645
|
}
|
|
506
646
|
catch (err) {
|
|
507
647
|
throw wrapPreflightError(err);
|
|
@@ -607,6 +747,8 @@ export async function startServer() {
|
|
|
607
747
|
},
|
|
608
748
|
}, async (args) => {
|
|
609
749
|
try {
|
|
750
|
+
// Check bundle completeness before any operation
|
|
751
|
+
await assertBundleComplete(cfg, args.bundleId);
|
|
610
752
|
// Resolve bundle location across storageDirs (more robust than a single effectiveDir).
|
|
611
753
|
const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
|
|
612
754
|
if (!storageDir) {
|
|
@@ -643,7 +785,11 @@ export async function startServer() {
|
|
|
643
785
|
});
|
|
644
786
|
server.registerTool('preflight_evidence_dependency_graph', {
|
|
645
787
|
title: 'Evidence: dependency graph (callers + imports)',
|
|
646
|
-
description: 'Generate an evidence-based dependency graph
|
|
788
|
+
description: 'Generate an evidence-based dependency graph. Two modes: ' +
|
|
789
|
+
'(1) TARGET MODE: provide target.file to analyze a specific file\'s imports and callers. ' +
|
|
790
|
+
'(2) GLOBAL MODE: omit target to generate a project-wide import graph of all code files. ' +
|
|
791
|
+
'For target mode, file path must be bundle-relative: repos/{owner}/{repo}/norm/{path}. ' +
|
|
792
|
+
'Use preflight_search_bundle to find file paths, or check OVERVIEW.md.',
|
|
647
793
|
inputSchema: DependencyGraphInputSchema,
|
|
648
794
|
outputSchema: {
|
|
649
795
|
meta: z.any(),
|
|
@@ -655,6 +801,8 @@ export async function startServer() {
|
|
|
655
801
|
},
|
|
656
802
|
}, async (args) => {
|
|
657
803
|
try {
|
|
804
|
+
// Check bundle completeness before generating dependency graph
|
|
805
|
+
await assertBundleComplete(cfg, args.bundleId);
|
|
658
806
|
const out = await generateDependencyGraph(cfg, args);
|
|
659
807
|
return {
|
|
660
808
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
@@ -679,6 +827,8 @@ export async function startServer() {
|
|
|
679
827
|
},
|
|
680
828
|
}, async (args) => {
|
|
681
829
|
try {
|
|
830
|
+
// Check bundle completeness before trace upsert
|
|
831
|
+
await assertBundleComplete(cfg, args.bundleId);
|
|
682
832
|
const out = await traceUpsert(cfg, args);
|
|
683
833
|
return {
|
|
684
834
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
@@ -715,6 +865,10 @@ export async function startServer() {
|
|
|
715
865
|
},
|
|
716
866
|
}, async (args) => {
|
|
717
867
|
try {
|
|
868
|
+
// Check bundle completeness if bundleId is provided
|
|
869
|
+
if (args.bundleId) {
|
|
870
|
+
await assertBundleComplete(cfg, args.bundleId);
|
|
871
|
+
}
|
|
718
872
|
const out = await traceQuery(cfg, args);
|
|
719
873
|
return {
|
|
720
874
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
@@ -763,6 +917,115 @@ export async function startServer() {
|
|
|
763
917
|
throw wrapPreflightError(err);
|
|
764
918
|
}
|
|
765
919
|
});
|
|
920
|
+
// Get task status - for checking progress of in-progress bundle creations
|
|
921
|
+
server.registerTool('preflight_get_task_status', {
|
|
922
|
+
title: 'Get task status',
|
|
923
|
+
description: 'Check status of bundle creation tasks (especially in-progress ones). Use when: "check bundle creation progress", "what is the status", "查看任务状态", "下载进度". Can query by taskId (from error), fingerprint, or repos.',
|
|
924
|
+
inputSchema: GetTaskStatusInputSchema,
|
|
925
|
+
outputSchema: {
|
|
926
|
+
found: z.boolean(),
|
|
927
|
+
task: z.object({
|
|
928
|
+
taskId: z.string(),
|
|
929
|
+
fingerprint: z.string(),
|
|
930
|
+
phase: z.string(),
|
|
931
|
+
progress: z.number(),
|
|
932
|
+
total: z.number().optional(),
|
|
933
|
+
message: z.string(),
|
|
934
|
+
startedAt: z.string(),
|
|
935
|
+
updatedAt: z.string(),
|
|
936
|
+
repos: z.array(z.string()),
|
|
937
|
+
bundleId: z.string().optional(),
|
|
938
|
+
error: z.string().optional(),
|
|
939
|
+
}).optional(),
|
|
940
|
+
inProgressLock: z.object({
|
|
941
|
+
bundleId: z.string(),
|
|
942
|
+
status: z.string(),
|
|
943
|
+
startedAt: z.string().optional(),
|
|
944
|
+
taskId: z.string().optional(),
|
|
945
|
+
repos: z.array(z.string()).optional(),
|
|
946
|
+
elapsedSeconds: z.number().optional(),
|
|
947
|
+
}).optional(),
|
|
948
|
+
activeTasks: z.array(z.object({
|
|
949
|
+
taskId: z.string(),
|
|
950
|
+
fingerprint: z.string(),
|
|
951
|
+
phase: z.string(),
|
|
952
|
+
progress: z.number(),
|
|
953
|
+
message: z.string(),
|
|
954
|
+
repos: z.array(z.string()),
|
|
955
|
+
startedAt: z.string(),
|
|
956
|
+
})).optional(),
|
|
957
|
+
},
|
|
958
|
+
annotations: {
|
|
959
|
+
readOnlyHint: true,
|
|
960
|
+
},
|
|
961
|
+
}, async (args) => {
|
|
962
|
+
try {
|
|
963
|
+
const tracker = getProgressTracker();
|
|
964
|
+
let result = { found: false };
|
|
965
|
+
// Compute fingerprint if repos provided
|
|
966
|
+
let fingerprint = args.fingerprint;
|
|
967
|
+
if (!fingerprint && args.repos?.length) {
|
|
968
|
+
fingerprint = computeCreateInputFingerprint({
|
|
969
|
+
repos: args.repos,
|
|
970
|
+
libraries: args.libraries,
|
|
971
|
+
topics: args.topics,
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
// Query by taskId
|
|
975
|
+
if (args.taskId) {
|
|
976
|
+
const task = tracker.getTask(args.taskId);
|
|
977
|
+
if (task) {
|
|
978
|
+
result = { found: true, task };
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
// Query by fingerprint
|
|
982
|
+
else if (fingerprint) {
|
|
983
|
+
const task = tracker.getTaskByFingerprint(fingerprint);
|
|
984
|
+
if (task) {
|
|
985
|
+
result = { found: true, task };
|
|
986
|
+
}
|
|
987
|
+
// Also check persistent in-progress lock
|
|
988
|
+
const lock = await checkInProgressLock(cfg, fingerprint);
|
|
989
|
+
if (lock) {
|
|
990
|
+
const elapsedSeconds = lock.startedAt
|
|
991
|
+
? Math.round((Date.now() - new Date(lock.startedAt).getTime()) / 1000)
|
|
992
|
+
: undefined;
|
|
993
|
+
result.inProgressLock = {
|
|
994
|
+
bundleId: lock.bundleId,
|
|
995
|
+
status: lock.status ?? 'unknown',
|
|
996
|
+
startedAt: lock.startedAt,
|
|
997
|
+
taskId: lock.taskId,
|
|
998
|
+
repos: lock.repos,
|
|
999
|
+
elapsedSeconds,
|
|
1000
|
+
};
|
|
1001
|
+
result.found = true;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
// If no specific query, return all active tasks
|
|
1005
|
+
else {
|
|
1006
|
+
const activeTasks = tracker.listActiveTasks();
|
|
1007
|
+
if (activeTasks.length > 0) {
|
|
1008
|
+
result = { found: true, activeTasks };
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const summary = result.found
|
|
1012
|
+
? result.task
|
|
1013
|
+
? `Task ${result.task.taskId}: ${result.task.phase} (${result.task.progress}%) - ${result.task.message}`
|
|
1014
|
+
: result.activeTasks
|
|
1015
|
+
? `${result.activeTasks.length} active task(s)`
|
|
1016
|
+
: result.inProgressLock
|
|
1017
|
+
? `In-progress lock found (started ${result.inProgressLock.elapsedSeconds}s ago)`
|
|
1018
|
+
: 'Status found'
|
|
1019
|
+
: 'No matching task found';
|
|
1020
|
+
return {
|
|
1021
|
+
content: [{ type: 'text', text: summary }],
|
|
1022
|
+
structuredContent: result,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
catch (err) {
|
|
1026
|
+
throw wrapPreflightError(err);
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
766
1029
|
// Provide backward-compatible parsing of the same URI via resources/read for clients that bypass templates.
|
|
767
1030
|
// This is a safety net: if a client gives us a fully-specified URI, we can still serve it.
|
|
768
1031
|
server.registerResource('bundle-file-compat', 'preflight://bundle-file', {
|