@shahmilsaari/memory-core 1.0.16 → 1.0.19
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 +78 -38
- package/dist/{chunk-ECYSBYMM.js → chunk-VQEIQHK6.js} +13 -6
- package/dist/cli.js +674 -135
- package/dist/dashboard/assets/index-Bz-Tzypa.css +1 -0
- package/dist/dashboard/assets/index-CaevtejN.js +2 -0
- package/dist/dashboard/index.html +6 -3
- package/dist/{dashboard-server-4WOUQTJN.js → dashboard-server-ZERRHWQS.js} +1 -1
- package/package.json +1 -1
- package/dist/dashboard/assets/index-CJyZEmIe.css +0 -1
- package/dist/dashboard/assets/index-DM82nOf5.js +0 -2
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
retrieveMemorySelection,
|
|
21
21
|
runMigrations,
|
|
22
22
|
seeds
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-VQEIQHK6.js";
|
|
24
24
|
|
|
25
25
|
// src/cli.ts
|
|
26
26
|
import { Command } from "commander";
|
|
@@ -33,7 +33,7 @@ import { homedir } from "os";
|
|
|
33
33
|
|
|
34
34
|
// src/hook.ts
|
|
35
35
|
import { execSync, spawnSync } from "child_process";
|
|
36
|
-
import { writeFileSync as writeFileSync2, existsSync as existsSync2, unlinkSync, readFileSync as readFileSync2, chmodSync } from "fs";
|
|
36
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync2, unlinkSync, readFileSync as readFileSync2, chmodSync, statSync } from "fs";
|
|
37
37
|
import { join as join2 } from "path";
|
|
38
38
|
import chalk from "chalk";
|
|
39
39
|
|
|
@@ -122,6 +122,11 @@ var reasonMap = new Map(
|
|
|
122
122
|
);
|
|
123
123
|
var HOOK_PATH = join2(".git", "hooks", "pre-commit");
|
|
124
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;
|
|
125
130
|
function buildHookBody(advisory, fast = false) {
|
|
126
131
|
const suffix = advisory ? " || true" : "";
|
|
127
132
|
const checkArgs = fast ? "check --staged --fast" : "check --staged";
|
|
@@ -166,6 +171,11 @@ function normalizeHookPreamble(content) {
|
|
|
166
171
|
}
|
|
167
172
|
return normalized.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
168
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
|
+
}
|
|
169
179
|
function readPositiveIntEnv(name, fallback) {
|
|
170
180
|
const raw = Number(process.env[name]);
|
|
171
181
|
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
|
|
@@ -199,7 +209,8 @@ function recordViolations(violations, source = "hook") {
|
|
|
199
209
|
stats.rules ??= {};
|
|
200
210
|
stats.files ??= {};
|
|
201
211
|
for (const violation of violations) {
|
|
202
|
-
|
|
212
|
+
const existing = toRuleStatEntry(stats.rules[violation.rule]);
|
|
213
|
+
stats.rules[violation.rule] = { count: existing.count + 1, falsePositives: existing.falsePositives };
|
|
203
214
|
if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
|
|
204
215
|
}
|
|
205
216
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -254,6 +265,40 @@ async function promptToSaveViolations(violations) {
|
|
|
254
265
|
`));
|
|
255
266
|
}
|
|
256
267
|
}
|
|
268
|
+
function readRuleCache(cwd) {
|
|
269
|
+
const cachePath = join2(cwd, RULE_CACHE_FILE);
|
|
270
|
+
const configPath = join2(cwd, ".memory-core.json");
|
|
271
|
+
if (!existsSync2(cachePath) || !existsSync2(configPath)) return null;
|
|
272
|
+
try {
|
|
273
|
+
const entry = JSON.parse(readFileSync2(cachePath, "utf-8"));
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
if (now - entry.timestamp > RULE_CACHE_TTL_MS) return null;
|
|
276
|
+
const configMtime = statSync(configPath).mtimeMs;
|
|
277
|
+
if (configMtime !== entry.configMtime) return null;
|
|
278
|
+
const dbVersionPath = join2(cwd, DB_VERSION_FILE);
|
|
279
|
+
const dbVersionMtime = existsSync2(dbVersionPath) ? statSync(dbVersionPath).mtimeMs : 0;
|
|
280
|
+
if (dbVersionMtime !== entry.dbVersionMtime) return null;
|
|
281
|
+
return entry;
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function saveRuleCache(cwd, data) {
|
|
287
|
+
const configPath = join2(cwd, ".memory-core.json");
|
|
288
|
+
try {
|
|
289
|
+
const configMtime = statSync(configPath).mtimeMs;
|
|
290
|
+
const dbVersionPath = join2(cwd, DB_VERSION_FILE);
|
|
291
|
+
const dbVersionMtime = existsSync2(dbVersionPath) ? statSync(dbVersionPath).mtimeMs : 0;
|
|
292
|
+
const entry = {
|
|
293
|
+
timestamp: Date.now(),
|
|
294
|
+
configMtime,
|
|
295
|
+
dbVersionMtime,
|
|
296
|
+
...data
|
|
297
|
+
};
|
|
298
|
+
writeFileSync2(join2(cwd, RULE_CACHE_FILE), JSON.stringify(entry, null, 2) + "\n", "utf-8");
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
}
|
|
257
302
|
async function loadIgnorePatterns() {
|
|
258
303
|
try {
|
|
259
304
|
const app = getDefaultApplicationContainer();
|
|
@@ -487,6 +532,28 @@ function loadRecentViolationsFromStats(cwd = process.cwd()) {
|
|
|
487
532
|
return [];
|
|
488
533
|
}
|
|
489
534
|
}
|
|
535
|
+
function incrementFalsePositivesForPatterns(learnedPatterns, violations, cwd = process.cwd()) {
|
|
536
|
+
if (learnedPatterns.length === 0 || violations.length === 0) return;
|
|
537
|
+
const statsPath = join2(cwd, ".memory-core-stats.json");
|
|
538
|
+
if (!existsSync2(statsPath)) return;
|
|
539
|
+
let stats;
|
|
540
|
+
try {
|
|
541
|
+
stats = JSON.parse(readFileSync2(statsPath, "utf-8"));
|
|
542
|
+
} catch {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
stats.rules ??= {};
|
|
546
|
+
for (const violation of violations) {
|
|
547
|
+
const haystack = `${violation.rule}
|
|
548
|
+
${violation.issue}
|
|
549
|
+
${violation.file}`.toLowerCase();
|
|
550
|
+
const matched = learnedPatterns.some((p) => haystack.includes(p.toLowerCase()));
|
|
551
|
+
if (!matched) continue;
|
|
552
|
+
const existing = toRuleStatEntry(stats.rules[violation.rule]);
|
|
553
|
+
stats.rules[violation.rule] = { count: existing.count, falsePositives: existing.falsePositives + 1 };
|
|
554
|
+
}
|
|
555
|
+
writeFileSync2(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
556
|
+
}
|
|
490
557
|
async function learnGlobalIgnoresFromFalsePositives(options) {
|
|
491
558
|
if (options.currentViolations.length === 0) return [];
|
|
492
559
|
const recentViolations = loadRecentViolationsFromStats();
|
|
@@ -545,6 +612,9 @@ ${JSON.stringify(options.allowPatterns, null, 2)}`;
|
|
|
545
612
|
} catch {
|
|
546
613
|
}
|
|
547
614
|
}
|
|
615
|
+
if (inserted.length > 0) {
|
|
616
|
+
incrementFalsePositivesForPatterns(inserted, options.currentViolations);
|
|
617
|
+
}
|
|
548
618
|
return inserted;
|
|
549
619
|
} catch {
|
|
550
620
|
return [];
|
|
@@ -629,6 +699,32 @@ function findDeterministicViolations(diff, rules, avoids, allowPatterns = []) {
|
|
|
629
699
|
}
|
|
630
700
|
return dedupeViolations(violations);
|
|
631
701
|
}
|
|
702
|
+
function suppressBatchRepetitions(violations, threshold = 3) {
|
|
703
|
+
const pairCounts = /* @__PURE__ */ new Map();
|
|
704
|
+
for (const v of violations) {
|
|
705
|
+
const key = `${v.rule}\0${v.file}`;
|
|
706
|
+
pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);
|
|
707
|
+
}
|
|
708
|
+
const suppressedKeys = /* @__PURE__ */ new Set();
|
|
709
|
+
for (const [key, count] of pairCounts) {
|
|
710
|
+
if (count >= threshold) suppressedKeys.add(key);
|
|
711
|
+
}
|
|
712
|
+
if (suppressedKeys.size === 0) return { filtered: violations, suppressedCount: 0 };
|
|
713
|
+
const filtered = violations.filter((v) => !suppressedKeys.has(`${v.rule}\0${v.file}`));
|
|
714
|
+
return { filtered, suppressedCount: violations.length - filtered.length };
|
|
715
|
+
}
|
|
716
|
+
function groupViolationsByRule(violations) {
|
|
717
|
+
const groups = /* @__PURE__ */ new Map();
|
|
718
|
+
for (const v of violations) {
|
|
719
|
+
const existing = groups.get(v.rule);
|
|
720
|
+
if (existing) {
|
|
721
|
+
existing.push(v);
|
|
722
|
+
} else {
|
|
723
|
+
groups.set(v.rule, [v]);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return groups;
|
|
727
|
+
}
|
|
632
728
|
function filterModelViolationsByStagedDiff(violations, stagedFiles, diff) {
|
|
633
729
|
if (violations.length === 0) return violations;
|
|
634
730
|
const changedFiles = new Set(stagedFiles.map((file) => normalizePath(file)));
|
|
@@ -681,6 +777,7 @@ ${preamble}`;
|
|
|
681
777
|
${body}
|
|
682
778
|
`);
|
|
683
779
|
chmodSync(HOOK_PATH, 493);
|
|
780
|
+
installCommitMsgHook(advisory);
|
|
684
781
|
const modeLabel2 = advisory ? chalk.cyan("advisory") : chalk.yellow("strict");
|
|
685
782
|
console.log(chalk.green("\n \u2713 Pre-commit hook updated") + chalk.dim(` (${modeLabel2} mode)`));
|
|
686
783
|
if (fast) console.log(chalk.gray(` Check mode: fast deterministic checks`));
|
|
@@ -691,9 +788,11 @@ ${body}
|
|
|
691
788
|
writeFileSync2(HOOK_PATH, script);
|
|
692
789
|
}
|
|
693
790
|
chmodSync(HOOK_PATH, 493);
|
|
791
|
+
installCommitMsgHook(advisory);
|
|
694
792
|
const modeLabel = advisory ? "advisory (logs violations, never blocks)" : "strict (blocks commits on violations)";
|
|
695
793
|
console.log(chalk.green("\n \u2713 Pre-commit hook installed") + chalk.dim(` \u2014 ${modeLabel}`));
|
|
696
794
|
console.log(chalk.gray(fast ? " Check mode: fast deterministic checks" : ` Chat model: ${process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"}`));
|
|
795
|
+
console.log(chalk.gray(" Commit message rules: memory-core commit-rules --list"));
|
|
697
796
|
console.log(chalk.gray(" To uninstall: memory-core hook uninstall\n"));
|
|
698
797
|
}
|
|
699
798
|
function uninstallHook() {
|
|
@@ -714,8 +813,118 @@ function uninstallHook() {
|
|
|
714
813
|
} else {
|
|
715
814
|
unlinkSync(HOOK_PATH);
|
|
716
815
|
}
|
|
816
|
+
uninstallCommitMsgHook();
|
|
717
817
|
console.log(chalk.green("\n \u2713 Pre-commit hook removed\n"));
|
|
718
818
|
}
|
|
819
|
+
function buildCommitMsgHookBody(advisory) {
|
|
820
|
+
const suffix = advisory ? " || true" : "";
|
|
821
|
+
return `${COMMIT_MSG_HOOK_MARKER}${advisory ? " advisory" : ""}
|
|
822
|
+
if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
|
|
823
|
+
exit 0
|
|
824
|
+
fi
|
|
825
|
+
if [ -n "\${SKIP:-}" ] && echo ",$SKIP," | grep -qiE ',(memory-core|archmind),'; then
|
|
826
|
+
exit 0
|
|
827
|
+
fi
|
|
828
|
+
if [ -n "\${SKIP_HOOKS:-}" ]; then
|
|
829
|
+
exit 0
|
|
830
|
+
fi
|
|
831
|
+
if command -v memory-core >/dev/null 2>&1; then
|
|
832
|
+
memory-core check --commit-msg "$1"${suffix}
|
|
833
|
+
elif [ -f "./node_modules/.bin/memory-core" ]; then
|
|
834
|
+
./node_modules/.bin/memory-core check --commit-msg "$1"${suffix}
|
|
835
|
+
elif [ -f "./dist/cli.js" ]; then
|
|
836
|
+
node ./dist/cli.js check --commit-msg "$1"${suffix}
|
|
837
|
+
else
|
|
838
|
+
npx --no-install memory-core check --commit-msg "$1" 2>/dev/null || exit 0
|
|
839
|
+
fi
|
|
840
|
+
`;
|
|
841
|
+
}
|
|
842
|
+
function installCommitMsgHook(advisory = true) {
|
|
843
|
+
const body = buildCommitMsgHookBody(advisory).trimEnd();
|
|
844
|
+
const script = `#!/bin/sh
|
|
845
|
+
|
|
846
|
+
${body}
|
|
847
|
+
`;
|
|
848
|
+
if (existsSync2(COMMIT_MSG_HOOK_PATH)) {
|
|
849
|
+
const existing = readFileSync2(COMMIT_MSG_HOOK_PATH, "utf-8");
|
|
850
|
+
if (existing.includes(COMMIT_MSG_HOOK_MARKER)) {
|
|
851
|
+
const markerIndex = existing.indexOf(COMMIT_MSG_HOOK_MARKER);
|
|
852
|
+
const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
|
|
853
|
+
const normalizedBefore = normalizeHookPreamble(beforeRaw);
|
|
854
|
+
const preamble = normalizedBefore.length > 0 ? normalizedBefore : "#!/bin/sh";
|
|
855
|
+
const preambleWithShebang = preamble.startsWith("#!/bin/sh") ? preamble : `#!/bin/sh
|
|
856
|
+
${preamble}`;
|
|
857
|
+
writeFileSync2(COMMIT_MSG_HOOK_PATH, `${preambleWithShebang}
|
|
858
|
+
|
|
859
|
+
${body}
|
|
860
|
+
`);
|
|
861
|
+
} else {
|
|
862
|
+
writeFileSync2(COMMIT_MSG_HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
writeFileSync2(COMMIT_MSG_HOOK_PATH, script);
|
|
866
|
+
}
|
|
867
|
+
chmodSync(COMMIT_MSG_HOOK_PATH, 493);
|
|
868
|
+
}
|
|
869
|
+
function uninstallCommitMsgHook() {
|
|
870
|
+
if (!existsSync2(COMMIT_MSG_HOOK_PATH)) return;
|
|
871
|
+
const content = readFileSync2(COMMIT_MSG_HOOK_PATH, "utf-8");
|
|
872
|
+
if (!content.includes(COMMIT_MSG_HOOK_MARKER)) return;
|
|
873
|
+
const markerIndex = content.indexOf(COMMIT_MSG_HOOK_MARKER);
|
|
874
|
+
const before = markerIndex > 1 ? normalizeHookPreamble(content.slice(0, markerIndex)) : "";
|
|
875
|
+
if (before && before !== "#!/bin/sh") {
|
|
876
|
+
writeFileSync2(COMMIT_MSG_HOOK_PATH, `${before}
|
|
877
|
+
`);
|
|
878
|
+
} else {
|
|
879
|
+
unlinkSync(COMMIT_MSG_HOOK_PATH);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async function checkCommitMsg(msgFile, options = {}) {
|
|
883
|
+
if (!existsSync2(msgFile)) {
|
|
884
|
+
if (options.verbose) console.log(chalk.gray(" No commit message file \u2014 skipping."));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const raw = readFileSync2(msgFile, "utf-8");
|
|
888
|
+
const cleanMsg = raw.split("\n").filter((l) => !l.startsWith("#")).join("\n").trim();
|
|
889
|
+
if (!cleanMsg) {
|
|
890
|
+
if (options.verbose) console.log(chalk.gray(" Empty commit message \u2014 skipping."));
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const configPath = join2(process.cwd(), ".memory-core.json");
|
|
894
|
+
if (!existsSync2(configPath)) return;
|
|
895
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
896
|
+
const rules = (config.commitRules ?? []).filter(Boolean);
|
|
897
|
+
if (rules.length === 0) return;
|
|
898
|
+
console.log(chalk.cyan("\n archmind \u2014 checking commit message\u2026"));
|
|
899
|
+
const violations = [];
|
|
900
|
+
for (const rule of rules) {
|
|
901
|
+
try {
|
|
902
|
+
const regex = new RegExp(rule.pattern, "im");
|
|
903
|
+
const matched = regex.test(cleanMsg);
|
|
904
|
+
const violated = rule.negate ? matched : !matched;
|
|
905
|
+
if (violated) violations.push({ rule });
|
|
906
|
+
} catch {
|
|
907
|
+
if (options.debug) console.log(chalk.yellow(` [debug] Invalid regex: "${rule.pattern}"`));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (violations.length === 0) {
|
|
911
|
+
console.log(chalk.green(" \u2713 Commit message OK.\n"));
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const blocking = violations.filter((v) => !v.rule.advisory);
|
|
915
|
+
violations.forEach(({ rule }) => {
|
|
916
|
+
const prefix = rule.advisory ? chalk.yellow(" \u26A0 ") : chalk.red(" \u2717 ");
|
|
917
|
+
console.log(prefix + rule.message);
|
|
918
|
+
const matchLabel = rule.negate ? "(must NOT match)" : "(must match)";
|
|
919
|
+
console.log(chalk.dim(` Pattern: ${rule.pattern} ${matchLabel}`));
|
|
920
|
+
});
|
|
921
|
+
console.log();
|
|
922
|
+
if (blocking.length === 0) return;
|
|
923
|
+
console.log(chalk.dim(" Fix the commit message, then commit again."));
|
|
924
|
+
console.log(chalk.dim(" To bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
|
|
925
|
+
console.log(chalk.dim(" Manage rules: memory-core commit-rules --list\n"));
|
|
926
|
+
process.exit(1);
|
|
927
|
+
}
|
|
719
928
|
async function checkStaged(options = {}) {
|
|
720
929
|
const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
721
930
|
let diff;
|
|
@@ -747,11 +956,34 @@ async function checkStaged(options = {}) {
|
|
|
747
956
|
const fast = isFastCheck(options);
|
|
748
957
|
const ruleLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_RULE_LOAD_TIMEOUT_MS", 2e3);
|
|
749
958
|
const ignoreLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_IGNORE_LOAD_TIMEOUT_MS", 1500);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
959
|
+
let rules;
|
|
960
|
+
let ignores;
|
|
961
|
+
let allowPatterns;
|
|
962
|
+
if (fast) {
|
|
963
|
+
rules = fallbackRules;
|
|
964
|
+
ignores = [];
|
|
965
|
+
allowPatterns = [...new Set(getAllowPatterns(config))];
|
|
966
|
+
} else {
|
|
967
|
+
const cwd = process.cwd();
|
|
968
|
+
const cached = readRuleCache(cwd);
|
|
969
|
+
if (cached) {
|
|
970
|
+
rules = cached.rules;
|
|
971
|
+
ignores = cached.ignores;
|
|
972
|
+
allowPatterns = cached.allowPatterns;
|
|
973
|
+
if (options.debug) {
|
|
974
|
+
console.log(chalk.gray(" [debug] using cached rules (TTL valid)"));
|
|
975
|
+
}
|
|
976
|
+
} else {
|
|
977
|
+
const [loadedRules, loadedIgnores] = await Promise.all([
|
|
978
|
+
withTimeout(loadRelevantRules(config, diff, stagedFiles, fallbackRules), ruleLoadTimeoutMs, fallbackRules),
|
|
979
|
+
withTimeout(loadIgnorePatterns(), ignoreLoadTimeoutMs, [])
|
|
980
|
+
]);
|
|
981
|
+
rules = loadedRules;
|
|
982
|
+
ignores = loadedIgnores;
|
|
983
|
+
allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...loadedIgnores])];
|
|
984
|
+
saveRuleCache(cwd, { rules, ignores, allowPatterns });
|
|
985
|
+
}
|
|
986
|
+
}
|
|
755
987
|
if (rules.length === 0) return;
|
|
756
988
|
const modelInputMaxChars = readPositiveIntEnv("MEMORY_CORE_MODEL_INPUT_MAX_CHARS", 8e3);
|
|
757
989
|
const modelInput = buildModelInputFromDiff(diff, modelInputMaxChars);
|
|
@@ -852,6 +1084,17 @@ ${modelInput.text}` }
|
|
|
852
1084
|
modelViolations = filterModelViolationsByStagedDiff(modelViolations, stagedFiles, diff);
|
|
853
1085
|
let violations = dedupeViolations([...deterministicViolations, ...astViolations, ...modelViolations]);
|
|
854
1086
|
violations = applyAllowPatterns(violations, allowPatterns);
|
|
1087
|
+
if (violations.length > 0) {
|
|
1088
|
+
const { filtered, suppressedCount } = suppressBatchRepetitions(violations);
|
|
1089
|
+
if (suppressedCount > 0) {
|
|
1090
|
+
console.log(
|
|
1091
|
+
chalk.dim(
|
|
1092
|
+
` \u2139 Auto-suppressed ${suppressedCount} repetitive violation${suppressedCount > 1 ? "s" : ""} (same rule fired \u22653\xD7 on the same file \u2014 consider tuning the rule)`
|
|
1093
|
+
)
|
|
1094
|
+
);
|
|
1095
|
+
violations = filtered;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
855
1098
|
if (!aiFallback && violations.length > 0) {
|
|
856
1099
|
const learnedPatterns = await learnGlobalIgnoresFromFalsePositives({
|
|
857
1100
|
diff,
|
|
@@ -879,16 +1122,66 @@ ${modelInput.text}` }
|
|
|
879
1122
|
`
|
|
880
1123
|
)
|
|
881
1124
|
);
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1125
|
+
let ruleStatsSnapshot = {};
|
|
1126
|
+
{
|
|
1127
|
+
const statsPath = join2(process.cwd(), ".memory-core-stats.json");
|
|
1128
|
+
if (existsSync2(statsPath)) {
|
|
1129
|
+
try {
|
|
1130
|
+
const parsed = JSON.parse(readFileSync2(statsPath, "utf-8"));
|
|
1131
|
+
ruleStatsSnapshot = parsed.rules ?? {};
|
|
1132
|
+
} catch {
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
const MAX_LOCATIONS = 5;
|
|
1137
|
+
const groups = groupViolationsByRule(violations);
|
|
1138
|
+
let groupIndex = 0;
|
|
1139
|
+
for (const [rule, group] of groups) {
|
|
1140
|
+
groupIndex++;
|
|
1141
|
+
const isCluster = group.length > 1;
|
|
1142
|
+
const first = group[0];
|
|
1143
|
+
if (isCluster) {
|
|
1144
|
+
console.log(chalk.bold.red(`
|
|
1145
|
+
[${groupIndex}] ${rule}`) + chalk.dim(` \xD7${group.length}`));
|
|
1146
|
+
} else {
|
|
1147
|
+
const loc = first.file ? first.line ? `${first.file}:${first.line}` : first.file : "unknown location";
|
|
1148
|
+
console.log(chalk.bold(`
|
|
1149
|
+
[${groupIndex}] ${loc}`));
|
|
1150
|
+
console.log(chalk.yellow(" Rule: ") + rule);
|
|
1151
|
+
}
|
|
1152
|
+
const why = first.reason ?? reasonMap.get(rule);
|
|
887
1153
|
if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
|
|
888
|
-
if (
|
|
889
|
-
if (
|
|
1154
|
+
if (first.suggestion) console.log(chalk.green(" Fix: ") + first.suggestion);
|
|
1155
|
+
if (isCluster) {
|
|
1156
|
+
console.log();
|
|
1157
|
+
const shown = group.slice(0, MAX_LOCATIONS);
|
|
1158
|
+
const overflow = group.length - MAX_LOCATIONS;
|
|
1159
|
+
for (const v of shown) {
|
|
1160
|
+
const loc = v.file ? v.line ? `${v.file}:${v.line}` : v.file : "unknown location";
|
|
1161
|
+
const issue = v.issue ? chalk.dim(` ${v.issue}`) : "";
|
|
1162
|
+
console.log(chalk.dim(` ${loc}`) + issue);
|
|
1163
|
+
}
|
|
1164
|
+
if (overflow > 0) {
|
|
1165
|
+
console.log(chalk.dim(` ... and ${overflow} more`));
|
|
1166
|
+
}
|
|
1167
|
+
} else {
|
|
1168
|
+
if (first.issue) console.log(chalk.red(" Issue: ") + first.issue);
|
|
1169
|
+
}
|
|
1170
|
+
const ruleEntry = toRuleStatEntry(ruleStatsSnapshot[rule]);
|
|
1171
|
+
if (ruleEntry.count > 5 && ruleEntry.falsePositives > 0) {
|
|
1172
|
+
const rate = Math.round(ruleEntry.falsePositives / ruleEntry.count * 100);
|
|
1173
|
+
if (rate > 40) {
|
|
1174
|
+
console.log(chalk.yellow(`
|
|
1175
|
+
Noisy: ${rate}% historical false-positive rate`));
|
|
1176
|
+
console.log(chalk.dim(` Silence: memory-core allow "${rule}"`));
|
|
1177
|
+
console.log(chalk.dim(` Review all: memory-core tune`));
|
|
1178
|
+
} else if (rate > 25) {
|
|
1179
|
+
console.log(chalk.dim(`
|
|
1180
|
+
Note: ${rate}% false-positive rate \u2014 run: memory-core tune`));
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
890
1183
|
console.log();
|
|
891
|
-
}
|
|
1184
|
+
}
|
|
892
1185
|
console.log(chalk.dim(" Fix the violations above, then commit again."));
|
|
893
1186
|
console.log(chalk.dim(" To bypass (not recommended): git commit --no-verify"));
|
|
894
1187
|
console.log(chalk.dim(" Env bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
|
|
@@ -1076,35 +1369,6 @@ function printBanner(projectName, agentCount, status) {
|
|
|
1076
1369
|
];
|
|
1077
1370
|
lines.forEach((l) => console.log(l));
|
|
1078
1371
|
}
|
|
1079
|
-
async function checkConnections(dbUrl, ollamaUrl, chatModel) {
|
|
1080
|
-
const spinner = ora("Checking connections\u2026").start();
|
|
1081
|
-
let postgresOk = false;
|
|
1082
|
-
let ollamaOk = false;
|
|
1083
|
-
try {
|
|
1084
|
-
const { Pool } = (await import("pg")).default;
|
|
1085
|
-
const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
|
|
1086
|
-
await testPool.query("SELECT 1");
|
|
1087
|
-
await testPool.end();
|
|
1088
|
-
postgresOk = true;
|
|
1089
|
-
} catch {
|
|
1090
|
-
postgresOk = false;
|
|
1091
|
-
}
|
|
1092
|
-
try {
|
|
1093
|
-
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1094
|
-
ollamaOk = res.ok;
|
|
1095
|
-
} catch {
|
|
1096
|
-
ollamaOk = false;
|
|
1097
|
-
}
|
|
1098
|
-
spinner.stop();
|
|
1099
|
-
console.log(
|
|
1100
|
-
postgresOk ? chalk2.green(" \u2713 PostgreSQL") + chalk2.dim(" \u2014 connected") : chalk2.red(" \u2717 PostgreSQL") + chalk2.dim(" \u2014 cannot connect. Check DATABASE_URL and that PostgreSQL is running.")
|
|
1101
|
-
);
|
|
1102
|
-
console.log(
|
|
1103
|
-
ollamaOk ? chalk2.green(" \u2713 Ollama ") + chalk2.dim(` \u2014 connected (${chatModel})`) : chalk2.red(" \u2717 Ollama ") + chalk2.dim(" \u2014 not reachable. Run: ollama serve")
|
|
1104
|
-
);
|
|
1105
|
-
console.log();
|
|
1106
|
-
return { postgresOk, ollamaOk, chatModel };
|
|
1107
|
-
}
|
|
1108
1372
|
var { version } = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1109
1373
|
var CONFIG_FILE = ".memory-core.json";
|
|
1110
1374
|
var LOCAL_GENERATED_FILES = [".memory-core-stats.json"];
|
|
@@ -1121,6 +1385,9 @@ function getEnvPath() {
|
|
|
1121
1385
|
const dotEnv = join3(process.cwd(), ".env");
|
|
1122
1386
|
return existsSync3(dotEnv) ? dotEnv : memoryEnv;
|
|
1123
1387
|
}
|
|
1388
|
+
function getWriteEnvPath() {
|
|
1389
|
+
return join3(process.cwd(), ".memory-core.env");
|
|
1390
|
+
}
|
|
1124
1391
|
function parseEnvFile(raw) {
|
|
1125
1392
|
const lines = raw.split(/\r?\n/);
|
|
1126
1393
|
const values = {};
|
|
@@ -1144,15 +1411,16 @@ function readRuntimeEnv() {
|
|
|
1144
1411
|
for (const [key, value] of Object.entries(process.env)) {
|
|
1145
1412
|
if (typeof value === "string" && value !== "") values[key] = value;
|
|
1146
1413
|
}
|
|
1147
|
-
return { envPath, values };
|
|
1414
|
+
return { envPath: getWriteEnvPath(), values };
|
|
1148
1415
|
}
|
|
1149
|
-
function writeRuntimeEnv(values, envPath =
|
|
1416
|
+
function writeRuntimeEnv(values, envPath = getWriteEnvPath()) {
|
|
1150
1417
|
const orderedKeys = [
|
|
1151
1418
|
"DATABASE_URL",
|
|
1152
1419
|
"OLLAMA_URL",
|
|
1153
1420
|
"OLLAMA_MODEL",
|
|
1154
1421
|
"CHAT_PROVIDER",
|
|
1155
1422
|
"CHAT_MODEL",
|
|
1423
|
+
"CHAT_BASE_URL",
|
|
1156
1424
|
"OLLAMA_CHAT_MODEL",
|
|
1157
1425
|
"CHAT_API_KEY"
|
|
1158
1426
|
];
|
|
@@ -1178,15 +1446,6 @@ function applyRuntimeEnv(values) {
|
|
|
1178
1446
|
process.env[key] = value;
|
|
1179
1447
|
}
|
|
1180
1448
|
}
|
|
1181
|
-
function ensureEnvFileIgnored(envPath = getEnvPath()) {
|
|
1182
|
-
const envFileName = envPath.split("/").pop() ?? ".memory-core.env";
|
|
1183
|
-
const gitignorePath = join3(process.cwd(), ".gitignore");
|
|
1184
|
-
const existing = existsSync3(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
|
|
1185
|
-
if (!existing.includes(envFileName)) {
|
|
1186
|
-
appendFileSync(gitignorePath, `${existing ? "\n" : ""}${envFileName}
|
|
1187
|
-
`);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
1449
|
function appendMissingGitignoreEntries(entries, heading) {
|
|
1191
1450
|
const gitignorePath = join3(process.cwd(), ".gitignore");
|
|
1192
1451
|
const existing = existsSync3(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
|
|
@@ -1251,10 +1510,10 @@ function removeProjectFiles(relativePaths) {
|
|
|
1251
1510
|
}
|
|
1252
1511
|
function normalizeProvider(value) {
|
|
1253
1512
|
const provider2 = value.trim().toLowerCase();
|
|
1254
|
-
if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax") {
|
|
1513
|
+
if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax" || provider2 === "openai-compatible") {
|
|
1255
1514
|
return provider2;
|
|
1256
1515
|
}
|
|
1257
|
-
throw new Error(`Unsupported provider "${value}". Use: ollama, openai, anthropic, minimax`);
|
|
1516
|
+
throw new Error(`Unsupported provider "${value}". Use: ollama, openai, anthropic, minimax, openai-compatible`);
|
|
1258
1517
|
}
|
|
1259
1518
|
function providerLabel(provider2) {
|
|
1260
1519
|
switch (provider2) {
|
|
@@ -1264,6 +1523,8 @@ function providerLabel(provider2) {
|
|
|
1264
1523
|
return "Anthropic";
|
|
1265
1524
|
case "minimax":
|
|
1266
1525
|
return "MiniMax";
|
|
1526
|
+
case "openai-compatible":
|
|
1527
|
+
return "OpenAI-compatible";
|
|
1267
1528
|
default:
|
|
1268
1529
|
return "Ollama";
|
|
1269
1530
|
}
|
|
@@ -1646,16 +1907,56 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1646
1907
|
console.log(chalk2.bold.cyan("\n memory-core init\n"));
|
|
1647
1908
|
const detected = detectProject();
|
|
1648
1909
|
const quick = opts.quick ?? false;
|
|
1910
|
+
let skipEnv = false;
|
|
1911
|
+
let skipProject = false;
|
|
1912
|
+
if (existsSync3(join3(process.cwd(), CONFIG_FILE)) && !quick) {
|
|
1913
|
+
const existing = readProjectConfig();
|
|
1914
|
+
const envVals = readRuntimeEnv().values;
|
|
1915
|
+
console.log(chalk2.dim(` Already initialized: ${existing?.projectName ?? "?"} (${existing?.projectType ?? "?"})`));
|
|
1916
|
+
console.log(chalk2.dim(` Provider: ${envVals.CHAT_PROVIDER ?? "ollama"} Model: ${envVals.CHAT_MODEL ?? "llama3.2"}`));
|
|
1917
|
+
console.log(chalk2.dim(` Hook: ${existsSync3(join3(".git", "hooks", "pre-commit")) ? "installed" : "not installed"} Agents: ${existing?.agents?.length ?? 0}
|
|
1918
|
+
`));
|
|
1919
|
+
const reinitChoice = await select({
|
|
1920
|
+
message: "Already initialized \u2014 what do you want to do?",
|
|
1921
|
+
choices: [
|
|
1922
|
+
{ value: "full", name: "Full re-init \u2014 update everything" },
|
|
1923
|
+
{ value: "connection", name: "Connection only \u2014 update DB / provider / model" },
|
|
1924
|
+
{ value: "project", name: "Project only \u2014 update arch, agents, hook" },
|
|
1925
|
+
{ value: "cancel", name: "Cancel" }
|
|
1926
|
+
]
|
|
1927
|
+
});
|
|
1928
|
+
if (reinitChoice === "cancel") {
|
|
1929
|
+
await closePool();
|
|
1930
|
+
process.exit(0);
|
|
1931
|
+
}
|
|
1932
|
+
skipEnv = reinitChoice === "project";
|
|
1933
|
+
skipProject = reinitChoice === "connection";
|
|
1934
|
+
}
|
|
1935
|
+
let pgOk = false;
|
|
1936
|
+
let ollamaOk = false;
|
|
1649
1937
|
const envPath = join3(process.cwd(), ".memory-core.env");
|
|
1650
1938
|
const hasEnv = existsSync3(envPath) || existsSync3(join3(process.cwd(), ".env")) || !!process.env.DATABASE_URL;
|
|
1651
|
-
if (
|
|
1939
|
+
if (skipEnv) {
|
|
1940
|
+
try {
|
|
1941
|
+
const { Pool } = (await import("pg")).default;
|
|
1942
|
+
const p = new Pool({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 3e3 });
|
|
1943
|
+
await p.query("SELECT 1");
|
|
1944
|
+
await p.end();
|
|
1945
|
+
pgOk = true;
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
try {
|
|
1949
|
+
const r = await fetch(`${process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3e3) });
|
|
1950
|
+
ollamaOk = r.ok;
|
|
1951
|
+
} catch {
|
|
1952
|
+
}
|
|
1953
|
+
} else if (!hasEnv && quick) {
|
|
1652
1954
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
1653
1955
|
const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
|
|
1654
|
-
const ollamaUrl = DEFAULT_OLLAMA_URL;
|
|
1655
1956
|
const chatModel = DEFAULT_CHAT_MODEL;
|
|
1656
1957
|
const envValues = {
|
|
1657
1958
|
DATABASE_URL: dbUrl,
|
|
1658
|
-
OLLAMA_URL:
|
|
1959
|
+
OLLAMA_URL: DEFAULT_OLLAMA_URL,
|
|
1659
1960
|
OLLAMA_MODEL: DEFAULT_EMBEDDING_MODEL,
|
|
1660
1961
|
CHAT_PROVIDER: "ollama",
|
|
1661
1962
|
CHAT_MODEL: chatModel,
|
|
@@ -1663,7 +1964,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1663
1964
|
};
|
|
1664
1965
|
writeRuntimeEnv(envValues, envPath);
|
|
1665
1966
|
applyRuntimeEnv(envValues);
|
|
1666
|
-
|
|
1967
|
+
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
1667
1968
|
console.log(chalk2.green(" \u2713 .memory-core.env created with local defaults"));
|
|
1668
1969
|
} else if (!hasEnv) {
|
|
1669
1970
|
console.log(chalk2.dim(" No .memory-core.env found \u2014 let's set up your connection.\n"));
|
|
@@ -1681,6 +1982,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1681
1982
|
await testPool.query("SELECT 1");
|
|
1682
1983
|
await testPool.end();
|
|
1683
1984
|
pgSpinner.succeed(chalk2.green("PostgreSQL connected"));
|
|
1985
|
+
pgOk = true;
|
|
1684
1986
|
break;
|
|
1685
1987
|
} catch (err) {
|
|
1686
1988
|
pgSpinner.fail(chalk2.red(`Cannot connect: ${err.message}`));
|
|
@@ -1690,14 +1992,15 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1690
1992
|
let ollamaUrl = "";
|
|
1691
1993
|
while (true) {
|
|
1692
1994
|
ollamaUrl = await input({
|
|
1693
|
-
message: "Ollama URL?",
|
|
1694
|
-
default: ollamaUrl ||
|
|
1995
|
+
message: "Ollama URL (used for search embeddings)?",
|
|
1996
|
+
default: ollamaUrl || DEFAULT_OLLAMA_URL
|
|
1695
1997
|
});
|
|
1696
1998
|
const ollamaSpinner = ora(" Testing Ollama connection\u2026").start();
|
|
1697
1999
|
try {
|
|
1698
2000
|
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1699
2001
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1700
2002
|
ollamaSpinner.succeed(chalk2.green("Ollama connected"));
|
|
2003
|
+
ollamaOk = true;
|
|
1701
2004
|
break;
|
|
1702
2005
|
} catch (err) {
|
|
1703
2006
|
ollamaSpinner.fail(chalk2.red(`Cannot reach Ollama: ${err.message}`));
|
|
@@ -1710,11 +2013,16 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1710
2013
|
{ name: "Local \u2014 Ollama (no API key, free)", value: "ollama" },
|
|
1711
2014
|
{ name: "OpenAI \u2014 gpt-4o, gpt-4o-mini", value: "openai" },
|
|
1712
2015
|
{ name: "Anthropic \u2014 claude-sonnet, claude-haiku", value: "anthropic" },
|
|
1713
|
-
{ name: "MiniMax \u2014 MiniMax-Text-01, abab6.5s-chat", value: "minimax" }
|
|
2016
|
+
{ name: "MiniMax \u2014 MiniMax-Text-01, abab6.5s-chat", value: "minimax" },
|
|
2017
|
+
{ name: "OpenAI-compatible \u2014 Groq, DeepSeek, xAI, Mistral, Together\u2026", value: "openai-compatible" }
|
|
1714
2018
|
]
|
|
1715
2019
|
});
|
|
2020
|
+
if (chatProvider !== "ollama") {
|
|
2021
|
+
console.log(chalk2.dim(" Note: Ollama is still used for search embeddings. Code checking uses the cloud provider above."));
|
|
2022
|
+
}
|
|
1716
2023
|
let chatModel = "";
|
|
1717
2024
|
let chatApiKey = "";
|
|
2025
|
+
let chatBaseUrl = "";
|
|
1718
2026
|
if (chatProvider === "ollama") {
|
|
1719
2027
|
while (true) {
|
|
1720
2028
|
const chatModelChoice = await select({
|
|
@@ -1767,6 +2075,13 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1767
2075
|
{ name: "MiniMax-Text-01 (flagship)", value: "MiniMax-Text-01" },
|
|
1768
2076
|
{ name: "abab6.5s-chat (fast, efficient)", value: "abab6.5s-chat" },
|
|
1769
2077
|
{ name: "Other (enter manually)", value: "__custom__" }
|
|
2078
|
+
],
|
|
2079
|
+
"openai-compatible": [
|
|
2080
|
+
{ name: "llama-3.1-70b-versatile (Groq)", value: "llama-3.1-70b-versatile" },
|
|
2081
|
+
{ name: "deepseek-coder (DeepSeek)", value: "deepseek-coder" },
|
|
2082
|
+
{ name: "grok-beta (xAI)", value: "grok-beta" },
|
|
2083
|
+
{ name: "mistral-large-latest (Mistral)", value: "mistral-large-latest" },
|
|
2084
|
+
{ name: "Other (enter manually)", value: "__custom__" }
|
|
1770
2085
|
]
|
|
1771
2086
|
};
|
|
1772
2087
|
const modelChoice = await select({
|
|
@@ -1774,8 +2089,14 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1774
2089
|
choices: modelChoices[chatProvider]
|
|
1775
2090
|
});
|
|
1776
2091
|
chatModel = modelChoice === "__custom__" ? await input({ message: "Model name?" }) : modelChoice;
|
|
2092
|
+
if (chatProvider === "openai-compatible") {
|
|
2093
|
+
chatBaseUrl = await input({
|
|
2094
|
+
message: "API base URL?",
|
|
2095
|
+
default: "https://api.groq.com/openai/v1"
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
1777
2098
|
chatApiKey = await input({
|
|
1778
|
-
message: `${
|
|
2099
|
+
message: `${providerLabel(chatProvider)} API key?`
|
|
1779
2100
|
});
|
|
1780
2101
|
console.log(chalk2.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
|
|
1781
2102
|
}
|
|
@@ -1787,19 +2108,34 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1787
2108
|
CHAT_MODEL: chatModel
|
|
1788
2109
|
};
|
|
1789
2110
|
if (chatProvider === "ollama") envValues.OLLAMA_CHAT_MODEL = chatModel;
|
|
2111
|
+
if (chatBaseUrl) envValues.CHAT_BASE_URL = chatBaseUrl;
|
|
1790
2112
|
if (chatApiKey) envValues.CHAT_API_KEY = chatApiKey;
|
|
1791
2113
|
writeRuntimeEnv(envValues, envPath);
|
|
1792
2114
|
applyRuntimeEnv(envValues);
|
|
1793
|
-
|
|
2115
|
+
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
1794
2116
|
console.log(chalk2.green("\n \u2713 .memory-core.env created"));
|
|
1795
2117
|
console.log(chalk2.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
|
|
2118
|
+
} else {
|
|
2119
|
+
try {
|
|
2120
|
+
const { Pool } = (await import("pg")).default;
|
|
2121
|
+
const p = new Pool({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 3e3 });
|
|
2122
|
+
await p.query("SELECT 1");
|
|
2123
|
+
await p.end();
|
|
2124
|
+
pgOk = true;
|
|
2125
|
+
} catch {
|
|
2126
|
+
}
|
|
2127
|
+
try {
|
|
2128
|
+
const r = await fetch(`${process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3e3) });
|
|
2129
|
+
ollamaOk = r.ok;
|
|
2130
|
+
} catch {
|
|
2131
|
+
}
|
|
1796
2132
|
}
|
|
1797
|
-
const projectName = quick ? process.cwd().split("/").pop() ?? "my-project" : await input({
|
|
2133
|
+
const projectName = quick || skipProject ? readProjectConfig()?.projectName ?? process.cwd().split("/").pop() ?? "my-project" : await input({
|
|
1798
2134
|
message: "Project name?",
|
|
1799
2135
|
default: process.cwd().split("/").pop() ?? "my-project"
|
|
1800
2136
|
});
|
|
1801
2137
|
const inferredProjectType = ["Nuxt.js"].includes(detected.framework) ? "fullstack" : ["React", "Vue.js", "Svelte"].includes(detected.framework) ? "frontend" : "backend";
|
|
1802
|
-
const projectType = quick ? inferredProjectType : await select({
|
|
2138
|
+
const projectType = quick || skipProject ? readProjectConfig()?.projectType ?? inferredProjectType : await select({
|
|
1803
2139
|
message: "Project type?",
|
|
1804
2140
|
choices: [
|
|
1805
2141
|
{ value: "backend", name: "Backend \u2014 API, server, microservice" },
|
|
@@ -1809,8 +2145,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1809
2145
|
});
|
|
1810
2146
|
let backendArchitecture;
|
|
1811
2147
|
if (projectType === "backend" || projectType === "fullstack") {
|
|
1812
|
-
if (quick) {
|
|
1813
|
-
backendArchitecture = detected.framework === "NestJS" ? "nestjs" : detected.framework === "Laravel" ? "laravel-service-repository" : detected.framework === "Go" ? "go-api" : "clean-architecture";
|
|
2148
|
+
if (quick || skipProject) {
|
|
2149
|
+
backendArchitecture = readProjectConfig()?.backendArchitecture ?? (detected.framework === "NestJS" ? "nestjs" : detected.framework === "Laravel" ? "laravel-service-repository" : detected.framework === "Go" ? "go-api" : "clean-architecture");
|
|
1814
2150
|
} else {
|
|
1815
2151
|
const backendProfiles = listProfiles("backend");
|
|
1816
2152
|
backendArchitecture = await select({
|
|
@@ -1824,14 +2160,14 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1824
2160
|
}
|
|
1825
2161
|
let frontendFramework;
|
|
1826
2162
|
if (projectType === "frontend" || projectType === "fullstack") {
|
|
1827
|
-
if (quick) {
|
|
2163
|
+
if (quick || skipProject) {
|
|
1828
2164
|
const frameworkMap = {
|
|
1829
2165
|
"Nuxt.js": "nuxt",
|
|
1830
2166
|
React: "react",
|
|
1831
2167
|
"Vue.js": "vue",
|
|
1832
2168
|
Svelte: "svelte"
|
|
1833
2169
|
};
|
|
1834
|
-
frontendFramework = frameworkMap[detected.framework] ?? "react";
|
|
2170
|
+
frontendFramework = readProjectConfig()?.frontendFramework ?? (frameworkMap[detected.framework] ?? "react");
|
|
1835
2171
|
} else {
|
|
1836
2172
|
const frontendProfiles = listProfiles("frontend");
|
|
1837
2173
|
frontendFramework = await select({
|
|
@@ -1843,43 +2179,20 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1843
2179
|
});
|
|
1844
2180
|
}
|
|
1845
2181
|
}
|
|
1846
|
-
const language = quick ? detected.language : await input({
|
|
2182
|
+
const language = quick || skipProject ? readProjectConfig()?.language ?? detected.language : await input({
|
|
1847
2183
|
message: "Language?",
|
|
1848
2184
|
default: detected.language
|
|
1849
2185
|
});
|
|
1850
|
-
const pullMemories = quick ? true : await confirm({
|
|
1851
|
-
message: "Pull relevant memories from previous projects?",
|
|
1852
|
-
default: true
|
|
1853
|
-
});
|
|
1854
|
-
let installCaveman = quick ? false : await confirm({
|
|
1855
|
-
message: "Install caveman token saver? Downloads and runs the upstream installer.",
|
|
1856
|
-
default: false
|
|
1857
|
-
});
|
|
1858
|
-
let cavemanIntensity = "full";
|
|
1859
|
-
if (installCaveman) {
|
|
1860
|
-
const allowRemoteInstaller = await confirm({
|
|
1861
|
-
message: `Security check: download and execute installer from ${CAVEMAN_INSTALL_URL}?`,
|
|
1862
|
-
default: false
|
|
1863
|
-
});
|
|
1864
|
-
if (!allowRemoteInstaller) {
|
|
1865
|
-
installCaveman = false;
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
if (installCaveman) {
|
|
1869
|
-
cavemanIntensity = await select({
|
|
1870
|
-
message: "Caveman intensity?",
|
|
1871
|
-
choices: [
|
|
1872
|
-
{ value: "full", name: "Full \u2014 caveman mode (default)" },
|
|
1873
|
-
{ value: "lite", name: "Lite \u2014 professional terseness" },
|
|
1874
|
-
{ value: "ultra", name: "Ultra \u2014 telegraphic, minimum words" }
|
|
1875
|
-
]
|
|
1876
|
-
});
|
|
1877
|
-
}
|
|
1878
2186
|
const selectedAgents = quick ? AGENT_NAMES.filter((a) => a !== "Shared") : await (async () => {
|
|
1879
2187
|
const { checkbox } = await import("@inquirer/prompts");
|
|
2188
|
+
const saved = new Set(readProjectConfig()?.agents ?? []);
|
|
1880
2189
|
return checkbox({
|
|
1881
2190
|
message: "Which AI agents do you want to generate files for?",
|
|
1882
|
-
choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({
|
|
2191
|
+
choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({
|
|
2192
|
+
name,
|
|
2193
|
+
value: name,
|
|
2194
|
+
checked: saved.size === 0 || saved.has(name)
|
|
2195
|
+
})),
|
|
1883
2196
|
instructions: " (Space to toggle, A to select all, Enter to confirm)"
|
|
1884
2197
|
});
|
|
1885
2198
|
})();
|
|
@@ -1898,6 +2211,42 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1898
2211
|
]
|
|
1899
2212
|
});
|
|
1900
2213
|
}
|
|
2214
|
+
let installCaveman = false;
|
|
2215
|
+
let cavemanIntensity = "full";
|
|
2216
|
+
if (!quick) {
|
|
2217
|
+
installCaveman = await confirm({
|
|
2218
|
+
message: "Enable caveman token saver? (compresses AI responses ~70%)",
|
|
2219
|
+
default: false
|
|
2220
|
+
});
|
|
2221
|
+
if (installCaveman) {
|
|
2222
|
+
cavemanIntensity = await select({
|
|
2223
|
+
message: "Intensity?",
|
|
2224
|
+
choices: [
|
|
2225
|
+
{ value: "full", name: "Full \u2014 caveman mode (default)" },
|
|
2226
|
+
{ value: "lite", name: "Lite \u2014 professional terseness" },
|
|
2227
|
+
{ value: "ultra", name: "Ultra \u2014 telegraphic, minimum words" }
|
|
2228
|
+
]
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
if (!quick) {
|
|
2233
|
+
const envVals = readRuntimeEnv().values;
|
|
2234
|
+
console.log(chalk2.bold("\n Ready to initialize\n"));
|
|
2235
|
+
console.log(` Project ${chalk2.white(projectName)} (${projectType})`);
|
|
2236
|
+
if (backendArchitecture) console.log(` Backend ${chalk2.white(backendArchitecture)}`);
|
|
2237
|
+
if (frontendFramework) console.log(` Frontend ${chalk2.white(frontendFramework)}`);
|
|
2238
|
+
console.log(` Language ${chalk2.white(language)}`);
|
|
2239
|
+
console.log(` Provider ${chalk2.white(envVals.CHAT_PROVIDER ?? "ollama")} / ${chalk2.white(envVals.CHAT_MODEL ?? DEFAULT_CHAT_MODEL)}`);
|
|
2240
|
+
console.log(` Agents ${chalk2.white(String(selectedAgents.length))} selected`);
|
|
2241
|
+
console.log(` Hook ${chalk2.white(enableHook ? hookAdvisory ? "advisory" : "strict" : "skip")}`);
|
|
2242
|
+
console.log();
|
|
2243
|
+
const proceed = await confirm({ message: "Generate files?", default: true });
|
|
2244
|
+
if (!proceed) {
|
|
2245
|
+
console.log(chalk2.yellow(" Cancelled.\n"));
|
|
2246
|
+
await closePool();
|
|
2247
|
+
process.exit(0);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
1901
2250
|
const config = {
|
|
1902
2251
|
projectName,
|
|
1903
2252
|
projectType,
|
|
@@ -1906,34 +2255,33 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1906
2255
|
language,
|
|
1907
2256
|
caveman: { enabled: installCaveman, intensity: cavemanIntensity },
|
|
1908
2257
|
agents: selectedAgents,
|
|
1909
|
-
allowPatterns: [],
|
|
2258
|
+
allowPatterns: readProjectConfig()?.allowPatterns ?? [],
|
|
2259
|
+
commitRules: readProjectConfig()?.commitRules,
|
|
1910
2260
|
autoSync: true
|
|
1911
2261
|
};
|
|
1912
2262
|
let memories = [];
|
|
1913
|
-
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
memories
|
|
1924
|
-
spinner2.succeed(`Found ${memories.length} relevant memories`);
|
|
2263
|
+
try {
|
|
2264
|
+
const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
|
|
2265
|
+
const selection = await retrieveMemorySelection({
|
|
2266
|
+
query: archQuery,
|
|
2267
|
+
cwd: process.cwd(),
|
|
2268
|
+
config,
|
|
2269
|
+
limit: 20
|
|
2270
|
+
});
|
|
2271
|
+
memories = selection.included;
|
|
2272
|
+
if (memories.length > 0) {
|
|
2273
|
+
console.log(chalk2.dim(` Found ${memories.length} relevant memories`));
|
|
1925
2274
|
printMemorySelection(selection);
|
|
1926
|
-
} catch (err) {
|
|
1927
|
-
spinner2.warn(`Could not retrieve memories: ${err.message}`);
|
|
1928
2275
|
}
|
|
2276
|
+
} catch {
|
|
1929
2277
|
}
|
|
1930
2278
|
if (installCaveman) {
|
|
1931
|
-
const
|
|
2279
|
+
const cavemanSpinner = ora("Installing caveman token saver\u2026").start();
|
|
1932
2280
|
try {
|
|
1933
2281
|
await installCavemanTokenSaver();
|
|
1934
|
-
|
|
2282
|
+
cavemanSpinner.succeed("Caveman installed");
|
|
1935
2283
|
} catch (err) {
|
|
1936
|
-
|
|
2284
|
+
cavemanSpinner.warn(`Caveman install failed: ${err.message}`);
|
|
1937
2285
|
}
|
|
1938
2286
|
}
|
|
1939
2287
|
const spinner = ora("Generating AI agent context files\u2026").start();
|
|
@@ -1954,12 +2302,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1954
2302
|
if (enableHook) {
|
|
1955
2303
|
installHook(hookAdvisory);
|
|
1956
2304
|
}
|
|
1957
|
-
const
|
|
1958
|
-
|
|
1959
|
-
process.env.OLLAMA_URL ?? "http://localhost:11434",
|
|
1960
|
-
process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"
|
|
1961
|
-
);
|
|
1962
|
-
printBanner(config.projectName, written.written.length, status);
|
|
2305
|
+
const chatModelForBanner = process.env.CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
|
|
2306
|
+
printBanner(config.projectName, written.written.length, { postgresOk: pgOk, ollamaOk, chatModel: chatModelForBanner });
|
|
1963
2307
|
await closePool();
|
|
1964
2308
|
});
|
|
1965
2309
|
program.command("sync").description("Re-pull memories and regenerate AI agent files").action(async () => {
|
|
@@ -2031,6 +2375,8 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
2031
2375
|
context: buildMemoryContext(opts),
|
|
2032
2376
|
tags: parseTags(opts.tags)
|
|
2033
2377
|
});
|
|
2378
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
2379
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2034
2380
|
const reasonLine = chalk2.gray(`
|
|
2035
2381
|
Why: ${storedReason}`);
|
|
2036
2382
|
spinner.succeed(chalk2.green(`Memory saved: "${text}"`) + reasonLine);
|
|
@@ -2119,6 +2465,8 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
|
|
|
2119
2465
|
}
|
|
2120
2466
|
spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
|
|
2121
2467
|
if (inserted > 0) {
|
|
2468
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
2469
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2122
2470
|
await autoSyncGeneratedFiles(config, "import", opts.sync);
|
|
2123
2471
|
}
|
|
2124
2472
|
} catch (err) {
|
|
@@ -2257,6 +2605,8 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
2257
2605
|
content: pattern,
|
|
2258
2606
|
tags: ["ignore"]
|
|
2259
2607
|
});
|
|
2608
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
2609
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2260
2610
|
console.log(chalk2.green(`Ignored pattern saved: "${pattern}"`));
|
|
2261
2611
|
await autoSyncGeneratedFiles(config, "ignore", opts.sync);
|
|
2262
2612
|
} catch (err) {
|
|
@@ -2296,6 +2646,61 @@ program.command("allow [pattern]").description("Manage project allow patterns in
|
|
|
2296
2646
|
}));
|
|
2297
2647
|
console.log(chalk2.green(`Allow pattern saved: "${pattern}"`));
|
|
2298
2648
|
});
|
|
2649
|
+
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) => {
|
|
2650
|
+
if (opts.list) {
|
|
2651
|
+
const rules = readProjectConfig()?.commitRules ?? [];
|
|
2652
|
+
if (rules.length === 0) {
|
|
2653
|
+
console.log(chalk2.yellow("\n No commit rules configured.\n"));
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
console.log(chalk2.bold("\n Commit message rules\n"));
|
|
2657
|
+
rules.forEach((rule, i) => {
|
|
2658
|
+
const flags = [
|
|
2659
|
+
rule.negate ? "must NOT match" : "must match",
|
|
2660
|
+
rule.advisory ? "advisory" : "blocking"
|
|
2661
|
+
].join(", ");
|
|
2662
|
+
console.log(` ${i + 1}. ${rule.pattern}`);
|
|
2663
|
+
console.log(chalk2.dim(` Message: ${rule.message}`));
|
|
2664
|
+
console.log(chalk2.dim(` Flags: ${flags}`));
|
|
2665
|
+
console.log();
|
|
2666
|
+
});
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
if (opts.remove) {
|
|
2670
|
+
updateProjectConfig((config) => ({
|
|
2671
|
+
...config,
|
|
2672
|
+
commitRules: (config.commitRules ?? []).filter((r) => r.pattern !== opts.remove)
|
|
2673
|
+
}));
|
|
2674
|
+
console.log(chalk2.green(`Commit rule removed: "${opts.remove}"`));
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
if (!pattern) {
|
|
2678
|
+
console.error(chalk2.red("Provide a pattern, --list, or --remove <pattern>"));
|
|
2679
|
+
process.exit(1);
|
|
2680
|
+
}
|
|
2681
|
+
if (!opts.message) {
|
|
2682
|
+
console.error(chalk2.red("--message is required when adding a commit rule"));
|
|
2683
|
+
process.exit(1);
|
|
2684
|
+
}
|
|
2685
|
+
try {
|
|
2686
|
+
new RegExp(pattern);
|
|
2687
|
+
} catch {
|
|
2688
|
+
console.error(chalk2.red(`Invalid regex pattern: "${pattern}"`));
|
|
2689
|
+
process.exit(1);
|
|
2690
|
+
}
|
|
2691
|
+
const newRule = {
|
|
2692
|
+
pattern,
|
|
2693
|
+
message: opts.message,
|
|
2694
|
+
...opts.negate && { negate: true },
|
|
2695
|
+
...opts.advisory && { advisory: true }
|
|
2696
|
+
};
|
|
2697
|
+
updateProjectConfig((config) => ({
|
|
2698
|
+
...config,
|
|
2699
|
+
commitRules: [...(config.commitRules ?? []).filter((r) => r.pattern !== pattern), newRule]
|
|
2700
|
+
}));
|
|
2701
|
+
console.log(chalk2.green(`Commit rule saved: "${pattern}"`));
|
|
2702
|
+
console.log(chalk2.dim(" Run: memory-core commit-rules --list to see all rules"));
|
|
2703
|
+
});
|
|
2299
2704
|
program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
|
|
2300
2705
|
const workflowPath = join3(process.cwd(), ".github", "workflows", "memory-core.yml");
|
|
2301
2706
|
mkdirSync(dirname(workflowPath), { recursive: true });
|
|
@@ -2360,7 +2765,7 @@ program.command("uninstall").description("Remove memory-core from the current pr
|
|
|
2360
2765
|
console.log(chalk2.gray(" \u2713 cleaned .gitignore memory-core block"));
|
|
2361
2766
|
}
|
|
2362
2767
|
});
|
|
2363
|
-
program.command("stats").description("Show violation counters recorded by check and watch").option("--reset", "Reset violation counters and recent history").action((opts) => {
|
|
2768
|
+
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) => {
|
|
2364
2769
|
const statsPath = join3(process.cwd(), ".memory-core-stats.json");
|
|
2365
2770
|
if (opts.reset) {
|
|
2366
2771
|
const emptyStats = {
|
|
@@ -2382,14 +2787,46 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2382
2787
|
return;
|
|
2383
2788
|
}
|
|
2384
2789
|
const stats = JSON.parse(readFileSync3(statsPath, "utf-8"));
|
|
2790
|
+
const toEntry = (raw) => {
|
|
2791
|
+
if (raw === void 0) return { count: 0, falsePositives: 0 };
|
|
2792
|
+
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
2793
|
+
return raw;
|
|
2794
|
+
};
|
|
2385
2795
|
const printTop = (label, values = {}) => {
|
|
2386
2796
|
console.log(chalk2.bold(`
|
|
2387
2797
|
${label}
|
|
2388
2798
|
`));
|
|
2389
|
-
Object.entries(values).sort((a, b) => b
|
|
2390
|
-
|
|
2799
|
+
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) => {
|
|
2800
|
+
const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
|
|
2801
|
+
const fpHint = rate > 0 ? chalk2.dim(` \u2014 ${rate}% false-positive rate`) + (rate > 25 ? " \u26A0\uFE0F" : "") : "";
|
|
2802
|
+
console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${entry.count} hits${fpHint}`);
|
|
2391
2803
|
});
|
|
2392
2804
|
};
|
|
2805
|
+
if (opts.tune) {
|
|
2806
|
+
const tuneThreshold = 40;
|
|
2807
|
+
const tuneMinCount = 5;
|
|
2808
|
+
const noisy = Object.entries(stats.rules ?? {}).map(([name, raw]) => ({ name, entry: toEntry(raw) })).map(({ name, entry }) => ({
|
|
2809
|
+
name,
|
|
2810
|
+
count: entry.count,
|
|
2811
|
+
rate: entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0
|
|
2812
|
+
})).filter((r) => r.rate > tuneThreshold && r.count >= tuneMinCount).sort((a, b) => b.rate - a.rate);
|
|
2813
|
+
if (noisy.length === 0) {
|
|
2814
|
+
console.log(chalk2.green(`
|
|
2815
|
+
\u2713 No noisy rules found (threshold: ${tuneThreshold}%, min hits: ${tuneMinCount})
|
|
2816
|
+
`));
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
console.log(chalk2.bold(`
|
|
2820
|
+
Noisy rules (>${tuneThreshold}% false-positive rate, \u2265${tuneMinCount} hits)
|
|
2821
|
+
`));
|
|
2822
|
+
noisy.forEach(({ name, count, rate }, i) => {
|
|
2823
|
+
console.log(` ${i + 1}. ${truncate(name, 50).padEnd(52)} ${count} hits \u2014 ${rate}% \u26A0\uFE0F`);
|
|
2824
|
+
console.log(chalk2.dim(` To disable: memory-core allow "${name}"`));
|
|
2825
|
+
console.log(chalk2.dim(` Interactive: memory-core tune`));
|
|
2826
|
+
console.log();
|
|
2827
|
+
});
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2393
2830
|
const liveRules = stats.live?.rules ?? {};
|
|
2394
2831
|
const liveFiles = stats.live?.files ?? {};
|
|
2395
2832
|
const hasLiveState = !!stats.live;
|
|
@@ -2413,6 +2850,87 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2413
2850
|
}
|
|
2414
2851
|
}
|
|
2415
2852
|
});
|
|
2853
|
+
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) => {
|
|
2854
|
+
const statsPath = join3(process.cwd(), ".memory-core-stats.json");
|
|
2855
|
+
if (!existsSync3(statsPath)) {
|
|
2856
|
+
console.log(chalk2.yellow("\n No violation stats yet. Run some commits first.\n"));
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
const threshold = Math.max(0, Math.min(100, parseInt(opts.threshold, 10) || 40));
|
|
2860
|
+
const minCount = Math.max(1, parseInt(opts.minCount, 10) || 5);
|
|
2861
|
+
const toEntry = (raw) => {
|
|
2862
|
+
if (raw === void 0) return { count: 0, falsePositives: 0 };
|
|
2863
|
+
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
2864
|
+
return raw;
|
|
2865
|
+
};
|
|
2866
|
+
const stats = JSON.parse(readFileSync3(statsPath, "utf-8"));
|
|
2867
|
+
const noisy = Object.entries(stats.rules ?? {}).map(([rule, raw]) => {
|
|
2868
|
+
const entry = toEntry(raw);
|
|
2869
|
+
const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
|
|
2870
|
+
return { rule, count: entry.count, rate };
|
|
2871
|
+
}).filter((r) => r.rate > threshold && r.count >= minCount).sort((a, b) => b.rate - a.rate);
|
|
2872
|
+
if (noisy.length === 0) {
|
|
2873
|
+
console.log(chalk2.green(`
|
|
2874
|
+
\u2713 All rules within acceptable noise (threshold: ${threshold}%, min hits: ${minCount})
|
|
2875
|
+
`));
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
console.log(chalk2.bold(`
|
|
2879
|
+
Found ${noisy.length} noisy rule${noisy.length > 1 ? "s" : ""} (>${threshold}% false-positive rate, \u2265${minCount} hits)
|
|
2880
|
+
`));
|
|
2881
|
+
const existingAllows = new Set(
|
|
2882
|
+
(readProjectConfig()?.allowPatterns ?? []).map((p) => p.toLowerCase())
|
|
2883
|
+
);
|
|
2884
|
+
const toAdd = /* @__PURE__ */ new Set();
|
|
2885
|
+
if (opts.yes) {
|
|
2886
|
+
for (const { rule, count, rate } of noisy) {
|
|
2887
|
+
const key = rule.toLowerCase();
|
|
2888
|
+
if (existingAllows.has(key)) {
|
|
2889
|
+
console.log(chalk2.dim(` \u2022 "${truncate(rule, 56)}" \u2014 already disabled`));
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
toAdd.add(key);
|
|
2893
|
+
console.log(chalk2.green(` \u2713 "${truncate(rule, 56)}"`) + chalk2.dim(` \u2014 ${count} hits, ${rate}% FP rate`));
|
|
2894
|
+
}
|
|
2895
|
+
} else {
|
|
2896
|
+
const { select: select2 } = await import("@inquirer/prompts");
|
|
2897
|
+
for (let i = 0; i < noisy.length; i++) {
|
|
2898
|
+
const { rule, count, rate } = noisy[i];
|
|
2899
|
+
const key = rule.toLowerCase();
|
|
2900
|
+
console.log(chalk2.bold(`
|
|
2901
|
+
[${i + 1}/${noisy.length}] "${truncate(rule, 60)}"`));
|
|
2902
|
+
console.log(chalk2.dim(` ${count} hits \u2014 ${rate}% false-positive rate \u26A0\uFE0F`));
|
|
2903
|
+
if (existingAllows.has(key)) {
|
|
2904
|
+
console.log(chalk2.dim(" Already in allow patterns \u2014 skipping"));
|
|
2905
|
+
continue;
|
|
2906
|
+
}
|
|
2907
|
+
const choice = await select2({
|
|
2908
|
+
message: "What would you like to do?",
|
|
2909
|
+
choices: [
|
|
2910
|
+
{ name: "Disable this rule (add to allow patterns)", value: "disable" },
|
|
2911
|
+
{ name: "Keep it (skip for now)", value: "keep" },
|
|
2912
|
+
{ name: "Quit tuning", value: "quit" }
|
|
2913
|
+
]
|
|
2914
|
+
});
|
|
2915
|
+
if (choice === "quit") break;
|
|
2916
|
+
if (choice === "disable") {
|
|
2917
|
+
toAdd.add(key);
|
|
2918
|
+
console.log(chalk2.green(" \u2713 Marked for disable"));
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
if (toAdd.size > 0) {
|
|
2923
|
+
updateProjectConfig((config) => ({
|
|
2924
|
+
...config,
|
|
2925
|
+
allowPatterns: [.../* @__PURE__ */ new Set([...config.allowPatterns ?? [], ...toAdd])]
|
|
2926
|
+
}));
|
|
2927
|
+
console.log(chalk2.green(`
|
|
2928
|
+
\u2713 Saved ${toAdd.size} allow pattern${toAdd.size > 1 ? "s" : ""} to .memory-core.json`));
|
|
2929
|
+
console.log(chalk2.dim(" These rules will no longer block commits.\n"));
|
|
2930
|
+
} else {
|
|
2931
|
+
console.log(chalk2.dim("\n No changes made.\n"));
|
|
2932
|
+
}
|
|
2933
|
+
});
|
|
2416
2934
|
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) => {
|
|
2417
2935
|
const resolveDashboardPath = () => {
|
|
2418
2936
|
if (typeof opts.path === "string" && opts.path.trim().length > 0) return opts.path;
|
|
@@ -2430,7 +2948,7 @@ program.command("dashboard").description("Start the live Svelte dashboard with W
|
|
|
2430
2948
|
}
|
|
2431
2949
|
return void 0;
|
|
2432
2950
|
};
|
|
2433
|
-
const { startDashboard } = await import("./dashboard-server-
|
|
2951
|
+
const { startDashboard } = await import("./dashboard-server-ZERRHWQS.js");
|
|
2434
2952
|
await startDashboard({
|
|
2435
2953
|
port: parseInt(opts.port, 10),
|
|
2436
2954
|
path: resolveDashboardPath(),
|
|
@@ -2476,6 +2994,10 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
2476
2994
|
skipped++;
|
|
2477
2995
|
}
|
|
2478
2996
|
}
|
|
2997
|
+
if (saved > 0) {
|
|
2998
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
2999
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
3000
|
+
}
|
|
2479
3001
|
console.log(chalk2.bold.green(`
|
|
2480
3002
|
Done. ${saved} memories seeded, ${skipped} skipped.
|
|
2481
3003
|
`));
|
|
@@ -2593,7 +3115,7 @@ read:
|
|
|
2593
3115
|
await closePool();
|
|
2594
3116
|
});
|
|
2595
3117
|
var provider = program.command("provider").description("Manage the code-checking provider configuration");
|
|
2596
|
-
provider.command("set <name>").description("Set the code-checking provider (ollama, openai, anthropic, minimax)").option("--model <model>", "Chat model to set alongside the provider").option("--api-key <key>", "API key for cloud providers").action(async (name, opts) => {
|
|
3118
|
+
provider.command("set <name>").description("Set the code-checking provider (ollama, openai, anthropic, minimax, openai-compatible)").option("--model <model>", "Chat model to set alongside the provider").option("--api-key <key>", "API key for cloud providers").option("--base-url <url>", "API base URL for openai-compatible providers").action(async (name, opts) => {
|
|
2597
3119
|
try {
|
|
2598
3120
|
const providerName = normalizeProvider(name);
|
|
2599
3121
|
const runtimeEnv = readRuntimeEnv();
|
|
@@ -2611,6 +3133,7 @@ provider.command("set <name>").description("Set the code-checking provider (olla
|
|
|
2611
3133
|
values.CHAT_MODEL = model2;
|
|
2612
3134
|
values.OLLAMA_CHAT_MODEL = model2;
|
|
2613
3135
|
delete values.CHAT_API_KEY;
|
|
3136
|
+
delete values.CHAT_BASE_URL;
|
|
2614
3137
|
} else {
|
|
2615
3138
|
delete values.OLLAMA_CHAT_MODEL;
|
|
2616
3139
|
if (opts.apiKey) {
|
|
@@ -2620,10 +3143,20 @@ provider.command("set <name>").description("Set the code-checking provider (olla
|
|
|
2620
3143
|
message: `${providerLabel(providerName)} API key?`
|
|
2621
3144
|
});
|
|
2622
3145
|
}
|
|
3146
|
+
if (providerName === "openai-compatible") {
|
|
3147
|
+
if (opts.baseUrl) {
|
|
3148
|
+
values.CHAT_BASE_URL = opts.baseUrl;
|
|
3149
|
+
} else if (!values.CHAT_BASE_URL) {
|
|
3150
|
+
values.CHAT_BASE_URL = await input({
|
|
3151
|
+
message: "API base URL?",
|
|
3152
|
+
default: "https://api.groq.com/openai/v1"
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
2623
3156
|
}
|
|
2624
3157
|
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
2625
3158
|
applyRuntimeEnv(values);
|
|
2626
|
-
|
|
3159
|
+
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
2627
3160
|
console.log(chalk2.green(`Updated provider: ${providerName}`));
|
|
2628
3161
|
console.log(chalk2.gray(` Chat model: ${getConfiguredChatModel(values)}`));
|
|
2629
3162
|
} catch (err) {
|
|
@@ -2648,7 +3181,7 @@ model.command("set <name>").description("Set the chat model used for code checki
|
|
|
2648
3181
|
values.OLLAMA_MODEL = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
|
|
2649
3182
|
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
2650
3183
|
applyRuntimeEnv(values);
|
|
2651
|
-
|
|
3184
|
+
appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
|
|
2652
3185
|
console.log(chalk2.green(`Updated ${opts.embedding ? "embedding" : "chat"} model: ${name}`));
|
|
2653
3186
|
console.log(chalk2.gray(` Provider: ${providerName}`));
|
|
2654
3187
|
} catch (err) {
|
|
@@ -2834,7 +3367,13 @@ hook.command("install").description("Install pre-commit hook (advisory mode by d
|
|
|
2834
3367
|
hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
|
|
2835
3368
|
uninstallHook();
|
|
2836
3369
|
});
|
|
2837
|
-
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("--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").action(async (opts) => {
|
|
3370
|
+
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("--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").action(async (opts) => {
|
|
3371
|
+
if (opts.commitMsg !== void 0) {
|
|
3372
|
+
const msgFile = typeof opts.commitMsg === "string" ? opts.commitMsg : join3(process.cwd(), ".git", "COMMIT_EDITMSG");
|
|
3373
|
+
await checkCommitMsg(msgFile, { verbose: opts.verbose ?? false, debug: opts.debug ?? false });
|
|
3374
|
+
await closePool();
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
2838
3377
|
if (opts.ci && opts.all) {
|
|
2839
3378
|
console.error(chalk2.red("\n Choose one mode: --ci or --all.\n"));
|
|
2840
3379
|
process.exit(1);
|