@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
JavaScript
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
};