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,249 @@
1
+ import zlib from 'node:zlib';
2
+ import { promisify } from 'node:util';
3
+ import { logger } from '../logging/logger.js';
4
+ const gzip = promisify(zlib.gzip);
5
+ const gunzip = promisify(zlib.gunzip);
6
+ const brotliCompress = promisify(zlib.brotliCompress);
7
+ const brotliDecompress = promisify(zlib.brotliDecompress);
8
+ const deflate = promisify(zlib.deflate);
9
+ const inflate = promisify(zlib.inflate);
10
+ export class CompressionManager {
11
+ defaultOptions = {
12
+ type: 'gzip',
13
+ level: 6,
14
+ threshold: 1024 // Only compress files larger than 1KB
15
+ };
16
+ constructor(options) {
17
+ if (options) {
18
+ this.defaultOptions = { ...this.defaultOptions, ...options };
19
+ }
20
+ }
21
+ /**
22
+ * 压缩数据
23
+ */
24
+ async compress(data, options) {
25
+ const opts = { ...this.defaultOptions, ...options };
26
+ const inputBuffer = typeof data === 'string' ? Buffer.from(data) : data;
27
+ const originalSize = inputBuffer.length;
28
+ // 如果数据太小,不压缩
29
+ if (originalSize < (opts.threshold || 0)) {
30
+ return {
31
+ compressed: false,
32
+ originalSize,
33
+ compressedSize: originalSize,
34
+ compressionRatio: 1,
35
+ type: 'none',
36
+ data: inputBuffer
37
+ };
38
+ }
39
+ // 如果不需要压缩
40
+ if (opts.type === 'none') {
41
+ return {
42
+ compressed: false,
43
+ originalSize,
44
+ compressedSize: originalSize,
45
+ compressionRatio: 1,
46
+ type: 'none',
47
+ data: inputBuffer
48
+ };
49
+ }
50
+ try {
51
+ let compressedData;
52
+ const startTime = Date.now();
53
+ switch (opts.type) {
54
+ case 'gzip':
55
+ compressedData = await gzip(inputBuffer, { level: opts.level });
56
+ break;
57
+ case 'br':
58
+ compressedData = await brotliCompress(inputBuffer, {
59
+ params: {
60
+ [zlib.constants.BROTLI_PARAM_QUALITY]: opts.level || 6
61
+ }
62
+ });
63
+ break;
64
+ case 'deflate':
65
+ compressedData = await deflate(inputBuffer, { level: opts.level });
66
+ break;
67
+ default:
68
+ throw new Error(`Unsupported compression type: ${opts.type}`);
69
+ }
70
+ const compressionTime = Date.now() - startTime;
71
+ const compressionRatio = compressedData.length / originalSize;
72
+ if (compressionRatio >= 0.95) {
73
+ logger.debug(`Compression ineffective`, { type: opts.type, ratio: compressionRatio.toFixed(3), timeMs: compressionTime });
74
+ return {
75
+ compressed: false,
76
+ originalSize,
77
+ compressedSize: originalSize,
78
+ compressionRatio: 1,
79
+ type: 'none',
80
+ data: inputBuffer
81
+ };
82
+ }
83
+ logger.debug(`Compressed data`, { type: opts.type, originalSize, compressedSize: compressedData.length, ratio: compressionRatio.toFixed(3), timeMs: compressionTime });
84
+ return {
85
+ compressed: true,
86
+ originalSize,
87
+ compressedSize: compressedData.length,
88
+ compressionRatio,
89
+ type: opts.type,
90
+ data: compressedData
91
+ };
92
+ }
93
+ catch (error) {
94
+ logger.debug(`Failed to compress data`, { error: error instanceof Error ? error.message : String(error) });
95
+ return {
96
+ compressed: false,
97
+ originalSize,
98
+ compressedSize: originalSize,
99
+ compressionRatio: 1,
100
+ type: 'none',
101
+ data: inputBuffer
102
+ };
103
+ }
104
+ }
105
+ /**
106
+ * 解压缩数据
107
+ */
108
+ async decompress(data, type) {
109
+ if (type === 'none') {
110
+ return data;
111
+ }
112
+ try {
113
+ const startTime = Date.now();
114
+ let decompressedData;
115
+ switch (type) {
116
+ case 'gzip':
117
+ decompressedData = await gunzip(data);
118
+ break;
119
+ case 'br':
120
+ decompressedData = await brotliDecompress(data);
121
+ break;
122
+ case 'deflate':
123
+ decompressedData = await inflate(data);
124
+ break;
125
+ default:
126
+ throw new Error(`Unsupported decompression type: ${type}`);
127
+ }
128
+ const decompressionTime = Date.now() - startTime;
129
+ logger.debug(`Decompressed data`, { type, originalSize: data.length, decompressedSize: decompressedData.length, timeMs: decompressionTime });
130
+ return decompressedData;
131
+ }
132
+ catch (error) {
133
+ logger.error(`Failed to decompress data`, error instanceof Error ? error : undefined);
134
+ throw error;
135
+ }
136
+ }
137
+ /**
138
+ * 检测数据的压缩类型
139
+ */
140
+ detectCompressionType(data) {
141
+ if (data.length < 2) {
142
+ return 'none';
143
+ }
144
+ // Gzip magic number: 0x1f 0x8b
145
+ if (data[0] === 0x1f && data[1] === 0x8b) {
146
+ return 'gzip';
147
+ }
148
+ // Brotli magic number varies, but common patterns
149
+ if (data.length >= 4 && this.isBrotli(data)) {
150
+ return 'br';
151
+ }
152
+ // Deflate (zlib) magic number: 0x78
153
+ if ((data[0] === 0x78 && (data[1] === 0x01 || data[1] === 0x9c || data[1] === 0xda))) {
154
+ return 'deflate';
155
+ }
156
+ return 'none';
157
+ }
158
+ isBrotli(data) {
159
+ // Brotli magic number detection
160
+ // This is a simplified check - real detection would be more complex
161
+ const header = data.readUInt32LE(0);
162
+ return (header & 0xFFFFFF) === 0x226849; // Common Brotli header
163
+ }
164
+ /**
165
+ * 估算压缩效果(不实际压缩)
166
+ */
167
+ estimateCompression(data) {
168
+ const inputBuffer = typeof data === 'string' ? Buffer.from(data) : data;
169
+ // 简单的启发式估算
170
+ const entropy = this.calculateEntropy(inputBuffer);
171
+ let estimatedRatio = 1;
172
+ let recommendedType = 'none';
173
+ if (inputBuffer.length < 1024) {
174
+ // 小文件不压缩
175
+ estimatedRatio = 1;
176
+ recommendedType = 'none';
177
+ }
178
+ else if (entropy < 3.0) {
179
+ // 低熵数据(如重复文本)压缩效果好
180
+ estimatedRatio = 0.3;
181
+ recommendedType = 'br'; // Brotli 通常对文本效果最好
182
+ }
183
+ else if (entropy < 6.0) {
184
+ // 中等熵数据
185
+ estimatedRatio = 0.6;
186
+ recommendedType = 'gzip'; // Gzip 是很好的通用选择
187
+ }
188
+ else {
189
+ // 高熵数据(如已压缩或随机数据)压缩效果差
190
+ estimatedRatio = 0.95;
191
+ recommendedType = 'none';
192
+ }
193
+ return Promise.resolve({ estimatedRatio, recommendedType });
194
+ }
195
+ calculateEntropy(data) {
196
+ const frequency = new Array(256).fill(0);
197
+ // 计算字节频率
198
+ for (const byte of data) {
199
+ frequency[byte]++;
200
+ }
201
+ // 计算熵
202
+ let entropy = 0;
203
+ const length = data.length;
204
+ for (const count of frequency) {
205
+ if (count > 0) {
206
+ const probability = count / length;
207
+ entropy -= probability * Math.log2(probability);
208
+ }
209
+ }
210
+ return entropy;
211
+ }
212
+ /**
213
+ * 获取压缩统计信息
214
+ */
215
+ getCompressionStats() {
216
+ return {
217
+ supportedTypes: ['none', 'gzip', 'br', 'deflate'],
218
+ defaultType: this.defaultOptions.type,
219
+ defaultLevel: this.defaultOptions.level || 6,
220
+ defaultThreshold: this.defaultOptions.threshold || 1024
221
+ };
222
+ }
223
+ }
224
+ // 默认压缩管理器实例
225
+ export const defaultCompressionManager = new CompressionManager();
226
+ /**
227
+ * 便捷函数:压缩数据
228
+ */
229
+ export async function compressData(data, options) {
230
+ return defaultCompressionManager.compress(data, options);
231
+ }
232
+ /**
233
+ * 便捷函数:解压缩数据
234
+ */
235
+ export async function decompressData(data, type) {
236
+ return defaultCompressionManager.decompress(data, type);
237
+ }
238
+ /**
239
+ * 便捷函数:检测压缩类型
240
+ */
241
+ export function detectCompressionType(data) {
242
+ return defaultCompressionManager.detectCompressionType(data);
243
+ }
244
+ /**
245
+ * 便捷函数:估算压缩效果
246
+ */
247
+ export async function estimateCompression(data) {
248
+ return defaultCompressionManager.estimateCompression(data);
249
+ }
@@ -0,0 +1,316 @@
1
+ import fs from 'node:fs/promises';
2
+ import nodePath from 'node:path';
3
+ export class LocalStorageAdapter {
4
+ name;
5
+ type = 'local';
6
+ basePath;
7
+ constructor(basePath) {
8
+ this.name = `LocalStorage(${basePath})`;
9
+ this.basePath = basePath;
10
+ }
11
+ getFullPath(filePath) {
12
+ // 确保路径是相对于基础路径的
13
+ if (filePath.startsWith(this.basePath)) {
14
+ return filePath;
15
+ }
16
+ return nodePath.join(this.basePath, filePath);
17
+ }
18
+ async exists(filePath) {
19
+ try {
20
+ await fs.access(this.getFullPath(filePath));
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ async readFile(filePath) {
28
+ return await fs.readFile(this.getFullPath(filePath));
29
+ }
30
+ async writeFile(filePath, data) {
31
+ const fullPath = this.getFullPath(filePath);
32
+ // 确保目录存在
33
+ const dir = nodePath.dirname(fullPath);
34
+ await this.createDirectory(dir);
35
+ await fs.writeFile(fullPath, data);
36
+ }
37
+ async deleteFile(filePath) {
38
+ await fs.unlink(this.getFullPath(filePath));
39
+ }
40
+ async createDirectory(dirPath) {
41
+ await fs.mkdir(this.getFullPath(dirPath), { recursive: true });
42
+ }
43
+ async deleteDirectory(dirPath, recursive = false) {
44
+ const fullPath = this.getFullPath(dirPath);
45
+ if (recursive) {
46
+ await fs.rm(fullPath, { recursive: true, force: true });
47
+ }
48
+ else {
49
+ await fs.rmdir(fullPath);
50
+ }
51
+ }
52
+ async listDirectory(dirPath) {
53
+ try {
54
+ const entries = await fs.readdir(this.getFullPath(dirPath), { withFileTypes: true });
55
+ return entries
56
+ .map((entry) => nodePath.join(dirPath, entry.name))
57
+ .filter((name) => !name.endsWith('/.'));
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }
63
+ async getStats(filePath) {
64
+ const checkPath = filePath ? this.getFullPath(filePath) : this.basePath;
65
+ try {
66
+ const stats = await fs.stat(checkPath);
67
+ return {
68
+ totalSize: 0, // 需要系统特定的实现
69
+ freeSpace: 0, // 需要系统特定的实现
70
+ usedSpace: stats.size,
71
+ accessible: true
72
+ };
73
+ }
74
+ catch {
75
+ return {
76
+ totalSize: 0,
77
+ freeSpace: 0,
78
+ usedSpace: 0,
79
+ accessible: false
80
+ };
81
+ }
82
+ }
83
+ async getFileSize(filePath) {
84
+ const stats = await fs.stat(this.getFullPath(filePath));
85
+ return stats.size;
86
+ }
87
+ async copyFile(source, destination) {
88
+ await fs.copyFile(this.getFullPath(source), this.getFullPath(destination));
89
+ }
90
+ async moveFile(source, destination) {
91
+ await fs.rename(this.getFullPath(source), this.getFullPath(destination));
92
+ }
93
+ async healthCheck() {
94
+ try {
95
+ await this.exists('');
96
+ return true;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ }
103
+ /**
104
+ * S3 Storage Adapter - NOT YET IMPLEMENTED
105
+ *
106
+ * TODO: This is a placeholder implementation. To use S3 storage:
107
+ * 1. Install AWS SDK: npm install @aws-sdk/client-s3
108
+ * 2. Implement the getS3Client method
109
+ * 3. Remove this warning comment
110
+ *
111
+ * @experimental This adapter is not functional. Use LocalStorageAdapter instead.
112
+ */
113
+ export class S3StorageAdapter {
114
+ name;
115
+ type = 's3';
116
+ bucket;
117
+ region;
118
+ accessKeyId;
119
+ secretAccessKey;
120
+ endpoint;
121
+ constructor(config) {
122
+ this.name = `S3Storage(${config.bucket})`;
123
+ this.bucket = config.bucket;
124
+ this.region = config.region;
125
+ this.accessKeyId = config.accessKeyId;
126
+ this.secretAccessKey = config.secretAccessKey;
127
+ this.endpoint = config.endpoint;
128
+ }
129
+ async getS3Client() {
130
+ // TODO: Import AWS SDK - requires @aws-sdk/client-s3
131
+ // This is a placeholder that throws to prevent accidental use.
132
+ throw new Error('S3StorageAdapter is not implemented. Install @aws-sdk/client-s3 and implement this method.');
133
+ }
134
+ async exists(path) {
135
+ try {
136
+ const s3 = await this.getS3Client();
137
+ await s3.headObject({ Bucket: this.bucket, Key: path });
138
+ return true;
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ }
144
+ async readFile(path) {
145
+ const s3 = await this.getS3Client();
146
+ const result = await s3.getObject({ Bucket: this.bucket, Key: path });
147
+ return result.Body;
148
+ }
149
+ async writeFile(path, data) {
150
+ const s3 = await this.getS3Client();
151
+ await s3.putObject({
152
+ Bucket: this.bucket,
153
+ Key: path,
154
+ Body: data
155
+ });
156
+ }
157
+ async deleteFile(path) {
158
+ const s3 = await this.getS3Client();
159
+ await s3.deleteObject({ Bucket: this.bucket, Key: path });
160
+ }
161
+ async createDirectory(path) {
162
+ // S3 doesn't require explicit directory creation, but we create a placeholder object
163
+ const dirPath = path.endsWith('/') ? path : `${path}/`;
164
+ await this.writeFile(dirPath, '');
165
+ }
166
+ async deleteDirectory(path, recursive = false) {
167
+ if (recursive) {
168
+ const s3 = await this.getS3Client();
169
+ const objects = await s3.listObjectsV2({
170
+ Bucket: this.bucket,
171
+ Prefix: path.endsWith('/') ? path : `${path}/`
172
+ });
173
+ if (objects.Contents && objects.Contents.length > 0) {
174
+ await s3.deleteObjects({
175
+ Bucket: this.bucket,
176
+ Delete: {
177
+ Objects: objects.Contents.map((obj) => ({ Key: obj.Key }))
178
+ }
179
+ });
180
+ }
181
+ }
182
+ else {
183
+ await this.deleteFile(path.endsWith('/') ? path : `${path}/`);
184
+ }
185
+ }
186
+ async listDirectory(path) {
187
+ const s3 = await this.getS3Client();
188
+ const prefix = path.endsWith('/') ? path : `${path}/`;
189
+ const result = await s3.listObjectsV2({
190
+ Bucket: this.bucket,
191
+ Prefix: prefix,
192
+ Delimiter: '/'
193
+ });
194
+ const files = [];
195
+ // 添加文件
196
+ if (result.Contents) {
197
+ files.push(...result.Contents.map((obj) => obj.Key).filter((key) => key !== prefix));
198
+ }
199
+ // 添加子目录
200
+ if (result.CommonPrefixes) {
201
+ files.push(...result.CommonPrefixes.map((p) => p.Prefix.slice(0, -1)));
202
+ }
203
+ return files;
204
+ }
205
+ async getStats(path) {
206
+ // S3 storage stats would need CloudWatch or similar service
207
+ return {
208
+ totalSize: 0,
209
+ freeSpace: 0,
210
+ usedSpace: 0,
211
+ accessible: await this.healthCheck()
212
+ };
213
+ }
214
+ async getFileSize(path) {
215
+ const s3 = await this.getS3Client();
216
+ const result = await s3.headObject({ Bucket: this.bucket, Key: path });
217
+ return result.ContentLength || 0;
218
+ }
219
+ async copyFile(source, destination) {
220
+ const s3 = await this.getS3Client();
221
+ await s3.copyObject({
222
+ Bucket: this.bucket,
223
+ Key: destination,
224
+ CopySource: `${this.bucket}/${source}`
225
+ });
226
+ }
227
+ async moveFile(source, destination) {
228
+ await this.copyFile(source, destination);
229
+ await this.deleteFile(source);
230
+ }
231
+ async healthCheck() {
232
+ try {
233
+ const s3 = await this.getS3Client();
234
+ await s3.headBucket({ Bucket: this.bucket });
235
+ return true;
236
+ }
237
+ catch {
238
+ return false;
239
+ }
240
+ }
241
+ }
242
+ export class StorageManager {
243
+ adapters = new Map();
244
+ primaryAdapter = 'default';
245
+ constructor(config) {
246
+ this.initializeAdapters(config);
247
+ }
248
+ initializeAdapters(config) {
249
+ // Initialize default local storage adapter
250
+ if (config.storageDirs && config.storageDirs.length > 0) {
251
+ const localAdapter = new LocalStorageAdapter(config.storageDirs[0]);
252
+ this.adapters.set('default', localAdapter);
253
+ this.primaryAdapter = 'default';
254
+ }
255
+ // TODO: Initialize other storage adapters here (S3, GCS, Azure Blob Storage, etc.)
256
+ // These are not yet implemented.
257
+ }
258
+ getAdapter(name = 'default') {
259
+ const adapter = this.adapters.get(name);
260
+ if (!adapter) {
261
+ throw new Error(`Storage adapter '${name}' not found`);
262
+ }
263
+ return adapter;
264
+ }
265
+ addAdapter(name, adapter) {
266
+ this.adapters.set(name, adapter);
267
+ }
268
+ removeAdapter(name) {
269
+ if (name === this.primaryAdapter) {
270
+ throw new Error('Cannot remove primary storage adapter');
271
+ }
272
+ return this.adapters.delete(name);
273
+ }
274
+ setPrimaryAdapter(name) {
275
+ if (!this.adapters.has(name)) {
276
+ throw new Error(`Storage adapter '${name}' not found`);
277
+ }
278
+ this.primaryAdapter = name;
279
+ }
280
+ getPrimaryAdapter() {
281
+ return this.getAdapter(this.primaryAdapter);
282
+ }
283
+ async getAdapterHealth() {
284
+ const health = {};
285
+ for (const [name, adapter] of this.adapters) {
286
+ try {
287
+ health[name] = await adapter.healthCheck();
288
+ }
289
+ catch {
290
+ health[name] = false;
291
+ }
292
+ }
293
+ return health;
294
+ }
295
+ listAdapters() {
296
+ return Array.from(this.adapters.entries()).map(([name, adapter]) => ({
297
+ name,
298
+ type: adapter.type,
299
+ isPrimary: name === this.primaryAdapter
300
+ }));
301
+ }
302
+ }
303
+ // 单例实例
304
+ let storageManager = null;
305
+ export function getStorageManager(config) {
306
+ if (!storageManager) {
307
+ if (!config) {
308
+ throw new Error('StorageManager requires config for initialization');
309
+ }
310
+ storageManager = new StorageManager(config);
311
+ }
312
+ return storageManager;
313
+ }
314
+ export function resetStorageManager() {
315
+ storageManager = null;
316
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Common utility functions used across the preflight-mcp codebase.
3
+ * Centralizes repeated helper functions to reduce code duplication.
4
+ */
5
+ import crypto from 'node:crypto';
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ /**
9
+ * Ensure a directory exists, creating it recursively if needed.
10
+ */
11
+ export async function ensureDir(p) {
12
+ await fs.mkdir(p, { recursive: true });
13
+ }
14
+ /**
15
+ * Remove a file or directory if it exists.
16
+ * Does nothing if the path doesn't exist.
17
+ */
18
+ export async function rmIfExists(p) {
19
+ await fs.rm(p, { recursive: true, force: true });
20
+ }
21
+ /**
22
+ * Get current timestamp in ISO format.
23
+ */
24
+ export function nowIso() {
25
+ return new Date().toISOString();
26
+ }
27
+ /**
28
+ * Convert a path to POSIX format (forward slashes).
29
+ */
30
+ export function toPosix(p) {
31
+ return p.replaceAll('\\', '/');
32
+ }
33
+ /**
34
+ * Calculate SHA256 hash of a buffer and return as hex string.
35
+ */
36
+ export function sha256Hex(buf) {
37
+ return crypto.createHash('sha256').update(buf).digest('hex');
38
+ }
39
+ /**
40
+ * Calculate SHA256 hash of a UTF-8 string and return as hex string.
41
+ */
42
+ export function sha256HexString(text) {
43
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
44
+ }
45
+ /**
46
+ * Check if a path is accessible (exists and can be read).
47
+ */
48
+ export async function isPathAvailable(p) {
49
+ try {
50
+ await fs.access(p);
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ /**
58
+ * Check if a path's parent directory is accessible.
59
+ */
60
+ export async function isParentAvailable(p) {
61
+ const parent = path.dirname(p);
62
+ return isPathAvailable(parent);
63
+ }
64
+ /**
65
+ * Copy directory recursively.
66
+ */
67
+ export async function copyDir(src, dest) {
68
+ await fs.cp(src, dest, { recursive: true, force: true });
69
+ }
70
+ /**
71
+ * Write JSON to a file with pretty formatting.
72
+ */
73
+ export async function writeJson(targetPath, obj) {
74
+ await ensureDir(path.dirname(targetPath));
75
+ await fs.writeFile(targetPath, JSON.stringify(obj, null, 2) + '\n', 'utf8');
76
+ }
77
+ /**
78
+ * Clip UTF-8 text to a maximum byte size.
79
+ * Returns the clipped text and a flag indicating if truncation occurred.
80
+ */
81
+ export function clipUtf8(text, maxBytes) {
82
+ const normalized = text.replace(/\r\n/g, '\n');
83
+ const buf = Buffer.from(normalized, 'utf8');
84
+ if (buf.length <= maxBytes)
85
+ return { text: normalized, truncated: false };
86
+ // Cutting at a byte boundary may split a multi-byte codepoint; Node will replace invalid sequences.
87
+ const clipped = buf.subarray(0, maxBytes).toString('utf8');
88
+ return { text: `${clipped}\n\n[TRUNCATED]\n`, truncated: true };
89
+ }
90
+ /**
91
+ * Create a URL-safe slug from a string.
92
+ */
93
+ export function slug(s) {
94
+ return s
95
+ .trim()
96
+ .toLowerCase()
97
+ .replace(/[^a-z0-9._-]+/g, '_')
98
+ .replace(/^_+|_+$/g, '')
99
+ .slice(0, 64);
100
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "preflight-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server that creates evidence-based preflight bundles for GitHub repositories and library docs.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "preflight-mcp": "dist/index.js"
9
+ },
10
+ "main": "dist/index.js",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "dev": "tsx src/index.ts",
19
+ "build": "tsc -p tsconfig.json",
20
+ "start": "node dist/index.js",
21
+ "typecheck": "tsc -p tsconfig.json --noEmit",
22
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
23
+ "smoke": "npm run build && node scripts/smoke.mjs",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.25.1",
28
+ "better-sqlite3": "^12.5.0",
29
+ "ignore": "^7.0.5",
30
+ "node-cron": "^4.2.1",
31
+ "zod": "^4.2.1"
32
+ },
33
+ "devDependencies": {
34
+ "@jest/globals": "^30.2.0",
35
+ "@types/better-sqlite3": "^7.6.13",
36
+ "@types/jest": "^30.0.0",
37
+ "@types/node": "^25.0.3",
38
+ "@types/node-cron": "^3.0.11",
39
+ "jest": "^30.2.0",
40
+ "ts-jest": "^29.4.6",
41
+ "tsx": "^4.21.0",
42
+ "typescript": "^5.9.3"
43
+ }
44
+ }