@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/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  retrieveMemorySelection,
21
21
  runMigrations,
22
22
  seeds
23
- } from "./chunk-ECYSBYMM.js";
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
- stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
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
- const [rules, ignores] = fast ? [fallbackRules, []] : await Promise.all([
751
- withTimeout(loadRelevantRules(config, diff, stagedFiles, fallbackRules), ruleLoadTimeoutMs, fallbackRules),
752
- withTimeout(loadIgnorePatterns(), ignoreLoadTimeoutMs, [])
753
- ]);
754
- const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...ignores])];
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
- violations.forEach((v, i) => {
883
- const loc = v.file ? v.line ? `${v.file}:${v.line}` : v.file : "unknown location";
884
- console.log(chalk.bold(` [${i + 1}] ${loc}`));
885
- console.log(chalk.yellow(" Rule: ") + v.rule);
886
- const why = v.reason ?? reasonMap.get(v.rule);
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 (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
889
- if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
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 (!hasEnv && quick) {
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: ollamaUrl,
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 || "http://localhost:11434"
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: `${chatProvider.charAt(0).toUpperCase() + chatProvider.slice(1)} API key?`
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) => ({ name, value: name, checked: true })),
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
- if (pullMemories) {
1914
- const spinner2 = ora("Retrieving relevant memories\u2026").start();
1915
- try {
1916
- const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
1917
- const selection = await retrieveMemorySelection({
1918
- query: archQuery,
1919
- cwd: process.cwd(),
1920
- config,
1921
- limit: 20
1922
- });
1923
- memories = selection.included;
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 spinner2 = ora("Installing caveman token saver\u2026").start();
2285
+ const cavemanSpinner = ora("Installing caveman token saver\u2026").start();
1932
2286
  try {
1933
2287
  await installCavemanTokenSaver();
1934
- spinner2.succeed("Caveman installed");
2288
+ cavemanSpinner.succeed("Caveman installed");
1935
2289
  } catch (err) {
1936
- spinner2.warn(`Caveman install failed: ${err.message}`);
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 status = await checkConnections(
1958
- process.env.DATABASE_URL ?? "",
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[1] - a[1]).slice(0, 10).forEach(([name, count], index) => {
2390
- console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${count}`);
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-4WOUQTJN.js");
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);