@shahmilsaari/memory-core 1.0.16 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-VQEIQHK6.js";
24
24
 
25
25
  // src/cli.ts
26
26
  import { Command } from "commander";
@@ -33,7 +33,7 @@ import { homedir } from "os";
33
33
 
34
34
  // src/hook.ts
35
35
  import { execSync, spawnSync } from "child_process";
36
- import { writeFileSync as writeFileSync2, existsSync as existsSync2, unlinkSync, readFileSync as readFileSync2, chmodSync } from "fs";
36
+ import { writeFileSync as writeFileSync2, existsSync as existsSync2, unlinkSync, readFileSync as readFileSync2, chmodSync, statSync } from "fs";
37
37
  import { join as join2 } from "path";
38
38
  import chalk from "chalk";
39
39
 
@@ -122,6 +122,11 @@ var reasonMap = new Map(
122
122
  );
123
123
  var HOOK_PATH = join2(".git", "hooks", "pre-commit");
124
124
  var HOOK_MARKER = "# archmind-memory-core";
125
+ var COMMIT_MSG_HOOK_PATH = join2(".git", "hooks", "commit-msg");
126
+ var COMMIT_MSG_HOOK_MARKER = "# archmind-memory-core commit-msg";
127
+ var RULE_CACHE_FILE = ".memory-core-rules-cache.json";
128
+ var DB_VERSION_FILE = ".memory-core-db-version";
129
+ var RULE_CACHE_TTL_MS = 5 * 60 * 1e3;
125
130
  function buildHookBody(advisory, fast = false) {
126
131
  const suffix = advisory ? " || true" : "";
127
132
  const checkArgs = fast ? "check --staged --fast" : "check --staged";
@@ -166,6 +171,11 @@ function normalizeHookPreamble(content) {
166
171
  }
167
172
  return normalized.join("\n").replace(/\n{3,}/g, "\n\n").trim();
168
173
  }
174
+ function toRuleStatEntry(raw) {
175
+ if (raw === void 0) return { count: 0, falsePositives: 0 };
176
+ if (typeof raw === "number") return { count: raw, falsePositives: 0 };
177
+ return raw;
178
+ }
169
179
  function readPositiveIntEnv(name, fallback) {
170
180
  const raw = Number(process.env[name]);
171
181
  return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
@@ -199,7 +209,8 @@ function recordViolations(violations, source = "hook") {
199
209
  stats.rules ??= {};
200
210
  stats.files ??= {};
201
211
  for (const violation of violations) {
202
- 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"];
@@ -1121,6 +1385,9 @@ function getEnvPath() {
1121
1385
  const dotEnv = join3(process.cwd(), ".env");
1122
1386
  return existsSync3(dotEnv) ? dotEnv : memoryEnv;
1123
1387
  }
1388
+ function getWriteEnvPath() {
1389
+ return join3(process.cwd(), ".memory-core.env");
1390
+ }
1124
1391
  function parseEnvFile(raw) {
1125
1392
  const lines = raw.split(/\r?\n/);
1126
1393
  const values = {};
@@ -1144,15 +1411,16 @@ function readRuntimeEnv() {
1144
1411
  for (const [key, value] of Object.entries(process.env)) {
1145
1412
  if (typeof value === "string" && value !== "") values[key] = value;
1146
1413
  }
1147
- return { envPath, values };
1414
+ return { envPath: getWriteEnvPath(), values };
1148
1415
  }
1149
- function writeRuntimeEnv(values, envPath = getEnvPath()) {
1416
+ function writeRuntimeEnv(values, envPath = getWriteEnvPath()) {
1150
1417
  const orderedKeys = [
1151
1418
  "DATABASE_URL",
1152
1419
  "OLLAMA_URL",
1153
1420
  "OLLAMA_MODEL",
1154
1421
  "CHAT_PROVIDER",
1155
1422
  "CHAT_MODEL",
1423
+ "CHAT_BASE_URL",
1156
1424
  "OLLAMA_CHAT_MODEL",
1157
1425
  "CHAT_API_KEY"
1158
1426
  ];
@@ -1178,15 +1446,6 @@ function applyRuntimeEnv(values) {
1178
1446
  process.env[key] = value;
1179
1447
  }
1180
1448
  }
1181
- function ensureEnvFileIgnored(envPath = getEnvPath()) {
1182
- const envFileName = envPath.split("/").pop() ?? ".memory-core.env";
1183
- const gitignorePath = join3(process.cwd(), ".gitignore");
1184
- const existing = existsSync3(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
1185
- if (!existing.includes(envFileName)) {
1186
- appendFileSync(gitignorePath, `${existing ? "\n" : ""}${envFileName}
1187
- `);
1188
- }
1189
- }
1190
1449
  function appendMissingGitignoreEntries(entries, heading) {
1191
1450
  const gitignorePath = join3(process.cwd(), ".gitignore");
1192
1451
  const existing = existsSync3(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
@@ -1251,10 +1510,10 @@ function removeProjectFiles(relativePaths) {
1251
1510
  }
1252
1511
  function normalizeProvider(value) {
1253
1512
  const provider2 = value.trim().toLowerCase();
1254
- if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax") {
1513
+ if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax" || provider2 === "openai-compatible") {
1255
1514
  return provider2;
1256
1515
  }
1257
- throw new Error(`Unsupported provider "${value}". Use: ollama, openai, anthropic, minimax`);
1516
+ throw new Error(`Unsupported provider "${value}". Use: ollama, openai, anthropic, minimax, openai-compatible`);
1258
1517
  }
1259
1518
  function providerLabel(provider2) {
1260
1519
  switch (provider2) {
@@ -1264,6 +1523,8 @@ function providerLabel(provider2) {
1264
1523
  return "Anthropic";
1265
1524
  case "minimax":
1266
1525
  return "MiniMax";
1526
+ case "openai-compatible":
1527
+ return "OpenAI-compatible";
1267
1528
  default:
1268
1529
  return "Ollama";
1269
1530
  }
@@ -1646,16 +1907,56 @@ program.command("init").description("Initialize memory-core in the current proje
1646
1907
  console.log(chalk2.bold.cyan("\n memory-core init\n"));
1647
1908
  const detected = detectProject();
1648
1909
  const quick = opts.quick ?? false;
1910
+ let skipEnv = false;
1911
+ let skipProject = false;
1912
+ if (existsSync3(join3(process.cwd(), CONFIG_FILE)) && !quick) {
1913
+ const existing = readProjectConfig();
1914
+ const envVals = readRuntimeEnv().values;
1915
+ console.log(chalk2.dim(` Already initialized: ${existing?.projectName ?? "?"} (${existing?.projectType ?? "?"})`));
1916
+ console.log(chalk2.dim(` Provider: ${envVals.CHAT_PROVIDER ?? "ollama"} Model: ${envVals.CHAT_MODEL ?? "llama3.2"}`));
1917
+ console.log(chalk2.dim(` Hook: ${existsSync3(join3(".git", "hooks", "pre-commit")) ? "installed" : "not installed"} Agents: ${existing?.agents?.length ?? 0}
1918
+ `));
1919
+ const reinitChoice = await select({
1920
+ message: "Already initialized \u2014 what do you want to do?",
1921
+ choices: [
1922
+ { value: "full", name: "Full re-init \u2014 update everything" },
1923
+ { value: "connection", name: "Connection only \u2014 update DB / provider / model" },
1924
+ { value: "project", name: "Project only \u2014 update arch, agents, hook" },
1925
+ { value: "cancel", name: "Cancel" }
1926
+ ]
1927
+ });
1928
+ if (reinitChoice === "cancel") {
1929
+ await closePool();
1930
+ process.exit(0);
1931
+ }
1932
+ skipEnv = reinitChoice === "project";
1933
+ skipProject = reinitChoice === "connection";
1934
+ }
1935
+ let pgOk = false;
1936
+ let ollamaOk = false;
1649
1937
  const envPath = join3(process.cwd(), ".memory-core.env");
1650
1938
  const hasEnv = existsSync3(envPath) || existsSync3(join3(process.cwd(), ".env")) || !!process.env.DATABASE_URL;
1651
- if (!hasEnv && quick) {
1939
+ if (skipEnv) {
1940
+ try {
1941
+ const { Pool } = (await import("pg")).default;
1942
+ const p = new Pool({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 3e3 });
1943
+ await p.query("SELECT 1");
1944
+ await p.end();
1945
+ pgOk = true;
1946
+ } catch {
1947
+ }
1948
+ try {
1949
+ const r = await fetch(`${process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3e3) });
1950
+ ollamaOk = r.ok;
1951
+ } catch {
1952
+ }
1953
+ } else if (!hasEnv && quick) {
1652
1954
  const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
1653
1955
  const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
1654
- const ollamaUrl = DEFAULT_OLLAMA_URL;
1655
1956
  const chatModel = DEFAULT_CHAT_MODEL;
1656
1957
  const envValues = {
1657
1958
  DATABASE_URL: dbUrl,
1658
- OLLAMA_URL: ollamaUrl,
1959
+ OLLAMA_URL: DEFAULT_OLLAMA_URL,
1659
1960
  OLLAMA_MODEL: DEFAULT_EMBEDDING_MODEL,
1660
1961
  CHAT_PROVIDER: "ollama",
1661
1962
  CHAT_MODEL: chatModel,
@@ -1663,7 +1964,7 @@ program.command("init").description("Initialize memory-core in the current proje
1663
1964
  };
1664
1965
  writeRuntimeEnv(envValues, envPath);
1665
1966
  applyRuntimeEnv(envValues);
1666
- ensureEnvFileIgnored(envPath);
1967
+ appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
1667
1968
  console.log(chalk2.green(" \u2713 .memory-core.env created with local defaults"));
1668
1969
  } else if (!hasEnv) {
1669
1970
  console.log(chalk2.dim(" No .memory-core.env found \u2014 let's set up your connection.\n"));
@@ -1681,6 +1982,7 @@ program.command("init").description("Initialize memory-core in the current proje
1681
1982
  await testPool.query("SELECT 1");
1682
1983
  await testPool.end();
1683
1984
  pgSpinner.succeed(chalk2.green("PostgreSQL connected"));
1985
+ pgOk = true;
1684
1986
  break;
1685
1987
  } catch (err) {
1686
1988
  pgSpinner.fail(chalk2.red(`Cannot connect: ${err.message}`));
@@ -1690,14 +1992,15 @@ program.command("init").description("Initialize memory-core in the current proje
1690
1992
  let ollamaUrl = "";
1691
1993
  while (true) {
1692
1994
  ollamaUrl = await input({
1693
- message: "Ollama URL?",
1694
- default: ollamaUrl || "http://localhost:11434"
1995
+ message: "Ollama URL (used for search embeddings)?",
1996
+ default: ollamaUrl || DEFAULT_OLLAMA_URL
1695
1997
  });
1696
1998
  const ollamaSpinner = ora(" Testing Ollama connection\u2026").start();
1697
1999
  try {
1698
2000
  const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1699
2001
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1700
2002
  ollamaSpinner.succeed(chalk2.green("Ollama connected"));
2003
+ ollamaOk = true;
1701
2004
  break;
1702
2005
  } catch (err) {
1703
2006
  ollamaSpinner.fail(chalk2.red(`Cannot reach Ollama: ${err.message}`));
@@ -1710,11 +2013,16 @@ program.command("init").description("Initialize memory-core in the current proje
1710
2013
  { name: "Local \u2014 Ollama (no API key, free)", value: "ollama" },
1711
2014
  { name: "OpenAI \u2014 gpt-4o, gpt-4o-mini", value: "openai" },
1712
2015
  { name: "Anthropic \u2014 claude-sonnet, claude-haiku", value: "anthropic" },
1713
- { name: "MiniMax \u2014 MiniMax-Text-01, abab6.5s-chat", value: "minimax" }
2016
+ { name: "MiniMax \u2014 MiniMax-Text-01, abab6.5s-chat", value: "minimax" },
2017
+ { name: "OpenAI-compatible \u2014 Groq, DeepSeek, xAI, Mistral, Together\u2026", value: "openai-compatible" }
1714
2018
  ]
1715
2019
  });
2020
+ if (chatProvider !== "ollama") {
2021
+ console.log(chalk2.dim(" Note: Ollama is still used for search embeddings. Code checking uses the cloud provider above."));
2022
+ }
1716
2023
  let chatModel = "";
1717
2024
  let chatApiKey = "";
2025
+ let chatBaseUrl = "";
1718
2026
  if (chatProvider === "ollama") {
1719
2027
  while (true) {
1720
2028
  const chatModelChoice = await select({
@@ -1767,6 +2075,13 @@ program.command("init").description("Initialize memory-core in the current proje
1767
2075
  { name: "MiniMax-Text-01 (flagship)", value: "MiniMax-Text-01" },
1768
2076
  { name: "abab6.5s-chat (fast, efficient)", value: "abab6.5s-chat" },
1769
2077
  { name: "Other (enter manually)", value: "__custom__" }
2078
+ ],
2079
+ "openai-compatible": [
2080
+ { name: "llama-3.1-70b-versatile (Groq)", value: "llama-3.1-70b-versatile" },
2081
+ { name: "deepseek-coder (DeepSeek)", value: "deepseek-coder" },
2082
+ { name: "grok-beta (xAI)", value: "grok-beta" },
2083
+ { name: "mistral-large-latest (Mistral)", value: "mistral-large-latest" },
2084
+ { name: "Other (enter manually)", value: "__custom__" }
1770
2085
  ]
1771
2086
  };
1772
2087
  const modelChoice = await select({
@@ -1774,8 +2089,14 @@ program.command("init").description("Initialize memory-core in the current proje
1774
2089
  choices: modelChoices[chatProvider]
1775
2090
  });
1776
2091
  chatModel = modelChoice === "__custom__" ? await input({ message: "Model name?" }) : modelChoice;
2092
+ if (chatProvider === "openai-compatible") {
2093
+ chatBaseUrl = await input({
2094
+ message: "API base URL?",
2095
+ default: "https://api.groq.com/openai/v1"
2096
+ });
2097
+ }
1777
2098
  chatApiKey = await input({
1778
- message: `${chatProvider.charAt(0).toUpperCase() + chatProvider.slice(1)} API key?`
2099
+ message: `${providerLabel(chatProvider)} API key?`
1779
2100
  });
1780
2101
  console.log(chalk2.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
1781
2102
  }
@@ -1787,19 +2108,34 @@ program.command("init").description("Initialize memory-core in the current proje
1787
2108
  CHAT_MODEL: chatModel
1788
2109
  };
1789
2110
  if (chatProvider === "ollama") envValues.OLLAMA_CHAT_MODEL = chatModel;
2111
+ if (chatBaseUrl) envValues.CHAT_BASE_URL = chatBaseUrl;
1790
2112
  if (chatApiKey) envValues.CHAT_API_KEY = chatApiKey;
1791
2113
  writeRuntimeEnv(envValues, envPath);
1792
2114
  applyRuntimeEnv(envValues);
1793
- ensureEnvFileIgnored(envPath);
2115
+ appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
1794
2116
  console.log(chalk2.green("\n \u2713 .memory-core.env created"));
1795
2117
  console.log(chalk2.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
2118
+ } else {
2119
+ try {
2120
+ const { Pool } = (await import("pg")).default;
2121
+ const p = new Pool({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 3e3 });
2122
+ await p.query("SELECT 1");
2123
+ await p.end();
2124
+ pgOk = true;
2125
+ } catch {
2126
+ }
2127
+ try {
2128
+ const r = await fetch(`${process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3e3) });
2129
+ ollamaOk = r.ok;
2130
+ } catch {
2131
+ }
1796
2132
  }
1797
- const projectName = quick ? process.cwd().split("/").pop() ?? "my-project" : await input({
2133
+ const projectName = quick || skipProject ? readProjectConfig()?.projectName ?? process.cwd().split("/").pop() ?? "my-project" : await input({
1798
2134
  message: "Project name?",
1799
2135
  default: process.cwd().split("/").pop() ?? "my-project"
1800
2136
  });
1801
2137
  const inferredProjectType = ["Nuxt.js"].includes(detected.framework) ? "fullstack" : ["React", "Vue.js", "Svelte"].includes(detected.framework) ? "frontend" : "backend";
1802
- const projectType = quick ? inferredProjectType : await select({
2138
+ const projectType = quick || skipProject ? readProjectConfig()?.projectType ?? inferredProjectType : await select({
1803
2139
  message: "Project type?",
1804
2140
  choices: [
1805
2141
  { value: "backend", name: "Backend \u2014 API, server, microservice" },
@@ -1809,8 +2145,8 @@ program.command("init").description("Initialize memory-core in the current proje
1809
2145
  });
1810
2146
  let backendArchitecture;
1811
2147
  if (projectType === "backend" || projectType === "fullstack") {
1812
- if (quick) {
1813
- backendArchitecture = detected.framework === "NestJS" ? "nestjs" : detected.framework === "Laravel" ? "laravel-service-repository" : detected.framework === "Go" ? "go-api" : "clean-architecture";
2148
+ if (quick || skipProject) {
2149
+ backendArchitecture = readProjectConfig()?.backendArchitecture ?? (detected.framework === "NestJS" ? "nestjs" : detected.framework === "Laravel" ? "laravel-service-repository" : detected.framework === "Go" ? "go-api" : "clean-architecture");
1814
2150
  } else {
1815
2151
  const backendProfiles = listProfiles("backend");
1816
2152
  backendArchitecture = await select({
@@ -1824,14 +2160,14 @@ program.command("init").description("Initialize memory-core in the current proje
1824
2160
  }
1825
2161
  let frontendFramework;
1826
2162
  if (projectType === "frontend" || projectType === "fullstack") {
1827
- if (quick) {
2163
+ if (quick || skipProject) {
1828
2164
  const frameworkMap = {
1829
2165
  "Nuxt.js": "nuxt",
1830
2166
  React: "react",
1831
2167
  "Vue.js": "vue",
1832
2168
  Svelte: "svelte"
1833
2169
  };
1834
- frontendFramework = frameworkMap[detected.framework] ?? "react";
2170
+ frontendFramework = readProjectConfig()?.frontendFramework ?? (frameworkMap[detected.framework] ?? "react");
1835
2171
  } else {
1836
2172
  const frontendProfiles = listProfiles("frontend");
1837
2173
  frontendFramework = await select({
@@ -1843,43 +2179,20 @@ program.command("init").description("Initialize memory-core in the current proje
1843
2179
  });
1844
2180
  }
1845
2181
  }
1846
- const language = quick ? detected.language : await input({
2182
+ const language = quick || skipProject ? readProjectConfig()?.language ?? detected.language : await input({
1847
2183
  message: "Language?",
1848
2184
  default: detected.language
1849
2185
  });
1850
- const pullMemories = quick ? true : await confirm({
1851
- message: "Pull relevant memories from previous projects?",
1852
- default: true
1853
- });
1854
- let installCaveman = quick ? false : await confirm({
1855
- message: "Install caveman token saver? Downloads and runs the upstream installer.",
1856
- default: false
1857
- });
1858
- let cavemanIntensity = "full";
1859
- if (installCaveman) {
1860
- const allowRemoteInstaller = await confirm({
1861
- message: `Security check: download and execute installer from ${CAVEMAN_INSTALL_URL}?`,
1862
- default: false
1863
- });
1864
- if (!allowRemoteInstaller) {
1865
- installCaveman = false;
1866
- }
1867
- }
1868
- if (installCaveman) {
1869
- cavemanIntensity = await select({
1870
- message: "Caveman intensity?",
1871
- choices: [
1872
- { value: "full", name: "Full \u2014 caveman mode (default)" },
1873
- { value: "lite", name: "Lite \u2014 professional terseness" },
1874
- { value: "ultra", name: "Ultra \u2014 telegraphic, minimum words" }
1875
- ]
1876
- });
1877
- }
1878
2186
  const selectedAgents = quick ? AGENT_NAMES.filter((a) => a !== "Shared") : await (async () => {
1879
2187
  const { checkbox } = await import("@inquirer/prompts");
2188
+ const saved = new Set(readProjectConfig()?.agents ?? []);
1880
2189
  return checkbox({
1881
2190
  message: "Which AI agents do you want to generate files for?",
1882
- choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({ name, value: name, checked: true })),
2191
+ choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({
2192
+ name,
2193
+ value: name,
2194
+ checked: saved.size === 0 || saved.has(name)
2195
+ })),
1883
2196
  instructions: " (Space to toggle, A to select all, Enter to confirm)"
1884
2197
  });
1885
2198
  })();
@@ -1898,6 +2211,42 @@ program.command("init").description("Initialize memory-core in the current proje
1898
2211
  ]
1899
2212
  });
1900
2213
  }
2214
+ let installCaveman = false;
2215
+ let cavemanIntensity = "full";
2216
+ if (!quick) {
2217
+ installCaveman = await confirm({
2218
+ message: "Enable caveman token saver? (compresses AI responses ~70%)",
2219
+ default: false
2220
+ });
2221
+ if (installCaveman) {
2222
+ cavemanIntensity = await select({
2223
+ message: "Intensity?",
2224
+ choices: [
2225
+ { value: "full", name: "Full \u2014 caveman mode (default)" },
2226
+ { value: "lite", name: "Lite \u2014 professional terseness" },
2227
+ { value: "ultra", name: "Ultra \u2014 telegraphic, minimum words" }
2228
+ ]
2229
+ });
2230
+ }
2231
+ }
2232
+ if (!quick) {
2233
+ const envVals = readRuntimeEnv().values;
2234
+ console.log(chalk2.bold("\n Ready to initialize\n"));
2235
+ console.log(` Project ${chalk2.white(projectName)} (${projectType})`);
2236
+ if (backendArchitecture) console.log(` Backend ${chalk2.white(backendArchitecture)}`);
2237
+ if (frontendFramework) console.log(` Frontend ${chalk2.white(frontendFramework)}`);
2238
+ console.log(` Language ${chalk2.white(language)}`);
2239
+ console.log(` Provider ${chalk2.white(envVals.CHAT_PROVIDER ?? "ollama")} / ${chalk2.white(envVals.CHAT_MODEL ?? DEFAULT_CHAT_MODEL)}`);
2240
+ console.log(` Agents ${chalk2.white(String(selectedAgents.length))} selected`);
2241
+ console.log(` Hook ${chalk2.white(enableHook ? hookAdvisory ? "advisory" : "strict" : "skip")}`);
2242
+ console.log();
2243
+ const proceed = await confirm({ message: "Generate files?", default: true });
2244
+ if (!proceed) {
2245
+ console.log(chalk2.yellow(" Cancelled.\n"));
2246
+ await closePool();
2247
+ process.exit(0);
2248
+ }
2249
+ }
1901
2250
  const config = {
1902
2251
  projectName,
1903
2252
  projectType,
@@ -1906,34 +2255,33 @@ program.command("init").description("Initialize memory-core in the current proje
1906
2255
  language,
1907
2256
  caveman: { enabled: installCaveman, intensity: cavemanIntensity },
1908
2257
  agents: selectedAgents,
1909
- allowPatterns: [],
2258
+ allowPatterns: readProjectConfig()?.allowPatterns ?? [],
2259
+ commitRules: readProjectConfig()?.commitRules,
1910
2260
  autoSync: true
1911
2261
  };
1912
2262
  let memories = [];
1913
- 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`);
2263
+ try {
2264
+ const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
2265
+ const selection = await retrieveMemorySelection({
2266
+ query: archQuery,
2267
+ cwd: process.cwd(),
2268
+ config,
2269
+ limit: 20
2270
+ });
2271
+ memories = selection.included;
2272
+ if (memories.length > 0) {
2273
+ console.log(chalk2.dim(` Found ${memories.length} relevant memories`));
1925
2274
  printMemorySelection(selection);
1926
- } catch (err) {
1927
- spinner2.warn(`Could not retrieve memories: ${err.message}`);
1928
2275
  }
2276
+ } catch {
1929
2277
  }
1930
2278
  if (installCaveman) {
1931
- const spinner2 = ora("Installing caveman token saver\u2026").start();
2279
+ const cavemanSpinner = ora("Installing caveman token saver\u2026").start();
1932
2280
  try {
1933
2281
  await installCavemanTokenSaver();
1934
- spinner2.succeed("Caveman installed");
2282
+ cavemanSpinner.succeed("Caveman installed");
1935
2283
  } catch (err) {
1936
- spinner2.warn(`Caveman install failed: ${err.message}`);
2284
+ cavemanSpinner.warn(`Caveman install failed: ${err.message}`);
1937
2285
  }
1938
2286
  }
1939
2287
  const spinner = ora("Generating AI agent context files\u2026").start();
@@ -1954,12 +2302,8 @@ program.command("init").description("Initialize memory-core in the current proje
1954
2302
  if (enableHook) {
1955
2303
  installHook(hookAdvisory);
1956
2304
  }
1957
- const 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);
2305
+ const chatModelForBanner = process.env.CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
2306
+ printBanner(config.projectName, written.written.length, { postgresOk: pgOk, ollamaOk, chatModel: chatModelForBanner });
1963
2307
  await closePool();
1964
2308
  });
1965
2309
  program.command("sync").description("Re-pull memories and regenerate AI agent files").action(async () => {
@@ -2031,6 +2375,8 @@ program.command("remember <text>").description("Save a new memory to the central
2031
2375
  context: buildMemoryContext(opts),
2032
2376
  tags: parseTags(opts.tags)
2033
2377
  });
2378
+ const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
2379
+ writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
2034
2380
  const reasonLine = chalk2.gray(`
2035
2381
  Why: ${storedReason}`);
2036
2382
  spinner.succeed(chalk2.green(`Memory saved: "${text}"`) + reasonLine);
@@ -2119,6 +2465,8 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
2119
2465
  }
2120
2466
  spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
2121
2467
  if (inserted > 0) {
2468
+ const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
2469
+ writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
2122
2470
  await autoSyncGeneratedFiles(config, "import", opts.sync);
2123
2471
  }
2124
2472
  } catch (err) {
@@ -2257,6 +2605,8 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
2257
2605
  content: pattern,
2258
2606
  tags: ["ignore"]
2259
2607
  });
2608
+ const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
2609
+ writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
2260
2610
  console.log(chalk2.green(`Ignored pattern saved: "${pattern}"`));
2261
2611
  await autoSyncGeneratedFiles(config, "ignore", opts.sync);
2262
2612
  } catch (err) {
@@ -2296,6 +2646,61 @@ program.command("allow [pattern]").description("Manage project allow patterns in
2296
2646
  }));
2297
2647
  console.log(chalk2.green(`Allow pattern saved: "${pattern}"`));
2298
2648
  });
2649
+ program.command("commit-rules [pattern]").description("Manage commit message rules in .memory-core.json").option("--message <msg>", "Error message shown when rule is violated (required when adding)").option("--negate", "Pattern must NOT match (default: must match)").option("--advisory", "Warn only \u2014 do not block commit").option("--list", "List current commit rules").option("--remove <pattern>", "Remove a commit rule by pattern").action((pattern, opts) => {
2650
+ if (opts.list) {
2651
+ const rules = readProjectConfig()?.commitRules ?? [];
2652
+ if (rules.length === 0) {
2653
+ console.log(chalk2.yellow("\n No commit rules configured.\n"));
2654
+ return;
2655
+ }
2656
+ console.log(chalk2.bold("\n Commit message rules\n"));
2657
+ rules.forEach((rule, i) => {
2658
+ const flags = [
2659
+ rule.negate ? "must NOT match" : "must match",
2660
+ rule.advisory ? "advisory" : "blocking"
2661
+ ].join(", ");
2662
+ console.log(` ${i + 1}. ${rule.pattern}`);
2663
+ console.log(chalk2.dim(` Message: ${rule.message}`));
2664
+ console.log(chalk2.dim(` Flags: ${flags}`));
2665
+ console.log();
2666
+ });
2667
+ return;
2668
+ }
2669
+ if (opts.remove) {
2670
+ updateProjectConfig((config) => ({
2671
+ ...config,
2672
+ commitRules: (config.commitRules ?? []).filter((r) => r.pattern !== opts.remove)
2673
+ }));
2674
+ console.log(chalk2.green(`Commit rule removed: "${opts.remove}"`));
2675
+ return;
2676
+ }
2677
+ if (!pattern) {
2678
+ console.error(chalk2.red("Provide a pattern, --list, or --remove <pattern>"));
2679
+ process.exit(1);
2680
+ }
2681
+ if (!opts.message) {
2682
+ console.error(chalk2.red("--message is required when adding a commit rule"));
2683
+ process.exit(1);
2684
+ }
2685
+ try {
2686
+ new RegExp(pattern);
2687
+ } catch {
2688
+ console.error(chalk2.red(`Invalid regex pattern: "${pattern}"`));
2689
+ process.exit(1);
2690
+ }
2691
+ const newRule = {
2692
+ pattern,
2693
+ message: opts.message,
2694
+ ...opts.negate && { negate: true },
2695
+ ...opts.advisory && { advisory: true }
2696
+ };
2697
+ updateProjectConfig((config) => ({
2698
+ ...config,
2699
+ commitRules: [...(config.commitRules ?? []).filter((r) => r.pattern !== pattern), newRule]
2700
+ }));
2701
+ console.log(chalk2.green(`Commit rule saved: "${pattern}"`));
2702
+ console.log(chalk2.dim(" Run: memory-core commit-rules --list to see all rules"));
2703
+ });
2299
2704
  program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
2300
2705
  const workflowPath = join3(process.cwd(), ".github", "workflows", "memory-core.yml");
2301
2706
  mkdirSync(dirname(workflowPath), { recursive: true });
@@ -2360,7 +2765,7 @@ program.command("uninstall").description("Remove memory-core from the current pr
2360
2765
  console.log(chalk2.gray(" \u2713 cleaned .gitignore memory-core block"));
2361
2766
  }
2362
2767
  });
2363
- program.command("stats").description("Show violation counters recorded by check and watch").option("--reset", "Reset violation counters and recent history").action((opts) => {
2768
+ program.command("stats").description("Show violation counters recorded by check and watch").option("--reset", "Reset violation counters and recent history").option("--tune", "Show only noisy rules (>40% false-positive rate) with disable commands").action((opts) => {
2364
2769
  const statsPath = join3(process.cwd(), ".memory-core-stats.json");
2365
2770
  if (opts.reset) {
2366
2771
  const emptyStats = {
@@ -2382,14 +2787,46 @@ program.command("stats").description("Show violation counters recorded by check
2382
2787
  return;
2383
2788
  }
2384
2789
  const stats = JSON.parse(readFileSync3(statsPath, "utf-8"));
2790
+ const toEntry = (raw) => {
2791
+ if (raw === void 0) return { count: 0, falsePositives: 0 };
2792
+ if (typeof raw === "number") return { count: raw, falsePositives: 0 };
2793
+ return raw;
2794
+ };
2385
2795
  const printTop = (label, values = {}) => {
2386
2796
  console.log(chalk2.bold(`
2387
2797
  ${label}
2388
2798
  `));
2389
- Object.entries(values).sort((a, b) => b[1] - a[1]).slice(0, 10).forEach(([name, count], index) => {
2390
- console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${count}`);
2799
+ Object.entries(values).map(([name, raw]) => ({ name, entry: toEntry(raw) })).sort((a, b) => b.entry.count - a.entry.count).slice(0, 10).forEach(({ name, entry }, index) => {
2800
+ const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
2801
+ const fpHint = rate > 0 ? chalk2.dim(` \u2014 ${rate}% false-positive rate`) + (rate > 25 ? " \u26A0\uFE0F" : "") : "";
2802
+ console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${entry.count} hits${fpHint}`);
2391
2803
  });
2392
2804
  };
2805
+ if (opts.tune) {
2806
+ const tuneThreshold = 40;
2807
+ const tuneMinCount = 5;
2808
+ const noisy = Object.entries(stats.rules ?? {}).map(([name, raw]) => ({ name, entry: toEntry(raw) })).map(({ name, entry }) => ({
2809
+ name,
2810
+ count: entry.count,
2811
+ rate: entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0
2812
+ })).filter((r) => r.rate > tuneThreshold && r.count >= tuneMinCount).sort((a, b) => b.rate - a.rate);
2813
+ if (noisy.length === 0) {
2814
+ console.log(chalk2.green(`
2815
+ \u2713 No noisy rules found (threshold: ${tuneThreshold}%, min hits: ${tuneMinCount})
2816
+ `));
2817
+ return;
2818
+ }
2819
+ console.log(chalk2.bold(`
2820
+ Noisy rules (>${tuneThreshold}% false-positive rate, \u2265${tuneMinCount} hits)
2821
+ `));
2822
+ noisy.forEach(({ name, count, rate }, i) => {
2823
+ console.log(` ${i + 1}. ${truncate(name, 50).padEnd(52)} ${count} hits \u2014 ${rate}% \u26A0\uFE0F`);
2824
+ console.log(chalk2.dim(` To disable: memory-core allow "${name}"`));
2825
+ console.log(chalk2.dim(` Interactive: memory-core tune`));
2826
+ console.log();
2827
+ });
2828
+ return;
2829
+ }
2393
2830
  const liveRules = stats.live?.rules ?? {};
2394
2831
  const liveFiles = stats.live?.files ?? {};
2395
2832
  const hasLiveState = !!stats.live;
@@ -2413,6 +2850,87 @@ program.command("stats").description("Show violation counters recorded by check
2413
2850
  }
2414
2851
  }
2415
2852
  });
2853
+ program.command("tune").description("Review and disable noisy rules with high false-positive rates").option("--threshold <percent>", "False-positive rate % above which a rule is noisy (default: 40)", "40").option("--min-count <n>", "Minimum hit count required to consider a rule (default: 5)", "5").option("--yes", "Disable all noisy rules without prompting").action(async (opts) => {
2854
+ const statsPath = join3(process.cwd(), ".memory-core-stats.json");
2855
+ if (!existsSync3(statsPath)) {
2856
+ console.log(chalk2.yellow("\n No violation stats yet. Run some commits first.\n"));
2857
+ return;
2858
+ }
2859
+ const threshold = Math.max(0, Math.min(100, parseInt(opts.threshold, 10) || 40));
2860
+ const minCount = Math.max(1, parseInt(opts.minCount, 10) || 5);
2861
+ const toEntry = (raw) => {
2862
+ if (raw === void 0) return { count: 0, falsePositives: 0 };
2863
+ if (typeof raw === "number") return { count: raw, falsePositives: 0 };
2864
+ return raw;
2865
+ };
2866
+ const stats = JSON.parse(readFileSync3(statsPath, "utf-8"));
2867
+ const noisy = Object.entries(stats.rules ?? {}).map(([rule, raw]) => {
2868
+ const entry = toEntry(raw);
2869
+ const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
2870
+ return { rule, count: entry.count, rate };
2871
+ }).filter((r) => r.rate > threshold && r.count >= minCount).sort((a, b) => b.rate - a.rate);
2872
+ if (noisy.length === 0) {
2873
+ console.log(chalk2.green(`
2874
+ \u2713 All rules within acceptable noise (threshold: ${threshold}%, min hits: ${minCount})
2875
+ `));
2876
+ return;
2877
+ }
2878
+ console.log(chalk2.bold(`
2879
+ Found ${noisy.length} noisy rule${noisy.length > 1 ? "s" : ""} (>${threshold}% false-positive rate, \u2265${minCount} hits)
2880
+ `));
2881
+ const existingAllows = new Set(
2882
+ (readProjectConfig()?.allowPatterns ?? []).map((p) => p.toLowerCase())
2883
+ );
2884
+ const toAdd = /* @__PURE__ */ new Set();
2885
+ if (opts.yes) {
2886
+ for (const { rule, count, rate } of noisy) {
2887
+ const key = rule.toLowerCase();
2888
+ if (existingAllows.has(key)) {
2889
+ console.log(chalk2.dim(` \u2022 "${truncate(rule, 56)}" \u2014 already disabled`));
2890
+ continue;
2891
+ }
2892
+ toAdd.add(key);
2893
+ console.log(chalk2.green(` \u2713 "${truncate(rule, 56)}"`) + chalk2.dim(` \u2014 ${count} hits, ${rate}% FP rate`));
2894
+ }
2895
+ } else {
2896
+ const { select: select2 } = await import("@inquirer/prompts");
2897
+ for (let i = 0; i < noisy.length; i++) {
2898
+ const { rule, count, rate } = noisy[i];
2899
+ const key = rule.toLowerCase();
2900
+ console.log(chalk2.bold(`
2901
+ [${i + 1}/${noisy.length}] "${truncate(rule, 60)}"`));
2902
+ console.log(chalk2.dim(` ${count} hits \u2014 ${rate}% false-positive rate \u26A0\uFE0F`));
2903
+ if (existingAllows.has(key)) {
2904
+ console.log(chalk2.dim(" Already in allow patterns \u2014 skipping"));
2905
+ continue;
2906
+ }
2907
+ const choice = await select2({
2908
+ message: "What would you like to do?",
2909
+ choices: [
2910
+ { name: "Disable this rule (add to allow patterns)", value: "disable" },
2911
+ { name: "Keep it (skip for now)", value: "keep" },
2912
+ { name: "Quit tuning", value: "quit" }
2913
+ ]
2914
+ });
2915
+ if (choice === "quit") break;
2916
+ if (choice === "disable") {
2917
+ toAdd.add(key);
2918
+ console.log(chalk2.green(" \u2713 Marked for disable"));
2919
+ }
2920
+ }
2921
+ }
2922
+ if (toAdd.size > 0) {
2923
+ updateProjectConfig((config) => ({
2924
+ ...config,
2925
+ allowPatterns: [.../* @__PURE__ */ new Set([...config.allowPatterns ?? [], ...toAdd])]
2926
+ }));
2927
+ console.log(chalk2.green(`
2928
+ \u2713 Saved ${toAdd.size} allow pattern${toAdd.size > 1 ? "s" : ""} to .memory-core.json`));
2929
+ console.log(chalk2.dim(" These rules will no longer block commits.\n"));
2930
+ } else {
2931
+ console.log(chalk2.dim("\n No changes made.\n"));
2932
+ }
2933
+ });
2416
2934
  program.command("dashboard").description("Start the live Svelte dashboard with WebSocket watch events").option("-p, --port <port>", "Dashboard port", "5178").option("--path <dir>", "Directory to watch (default: current directory)").option("--no-watch", "Serve the dashboard without starting file watch").action(async (opts) => {
2417
2935
  const resolveDashboardPath = () => {
2418
2936
  if (typeof opts.path === "string" && opts.path.trim().length > 0) return opts.path;
@@ -2430,7 +2948,7 @@ program.command("dashboard").description("Start the live Svelte dashboard with W
2430
2948
  }
2431
2949
  return void 0;
2432
2950
  };
2433
- const { startDashboard } = await import("./dashboard-server-4WOUQTJN.js");
2951
+ const { startDashboard } = await import("./dashboard-server-ZERRHWQS.js");
2434
2952
  await startDashboard({
2435
2953
  port: parseInt(opts.port, 10),
2436
2954
  path: resolveDashboardPath(),
@@ -2476,6 +2994,10 @@ program.command("seed").description("Load all predefined memories into the datab
2476
2994
  skipped++;
2477
2995
  }
2478
2996
  }
2997
+ if (saved > 0) {
2998
+ const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
2999
+ writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
3000
+ }
2479
3001
  console.log(chalk2.bold.green(`
2480
3002
  Done. ${saved} memories seeded, ${skipped} skipped.
2481
3003
  `));
@@ -2593,7 +3115,7 @@ read:
2593
3115
  await closePool();
2594
3116
  });
2595
3117
  var provider = program.command("provider").description("Manage the code-checking provider configuration");
2596
- provider.command("set <name>").description("Set the code-checking provider (ollama, openai, anthropic, minimax)").option("--model <model>", "Chat model to set alongside the provider").option("--api-key <key>", "API key for cloud providers").action(async (name, opts) => {
3118
+ provider.command("set <name>").description("Set the code-checking provider (ollama, openai, anthropic, minimax, openai-compatible)").option("--model <model>", "Chat model to set alongside the provider").option("--api-key <key>", "API key for cloud providers").option("--base-url <url>", "API base URL for openai-compatible providers").action(async (name, opts) => {
2597
3119
  try {
2598
3120
  const providerName = normalizeProvider(name);
2599
3121
  const runtimeEnv = readRuntimeEnv();
@@ -2611,6 +3133,7 @@ provider.command("set <name>").description("Set the code-checking provider (olla
2611
3133
  values.CHAT_MODEL = model2;
2612
3134
  values.OLLAMA_CHAT_MODEL = model2;
2613
3135
  delete values.CHAT_API_KEY;
3136
+ delete values.CHAT_BASE_URL;
2614
3137
  } else {
2615
3138
  delete values.OLLAMA_CHAT_MODEL;
2616
3139
  if (opts.apiKey) {
@@ -2620,10 +3143,20 @@ provider.command("set <name>").description("Set the code-checking provider (olla
2620
3143
  message: `${providerLabel(providerName)} API key?`
2621
3144
  });
2622
3145
  }
3146
+ if (providerName === "openai-compatible") {
3147
+ if (opts.baseUrl) {
3148
+ values.CHAT_BASE_URL = opts.baseUrl;
3149
+ } else if (!values.CHAT_BASE_URL) {
3150
+ values.CHAT_BASE_URL = await input({
3151
+ message: "API base URL?",
3152
+ default: "https://api.groq.com/openai/v1"
3153
+ });
3154
+ }
3155
+ }
2623
3156
  }
2624
3157
  writeRuntimeEnv(values, runtimeEnv.envPath);
2625
3158
  applyRuntimeEnv(values);
2626
- ensureEnvFileIgnored(runtimeEnv.envPath);
3159
+ appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
2627
3160
  console.log(chalk2.green(`Updated provider: ${providerName}`));
2628
3161
  console.log(chalk2.gray(` Chat model: ${getConfiguredChatModel(values)}`));
2629
3162
  } catch (err) {
@@ -2648,7 +3181,7 @@ model.command("set <name>").description("Set the chat model used for code checki
2648
3181
  values.OLLAMA_MODEL = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
2649
3182
  writeRuntimeEnv(values, runtimeEnv.envPath);
2650
3183
  applyRuntimeEnv(values);
2651
- ensureEnvFileIgnored(runtimeEnv.envPath);
3184
+ appendMissingGitignoreEntries(LOCAL_STATE_FILES, GITIGNORE_HEADING);
2652
3185
  console.log(chalk2.green(`Updated ${opts.embedding ? "embedding" : "chat"} model: ${name}`));
2653
3186
  console.log(chalk2.gray(` Provider: ${providerName}`));
2654
3187
  } catch (err) {
@@ -2834,7 +3367,13 @@ hook.command("install").description("Install pre-commit hook (advisory mode by d
2834
3367
  hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
2835
3368
  uninstallHook();
2836
3369
  });
2837
- program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--ci", `Check CI diff using ${MEMORY_FILE}`).option("--all", "Check all tracked source files, including already-committed files").option("--path <dir>", "Directory to check for --all mode (default: current directory)").option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").option("--fast", "Skip AI and memory retrieval; run deterministic checks only").action(async (opts) => {
3370
+ program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--ci", `Check CI diff using ${MEMORY_FILE}`).option("--all", "Check all tracked source files, including already-committed files").option("--path <dir>", "Directory to check for --all mode (default: current directory)").option("--commit-msg [file]", "Check commit message (defaults to .git/COMMIT_EDITMSG)").option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").option("--fast", "Skip AI and memory retrieval; run deterministic checks only").action(async (opts) => {
3371
+ if (opts.commitMsg !== void 0) {
3372
+ const msgFile = typeof opts.commitMsg === "string" ? opts.commitMsg : join3(process.cwd(), ".git", "COMMIT_EDITMSG");
3373
+ await checkCommitMsg(msgFile, { verbose: opts.verbose ?? false, debug: opts.debug ?? false });
3374
+ await closePool();
3375
+ return;
3376
+ }
2838
3377
  if (opts.ci && opts.all) {
2839
3378
  console.error(chalk2.red("\n Choose one mode: --ci or --all.\n"));
2840
3379
  process.exit(1);