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

370 lines (369 loc) 12.1 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { QueryTemplates, InlineModifierParser } from "./query-templates.js"; var FrameType = /* @__PURE__ */ ((FrameType2) => { FrameType2["TASK"] = "task"; FrameType2["DEBUG"] = "debug"; FrameType2["FEATURE"] = "feature"; FrameType2["ARCHITECTURE"] = "architecture"; FrameType2["BUG"] = "bug"; FrameType2["REFACTOR"] = "refactor"; return FrameType2; })(FrameType || {}); var FrameStatus = /* @__PURE__ */ ((FrameStatus2) => { FrameStatus2["OPEN"] = "open"; FrameStatus2["CLOSED"] = "closed"; FrameStatus2["STALLED"] = "stalled"; return FrameStatus2; })(FrameStatus || {}); class QueryParser { templates = new QueryTemplates(); inlineParser = new InlineModifierParser(); shortcuts = /* @__PURE__ */ new Map([ ["today", { time: { last: "24h" } }], [ "yesterday", { time: { last: "48h", since: new Date(Date.now() - 48 * 36e5) } } ], ["this week", { time: { last: "7d" } }], ["last week", { time: { last: "1w" } }], ["this month", { time: { last: "30d" } }], ["bugs", { frame: { type: ["bug" /* BUG */, "debug" /* DEBUG */] } }], ["features", { frame: { type: ["feature" /* FEATURE */] } }], ["architecture", { frame: { type: ["architecture" /* ARCHITECTURE */] } }], ["refactoring", { frame: { type: ["refactor" /* REFACTOR */] } }], ["critical", { frame: { score: { min: 0.8 } } }], ["recent", { time: { last: "4h" } }], ["stalled", { frame: { status: ["stalled" /* STALLED */] } }], ["my work", { people: { owner: ["$current_user"] } }], ["team work", { people: { team: "$current_team" } }] ]); /** * Parse natural language query into structured format */ parseNaturalLanguage(query) { const templateResult = this.templates.matchTemplate(query); if (templateResult) { const structured = templateResult; if (!structured.output) { structured.output = { limit: 50, sort: "time", format: "summary" }; } return this.parseStructured(structured); } const { cleanQuery, modifiers } = this.inlineParser.parse(query); const result = {}; const lowerQuery = cleanQuery.toLowerCase(); this.parseTimePatterns(lowerQuery, result); this.parseTopicPatterns(lowerQuery, result); this.parsePeoplePatterns(lowerQuery, result); this.expandShortcuts(lowerQuery, result); const merged = this.mergeQueries(result, modifiers); if (!merged.output) { merged.output = { limit: 50, sort: "time", format: "summary" }; } else { if (!merged.output.limit) merged.output.limit = 50; if (!merged.output.sort) merged.output.sort = "time"; if (!merged.output.format) merged.output.format = "summary"; } return merged; } /** * Parse structured query with validation */ parseStructured(query) { if (query.frame?.score) { if (query.frame.score.min !== void 0) { query.frame.score.min = Math.max(0, Math.min(1, query.frame.score.min)); } if (query.frame.score.max !== void 0) { query.frame.score.max = Math.max(0, Math.min(1, query.frame.score.max)); } } if (!query.output) { query.output = { limit: 50, sort: "time", format: "full" }; } return query; } /** * Parse hybrid query (natural language with structured modifiers) */ parseHybrid(naturalQuery, modifiers) { const nlQuery = this.parseNaturalLanguage(naturalQuery); return this.mergeQueries(nlQuery, modifiers || {}); } parseTimePatterns(query, result) { const lastPattern = /last\s+(\d+)?\s*(day|hour|week|month)s?/i; const match = query.match(lastPattern); if (match) { const quantity = match[1] ? parseInt(match[1]) : 1; const unit = match[2].toLowerCase(); const unitMap = { hour: "h", day: "d", week: "w", month: "m" }; result.time = { last: `${quantity}${unitMap[unit]}` }; } for (const [shortcut, value] of this.shortcuts) { if (query.includes(shortcut) && value.time) { result.time = { ...result.time, ...value.time }; } } const datePattern = /(\d{4}-\d{2}-\d{2})|((jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+\d{1,2})/i; const dateMatch = query.match(datePattern); if (dateMatch) { try { const date = new Date(dateMatch[0]); if (!isNaN(date.getTime())) { result.time = { ...result.time, specific: date }; } } catch { } } } parseTopicPatterns(query, result) { const topics = [ "auth", "authentication", "login", "oauth", "database", "migration", "cache", "api", "bug", "bugs", "error", "fix", "feature", "features", "test", "security", "performance" ]; const foundTopics = []; for (const topic of topics) { const regex = new RegExp(`\\b${topic}\\b`, "i"); if (regex.test(query)) { const normalized = topic === "bugs" ? "bug" : topic === "features" ? "feature" : topic; if (!foundTopics.includes(normalized)) { foundTopics.push(normalized); } } } if (foundTopics.length > 0) { result.content = { ...result.content, topic: foundTopics }; } const filePattern = /(\w+\.\w+)|(\*\.\w+)/g; const files = query.match(filePattern); if (files) { result.content = { ...result.content, files }; } } parsePeoplePatterns(query, result) { const mentionPattern = /@(\w+)/g; const mentions = [...query.matchAll(mentionPattern)].map((m) => m[1]); if (mentions.length > 0) { result.people = { owner: mentions }; } const possessivePattern = /(\w+)'s\s+(work|changes|commits|frames)/i; const possMatch = query.match(possessivePattern); if (possMatch) { const person = possMatch[1].toLowerCase(); if (!result.people) result.people = {}; result.people = { ...result.people, owner: [person] }; } if (/\bteam\b/.test(query)) { if (!result.people) result.people = {}; result.people = { ...result.people, team: "$current_team" }; } } expandShortcuts(query, result) { if (query.includes("critical")) { result.frame = { ...result.frame, score: { min: 0.8 } }; } else if (query.includes("high")) { result.frame = { ...result.frame, score: { min: 0.7 } }; } if (query.includes("low priority")) { result.frame = { ...result.frame, score: { max: 0.3 } }; } if (query.includes("open") || query.includes("active")) { result.frame = { ...result.frame, status: ["open" /* OPEN */] }; } if (query.includes("closed") || query.includes("done")) { result.frame = { ...result.frame, status: ["closed" /* CLOSED */] }; } } mergeQueries(base, overlay) { const merged = {}; if (base.time || overlay.time) { merged.time = { ...base.time, ...overlay.time }; } if (base.content || overlay.content) { merged.content = { ...base.content, ...overlay.content }; } if (base.frame || overlay.frame) { merged.frame = { ...base.frame, ...overlay.frame }; } if (base.people || overlay.people) { merged.people = { ...base.people, ...overlay.people }; } if (base.output || overlay.output) { merged.output = { ...base.output, ...overlay.output }; } return merged; } /** * Expand query with synonyms and related terms */ expandQuery(query) { const synonyms = { auth: ["authentication", "oauth", "login", "session", "jwt"], authentication: ["auth", "oauth", "login", "session", "jwt"], bug: ["error", "issue", "problem", "fix", "defect"], database: ["db", "sql", "postgres", "migration", "schema"], test: ["testing", "spec", "unit", "integration", "e2e"] }; if (query.content?.topic) { const expandedTopics = new Set(query.content.topic); for (const topic of query.content.topic) { const syns = synonyms[topic.toLowerCase()]; if (syns) { syns.forEach((s) => expandedTopics.add(s)); } } query.content.topic = Array.from(expandedTopics); } return query; } /** * Main parse method that returns a complete QueryResponse */ parse(query) { const original = typeof query === "string" ? query : JSON.stringify(query); const interpreted = typeof query === "string" ? this.parseNaturalLanguage(query) : this.parseStructured(JSON.parse(JSON.stringify(query))); const validationErrors = this.validateQuery(interpreted); const expanded = this.expandQuery(JSON.parse(JSON.stringify(interpreted))); const suggestions = this.generateSuggestions(interpreted, validationErrors); return { original, interpreted, expanded, suggestions, validationErrors: validationErrors.length > 0 ? validationErrors : void 0 }; } /** * Validate query for errors and inconsistencies */ validateQuery(query) { const errors = []; if (query.time) { if (query.time.since && query.time.until) { if (query.time.since > query.time.until) { errors.push('Time filter: "since" date is after "until" date'); } } if (query.time.between) { if (query.time.between[0] > query.time.between[1]) { errors.push('Time filter: Invalid date range in "between"'); } } } if (query.frame?.score) { if (query.frame.score.min !== void 0 && query.frame.score.max !== void 0) { if (query.frame.score.min > query.frame.score.max) { errors.push( "Frame filter: Minimum score is greater than maximum score" ); } } } if (query.frame?.depth) { if (query.frame.depth.min !== void 0 && query.frame.depth.max !== void 0) { if (query.frame.depth.min > query.frame.depth.max) { errors.push( "Frame filter: Minimum depth is greater than maximum depth" ); } } } if (query.output?.limit !== void 0) { if (query.output.limit < 1 || query.output.limit > 1e3) { errors.push("Output limit must be between 1 and 1000"); } } return errors; } /** * Generate query suggestions based on the interpreted query */ generateSuggestions(query, errors) { const suggestions = []; if (!query.time || !query.time.last && !query.time.since && !query.time.between && !query.time.specific) { suggestions.push('Try adding a time filter like "last 24h" or "today"'); } if (!query.content?.topic && !query.frame?.type && !query.people && !query.content?.keywords) { suggestions.push( "Consider filtering by topic, frame type, or people to narrow results" ); } if (query.frame?.type?.includes("bug" /* BUG */) && !query.time) { suggestions.push("Add a time filter to focus on recent bugs"); } if (query.frame?.score?.min && query.frame.score.min >= 0.8 && !query.frame?.type) { suggestions.push( "Consider adding frame type filter with high score threshold" ); } if (query.time?.last === "24h") { suggestions.push('You can use "today" as a shortcut for last 24 hours'); } if (query.frame?.type?.includes("bug" /* BUG */) && query.frame?.type?.includes("debug" /* DEBUG */)) { suggestions.push( 'You can use "bugs" as a shortcut for bug and debug frames' ); } if (errors.length > 0) { suggestions.push( "Please correct the validation errors before running the query" ); } return suggestions; } } export { FrameStatus, FrameType, QueryParser };