@shahmilsaari/memory-core 1.0.22 → 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -868
- package/dist/approval-queue-YBYRGBHP.js +7 -0
- package/dist/ast-analyzer-JM4CIOFY.js +44 -0
- package/dist/check-cache-6NWRTZJD.js +52 -0
- package/dist/check-logger-5HYSWA3S.js +21 -0
- package/dist/{chunk-UZDALJVQ.js → chunk-3XTHE74V.js} +2488 -1461
- package/dist/chunk-M7NKSXFS.js +301 -0
- package/dist/chunk-PQBWHAZN.js +156 -0
- package/dist/chunk-W6WEAV3S.js +69 -0
- package/dist/chunk-ZZBQEXEO.js +183 -0
- package/dist/classifier-MZ65R7FK.js +60 -0
- package/dist/cli.js +868 -1585
- package/dist/confidence-gate-ZQDAOS6P.js +64 -0
- package/dist/dashboard/assets/index-CE3AMEOD.js +2 -0
- package/dist/dashboard/assets/{index-DXXHB1Ik.css → index-CNc2vvZF.css} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/{dashboard-server-VOT2ZRVN.js → dashboard-server-EEFNE6NX.js} +161 -14
- package/dist/db-PRDHI2CN.js +29 -0
- package/dist/deepseek-critique-MALVIYGF.js +82 -0
- package/dist/deterministic-validator-PP56B46I.js +18 -0
- package/dist/evidence-HVMSONTT.js +65 -0
- package/dist/graph-TFNTB5OK.js +98 -0
- package/dist/incident-capture-RVPZULS7.js +20 -0
- package/dist/ollama-judge-D2LFK5PB.js +137 -0
- package/dist/rate-limiter-SLIPCXRF.js +41 -0
- package/dist/rules-V3QMN3AR.js +95 -0
- package/dist/watch-errors-B3FA26N4.js +99 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-BRqvIBnm.js +0 -2
package/dist/cli.js
CHANGED
|
@@ -1,1293 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
AGENT_NAMES,
|
|
4
|
+
MEMORY_FILE,
|
|
4
5
|
OUTPUT_FILES,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
checkCi,
|
|
7
|
+
checkCommitMsg,
|
|
8
|
+
checkFile,
|
|
9
|
+
checkStaged,
|
|
8
10
|
detectProject,
|
|
9
|
-
|
|
11
|
+
findSchemaViolations,
|
|
10
12
|
generate,
|
|
11
13
|
getAllowPatterns,
|
|
12
|
-
getChatProviderLabel,
|
|
13
14
|
getDefaultApplicationContainer,
|
|
14
|
-
getPool,
|
|
15
15
|
inferProjectArchitectures,
|
|
16
|
+
installHook,
|
|
16
17
|
listProfiles,
|
|
17
18
|
migrateGraphSnapshots,
|
|
19
|
+
parseMemoryFile,
|
|
18
20
|
probeGraphSnapshotStore,
|
|
19
|
-
|
|
21
|
+
readBypassStats,
|
|
22
|
+
readMemoryFile,
|
|
23
|
+
readMemoryFileFromUrl,
|
|
24
|
+
recordBypass,
|
|
20
25
|
retrieveMemorySelection,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
seeds,
|
|
27
|
+
toPortableMemory,
|
|
28
|
+
uninstallHook,
|
|
29
|
+
writeMemoryFile
|
|
30
|
+
} from "./chunk-3XTHE74V.js";
|
|
31
|
+
import "./chunk-PQBWHAZN.js";
|
|
32
|
+
import {
|
|
33
|
+
closePool,
|
|
34
|
+
getPool,
|
|
35
|
+
runMigrations
|
|
36
|
+
} from "./chunk-M7NKSXFS.js";
|
|
37
|
+
import "./chunk-ZZBQEXEO.js";
|
|
24
38
|
|
|
25
39
|
// src/cli.ts
|
|
26
40
|
import { Command } from "commander";
|
|
27
41
|
import { input, select, confirm } from "@inquirer/prompts";
|
|
28
|
-
import
|
|
42
|
+
import chalk from "chalk";
|
|
29
43
|
import ora from "ora";
|
|
30
|
-
import { readFileSync
|
|
31
|
-
import { join
|
|
44
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, rmSync } from "fs";
|
|
45
|
+
import { join, dirname, resolve } from "path";
|
|
32
46
|
import { homedir } from "os";
|
|
33
|
-
|
|
34
|
-
// src/hook.ts
|
|
35
|
-
import { execSync, spawnSync } from "child_process";
|
|
36
|
-
import { writeFileSync as writeFileSync2, existsSync as existsSync2, unlinkSync, readFileSync as readFileSync2, chmodSync, statSync } from "fs";
|
|
37
|
-
import { join as join2 } from "path";
|
|
38
|
-
import chalk from "chalk";
|
|
39
|
-
|
|
40
|
-
// src/memory-file.ts
|
|
41
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
42
|
-
import { join } from "path";
|
|
43
|
-
var MEMORY_FILE = "memories.json";
|
|
44
|
-
function toPortableMemory(memory) {
|
|
45
|
-
return {
|
|
46
|
-
type: memory.type,
|
|
47
|
-
scope: memory.scope,
|
|
48
|
-
architecture: memory.architecture,
|
|
49
|
-
projectName: memory.project_name,
|
|
50
|
-
title: memory.title,
|
|
51
|
-
content: memory.content,
|
|
52
|
-
reason: memory.reason,
|
|
53
|
-
context: memory.context,
|
|
54
|
-
tags: memory.tags ?? []
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function normalizeStringArray(value) {
|
|
58
|
-
if (!Array.isArray(value)) return void 0;
|
|
59
|
-
const entries = value.filter((entry) => typeof entry === "string" && entry.trim() !== "");
|
|
60
|
-
return entries.length ? entries : void 0;
|
|
61
|
-
}
|
|
62
|
-
function parseContext(value) {
|
|
63
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
64
|
-
const record = value;
|
|
65
|
-
const context = {};
|
|
66
|
-
const appliesTo = normalizeStringArray(record.appliesTo);
|
|
67
|
-
const avoidWhen = normalizeStringArray(record.avoidWhen);
|
|
68
|
-
const examples = normalizeStringArray(record.examples);
|
|
69
|
-
if (appliesTo) context.appliesTo = appliesTo;
|
|
70
|
-
if (avoidWhen) context.avoidWhen = avoidWhen;
|
|
71
|
-
if (examples) context.examples = examples;
|
|
72
|
-
if (typeof record.source === "string" && record.source.trim() !== "") context.source = record.source;
|
|
73
|
-
return Object.keys(context).length ? context : void 0;
|
|
74
|
-
}
|
|
75
|
-
function writeMemoryFile(memories, cwd = process.cwd()) {
|
|
76
|
-
const path = join(cwd, MEMORY_FILE);
|
|
77
|
-
writeFileSync(path, JSON.stringify(memories, null, 2) + "\n", "utf-8");
|
|
78
|
-
return path;
|
|
79
|
-
}
|
|
80
|
-
function readMemoryFile(cwd = process.cwd()) {
|
|
81
|
-
const path = join(cwd, MEMORY_FILE);
|
|
82
|
-
if (!existsSync(path)) {
|
|
83
|
-
throw new Error(`${MEMORY_FILE} not found. Run: memory-core export`);
|
|
84
|
-
}
|
|
85
|
-
return parseMemoryFile(readFileSync(path, "utf-8"));
|
|
86
|
-
}
|
|
87
|
-
async function readMemoryFileFromUrl(url) {
|
|
88
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
89
|
-
if (!res.ok) throw new Error(`Failed to download ${url}: HTTP ${res.status}`);
|
|
90
|
-
return parseMemoryFile(await res.text());
|
|
91
|
-
}
|
|
92
|
-
function parseMemoryFile(raw) {
|
|
93
|
-
const parsed = JSON.parse(raw);
|
|
94
|
-
if (!Array.isArray(parsed)) {
|
|
95
|
-
throw new Error(`${MEMORY_FILE} must be a JSON array`);
|
|
96
|
-
}
|
|
97
|
-
return parsed.map((item, index) => {
|
|
98
|
-
if (!item || typeof item !== "object") {
|
|
99
|
-
throw new Error(`Memory at index ${index} must be an object`);
|
|
100
|
-
}
|
|
101
|
-
const record = item;
|
|
102
|
-
if (typeof record.content !== "string" || record.content.trim() === "") {
|
|
103
|
-
throw new Error(`Memory at index ${index} is missing content`);
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
type: typeof record.type === "string" ? record.type : "rule",
|
|
107
|
-
scope: typeof record.scope === "string" ? record.scope : "project",
|
|
108
|
-
architecture: typeof record.architecture === "string" ? record.architecture : void 0,
|
|
109
|
-
projectName: typeof record.projectName === "string" ? record.projectName : void 0,
|
|
110
|
-
title: typeof record.title === "string" ? record.title : void 0,
|
|
111
|
-
content: record.content,
|
|
112
|
-
reason: typeof record.reason === "string" ? record.reason : void 0,
|
|
113
|
-
context: parseContext(record.context),
|
|
114
|
-
tags: Array.isArray(record.tags) ? record.tags.filter((tag) => typeof tag === "string") : []
|
|
115
|
-
};
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// src/hook.ts
|
|
120
|
-
var reasonMap = new Map(
|
|
121
|
-
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
122
|
-
);
|
|
123
|
-
var HOOK_PATH = join2(".git", "hooks", "pre-commit");
|
|
124
|
-
var HOOK_MARKER = "# archmind-memory-core";
|
|
125
|
-
var COMMIT_MSG_HOOK_PATH = join2(".git", "hooks", "commit-msg");
|
|
126
|
-
var COMMIT_MSG_HOOK_MARKER = "# archmind-memory-core commit-msg";
|
|
127
|
-
var RULE_CACHE_FILE = ".memory-core-rules-cache.json";
|
|
128
|
-
var DB_VERSION_FILE = ".memory-core-db-version";
|
|
129
|
-
var RULE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
130
|
-
function buildHookBody(advisory, fast = false) {
|
|
131
|
-
const suffix = advisory ? " || true" : "";
|
|
132
|
-
const checkArgs = fast ? "check --staged --fast" : "check --staged";
|
|
133
|
-
return `${HOOK_MARKER}${advisory ? " advisory" : ""}
|
|
134
|
-
if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
|
|
135
|
-
exit 0
|
|
136
|
-
fi
|
|
137
|
-
if [ -n "\${SKIP:-}" ] && echo ",$SKIP," | grep -qiE ',(memory-core|archmind),'; then
|
|
138
|
-
exit 0
|
|
139
|
-
fi
|
|
140
|
-
if [ -n "\${SKIP_HOOKS:-}" ]; then
|
|
141
|
-
exit 0
|
|
142
|
-
fi
|
|
143
|
-
if command -v memory-core >/dev/null 2>&1; then
|
|
144
|
-
memory-core ${checkArgs}${suffix}
|
|
145
|
-
elif [ -f "./node_modules/.bin/memory-core" ]; then
|
|
146
|
-
./node_modules/.bin/memory-core ${checkArgs}${suffix}
|
|
147
|
-
elif [ -f "./dist/cli.js" ]; then
|
|
148
|
-
node ./dist/cli.js ${checkArgs}${suffix}
|
|
149
|
-
else
|
|
150
|
-
exit 0
|
|
151
|
-
fi
|
|
152
|
-
`;
|
|
153
|
-
}
|
|
154
|
-
function buildHookScript(advisory, fast = false) {
|
|
155
|
-
return `#!/bin/sh
|
|
156
|
-
|
|
157
|
-
${buildHookBody(advisory, fast)}`;
|
|
158
|
-
}
|
|
159
|
-
function normalizeHookPreamble(content) {
|
|
160
|
-
const lines = content.split("\n");
|
|
161
|
-
const normalized = [];
|
|
162
|
-
let shebangSeen = false;
|
|
163
|
-
for (const line of lines) {
|
|
164
|
-
if (/^\s*#!\/bin\/sh\s*$/.test(line)) {
|
|
165
|
-
if (shebangSeen) continue;
|
|
166
|
-
shebangSeen = true;
|
|
167
|
-
normalized.push("#!/bin/sh");
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
normalized.push(line);
|
|
171
|
-
}
|
|
172
|
-
return normalized.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
173
|
-
}
|
|
174
|
-
function toRuleStatEntry(raw) {
|
|
175
|
-
if (raw === void 0) return { count: 0, falsePositives: 0 };
|
|
176
|
-
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
177
|
-
return raw;
|
|
178
|
-
}
|
|
179
|
-
function readPositiveIntEnv(name, fallback) {
|
|
180
|
-
const raw = Number(process.env[name]);
|
|
181
|
-
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
|
|
182
|
-
}
|
|
183
|
-
function isFastCheck(options) {
|
|
184
|
-
return options.fast === true || process.env.MEMORY_CORE_CHECK_FAST === "1";
|
|
185
|
-
}
|
|
186
|
-
async function withTimeout(promise, timeoutMs, fallback) {
|
|
187
|
-
let timer;
|
|
188
|
-
try {
|
|
189
|
-
return await Promise.race([
|
|
190
|
-
promise,
|
|
191
|
-
new Promise((resolve2) => {
|
|
192
|
-
timer = setTimeout(() => resolve2(fallback), timeoutMs);
|
|
193
|
-
})
|
|
194
|
-
]);
|
|
195
|
-
} finally {
|
|
196
|
-
if (timer) clearTimeout(timer);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
function recordViolations(violations, source = "hook") {
|
|
200
|
-
const statsPath = join2(process.cwd(), ".memory-core-stats.json");
|
|
201
|
-
let stats = { rules: {}, files: {} };
|
|
202
|
-
if (existsSync2(statsPath)) {
|
|
203
|
-
try {
|
|
204
|
-
stats = JSON.parse(readFileSync2(statsPath, "utf-8"));
|
|
205
|
-
} catch {
|
|
206
|
-
stats = { rules: {}, files: {} };
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
stats.rules ??= {};
|
|
210
|
-
stats.files ??= {};
|
|
211
|
-
for (const violation of violations) {
|
|
212
|
-
const existing = toRuleStatEntry(stats.rules[violation.rule]);
|
|
213
|
-
stats.rules[violation.rule] = { count: existing.count + 1, falsePositives: existing.falsePositives };
|
|
214
|
-
if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
|
|
215
|
-
}
|
|
216
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
217
|
-
const recent = violations.map((violation) => ({ ...violation, timestamp, source }));
|
|
218
|
-
stats.recentViolations = [...recent, ...stats.recentViolations ?? []].slice(0, 50);
|
|
219
|
-
writeFileSync2(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
220
|
-
}
|
|
221
|
-
function resetViolationStats(cwd = process.cwd()) {
|
|
222
|
-
const statsPath = join2(cwd, ".memory-core-stats.json");
|
|
223
|
-
if (!existsSync2(statsPath)) return;
|
|
224
|
-
let stats = {};
|
|
225
|
-
try {
|
|
226
|
-
const parsed = JSON.parse(readFileSync2(statsPath, "utf-8"));
|
|
227
|
-
if (parsed && typeof parsed === "object") {
|
|
228
|
-
stats = parsed;
|
|
229
|
-
}
|
|
230
|
-
} catch {
|
|
231
|
-
stats = {};
|
|
232
|
-
}
|
|
233
|
-
stats.rules = {};
|
|
234
|
-
stats.files = {};
|
|
235
|
-
writeFileSync2(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
236
|
-
}
|
|
237
|
-
async function promptToSaveViolations(violations) {
|
|
238
|
-
if (!process.stdin.isTTY || violations.length === 0) return;
|
|
239
|
-
try {
|
|
240
|
-
const app = getDefaultApplicationContainer();
|
|
241
|
-
const { confirm: confirm2, input: input2 } = await import("@inquirer/prompts");
|
|
242
|
-
const save = await confirm2({
|
|
243
|
-
message: "Save a caught violation as a project rule?",
|
|
244
|
-
default: false
|
|
245
|
-
});
|
|
246
|
-
if (!save) return;
|
|
247
|
-
const choices = violations.map((violation, index) => `${index + 1}. ${violation.rule}`);
|
|
248
|
-
const selected = violations.length === 1 ? violations[0] : violations[Number(await input2({ message: `Which violation? ${choices.join(" | ")}`, default: "1" })) - 1] ?? violations[0];
|
|
249
|
-
const reason = await input2({
|
|
250
|
-
message: "Why should this rule exist?",
|
|
251
|
-
default: selected.reason ?? selected.issue ?? ""
|
|
252
|
-
});
|
|
253
|
-
const storedReason = reason.trim() || selected.reason || selected.issue || `Captured from violation: ${selected.rule}`;
|
|
254
|
-
await app.services.memoryEngine.remember({
|
|
255
|
-
type: "rule",
|
|
256
|
-
scope: "project",
|
|
257
|
-
content: selected.rule,
|
|
258
|
-
reason: storedReason,
|
|
259
|
-
tags: ["violation"]
|
|
260
|
-
});
|
|
261
|
-
console.log(chalk.green(" \u2713 Saved as project rule. Run memory-core sync to propagate it.\n"));
|
|
262
|
-
} catch (err) {
|
|
263
|
-
console.log(chalk.yellow(` Could not save violation: ${err.message}
|
|
264
|
-
`));
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
function readRuleCache(cwd) {
|
|
268
|
-
const cachePath = join2(cwd, RULE_CACHE_FILE);
|
|
269
|
-
const configPath = join2(cwd, ".memory-core.json");
|
|
270
|
-
if (!existsSync2(cachePath) || !existsSync2(configPath)) return null;
|
|
271
|
-
try {
|
|
272
|
-
const entry = JSON.parse(readFileSync2(cachePath, "utf-8"));
|
|
273
|
-
const now = Date.now();
|
|
274
|
-
if (now - entry.timestamp > RULE_CACHE_TTL_MS) return null;
|
|
275
|
-
const configMtime = statSync(configPath).mtimeMs;
|
|
276
|
-
if (configMtime !== entry.configMtime) return null;
|
|
277
|
-
const dbVersionPath = join2(cwd, DB_VERSION_FILE);
|
|
278
|
-
const dbVersionMtime = existsSync2(dbVersionPath) ? statSync(dbVersionPath).mtimeMs : 0;
|
|
279
|
-
if (dbVersionMtime !== entry.dbVersionMtime) return null;
|
|
280
|
-
return entry;
|
|
281
|
-
} catch {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
function saveRuleCache(cwd, data) {
|
|
286
|
-
const configPath = join2(cwd, ".memory-core.json");
|
|
287
|
-
try {
|
|
288
|
-
const configMtime = statSync(configPath).mtimeMs;
|
|
289
|
-
const dbVersionPath = join2(cwd, DB_VERSION_FILE);
|
|
290
|
-
const dbVersionMtime = existsSync2(dbVersionPath) ? statSync(dbVersionPath).mtimeMs : 0;
|
|
291
|
-
const entry = {
|
|
292
|
-
timestamp: Date.now(),
|
|
293
|
-
configMtime,
|
|
294
|
-
dbVersionMtime,
|
|
295
|
-
...data
|
|
296
|
-
};
|
|
297
|
-
writeFileSync2(join2(cwd, RULE_CACHE_FILE), JSON.stringify(entry, null, 2) + "\n", "utf-8");
|
|
298
|
-
} catch {
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
async function loadIgnorePatterns() {
|
|
302
|
-
try {
|
|
303
|
-
const app = getDefaultApplicationContainer();
|
|
304
|
-
const ignores = await app.services.memoryEngine.list({ type: "ignore", limit: 1e3 });
|
|
305
|
-
return ignores.map((ignore) => ignore.content);
|
|
306
|
-
} catch {
|
|
307
|
-
return [];
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
function getProfileRules(config) {
|
|
311
|
-
const rules = [];
|
|
312
|
-
const avoids = [];
|
|
313
|
-
if (config.backendArchitecture) {
|
|
314
|
-
const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
|
|
315
|
-
if (profile) {
|
|
316
|
-
rules.push(...profile.rules);
|
|
317
|
-
avoids.push(...profile.avoid);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
if (config.frontendFramework) {
|
|
321
|
-
const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
|
|
322
|
-
if (profile) {
|
|
323
|
-
rules.push(...profile.rules);
|
|
324
|
-
avoids.push(...profile.avoid);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return { rules, avoids };
|
|
328
|
-
}
|
|
329
|
-
async function loadRelevantRules(config, diff, stagedFiles, fallbackRules) {
|
|
330
|
-
try {
|
|
331
|
-
const query = buildContextQuery([
|
|
332
|
-
stagedFiles.join("\n"),
|
|
333
|
-
diff.slice(0, 1200),
|
|
334
|
-
config.backendArchitecture,
|
|
335
|
-
config.frontendFramework,
|
|
336
|
-
config.language
|
|
337
|
-
]);
|
|
338
|
-
const memories = await retrieveContextualMemories({
|
|
339
|
-
query,
|
|
340
|
-
cwd: process.cwd(),
|
|
341
|
-
config,
|
|
342
|
-
limit: 15
|
|
343
|
-
});
|
|
344
|
-
const selected = memories.filter((memory) => ["rule", "pattern", "decision"].includes(memory.type)).map((memory) => memory.content);
|
|
345
|
-
return selected.length > 0 ? selected : fallbackRules;
|
|
346
|
-
} catch {
|
|
347
|
-
return fallbackRules;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
function applyAllowPatterns(violations, allowPatterns) {
|
|
351
|
-
if (allowPatterns.length === 0) return violations;
|
|
352
|
-
return violations.filter((violation) => {
|
|
353
|
-
const haystack = `${violation.rule}
|
|
354
|
-
${violation.issue}
|
|
355
|
-
${violation.file}`.toLowerCase();
|
|
356
|
-
return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
function normalizeViolation(value) {
|
|
360
|
-
if (!value || typeof value !== "object") return null;
|
|
361
|
-
const candidate = value;
|
|
362
|
-
if (typeof candidate.rule !== "string" || typeof candidate.issue !== "string") return null;
|
|
363
|
-
return {
|
|
364
|
-
rule: candidate.rule,
|
|
365
|
-
file: typeof candidate.file === "string" ? candidate.file : "diff",
|
|
366
|
-
line: typeof candidate.line === "number" ? candidate.line : void 0,
|
|
367
|
-
issue: candidate.issue,
|
|
368
|
-
suggestion: typeof candidate.suggestion === "string" ? candidate.suggestion : void 0,
|
|
369
|
-
reason: typeof candidate.reason === "string" ? candidate.reason : void 0
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
function parseModelViolations(raw) {
|
|
373
|
-
const candidates = [
|
|
374
|
-
raw,
|
|
375
|
-
raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "")
|
|
376
|
-
];
|
|
377
|
-
const objectStart = raw.indexOf("{");
|
|
378
|
-
const objectEnd = raw.lastIndexOf("}");
|
|
379
|
-
if (objectStart !== -1 && objectEnd > objectStart) {
|
|
380
|
-
candidates.push(raw.slice(objectStart, objectEnd + 1));
|
|
381
|
-
}
|
|
382
|
-
const arrayStart = raw.indexOf("[");
|
|
383
|
-
const arrayEnd = raw.lastIndexOf("]");
|
|
384
|
-
if (arrayStart !== -1 && arrayEnd > arrayStart) {
|
|
385
|
-
candidates.push(raw.slice(arrayStart, arrayEnd + 1));
|
|
386
|
-
}
|
|
387
|
-
for (const candidate of candidates) {
|
|
388
|
-
try {
|
|
389
|
-
const parsed = JSON.parse(candidate);
|
|
390
|
-
const items = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.violations) ? parsed.violations : parsed?.rule ? [parsed] : null;
|
|
391
|
-
if (!items) continue;
|
|
392
|
-
return {
|
|
393
|
-
valid: true,
|
|
394
|
-
violations: items.map(normalizeViolation).filter((violation) => violation !== null)
|
|
395
|
-
};
|
|
396
|
-
} catch {
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
return { valid: false, violations: [] };
|
|
400
|
-
}
|
|
401
|
-
function getAddedLines(diff) {
|
|
402
|
-
const lines = [];
|
|
403
|
-
let currentFile = "diff";
|
|
404
|
-
let newLineNumber = 0;
|
|
405
|
-
for (const line of diff.split("\n")) {
|
|
406
|
-
if (line.startsWith("+++ b/")) {
|
|
407
|
-
currentFile = line.slice("+++ b/".length);
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
411
|
-
if (hunk) {
|
|
412
|
-
newLineNumber = Number(hunk[1]);
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
416
|
-
lines.push({
|
|
417
|
-
file: currentFile,
|
|
418
|
-
line: Number.isFinite(newLineNumber) ? newLineNumber : void 0,
|
|
419
|
-
content: line.slice(1)
|
|
420
|
-
});
|
|
421
|
-
newLineNumber += 1;
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
if (!line.startsWith("-") && newLineNumber > 0) {
|
|
425
|
-
newLineNumber += 1;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
return lines;
|
|
429
|
-
}
|
|
430
|
-
function dedupeViolations(violations) {
|
|
431
|
-
const seen = /* @__PURE__ */ new Set();
|
|
432
|
-
const deduped = [];
|
|
433
|
-
for (const violation of violations) {
|
|
434
|
-
const key = [
|
|
435
|
-
violation.rule,
|
|
436
|
-
violation.file,
|
|
437
|
-
violation.line ?? "",
|
|
438
|
-
violation.issue
|
|
439
|
-
].join("\0");
|
|
440
|
-
if (seen.has(key)) continue;
|
|
441
|
-
seen.add(key);
|
|
442
|
-
deduped.push(violation);
|
|
443
|
-
}
|
|
444
|
-
return deduped;
|
|
445
|
-
}
|
|
446
|
-
function normalizeKeyPath(value) {
|
|
447
|
-
return value.replace(/\\/g, "/").replace(/^\.\/+/, "").toLowerCase();
|
|
448
|
-
}
|
|
449
|
-
function violationRecurrenceKey(violation) {
|
|
450
|
-
return [
|
|
451
|
-
violation.rule.trim().toLowerCase(),
|
|
452
|
-
normalizeKeyPath(violation.file || "diff"),
|
|
453
|
-
violation.issue.trim().toLowerCase()
|
|
454
|
-
].join("\0");
|
|
455
|
-
}
|
|
456
|
-
function findRecurringViolations(currentViolations, recentViolations, minCount = 2) {
|
|
457
|
-
if (currentViolations.length === 0 || recentViolations.length === 0) return [];
|
|
458
|
-
const counts = /* @__PURE__ */ new Map();
|
|
459
|
-
for (const recent of recentViolations) {
|
|
460
|
-
const key = violationRecurrenceKey(recent);
|
|
461
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
462
|
-
}
|
|
463
|
-
return currentViolations.filter((violation) => (counts.get(violationRecurrenceKey(violation)) ?? 0) >= minCount);
|
|
464
|
-
}
|
|
465
|
-
function extractIssuePhrase(issue) {
|
|
466
|
-
const quoted = issue.match(/"([^"]{3,160})"/);
|
|
467
|
-
if (quoted?.[1]) return quoted[1].trim();
|
|
468
|
-
const afterColon = issue.split(":").slice(1).join(":").trim();
|
|
469
|
-
if (afterColon.length >= 3) return afterColon.slice(0, 180);
|
|
470
|
-
const fallback = issue.trim();
|
|
471
|
-
return fallback.length >= 3 ? fallback.slice(0, 180) : null;
|
|
472
|
-
}
|
|
473
|
-
function buildIgnorePatternFromDecision(decision) {
|
|
474
|
-
const explicit = decision.pattern?.trim();
|
|
475
|
-
if (explicit && explicit.length >= 3) return explicit;
|
|
476
|
-
return extractIssuePhrase(decision.issue);
|
|
477
|
-
}
|
|
478
|
-
function parseFalsePositiveDecision(value) {
|
|
479
|
-
if (!value || typeof value !== "object") return null;
|
|
480
|
-
const candidate = value;
|
|
481
|
-
if (typeof candidate.rule !== "string" || typeof candidate.issue !== "string") return null;
|
|
482
|
-
return {
|
|
483
|
-
rule: candidate.rule,
|
|
484
|
-
file: typeof candidate.file === "string" ? candidate.file : void 0,
|
|
485
|
-
line: typeof candidate.line === "number" ? candidate.line : void 0,
|
|
486
|
-
issue: candidate.issue,
|
|
487
|
-
falsePositive: candidate.falsePositive === true,
|
|
488
|
-
pattern: typeof candidate.pattern === "string" ? candidate.pattern : void 0,
|
|
489
|
-
reason: typeof candidate.reason === "string" ? candidate.reason : void 0
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
function parseFalsePositiveDecisions(raw) {
|
|
493
|
-
const candidates = [
|
|
494
|
-
raw,
|
|
495
|
-
raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "")
|
|
496
|
-
];
|
|
497
|
-
const objectStart = raw.indexOf("{");
|
|
498
|
-
const objectEnd = raw.lastIndexOf("}");
|
|
499
|
-
if (objectStart !== -1 && objectEnd > objectStart) {
|
|
500
|
-
candidates.push(raw.slice(objectStart, objectEnd + 1));
|
|
501
|
-
}
|
|
502
|
-
const arrayStart = raw.indexOf("[");
|
|
503
|
-
const arrayEnd = raw.lastIndexOf("]");
|
|
504
|
-
if (arrayStart !== -1 && arrayEnd > arrayStart) {
|
|
505
|
-
candidates.push(raw.slice(arrayStart, arrayEnd + 1));
|
|
506
|
-
}
|
|
507
|
-
for (const candidate of candidates) {
|
|
508
|
-
try {
|
|
509
|
-
const parsed = JSON.parse(candidate);
|
|
510
|
-
const items = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.decisions) ? parsed.decisions : parsed?.rule ? [parsed] : null;
|
|
511
|
-
if (!items) continue;
|
|
512
|
-
return {
|
|
513
|
-
valid: true,
|
|
514
|
-
decisions: items.map(parseFalsePositiveDecision).filter((decision) => decision !== null)
|
|
515
|
-
};
|
|
516
|
-
} catch {
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return { valid: false, decisions: [] };
|
|
520
|
-
}
|
|
521
|
-
function loadRecentViolationsFromStats(cwd = process.cwd()) {
|
|
522
|
-
const statsPath = join2(cwd, ".memory-core-stats.json");
|
|
523
|
-
if (!existsSync2(statsPath)) return [];
|
|
524
|
-
try {
|
|
525
|
-
const parsed = JSON.parse(readFileSync2(statsPath, "utf-8"));
|
|
526
|
-
if (!Array.isArray(parsed.recentViolations)) return [];
|
|
527
|
-
return parsed.recentViolations.filter(
|
|
528
|
-
(entry) => Boolean(entry) && typeof entry.rule === "string" && typeof entry.issue === "string" && typeof entry.file === "string" && typeof entry.timestamp === "string"
|
|
529
|
-
);
|
|
530
|
-
} catch {
|
|
531
|
-
return [];
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
function incrementFalsePositivesForPatterns(learnedPatterns, violations, cwd = process.cwd()) {
|
|
535
|
-
if (learnedPatterns.length === 0 || violations.length === 0) return;
|
|
536
|
-
const statsPath = join2(cwd, ".memory-core-stats.json");
|
|
537
|
-
if (!existsSync2(statsPath)) return;
|
|
538
|
-
let stats;
|
|
539
|
-
try {
|
|
540
|
-
stats = JSON.parse(readFileSync2(statsPath, "utf-8"));
|
|
541
|
-
} catch {
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
stats.rules ??= {};
|
|
545
|
-
for (const violation of violations) {
|
|
546
|
-
const haystack = `${violation.rule}
|
|
547
|
-
${violation.issue}
|
|
548
|
-
${violation.file}`.toLowerCase();
|
|
549
|
-
const matched = learnedPatterns.some((p) => haystack.includes(p.toLowerCase()));
|
|
550
|
-
if (!matched) continue;
|
|
551
|
-
const existing = toRuleStatEntry(stats.rules[violation.rule]);
|
|
552
|
-
stats.rules[violation.rule] = { count: existing.count, falsePositives: existing.falsePositives + 1 };
|
|
553
|
-
}
|
|
554
|
-
writeFileSync2(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
555
|
-
}
|
|
556
|
-
async function learnGlobalIgnoresFromFalsePositives(options) {
|
|
557
|
-
if (options.currentViolations.length === 0) return [];
|
|
558
|
-
const recentViolations = loadRecentViolationsFromStats();
|
|
559
|
-
const recurring = findRecurringViolations(options.currentViolations, recentViolations, 2);
|
|
560
|
-
if (recurring.length === 0) return [];
|
|
561
|
-
const systemPrompt = `You are verifying repeated architecture-rule alerts.
|
|
562
|
-
Mark falsePositive=true ONLY when the alert is clearly a false positive for this staged diff.
|
|
563
|
-
For each false positive, return a concise ignore pattern that will suppress only this recurring false alert.
|
|
564
|
-
Prefer exact snippets from the issue text.
|
|
565
|
-
|
|
566
|
-
Return strict JSON:
|
|
567
|
-
{"decisions":[{"rule":"...","file":"...","line":1,"issue":"...","falsePositive":true,"pattern":"...","reason":"..."}]}
|
|
568
|
-
Do not include any text outside JSON.`;
|
|
569
|
-
const userPrompt = `Staged diff:
|
|
570
|
-
${options.diff.slice(0, 6e3)}
|
|
571
|
-
|
|
572
|
-
Recurring violations:
|
|
573
|
-
${JSON.stringify(recurring, null, 2)}
|
|
574
|
-
|
|
575
|
-
Existing allow patterns:
|
|
576
|
-
${JSON.stringify(options.allowPatterns, null, 2)}`;
|
|
577
|
-
if (options.debug) {
|
|
578
|
-
console.log(chalk.gray("\n [debug] false-positive recheck prompt:"));
|
|
579
|
-
console.log(chalk.dim(systemPrompt));
|
|
580
|
-
console.log(chalk.dim(userPrompt));
|
|
581
|
-
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
582
|
-
}
|
|
583
|
-
try {
|
|
584
|
-
const recheckTimeoutMs = readPositiveIntEnv("MEMORY_CORE_FALSE_POSITIVE_TIMEOUT_MS", 6e3);
|
|
585
|
-
const raw = await callChatModel([
|
|
586
|
-
{ role: "system", content: systemPrompt },
|
|
587
|
-
{ role: "user", content: userPrompt }
|
|
588
|
-
], { timeoutMs: recheckTimeoutMs });
|
|
589
|
-
const parsed = parseFalsePositiveDecisions(raw);
|
|
590
|
-
if (!parsed.valid) return [];
|
|
591
|
-
const existing = new Set(options.allowPatterns.map((pattern) => pattern.toLowerCase()));
|
|
592
|
-
const app = getDefaultApplicationContainer();
|
|
593
|
-
const inserted = [];
|
|
594
|
-
for (const decision of parsed.decisions) {
|
|
595
|
-
if (!decision.falsePositive) continue;
|
|
596
|
-
const pattern = buildIgnorePatternFromDecision(decision);
|
|
597
|
-
if (!pattern) continue;
|
|
598
|
-
const normalized = pattern.toLowerCase();
|
|
599
|
-
if (existing.has(normalized)) continue;
|
|
600
|
-
try {
|
|
601
|
-
await app.services.memoryEngine.remember({
|
|
602
|
-
type: "ignore",
|
|
603
|
-
scope: "global",
|
|
604
|
-
architecture: "global",
|
|
605
|
-
content: pattern,
|
|
606
|
-
reason: `Auto-added from repeated false-positive recheck for "${decision.rule}"${decision.reason ? `: ${decision.reason}` : ""}`,
|
|
607
|
-
tags: ["ignore", "auto-false-positive"]
|
|
608
|
-
});
|
|
609
|
-
existing.add(normalized);
|
|
610
|
-
inserted.push(pattern);
|
|
611
|
-
} catch {
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
if (inserted.length > 0) {
|
|
615
|
-
incrementFalsePositivesForPatterns(inserted, options.currentViolations);
|
|
616
|
-
}
|
|
617
|
-
return inserted;
|
|
618
|
-
} catch {
|
|
619
|
-
return [];
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
function normalizePath(value) {
|
|
623
|
-
return value.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
624
|
-
}
|
|
625
|
-
function resolveChangedFile(candidate, changedFiles) {
|
|
626
|
-
const normalizedCandidate = normalizePath(candidate);
|
|
627
|
-
const candidates = [normalizedCandidate];
|
|
628
|
-
if (/^(?:a|b)\//.test(normalizedCandidate)) {
|
|
629
|
-
candidates.push(normalizedCandidate.slice(2));
|
|
630
|
-
}
|
|
631
|
-
for (const current of candidates) {
|
|
632
|
-
if (changedFiles.has(current)) return current;
|
|
633
|
-
for (const changed of changedFiles) {
|
|
634
|
-
if (changed.endsWith(`/${current}`) || current.endsWith(`/${changed}`)) {
|
|
635
|
-
return changed;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
return void 0;
|
|
640
|
-
}
|
|
641
|
-
function buildModelInputFromDiff(diff, maxChars = 8e3) {
|
|
642
|
-
const addedLines = getAddedLines(diff);
|
|
643
|
-
if (addedLines.length === 0) {
|
|
644
|
-
const truncated2 = diff.length > maxChars;
|
|
645
|
-
return {
|
|
646
|
-
text: truncated2 ? diff.slice(0, maxChars) + "\n\n[diff truncated]" : diff,
|
|
647
|
-
source: "diff",
|
|
648
|
-
truncated: truncated2
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
const chunks = [];
|
|
652
|
-
let currentFile = "";
|
|
653
|
-
for (const addedLine of addedLines) {
|
|
654
|
-
if (addedLine.file !== currentFile) {
|
|
655
|
-
currentFile = addedLine.file;
|
|
656
|
-
chunks.push(`
|
|
657
|
-
# ${currentFile}`);
|
|
658
|
-
}
|
|
659
|
-
const line = addedLine.line ?? "?";
|
|
660
|
-
chunks.push(`${line}: ${addedLine.content}`);
|
|
661
|
-
}
|
|
662
|
-
const summary = chunks.join("\n").trim();
|
|
663
|
-
const truncated = summary.length > maxChars;
|
|
664
|
-
return {
|
|
665
|
-
text: truncated ? summary.slice(0, maxChars) + "\n\n[added lines truncated]" : summary,
|
|
666
|
-
source: "added-lines",
|
|
667
|
-
truncated
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
function findDeterministicViolations(diff, rules, avoids, allowPatterns = []) {
|
|
671
|
-
const rulePhrases = rules.flatMap(
|
|
672
|
-
(rule) => extractForbiddenPhrases(rule).map((phrase) => ({ rule, phrase }))
|
|
673
|
-
);
|
|
674
|
-
const avoidPhrases = avoids.map((avoid) => ({
|
|
675
|
-
rule: `Avoid: ${avoid}`,
|
|
676
|
-
phrase: avoid.toLowerCase()
|
|
677
|
-
}));
|
|
678
|
-
const phrases = [...rulePhrases, ...avoidPhrases].filter((item) => item.phrase.length > 0);
|
|
679
|
-
if (phrases.length === 0) return [];
|
|
680
|
-
const violations = [];
|
|
681
|
-
for (const addedLine of getAddedLines(diff)) {
|
|
682
|
-
const normalizedLine = addedLine.content.toLowerCase();
|
|
683
|
-
if (allowPatterns.some((pattern) => normalizedLine.includes(pattern.toLowerCase()))) {
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
for (const { rule, phrase } of phrases) {
|
|
687
|
-
if (normalizedLine.includes(phrase)) {
|
|
688
|
-
violations.push({
|
|
689
|
-
rule,
|
|
690
|
-
file: addedLine.file,
|
|
691
|
-
line: addedLine.line,
|
|
692
|
-
issue: `Added line contains forbidden phrase: "${phrase}"`,
|
|
693
|
-
suggestion: "Remove this pattern or add an explicit ignore memory if it is intentional.",
|
|
694
|
-
reason: reasonMap.get(rule)
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
return dedupeViolations(violations);
|
|
700
|
-
}
|
|
701
|
-
function suppressBatchRepetitions(violations, threshold = 3) {
|
|
702
|
-
const pairCounts = /* @__PURE__ */ new Map();
|
|
703
|
-
for (const v of violations) {
|
|
704
|
-
const key = `${v.rule}\0${v.file}`;
|
|
705
|
-
pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);
|
|
706
|
-
}
|
|
707
|
-
const suppressedKeys = /* @__PURE__ */ new Set();
|
|
708
|
-
for (const [key, count] of pairCounts) {
|
|
709
|
-
if (count >= threshold) suppressedKeys.add(key);
|
|
710
|
-
}
|
|
711
|
-
if (suppressedKeys.size === 0) return { filtered: violations, suppressedCount: 0 };
|
|
712
|
-
const filtered = violations.filter((v) => !suppressedKeys.has(`${v.rule}\0${v.file}`));
|
|
713
|
-
return { filtered, suppressedCount: violations.length - filtered.length };
|
|
714
|
-
}
|
|
715
|
-
function groupViolationsByRule(violations) {
|
|
716
|
-
const groups = /* @__PURE__ */ new Map();
|
|
717
|
-
for (const v of violations) {
|
|
718
|
-
const existing = groups.get(v.rule);
|
|
719
|
-
if (existing) {
|
|
720
|
-
existing.push(v);
|
|
721
|
-
} else {
|
|
722
|
-
groups.set(v.rule, [v]);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
return groups;
|
|
726
|
-
}
|
|
727
|
-
function filterModelViolationsByStagedDiff(violations, stagedFiles, diff) {
|
|
728
|
-
if (violations.length === 0) return violations;
|
|
729
|
-
const changedFiles = new Set(stagedFiles.map((file) => normalizePath(file)));
|
|
730
|
-
if (changedFiles.size === 0) return [];
|
|
731
|
-
const linesByFile = /* @__PURE__ */ new Map();
|
|
732
|
-
for (const addedLine of getAddedLines(diff)) {
|
|
733
|
-
const file = normalizePath(addedLine.file);
|
|
734
|
-
if (!changedFiles.has(file)) continue;
|
|
735
|
-
if (typeof addedLine.line !== "number") continue;
|
|
736
|
-
const list = linesByFile.get(file) ?? [];
|
|
737
|
-
list.push(addedLine.line);
|
|
738
|
-
linesByFile.set(file, list);
|
|
739
|
-
}
|
|
740
|
-
const LINE_TOLERANCE = 3;
|
|
741
|
-
const filtered = [];
|
|
742
|
-
for (const violation of violations) {
|
|
743
|
-
if (!violation.file || violation.file === "diff") {
|
|
744
|
-
filtered.push(violation);
|
|
745
|
-
continue;
|
|
746
|
-
}
|
|
747
|
-
const resolvedFile = resolveChangedFile(violation.file, changedFiles);
|
|
748
|
-
if (!resolvedFile) continue;
|
|
749
|
-
const candidateLines = linesByFile.get(resolvedFile) ?? [];
|
|
750
|
-
if (typeof violation.line === "number" && candidateLines.length > 0) {
|
|
751
|
-
const supported = candidateLines.some((line) => Math.abs(line - violation.line) <= LINE_TOLERANCE);
|
|
752
|
-
if (!supported) continue;
|
|
753
|
-
}
|
|
754
|
-
filtered.push({ ...violation, file: resolvedFile });
|
|
755
|
-
}
|
|
756
|
-
return filtered;
|
|
757
|
-
}
|
|
758
|
-
function installHook(advisory = true, fast = false) {
|
|
759
|
-
if (!existsSync2(".git")) {
|
|
760
|
-
console.error(chalk.red("\n Not a git repository. Run from project root.\n"));
|
|
761
|
-
process.exit(1);
|
|
762
|
-
}
|
|
763
|
-
const script = buildHookScript(advisory, fast);
|
|
764
|
-
const body = buildHookBody(advisory, fast).trimEnd();
|
|
765
|
-
if (existsSync2(HOOK_PATH)) {
|
|
766
|
-
const existing = readFileSync2(HOOK_PATH, "utf-8");
|
|
767
|
-
if (existing.includes(HOOK_MARKER)) {
|
|
768
|
-
const markerIndex = existing.indexOf(HOOK_MARKER);
|
|
769
|
-
const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
|
|
770
|
-
const normalizedBefore = normalizeHookPreamble(beforeRaw);
|
|
771
|
-
const preamble = normalizedBefore.length > 0 ? normalizedBefore : "#!/bin/sh";
|
|
772
|
-
const preambleWithShebang = preamble.startsWith("#!/bin/sh") ? preamble : `#!/bin/sh
|
|
773
|
-
${preamble}`;
|
|
774
|
-
writeFileSync2(HOOK_PATH, `${preambleWithShebang}
|
|
775
|
-
|
|
776
|
-
${body}
|
|
777
|
-
`);
|
|
778
|
-
chmodSync(HOOK_PATH, 493);
|
|
779
|
-
installCommitMsgHook(advisory);
|
|
780
|
-
const modeLabel2 = advisory ? chalk.cyan("advisory") : chalk.yellow("strict");
|
|
781
|
-
console.log(chalk.green("\n \u2713 Pre-commit hook updated") + chalk.dim(` (${modeLabel2} mode)`));
|
|
782
|
-
if (fast) console.log(chalk.gray(` Check mode: fast deterministic checks`));
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
writeFileSync2(HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
|
|
786
|
-
} else {
|
|
787
|
-
writeFileSync2(HOOK_PATH, script);
|
|
788
|
-
}
|
|
789
|
-
chmodSync(HOOK_PATH, 493);
|
|
790
|
-
installCommitMsgHook(advisory);
|
|
791
|
-
const modeLabel = advisory ? "advisory (logs violations, never blocks)" : "strict (blocks commits on violations)";
|
|
792
|
-
console.log(chalk.green("\n \u2713 Pre-commit hook installed") + chalk.dim(` \u2014 ${modeLabel}`));
|
|
793
|
-
console.log(chalk.gray(fast ? " Check mode: fast deterministic checks" : ` Chat model: ${process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"}`));
|
|
794
|
-
console.log(chalk.gray(" Commit message rules: memory-core commit-rules --list"));
|
|
795
|
-
console.log(chalk.gray(" To uninstall: memory-core hook uninstall\n"));
|
|
796
|
-
}
|
|
797
|
-
function uninstallHook() {
|
|
798
|
-
if (!existsSync2(HOOK_PATH)) {
|
|
799
|
-
console.log(chalk.yellow("\n No pre-commit hook found.\n"));
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
const content = readFileSync2(HOOK_PATH, "utf-8");
|
|
803
|
-
if (!content.includes(HOOK_MARKER)) {
|
|
804
|
-
console.log(chalk.yellow("\n ArchMind hook not found in pre-commit \u2014 nothing to remove.\n"));
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
const markerIndex = content.indexOf(HOOK_MARKER);
|
|
808
|
-
const before = markerIndex > 1 ? normalizeHookPreamble(content.slice(0, markerIndex)) : "";
|
|
809
|
-
if (before && before !== "#!/bin/sh") {
|
|
810
|
-
writeFileSync2(HOOK_PATH, `${before}
|
|
811
|
-
`);
|
|
812
|
-
} else {
|
|
813
|
-
unlinkSync(HOOK_PATH);
|
|
814
|
-
}
|
|
815
|
-
uninstallCommitMsgHook();
|
|
816
|
-
console.log(chalk.green("\n \u2713 Pre-commit hook removed\n"));
|
|
817
|
-
}
|
|
818
|
-
function buildCommitMsgHookBody(advisory) {
|
|
819
|
-
const suffix = advisory ? " || true" : "";
|
|
820
|
-
return `${COMMIT_MSG_HOOK_MARKER}${advisory ? " advisory" : ""}
|
|
821
|
-
if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
|
|
822
|
-
exit 0
|
|
823
|
-
fi
|
|
824
|
-
if [ -n "\${SKIP:-}" ] && echo ",$SKIP," | grep -qiE ',(memory-core|archmind),'; then
|
|
825
|
-
exit 0
|
|
826
|
-
fi
|
|
827
|
-
if [ -n "\${SKIP_HOOKS:-}" ]; then
|
|
828
|
-
exit 0
|
|
829
|
-
fi
|
|
830
|
-
if command -v memory-core >/dev/null 2>&1; then
|
|
831
|
-
memory-core check --commit-msg "$1"${suffix}
|
|
832
|
-
elif [ -f "./node_modules/.bin/memory-core" ]; then
|
|
833
|
-
./node_modules/.bin/memory-core check --commit-msg "$1"${suffix}
|
|
834
|
-
elif [ -f "./dist/cli.js" ]; then
|
|
835
|
-
node ./dist/cli.js check --commit-msg "$1"${suffix}
|
|
836
|
-
else
|
|
837
|
-
exit 0
|
|
838
|
-
fi
|
|
839
|
-
`;
|
|
840
|
-
}
|
|
841
|
-
function installCommitMsgHook(advisory = true) {
|
|
842
|
-
const body = buildCommitMsgHookBody(advisory).trimEnd();
|
|
843
|
-
const script = `#!/bin/sh
|
|
844
|
-
|
|
845
|
-
${body}
|
|
846
|
-
`;
|
|
847
|
-
if (existsSync2(COMMIT_MSG_HOOK_PATH)) {
|
|
848
|
-
const existing = readFileSync2(COMMIT_MSG_HOOK_PATH, "utf-8");
|
|
849
|
-
if (existing.includes(COMMIT_MSG_HOOK_MARKER)) {
|
|
850
|
-
const markerIndex = existing.indexOf(COMMIT_MSG_HOOK_MARKER);
|
|
851
|
-
const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
|
|
852
|
-
const normalizedBefore = normalizeHookPreamble(beforeRaw);
|
|
853
|
-
const preamble = normalizedBefore.length > 0 ? normalizedBefore : "#!/bin/sh";
|
|
854
|
-
const preambleWithShebang = preamble.startsWith("#!/bin/sh") ? preamble : `#!/bin/sh
|
|
855
|
-
${preamble}`;
|
|
856
|
-
writeFileSync2(COMMIT_MSG_HOOK_PATH, `${preambleWithShebang}
|
|
857
|
-
|
|
858
|
-
${body}
|
|
859
|
-
`);
|
|
860
|
-
} else {
|
|
861
|
-
writeFileSync2(COMMIT_MSG_HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
|
|
862
|
-
}
|
|
863
|
-
} else {
|
|
864
|
-
writeFileSync2(COMMIT_MSG_HOOK_PATH, script);
|
|
865
|
-
}
|
|
866
|
-
chmodSync(COMMIT_MSG_HOOK_PATH, 493);
|
|
867
|
-
}
|
|
868
|
-
function uninstallCommitMsgHook() {
|
|
869
|
-
if (!existsSync2(COMMIT_MSG_HOOK_PATH)) return;
|
|
870
|
-
const content = readFileSync2(COMMIT_MSG_HOOK_PATH, "utf-8");
|
|
871
|
-
if (!content.includes(COMMIT_MSG_HOOK_MARKER)) return;
|
|
872
|
-
const markerIndex = content.indexOf(COMMIT_MSG_HOOK_MARKER);
|
|
873
|
-
const before = markerIndex > 1 ? normalizeHookPreamble(content.slice(0, markerIndex)) : "";
|
|
874
|
-
if (before && before !== "#!/bin/sh") {
|
|
875
|
-
writeFileSync2(COMMIT_MSG_HOOK_PATH, `${before}
|
|
876
|
-
`);
|
|
877
|
-
} else {
|
|
878
|
-
unlinkSync(COMMIT_MSG_HOOK_PATH);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
async function checkCommitMsg(msgFile, options = {}) {
|
|
882
|
-
if (!existsSync2(msgFile)) {
|
|
883
|
-
if (options.verbose) console.log(chalk.gray(" No commit message file \u2014 skipping."));
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
const raw = readFileSync2(msgFile, "utf-8");
|
|
887
|
-
const cleanMsg = raw.split("\n").filter((l) => !l.startsWith("#")).join("\n").trim();
|
|
888
|
-
if (!cleanMsg) {
|
|
889
|
-
if (options.verbose) console.log(chalk.gray(" Empty commit message \u2014 skipping."));
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
const configPath = join2(process.cwd(), ".memory-core.json");
|
|
893
|
-
if (!existsSync2(configPath)) return;
|
|
894
|
-
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
895
|
-
const rules = (config.commitRules ?? []).filter(Boolean);
|
|
896
|
-
if (rules.length === 0) return;
|
|
897
|
-
console.log(chalk.cyan("\n archmind \u2014 checking commit message\u2026"));
|
|
898
|
-
const violations = [];
|
|
899
|
-
for (const rule of rules) {
|
|
900
|
-
try {
|
|
901
|
-
const regex = new RegExp(rule.pattern, "im");
|
|
902
|
-
const matched = regex.test(cleanMsg);
|
|
903
|
-
const violated = rule.negate ? matched : !matched;
|
|
904
|
-
if (violated) violations.push({ rule });
|
|
905
|
-
} catch {
|
|
906
|
-
if (options.debug) console.log(chalk.yellow(` [debug] Invalid regex: "${rule.pattern}"`));
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
if (violations.length === 0) {
|
|
910
|
-
console.log(chalk.green(" \u2713 Commit message OK.\n"));
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
const blocking = violations.filter((v) => !v.rule.advisory);
|
|
914
|
-
violations.forEach(({ rule }) => {
|
|
915
|
-
const prefix = rule.advisory ? chalk.yellow(" \u26A0 ") : chalk.red(" \u2717 ");
|
|
916
|
-
console.log(prefix + rule.message);
|
|
917
|
-
const matchLabel = rule.negate ? "(must NOT match)" : "(must match)";
|
|
918
|
-
console.log(chalk.dim(` Pattern: ${rule.pattern} ${matchLabel}`));
|
|
919
|
-
});
|
|
920
|
-
console.log();
|
|
921
|
-
if (blocking.length === 0) return;
|
|
922
|
-
console.log(chalk.dim(" Fix the commit message, then commit again."));
|
|
923
|
-
console.log(chalk.dim(" To bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
|
|
924
|
-
console.log(chalk.dim(" Manage rules: memory-core commit-rules --list\n"));
|
|
925
|
-
process.exit(1);
|
|
926
|
-
}
|
|
927
|
-
async function checkStaged(options = {}) {
|
|
928
|
-
const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
929
|
-
let diff;
|
|
930
|
-
let stagedFiles = [];
|
|
931
|
-
try {
|
|
932
|
-
stagedFiles = execSync("git diff --cached --name-only --diff-filter=ACMRT", { encoding: "utf-8" }).split("\n").filter((f) => f && SOURCE_EXTENSIONS.test(f)).map((f) => normalizePath(f));
|
|
933
|
-
if (stagedFiles.length === 0) {
|
|
934
|
-
if (options.verbose) console.log(chalk.gray(" No source files staged \u2014 skipping rule check."));
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
const result = spawnSync(
|
|
938
|
-
"git",
|
|
939
|
-
["diff", "--cached", "--unified=0", "--diff-filter=ACMRT", "--", ...stagedFiles],
|
|
940
|
-
{ encoding: "utf-8" }
|
|
941
|
-
);
|
|
942
|
-
diff = result.stdout ?? "";
|
|
943
|
-
} catch {
|
|
944
|
-
console.error(chalk.red(" Failed to read staged diff."));
|
|
945
|
-
process.exit(1);
|
|
946
|
-
}
|
|
947
|
-
if (!diff.trim()) {
|
|
948
|
-
if (options.verbose) console.log(chalk.gray(" No staged changes to check."));
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
const configPath = join2(process.cwd(), ".memory-core.json");
|
|
952
|
-
if (!existsSync2(configPath)) return;
|
|
953
|
-
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
954
|
-
const { rules: fallbackRules, avoids } = getProfileRules(config);
|
|
955
|
-
const fast = isFastCheck(options);
|
|
956
|
-
const ruleLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_RULE_LOAD_TIMEOUT_MS", 2e3);
|
|
957
|
-
const ignoreLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_IGNORE_LOAD_TIMEOUT_MS", 1500);
|
|
958
|
-
let rules;
|
|
959
|
-
let ignores;
|
|
960
|
-
let allowPatterns;
|
|
961
|
-
if (fast) {
|
|
962
|
-
rules = fallbackRules;
|
|
963
|
-
ignores = [];
|
|
964
|
-
allowPatterns = [...new Set(getAllowPatterns(config))];
|
|
965
|
-
} else {
|
|
966
|
-
const cwd = process.cwd();
|
|
967
|
-
const cached = readRuleCache(cwd);
|
|
968
|
-
if (cached) {
|
|
969
|
-
rules = cached.rules;
|
|
970
|
-
ignores = cached.ignores;
|
|
971
|
-
allowPatterns = cached.allowPatterns;
|
|
972
|
-
if (options.debug) {
|
|
973
|
-
console.log(chalk.gray(" [debug] using cached rules (TTL valid)"));
|
|
974
|
-
}
|
|
975
|
-
} else {
|
|
976
|
-
const [loadedRules, loadedIgnores] = await Promise.all([
|
|
977
|
-
withTimeout(loadRelevantRules(config, diff, stagedFiles, fallbackRules), ruleLoadTimeoutMs, fallbackRules),
|
|
978
|
-
withTimeout(loadIgnorePatterns(), ignoreLoadTimeoutMs, [])
|
|
979
|
-
]);
|
|
980
|
-
rules = loadedRules;
|
|
981
|
-
ignores = loadedIgnores;
|
|
982
|
-
allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...loadedIgnores])];
|
|
983
|
-
saveRuleCache(cwd, { rules, ignores, allowPatterns });
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
if (rules.length === 0) return;
|
|
987
|
-
const modelInputMaxChars = readPositiveIntEnv("MEMORY_CORE_MODEL_INPUT_MAX_CHARS", 8e3);
|
|
988
|
-
const modelInput = buildModelInputFromDiff(diff, modelInputMaxChars);
|
|
989
|
-
console.log(chalk.cyan("\n archmind \u2014 checking staged changes against rules\u2026"));
|
|
990
|
-
if (options.verbose || options.debug) {
|
|
991
|
-
const sourceLabel = modelInput.source === "added-lines" ? "added lines" : "diff";
|
|
992
|
-
const modelLabel = fast ? "skipped (--fast)" : getChatProviderLabel();
|
|
993
|
-
console.log(chalk.gray(` model: ${modelLabel} rules: ${rules.length} diff: ${diff.length} chars input: ${sourceLabel}${modelInput.truncated ? " (truncated)" : ""}`));
|
|
994
|
-
}
|
|
995
|
-
const rulesWithReasons = rules.map((r, i) => {
|
|
996
|
-
const why = reasonMap.get(r);
|
|
997
|
-
return why ? `${i + 1}. ${r}
|
|
998
|
-
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
999
|
-
}).join("\n");
|
|
1000
|
-
const systemPrompt = `You are a strict code reviewer enforcing architecture and framework rules.
|
|
1001
|
-
Analyze the provided staged changes and identify ONLY clear, definite rule violations \u2014 not style preferences.
|
|
1002
|
-
Use the WHY for each rule to understand intent and judge edge cases correctly.
|
|
1003
|
-
|
|
1004
|
-
Rules to enforce:
|
|
1005
|
-
${rulesWithReasons}
|
|
1006
|
-
|
|
1007
|
-
Things that must never appear:
|
|
1008
|
-
${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
|
|
1009
|
-
|
|
1010
|
-
Never flag these accepted project patterns:
|
|
1011
|
-
${allowPatterns.length ? allowPatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
|
|
1012
|
-
|
|
1013
|
-
IMPORTANT: You MUST respond with a JSON object that has a "violations" key containing an array.
|
|
1014
|
-
For each violation include a "reason" field \u2014 copy the WHY from the rule to explain to the developer why this matters.
|
|
1015
|
-
Example with violations: {"violations":[{"rule":"Use functional components only","file":"User.tsx","line":3,"issue":"Class component used","suggestion":"Convert to a function component using hooks","reason":"Class components cannot use hooks and the entire React ecosystem now assumes functional components"}]}
|
|
1016
|
-
Example with no violations: {"violations":[]}
|
|
1017
|
-
Do not include any text outside the JSON object.`;
|
|
1018
|
-
if (options.debug) {
|
|
1019
|
-
console.log(chalk.gray("\n [debug] prompt:"));
|
|
1020
|
-
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1021
|
-
console.log(systemPrompt);
|
|
1022
|
-
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1023
|
-
console.log(chalk.gray(` [debug] diff length: ${diff.length} chars`));
|
|
1024
|
-
console.log(chalk.gray(` [debug] model input source: ${modelInput.source}`));
|
|
1025
|
-
console.log(chalk.dim(modelInput.text));
|
|
1026
|
-
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1027
|
-
}
|
|
1028
|
-
const deterministicViolations = findDeterministicViolations(diff, rules, avoids, allowPatterns);
|
|
1029
|
-
const astViolations = findAstDeterministicViolationsForDiff(diff, {
|
|
1030
|
-
cwd: process.cwd(),
|
|
1031
|
-
config,
|
|
1032
|
-
rules,
|
|
1033
|
-
reasonLookup: reasonMap
|
|
1034
|
-
});
|
|
1035
|
-
let modelViolations = [];
|
|
1036
|
-
let aiFallback = fast;
|
|
1037
|
-
if (fast) {
|
|
1038
|
-
if (options.verbose || options.debug) {
|
|
1039
|
-
console.log(chalk.gray(" AI check skipped; running deterministic checks only."));
|
|
1040
|
-
}
|
|
1041
|
-
} else try {
|
|
1042
|
-
const checkTimeoutMs = readPositiveIntEnv("MEMORY_CORE_CHECK_TIMEOUT_MS", readPositiveIntEnv("CHAT_TIMEOUT_MS", 2e4));
|
|
1043
|
-
const raw = await callChatModel([
|
|
1044
|
-
{ role: "system", content: systemPrompt },
|
|
1045
|
-
{ role: "user", content: `Review these staged changes:
|
|
1046
|
-
|
|
1047
|
-
${modelInput.text}` }
|
|
1048
|
-
], { timeoutMs: checkTimeoutMs });
|
|
1049
|
-
if (options.verbose || options.debug) {
|
|
1050
|
-
console.log(chalk.gray(` raw response: ${options.debug ? raw : raw.slice(0, 200)}`));
|
|
1051
|
-
}
|
|
1052
|
-
const parsed = parseModelViolations(raw);
|
|
1053
|
-
if (parsed.valid) {
|
|
1054
|
-
modelViolations = parsed.violations;
|
|
1055
|
-
} else {
|
|
1056
|
-
console.log(chalk.yellow(" \u26A0 AI returned invalid JSON \u2014 using deterministic checks only."));
|
|
1057
|
-
}
|
|
1058
|
-
} catch (err) {
|
|
1059
|
-
if (err.message?.startsWith("MODEL_NOT_FOUND:")) {
|
|
1060
|
-
printModelMissing(err.message.split(":")[1]);
|
|
1061
|
-
aiFallback = true;
|
|
1062
|
-
modelViolations = [];
|
|
1063
|
-
} else if (err.message?.startsWith("TIMEOUT:")) {
|
|
1064
|
-
const timeoutMs = err.message.split(":")[1];
|
|
1065
|
-
console.log(chalk.yellow(`
|
|
1066
|
-
\u26A0 AI check timed out after ${timeoutMs}ms \u2014 switching to fast deterministic checks for this run.`));
|
|
1067
|
-
console.log(chalk.gray(" Set MEMORY_CORE_CHECK_TIMEOUT_MS to tune this.\n"));
|
|
1068
|
-
aiFallback = true;
|
|
1069
|
-
modelViolations = [];
|
|
1070
|
-
} else if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
|
|
1071
|
-
console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 using deterministic checks only."));
|
|
1072
|
-
console.log(chalk.gray(" Start it: ollama serve\n"));
|
|
1073
|
-
aiFallback = true;
|
|
1074
|
-
modelViolations = [];
|
|
1075
|
-
} else {
|
|
1076
|
-
console.log(chalk.yellow(`
|
|
1077
|
-
\u26A0 AI rule check failed: ${err.message}`));
|
|
1078
|
-
console.log(chalk.gray(" Using deterministic checks only.\n"));
|
|
1079
|
-
aiFallback = true;
|
|
1080
|
-
modelViolations = [];
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
modelViolations = filterModelViolationsByStagedDiff(modelViolations, stagedFiles, diff);
|
|
1084
|
-
let violations = dedupeViolations([...deterministicViolations, ...astViolations, ...modelViolations]);
|
|
1085
|
-
violations = applyAllowPatterns(violations, allowPatterns);
|
|
1086
|
-
if (violations.length > 0) {
|
|
1087
|
-
const { filtered, suppressedCount } = suppressBatchRepetitions(violations);
|
|
1088
|
-
if (suppressedCount > 0) {
|
|
1089
|
-
console.log(
|
|
1090
|
-
chalk.dim(
|
|
1091
|
-
` \u2139 Auto-suppressed ${suppressedCount} repetitive violation${suppressedCount > 1 ? "s" : ""} (same rule fired \u22653\xD7 on the same file \u2014 consider tuning the rule)`
|
|
1092
|
-
)
|
|
1093
|
-
);
|
|
1094
|
-
violations = filtered;
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
if (!aiFallback && violations.length > 0) {
|
|
1098
|
-
const learnedPatterns = await learnGlobalIgnoresFromFalsePositives({
|
|
1099
|
-
diff,
|
|
1100
|
-
currentViolations: violations,
|
|
1101
|
-
allowPatterns,
|
|
1102
|
-
debug: options.debug
|
|
1103
|
-
});
|
|
1104
|
-
if (learnedPatterns.length > 0) {
|
|
1105
|
-
if (options.verbose || options.debug) {
|
|
1106
|
-
console.log(chalk.gray(` learned ${learnedPatterns.length} global ignore pattern${learnedPatterns.length > 1 ? "s" : ""} from false-positive recheck`));
|
|
1107
|
-
}
|
|
1108
|
-
const refinedAllowPatterns = [.../* @__PURE__ */ new Set([...allowPatterns, ...learnedPatterns])];
|
|
1109
|
-
violations = applyAllowPatterns(violations, refinedAllowPatterns);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
if (violations.length === 0) {
|
|
1113
|
-
resetViolationStats();
|
|
1114
|
-
console.log(chalk.green(" \u2713 No rule violations \u2014 commit allowed.\n"));
|
|
1115
|
-
return;
|
|
1116
|
-
}
|
|
1117
|
-
console.log(
|
|
1118
|
-
chalk.red.bold(
|
|
1119
|
-
`
|
|
1120
|
-
\u2717 ${violations.length} rule violation${violations.length > 1 ? "s" : ""} found \u2014 commit blocked
|
|
1121
|
-
`
|
|
1122
|
-
)
|
|
1123
|
-
);
|
|
1124
|
-
let ruleStatsSnapshot = {};
|
|
1125
|
-
{
|
|
1126
|
-
const statsPath = join2(process.cwd(), ".memory-core-stats.json");
|
|
1127
|
-
if (existsSync2(statsPath)) {
|
|
1128
|
-
try {
|
|
1129
|
-
const parsed = JSON.parse(readFileSync2(statsPath, "utf-8"));
|
|
1130
|
-
ruleStatsSnapshot = parsed.rules ?? {};
|
|
1131
|
-
} catch {
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
const MAX_LOCATIONS = 5;
|
|
1136
|
-
const groups = groupViolationsByRule(violations);
|
|
1137
|
-
let groupIndex = 0;
|
|
1138
|
-
for (const [rule, group] of groups) {
|
|
1139
|
-
groupIndex++;
|
|
1140
|
-
const isCluster = group.length > 1;
|
|
1141
|
-
const first = group[0];
|
|
1142
|
-
if (isCluster) {
|
|
1143
|
-
console.log(chalk.bold.red(`
|
|
1144
|
-
[${groupIndex}] ${rule}`) + chalk.dim(` \xD7${group.length}`));
|
|
1145
|
-
} else {
|
|
1146
|
-
const loc = first.file ? first.line ? `${first.file}:${first.line}` : first.file : "unknown location";
|
|
1147
|
-
console.log(chalk.bold(`
|
|
1148
|
-
[${groupIndex}] ${loc}`));
|
|
1149
|
-
console.log(chalk.yellow(" Rule: ") + rule);
|
|
1150
|
-
}
|
|
1151
|
-
const why = first.reason ?? reasonMap.get(rule);
|
|
1152
|
-
if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
|
|
1153
|
-
if (first.suggestion) console.log(chalk.green(" Fix: ") + first.suggestion);
|
|
1154
|
-
if (isCluster) {
|
|
1155
|
-
console.log();
|
|
1156
|
-
const shown = group.slice(0, MAX_LOCATIONS);
|
|
1157
|
-
const overflow = group.length - MAX_LOCATIONS;
|
|
1158
|
-
for (const v of shown) {
|
|
1159
|
-
const loc = v.file ? v.line ? `${v.file}:${v.line}` : v.file : "unknown location";
|
|
1160
|
-
const issue = v.issue ? chalk.dim(` ${v.issue}`) : "";
|
|
1161
|
-
console.log(chalk.dim(` ${loc}`) + issue);
|
|
1162
|
-
}
|
|
1163
|
-
if (overflow > 0) {
|
|
1164
|
-
console.log(chalk.dim(` ... and ${overflow} more`));
|
|
1165
|
-
}
|
|
1166
|
-
} else {
|
|
1167
|
-
if (first.issue) console.log(chalk.red(" Issue: ") + first.issue);
|
|
1168
|
-
}
|
|
1169
|
-
const ruleEntry = toRuleStatEntry(ruleStatsSnapshot[rule]);
|
|
1170
|
-
if (ruleEntry.count > 5 && ruleEntry.falsePositives > 0) {
|
|
1171
|
-
const rate = Math.round(ruleEntry.falsePositives / ruleEntry.count * 100);
|
|
1172
|
-
if (rate > 40) {
|
|
1173
|
-
console.log(chalk.yellow(`
|
|
1174
|
-
Noisy: ${rate}% historical false-positive rate`));
|
|
1175
|
-
console.log(chalk.dim(` Silence: memory-core allow "${rule}"`));
|
|
1176
|
-
console.log(chalk.dim(` Review all: memory-core tune`));
|
|
1177
|
-
} else if (rate > 25) {
|
|
1178
|
-
console.log(chalk.dim(`
|
|
1179
|
-
Note: ${rate}% false-positive rate \u2014 run: memory-core tune`));
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
console.log();
|
|
1183
|
-
}
|
|
1184
|
-
console.log(chalk.dim(" Fix the violations above, then commit again."));
|
|
1185
|
-
console.log(chalk.dim(" To bypass (not recommended): git commit --no-verify"));
|
|
1186
|
-
console.log(chalk.dim(" Env bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
|
|
1187
|
-
console.log(chalk.dim(' To save as memory: memory-core remember "<lesson>"'));
|
|
1188
|
-
console.log();
|
|
1189
|
-
recordViolations(violations);
|
|
1190
|
-
await promptToSaveViolations(violations);
|
|
1191
|
-
process.exit(1);
|
|
1192
|
-
}
|
|
1193
|
-
function extractForbiddenPhrases(content) {
|
|
1194
|
-
const phrases = [];
|
|
1195
|
-
const normalized = content.replace(/\s+/g, " ");
|
|
1196
|
-
const patterns = [
|
|
1197
|
-
/\bnever\s+([^.;]+)/gi,
|
|
1198
|
-
/\bmust not\s+([^.;]+)/gi,
|
|
1199
|
-
/\bdo not\s+([^.;]+)/gi
|
|
1200
|
-
];
|
|
1201
|
-
for (const pattern of patterns) {
|
|
1202
|
-
for (const match of normalized.matchAll(pattern)) {
|
|
1203
|
-
const phrase = match[1]?.trim();
|
|
1204
|
-
if (phrase && phrase.split(/\s+/).length >= 2) phrases.push(phrase.toLowerCase());
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
return phrases;
|
|
1208
|
-
}
|
|
1209
|
-
function getCiDiff() {
|
|
1210
|
-
const baseRef = process.env.GITHUB_BASE_REF;
|
|
1211
|
-
const commands = [
|
|
1212
|
-
baseRef ? `git diff --unified=0 --diff-filter=ACMRT origin/${baseRef}...HEAD` : "",
|
|
1213
|
-
"git diff --unified=0 --diff-filter=ACMRT HEAD~1 HEAD",
|
|
1214
|
-
"git diff --cached --unified=0 --diff-filter=ACMRT"
|
|
1215
|
-
].filter(Boolean);
|
|
1216
|
-
for (const command of commands) {
|
|
1217
|
-
try {
|
|
1218
|
-
const diff = execSync(command, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1219
|
-
if (diff.trim()) return diff;
|
|
1220
|
-
} catch {
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
return "";
|
|
1224
|
-
}
|
|
1225
|
-
async function checkCi(options = {}) {
|
|
1226
|
-
let memories;
|
|
1227
|
-
try {
|
|
1228
|
-
memories = readMemoryFile();
|
|
1229
|
-
} catch (err) {
|
|
1230
|
-
console.error(chalk.red(`
|
|
1231
|
-
CI check failed: ${err.message}
|
|
1232
|
-
`));
|
|
1233
|
-
process.exit(1);
|
|
1234
|
-
}
|
|
1235
|
-
const rules = memories.filter((memory) => memory.type !== "ignore");
|
|
1236
|
-
const ignores = memories.filter((memory) => memory.type === "ignore").map((memory) => memory.content.toLowerCase());
|
|
1237
|
-
const phrases = rules.flatMap(
|
|
1238
|
-
(memory) => extractForbiddenPhrases(memory.content).map((phrase) => ({ rule: memory.content, phrase }))
|
|
1239
|
-
);
|
|
1240
|
-
const diff = getCiDiff();
|
|
1241
|
-
const addedLines = diff.split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
|
|
1242
|
-
if (options.debug) {
|
|
1243
|
-
console.log(chalk.gray(`
|
|
1244
|
-
[debug] memories: ${memories.length}`));
|
|
1245
|
-
console.log(chalk.gray(` [debug] text rules: ${phrases.length}`));
|
|
1246
|
-
console.log(chalk.gray(` [debug] diff length: ${diff.length} chars
|
|
1247
|
-
`));
|
|
1248
|
-
}
|
|
1249
|
-
const violations = [];
|
|
1250
|
-
for (const line of addedLines) {
|
|
1251
|
-
const normalizedLine = line.toLowerCase();
|
|
1252
|
-
if (ignores.some((ignore) => normalizedLine.includes(ignore))) continue;
|
|
1253
|
-
for (const { rule, phrase } of phrases) {
|
|
1254
|
-
if (normalizedLine.includes(phrase)) {
|
|
1255
|
-
violations.push({
|
|
1256
|
-
rule,
|
|
1257
|
-
file: "diff",
|
|
1258
|
-
issue: `Added line contains forbidden phrase: "${phrase}"`
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
if (violations.length === 0) {
|
|
1264
|
-
console.log(chalk.green(`
|
|
1265
|
-
\u2713 CI memory check passed (${rules.length} rules loaded from memories.json)
|
|
1266
|
-
`));
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
console.log(chalk.red.bold(`
|
|
1270
|
-
\u2717 ${violations.length} CI violation${violations.length > 1 ? "s" : ""} found
|
|
1271
|
-
`));
|
|
1272
|
-
violations.forEach((violation, index) => {
|
|
1273
|
-
console.log(chalk.bold(` [${index + 1}] ${violation.file}`));
|
|
1274
|
-
console.log(chalk.yellow(" Rule: ") + violation.rule);
|
|
1275
|
-
console.log(chalk.red(" Issue: ") + violation.issue);
|
|
1276
|
-
console.log();
|
|
1277
|
-
});
|
|
1278
|
-
recordViolations(violations, "ci");
|
|
1279
|
-
process.exit(1);
|
|
1280
|
-
}
|
|
1281
|
-
function printModelMissing(model2) {
|
|
1282
|
-
console.log(chalk.yellow(`
|
|
1283
|
-
\u26A0 Chat model "${model2}" not found in Ollama.`));
|
|
1284
|
-
console.log(chalk.gray(` Pull a model: ollama pull ${model2}`));
|
|
1285
|
-
console.log(chalk.gray(" Or set OLLAMA_CHAT_MODEL=<model> in .env"));
|
|
1286
|
-
console.log(chalk.gray(" Recommended: llama3.2 | qwen2.5-coder:3b | mistral\n"));
|
|
1287
|
-
}
|
|
47
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1288
48
|
|
|
1289
49
|
// src/remote-install.ts
|
|
1290
|
-
import { spawnSync
|
|
50
|
+
import { spawnSync } from "child_process";
|
|
1291
51
|
var CAVEMAN_INSTALL_URL = "https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh";
|
|
1292
52
|
var MAX_INSTALLER_BYTES = 2e5;
|
|
1293
53
|
var TRUSTED_INSTALL_HOSTS = /* @__PURE__ */ new Set(["raw.githubusercontent.com"]);
|
|
@@ -1301,7 +61,7 @@ function assertTrustedInstallerUrl(url) {
|
|
|
1301
61
|
}
|
|
1302
62
|
}
|
|
1303
63
|
function defaultRunScript(script) {
|
|
1304
|
-
return
|
|
64
|
+
return spawnSync("bash", ["-s"], {
|
|
1305
65
|
input: script,
|
|
1306
66
|
encoding: "utf-8",
|
|
1307
67
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1337,38 +97,38 @@ async function installCavemanTokenSaver(options = {}) {
|
|
|
1337
97
|
|
|
1338
98
|
// src/cli.ts
|
|
1339
99
|
function printBanner(projectName, agentCount, status) {
|
|
1340
|
-
const pg = status ? status.postgresOk ?
|
|
1341
|
-
const ol = status ? status.ollamaOk ?
|
|
100
|
+
const pg = status ? status.postgresOk ? chalk.green(" \u2713 PostgreSQL ") + chalk.bold("connected") : chalk.red(" \u2717 PostgreSQL ") + chalk.bold("not connected \u2014 check DATABASE_URL") : chalk.green(" \u2713 Memory ") + chalk.bold("PostgreSQL + pgvector ready");
|
|
101
|
+
const ol = status ? status.ollamaOk ? chalk.green(" \u2713 Ollama ") + chalk.bold(`connected (model: ${status.chatModel})`) : chalk.red(" \u2717 Ollama ") + chalk.bold("not running \u2014 start with: ollama serve") : null;
|
|
1342
102
|
const lines = [
|
|
1343
103
|
"",
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
104
|
+
chalk.cyan(" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 "),
|
|
105
|
+
chalk.cyan(" \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557"),
|
|
106
|
+
chalk.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551"),
|
|
107
|
+
chalk.cyan(" \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551"),
|
|
108
|
+
chalk.cyan(" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D"),
|
|
109
|
+
chalk.cyan(" \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D"),
|
|
1350
110
|
"",
|
|
1351
|
-
|
|
111
|
+
chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 ") + chalk.bold.white("C O R E") + chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),
|
|
1352
112
|
"",
|
|
1353
|
-
|
|
1354
|
-
|
|
113
|
+
chalk.green(` \u2713 Project `) + chalk.bold(projectName),
|
|
114
|
+
chalk.green(` \u2713 Agents `) + chalk.bold(`${agentCount} AI agents configured`),
|
|
1355
115
|
pg,
|
|
1356
116
|
...ol ? [ol] : [],
|
|
1357
117
|
"",
|
|
1358
|
-
|
|
1359
|
-
|
|
118
|
+
chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),
|
|
119
|
+
chalk.dim(" Built by ") + chalk.bold.white("Shahmil Saari"),
|
|
1360
120
|
"",
|
|
1361
|
-
|
|
121
|
+
chalk.bold(" Every AI agent in this project now follows your rules."),
|
|
1362
122
|
"",
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
123
|
+
chalk.gray(" Next steps:"),
|
|
124
|
+
chalk.gray(' memory-core remember "Your architectural decision"'),
|
|
125
|
+
chalk.gray(' memory-core search "query"'),
|
|
126
|
+
chalk.gray(" memory-core sync"),
|
|
1367
127
|
""
|
|
1368
128
|
];
|
|
1369
129
|
lines.forEach((l) => console.log(l));
|
|
1370
130
|
}
|
|
1371
|
-
var { version } = JSON.parse(
|
|
131
|
+
var { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1372
132
|
var CONFIG_FILE = ".memory-core.json";
|
|
1373
133
|
var LOCAL_GENERATED_FILES = [".memory-core-stats.json"];
|
|
1374
134
|
var LOCAL_STATE_FILES = [CONFIG_FILE, ".memory-core.env", ...LOCAL_GENERATED_FILES];
|
|
@@ -1379,13 +139,10 @@ var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
|
|
|
1379
139
|
var DEFAULT_CHAT_MODEL = "llama3.2";
|
|
1380
140
|
var phase1 = getDefaultApplicationContainer();
|
|
1381
141
|
function getEnvPath() {
|
|
1382
|
-
|
|
1383
|
-
if (existsSync3(memoryEnv)) return memoryEnv;
|
|
1384
|
-
const dotEnv = join3(process.cwd(), ".env");
|
|
1385
|
-
return existsSync3(dotEnv) ? dotEnv : memoryEnv;
|
|
142
|
+
return join(process.cwd(), ".memory-core.env");
|
|
1386
143
|
}
|
|
1387
144
|
function getWriteEnvPath() {
|
|
1388
|
-
return
|
|
145
|
+
return join(process.cwd(), ".memory-core.env");
|
|
1389
146
|
}
|
|
1390
147
|
function parseEnvFile(raw) {
|
|
1391
148
|
const lines = raw.split(/\r?\n/);
|
|
@@ -1403,7 +160,7 @@ function parseEnvFile(raw) {
|
|
|
1403
160
|
}
|
|
1404
161
|
function readRuntimeEnv() {
|
|
1405
162
|
const envPath = getEnvPath();
|
|
1406
|
-
const fileValues =
|
|
163
|
+
const fileValues = existsSync(envPath) ? parseEnvFile(readFileSync(envPath, "utf-8")) : {};
|
|
1407
164
|
const values = {
|
|
1408
165
|
...fileValues
|
|
1409
166
|
};
|
|
@@ -1437,7 +194,7 @@ function writeRuntimeEnv(values, envPath = getWriteEnvPath()) {
|
|
|
1437
194
|
const value = values[key];
|
|
1438
195
|
if (value) lines.push(`${key}=${value}`);
|
|
1439
196
|
}
|
|
1440
|
-
|
|
197
|
+
writeFileSync(envPath, `${lines.join("\n")}
|
|
1441
198
|
`, "utf-8");
|
|
1442
199
|
}
|
|
1443
200
|
function applyRuntimeEnv(values) {
|
|
@@ -1446,8 +203,8 @@ function applyRuntimeEnv(values) {
|
|
|
1446
203
|
}
|
|
1447
204
|
}
|
|
1448
205
|
function appendMissingGitignoreEntries(entries, heading) {
|
|
1449
|
-
const gitignorePath =
|
|
1450
|
-
const existing =
|
|
206
|
+
const gitignorePath = join(process.cwd(), ".gitignore");
|
|
207
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
|
|
1451
208
|
const existingEntries = new Set(
|
|
1452
209
|
existing.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
1453
210
|
);
|
|
@@ -1462,9 +219,9 @@ ${toAdd.join("\n")}
|
|
|
1462
219
|
return toAdd.length;
|
|
1463
220
|
}
|
|
1464
221
|
function removeMemoryCoreGitignoreBlock(entries, heading = GITIGNORE_HEADING) {
|
|
1465
|
-
const gitignorePath =
|
|
1466
|
-
if (!
|
|
1467
|
-
const existing =
|
|
222
|
+
const gitignorePath = join(process.cwd(), ".gitignore");
|
|
223
|
+
if (!existsSync(gitignorePath)) return false;
|
|
224
|
+
const existing = readFileSync(gitignorePath, "utf-8");
|
|
1468
225
|
const entrySet = new Set(entries);
|
|
1469
226
|
const lines = existing.split(/\r?\n/);
|
|
1470
227
|
const kept = [];
|
|
@@ -1493,15 +250,15 @@ function removeMemoryCoreGitignoreBlock(entries, heading = GITIGNORE_HEADING) {
|
|
|
1493
250
|
}
|
|
1494
251
|
if (!changed) return false;
|
|
1495
252
|
const content = kept.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/u, "");
|
|
1496
|
-
|
|
253
|
+
writeFileSync(gitignorePath, content ? `${content}
|
|
1497
254
|
` : "", "utf-8");
|
|
1498
255
|
return true;
|
|
1499
256
|
}
|
|
1500
257
|
function removeProjectFiles(relativePaths) {
|
|
1501
258
|
const removed = [];
|
|
1502
259
|
for (const relativePath of [...new Set(relativePaths)]) {
|
|
1503
|
-
const target =
|
|
1504
|
-
if (!
|
|
260
|
+
const target = join(process.cwd(), relativePath);
|
|
261
|
+
if (!existsSync(target)) continue;
|
|
1505
262
|
rmSync(target, { force: true, recursive: true });
|
|
1506
263
|
removed.push(relativePath);
|
|
1507
264
|
}
|
|
@@ -1568,16 +325,16 @@ async function verifyOllamaConnection(ollamaUrl) {
|
|
|
1568
325
|
}
|
|
1569
326
|
}
|
|
1570
327
|
function readProjectConfig() {
|
|
1571
|
-
const path =
|
|
1572
|
-
if (!
|
|
328
|
+
const path = join(process.cwd(), CONFIG_FILE);
|
|
329
|
+
if (!existsSync(path)) return null;
|
|
1573
330
|
try {
|
|
1574
|
-
return JSON.parse(
|
|
331
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
1575
332
|
} catch {
|
|
1576
333
|
return null;
|
|
1577
334
|
}
|
|
1578
335
|
}
|
|
1579
336
|
function writeProjectConfig(config) {
|
|
1580
|
-
|
|
337
|
+
writeFileSync(join(process.cwd(), CONFIG_FILE), JSON.stringify(config, null, 2));
|
|
1581
338
|
}
|
|
1582
339
|
function updateProjectConfig(mutator) {
|
|
1583
340
|
const current = readProjectConfig() ?? {
|
|
@@ -1649,11 +406,11 @@ function toPortableFromRecord(memory) {
|
|
|
1649
406
|
});
|
|
1650
407
|
}
|
|
1651
408
|
function printMemoryTable(memories, title = "Rules in memory") {
|
|
1652
|
-
console.log(
|
|
409
|
+
console.log(chalk.bold(`
|
|
1653
410
|
${title} (${memories.length} total)
|
|
1654
411
|
`));
|
|
1655
|
-
console.log(
|
|
1656
|
-
console.log(
|
|
412
|
+
console.log(chalk.dim(" ID Type Scope Title / Content"));
|
|
413
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1657
414
|
memories.forEach((memory) => {
|
|
1658
415
|
const id = String(memory.id).padEnd(4);
|
|
1659
416
|
const type = memory.type.padEnd(10);
|
|
@@ -1661,19 +418,19 @@ function printMemoryTable(memories, title = "Rules in memory") {
|
|
|
1661
418
|
const label = truncate(memory.title || memory.content, 64);
|
|
1662
419
|
console.log(` ${id} ${type} ${scope} ${label}`);
|
|
1663
420
|
});
|
|
1664
|
-
console.log(
|
|
421
|
+
console.log(chalk.gray("\n Use: memory-core remove <id> | memory-core edit <id>\n"));
|
|
1665
422
|
}
|
|
1666
423
|
function getCurrentListArchitectures(config) {
|
|
1667
424
|
return inferProjectArchitectures(process.cwd(), config).filter((architecture) => architecture !== "global");
|
|
1668
425
|
}
|
|
1669
426
|
function printStatusLine(label, value) {
|
|
1670
|
-
console.log(` ${
|
|
427
|
+
console.log(` ${chalk.dim(label.padEnd(18))} ${value}`);
|
|
1671
428
|
}
|
|
1672
429
|
function abbreviate(value, max = 96) {
|
|
1673
430
|
return value.length > max ? `${value.slice(0, max - 1)}\u2026` : value;
|
|
1674
431
|
}
|
|
1675
432
|
function graphStoreFilePath(cwd = process.cwd()) {
|
|
1676
|
-
return
|
|
433
|
+
return join(cwd, ".memory-core", "graph-snapshots.json");
|
|
1677
434
|
}
|
|
1678
435
|
function graphBackendLabel(values) {
|
|
1679
436
|
return values.DATABASE_URL ? "postgres (file fallback enabled)" : "file";
|
|
@@ -1693,8 +450,8 @@ async function runModelDoctor() {
|
|
|
1693
450
|
const ollamaUrl = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
|
|
1694
451
|
const embeddingModel = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
|
|
1695
452
|
const dbUrl = values.DATABASE_URL ?? "";
|
|
1696
|
-
console.log(
|
|
1697
|
-
printStatusLine("Env file",
|
|
453
|
+
console.log(chalk.bold("\n memory-core model doctor\n"));
|
|
454
|
+
printStatusLine("Env file", existsSync(envPath) ? envPath : `${envPath} ${chalk.yellow("(will be created on first write)")}`);
|
|
1698
455
|
printStatusLine("Provider", provider2);
|
|
1699
456
|
printStatusLine("Chat model", model2);
|
|
1700
457
|
printStatusLine("Embedding model", embeddingModel);
|
|
@@ -1704,56 +461,56 @@ async function runModelDoctor() {
|
|
|
1704
461
|
const dbError = await verifyDatabaseConnection(dbUrl);
|
|
1705
462
|
if (dbError) {
|
|
1706
463
|
ok = false;
|
|
1707
|
-
console.log(
|
|
464
|
+
console.log(chalk.red(" \u2717 PostgreSQL ") + chalk.dim(dbError));
|
|
1708
465
|
} else {
|
|
1709
|
-
console.log(
|
|
466
|
+
console.log(chalk.green(" \u2713 PostgreSQL ") + chalk.dim("connected"));
|
|
1710
467
|
}
|
|
1711
468
|
const ollamaError = await verifyOllamaConnection(ollamaUrl);
|
|
1712
469
|
if (ollamaError) {
|
|
1713
470
|
ok = false;
|
|
1714
|
-
console.log(
|
|
471
|
+
console.log(chalk.red(" \u2717 Ollama ") + chalk.dim(ollamaError));
|
|
1715
472
|
} else {
|
|
1716
|
-
console.log(
|
|
473
|
+
console.log(chalk.green(" \u2713 Ollama ") + chalk.dim("reachable"));
|
|
1717
474
|
}
|
|
1718
475
|
if (!ollamaError) {
|
|
1719
476
|
try {
|
|
1720
477
|
const installedEmbeddingModel = await resolveOllamaInstalledModel(ollamaUrl, embeddingModel);
|
|
1721
478
|
if (installedEmbeddingModel) {
|
|
1722
|
-
console.log(
|
|
479
|
+
console.log(chalk.green(" \u2713 Embedding ") + chalk.dim(`${installedEmbeddingModel} installed`));
|
|
1723
480
|
} else {
|
|
1724
481
|
ok = false;
|
|
1725
|
-
console.log(
|
|
482
|
+
console.log(chalk.red(" \u2717 Embedding ") + chalk.dim(`${embeddingModel} not installed in Ollama`));
|
|
1726
483
|
}
|
|
1727
484
|
} catch (err) {
|
|
1728
485
|
ok = false;
|
|
1729
|
-
console.log(
|
|
486
|
+
console.log(chalk.red(" \u2717 Embedding ") + chalk.dim(err.message));
|
|
1730
487
|
}
|
|
1731
488
|
}
|
|
1732
489
|
if (provider2 === "ollama") {
|
|
1733
490
|
if (ollamaError) {
|
|
1734
491
|
ok = false;
|
|
1735
|
-
console.log(
|
|
492
|
+
console.log(chalk.red(" \u2717 Chat model ") + chalk.dim("cannot verify while Ollama is unreachable"));
|
|
1736
493
|
} else {
|
|
1737
494
|
try {
|
|
1738
495
|
const installedChatModel = await resolveOllamaInstalledModel(ollamaUrl, model2);
|
|
1739
496
|
if (installedChatModel) {
|
|
1740
|
-
console.log(
|
|
497
|
+
console.log(chalk.green(" \u2713 Chat model ") + chalk.dim(`${installedChatModel} installed`));
|
|
1741
498
|
} else {
|
|
1742
499
|
ok = false;
|
|
1743
|
-
console.log(
|
|
500
|
+
console.log(chalk.red(" \u2717 Chat model ") + chalk.dim(`${model2} not installed in Ollama`));
|
|
1744
501
|
}
|
|
1745
502
|
} catch (err) {
|
|
1746
503
|
ok = false;
|
|
1747
|
-
console.log(
|
|
504
|
+
console.log(chalk.red(" \u2717 Chat model ") + chalk.dim(err.message));
|
|
1748
505
|
}
|
|
1749
506
|
}
|
|
1750
507
|
} else {
|
|
1751
508
|
if (!values.CHAT_API_KEY) {
|
|
1752
509
|
ok = false;
|
|
1753
|
-
console.log(
|
|
510
|
+
console.log(chalk.red(` \u2717 ${providerLabel(provider2)} API`) + chalk.dim(" CHAT_API_KEY is missing"));
|
|
1754
511
|
} else {
|
|
1755
|
-
console.log(
|
|
1756
|
-
console.log(
|
|
512
|
+
console.log(chalk.green(` \u2713 ${providerLabel(provider2)} API`) + chalk.dim(" key configured"));
|
|
513
|
+
console.log(chalk.gray(" Remote provider connectivity is not verified live by doctor."));
|
|
1757
514
|
}
|
|
1758
515
|
}
|
|
1759
516
|
console.log();
|
|
@@ -1765,33 +522,33 @@ async function printProjectStatus() {
|
|
|
1765
522
|
const provider2 = getConfiguredProvider(values);
|
|
1766
523
|
const model2 = getConfiguredChatModel(values);
|
|
1767
524
|
const architectures = inferProjectArchitectures(process.cwd(), config);
|
|
1768
|
-
const generatedFiles = OUTPUT_FILES.map((entry) => entry.path).filter((relativePath) =>
|
|
1769
|
-
const hookPath =
|
|
1770
|
-
const memoryFilePath =
|
|
1771
|
-
const statsPath =
|
|
525
|
+
const generatedFiles = OUTPUT_FILES.map((entry) => entry.path).filter((relativePath) => existsSync(join(process.cwd(), relativePath)));
|
|
526
|
+
const hookPath = join(process.cwd(), ".git", "hooks", "pre-commit");
|
|
527
|
+
const memoryFilePath = join(process.cwd(), MEMORY_FILE);
|
|
528
|
+
const statsPath = join(process.cwd(), ".memory-core-stats.json");
|
|
1772
529
|
const dbError = await verifyDatabaseConnection(values.DATABASE_URL ?? "");
|
|
1773
530
|
const ollamaError = await verifyOllamaConnection(values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
|
|
1774
531
|
const graphCount = await getGraphSnapshotCount(process.cwd());
|
|
1775
|
-
console.log(
|
|
532
|
+
console.log(chalk.bold("\n memory-core status\n"));
|
|
1776
533
|
printStatusLine("Project", config?.projectName ?? process.cwd().split("/").pop() ?? "unknown");
|
|
1777
|
-
printStatusLine("Project type", config?.projectType ??
|
|
534
|
+
printStatusLine("Project type", config?.projectType ?? chalk.yellow("not initialized"));
|
|
1778
535
|
printStatusLine("Language", config?.language ?? detectProject().language);
|
|
1779
|
-
printStatusLine("Backend arch", config?.backendArchitecture ??
|
|
1780
|
-
printStatusLine("Frontend fw", config?.frontendFramework ??
|
|
1781
|
-
printStatusLine("Architectures", architectures.length ? architectures.join(", ") :
|
|
1782
|
-
printStatusLine("Agents", config?.agents?.length ? `${config.agents.length} selected` :
|
|
536
|
+
printStatusLine("Backend arch", config?.backendArchitecture ?? chalk.gray("\u2014"));
|
|
537
|
+
printStatusLine("Frontend fw", config?.frontendFramework ?? chalk.gray("\u2014"));
|
|
538
|
+
printStatusLine("Architectures", architectures.length ? architectures.join(", ") : chalk.gray("none detected"));
|
|
539
|
+
printStatusLine("Agents", config?.agents?.length ? `${config.agents.length} selected` : chalk.gray("none saved"));
|
|
1783
540
|
printStatusLine("Caveman", config?.caveman?.enabled ? `enabled (${config.caveman.intensity})` : "disabled");
|
|
1784
541
|
printStatusLine("Auto sync", config?.autoSync === false ? "disabled" : "enabled");
|
|
1785
542
|
printStatusLine("Allow patterns", String(getAllowPatterns(config).length));
|
|
1786
|
-
printStatusLine("Env file", `${
|
|
1787
|
-
printStatusLine("Memory file",
|
|
1788
|
-
printStatusLine("Project config",
|
|
543
|
+
printStatusLine("Env file", `${existsSync(envPath) ? "present" : "missing"} (${envPath.split("/").pop()})`);
|
|
544
|
+
printStatusLine("Memory file", existsSync(memoryFilePath) ? MEMORY_FILE : chalk.gray("not exported"));
|
|
545
|
+
printStatusLine("Project config", existsSync(join(process.cwd(), CONFIG_FILE)) ? CONFIG_FILE : chalk.gray("missing"));
|
|
1789
546
|
printStatusLine("Generated files", String(generatedFiles.length));
|
|
1790
|
-
printStatusLine("Hook",
|
|
1791
|
-
printStatusLine("Stats file",
|
|
547
|
+
printStatusLine("Hook", existsSync(hookPath) ? "installed" : "not installed");
|
|
548
|
+
printStatusLine("Stats file", existsSync(statsPath) ? ".memory-core-stats.json" : chalk.gray("none"));
|
|
1792
549
|
printStatusLine("Graph backend", graphBackendLabel(values));
|
|
1793
550
|
printStatusLine("Graph store", graphStoreFilePath(process.cwd()));
|
|
1794
|
-
printStatusLine("Graph snapshots", graphCount === null ?
|
|
551
|
+
printStatusLine("Graph snapshots", graphCount === null ? chalk.gray("unavailable") : String(graphCount));
|
|
1795
552
|
console.log();
|
|
1796
553
|
printStatusLine("Database URL", redactDatabaseUrl(values.DATABASE_URL ?? ""));
|
|
1797
554
|
printStatusLine("Ollama URL", values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
|
|
@@ -1800,38 +557,38 @@ async function printProjectStatus() {
|
|
|
1800
557
|
printStatusLine("Chat model", model2);
|
|
1801
558
|
console.log();
|
|
1802
559
|
console.log(
|
|
1803
|
-
dbError ?
|
|
560
|
+
dbError ? chalk.red(" \u2717 PostgreSQL ") + chalk.dim(dbError) : chalk.green(" \u2713 PostgreSQL ") + chalk.dim("connected")
|
|
1804
561
|
);
|
|
1805
562
|
console.log(
|
|
1806
|
-
ollamaError ?
|
|
563
|
+
ollamaError ? chalk.red(" \u2717 Ollama ") + chalk.dim(ollamaError) : chalk.green(" \u2713 Ollama ") + chalk.dim("reachable")
|
|
1807
564
|
);
|
|
1808
565
|
if (provider2 !== "ollama") {
|
|
1809
566
|
console.log(
|
|
1810
|
-
values.CHAT_API_KEY ?
|
|
567
|
+
values.CHAT_API_KEY ? chalk.green(` \u2713 ${providerLabel(provider2)} API`) + chalk.dim(" key configured") : chalk.red(` \u2717 ${providerLabel(provider2)} API`) + chalk.dim(" CHAT_API_KEY is missing")
|
|
1811
568
|
);
|
|
1812
569
|
}
|
|
1813
570
|
console.log();
|
|
1814
571
|
}
|
|
1815
572
|
function printMemorySelection(selection, limit = 4) {
|
|
1816
573
|
const active = selection.activeArchitectures.join(", ") || "none detected";
|
|
1817
|
-
console.log(
|
|
574
|
+
console.log(chalk.gray(` Stack filter: ${active}`));
|
|
1818
575
|
const included = selection.decisions.filter((decision) => decision.status === "included");
|
|
1819
576
|
if (included.length > 0) {
|
|
1820
|
-
console.log(
|
|
577
|
+
console.log(chalk.gray(` Included ${included.length}:`));
|
|
1821
578
|
for (const decision of included.slice(0, limit)) {
|
|
1822
|
-
console.log(
|
|
579
|
+
console.log(chalk.gray(` + ${decision.memory.content} (${decision.reason})`));
|
|
1823
580
|
}
|
|
1824
581
|
if (included.length > limit) {
|
|
1825
|
-
console.log(
|
|
582
|
+
console.log(chalk.gray(` \u2026 ${included.length - limit} more included`));
|
|
1826
583
|
}
|
|
1827
584
|
}
|
|
1828
585
|
if (selection.excluded.length > 0) {
|
|
1829
|
-
console.log(
|
|
586
|
+
console.log(chalk.gray(` Excluded ${selection.excluded.length}:`));
|
|
1830
587
|
for (const decision of selection.excluded.slice(0, limit)) {
|
|
1831
|
-
console.log(
|
|
588
|
+
console.log(chalk.gray(` - ${decision.memory.content} (${decision.reason})`));
|
|
1832
589
|
}
|
|
1833
590
|
if (selection.excluded.length > limit) {
|
|
1834
|
-
console.log(
|
|
591
|
+
console.log(chalk.gray(` \u2026 ${selection.excluded.length - limit} more excluded`));
|
|
1835
592
|
}
|
|
1836
593
|
}
|
|
1837
594
|
}
|
|
@@ -1873,22 +630,22 @@ async function syncGeneratedFiles(config, agents, options = {}) {
|
|
|
1873
630
|
agents
|
|
1874
631
|
);
|
|
1875
632
|
spinner.succeed(
|
|
1876
|
-
`Synced \u2014 ${
|
|
633
|
+
`Synced \u2014 ${chalk.green(`${result.written.length} updated`)}, ${chalk.dim(`${result.skipped.length} already up to date`)}`
|
|
1877
634
|
);
|
|
1878
635
|
if (result.written.length > 0) {
|
|
1879
|
-
result.written.forEach((file) => console.log(
|
|
636
|
+
result.written.forEach((file) => console.log(chalk.gray(` \u2713 ${file}`)));
|
|
1880
637
|
}
|
|
1881
638
|
}
|
|
1882
639
|
async function autoSyncGeneratedFiles(config, action, enabled = true) {
|
|
1883
640
|
if (!enabled) {
|
|
1884
|
-
console.log(
|
|
641
|
+
console.log(chalk.gray(" Auto-sync skipped (--no-sync). Run memory-core sync when ready."));
|
|
1885
642
|
return;
|
|
1886
643
|
}
|
|
1887
644
|
if (!config) {
|
|
1888
645
|
return;
|
|
1889
646
|
}
|
|
1890
647
|
if (config.autoSync === false) {
|
|
1891
|
-
console.log(
|
|
648
|
+
console.log(chalk.gray(" Auto-sync disabled for this project. Run memory-core sync when ready."));
|
|
1892
649
|
return;
|
|
1893
650
|
}
|
|
1894
651
|
try {
|
|
@@ -1896,24 +653,24 @@ async function autoSyncGeneratedFiles(config, action, enabled = true) {
|
|
|
1896
653
|
label: `Auto-syncing agent files after ${action}\u2026`
|
|
1897
654
|
});
|
|
1898
655
|
} catch (err) {
|
|
1899
|
-
console.log(
|
|
1900
|
-
console.log(
|
|
656
|
+
console.log(chalk.yellow(` Auto-sync skipped: ${err.message}`));
|
|
657
|
+
console.log(chalk.gray(" Run memory-core sync manually when ready."));
|
|
1901
658
|
}
|
|
1902
659
|
}
|
|
1903
660
|
var program = new Command();
|
|
1904
661
|
program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version(version);
|
|
1905
662
|
program.command("init").description("Initialize memory-core in the current project").option("--quick", "Use smart defaults and skip optional prompts").action(async (opts) => {
|
|
1906
|
-
console.log(
|
|
663
|
+
console.log(chalk.bold.cyan("\n memory-core init\n"));
|
|
1907
664
|
const detected = detectProject();
|
|
1908
665
|
const quick = opts.quick ?? false;
|
|
1909
666
|
let skipEnv = false;
|
|
1910
667
|
let skipProject = false;
|
|
1911
|
-
if (
|
|
668
|
+
if (existsSync(join(process.cwd(), CONFIG_FILE)) && !quick) {
|
|
1912
669
|
const existing = readProjectConfig();
|
|
1913
670
|
const envVals = readRuntimeEnv().values;
|
|
1914
|
-
console.log(
|
|
1915
|
-
console.log(
|
|
1916
|
-
console.log(
|
|
671
|
+
console.log(chalk.dim(` Already initialized: ${existing?.projectName ?? "?"} (${existing?.projectType ?? "?"})`));
|
|
672
|
+
console.log(chalk.dim(` Provider: ${envVals.CHAT_PROVIDER ?? "ollama"} Model: ${envVals.CHAT_MODEL ?? "llama3.2"}`));
|
|
673
|
+
console.log(chalk.dim(` Hook: ${existsSync(join(".git", "hooks", "pre-commit")) ? "installed" : "not installed"} Agents: ${existing?.agents?.length ?? 0}
|
|
1917
674
|
`));
|
|
1918
675
|
const reinitChoice = await select({
|
|
1919
676
|
message: "Already initialized \u2014 what do you want to do?",
|
|
@@ -1933,8 +690,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1933
690
|
}
|
|
1934
691
|
let pgOk = false;
|
|
1935
692
|
let ollamaOk = false;
|
|
1936
|
-
const envPath =
|
|
1937
|
-
const hasEnv =
|
|
693
|
+
const envPath = join(process.cwd(), ".memory-core.env");
|
|
694
|
+
const hasEnv = existsSync(envPath);
|
|
1938
695
|
if (skipEnv) {
|
|
1939
696
|
try {
|
|
1940
697
|
const { Pool } = (await import("pg")).default;
|
|
@@ -1964,9 +721,9 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1964
721
|
writeRuntimeEnv(envValues, envPath);
|
|
1965
722
|
applyRuntimeEnv(envValues);
|
|
1966
723
|
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
1967
|
-
console.log(
|
|
724
|
+
console.log(chalk.green(" \u2713 .memory-core.env created with local defaults"));
|
|
1968
725
|
} else if (!hasEnv) {
|
|
1969
|
-
console.log(
|
|
726
|
+
console.log(chalk.dim(" No .memory-core.env found \u2014 let's set up your connection.\n"));
|
|
1970
727
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
1971
728
|
let dbUrl = "";
|
|
1972
729
|
while (true) {
|
|
@@ -1980,12 +737,12 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1980
737
|
const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
|
|
1981
738
|
await testPool.query("SELECT 1");
|
|
1982
739
|
await testPool.end();
|
|
1983
|
-
pgSpinner.succeed(
|
|
740
|
+
pgSpinner.succeed(chalk.green("PostgreSQL connected"));
|
|
1984
741
|
pgOk = true;
|
|
1985
742
|
break;
|
|
1986
743
|
} catch (err) {
|
|
1987
|
-
pgSpinner.fail(
|
|
1988
|
-
console.log(
|
|
744
|
+
pgSpinner.fail(chalk.red(`Cannot connect: ${err.message}`));
|
|
745
|
+
console.log(chalk.yellow(" Please check that PostgreSQL is running and the URL is correct.\n"));
|
|
1989
746
|
}
|
|
1990
747
|
}
|
|
1991
748
|
let ollamaUrl = "";
|
|
@@ -1998,12 +755,12 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1998
755
|
try {
|
|
1999
756
|
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
2000
757
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2001
|
-
ollamaSpinner.succeed(
|
|
758
|
+
ollamaSpinner.succeed(chalk.green("Ollama connected"));
|
|
2002
759
|
ollamaOk = true;
|
|
2003
760
|
break;
|
|
2004
761
|
} catch (err) {
|
|
2005
|
-
ollamaSpinner.fail(
|
|
2006
|
-
console.log(
|
|
762
|
+
ollamaSpinner.fail(chalk.red(`Cannot reach Ollama: ${err.message}`));
|
|
763
|
+
console.log(chalk.yellow(" Make sure Ollama is running: ollama serve\n"));
|
|
2007
764
|
}
|
|
2008
765
|
}
|
|
2009
766
|
const chatProvider = await select({
|
|
@@ -2017,7 +774,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2017
774
|
]
|
|
2018
775
|
});
|
|
2019
776
|
if (chatProvider !== "ollama") {
|
|
2020
|
-
console.log(
|
|
777
|
+
console.log(chalk.dim(" Note: Ollama is still used for search embeddings. Code checking uses the cloud provider above."));
|
|
2021
778
|
}
|
|
2022
779
|
let chatModel = "";
|
|
2023
780
|
let chatApiKey = "";
|
|
@@ -2045,15 +802,15 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2045
802
|
const match = exact ?? prefixed;
|
|
2046
803
|
if (match) {
|
|
2047
804
|
chatModel = match.name;
|
|
2048
|
-
modelSpinner.succeed(
|
|
805
|
+
modelSpinner.succeed(chalk.green(`${chatModel} is installed and ready`));
|
|
2049
806
|
break;
|
|
2050
807
|
} else {
|
|
2051
|
-
modelSpinner.fail(
|
|
2052
|
-
console.log(
|
|
808
|
+
modelSpinner.fail(chalk.red(`${chatModel} is not installed in your Ollama`));
|
|
809
|
+
console.log(chalk.yellow(` Run: ollama pull ${chatModel} \u2014 or pick a different model.
|
|
2053
810
|
`));
|
|
2054
811
|
}
|
|
2055
812
|
} catch {
|
|
2056
|
-
modelSpinner.warn(
|
|
813
|
+
modelSpinner.warn(chalk.yellow("Could not verify model \u2014 continuing anyway"));
|
|
2057
814
|
break;
|
|
2058
815
|
}
|
|
2059
816
|
}
|
|
@@ -2097,7 +854,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2097
854
|
chatApiKey = await input({
|
|
2098
855
|
message: `${providerLabel(chatProvider)} API key?`
|
|
2099
856
|
});
|
|
2100
|
-
console.log(
|
|
857
|
+
console.log(chalk.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
|
|
2101
858
|
}
|
|
2102
859
|
const envValues = {
|
|
2103
860
|
DATABASE_URL: dbUrl,
|
|
@@ -2112,8 +869,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2112
869
|
writeRuntimeEnv(envValues, envPath);
|
|
2113
870
|
applyRuntimeEnv(envValues);
|
|
2114
871
|
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
2115
|
-
console.log(
|
|
2116
|
-
console.log(
|
|
872
|
+
console.log(chalk.green("\n \u2713 .memory-core.env created"));
|
|
873
|
+
console.log(chalk.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
|
|
2117
874
|
} else {
|
|
2118
875
|
try {
|
|
2119
876
|
const { Pool } = (await import("pg")).default;
|
|
@@ -2150,10 +907,13 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2150
907
|
const backendProfiles = listProfiles("backend");
|
|
2151
908
|
backendArchitecture = await select({
|
|
2152
909
|
message: "Backend architecture?",
|
|
2153
|
-
choices:
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
910
|
+
choices: [
|
|
911
|
+
...backendProfiles.map((p) => ({
|
|
912
|
+
value: p.name,
|
|
913
|
+
name: `${p.displayName} \u2014 ${p.description}`
|
|
914
|
+
})),
|
|
915
|
+
{ value: "custom", name: "Custom / Not defined yet \u2014 I'll add rules with memory-core remember" }
|
|
916
|
+
]
|
|
2157
917
|
});
|
|
2158
918
|
}
|
|
2159
919
|
}
|
|
@@ -2171,10 +931,13 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2171
931
|
const frontendProfiles = listProfiles("frontend");
|
|
2172
932
|
frontendFramework = await select({
|
|
2173
933
|
message: "Frontend framework?",
|
|
2174
|
-
choices:
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
934
|
+
choices: [
|
|
935
|
+
...frontendProfiles.map((p) => ({
|
|
936
|
+
value: p.name,
|
|
937
|
+
name: `${p.displayName} \u2014 ${p.description}`
|
|
938
|
+
})),
|
|
939
|
+
{ value: "custom", name: "Custom / Not defined yet \u2014 I'll add rules with memory-core remember" }
|
|
940
|
+
]
|
|
2178
941
|
});
|
|
2179
942
|
}
|
|
2180
943
|
}
|
|
@@ -2230,18 +993,18 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2230
993
|
}
|
|
2231
994
|
if (!quick) {
|
|
2232
995
|
const envVals = readRuntimeEnv().values;
|
|
2233
|
-
console.log(
|
|
2234
|
-
console.log(` Project ${
|
|
2235
|
-
if (backendArchitecture) console.log(` Backend ${
|
|
2236
|
-
if (frontendFramework) console.log(` Frontend ${
|
|
2237
|
-
console.log(` Language ${
|
|
2238
|
-
console.log(` Provider ${
|
|
2239
|
-
console.log(` Agents ${
|
|
2240
|
-
console.log(` Hook ${
|
|
996
|
+
console.log(chalk.bold("\n Ready to initialize\n"));
|
|
997
|
+
console.log(` Project ${chalk.white(projectName)} (${projectType})`);
|
|
998
|
+
if (backendArchitecture) console.log(` Backend ${chalk.white(backendArchitecture === "custom" ? "Custom (no profile)" : backendArchitecture)}`);
|
|
999
|
+
if (frontendFramework) console.log(` Frontend ${chalk.white(frontendFramework === "custom" ? "Custom (no profile)" : frontendFramework)}`);
|
|
1000
|
+
console.log(` Language ${chalk.white(language)}`);
|
|
1001
|
+
console.log(` Provider ${chalk.white(envVals.CHAT_PROVIDER ?? "ollama")} / ${chalk.white(envVals.CHAT_MODEL ?? DEFAULT_CHAT_MODEL)}`);
|
|
1002
|
+
console.log(` Agents ${chalk.white(String(selectedAgents.length))} selected`);
|
|
1003
|
+
console.log(` Hook ${chalk.white(enableHook ? hookAdvisory ? "advisory" : "strict" : "skip")}`);
|
|
2241
1004
|
console.log();
|
|
2242
1005
|
const proceed = await confirm({ message: "Generate files?", default: true });
|
|
2243
1006
|
if (!proceed) {
|
|
2244
|
-
console.log(
|
|
1007
|
+
console.log(chalk.yellow(" Cancelled.\n"));
|
|
2245
1008
|
await closePool();
|
|
2246
1009
|
process.exit(0);
|
|
2247
1010
|
}
|
|
@@ -2260,16 +1023,16 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2260
1023
|
};
|
|
2261
1024
|
let memories = [];
|
|
2262
1025
|
try {
|
|
2263
|
-
const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
|
|
1026
|
+
const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).filter((a) => a !== "custom").join(" ");
|
|
2264
1027
|
const selection = await retrieveMemorySelection({
|
|
2265
|
-
query: archQuery,
|
|
1028
|
+
query: archQuery || language,
|
|
2266
1029
|
cwd: process.cwd(),
|
|
2267
1030
|
config,
|
|
2268
1031
|
limit: 20
|
|
2269
1032
|
});
|
|
2270
1033
|
memories = selection.included;
|
|
2271
1034
|
if (memories.length > 0) {
|
|
2272
|
-
console.log(
|
|
1035
|
+
console.log(chalk.dim(` Found ${memories.length} relevant memories`));
|
|
2273
1036
|
printMemorySelection(selection);
|
|
2274
1037
|
}
|
|
2275
1038
|
} catch {
|
|
@@ -2291,11 +1054,39 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2291
1054
|
);
|
|
2292
1055
|
writeProjectConfig(config);
|
|
2293
1056
|
spinner.succeed(`Generated ${written.written.length} files`);
|
|
1057
|
+
try {
|
|
1058
|
+
const archmindDir = join(process.cwd(), ".archmind");
|
|
1059
|
+
mkdirSync(archmindDir, { recursive: true });
|
|
1060
|
+
const layersPath = join(archmindDir, "layers.json");
|
|
1061
|
+
const rulesPath = join(archmindDir, "rules.json");
|
|
1062
|
+
const archmindGitignorePath = join(archmindDir, ".gitignore");
|
|
1063
|
+
if (!existsSync(layersPath)) {
|
|
1064
|
+
const arch2 = backendArchitecture ?? frontendFramework ?? "custom";
|
|
1065
|
+
const layersTemplate = buildLayersTemplate(arch2);
|
|
1066
|
+
writeFileSync(layersPath, JSON.stringify(layersTemplate, null, 2) + "\n", "utf-8");
|
|
1067
|
+
console.log(chalk.green(" \u2713 Created .archmind/layers.json"));
|
|
1068
|
+
}
|
|
1069
|
+
if (!existsSync(rulesPath)) {
|
|
1070
|
+
writeFileSync(rulesPath, JSON.stringify(DEFAULT_ARCH_RULES, null, 2) + "\n", "utf-8");
|
|
1071
|
+
console.log(chalk.green(" \u2713 Created .archmind/rules.json"));
|
|
1072
|
+
}
|
|
1073
|
+
if (!existsSync(archmindGitignorePath)) {
|
|
1074
|
+
writeFileSync(archmindGitignorePath, "approval-queue.json\nwatch-errors.json\n", "utf-8");
|
|
1075
|
+
}
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
const graphSpinner = ora(" Building dependency graph snapshot\u2026").start();
|
|
1080
|
+
const { snapshot } = await phase1.services.graphEngine.buildAndStoreSnapshot({ cwd: process.cwd() });
|
|
1081
|
+
graphSpinner.succeed(` Graph snapshot ready (${snapshot.nodes.length} nodes, ${snapshot.edges.length} edges)`);
|
|
1082
|
+
} catch {
|
|
1083
|
+
console.log(chalk.dim(" (graph build skipped \u2014 run `memory-core graph build` once manually)"));
|
|
1084
|
+
}
|
|
2294
1085
|
const gitignoreEntries = [...written.written, ...LOCAL_GENERATED_FILES];
|
|
2295
1086
|
if (gitignoreEntries.length > 0) {
|
|
2296
1087
|
const added = appendMissingGitignoreEntries(gitignoreEntries, GITIGNORE_HEADING);
|
|
2297
1088
|
if (added > 0) {
|
|
2298
|
-
console.log(
|
|
1089
|
+
console.log(chalk.green(` \u2713 Added ${added} generated files to .gitignore`));
|
|
2299
1090
|
}
|
|
2300
1091
|
}
|
|
2301
1092
|
if (enableHook) {
|
|
@@ -2303,12 +1094,19 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2303
1094
|
}
|
|
2304
1095
|
const chatModelForBanner = process.env.CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
|
|
2305
1096
|
printBanner(config.projectName, written.written.length, { postgresOk: pgOk, ollamaOk, chatModel: chatModelForBanner });
|
|
1097
|
+
if (backendArchitecture === "custom" || frontendFramework === "custom") {
|
|
1098
|
+
console.log(chalk.yellow("\n Custom architecture \u2014 no profile rules loaded."));
|
|
1099
|
+
console.log(chalk.dim(" Add rules as your architecture takes shape:"));
|
|
1100
|
+
console.log(chalk.dim(' memory-core remember "Your rule" --type rule'));
|
|
1101
|
+
console.log(chalk.dim(" Or load a profile later when you decide:"));
|
|
1102
|
+
console.log(chalk.dim(" memory-core seed --arch clean-architecture\n"));
|
|
1103
|
+
}
|
|
2306
1104
|
await closePool();
|
|
2307
1105
|
});
|
|
2308
1106
|
program.command("sync").description("Re-pull memories and regenerate AI agent files").action(async () => {
|
|
2309
1107
|
const config = readProjectConfig();
|
|
2310
1108
|
if (!config) {
|
|
2311
|
-
console.error(
|
|
1109
|
+
console.error(chalk.red("No .memory-core.json found. Run: memory-core init"));
|
|
2312
1110
|
process.exit(1);
|
|
2313
1111
|
}
|
|
2314
1112
|
const { checkbox } = await import("@inquirer/prompts");
|
|
@@ -2323,7 +1121,7 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
2323
1121
|
instructions: " (Space to toggle, A to select all, Enter to confirm)"
|
|
2324
1122
|
});
|
|
2325
1123
|
if (selectedAgents.length === 0) {
|
|
2326
|
-
console.log(
|
|
1124
|
+
console.log(chalk.yellow(" No agents selected \u2014 nothing to sync."));
|
|
2327
1125
|
process.exit(0);
|
|
2328
1126
|
}
|
|
2329
1127
|
await syncGeneratedFiles(config, [...selectedAgents, "Shared"], { showSelection: true });
|
|
@@ -2332,24 +1130,24 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
2332
1130
|
program.command("auto-sync [mode]").description("Show or change automatic agent file sync (on|off)").action((mode) => {
|
|
2333
1131
|
const config = readProjectConfig();
|
|
2334
1132
|
if (!config) {
|
|
2335
|
-
console.error(
|
|
1133
|
+
console.error(chalk.red("No .memory-core.json found. Run: memory-core init"));
|
|
2336
1134
|
process.exit(1);
|
|
2337
1135
|
}
|
|
2338
1136
|
const normalized = mode?.trim().toLowerCase();
|
|
2339
1137
|
if (!normalized || normalized === "status") {
|
|
2340
|
-
console.log(
|
|
2341
|
-
console.log(` Status: ${config.autoSync === false ?
|
|
2342
|
-
console.log(
|
|
1138
|
+
console.log(chalk.bold("\n Auto-sync\n"));
|
|
1139
|
+
console.log(` Status: ${config.autoSync === false ? chalk.yellow("disabled") : chalk.green("enabled")}`);
|
|
1140
|
+
console.log(chalk.gray(" Manual sync is always available: memory-core sync\n"));
|
|
2343
1141
|
return;
|
|
2344
1142
|
}
|
|
2345
1143
|
if (normalized !== "on" && normalized !== "off") {
|
|
2346
|
-
console.error(
|
|
1144
|
+
console.error(chalk.red("Use: memory-core auto-sync [on|off|status]"));
|
|
2347
1145
|
process.exit(1);
|
|
2348
1146
|
}
|
|
2349
1147
|
const enabled = normalized === "on";
|
|
2350
1148
|
writeProjectConfig({ ...config, autoSync: enabled });
|
|
2351
|
-
console.log(
|
|
2352
|
-
console.log(
|
|
1149
|
+
console.log(chalk.green(`Auto-sync ${enabled ? "enabled" : "disabled"}`));
|
|
1150
|
+
console.log(chalk.gray(" Manual sync is always available: memory-core sync"));
|
|
2353
1151
|
});
|
|
2354
1152
|
program.command("remember <text>").description("Save a new memory to the central database").option("-t, --type <type>", "Memory type (decision|rule|pattern|note)", "decision").option("-s, --scope <scope>", "Scope (global|project)", "project").option("--tags <tags>", "Comma-separated tags").option("-r, --reason <reason>", "Why this rule exists \u2014 helps agents understand intent and debug violations").option("--applies-to <items>", "Comma-separated situations where this memory applies").option("--avoid-when <items>", "Comma-separated situations where this memory should not be used").option("--example <items>", "Comma-separated examples that teach agents how to apply this memory").option("--source <source>", "Human-readable source for this memory").option("--no-sync", "Skip automatic agent file sync after saving").action(async (text, opts) => {
|
|
2355
1153
|
const config = readProjectConfig();
|
|
@@ -2357,7 +1155,7 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
2357
1155
|
let reason = opts.reason;
|
|
2358
1156
|
if (!reason) {
|
|
2359
1157
|
reason = await input({
|
|
2360
|
-
message:
|
|
1158
|
+
message: chalk.dim("Why should this memory exist?"),
|
|
2361
1159
|
default: ""
|
|
2362
1160
|
});
|
|
2363
1161
|
}
|
|
@@ -2374,11 +1172,11 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
2374
1172
|
context: buildMemoryContext(opts),
|
|
2375
1173
|
tags: parseTags(opts.tags)
|
|
2376
1174
|
});
|
|
2377
|
-
const dbVersionPath =
|
|
2378
|
-
|
|
2379
|
-
const reasonLine =
|
|
1175
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
1176
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
1177
|
+
const reasonLine = chalk.gray(`
|
|
2380
1178
|
Why: ${storedReason}`);
|
|
2381
|
-
spinner.succeed(
|
|
1179
|
+
spinner.succeed(chalk.green(`Memory saved: "${text}"`) + reasonLine);
|
|
2382
1180
|
await autoSyncGeneratedFiles(config, "remember", opts.sync);
|
|
2383
1181
|
} catch (err) {
|
|
2384
1182
|
spinner.fail(`Failed: ${err.message}`);
|
|
@@ -2399,20 +1197,20 @@ program.command("search <query>").description("Search memories using semantic si
|
|
|
2399
1197
|
const results = result.items;
|
|
2400
1198
|
spinner.stop();
|
|
2401
1199
|
if (results.length === 0) {
|
|
2402
|
-
console.log(
|
|
1200
|
+
console.log(chalk.yellow("No memories found."));
|
|
2403
1201
|
} else {
|
|
2404
|
-
console.log(
|
|
1202
|
+
console.log(chalk.bold(`
|
|
2405
1203
|
${results.length} results for "${query}"
|
|
2406
1204
|
`));
|
|
2407
1205
|
results.forEach((m, i) => {
|
|
2408
|
-
const sim = m.similarity ?
|
|
2409
|
-
console.log(
|
|
2410
|
-
console.log(
|
|
2411
|
-
if (m.reason) console.log(
|
|
2412
|
-
if (m.context?.appliesTo?.length) console.log(
|
|
2413
|
-
if (m.context?.avoidWhen?.length) console.log(
|
|
2414
|
-
if (m.context?.examples?.length) console.log(
|
|
2415
|
-
if (m.tags?.length) console.log(
|
|
1206
|
+
const sim = m.similarity ? chalk.gray(` (${(m.similarity * 100).toFixed(0)}% match)`) : "";
|
|
1207
|
+
console.log(chalk.cyan(` ${i + 1}. [${m.type}] ${m.title ?? ""}`));
|
|
1208
|
+
console.log(chalk.white(` ${m.content}`) + sim);
|
|
1209
|
+
if (m.reason) console.log(chalk.gray(` why: ${m.reason}`));
|
|
1210
|
+
if (m.context?.appliesTo?.length) console.log(chalk.gray(` use when: ${m.context.appliesTo.join("; ")}`));
|
|
1211
|
+
if (m.context?.avoidWhen?.length) console.log(chalk.gray(` avoid when: ${m.context.avoidWhen.join("; ")}`));
|
|
1212
|
+
if (m.context?.examples?.length) console.log(chalk.gray(` examples: ${m.context.examples.join("; ")}`));
|
|
1213
|
+
if (m.tags?.length) console.log(chalk.gray(` tags: ${m.tags.join(", ")}`));
|
|
2416
1214
|
console.log();
|
|
2417
1215
|
});
|
|
2418
1216
|
}
|
|
@@ -2427,9 +1225,9 @@ program.command("export").description(`Export DB memories to ${MEMORY_FILE}`).op
|
|
|
2427
1225
|
try {
|
|
2428
1226
|
const memories = await phase1.services.memoryEngine.list({ limit: 1e4 });
|
|
2429
1227
|
const portable = memories.map(toPortableFromRecord);
|
|
2430
|
-
const outputPath = opts.output ?
|
|
1228
|
+
const outputPath = opts.output ? join(process.cwd(), opts.output) : writeMemoryFile(portable);
|
|
2431
1229
|
if (opts.output) {
|
|
2432
|
-
|
|
1230
|
+
writeFileSync(outputPath, JSON.stringify(portable, null, 2) + "\n", "utf-8");
|
|
2433
1231
|
}
|
|
2434
1232
|
spinner.succeed(`Exported ${portable.length} memories to ${outputPath}`);
|
|
2435
1233
|
} catch (err) {
|
|
@@ -2443,7 +1241,7 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
|
|
|
2443
1241
|
const spinner = ora("Reading memories\u2026").start();
|
|
2444
1242
|
try {
|
|
2445
1243
|
const config = readProjectConfig();
|
|
2446
|
-
const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(
|
|
1244
|
+
const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(readFileSync(join(process.cwd(), opts.file), "utf-8")) : readMemoryFile();
|
|
2447
1245
|
let inserted = 0;
|
|
2448
1246
|
let skipped = 0;
|
|
2449
1247
|
spinner.text = `Importing ${memories.length} memories\u2026`;
|
|
@@ -2464,8 +1262,8 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
|
|
|
2464
1262
|
}
|
|
2465
1263
|
spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
|
|
2466
1264
|
if (inserted > 0) {
|
|
2467
|
-
const dbVersionPath =
|
|
2468
|
-
|
|
1265
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
1266
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2469
1267
|
await autoSyncGeneratedFiles(config, "import", opts.sync);
|
|
2470
1268
|
}
|
|
2471
1269
|
} catch (err) {
|
|
@@ -2491,10 +1289,10 @@ program.command("list").description("List memories from the local database").opt
|
|
|
2491
1289
|
const title = opts.all ? "All memories" : `Current project memories${architectures ? ` (${Array.isArray(architectures) ? architectures.join(", ") : architectures})` : ""}`;
|
|
2492
1290
|
printMemoryTable(memories.map(toMemoryTableRow), title);
|
|
2493
1291
|
if (!opts.all) {
|
|
2494
|
-
console.log(
|
|
1292
|
+
console.log(chalk.gray(" Showing current project context plus shared/global memories. Use --all for the full database.\n"));
|
|
2495
1293
|
}
|
|
2496
1294
|
} catch (err) {
|
|
2497
|
-
console.error(
|
|
1295
|
+
console.error(chalk.red(`List failed: ${err.message}`));
|
|
2498
1296
|
process.exit(1);
|
|
2499
1297
|
} finally {
|
|
2500
1298
|
await closePool();
|
|
@@ -2505,13 +1303,13 @@ program.command("remove <id>").description("Remove a memory by ID").option("--no
|
|
|
2505
1303
|
const config = readProjectConfig();
|
|
2506
1304
|
const deleted = await phase1.services.memoryEngine.removeById(parseInt(id, 10));
|
|
2507
1305
|
if (!deleted) {
|
|
2508
|
-
console.log(
|
|
1306
|
+
console.log(chalk.yellow(`No memory found with ID ${id}`));
|
|
2509
1307
|
process.exit(1);
|
|
2510
1308
|
}
|
|
2511
|
-
console.log(
|
|
1309
|
+
console.log(chalk.green(`Removed memory ${id}`));
|
|
2512
1310
|
await autoSyncGeneratedFiles(config, "remove", opts.sync);
|
|
2513
1311
|
} catch (err) {
|
|
2514
|
-
console.error(
|
|
1312
|
+
console.error(chalk.red(`Remove failed: ${err.message}`));
|
|
2515
1313
|
process.exit(1);
|
|
2516
1314
|
} finally {
|
|
2517
1315
|
await closePool();
|
|
@@ -2526,12 +1324,12 @@ program.command("forget").description("Bulk-delete memories by tag, scope, type,
|
|
|
2526
1324
|
type: opts.type,
|
|
2527
1325
|
architecture: opts.arch
|
|
2528
1326
|
});
|
|
2529
|
-
console.log(
|
|
1327
|
+
console.log(chalk.green(`Deleted ${deleted} memories`));
|
|
2530
1328
|
if (deleted > 0) {
|
|
2531
1329
|
await autoSyncGeneratedFiles(config, "forget", opts.sync);
|
|
2532
1330
|
}
|
|
2533
1331
|
} catch (err) {
|
|
2534
|
-
console.error(
|
|
1332
|
+
console.error(chalk.red(`Forget failed: ${err.message}`));
|
|
2535
1333
|
process.exit(1);
|
|
2536
1334
|
} finally {
|
|
2537
1335
|
await closePool();
|
|
@@ -2543,7 +1341,7 @@ program.command("edit <id>").description("Edit a memory interactively").option("
|
|
|
2543
1341
|
const config = readProjectConfig();
|
|
2544
1342
|
const existing = await phase1.services.memoryEngine.getById(memoryId);
|
|
2545
1343
|
if (!existing) {
|
|
2546
|
-
console.log(
|
|
1344
|
+
console.log(chalk.yellow(`No memory found with ID ${id}`));
|
|
2547
1345
|
process.exit(1);
|
|
2548
1346
|
}
|
|
2549
1347
|
const type = await input({ message: "Type?", default: existing.type });
|
|
@@ -2565,10 +1363,10 @@ program.command("edit <id>").description("Edit a memory interactively").option("
|
|
|
2565
1363
|
context: buildMemoryContext({ appliesTo, avoidWhen, example: examples, source }),
|
|
2566
1364
|
tags: parseTags(tags)
|
|
2567
1365
|
});
|
|
2568
|
-
console.log(
|
|
1366
|
+
console.log(chalk.green(`Updated memory ${id}`));
|
|
2569
1367
|
await autoSyncGeneratedFiles(config, "edit", opts.sync);
|
|
2570
1368
|
} catch (err) {
|
|
2571
|
-
console.error(
|
|
1369
|
+
console.error(chalk.red(`Edit failed: ${err.message}`));
|
|
2572
1370
|
process.exit(1);
|
|
2573
1371
|
} finally {
|
|
2574
1372
|
await closePool();
|
|
@@ -2585,15 +1383,15 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
2585
1383
|
if (opts.remove) {
|
|
2586
1384
|
const deleted = await phase1.services.memoryEngine.removeById(parseInt(opts.remove, 10));
|
|
2587
1385
|
if (!deleted) {
|
|
2588
|
-
console.log(
|
|
1386
|
+
console.log(chalk.yellow(`No ignore pattern found with ID ${opts.remove}`));
|
|
2589
1387
|
process.exit(1);
|
|
2590
1388
|
}
|
|
2591
|
-
console.log(
|
|
1389
|
+
console.log(chalk.green(`Removed ignore pattern ${opts.remove}`));
|
|
2592
1390
|
await autoSyncGeneratedFiles(config, "ignore remove", opts.sync);
|
|
2593
1391
|
return;
|
|
2594
1392
|
}
|
|
2595
1393
|
if (!pattern) {
|
|
2596
|
-
console.error(
|
|
1394
|
+
console.error(chalk.red("Provide a pattern, --list, or --remove <id>"));
|
|
2597
1395
|
process.exit(1);
|
|
2598
1396
|
}
|
|
2599
1397
|
await phase1.services.memoryEngine.remember({
|
|
@@ -2604,12 +1402,12 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
2604
1402
|
content: pattern,
|
|
2605
1403
|
tags: ["ignore"]
|
|
2606
1404
|
});
|
|
2607
|
-
const dbVersionPath =
|
|
2608
|
-
|
|
2609
|
-
console.log(
|
|
1405
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
1406
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
1407
|
+
console.log(chalk.green(`Ignored pattern saved: "${pattern}"`));
|
|
2610
1408
|
await autoSyncGeneratedFiles(config, "ignore", opts.sync);
|
|
2611
1409
|
} catch (err) {
|
|
2612
|
-
console.error(
|
|
1410
|
+
console.error(chalk.red(`Ignore failed: ${err.message}`));
|
|
2613
1411
|
process.exit(1);
|
|
2614
1412
|
} finally {
|
|
2615
1413
|
await closePool();
|
|
@@ -2619,10 +1417,10 @@ program.command("allow [pattern]").description("Manage project allow patterns in
|
|
|
2619
1417
|
if (opts.list) {
|
|
2620
1418
|
const patterns = getAllowPatterns(readProjectConfig());
|
|
2621
1419
|
if (patterns.length === 0) {
|
|
2622
|
-
console.log(
|
|
1420
|
+
console.log(chalk.yellow("\n No allow patterns configured.\n"));
|
|
2623
1421
|
return;
|
|
2624
1422
|
}
|
|
2625
|
-
console.log(
|
|
1423
|
+
console.log(chalk.bold("\n Allow patterns\n"));
|
|
2626
1424
|
patterns.forEach((entry, index) => console.log(` ${index + 1}. ${entry}`));
|
|
2627
1425
|
console.log();
|
|
2628
1426
|
return;
|
|
@@ -2632,35 +1430,35 @@ program.command("allow [pattern]").description("Manage project allow patterns in
|
|
|
2632
1430
|
...config,
|
|
2633
1431
|
allowPatterns: getAllowPatterns(config).filter((entry) => entry !== opts.remove)
|
|
2634
1432
|
}));
|
|
2635
|
-
console.log(
|
|
1433
|
+
console.log(chalk.green(`Removed allow pattern: "${opts.remove}"`));
|
|
2636
1434
|
return;
|
|
2637
1435
|
}
|
|
2638
1436
|
if (!pattern) {
|
|
2639
|
-
console.error(
|
|
1437
|
+
console.error(chalk.red("Provide a pattern, --list, or --remove <pattern>"));
|
|
2640
1438
|
process.exit(1);
|
|
2641
1439
|
}
|
|
2642
1440
|
updateProjectConfig((config) => ({
|
|
2643
1441
|
...config,
|
|
2644
1442
|
allowPatterns: [.../* @__PURE__ */ new Set([...getAllowPatterns(config), pattern])]
|
|
2645
1443
|
}));
|
|
2646
|
-
console.log(
|
|
1444
|
+
console.log(chalk.green(`Allow pattern saved: "${pattern}"`));
|
|
2647
1445
|
});
|
|
2648
1446
|
program.command("commit-rules [pattern]").description("Manage commit message rules in .memory-core.json").option("--message <msg>", "Error message shown when rule is violated (required when adding)").option("--negate", "Pattern must NOT match (default: must match)").option("--advisory", "Warn only \u2014 do not block commit").option("--list", "List current commit rules").option("--remove <pattern>", "Remove a commit rule by pattern").action((pattern, opts) => {
|
|
2649
1447
|
if (opts.list) {
|
|
2650
1448
|
const rules = readProjectConfig()?.commitRules ?? [];
|
|
2651
1449
|
if (rules.length === 0) {
|
|
2652
|
-
console.log(
|
|
1450
|
+
console.log(chalk.yellow("\n No commit rules configured.\n"));
|
|
2653
1451
|
return;
|
|
2654
1452
|
}
|
|
2655
|
-
console.log(
|
|
1453
|
+
console.log(chalk.bold("\n Commit message rules\n"));
|
|
2656
1454
|
rules.forEach((rule, i) => {
|
|
2657
1455
|
const flags = [
|
|
2658
1456
|
rule.negate ? "must NOT match" : "must match",
|
|
2659
1457
|
rule.advisory ? "advisory" : "blocking"
|
|
2660
1458
|
].join(", ");
|
|
2661
1459
|
console.log(` ${i + 1}. ${rule.pattern}`);
|
|
2662
|
-
console.log(
|
|
2663
|
-
console.log(
|
|
1460
|
+
console.log(chalk.dim(` Message: ${rule.message}`));
|
|
1461
|
+
console.log(chalk.dim(` Flags: ${flags}`));
|
|
2664
1462
|
console.log();
|
|
2665
1463
|
});
|
|
2666
1464
|
return;
|
|
@@ -2670,21 +1468,21 @@ program.command("commit-rules [pattern]").description("Manage commit message rul
|
|
|
2670
1468
|
...config,
|
|
2671
1469
|
commitRules: (config.commitRules ?? []).filter((r) => r.pattern !== opts.remove)
|
|
2672
1470
|
}));
|
|
2673
|
-
console.log(
|
|
1471
|
+
console.log(chalk.green(`Commit rule removed: "${opts.remove}"`));
|
|
2674
1472
|
return;
|
|
2675
1473
|
}
|
|
2676
1474
|
if (!pattern) {
|
|
2677
|
-
console.error(
|
|
1475
|
+
console.error(chalk.red("Provide a pattern, --list, or --remove <pattern>"));
|
|
2678
1476
|
process.exit(1);
|
|
2679
1477
|
}
|
|
2680
1478
|
if (!opts.message) {
|
|
2681
|
-
console.error(
|
|
1479
|
+
console.error(chalk.red("--message is required when adding a commit rule"));
|
|
2682
1480
|
process.exit(1);
|
|
2683
1481
|
}
|
|
2684
1482
|
try {
|
|
2685
1483
|
new RegExp(pattern);
|
|
2686
1484
|
} catch {
|
|
2687
|
-
console.error(
|
|
1485
|
+
console.error(chalk.red(`Invalid regex pattern: "${pattern}"`));
|
|
2688
1486
|
process.exit(1);
|
|
2689
1487
|
}
|
|
2690
1488
|
const newRule = {
|
|
@@ -2697,13 +1495,13 @@ program.command("commit-rules [pattern]").description("Manage commit message rul
|
|
|
2697
1495
|
...config,
|
|
2698
1496
|
commitRules: [...(config.commitRules ?? []).filter((r) => r.pattern !== pattern), newRule]
|
|
2699
1497
|
}));
|
|
2700
|
-
console.log(
|
|
2701
|
-
console.log(
|
|
1498
|
+
console.log(chalk.green(`Commit rule saved: "${pattern}"`));
|
|
1499
|
+
console.log(chalk.dim(" Run: memory-core commit-rules --list to see all rules"));
|
|
2702
1500
|
});
|
|
2703
1501
|
program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
|
|
2704
|
-
const workflowPath =
|
|
1502
|
+
const workflowPath = join(process.cwd(), ".github", "workflows", "memory-core.yml");
|
|
2705
1503
|
mkdirSync(dirname(workflowPath), { recursive: true });
|
|
2706
|
-
|
|
1504
|
+
writeFileSync(workflowPath, `name: memory-core
|
|
2707
1505
|
on: [pull_request]
|
|
2708
1506
|
jobs:
|
|
2709
1507
|
check:
|
|
@@ -2714,7 +1512,7 @@ jobs:
|
|
|
2714
1512
|
fetch-depth: 0
|
|
2715
1513
|
- run: npx @shahmilsaari/memory-core check --ci
|
|
2716
1514
|
`, "utf-8");
|
|
2717
|
-
console.log(
|
|
1515
|
+
console.log(chalk.green(`Generated ${workflowPath}`));
|
|
2718
1516
|
});
|
|
2719
1517
|
program.command("reset").description("Remove memory-core generated files and local project config").option("--soft", "Only remove generated files; keep config and DB").option("--db", "Also drop the memories table after confirmation").action(async (opts) => {
|
|
2720
1518
|
let removed = removeProjectFiles(OUTPUT_FILES.map((file) => file.path)).length;
|
|
@@ -2730,14 +1528,14 @@ program.command("reset").description("Remove memory-core generated files and loc
|
|
|
2730
1528
|
if (ok) {
|
|
2731
1529
|
await getPool().query("DROP TABLE IF EXISTS memories");
|
|
2732
1530
|
await closePool();
|
|
2733
|
-
console.log(
|
|
1531
|
+
console.log(chalk.yellow("Dropped memories table"));
|
|
2734
1532
|
}
|
|
2735
1533
|
}
|
|
2736
|
-
console.log(
|
|
1534
|
+
console.log(chalk.green(`Reset complete. Removed ${removed} files.`));
|
|
2737
1535
|
});
|
|
2738
1536
|
program.command("uninstall").description("Remove memory-core from the current project").option("--db", "Also drop the memories table after confirmation").action(async (opts) => {
|
|
2739
1537
|
const generatedFiles = OUTPUT_FILES.map((file) => file.path);
|
|
2740
|
-
const gitignoreEntries = [...generatedFiles, ...
|
|
1538
|
+
const gitignoreEntries = [...generatedFiles, ...LOCAL_STATE_FILES];
|
|
2741
1539
|
const removed = removeProjectFiles([
|
|
2742
1540
|
...generatedFiles,
|
|
2743
1541
|
...LOCAL_STATE_FILES,
|
|
@@ -2753,19 +1551,19 @@ program.command("uninstall").description("Remove memory-core from the current pr
|
|
|
2753
1551
|
if (ok) {
|
|
2754
1552
|
await getPool().query("DROP TABLE IF EXISTS memories");
|
|
2755
1553
|
await closePool();
|
|
2756
|
-
console.log(
|
|
1554
|
+
console.log(chalk.yellow("Dropped memories table"));
|
|
2757
1555
|
}
|
|
2758
1556
|
}
|
|
2759
|
-
console.log(
|
|
1557
|
+
console.log(chalk.green(`Uninstall complete. Removed ${removed.length} files.`));
|
|
2760
1558
|
if (removed.length > 0) {
|
|
2761
|
-
removed.forEach((file) => console.log(
|
|
1559
|
+
removed.forEach((file) => console.log(chalk.gray(` \u2713 ${file}`)));
|
|
2762
1560
|
}
|
|
2763
1561
|
if (cleanedGitignore) {
|
|
2764
|
-
console.log(
|
|
1562
|
+
console.log(chalk.gray(" \u2713 cleaned .gitignore memory-core block"));
|
|
2765
1563
|
}
|
|
2766
1564
|
});
|
|
2767
1565
|
program.command("stats").description("Show violation counters recorded by check and watch").option("--reset", "Reset violation counters and recent history").option("--tune", "Show only noisy rules (>40% false-positive rate) with disable commands").action((opts) => {
|
|
2768
|
-
const statsPath =
|
|
1566
|
+
const statsPath = join(process.cwd(), ".memory-core-stats.json");
|
|
2769
1567
|
if (opts.reset) {
|
|
2770
1568
|
const emptyStats = {
|
|
2771
1569
|
rules: {},
|
|
@@ -2777,27 +1575,27 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2777
1575
|
},
|
|
2778
1576
|
recentViolations: []
|
|
2779
1577
|
};
|
|
2780
|
-
|
|
2781
|
-
console.log(
|
|
1578
|
+
writeFileSync(statsPath, JSON.stringify(emptyStats, null, 2) + "\n", "utf-8");
|
|
1579
|
+
console.log(chalk.green("\n Violation stats reset.\n"));
|
|
2782
1580
|
return;
|
|
2783
1581
|
}
|
|
2784
|
-
if (!
|
|
2785
|
-
console.log(
|
|
1582
|
+
if (!existsSync(statsPath)) {
|
|
1583
|
+
console.log(chalk.yellow("\n No violation stats recorded yet.\n"));
|
|
2786
1584
|
return;
|
|
2787
1585
|
}
|
|
2788
|
-
const stats = JSON.parse(
|
|
1586
|
+
const stats = JSON.parse(readFileSync(statsPath, "utf-8"));
|
|
2789
1587
|
const toEntry = (raw) => {
|
|
2790
1588
|
if (raw === void 0) return { count: 0, falsePositives: 0 };
|
|
2791
1589
|
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
2792
1590
|
return raw;
|
|
2793
1591
|
};
|
|
2794
1592
|
const printTop = (label, values = {}) => {
|
|
2795
|
-
console.log(
|
|
1593
|
+
console.log(chalk.bold(`
|
|
2796
1594
|
${label}
|
|
2797
1595
|
`));
|
|
2798
1596
|
Object.entries(values).map(([name, raw]) => ({ name, entry: toEntry(raw) })).sort((a, b) => b.entry.count - a.entry.count).slice(0, 10).forEach(({ name, entry }, index) => {
|
|
2799
1597
|
const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
|
|
2800
|
-
const fpHint = rate > 0 ?
|
|
1598
|
+
const fpHint = rate > 0 ? chalk.dim(` \u2014 ${rate}% false-positive rate`) + (rate > 25 ? " \u26A0\uFE0F" : "") : "";
|
|
2801
1599
|
console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${entry.count} hits${fpHint}`);
|
|
2802
1600
|
});
|
|
2803
1601
|
};
|
|
@@ -2810,18 +1608,18 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2810
1608
|
rate: entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0
|
|
2811
1609
|
})).filter((r) => r.rate > tuneThreshold && r.count >= tuneMinCount).sort((a, b) => b.rate - a.rate);
|
|
2812
1610
|
if (noisy.length === 0) {
|
|
2813
|
-
console.log(
|
|
1611
|
+
console.log(chalk.green(`
|
|
2814
1612
|
\u2713 No noisy rules found (threshold: ${tuneThreshold}%, min hits: ${tuneMinCount})
|
|
2815
1613
|
`));
|
|
2816
1614
|
return;
|
|
2817
1615
|
}
|
|
2818
|
-
console.log(
|
|
1616
|
+
console.log(chalk.bold(`
|
|
2819
1617
|
Noisy rules (>${tuneThreshold}% false-positive rate, \u2265${tuneMinCount} hits)
|
|
2820
1618
|
`));
|
|
2821
1619
|
noisy.forEach(({ name, count, rate }, i) => {
|
|
2822
1620
|
console.log(` ${i + 1}. ${truncate(name, 50).padEnd(52)} ${count} hits \u2014 ${rate}% \u26A0\uFE0F`);
|
|
2823
|
-
console.log(
|
|
2824
|
-
console.log(
|
|
1621
|
+
console.log(chalk.dim(` To disable: memory-core allow "${name}"`));
|
|
1622
|
+
console.log(chalk.dim(` Interactive: memory-core tune`));
|
|
2825
1623
|
console.log();
|
|
2826
1624
|
});
|
|
2827
1625
|
return;
|
|
@@ -2839,20 +1637,20 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2839
1637
|
hasLiveState ? liveFiles : stats.files
|
|
2840
1638
|
);
|
|
2841
1639
|
if (!hasLiveState) {
|
|
2842
|
-
console.log(
|
|
2843
|
-
console.log(
|
|
1640
|
+
console.log(chalk.dim("\n Note: these counters are historical events, not live current code state."));
|
|
1641
|
+
console.log(chalk.dim(" Start watch for live counters, or reset with: memory-core stats --reset\n"));
|
|
2844
1642
|
} else {
|
|
2845
1643
|
if (hasLiveViolations) {
|
|
2846
|
-
console.log(
|
|
1644
|
+
console.log(chalk.dim("\n Live counters auto-refresh while watch is running.\n"));
|
|
2847
1645
|
} else {
|
|
2848
|
-
console.log(
|
|
1646
|
+
console.log(chalk.dim("\n Current live state has no violations.\n"));
|
|
2849
1647
|
}
|
|
2850
1648
|
}
|
|
2851
1649
|
});
|
|
2852
1650
|
program.command("tune").description("Review and disable noisy rules with high false-positive rates").option("--threshold <percent>", "False-positive rate % above which a rule is noisy (default: 40)", "40").option("--min-count <n>", "Minimum hit count required to consider a rule (default: 5)", "5").option("--yes", "Disable all noisy rules without prompting").action(async (opts) => {
|
|
2853
|
-
const statsPath =
|
|
2854
|
-
if (!
|
|
2855
|
-
console.log(
|
|
1651
|
+
const statsPath = join(process.cwd(), ".memory-core-stats.json");
|
|
1652
|
+
if (!existsSync(statsPath)) {
|
|
1653
|
+
console.log(chalk.yellow("\n No violation stats yet. Run some commits first.\n"));
|
|
2856
1654
|
return;
|
|
2857
1655
|
}
|
|
2858
1656
|
const threshold = Math.max(0, Math.min(100, parseInt(opts.threshold, 10) || 40));
|
|
@@ -2862,19 +1660,19 @@ program.command("tune").description("Review and disable noisy rules with high fa
|
|
|
2862
1660
|
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
2863
1661
|
return raw;
|
|
2864
1662
|
};
|
|
2865
|
-
const stats = JSON.parse(
|
|
1663
|
+
const stats = JSON.parse(readFileSync(statsPath, "utf-8"));
|
|
2866
1664
|
const noisy = Object.entries(stats.rules ?? {}).map(([rule, raw]) => {
|
|
2867
1665
|
const entry = toEntry(raw);
|
|
2868
1666
|
const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
|
|
2869
1667
|
return { rule, count: entry.count, rate };
|
|
2870
1668
|
}).filter((r) => r.rate > threshold && r.count >= minCount).sort((a, b) => b.rate - a.rate);
|
|
2871
1669
|
if (noisy.length === 0) {
|
|
2872
|
-
console.log(
|
|
1670
|
+
console.log(chalk.green(`
|
|
2873
1671
|
\u2713 All rules within acceptable noise (threshold: ${threshold}%, min hits: ${minCount})
|
|
2874
1672
|
`));
|
|
2875
1673
|
return;
|
|
2876
1674
|
}
|
|
2877
|
-
console.log(
|
|
1675
|
+
console.log(chalk.bold(`
|
|
2878
1676
|
Found ${noisy.length} noisy rule${noisy.length > 1 ? "s" : ""} (>${threshold}% false-positive rate, \u2265${minCount} hits)
|
|
2879
1677
|
`));
|
|
2880
1678
|
const existingAllows = new Set(
|
|
@@ -2885,22 +1683,22 @@ program.command("tune").description("Review and disable noisy rules with high fa
|
|
|
2885
1683
|
for (const { rule, count, rate } of noisy) {
|
|
2886
1684
|
const key = rule.toLowerCase();
|
|
2887
1685
|
if (existingAllows.has(key)) {
|
|
2888
|
-
console.log(
|
|
1686
|
+
console.log(chalk.dim(` \u2022 "${truncate(rule, 56)}" \u2014 already disabled`));
|
|
2889
1687
|
continue;
|
|
2890
1688
|
}
|
|
2891
1689
|
toAdd.add(key);
|
|
2892
|
-
console.log(
|
|
1690
|
+
console.log(chalk.green(` \u2713 "${truncate(rule, 56)}"`) + chalk.dim(` \u2014 ${count} hits, ${rate}% FP rate`));
|
|
2893
1691
|
}
|
|
2894
1692
|
} else {
|
|
2895
1693
|
const { select: select2 } = await import("@inquirer/prompts");
|
|
2896
1694
|
for (let i = 0; i < noisy.length; i++) {
|
|
2897
1695
|
const { rule, count, rate } = noisy[i];
|
|
2898
1696
|
const key = rule.toLowerCase();
|
|
2899
|
-
console.log(
|
|
1697
|
+
console.log(chalk.bold(`
|
|
2900
1698
|
[${i + 1}/${noisy.length}] "${truncate(rule, 60)}"`));
|
|
2901
|
-
console.log(
|
|
1699
|
+
console.log(chalk.dim(` ${count} hits \u2014 ${rate}% false-positive rate \u26A0\uFE0F`));
|
|
2902
1700
|
if (existingAllows.has(key)) {
|
|
2903
|
-
console.log(
|
|
1701
|
+
console.log(chalk.dim(" Already in allow patterns \u2014 skipping"));
|
|
2904
1702
|
continue;
|
|
2905
1703
|
}
|
|
2906
1704
|
const choice = await select2({
|
|
@@ -2914,7 +1712,7 @@ program.command("tune").description("Review and disable noisy rules with high fa
|
|
|
2914
1712
|
if (choice === "quit") break;
|
|
2915
1713
|
if (choice === "disable") {
|
|
2916
1714
|
toAdd.add(key);
|
|
2917
|
-
console.log(
|
|
1715
|
+
console.log(chalk.green(" \u2713 Marked for disable"));
|
|
2918
1716
|
}
|
|
2919
1717
|
}
|
|
2920
1718
|
}
|
|
@@ -2923,11 +1721,11 @@ program.command("tune").description("Review and disable noisy rules with high fa
|
|
|
2923
1721
|
...config,
|
|
2924
1722
|
allowPatterns: [.../* @__PURE__ */ new Set([...config.allowPatterns ?? [], ...toAdd])]
|
|
2925
1723
|
}));
|
|
2926
|
-
console.log(
|
|
1724
|
+
console.log(chalk.green(`
|
|
2927
1725
|
\u2713 Saved ${toAdd.size} allow pattern${toAdd.size > 1 ? "s" : ""} to .memory-core.json`));
|
|
2928
|
-
console.log(
|
|
1726
|
+
console.log(chalk.dim(" These rules will no longer block commits.\n"));
|
|
2929
1727
|
} else {
|
|
2930
|
-
console.log(
|
|
1728
|
+
console.log(chalk.dim("\n No changes made.\n"));
|
|
2931
1729
|
}
|
|
2932
1730
|
});
|
|
2933
1731
|
program.command("dashboard").description("Start the live Svelte dashboard with WebSocket watch events").option("-p, --port <port>", "Dashboard port", "5178").option("--path <dir>", "Directory to watch (default: current directory)").option("--no-watch", "Serve the dashboard without starting file watch").action(async (opts) => {
|
|
@@ -2947,7 +1745,7 @@ program.command("dashboard").description("Start the live Svelte dashboard with W
|
|
|
2947
1745
|
}
|
|
2948
1746
|
return void 0;
|
|
2949
1747
|
};
|
|
2950
|
-
const { startDashboard } = await import("./dashboard-server-
|
|
1748
|
+
const { startDashboard } = await import("./dashboard-server-EEFNE6NX.js");
|
|
2951
1749
|
await startDashboard({
|
|
2952
1750
|
port: parseInt(opts.port, 10),
|
|
2953
1751
|
path: resolveDashboardPath(),
|
|
@@ -2957,7 +1755,7 @@ program.command("dashboard").description("Start the live Svelte dashboard with W
|
|
|
2957
1755
|
program.command("seed").description("Load all predefined memories into the database").option("--arch <architecture>", "Only seed a specific architecture (e.g. clean-architecture)").option("--force", "Re-seed even if memories already exist", false).action(async (opts) => {
|
|
2958
1756
|
await runMigrations();
|
|
2959
1757
|
const filtered = opts.arch ? seeds.filter((s) => s.architecture === opts.arch || s.architecture === "global") : seeds;
|
|
2960
|
-
console.log(
|
|
1758
|
+
console.log(chalk.bold.cyan(`
|
|
2961
1759
|
Seeding ${filtered.length} memories\u2026
|
|
2962
1760
|
`));
|
|
2963
1761
|
let saved = 0;
|
|
@@ -2977,15 +1775,15 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
2977
1775
|
if (opts.force) {
|
|
2978
1776
|
await phase1.services.memoryEngine.rememberForce(payload);
|
|
2979
1777
|
saved++;
|
|
2980
|
-
spinner.succeed(
|
|
1778
|
+
spinner.succeed(chalk.gray(`[${seed.architecture}] ${seed.title}`));
|
|
2981
1779
|
} else {
|
|
2982
1780
|
const result = await phase1.services.memoryEngine.remember(payload);
|
|
2983
1781
|
if (result === "inserted") {
|
|
2984
1782
|
saved++;
|
|
2985
|
-
spinner.succeed(
|
|
1783
|
+
spinner.succeed(chalk.gray(`[${seed.architecture}] ${seed.title}`));
|
|
2986
1784
|
} else {
|
|
2987
1785
|
skipped++;
|
|
2988
|
-
spinner.info(
|
|
1786
|
+
spinner.info(chalk.gray(`Already exists \u2014 [${seed.architecture}] ${seed.title}`));
|
|
2989
1787
|
}
|
|
2990
1788
|
}
|
|
2991
1789
|
} catch (err) {
|
|
@@ -2994,10 +1792,10 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
2994
1792
|
}
|
|
2995
1793
|
}
|
|
2996
1794
|
if (saved > 0) {
|
|
2997
|
-
const dbVersionPath =
|
|
2998
|
-
|
|
1795
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
1796
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2999
1797
|
}
|
|
3000
|
-
console.log(
|
|
1798
|
+
console.log(chalk.bold.green(`
|
|
3001
1799
|
Done. ${saved} memories seeded, ${skipped} skipped.
|
|
3002
1800
|
`));
|
|
3003
1801
|
await closePool();
|
|
@@ -3006,21 +1804,21 @@ program.command("global").description("Sync your memory into every AI agent glob
|
|
|
3006
1804
|
const home = homedir();
|
|
3007
1805
|
const GLOBAL_TARGETS = [
|
|
3008
1806
|
// Claude Code
|
|
3009
|
-
{ label: "Claude Code", path:
|
|
1807
|
+
{ label: "Claude Code", path: join(home, ".claude/CLAUDE.md"), type: "md" },
|
|
3010
1808
|
// GitHub Copilot (VS Code)
|
|
3011
|
-
{ label: "Copilot", path:
|
|
1809
|
+
{ label: "Copilot", path: join(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-copilot" },
|
|
3012
1810
|
// Cursor global rules
|
|
3013
|
-
{ label: "Cursor", path:
|
|
1811
|
+
{ label: "Cursor", path: join(home, ".cursor/rules/memory-core.mdc"), type: "md" },
|
|
3014
1812
|
// Cline (VS Code)
|
|
3015
|
-
{ label: "Cline", path:
|
|
1813
|
+
{ label: "Cline", path: join(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-cline" },
|
|
3016
1814
|
// Continue.dev global config
|
|
3017
|
-
{ label: "Continue.dev", path:
|
|
1815
|
+
{ label: "Continue.dev", path: join(home, ".continue/config.json"), type: "continue" },
|
|
3018
1816
|
// Aider global config
|
|
3019
|
-
{ label: "Aider", path:
|
|
1817
|
+
{ label: "Aider", path: join(home, ".aider.conf.yml"), type: "aider" },
|
|
3020
1818
|
// Zed global settings
|
|
3021
|
-
{ label: "Zed AI", path:
|
|
1819
|
+
{ label: "Zed AI", path: join(home, ".config/zed/settings.json"), type: "zed" },
|
|
3022
1820
|
// Windsurf global rules
|
|
3023
|
-
{ label: "Windsurf", path:
|
|
1821
|
+
{ label: "Windsurf", path: join(home, ".windsurf/rules/memory-core.md"), type: "md" }
|
|
3024
1822
|
];
|
|
3025
1823
|
const spinner = ora("Fetching global memories\u2026").start();
|
|
3026
1824
|
let memories = [];
|
|
@@ -3050,12 +1848,12 @@ ${rulesText}
|
|
|
3050
1848
|
const skipped = [];
|
|
3051
1849
|
const writeFile = (filePath, content) => {
|
|
3052
1850
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
3053
|
-
|
|
1851
|
+
writeFileSync(filePath, content, "utf-8");
|
|
3054
1852
|
};
|
|
3055
1853
|
const readJson = (filePath) => {
|
|
3056
|
-
if (!
|
|
1854
|
+
if (!existsSync(filePath)) return {};
|
|
3057
1855
|
try {
|
|
3058
|
-
return JSON.parse(
|
|
1856
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
3059
1857
|
} catch {
|
|
3060
1858
|
return {};
|
|
3061
1859
|
}
|
|
@@ -3103,14 +1901,14 @@ read:
|
|
|
3103
1901
|
skipped.push(target.label);
|
|
3104
1902
|
}
|
|
3105
1903
|
}
|
|
3106
|
-
spinner.succeed(
|
|
3107
|
-
console.log(
|
|
3108
|
-
written.forEach((l) => console.log(
|
|
1904
|
+
spinner.succeed(chalk.green(`Synced ${memories.length} memories \u2192 ${written.length} agents`));
|
|
1905
|
+
console.log(chalk.green("\n Updated:"));
|
|
1906
|
+
written.forEach((l) => console.log(chalk.gray(` \u2713 ${l}`)));
|
|
3109
1907
|
if (skipped.length) {
|
|
3110
|
-
console.log(
|
|
3111
|
-
skipped.forEach((l) => console.log(
|
|
1908
|
+
console.log(chalk.yellow("\n Skipped (not installed):"));
|
|
1909
|
+
skipped.forEach((l) => console.log(chalk.gray(` \u2717 ${l}`)));
|
|
3112
1910
|
}
|
|
3113
|
-
console.log(
|
|
1911
|
+
console.log(chalk.bold("\n Every AI agent now follows your memory globally.\n"));
|
|
3114
1912
|
await closePool();
|
|
3115
1913
|
});
|
|
3116
1914
|
var provider = program.command("provider").description("Manage the code-checking provider configuration");
|
|
@@ -3156,10 +1954,10 @@ provider.command("set <name>").description("Set the code-checking provider (olla
|
|
|
3156
1954
|
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
3157
1955
|
applyRuntimeEnv(values);
|
|
3158
1956
|
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
3159
|
-
console.log(
|
|
3160
|
-
console.log(
|
|
1957
|
+
console.log(chalk.green(`Updated provider: ${providerName}`));
|
|
1958
|
+
console.log(chalk.gray(` Chat model: ${getConfiguredChatModel(values)}`));
|
|
3161
1959
|
} catch (err) {
|
|
3162
|
-
console.error(
|
|
1960
|
+
console.error(chalk.red(`Provider update failed: ${err.message}`));
|
|
3163
1961
|
process.exit(1);
|
|
3164
1962
|
}
|
|
3165
1963
|
});
|
|
@@ -3181,10 +1979,10 @@ model.command("set <name>").description("Set the chat model used for code checki
|
|
|
3181
1979
|
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
3182
1980
|
applyRuntimeEnv(values);
|
|
3183
1981
|
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
3184
|
-
console.log(
|
|
3185
|
-
console.log(
|
|
1982
|
+
console.log(chalk.green(`Updated ${opts.embedding ? "embedding" : "chat"} model: ${name}`));
|
|
1983
|
+
console.log(chalk.gray(` Provider: ${providerName}`));
|
|
3186
1984
|
} catch (err) {
|
|
3187
|
-
console.error(
|
|
1985
|
+
console.error(chalk.red(`Model update failed: ${err.message}`));
|
|
3188
1986
|
process.exit(1);
|
|
3189
1987
|
}
|
|
3190
1988
|
});
|
|
@@ -3196,7 +1994,7 @@ program.command("status").description("Show the current memory-core project and
|
|
|
3196
1994
|
try {
|
|
3197
1995
|
await printProjectStatus();
|
|
3198
1996
|
} catch (err) {
|
|
3199
|
-
console.error(
|
|
1997
|
+
console.error(chalk.red(`Status failed: ${err.message}`));
|
|
3200
1998
|
process.exit(1);
|
|
3201
1999
|
}
|
|
3202
2000
|
});
|
|
@@ -3204,7 +2002,7 @@ var graph = program.command("graph").description("Build and inspect dependency g
|
|
|
3204
2002
|
graph.command("migrate").description("Create or update PostgreSQL graph snapshot schema").action(async () => {
|
|
3205
2003
|
const { values } = readRuntimeEnv();
|
|
3206
2004
|
if (!values.DATABASE_URL) {
|
|
3207
|
-
console.error(
|
|
2005
|
+
console.error(chalk.red("Graph migration requires DATABASE_URL. Configure it in .memory-core.env or .env."));
|
|
3208
2006
|
process.exit(1);
|
|
3209
2007
|
}
|
|
3210
2008
|
const spinner = ora("Migrating graph snapshot schema\u2026").start();
|
|
@@ -3222,34 +2020,34 @@ graph.command("doctor").description("Inspect graph storage backend health (Postg
|
|
|
3222
2020
|
const filePath = graphStoreFilePath(cwd);
|
|
3223
2021
|
const usesPostgres = Boolean(values.DATABASE_URL);
|
|
3224
2022
|
let ok = true;
|
|
3225
|
-
console.log(
|
|
2023
|
+
console.log(chalk.bold("\n graph doctor\n"));
|
|
3226
2024
|
printStatusLine("Project path", cwd);
|
|
3227
2025
|
printStatusLine("Configured backend", usesPostgres ? "postgres + file fallback" : "file");
|
|
3228
2026
|
printStatusLine("File store", filePath);
|
|
3229
2027
|
console.log();
|
|
3230
2028
|
if (!usesPostgres) {
|
|
3231
|
-
console.log(
|
|
2029
|
+
console.log(chalk.yellow(" \u26A0 DATABASE_URL not set \u2014 using file backend only."));
|
|
3232
2030
|
} else {
|
|
3233
2031
|
try {
|
|
3234
2032
|
await migrateGraphSnapshots();
|
|
3235
2033
|
const probe = await probeGraphSnapshotStore(cwd);
|
|
3236
|
-
console.log(
|
|
2034
|
+
console.log(chalk.green(" \u2713 Graph PostgreSQL") + chalk.dim(` ready (${probe.snapshotCount} snapshot records for this root)`));
|
|
3237
2035
|
} catch (err) {
|
|
3238
2036
|
ok = false;
|
|
3239
|
-
console.log(
|
|
2037
|
+
console.log(chalk.red(" \u2717 Graph PostgreSQL") + chalk.dim(` ${err.message}`));
|
|
3240
2038
|
}
|
|
3241
2039
|
}
|
|
3242
2040
|
try {
|
|
3243
2041
|
const count = await getGraphSnapshotCount(cwd);
|
|
3244
|
-
console.log(
|
|
2042
|
+
console.log(chalk.green(" \u2713 Graph service") + chalk.dim(` readable (${count ?? 0} snapshots visible)`));
|
|
3245
2043
|
} catch (err) {
|
|
3246
2044
|
ok = false;
|
|
3247
|
-
console.log(
|
|
2045
|
+
console.log(chalk.red(" \u2717 Graph service") + chalk.dim(` ${err.message}`));
|
|
3248
2046
|
}
|
|
3249
|
-
if (
|
|
3250
|
-
console.log(
|
|
2047
|
+
if (existsSync(filePath)) {
|
|
2048
|
+
console.log(chalk.green(" \u2713 File fallback") + chalk.dim(" store exists"));
|
|
3251
2049
|
} else {
|
|
3252
|
-
console.log(
|
|
2050
|
+
console.log(chalk.yellow(" \u26A0 File fallback") + chalk.dim(" store not created yet (run graph build once)"));
|
|
3253
2051
|
}
|
|
3254
2052
|
console.log();
|
|
3255
2053
|
if (!ok) process.exit(1);
|
|
@@ -3260,9 +2058,9 @@ graph.command("build").description("Build a dependency graph snapshot and persis
|
|
|
3260
2058
|
try {
|
|
3261
2059
|
const { snapshot } = await phase1.services.graphEngine.buildAndStoreSnapshot({ cwd });
|
|
3262
2060
|
spinner.succeed(`Graph snapshot saved: ${snapshot.id}`);
|
|
3263
|
-
console.log(
|
|
3264
|
-
console.log(
|
|
3265
|
-
console.log(
|
|
2061
|
+
console.log(chalk.gray(` Root: ${snapshot.rootPath}`));
|
|
2062
|
+
console.log(chalk.gray(` Nodes: ${snapshot.nodes.length}`));
|
|
2063
|
+
console.log(chalk.gray(` Edges: ${snapshot.edges.length}
|
|
3266
2064
|
`));
|
|
3267
2065
|
} catch (err) {
|
|
3268
2066
|
spinner.fail(`Graph build failed: ${err.message}`);
|
|
@@ -3274,16 +2072,16 @@ graph.command("list").description("List saved dependency graph snapshots").optio
|
|
|
3274
2072
|
try {
|
|
3275
2073
|
const snapshots = await phase1.services.graphEngine.listSnapshots(cwd, parseInt(opts.limit, 10));
|
|
3276
2074
|
if (snapshots.length === 0) {
|
|
3277
|
-
console.log(
|
|
2075
|
+
console.log(chalk.yellow("\n No graph snapshots found. Run: memory-core graph build\n"));
|
|
3278
2076
|
return;
|
|
3279
2077
|
}
|
|
3280
|
-
console.log(
|
|
2078
|
+
console.log(chalk.bold("\n Graph snapshots\n"));
|
|
3281
2079
|
snapshots.forEach((snapshot, index) => {
|
|
3282
|
-
console.log(` ${index + 1}. ${snapshot.id} ${
|
|
2080
|
+
console.log(` ${index + 1}. ${snapshot.id} ${chalk.dim(snapshot.createdAt)} nodes=${snapshot.nodes.length} edges=${snapshot.edges.length}`);
|
|
3283
2081
|
});
|
|
3284
2082
|
console.log();
|
|
3285
2083
|
} catch (err) {
|
|
3286
|
-
console.error(
|
|
2084
|
+
console.error(chalk.red(`Graph list failed: ${err.message}`));
|
|
3287
2085
|
process.exit(1);
|
|
3288
2086
|
}
|
|
3289
2087
|
});
|
|
@@ -3292,30 +2090,30 @@ graph.command("show [snapshotId]").description("Show a saved graph snapshot (lat
|
|
|
3292
2090
|
try {
|
|
3293
2091
|
const snapshot = snapshotId ? await phase1.services.graphEngine.getSnapshot(cwd, snapshotId) : await phase1.services.graphEngine.latest(cwd);
|
|
3294
2092
|
if (!snapshot) {
|
|
3295
|
-
console.log(
|
|
2093
|
+
console.log(chalk.yellow("\n No matching graph snapshot found. Run: memory-core graph build\n"));
|
|
3296
2094
|
return;
|
|
3297
2095
|
}
|
|
3298
|
-
console.log(
|
|
2096
|
+
console.log(chalk.bold(`
|
|
3299
2097
|
Graph ${snapshot.id ?? "(latest)"}
|
|
3300
2098
|
`));
|
|
3301
|
-
console.log(
|
|
3302
|
-
console.log(
|
|
3303
|
-
console.log(
|
|
3304
|
-
console.log(
|
|
2099
|
+
console.log(chalk.gray(` Root: ${snapshot.rootPath}`));
|
|
2100
|
+
console.log(chalk.gray(` Created: ${snapshot.createdAt ?? "unknown"}`));
|
|
2101
|
+
console.log(chalk.gray(` Nodes: ${snapshot.nodes.length}`));
|
|
2102
|
+
console.log(chalk.gray(` Edges: ${snapshot.edges.length}
|
|
3305
2103
|
`));
|
|
3306
2104
|
const limit = parseInt(opts.edges, 10);
|
|
3307
2105
|
snapshot.edges.slice(0, limit).forEach((edge, index) => {
|
|
3308
|
-
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${
|
|
2106
|
+
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${chalk.dim(`--${edge.kind}-->`)} ${abbreviate(edge.to)}`);
|
|
3309
2107
|
});
|
|
3310
2108
|
if (snapshot.edges.length > limit) {
|
|
3311
|
-
console.log(
|
|
2109
|
+
console.log(chalk.dim(`
|
|
3312
2110
|
\u2026 ${snapshot.edges.length - limit} more edges not shown
|
|
3313
2111
|
`));
|
|
3314
2112
|
} else {
|
|
3315
2113
|
console.log();
|
|
3316
2114
|
}
|
|
3317
2115
|
} catch (err) {
|
|
3318
|
-
console.error(
|
|
2116
|
+
console.error(chalk.red(`Graph show failed: ${err.message}`));
|
|
3319
2117
|
process.exit(1);
|
|
3320
2118
|
}
|
|
3321
2119
|
});
|
|
@@ -3324,37 +2122,37 @@ graph.command("diff <leftSnapshotId> [rightSnapshotId]").description("Diff two s
|
|
|
3324
2122
|
try {
|
|
3325
2123
|
const left = await phase1.services.graphEngine.getSnapshot(cwd, leftSnapshotId);
|
|
3326
2124
|
if (!left) {
|
|
3327
|
-
console.error(
|
|
2125
|
+
console.error(chalk.red(`Left snapshot not found: ${leftSnapshotId}`));
|
|
3328
2126
|
process.exit(1);
|
|
3329
2127
|
}
|
|
3330
2128
|
const right = rightSnapshotId ? await phase1.services.graphEngine.getSnapshot(cwd, rightSnapshotId) : await phase1.services.graphEngine.latest(cwd);
|
|
3331
2129
|
if (!right) {
|
|
3332
|
-
console.error(
|
|
2130
|
+
console.error(chalk.red(`Right snapshot not found${rightSnapshotId ? `: ${rightSnapshotId}` : ""}`));
|
|
3333
2131
|
process.exit(1);
|
|
3334
2132
|
}
|
|
3335
2133
|
const diff = phase1.services.graphEngine.diffGraphs(left, right);
|
|
3336
|
-
console.log(
|
|
3337
|
-
console.log(
|
|
3338
|
-
console.log(
|
|
3339
|
-
console.log(
|
|
3340
|
-
console.log(
|
|
3341
|
-
console.log(
|
|
3342
|
-
console.log(
|
|
2134
|
+
console.log(chalk.bold("\n Graph diff\n"));
|
|
2135
|
+
console.log(chalk.gray(` Left: ${left.id}`));
|
|
2136
|
+
console.log(chalk.gray(` Right: ${right.id}`));
|
|
2137
|
+
console.log(chalk.green(` + Nodes: ${diff.addedNodes.length}`));
|
|
2138
|
+
console.log(chalk.red(` - Nodes: ${diff.removedNodes.length}`));
|
|
2139
|
+
console.log(chalk.green(` + Edges: ${diff.addedEdges.length}`));
|
|
2140
|
+
console.log(chalk.red(` - Edges: ${diff.removedEdges.length}`));
|
|
3343
2141
|
if (diff.addedEdges.length > 0) {
|
|
3344
|
-
console.log(
|
|
2142
|
+
console.log(chalk.green("\n Added edges"));
|
|
3345
2143
|
diff.addedEdges.slice(0, 20).forEach((edge, index) => {
|
|
3346
|
-
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${
|
|
2144
|
+
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${chalk.dim(`--${edge.kind}-->`)} ${abbreviate(edge.to)}`);
|
|
3347
2145
|
});
|
|
3348
2146
|
}
|
|
3349
2147
|
if (diff.removedEdges.length > 0) {
|
|
3350
|
-
console.log(
|
|
2148
|
+
console.log(chalk.red("\n Removed edges"));
|
|
3351
2149
|
diff.removedEdges.slice(0, 20).forEach((edge, index) => {
|
|
3352
|
-
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${
|
|
2150
|
+
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${chalk.dim(`--${edge.kind}-->`)} ${abbreviate(edge.to)}`);
|
|
3353
2151
|
});
|
|
3354
2152
|
}
|
|
3355
2153
|
console.log();
|
|
3356
2154
|
} catch (err) {
|
|
3357
|
-
console.error(
|
|
2155
|
+
console.error(chalk.red(`Graph diff failed: ${err.message}`));
|
|
3358
2156
|
process.exit(1);
|
|
3359
2157
|
}
|
|
3360
2158
|
});
|
|
@@ -3366,15 +2164,302 @@ hook.command("install").description("Install pre-commit hook (advisory mode by d
|
|
|
3366
2164
|
hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
|
|
3367
2165
|
uninstallHook();
|
|
3368
2166
|
});
|
|
3369
|
-
|
|
2167
|
+
hook.command("bypass-prompt").description("Prompt developer for a bypass reason and save it as a rule (called automatically by hook)").action(async () => {
|
|
2168
|
+
if (!process.stdin.isTTY) return;
|
|
2169
|
+
const bypassStats = readBypassStats();
|
|
2170
|
+
const unrecorded = bypassStats.withoutReason;
|
|
2171
|
+
if (unrecorded > 0) {
|
|
2172
|
+
console.log(
|
|
2173
|
+
chalk.yellow(`
|
|
2174
|
+
\u26A0 ${unrecorded} bypass${unrecorded > 1 ? "es" : ""} in this project had no reason recorded \u2014 lost signal.`)
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
const config = readProjectConfig();
|
|
2178
|
+
let reason = await input({
|
|
2179
|
+
message: chalk.yellow("Why are you bypassing the hook?"),
|
|
2180
|
+
default: ""
|
|
2181
|
+
});
|
|
2182
|
+
if (!reason.trim()) {
|
|
2183
|
+
reason = await input({
|
|
2184
|
+
message: chalk.dim(" Skipping loses signal. Enter a reason or leave blank to proceed:"),
|
|
2185
|
+
default: ""
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
const hadReason = reason.trim().length > 0;
|
|
2189
|
+
const stats = recordBypass(hadReason);
|
|
2190
|
+
if (!hadReason) {
|
|
2191
|
+
console.log(
|
|
2192
|
+
chalk.dim(` Bypass recorded without reason. (${stats.withoutReason} total unrecorded \u2014 run: memory-core stats)
|
|
2193
|
+
`)
|
|
2194
|
+
);
|
|
2195
|
+
await closePool();
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
try {
|
|
2199
|
+
await phase1.services.memoryEngine.remember({
|
|
2200
|
+
type: "rule",
|
|
2201
|
+
scope: "project",
|
|
2202
|
+
architecture: config?.backendArchitecture ?? config?.frontendFramework,
|
|
2203
|
+
projectName: config?.projectName,
|
|
2204
|
+
content: reason.trim(),
|
|
2205
|
+
reason: "Captured automatically from hook bypass"
|
|
2206
|
+
});
|
|
2207
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
2208
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2209
|
+
console.log(chalk.green(` \u2713 Bypass reason saved: "${reason.trim()}"
|
|
2210
|
+
`));
|
|
2211
|
+
} catch {
|
|
2212
|
+
}
|
|
2213
|
+
await closePool();
|
|
2214
|
+
});
|
|
2215
|
+
program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--ci", `Check CI diff using ${MEMORY_FILE}`).option("--all", "Check all tracked source files, including already-committed files").option("--path <dir>", "Directory to check for --all mode (default: current directory)").option("--file <path>", "Check a specific file retroactively against all rules").option("--commit-msg [file]", "Check commit message (defaults to .git/COMMIT_EDITMSG)").option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").option("--fast", "Skip AI and memory retrieval; run deterministic checks only").option("--dry-run", "Show what would be flagged without blocking the commit").option("--diff <ref>", "Check a git diff for architecture violations (evidence-based)").option("--second-opinion", "Run a second critique pass for uncertain decisions (use with --diff)").option("--json", "Output full JSON result (use with --diff)").action(async (opts) => {
|
|
2216
|
+
if (opts.diff !== void 0) {
|
|
2217
|
+
const { values: envValues } = readRuntimeEnv();
|
|
2218
|
+
applyRuntimeEnv(envValues);
|
|
2219
|
+
const startMs = Date.now();
|
|
2220
|
+
const diffRef = opts.diff || "HEAD~1";
|
|
2221
|
+
const diag = opts.json ? (msg) => process.stderr.write(msg + "\n") : (msg) => console.log(msg);
|
|
2222
|
+
const diagSpinner = (text) => ora({ text, stream: opts.json ? process.stderr : process.stdout });
|
|
2223
|
+
const { CheckRateLimiter } = await import("./rate-limiter-SLIPCXRF.js");
|
|
2224
|
+
const release = await new CheckRateLimiter(process.cwd()).acquire();
|
|
2225
|
+
process.on("exit", release);
|
|
2226
|
+
const { FileClassifier } = await import("./classifier-MZ65R7FK.js");
|
|
2227
|
+
const { ASTAnalyzer } = await import("./ast-analyzer-JM4CIOFY.js");
|
|
2228
|
+
const { GraphBuilder } = await import("./graph-TFNTB5OK.js");
|
|
2229
|
+
const { RuleMatcher } = await import("./rules-V3QMN3AR.js");
|
|
2230
|
+
const { EvidencePacketBuilder } = await import("./evidence-HVMSONTT.js");
|
|
2231
|
+
const { DeterministicValidator } = await import("./deterministic-validator-PP56B46I.js");
|
|
2232
|
+
const { OllamaJudge } = await import("./ollama-judge-D2LFK5PB.js");
|
|
2233
|
+
const { DeepSeekCritique } = await import("./deepseek-critique-MALVIYGF.js");
|
|
2234
|
+
const { ConfidenceGate, defaultGateConfig } = await import("./confidence-gate-ZQDAOS6P.js");
|
|
2235
|
+
const { CheckCache } = await import("./check-cache-6NWRTZJD.js");
|
|
2236
|
+
const { CheckLogger } = await import("./check-logger-5HYSWA3S.js");
|
|
2237
|
+
const configDir = getArchmindDir();
|
|
2238
|
+
const classifier = FileClassifier.loadFromConfig(configDir);
|
|
2239
|
+
const ruleMatcher = RuleMatcher.loadFromConfig(configDir);
|
|
2240
|
+
const astAnalyzer = new ASTAnalyzer(process.cwd());
|
|
2241
|
+
const graphBuilder = new GraphBuilder(astAnalyzer, classifier, process.cwd());
|
|
2242
|
+
const logger = new CheckLogger(process.cwd());
|
|
2243
|
+
let diffOutput = "";
|
|
2244
|
+
try {
|
|
2245
|
+
const result = spawnSync2("git", ["diff", "--name-status", diffRef], { encoding: "utf-8" });
|
|
2246
|
+
if (result.status !== 0) throw new Error(result.stderr);
|
|
2247
|
+
diffOutput = result.stdout;
|
|
2248
|
+
} catch {
|
|
2249
|
+
console.error("Could not run git diff against " + diffRef);
|
|
2250
|
+
process.exit(1);
|
|
2251
|
+
}
|
|
2252
|
+
const added = [];
|
|
2253
|
+
const modified = [];
|
|
2254
|
+
const deleted = [];
|
|
2255
|
+
for (const line of diffOutput.split("\n").filter(Boolean)) {
|
|
2256
|
+
const [status, ...rest] = line.split(" ");
|
|
2257
|
+
const file = rest.join(" ").trim();
|
|
2258
|
+
if (!file) continue;
|
|
2259
|
+
if (status.startsWith("A")) added.push(file);
|
|
2260
|
+
else if (status.startsWith("D")) deleted.push(file);
|
|
2261
|
+
else modified.push(file);
|
|
2262
|
+
}
|
|
2263
|
+
const allChanged = [...added, ...modified].filter((f) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(f));
|
|
2264
|
+
const graph2 = await graphBuilder.buildFromFiles(allChanged);
|
|
2265
|
+
const builder = new EvidencePacketBuilder(process.cwd());
|
|
2266
|
+
const packet = await builder.build({ added, modified, deleted }, astAnalyzer, classifier, ruleMatcher, graph2);
|
|
2267
|
+
try {
|
|
2268
|
+
const fullGraph = await phase1.services.graphEngine.latest(process.cwd());
|
|
2269
|
+
if (fullGraph) {
|
|
2270
|
+
const snapshotAge = fullGraph.createdAt ? Date.now() - new Date(fullGraph.createdAt).getTime() : null;
|
|
2271
|
+
const STALE_MS = 24 * 60 * 60 * 1e3;
|
|
2272
|
+
if (snapshotAge !== null && snapshotAge > STALE_MS) {
|
|
2273
|
+
const hours = Math.round(snapshotAge / 36e5);
|
|
2274
|
+
diag(chalk.yellow(` \u26A0 Graph snapshot is ${hours}h old \u2014 run \`memory-core graph build\` to refresh`));
|
|
2275
|
+
}
|
|
2276
|
+
const changedSet = /* @__PURE__ */ new Set([...added, ...modified]);
|
|
2277
|
+
for (const edge of fullGraph.edges) {
|
|
2278
|
+
if (!changedSet.has(edge.from) || edge.to.startsWith("pkg:")) continue;
|
|
2279
|
+
const fromClass = classifier.classifyFile(edge.from);
|
|
2280
|
+
const toClass = classifier.classifyFile(edge.to);
|
|
2281
|
+
if (!fromClass || !toClass) continue;
|
|
2282
|
+
const violating = ruleMatcher.findViolatingRules(fromClass.layer, toClass.layer);
|
|
2283
|
+
for (const rule of violating) {
|
|
2284
|
+
const alreadyReported = packet.graphViolations.some(
|
|
2285
|
+
(v) => v.from === edge.from && v.to === edge.to && v.ruleName === rule.name
|
|
2286
|
+
);
|
|
2287
|
+
if (!alreadyReported) {
|
|
2288
|
+
packet.graphViolations.push({
|
|
2289
|
+
from: edge.from,
|
|
2290
|
+
fromLayer: fromClass.layer,
|
|
2291
|
+
to: edge.to,
|
|
2292
|
+
toLayer: toClass.layer,
|
|
2293
|
+
ruleName: rule.name,
|
|
2294
|
+
path: [edge.from, edge.to]
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
} else {
|
|
2300
|
+
diag(chalk.yellow(" \u26A0 No graph snapshot \u2014 building now for full codebase enforcement\u2026"));
|
|
2301
|
+
const autoSpinner = diagSpinner(" Building dependency graph\u2026").start();
|
|
2302
|
+
try {
|
|
2303
|
+
const buildResult = await Promise.race([
|
|
2304
|
+
phase1.services.graphEngine.buildAndStoreSnapshot({ cwd: process.cwd() }),
|
|
2305
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("graph build timeout")), 15e3))
|
|
2306
|
+
]);
|
|
2307
|
+
autoSpinner.succeed(` Graph snapshot ready (${buildResult.snapshot.nodes.length} nodes, ${buildResult.snapshot.edges.length} edges)`);
|
|
2308
|
+
const freshGraph = await phase1.services.graphEngine.latest(process.cwd());
|
|
2309
|
+
if (freshGraph) {
|
|
2310
|
+
const changedSet2 = /* @__PURE__ */ new Set([...added, ...modified]);
|
|
2311
|
+
for (const edge of freshGraph.edges) {
|
|
2312
|
+
if (!changedSet2.has(edge.from) || edge.to.startsWith("pkg:")) continue;
|
|
2313
|
+
const fromClass = classifier.classifyFile(edge.from);
|
|
2314
|
+
const toClass = classifier.classifyFile(edge.to);
|
|
2315
|
+
if (!fromClass || !toClass) continue;
|
|
2316
|
+
const violating = ruleMatcher.findViolatingRules(fromClass.layer, toClass.layer);
|
|
2317
|
+
for (const rule of violating) {
|
|
2318
|
+
const alreadyReported = packet.graphViolations.some(
|
|
2319
|
+
(v) => v.from === edge.from && v.to === edge.to && v.ruleName === rule.name
|
|
2320
|
+
);
|
|
2321
|
+
if (!alreadyReported) {
|
|
2322
|
+
packet.graphViolations.push({
|
|
2323
|
+
from: edge.from,
|
|
2324
|
+
fromLayer: fromClass.layer,
|
|
2325
|
+
to: edge.to,
|
|
2326
|
+
toLayer: toClass.layer,
|
|
2327
|
+
ruleName: rule.name,
|
|
2328
|
+
path: [edge.from, edge.to]
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
} catch (e) {
|
|
2335
|
+
autoSpinner.stop();
|
|
2336
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2337
|
+
diag(chalk.dim(` (auto-build skipped: ${msg} \u2014 run \`memory-core graph build\` manually)`));
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
} catch {
|
|
2341
|
+
}
|
|
2342
|
+
const det = new DeterministicValidator();
|
|
2343
|
+
if (det.validate(packet) === "block") {
|
|
2344
|
+
const out = {
|
|
2345
|
+
decision: "block",
|
|
2346
|
+
source: "deterministic",
|
|
2347
|
+
confidence: 1,
|
|
2348
|
+
violations: packet.violations.map((v) => ({ rule: v.rule.name, from: v.fromFile, to: v.toFile })),
|
|
2349
|
+
graphViolations: packet.graphViolations,
|
|
2350
|
+
...opts.json ? { evidence: packet } : {}
|
|
2351
|
+
};
|
|
2352
|
+
console.log(JSON.stringify(out, null, 2));
|
|
2353
|
+
logger.append({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), ref: diffRef, decision: "block", source: "deterministic", confidence: 1, violationCount: packet.violations.length, graphViolationCount: packet.graphViolations.length, changedFiles: allChanged.length, durationMs: Date.now() - startMs, fast: opts.fast });
|
|
2354
|
+
process.exit(1);
|
|
2355
|
+
}
|
|
2356
|
+
if (opts.fast) {
|
|
2357
|
+
const out = { decision: "allow", source: "deterministic", confidence: 1, violations: [], graphViolations: packet.graphViolations };
|
|
2358
|
+
if (opts.json) {
|
|
2359
|
+
console.log(JSON.stringify(out, null, 2));
|
|
2360
|
+
} else {
|
|
2361
|
+
diag(chalk.green("\n Decision: ALLOW") + chalk.dim(" (source: deterministic+fast, confidence: 1.00)"));
|
|
2362
|
+
}
|
|
2363
|
+
logger.append({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), ref: diffRef, decision: "allow", source: "deterministic", confidence: 1, violationCount: 0, graphViolationCount: packet.graphViolations.length, changedFiles: allChanged.length, durationMs: Date.now() - startMs, fast: true });
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
let storedMemories = [];
|
|
2367
|
+
try {
|
|
2368
|
+
const { listMemories } = await import("./db-PRDHI2CN.js");
|
|
2369
|
+
const allMemories = await listMemories({ limit: 1e4 });
|
|
2370
|
+
const ranked = allMemories.filter((m) => ["rule", "pattern", "decision"].includes(m.type));
|
|
2371
|
+
const changedLayers = packet.layersAffected;
|
|
2372
|
+
const scored = ranked.map((m) => {
|
|
2373
|
+
const text = m.content.toLowerCase();
|
|
2374
|
+
const score = changedLayers.reduce((s, l) => s + (text.includes(l) ? 2 : 0), 0);
|
|
2375
|
+
return { m, score };
|
|
2376
|
+
});
|
|
2377
|
+
scored.sort((a, b) => b.score - a.score);
|
|
2378
|
+
storedMemories = scored.slice(0, 30).map(({ m }) => ({
|
|
2379
|
+
type: m.type,
|
|
2380
|
+
content: m.content,
|
|
2381
|
+
reason: m.reason ?? void 0
|
|
2382
|
+
}));
|
|
2383
|
+
} catch {
|
|
2384
|
+
}
|
|
2385
|
+
const cache = new CheckCache(process.cwd());
|
|
2386
|
+
const mHash = cache.memoriesHash(storedMemories);
|
|
2387
|
+
const rHash = cache.rulesHash(configDir);
|
|
2388
|
+
const cacheKey = cache.key(diffOutput, mHash, rHash);
|
|
2389
|
+
const cached = !opts.secondOpinion && cache.get(cacheKey);
|
|
2390
|
+
if (cached) {
|
|
2391
|
+
diag(chalk.dim(" (cached result)"));
|
|
2392
|
+
const output2 = {
|
|
2393
|
+
decision: cached.decision,
|
|
2394
|
+
source: cached.source + "+cache",
|
|
2395
|
+
violations: cached.violations,
|
|
2396
|
+
suggestedFix: cached.suggestedFix,
|
|
2397
|
+
reasoning: cached.reasoning,
|
|
2398
|
+
confidence: cached.confidence
|
|
2399
|
+
};
|
|
2400
|
+
if (opts.json) {
|
|
2401
|
+
console.log(JSON.stringify(output2, null, 2));
|
|
2402
|
+
} else {
|
|
2403
|
+
const color = cached.decision === "block" ? chalk.red : cached.decision === "warn" ? chalk.yellow : chalk.green;
|
|
2404
|
+
diag(color(`
|
|
2405
|
+
Decision: ${cached.decision.toUpperCase()}`) + chalk.dim(` (source: ${cached.source}+cache, confidence: ${cached.confidence.toFixed(2)})`));
|
|
2406
|
+
}
|
|
2407
|
+
logger.append({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), ref: diffRef, decision: cached.decision, source: cached.source + "+cache", confidence: cached.confidence, violationCount: cached.violations.length, graphViolationCount: packet.graphViolations.length, changedFiles: allChanged.length, durationMs: Date.now() - startMs, cached: true });
|
|
2408
|
+
if (cached.decision === "block") process.exit(1);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
const chatModel = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "unknown";
|
|
2412
|
+
const provider2 = process.env.CHAT_PROVIDER ?? "ollama";
|
|
2413
|
+
const spinner = diagSpinner(` Analyzing with ${provider2}/${chatModel}\u2026`).start();
|
|
2414
|
+
const primaryDecision = await new OllamaJudge().judge(packet, storedMemories);
|
|
2415
|
+
spinner.stop();
|
|
2416
|
+
const critiqueEnabled = opts.secondOpinion ?? false;
|
|
2417
|
+
const critiqueFn = critiqueEnabled ? async (p) => new DeepSeekCritique().critique(p, primaryDecision) : async () => ({ decision: primaryDecision.decision, confidence: primaryDecision.confidence, reasoning: "skipped", override: false });
|
|
2418
|
+
const gateDecision = await new ConfidenceGate(defaultGateConfig(critiqueEnabled)).decide(packet, primaryDecision, critiqueFn);
|
|
2419
|
+
cache.set(cacheKey, {
|
|
2420
|
+
decision: gateDecision.final,
|
|
2421
|
+
source: gateDecision.source,
|
|
2422
|
+
confidence: primaryDecision.confidence,
|
|
2423
|
+
violations: primaryDecision.violations,
|
|
2424
|
+
reasoning: primaryDecision.reasoning,
|
|
2425
|
+
suggestedFix: primaryDecision.suggestedFix
|
|
2426
|
+
});
|
|
2427
|
+
const output = {
|
|
2428
|
+
decision: gateDecision.final,
|
|
2429
|
+
source: gateDecision.source,
|
|
2430
|
+
violations: primaryDecision.violations,
|
|
2431
|
+
suggestedFix: primaryDecision.suggestedFix,
|
|
2432
|
+
reasoning: primaryDecision.reasoning,
|
|
2433
|
+
confidence: primaryDecision.confidence,
|
|
2434
|
+
...opts.json ? { judge: primaryDecision, critique: gateDecision.deepseek, selfReview: gateDecision.selfReview, evidence: packet } : {}
|
|
2435
|
+
};
|
|
2436
|
+
if (opts.json) {
|
|
2437
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2438
|
+
} else {
|
|
2439
|
+
const color = gateDecision.final === "block" ? chalk.red : gateDecision.final === "warn" ? chalk.yellow : chalk.green;
|
|
2440
|
+
diag(color(`
|
|
2441
|
+
Decision: ${gateDecision.final.toUpperCase()}`) + chalk.dim(` (source: ${gateDecision.source}, confidence: ${primaryDecision.confidence.toFixed(2)})`));
|
|
2442
|
+
if (primaryDecision.violations.length > 0) diag(chalk.dim(` Violations: ${primaryDecision.violations.join(", ")}`));
|
|
2443
|
+
if (primaryDecision.suggestedFix) diag(chalk.dim(` Fix: ${primaryDecision.suggestedFix}`));
|
|
2444
|
+
diag("");
|
|
2445
|
+
}
|
|
2446
|
+
logger.append({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), ref: diffRef, decision: gateDecision.final, source: gateDecision.source, confidence: primaryDecision.confidence, violationCount: primaryDecision.violations.length, graphViolationCount: packet.graphViolations.length, changedFiles: allChanged.length, durationMs: Date.now() - startMs });
|
|
2447
|
+
if (gateDecision.final === "block") process.exit(1);
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
if (opts.file) {
|
|
2451
|
+
await checkFile(opts.file, { verbose: opts.verbose ?? false, debug: opts.debug ?? false, fast: opts.fast ?? false, dryRun: opts.dryRun ?? false });
|
|
2452
|
+
await closePool();
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
3370
2455
|
if (opts.commitMsg !== void 0) {
|
|
3371
|
-
const msgFile = typeof opts.commitMsg === "string" ? opts.commitMsg :
|
|
2456
|
+
const msgFile = typeof opts.commitMsg === "string" ? opts.commitMsg : join(process.cwd(), ".git", "COMMIT_EDITMSG");
|
|
3372
2457
|
await checkCommitMsg(msgFile, { verbose: opts.verbose ?? false, debug: opts.debug ?? false });
|
|
3373
2458
|
await closePool();
|
|
3374
2459
|
return;
|
|
3375
2460
|
}
|
|
3376
2461
|
if (opts.ci && opts.all) {
|
|
3377
|
-
console.error(
|
|
2462
|
+
console.error(chalk.red("\n Choose one mode: --ci or --all.\n"));
|
|
3378
2463
|
process.exit(1);
|
|
3379
2464
|
}
|
|
3380
2465
|
if (opts.ci) {
|
|
@@ -3390,14 +2475,212 @@ program.command("check").description("Check staged changes against architecture
|
|
|
3390
2475
|
if (summary.violations > 0) process.exit(1);
|
|
3391
2476
|
return;
|
|
3392
2477
|
}
|
|
3393
|
-
await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false, fast: opts.fast ?? false });
|
|
2478
|
+
await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false, fast: opts.fast ?? false, dryRun: opts.dryRun ?? false });
|
|
3394
2479
|
});
|
|
3395
|
-
program.command("
|
|
2480
|
+
var schema = program.command("schema").description("Manage schema alignment rules between TypeScript and Go files");
|
|
2481
|
+
schema.command("add").description("Track schema alignment between a TypeScript file and a Go file").requiredOption("--ts <file>", "Path to TypeScript file (relative to project root)").requiredOption("--go <file>", "Path to Go file (relative to project root)").action(async (opts) => {
|
|
2482
|
+
const config = readProjectConfig();
|
|
2483
|
+
const content = JSON.stringify({ tsFile: opts.ts, goFile: opts.go });
|
|
2484
|
+
const spinner = ora("Saving schema rule\u2026").start();
|
|
2485
|
+
try {
|
|
2486
|
+
await phase1.services.memoryEngine.remember({
|
|
2487
|
+
type: "schema",
|
|
2488
|
+
scope: "project",
|
|
2489
|
+
architecture: config?.backendArchitecture ?? config?.frontendFramework,
|
|
2490
|
+
projectName: config?.projectName,
|
|
2491
|
+
content,
|
|
2492
|
+
reason: "Schema alignment rule \u2014 TypeScript and Go structs must stay in sync"
|
|
2493
|
+
});
|
|
2494
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
2495
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2496
|
+
spinner.succeed(chalk.green(`Schema rule saved: ${opts.ts} \u2194 ${opts.go}`));
|
|
2497
|
+
} catch (err) {
|
|
2498
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
2499
|
+
process.exit(1);
|
|
2500
|
+
}
|
|
2501
|
+
await closePool();
|
|
2502
|
+
});
|
|
2503
|
+
schema.command("list").description("List all schema alignment rules").action(async () => {
|
|
2504
|
+
const memories = await phase1.services.memoryEngine.list({ type: "schema", limit: 100 });
|
|
2505
|
+
if (memories.length === 0) {
|
|
2506
|
+
console.log(chalk.dim("\n No schema rules defined. Use: memory-core schema add --ts <file> --go <file>\n"));
|
|
2507
|
+
await closePool();
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
console.log(chalk.cyan(`
|
|
2511
|
+
${memories.length} schema rule${memories.length > 1 ? "s" : ""}:
|
|
2512
|
+
`));
|
|
2513
|
+
for (const m of memories) {
|
|
2514
|
+
try {
|
|
2515
|
+
const rule = JSON.parse(m.content);
|
|
2516
|
+
console.log(chalk.white(` ${rule.tsFile}`) + chalk.dim(" \u2194 ") + chalk.white(rule.goFile));
|
|
2517
|
+
} catch {
|
|
2518
|
+
console.log(chalk.dim(` ${m.content}`));
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
console.log();
|
|
2522
|
+
await closePool();
|
|
2523
|
+
});
|
|
2524
|
+
schema.command("check").description("Run schema alignment checks against all tracked file pairs").action(async () => {
|
|
2525
|
+
const app = getDefaultApplicationContainer();
|
|
2526
|
+
const violations = await findSchemaViolations({
|
|
2527
|
+
cwd: process.cwd(),
|
|
2528
|
+
memoryEngine: app.services.memoryEngine
|
|
2529
|
+
});
|
|
2530
|
+
if (violations.length === 0) {
|
|
2531
|
+
console.log(chalk.green("\n \u2713 All schema rules pass \u2014 TypeScript and Go are in sync.\n"));
|
|
2532
|
+
} else {
|
|
2533
|
+
console.log(chalk.red.bold(`
|
|
2534
|
+
\u2717 ${violations.length} schema violation${violations.length > 1 ? "s" : ""} found
|
|
2535
|
+
`));
|
|
2536
|
+
for (const v of violations) {
|
|
2537
|
+
console.log(chalk.bold(` ${v.file}`));
|
|
2538
|
+
console.log(chalk.yellow(" Rule: ") + v.rule);
|
|
2539
|
+
console.log(chalk.red(" Issue: ") + v.issue);
|
|
2540
|
+
if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
|
|
2541
|
+
console.log();
|
|
2542
|
+
}
|
|
2543
|
+
process.exit(1);
|
|
2544
|
+
}
|
|
2545
|
+
await closePool();
|
|
2546
|
+
});
|
|
2547
|
+
program.command("watch").description("Watch source files and check violations in real-time on every save").option("--path <dir>", "Directory to watch (default: current directory)").option("--scan-on-start", "Run an initial full snapshot scan before watching file changes").option("--auto-fix", "Automatically rewrite files that violate rules using AI").option("--verbose", "Show diff size and model details per file").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
|
|
3396
2548
|
await phase1.providers.watchService.start({
|
|
3397
2549
|
path: opts.path,
|
|
3398
2550
|
scanOnStart: opts.scanOnStart,
|
|
2551
|
+
autoFix: opts.autoFix,
|
|
3399
2552
|
verbose: opts.verbose,
|
|
3400
2553
|
debug: opts.debug
|
|
3401
2554
|
});
|
|
3402
2555
|
});
|
|
2556
|
+
var ARCHMIND_DIR = join(process.cwd(), ".archmind");
|
|
2557
|
+
var DEFAULT_ARCH_RULES = {
|
|
2558
|
+
rules: [
|
|
2559
|
+
{ id: "domain-no-infra", name: "Domain must not import infrastructure", fromLayer: "domain", toLayer: "infrastructure", allowed: false, severity: "critical", enforcement: "block" },
|
|
2560
|
+
{ id: "domain-no-api", name: "Domain must not import API layer", fromLayer: "domain", toLayer: "api", allowed: false, severity: "critical", enforcement: "block" },
|
|
2561
|
+
{ id: "api-no-infra-direct", name: "API should not import infrastructure directly", fromLayer: "api", toLayer: "infrastructure", allowed: false, severity: "medium", enforcement: "warn" },
|
|
2562
|
+
{ id: "api-depends-on-domain", name: "API depends on domain", fromLayer: "api", toLayer: "domain", allowed: true, severity: "low", enforcement: "suggest" },
|
|
2563
|
+
{ id: "no-circular", name: "No circular dependencies", fromLayer: "*", toLayer: "*", allowed: false, severity: "critical", enforcement: "block" },
|
|
2564
|
+
{ id: "application-no-api", name: "Application must not import API layer", fromLayer: "application", toLayer: "api", allowed: false, severity: "critical", enforcement: "block" },
|
|
2565
|
+
{ id: "application-uses-domain", name: "Application depends on domain", fromLayer: "application", toLayer: "domain", allowed: true, severity: "low", enforcement: "suggest" },
|
|
2566
|
+
{ id: "shared-no-domain", name: "Shared must not import domain", fromLayer: "shared", toLayer: "domain", allowed: false, severity: "medium", enforcement: "warn" }
|
|
2567
|
+
]
|
|
2568
|
+
};
|
|
2569
|
+
function buildLayersTemplate(arch2) {
|
|
2570
|
+
const mvc = ["nestjs", "mvc", "laravel", "go-api"].includes(arch2);
|
|
2571
|
+
const frontend = ["react", "vue", "angular", "svelte", "nuxt", "react-native"].includes(arch2);
|
|
2572
|
+
if (frontend) {
|
|
2573
|
+
return {
|
|
2574
|
+
layers: [
|
|
2575
|
+
{ name: "domain", paths: ["src/domain/**", "src/store/**", "src/state/**"], description: "State, models, business logic" },
|
|
2576
|
+
{ name: "application", paths: ["src/services/**", "src/hooks/**", "src/composables/**"], description: "App services and hooks" },
|
|
2577
|
+
{ name: "infrastructure", paths: ["src/api/**", "src/lib/**", "src/clients/**"], description: "External API clients, adapters" },
|
|
2578
|
+
{ name: "shared", paths: ["src/utils/**", "src/helpers/**", "src/constants/**"], description: "Shared utilities" }
|
|
2579
|
+
]
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
if (mvc) {
|
|
2583
|
+
return {
|
|
2584
|
+
layers: [
|
|
2585
|
+
{ name: "domain", paths: ["src/models/**", "src/entities/**"], description: "Data models and entities" },
|
|
2586
|
+
{ name: "application", paths: ["src/services/**"], description: "Business logic services" },
|
|
2587
|
+
{ name: "infrastructure", paths: ["src/repositories/**", "src/database/**", "src/infrastructure/**"], description: "DB, external APIs" },
|
|
2588
|
+
{ name: "api", paths: ["src/controllers/**", "src/routes/**", "src/handlers/**"], description: "HTTP handlers" },
|
|
2589
|
+
{ name: "shared", paths: ["src/utils/**", "src/helpers/**", "src/shared/**"], description: "Shared utilities" }
|
|
2590
|
+
]
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
return {
|
|
2594
|
+
layers: [
|
|
2595
|
+
{ name: "domain", paths: ["src/domain/**", "src/core/domain/**", "src/modules/*/domain/**"], description: "Business logic, entities, use cases" },
|
|
2596
|
+
{ name: "application", paths: ["src/application/**", "src/core/application/**", "src/modules/*/application/**"], description: "Use cases and orchestration" },
|
|
2597
|
+
{ name: "infrastructure", paths: ["src/infrastructure/**", "src/modules/*/infrastructure/**"], description: "Database, external APIs, low-level I/O" },
|
|
2598
|
+
{ name: "api", paths: ["src/api/**", "src/interfaces/**", "src/controllers/**"], description: "HTTP handlers, request/response" },
|
|
2599
|
+
{ name: "shared", paths: ["src/shared/**", "src/utils/**", "src/helpers/**"], description: "Utilities, helpers, constants" }
|
|
2600
|
+
]
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
function getArchmindDir() {
|
|
2604
|
+
return ARCHMIND_DIR;
|
|
2605
|
+
}
|
|
2606
|
+
var arch = program.command("arch").description("Architecture enforcement: check, review captured rules, watch for errors, log incidents");
|
|
2607
|
+
arch.command("review").description("Review and approve/reject auto-captured architecture rules").option("--approve-all", "Approve everything without prompting").option("--reject-all", "Reject everything without prompting").action(async (opts) => {
|
|
2608
|
+
const { ApprovalQueue } = await import("./approval-queue-YBYRGBHP.js");
|
|
2609
|
+
const { join: join2 } = await import("path");
|
|
2610
|
+
const configDir = getArchmindDir();
|
|
2611
|
+
const queue = new ApprovalQueue(configDir);
|
|
2612
|
+
const pending = queue.pending();
|
|
2613
|
+
if (pending.length === 0) {
|
|
2614
|
+
console.log(chalk.dim(" No pending rules."));
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
console.log(chalk.bold(`
|
|
2618
|
+
Pending rules (${pending.length}):
|
|
2619
|
+
`));
|
|
2620
|
+
if (opts.approveAll) {
|
|
2621
|
+
for (const item of pending) queue.approve(item.id);
|
|
2622
|
+
const count2 = queue.persistApproved(join2(configDir, "rules.json"));
|
|
2623
|
+
console.log(chalk.green(` \u2713 Approved and saved ${count2} rules`));
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
if (opts.rejectAll) {
|
|
2627
|
+
for (const item of pending) queue.reject(item.id);
|
|
2628
|
+
console.log(chalk.yellow(` \u2717 Rejected ${pending.length} rules`));
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
for (const item of pending) {
|
|
2632
|
+
console.log(chalk.cyan(` [${item.source.toUpperCase()}]`) + ` ${item.rule.name}`);
|
|
2633
|
+
console.log(chalk.dim(` ${item.rule.description}`));
|
|
2634
|
+
console.log(chalk.dim(` captured: ${item.capturedAt} confidence: ${item.confidence}`));
|
|
2635
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
2636
|
+
const accept = await confirm2({ message: " Keep this rule?" });
|
|
2637
|
+
accept ? queue.approve(item.id) : queue.reject(item.id);
|
|
2638
|
+
console.log(accept ? chalk.green(" \u2713 Kept") : chalk.yellow(" \u2717 Dropped"));
|
|
2639
|
+
console.log();
|
|
2640
|
+
}
|
|
2641
|
+
const count = queue.persistApproved(join2(configDir, "rules.json"));
|
|
2642
|
+
if (count > 0) console.log(chalk.green(` \u2713 Saved ${count} rules to .archmind/rules.json`));
|
|
2643
|
+
});
|
|
2644
|
+
arch.command("watch").description("Watch your dev processes and auto-draft rules from errors").action(async () => {
|
|
2645
|
+
const { WatchErrors, loadWatchConfig } = await import("./watch-errors-B3FA26N4.js");
|
|
2646
|
+
const configDir = getArchmindDir();
|
|
2647
|
+
const config = loadWatchConfig(configDir);
|
|
2648
|
+
console.log(chalk.cyan("\n Watching:"));
|
|
2649
|
+
for (const cmd of config.commands) console.log(chalk.dim(` \u2022 ${cmd}`));
|
|
2650
|
+
console.log(chalk.dim("\n Press Ctrl+C to stop.\n"));
|
|
2651
|
+
const watcher = new WatchErrors(config, configDir);
|
|
2652
|
+
watcher.start((rule) => {
|
|
2653
|
+
console.log(chalk.yellow(` [DRAFT] ${rule.name}`));
|
|
2654
|
+
console.log(chalk.dim(` ${rule.description.slice(0, 100)}`));
|
|
2655
|
+
console.log(chalk.dim(' Run "memory-core arch review" to approve.\n'));
|
|
2656
|
+
});
|
|
2657
|
+
process.on("SIGINT", () => {
|
|
2658
|
+
watcher.stop();
|
|
2659
|
+
console.log(chalk.dim("\n Stopped."));
|
|
2660
|
+
process.exit(0);
|
|
2661
|
+
});
|
|
2662
|
+
await new Promise(() => {
|
|
2663
|
+
});
|
|
2664
|
+
});
|
|
2665
|
+
arch.command("incident").description("Log a production incident and capture it as an architecture rule").action(async () => {
|
|
2666
|
+
const { input: input2, confirm: confirm2 } = await import("@inquirer/prompts");
|
|
2667
|
+
const { IncidentCaptureService } = await import("./incident-capture-RVPZULS7.js");
|
|
2668
|
+
const { ApprovalQueue } = await import("./approval-queue-YBYRGBHP.js");
|
|
2669
|
+
console.log(chalk.cyan("\n Incident capture\n"));
|
|
2670
|
+
const what = await input2({ message: "What broke?" });
|
|
2671
|
+
const why = await input2({ message: "Root cause?" });
|
|
2672
|
+
const where = await input2({ message: "Which file / layer / pattern?" });
|
|
2673
|
+
const rule = new IncidentCaptureService().draftRule({ what, why, where, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2674
|
+
console.log(chalk.dim(`
|
|
2675
|
+
Rule: ${rule.name}`));
|
|
2676
|
+
console.log(chalk.dim(` Description: ${rule.description}
|
|
2677
|
+
`));
|
|
2678
|
+
const save = await confirm2({ message: "Save to approval queue?" });
|
|
2679
|
+
if (save) {
|
|
2680
|
+
new ApprovalQueue(getArchmindDir()).add(rule, "incident", "high");
|
|
2681
|
+
console.log(chalk.green(' \u2713 Saved. Run "memory-core arch review" to approve.\n'));
|
|
2682
|
+
} else {
|
|
2683
|
+
console.log(chalk.dim(" Discarded.\n"));
|
|
2684
|
+
}
|
|
2685
|
+
});
|
|
3403
2686
|
program.parseAsync(process.argv);
|