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/README.md
CHANGED
|
@@ -15,8 +15,12 @@ Each bundle contains:
|
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
18
|
+
- **13 MCP tools** to create/update/repair/search/read bundles, generate evidence graphs, and manage trace links
|
|
19
|
+
- **Progress tracking**: Real-time progress reporting for long-running operations (create/update bundles)
|
|
20
|
+
- **Bundle integrity check**: Prevents operations on incomplete bundles with helpful error messages
|
|
21
|
+
- **De-duplication with in-progress lock**: Prevent duplicate bundle creation even during MCP timeouts
|
|
22
|
+
- **Global dependency graph**: Generate project-wide import relationship graphs
|
|
23
|
+
- **Batch file reading**: Read all key bundle files in a single call
|
|
20
24
|
- **Resilient GitHub fetching**: configurable git clone timeout + GitHub archive (zipball) fallback
|
|
21
25
|
- **Offline repair**: rebuild missing/empty derived artifacts (index/guides/overview) without re-fetching
|
|
22
26
|
- **Static facts extraction** via `analysis/FACTS.json` (non-LLM)
|
|
@@ -117,7 +121,7 @@ Run end-to-end smoke test:
|
|
|
117
121
|
npm run smoke
|
|
118
122
|
```
|
|
119
123
|
|
|
120
|
-
## Tools (
|
|
124
|
+
## Tools (13 total)
|
|
121
125
|
|
|
122
126
|
### `preflight_list_bundles`
|
|
123
127
|
List bundle IDs in storage.
|
|
@@ -146,8 +150,10 @@ Input (example):
|
|
|
146
150
|
**Note**: If the bundle contains code files, consider using `preflight_evidence_dependency_graph` for dependency analysis or `preflight_trace_upsert` for trace links.
|
|
147
151
|
|
|
148
152
|
### `preflight_read_file`
|
|
149
|
-
Read
|
|
150
|
-
-
|
|
153
|
+
Read file(s) from bundle. Two modes:
|
|
154
|
+
- **Batch mode** (omit `file`): Returns ALL key files (OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json, repo READMEs) in one call
|
|
155
|
+
- **Single file mode** (provide `file`): Returns that specific file
|
|
156
|
+
- Triggers: "查看bundle", "bundle概览", "项目信息", "show bundle"
|
|
151
157
|
- Use `file: "manifest.json"` to get bundle metadata (repos, timestamps, tags, etc.)
|
|
152
158
|
|
|
153
159
|
### `preflight_delete_bundle`
|
|
@@ -189,10 +195,12 @@ Optional parameters:
|
|
|
189
195
|
- `limit`: Max total hits across all bundles
|
|
190
196
|
|
|
191
197
|
### `preflight_evidence_dependency_graph`
|
|
192
|
-
Generate an evidence-based dependency graph
|
|
198
|
+
Generate an evidence-based dependency graph. Two modes:
|
|
199
|
+
- **Target mode** (provide `target.file`): Analyze a specific file's imports and callers
|
|
200
|
+
- **Global mode** (omit `target`): Generate project-wide import graph of all code files
|
|
193
201
|
- Deterministic output with source ranges for edges.
|
|
194
202
|
- Uses Tree-sitter parsing when `PREFLIGHT_AST_ENGINE=wasm`; falls back to regex extraction otherwise.
|
|
195
|
-
- Emits `imports` edges (file → module) and
|
|
203
|
+
- Emits `imports` edges (file → module) and `imports_resolved` edges (file → internal file).
|
|
196
204
|
|
|
197
205
|
### `preflight_trace_upsert`
|
|
198
206
|
Upsert traceability links (commit↔ticket, symbol↔test, code↔doc, etc.) for a bundle.
|
|
@@ -210,6 +218,12 @@ Parameters:
|
|
|
210
218
|
|
|
211
219
|
Note: This is also automatically executed on server startup (background, non-blocking).
|
|
212
220
|
|
|
221
|
+
### `preflight_get_task_status`
|
|
222
|
+
Check status of bundle creation/update tasks (progress tracking).
|
|
223
|
+
- Triggers: "check progress", "what is the status", "查看任务状态", "下载进度"
|
|
224
|
+
- Query by `taskId` (from error), `fingerprint`, or `repos`
|
|
225
|
+
- Shows: phase, progress percentage, message, elapsed time
|
|
226
|
+
|
|
213
227
|
## Resources
|
|
214
228
|
|
|
215
229
|
### `preflight://bundles`
|
package/dist/bundle/github.js
CHANGED
|
@@ -13,8 +13,26 @@ export function parseOwnerRepo(input) {
|
|
|
13
13
|
export function toCloneUrl(ref) {
|
|
14
14
|
return `https://github.com/${ref.owner}/${ref.repo}.git`;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse git clone progress from stderr.
|
|
18
|
+
* Git outputs progress like:
|
|
19
|
+
* - "Receiving objects: 45% (1234/2741)"
|
|
20
|
+
* - "Resolving deltas: 60% (100/167)"
|
|
21
|
+
*/
|
|
22
|
+
function parseGitProgress(line) {
|
|
23
|
+
// Match patterns like "Receiving objects: 45% (1234/2741)"
|
|
24
|
+
const match = line.match(/(Receiving objects|Resolving deltas|Counting objects|Compressing objects):\s+(\d+)%/);
|
|
25
|
+
if (match) {
|
|
26
|
+
return {
|
|
27
|
+
phase: match[1],
|
|
28
|
+
percent: parseInt(match[2], 10),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
16
33
|
async function runGit(args, opts) {
|
|
17
34
|
const timeoutMs = opts?.timeoutMs ?? 5 * 60_000;
|
|
35
|
+
const onProgress = opts?.onProgress;
|
|
18
36
|
return new Promise((resolve, reject) => {
|
|
19
37
|
const child = spawn('git', args, {
|
|
20
38
|
cwd: opts?.cwd,
|
|
@@ -65,7 +83,19 @@ async function runGit(args, opts) {
|
|
|
65
83
|
stdout += data.toString('utf8');
|
|
66
84
|
});
|
|
67
85
|
child.stderr?.on('data', (data) => {
|
|
68
|
-
|
|
86
|
+
const chunk = data.toString('utf8');
|
|
87
|
+
stderr += chunk;
|
|
88
|
+
// Parse and report progress
|
|
89
|
+
if (onProgress) {
|
|
90
|
+
// Git progress can come in chunks, split by lines
|
|
91
|
+
const lines = chunk.split(/[\r\n]+/);
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
const progress = parseGitProgress(line);
|
|
94
|
+
if (progress) {
|
|
95
|
+
onProgress(progress.phase, progress.percent, `${progress.phase}: ${progress.percent}%`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
69
99
|
});
|
|
70
100
|
child.on('error', (err) => {
|
|
71
101
|
cleanup();
|
|
@@ -125,14 +155,15 @@ export async function shallowClone(cloneUrl, destDir, opts) {
|
|
|
125
155
|
await fs.mkdir(path.dirname(destDir), { recursive: true });
|
|
126
156
|
// Clean dest if exists.
|
|
127
157
|
await fs.rm(destDir, { recursive: true, force: true });
|
|
128
|
-
|
|
158
|
+
// Use --progress to force progress output even when not attached to a terminal
|
|
159
|
+
const args = ['-c', 'core.autocrlf=false', 'clone', '--depth', '1', '--no-tags', '--single-branch', '--progress'];
|
|
129
160
|
if (opts?.ref) {
|
|
130
161
|
// Validate ref before using it in git command
|
|
131
162
|
validateGitRef(opts.ref);
|
|
132
163
|
args.push('--branch', opts.ref);
|
|
133
164
|
}
|
|
134
165
|
args.push(cloneUrl, destDir);
|
|
135
|
-
await runGit(args, { timeoutMs: opts?.timeoutMs ?? 15 * 60_000 });
|
|
166
|
+
await runGit(args, { timeoutMs: opts?.timeoutMs ?? 15 * 60_000, onProgress: opts?.onProgress });
|
|
136
167
|
}
|
|
137
168
|
export async function getLocalHeadSha(repoDir) {
|
|
138
169
|
const { stdout } = await runGit(['-C', repoDir, 'rev-parse', 'HEAD']);
|
|
@@ -7,7 +7,7 @@ function nowIso() {
|
|
|
7
7
|
}
|
|
8
8
|
function githubHeaders(cfg) {
|
|
9
9
|
const headers = {
|
|
10
|
-
'User-Agent': 'preflight-mcp/0.1.
|
|
10
|
+
'User-Agent': 'preflight-mcp/0.1.5',
|
|
11
11
|
Accept: 'application/vnd.github+json',
|
|
12
12
|
};
|
|
13
13
|
if (cfg.githubToken) {
|
|
@@ -36,7 +36,7 @@ async function fetchJson(url, headers, timeoutMs = DEFAULT_API_TIMEOUT_MS) {
|
|
|
36
36
|
clearTimeout(timeoutId);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
async function downloadToFile(url, headers, destPath, timeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS) {
|
|
39
|
+
async function downloadToFile(url, headers, destPath, timeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS, onProgress) {
|
|
40
40
|
const controller = new AbortController();
|
|
41
41
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
42
42
|
try {
|
|
@@ -44,12 +44,51 @@ async function downloadToFile(url, headers, destPath, timeoutMs = DEFAULT_DOWNLO
|
|
|
44
44
|
if (!res.ok) {
|
|
45
45
|
throw new Error(`Download error ${res.status}: ${res.statusText}`);
|
|
46
46
|
}
|
|
47
|
-
//
|
|
47
|
+
// Get content length for progress reporting
|
|
48
|
+
const contentLengthHeader = res.headers.get('content-length');
|
|
49
|
+
const totalBytes = contentLengthHeader ? parseInt(contentLengthHeader, 10) : undefined;
|
|
50
|
+
await ensureDir(path.dirname(destPath));
|
|
51
|
+
// Use streaming to report progress
|
|
48
52
|
const anyRes = res;
|
|
49
53
|
const body = anyRes.body;
|
|
50
|
-
|
|
54
|
+
if (body && typeof body[Symbol.asyncIterator] === 'function') {
|
|
55
|
+
// Async iterator for progress tracking
|
|
56
|
+
const fsModule = await import('node:fs');
|
|
57
|
+
const ws = fsModule.createWriteStream(destPath);
|
|
58
|
+
let downloadedBytes = 0;
|
|
59
|
+
let lastReportTime = Date.now();
|
|
60
|
+
const reportIntervalMs = 500; // Report at most every 500ms
|
|
61
|
+
try {
|
|
62
|
+
for await (const chunk of body) {
|
|
63
|
+
ws.write(chunk);
|
|
64
|
+
downloadedBytes += chunk.length;
|
|
65
|
+
// Throttle progress reports
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
if (onProgress && (now - lastReportTime > reportIntervalMs)) {
|
|
68
|
+
lastReportTime = now;
|
|
69
|
+
const percent = totalBytes ? Math.round((downloadedBytes / totalBytes) * 100) : 0;
|
|
70
|
+
const msg = totalBytes
|
|
71
|
+
? `Downloaded ${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)} (${percent}%)`
|
|
72
|
+
: `Downloaded ${formatBytes(downloadedBytes)}`;
|
|
73
|
+
onProgress(downloadedBytes, totalBytes, msg);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
ws.end();
|
|
79
|
+
await new Promise((resolve) => ws.on('finish', () => resolve()));
|
|
80
|
+
}
|
|
81
|
+
// Final progress report
|
|
82
|
+
if (onProgress) {
|
|
83
|
+
const msg = totalBytes
|
|
84
|
+
? `Downloaded ${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)} (100%)`
|
|
85
|
+
: `Downloaded ${formatBytes(downloadedBytes)}`;
|
|
86
|
+
onProgress(downloadedBytes, totalBytes, msg);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
51
90
|
if (body && typeof body.pipe === 'function') {
|
|
52
|
-
// Node.js stream
|
|
91
|
+
// Node.js stream (fallback without progress)
|
|
53
92
|
const ws = (await import('node:fs')).createWriteStream(destPath);
|
|
54
93
|
await new Promise((resolve, reject) => {
|
|
55
94
|
body.pipe(ws);
|
|
@@ -59,14 +98,27 @@ async function downloadToFile(url, headers, destPath, timeoutMs = DEFAULT_DOWNLO
|
|
|
59
98
|
});
|
|
60
99
|
return;
|
|
61
100
|
}
|
|
62
|
-
// Web stream or no stream support
|
|
101
|
+
// Web stream or no stream support (fallback without progress)
|
|
63
102
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
64
103
|
await fs.writeFile(destPath, buf);
|
|
104
|
+
if (onProgress) {
|
|
105
|
+
onProgress(buf.length, buf.length, `Downloaded ${formatBytes(buf.length)}`);
|
|
106
|
+
}
|
|
65
107
|
}
|
|
66
108
|
finally {
|
|
67
109
|
clearTimeout(timeoutId);
|
|
68
110
|
}
|
|
69
111
|
}
|
|
112
|
+
/** Format bytes for display */
|
|
113
|
+
function formatBytes(bytes) {
|
|
114
|
+
if (bytes < 1024)
|
|
115
|
+
return `${bytes}B`;
|
|
116
|
+
if (bytes < 1024 * 1024)
|
|
117
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
118
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
119
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
120
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
|
|
121
|
+
}
|
|
70
122
|
async function extractZip(zipPath, destDir) {
|
|
71
123
|
await ensureDir(destDir);
|
|
72
124
|
const zip = new AdmZip(zipPath);
|
|
@@ -91,7 +143,7 @@ export async function downloadAndExtractGitHubArchive(params) {
|
|
|
91
143
|
// Use the API zipball endpoint so ref can be branch/tag/SHA (including slashes via URL-encoding).
|
|
92
144
|
const zipballUrl = `https://api.github.com/repos/${params.owner}/${params.repo}/zipball/${encodeURIComponent(refUsed)}`;
|
|
93
145
|
await ensureDir(params.destDir);
|
|
94
|
-
await downloadToFile(zipballUrl, headers, zipPath);
|
|
146
|
+
await downloadToFile(zipballUrl, headers, zipPath, DEFAULT_DOWNLOAD_TIMEOUT_MS, params.onProgress);
|
|
95
147
|
const extractDir = path.join(params.destDir, `extracted-${Date.now()}`);
|
|
96
148
|
await extractZip(zipPath, extractDir);
|
|
97
149
|
const repoRoot = await findSingleTopLevelDir(extractDir);
|