@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
251 lines (250 loc) • 8.19 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 chalk from "chalk";
import Database from "better-sqlite3";
import * as fs from "fs";
import * as path from "path";
import { RuleEngine } from "../../core/rules/rule-engine.js";
import { filterByScope } from "../../core/rules/built-in-rules.js";
function getDb() {
const smDir = path.join(process.cwd(), ".stackmemory");
if (!fs.existsSync(smDir)) {
fs.mkdirSync(smDir, { recursive: true });
}
return new Database(path.join(smDir, "context.db"));
}
function severityColor(severity) {
switch (severity) {
case "error":
return chalk.red;
case "warn":
return chalk.yellow;
case "info":
return chalk.blue;
default:
return chalk.gray;
}
}
function severityIcon(severity) {
switch (severity) {
case "error":
return "x";
case "warn":
return "!";
case "info":
return "i";
default:
return "-";
}
}
function createRulesCommand() {
const cmd = new Command("rule").description(
"Manage project rules (lint, commit, migration checks)"
);
cmd.command("list").description("List configured rules").option("-t, --trigger <type>", "Filter by trigger type").option("-a, --all", "Include disabled rules").option("--json", "Output as JSON").action((options) => {
const db = getDb();
try {
const engine = new RuleEngine(db);
const rules = engine.listRules({
trigger: options.trigger,
enabled: options.all ? false : void 0
});
if (options.json) {
console.log(JSON.stringify(rules, null, 2));
return;
}
if (rules.length === 0) {
console.log(chalk.gray("No rules found."));
return;
}
console.log(chalk.cyan(`
Rules (${rules.length})
`));
for (const rule of rules) {
const enabled = rule.enabled ? chalk.green("on") : chalk.gray("off");
const sev = severityColor(rule.severity)(rule.severity.toUpperCase());
const builtin = rule.builtin ? chalk.gray(" [built-in]") : "";
console.log(
` ${enabled} ${sev} ${chalk.white(rule.id)}${builtin}`
);
console.log(` ${chalk.gray(rule.description)}`);
console.log(
` trigger: ${rule.trigger_type} scope: ${rule.scope}`
);
console.log();
}
} finally {
db.close();
}
});
cmd.command("check").description("Run rules against files or commit message").option("-t, --trigger <type>", "Trigger type filter", "on-demand").option("-f, --files <glob>", "File glob to check").option("-m, --commit-message <msg>", "Commit message to check").option("--all", "Run all rules regardless of trigger").option("--json", "Output as JSON").action(
(options) => {
const db = getDb();
try {
const engine = new RuleEngine(db);
const projectRoot = process.cwd();
let files = [];
if (options.files) {
files = collectFiles(projectRoot, options.files);
}
const content = /* @__PURE__ */ new Map();
for (const file of files) {
const fullPath = path.isAbsolute(file) ? file : path.join(projectRoot, file);
try {
content.set(file, fs.readFileSync(fullPath, "utf-8"));
} catch {
}
}
const ctx = {
trigger: options.trigger ?? "on-demand",
files,
content,
commitMessage: options.commitMessage ?? "",
projectRoot
};
const result = options.all ? engine.evaluateAll(ctx) : engine.evaluate(ctx);
if (options.json) {
console.log(JSON.stringify(result, null, 2));
process.exitCode = result.passed ? 0 : 1;
return;
}
if (result.passed) {
console.log(chalk.green("\n All rules passed.\n"));
return;
}
console.log(
chalk.red(`
${result.violations.length} violation(s) found
`)
);
for (const v of result.violations) {
const icon = severityIcon(v.severity);
const color = severityColor(v.severity);
const loc = v.file ? `${v.file}${v.line ? `:${v.line}` : ""}` : "";
console.log(` ${color(`[${icon}]`)} ${chalk.white(v.ruleName)}`);
console.log(` ${v.message}`);
if (loc) console.log(` ${chalk.gray(loc)}`);
if (v.suggestion) console.log(` ${chalk.cyan(v.suggestion)}`);
console.log();
}
const errors = result.violations.filter(
(v) => v.severity === "error"
);
if (errors.length > 0) {
process.exitCode = 1;
}
} finally {
db.close();
}
}
);
cmd.command("enable <id>").description("Enable a rule").action((id) => {
const db = getDb();
try {
const engine = new RuleEngine(db);
if (engine.enableRule(id)) {
console.log(chalk.green(`Rule '${id}' enabled.`));
} else {
console.log(chalk.red(`Rule '${id}' not found.`));
process.exitCode = 1;
}
} finally {
db.close();
}
});
cmd.command("disable <id>").description("Disable a rule").action((id) => {
const db = getDb();
try {
const engine = new RuleEngine(db);
if (engine.disableRule(id)) {
console.log(chalk.yellow(`Rule '${id}' disabled.`));
} else {
console.log(chalk.red(`Rule '${id}' not found.`));
process.exitCode = 1;
}
} finally {
db.close();
}
});
cmd.command("seed").description("Re-seed built-in rules (useful after upgrades)").action(() => {
const db = getDb();
try {
const engine = new RuleEngine(db);
const rules = engine.listRules();
const builtins = rules.filter((r) => r.builtin);
console.log(chalk.green(`Seeded ${builtins.length} built-in rules.`));
} finally {
db.close();
}
});
cmd.command("add <id>").description("Add a custom rule (metadata only)").requiredOption("-n, --name <name>", "Rule display name").option("-d, --description <desc>", "Rule description", "").option("-t, --trigger <type>", "Trigger type", "on-demand").option("-s, --severity <level>", "Severity level", "warn").option("--scope <glob>", "File scope glob", "**/*").action(
(id, options) => {
const db = getDb();
try {
const engine = new RuleEngine(db);
engine.getStore().upsert({
id,
name: options.name,
description: options.description,
trigger_type: options.trigger,
severity: options.severity,
scope: options.scope,
enabled: 1,
builtin: 0
});
console.log(chalk.green(`Rule '${id}' added.`));
} finally {
db.close();
}
}
);
return cmd;
}
function collectFiles(root, pattern) {
const results = [];
if (pattern.includes("*")) {
walkDir(root, root, pattern, results);
} else {
const fullPath = path.join(root, pattern);
if (fs.existsSync(fullPath)) {
const stat = fs.statSync(fullPath);
if (stat.isFile()) {
results.push(pattern);
} else if (stat.isDirectory()) {
walkDir(fullPath, root, "**/*", results);
}
}
}
return results;
}
function walkDir(dir, root, pattern, results) {
const SKIP = /* @__PURE__ */ new Set([
"node_modules",
".git",
"dist",
"coverage",
".stackmemory"
]);
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (SKIP.has(entry.name)) continue;
const fullPath = path.join(dir, entry.name);
const relPath = path.relative(root, fullPath);
if (entry.isDirectory()) {
walkDir(fullPath, root, pattern, results);
} else if (entry.isFile()) {
if (filterByScope([relPath], pattern).length > 0) {
results.push(relPath);
}
}
}
} catch {
}
}
export {
createRulesCommand
};