@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
174 lines (173 loc) • 5.99 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 { Command } from "commander";
import Database from "better-sqlite3";
import { join } from "path";
import { existsSync } from "fs";
import { z } from "zod";
const SearchQuerySchema = z.string().min(1, "Search query is required").max(500, "Search query too long (max 500 characters)").transform((val) => {
return val.replace(/[%_\\]/g, "\\$&");
});
const LimitSchema = z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().min(1).max(100).default(20));
function createSearchCommand() {
const search = new Command("search").alias("find").description("Search across tasks and context").argument("<query>", "Search query").option("-t, --tasks", "Search only tasks").option("-c, --context", "Search only context").option("-l, --limit <n>", "Limit results", "20").action(async (rawQuery, options) => {
const projectRoot = process.cwd();
const dbPath = join(projectRoot, ".stackmemory", "context.db");
if (!existsSync(dbPath)) {
console.log(
'\u274C StackMemory not initialized. Run "stackmemory init" first.'
);
return;
}
let query;
let limit;
try {
query = SearchQuerySchema.parse(rawQuery);
limit = LimitSchema.parse(options.limit);
} catch (error) {
if (error instanceof z.ZodError) {
console.error("\u274C Invalid input:", error.errors[0].message);
} else {
console.error("\u274C Invalid input");
}
return;
}
const db = new Database(dbPath);
const searchTasks = !options.context || options.tasks;
const searchContext = !options.tasks || options.context;
console.log(`
\u{1F50D} Searching for "${rawQuery}"...
`);
let totalResults = 0;
if (searchTasks) {
try {
const tasks = db.prepare(
`
SELECT id, title, description, status, priority, created_at
FROM task_cache
WHERE title LIKE ? OR description LIKE ?
ORDER BY created_at DESC
LIMIT ?
`
).all(`%${query}%`, `%${query}%`, limit);
if (tasks.length > 0) {
console.log(`\u{1F4CB} Tasks (${tasks.length})
`);
const priorityIcon = {
urgent: "\u{1F534}",
high: "\u{1F7E0}",
medium: "\u{1F7E1}",
low: "\u{1F7E2}"
};
const statusIcon = {
pending: "\u23F3",
in_progress: "\u{1F504}",
completed: "\u2705",
blocked: "\u{1F6AB}"
};
tasks.forEach((task) => {
const pIcon = priorityIcon[task.priority] || "\u26AA";
const sIcon = statusIcon[task.status] || "\u26AA";
console.log(`${sIcon} ${pIcon} ${task.title}`);
if (task.description) {
const desc = task.description.split("\n")[0];
const matchIdx = desc.toLowerCase().indexOf(query.toLowerCase());
if (matchIdx >= 0) {
const start = Math.max(0, matchIdx - 20);
const end = Math.min(
desc.length,
matchIdx + query.length + 20
);
const snippet = (start > 0 ? "..." : "") + desc.slice(start, end) + (end < desc.length ? "..." : "");
console.log(` ${snippet}`);
}
}
});
console.log("");
totalResults += tasks.length;
}
} catch (error) {
}
}
if (searchContext) {
try {
const contexts = db.prepare(
`
SELECT id, type, name, metadata, created_at
FROM frames
WHERE name LIKE ? OR metadata LIKE ?
ORDER BY created_at DESC
LIMIT ?
`
).all(`%${query}%`, `%${query}%`, limit);
if (contexts.length > 0) {
console.log(`\u{1F4C1} Context Frames (${contexts.length})
`);
const typeIcon = {
session: "\u{1F537}",
task: "\u{1F4CB}",
command: "\u26A1",
file: "\u{1F4C4}",
decision: "\u{1F4A1}"
};
contexts.forEach((ctx) => {
const icon = typeIcon[ctx.type] || "\u{1F4E6}";
const date = new Date(ctx.created_at * 1e3).toLocaleDateString();
console.log(
`${icon} [${ctx.type}] ${ctx.name || ctx.id.slice(0, 10)}`
);
console.log(` Created: ${date}`);
});
console.log("");
totalResults += contexts.length;
}
} catch (error) {
}
}
if (searchContext) {
try {
const events = db.prepare(
`
SELECT id, type, data, timestamp
FROM events
WHERE data LIKE ?
ORDER BY timestamp DESC
LIMIT ?
`
).all(`%${query}%`, limit);
if (events.length > 0) {
console.log(`\u{1F4DD} Events (${events.length})
`);
events.forEach((evt) => {
const date = new Date(evt.timestamp * 1e3).toLocaleDateString();
let data = {};
try {
data = JSON.parse(evt.data);
} catch {
}
const summary = data.content || data.message || data.decision || evt.type;
console.log(`\u26A1 [${evt.type}] ${String(summary).slice(0, 60)}`);
console.log(` ${date}`);
});
console.log("");
totalResults += events.length;
}
} catch (error) {
}
}
db.close();
if (totalResults === 0) {
console.log("No results found.\n");
} else {
console.log(`Found ${totalResults} results.
`);
}
});
return search;
}
export {
createSearchCommand
};
//# sourceMappingURL=search.js.map