UNPKG

mlld

Version:

mlld: llm scripting language

902 lines (897 loc) 29.4 kB
import { ResolverError, MlldFileNotFoundError } from './chunk-V5XE5YB5.mjs'; import { __name, __publicField } from './chunk-NJQT543K.mjs'; import * as path2 from 'path'; var _PathMatcher = class _PathMatcher { constructor(fileSystem) { __publicField(this, "fileSystem"); __publicField(this, "directoryCache", /* @__PURE__ */ new Map()); __publicField(this, "CACHE_TTL", 5e3); this.fileSystem = fileSystem; } /** * Find a matching path with fuzzy matching support */ async findMatch(targetPath, basePath, config, maxDepth) { const cfg = { ..._PathMatcher.DEFAULT_CONFIG, ...config }; if (targetPath.includes("..")) { return { exact: false, confidence: 0 }; } if (!cfg.enabled) { const fullPath = path2.join(basePath, targetPath); const exists = await this.fileSystem.exists(fullPath); return { path: exists ? fullPath : void 0, exact: true, confidence: exists ? 1 : 0 }; } const cleanPath = targetPath.startsWith("./") ? targetPath.substring(2) : targetPath; const segments = cleanPath.split("/").filter((s) => s.length > 0); if (segments.length > 0 && maxDepth !== void 0) { const depth = Math.max(0, segments.length - 1); if (depth >= maxDepth) { return { exact: false, confidence: 0 }; } } const exactPath = path2.join(basePath, targetPath); if (await this.fileSystem.exists(exactPath)) { return { path: exactPath, exact: true, confidence: 1 }; } if (process.env.DEBUG_FUZZY) { console.log(`No exact match for ${exactPath}, trying fuzzy...`); } if (segments.length === 0) { return { exact: false, confidence: 0 }; } try { const result = await this.matchSegments(segments, basePath, cfg); if (result.candidates && result.candidates.length > 1) { const topConfidence = result.candidates[0].confidence; const equalMatches = result.candidates.filter((c) => c.confidence === topConfidence); if (equalMatches.length > 1) { return { exact: false, confidence: 0, candidates: equalMatches }; } } return result; } catch (error) { return { exact: false, confidence: 0 }; } } /** * Clear the directory cache */ clearCache() { this.directoryCache.clear(); } /** * Recursively match path segments with fuzzy matching */ async matchSegments(segments, currentPath, config, depth = 0) { if (process.env.DEBUG_FUZZY) { console.log(`matchSegments: segments=${segments.join("/")}, currentPath=${currentPath}, depth=${depth}`); } if (segments.length === 0) { return { path: currentPath, exact: false, confidence: 1 }; } const [currentSegment, ...remainingSegments] = segments; const entries = await this.getCachedDirectoryEntries(currentPath); if (!entries) { if (process.env.DEBUG_FUZZY) { console.log(`No entries found in directory: ${currentPath}`); } return { exact: false, confidence: 0 }; } if (process.env.DEBUG_FUZZY) { console.log(`Directory entries: ${entries.join(", ")}`); } const matches = await this.findSegmentMatches(currentSegment, entries, currentPath, config); if (matches.length === 0) { const suggestions = this.generateSuggestions(currentSegment, entries, config); if (process.env.DEBUG_FUZZY) { console.log(`No matches for segment '${currentSegment}', generated ${suggestions.length} suggestions`); } return { exact: false, confidence: 0, suggestions: suggestions.slice(0, 3) // Top 3 suggestions }; } if (remainingSegments.length === 0) { matches.sort((a, b) => { if (a.confidence !== b.confidence) { return b.confidence - a.confidence; } const typePriority = { exact: 4, case: 3, whitespace: 2, fuzzy: 1 }; return typePriority[b.matchType] - typePriority[a.matchType]; }); return { path: matches[0].path, exact: matches[0].matchType === "exact", confidence: matches[0].confidence, candidates: matches.length > 1 ? matches : void 0 }; } const results = []; for (const match of matches) { const subResult = await this.matchSegments(remainingSegments, match.path, config, depth + 1); if (subResult.path) { results.push({ ...subResult, confidence: match.confidence * subResult.confidence }); } } if (results.length === 0) { return { exact: false, confidence: 0 }; } results.sort((a, b) => b.confidence - a.confidence); const topConfidence = results[0].confidence; const equalResults = results.filter((r) => r.confidence === topConfidence); if (equalResults.length > 1) { return { exact: false, confidence: 0, candidates: equalResults.map((r) => ({ path: r.path, confidence: r.confidence, matchType: "fuzzy" })) }; } return results[0]; } /** * Find matching entries for a path segment */ async findSegmentMatches(segment, entries, basePath, config) { const matches = []; for (const entry of entries) { const entryPath = path2.join(basePath, entry); const stats = await this.fileSystem.stat(entryPath); const entryParsed = path2.parse(entry); const segmentParsed = path2.parse(segment); const entryName = stats.isFile() ? entryParsed.name : entry; const segmentName = segmentParsed.name; const entryExt = entryParsed.ext; const segmentExt = segmentParsed.ext; if (segmentExt && stats.isFile() && entryExt.toLowerCase() !== segmentExt.toLowerCase()) { continue; } if (entry === segment || stats.isFile() && entryName === segmentName) { matches.push({ path: entryPath, confidence: 1, matchType: "exact" }); continue; } if (config.caseInsensitive) { if (entry.toLowerCase() === segment.toLowerCase() || stats.isFile() && entryName.toLowerCase() === segmentName.toLowerCase()) { matches.push({ path: entryPath, confidence: 0.95, matchType: "case" }); continue; } } if (config.normalizeWhitespace) { const normalizedEntry = this.normalizeWhitespace(entryName); const normalizedSegment = this.normalizeWhitespace(segmentName); if (normalizedEntry === normalizedSegment) { const confidence = this.calculateWhitespaceConfidence(entryName, segmentName); matches.push({ path: entryPath, confidence, matchType: "whitespace" }); continue; } if (config.caseInsensitive && normalizedEntry.toLowerCase() === normalizedSegment.toLowerCase()) { const confidence = this.calculateWhitespaceConfidence(entryName, segmentName) * 0.95; matches.push({ path: entryPath, confidence, matchType: "fuzzy" }); } } } return matches; } /** * Normalize whitespace in a string (spaces, dashes, underscores) */ normalizeWhitespace(str) { return str.replace(/[\s\-_]+/g, "-"); } /** * Calculate confidence based on whitespace transformation */ calculateWhitespaceConfidence(original, target) { const originalChars = this.getWhitespaceChars(original); const targetChars = this.getWhitespaceChars(target); if (originalChars === targetChars) return 0.9; const charPriority = { "-": 3, "_": 2, " ": 1, "mixed": 0 }; const originalPriority = charPriority[originalChars] || 0; const targetPriority = charPriority[targetChars] || 0; const priorityDiff = Math.abs(originalPriority - targetPriority); return 0.9 - priorityDiff * 0.1; } /** * Get the predominant whitespace character type */ getWhitespaceChars(str) { const dashes = (str.match(/-/g) || []).length; const underscores = (str.match(/_/g) || []).length; const spaces = (str.match(/ /g) || []).length; if (dashes > 0 && underscores === 0 && spaces === 0) return "-"; if (underscores > 0 && dashes === 0 && spaces === 0) return "_"; if (spaces > 0 && dashes === 0 && underscores === 0) return " "; return "mixed"; } /** * Generate suggestions for a failed match */ generateSuggestions(segment, entries, config) { const suggestions = []; if (process.env.DEBUG_FUZZY) { console.log(`Generating suggestions for '${segment}' from entries:`, entries); } for (const entry of entries) { const score = this.calculateSimilarity(segment, entry, config); if (process.env.DEBUG_FUZZY) { console.log(` ${entry}: score=${score}, threshold=${config.suggestionThreshold}`); } if (score >= config.suggestionThreshold) { suggestions.push({ entry, score }); } } return suggestions.sort((a, b) => b.score - a.score).map((s) => s.entry); } /** * Calculate similarity score between two strings */ calculateSimilarity(str1, str2, config) { let s1 = str1; let s2 = str2; if (config.normalizeWhitespace) { s1 = this.normalizeWhitespace(s1); s2 = this.normalizeWhitespace(s2); } if (config.caseInsensitive) { s1 = s1.toLowerCase(); s2 = s2.toLowerCase(); } const maxLen = Math.max(s1.length, s2.length); if (maxLen === 0) return 1; const distance = this.levenshteinDistance(s1, s2); return 1 - distance / maxLen; } /** * Calculate Levenshtein distance between two strings */ levenshteinDistance(str1, str2) { const m = str1.length; const n = str2.length; const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0)); for (let i = 0; i <= m; i++) dp[i][0] = i; for (let j = 0; j <= n; j++) dp[0][j] = j; for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (str1[i - 1] === str2[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + Math.min( dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1] // substitution ); } } } return dp[m][n]; } /** * Get cached directory entries or read from filesystem */ async getCachedDirectoryEntries(dirPath) { const now = Date.now(); const cached = this.directoryCache.get(dirPath); if (cached && now - cached.timestamp < this.CACHE_TTL) { return cached.entries; } try { const entries = await this.fileSystem.readdir(dirPath); this.directoryCache.set(dirPath, { entries, timestamp: now }); return entries; } catch (error) { return null; } } }; __name(_PathMatcher, "PathMatcher"); __publicField(_PathMatcher, "DEFAULT_CONFIG", { enabled: true, caseInsensitive: true, normalizeWhitespace: true, suggestionThreshold: 0.7 }); var PathMatcher = _PathMatcher; // core/resolvers/LocalResolver.ts var _LocalResolver = class _LocalResolver { constructor(fileSystem) { __publicField(this, "fileSystem"); __publicField(this, "name", "LOCAL"); __publicField(this, "description", "Resolves modules from local filesystem paths"); __publicField(this, "type", "io"); __publicField(this, "capabilities", { io: { read: true, write: false, list: true }, contexts: { import: true, path: true, output: false }, supportedContentTypes: [ "module", "data", "text" ], defaultContentType: "text", priority: 20, cache: { strategy: "none" } // Local files don't need caching }); __publicField(this, "pathMatcher"); this.fileSystem = fileSystem; this.pathMatcher = new PathMatcher(fileSystem); } /** * Check if this resolver can handle the reference * Always returns true since prefix matching is done by ResolverManager */ canResolve(ref, config) { return !!config?.basePath || path2.isAbsolute(ref); } /** * Resolve a reference to local file content */ async resolve(ref, config) { if (!config?.basePath && !path2.isAbsolute(ref)) { throw new ResolverError("LocalResolver requires basePath in configuration for relative paths", { resolverName: "LocalResolver", operation: "resolve" }); } const relativePath = this.extractRelativePath(ref, config); if (config?.basePath) { const mldMdPath = path2.join(config.basePath, relativePath + ".mld.md"); if (await this.fileSystem.exists(mldMdPath)) { const content = await this.fileSystem.readFile(mldMdPath, "utf8"); const metadata = { source: `file://${mldMdPath}`, mimeType: "text/x-mlld-module", size: Buffer.byteLength(content, "utf8"), timestamp: /* @__PURE__ */ new Date(), taint: [] }; return { content, contentType: "module", mx: metadata, metadata }; } } if (relativePath.includes("..") || path2.isAbsolute(relativePath)) { try { const securePath = await this.resolveFullPath(relativePath, config); } catch (error) { throw error; } } const fuzzyConfig = config?.fuzzyMatch !== void 0 ? config.fuzzyMatch : true; const fuzzyEnabled = typeof fuzzyConfig === "boolean" ? fuzzyConfig : fuzzyConfig.enabled !== false; let fullPath; if (fuzzyEnabled && config?.basePath) { const matchResult = await this.pathMatcher.findMatch(relativePath, config.basePath, typeof fuzzyConfig === "object" ? fuzzyConfig : void 0, config.maxDepth); if (matchResult.candidates && matchResult.candidates.length > 1) { const candidatePaths = matchResult.candidates.map((c) => ` - ${path2.relative(config.basePath, c.path)} (${c.matchType} match)`).join("\n"); throw new ResolverError(`Ambiguous path '${relativePath}' matches multiple files: ${candidatePaths} Please use a more specific path.`, { resolverName: "LocalResolver", reference: ref, operation: "resolve" }); } if (matchResult.path) { fullPath = matchResult.path; } else { fullPath = await this.resolveFullPath(relativePath, config); if (!path2.extname(fullPath)) { const existsAsIs = await this.fileSystem.exists(fullPath); if (!existsAsIs) { const extensions = [ ".mld.md", ".mld", ".md", ".mlld.md" ]; let foundWithExtension = false; for (const ext of extensions) { const pathWithExt = relativePath + ext; const extMatchResult = await this.pathMatcher.findMatch(pathWithExt, config.basePath, typeof fuzzyConfig === "object" ? fuzzyConfig : void 0, config.maxDepth); if (extMatchResult.path) { fullPath = extMatchResult.path; foundWithExtension = true; break; } } if (!foundWithExtension && matchResult.suggestions && matchResult.suggestions.length > 0) { const suggestions = matchResult.suggestions.slice(0, 3).map((s) => ` - ${s}`).join("\n"); throw new MlldFileNotFoundError(`File not found: ${relativePath} Did you mean: ${suggestions}`, { details: { filePath: relativePath, operation: "resolve" } }); } } } } } else { fullPath = await this.resolveFullPath(relativePath, config); if (!path2.extname(fullPath)) { const existsAsIs = await this.fileSystem.exists(fullPath); if (!existsAsIs) { const withMldMd = fullPath + ".mld.md"; if (await this.fileSystem.exists(withMldMd)) { fullPath = withMldMd; } else { const withMld = fullPath + ".mld"; if (await this.fileSystem.exists(withMld)) { fullPath = withMld; } else { const withMd = fullPath + ".md"; if (await this.fileSystem.exists(withMd)) { fullPath = withMd; } else { const withLegacy = fullPath + ".mlld.md"; if (await this.fileSystem.exists(withLegacy)) { fullPath = withLegacy; } } } } } } } if (config.allowedExtensions) { const ext = path2.extname(fullPath).toLowerCase(); if (!config.allowedExtensions.includes(ext)) { throw new ResolverError(`File extension '${ext}' not allowed. Allowed: ${config.allowedExtensions.join(", ")}`, { resolverName: "LocalResolver", reference: ref, operation: "resolve" }); } } try { const content = await this.fileSystem.readFile(fullPath); const stats = await this.fileSystem.stat(fullPath); const contentType = await this.detectContentType(fullPath, content); const metadata = { source: `file://${fullPath}`, timestamp: /* @__PURE__ */ new Date(), taint: [], size: content.length, mimeType: this.getMimeType(fullPath) }; return { content, contentType, mx: metadata, metadata }; } catch (error) { if (error.code === "ENOENT" || error.message?.includes("File not found")) { throw new MlldFileNotFoundError(`File not found: ${relativePath}`, { details: { filePath: relativePath, operation: "resolve" } }); } throw ResolverError.resolutionFailed("LocalResolver", ref, error); } } /** * Write content to a local file */ async write(ref, content, config) { if (!config?.basePath) { throw new ResolverError("LocalResolver requires basePath in configuration", { resolverName: "LocalResolver", operation: "write" }); } if (config.readonly) { throw new ResolverError("Cannot write: LocalResolver is configured as read-only", { resolverName: "LocalResolver", operation: "write", reference: ref }); } const relativePath = this.extractRelativePath(ref, config); const fullPath = await this.resolveFullPath(relativePath, config); if (config.allowedExtensions) { const ext = path2.extname(fullPath).toLowerCase(); if (!config.allowedExtensions.includes(ext)) { throw new ResolverError(`File extension '${ext}' not allowed. Allowed: ${config.allowedExtensions.join(", ")}`, { resolverName: "LocalResolver", reference: ref, operation: "resolve" }); } } try { const dir = path2.dirname(fullPath); await this.fileSystem.mkdir(dir, { recursive: true }); await this.fileSystem.writeFile(fullPath, content); } catch (error) { throw new ResolverError(`Failed to write file: ${error.message}`, { resolverName: "LocalResolver", reference: ref, operation: "write", originalError: error }); } } /** * List files under a prefix */ async list(prefix, config) { if (!config?.basePath) { return []; } const relativePath = this.extractRelativePath(prefix, config); const fuzzyConfig = config?.fuzzyMatch !== void 0 ? config.fuzzyMatch : true; const fuzzyEnabled = typeof fuzzyConfig === "boolean" ? fuzzyConfig : fuzzyConfig.enabled !== false; let fullPath; if (fuzzyEnabled) { const matchResult = await this.pathMatcher.findMatch(relativePath, config.basePath, typeof fuzzyConfig === "object" ? fuzzyConfig : void 0, config.maxDepth); if (!matchResult.path) { return []; } fullPath = matchResult.path; } else { fullPath = await this.resolveFullPath(relativePath, config); } try { const stats = await this.fileSystem.stat(fullPath); if (!stats.isDirectory()) { return []; } const entries = await this.fileSystem.readdir(fullPath); const results = []; for (const entryName of entries) { const entryPath = path2.join(fullPath, entryName); try { const entryStats = await this.fileSystem.stat(entryPath); if (config.allowedExtensions && entryStats.isFile()) { const ext = path2.extname(entryName).toLowerCase(); if (!config.allowedExtensions.includes(ext)) { continue; } } results.push({ path: path2.join(prefix, entryName), type: entryStats.isDirectory() ? "directory" : "file", size: 0, lastModified: /* @__PURE__ */ new Date() // Use current time }); } catch (error) { continue; } } return results; } catch (error) { if (error.code === "ENOENT" || error.message?.includes("Path not found")) { return []; } throw error; } } /** * Validate configuration */ validateConfig(config) { const errors = []; const cfg = config ?? {}; if (!cfg.basePath) { errors.push("basePath is required"); } else if (typeof cfg.basePath !== "string") { errors.push("basePath must be a string"); } if (cfg.readonly !== void 0 && typeof cfg.readonly !== "boolean") { errors.push("readonly must be a boolean"); } if (cfg.allowedExtensions !== void 0) { if (!Array.isArray(cfg.allowedExtensions)) { errors.push("allowedExtensions must be an array"); } else if (!cfg.allowedExtensions.every((ext) => typeof ext === "string")) { errors.push("allowedExtensions must contain only strings"); } } if (cfg.followSymlinks !== void 0 && typeof cfg.followSymlinks !== "boolean") { errors.push("followSymlinks must be a boolean"); } if (cfg.maxDepth !== void 0) { if (typeof cfg.maxDepth !== "number" || cfg.maxDepth < 0) { errors.push("maxDepth must be a non-negative number"); } } return errors; } /** * Check access permissions */ async checkAccess(ref, operation, config) { if (!config?.basePath) { return false; } if (operation === "write" && config.readonly) { return false; } try { const relativePath = this.extractRelativePath(ref, config); const mldMdPath = path2.join(config.basePath, relativePath + ".mld.md"); if (await this.fileSystem.exists(mldMdPath)) { return true; } const fuzzyConfig = config?.fuzzyMatch !== void 0 ? config.fuzzyMatch : true; const fuzzyEnabled = typeof fuzzyConfig === "boolean" ? fuzzyConfig : fuzzyConfig.enabled !== false; if (fuzzyEnabled && operation === "read") { const matchResult = await this.pathMatcher.findMatch(relativePath, config.basePath, typeof fuzzyConfig === "object" ? fuzzyConfig : void 0, config.maxDepth); if (matchResult.path) { return true; } const extensions = [ ".mld.md", ".mld", ".md", ".mlld.md" ]; for (const ext of extensions) { const pathWithExt = relativePath + ext; const extMatchResult = await this.pathMatcher.findMatch(pathWithExt, config.basePath, typeof fuzzyConfig === "object" ? fuzzyConfig : void 0, config.maxDepth); if (extMatchResult.path) { return true; } } return false; } else { const fullPath = await this.resolveFullPath(relativePath, config); if (operation === "read" && !path2.extname(fullPath)) { const existsAsIs = await this.fileSystem.exists(fullPath); if (!existsAsIs) { const withMldMd = fullPath + ".mld.md"; if (await this.fileSystem.exists(withMldMd)) { return true; } const withMld = fullPath + ".mld"; if (await this.fileSystem.exists(withMld)) { return true; } const withMd = fullPath + ".md"; if (await this.fileSystem.exists(withMd)) { return true; } const withLegacy = fullPath + ".mlld.md"; if (await this.fileSystem.exists(withLegacy)) { return true; } } } if (operation === "write") { const dir = path2.dirname(fullPath); const dirExists = await this.fileSystem.exists(dir); return dirExists; } else { return await this.fileSystem.exists(fullPath); } } } catch (error) { return false; } } /** * Extract relative path from reference * Assumes the ResolverManager has already matched and removed the prefix */ extractRelativePath(ref, config) { const prefixMatch = ref.match(/^@[^/]+\/(.*)/); if (prefixMatch) { return prefixMatch[1]; } return ref; } /** * Resolve and validate the full filesystem path */ async resolveFullPath(relativePath, config) { const basePath = config?.basePath || "/"; const normalizedBase = path2.resolve(basePath); if (path2.isAbsolute(relativePath)) { const normalizedPath = path2.resolve(relativePath); if (normalizedPath.startsWith(normalizedBase)) { return normalizedPath; } throw new ResolverError("Path traversal detected: absolute path is outside base directory", { resolverName: "LocalResolver", reference: relativePath, operation: "resolve" }); } const cleanPath = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath; const fullPath = path2.resolve(config.basePath, cleanPath); const normalizedFull = path2.resolve(fullPath); if (!normalizedFull.startsWith(normalizedBase)) { throw new ResolverError("Path traversal detected: resolved path is outside base directory", { resolverName: "LocalResolver", reference: relativePath, operation: "resolve" }); } if (config.maxDepth !== void 0) { const relativeParts = path2.relative(normalizedBase, normalizedFull).split(path2.sep).filter((p) => p && p !== "."); const depth = Math.max(0, relativeParts.length - 1); if (depth >= config.maxDepth) { throw new ResolverError(`Path exceeds maximum depth of ${config.maxDepth}`, { resolverName: "LocalResolver", reference: relativePath, operation: "resolve" }); } } return normalizedFull; } /** * Detect content type based on file extension and content */ async detectContentType(filePath, content) { const mlldExtensions = [ ".mld.md", ".mld", ".md", ".mlld.md", ".mlld" ]; const hasMLLDExtension = mlldExtensions.some((ext) => filePath.endsWith(ext)); if (hasMLLDExtension || !filePath.endsWith(".json")) { try { const { parse: parse2 } = await import('./parser-6HNWFG6W.mjs'); const { inferMlldMode } = await import('./mode-F33H23O4.mjs'); const mode = inferMlldMode(filePath); const result = await parse2(content, { mode }); if (result.success && this.hasModuleExports(result.ast)) { return "module"; } } catch { } } if (filePath.endsWith(".json")) { return "data"; } try { JSON.parse(content); return "data"; } catch { } return "text"; } /** * Check if AST has module exports */ hasModuleExports(ast) { if (!ast || !Array.isArray(ast)) return false; return ast.some((node) => node && node.type === "Directive" && [ "var", "exe", "path" ].includes(node.kind)); } /** * Get MIME type based on file extension */ getMimeType(filePath) { const ext = path2.extname(filePath).toLowerCase(); const mimeTypes = { ".mld": "text/x-mlld", ".mlld": "text/x-mlld", ".md": "text/markdown", ".txt": "text/plain", ".json": "application/json", ".yaml": "text/yaml", ".yml": "text/yaml", ".js": "text/javascript", ".ts": "text/typescript", ".py": "text/x-python", ".sh": "text/x-shellscript", ".xml": "text/xml", ".html": "text/html", ".css": "text/css" }; return mimeTypes[ext] || "text/plain"; } }; __name(_LocalResolver, "LocalResolver"); var LocalResolver = _LocalResolver; export { LocalResolver, PathMatcher }; //# sourceMappingURL=chunk-CIWUSUU2.mjs.map //# sourceMappingURL=chunk-CIWUSUU2.mjs.map