UNPKG

@vibeship/devtools

Version:

Comprehensive markdown-based project management system with AI capabilities for Next.js applications

1 lines 142 kB
{"version":3,"sources":["../src/server/security/path-validator.ts","../src/server/file-scanner.ts","../src/server/task-extractor.ts","../src/server/markdown-parser.ts","../src/server/security/rate-limiter.ts","../src/server/security/index.ts","../src/server/cache-manager.ts","../src/server/logger.ts","../src/server/error-handler.ts","../src/server/types.ts","../src/server/index.ts","../src/api/config-loader.ts","../src/templates/utils/fallback-handlers.ts","../src/api/routes/tasks.ts","../src/api/routes/files.ts","../src/api/routes/health.ts","../src/api/responses.ts","../src/api/routes/stream.ts","../src/api/router.ts","../src/api/middleware.ts","../src/api/handler.ts","../src/api/routes.ts"],"sourcesContent":["import * as path from 'path';\nimport * as fs from 'fs';\n\nexport interface PathValidatorOptions {\n allowSymlinks?: boolean;\n maxDepth?: number;\n blockPatterns?: RegExp[];\n}\n\nexport class PathValidator {\n private normalizedAllowedPaths: string[];\n private options: PathValidatorOptions;\n\n constructor(\n private allowedPaths: string[],\n options: PathValidatorOptions = {}\n ) {\n this.options = {\n allowSymlinks: false,\n maxDepth: 20,\n blockPatterns: [\n /\\.git\\//,\n /node_modules\\//,\n /\\.env/,\n /\\.ssh\\//,\n /\\.aws\\//,\n ],\n ...options,\n };\n\n // Pre-normalize allowed paths for performance\n this.normalizedAllowedPaths = allowedPaths.map(p => \n path.normalize(path.resolve(p))\n );\n }\n\n /**\n * Validate if a path is safe and allowed\n */\n isValid(filePath: string): boolean {\n if (!filePath || typeof filePath !== 'string') {\n return false;\n }\n\n try {\n // Check for path traversal attempts\n if (this.hasPathTraversal(filePath)) {\n return false;\n }\n\n // Normalize and resolve the path\n const normalizedPath = path.normalize(path.resolve(filePath));\n\n // Check against blocked patterns\n if (this.isBlockedPattern(normalizedPath)) {\n return false;\n }\n\n // Check if path is within allowed directories\n const isWithinAllowed = this.normalizedAllowedPaths.some(allowedPath => \n normalizedPath.startsWith(allowedPath)\n );\n\n if (!isWithinAllowed) {\n return false;\n }\n\n // Check symlinks if not allowed\n if (!this.options.allowSymlinks && fs.existsSync(normalizedPath)) {\n try {\n const stats = fs.lstatSync(normalizedPath);\n if (stats.isSymbolicLink()) {\n return false;\n }\n } catch {\n return false;\n }\n }\n\n // Check depth limit\n if (this.options.maxDepth) {\n const depth = normalizedPath.split(path.sep).length;\n const maxAllowedDepth = Math.max(\n ...this.normalizedAllowedPaths.map(p => p.split(path.sep).length)\n ) + this.options.maxDepth;\n \n if (depth > maxAllowedDepth) {\n return false;\n }\n }\n\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Check for path traversal attempts\n */\n private hasPathTraversal(filePath: string): boolean {\n const dangerous = [\n '../',\n '..\\\\',\n '%2e%2e%2f',\n '%2e%2e/',\n '..%2f',\n '%2e%2e\\\\',\n '..%5c',\n '%2e%2e%5c',\n '..\\\\..\\\\',\n '../..',\n ];\n\n const lowerPath = filePath.toLowerCase();\n return dangerous.some(pattern => lowerPath.includes(pattern));\n }\n\n /**\n * Check if path matches blocked patterns\n */\n private isBlockedPattern(filePath: string): boolean {\n return this.options.blockPatterns?.some(pattern => \n pattern.test(filePath)\n ) || false;\n }\n\n /**\n * Sanitize a file path for safe usage\n */\n sanitize(filePath: string): string {\n if (!filePath || typeof filePath !== 'string') {\n return '';\n }\n\n // Remove null bytes and control characters\n let sanitized = filePath.replace(/\\0/g, '').replace(/[\\x00-\\x1F\\x7F]/g, '');\n \n // Remove redundant slashes\n sanitized = sanitized.replace(/\\/+/g, '/').replace(/\\\\+/g, '\\\\');\n \n // Remove trailing dots and spaces (Windows)\n sanitized = sanitized.replace(/[\\s.]+$/g, '');\n \n // Normalize the path\n return path.normalize(sanitized);\n }\n\n /**\n * Get the relative path from allowed base\n */\n getRelativePath(filePath: string): string | null {\n const normalizedPath = path.normalize(path.resolve(filePath));\n \n for (const allowedPath of this.normalizedAllowedPaths) {\n if (normalizedPath.startsWith(allowedPath)) {\n return path.relative(allowedPath, normalizedPath);\n }\n }\n \n return null;\n }\n\n /**\n * Add a new allowed path\n */\n addAllowedPath(newPath: string): void {\n const normalized = path.normalize(path.resolve(newPath));\n if (!this.normalizedAllowedPaths.includes(normalized)) {\n this.allowedPaths.push(newPath);\n this.normalizedAllowedPaths.push(normalized);\n }\n }\n\n /**\n * Remove an allowed path\n */\n removeAllowedPath(removePath: string): void {\n const normalized = path.normalize(path.resolve(removePath));\n const index = this.normalizedAllowedPaths.indexOf(normalized);\n \n if (index !== -1) {\n this.allowedPaths.splice(index, 1);\n this.normalizedAllowedPaths.splice(index, 1);\n }\n }\n}","import * as fs from 'fs/promises';\nimport * as path from 'path';\nimport { glob } from 'glob';\nimport { VibeshipConfig } from '../config/types';\nimport { PathValidator } from './security/path-validator';\nimport { FileInfo, ScanOptions, ScanProgress } from './types';\n\nexport class FileScanner {\n private pathValidator: PathValidator;\n private progressCallback?: (progress: ScanProgress) => void;\n\n constructor(private config: VibeshipConfig) {\n this.pathValidator = new PathValidator(config.scanPaths);\n }\n\n /**\n * Scan for files matching the configured patterns\n */\n async scan(options?: ScanOptions): Promise<string[]> {\n const files: string[] = [];\n const scanPaths = options?.paths || this.config.scanPaths;\n const includePatterns = options?.include || this.config.include;\n const excludePatterns = options?.exclude || this.config.exclude;\n \n let totalScanned = 0;\n const totalPaths = scanPaths.length;\n \n for (const scanPath of scanPaths) {\n // Validate scan path\n if (!this.pathValidator.isValid(scanPath)) {\n throw new Error(`Invalid scan path: ${scanPath}`);\n }\n\n try {\n // Check if scanPath is a file or directory\n const stats = await fs.stat(scanPath).catch(() => null);\n \n if (stats?.isFile()) {\n // If it's a file, check if it matches include patterns and add directly\n const fileName = path.basename(scanPath);\n const shouldInclude = includePatterns.some((pattern: string) => {\n // Simple pattern matching for file extensions\n if (pattern.includes('*')) {\n const ext = pattern.replace('**/', '').replace('*', '');\n return fileName.endsWith(ext);\n }\n return fileName === pattern;\n });\n \n if (shouldInclude) {\n files.push(path.resolve(scanPath));\n }\n } else if (stats?.isDirectory()) {\n // If it's a directory, use glob as before\n for (const pattern of includePatterns) {\n const matches = await glob(pattern, {\n cwd: scanPath,\n ignore: excludePatterns,\n absolute: true,\n nodir: true,\n dot: true,\n });\n \n files.push(...matches);\n }\n } else {\n // Path doesn't exist, try glob from current directory\n const matches = await glob(scanPath, {\n ignore: excludePatterns,\n absolute: true,\n nodir: true,\n dot: true,\n });\n \n // Filter matches by include patterns\n for (const match of matches) {\n const shouldInclude = includePatterns.some((pattern: string) => {\n const ext = pattern.replace('**/', '').replace('*', '');\n return match.endsWith(ext);\n });\n if (shouldInclude) {\n files.push(match);\n }\n }\n }\n } catch (error) {\n console.warn(`Failed to scan path ${scanPath}:`, error);\n }\n \n totalScanned++;\n \n // Report progress\n if (this.progressCallback) {\n this.progressCallback({\n current: totalScanned,\n total: totalPaths,\n currentPath: scanPath,\n filesFound: files.length,\n });\n }\n }\n \n // Remove duplicates\n return [...new Set(files)];\n }\n\n /**\n * Scan with detailed file information\n */\n async scanWithInfo(options?: ScanOptions): Promise<FileInfo[]> {\n const filePaths = await this.scan(options);\n const fileInfos: FileInfo[] = [];\n \n for (const filePath of filePaths) {\n try {\n const stats = await fs.stat(filePath);\n const content = await this.readFile(filePath);\n \n fileInfos.push({\n path: filePath,\n content,\n stats: {\n size: stats.size,\n modified: stats.mtime,\n },\n });\n } catch (error) {\n // Skip files that can't be read\n console.warn(`Failed to read file ${filePath}:`, error);\n }\n }\n \n return fileInfos;\n }\n\n /**\n * Read a file with validation\n */\n async readFile(filePath: string): Promise<string> {\n // Validate file path\n if (!this.pathValidator.isValid(filePath)) {\n throw new Error(`Access denied: ${filePath}`);\n }\n \n try {\n return await fs.readFile(filePath, 'utf-8');\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n throw new Error(`File not found: ${filePath}`);\n }\n if (error.code === 'EACCES') {\n throw new Error(`Permission denied: ${filePath}`);\n }\n throw error;\n }\n }\n\n /**\n * Check if a file exists\n */\n async exists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Get file stats\n */\n async getStats(filePath: string): Promise<Awaited<ReturnType<typeof fs.stat>>> {\n if (!this.pathValidator.isValid(filePath)) {\n throw new Error(`Access denied: ${filePath}`);\n }\n \n return fs.stat(filePath);\n }\n\n /**\n * Set progress callback for long operations\n */\n onProgress(callback: (progress: ScanProgress) => void): void {\n this.progressCallback = callback;\n }\n\n /**\n * Get supported file extensions from include patterns\n */\n getSupportedExtensions(): string[] {\n const extensions = new Set<string>();\n \n this.config.include.forEach((pattern: string) => {\n const match = pattern.match(/\\*\\.(\\w+)$/);\n if (match) {\n extensions.add(`.${match[1]}`);\n }\n });\n \n return Array.from(extensions);\n }\n}","export interface Task {\n id: string;\n type: 'TODO' | 'FIXME' | 'HACK' | 'NOTE' | 'BUG' | 'OPTIMIZE' | 'REFACTOR';\n text: string;\n file: string;\n line: number;\n column: number;\n priority?: 'low' | 'medium' | 'high';\n assignee?: string;\n date?: string;\n context?: string;\n metadata?: Record<string, any>;\n}\n\nexport interface TaskPattern {\n pattern: RegExp;\n priority?: 'low' | 'medium' | 'high';\n}\n\nexport interface ExtractionOptions {\n customPatterns?: Record<string, TaskPattern>;\n includeContext?: boolean;\n contextLines?: number;\n parseMetadata?: boolean;\n}\n\nexport class TaskExtractor {\n private defaultPatterns: Record<string, TaskPattern> = {\n TODO: { pattern: /TODO[:\\s]+(.+)/gi, priority: 'medium' },\n FIXME: { pattern: /FIXME[:\\s]+(.+)/gi, priority: 'high' },\n HACK: { pattern: /HACK[:\\s]+(.+)/gi, priority: 'low' },\n NOTE: { pattern: /NOTE[:\\s]+(.+)/gi, priority: 'low' },\n BUG: { pattern: /BUG[:\\s]+(.+)/gi, priority: 'high' },\n OPTIMIZE: { pattern: /OPTIMIZE[:\\s]+(.+)/gi, priority: 'medium' },\n REFACTOR: { pattern: /REFACTOR[:\\s]+(.+)/gi, priority: 'medium' },\n };\n\n /**\n * Extract tasks from content\n */\n extract(content: string, filePath: string, options?: ExtractionOptions): Task[] {\n const tasks: Task[] = [];\n const lines = content.split('\\n');\n const patterns = options?.customPatterns || this.defaultPatterns;\n\n lines.forEach((line, lineIndex) => {\n Object.entries(patterns).forEach(([type, config]) => {\n const matches = [...line.matchAll(config.pattern)];\n \n matches.forEach(match => {\n if (match.index !== undefined) {\n const task: Task = {\n id: `${filePath}:${lineIndex + 1}:${match.index}`,\n type: type as Task['type'],\n text: match[1].trim(),\n file: filePath,\n line: lineIndex + 1,\n column: match.index + 1,\n priority: config.priority,\n };\n\n // Extract metadata if enabled\n if (options?.parseMetadata) {\n const metadata = this.extractMetadata(task.text);\n task.text = metadata.text;\n task.assignee = metadata.assignee;\n task.date = metadata.date;\n task.priority = metadata.priority || task.priority;\n task.metadata = metadata.extra;\n }\n\n // Add context if requested\n if (options?.includeContext) {\n const contextLines = options.contextLines || 2;\n task.context = this.getContext(lines, lineIndex, contextLines);\n }\n\n tasks.push(task);\n }\n });\n });\n });\n\n return tasks;\n }\n\n /**\n * Extract metadata from task text\n * Format: TODO(assignee): text [priority:high] [due:2024-01-01] {key:value}\n */\n private extractMetadata(text: string): {\n text: string;\n assignee?: string;\n date?: string;\n priority?: 'low' | 'medium' | 'high';\n extra: Record<string, any>;\n } {\n let cleanText = text;\n const metadata: any = { extra: {} };\n\n // Extract assignee (TODO(john): ...)\n const assigneeMatch = text.match(/^\\(([^)]+)\\):\\s*/);\n if (assigneeMatch) {\n metadata.assignee = assigneeMatch[1];\n cleanText = cleanText.replace(assigneeMatch[0], '');\n }\n\n // Extract priority [priority:high]\n const priorityMatch = cleanText.match(/\\[priority:(low|medium|high)\\]/i);\n if (priorityMatch) {\n metadata.priority = priorityMatch[1].toLowerCase() as Task['priority'];\n cleanText = cleanText.replace(priorityMatch[0], '');\n }\n\n // Extract date [due:2024-01-01]\n const dateMatch = cleanText.match(/\\[due:(\\d{4}-\\d{2}-\\d{2})\\]/);\n if (dateMatch) {\n metadata.date = dateMatch[1];\n cleanText = cleanText.replace(dateMatch[0], '');\n }\n\n // Extract custom metadata {key:value}\n const metadataMatches = [...cleanText.matchAll(/\\{([^:}]+):([^}]+)\\}/g)];\n metadataMatches.forEach(match => {\n metadata.extra[match[1]] = match[2];\n cleanText = cleanText.replace(match[0], '');\n });\n\n return {\n text: cleanText.trim(),\n ...metadata,\n };\n }\n\n /**\n * Get context lines around a task\n */\n private getContext(lines: string[], lineIndex: number, contextLines: number): string {\n const start = Math.max(0, lineIndex - contextLines);\n const end = Math.min(lines.length, lineIndex + contextLines + 1);\n \n return lines\n .slice(start, end)\n .map((line, idx) => {\n const actualLine = start + idx;\n const prefix = actualLine === lineIndex ? '>' : ' ';\n return `${prefix} ${actualLine + 1}: ${line}`;\n })\n .join('\\n');\n }\n\n /**\n * Extract tasks from multiple files\n */\n async extractFromFiles(files: Array<{ path: string; content: string }>, options?: ExtractionOptions): Promise<Task[]> {\n const allTasks: Task[] = [];\n\n for (const file of files) {\n const tasks = this.extract(file.content, file.path, options);\n allTasks.push(...tasks);\n }\n\n return allTasks;\n }\n\n /**\n * Group tasks by type\n */\n groupByType(tasks: Task[]): Record<string, Task[]> {\n return tasks.reduce((groups, task) => {\n const type = task.type;\n if (!groups[type]) {\n groups[type] = [];\n }\n groups[type].push(task);\n return groups;\n }, {} as Record<string, Task[]>);\n }\n\n /**\n * Group tasks by file\n */\n groupByFile(tasks: Task[]): Record<string, Task[]> {\n return tasks.reduce((groups, task) => {\n const file = task.file;\n if (!groups[file]) {\n groups[file] = [];\n }\n groups[file].push(task);\n return groups;\n }, {} as Record<string, Task[]>);\n }\n\n /**\n * Filter tasks by priority\n */\n filterByPriority(tasks: Task[], priority: 'low' | 'medium' | 'high'): Task[] {\n return tasks.filter(task => task.priority === priority);\n }\n\n /**\n * Sort tasks by various criteria\n */\n sortTasks(tasks: Task[], by: 'priority' | 'type' | 'file' | 'line'): Task[] {\n const sorted = [...tasks];\n\n switch (by) {\n case 'priority':\n const priorityOrder = { high: 0, medium: 1, low: 2, undefined: 3 };\n sorted.sort((a, b) => \n (priorityOrder[a.priority || 'undefined'] || 3) - \n (priorityOrder[b.priority || 'undefined'] || 3)\n );\n break;\n \n case 'type':\n sorted.sort((a, b) => a.type.localeCompare(b.type));\n break;\n \n case 'file':\n sorted.sort((a, b) => a.file.localeCompare(b.file));\n break;\n \n case 'line':\n sorted.sort((a, b) => {\n const fileCompare = a.file.localeCompare(b.file);\n return fileCompare !== 0 ? fileCompare : a.line - b.line;\n });\n break;\n }\n\n return sorted;\n }\n}","import matter from 'gray-matter';\nimport * as crypto from 'crypto';\n\nexport interface ParsedMarkdown {\n content: string;\n data: Record<string, any>;\n excerpt?: string;\n toc?: TableOfContentsItem[];\n wordCount?: number;\n readingTime?: number;\n}\n\nexport interface TableOfContentsItem {\n id: string;\n text: string;\n level: number;\n children: TableOfContentsItem[];\n}\n\nexport interface MarkdownSection {\n id: string;\n title: string;\n content: string;\n level: number;\n}\n\nexport interface MarkdownParserOptions {\n excerpt?: boolean;\n excerptSeparator?: string;\n generateTOC?: boolean;\n calculateStats?: boolean;\n extractSections?: boolean;\n}\n\nexport class MarkdownParser {\n private defaultOptions: MarkdownParserOptions = {\n excerpt: true,\n excerptSeparator: '<!-- more -->',\n generateTOC: true,\n calculateStats: true,\n extractSections: false,\n };\n\n /**\n * Parse markdown content with frontmatter\n */\n parse(content: string, options?: MarkdownParserOptions): ParsedMarkdown {\n const opts = { ...this.defaultOptions, ...options };\n \n // Parse frontmatter\n const { content: markdownContent, data, excerpt } = matter(content, {\n excerpt: opts.excerpt,\n excerpt_separator: opts.excerptSeparator,\n });\n\n const result: ParsedMarkdown = {\n content: markdownContent,\n data,\n excerpt,\n };\n\n // Generate table of contents\n if (opts.generateTOC) {\n result.toc = this.generateTableOfContents(markdownContent);\n }\n\n // Calculate stats\n if (opts.calculateStats) {\n const stats = this.calculateStats(markdownContent);\n result.wordCount = stats.wordCount;\n result.readingTime = stats.readingTime;\n }\n\n return result;\n }\n\n /**\n * Extract all headings with hierarchy\n */\n extractHeadings(content: string): Array<{ text: string; level: number; id: string }> {\n const headingPattern = /^(#{1,6})\\s+(.+)$/gm;\n const headings: Array<{ text: string; level: number; id: string }> = [];\n let match;\n\n while ((match = headingPattern.exec(content)) !== null) {\n const level = match[1].length;\n const text = match[2].trim();\n const id = this.createSlug(text);\n \n headings.push({ text, level, id });\n }\n\n return headings;\n }\n\n /**\n * Generate table of contents with nested structure\n */\n generateTableOfContents(content: string): TableOfContentsItem[] {\n const headings = this.extractHeadings(content);\n const toc: TableOfContentsItem[] = [];\n const stack: TableOfContentsItem[] = [];\n\n headings.forEach(heading => {\n const item: TableOfContentsItem = {\n id: heading.id,\n text: heading.text,\n level: heading.level,\n children: [],\n };\n\n // Find parent level\n while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) {\n stack.pop();\n }\n\n // Add to parent or root\n if (stack.length === 0) {\n toc.push(item);\n } else {\n stack[stack.length - 1].children.push(item);\n }\n\n stack.push(item);\n });\n\n return toc;\n }\n\n /**\n * Extract sections by headings\n */\n extractSections(content: string): MarkdownSection[] {\n const lines = content.split('\\n');\n const sections: MarkdownSection[] = [];\n let currentSection: MarkdownSection | null = null;\n let sectionContent: string[] = [];\n\n lines.forEach(line => {\n const headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n \n if (headingMatch) {\n // Save previous section\n if (currentSection) {\n currentSection.content = sectionContent.join('\\n').trim();\n sections.push(currentSection);\n }\n\n // Start new section\n const level = headingMatch[1].length;\n const title = headingMatch[2].trim();\n currentSection = {\n id: this.createSlug(title),\n title,\n content: '',\n level,\n };\n sectionContent = [];\n } else if (currentSection) {\n sectionContent.push(line);\n }\n });\n\n // Save last section\n if (currentSection) {\n (currentSection as MarkdownSection).content = sectionContent.join('\\n').trim();\n sections.push(currentSection);\n }\n\n return sections;\n }\n\n /**\n * Extract links from markdown\n */\n extractLinks(content: string): Array<{ text: string; url: string; title?: string }> {\n const links: Array<{ text: string; url: string; title?: string }> = [];\n \n // Inline links: [text](url \"title\")\n const inlineLinkPattern = /\\[([^\\]]+)\\]\\(([^)]+?)(?:\\s+\"([^\"]+)\")?\\)/g;\n let match;\n \n while ((match = inlineLinkPattern.exec(content)) !== null) {\n links.push({\n text: match[1],\n url: match[2],\n title: match[3],\n });\n }\n\n // Reference links: [text][ref]\n const refLinkPattern = /\\[([^\\]]+)\\]\\[([^\\]]+)\\]/g;\n const refDefinitions = this.extractReferenceDefinitions(content);\n \n while ((match = refLinkPattern.exec(content)) !== null) {\n const ref = match[2];\n if (refDefinitions[ref]) {\n links.push({\n text: match[1],\n url: refDefinitions[ref].url,\n title: refDefinitions[ref].title,\n });\n }\n }\n\n return links;\n }\n\n /**\n * Extract reference link definitions\n */\n private extractReferenceDefinitions(content: string): Record<string, { url: string; title?: string }> {\n const definitions: Record<string, { url: string; title?: string }> = {};\n const pattern = /^\\[([^\\]]+)\\]:\\s+(\\S+)(?:\\s+\"([^\"]+)\")?$/gm;\n let match;\n\n while ((match = pattern.exec(content)) !== null) {\n definitions[match[1]] = {\n url: match[2],\n title: match[3],\n };\n }\n\n return definitions;\n }\n\n /**\n * Extract code blocks\n */\n extractCodeBlocks(content: string): Array<{ lang?: string; code: string }> {\n const blocks: Array<{ lang?: string; code: string }> = [];\n const pattern = /```(\\w+)?\\n([\\s\\S]*?)```/g;\n let match;\n\n while ((match = pattern.exec(content)) !== null) {\n blocks.push({\n lang: match[1],\n code: match[2].trim(),\n });\n }\n\n return blocks;\n }\n\n /**\n * Calculate word count and reading time\n */\n calculateStats(content: string): { wordCount: number; readingTime: number } {\n // Remove code blocks\n const withoutCode = content.replace(/```[\\s\\S]*?```/g, '');\n \n // Remove markdown syntax\n const plainText = withoutCode\n .replace(/^#{1,6}\\s+/gm, '') // Headers\n .replace(/[*_~`]/g, '') // Emphasis\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1') // Links\n .replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, '') // Images\n .replace(/^\\s*[-*+]\\s+/gm, '') // Lists\n .replace(/^\\s*\\d+\\.\\s+/gm, '') // Numbered lists\n .replace(/^\\s*>/gm, ''); // Blockquotes\n\n // Count words\n const words = plainText.match(/\\b\\w+\\b/g) || [];\n const wordCount = words.length;\n\n // Calculate reading time (assuming 200 words per minute)\n const readingTime = Math.ceil(wordCount / 200);\n\n return { wordCount, readingTime };\n }\n\n /**\n * Create URL-friendly slug from text\n */\n private createSlug(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n }\n\n /**\n * Parse markdown table\n */\n parseTables(content: string): Array<{ headers: string[]; rows: string[][] }> {\n const tables: Array<{ headers: string[]; rows: string[][] }> = [];\n const lines = content.split('\\n');\n let i = 0;\n\n while (i < lines.length) {\n // Look for table header\n if (i + 1 < lines.length && \n lines[i].includes('|') && \n lines[i + 1].match(/^\\s*\\|?\\s*:?-+:?\\s*\\|/)) {\n \n const headers = lines[i]\n .split('|')\n .map(h => h.trim())\n .filter(h => h);\n \n const rows: string[][] = [];\n i += 2; // Skip header and separator\n \n // Parse rows\n while (i < lines.length && lines[i].includes('|')) {\n const row = lines[i]\n .split('|')\n .map(cell => cell.trim())\n .filter(cell => cell);\n \n if (row.length > 0) {\n rows.push(row);\n }\n i++;\n }\n \n tables.push({ headers, rows });\n } else {\n i++;\n }\n }\n\n return tables;\n }\n\n /**\n * Convert markdown to plain text\n */\n toPlainText(content: string): string {\n return content\n .replace(/```[\\s\\S]*?```/g, '') // Remove code blocks\n .replace(/^#{1,6}\\s+/gm, '') // Remove headers\n .replace(/[*_~]/g, '') // Remove emphasis\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1') // Convert links to text\n .replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, '') // Remove images\n .replace(/^\\s*[-*+]\\s+/gm, '') // Remove list markers\n .replace(/^\\s*\\d+\\.\\s+/gm, '') // Remove numbered list markers\n .replace(/^\\s*>/gm, '') // Remove blockquotes\n .replace(/\\n{3,}/g, '\\n\\n') // Normalize line breaks\n .trim();\n }\n\n /**\n * Generate a content hash for caching\n */\n generateHash(content: string): string {\n return crypto.createHash('sha256').update(content).digest('hex');\n }\n}","export interface RateLimiterOptions {\n windowMs: number;\n maxRequests: number;\n keyGenerator?: (context: any) => string;\n skipSuccessfulRequests?: boolean;\n skipFailedRequests?: boolean;\n onLimitReached?: (identifier: string, info: RateLimitInfo) => void;\n}\n\nexport interface RateLimitInfo {\n limit: number;\n current: number;\n remaining: number;\n resetTime: number;\n retryAfter: number;\n}\n\ninterface RequestRecord {\n timestamps: number[];\n blockedUntil?: number;\n}\n\nexport class RateLimiter {\n private requests: Map<string, RequestRecord> = new Map();\n private cleanupInterval?: NodeJS.Timeout;\n\n constructor(private options: RateLimiterOptions) {\n // Start cleanup interval to remove old entries\n this.cleanupInterval = setInterval(() => {\n this.cleanup();\n }, Math.min(options.windowMs, 60000)); // Cleanup at least every minute\n }\n\n /**\n * Check if a request is allowed\n */\n isAllowed(identifier: string, context?: any): boolean {\n const key = this.options.keyGenerator ? \n this.options.keyGenerator(context) : identifier;\n \n const now = Date.now();\n const record = this.requests.get(key) || { timestamps: [] };\n\n // Check if temporarily blocked\n if (record.blockedUntil && now < record.blockedUntil) {\n return false;\n }\n\n // Remove expired timestamps\n record.timestamps = record.timestamps.filter(\n timestamp => now - timestamp < this.options.windowMs\n );\n\n // Check rate limit\n if (record.timestamps.length >= this.options.maxRequests) {\n const oldestTimestamp = record.timestamps[0];\n const resetTime = oldestTimestamp + this.options.windowMs;\n const retryAfter = resetTime - now;\n\n // Temporary block for repeated violations\n if (record.timestamps.length > this.options.maxRequests * 1.5) {\n record.blockedUntil = now + this.options.windowMs * 2;\n }\n\n // Call limit reached callback\n if (this.options.onLimitReached) {\n const info: RateLimitInfo = {\n limit: this.options.maxRequests,\n current: record.timestamps.length,\n remaining: 0,\n resetTime,\n retryAfter: Math.ceil(retryAfter / 1000),\n };\n this.options.onLimitReached(key, info);\n }\n\n return false;\n }\n\n // Add current request\n record.timestamps.push(now);\n this.requests.set(key, record);\n\n return true;\n }\n\n /**\n * Get current rate limit info for an identifier\n */\n getInfo(identifier: string): RateLimitInfo {\n const now = Date.now();\n const record = this.requests.get(identifier) || { timestamps: [] };\n \n // Remove expired timestamps\n const validTimestamps = record.timestamps.filter(\n timestamp => now - timestamp < this.options.windowMs\n );\n\n const current = validTimestamps.length;\n const remaining = Math.max(0, this.options.maxRequests - current);\n \n let resetTime = now + this.options.windowMs;\n let retryAfter = 0;\n\n if (validTimestamps.length > 0) {\n const oldestTimestamp = validTimestamps[0];\n resetTime = oldestTimestamp + this.options.windowMs;\n \n if (current >= this.options.maxRequests) {\n retryAfter = Math.ceil((resetTime - now) / 1000);\n }\n }\n\n return {\n limit: this.options.maxRequests,\n current,\n remaining,\n resetTime,\n retryAfter,\n };\n }\n\n /**\n * Reset rate limit for an identifier\n */\n reset(identifier: string): void {\n this.requests.delete(identifier);\n }\n\n /**\n * Reset all rate limits\n */\n resetAll(): void {\n this.requests.clear();\n }\n\n /**\n * Record a request result (for conditional limiting)\n */\n recordResult(identifier: string, success: boolean): void {\n if (success && this.options.skipSuccessfulRequests) {\n // Remove the last timestamp if we should skip successful requests\n const record = this.requests.get(identifier);\n if (record && record.timestamps.length > 0) {\n record.timestamps.pop();\n }\n } else if (!success && this.options.skipFailedRequests) {\n // Remove the last timestamp if we should skip failed requests\n const record = this.requests.get(identifier);\n if (record && record.timestamps.length > 0) {\n record.timestamps.pop();\n }\n }\n }\n\n /**\n * Clean up old entries\n */\n private cleanup(): void {\n const now = Date.now();\n const expiredKeys: string[] = [];\n\n for (const [key, record] of this.requests.entries()) {\n // Remove expired timestamps\n record.timestamps = record.timestamps.filter(\n timestamp => now - timestamp < this.options.windowMs\n );\n\n // Remove entries with no timestamps and no active block\n if (record.timestamps.length === 0 && \n (!record.blockedUntil || now >= record.blockedUntil)) {\n expiredKeys.push(key);\n }\n }\n\n // Delete expired entries\n expiredKeys.forEach(key => this.requests.delete(key));\n }\n\n /**\n * Get statistics about current rate limiting\n */\n getStats(): {\n totalIdentifiers: number;\n blockedIdentifiers: number;\n totalRequests: number;\n } {\n const now = Date.now();\n let blockedCount = 0;\n let totalRequests = 0;\n\n for (const record of this.requests.values()) {\n const validTimestamps = record.timestamps.filter(\n timestamp => now - timestamp < this.options.windowMs\n );\n \n totalRequests += validTimestamps.length;\n \n if (validTimestamps.length >= this.options.maxRequests ||\n (record.blockedUntil && now < record.blockedUntil)) {\n blockedCount++;\n }\n }\n\n return {\n totalIdentifiers: this.requests.size,\n blockedIdentifiers: blockedCount,\n totalRequests,\n };\n }\n\n /**\n * Destroy the rate limiter and clean up resources\n */\n destroy(): void {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n }\n this.requests.clear();\n }\n}","export * from './path-validator';\nexport * from './rate-limiter';","import * as crypto from 'crypto';\n\nexport interface CacheOptions {\n ttl: number; // Time to live in milliseconds\n maxSize?: number; // Maximum number of entries\n updateOnGet?: boolean; // Reset TTL on get\n onEvict?: (key: string, value: any) => void; // Callback on eviction\n}\n\ninterface CacheEntry<T> {\n value: T;\n expiry: number;\n size: number;\n hits: number;\n lastAccess: number;\n}\n\nexport interface CacheStats {\n entries: number;\n hits: number;\n misses: number;\n evictions: number;\n size: number;\n}\n\nexport class CacheManager {\n private cache: Map<string, CacheEntry<any>> = new Map();\n private stats: CacheStats = {\n entries: 0,\n hits: 0,\n misses: 0,\n evictions: 0,\n size: 0,\n };\n private cleanupInterval?: NodeJS.Timeout;\n private defaultOptions: Partial<CacheOptions> = {\n ttl: 5 * 60 * 1000, // 5 minutes default\n maxSize: 1000,\n updateOnGet: false,\n };\n\n constructor(options?: Partial<CacheOptions>) {\n this.defaultOptions = { ...this.defaultOptions, ...options };\n this.startCleanupInterval();\n }\n\n /**\n * Set a value in the cache\n */\n set<T>(key: string, value: T, options?: Partial<CacheOptions>): void {\n const opts = { ...this.defaultOptions, ...options } as CacheOptions;\n const expiry = Date.now() + opts.ttl;\n const size = this.estimateSize(value);\n\n // Check if we need to evict entries\n if (opts.maxSize && this.cache.size >= opts.maxSize) {\n this.evictLRU();\n }\n\n const entry: CacheEntry<T> = {\n value,\n expiry,\n size,\n hits: 0,\n lastAccess: Date.now(),\n };\n\n this.cache.set(key, entry);\n this.stats.entries = this.cache.size;\n this.stats.size += size;\n }\n\n /**\n * Get a value from the cache\n */\n get<T>(key: string, options?: { updateOnGet?: boolean }): T | undefined {\n const entry = this.cache.get(key);\n \n if (!entry) {\n this.stats.misses++;\n return undefined;\n }\n \n if (Date.now() > entry.expiry) {\n this.delete(key);\n this.stats.misses++;\n return undefined;\n }\n\n // Update stats\n entry.hits++;\n entry.lastAccess = Date.now();\n this.stats.hits++;\n\n // Update expiry if requested\n if (options?.updateOnGet ?? this.defaultOptions.updateOnGet) {\n entry.expiry = Date.now() + (this.defaultOptions.ttl || 5 * 60 * 1000);\n }\n \n return entry.value;\n }\n\n /**\n * Get or set a value (memoization helper)\n */\n async getOrSet<T>(\n key: string,\n factory: () => Promise<T>,\n options?: Partial<CacheOptions>\n ): Promise<T> {\n const cached = this.get<T>(key);\n if (cached !== undefined) {\n return cached;\n }\n\n const value = await factory();\n this.set(key, value, options);\n return value;\n }\n\n /**\n * Check if key exists and is not expired\n */\n has(key: string): boolean {\n const entry = this.cache.get(key);\n if (!entry) return false;\n \n if (Date.now() > entry.expiry) {\n this.delete(key);\n return false;\n }\n \n return true;\n }\n\n /**\n * Delete a key from the cache\n */\n delete(key: string): boolean {\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n this.stats.size -= entry.size;\n this.stats.entries--;\n \n const deleted = this.cache.delete(key);\n \n if (deleted && this.defaultOptions.onEvict) {\n this.defaultOptions.onEvict(key, entry.value);\n }\n \n return deleted;\n }\n\n /**\n * Clear all entries\n */\n clear(): void {\n if (this.defaultOptions.onEvict) {\n for (const [key, entry] of this.cache.entries()) {\n this.defaultOptions.onEvict(key, entry.value);\n }\n }\n \n this.cache.clear();\n this.stats.entries = 0;\n this.stats.size = 0;\n }\n\n /**\n * Get cache statistics\n */\n getStats(): CacheStats {\n return { ...this.stats };\n }\n\n /**\n * Get all keys\n */\n keys(): string[] {\n return Array.from(this.cache.keys());\n }\n\n /**\n * Get cache size\n */\n size(): number {\n return this.cache.size;\n }\n\n /**\n * Create a cache key from multiple parts\n */\n static createKey(...parts: any[]): string {\n const str = parts.map(p => JSON.stringify(p)).join(':');\n return crypto.createHash('md5').update(str).digest('hex');\n }\n\n /**\n * Clean up expired entries\n */\n private cleanup(): void {\n const now = Date.now();\n let evicted = 0;\n \n for (const [key, entry] of this.cache.entries()) {\n if (now > entry.expiry) {\n this.delete(key);\n evicted++;\n }\n }\n \n this.stats.evictions += evicted;\n }\n\n /**\n * Evict least recently used entry\n */\n private evictLRU(): void {\n let lruKey: string | null = null;\n let lruTime = Infinity;\n\n for (const [key, entry] of this.cache.entries()) {\n if (entry.lastAccess < lruTime) {\n lruTime = entry.lastAccess;\n lruKey = key;\n }\n }\n\n if (lruKey) {\n this.delete(lruKey);\n this.stats.evictions++;\n }\n }\n\n /**\n * Estimate size of a value in bytes\n */\n private estimateSize(value: any): number {\n if (value === null || value === undefined) return 0;\n if (typeof value === 'string') return value.length * 2; // UTF-16\n if (typeof value === 'number') return 8;\n if (typeof value === 'boolean') return 4;\n if (value instanceof Date) return 8;\n if (Buffer.isBuffer(value)) return value.length;\n \n // For objects/arrays, use JSON stringify as approximation\n try {\n return JSON.stringify(value).length * 2;\n } catch {\n return 1024; // Default size for complex objects\n }\n }\n\n /**\n * Start automatic cleanup interval\n */\n private startCleanupInterval(): void {\n // Clean up every minute\n this.cleanupInterval = setInterval(() => {\n this.cleanup();\n }, 60 * 1000);\n }\n\n /**\n * Stop the cache manager and cleanup\n */\n destroy(): void {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n }\n this.clear();\n }\n}","export enum LogLevel {\n ERROR = 0,\n WARN = 1,\n INFO = 2,\n DEBUG = 3,\n}\n\nexport interface LoggerOptions {\n level: LogLevel;\n prefix?: string;\n timestamp?: boolean;\n colors?: boolean;\n}\n\nexport interface LogEntry {\n level: LogLevel;\n message: string;\n timestamp: Date;\n prefix?: string;\n data?: any;\n error?: Error;\n}\n\nexport class Logger {\n private options: LoggerOptions;\n private handlers: ((entry: LogEntry) => void)[] = [];\n\n constructor(options: Partial<LoggerOptions> = {}) {\n this.options = {\n level: LogLevel.INFO,\n timestamp: true,\n colors: true,\n ...options,\n };\n }\n\n /**\n * Add a custom log handler\n */\n addHandler(handler: (entry: LogEntry) => void): void {\n this.handlers.push(handler);\n }\n\n /**\n * Remove a log handler\n */\n removeHandler(handler: (entry: LogEntry) => void): void {\n const index = this.handlers.indexOf(handler);\n if (index !== -1) {\n this.handlers.splice(index, 1);\n }\n }\n\n /**\n * Log an error message\n */\n error(message: string, error?: Error | any): void {\n this.log(LogLevel.ERROR, message, error);\n }\n\n /**\n * Log a warning message\n */\n warn(message: string, data?: any): void {\n this.log(LogLevel.WARN, message, data);\n }\n\n /**\n * Log an info message\n */\n info(message: string, data?: any): void {\n this.log(LogLevel.INFO, message, data);\n }\n\n /**\n * Log a debug message\n */\n debug(message: string, data?: any): void {\n this.log(LogLevel.DEBUG, message, data);\n }\n\n /**\n * Core logging method\n */\n private log(level: LogLevel, message: string, data?: any): void {\n if (level > this.options.level) {\n return;\n }\n\n const entry: LogEntry = {\n level,\n message,\n timestamp: new Date(),\n prefix: this.options.prefix,\n data,\n };\n\n if (data instanceof Error) {\n entry.error = data;\n entry.data = {\n name: data.name,\n message: data.message,\n stack: data.stack,\n };\n }\n\n // Call custom handlers\n this.handlers.forEach(handler => handler(entry));\n\n // Default console output\n this.consoleOutput(entry);\n }\n\n /**\n * Format and output to console\n */\n private consoleOutput(entry: LogEntry): void {\n const parts: string[] = [];\n\n // Timestamp\n if (this.options.timestamp) {\n parts.push(`[${entry.timestamp.toISOString()}]`);\n }\n\n // Level\n const levelName = LogLevel[entry.level];\n if (this.options.colors) {\n parts.push(this.colorize(levelName, entry.level));\n } else {\n parts.push(`[${levelName}]`);\n }\n\n // Prefix\n if (entry.prefix) {\n parts.push(`[${entry.prefix}]`);\n }\n\n // Message\n parts.push(entry.message);\n\n const logMessage = parts.join(' ');\n\n // Output based on level\n switch (entry.level) {\n case LogLevel.ERROR:\n console.error(logMessage);\n if (entry.error) {\n console.error(entry.error);\n }\n break;\n case LogLevel.WARN:\n console.warn(logMessage);\n break;\n case LogLevel.DEBUG:\n console.debug(logMessage);\n break;\n default:\n console.log(logMessage);\n }\n\n // Log additional data if present\n if (entry.data && !entry.error) {\n console.log(JSON.stringify(entry.data, null, 2));\n }\n }\n\n /**\n * Colorize text based on log level\n */\n private colorize(text: string, level: LogLevel): string {\n const colors = {\n [LogLevel.ERROR]: '\\x1b[31m', // Red\n [LogLevel.WARN]: '\\x1b[33m', // Yellow\n [LogLevel.INFO]: '\\x1b[36m', // Cyan\n [LogLevel.DEBUG]: '\\x1b[90m', // Gray\n };\n\n const color = colors[level] || '';\n const reset = '\\x1b[0m';\n\n return `${color}[${text}]${reset}`;\n }\n\n /**\n * Create a child logger with a prefix\n */\n child(prefix: string): Logger {\n const childPrefix = this.options.prefix \n ? `${this.options.prefix}:${prefix}`\n : prefix;\n\n const child = new Logger({\n ...this.options,\n prefix: childPrefix,\n });\n\n // Copy handlers\n this.handlers.forEach(handler => child.addHandler(handler));\n\n return child;\n }\n\n /**\n * Set the log level\n */\n setLevel(level: LogLevel): void {\n this.options.level = level;\n }\n\n /**\n * Get the current log level\n */\n getLevel(): LogLevel {\n return this.options.level;\n }\n}\n\n// Default logger instance\nexport const logger = new Logger();","import { logger } from './logger';\n\nexport interface ErrorContext {\n operation?: string;\n file?: string;\n user?: string;\n metadata?: Record<string, any>;\n}\n\nexport class VibecodeError extends Error {\n public code: string;\n public statusCode: number;\n public context?: ErrorContext;\n public isOperational: boolean;\n\n constructor(\n message: string,\n code: string = 'UNKNOWN_ERROR',\n statusCode: number = 500,\n isOperational: boolean = true,\n context?: ErrorContext\n ) {\n super(message);\n this.name = 'VibecodeError';\n this.code = code;\n this.statusCode = statusCode;\n this.isOperational = isOperational;\n this.context = context;\n\n // Maintain proper stack trace\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n}\n\nexport class ValidationError extends VibecodeError {\n constructor(message: string, context?: ErrorContext) {\n super(message, 'VALIDATION_ERROR', 400, true, context);\n this.name = 'ValidationError';\n }\n}\n\nexport class AuthorizationError extends VibecodeError {\n constructor(message: string, context?: ErrorContext) {\n super(message, 'AUTHORIZATION_ERROR', 403, true, context);\n this.name = 'AuthorizationError';\n }\n}\n\nexport class NotFoundError extends VibecodeError {\n constructor(message: string, context?: ErrorContext) {\n super(message, 'NOT_FOUND', 404, true, context);\n this.name = 'NotFoundError';\n }\n}\n\nexport class RateLimitError extends VibecodeError {\n public retryAfter: number;\n\n constructor(message: string, retryAfter: number, context?: ErrorContext) {\n super(message, 'RATE_LIMIT_EXCEEDED', 429, true, context);\n this.name = 'RateLimitError';\n this.retryAfter = retryAfter;\n }\n}\n\nexport class ErrorHandler {\n private errorHandlers: Map<string, (error: VibecodeError) => void> = new Map();\n\n /**\n * Register a custom error handler for specific error codes\n */\n registerHandler(errorCode: string, handler: (error: VibecodeError) => void): void {\n this.errorHandlers.set(errorCode, handler);\n }\n\n /**\n * Handle an error\n */\n handle(error: Error | VibecodeError, context?: ErrorContext): {\n message: string;\n code: string;\n statusCode: number;\n details?: any;\n } {\n // Convert to VibecodeError if needed\n const vibecodeError = this.toVibecodeError(error, context);\n\n // Log the error\n this.logError(vibecodeError);\n\n // Call custom handler if registered\n const customHandler = this.errorHandlers.get(vibecodeError.code);\n if (customHandler) {\n customHandler(vibecodeError);\n }\n\n // Return error response\n return this.createErrorResponse(vibecodeError);\n }\n\n /**\n * Convert any error to VibecodeError\n */\n private toVibecodeError(error: Error | VibecodeError, context?: ErrorContext): VibecodeError {\n if (error instanceof VibecodeError) {\n // Merge context if provided\n if (context) {\n error.context = { ...error.context, ...context };\n }\n return error;\n }\n\n // Handle specific error types\n if (error.name === 'ValidationError') {\n return new ValidationError(error.message, context);\n }\n\n if (error.message.includes('ENOENT')) {\n return new NotFoundError(`File not found: ${error.message}`, context);\n }\n\n if (error.message.includes('EACCES')) {\n return new AuthorizationError(`Permission denied: ${error.message}`, context);\n }\n\n // Default error\n return new VibecodeError(\n error.message,\n 'INTERNAL_ERROR',\n 500,\n false,\n context\n );\n }\n\n /**\n * Log error with appropriate level\n */\n private logError(error: VibecodeError): void {\n const logContext = {\n code: error.code,\n statusCode: error.statusCode,\n isOperational: error.isOperational,\n context: error.context,\n stack: error.stack,\n };\n\n if (error.isOperational) {\n logger.warn(`Operational error: ${error.message}`, logContext);\n } else {\n logger.error(`System error: ${error.message}`, error);\n }\n }\n\n /**\n * Create error response\n */\n private createErrorResponse(error: VibecodeError): {\n message: string;\n code: string;\n statusCode: number;\n details?: any;\n } {\n const response: any = {\n message: error.message,\n code: error.code,\n statusCode: error.statusCode,\n };\n\n // Add additional details for specific error types\n if (error instanceof RateLimitError) {\n response.details = {\n retryAfter: error.retryAfter,\n };\n