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.
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Auto-detect tags for a bundle based on its content
3
+ */
4
+ export function autoDetectTags(params) {
5
+ const tags = new Set();
6
+ // 1. Detect by repo name patterns
7
+ for (const repoId of params.repoIds) {
8
+ const lowerRepo = repoId.toLowerCase();
9
+ // MCP related
10
+ if (lowerRepo.includes('mcp') || lowerRepo.includes('model-context-protocol')) {
11
+ tags.add('mcp');
12
+ tags.add('ai-tools');
13
+ }
14
+ // Agent frameworks
15
+ if (lowerRepo.includes('agent') || lowerRepo.includes('langchain') || lowerRepo.includes('autogen')) {
16
+ tags.add('agents');
17
+ tags.add('ai');
18
+ }
19
+ // Development tools
20
+ if (lowerRepo.includes('tool') || lowerRepo.includes('cli') || lowerRepo.includes('util')) {
21
+ tags.add('dev-tools');
22
+ }
23
+ // Testing/debugging
24
+ if (lowerRepo.includes('test') || lowerRepo.includes('debug') || lowerRepo.includes('mock')) {
25
+ tags.add('testing');
26
+ tags.add('debugging');
27
+ }
28
+ // Web scraping / crawling
29
+ if (lowerRepo.includes('scraper') || lowerRepo.includes('crawler') || lowerRepo.includes('spider')) {
30
+ tags.add('web-scraping');
31
+ }
32
+ // Anti-detection / bypassing
33
+ if (lowerRepo.includes('bypass') || lowerRepo.includes('anti') || lowerRepo.includes('stealth')) {
34
+ tags.add('anti-detection');
35
+ tags.add('web-scraping');
36
+ }
37
+ // Code analysis
38
+ if (lowerRepo.includes('lint') || lowerRepo.includes('analyzer') || lowerRepo.includes('ast')) {
39
+ tags.add('code-analysis');
40
+ tags.add('dev-tools');
41
+ }
42
+ }
43
+ // 2. Detect by frameworks (if facts available)
44
+ if (params.facts) {
45
+ for (const framework of params.facts.frameworks) {
46
+ const lowerFw = framework.toLowerCase();
47
+ if (lowerFw.includes('react') || lowerFw.includes('vue') || lowerFw.includes('angular')) {
48
+ tags.add('frontend');
49
+ tags.add('web-framework');
50
+ }
51
+ if (lowerFw.includes('express') || lowerFw.includes('fastify') || lowerFw.includes('nestjs')) {
52
+ tags.add('backend');
53
+ tags.add('web-framework');
54
+ }
55
+ if (lowerFw.includes('next') || lowerFw.includes('nuxt')) {
56
+ tags.add('full-stack');
57
+ tags.add('web-framework');
58
+ }
59
+ if (lowerFw.includes('django') || lowerFw.includes('flask') || lowerFw.includes('fastapi')) {
60
+ tags.add('backend');
61
+ tags.add('python');
62
+ }
63
+ if (lowerFw.includes('jest') || lowerFw.includes('vitest') || lowerFw.includes('pytest')) {
64
+ tags.add('testing');
65
+ }
66
+ }
67
+ // Language tags
68
+ for (const lang of params.facts.languages) {
69
+ const lowerLang = lang.language.toLowerCase();
70
+ if (lowerLang === 'typescript' || lowerLang === 'javascript') {
71
+ tags.add('javascript');
72
+ }
73
+ else if (lowerLang === 'python') {
74
+ tags.add('python');
75
+ }
76
+ else if (lowerLang === 'go') {
77
+ tags.add('golang');
78
+ }
79
+ else if (lowerLang === 'rust') {
80
+ tags.add('rust');
81
+ }
82
+ }
83
+ // Dependency-based detection
84
+ const allDeps = [
85
+ ...params.facts.dependencies.runtime.map((d) => d.name.toLowerCase()),
86
+ ...params.facts.dependencies.dev.map((d) => d.name.toLowerCase()),
87
+ ];
88
+ if (allDeps.some((d) => d.includes('puppeteer') || d.includes('playwright') || d.includes('selenium'))) {
89
+ tags.add('browser-automation');
90
+ tags.add('web-scraping');
91
+ }
92
+ if (allDeps.some((d) => d.includes('axios') || d.includes('fetch') || d.includes('request'))) {
93
+ tags.add('http-client');
94
+ }
95
+ if (allDeps.some((d) => d.includes('cheerio') || d.includes('beautifulsoup') || d.includes('jsdom'))) {
96
+ tags.add('html-parsing');
97
+ tags.add('web-scraping');
98
+ }
99
+ }
100
+ // 3. Detect by file patterns
101
+ const fileNames = params.files.map((f) => f.repoRelativePath.toLowerCase());
102
+ if (fileNames.some((f) => f.includes('dockerfile') || f.includes('docker-compose'))) {
103
+ tags.add('docker');
104
+ tags.add('devops');
105
+ }
106
+ if (fileNames.some((f) => f.includes('kubernetes') || f.includes('k8s'))) {
107
+ tags.add('kubernetes');
108
+ tags.add('devops');
109
+ }
110
+ if (fileNames.some((f) => f.includes('ci') || f.includes('github/workflows'))) {
111
+ tags.add('ci-cd');
112
+ tags.add('devops');
113
+ }
114
+ if (fileNames.some((f) => f.includes('readme') || f.includes('docs/'))) {
115
+ tags.add('documented');
116
+ }
117
+ return Array.from(tags).sort();
118
+ }
119
+ /**
120
+ * Generate a human-readable display name from repo IDs
121
+ */
122
+ export function generateDisplayName(repoIds) {
123
+ if (repoIds.length === 0)
124
+ return 'Empty Bundle';
125
+ if (repoIds.length === 1) {
126
+ // Single repo: use repo name
127
+ const parts = repoIds[0].split('/');
128
+ return parts[1] || parts[0] || 'Unknown';
129
+ }
130
+ // Multiple repos: combine smartly
131
+ const repoNames = repoIds.map((id) => {
132
+ const parts = id.split('/');
133
+ return parts[1] || parts[0] || 'unknown';
134
+ });
135
+ if (repoNames.length <= 3) {
136
+ return repoNames.join(' + ');
137
+ }
138
+ return `${repoNames[0]} + ${repoNames.length - 1} more`;
139
+ }
140
+ /**
141
+ * Generate a brief description from facts
142
+ */
143
+ export function generateDescription(params) {
144
+ if (!params.facts) {
145
+ return `Bundle containing ${params.repoIds.length} repository(ies)`;
146
+ }
147
+ const parts = [];
148
+ // Primary language
149
+ if (params.facts.languages.length > 0) {
150
+ const topLang = params.facts.languages[0];
151
+ if (topLang) {
152
+ parts.push(`${topLang.language} project`);
153
+ }
154
+ }
155
+ // Project type
156
+ if (params.facts.frameworks.length > 0) {
157
+ const frameworks = params.facts.frameworks.slice(0, 2).join(', ');
158
+ parts.push(`using ${frameworks}`);
159
+ }
160
+ // Special categories
161
+ if (params.tags.includes('mcp')) {
162
+ parts.push('(MCP Server)');
163
+ }
164
+ else if (params.tags.includes('agents')) {
165
+ parts.push('(AI Agent)');
166
+ }
167
+ else if (params.tags.includes('web-scraping')) {
168
+ parts.push('(Web Scraping)');
169
+ }
170
+ if (parts.length === 0) {
171
+ return `Bundle with ${params.facts.fileStructure.totalFiles} files`;
172
+ }
173
+ return parts.join(' ');
174
+ }
175
+ /**
176
+ * Get category for a bundle based on tags (for grouping)
177
+ */
178
+ export function getCategoryFromTags(tags) {
179
+ // Priority order for categorization
180
+ if (tags.includes('mcp'))
181
+ return 'mcp-servers';
182
+ if (tags.includes('agents'))
183
+ return 'ai-agents';
184
+ if (tags.includes('web-scraping'))
185
+ return 'web-scraping';
186
+ if (tags.includes('code-analysis'))
187
+ return 'code-analysis';
188
+ if (tags.includes('testing') || tags.includes('debugging'))
189
+ return 'testing-debugging';
190
+ if (tags.includes('web-framework'))
191
+ return 'web-frameworks';
192
+ if (tags.includes('dev-tools'))
193
+ return 'dev-tools';
194
+ if (tags.includes('devops'))
195
+ return 'devops';
196
+ // Language-based fallback
197
+ if (tags.includes('javascript'))
198
+ return 'javascript';
199
+ if (tags.includes('python'))
200
+ return 'python';
201
+ if (tags.includes('golang'))
202
+ return 'golang';
203
+ if (tags.includes('rust'))
204
+ return 'rust';
205
+ return 'uncategorized';
206
+ }
package/dist/config.js ADDED
@@ -0,0 +1,65 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ function envNumber(name, fallback) {
4
+ const raw = process.env[name];
5
+ if (!raw)
6
+ return fallback;
7
+ const n = Number(raw);
8
+ return Number.isFinite(n) && n > 0 ? n : fallback;
9
+ }
10
+ function parseAnalysisMode(raw) {
11
+ const v = (raw ?? '').trim().toLowerCase();
12
+ if (v === 'none')
13
+ return 'none';
14
+ if (v === 'quick')
15
+ return 'quick';
16
+ // Back-compat: deep used to exist; treat it as quick (but never run LLM).
17
+ if (v === 'deep')
18
+ return 'quick';
19
+ return 'quick';
20
+ }
21
+ /**
22
+ * Parse storage directories from environment.
23
+ * Supports:
24
+ * - PREFLIGHT_STORAGE_DIRS (semicolon-separated, e.g. "D:\path1;E:\path2")
25
+ * - PREFLIGHT_STORAGE_DIR (single path, for backward compatibility)
26
+ */
27
+ function parseStorageDirs() {
28
+ // Multi-path takes precedence
29
+ const multiPath = process.env.PREFLIGHT_STORAGE_DIRS;
30
+ if (multiPath) {
31
+ return multiPath.split(';').map((p) => p.trim()).filter((p) => p.length > 0);
32
+ }
33
+ // Fallback to single path
34
+ const singlePath = process.env.PREFLIGHT_STORAGE_DIR;
35
+ if (singlePath) {
36
+ return [singlePath];
37
+ }
38
+ // Default
39
+ return [path.join(os.homedir(), '.preflight-mcp', 'bundles')];
40
+ }
41
+ export function getConfig() {
42
+ const storageDirs = parseStorageDirs();
43
+ const storageDir = storageDirs[0]; // Primary for new bundles (always at least one from default)
44
+ const tmpDir = process.env.PREFLIGHT_TMP_DIR ?? path.join(os.tmpdir(), 'preflight-mcp');
45
+ const analysisMode = parseAnalysisMode(process.env.PREFLIGHT_ANALYSIS_MODE);
46
+ return {
47
+ storageDir,
48
+ storageDirs,
49
+ tmpDir,
50
+ githubToken: process.env.GITHUB_TOKEN,
51
+ context7ApiKey: process.env.CONTEXT7_API_KEY,
52
+ context7McpUrl: process.env.CONTEXT7_MCP_URL ?? 'https://mcp.context7.com/mcp',
53
+ maxFileBytes: envNumber('PREFLIGHT_MAX_FILE_BYTES', 512 * 1024),
54
+ maxTotalBytes: envNumber('PREFLIGHT_MAX_TOTAL_BYTES', 50 * 1024 * 1024),
55
+ analysisMode,
56
+ // Tuning parameters with defaults (can be overridden via env vars)
57
+ maxContext7Libraries: envNumber('PREFLIGHT_MAX_CONTEXT7_LIBRARIES', 20),
58
+ maxContext7Topics: envNumber('PREFLIGHT_MAX_CONTEXT7_TOPICS', 10),
59
+ maxFtsQueryTokens: envNumber('PREFLIGHT_MAX_FTS_QUERY_TOKENS', 12),
60
+ maxSkippedNotes: envNumber('PREFLIGHT_MAX_SKIPPED_NOTES', 50),
61
+ defaultMaxAgeHours: envNumber('PREFLIGHT_DEFAULT_MAX_AGE_HOURS', 24),
62
+ maxSearchLimit: envNumber('PREFLIGHT_MAX_SEARCH_LIMIT', 200),
63
+ defaultSearchLimit: envNumber('PREFLIGHT_DEFAULT_SEARCH_LIMIT', 30),
64
+ };
65
+ }
@@ -0,0 +1,30 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ export async function connectContext7(cfg) {
4
+ const url = new URL(cfg.context7McpUrl);
5
+ const headers = {};
6
+ // Context7 supports running without a key (rate-limited). If present, pass it.
7
+ if (cfg.context7ApiKey) {
8
+ headers['CONTEXT7_API_KEY'] = cfg.context7ApiKey;
9
+ }
10
+ const transport = new StreamableHTTPClientTransport(url, {
11
+ requestInit: {
12
+ headers,
13
+ },
14
+ reconnectionOptions: {
15
+ // Keep retries low to avoid hanging bundle generation.
16
+ initialReconnectionDelay: 500,
17
+ maxReconnectionDelay: 2000,
18
+ reconnectionDelayGrowFactor: 1.5,
19
+ maxRetries: 1,
20
+ },
21
+ });
22
+ const client = new Client({ name: 'preflight-context7', version: '0.1.0' });
23
+ await client.connect(transport);
24
+ return {
25
+ client,
26
+ close: async () => {
27
+ await client.close().catch(() => undefined);
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,58 @@
1
+ export function textFromToolResult(res) {
2
+ const content = 'content' in res ? res.content : undefined;
3
+ if (!Array.isArray(content))
4
+ return '';
5
+ const parts = [];
6
+ for (const c of content) {
7
+ if (c && c.type === 'text' && typeof c.text === 'string') {
8
+ parts.push(c.text);
9
+ }
10
+ }
11
+ return parts.join('\n');
12
+ }
13
+ function collectStrings(value, out) {
14
+ if (typeof value === 'string') {
15
+ out.push(value);
16
+ return;
17
+ }
18
+ if (!value || typeof value !== 'object')
19
+ return;
20
+ if (Array.isArray(value)) {
21
+ for (const v of value)
22
+ collectStrings(v, out);
23
+ return;
24
+ }
25
+ for (const v of Object.values(value)) {
26
+ collectStrings(v, out);
27
+ }
28
+ }
29
+ export function extractContext7IdsFromResult(res) {
30
+ const candidates = [];
31
+ if (res.structuredContent) {
32
+ collectStrings(res.structuredContent, candidates);
33
+ }
34
+ const text = textFromToolResult(res);
35
+ if (text) {
36
+ // Match strings like /owner/repo, /scope/pkg, etc.
37
+ const re = /\/[A-Za-z0-9@._-]+(?:\/[A-Za-z0-9@._-]+)+/g;
38
+ for (const m of text.matchAll(re)) {
39
+ if (m[0])
40
+ candidates.push(m[0]);
41
+ }
42
+ }
43
+ // Normalize + de-dupe, keep order.
44
+ const seen = new Set();
45
+ const ids = [];
46
+ for (const raw of candidates) {
47
+ const s = raw.trim();
48
+ if (!s.startsWith('/'))
49
+ continue;
50
+ if (!s.includes('/'))
51
+ continue;
52
+ if (seen.has(s))
53
+ continue;
54
+ seen.add(s);
55
+ ids.push(s);
56
+ }
57
+ return ids;
58
+ }
@@ -0,0 +1,166 @@
1
+ import cron from "node-cron";
2
+ import { logger } from '../logging/logger.js';
3
+ export class Job {
4
+ // 可选的失败重试配置
5
+ getMaxRetries() {
6
+ return 3;
7
+ }
8
+ getRetryDelay() {
9
+ return 1000; // 1秒
10
+ }
11
+ }
12
+ export class Scheduler {
13
+ tasks = new Map();
14
+ isRunning = false;
15
+ async start() {
16
+ if (this.isRunning) {
17
+ return;
18
+ }
19
+ this.isRunning = true;
20
+ // Start any tasks that were scheduled while the scheduler was stopped.
21
+ for (const [name, jobTask] of this.tasks) {
22
+ jobTask.task.start();
23
+ logger.debug(`Started job: ${name}`);
24
+ }
25
+ logger.info('Scheduler started');
26
+ }
27
+ build(JobClass) {
28
+ const job = new JobClass();
29
+ const jobName = job.getName();
30
+ return {
31
+ schedule: (cronExpression) => {
32
+ const task = cron.createTask(cronExpression, async () => {
33
+ await this.executeJob(jobName, job);
34
+ });
35
+ const jobTask = {
36
+ job,
37
+ task,
38
+ cronExpression,
39
+ retries: 0,
40
+ running: false
41
+ };
42
+ this.tasks.set(jobName, jobTask);
43
+ if (this.isRunning) {
44
+ task.start();
45
+ }
46
+ logger.info(`Scheduled job ${jobName}`, { cronExpression });
47
+ },
48
+ };
49
+ }
50
+ async executeJob(jobName, job, isRetry = false) {
51
+ const jobTask = this.tasks.get(jobName);
52
+ if (!jobTask) {
53
+ logger.error(`Job ${jobName} not found in task registry`);
54
+ return;
55
+ }
56
+ if (!this.isRunning) {
57
+ // Scheduler stopped: don't execute or schedule retries.
58
+ return;
59
+ }
60
+ // Avoid overlapping executions.
61
+ if (jobTask.running) {
62
+ return;
63
+ }
64
+ // If a retry is already scheduled, let it run instead of piling up executions from cron.
65
+ if (!isRetry && jobTask.retryTimeout) {
66
+ return;
67
+ }
68
+ jobTask.running = true;
69
+ try {
70
+ const startTime = Date.now();
71
+ logger.debug(`Executing job: ${jobName}`);
72
+ await job.run();
73
+ const duration = Date.now() - startTime;
74
+ jobTask.lastRun = new Date();
75
+ jobTask.retries = 0;
76
+ jobTask.lastError = undefined;
77
+ if (jobTask.retryTimeout) {
78
+ clearTimeout(jobTask.retryTimeout);
79
+ jobTask.retryTimeout = undefined;
80
+ }
81
+ logger.info(`Job ${jobName} completed`, { durationMs: duration });
82
+ }
83
+ catch (error) {
84
+ jobTask.lastError = error instanceof Error ? error : new Error(String(error));
85
+ logger.error(`Job ${jobName} failed`, error instanceof Error ? error : undefined);
86
+ if (!this.isRunning) {
87
+ return;
88
+ }
89
+ const maxRetries = job.getMaxRetries();
90
+ if (jobTask.retries < maxRetries) {
91
+ jobTask.retries++;
92
+ const delay = job.getRetryDelay() * Math.pow(2, jobTask.retries - 1);
93
+ logger.info(`Retrying job ${jobName}`, { attempt: jobTask.retries, maxRetries, delayMs: delay });
94
+ if (jobTask.retryTimeout) {
95
+ clearTimeout(jobTask.retryTimeout);
96
+ }
97
+ jobTask.retryTimeout = setTimeout(() => {
98
+ jobTask.retryTimeout = undefined;
99
+ void this.executeJob(jobName, job, true);
100
+ }, delay);
101
+ jobTask.retryTimeout.unref?.();
102
+ }
103
+ else {
104
+ logger.error(`Job ${jobName} failed after ${maxRetries} retries`);
105
+ }
106
+ }
107
+ finally {
108
+ jobTask.running = false;
109
+ }
110
+ }
111
+ async stop() {
112
+ if (!this.isRunning) {
113
+ return;
114
+ }
115
+ // Mark stopped first so in-flight jobs won't schedule retries.
116
+ this.isRunning = false;
117
+ for (const [name, jobTask] of this.tasks) {
118
+ jobTask.task.stop();
119
+ if (jobTask.retryTimeout) {
120
+ clearTimeout(jobTask.retryTimeout);
121
+ jobTask.retryTimeout = undefined;
122
+ }
123
+ jobTask.running = false;
124
+ logger.debug(`Stopped job: ${name}`);
125
+ }
126
+ logger.info('Scheduler stopped');
127
+ }
128
+ async clear() {
129
+ for (const [name, jobTask] of this.tasks) {
130
+ if (jobTask.retryTimeout) {
131
+ clearTimeout(jobTask.retryTimeout);
132
+ jobTask.retryTimeout = undefined;
133
+ }
134
+ jobTask.running = false;
135
+ jobTask.task.destroy();
136
+ logger.debug(`Destroyed job: ${name}`);
137
+ }
138
+ this.tasks.clear();
139
+ logger.info('Scheduler cleared all tasks');
140
+ }
141
+ // 获取任务状态
142
+ getJobStatus(name) {
143
+ const jobTask = this.tasks.get(name);
144
+ if (!jobTask) {
145
+ return null;
146
+ }
147
+ const status = jobTask.task.getStatus();
148
+ return {
149
+ scheduled: !['stopped', 'destroyed'].includes(status),
150
+ lastRun: jobTask.lastRun,
151
+ lastError: jobTask.lastError,
152
+ retries: jobTask.retries,
153
+ cronExpression: jobTask.cronExpression
154
+ };
155
+ }
156
+ // 获取所有任务状态
157
+ getAllJobsStatus() {
158
+ const status = {};
159
+ for (const [name] of this.tasks) {
160
+ status[name] = this.getJobStatus(name);
161
+ }
162
+ return status;
163
+ }
164
+ }
165
+ // 单例实例
166
+ export const PreflightScheduler = new Scheduler();
package/dist/errors.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Custom error types for preflight-mcp.
3
+ * Provides structured error handling with error codes and context.
4
+ */
5
+ /**
6
+ * Base error class for all preflight-mcp errors.
7
+ */
8
+ export class PreflightError extends Error {
9
+ code;
10
+ context;
11
+ cause;
12
+ constructor(message, code, options) {
13
+ super(message);
14
+ this.name = 'PreflightError';
15
+ this.code = code;
16
+ this.context = options?.context;
17
+ this.cause = options?.cause;
18
+ // Maintains proper stack trace for where our error was thrown (V8 only)
19
+ if (Error.captureStackTrace) {
20
+ Error.captureStackTrace(this, this.constructor);
21
+ }
22
+ }
23
+ toJSON() {
24
+ return {
25
+ name: this.name,
26
+ code: this.code,
27
+ message: this.message,
28
+ context: this.context,
29
+ cause: this.cause?.message,
30
+ stack: this.stack,
31
+ };
32
+ }
33
+ }
34
+ /**
35
+ * Error thrown when a bundle is not found.
36
+ */
37
+ export class BundleNotFoundError extends PreflightError {
38
+ constructor(bundleId) {
39
+ super(`Bundle not found: ${bundleId}`, 'BUNDLE_NOT_FOUND', {
40
+ context: { bundleId },
41
+ });
42
+ this.name = 'BundleNotFoundError';
43
+ }
44
+ }
45
+ /**
46
+ * Error thrown when storage operations fail.
47
+ */
48
+ export class StorageError extends PreflightError {
49
+ constructor(message, options) {
50
+ super(message, 'STORAGE_ERROR', options);
51
+ this.name = 'StorageError';
52
+ }
53
+ }
54
+ /**
55
+ * Error thrown when no storage directory is available.
56
+ */
57
+ export class StorageUnavailableError extends StorageError {
58
+ constructor(attemptedPaths) {
59
+ super('No storage directory available. All mount points are inaccessible.', {
60
+ context: { attemptedPaths },
61
+ });
62
+ this.name = 'StorageUnavailableError';
63
+ }
64
+ }
65
+ /**
66
+ * Error thrown when bundle creation fails.
67
+ */
68
+ export class BundleCreationError extends PreflightError {
69
+ constructor(message, bundleId, options) {
70
+ super(`Failed to create bundle: ${message}`, 'BUNDLE_CREATION_ERROR', {
71
+ ...options,
72
+ context: { ...options?.context, bundleId },
73
+ });
74
+ this.name = 'BundleCreationError';
75
+ }
76
+ }
77
+ /**
78
+ * Error thrown when bundle validation fails.
79
+ */
80
+ export class BundleValidationError extends PreflightError {
81
+ constructor(bundleId, missingComponents) {
82
+ super(`Bundle creation incomplete. Missing: ${missingComponents.join(', ')}`, 'BUNDLE_VALIDATION_ERROR', {
83
+ context: { bundleId, missingComponents },
84
+ });
85
+ this.name = 'BundleValidationError';
86
+ }
87
+ }
88
+ /**
89
+ * Error thrown when GitHub operations fail.
90
+ */
91
+ export class GitHubError extends PreflightError {
92
+ constructor(message, options) {
93
+ super(message, 'GITHUB_ERROR', options);
94
+ this.name = 'GitHubError';
95
+ }
96
+ }
97
+ /**
98
+ * Error thrown when Context7 operations fail.
99
+ */
100
+ export class Context7Error extends PreflightError {
101
+ constructor(message, options) {
102
+ super(message, 'CONTEXT7_ERROR', options);
103
+ this.name = 'Context7Error';
104
+ }
105
+ }
106
+ /**
107
+ * Error thrown when search operations fail.
108
+ */
109
+ export class SearchError extends PreflightError {
110
+ constructor(message, options) {
111
+ super(message, 'SEARCH_ERROR', options);
112
+ this.name = 'SearchError';
113
+ }
114
+ }
115
+ /**
116
+ * Error thrown when file ingestion fails.
117
+ */
118
+ export class IngestError extends PreflightError {
119
+ constructor(message, options) {
120
+ super(message, 'INGEST_ERROR', options);
121
+ this.name = 'IngestError';
122
+ }
123
+ }
124
+ /**
125
+ * Error thrown for configuration-related issues.
126
+ */
127
+ export class ConfigError extends PreflightError {
128
+ constructor(message, options) {
129
+ super(message, 'CONFIG_ERROR', options);
130
+ this.name = 'ConfigError';
131
+ }
132
+ }
133
+ /**
134
+ * Helper to wrap unknown errors as PreflightError.
135
+ */
136
+ export function wrapError(err, code = 'UNKNOWN_ERROR') {
137
+ if (err instanceof PreflightError) {
138
+ return err;
139
+ }
140
+ if (err instanceof Error) {
141
+ return new PreflightError(err.message, code, { cause: err });
142
+ }
143
+ return new PreflightError(String(err), code);
144
+ }
145
+ /**
146
+ * Type guard to check if an error is a PreflightError.
147
+ */
148
+ export function isPreflightError(err) {
149
+ return err instanceof PreflightError;
150
+ }
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from './server.js';
3
+ startServer().catch((err) => {
4
+ // Stdio MCP servers should write logs to stderr.
5
+ console.error('[preflight-mcp] fatal:', err);
6
+ process.exitCode = 1;
7
+ });