@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
179 lines (178 loc) • 5.56 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 extractFilePaths(prompt) {
const paths = /* @__PURE__ */ new Set();
const extensionPattern = /(?:^|\s|["'`])([\w\-./]+\.(?:[tj]sx?|json|gql|ya?ml|md|sh))\b/gi;
let match;
while ((match = extensionPattern.exec(prompt)) !== null) {
paths.add(match[1]);
}
const dirPattern = /(?:^|\s|["'`])((?:src|app|components|screens|hooks|utils|services|navigation|graphql|localization|\.claude|\.github|\.maestro)\/[\w\-./]+)/gi;
while ((match = dirPattern.exec(prompt)) !== null) {
paths.add(match[1]);
}
const quotedPattern = /["'`]([\w\-./]+\/[\w\-./]+)["'`]/g;
while ((match = quotedPattern.exec(prompt)) !== null) {
paths.add(match[1]);
}
return Array.from(paths);
}
function matchesPattern(text, pattern, flags = "i") {
try {
return new RegExp(pattern, flags).test(text);
} catch {
return false;
}
}
function matchesGlob(filePath, globPattern) {
const regexPattern = globPattern.replace(/\./g, "\\.").replace(/\?/g, "<<<QUESTION>>>").replace(/\*\*\//g, "<<<DOUBLESTARSLASH>>>").replace(/\*\*/g, "<<<DOUBLESTAR>>>").replace(/\*/g, "[^/]*").replace(/<<<DOUBLESTARSLASH>>>/g, "(?:.*\\/)?").replace(/<<<DOUBLESTAR>>>/g, ".*").replace(/<<<QUESTION>>>/g, ".");
try {
return new RegExp(`^${regexPattern}$`, "i").test(filePath);
} catch {
return false;
}
}
function matchDirectoryMapping(filePath, mappings) {
for (const [dir, skillName] of Object.entries(mappings)) {
if (filePath === dir || filePath.startsWith(dir + "/")) {
return skillName;
}
}
return void 0;
}
function evaluateSkill(skillName, skill, prompt, promptLower, filePaths, scoring, directoryMappings) {
const { triggers, excludePatterns = [], priority = 5 } = skill;
let score = 0;
const reasons = [];
for (const excludePattern of excludePatterns) {
if (matchesPattern(promptLower, excludePattern)) {
return void 0;
}
}
if (triggers.keywords) {
for (const keyword of triggers.keywords) {
if (promptLower.includes(keyword.toLowerCase())) {
score += scoring.keyword;
reasons.push(`keyword "${keyword}"`);
}
}
}
if (triggers.keywordPatterns) {
for (const pattern of triggers.keywordPatterns) {
if (matchesPattern(promptLower, pattern)) {
score += scoring.keywordPattern;
reasons.push(`pattern /${pattern}/`);
}
}
}
if (triggers.intentPatterns) {
for (const pattern of triggers.intentPatterns) {
if (matchesPattern(promptLower, pattern)) {
score += scoring.intentPattern;
reasons.push("intent detected");
break;
}
}
}
if (triggers.contextPatterns) {
for (const pattern of triggers.contextPatterns) {
if (promptLower.includes(pattern.toLowerCase())) {
score += scoring.contextPattern;
reasons.push(`context "${pattern}"`);
}
}
}
if (triggers.pathPatterns && filePaths.length > 0) {
for (const filePath of filePaths) {
for (const pattern of triggers.pathPatterns) {
if (matchesGlob(filePath, pattern)) {
score += scoring.pathPattern;
reasons.push(`path "${filePath}"`);
break;
}
}
}
}
if (filePaths.length > 0) {
for (const filePath of filePaths) {
const mappedSkill = matchDirectoryMapping(filePath, directoryMappings);
if (mappedSkill === skillName) {
score += scoring.directoryMatch;
reasons.push("directory mapping");
break;
}
}
}
if (triggers.contentPatterns) {
for (const pattern of triggers.contentPatterns) {
if (matchesPattern(prompt, pattern)) {
score += scoring.contentPattern;
reasons.push("code pattern detected");
break;
}
}
}
if (score > 0) {
return {
name: skillName,
score,
reasons: [...new Set(reasons)],
priority
};
}
return void 0;
}
function getRelatedSkills(matches, rules) {
const matchedNames = new Set(matches.map((m) => m.name));
const related = /* @__PURE__ */ new Set();
for (const m of matches) {
const skill = rules[m.name];
if (skill?.relatedSkills) {
for (const relatedName of skill.relatedSkills) {
if (!matchedNames.has(relatedName)) {
related.add(relatedName);
}
}
}
}
return Array.from(related);
}
function formatConfidence(score, minScore) {
if (score >= minScore * 3) return "HIGH";
if (score >= minScore * 2) return "MEDIUM";
return "LOW";
}
function matchPrompt(prompt, rules, config, scoring, directoryMappings = {}) {
const promptLower = prompt.toLowerCase();
const filePaths = extractFilePaths(prompt);
const matches = [];
for (const [name, skill] of Object.entries(rules)) {
const m = evaluateSkill(
name,
skill,
prompt,
promptLower,
filePaths,
scoring,
directoryMappings
);
if (m && m.score >= config.minConfidenceScore) {
matches.push(m);
}
}
matches.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
return b.priority - a.priority;
});
const topMatches = matches.slice(0, config.maxSkillsToShow);
const relatedSkills = getRelatedSkills(topMatches, rules);
return { matches: topMatches, filePaths, relatedSkills };
}
export {
extractFilePaths,
formatConfidence,
matchPrompt,
matchesGlob
};