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