Skip to main content
Glama
index.ts•32.3 kB
#!/usr/bin/env node import 'dotenv/config'; import { Command } from 'commander'; import { EGWDatabase, EGWApiClientNew as EGWApiClient, ContentDownloader, createAuthManager } from '@surgbc/egw-writings-shared'; import { readdirSync, existsSync } from 'fs'; import { execSync } from 'child_process'; import path from 'path'; import * as readline from 'readline'; const program = new Command(); program .name('egw-downloader') .description('šŸ“š EGW Writings Downloader - Download and index Ellen G. White writings for educational and research use') .version('1.0.0') .addHelpText('afterAll', ` 🌟 Examples: egw-downloader quick-start --zip Quick setup with ZIP method egw-downloader download:zips --limit 10 Download 10 book ZIPs egw-downloader parse:zips Parse all downloaded ZIPs egw-downloader stats Show database statistics šŸ“– Documentation: https://github.com/gospelsounders/egw-writings-mcp#readme šŸ› Issues: https://github.com/gospelsounders/egw-writings-mcp/issues`); program .command('languages') .description('Download and index all available languages') .action(async () => { console.log('šŸ“š Downloading languages...'); const authManager = createAuthManager(); const apiClient = new EGWApiClient({ authManager }); const db = new EGWDatabase(); const downloader = new ContentDownloader(apiClient, db); try { await downloader.downloadLanguages((progress) => { console.log(`Progress: ${progress.completed}/${progress.total} - ${progress.currentItem}`); }); console.log('šŸŽ‰ Successfully downloaded and indexed all languages'); } catch (error) { console.error('āŒ Error downloading languages:', error); process.exit(1); } finally { db.close(); } }); program .command('books') .description('Download book metadata for a language') .option('-l, --lang <language>', 'Language code', 'en') .action(async (options) => { console.log(`šŸ“– Downloading books for language: ${options.lang}`); const authManager = createAuthManager(); const apiClient = new EGWApiClient({ authManager }); const db = new EGWDatabase(); const downloader = new ContentDownloader(apiClient, db); try { // Download folders first await downloader.downloadFolders(options.lang, (progress) => { console.log(`Folders: ${progress.completed}/${progress.total} - ${progress.currentItem}`); }); // Download books for the language await downloader.downloadBooks({ languageCode: options.lang, onProgress: (progress) => { console.log(`Books: ${progress.completed}/${progress.total} - ${progress.currentItem}`); } }); console.log('šŸŽ‰ Successfully downloaded books and folders'); } catch (error) { console.error('āŒ Error downloading books:', error); process.exit(1); } finally { db.close(); } }); program .command('content') .description('šŸ“„ Download full content for books (supports both API and ZIP methods)') .option('-l, --lang <language>', 'Language code', 'en') .option('-b, --book <bookId>', 'Specific book ID to download') .option('--limit <number>', 'Limit number of books', '10') .option('--zip', 'Use ZIP download method instead of API method') .action(async (options) => { console.log('šŸ“„ Downloading book content...'); const authManager = createAuthManager(); const apiClient = new EGWApiClient({ authManager }); const db = new EGWDatabase(); const downloader = new ContentDownloader(apiClient, db); try { if (options.book) { // Download specific book content const bookId = parseInt(options.book); console.log(`Downloading content for book ID: ${bookId} (${options.zip ? 'ZIP method' : 'API method'})`); await downloader.downloadBookContent(bookId, options.zip || false, (progress) => { console.log(`Content: ${progress.completed}/${progress.total} - ${progress.currentItem}`); }); } else { // Download content for sample books const books = await db.getBooks(options.lang); const limit = parseInt(options.limit); const sampleBooks = books.slice(0, Math.min(limit, books.length)); console.log(`Downloading content for ${sampleBooks.length} books...`); for (let i = 0; i < sampleBooks.length; i++) { const book = sampleBooks[i]; console.log(`\n[${i + 1}/${sampleBooks.length}] ${book.title}`); try { await downloader.downloadBookContent(book.book_id, options.zip || false, (progress) => { console.log(` Content: ${progress.completed}/${progress.total} - ${progress.currentItem}`); }); } catch (error) { console.error(` āŒ Failed to download content for ${book.title}:`, error); } } } console.log('šŸŽ‰ Content download complete!'); } catch (error) { console.error('āŒ Error downloading content:', error); process.exit(1); } finally { db.close(); } }); program .command('stats') .description('Show database statistics') .action(async () => { const db = new EGWDatabase(); try { const stats = await db.getStats(); console.log('šŸ“Š Database Statistics:'); console.log(` Languages: ${stats.languages}`); console.log(` Books: ${stats.books}`); console.log(` Downloaded Books: ${stats.downloadedBooks}`); console.log(` Paragraphs: ${stats.paragraphs}`); // Show category breakdown const categories = await db.getBooksByCategories(); if (categories.length > 0) { console.log('\nšŸ“š Books by Category:'); categories.forEach((cat: any) => { console.log(` ${cat.category}/${cat.subcategory}: ${cat.count} books`); }); } const progress = await db.getDownloadProgress(); if (progress.length > 0) { console.log('\nšŸ“ˆ Recent Download Progress:'); progress.slice(0, 5).forEach((p: any) => { console.log(` ${p.task_type}: ${p.completed_items}/${p.total_items || '?'} (${p.status})`); }); } } catch (error) { console.error('āŒ Error getting stats:', error); process.exit(1); } finally { db.close(); } }); program .command('categorize') .description('Update existing books with category information') .action(async () => { console.log('šŸ·ļø Updating book categories...'); const db = new EGWDatabase(); try { const updated = await db.updateBookCategories(); console.log(`āœ… Updated ${updated} books with category information`); // Show updated breakdown const categories = await db.getBooksByCategories(); console.log('\nšŸ“š Updated Books by Category:'); categories.forEach((cat: any) => { console.log(` ${cat.category}/${cat.subcategory}: ${cat.count} books`); }); } catch (error) { console.error('āŒ Error updating categories:', error); process.exit(1); } finally { db.close(); } }); program .command('quick-start') .description('šŸš€ Quick setup: download languages, sample books, and content - Perfect for getting started') .option('--zip', 'Use ZIP download method for content') .action(async (options) => { console.log('šŸš€ Quick Start: Setting up EGW Writings database...\n'); const authManager = createAuthManager(); const apiClient = new EGWApiClient({ authManager }); const db = new EGWDatabase(); const downloader = new ContentDownloader(apiClient, db); try { // Use the comprehensive download method with sample content await downloader.downloadAll('en', { includeContent: true, maxBooks: 5, useZip: options.zip || false, onProgress: (progress) => { console.log(`${progress.taskType}: ${progress.completed}/${progress.total} - ${progress.currentItem || ''}`); } }); console.log('\nšŸŽ‰ Quick start complete!'); const stats = await db.getStats(); console.log('\nšŸ“Š Final Statistics:'); console.log(` Languages: ${stats.languages}`); console.log(` Books: ${stats.books}`); console.log(` Downloaded Books: ${stats.downloadedBooks}`); console.log(` Paragraphs: ${stats.paragraphs}`); // Show category breakdown const categories = await db.getBooksByCategories(); if (categories.length > 0) { console.log('\nšŸ“š Books by Category:'); categories.forEach((cat: any) => { console.log(` ${cat.category}/${cat.subcategory}: ${cat.count} books`); }); } console.log('\nšŸ’” Try: pnpm --filter website dev'); } catch (error) { console.error('āŒ Quick start failed:', error); process.exit(1); } finally { db.close(); } }); program .command('download:zips') .description('šŸ“¦ Download ZIP files only (fast, no extraction/parsing) - Recommended for bulk downloads') .option('-l, --lang <language>', 'Language code', 'en') .option('-b, --book <bookId>', 'Specific book ID to download') .option('--limit <number>', 'Limit number of books (default: all books)') .option('--concurrency <number>', 'Number of parallel downloads', '5') .action(async (options) => { console.log('šŸ“¦ Downloading ZIP files only...'); const authManager = createAuthManager(); const apiClient = new EGWApiClient({ authManager }); const db = new EGWDatabase(); const downloader = new ContentDownloader(apiClient, db); const overallStartTime = Date.now(); try { if (options.book) { // Download specific book ZIP const bookId = parseInt(options.book); console.log(`Downloading ZIP for book ID: ${bookId}`); await downloader.downloadBookZipOnly(bookId, (progress) => { console.log(`ZIP: ${progress.completed}/${progress.total} - ${progress.currentItem}`); }); } else { // Download ZIPs for books - fetch directly from API try { // Get ALL books from API (handle pagination) console.log(`šŸ“š Fetching ALL books from API for language: ${options.lang}`); let allBooks: any[] = []; let currentPage = 1; let hasMore = true; // Track duplicates const bookIds = new Set<number>(); const duplicates: any[] = []; let apiReportedCount: number | null = null; while (hasMore) { console.log(` šŸ“„ Fetching page ${currentPage}...`); const booksResponse = await apiClient.getBooks({ lang: options.lang, limit: 100, // Get max per page page: currentPage }); // SHOW RAW API RESPONSE (first page only) if (currentPage === 1) { console.log('\n=== RAW API RESPONSE (FIRST PAGE) ==='); console.log(JSON.stringify(booksResponse, null, 2)); console.log('=== END RAW RESPONSE ===\n'); apiReportedCount = (booksResponse as any)?.count || null; } // Handle response structure - API returns paginated response with 'results' array const pageBooks = Array.isArray(booksResponse) ? booksResponse : (booksResponse as any)?.results || []; // Check for duplicates before adding to allBooks pageBooks.forEach((book: any) => { if (bookIds.has(book.book_id)) { duplicates.push({ page: currentPage, book_id: book.book_id, title: book.title }); } else { bookIds.add(book.book_id); allBooks.push(book); } }); // Check if there are more pages const hasNext = (booksResponse as any)?.next; const totalCount = (booksResponse as any)?.count; hasMore = hasNext && pageBooks.length > 0; currentPage++; // Show progress if we know total count if (totalCount) { console.log(` šŸ“Š Progress: ${allBooks.length}/${totalCount} books fetched (${duplicates.length} duplicates)`); } else { console.log(` šŸ“Š Progress: ${allBooks.length} unique books fetched (${duplicates.length} duplicates)`); } // Safety limit to prevent infinite loops (removed to find true total) if (currentPage > 100) { console.log(`āš ļø Reached page limit (100), stopping fetch for safety`); break; } } console.log(`\nšŸ“‹ PAGINATION SUMMARY:`); console.log(` API reported count: ${apiReportedCount || 'unknown'}`); console.log(` Pages fetched: ${currentPage - 1}`); console.log(` Total raw entries: ${allBooks.length + duplicates.length}`); console.log(` Unique books found: ${allBooks.length}`); console.log(` Duplicates detected: ${duplicates.length}`); if (duplicates.length > 0) { console.log(`\nšŸ” DUPLICATE ANALYSIS (first 10):`); duplicates.slice(0, 10).forEach((dup: any) => { console.log(` Page ${dup.page}: Book ${dup.book_id} - ${dup.title}`); }); } if (allBooks.length === 0) { console.log('āš ļø No books found. This might be an API issue or authentication problem.'); process.exit(1); } // If no limit specified, download all books const limit = options.limit ? parseInt(options.limit) : allBooks.length; const selectedBooks = allBooks.slice(0, Math.min(limit, allBooks.length)); if (!options.limit) { console.log(`šŸ“š No limit specified - downloading ALL ${selectedBooks.length} books!`); } console.log(`Downloading ZIPs for ${selectedBooks.length} books...`); // Parallel download with concurrency control const concurrency = parseInt(options.concurrency) || 5; // Download books simultaneously const downloadStartTime = Date.now(); let completedDownloads = 0; let failedDownloads = 0; console.log(`šŸš€ Starting parallel downloads (${concurrency} concurrent)...`); const downloadBook = async (book: any, index: number) => { try { console.log(`\n[${index + 1}/${selectedBooks.length}] Starting ${book.title}`); await downloader.downloadBookZipOnly(book.book_id, (progress) => { // Simplified progress for parallel downloads if (progress.completed === progress.total) { console.log(` āœ… [${index + 1}/${selectedBooks.length}] Completed ${book.title}`); } }); completedDownloads++; } catch (error) { console.error(` āŒ [${index + 1}/${selectedBooks.length}] Failed ${book.title}:`, error); failedDownloads++; } }; // Process books in batches of 'concurrency' size for (let i = 0; i < selectedBooks.length; i += concurrency) { const batch = selectedBooks.slice(i, i + concurrency); const promises = batch.map((book, batchIndex) => downloadBook(book, i + batchIndex) ); await Promise.all(promises); const elapsed = (Date.now() - downloadStartTime) / 1000; const completed = Math.min(i + concurrency, selectedBooks.length); const rate = completed / elapsed; const remaining = selectedBooks.length - completed; const eta = remaining > 0 ? remaining / rate : 0; console.log(`\nšŸ“Š Progress: ${completed}/${selectedBooks.length} completed in ${elapsed.toFixed(1)}s (${rate.toFixed(1)}/s, ETA: ${eta.toFixed(0)}s)`); } console.log(`\nšŸ“ˆ Download Summary: ${completedDownloads} successful, ${failedDownloads} failed`); } catch (error) { console.error('āŒ Failed to fetch books from API:', error); console.log('šŸ’” Try running "egw-books" command first to ensure authentication is working.'); process.exit(1); } } console.log('šŸŽ‰ ZIP download complete!'); // Report ZIP directory statistics with timing try { const endTime = Date.now(); const totalTime = (endTime - overallStartTime) / 1000; // Convert to seconds const zipCount = execSync('find data/zips -name "*.zip" | wc -l', { encoding: 'utf-8' }).trim(); const zipSize = execSync('du -sh data/zips', { encoding: 'utf-8' }).split('\t')[0]; console.log('\nšŸ“Š ZIP DIRECTORY STATS:'); console.log(` šŸ“ ZIP files: ${zipCount}`); console.log(` šŸ’¾ Total size: ${zipSize}`); console.log(` ā±ļø Download time: ${totalTime.toFixed(1)}s`); if (parseInt(zipCount) > 0) { console.log(` šŸ“ˆ Average: ${(totalTime / parseInt(zipCount)).toFixed(2)}s per ZIP`); } console.log(` šŸ“‚ Location: data/zips/`); } catch (error) { console.error('šŸ“Š ZIP stats error:', error); console.log('šŸ“Š ZIP stats unavailable'); } } catch (error) { console.error('āŒ Error downloading ZIPs:', error); process.exit(1); } finally { db.close(); } }); program .command('parse:zips') .description('šŸ” Parse downloaded ZIP files into database (can be done offline)') .option('--zip-dir <directory>', 'Directory containing ZIP files', './data/zips') .option('-b, --book <bookId>', 'Specific book ID to parse') .action(async (options) => { console.log('šŸ” Parsing existing ZIP files...'); const authManager = createAuthManager(); const apiClient = new EGWApiClient({ authManager }); const db = new EGWDatabase(); const downloader = new ContentDownloader(apiClient, db); try { const zipDir = path.resolve(options.zipDir); if (!existsSync(zipDir)) { console.error(`āŒ ZIP directory does not exist: ${zipDir}`); process.exit(1); } // Helper function to recursively find ZIP files const findZipFiles = (dir: string): string[] => { const files: string[] = []; const items = readdirSync(dir, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dir, item.name); if (item.isDirectory()) { files.push(...findZipFiles(fullPath)); } else if (item.isFile() && item.name.endsWith('.zip')) { files.push(fullPath); } } return files; }; if (options.book) { // Parse specific book ZIP - search recursively const bookId = parseInt(options.book); const allZipFiles = findZipFiles(zipDir); const zipFiles = allZipFiles.filter(file => file.includes(`_${bookId}.zip`) ); if (zipFiles.length === 0) { console.error(`āŒ No ZIP file found for book ID: ${bookId}`); console.log(`Searched in: ${zipDir}`); process.exit(1); } const zipPath = zipFiles[0]; console.log(`Parsing ZIP: ${zipPath}`); await downloader.parseExistingZip(zipPath, (progress) => { console.log(`Parse: ${progress.completed}/${progress.total} - ${progress.currentItem}`); }); } else { // Parse all ZIP files - search recursively in category folders const allZipFiles = findZipFiles(zipDir); if (allZipFiles.length === 0) { console.log('āš ļø No ZIP files found in directory:', zipDir); console.log('šŸ’” Tip: ZIPs are now organized in category/subcategory folders'); process.exit(1); } console.log(`Found ${allZipFiles.length} ZIP files to parse...`); for (let i = 0; i < allZipFiles.length; i++) { const zipPath = allZipFiles[i]; const zipFile = path.basename(zipPath); const relativePath = path.relative(zipDir, zipPath); console.log(`\n[${i + 1}/${allZipFiles.length}] Parsing ${relativePath}`); try { await downloader.parseExistingZip(zipPath, (progress) => { console.log(` Parse: ${progress.completed}/${progress.total} - ${progress.currentItem}`); }); } catch (error) { console.error(` āŒ Failed to parse ${zipFile}:`, error); } } } console.log('\nšŸŽ‰ ZIP parsing complete!'); const stats = await db.getStats(); console.log('\nšŸ“Š Final Statistics:'); console.log(` Languages: ${stats.languages}`); console.log(` Books: ${stats.books}`); console.log(` Downloaded Books: ${stats.downloadedBooks}`); console.log(` Paragraphs: ${stats.paragraphs}`); } catch (error) { console.error('āŒ Error parsing ZIPs:', error); process.exit(1); } finally { db.close(); } }); program .command('download:all') .description('Download everything: languages, books, and content') .option('-l, --lang <language>', 'Language code', 'en') .option('--content', 'Include full content download') .option('--limit <number>', 'Limit number of books for content', '20') .option('--zip', 'Use ZIP download method for content') .action(async (options) => { console.log('šŸš€ Starting comprehensive download...\n'); const authManager = createAuthManager(); const apiClient = new EGWApiClient({ authManager }); const db = new EGWDatabase(); const downloader = new ContentDownloader(apiClient, db); try { await downloader.downloadAll(options.lang, { includeContent: options.content, maxBooks: parseInt(options.limit), useZip: options.zip || false, onProgress: (progress) => { console.log(`${progress.taskType}: ${progress.completed}/${progress.total} - ${progress.currentItem || ''}`); } }); console.log('\nšŸŽ‰ Comprehensive download complete!'); const stats = await db.getStats(); console.log('\nšŸ“Š Final Statistics:'); console.log(` Languages: ${stats.languages}`); console.log(` Books: ${stats.books}`); console.log(` Downloaded Books: ${stats.downloadedBooks}`); console.log(` Paragraphs: ${stats.paragraphs}`); // Show category breakdown const categories = await db.getBooksByCategories(); if (categories.length > 0) { console.log('\nšŸ“š Books by Category:'); categories.forEach((cat: any) => { console.log(` ${cat.category}/${cat.subcategory}: ${cat.count} books`); }); } } catch (error) { console.error('āŒ Comprehensive download failed:', error); process.exit(1); } finally { db.close(); } }); program .command('download:resume') .description('šŸ”„ Resume interrupted download - downloads content for books that are missing paragraphs') .option('-l, --lang <language>', 'Language code', 'en') .option('--limit <number>', 'Limit number of books to process (default: all remaining)') .action(async (options) => { console.log('šŸ”„ Resuming interrupted download...\n'); const authManager = createAuthManager(); const apiClient = new EGWApiClient({ authManager }); const db = new EGWDatabase(); const downloader = new ContentDownloader(apiClient, db); try { // Get all books for the language const allBooks = await db.getBooks(options.lang); console.log(`šŸ“š Found ${allBooks.length} books in database for language: ${options.lang}`); // Find books that need content (no downloaded_at timestamp OR no paragraphs) const booksNeedingContent: any[] = []; for (const book of allBooks) { // Check if book has downloaded_at timestamp const hasDownloadedTimestamp = book.downloaded_at !== null; // Check if book has any paragraphs const paragraphs = await db.getParagraphs(book.book_id, 1); const hasParagraphs = paragraphs.length > 0; // Book needs content if either: // 1. No downloaded_at timestamp (never attempted download) // 2. Has downloaded_at but no paragraphs (download failed or incomplete) if (!hasDownloadedTimestamp || !hasParagraphs) { booksNeedingContent.push({ ...book, status: !hasDownloadedTimestamp ? 'never_downloaded' : 'incomplete_download' }); } } console.log(`šŸ“Š Analysis complete:`); console.log(` šŸ“– Total books: ${allBooks.length}`); console.log(` āœ… Books with complete content: ${allBooks.length - booksNeedingContent.length}`); console.log(` šŸ”„ Books needing content: ${booksNeedingContent.length}`); if (booksNeedingContent.length === 0) { console.log('šŸŽ‰ All books already have complete content!'); return; } // Apply limit if specified const limit = options.limit ? parseInt(options.limit) : booksNeedingContent.length; const booksToProcess = booksNeedingContent.slice(0, Math.min(limit, booksNeedingContent.length)); if (options.limit) { console.log(`šŸ“‹ Processing ${booksToProcess.length} books (limited to ${limit})`); } else { console.log(`šŸ“‹ Processing all ${booksToProcess.length} remaining books`); } console.log('\nšŸš€ Starting sequential download (API method)...'); console.log('āš ļø Will stop on first error to ensure no books are left behind\n'); // File to track skipped books const skippedBooksFile = path.join(process.cwd(), 'data', 'skipped-books.json'); let skippedBooks: Array<{id: number, title: string, author: string, error: string, timestamp: string}> = []; // Load existing skipped books if file exists try { if (existsSync(skippedBooksFile)) { const existingData = await import('fs/promises').then(fs => fs.readFile(skippedBooksFile, 'utf-8')); skippedBooks = JSON.parse(existingData); console.log(`šŸ“ Loaded ${skippedBooks.length} previously skipped books`); } } catch (error) { // File doesn't exist or is invalid, start fresh skippedBooks = []; } let skipAllErrors = false; // Download content sequentially using API method for (let i = 0; i < booksToProcess.length; i++) { const book = booksToProcess[i]; console.log(`\n[${i + 1}/${booksToProcess.length}] šŸ“– ${book.title}`); console.log(` Author: ${book.author}`); console.log(` Status: ${book.status === 'never_downloaded' ? 'Never downloaded' : 'Incomplete download'}`); try { // Use API method (false = don't use ZIP) await downloader.downloadBookContent(book.book_id, false, (progress) => { console.log(` šŸ“„ Progress: ${progress.completed}/${progress.total} - ${progress.currentItem || 'Processing chapters...'}`); }); console.log(` āœ… Successfully downloaded content for "${book.title}"`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`\nāŒ ERROR: Failed to download content for "${book.title}"`); console.error(` Book ID: ${book.book_id}`); console.error(` Error: ${errorMessage}`); // If skipAllErrors is enabled, automatically skip if (skipAllErrors) { console.log(` šŸ”„ Auto-skipping "${book.title}" (skip-all mode)`); // Add to skipped books list skippedBooks.push({ id: book.book_id, title: book.title, author: book.author || 'Unknown', error: errorMessage, timestamp: new Date().toISOString() }); // Save skipped books to file await import('fs/promises').then(fs => fs.writeFile(skippedBooksFile, JSON.stringify(skippedBooks, null, 2)) ); continue; } // Ask user whether to skip or stop console.log(`\nā“ What would you like to do?`); console.log(` [y] Skip this book and continue with the next one`); console.log(` [n] Stop the download and fix the issue`); console.log(` [s] Skip all remaining books with errors and continue`); // Create a simple prompt using readline const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); try { const answer = await new Promise<string>((resolve) => { rl.question(' Your choice (y/n/s): ', resolve); }); rl.close(); if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 's') { // Add to skipped books list skippedBooks.push({ id: book.book_id, title: book.title, author: book.author || 'Unknown', error: errorMessage, timestamp: new Date().toISOString() }); // Save skipped books to file await import('fs/promises').then(fs => fs.writeFile(skippedBooksFile, JSON.stringify(skippedBooks, null, 2)) ); if (answer.toLowerCase() === 'y') { console.log(` āœ… Skipping "${book.title}" and continuing...`); console.log(` šŸ“ Added to skipped books list (${skippedBooks.length} total)`); } else { console.log(` āœ… Skipping "${book.title}" and will skip all future errors`); console.log(` šŸ“ Added to skipped books list (${skippedBooks.length} total)`); skipAllErrors = true; } } else { console.log(`\nšŸ›‘ Download stopped by user choice.`); console.log(` Fix the issue and run 'download:resume' again to continue.`); console.log(` šŸ“ ${skippedBooks.length} books were skipped and saved to: ${skippedBooksFile}`); process.exit(1); } } catch (promptError) { console.error(`\nšŸ›‘ Error with user prompt: ${promptError}`); console.log(` Stopping download for safety.`); process.exit(1); } } } console.log('\nšŸŽ‰ Resume download completed successfully!'); // Show final statistics const stats = await db.getStats(); console.log('\nšŸ“Š Final Statistics:'); console.log(` Languages: ${stats.languages}`); console.log(` Books: ${stats.books}`); console.log(` Downloaded Books: ${stats.downloadedBooks}`); console.log(` Paragraphs: ${stats.paragraphs}`); } catch (error) { console.error('āŒ Resume download failed:', error); process.exit(1); } finally { db.close(); } }); program.parse();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pythondev-pro/egw_writings_mcp_server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server