@shahmilsaari/memory-core 1.0.22 → 1.0.25
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 +249 -458
- package/dist/{chunk-UZDALJVQ.js → chunk-35ZWQFTO.js} +2594 -976
- package/dist/cli.js +471 -1582
- package/dist/dashboard/assets/index-B7gd4JQc.js +2 -0
- package/dist/dashboard/assets/{index-DXXHB1Ik.css → index-CHgjllWU.css} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/{dashboard-server-VOT2ZRVN.js → dashboard-server-SSYZLQKB.js} +68 -5
- package/package.json +1 -1
- package/dist/dashboard/assets/index-BRqvIBnm.js +0 -2
package/dist/cli.js
CHANGED
|
@@ -1,1293 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
AGENT_NAMES,
|
|
4
|
+
MEMORY_FILE,
|
|
4
5
|
OUTPUT_FILES,
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
checkCi,
|
|
7
|
+
checkCommitMsg,
|
|
8
|
+
checkFile,
|
|
9
|
+
checkStaged,
|
|
7
10
|
closePool,
|
|
8
11
|
detectProject,
|
|
9
|
-
|
|
12
|
+
findSchemaViolations,
|
|
10
13
|
generate,
|
|
11
14
|
getAllowPatterns,
|
|
12
|
-
getChatProviderLabel,
|
|
13
15
|
getDefaultApplicationContainer,
|
|
14
16
|
getPool,
|
|
15
17
|
inferProjectArchitectures,
|
|
18
|
+
installHook,
|
|
16
19
|
listProfiles,
|
|
17
20
|
migrateGraphSnapshots,
|
|
21
|
+
parseMemoryFile,
|
|
18
22
|
probeGraphSnapshotStore,
|
|
19
|
-
|
|
23
|
+
readBypassStats,
|
|
24
|
+
readMemoryFile,
|
|
25
|
+
readMemoryFileFromUrl,
|
|
26
|
+
recordBypass,
|
|
20
27
|
retrieveMemorySelection,
|
|
21
28
|
runMigrations,
|
|
22
|
-
seeds
|
|
23
|
-
|
|
29
|
+
seeds,
|
|
30
|
+
toPortableMemory,
|
|
31
|
+
uninstallHook,
|
|
32
|
+
writeMemoryFile
|
|
33
|
+
} from "./chunk-35ZWQFTO.js";
|
|
24
34
|
|
|
25
35
|
// src/cli.ts
|
|
26
36
|
import { Command } from "commander";
|
|
27
37
|
import { input, select, confirm } from "@inquirer/prompts";
|
|
28
|
-
import
|
|
38
|
+
import chalk from "chalk";
|
|
29
39
|
import ora from "ora";
|
|
30
|
-
import { readFileSync
|
|
31
|
-
import { join
|
|
40
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, rmSync } from "fs";
|
|
41
|
+
import { join, dirname, resolve } from "path";
|
|
32
42
|
import { homedir } from "os";
|
|
33
43
|
|
|
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
|
-
}
|
|
1288
|
-
|
|
1289
44
|
// src/remote-install.ts
|
|
1290
|
-
import { spawnSync
|
|
45
|
+
import { spawnSync } from "child_process";
|
|
1291
46
|
var CAVEMAN_INSTALL_URL = "https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh";
|
|
1292
47
|
var MAX_INSTALLER_BYTES = 2e5;
|
|
1293
48
|
var TRUSTED_INSTALL_HOSTS = /* @__PURE__ */ new Set(["raw.githubusercontent.com"]);
|
|
@@ -1301,7 +56,7 @@ function assertTrustedInstallerUrl(url) {
|
|
|
1301
56
|
}
|
|
1302
57
|
}
|
|
1303
58
|
function defaultRunScript(script) {
|
|
1304
|
-
return
|
|
59
|
+
return spawnSync("bash", ["-s"], {
|
|
1305
60
|
input: script,
|
|
1306
61
|
encoding: "utf-8",
|
|
1307
62
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1337,38 +92,38 @@ async function installCavemanTokenSaver(options = {}) {
|
|
|
1337
92
|
|
|
1338
93
|
// src/cli.ts
|
|
1339
94
|
function printBanner(projectName, agentCount, status) {
|
|
1340
|
-
const pg = status ? status.postgresOk ?
|
|
1341
|
-
const ol = status ? status.ollamaOk ?
|
|
95
|
+
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");
|
|
96
|
+
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
97
|
const lines = [
|
|
1343
98
|
"",
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
99
|
+
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 "),
|
|
100
|
+
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"),
|
|
101
|
+
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"),
|
|
102
|
+
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"),
|
|
103
|
+
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"),
|
|
104
|
+
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
105
|
"",
|
|
1351
|
-
|
|
106
|
+
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
107
|
"",
|
|
1353
|
-
|
|
1354
|
-
|
|
108
|
+
chalk.green(` \u2713 Project `) + chalk.bold(projectName),
|
|
109
|
+
chalk.green(` \u2713 Agents `) + chalk.bold(`${agentCount} AI agents configured`),
|
|
1355
110
|
pg,
|
|
1356
111
|
...ol ? [ol] : [],
|
|
1357
112
|
"",
|
|
1358
|
-
|
|
1359
|
-
|
|
113
|
+
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"),
|
|
114
|
+
chalk.dim(" Built by ") + chalk.bold.white("Shahmil Saari"),
|
|
1360
115
|
"",
|
|
1361
|
-
|
|
116
|
+
chalk.bold(" Every AI agent in this project now follows your rules."),
|
|
1362
117
|
"",
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
118
|
+
chalk.gray(" Next steps:"),
|
|
119
|
+
chalk.gray(' memory-core remember "Your architectural decision"'),
|
|
120
|
+
chalk.gray(' memory-core search "query"'),
|
|
121
|
+
chalk.gray(" memory-core sync"),
|
|
1367
122
|
""
|
|
1368
123
|
];
|
|
1369
124
|
lines.forEach((l) => console.log(l));
|
|
1370
125
|
}
|
|
1371
|
-
var { version } = JSON.parse(
|
|
126
|
+
var { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1372
127
|
var CONFIG_FILE = ".memory-core.json";
|
|
1373
128
|
var LOCAL_GENERATED_FILES = [".memory-core-stats.json"];
|
|
1374
129
|
var LOCAL_STATE_FILES = [CONFIG_FILE, ".memory-core.env", ...LOCAL_GENERATED_FILES];
|
|
@@ -1379,13 +134,13 @@ var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
|
|
|
1379
134
|
var DEFAULT_CHAT_MODEL = "llama3.2";
|
|
1380
135
|
var phase1 = getDefaultApplicationContainer();
|
|
1381
136
|
function getEnvPath() {
|
|
1382
|
-
const memoryEnv =
|
|
1383
|
-
if (
|
|
1384
|
-
const dotEnv =
|
|
1385
|
-
return
|
|
137
|
+
const memoryEnv = join(process.cwd(), ".memory-core.env");
|
|
138
|
+
if (existsSync(memoryEnv)) return memoryEnv;
|
|
139
|
+
const dotEnv = join(process.cwd(), ".env");
|
|
140
|
+
return existsSync(dotEnv) ? dotEnv : memoryEnv;
|
|
1386
141
|
}
|
|
1387
142
|
function getWriteEnvPath() {
|
|
1388
|
-
return
|
|
143
|
+
return join(process.cwd(), ".memory-core.env");
|
|
1389
144
|
}
|
|
1390
145
|
function parseEnvFile(raw) {
|
|
1391
146
|
const lines = raw.split(/\r?\n/);
|
|
@@ -1403,7 +158,7 @@ function parseEnvFile(raw) {
|
|
|
1403
158
|
}
|
|
1404
159
|
function readRuntimeEnv() {
|
|
1405
160
|
const envPath = getEnvPath();
|
|
1406
|
-
const fileValues =
|
|
161
|
+
const fileValues = existsSync(envPath) ? parseEnvFile(readFileSync(envPath, "utf-8")) : {};
|
|
1407
162
|
const values = {
|
|
1408
163
|
...fileValues
|
|
1409
164
|
};
|
|
@@ -1437,7 +192,7 @@ function writeRuntimeEnv(values, envPath = getWriteEnvPath()) {
|
|
|
1437
192
|
const value = values[key];
|
|
1438
193
|
if (value) lines.push(`${key}=${value}`);
|
|
1439
194
|
}
|
|
1440
|
-
|
|
195
|
+
writeFileSync(envPath, `${lines.join("\n")}
|
|
1441
196
|
`, "utf-8");
|
|
1442
197
|
}
|
|
1443
198
|
function applyRuntimeEnv(values) {
|
|
@@ -1446,8 +201,8 @@ function applyRuntimeEnv(values) {
|
|
|
1446
201
|
}
|
|
1447
202
|
}
|
|
1448
203
|
function appendMissingGitignoreEntries(entries, heading) {
|
|
1449
|
-
const gitignorePath =
|
|
1450
|
-
const existing =
|
|
204
|
+
const gitignorePath = join(process.cwd(), ".gitignore");
|
|
205
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
|
|
1451
206
|
const existingEntries = new Set(
|
|
1452
207
|
existing.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
1453
208
|
);
|
|
@@ -1462,9 +217,9 @@ ${toAdd.join("\n")}
|
|
|
1462
217
|
return toAdd.length;
|
|
1463
218
|
}
|
|
1464
219
|
function removeMemoryCoreGitignoreBlock(entries, heading = GITIGNORE_HEADING) {
|
|
1465
|
-
const gitignorePath =
|
|
1466
|
-
if (!
|
|
1467
|
-
const existing =
|
|
220
|
+
const gitignorePath = join(process.cwd(), ".gitignore");
|
|
221
|
+
if (!existsSync(gitignorePath)) return false;
|
|
222
|
+
const existing = readFileSync(gitignorePath, "utf-8");
|
|
1468
223
|
const entrySet = new Set(entries);
|
|
1469
224
|
const lines = existing.split(/\r?\n/);
|
|
1470
225
|
const kept = [];
|
|
@@ -1493,15 +248,15 @@ function removeMemoryCoreGitignoreBlock(entries, heading = GITIGNORE_HEADING) {
|
|
|
1493
248
|
}
|
|
1494
249
|
if (!changed) return false;
|
|
1495
250
|
const content = kept.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/u, "");
|
|
1496
|
-
|
|
251
|
+
writeFileSync(gitignorePath, content ? `${content}
|
|
1497
252
|
` : "", "utf-8");
|
|
1498
253
|
return true;
|
|
1499
254
|
}
|
|
1500
255
|
function removeProjectFiles(relativePaths) {
|
|
1501
256
|
const removed = [];
|
|
1502
257
|
for (const relativePath of [...new Set(relativePaths)]) {
|
|
1503
|
-
const target =
|
|
1504
|
-
if (!
|
|
258
|
+
const target = join(process.cwd(), relativePath);
|
|
259
|
+
if (!existsSync(target)) continue;
|
|
1505
260
|
rmSync(target, { force: true, recursive: true });
|
|
1506
261
|
removed.push(relativePath);
|
|
1507
262
|
}
|
|
@@ -1568,16 +323,16 @@ async function verifyOllamaConnection(ollamaUrl) {
|
|
|
1568
323
|
}
|
|
1569
324
|
}
|
|
1570
325
|
function readProjectConfig() {
|
|
1571
|
-
const path =
|
|
1572
|
-
if (!
|
|
326
|
+
const path = join(process.cwd(), CONFIG_FILE);
|
|
327
|
+
if (!existsSync(path)) return null;
|
|
1573
328
|
try {
|
|
1574
|
-
return JSON.parse(
|
|
329
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
1575
330
|
} catch {
|
|
1576
331
|
return null;
|
|
1577
332
|
}
|
|
1578
333
|
}
|
|
1579
334
|
function writeProjectConfig(config) {
|
|
1580
|
-
|
|
335
|
+
writeFileSync(join(process.cwd(), CONFIG_FILE), JSON.stringify(config, null, 2));
|
|
1581
336
|
}
|
|
1582
337
|
function updateProjectConfig(mutator) {
|
|
1583
338
|
const current = readProjectConfig() ?? {
|
|
@@ -1649,11 +404,11 @@ function toPortableFromRecord(memory) {
|
|
|
1649
404
|
});
|
|
1650
405
|
}
|
|
1651
406
|
function printMemoryTable(memories, title = "Rules in memory") {
|
|
1652
|
-
console.log(
|
|
407
|
+
console.log(chalk.bold(`
|
|
1653
408
|
${title} (${memories.length} total)
|
|
1654
409
|
`));
|
|
1655
|
-
console.log(
|
|
1656
|
-
console.log(
|
|
410
|
+
console.log(chalk.dim(" ID Type Scope Title / Content"));
|
|
411
|
+
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
412
|
memories.forEach((memory) => {
|
|
1658
413
|
const id = String(memory.id).padEnd(4);
|
|
1659
414
|
const type = memory.type.padEnd(10);
|
|
@@ -1661,19 +416,19 @@ function printMemoryTable(memories, title = "Rules in memory") {
|
|
|
1661
416
|
const label = truncate(memory.title || memory.content, 64);
|
|
1662
417
|
console.log(` ${id} ${type} ${scope} ${label}`);
|
|
1663
418
|
});
|
|
1664
|
-
console.log(
|
|
419
|
+
console.log(chalk.gray("\n Use: memory-core remove <id> | memory-core edit <id>\n"));
|
|
1665
420
|
}
|
|
1666
421
|
function getCurrentListArchitectures(config) {
|
|
1667
422
|
return inferProjectArchitectures(process.cwd(), config).filter((architecture) => architecture !== "global");
|
|
1668
423
|
}
|
|
1669
424
|
function printStatusLine(label, value) {
|
|
1670
|
-
console.log(` ${
|
|
425
|
+
console.log(` ${chalk.dim(label.padEnd(18))} ${value}`);
|
|
1671
426
|
}
|
|
1672
427
|
function abbreviate(value, max = 96) {
|
|
1673
428
|
return value.length > max ? `${value.slice(0, max - 1)}\u2026` : value;
|
|
1674
429
|
}
|
|
1675
430
|
function graphStoreFilePath(cwd = process.cwd()) {
|
|
1676
|
-
return
|
|
431
|
+
return join(cwd, ".memory-core", "graph-snapshots.json");
|
|
1677
432
|
}
|
|
1678
433
|
function graphBackendLabel(values) {
|
|
1679
434
|
return values.DATABASE_URL ? "postgres (file fallback enabled)" : "file";
|
|
@@ -1693,8 +448,8 @@ async function runModelDoctor() {
|
|
|
1693
448
|
const ollamaUrl = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
|
|
1694
449
|
const embeddingModel = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
|
|
1695
450
|
const dbUrl = values.DATABASE_URL ?? "";
|
|
1696
|
-
console.log(
|
|
1697
|
-
printStatusLine("Env file",
|
|
451
|
+
console.log(chalk.bold("\n memory-core model doctor\n"));
|
|
452
|
+
printStatusLine("Env file", existsSync(envPath) ? envPath : `${envPath} ${chalk.yellow("(will be created on first write)")}`);
|
|
1698
453
|
printStatusLine("Provider", provider2);
|
|
1699
454
|
printStatusLine("Chat model", model2);
|
|
1700
455
|
printStatusLine("Embedding model", embeddingModel);
|
|
@@ -1704,56 +459,56 @@ async function runModelDoctor() {
|
|
|
1704
459
|
const dbError = await verifyDatabaseConnection(dbUrl);
|
|
1705
460
|
if (dbError) {
|
|
1706
461
|
ok = false;
|
|
1707
|
-
console.log(
|
|
462
|
+
console.log(chalk.red(" \u2717 PostgreSQL ") + chalk.dim(dbError));
|
|
1708
463
|
} else {
|
|
1709
|
-
console.log(
|
|
464
|
+
console.log(chalk.green(" \u2713 PostgreSQL ") + chalk.dim("connected"));
|
|
1710
465
|
}
|
|
1711
466
|
const ollamaError = await verifyOllamaConnection(ollamaUrl);
|
|
1712
467
|
if (ollamaError) {
|
|
1713
468
|
ok = false;
|
|
1714
|
-
console.log(
|
|
469
|
+
console.log(chalk.red(" \u2717 Ollama ") + chalk.dim(ollamaError));
|
|
1715
470
|
} else {
|
|
1716
|
-
console.log(
|
|
471
|
+
console.log(chalk.green(" \u2713 Ollama ") + chalk.dim("reachable"));
|
|
1717
472
|
}
|
|
1718
473
|
if (!ollamaError) {
|
|
1719
474
|
try {
|
|
1720
475
|
const installedEmbeddingModel = await resolveOllamaInstalledModel(ollamaUrl, embeddingModel);
|
|
1721
476
|
if (installedEmbeddingModel) {
|
|
1722
|
-
console.log(
|
|
477
|
+
console.log(chalk.green(" \u2713 Embedding ") + chalk.dim(`${installedEmbeddingModel} installed`));
|
|
1723
478
|
} else {
|
|
1724
479
|
ok = false;
|
|
1725
|
-
console.log(
|
|
480
|
+
console.log(chalk.red(" \u2717 Embedding ") + chalk.dim(`${embeddingModel} not installed in Ollama`));
|
|
1726
481
|
}
|
|
1727
482
|
} catch (err) {
|
|
1728
483
|
ok = false;
|
|
1729
|
-
console.log(
|
|
484
|
+
console.log(chalk.red(" \u2717 Embedding ") + chalk.dim(err.message));
|
|
1730
485
|
}
|
|
1731
486
|
}
|
|
1732
487
|
if (provider2 === "ollama") {
|
|
1733
488
|
if (ollamaError) {
|
|
1734
489
|
ok = false;
|
|
1735
|
-
console.log(
|
|
490
|
+
console.log(chalk.red(" \u2717 Chat model ") + chalk.dim("cannot verify while Ollama is unreachable"));
|
|
1736
491
|
} else {
|
|
1737
492
|
try {
|
|
1738
493
|
const installedChatModel = await resolveOllamaInstalledModel(ollamaUrl, model2);
|
|
1739
494
|
if (installedChatModel) {
|
|
1740
|
-
console.log(
|
|
495
|
+
console.log(chalk.green(" \u2713 Chat model ") + chalk.dim(`${installedChatModel} installed`));
|
|
1741
496
|
} else {
|
|
1742
497
|
ok = false;
|
|
1743
|
-
console.log(
|
|
498
|
+
console.log(chalk.red(" \u2717 Chat model ") + chalk.dim(`${model2} not installed in Ollama`));
|
|
1744
499
|
}
|
|
1745
500
|
} catch (err) {
|
|
1746
501
|
ok = false;
|
|
1747
|
-
console.log(
|
|
502
|
+
console.log(chalk.red(" \u2717 Chat model ") + chalk.dim(err.message));
|
|
1748
503
|
}
|
|
1749
504
|
}
|
|
1750
505
|
} else {
|
|
1751
506
|
if (!values.CHAT_API_KEY) {
|
|
1752
507
|
ok = false;
|
|
1753
|
-
console.log(
|
|
508
|
+
console.log(chalk.red(` \u2717 ${providerLabel(provider2)} API`) + chalk.dim(" CHAT_API_KEY is missing"));
|
|
1754
509
|
} else {
|
|
1755
|
-
console.log(
|
|
1756
|
-
console.log(
|
|
510
|
+
console.log(chalk.green(` \u2713 ${providerLabel(provider2)} API`) + chalk.dim(" key configured"));
|
|
511
|
+
console.log(chalk.gray(" Remote provider connectivity is not verified live by doctor."));
|
|
1757
512
|
}
|
|
1758
513
|
}
|
|
1759
514
|
console.log();
|
|
@@ -1765,33 +520,33 @@ async function printProjectStatus() {
|
|
|
1765
520
|
const provider2 = getConfiguredProvider(values);
|
|
1766
521
|
const model2 = getConfiguredChatModel(values);
|
|
1767
522
|
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 =
|
|
523
|
+
const generatedFiles = OUTPUT_FILES.map((entry) => entry.path).filter((relativePath) => existsSync(join(process.cwd(), relativePath)));
|
|
524
|
+
const hookPath = join(process.cwd(), ".git", "hooks", "pre-commit");
|
|
525
|
+
const memoryFilePath = join(process.cwd(), MEMORY_FILE);
|
|
526
|
+
const statsPath = join(process.cwd(), ".memory-core-stats.json");
|
|
1772
527
|
const dbError = await verifyDatabaseConnection(values.DATABASE_URL ?? "");
|
|
1773
528
|
const ollamaError = await verifyOllamaConnection(values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
|
|
1774
529
|
const graphCount = await getGraphSnapshotCount(process.cwd());
|
|
1775
|
-
console.log(
|
|
530
|
+
console.log(chalk.bold("\n memory-core status\n"));
|
|
1776
531
|
printStatusLine("Project", config?.projectName ?? process.cwd().split("/").pop() ?? "unknown");
|
|
1777
|
-
printStatusLine("Project type", config?.projectType ??
|
|
532
|
+
printStatusLine("Project type", config?.projectType ?? chalk.yellow("not initialized"));
|
|
1778
533
|
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` :
|
|
534
|
+
printStatusLine("Backend arch", config?.backendArchitecture ?? chalk.gray("\u2014"));
|
|
535
|
+
printStatusLine("Frontend fw", config?.frontendFramework ?? chalk.gray("\u2014"));
|
|
536
|
+
printStatusLine("Architectures", architectures.length ? architectures.join(", ") : chalk.gray("none detected"));
|
|
537
|
+
printStatusLine("Agents", config?.agents?.length ? `${config.agents.length} selected` : chalk.gray("none saved"));
|
|
1783
538
|
printStatusLine("Caveman", config?.caveman?.enabled ? `enabled (${config.caveman.intensity})` : "disabled");
|
|
1784
539
|
printStatusLine("Auto sync", config?.autoSync === false ? "disabled" : "enabled");
|
|
1785
540
|
printStatusLine("Allow patterns", String(getAllowPatterns(config).length));
|
|
1786
|
-
printStatusLine("Env file", `${
|
|
1787
|
-
printStatusLine("Memory file",
|
|
1788
|
-
printStatusLine("Project config",
|
|
541
|
+
printStatusLine("Env file", `${existsSync(envPath) ? "present" : "missing"} (${envPath.split("/").pop()})`);
|
|
542
|
+
printStatusLine("Memory file", existsSync(memoryFilePath) ? MEMORY_FILE : chalk.gray("not exported"));
|
|
543
|
+
printStatusLine("Project config", existsSync(join(process.cwd(), CONFIG_FILE)) ? CONFIG_FILE : chalk.gray("missing"));
|
|
1789
544
|
printStatusLine("Generated files", String(generatedFiles.length));
|
|
1790
|
-
printStatusLine("Hook",
|
|
1791
|
-
printStatusLine("Stats file",
|
|
545
|
+
printStatusLine("Hook", existsSync(hookPath) ? "installed" : "not installed");
|
|
546
|
+
printStatusLine("Stats file", existsSync(statsPath) ? ".memory-core-stats.json" : chalk.gray("none"));
|
|
1792
547
|
printStatusLine("Graph backend", graphBackendLabel(values));
|
|
1793
548
|
printStatusLine("Graph store", graphStoreFilePath(process.cwd()));
|
|
1794
|
-
printStatusLine("Graph snapshots", graphCount === null ?
|
|
549
|
+
printStatusLine("Graph snapshots", graphCount === null ? chalk.gray("unavailable") : String(graphCount));
|
|
1795
550
|
console.log();
|
|
1796
551
|
printStatusLine("Database URL", redactDatabaseUrl(values.DATABASE_URL ?? ""));
|
|
1797
552
|
printStatusLine("Ollama URL", values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
|
|
@@ -1800,38 +555,38 @@ async function printProjectStatus() {
|
|
|
1800
555
|
printStatusLine("Chat model", model2);
|
|
1801
556
|
console.log();
|
|
1802
557
|
console.log(
|
|
1803
|
-
dbError ?
|
|
558
|
+
dbError ? chalk.red(" \u2717 PostgreSQL ") + chalk.dim(dbError) : chalk.green(" \u2713 PostgreSQL ") + chalk.dim("connected")
|
|
1804
559
|
);
|
|
1805
560
|
console.log(
|
|
1806
|
-
ollamaError ?
|
|
561
|
+
ollamaError ? chalk.red(" \u2717 Ollama ") + chalk.dim(ollamaError) : chalk.green(" \u2713 Ollama ") + chalk.dim("reachable")
|
|
1807
562
|
);
|
|
1808
563
|
if (provider2 !== "ollama") {
|
|
1809
564
|
console.log(
|
|
1810
|
-
values.CHAT_API_KEY ?
|
|
565
|
+
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
566
|
);
|
|
1812
567
|
}
|
|
1813
568
|
console.log();
|
|
1814
569
|
}
|
|
1815
570
|
function printMemorySelection(selection, limit = 4) {
|
|
1816
571
|
const active = selection.activeArchitectures.join(", ") || "none detected";
|
|
1817
|
-
console.log(
|
|
572
|
+
console.log(chalk.gray(` Stack filter: ${active}`));
|
|
1818
573
|
const included = selection.decisions.filter((decision) => decision.status === "included");
|
|
1819
574
|
if (included.length > 0) {
|
|
1820
|
-
console.log(
|
|
575
|
+
console.log(chalk.gray(` Included ${included.length}:`));
|
|
1821
576
|
for (const decision of included.slice(0, limit)) {
|
|
1822
|
-
console.log(
|
|
577
|
+
console.log(chalk.gray(` + ${decision.memory.content} (${decision.reason})`));
|
|
1823
578
|
}
|
|
1824
579
|
if (included.length > limit) {
|
|
1825
|
-
console.log(
|
|
580
|
+
console.log(chalk.gray(` \u2026 ${included.length - limit} more included`));
|
|
1826
581
|
}
|
|
1827
582
|
}
|
|
1828
583
|
if (selection.excluded.length > 0) {
|
|
1829
|
-
console.log(
|
|
584
|
+
console.log(chalk.gray(` Excluded ${selection.excluded.length}:`));
|
|
1830
585
|
for (const decision of selection.excluded.slice(0, limit)) {
|
|
1831
|
-
console.log(
|
|
586
|
+
console.log(chalk.gray(` - ${decision.memory.content} (${decision.reason})`));
|
|
1832
587
|
}
|
|
1833
588
|
if (selection.excluded.length > limit) {
|
|
1834
|
-
console.log(
|
|
589
|
+
console.log(chalk.gray(` \u2026 ${selection.excluded.length - limit} more excluded`));
|
|
1835
590
|
}
|
|
1836
591
|
}
|
|
1837
592
|
}
|
|
@@ -1873,22 +628,22 @@ async function syncGeneratedFiles(config, agents, options = {}) {
|
|
|
1873
628
|
agents
|
|
1874
629
|
);
|
|
1875
630
|
spinner.succeed(
|
|
1876
|
-
`Synced \u2014 ${
|
|
631
|
+
`Synced \u2014 ${chalk.green(`${result.written.length} updated`)}, ${chalk.dim(`${result.skipped.length} already up to date`)}`
|
|
1877
632
|
);
|
|
1878
633
|
if (result.written.length > 0) {
|
|
1879
|
-
result.written.forEach((file) => console.log(
|
|
634
|
+
result.written.forEach((file) => console.log(chalk.gray(` \u2713 ${file}`)));
|
|
1880
635
|
}
|
|
1881
636
|
}
|
|
1882
637
|
async function autoSyncGeneratedFiles(config, action, enabled = true) {
|
|
1883
638
|
if (!enabled) {
|
|
1884
|
-
console.log(
|
|
639
|
+
console.log(chalk.gray(" Auto-sync skipped (--no-sync). Run memory-core sync when ready."));
|
|
1885
640
|
return;
|
|
1886
641
|
}
|
|
1887
642
|
if (!config) {
|
|
1888
643
|
return;
|
|
1889
644
|
}
|
|
1890
645
|
if (config.autoSync === false) {
|
|
1891
|
-
console.log(
|
|
646
|
+
console.log(chalk.gray(" Auto-sync disabled for this project. Run memory-core sync when ready."));
|
|
1892
647
|
return;
|
|
1893
648
|
}
|
|
1894
649
|
try {
|
|
@@ -1896,24 +651,24 @@ async function autoSyncGeneratedFiles(config, action, enabled = true) {
|
|
|
1896
651
|
label: `Auto-syncing agent files after ${action}\u2026`
|
|
1897
652
|
});
|
|
1898
653
|
} catch (err) {
|
|
1899
|
-
console.log(
|
|
1900
|
-
console.log(
|
|
654
|
+
console.log(chalk.yellow(` Auto-sync skipped: ${err.message}`));
|
|
655
|
+
console.log(chalk.gray(" Run memory-core sync manually when ready."));
|
|
1901
656
|
}
|
|
1902
657
|
}
|
|
1903
658
|
var program = new Command();
|
|
1904
659
|
program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version(version);
|
|
1905
660
|
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(
|
|
661
|
+
console.log(chalk.bold.cyan("\n memory-core init\n"));
|
|
1907
662
|
const detected = detectProject();
|
|
1908
663
|
const quick = opts.quick ?? false;
|
|
1909
664
|
let skipEnv = false;
|
|
1910
665
|
let skipProject = false;
|
|
1911
|
-
if (
|
|
666
|
+
if (existsSync(join(process.cwd(), CONFIG_FILE)) && !quick) {
|
|
1912
667
|
const existing = readProjectConfig();
|
|
1913
668
|
const envVals = readRuntimeEnv().values;
|
|
1914
|
-
console.log(
|
|
1915
|
-
console.log(
|
|
1916
|
-
console.log(
|
|
669
|
+
console.log(chalk.dim(` Already initialized: ${existing?.projectName ?? "?"} (${existing?.projectType ?? "?"})`));
|
|
670
|
+
console.log(chalk.dim(` Provider: ${envVals.CHAT_PROVIDER ?? "ollama"} Model: ${envVals.CHAT_MODEL ?? "llama3.2"}`));
|
|
671
|
+
console.log(chalk.dim(` Hook: ${existsSync(join(".git", "hooks", "pre-commit")) ? "installed" : "not installed"} Agents: ${existing?.agents?.length ?? 0}
|
|
1917
672
|
`));
|
|
1918
673
|
const reinitChoice = await select({
|
|
1919
674
|
message: "Already initialized \u2014 what do you want to do?",
|
|
@@ -1933,8 +688,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1933
688
|
}
|
|
1934
689
|
let pgOk = false;
|
|
1935
690
|
let ollamaOk = false;
|
|
1936
|
-
const envPath =
|
|
1937
|
-
const hasEnv =
|
|
691
|
+
const envPath = join(process.cwd(), ".memory-core.env");
|
|
692
|
+
const hasEnv = existsSync(envPath);
|
|
1938
693
|
if (skipEnv) {
|
|
1939
694
|
try {
|
|
1940
695
|
const { Pool } = (await import("pg")).default;
|
|
@@ -1964,9 +719,9 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1964
719
|
writeRuntimeEnv(envValues, envPath);
|
|
1965
720
|
applyRuntimeEnv(envValues);
|
|
1966
721
|
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
1967
|
-
console.log(
|
|
722
|
+
console.log(chalk.green(" \u2713 .memory-core.env created with local defaults"));
|
|
1968
723
|
} else if (!hasEnv) {
|
|
1969
|
-
console.log(
|
|
724
|
+
console.log(chalk.dim(" No .memory-core.env found \u2014 let's set up your connection.\n"));
|
|
1970
725
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
1971
726
|
let dbUrl = "";
|
|
1972
727
|
while (true) {
|
|
@@ -1980,12 +735,12 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1980
735
|
const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
|
|
1981
736
|
await testPool.query("SELECT 1");
|
|
1982
737
|
await testPool.end();
|
|
1983
|
-
pgSpinner.succeed(
|
|
738
|
+
pgSpinner.succeed(chalk.green("PostgreSQL connected"));
|
|
1984
739
|
pgOk = true;
|
|
1985
740
|
break;
|
|
1986
741
|
} catch (err) {
|
|
1987
|
-
pgSpinner.fail(
|
|
1988
|
-
console.log(
|
|
742
|
+
pgSpinner.fail(chalk.red(`Cannot connect: ${err.message}`));
|
|
743
|
+
console.log(chalk.yellow(" Please check that PostgreSQL is running and the URL is correct.\n"));
|
|
1989
744
|
}
|
|
1990
745
|
}
|
|
1991
746
|
let ollamaUrl = "";
|
|
@@ -1998,12 +753,12 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1998
753
|
try {
|
|
1999
754
|
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
2000
755
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2001
|
-
ollamaSpinner.succeed(
|
|
756
|
+
ollamaSpinner.succeed(chalk.green("Ollama connected"));
|
|
2002
757
|
ollamaOk = true;
|
|
2003
758
|
break;
|
|
2004
759
|
} catch (err) {
|
|
2005
|
-
ollamaSpinner.fail(
|
|
2006
|
-
console.log(
|
|
760
|
+
ollamaSpinner.fail(chalk.red(`Cannot reach Ollama: ${err.message}`));
|
|
761
|
+
console.log(chalk.yellow(" Make sure Ollama is running: ollama serve\n"));
|
|
2007
762
|
}
|
|
2008
763
|
}
|
|
2009
764
|
const chatProvider = await select({
|
|
@@ -2017,7 +772,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2017
772
|
]
|
|
2018
773
|
});
|
|
2019
774
|
if (chatProvider !== "ollama") {
|
|
2020
|
-
console.log(
|
|
775
|
+
console.log(chalk.dim(" Note: Ollama is still used for search embeddings. Code checking uses the cloud provider above."));
|
|
2021
776
|
}
|
|
2022
777
|
let chatModel = "";
|
|
2023
778
|
let chatApiKey = "";
|
|
@@ -2045,15 +800,15 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2045
800
|
const match = exact ?? prefixed;
|
|
2046
801
|
if (match) {
|
|
2047
802
|
chatModel = match.name;
|
|
2048
|
-
modelSpinner.succeed(
|
|
803
|
+
modelSpinner.succeed(chalk.green(`${chatModel} is installed and ready`));
|
|
2049
804
|
break;
|
|
2050
805
|
} else {
|
|
2051
|
-
modelSpinner.fail(
|
|
2052
|
-
console.log(
|
|
806
|
+
modelSpinner.fail(chalk.red(`${chatModel} is not installed in your Ollama`));
|
|
807
|
+
console.log(chalk.yellow(` Run: ollama pull ${chatModel} \u2014 or pick a different model.
|
|
2053
808
|
`));
|
|
2054
809
|
}
|
|
2055
810
|
} catch {
|
|
2056
|
-
modelSpinner.warn(
|
|
811
|
+
modelSpinner.warn(chalk.yellow("Could not verify model \u2014 continuing anyway"));
|
|
2057
812
|
break;
|
|
2058
813
|
}
|
|
2059
814
|
}
|
|
@@ -2097,7 +852,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2097
852
|
chatApiKey = await input({
|
|
2098
853
|
message: `${providerLabel(chatProvider)} API key?`
|
|
2099
854
|
});
|
|
2100
|
-
console.log(
|
|
855
|
+
console.log(chalk.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
|
|
2101
856
|
}
|
|
2102
857
|
const envValues = {
|
|
2103
858
|
DATABASE_URL: dbUrl,
|
|
@@ -2112,8 +867,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2112
867
|
writeRuntimeEnv(envValues, envPath);
|
|
2113
868
|
applyRuntimeEnv(envValues);
|
|
2114
869
|
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
2115
|
-
console.log(
|
|
2116
|
-
console.log(
|
|
870
|
+
console.log(chalk.green("\n \u2713 .memory-core.env created"));
|
|
871
|
+
console.log(chalk.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
|
|
2117
872
|
} else {
|
|
2118
873
|
try {
|
|
2119
874
|
const { Pool } = (await import("pg")).default;
|
|
@@ -2150,10 +905,13 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2150
905
|
const backendProfiles = listProfiles("backend");
|
|
2151
906
|
backendArchitecture = await select({
|
|
2152
907
|
message: "Backend architecture?",
|
|
2153
|
-
choices:
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
908
|
+
choices: [
|
|
909
|
+
...backendProfiles.map((p) => ({
|
|
910
|
+
value: p.name,
|
|
911
|
+
name: `${p.displayName} \u2014 ${p.description}`
|
|
912
|
+
})),
|
|
913
|
+
{ value: "custom", name: "Custom / Not defined yet \u2014 I'll add rules with memory-core remember" }
|
|
914
|
+
]
|
|
2157
915
|
});
|
|
2158
916
|
}
|
|
2159
917
|
}
|
|
@@ -2171,10 +929,13 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2171
929
|
const frontendProfiles = listProfiles("frontend");
|
|
2172
930
|
frontendFramework = await select({
|
|
2173
931
|
message: "Frontend framework?",
|
|
2174
|
-
choices:
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
932
|
+
choices: [
|
|
933
|
+
...frontendProfiles.map((p) => ({
|
|
934
|
+
value: p.name,
|
|
935
|
+
name: `${p.displayName} \u2014 ${p.description}`
|
|
936
|
+
})),
|
|
937
|
+
{ value: "custom", name: "Custom / Not defined yet \u2014 I'll add rules with memory-core remember" }
|
|
938
|
+
]
|
|
2178
939
|
});
|
|
2179
940
|
}
|
|
2180
941
|
}
|
|
@@ -2230,18 +991,18 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2230
991
|
}
|
|
2231
992
|
if (!quick) {
|
|
2232
993
|
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 ${
|
|
994
|
+
console.log(chalk.bold("\n Ready to initialize\n"));
|
|
995
|
+
console.log(` Project ${chalk.white(projectName)} (${projectType})`);
|
|
996
|
+
if (backendArchitecture) console.log(` Backend ${chalk.white(backendArchitecture === "custom" ? "Custom (no profile)" : backendArchitecture)}`);
|
|
997
|
+
if (frontendFramework) console.log(` Frontend ${chalk.white(frontendFramework === "custom" ? "Custom (no profile)" : frontendFramework)}`);
|
|
998
|
+
console.log(` Language ${chalk.white(language)}`);
|
|
999
|
+
console.log(` Provider ${chalk.white(envVals.CHAT_PROVIDER ?? "ollama")} / ${chalk.white(envVals.CHAT_MODEL ?? DEFAULT_CHAT_MODEL)}`);
|
|
1000
|
+
console.log(` Agents ${chalk.white(String(selectedAgents.length))} selected`);
|
|
1001
|
+
console.log(` Hook ${chalk.white(enableHook ? hookAdvisory ? "advisory" : "strict" : "skip")}`);
|
|
2241
1002
|
console.log();
|
|
2242
1003
|
const proceed = await confirm({ message: "Generate files?", default: true });
|
|
2243
1004
|
if (!proceed) {
|
|
2244
|
-
console.log(
|
|
1005
|
+
console.log(chalk.yellow(" Cancelled.\n"));
|
|
2245
1006
|
await closePool();
|
|
2246
1007
|
process.exit(0);
|
|
2247
1008
|
}
|
|
@@ -2260,16 +1021,16 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2260
1021
|
};
|
|
2261
1022
|
let memories = [];
|
|
2262
1023
|
try {
|
|
2263
|
-
const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
|
|
1024
|
+
const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).filter((a) => a !== "custom").join(" ");
|
|
2264
1025
|
const selection = await retrieveMemorySelection({
|
|
2265
|
-
query: archQuery,
|
|
1026
|
+
query: archQuery || language,
|
|
2266
1027
|
cwd: process.cwd(),
|
|
2267
1028
|
config,
|
|
2268
1029
|
limit: 20
|
|
2269
1030
|
});
|
|
2270
1031
|
memories = selection.included;
|
|
2271
1032
|
if (memories.length > 0) {
|
|
2272
|
-
console.log(
|
|
1033
|
+
console.log(chalk.dim(` Found ${memories.length} relevant memories`));
|
|
2273
1034
|
printMemorySelection(selection);
|
|
2274
1035
|
}
|
|
2275
1036
|
} catch {
|
|
@@ -2295,7 +1056,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2295
1056
|
if (gitignoreEntries.length > 0) {
|
|
2296
1057
|
const added = appendMissingGitignoreEntries(gitignoreEntries, GITIGNORE_HEADING);
|
|
2297
1058
|
if (added > 0) {
|
|
2298
|
-
console.log(
|
|
1059
|
+
console.log(chalk.green(` \u2713 Added ${added} generated files to .gitignore`));
|
|
2299
1060
|
}
|
|
2300
1061
|
}
|
|
2301
1062
|
if (enableHook) {
|
|
@@ -2303,12 +1064,19 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2303
1064
|
}
|
|
2304
1065
|
const chatModelForBanner = process.env.CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
|
|
2305
1066
|
printBanner(config.projectName, written.written.length, { postgresOk: pgOk, ollamaOk, chatModel: chatModelForBanner });
|
|
1067
|
+
if (backendArchitecture === "custom" || frontendFramework === "custom") {
|
|
1068
|
+
console.log(chalk.yellow("\n Custom architecture \u2014 no profile rules loaded."));
|
|
1069
|
+
console.log(chalk.dim(" Add rules as your architecture takes shape:"));
|
|
1070
|
+
console.log(chalk.dim(' memory-core remember "Your rule" --type rule'));
|
|
1071
|
+
console.log(chalk.dim(" Or load a profile later when you decide:"));
|
|
1072
|
+
console.log(chalk.dim(" memory-core seed --arch clean-architecture\n"));
|
|
1073
|
+
}
|
|
2306
1074
|
await closePool();
|
|
2307
1075
|
});
|
|
2308
1076
|
program.command("sync").description("Re-pull memories and regenerate AI agent files").action(async () => {
|
|
2309
1077
|
const config = readProjectConfig();
|
|
2310
1078
|
if (!config) {
|
|
2311
|
-
console.error(
|
|
1079
|
+
console.error(chalk.red("No .memory-core.json found. Run: memory-core init"));
|
|
2312
1080
|
process.exit(1);
|
|
2313
1081
|
}
|
|
2314
1082
|
const { checkbox } = await import("@inquirer/prompts");
|
|
@@ -2323,7 +1091,7 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
2323
1091
|
instructions: " (Space to toggle, A to select all, Enter to confirm)"
|
|
2324
1092
|
});
|
|
2325
1093
|
if (selectedAgents.length === 0) {
|
|
2326
|
-
console.log(
|
|
1094
|
+
console.log(chalk.yellow(" No agents selected \u2014 nothing to sync."));
|
|
2327
1095
|
process.exit(0);
|
|
2328
1096
|
}
|
|
2329
1097
|
await syncGeneratedFiles(config, [...selectedAgents, "Shared"], { showSelection: true });
|
|
@@ -2332,24 +1100,24 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
2332
1100
|
program.command("auto-sync [mode]").description("Show or change automatic agent file sync (on|off)").action((mode) => {
|
|
2333
1101
|
const config = readProjectConfig();
|
|
2334
1102
|
if (!config) {
|
|
2335
|
-
console.error(
|
|
1103
|
+
console.error(chalk.red("No .memory-core.json found. Run: memory-core init"));
|
|
2336
1104
|
process.exit(1);
|
|
2337
1105
|
}
|
|
2338
1106
|
const normalized = mode?.trim().toLowerCase();
|
|
2339
1107
|
if (!normalized || normalized === "status") {
|
|
2340
|
-
console.log(
|
|
2341
|
-
console.log(` Status: ${config.autoSync === false ?
|
|
2342
|
-
console.log(
|
|
1108
|
+
console.log(chalk.bold("\n Auto-sync\n"));
|
|
1109
|
+
console.log(` Status: ${config.autoSync === false ? chalk.yellow("disabled") : chalk.green("enabled")}`);
|
|
1110
|
+
console.log(chalk.gray(" Manual sync is always available: memory-core sync\n"));
|
|
2343
1111
|
return;
|
|
2344
1112
|
}
|
|
2345
1113
|
if (normalized !== "on" && normalized !== "off") {
|
|
2346
|
-
console.error(
|
|
1114
|
+
console.error(chalk.red("Use: memory-core auto-sync [on|off|status]"));
|
|
2347
1115
|
process.exit(1);
|
|
2348
1116
|
}
|
|
2349
1117
|
const enabled = normalized === "on";
|
|
2350
1118
|
writeProjectConfig({ ...config, autoSync: enabled });
|
|
2351
|
-
console.log(
|
|
2352
|
-
console.log(
|
|
1119
|
+
console.log(chalk.green(`Auto-sync ${enabled ? "enabled" : "disabled"}`));
|
|
1120
|
+
console.log(chalk.gray(" Manual sync is always available: memory-core sync"));
|
|
2353
1121
|
});
|
|
2354
1122
|
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
1123
|
const config = readProjectConfig();
|
|
@@ -2357,7 +1125,7 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
2357
1125
|
let reason = opts.reason;
|
|
2358
1126
|
if (!reason) {
|
|
2359
1127
|
reason = await input({
|
|
2360
|
-
message:
|
|
1128
|
+
message: chalk.dim("Why should this memory exist?"),
|
|
2361
1129
|
default: ""
|
|
2362
1130
|
});
|
|
2363
1131
|
}
|
|
@@ -2374,11 +1142,11 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
2374
1142
|
context: buildMemoryContext(opts),
|
|
2375
1143
|
tags: parseTags(opts.tags)
|
|
2376
1144
|
});
|
|
2377
|
-
const dbVersionPath =
|
|
2378
|
-
|
|
2379
|
-
const reasonLine =
|
|
1145
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
1146
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
1147
|
+
const reasonLine = chalk.gray(`
|
|
2380
1148
|
Why: ${storedReason}`);
|
|
2381
|
-
spinner.succeed(
|
|
1149
|
+
spinner.succeed(chalk.green(`Memory saved: "${text}"`) + reasonLine);
|
|
2382
1150
|
await autoSyncGeneratedFiles(config, "remember", opts.sync);
|
|
2383
1151
|
} catch (err) {
|
|
2384
1152
|
spinner.fail(`Failed: ${err.message}`);
|
|
@@ -2399,20 +1167,20 @@ program.command("search <query>").description("Search memories using semantic si
|
|
|
2399
1167
|
const results = result.items;
|
|
2400
1168
|
spinner.stop();
|
|
2401
1169
|
if (results.length === 0) {
|
|
2402
|
-
console.log(
|
|
1170
|
+
console.log(chalk.yellow("No memories found."));
|
|
2403
1171
|
} else {
|
|
2404
|
-
console.log(
|
|
1172
|
+
console.log(chalk.bold(`
|
|
2405
1173
|
${results.length} results for "${query}"
|
|
2406
1174
|
`));
|
|
2407
1175
|
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(
|
|
1176
|
+
const sim = m.similarity ? chalk.gray(` (${(m.similarity * 100).toFixed(0)}% match)`) : "";
|
|
1177
|
+
console.log(chalk.cyan(` ${i + 1}. [${m.type}] ${m.title ?? ""}`));
|
|
1178
|
+
console.log(chalk.white(` ${m.content}`) + sim);
|
|
1179
|
+
if (m.reason) console.log(chalk.gray(` why: ${m.reason}`));
|
|
1180
|
+
if (m.context?.appliesTo?.length) console.log(chalk.gray(` use when: ${m.context.appliesTo.join("; ")}`));
|
|
1181
|
+
if (m.context?.avoidWhen?.length) console.log(chalk.gray(` avoid when: ${m.context.avoidWhen.join("; ")}`));
|
|
1182
|
+
if (m.context?.examples?.length) console.log(chalk.gray(` examples: ${m.context.examples.join("; ")}`));
|
|
1183
|
+
if (m.tags?.length) console.log(chalk.gray(` tags: ${m.tags.join(", ")}`));
|
|
2416
1184
|
console.log();
|
|
2417
1185
|
});
|
|
2418
1186
|
}
|
|
@@ -2427,9 +1195,9 @@ program.command("export").description(`Export DB memories to ${MEMORY_FILE}`).op
|
|
|
2427
1195
|
try {
|
|
2428
1196
|
const memories = await phase1.services.memoryEngine.list({ limit: 1e4 });
|
|
2429
1197
|
const portable = memories.map(toPortableFromRecord);
|
|
2430
|
-
const outputPath = opts.output ?
|
|
1198
|
+
const outputPath = opts.output ? join(process.cwd(), opts.output) : writeMemoryFile(portable);
|
|
2431
1199
|
if (opts.output) {
|
|
2432
|
-
|
|
1200
|
+
writeFileSync(outputPath, JSON.stringify(portable, null, 2) + "\n", "utf-8");
|
|
2433
1201
|
}
|
|
2434
1202
|
spinner.succeed(`Exported ${portable.length} memories to ${outputPath}`);
|
|
2435
1203
|
} catch (err) {
|
|
@@ -2443,7 +1211,7 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
|
|
|
2443
1211
|
const spinner = ora("Reading memories\u2026").start();
|
|
2444
1212
|
try {
|
|
2445
1213
|
const config = readProjectConfig();
|
|
2446
|
-
const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(
|
|
1214
|
+
const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(readFileSync(join(process.cwd(), opts.file), "utf-8")) : readMemoryFile();
|
|
2447
1215
|
let inserted = 0;
|
|
2448
1216
|
let skipped = 0;
|
|
2449
1217
|
spinner.text = `Importing ${memories.length} memories\u2026`;
|
|
@@ -2464,8 +1232,8 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
|
|
|
2464
1232
|
}
|
|
2465
1233
|
spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
|
|
2466
1234
|
if (inserted > 0) {
|
|
2467
|
-
const dbVersionPath =
|
|
2468
|
-
|
|
1235
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
1236
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2469
1237
|
await autoSyncGeneratedFiles(config, "import", opts.sync);
|
|
2470
1238
|
}
|
|
2471
1239
|
} catch (err) {
|
|
@@ -2491,10 +1259,10 @@ program.command("list").description("List memories from the local database").opt
|
|
|
2491
1259
|
const title = opts.all ? "All memories" : `Current project memories${architectures ? ` (${Array.isArray(architectures) ? architectures.join(", ") : architectures})` : ""}`;
|
|
2492
1260
|
printMemoryTable(memories.map(toMemoryTableRow), title);
|
|
2493
1261
|
if (!opts.all) {
|
|
2494
|
-
console.log(
|
|
1262
|
+
console.log(chalk.gray(" Showing current project context plus shared/global memories. Use --all for the full database.\n"));
|
|
2495
1263
|
}
|
|
2496
1264
|
} catch (err) {
|
|
2497
|
-
console.error(
|
|
1265
|
+
console.error(chalk.red(`List failed: ${err.message}`));
|
|
2498
1266
|
process.exit(1);
|
|
2499
1267
|
} finally {
|
|
2500
1268
|
await closePool();
|
|
@@ -2505,13 +1273,13 @@ program.command("remove <id>").description("Remove a memory by ID").option("--no
|
|
|
2505
1273
|
const config = readProjectConfig();
|
|
2506
1274
|
const deleted = await phase1.services.memoryEngine.removeById(parseInt(id, 10));
|
|
2507
1275
|
if (!deleted) {
|
|
2508
|
-
console.log(
|
|
1276
|
+
console.log(chalk.yellow(`No memory found with ID ${id}`));
|
|
2509
1277
|
process.exit(1);
|
|
2510
1278
|
}
|
|
2511
|
-
console.log(
|
|
1279
|
+
console.log(chalk.green(`Removed memory ${id}`));
|
|
2512
1280
|
await autoSyncGeneratedFiles(config, "remove", opts.sync);
|
|
2513
1281
|
} catch (err) {
|
|
2514
|
-
console.error(
|
|
1282
|
+
console.error(chalk.red(`Remove failed: ${err.message}`));
|
|
2515
1283
|
process.exit(1);
|
|
2516
1284
|
} finally {
|
|
2517
1285
|
await closePool();
|
|
@@ -2526,12 +1294,12 @@ program.command("forget").description("Bulk-delete memories by tag, scope, type,
|
|
|
2526
1294
|
type: opts.type,
|
|
2527
1295
|
architecture: opts.arch
|
|
2528
1296
|
});
|
|
2529
|
-
console.log(
|
|
1297
|
+
console.log(chalk.green(`Deleted ${deleted} memories`));
|
|
2530
1298
|
if (deleted > 0) {
|
|
2531
1299
|
await autoSyncGeneratedFiles(config, "forget", opts.sync);
|
|
2532
1300
|
}
|
|
2533
1301
|
} catch (err) {
|
|
2534
|
-
console.error(
|
|
1302
|
+
console.error(chalk.red(`Forget failed: ${err.message}`));
|
|
2535
1303
|
process.exit(1);
|
|
2536
1304
|
} finally {
|
|
2537
1305
|
await closePool();
|
|
@@ -2543,7 +1311,7 @@ program.command("edit <id>").description("Edit a memory interactively").option("
|
|
|
2543
1311
|
const config = readProjectConfig();
|
|
2544
1312
|
const existing = await phase1.services.memoryEngine.getById(memoryId);
|
|
2545
1313
|
if (!existing) {
|
|
2546
|
-
console.log(
|
|
1314
|
+
console.log(chalk.yellow(`No memory found with ID ${id}`));
|
|
2547
1315
|
process.exit(1);
|
|
2548
1316
|
}
|
|
2549
1317
|
const type = await input({ message: "Type?", default: existing.type });
|
|
@@ -2565,10 +1333,10 @@ program.command("edit <id>").description("Edit a memory interactively").option("
|
|
|
2565
1333
|
context: buildMemoryContext({ appliesTo, avoidWhen, example: examples, source }),
|
|
2566
1334
|
tags: parseTags(tags)
|
|
2567
1335
|
});
|
|
2568
|
-
console.log(
|
|
1336
|
+
console.log(chalk.green(`Updated memory ${id}`));
|
|
2569
1337
|
await autoSyncGeneratedFiles(config, "edit", opts.sync);
|
|
2570
1338
|
} catch (err) {
|
|
2571
|
-
console.error(
|
|
1339
|
+
console.error(chalk.red(`Edit failed: ${err.message}`));
|
|
2572
1340
|
process.exit(1);
|
|
2573
1341
|
} finally {
|
|
2574
1342
|
await closePool();
|
|
@@ -2585,15 +1353,15 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
2585
1353
|
if (opts.remove) {
|
|
2586
1354
|
const deleted = await phase1.services.memoryEngine.removeById(parseInt(opts.remove, 10));
|
|
2587
1355
|
if (!deleted) {
|
|
2588
|
-
console.log(
|
|
1356
|
+
console.log(chalk.yellow(`No ignore pattern found with ID ${opts.remove}`));
|
|
2589
1357
|
process.exit(1);
|
|
2590
1358
|
}
|
|
2591
|
-
console.log(
|
|
1359
|
+
console.log(chalk.green(`Removed ignore pattern ${opts.remove}`));
|
|
2592
1360
|
await autoSyncGeneratedFiles(config, "ignore remove", opts.sync);
|
|
2593
1361
|
return;
|
|
2594
1362
|
}
|
|
2595
1363
|
if (!pattern) {
|
|
2596
|
-
console.error(
|
|
1364
|
+
console.error(chalk.red("Provide a pattern, --list, or --remove <id>"));
|
|
2597
1365
|
process.exit(1);
|
|
2598
1366
|
}
|
|
2599
1367
|
await phase1.services.memoryEngine.remember({
|
|
@@ -2604,12 +1372,12 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
2604
1372
|
content: pattern,
|
|
2605
1373
|
tags: ["ignore"]
|
|
2606
1374
|
});
|
|
2607
|
-
const dbVersionPath =
|
|
2608
|
-
|
|
2609
|
-
console.log(
|
|
1375
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
1376
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
1377
|
+
console.log(chalk.green(`Ignored pattern saved: "${pattern}"`));
|
|
2610
1378
|
await autoSyncGeneratedFiles(config, "ignore", opts.sync);
|
|
2611
1379
|
} catch (err) {
|
|
2612
|
-
console.error(
|
|
1380
|
+
console.error(chalk.red(`Ignore failed: ${err.message}`));
|
|
2613
1381
|
process.exit(1);
|
|
2614
1382
|
} finally {
|
|
2615
1383
|
await closePool();
|
|
@@ -2619,10 +1387,10 @@ program.command("allow [pattern]").description("Manage project allow patterns in
|
|
|
2619
1387
|
if (opts.list) {
|
|
2620
1388
|
const patterns = getAllowPatterns(readProjectConfig());
|
|
2621
1389
|
if (patterns.length === 0) {
|
|
2622
|
-
console.log(
|
|
1390
|
+
console.log(chalk.yellow("\n No allow patterns configured.\n"));
|
|
2623
1391
|
return;
|
|
2624
1392
|
}
|
|
2625
|
-
console.log(
|
|
1393
|
+
console.log(chalk.bold("\n Allow patterns\n"));
|
|
2626
1394
|
patterns.forEach((entry, index) => console.log(` ${index + 1}. ${entry}`));
|
|
2627
1395
|
console.log();
|
|
2628
1396
|
return;
|
|
@@ -2632,35 +1400,35 @@ program.command("allow [pattern]").description("Manage project allow patterns in
|
|
|
2632
1400
|
...config,
|
|
2633
1401
|
allowPatterns: getAllowPatterns(config).filter((entry) => entry !== opts.remove)
|
|
2634
1402
|
}));
|
|
2635
|
-
console.log(
|
|
1403
|
+
console.log(chalk.green(`Removed allow pattern: "${opts.remove}"`));
|
|
2636
1404
|
return;
|
|
2637
1405
|
}
|
|
2638
1406
|
if (!pattern) {
|
|
2639
|
-
console.error(
|
|
1407
|
+
console.error(chalk.red("Provide a pattern, --list, or --remove <pattern>"));
|
|
2640
1408
|
process.exit(1);
|
|
2641
1409
|
}
|
|
2642
1410
|
updateProjectConfig((config) => ({
|
|
2643
1411
|
...config,
|
|
2644
1412
|
allowPatterns: [.../* @__PURE__ */ new Set([...getAllowPatterns(config), pattern])]
|
|
2645
1413
|
}));
|
|
2646
|
-
console.log(
|
|
1414
|
+
console.log(chalk.green(`Allow pattern saved: "${pattern}"`));
|
|
2647
1415
|
});
|
|
2648
1416
|
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
1417
|
if (opts.list) {
|
|
2650
1418
|
const rules = readProjectConfig()?.commitRules ?? [];
|
|
2651
1419
|
if (rules.length === 0) {
|
|
2652
|
-
console.log(
|
|
1420
|
+
console.log(chalk.yellow("\n No commit rules configured.\n"));
|
|
2653
1421
|
return;
|
|
2654
1422
|
}
|
|
2655
|
-
console.log(
|
|
1423
|
+
console.log(chalk.bold("\n Commit message rules\n"));
|
|
2656
1424
|
rules.forEach((rule, i) => {
|
|
2657
1425
|
const flags = [
|
|
2658
1426
|
rule.negate ? "must NOT match" : "must match",
|
|
2659
1427
|
rule.advisory ? "advisory" : "blocking"
|
|
2660
1428
|
].join(", ");
|
|
2661
1429
|
console.log(` ${i + 1}. ${rule.pattern}`);
|
|
2662
|
-
console.log(
|
|
2663
|
-
console.log(
|
|
1430
|
+
console.log(chalk.dim(` Message: ${rule.message}`));
|
|
1431
|
+
console.log(chalk.dim(` Flags: ${flags}`));
|
|
2664
1432
|
console.log();
|
|
2665
1433
|
});
|
|
2666
1434
|
return;
|
|
@@ -2670,21 +1438,21 @@ program.command("commit-rules [pattern]").description("Manage commit message rul
|
|
|
2670
1438
|
...config,
|
|
2671
1439
|
commitRules: (config.commitRules ?? []).filter((r) => r.pattern !== opts.remove)
|
|
2672
1440
|
}));
|
|
2673
|
-
console.log(
|
|
1441
|
+
console.log(chalk.green(`Commit rule removed: "${opts.remove}"`));
|
|
2674
1442
|
return;
|
|
2675
1443
|
}
|
|
2676
1444
|
if (!pattern) {
|
|
2677
|
-
console.error(
|
|
1445
|
+
console.error(chalk.red("Provide a pattern, --list, or --remove <pattern>"));
|
|
2678
1446
|
process.exit(1);
|
|
2679
1447
|
}
|
|
2680
1448
|
if (!opts.message) {
|
|
2681
|
-
console.error(
|
|
1449
|
+
console.error(chalk.red("--message is required when adding a commit rule"));
|
|
2682
1450
|
process.exit(1);
|
|
2683
1451
|
}
|
|
2684
1452
|
try {
|
|
2685
1453
|
new RegExp(pattern);
|
|
2686
1454
|
} catch {
|
|
2687
|
-
console.error(
|
|
1455
|
+
console.error(chalk.red(`Invalid regex pattern: "${pattern}"`));
|
|
2688
1456
|
process.exit(1);
|
|
2689
1457
|
}
|
|
2690
1458
|
const newRule = {
|
|
@@ -2697,13 +1465,13 @@ program.command("commit-rules [pattern]").description("Manage commit message rul
|
|
|
2697
1465
|
...config,
|
|
2698
1466
|
commitRules: [...(config.commitRules ?? []).filter((r) => r.pattern !== pattern), newRule]
|
|
2699
1467
|
}));
|
|
2700
|
-
console.log(
|
|
2701
|
-
console.log(
|
|
1468
|
+
console.log(chalk.green(`Commit rule saved: "${pattern}"`));
|
|
1469
|
+
console.log(chalk.dim(" Run: memory-core commit-rules --list to see all rules"));
|
|
2702
1470
|
});
|
|
2703
1471
|
program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
|
|
2704
|
-
const workflowPath =
|
|
1472
|
+
const workflowPath = join(process.cwd(), ".github", "workflows", "memory-core.yml");
|
|
2705
1473
|
mkdirSync(dirname(workflowPath), { recursive: true });
|
|
2706
|
-
|
|
1474
|
+
writeFileSync(workflowPath, `name: memory-core
|
|
2707
1475
|
on: [pull_request]
|
|
2708
1476
|
jobs:
|
|
2709
1477
|
check:
|
|
@@ -2714,7 +1482,7 @@ jobs:
|
|
|
2714
1482
|
fetch-depth: 0
|
|
2715
1483
|
- run: npx @shahmilsaari/memory-core check --ci
|
|
2716
1484
|
`, "utf-8");
|
|
2717
|
-
console.log(
|
|
1485
|
+
console.log(chalk.green(`Generated ${workflowPath}`));
|
|
2718
1486
|
});
|
|
2719
1487
|
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
1488
|
let removed = removeProjectFiles(OUTPUT_FILES.map((file) => file.path)).length;
|
|
@@ -2730,14 +1498,14 @@ program.command("reset").description("Remove memory-core generated files and loc
|
|
|
2730
1498
|
if (ok) {
|
|
2731
1499
|
await getPool().query("DROP TABLE IF EXISTS memories");
|
|
2732
1500
|
await closePool();
|
|
2733
|
-
console.log(
|
|
1501
|
+
console.log(chalk.yellow("Dropped memories table"));
|
|
2734
1502
|
}
|
|
2735
1503
|
}
|
|
2736
|
-
console.log(
|
|
1504
|
+
console.log(chalk.green(`Reset complete. Removed ${removed} files.`));
|
|
2737
1505
|
});
|
|
2738
1506
|
program.command("uninstall").description("Remove memory-core from the current project").option("--db", "Also drop the memories table after confirmation").action(async (opts) => {
|
|
2739
1507
|
const generatedFiles = OUTPUT_FILES.map((file) => file.path);
|
|
2740
|
-
const gitignoreEntries = [...generatedFiles, ...
|
|
1508
|
+
const gitignoreEntries = [...generatedFiles, ...LOCAL_STATE_FILES];
|
|
2741
1509
|
const removed = removeProjectFiles([
|
|
2742
1510
|
...generatedFiles,
|
|
2743
1511
|
...LOCAL_STATE_FILES,
|
|
@@ -2753,19 +1521,19 @@ program.command("uninstall").description("Remove memory-core from the current pr
|
|
|
2753
1521
|
if (ok) {
|
|
2754
1522
|
await getPool().query("DROP TABLE IF EXISTS memories");
|
|
2755
1523
|
await closePool();
|
|
2756
|
-
console.log(
|
|
1524
|
+
console.log(chalk.yellow("Dropped memories table"));
|
|
2757
1525
|
}
|
|
2758
1526
|
}
|
|
2759
|
-
console.log(
|
|
1527
|
+
console.log(chalk.green(`Uninstall complete. Removed ${removed.length} files.`));
|
|
2760
1528
|
if (removed.length > 0) {
|
|
2761
|
-
removed.forEach((file) => console.log(
|
|
1529
|
+
removed.forEach((file) => console.log(chalk.gray(` \u2713 ${file}`)));
|
|
2762
1530
|
}
|
|
2763
1531
|
if (cleanedGitignore) {
|
|
2764
|
-
console.log(
|
|
1532
|
+
console.log(chalk.gray(" \u2713 cleaned .gitignore memory-core block"));
|
|
2765
1533
|
}
|
|
2766
1534
|
});
|
|
2767
1535
|
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 =
|
|
1536
|
+
const statsPath = join(process.cwd(), ".memory-core-stats.json");
|
|
2769
1537
|
if (opts.reset) {
|
|
2770
1538
|
const emptyStats = {
|
|
2771
1539
|
rules: {},
|
|
@@ -2777,27 +1545,27 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2777
1545
|
},
|
|
2778
1546
|
recentViolations: []
|
|
2779
1547
|
};
|
|
2780
|
-
|
|
2781
|
-
console.log(
|
|
1548
|
+
writeFileSync(statsPath, JSON.stringify(emptyStats, null, 2) + "\n", "utf-8");
|
|
1549
|
+
console.log(chalk.green("\n Violation stats reset.\n"));
|
|
2782
1550
|
return;
|
|
2783
1551
|
}
|
|
2784
|
-
if (!
|
|
2785
|
-
console.log(
|
|
1552
|
+
if (!existsSync(statsPath)) {
|
|
1553
|
+
console.log(chalk.yellow("\n No violation stats recorded yet.\n"));
|
|
2786
1554
|
return;
|
|
2787
1555
|
}
|
|
2788
|
-
const stats = JSON.parse(
|
|
1556
|
+
const stats = JSON.parse(readFileSync(statsPath, "utf-8"));
|
|
2789
1557
|
const toEntry = (raw) => {
|
|
2790
1558
|
if (raw === void 0) return { count: 0, falsePositives: 0 };
|
|
2791
1559
|
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
2792
1560
|
return raw;
|
|
2793
1561
|
};
|
|
2794
1562
|
const printTop = (label, values = {}) => {
|
|
2795
|
-
console.log(
|
|
1563
|
+
console.log(chalk.bold(`
|
|
2796
1564
|
${label}
|
|
2797
1565
|
`));
|
|
2798
1566
|
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
1567
|
const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
|
|
2800
|
-
const fpHint = rate > 0 ?
|
|
1568
|
+
const fpHint = rate > 0 ? chalk.dim(` \u2014 ${rate}% false-positive rate`) + (rate > 25 ? " \u26A0\uFE0F" : "") : "";
|
|
2801
1569
|
console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${entry.count} hits${fpHint}`);
|
|
2802
1570
|
});
|
|
2803
1571
|
};
|
|
@@ -2810,18 +1578,18 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2810
1578
|
rate: entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0
|
|
2811
1579
|
})).filter((r) => r.rate > tuneThreshold && r.count >= tuneMinCount).sort((a, b) => b.rate - a.rate);
|
|
2812
1580
|
if (noisy.length === 0) {
|
|
2813
|
-
console.log(
|
|
1581
|
+
console.log(chalk.green(`
|
|
2814
1582
|
\u2713 No noisy rules found (threshold: ${tuneThreshold}%, min hits: ${tuneMinCount})
|
|
2815
1583
|
`));
|
|
2816
1584
|
return;
|
|
2817
1585
|
}
|
|
2818
|
-
console.log(
|
|
1586
|
+
console.log(chalk.bold(`
|
|
2819
1587
|
Noisy rules (>${tuneThreshold}% false-positive rate, \u2265${tuneMinCount} hits)
|
|
2820
1588
|
`));
|
|
2821
1589
|
noisy.forEach(({ name, count, rate }, i) => {
|
|
2822
1590
|
console.log(` ${i + 1}. ${truncate(name, 50).padEnd(52)} ${count} hits \u2014 ${rate}% \u26A0\uFE0F`);
|
|
2823
|
-
console.log(
|
|
2824
|
-
console.log(
|
|
1591
|
+
console.log(chalk.dim(` To disable: memory-core allow "${name}"`));
|
|
1592
|
+
console.log(chalk.dim(` Interactive: memory-core tune`));
|
|
2825
1593
|
console.log();
|
|
2826
1594
|
});
|
|
2827
1595
|
return;
|
|
@@ -2839,20 +1607,20 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2839
1607
|
hasLiveState ? liveFiles : stats.files
|
|
2840
1608
|
);
|
|
2841
1609
|
if (!hasLiveState) {
|
|
2842
|
-
console.log(
|
|
2843
|
-
console.log(
|
|
1610
|
+
console.log(chalk.dim("\n Note: these counters are historical events, not live current code state."));
|
|
1611
|
+
console.log(chalk.dim(" Start watch for live counters, or reset with: memory-core stats --reset\n"));
|
|
2844
1612
|
} else {
|
|
2845
1613
|
if (hasLiveViolations) {
|
|
2846
|
-
console.log(
|
|
1614
|
+
console.log(chalk.dim("\n Live counters auto-refresh while watch is running.\n"));
|
|
2847
1615
|
} else {
|
|
2848
|
-
console.log(
|
|
1616
|
+
console.log(chalk.dim("\n Current live state has no violations.\n"));
|
|
2849
1617
|
}
|
|
2850
1618
|
}
|
|
2851
1619
|
});
|
|
2852
1620
|
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(
|
|
1621
|
+
const statsPath = join(process.cwd(), ".memory-core-stats.json");
|
|
1622
|
+
if (!existsSync(statsPath)) {
|
|
1623
|
+
console.log(chalk.yellow("\n No violation stats yet. Run some commits first.\n"));
|
|
2856
1624
|
return;
|
|
2857
1625
|
}
|
|
2858
1626
|
const threshold = Math.max(0, Math.min(100, parseInt(opts.threshold, 10) || 40));
|
|
@@ -2862,19 +1630,19 @@ program.command("tune").description("Review and disable noisy rules with high fa
|
|
|
2862
1630
|
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
2863
1631
|
return raw;
|
|
2864
1632
|
};
|
|
2865
|
-
const stats = JSON.parse(
|
|
1633
|
+
const stats = JSON.parse(readFileSync(statsPath, "utf-8"));
|
|
2866
1634
|
const noisy = Object.entries(stats.rules ?? {}).map(([rule, raw]) => {
|
|
2867
1635
|
const entry = toEntry(raw);
|
|
2868
1636
|
const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
|
|
2869
1637
|
return { rule, count: entry.count, rate };
|
|
2870
1638
|
}).filter((r) => r.rate > threshold && r.count >= minCount).sort((a, b) => b.rate - a.rate);
|
|
2871
1639
|
if (noisy.length === 0) {
|
|
2872
|
-
console.log(
|
|
1640
|
+
console.log(chalk.green(`
|
|
2873
1641
|
\u2713 All rules within acceptable noise (threshold: ${threshold}%, min hits: ${minCount})
|
|
2874
1642
|
`));
|
|
2875
1643
|
return;
|
|
2876
1644
|
}
|
|
2877
|
-
console.log(
|
|
1645
|
+
console.log(chalk.bold(`
|
|
2878
1646
|
Found ${noisy.length} noisy rule${noisy.length > 1 ? "s" : ""} (>${threshold}% false-positive rate, \u2265${minCount} hits)
|
|
2879
1647
|
`));
|
|
2880
1648
|
const existingAllows = new Set(
|
|
@@ -2885,22 +1653,22 @@ program.command("tune").description("Review and disable noisy rules with high fa
|
|
|
2885
1653
|
for (const { rule, count, rate } of noisy) {
|
|
2886
1654
|
const key = rule.toLowerCase();
|
|
2887
1655
|
if (existingAllows.has(key)) {
|
|
2888
|
-
console.log(
|
|
1656
|
+
console.log(chalk.dim(` \u2022 "${truncate(rule, 56)}" \u2014 already disabled`));
|
|
2889
1657
|
continue;
|
|
2890
1658
|
}
|
|
2891
1659
|
toAdd.add(key);
|
|
2892
|
-
console.log(
|
|
1660
|
+
console.log(chalk.green(` \u2713 "${truncate(rule, 56)}"`) + chalk.dim(` \u2014 ${count} hits, ${rate}% FP rate`));
|
|
2893
1661
|
}
|
|
2894
1662
|
} else {
|
|
2895
1663
|
const { select: select2 } = await import("@inquirer/prompts");
|
|
2896
1664
|
for (let i = 0; i < noisy.length; i++) {
|
|
2897
1665
|
const { rule, count, rate } = noisy[i];
|
|
2898
1666
|
const key = rule.toLowerCase();
|
|
2899
|
-
console.log(
|
|
1667
|
+
console.log(chalk.bold(`
|
|
2900
1668
|
[${i + 1}/${noisy.length}] "${truncate(rule, 60)}"`));
|
|
2901
|
-
console.log(
|
|
1669
|
+
console.log(chalk.dim(` ${count} hits \u2014 ${rate}% false-positive rate \u26A0\uFE0F`));
|
|
2902
1670
|
if (existingAllows.has(key)) {
|
|
2903
|
-
console.log(
|
|
1671
|
+
console.log(chalk.dim(" Already in allow patterns \u2014 skipping"));
|
|
2904
1672
|
continue;
|
|
2905
1673
|
}
|
|
2906
1674
|
const choice = await select2({
|
|
@@ -2914,7 +1682,7 @@ program.command("tune").description("Review and disable noisy rules with high fa
|
|
|
2914
1682
|
if (choice === "quit") break;
|
|
2915
1683
|
if (choice === "disable") {
|
|
2916
1684
|
toAdd.add(key);
|
|
2917
|
-
console.log(
|
|
1685
|
+
console.log(chalk.green(" \u2713 Marked for disable"));
|
|
2918
1686
|
}
|
|
2919
1687
|
}
|
|
2920
1688
|
}
|
|
@@ -2923,11 +1691,11 @@ program.command("tune").description("Review and disable noisy rules with high fa
|
|
|
2923
1691
|
...config,
|
|
2924
1692
|
allowPatterns: [.../* @__PURE__ */ new Set([...config.allowPatterns ?? [], ...toAdd])]
|
|
2925
1693
|
}));
|
|
2926
|
-
console.log(
|
|
1694
|
+
console.log(chalk.green(`
|
|
2927
1695
|
\u2713 Saved ${toAdd.size} allow pattern${toAdd.size > 1 ? "s" : ""} to .memory-core.json`));
|
|
2928
|
-
console.log(
|
|
1696
|
+
console.log(chalk.dim(" These rules will no longer block commits.\n"));
|
|
2929
1697
|
} else {
|
|
2930
|
-
console.log(
|
|
1698
|
+
console.log(chalk.dim("\n No changes made.\n"));
|
|
2931
1699
|
}
|
|
2932
1700
|
});
|
|
2933
1701
|
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 +1715,7 @@ program.command("dashboard").description("Start the live Svelte dashboard with W
|
|
|
2947
1715
|
}
|
|
2948
1716
|
return void 0;
|
|
2949
1717
|
};
|
|
2950
|
-
const { startDashboard } = await import("./dashboard-server-
|
|
1718
|
+
const { startDashboard } = await import("./dashboard-server-SSYZLQKB.js");
|
|
2951
1719
|
await startDashboard({
|
|
2952
1720
|
port: parseInt(opts.port, 10),
|
|
2953
1721
|
path: resolveDashboardPath(),
|
|
@@ -2957,7 +1725,7 @@ program.command("dashboard").description("Start the live Svelte dashboard with W
|
|
|
2957
1725
|
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
1726
|
await runMigrations();
|
|
2959
1727
|
const filtered = opts.arch ? seeds.filter((s) => s.architecture === opts.arch || s.architecture === "global") : seeds;
|
|
2960
|
-
console.log(
|
|
1728
|
+
console.log(chalk.bold.cyan(`
|
|
2961
1729
|
Seeding ${filtered.length} memories\u2026
|
|
2962
1730
|
`));
|
|
2963
1731
|
let saved = 0;
|
|
@@ -2977,15 +1745,15 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
2977
1745
|
if (opts.force) {
|
|
2978
1746
|
await phase1.services.memoryEngine.rememberForce(payload);
|
|
2979
1747
|
saved++;
|
|
2980
|
-
spinner.succeed(
|
|
1748
|
+
spinner.succeed(chalk.gray(`[${seed.architecture}] ${seed.title}`));
|
|
2981
1749
|
} else {
|
|
2982
1750
|
const result = await phase1.services.memoryEngine.remember(payload);
|
|
2983
1751
|
if (result === "inserted") {
|
|
2984
1752
|
saved++;
|
|
2985
|
-
spinner.succeed(
|
|
1753
|
+
spinner.succeed(chalk.gray(`[${seed.architecture}] ${seed.title}`));
|
|
2986
1754
|
} else {
|
|
2987
1755
|
skipped++;
|
|
2988
|
-
spinner.info(
|
|
1756
|
+
spinner.info(chalk.gray(`Already exists \u2014 [${seed.architecture}] ${seed.title}`));
|
|
2989
1757
|
}
|
|
2990
1758
|
}
|
|
2991
1759
|
} catch (err) {
|
|
@@ -2994,10 +1762,10 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
2994
1762
|
}
|
|
2995
1763
|
}
|
|
2996
1764
|
if (saved > 0) {
|
|
2997
|
-
const dbVersionPath =
|
|
2998
|
-
|
|
1765
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
1766
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2999
1767
|
}
|
|
3000
|
-
console.log(
|
|
1768
|
+
console.log(chalk.bold.green(`
|
|
3001
1769
|
Done. ${saved} memories seeded, ${skipped} skipped.
|
|
3002
1770
|
`));
|
|
3003
1771
|
await closePool();
|
|
@@ -3006,21 +1774,21 @@ program.command("global").description("Sync your memory into every AI agent glob
|
|
|
3006
1774
|
const home = homedir();
|
|
3007
1775
|
const GLOBAL_TARGETS = [
|
|
3008
1776
|
// Claude Code
|
|
3009
|
-
{ label: "Claude Code", path:
|
|
1777
|
+
{ label: "Claude Code", path: join(home, ".claude/CLAUDE.md"), type: "md" },
|
|
3010
1778
|
// GitHub Copilot (VS Code)
|
|
3011
|
-
{ label: "Copilot", path:
|
|
1779
|
+
{ label: "Copilot", path: join(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-copilot" },
|
|
3012
1780
|
// Cursor global rules
|
|
3013
|
-
{ label: "Cursor", path:
|
|
1781
|
+
{ label: "Cursor", path: join(home, ".cursor/rules/memory-core.mdc"), type: "md" },
|
|
3014
1782
|
// Cline (VS Code)
|
|
3015
|
-
{ label: "Cline", path:
|
|
1783
|
+
{ label: "Cline", path: join(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-cline" },
|
|
3016
1784
|
// Continue.dev global config
|
|
3017
|
-
{ label: "Continue.dev", path:
|
|
1785
|
+
{ label: "Continue.dev", path: join(home, ".continue/config.json"), type: "continue" },
|
|
3018
1786
|
// Aider global config
|
|
3019
|
-
{ label: "Aider", path:
|
|
1787
|
+
{ label: "Aider", path: join(home, ".aider.conf.yml"), type: "aider" },
|
|
3020
1788
|
// Zed global settings
|
|
3021
|
-
{ label: "Zed AI", path:
|
|
1789
|
+
{ label: "Zed AI", path: join(home, ".config/zed/settings.json"), type: "zed" },
|
|
3022
1790
|
// Windsurf global rules
|
|
3023
|
-
{ label: "Windsurf", path:
|
|
1791
|
+
{ label: "Windsurf", path: join(home, ".windsurf/rules/memory-core.md"), type: "md" }
|
|
3024
1792
|
];
|
|
3025
1793
|
const spinner = ora("Fetching global memories\u2026").start();
|
|
3026
1794
|
let memories = [];
|
|
@@ -3050,12 +1818,12 @@ ${rulesText}
|
|
|
3050
1818
|
const skipped = [];
|
|
3051
1819
|
const writeFile = (filePath, content) => {
|
|
3052
1820
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
3053
|
-
|
|
1821
|
+
writeFileSync(filePath, content, "utf-8");
|
|
3054
1822
|
};
|
|
3055
1823
|
const readJson = (filePath) => {
|
|
3056
|
-
if (!
|
|
1824
|
+
if (!existsSync(filePath)) return {};
|
|
3057
1825
|
try {
|
|
3058
|
-
return JSON.parse(
|
|
1826
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
3059
1827
|
} catch {
|
|
3060
1828
|
return {};
|
|
3061
1829
|
}
|
|
@@ -3103,14 +1871,14 @@ read:
|
|
|
3103
1871
|
skipped.push(target.label);
|
|
3104
1872
|
}
|
|
3105
1873
|
}
|
|
3106
|
-
spinner.succeed(
|
|
3107
|
-
console.log(
|
|
3108
|
-
written.forEach((l) => console.log(
|
|
1874
|
+
spinner.succeed(chalk.green(`Synced ${memories.length} memories \u2192 ${written.length} agents`));
|
|
1875
|
+
console.log(chalk.green("\n Updated:"));
|
|
1876
|
+
written.forEach((l) => console.log(chalk.gray(` \u2713 ${l}`)));
|
|
3109
1877
|
if (skipped.length) {
|
|
3110
|
-
console.log(
|
|
3111
|
-
skipped.forEach((l) => console.log(
|
|
1878
|
+
console.log(chalk.yellow("\n Skipped (not installed):"));
|
|
1879
|
+
skipped.forEach((l) => console.log(chalk.gray(` \u2717 ${l}`)));
|
|
3112
1880
|
}
|
|
3113
|
-
console.log(
|
|
1881
|
+
console.log(chalk.bold("\n Every AI agent now follows your memory globally.\n"));
|
|
3114
1882
|
await closePool();
|
|
3115
1883
|
});
|
|
3116
1884
|
var provider = program.command("provider").description("Manage the code-checking provider configuration");
|
|
@@ -3156,10 +1924,10 @@ provider.command("set <name>").description("Set the code-checking provider (olla
|
|
|
3156
1924
|
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
3157
1925
|
applyRuntimeEnv(values);
|
|
3158
1926
|
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
3159
|
-
console.log(
|
|
3160
|
-
console.log(
|
|
1927
|
+
console.log(chalk.green(`Updated provider: ${providerName}`));
|
|
1928
|
+
console.log(chalk.gray(` Chat model: ${getConfiguredChatModel(values)}`));
|
|
3161
1929
|
} catch (err) {
|
|
3162
|
-
console.error(
|
|
1930
|
+
console.error(chalk.red(`Provider update failed: ${err.message}`));
|
|
3163
1931
|
process.exit(1);
|
|
3164
1932
|
}
|
|
3165
1933
|
});
|
|
@@ -3181,10 +1949,10 @@ model.command("set <name>").description("Set the chat model used for code checki
|
|
|
3181
1949
|
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
3182
1950
|
applyRuntimeEnv(values);
|
|
3183
1951
|
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
3184
|
-
console.log(
|
|
3185
|
-
console.log(
|
|
1952
|
+
console.log(chalk.green(`Updated ${opts.embedding ? "embedding" : "chat"} model: ${name}`));
|
|
1953
|
+
console.log(chalk.gray(` Provider: ${providerName}`));
|
|
3186
1954
|
} catch (err) {
|
|
3187
|
-
console.error(
|
|
1955
|
+
console.error(chalk.red(`Model update failed: ${err.message}`));
|
|
3188
1956
|
process.exit(1);
|
|
3189
1957
|
}
|
|
3190
1958
|
});
|
|
@@ -3196,7 +1964,7 @@ program.command("status").description("Show the current memory-core project and
|
|
|
3196
1964
|
try {
|
|
3197
1965
|
await printProjectStatus();
|
|
3198
1966
|
} catch (err) {
|
|
3199
|
-
console.error(
|
|
1967
|
+
console.error(chalk.red(`Status failed: ${err.message}`));
|
|
3200
1968
|
process.exit(1);
|
|
3201
1969
|
}
|
|
3202
1970
|
});
|
|
@@ -3204,7 +1972,7 @@ var graph = program.command("graph").description("Build and inspect dependency g
|
|
|
3204
1972
|
graph.command("migrate").description("Create or update PostgreSQL graph snapshot schema").action(async () => {
|
|
3205
1973
|
const { values } = readRuntimeEnv();
|
|
3206
1974
|
if (!values.DATABASE_URL) {
|
|
3207
|
-
console.error(
|
|
1975
|
+
console.error(chalk.red("Graph migration requires DATABASE_URL. Configure it in .memory-core.env or .env."));
|
|
3208
1976
|
process.exit(1);
|
|
3209
1977
|
}
|
|
3210
1978
|
const spinner = ora("Migrating graph snapshot schema\u2026").start();
|
|
@@ -3222,34 +1990,34 @@ graph.command("doctor").description("Inspect graph storage backend health (Postg
|
|
|
3222
1990
|
const filePath = graphStoreFilePath(cwd);
|
|
3223
1991
|
const usesPostgres = Boolean(values.DATABASE_URL);
|
|
3224
1992
|
let ok = true;
|
|
3225
|
-
console.log(
|
|
1993
|
+
console.log(chalk.bold("\n graph doctor\n"));
|
|
3226
1994
|
printStatusLine("Project path", cwd);
|
|
3227
1995
|
printStatusLine("Configured backend", usesPostgres ? "postgres + file fallback" : "file");
|
|
3228
1996
|
printStatusLine("File store", filePath);
|
|
3229
1997
|
console.log();
|
|
3230
1998
|
if (!usesPostgres) {
|
|
3231
|
-
console.log(
|
|
1999
|
+
console.log(chalk.yellow(" \u26A0 DATABASE_URL not set \u2014 using file backend only."));
|
|
3232
2000
|
} else {
|
|
3233
2001
|
try {
|
|
3234
2002
|
await migrateGraphSnapshots();
|
|
3235
2003
|
const probe = await probeGraphSnapshotStore(cwd);
|
|
3236
|
-
console.log(
|
|
2004
|
+
console.log(chalk.green(" \u2713 Graph PostgreSQL") + chalk.dim(` ready (${probe.snapshotCount} snapshot records for this root)`));
|
|
3237
2005
|
} catch (err) {
|
|
3238
2006
|
ok = false;
|
|
3239
|
-
console.log(
|
|
2007
|
+
console.log(chalk.red(" \u2717 Graph PostgreSQL") + chalk.dim(` ${err.message}`));
|
|
3240
2008
|
}
|
|
3241
2009
|
}
|
|
3242
2010
|
try {
|
|
3243
2011
|
const count = await getGraphSnapshotCount(cwd);
|
|
3244
|
-
console.log(
|
|
2012
|
+
console.log(chalk.green(" \u2713 Graph service") + chalk.dim(` readable (${count ?? 0} snapshots visible)`));
|
|
3245
2013
|
} catch (err) {
|
|
3246
2014
|
ok = false;
|
|
3247
|
-
console.log(
|
|
2015
|
+
console.log(chalk.red(" \u2717 Graph service") + chalk.dim(` ${err.message}`));
|
|
3248
2016
|
}
|
|
3249
|
-
if (
|
|
3250
|
-
console.log(
|
|
2017
|
+
if (existsSync(filePath)) {
|
|
2018
|
+
console.log(chalk.green(" \u2713 File fallback") + chalk.dim(" store exists"));
|
|
3251
2019
|
} else {
|
|
3252
|
-
console.log(
|
|
2020
|
+
console.log(chalk.yellow(" \u26A0 File fallback") + chalk.dim(" store not created yet (run graph build once)"));
|
|
3253
2021
|
}
|
|
3254
2022
|
console.log();
|
|
3255
2023
|
if (!ok) process.exit(1);
|
|
@@ -3260,9 +2028,9 @@ graph.command("build").description("Build a dependency graph snapshot and persis
|
|
|
3260
2028
|
try {
|
|
3261
2029
|
const { snapshot } = await phase1.services.graphEngine.buildAndStoreSnapshot({ cwd });
|
|
3262
2030
|
spinner.succeed(`Graph snapshot saved: ${snapshot.id}`);
|
|
3263
|
-
console.log(
|
|
3264
|
-
console.log(
|
|
3265
|
-
console.log(
|
|
2031
|
+
console.log(chalk.gray(` Root: ${snapshot.rootPath}`));
|
|
2032
|
+
console.log(chalk.gray(` Nodes: ${snapshot.nodes.length}`));
|
|
2033
|
+
console.log(chalk.gray(` Edges: ${snapshot.edges.length}
|
|
3266
2034
|
`));
|
|
3267
2035
|
} catch (err) {
|
|
3268
2036
|
spinner.fail(`Graph build failed: ${err.message}`);
|
|
@@ -3274,16 +2042,16 @@ graph.command("list").description("List saved dependency graph snapshots").optio
|
|
|
3274
2042
|
try {
|
|
3275
2043
|
const snapshots = await phase1.services.graphEngine.listSnapshots(cwd, parseInt(opts.limit, 10));
|
|
3276
2044
|
if (snapshots.length === 0) {
|
|
3277
|
-
console.log(
|
|
2045
|
+
console.log(chalk.yellow("\n No graph snapshots found. Run: memory-core graph build\n"));
|
|
3278
2046
|
return;
|
|
3279
2047
|
}
|
|
3280
|
-
console.log(
|
|
2048
|
+
console.log(chalk.bold("\n Graph snapshots\n"));
|
|
3281
2049
|
snapshots.forEach((snapshot, index) => {
|
|
3282
|
-
console.log(` ${index + 1}. ${snapshot.id} ${
|
|
2050
|
+
console.log(` ${index + 1}. ${snapshot.id} ${chalk.dim(snapshot.createdAt)} nodes=${snapshot.nodes.length} edges=${snapshot.edges.length}`);
|
|
3283
2051
|
});
|
|
3284
2052
|
console.log();
|
|
3285
2053
|
} catch (err) {
|
|
3286
|
-
console.error(
|
|
2054
|
+
console.error(chalk.red(`Graph list failed: ${err.message}`));
|
|
3287
2055
|
process.exit(1);
|
|
3288
2056
|
}
|
|
3289
2057
|
});
|
|
@@ -3292,30 +2060,30 @@ graph.command("show [snapshotId]").description("Show a saved graph snapshot (lat
|
|
|
3292
2060
|
try {
|
|
3293
2061
|
const snapshot = snapshotId ? await phase1.services.graphEngine.getSnapshot(cwd, snapshotId) : await phase1.services.graphEngine.latest(cwd);
|
|
3294
2062
|
if (!snapshot) {
|
|
3295
|
-
console.log(
|
|
2063
|
+
console.log(chalk.yellow("\n No matching graph snapshot found. Run: memory-core graph build\n"));
|
|
3296
2064
|
return;
|
|
3297
2065
|
}
|
|
3298
|
-
console.log(
|
|
2066
|
+
console.log(chalk.bold(`
|
|
3299
2067
|
Graph ${snapshot.id ?? "(latest)"}
|
|
3300
2068
|
`));
|
|
3301
|
-
console.log(
|
|
3302
|
-
console.log(
|
|
3303
|
-
console.log(
|
|
3304
|
-
console.log(
|
|
2069
|
+
console.log(chalk.gray(` Root: ${snapshot.rootPath}`));
|
|
2070
|
+
console.log(chalk.gray(` Created: ${snapshot.createdAt ?? "unknown"}`));
|
|
2071
|
+
console.log(chalk.gray(` Nodes: ${snapshot.nodes.length}`));
|
|
2072
|
+
console.log(chalk.gray(` Edges: ${snapshot.edges.length}
|
|
3305
2073
|
`));
|
|
3306
2074
|
const limit = parseInt(opts.edges, 10);
|
|
3307
2075
|
snapshot.edges.slice(0, limit).forEach((edge, index) => {
|
|
3308
|
-
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${
|
|
2076
|
+
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${chalk.dim(`--${edge.kind}-->`)} ${abbreviate(edge.to)}`);
|
|
3309
2077
|
});
|
|
3310
2078
|
if (snapshot.edges.length > limit) {
|
|
3311
|
-
console.log(
|
|
2079
|
+
console.log(chalk.dim(`
|
|
3312
2080
|
\u2026 ${snapshot.edges.length - limit} more edges not shown
|
|
3313
2081
|
`));
|
|
3314
2082
|
} else {
|
|
3315
2083
|
console.log();
|
|
3316
2084
|
}
|
|
3317
2085
|
} catch (err) {
|
|
3318
|
-
console.error(
|
|
2086
|
+
console.error(chalk.red(`Graph show failed: ${err.message}`));
|
|
3319
2087
|
process.exit(1);
|
|
3320
2088
|
}
|
|
3321
2089
|
});
|
|
@@ -3324,37 +2092,37 @@ graph.command("diff <leftSnapshotId> [rightSnapshotId]").description("Diff two s
|
|
|
3324
2092
|
try {
|
|
3325
2093
|
const left = await phase1.services.graphEngine.getSnapshot(cwd, leftSnapshotId);
|
|
3326
2094
|
if (!left) {
|
|
3327
|
-
console.error(
|
|
2095
|
+
console.error(chalk.red(`Left snapshot not found: ${leftSnapshotId}`));
|
|
3328
2096
|
process.exit(1);
|
|
3329
2097
|
}
|
|
3330
2098
|
const right = rightSnapshotId ? await phase1.services.graphEngine.getSnapshot(cwd, rightSnapshotId) : await phase1.services.graphEngine.latest(cwd);
|
|
3331
2099
|
if (!right) {
|
|
3332
|
-
console.error(
|
|
2100
|
+
console.error(chalk.red(`Right snapshot not found${rightSnapshotId ? `: ${rightSnapshotId}` : ""}`));
|
|
3333
2101
|
process.exit(1);
|
|
3334
2102
|
}
|
|
3335
2103
|
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(
|
|
2104
|
+
console.log(chalk.bold("\n Graph diff\n"));
|
|
2105
|
+
console.log(chalk.gray(` Left: ${left.id}`));
|
|
2106
|
+
console.log(chalk.gray(` Right: ${right.id}`));
|
|
2107
|
+
console.log(chalk.green(` + Nodes: ${diff.addedNodes.length}`));
|
|
2108
|
+
console.log(chalk.red(` - Nodes: ${diff.removedNodes.length}`));
|
|
2109
|
+
console.log(chalk.green(` + Edges: ${diff.addedEdges.length}`));
|
|
2110
|
+
console.log(chalk.red(` - Edges: ${diff.removedEdges.length}`));
|
|
3343
2111
|
if (diff.addedEdges.length > 0) {
|
|
3344
|
-
console.log(
|
|
2112
|
+
console.log(chalk.green("\n Added edges"));
|
|
3345
2113
|
diff.addedEdges.slice(0, 20).forEach((edge, index) => {
|
|
3346
|
-
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${
|
|
2114
|
+
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${chalk.dim(`--${edge.kind}-->`)} ${abbreviate(edge.to)}`);
|
|
3347
2115
|
});
|
|
3348
2116
|
}
|
|
3349
2117
|
if (diff.removedEdges.length > 0) {
|
|
3350
|
-
console.log(
|
|
2118
|
+
console.log(chalk.red("\n Removed edges"));
|
|
3351
2119
|
diff.removedEdges.slice(0, 20).forEach((edge, index) => {
|
|
3352
|
-
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${
|
|
2120
|
+
console.log(` ${index + 1}. ${abbreviate(edge.from)} ${chalk.dim(`--${edge.kind}-->`)} ${abbreviate(edge.to)}`);
|
|
3353
2121
|
});
|
|
3354
2122
|
}
|
|
3355
2123
|
console.log();
|
|
3356
2124
|
} catch (err) {
|
|
3357
|
-
console.error(
|
|
2125
|
+
console.error(chalk.red(`Graph diff failed: ${err.message}`));
|
|
3358
2126
|
process.exit(1);
|
|
3359
2127
|
}
|
|
3360
2128
|
});
|
|
@@ -3366,15 +2134,68 @@ hook.command("install").description("Install pre-commit hook (advisory mode by d
|
|
|
3366
2134
|
hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
|
|
3367
2135
|
uninstallHook();
|
|
3368
2136
|
});
|
|
3369
|
-
|
|
2137
|
+
hook.command("bypass-prompt").description("Prompt developer for a bypass reason and save it as a rule (called automatically by hook)").action(async () => {
|
|
2138
|
+
if (!process.stdin.isTTY) return;
|
|
2139
|
+
const bypassStats = readBypassStats();
|
|
2140
|
+
const unrecorded = bypassStats.withoutReason;
|
|
2141
|
+
if (unrecorded > 0) {
|
|
2142
|
+
console.log(
|
|
2143
|
+
chalk.yellow(`
|
|
2144
|
+
\u26A0 ${unrecorded} bypass${unrecorded > 1 ? "es" : ""} in this project had no reason recorded \u2014 lost signal.`)
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
const config = readProjectConfig();
|
|
2148
|
+
let reason = await input({
|
|
2149
|
+
message: chalk.yellow("Why are you bypassing the hook?"),
|
|
2150
|
+
default: ""
|
|
2151
|
+
});
|
|
2152
|
+
if (!reason.trim()) {
|
|
2153
|
+
reason = await input({
|
|
2154
|
+
message: chalk.dim(" Skipping loses signal. Enter a reason or leave blank to proceed:"),
|
|
2155
|
+
default: ""
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
const hadReason = reason.trim().length > 0;
|
|
2159
|
+
const stats = recordBypass(hadReason);
|
|
2160
|
+
if (!hadReason) {
|
|
2161
|
+
console.log(
|
|
2162
|
+
chalk.dim(` Bypass recorded without reason. (${stats.withoutReason} total unrecorded \u2014 run: memory-core stats)
|
|
2163
|
+
`)
|
|
2164
|
+
);
|
|
2165
|
+
await closePool();
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
try {
|
|
2169
|
+
await phase1.services.memoryEngine.remember({
|
|
2170
|
+
type: "rule",
|
|
2171
|
+
scope: "project",
|
|
2172
|
+
architecture: config?.backendArchitecture ?? config?.frontendFramework,
|
|
2173
|
+
projectName: config?.projectName,
|
|
2174
|
+
content: reason.trim(),
|
|
2175
|
+
reason: "Captured automatically from hook bypass"
|
|
2176
|
+
});
|
|
2177
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
2178
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2179
|
+
console.log(chalk.green(` \u2713 Bypass reason saved: "${reason.trim()}"
|
|
2180
|
+
`));
|
|
2181
|
+
} catch {
|
|
2182
|
+
}
|
|
2183
|
+
await closePool();
|
|
2184
|
+
});
|
|
2185
|
+
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").action(async (opts) => {
|
|
2186
|
+
if (opts.file) {
|
|
2187
|
+
await checkFile(opts.file, { verbose: opts.verbose ?? false, debug: opts.debug ?? false, fast: opts.fast ?? false, dryRun: opts.dryRun ?? false });
|
|
2188
|
+
await closePool();
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
3370
2191
|
if (opts.commitMsg !== void 0) {
|
|
3371
|
-
const msgFile = typeof opts.commitMsg === "string" ? opts.commitMsg :
|
|
2192
|
+
const msgFile = typeof opts.commitMsg === "string" ? opts.commitMsg : join(process.cwd(), ".git", "COMMIT_EDITMSG");
|
|
3372
2193
|
await checkCommitMsg(msgFile, { verbose: opts.verbose ?? false, debug: opts.debug ?? false });
|
|
3373
2194
|
await closePool();
|
|
3374
2195
|
return;
|
|
3375
2196
|
}
|
|
3376
2197
|
if (opts.ci && opts.all) {
|
|
3377
|
-
console.error(
|
|
2198
|
+
console.error(chalk.red("\n Choose one mode: --ci or --all.\n"));
|
|
3378
2199
|
process.exit(1);
|
|
3379
2200
|
}
|
|
3380
2201
|
if (opts.ci) {
|
|
@@ -3390,12 +2211,80 @@ program.command("check").description("Check staged changes against architecture
|
|
|
3390
2211
|
if (summary.violations > 0) process.exit(1);
|
|
3391
2212
|
return;
|
|
3392
2213
|
}
|
|
3393
|
-
await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false, fast: opts.fast ?? false });
|
|
2214
|
+
await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false, fast: opts.fast ?? false, dryRun: opts.dryRun ?? false });
|
|
2215
|
+
});
|
|
2216
|
+
var schema = program.command("schema").description("Manage schema alignment rules between TypeScript and Go files");
|
|
2217
|
+
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) => {
|
|
2218
|
+
const config = readProjectConfig();
|
|
2219
|
+
const content = JSON.stringify({ tsFile: opts.ts, goFile: opts.go });
|
|
2220
|
+
const spinner = ora("Saving schema rule\u2026").start();
|
|
2221
|
+
try {
|
|
2222
|
+
await phase1.services.memoryEngine.remember({
|
|
2223
|
+
type: "schema",
|
|
2224
|
+
scope: "project",
|
|
2225
|
+
architecture: config?.backendArchitecture ?? config?.frontendFramework,
|
|
2226
|
+
projectName: config?.projectName,
|
|
2227
|
+
content,
|
|
2228
|
+
reason: "Schema alignment rule \u2014 TypeScript and Go structs must stay in sync"
|
|
2229
|
+
});
|
|
2230
|
+
const dbVersionPath = join(process.cwd(), ".memory-core-db-version");
|
|
2231
|
+
writeFileSync(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2232
|
+
spinner.succeed(chalk.green(`Schema rule saved: ${opts.ts} \u2194 ${opts.go}`));
|
|
2233
|
+
} catch (err) {
|
|
2234
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
2235
|
+
process.exit(1);
|
|
2236
|
+
}
|
|
2237
|
+
await closePool();
|
|
2238
|
+
});
|
|
2239
|
+
schema.command("list").description("List all schema alignment rules").action(async () => {
|
|
2240
|
+
const memories = await phase1.services.memoryEngine.list({ type: "schema", limit: 100 });
|
|
2241
|
+
if (memories.length === 0) {
|
|
2242
|
+
console.log(chalk.dim("\n No schema rules defined. Use: memory-core schema add --ts <file> --go <file>\n"));
|
|
2243
|
+
await closePool();
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
console.log(chalk.cyan(`
|
|
2247
|
+
${memories.length} schema rule${memories.length > 1 ? "s" : ""}:
|
|
2248
|
+
`));
|
|
2249
|
+
for (const m of memories) {
|
|
2250
|
+
try {
|
|
2251
|
+
const rule = JSON.parse(m.content);
|
|
2252
|
+
console.log(chalk.white(` ${rule.tsFile}`) + chalk.dim(" \u2194 ") + chalk.white(rule.goFile));
|
|
2253
|
+
} catch {
|
|
2254
|
+
console.log(chalk.dim(` ${m.content}`));
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
console.log();
|
|
2258
|
+
await closePool();
|
|
2259
|
+
});
|
|
2260
|
+
schema.command("check").description("Run schema alignment checks against all tracked file pairs").action(async () => {
|
|
2261
|
+
const app = getDefaultApplicationContainer();
|
|
2262
|
+
const violations = await findSchemaViolations({
|
|
2263
|
+
cwd: process.cwd(),
|
|
2264
|
+
memoryEngine: app.services.memoryEngine
|
|
2265
|
+
});
|
|
2266
|
+
if (violations.length === 0) {
|
|
2267
|
+
console.log(chalk.green("\n \u2713 All schema rules pass \u2014 TypeScript and Go are in sync.\n"));
|
|
2268
|
+
} else {
|
|
2269
|
+
console.log(chalk.red.bold(`
|
|
2270
|
+
\u2717 ${violations.length} schema violation${violations.length > 1 ? "s" : ""} found
|
|
2271
|
+
`));
|
|
2272
|
+
for (const v of violations) {
|
|
2273
|
+
console.log(chalk.bold(` ${v.file}`));
|
|
2274
|
+
console.log(chalk.yellow(" Rule: ") + v.rule);
|
|
2275
|
+
console.log(chalk.red(" Issue: ") + v.issue);
|
|
2276
|
+
if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
|
|
2277
|
+
console.log();
|
|
2278
|
+
}
|
|
2279
|
+
process.exit(1);
|
|
2280
|
+
}
|
|
2281
|
+
await closePool();
|
|
3394
2282
|
});
|
|
3395
|
-
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("--verbose", "Show diff size and model details per file").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
|
|
2283
|
+
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
2284
|
await phase1.providers.watchService.start({
|
|
3397
2285
|
path: opts.path,
|
|
3398
2286
|
scanOnStart: opts.scanOnStart,
|
|
2287
|
+
autoFix: opts.autoFix,
|
|
3399
2288
|
verbose: opts.verbose,
|
|
3400
2289
|
debug: opts.debug
|
|
3401
2290
|
});
|