preflight-mcp 0.1.0
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 +208 -0
- package/README.zh-CN.md +406 -0
- package/dist/bundle/analysis.js +91 -0
- package/dist/bundle/context7.js +301 -0
- package/dist/bundle/deepwiki.js +206 -0
- package/dist/bundle/facts.js +296 -0
- package/dist/bundle/github.js +55 -0
- package/dist/bundle/guides.js +65 -0
- package/dist/bundle/ingest.js +152 -0
- package/dist/bundle/manifest.js +14 -0
- package/dist/bundle/overview.js +222 -0
- package/dist/bundle/paths.js +29 -0
- package/dist/bundle/service.js +803 -0
- package/dist/bundle/tagging.js +206 -0
- package/dist/config.js +65 -0
- package/dist/context7/client.js +30 -0
- package/dist/context7/tools.js +58 -0
- package/dist/core/scheduler.js +166 -0
- package/dist/errors.js +150 -0
- package/dist/index.js +7 -0
- package/dist/jobs/bundle-auto-update-job.js +71 -0
- package/dist/jobs/health-check-job.js +172 -0
- package/dist/jobs/storage-cleanup-job.js +148 -0
- package/dist/logging/logger.js +311 -0
- package/dist/mcp/uris.js +45 -0
- package/dist/search/sqliteFts.js +481 -0
- package/dist/server/optimized-server.js +255 -0
- package/dist/server.js +778 -0
- package/dist/storage/compression.js +249 -0
- package/dist/storage/storage-adapter.js +316 -0
- package/dist/utils/index.js +100 -0
- package/package.json +44 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export var LogLevel;
|
|
4
|
+
(function (LogLevel) {
|
|
5
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
6
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
7
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
8
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
9
|
+
LogLevel[LogLevel["FATAL"] = 4] = "FATAL";
|
|
10
|
+
})(LogLevel || (LogLevel = {}));
|
|
11
|
+
export class StructuredLogger {
|
|
12
|
+
config;
|
|
13
|
+
logBuffer = [];
|
|
14
|
+
bufferSize = 1000;
|
|
15
|
+
flushInterval = 5000; // 5秒
|
|
16
|
+
flushTimer;
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this.config = {
|
|
19
|
+
level: LogLevel.INFO,
|
|
20
|
+
output: 'both',
|
|
21
|
+
maxFileSize: 10, // 10MB
|
|
22
|
+
maxFiles: 5,
|
|
23
|
+
enableColors: true,
|
|
24
|
+
enableTimestamp: true,
|
|
25
|
+
enableMetadata: true,
|
|
26
|
+
enableStackTrace: true,
|
|
27
|
+
format: 'text',
|
|
28
|
+
...config
|
|
29
|
+
};
|
|
30
|
+
this.startFlushTimer();
|
|
31
|
+
}
|
|
32
|
+
startFlushTimer() {
|
|
33
|
+
this.flushTimer = setInterval(() => {
|
|
34
|
+
this.flush().catch(error => {
|
|
35
|
+
console.error('Failed to flush logs:', error);
|
|
36
|
+
});
|
|
37
|
+
}, this.flushInterval);
|
|
38
|
+
// Don't keep the process alive just for log flushing (important for tests/CLI runs).
|
|
39
|
+
this.flushTimer.unref?.();
|
|
40
|
+
}
|
|
41
|
+
async flush() {
|
|
42
|
+
if (this.logBuffer.length === 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const entries = [...this.logBuffer];
|
|
46
|
+
this.logBuffer = [];
|
|
47
|
+
if (this.config.output === 'file' || this.config.output === 'both') {
|
|
48
|
+
await this.writeToFile(entries);
|
|
49
|
+
}
|
|
50
|
+
if (this.config.output === 'console' || this.config.output === 'both') {
|
|
51
|
+
this.writeToConsole(entries);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async writeToFile(entries) {
|
|
55
|
+
if (!this.config.filePath) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
// Ensure log directory exists
|
|
60
|
+
const logDir = path.dirname(this.config.filePath);
|
|
61
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
62
|
+
// Check file size and rotate if over limit
|
|
63
|
+
await this.rotateLogFile();
|
|
64
|
+
// Write log entries
|
|
65
|
+
const logLines = entries.map(entry => this.formatLogEntry(entry, 'json'));
|
|
66
|
+
await fs.appendFile(this.config.filePath, logLines.join('\n') + '\n');
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error('Failed to write logs to file:', error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async rotateLogFile() {
|
|
73
|
+
if (!this.config.filePath) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const stats = await fs.stat(this.config.filePath);
|
|
78
|
+
const maxSizeBytes = (this.config.maxFileSize || 10) * 1024 * 1024;
|
|
79
|
+
if (stats.size > maxSizeBytes) {
|
|
80
|
+
// Rotate log file
|
|
81
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
82
|
+
const rotatedPath = `${this.config.filePath}.${timestamp}`;
|
|
83
|
+
await fs.rename(this.config.filePath, rotatedPath);
|
|
84
|
+
// Cleanup old log files
|
|
85
|
+
await this.cleanupOldLogFiles();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// File doesn't exist or other error, ignore
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async cleanupOldLogFiles() {
|
|
93
|
+
if (!this.config.filePath) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const logDir = path.dirname(this.config.filePath);
|
|
98
|
+
const logName = path.basename(this.config.filePath);
|
|
99
|
+
const files = await fs.readdir(logDir);
|
|
100
|
+
// Get file info and sort by mtime
|
|
101
|
+
const logFilesWithMtime = [];
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
if (file.startsWith(logName) && file !== logName) {
|
|
104
|
+
const filePath = path.join(logDir, file);
|
|
105
|
+
try {
|
|
106
|
+
const stats = await fs.stat(filePath);
|
|
107
|
+
logFilesWithMtime.push({
|
|
108
|
+
name: file,
|
|
109
|
+
path: filePath,
|
|
110
|
+
mtime: stats.mtime
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Skip files that can't be stat'd
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Sort by modification time
|
|
119
|
+
logFilesWithMtime.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
120
|
+
// Keep the newest files, delete the rest
|
|
121
|
+
const maxFiles = this.config.maxFiles || 5;
|
|
122
|
+
if (logFilesWithMtime.length > maxFiles) {
|
|
123
|
+
const filesToDelete = logFilesWithMtime.slice(maxFiles);
|
|
124
|
+
for (const file of filesToDelete) {
|
|
125
|
+
try {
|
|
126
|
+
await fs.unlink(file.path);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error(`Failed to delete old log file ${file.path}:`, error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.error('Failed to cleanup old log files:', error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
writeToConsole(entries) {
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const formatted = this.formatLogEntry(entry, 'text');
|
|
141
|
+
if (this.config.enableColors) {
|
|
142
|
+
this.writeColoredConsole(entry, formatted);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// MCP stdio servers must log to stderr to avoid interfering with protocol
|
|
146
|
+
console.error(formatted);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
writeColoredConsole(entry, formatted) {
|
|
151
|
+
const colors = {
|
|
152
|
+
[LogLevel.DEBUG]: '\x1b[36m', // Cyan
|
|
153
|
+
[LogLevel.INFO]: '\x1b[32m', // Green
|
|
154
|
+
[LogLevel.WARN]: '\x1b[33m', // Yellow
|
|
155
|
+
[LogLevel.ERROR]: '\x1b[31m', // Red
|
|
156
|
+
[LogLevel.FATAL]: '\x1b[35m' // Magenta
|
|
157
|
+
};
|
|
158
|
+
const reset = '\x1b[0m';
|
|
159
|
+
const color = colors[entry.level] || '';
|
|
160
|
+
// MCP stdio servers must log to stderr to avoid interfering with protocol
|
|
161
|
+
console.error(`${color}${formatted}${reset}`);
|
|
162
|
+
}
|
|
163
|
+
formatLogEntry(entry, format) {
|
|
164
|
+
if (format === 'json') {
|
|
165
|
+
return JSON.stringify(entry);
|
|
166
|
+
}
|
|
167
|
+
const parts = [];
|
|
168
|
+
// Timestamp
|
|
169
|
+
if (this.config.enableTimestamp) {
|
|
170
|
+
parts.push(`[${entry.timestamp}]`);
|
|
171
|
+
}
|
|
172
|
+
// Log level
|
|
173
|
+
parts.push(`[${entry.levelName}]`);
|
|
174
|
+
// Module and function info
|
|
175
|
+
if (entry.module || entry.function) {
|
|
176
|
+
const location = [entry.module, entry.function].filter(Boolean).join('.');
|
|
177
|
+
parts.push(`[${location}]`);
|
|
178
|
+
}
|
|
179
|
+
// Main message
|
|
180
|
+
parts.push(entry.message);
|
|
181
|
+
// Metadata
|
|
182
|
+
if (entry.metadata && Object.keys(entry.metadata).length > 0) {
|
|
183
|
+
parts.push(`| ${JSON.stringify(entry.metadata)}`);
|
|
184
|
+
}
|
|
185
|
+
// Error info
|
|
186
|
+
if (entry.error && this.config.enableStackTrace && entry.error.stack) {
|
|
187
|
+
parts.push(`\n${entry.error.stack}`);
|
|
188
|
+
}
|
|
189
|
+
return parts.join(' ');
|
|
190
|
+
}
|
|
191
|
+
createLogEntry(level, message, metadata, error) {
|
|
192
|
+
const stack = new Error().stack;
|
|
193
|
+
const callerLine = stack?.split('\n')[3]; // 获取调用栈的第3行
|
|
194
|
+
let module;
|
|
195
|
+
let func;
|
|
196
|
+
let line;
|
|
197
|
+
if (callerLine) {
|
|
198
|
+
const match = callerLine.match(/at\s+(.+?)\s+\((.+?):(\d+):\d+\)/);
|
|
199
|
+
if (match && match[1] && match[2] && match[3]) {
|
|
200
|
+
func = match[1];
|
|
201
|
+
const filePath = match[2];
|
|
202
|
+
line = parseInt(match[3], 10);
|
|
203
|
+
module = path.basename(filePath, '.js');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
timestamp: new Date().toISOString(),
|
|
208
|
+
level,
|
|
209
|
+
levelName: LogLevel[level],
|
|
210
|
+
message,
|
|
211
|
+
module,
|
|
212
|
+
function: func,
|
|
213
|
+
line,
|
|
214
|
+
metadata,
|
|
215
|
+
error: error ? {
|
|
216
|
+
name: error.name,
|
|
217
|
+
message: error.message,
|
|
218
|
+
stack: error.stack
|
|
219
|
+
} : undefined
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
log(level, message, metadata, error) {
|
|
223
|
+
if (level < this.config.level) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const entry = this.createLogEntry(level, message, metadata, error);
|
|
227
|
+
if (this.config.output === 'console' || this.config.output === 'both') {
|
|
228
|
+
// For console output, write immediately
|
|
229
|
+
this.writeToConsole([entry]);
|
|
230
|
+
}
|
|
231
|
+
if (this.config.output === 'file' || this.config.output === 'both') {
|
|
232
|
+
// For file output, buffer and batch write
|
|
233
|
+
this.logBuffer.push(entry);
|
|
234
|
+
if (this.logBuffer.length >= this.bufferSize) {
|
|
235
|
+
this.flush().catch(error => {
|
|
236
|
+
console.error('Failed to flush logs:', error);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
debug(message, metadata) {
|
|
242
|
+
this.log(LogLevel.DEBUG, message, metadata);
|
|
243
|
+
}
|
|
244
|
+
info(message, metadata) {
|
|
245
|
+
this.log(LogLevel.INFO, message, metadata);
|
|
246
|
+
}
|
|
247
|
+
warn(message, metadata) {
|
|
248
|
+
this.log(LogLevel.WARN, message, metadata);
|
|
249
|
+
}
|
|
250
|
+
error(message, error, metadata) {
|
|
251
|
+
this.log(LogLevel.ERROR, message, metadata, error);
|
|
252
|
+
}
|
|
253
|
+
fatal(message, error, metadata) {
|
|
254
|
+
this.log(LogLevel.FATAL, message, metadata, error);
|
|
255
|
+
}
|
|
256
|
+
// Flush buffer immediately
|
|
257
|
+
async flushNow() {
|
|
258
|
+
await this.flush();
|
|
259
|
+
}
|
|
260
|
+
// Update configuration
|
|
261
|
+
updateConfig(config) {
|
|
262
|
+
this.config = { ...this.config, ...config };
|
|
263
|
+
}
|
|
264
|
+
// Get current configuration
|
|
265
|
+
getConfig() {
|
|
266
|
+
return { ...this.config };
|
|
267
|
+
}
|
|
268
|
+
// Close the logger
|
|
269
|
+
async close() {
|
|
270
|
+
if (this.flushTimer) {
|
|
271
|
+
clearInterval(this.flushTimer);
|
|
272
|
+
}
|
|
273
|
+
await this.flush();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Default logger instance
|
|
277
|
+
export const defaultLogger = new StructuredLogger({
|
|
278
|
+
level: LogLevel.INFO,
|
|
279
|
+
output: 'both',
|
|
280
|
+
filePath: './logs/preflight-mcp.log',
|
|
281
|
+
format: 'text'
|
|
282
|
+
});
|
|
283
|
+
// Convenience functions
|
|
284
|
+
export const logger = {
|
|
285
|
+
debug: (message, metadata) => defaultLogger.debug(message, metadata),
|
|
286
|
+
info: (message, metadata) => defaultLogger.info(message, metadata),
|
|
287
|
+
warn: (message, metadata) => defaultLogger.warn(message, metadata),
|
|
288
|
+
error: (message, error, metadata) => defaultLogger.error(message, error, metadata),
|
|
289
|
+
fatal: (message, error, metadata) => defaultLogger.fatal(message, error, metadata),
|
|
290
|
+
flush: () => defaultLogger.flushNow(),
|
|
291
|
+
updateConfig: (config) => defaultLogger.updateConfig(config),
|
|
292
|
+
getConfig: () => defaultLogger.getConfig(),
|
|
293
|
+
close: () => defaultLogger.close()
|
|
294
|
+
};
|
|
295
|
+
// Create a module-specific logger
|
|
296
|
+
export function createModuleLogger(moduleName, config) {
|
|
297
|
+
const moduleConfig = {
|
|
298
|
+
...config,
|
|
299
|
+
// Module-specific configuration can be added here
|
|
300
|
+
};
|
|
301
|
+
const moduleLogger = new StructuredLogger(moduleConfig);
|
|
302
|
+
return {
|
|
303
|
+
debug: (message, metadata) => moduleLogger.debug(message, { module: moduleName, ...metadata }),
|
|
304
|
+
info: (message, metadata) => moduleLogger.info(message, { module: moduleName, ...metadata }),
|
|
305
|
+
warn: (message, metadata) => moduleLogger.warn(message, { module: moduleName, ...metadata }),
|
|
306
|
+
error: (message, error, metadata) => moduleLogger.error(message, error, { module: moduleName, ...metadata }),
|
|
307
|
+
fatal: (message, error, metadata) => moduleLogger.fatal(message, error, { module: moduleName, ...metadata }),
|
|
308
|
+
flush: () => moduleLogger.flushNow(),
|
|
309
|
+
close: () => moduleLogger.close()
|
|
310
|
+
};
|
|
311
|
+
}
|
package/dist/mcp/uris.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export const PREFLIGHT_URI_PREFIX = 'preflight://bundle/';
|
|
3
|
+
export function toBundleFileUri(ref) {
|
|
4
|
+
// encodedPath must not contain slashes so it can live as a single path segment
|
|
5
|
+
const encodedPath = encodeURIComponent(ref.relativePath);
|
|
6
|
+
return `${PREFLIGHT_URI_PREFIX}${ref.bundleId}/file/${encodedPath}`;
|
|
7
|
+
}
|
|
8
|
+
export function parseBundleFileUri(uri) {
|
|
9
|
+
if (!uri.startsWith(PREFLIGHT_URI_PREFIX))
|
|
10
|
+
return null;
|
|
11
|
+
const rest = uri.slice(PREFLIGHT_URI_PREFIX.length);
|
|
12
|
+
// rest = <bundleId>/file/<encodedPath>
|
|
13
|
+
const parts = rest.split('/');
|
|
14
|
+
if (parts.length !== 3)
|
|
15
|
+
return null;
|
|
16
|
+
const [bundleId, fileLiteral, encodedPath] = parts;
|
|
17
|
+
if (!bundleId || fileLiteral !== 'file' || !encodedPath)
|
|
18
|
+
return null;
|
|
19
|
+
const relativePath = decodeURIComponent(encodedPath);
|
|
20
|
+
return {
|
|
21
|
+
bundleId,
|
|
22
|
+
relativePath,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function normalizeRelativePath(p) {
|
|
26
|
+
// Ensure forward slashes and no leading slash.
|
|
27
|
+
return p.replaceAll('\\', '/').replace(/^\/+/, '');
|
|
28
|
+
}
|
|
29
|
+
export function safeJoin(rootDir, relativePath) {
|
|
30
|
+
// Block absolute paths BEFORE normalization.
|
|
31
|
+
// This catches Unix-style /etc/passwd and Windows-style C:\path.
|
|
32
|
+
const trimmed = relativePath.trim();
|
|
33
|
+
if (trimmed.startsWith('/') || trimmed.startsWith('\\') || /^[a-zA-Z]:/.test(trimmed)) {
|
|
34
|
+
throw new Error('Unsafe path traversal attempt');
|
|
35
|
+
}
|
|
36
|
+
// Convert to platform separator for join, but validate containment by resolving.
|
|
37
|
+
const norm = normalizeRelativePath(relativePath);
|
|
38
|
+
const joined = path.resolve(rootDir, norm.split('/').join(path.sep));
|
|
39
|
+
const rootResolved = path.resolve(rootDir);
|
|
40
|
+
const rel = path.relative(rootResolved, joined);
|
|
41
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
42
|
+
throw new Error('Unsafe path traversal attempt');
|
|
43
|
+
}
|
|
44
|
+
return joined;
|
|
45
|
+
}
|