@shahmilsaari/memory-core 1.0.16 → 1.0.18
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-UNGXRKD2.js} +11 -4
- package/dist/cli.js +665 -120
- 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-ZBGR4CO7.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-UNGXRKD2.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"];
|
|
@@ -1153,6 +1417,7 @@ function writeRuntimeEnv(values, envPath = getEnvPath()) {
|
|
|
1153
1417
|
"OLLAMA_MODEL",
|
|
1154
1418
|
"CHAT_PROVIDER",
|
|
1155
1419
|
"CHAT_MODEL",
|
|
1420
|
+
"CHAT_BASE_URL",
|
|
1156
1421
|
"OLLAMA_CHAT_MODEL",
|
|
1157
1422
|
"CHAT_API_KEY"
|
|
1158
1423
|
];
|
|
@@ -1251,10 +1516,10 @@ function removeProjectFiles(relativePaths) {
|
|
|
1251
1516
|
}
|
|
1252
1517
|
function normalizeProvider(value) {
|
|
1253
1518
|
const provider2 = value.trim().toLowerCase();
|
|
1254
|
-
if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax") {
|
|
1519
|
+
if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax" || provider2 === "openai-compatible") {
|
|
1255
1520
|
return provider2;
|
|
1256
1521
|
}
|
|
1257
|
-
throw new Error(`Unsupported provider "${value}". Use: ollama, openai, anthropic, minimax`);
|
|
1522
|
+
throw new Error(`Unsupported provider "${value}". Use: ollama, openai, anthropic, minimax, openai-compatible`);
|
|
1258
1523
|
}
|
|
1259
1524
|
function providerLabel(provider2) {
|
|
1260
1525
|
switch (provider2) {
|
|
@@ -1264,6 +1529,8 @@ function providerLabel(provider2) {
|
|
|
1264
1529
|
return "Anthropic";
|
|
1265
1530
|
case "minimax":
|
|
1266
1531
|
return "MiniMax";
|
|
1532
|
+
case "openai-compatible":
|
|
1533
|
+
return "OpenAI-compatible";
|
|
1267
1534
|
default:
|
|
1268
1535
|
return "Ollama";
|
|
1269
1536
|
}
|
|
@@ -1646,16 +1913,56 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1646
1913
|
console.log(chalk2.bold.cyan("\n memory-core init\n"));
|
|
1647
1914
|
const detected = detectProject();
|
|
1648
1915
|
const quick = opts.quick ?? false;
|
|
1916
|
+
let skipEnv = false;
|
|
1917
|
+
let skipProject = false;
|
|
1918
|
+
if (existsSync3(join3(process.cwd(), CONFIG_FILE)) && !quick) {
|
|
1919
|
+
const existing = readProjectConfig();
|
|
1920
|
+
const envVals = readRuntimeEnv().values;
|
|
1921
|
+
console.log(chalk2.dim(` Already initialized: ${existing?.projectName ?? "?"} (${existing?.projectType ?? "?"})`));
|
|
1922
|
+
console.log(chalk2.dim(` Provider: ${envVals.CHAT_PROVIDER ?? "ollama"} Model: ${envVals.CHAT_MODEL ?? "llama3.2"}`));
|
|
1923
|
+
console.log(chalk2.dim(` Hook: ${existsSync3(join3(".git", "hooks", "pre-commit")) ? "installed" : "not installed"} Agents: ${existing?.agents?.length ?? 0}
|
|
1924
|
+
`));
|
|
1925
|
+
const reinitChoice = await select({
|
|
1926
|
+
message: "Already initialized \u2014 what do you want to do?",
|
|
1927
|
+
choices: [
|
|
1928
|
+
{ value: "full", name: "Full re-init \u2014 update everything" },
|
|
1929
|
+
{ value: "connection", name: "Connection only \u2014 update DB / provider / model" },
|
|
1930
|
+
{ value: "project", name: "Project only \u2014 update arch, agents, hook" },
|
|
1931
|
+
{ value: "cancel", name: "Cancel" }
|
|
1932
|
+
]
|
|
1933
|
+
});
|
|
1934
|
+
if (reinitChoice === "cancel") {
|
|
1935
|
+
await closePool();
|
|
1936
|
+
process.exit(0);
|
|
1937
|
+
}
|
|
1938
|
+
skipEnv = reinitChoice === "project";
|
|
1939
|
+
skipProject = reinitChoice === "connection";
|
|
1940
|
+
}
|
|
1941
|
+
let pgOk = false;
|
|
1942
|
+
let ollamaOk = false;
|
|
1649
1943
|
const envPath = join3(process.cwd(), ".memory-core.env");
|
|
1650
1944
|
const hasEnv = existsSync3(envPath) || existsSync3(join3(process.cwd(), ".env")) || !!process.env.DATABASE_URL;
|
|
1651
|
-
if (
|
|
1945
|
+
if (skipEnv) {
|
|
1946
|
+
try {
|
|
1947
|
+
const { Pool } = (await import("pg")).default;
|
|
1948
|
+
const p = new Pool({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 3e3 });
|
|
1949
|
+
await p.query("SELECT 1");
|
|
1950
|
+
await p.end();
|
|
1951
|
+
pgOk = true;
|
|
1952
|
+
} catch {
|
|
1953
|
+
}
|
|
1954
|
+
try {
|
|
1955
|
+
const r = await fetch(`${process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3e3) });
|
|
1956
|
+
ollamaOk = r.ok;
|
|
1957
|
+
} catch {
|
|
1958
|
+
}
|
|
1959
|
+
} else if (!hasEnv && quick) {
|
|
1652
1960
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
1653
1961
|
const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
|
|
1654
|
-
const ollamaUrl = DEFAULT_OLLAMA_URL;
|
|
1655
1962
|
const chatModel = DEFAULT_CHAT_MODEL;
|
|
1656
1963
|
const envValues = {
|
|
1657
1964
|
DATABASE_URL: dbUrl,
|
|
1658
|
-
OLLAMA_URL:
|
|
1965
|
+
OLLAMA_URL: DEFAULT_OLLAMA_URL,
|
|
1659
1966
|
OLLAMA_MODEL: DEFAULT_EMBEDDING_MODEL,
|
|
1660
1967
|
CHAT_PROVIDER: "ollama",
|
|
1661
1968
|
CHAT_MODEL: chatModel,
|
|
@@ -1681,6 +1988,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1681
1988
|
await testPool.query("SELECT 1");
|
|
1682
1989
|
await testPool.end();
|
|
1683
1990
|
pgSpinner.succeed(chalk2.green("PostgreSQL connected"));
|
|
1991
|
+
pgOk = true;
|
|
1684
1992
|
break;
|
|
1685
1993
|
} catch (err) {
|
|
1686
1994
|
pgSpinner.fail(chalk2.red(`Cannot connect: ${err.message}`));
|
|
@@ -1690,14 +1998,15 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1690
1998
|
let ollamaUrl = "";
|
|
1691
1999
|
while (true) {
|
|
1692
2000
|
ollamaUrl = await input({
|
|
1693
|
-
message: "Ollama URL?",
|
|
1694
|
-
default: ollamaUrl ||
|
|
2001
|
+
message: "Ollama URL (used for search embeddings)?",
|
|
2002
|
+
default: ollamaUrl || DEFAULT_OLLAMA_URL
|
|
1695
2003
|
});
|
|
1696
2004
|
const ollamaSpinner = ora(" Testing Ollama connection\u2026").start();
|
|
1697
2005
|
try {
|
|
1698
2006
|
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1699
2007
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1700
2008
|
ollamaSpinner.succeed(chalk2.green("Ollama connected"));
|
|
2009
|
+
ollamaOk = true;
|
|
1701
2010
|
break;
|
|
1702
2011
|
} catch (err) {
|
|
1703
2012
|
ollamaSpinner.fail(chalk2.red(`Cannot reach Ollama: ${err.message}`));
|
|
@@ -1710,11 +2019,16 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1710
2019
|
{ name: "Local \u2014 Ollama (no API key, free)", value: "ollama" },
|
|
1711
2020
|
{ name: "OpenAI \u2014 gpt-4o, gpt-4o-mini", value: "openai" },
|
|
1712
2021
|
{ name: "Anthropic \u2014 claude-sonnet, claude-haiku", value: "anthropic" },
|
|
1713
|
-
{ name: "MiniMax \u2014 MiniMax-Text-01, abab6.5s-chat", value: "minimax" }
|
|
2022
|
+
{ name: "MiniMax \u2014 MiniMax-Text-01, abab6.5s-chat", value: "minimax" },
|
|
2023
|
+
{ name: "OpenAI-compatible \u2014 Groq, DeepSeek, xAI, Mistral, Together\u2026", value: "openai-compatible" }
|
|
1714
2024
|
]
|
|
1715
2025
|
});
|
|
2026
|
+
if (chatProvider !== "ollama") {
|
|
2027
|
+
console.log(chalk2.dim(" Note: Ollama is still used for search embeddings. Code checking uses the cloud provider above."));
|
|
2028
|
+
}
|
|
1716
2029
|
let chatModel = "";
|
|
1717
2030
|
let chatApiKey = "";
|
|
2031
|
+
let chatBaseUrl = "";
|
|
1718
2032
|
if (chatProvider === "ollama") {
|
|
1719
2033
|
while (true) {
|
|
1720
2034
|
const chatModelChoice = await select({
|
|
@@ -1767,6 +2081,13 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1767
2081
|
{ name: "MiniMax-Text-01 (flagship)", value: "MiniMax-Text-01" },
|
|
1768
2082
|
{ name: "abab6.5s-chat (fast, efficient)", value: "abab6.5s-chat" },
|
|
1769
2083
|
{ name: "Other (enter manually)", value: "__custom__" }
|
|
2084
|
+
],
|
|
2085
|
+
"openai-compatible": [
|
|
2086
|
+
{ name: "llama-3.1-70b-versatile (Groq)", value: "llama-3.1-70b-versatile" },
|
|
2087
|
+
{ name: "deepseek-coder (DeepSeek)", value: "deepseek-coder" },
|
|
2088
|
+
{ name: "grok-beta (xAI)", value: "grok-beta" },
|
|
2089
|
+
{ name: "mistral-large-latest (Mistral)", value: "mistral-large-latest" },
|
|
2090
|
+
{ name: "Other (enter manually)", value: "__custom__" }
|
|
1770
2091
|
]
|
|
1771
2092
|
};
|
|
1772
2093
|
const modelChoice = await select({
|
|
@@ -1774,8 +2095,14 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1774
2095
|
choices: modelChoices[chatProvider]
|
|
1775
2096
|
});
|
|
1776
2097
|
chatModel = modelChoice === "__custom__" ? await input({ message: "Model name?" }) : modelChoice;
|
|
2098
|
+
if (chatProvider === "openai-compatible") {
|
|
2099
|
+
chatBaseUrl = await input({
|
|
2100
|
+
message: "API base URL?",
|
|
2101
|
+
default: "https://api.groq.com/openai/v1"
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
1777
2104
|
chatApiKey = await input({
|
|
1778
|
-
message: `${
|
|
2105
|
+
message: `${providerLabel(chatProvider)} API key?`
|
|
1779
2106
|
});
|
|
1780
2107
|
console.log(chalk2.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
|
|
1781
2108
|
}
|
|
@@ -1787,19 +2114,34 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1787
2114
|
CHAT_MODEL: chatModel
|
|
1788
2115
|
};
|
|
1789
2116
|
if (chatProvider === "ollama") envValues.OLLAMA_CHAT_MODEL = chatModel;
|
|
2117
|
+
if (chatBaseUrl) envValues.CHAT_BASE_URL = chatBaseUrl;
|
|
1790
2118
|
if (chatApiKey) envValues.CHAT_API_KEY = chatApiKey;
|
|
1791
2119
|
writeRuntimeEnv(envValues, envPath);
|
|
1792
2120
|
applyRuntimeEnv(envValues);
|
|
1793
2121
|
ensureEnvFileIgnored(envPath);
|
|
1794
2122
|
console.log(chalk2.green("\n \u2713 .memory-core.env created"));
|
|
1795
2123
|
console.log(chalk2.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
|
|
2124
|
+
} else {
|
|
2125
|
+
try {
|
|
2126
|
+
const { Pool } = (await import("pg")).default;
|
|
2127
|
+
const p = new Pool({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 3e3 });
|
|
2128
|
+
await p.query("SELECT 1");
|
|
2129
|
+
await p.end();
|
|
2130
|
+
pgOk = true;
|
|
2131
|
+
} catch {
|
|
2132
|
+
}
|
|
2133
|
+
try {
|
|
2134
|
+
const r = await fetch(`${process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3e3) });
|
|
2135
|
+
ollamaOk = r.ok;
|
|
2136
|
+
} catch {
|
|
2137
|
+
}
|
|
1796
2138
|
}
|
|
1797
|
-
const projectName = quick ? process.cwd().split("/").pop() ?? "my-project" : await input({
|
|
2139
|
+
const projectName = quick || skipProject ? readProjectConfig()?.projectName ?? process.cwd().split("/").pop() ?? "my-project" : await input({
|
|
1798
2140
|
message: "Project name?",
|
|
1799
2141
|
default: process.cwd().split("/").pop() ?? "my-project"
|
|
1800
2142
|
});
|
|
1801
2143
|
const inferredProjectType = ["Nuxt.js"].includes(detected.framework) ? "fullstack" : ["React", "Vue.js", "Svelte"].includes(detected.framework) ? "frontend" : "backend";
|
|
1802
|
-
const projectType = quick ? inferredProjectType : await select({
|
|
2144
|
+
const projectType = quick || skipProject ? readProjectConfig()?.projectType ?? inferredProjectType : await select({
|
|
1803
2145
|
message: "Project type?",
|
|
1804
2146
|
choices: [
|
|
1805
2147
|
{ value: "backend", name: "Backend \u2014 API, server, microservice" },
|
|
@@ -1809,8 +2151,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1809
2151
|
});
|
|
1810
2152
|
let backendArchitecture;
|
|
1811
2153
|
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";
|
|
2154
|
+
if (quick || skipProject) {
|
|
2155
|
+
backendArchitecture = readProjectConfig()?.backendArchitecture ?? (detected.framework === "NestJS" ? "nestjs" : detected.framework === "Laravel" ? "laravel-service-repository" : detected.framework === "Go" ? "go-api" : "clean-architecture");
|
|
1814
2156
|
} else {
|
|
1815
2157
|
const backendProfiles = listProfiles("backend");
|
|
1816
2158
|
backendArchitecture = await select({
|
|
@@ -1824,14 +2166,14 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1824
2166
|
}
|
|
1825
2167
|
let frontendFramework;
|
|
1826
2168
|
if (projectType === "frontend" || projectType === "fullstack") {
|
|
1827
|
-
if (quick) {
|
|
2169
|
+
if (quick || skipProject) {
|
|
1828
2170
|
const frameworkMap = {
|
|
1829
2171
|
"Nuxt.js": "nuxt",
|
|
1830
2172
|
React: "react",
|
|
1831
2173
|
"Vue.js": "vue",
|
|
1832
2174
|
Svelte: "svelte"
|
|
1833
2175
|
};
|
|
1834
|
-
frontendFramework = frameworkMap[detected.framework] ?? "react";
|
|
2176
|
+
frontendFramework = readProjectConfig()?.frontendFramework ?? (frameworkMap[detected.framework] ?? "react");
|
|
1835
2177
|
} else {
|
|
1836
2178
|
const frontendProfiles = listProfiles("frontend");
|
|
1837
2179
|
frontendFramework = await select({
|
|
@@ -1843,43 +2185,20 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1843
2185
|
});
|
|
1844
2186
|
}
|
|
1845
2187
|
}
|
|
1846
|
-
const language = quick ? detected.language : await input({
|
|
2188
|
+
const language = quick || skipProject ? readProjectConfig()?.language ?? detected.language : await input({
|
|
1847
2189
|
message: "Language?",
|
|
1848
2190
|
default: detected.language
|
|
1849
2191
|
});
|
|
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
2192
|
const selectedAgents = quick ? AGENT_NAMES.filter((a) => a !== "Shared") : await (async () => {
|
|
1879
2193
|
const { checkbox } = await import("@inquirer/prompts");
|
|
2194
|
+
const saved = new Set(readProjectConfig()?.agents ?? []);
|
|
1880
2195
|
return checkbox({
|
|
1881
2196
|
message: "Which AI agents do you want to generate files for?",
|
|
1882
|
-
choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({
|
|
2197
|
+
choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({
|
|
2198
|
+
name,
|
|
2199
|
+
value: name,
|
|
2200
|
+
checked: saved.size === 0 || saved.has(name)
|
|
2201
|
+
})),
|
|
1883
2202
|
instructions: " (Space to toggle, A to select all, Enter to confirm)"
|
|
1884
2203
|
});
|
|
1885
2204
|
})();
|
|
@@ -1898,6 +2217,42 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1898
2217
|
]
|
|
1899
2218
|
});
|
|
1900
2219
|
}
|
|
2220
|
+
let installCaveman = false;
|
|
2221
|
+
let cavemanIntensity = "full";
|
|
2222
|
+
if (!quick) {
|
|
2223
|
+
installCaveman = await confirm({
|
|
2224
|
+
message: "Enable caveman token saver? (compresses AI responses ~70%)",
|
|
2225
|
+
default: false
|
|
2226
|
+
});
|
|
2227
|
+
if (installCaveman) {
|
|
2228
|
+
cavemanIntensity = await select({
|
|
2229
|
+
message: "Intensity?",
|
|
2230
|
+
choices: [
|
|
2231
|
+
{ value: "full", name: "Full \u2014 caveman mode (default)" },
|
|
2232
|
+
{ value: "lite", name: "Lite \u2014 professional terseness" },
|
|
2233
|
+
{ value: "ultra", name: "Ultra \u2014 telegraphic, minimum words" }
|
|
2234
|
+
]
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
if (!quick) {
|
|
2239
|
+
const envVals = readRuntimeEnv().values;
|
|
2240
|
+
console.log(chalk2.bold("\n Ready to initialize\n"));
|
|
2241
|
+
console.log(` Project ${chalk2.white(projectName)} (${projectType})`);
|
|
2242
|
+
if (backendArchitecture) console.log(` Backend ${chalk2.white(backendArchitecture)}`);
|
|
2243
|
+
if (frontendFramework) console.log(` Frontend ${chalk2.white(frontendFramework)}`);
|
|
2244
|
+
console.log(` Language ${chalk2.white(language)}`);
|
|
2245
|
+
console.log(` Provider ${chalk2.white(envVals.CHAT_PROVIDER ?? "ollama")} / ${chalk2.white(envVals.CHAT_MODEL ?? DEFAULT_CHAT_MODEL)}`);
|
|
2246
|
+
console.log(` Agents ${chalk2.white(String(selectedAgents.length))} selected`);
|
|
2247
|
+
console.log(` Hook ${chalk2.white(enableHook ? hookAdvisory ? "advisory" : "strict" : "skip")}`);
|
|
2248
|
+
console.log();
|
|
2249
|
+
const proceed = await confirm({ message: "Generate files?", default: true });
|
|
2250
|
+
if (!proceed) {
|
|
2251
|
+
console.log(chalk2.yellow(" Cancelled.\n"));
|
|
2252
|
+
await closePool();
|
|
2253
|
+
process.exit(0);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
1901
2256
|
const config = {
|
|
1902
2257
|
projectName,
|
|
1903
2258
|
projectType,
|
|
@@ -1906,34 +2261,33 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1906
2261
|
language,
|
|
1907
2262
|
caveman: { enabled: installCaveman, intensity: cavemanIntensity },
|
|
1908
2263
|
agents: selectedAgents,
|
|
1909
|
-
allowPatterns: [],
|
|
2264
|
+
allowPatterns: readProjectConfig()?.allowPatterns ?? [],
|
|
2265
|
+
commitRules: readProjectConfig()?.commitRules,
|
|
1910
2266
|
autoSync: true
|
|
1911
2267
|
};
|
|
1912
2268
|
let memories = [];
|
|
1913
|
-
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
memories
|
|
1924
|
-
spinner2.succeed(`Found ${memories.length} relevant memories`);
|
|
2269
|
+
try {
|
|
2270
|
+
const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
|
|
2271
|
+
const selection = await retrieveMemorySelection({
|
|
2272
|
+
query: archQuery,
|
|
2273
|
+
cwd: process.cwd(),
|
|
2274
|
+
config,
|
|
2275
|
+
limit: 20
|
|
2276
|
+
});
|
|
2277
|
+
memories = selection.included;
|
|
2278
|
+
if (memories.length > 0) {
|
|
2279
|
+
console.log(chalk2.dim(` Found ${memories.length} relevant memories`));
|
|
1925
2280
|
printMemorySelection(selection);
|
|
1926
|
-
} catch (err) {
|
|
1927
|
-
spinner2.warn(`Could not retrieve memories: ${err.message}`);
|
|
1928
2281
|
}
|
|
2282
|
+
} catch {
|
|
1929
2283
|
}
|
|
1930
2284
|
if (installCaveman) {
|
|
1931
|
-
const
|
|
2285
|
+
const cavemanSpinner = ora("Installing caveman token saver\u2026").start();
|
|
1932
2286
|
try {
|
|
1933
2287
|
await installCavemanTokenSaver();
|
|
1934
|
-
|
|
2288
|
+
cavemanSpinner.succeed("Caveman installed");
|
|
1935
2289
|
} catch (err) {
|
|
1936
|
-
|
|
2290
|
+
cavemanSpinner.warn(`Caveman install failed: ${err.message}`);
|
|
1937
2291
|
}
|
|
1938
2292
|
}
|
|
1939
2293
|
const spinner = ora("Generating AI agent context files\u2026").start();
|
|
@@ -1954,12 +2308,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1954
2308
|
if (enableHook) {
|
|
1955
2309
|
installHook(hookAdvisory);
|
|
1956
2310
|
}
|
|
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);
|
|
2311
|
+
const chatModelForBanner = process.env.CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
|
|
2312
|
+
printBanner(config.projectName, written.written.length, { postgresOk: pgOk, ollamaOk, chatModel: chatModelForBanner });
|
|
1963
2313
|
await closePool();
|
|
1964
2314
|
});
|
|
1965
2315
|
program.command("sync").description("Re-pull memories and regenerate AI agent files").action(async () => {
|
|
@@ -2031,6 +2381,8 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
2031
2381
|
context: buildMemoryContext(opts),
|
|
2032
2382
|
tags: parseTags(opts.tags)
|
|
2033
2383
|
});
|
|
2384
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
2385
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2034
2386
|
const reasonLine = chalk2.gray(`
|
|
2035
2387
|
Why: ${storedReason}`);
|
|
2036
2388
|
spinner.succeed(chalk2.green(`Memory saved: "${text}"`) + reasonLine);
|
|
@@ -2119,6 +2471,8 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
|
|
|
2119
2471
|
}
|
|
2120
2472
|
spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
|
|
2121
2473
|
if (inserted > 0) {
|
|
2474
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
2475
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2122
2476
|
await autoSyncGeneratedFiles(config, "import", opts.sync);
|
|
2123
2477
|
}
|
|
2124
2478
|
} catch (err) {
|
|
@@ -2257,6 +2611,8 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
2257
2611
|
content: pattern,
|
|
2258
2612
|
tags: ["ignore"]
|
|
2259
2613
|
});
|
|
2614
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
2615
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2260
2616
|
console.log(chalk2.green(`Ignored pattern saved: "${pattern}"`));
|
|
2261
2617
|
await autoSyncGeneratedFiles(config, "ignore", opts.sync);
|
|
2262
2618
|
} catch (err) {
|
|
@@ -2296,6 +2652,61 @@ program.command("allow [pattern]").description("Manage project allow patterns in
|
|
|
2296
2652
|
}));
|
|
2297
2653
|
console.log(chalk2.green(`Allow pattern saved: "${pattern}"`));
|
|
2298
2654
|
});
|
|
2655
|
+
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) => {
|
|
2656
|
+
if (opts.list) {
|
|
2657
|
+
const rules = readProjectConfig()?.commitRules ?? [];
|
|
2658
|
+
if (rules.length === 0) {
|
|
2659
|
+
console.log(chalk2.yellow("\n No commit rules configured.\n"));
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
console.log(chalk2.bold("\n Commit message rules\n"));
|
|
2663
|
+
rules.forEach((rule, i) => {
|
|
2664
|
+
const flags = [
|
|
2665
|
+
rule.negate ? "must NOT match" : "must match",
|
|
2666
|
+
rule.advisory ? "advisory" : "blocking"
|
|
2667
|
+
].join(", ");
|
|
2668
|
+
console.log(` ${i + 1}. ${rule.pattern}`);
|
|
2669
|
+
console.log(chalk2.dim(` Message: ${rule.message}`));
|
|
2670
|
+
console.log(chalk2.dim(` Flags: ${flags}`));
|
|
2671
|
+
console.log();
|
|
2672
|
+
});
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
if (opts.remove) {
|
|
2676
|
+
updateProjectConfig((config) => ({
|
|
2677
|
+
...config,
|
|
2678
|
+
commitRules: (config.commitRules ?? []).filter((r) => r.pattern !== opts.remove)
|
|
2679
|
+
}));
|
|
2680
|
+
console.log(chalk2.green(`Commit rule removed: "${opts.remove}"`));
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
if (!pattern) {
|
|
2684
|
+
console.error(chalk2.red("Provide a pattern, --list, or --remove <pattern>"));
|
|
2685
|
+
process.exit(1);
|
|
2686
|
+
}
|
|
2687
|
+
if (!opts.message) {
|
|
2688
|
+
console.error(chalk2.red("--message is required when adding a commit rule"));
|
|
2689
|
+
process.exit(1);
|
|
2690
|
+
}
|
|
2691
|
+
try {
|
|
2692
|
+
new RegExp(pattern);
|
|
2693
|
+
} catch {
|
|
2694
|
+
console.error(chalk2.red(`Invalid regex pattern: "${pattern}"`));
|
|
2695
|
+
process.exit(1);
|
|
2696
|
+
}
|
|
2697
|
+
const newRule = {
|
|
2698
|
+
pattern,
|
|
2699
|
+
message: opts.message,
|
|
2700
|
+
...opts.negate && { negate: true },
|
|
2701
|
+
...opts.advisory && { advisory: true }
|
|
2702
|
+
};
|
|
2703
|
+
updateProjectConfig((config) => ({
|
|
2704
|
+
...config,
|
|
2705
|
+
commitRules: [...(config.commitRules ?? []).filter((r) => r.pattern !== pattern), newRule]
|
|
2706
|
+
}));
|
|
2707
|
+
console.log(chalk2.green(`Commit rule saved: "${pattern}"`));
|
|
2708
|
+
console.log(chalk2.dim(" Run: memory-core commit-rules --list to see all rules"));
|
|
2709
|
+
});
|
|
2299
2710
|
program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
|
|
2300
2711
|
const workflowPath = join3(process.cwd(), ".github", "workflows", "memory-core.yml");
|
|
2301
2712
|
mkdirSync(dirname(workflowPath), { recursive: true });
|
|
@@ -2360,7 +2771,7 @@ program.command("uninstall").description("Remove memory-core from the current pr
|
|
|
2360
2771
|
console.log(chalk2.gray(" \u2713 cleaned .gitignore memory-core block"));
|
|
2361
2772
|
}
|
|
2362
2773
|
});
|
|
2363
|
-
program.command("stats").description("Show violation counters recorded by check and watch").option("--reset", "Reset violation counters and recent history").action((opts) => {
|
|
2774
|
+
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
2775
|
const statsPath = join3(process.cwd(), ".memory-core-stats.json");
|
|
2365
2776
|
if (opts.reset) {
|
|
2366
2777
|
const emptyStats = {
|
|
@@ -2382,14 +2793,46 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2382
2793
|
return;
|
|
2383
2794
|
}
|
|
2384
2795
|
const stats = JSON.parse(readFileSync3(statsPath, "utf-8"));
|
|
2796
|
+
const toEntry = (raw) => {
|
|
2797
|
+
if (raw === void 0) return { count: 0, falsePositives: 0 };
|
|
2798
|
+
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
2799
|
+
return raw;
|
|
2800
|
+
};
|
|
2385
2801
|
const printTop = (label, values = {}) => {
|
|
2386
2802
|
console.log(chalk2.bold(`
|
|
2387
2803
|
${label}
|
|
2388
2804
|
`));
|
|
2389
|
-
Object.entries(values).sort((a, b) => b
|
|
2390
|
-
|
|
2805
|
+
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) => {
|
|
2806
|
+
const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
|
|
2807
|
+
const fpHint = rate > 0 ? chalk2.dim(` \u2014 ${rate}% false-positive rate`) + (rate > 25 ? " \u26A0\uFE0F" : "") : "";
|
|
2808
|
+
console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${entry.count} hits${fpHint}`);
|
|
2391
2809
|
});
|
|
2392
2810
|
};
|
|
2811
|
+
if (opts.tune) {
|
|
2812
|
+
const tuneThreshold = 40;
|
|
2813
|
+
const tuneMinCount = 5;
|
|
2814
|
+
const noisy = Object.entries(stats.rules ?? {}).map(([name, raw]) => ({ name, entry: toEntry(raw) })).map(({ name, entry }) => ({
|
|
2815
|
+
name,
|
|
2816
|
+
count: entry.count,
|
|
2817
|
+
rate: entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0
|
|
2818
|
+
})).filter((r) => r.rate > tuneThreshold && r.count >= tuneMinCount).sort((a, b) => b.rate - a.rate);
|
|
2819
|
+
if (noisy.length === 0) {
|
|
2820
|
+
console.log(chalk2.green(`
|
|
2821
|
+
\u2713 No noisy rules found (threshold: ${tuneThreshold}%, min hits: ${tuneMinCount})
|
|
2822
|
+
`));
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
console.log(chalk2.bold(`
|
|
2826
|
+
Noisy rules (>${tuneThreshold}% false-positive rate, \u2265${tuneMinCount} hits)
|
|
2827
|
+
`));
|
|
2828
|
+
noisy.forEach(({ name, count, rate }, i) => {
|
|
2829
|
+
console.log(` ${i + 1}. ${truncate(name, 50).padEnd(52)} ${count} hits \u2014 ${rate}% \u26A0\uFE0F`);
|
|
2830
|
+
console.log(chalk2.dim(` To disable: memory-core allow "${name}"`));
|
|
2831
|
+
console.log(chalk2.dim(` Interactive: memory-core tune`));
|
|
2832
|
+
console.log();
|
|
2833
|
+
});
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2393
2836
|
const liveRules = stats.live?.rules ?? {};
|
|
2394
2837
|
const liveFiles = stats.live?.files ?? {};
|
|
2395
2838
|
const hasLiveState = !!stats.live;
|
|
@@ -2413,6 +2856,87 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2413
2856
|
}
|
|
2414
2857
|
}
|
|
2415
2858
|
});
|
|
2859
|
+
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) => {
|
|
2860
|
+
const statsPath = join3(process.cwd(), ".memory-core-stats.json");
|
|
2861
|
+
if (!existsSync3(statsPath)) {
|
|
2862
|
+
console.log(chalk2.yellow("\n No violation stats yet. Run some commits first.\n"));
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
const threshold = Math.max(0, Math.min(100, parseInt(opts.threshold, 10) || 40));
|
|
2866
|
+
const minCount = Math.max(1, parseInt(opts.minCount, 10) || 5);
|
|
2867
|
+
const toEntry = (raw) => {
|
|
2868
|
+
if (raw === void 0) return { count: 0, falsePositives: 0 };
|
|
2869
|
+
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
2870
|
+
return raw;
|
|
2871
|
+
};
|
|
2872
|
+
const stats = JSON.parse(readFileSync3(statsPath, "utf-8"));
|
|
2873
|
+
const noisy = Object.entries(stats.rules ?? {}).map(([rule, raw]) => {
|
|
2874
|
+
const entry = toEntry(raw);
|
|
2875
|
+
const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
|
|
2876
|
+
return { rule, count: entry.count, rate };
|
|
2877
|
+
}).filter((r) => r.rate > threshold && r.count >= minCount).sort((a, b) => b.rate - a.rate);
|
|
2878
|
+
if (noisy.length === 0) {
|
|
2879
|
+
console.log(chalk2.green(`
|
|
2880
|
+
\u2713 All rules within acceptable noise (threshold: ${threshold}%, min hits: ${minCount})
|
|
2881
|
+
`));
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
console.log(chalk2.bold(`
|
|
2885
|
+
Found ${noisy.length} noisy rule${noisy.length > 1 ? "s" : ""} (>${threshold}% false-positive rate, \u2265${minCount} hits)
|
|
2886
|
+
`));
|
|
2887
|
+
const existingAllows = new Set(
|
|
2888
|
+
(readProjectConfig()?.allowPatterns ?? []).map((p) => p.toLowerCase())
|
|
2889
|
+
);
|
|
2890
|
+
const toAdd = /* @__PURE__ */ new Set();
|
|
2891
|
+
if (opts.yes) {
|
|
2892
|
+
for (const { rule, count, rate } of noisy) {
|
|
2893
|
+
const key = rule.toLowerCase();
|
|
2894
|
+
if (existingAllows.has(key)) {
|
|
2895
|
+
console.log(chalk2.dim(` \u2022 "${truncate(rule, 56)}" \u2014 already disabled`));
|
|
2896
|
+
continue;
|
|
2897
|
+
}
|
|
2898
|
+
toAdd.add(key);
|
|
2899
|
+
console.log(chalk2.green(` \u2713 "${truncate(rule, 56)}"`) + chalk2.dim(` \u2014 ${count} hits, ${rate}% FP rate`));
|
|
2900
|
+
}
|
|
2901
|
+
} else {
|
|
2902
|
+
const { select: select2 } = await import("@inquirer/prompts");
|
|
2903
|
+
for (let i = 0; i < noisy.length; i++) {
|
|
2904
|
+
const { rule, count, rate } = noisy[i];
|
|
2905
|
+
const key = rule.toLowerCase();
|
|
2906
|
+
console.log(chalk2.bold(`
|
|
2907
|
+
[${i + 1}/${noisy.length}] "${truncate(rule, 60)}"`));
|
|
2908
|
+
console.log(chalk2.dim(` ${count} hits \u2014 ${rate}% false-positive rate \u26A0\uFE0F`));
|
|
2909
|
+
if (existingAllows.has(key)) {
|
|
2910
|
+
console.log(chalk2.dim(" Already in allow patterns \u2014 skipping"));
|
|
2911
|
+
continue;
|
|
2912
|
+
}
|
|
2913
|
+
const choice = await select2({
|
|
2914
|
+
message: "What would you like to do?",
|
|
2915
|
+
choices: [
|
|
2916
|
+
{ name: "Disable this rule (add to allow patterns)", value: "disable" },
|
|
2917
|
+
{ name: "Keep it (skip for now)", value: "keep" },
|
|
2918
|
+
{ name: "Quit tuning", value: "quit" }
|
|
2919
|
+
]
|
|
2920
|
+
});
|
|
2921
|
+
if (choice === "quit") break;
|
|
2922
|
+
if (choice === "disable") {
|
|
2923
|
+
toAdd.add(key);
|
|
2924
|
+
console.log(chalk2.green(" \u2713 Marked for disable"));
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
if (toAdd.size > 0) {
|
|
2929
|
+
updateProjectConfig((config) => ({
|
|
2930
|
+
...config,
|
|
2931
|
+
allowPatterns: [.../* @__PURE__ */ new Set([...config.allowPatterns ?? [], ...toAdd])]
|
|
2932
|
+
}));
|
|
2933
|
+
console.log(chalk2.green(`
|
|
2934
|
+
\u2713 Saved ${toAdd.size} allow pattern${toAdd.size > 1 ? "s" : ""} to .memory-core.json`));
|
|
2935
|
+
console.log(chalk2.dim(" These rules will no longer block commits.\n"));
|
|
2936
|
+
} else {
|
|
2937
|
+
console.log(chalk2.dim("\n No changes made.\n"));
|
|
2938
|
+
}
|
|
2939
|
+
});
|
|
2416
2940
|
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
2941
|
const resolveDashboardPath = () => {
|
|
2418
2942
|
if (typeof opts.path === "string" && opts.path.trim().length > 0) return opts.path;
|
|
@@ -2430,7 +2954,7 @@ program.command("dashboard").description("Start the live Svelte dashboard with W
|
|
|
2430
2954
|
}
|
|
2431
2955
|
return void 0;
|
|
2432
2956
|
};
|
|
2433
|
-
const { startDashboard } = await import("./dashboard-server-
|
|
2957
|
+
const { startDashboard } = await import("./dashboard-server-ZBGR4CO7.js");
|
|
2434
2958
|
await startDashboard({
|
|
2435
2959
|
port: parseInt(opts.port, 10),
|
|
2436
2960
|
path: resolveDashboardPath(),
|
|
@@ -2476,6 +3000,10 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
2476
3000
|
skipped++;
|
|
2477
3001
|
}
|
|
2478
3002
|
}
|
|
3003
|
+
if (saved > 0) {
|
|
3004
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
3005
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
3006
|
+
}
|
|
2479
3007
|
console.log(chalk2.bold.green(`
|
|
2480
3008
|
Done. ${saved} memories seeded, ${skipped} skipped.
|
|
2481
3009
|
`));
|
|
@@ -2593,7 +3121,7 @@ read:
|
|
|
2593
3121
|
await closePool();
|
|
2594
3122
|
});
|
|
2595
3123
|
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) => {
|
|
3124
|
+
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
3125
|
try {
|
|
2598
3126
|
const providerName = normalizeProvider(name);
|
|
2599
3127
|
const runtimeEnv = readRuntimeEnv();
|
|
@@ -2611,6 +3139,7 @@ provider.command("set <name>").description("Set the code-checking provider (olla
|
|
|
2611
3139
|
values.CHAT_MODEL = model2;
|
|
2612
3140
|
values.OLLAMA_CHAT_MODEL = model2;
|
|
2613
3141
|
delete values.CHAT_API_KEY;
|
|
3142
|
+
delete values.CHAT_BASE_URL;
|
|
2614
3143
|
} else {
|
|
2615
3144
|
delete values.OLLAMA_CHAT_MODEL;
|
|
2616
3145
|
if (opts.apiKey) {
|
|
@@ -2620,6 +3149,16 @@ provider.command("set <name>").description("Set the code-checking provider (olla
|
|
|
2620
3149
|
message: `${providerLabel(providerName)} API key?`
|
|
2621
3150
|
});
|
|
2622
3151
|
}
|
|
3152
|
+
if (providerName === "openai-compatible") {
|
|
3153
|
+
if (opts.baseUrl) {
|
|
3154
|
+
values.CHAT_BASE_URL = opts.baseUrl;
|
|
3155
|
+
} else if (!values.CHAT_BASE_URL) {
|
|
3156
|
+
values.CHAT_BASE_URL = await input({
|
|
3157
|
+
message: "API base URL?",
|
|
3158
|
+
default: "https://api.groq.com/openai/v1"
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
2623
3162
|
}
|
|
2624
3163
|
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
2625
3164
|
applyRuntimeEnv(values);
|
|
@@ -2834,7 +3373,13 @@ hook.command("install").description("Install pre-commit hook (advisory mode by d
|
|
|
2834
3373
|
hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
|
|
2835
3374
|
uninstallHook();
|
|
2836
3375
|
});
|
|
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) => {
|
|
3376
|
+
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) => {
|
|
3377
|
+
if (opts.commitMsg !== void 0) {
|
|
3378
|
+
const msgFile = typeof opts.commitMsg === "string" ? opts.commitMsg : join3(process.cwd(), ".git", "COMMIT_EDITMSG");
|
|
3379
|
+
await checkCommitMsg(msgFile, { verbose: opts.verbose ?? false, debug: opts.debug ?? false });
|
|
3380
|
+
await closePool();
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
2838
3383
|
if (opts.ci && opts.all) {
|
|
2839
3384
|
console.error(chalk2.red("\n Choose one mode: --ci or --all.\n"));
|
|
2840
3385
|
process.exit(1);
|