@every-env/sparkle-mcp-server
Version:
MCP server for secure Sparkle folder file access with Claude AI, including clipboard history support
824 lines (820 loc) • 34.4 kB
JavaScript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { SparkleFolder } from "./sparkle-folder.js";
import { FileSearchEngine } from "./search-engine.js";
import { PathValidator, RateLimiter } from "./security.js";
import { loadConfig } from "./config.js";
import { ClipboardHistoryManager } from "./clipboard-history.js";
import * as fs from "fs/promises";
import * as path from "path";
import * as os from "os";
// Tool schemas
const GetRelevantFilesSchema = z.object({
query: z.string().describe("Natural language query about files needed"),
maxFiles: z.number().optional().default(10).describe("Maximum files to return"),
});
const SearchFilesSchema = z.object({
path: z.string().describe("Directory path to search (relative to Sparkle folder)"),
pattern: z.string().describe("Search pattern to match against file/directory names"),
excludePatterns: z.array(z.string()).optional().describe("Glob patterns to exclude from search"),
});
const ReadFileSchema = z.object({
path: z.string().describe("Path to file to read (relative to Sparkle folder)"),
});
const WriteFileSchema = z.object({
path: z.string().describe("Path to file to write (relative to Sparkle folder)"),
content: z.string().describe("Content to write to the file"),
});
const ListDirectorySchema = z.object({
path: z.string().describe("Directory path to list (relative to Sparkle folder)"),
});
const CreateDirectorySchema = z.object({
path: z.string().describe("Directory path to create (relative to Sparkle folder)"),
});
const MoveFileSchema = z.object({
source: z.string().describe("Source path (relative to Sparkle folder)"),
destination: z.string().describe("Destination path (relative to Sparkle folder)"),
});
const GetFileInfoSchema = z.object({
path: z.string().describe("File path to get info for (relative to Sparkle folder)"),
});
const HealthCheckSchema = z.object({});
// Clipboard schemas
const SearchClipboardSchema = z.object({
query: z.string().optional().describe("Text to search for in clipboard history"),
startDate: z.string().optional().describe("Start date (YYYY-MM-DD) for search range"),
endDate: z.string().optional().describe("End date (YYYY-MM-DD) for search range"),
type: z.string().optional().describe("Type of clipboard entry (text, url, image, file-path)"),
limit: z.number().optional().default(50).describe("Maximum number of entries to return"),
});
const GetClipboardByDateSchema = z.object({
date: z.string().describe("Date (YYYY-MM-DD) to get clipboard entries for"),
});
const GetRecentClipboardSchema = z.object({
days: z.number().optional().default(7).describe("Number of days to look back"),
limit: z.number().optional().default(50).describe("Maximum number of entries to return"),
});
const ClipboardStatsSchema = z.object({});
// Main server class
class SparkleMCPServer {
server;
sparkleFolder;
searchEngine;
pathValidator;
rateLimiter;
clipboardHistory;
config = null;
startupTime;
constructor() {
this.startupTime = new Date();
this.server = new Server({
name: "sparkle-mcp",
version: "1.0.0",
}, {
capabilities: {
tools: {},
},
});
// Initialize components with Sparkle folder
const sparkleDir = "~/Sparkle";
this.sparkleFolder = new SparkleFolder(sparkleDir);
this.searchEngine = new FileSearchEngine();
this.clipboardHistory = new ClipboardHistoryManager(sparkleDir);
// IMPORTANT: Only allow access to Sparkle folder
// Use expanded path for PathValidator
this.pathValidator = new PathValidator({
allowedPaths: [this.expandPath(sparkleDir)], // ONLY Sparkle folder (expanded)
maxFileSize: 100 * 1024 * 1024, // 100MB max
allowSymlinks: false,
});
this.rateLimiter = new RateLimiter(100, 60000); // 100 requests per minute
this.setupHandlers();
// Ensure Sparkle folder exists on startup
this.ensureSparkleFolder();
// Load configuration
this.loadConfiguration();
}
setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_relevant_files",
description: "Automatically retrieves files relevant to the query from your Sparkle folder. " +
"This tool is called automatically when AI needs file context. " +
"Only searches within the ~/Sparkle folder for security.",
inputSchema: zodToJsonSchema(GetRelevantFilesSchema),
},
{
name: "search_files",
description: "Recursively search for files and directories in Sparkle folder matching a pattern. " +
"Case-insensitive partial name matching with optional exclude patterns.",
inputSchema: zodToJsonSchema(SearchFilesSchema),
},
{
name: "read_file",
description: "Read the complete contents of a file in the Sparkle folder.",
inputSchema: zodToJsonSchema(ReadFileSchema),
},
{
name: "write_file",
description: "Create or overwrite a file in the Sparkle folder with the provided content.",
inputSchema: zodToJsonSchema(WriteFileSchema),
},
{
name: "list_directory",
description: "List the contents of a directory in the Sparkle folder.",
inputSchema: zodToJsonSchema(ListDirectorySchema),
},
{
name: "create_directory",
description: "Create a new directory in the Sparkle folder.",
inputSchema: zodToJsonSchema(CreateDirectorySchema),
},
{
name: "move_file",
description: "Move or rename a file or directory within the Sparkle folder.",
inputSchema: zodToJsonSchema(MoveFileSchema),
},
{
name: "get_file_info",
description: "Get detailed information about a file or directory in the Sparkle folder.",
inputSchema: zodToJsonSchema(GetFileInfoSchema),
},
{
name: "health_check",
description: "Check the health status of the Sparkle MCP server.",
inputSchema: zodToJsonSchema(HealthCheckSchema),
},
{
name: "search_clipboard",
description: "Search clipboard history in the Pasteboard folder with filters for date, content, and type.",
inputSchema: zodToJsonSchema(SearchClipboardSchema),
},
{
name: "get_clipboard_by_date",
description: "Get all clipboard entries for a specific date from the Pasteboard folder.",
inputSchema: zodToJsonSchema(GetClipboardByDateSchema),
},
{
name: "get_recent_clipboard",
description: "Get recent clipboard entries from the last N days.",
inputSchema: zodToJsonSchema(GetRecentClipboardSchema),
},
{
name: "clipboard_stats",
description: "Get statistics about clipboard usage and history.",
inputSchema: zodToJsonSchema(ClipboardStatsSchema),
},
],
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
console.error(`call_tool: ${name}`);
switch (name) {
case "get_relevant_files":
return await this.handleGetRelevantFiles(args);
case "search_files":
return await this.handleSearchFiles(args);
case "read_file":
return await this.handleReadFile(args);
case "write_file":
return await this.handleWriteFile(args);
case "list_directory":
return await this.handleListDirectory(args);
case "create_directory":
return await this.handleCreateDirectory(args);
case "move_file":
return await this.handleMoveFile(args);
case "get_file_info":
return await this.handleGetFileInfo(args);
case "health_check":
return await this.handleHealthCheck(args);
case "search_clipboard":
return await this.handleSearchClipboard(args);
case "get_clipboard_by_date":
return await this.handleGetClipboardByDate(args);
case "get_recent_clipboard":
return await this.handleGetRecentClipboard(args);
case "clipboard_stats":
return await this.handleClipboardStats(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
}
async handleGetRelevantFiles(args) {
const { query, maxFiles } = GetRelevantFilesSchema.parse(args);
try {
// Rate limiting check
if (!this.rateLimiter.checkLimit("get_files")) {
throw new Error("Rate limit exceeded. Please try again later.");
}
// ONLY search in Sparkle folder
const sparkleFiles = await this.sparkleFolder.findRelevant(query, maxFiles);
// All files from SparkleFolder are already validated
const validatedFiles = sparkleFiles;
const finalResults = validatedFiles
.slice(0, maxFiles)
.sort((a, b) => b.relevance - a.relevance);
return {
content: [{
type: "text",
text: this.formatFileResults(finalResults, query),
}],
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error retrieving files: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
async handleSearchFiles(args) {
const { path: searchPath, pattern, excludePatterns = [] } = SearchFilesSchema.parse(args);
try {
console.error(`search_files called with path: "${searchPath}", pattern: "${pattern}"`);
// Rate limiting check
if (!this.rateLimiter.checkLimit("search")) {
throw new Error("Rate limit exceeded. Please try again later.");
}
// Build full path within Sparkle folder
const sparkleRoot = this.expandPath("~/Sparkle");
console.error(`Sparkle root: ${sparkleRoot}`);
// Handle both relative and absolute paths
let fullSearchPath;
if (searchPath && path.isAbsolute(searchPath)) {
// If absolute path is provided, use it directly but validate it's in Sparkle
fullSearchPath = searchPath;
}
else {
// Otherwise resolve relative to Sparkle root
// Empty string or "." should resolve to sparkle root
fullSearchPath = searchPath ? path.resolve(sparkleRoot, searchPath) : sparkleRoot;
}
console.error(`Full search path: ${fullSearchPath}`);
// Ensure the path doesn't escape the Sparkle folder
if (!fullSearchPath.startsWith(sparkleRoot)) {
throw new Error(`Access denied: Path is outside Sparkle folder - ${fullSearchPath} does not start with ${sparkleRoot}`);
}
// Check if the path exists
try {
await fs.stat(fullSearchPath);
}
catch (error) {
throw new Error(`Path does not exist: ${fullSearchPath}`);
}
const results = await this.recursiveSearch(fullSearchPath, pattern, excludePatterns);
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2),
}],
};
}
catch (error) {
console.error("Search error:", error);
return {
content: [{
type: "text",
text: `Search error: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
async recursiveSearch(searchPath, pattern, excludePatterns) {
const results = [];
try {
const entries = await fs.readdir(searchPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(searchPath, entry.name);
const relativePath = path.relative(this.expandPath("~/Sparkle"), fullPath);
// Check if excluded
const isExcluded = excludePatterns.some(excludePattern => {
return entry.name.includes(excludePattern) || relativePath.includes(excludePattern);
});
if (isExcluded)
continue;
// Check if matches pattern
let matches = false;
if (pattern === '*') {
// Match all files
matches = true;
}
else if (pattern.startsWith('*.')) {
// Extension matching (e.g., *.txt)
const ext = pattern.slice(1); // Remove the *
matches = entry.name.endsWith(ext);
}
else if (pattern.includes('*')) {
// Simple glob pattern
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i');
matches = regex.test(entry.name);
}
else {
// Partial name matching (case-insensitive)
matches = entry.name.toLowerCase().includes(pattern.toLowerCase());
}
if (matches) {
results.push(relativePath);
}
// Recurse into directories
if (entry.isDirectory() && !entry.name.startsWith('.')) {
const subResults = await this.recursiveSearch(fullPath, pattern, excludePatterns);
results.push(...subResults);
}
}
}
catch (error) {
console.error(`Error searching ${searchPath}:`, error);
}
return results;
}
async handleReadFile(args) {
const { path: filePath } = ReadFileSchema.parse(args);
try {
const sparkleRoot = this.expandPath("~/Sparkle");
const fullPath = path.resolve(sparkleRoot, filePath);
// Ensure path is within Sparkle folder
if (!fullPath.startsWith(sparkleRoot)) {
throw new Error("Access denied: Path is outside Sparkle folder");
}
// Check file extension to determine if it's binary
const ext = path.extname(fullPath).toLowerCase();
const binaryExtensions = ['.pdf', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.tiff', '.webp', '.svg', '.mp3', '.mp4', '.wav', '.mov', '.avi', '.zip', '.tar', '.gz', '.7z', '.rar'];
const isBinary = binaryExtensions.includes(ext);
if (isBinary) {
// Read as binary and return base64
const buffer = await fs.readFile(fullPath);
const base64 = buffer.toString('base64');
return {
content: [{
type: "text",
text: `[Binary file: ${ext}]\nSize: ${buffer.length} bytes\nBase64 encoding:\n${base64}`,
}],
};
}
else {
// Read as text
const content = await fs.readFile(fullPath, 'utf-8');
return {
content: [{
type: "text",
text: content,
}],
};
}
}
catch (error) {
return {
content: [{
type: "text",
text: `Error reading file: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
async handleWriteFile(args) {
const { path: filePath, content } = WriteFileSchema.parse(args);
try {
const sparkleRoot = this.expandPath("~/Sparkle");
const fullPath = path.resolve(sparkleRoot, filePath);
// Ensure directory exists
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, content, 'utf-8');
return {
content: [{
type: "text",
text: `Successfully wrote to ${filePath}`,
}],
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error writing file: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
async handleListDirectory(args) {
const { path: dirPath } = ListDirectorySchema.parse(args);
try {
console.error(`list_directory called with path: "${dirPath}"`);
const sparkleRoot = this.expandPath("~/Sparkle");
console.error(`Sparkle root: ${sparkleRoot}`);
// Handle empty path or "." as sparkle root
const fullPath = dirPath ? path.resolve(sparkleRoot, dirPath) : sparkleRoot;
console.error(`Full path: ${fullPath}`);
// Ensure the path doesn't escape the Sparkle folder
if (!fullPath.startsWith(sparkleRoot)) {
throw new Error(`Access denied: Path is outside Sparkle folder - ${fullPath} does not start with ${sparkleRoot}`);
}
// Check if the path exists and is a directory
const stats = await fs.stat(fullPath);
if (!stats.isDirectory()) {
throw new Error(`Not a directory: ${fullPath}`);
}
const entries = await fs.readdir(fullPath, { withFileTypes: true });
const formatted = entries.map(entry => {
const prefix = entry.isDirectory() ? "[DIR]" : "[FILE]";
return `${prefix} ${entry.name}`;
});
console.error(`Found ${entries.length} entries in ${fullPath}`);
return {
content: [{
type: "text",
text: formatted.length > 0 ? formatted.join('\n') : "Empty directory",
}],
};
}
catch (error) {
console.error("List directory error:", error);
return {
content: [{
type: "text",
text: `Error listing directory: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
async handleCreateDirectory(args) {
const { path: dirPath } = CreateDirectorySchema.parse(args);
try {
const sparkleRoot = this.expandPath("~/Sparkle");
const fullPath = path.resolve(sparkleRoot, dirPath);
await fs.mkdir(fullPath, { recursive: true });
return {
content: [{
type: "text",
text: `Successfully created directory ${dirPath}`,
}],
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error creating directory: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
async handleMoveFile(args) {
const { source, destination } = MoveFileSchema.parse(args);
try {
const sparkleRoot = this.expandPath("~/Sparkle");
const sourcePath = path.resolve(sparkleRoot, source);
const destPath = path.resolve(sparkleRoot, destination);
// Ensure source path is within Sparkle folder
if (!sourcePath.startsWith(sparkleRoot) || !destPath.startsWith(sparkleRoot)) {
throw new Error("Access denied: Path is outside Sparkle folder");
}
// Ensure destination directory exists
await fs.mkdir(path.dirname(destPath), { recursive: true });
await fs.rename(sourcePath, destPath);
return {
content: [{
type: "text",
text: `Successfully moved ${source} to ${destination}`,
}],
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error moving file: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
async handleGetFileInfo(args) {
const { path: filePath } = GetFileInfoSchema.parse(args);
try {
const sparkleRoot = this.expandPath("~/Sparkle");
const fullPath = path.resolve(sparkleRoot, filePath);
// Ensure path is within Sparkle folder
if (!fullPath.startsWith(sparkleRoot)) {
throw new Error("Access denied: Path is outside Sparkle folder");
}
const stats = await fs.stat(fullPath);
const info = {
path: filePath,
size: stats.size,
type: stats.isDirectory() ? 'directory' : 'file',
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
permissions: '0' + (stats.mode & parseInt('777', 8)).toString(8),
};
return {
content: [{
type: "text",
text: JSON.stringify(info, null, 2),
}],
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error getting file info: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
hasGoodResults(files, needed) {
// Check if we have enough high-relevance results
const highRelevance = files.filter(f => f.relevance > 0.7);
return highRelevance.length >= Math.min(needed / 2, 3);
}
formatFileResults(files, query) {
if (files.length === 0) {
return `No files found matching "${query}"`;
}
let result = `Found ${files.length} relevant files for "${query}":\\n\\n`;
files.forEach((file, index) => {
result += `${index + 1}. ${file.path}\\n`;
result += ` Relevance: ${(file.relevance * 100).toFixed(0)}%\\n`;
if (file.summary) {
result += ` Summary: ${file.summary}\\n`;
}
result += `\\n`;
});
return result;
}
formatSearchResults(results, query) {
if (results.length === 0) {
return `No files found matching "${query}"`;
}
let output = `Search results for "${query}":\\n\\n`;
// Group by file type
const grouped = results.reduce((acc, file) => {
const ext = file.path.split('.').pop() || 'other';
if (!acc[ext])
acc[ext] = [];
acc[ext].push(file);
return acc;
}, {});
Object.entries(grouped).forEach(([type, files]) => {
output += `\\n${type.toUpperCase()} files:\\n`;
files.forEach((file) => {
output += ` - ${file.path}\\n`;
if (file.matchedContent) {
output += ` Match: "${file.matchedContent}"\\n`;
}
});
});
return output;
}
async ensureSparkleFolder() {
try {
const folderName = "~/Sparkle";
const sparkleDir = this.expandPath(folderName);
await fs.mkdir(sparkleDir, { recursive: true });
// Create a welcome file if folder is new
const welcomePath = path.join(sparkleDir, "README.txt");
try {
await fs.access(welcomePath);
}
catch {
// File doesn't exist, create it
const welcomeContent = `Welcome to your Sparkle folder! 🌟
This is your special folder for AI-accessible files.
How to use:
1. Drop any files here that you want Claude to access
2. Ask Claude about them naturally:
- "What files are in my Sparkle folder?"
- "Find my tax documents"
- "Show me the PDF I just added"
Important:
- Only files in THIS folder are accessible to Claude
- Files are indexed automatically when added
- You can organize with subfolders
Happy organizing!
`;
await fs.writeFile(welcomePath, welcomeContent);
console.error("Created Sparkle folder at:", sparkleDir);
}
}
catch (error) {
console.error("Error creating Sparkle folder:", error);
}
}
expandPath(folderPath) {
if (folderPath.startsWith("~/")) {
return path.join(os.homedir(), folderPath.slice(2));
}
return folderPath;
}
async loadConfiguration() {
try {
this.config = await loadConfig();
console.error("Configuration loaded:", this.config);
}
catch (error) {
console.error("Failed to load configuration, using defaults");
}
}
async handleHealthCheck(args) {
try {
console.error("health_check: start");
const sparkleDir = this.expandPath("~/Sparkle");
const stats = await fs.stat(sparkleDir);
const health = {
status: "healthy",
version: "1.0.0",
uptime: Math.floor((Date.now() - this.startupTime.getTime()) / 1000),
sparkleFolder: {
path: sparkleDir,
exists: stats.isDirectory(),
writable: true,
},
indexedFiles: this.sparkleFolder.getFileCount(),
configuration: this.config || "default",
rateLimiter: {
remaining: this.rateLimiter.getRemainingRequests('health_check')
},
timestamp: new Date().toISOString(),
};
const response = {
content: [{
type: "text",
text: JSON.stringify(health, null, 2),
}],
};
console.error("health_check: success");
return response;
}
catch (error) {
console.error("health_check: error", error);
return {
content: [{
type: "text",
text: JSON.stringify({
status: "unhealthy",
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
}, null, 2),
}],
isError: true,
};
}
}
async handleSearchClipboard(args) {
const { query, startDate, endDate, type, limit } = SearchClipboardSchema.parse(args);
try {
console.error("search_clipboard: start");
const searchOptions = { limit };
if (query)
searchOptions.query = query;
if (startDate)
searchOptions.startDate = new Date(startDate);
if (endDate)
searchOptions.endDate = new Date(endDate);
if (type)
searchOptions.type = type;
const results = await this.clipboardHistory.searchClipboardHistory(searchOptions);
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2),
}],
};
}
catch (error) {
console.error("search_clipboard: error", error);
return {
content: [{
type: "text",
text: `Error searching clipboard: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
async handleGetClipboardByDate(args) {
const { date } = GetClipboardByDateSchema.parse(args);
try {
console.error(`get_clipboard_by_date: ${date}`);
const result = await this.clipboardHistory.getClipboardByDate(new Date(date));
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2),
}],
};
}
catch (error) {
console.error("get_clipboard_by_date: error", error);
return {
content: [{
type: "text",
text: `Error getting clipboard for date: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
async handleGetRecentClipboard(args) {
const { days, limit } = GetRecentClipboardSchema.parse(args);
try {
console.error(`get_recent_clipboard: ${days} days`);
const results = await this.clipboardHistory.getRecentClipboard(days, limit);
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2),
}],
};
}
catch (error) {
console.error("get_recent_clipboard: error", error);
return {
content: [{
type: "text",
text: `Error getting recent clipboard: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
async handleClipboardStats(args) {
try {
console.error("clipboard_stats: start");
const stats = await this.clipboardHistory.getClipboardStats();
return {
content: [{
type: "text",
text: JSON.stringify(stats, null, 2),
}],
};
}
catch (error) {
console.error("clipboard_stats: error", error);
return {
content: [{
type: "text",
text: `Error getting clipboard stats: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Sparkle MCP Server running...");
console.error("Sparkle folder:", this.expandPath("~/Sparkle"));
}
getSparkleFolder() {
return this.sparkleFolder;
}
}
// Start the server
const server = new SparkleMCPServer();
// Handle graceful shutdown
process.on('SIGTERM', async () => {
console.error('Received SIGTERM, shutting down gracefully...');
await server.getSparkleFolder()?.cleanup();
process.exit(0);
});
process.on('SIGINT', async () => {
console.error('Received SIGINT, shutting down gracefully...');
await server.getSparkleFolder()?.cleanup();
process.exit(0);
});
// Handle uncaught errors
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
// Attempt to continue running
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
// Attempt to continue running
});
server.run().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
//# sourceMappingURL=index.js.map