UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

163 lines (162 loc) 5.03 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); function levenshtein(a, b) { const m = a.length; const n = b.length; const dp = Array.from( { length: m + 1 }, () => new 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++) { const cost = a[i - 1] === b[j - 1] ? 0 : 1; dp[i][j] = Math.min( dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost ); } } return dp[m][n]; } function normalizeWhitespace(text) { return text.split("\n").map((line) => line.trim().replace(/\s+/g, " ")).join("\n"); } function stripIndentation(text) { return text.split("\n").map((line) => line.trimStart()).join("\n"); } function fuzzyMatch(content, oldString, threshold = 0.85) { if (!oldString) return null; const exactIdx = content.indexOf(oldString); if (exactIdx !== -1) { return { found: true, startIndex: exactIdx, endIndex: exactIdx + oldString.length, confidence: 1, matchedText: oldString, method: "exact" }; } const normContent = normalizeWhitespace(content); const normOld = normalizeWhitespace(oldString); const normIdx = normContent.indexOf(normOld); if (normIdx !== -1) { const match = mapNormalizedToOriginal( content, normContent, normOld, normIdx ); if (match) { return { found: true, startIndex: match.start, endIndex: match.end, confidence: 0.95, matchedText: content.slice(match.start, match.end), method: "whitespace-normalized" }; } } const stripContent = stripIndentation(content); const stripOld = stripIndentation(oldString); const stripIdx = stripContent.indexOf(stripOld); if (stripIdx !== -1) { const match = mapStrippedToOriginal( content, stripContent, stripOld, stripIdx ); if (match) { return { found: true, startIndex: match.start, endIndex: match.end, confidence: 0.9, matchedText: content.slice(match.start, match.end), method: "indentation-insensitive" }; } } const contentLines = content.split("\n"); const oldLines = oldString.split("\n"); const windowSize = oldLines.length; if (windowSize === 0 || contentLines.length < windowSize) return null; let bestScore = 0; let bestStart = -1; let bestEnd = -1; for (let i = 0; i <= contentLines.length - windowSize; i++) { const windowText = contentLines.slice(i, i + windowSize).join("\n"); const maxLen = Math.max(windowText.length, oldString.length); if (maxLen === 0) continue; const dist = levenshtein(windowText, oldString); const similarity = 1 - dist / maxLen; if (similarity > bestScore) { bestScore = similarity; bestStart = i; bestEnd = i + windowSize; } } if (bestScore >= threshold && bestStart >= 0) { let startCharIdx = 0; for (let i = 0; i < bestStart; i++) { startCharIdx += contentLines[i].length + 1; } let endCharIdx = startCharIdx; for (let i = bestStart; i < bestEnd; i++) { endCharIdx += contentLines[i].length + (i < bestEnd - 1 ? 1 : 0); } return { found: true, startIndex: startCharIdx, endIndex: endCharIdx, confidence: Math.round(bestScore * 100) / 100, matchedText: contentLines.slice(bestStart, bestEnd).join("\n"), method: "line-fuzzy" }; } return null; } function fuzzyEdit(content, oldString, newString, threshold = 0.85) { const match = fuzzyMatch(content, oldString, threshold); if (!match) return null; const result = content.slice(0, match.startIndex) + newString + content.slice(match.endIndex); return { content: result, match }; } function mapNormalizedToOriginal(original, _normalized, normNeedle, normIdx) { const normLines = _normalized.split("\n"); const origLines = original.split("\n"); let charCount = 0; let startLine = 0; for (let i = 0; i < normLines.length; i++) { if (charCount + normLines[i].length >= normIdx) { startLine = i; break; } charCount += normLines[i].length + 1; } const needleLineCount = normNeedle.split("\n").length; const endLine = startLine + needleLineCount; let startChar = 0; for (let i = 0; i < startLine; i++) { startChar += origLines[i].length + 1; } let endChar = startChar; for (let i = startLine; i < endLine && i < origLines.length; i++) { endChar += origLines[i].length + (i < endLine - 1 ? 1 : 0); } return { start: startChar, end: endChar }; } function mapStrippedToOriginal(original, stripped, stripNeedle, stripIdx) { return mapNormalizedToOriginal(original, stripped, stripNeedle, stripIdx); } export { fuzzyEdit, fuzzyMatch };