@shahmilsaari/memory-core 1.0.13 → 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-PRRVI3YM.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,8 +122,14 @@ 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
- function buildHookBody(advisory) {
125
+ var COMMIT_MSG_HOOK_PATH = join2(".git", "hooks", "commit-msg");
126
+ var COMMIT_MSG_HOOK_MARKER = "# archmind-memory-core commit-msg";
127
+ var RULE_CACHE_FILE = ".memory-core-rules-cache.json";
128
+ var DB_VERSION_FILE = ".memory-core-db-version";
129
+ var RULE_CACHE_TTL_MS = 5 * 60 * 1e3;
130
+ function buildHookBody(advisory, fast = false) {
126
131
  const suffix = advisory ? " || true" : "";
132
+ const checkArgs = fast ? "check --staged --fast" : "check --staged";
127
133
  return `${HOOK_MARKER}${advisory ? " advisory" : ""}
128
134
  if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
129
135
  exit 0
@@ -135,20 +141,20 @@ if [ -n "\${SKIP_HOOKS:-}" ]; then
135
141
  exit 0
136
142
  fi
137
143
  if command -v memory-core >/dev/null 2>&1; then
138
- memory-core check --staged${suffix}
144
+ memory-core ${checkArgs}${suffix}
139
145
  elif [ -f "./node_modules/.bin/memory-core" ]; then
140
- ./node_modules/.bin/memory-core check --staged${suffix}
146
+ ./node_modules/.bin/memory-core ${checkArgs}${suffix}
141
147
  elif [ -f "./dist/cli.js" ]; then
142
- node ./dist/cli.js check --staged${suffix}
148
+ node ./dist/cli.js ${checkArgs}${suffix}
143
149
  else
144
- npx --no-install memory-core check --staged 2>/dev/null || exit 0
150
+ npx --no-install memory-core ${checkArgs} 2>/dev/null || exit 0
145
151
  fi
146
152
  `;
147
153
  }
148
- function buildHookScript(advisory) {
154
+ function buildHookScript(advisory, fast = false) {
149
155
  return `#!/bin/sh
150
156
 
151
- ${buildHookBody(advisory)}`;
157
+ ${buildHookBody(advisory, fast)}`;
152
158
  }
153
159
  function normalizeHookPreamble(content) {
154
160
  const lines = content.split("\n");
@@ -165,6 +171,31 @@ function normalizeHookPreamble(content) {
165
171
  }
166
172
  return normalized.join("\n").replace(/\n{3,}/g, "\n\n").trim();
167
173
  }
174
+ function toRuleStatEntry(raw) {
175
+ if (raw === void 0) return { count: 0, falsePositives: 0 };
176
+ if (typeof raw === "number") return { count: raw, falsePositives: 0 };
177
+ return raw;
178
+ }
179
+ function readPositiveIntEnv(name, fallback) {
180
+ const raw = Number(process.env[name]);
181
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
182
+ }
183
+ function isFastCheck(options) {
184
+ return options.fast === true || process.env.MEMORY_CORE_CHECK_FAST === "1";
185
+ }
186
+ async function withTimeout(promise, timeoutMs, fallback) {
187
+ let timer;
188
+ try {
189
+ return await Promise.race([
190
+ promise,
191
+ new Promise((resolve2) => {
192
+ timer = setTimeout(() => resolve2(fallback), timeoutMs);
193
+ })
194
+ ]);
195
+ } finally {
196
+ if (timer) clearTimeout(timer);
197
+ }
198
+ }
168
199
  function recordViolations(violations, source = "hook") {
169
200
  const statsPath = join2(process.cwd(), ".memory-core-stats.json");
170
201
  let stats = { rules: {}, files: {} };
@@ -178,7 +209,8 @@ function recordViolations(violations, source = "hook") {
178
209
  stats.rules ??= {};
179
210
  stats.files ??= {};
180
211
  for (const violation of violations) {
181
- 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 };
182
214
  if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
183
215
  }
184
216
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -219,11 +251,12 @@ async function promptToSaveViolations(violations) {
219
251
  message: "Why should this rule exist?",
220
252
  default: selected.reason ?? selected.issue ?? ""
221
253
  });
254
+ const storedReason = reason.trim() || selected.reason || selected.issue || `Captured from violation: ${selected.rule}`;
222
255
  await app.services.memoryEngine.remember({
223
256
  type: "rule",
224
257
  scope: "project",
225
258
  content: selected.rule,
226
- reason: reason || void 0,
259
+ reason: storedReason,
227
260
  tags: ["violation"]
228
261
  });
229
262
  console.log(chalk.green(" \u2713 Saved as project rule. Run memory-core sync to propagate it.\n"));
@@ -232,6 +265,40 @@ async function promptToSaveViolations(violations) {
232
265
  `));
233
266
  }
234
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
+ }
235
302
  async function loadIgnorePatterns() {
236
303
  try {
237
304
  const app = getDefaultApplicationContainer();
@@ -465,6 +532,28 @@ function loadRecentViolationsFromStats(cwd = process.cwd()) {
465
532
  return [];
466
533
  }
467
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
+ }
468
557
  async function learnGlobalIgnoresFromFalsePositives(options) {
469
558
  if (options.currentViolations.length === 0) return [];
470
559
  const recentViolations = loadRecentViolationsFromStats();
@@ -493,10 +582,11 @@ ${JSON.stringify(options.allowPatterns, null, 2)}`;
493
582
  console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
494
583
  }
495
584
  try {
585
+ const recheckTimeoutMs = readPositiveIntEnv("MEMORY_CORE_FALSE_POSITIVE_TIMEOUT_MS", 6e3);
496
586
  const raw = await callChatModel([
497
587
  { role: "system", content: systemPrompt },
498
588
  { role: "user", content: userPrompt }
499
- ]);
589
+ ], { timeoutMs: recheckTimeoutMs });
500
590
  const parsed = parseFalsePositiveDecisions(raw);
501
591
  if (!parsed.valid) return [];
502
592
  const existing = new Set(options.allowPatterns.map((pattern) => pattern.toLowerCase()));
@@ -522,6 +612,9 @@ ${JSON.stringify(options.allowPatterns, null, 2)}`;
522
612
  } catch {
523
613
  }
524
614
  }
615
+ if (inserted.length > 0) {
616
+ incrementFalsePositivesForPatterns(inserted, options.currentViolations);
617
+ }
525
618
  return inserted;
526
619
  } catch {
527
620
  return [];
@@ -606,6 +699,32 @@ function findDeterministicViolations(diff, rules, avoids, allowPatterns = []) {
606
699
  }
607
700
  return dedupeViolations(violations);
608
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
+ }
609
728
  function filterModelViolationsByStagedDiff(violations, stagedFiles, diff) {
610
729
  if (violations.length === 0) return violations;
611
730
  const changedFiles = new Set(stagedFiles.map((file) => normalizePath(file)));
@@ -637,13 +756,13 @@ function filterModelViolationsByStagedDiff(violations, stagedFiles, diff) {
637
756
  }
638
757
  return filtered;
639
758
  }
640
- function installHook(advisory = true) {
759
+ function installHook(advisory = true, fast = false) {
641
760
  if (!existsSync2(".git")) {
642
761
  console.error(chalk.red("\n Not a git repository. Run from project root.\n"));
643
762
  process.exit(1);
644
763
  }
645
- const script = buildHookScript(advisory);
646
- const body = buildHookBody(advisory).trimEnd();
764
+ const script = buildHookScript(advisory, fast);
765
+ const body = buildHookBody(advisory, fast).trimEnd();
647
766
  if (existsSync2(HOOK_PATH)) {
648
767
  const existing = readFileSync2(HOOK_PATH, "utf-8");
649
768
  if (existing.includes(HOOK_MARKER)) {
@@ -658,8 +777,10 @@ ${preamble}`;
658
777
  ${body}
659
778
  `);
660
779
  chmodSync(HOOK_PATH, 493);
780
+ installCommitMsgHook(advisory);
661
781
  const modeLabel2 = advisory ? chalk.cyan("advisory") : chalk.yellow("strict");
662
782
  console.log(chalk.green("\n \u2713 Pre-commit hook updated") + chalk.dim(` (${modeLabel2} mode)`));
783
+ if (fast) console.log(chalk.gray(` Check mode: fast deterministic checks`));
663
784
  return;
664
785
  }
665
786
  writeFileSync2(HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
@@ -667,9 +788,11 @@ ${body}
667
788
  writeFileSync2(HOOK_PATH, script);
668
789
  }
669
790
  chmodSync(HOOK_PATH, 493);
791
+ installCommitMsgHook(advisory);
670
792
  const modeLabel = advisory ? "advisory (logs violations, never blocks)" : "strict (blocks commits on violations)";
671
793
  console.log(chalk.green("\n \u2713 Pre-commit hook installed") + chalk.dim(` \u2014 ${modeLabel}`));
672
- console.log(chalk.gray(` Chat model: ${process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"}`));
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"));
673
796
  console.log(chalk.gray(" To uninstall: memory-core hook uninstall\n"));
674
797
  }
675
798
  function uninstallHook() {
@@ -690,8 +813,118 @@ function uninstallHook() {
690
813
  } else {
691
814
  unlinkSync(HOOK_PATH);
692
815
  }
816
+ uninstallCommitMsgHook();
693
817
  console.log(chalk.green("\n \u2713 Pre-commit hook removed\n"));
694
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
+ }
695
928
  async function checkStaged(options = {}) {
696
929
  const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
697
930
  let diff;
@@ -720,17 +953,45 @@ async function checkStaged(options = {}) {
720
953
  if (!existsSync2(configPath)) return;
721
954
  const config = JSON.parse(readFileSync2(configPath, "utf-8"));
722
955
  const { rules: fallbackRules, avoids } = getProfileRules(config);
723
- const [rules, ignores] = await Promise.all([
724
- loadRelevantRules(config, diff, stagedFiles, fallbackRules),
725
- loadIgnorePatterns()
726
- ]);
727
- const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...ignores])];
956
+ const fast = isFastCheck(options);
957
+ const ruleLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_RULE_LOAD_TIMEOUT_MS", 2e3);
958
+ const ignoreLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_IGNORE_LOAD_TIMEOUT_MS", 1500);
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
+ }
728
987
  if (rules.length === 0) return;
729
- const modelInput = buildModelInputFromDiff(diff, 8e3);
988
+ const modelInputMaxChars = readPositiveIntEnv("MEMORY_CORE_MODEL_INPUT_MAX_CHARS", 8e3);
989
+ const modelInput = buildModelInputFromDiff(diff, modelInputMaxChars);
730
990
  console.log(chalk.cyan("\n archmind \u2014 checking staged changes against rules\u2026"));
731
991
  if (options.verbose || options.debug) {
732
992
  const sourceLabel = modelInput.source === "added-lines" ? "added lines" : "diff";
733
- console.log(chalk.gray(` model: ${getChatProviderLabel()} rules: ${rules.length} diff: ${diff.length} chars input: ${sourceLabel}${modelInput.truncated ? " (truncated)" : ""}`));
993
+ const modelLabel = fast ? "skipped (--fast)" : getChatProviderLabel();
994
+ console.log(chalk.gray(` model: ${modelLabel} rules: ${rules.length} diff: ${diff.length} chars input: ${sourceLabel}${modelInput.truncated ? " (truncated)" : ""}`));
734
995
  }
735
996
  const rulesWithReasons = rules.map((r, i) => {
736
997
  const why = reasonMap.get(r);
@@ -773,13 +1034,19 @@ Do not include any text outside the JSON object.`;
773
1034
  reasonLookup: reasonMap
774
1035
  });
775
1036
  let modelViolations = [];
776
- try {
1037
+ let aiFallback = fast;
1038
+ if (fast) {
1039
+ if (options.verbose || options.debug) {
1040
+ console.log(chalk.gray(" AI check skipped; running deterministic checks only."));
1041
+ }
1042
+ } else try {
1043
+ const checkTimeoutMs = readPositiveIntEnv("MEMORY_CORE_CHECK_TIMEOUT_MS", readPositiveIntEnv("CHAT_TIMEOUT_MS", 2e4));
777
1044
  const raw = await callChatModel([
778
1045
  { role: "system", content: systemPrompt },
779
1046
  { role: "user", content: `Review these staged changes:
780
1047
 
781
1048
  ${modelInput.text}` }
782
- ]);
1049
+ ], { timeoutMs: checkTimeoutMs });
783
1050
  if (options.verbose || options.debug) {
784
1051
  console.log(chalk.gray(` raw response: ${options.debug ? raw : raw.slice(0, 200)}`));
785
1052
  }
@@ -792,15 +1059,25 @@ ${modelInput.text}` }
792
1059
  } catch (err) {
793
1060
  if (err.message?.startsWith("MODEL_NOT_FOUND:")) {
794
1061
  printModelMissing(err.message.split(":")[1]);
1062
+ aiFallback = true;
1063
+ modelViolations = [];
1064
+ } else if (err.message?.startsWith("TIMEOUT:")) {
1065
+ const timeoutMs = err.message.split(":")[1];
1066
+ console.log(chalk.yellow(`
1067
+ \u26A0 AI check timed out after ${timeoutMs}ms \u2014 switching to fast deterministic checks for this run.`));
1068
+ console.log(chalk.gray(" Set MEMORY_CORE_CHECK_TIMEOUT_MS to tune this.\n"));
1069
+ aiFallback = true;
795
1070
  modelViolations = [];
796
1071
  } else if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
797
1072
  console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 using deterministic checks only."));
798
1073
  console.log(chalk.gray(" Start it: ollama serve\n"));
1074
+ aiFallback = true;
799
1075
  modelViolations = [];
800
1076
  } else {
801
1077
  console.log(chalk.yellow(`
802
1078
  \u26A0 AI rule check failed: ${err.message}`));
803
1079
  console.log(chalk.gray(" Using deterministic checks only.\n"));
1080
+ aiFallback = true;
804
1081
  modelViolations = [];
805
1082
  }
806
1083
  }
@@ -808,6 +1085,17 @@ ${modelInput.text}` }
808
1085
  let violations = dedupeViolations([...deterministicViolations, ...astViolations, ...modelViolations]);
809
1086
  violations = applyAllowPatterns(violations, allowPatterns);
810
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
+ }
1098
+ if (!aiFallback && violations.length > 0) {
811
1099
  const learnedPatterns = await learnGlobalIgnoresFromFalsePositives({
812
1100
  diff,
813
1101
  currentViolations: violations,
@@ -834,16 +1122,66 @@ ${modelInput.text}` }
834
1122
  `
835
1123
  )
836
1124
  );
837
- violations.forEach((v, i) => {
838
- const loc = v.file ? v.line ? `${v.file}:${v.line}` : v.file : "unknown location";
839
- console.log(chalk.bold(` [${i + 1}] ${loc}`));
840
- console.log(chalk.yellow(" Rule: ") + v.rule);
841
- 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);
842
1153
  if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
843
- if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
844
- 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
+ }
845
1183
  console.log();
846
- });
1184
+ }
847
1185
  console.log(chalk.dim(" Fix the violations above, then commit again."));
848
1186
  console.log(chalk.dim(" To bypass (not recommended): git commit --no-verify"));
849
1187
  console.log(chalk.dim(" Env bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
@@ -1031,35 +1369,6 @@ function printBanner(projectName, agentCount, status) {
1031
1369
  ];
1032
1370
  lines.forEach((l) => console.log(l));
1033
1371
  }
1034
- async function checkConnections(dbUrl, ollamaUrl, chatModel) {
1035
- const spinner = ora("Checking connections\u2026").start();
1036
- let postgresOk = false;
1037
- let ollamaOk = false;
1038
- try {
1039
- const { Pool } = (await import("pg")).default;
1040
- const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
1041
- await testPool.query("SELECT 1");
1042
- await testPool.end();
1043
- postgresOk = true;
1044
- } catch {
1045
- postgresOk = false;
1046
- }
1047
- try {
1048
- const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1049
- ollamaOk = res.ok;
1050
- } catch {
1051
- ollamaOk = false;
1052
- }
1053
- spinner.stop();
1054
- console.log(
1055
- 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.")
1056
- );
1057
- console.log(
1058
- ollamaOk ? chalk2.green(" \u2713 Ollama ") + chalk2.dim(` \u2014 connected (${chatModel})`) : chalk2.red(" \u2717 Ollama ") + chalk2.dim(" \u2014 not reachable. Run: ollama serve")
1059
- );
1060
- console.log();
1061
- return { postgresOk, ollamaOk, chatModel };
1062
- }
1063
1372
  var { version } = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf-8"));
1064
1373
  var CONFIG_FILE = ".memory-core.json";
1065
1374
  var LOCAL_GENERATED_FILES = [".memory-core-stats.json"];
@@ -1108,6 +1417,7 @@ function writeRuntimeEnv(values, envPath = getEnvPath()) {
1108
1417
  "OLLAMA_MODEL",
1109
1418
  "CHAT_PROVIDER",
1110
1419
  "CHAT_MODEL",
1420
+ "CHAT_BASE_URL",
1111
1421
  "OLLAMA_CHAT_MODEL",
1112
1422
  "CHAT_API_KEY"
1113
1423
  ];
@@ -1206,10 +1516,10 @@ function removeProjectFiles(relativePaths) {
1206
1516
  }
1207
1517
  function normalizeProvider(value) {
1208
1518
  const provider2 = value.trim().toLowerCase();
1209
- if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax") {
1519
+ if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax" || provider2 === "openai-compatible") {
1210
1520
  return provider2;
1211
1521
  }
1212
- 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`);
1213
1523
  }
1214
1524
  function providerLabel(provider2) {
1215
1525
  switch (provider2) {
@@ -1219,6 +1529,8 @@ function providerLabel(provider2) {
1219
1529
  return "Anthropic";
1220
1530
  case "minimax":
1221
1531
  return "MiniMax";
1532
+ case "openai-compatible":
1533
+ return "OpenAI-compatible";
1222
1534
  default:
1223
1535
  return "Ollama";
1224
1536
  }
@@ -1305,6 +1617,11 @@ function buildMemoryContext(opts) {
1305
1617
  if (opts.source?.trim()) context.source = opts.source.trim();
1306
1618
  return Object.keys(context).length ? context : void 0;
1307
1619
  }
1620
+ function memoryReasonOrFallback(reason, content, type = "memory") {
1621
+ const trimmed = reason?.trim();
1622
+ if (trimmed) return trimmed;
1623
+ return `Captured as a ${type} memory because it should be remembered: ${content}`;
1624
+ }
1308
1625
  function truncate(value, length) {
1309
1626
  if (!value) return "";
1310
1627
  return value.length > length ? `${value.slice(0, Math.max(0, length - 1))}\u2026` : value;
@@ -1596,16 +1913,56 @@ program.command("init").description("Initialize memory-core in the current proje
1596
1913
  console.log(chalk2.bold.cyan("\n memory-core init\n"));
1597
1914
  const detected = detectProject();
1598
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;
1599
1943
  const envPath = join3(process.cwd(), ".memory-core.env");
1600
1944
  const hasEnv = existsSync3(envPath) || existsSync3(join3(process.cwd(), ".env")) || !!process.env.DATABASE_URL;
1601
- 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) {
1602
1960
  const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
1603
1961
  const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
1604
- const ollamaUrl = DEFAULT_OLLAMA_URL;
1605
1962
  const chatModel = DEFAULT_CHAT_MODEL;
1606
1963
  const envValues = {
1607
1964
  DATABASE_URL: dbUrl,
1608
- OLLAMA_URL: ollamaUrl,
1965
+ OLLAMA_URL: DEFAULT_OLLAMA_URL,
1609
1966
  OLLAMA_MODEL: DEFAULT_EMBEDDING_MODEL,
1610
1967
  CHAT_PROVIDER: "ollama",
1611
1968
  CHAT_MODEL: chatModel,
@@ -1631,6 +1988,7 @@ program.command("init").description("Initialize memory-core in the current proje
1631
1988
  await testPool.query("SELECT 1");
1632
1989
  await testPool.end();
1633
1990
  pgSpinner.succeed(chalk2.green("PostgreSQL connected"));
1991
+ pgOk = true;
1634
1992
  break;
1635
1993
  } catch (err) {
1636
1994
  pgSpinner.fail(chalk2.red(`Cannot connect: ${err.message}`));
@@ -1640,14 +1998,15 @@ program.command("init").description("Initialize memory-core in the current proje
1640
1998
  let ollamaUrl = "";
1641
1999
  while (true) {
1642
2000
  ollamaUrl = await input({
1643
- message: "Ollama URL?",
1644
- default: ollamaUrl || "http://localhost:11434"
2001
+ message: "Ollama URL (used for search embeddings)?",
2002
+ default: ollamaUrl || DEFAULT_OLLAMA_URL
1645
2003
  });
1646
2004
  const ollamaSpinner = ora(" Testing Ollama connection\u2026").start();
1647
2005
  try {
1648
2006
  const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1649
2007
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1650
2008
  ollamaSpinner.succeed(chalk2.green("Ollama connected"));
2009
+ ollamaOk = true;
1651
2010
  break;
1652
2011
  } catch (err) {
1653
2012
  ollamaSpinner.fail(chalk2.red(`Cannot reach Ollama: ${err.message}`));
@@ -1660,11 +2019,16 @@ program.command("init").description("Initialize memory-core in the current proje
1660
2019
  { name: "Local \u2014 Ollama (no API key, free)", value: "ollama" },
1661
2020
  { name: "OpenAI \u2014 gpt-4o, gpt-4o-mini", value: "openai" },
1662
2021
  { name: "Anthropic \u2014 claude-sonnet, claude-haiku", value: "anthropic" },
1663
- { 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" }
1664
2024
  ]
1665
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
+ }
1666
2029
  let chatModel = "";
1667
2030
  let chatApiKey = "";
2031
+ let chatBaseUrl = "";
1668
2032
  if (chatProvider === "ollama") {
1669
2033
  while (true) {
1670
2034
  const chatModelChoice = await select({
@@ -1717,6 +2081,13 @@ program.command("init").description("Initialize memory-core in the current proje
1717
2081
  { name: "MiniMax-Text-01 (flagship)", value: "MiniMax-Text-01" },
1718
2082
  { name: "abab6.5s-chat (fast, efficient)", value: "abab6.5s-chat" },
1719
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__" }
1720
2091
  ]
1721
2092
  };
1722
2093
  const modelChoice = await select({
@@ -1724,8 +2095,14 @@ program.command("init").description("Initialize memory-core in the current proje
1724
2095
  choices: modelChoices[chatProvider]
1725
2096
  });
1726
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
+ }
1727
2104
  chatApiKey = await input({
1728
- message: `${chatProvider.charAt(0).toUpperCase() + chatProvider.slice(1)} API key?`
2105
+ message: `${providerLabel(chatProvider)} API key?`
1729
2106
  });
1730
2107
  console.log(chalk2.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
1731
2108
  }
@@ -1737,19 +2114,34 @@ program.command("init").description("Initialize memory-core in the current proje
1737
2114
  CHAT_MODEL: chatModel
1738
2115
  };
1739
2116
  if (chatProvider === "ollama") envValues.OLLAMA_CHAT_MODEL = chatModel;
2117
+ if (chatBaseUrl) envValues.CHAT_BASE_URL = chatBaseUrl;
1740
2118
  if (chatApiKey) envValues.CHAT_API_KEY = chatApiKey;
1741
2119
  writeRuntimeEnv(envValues, envPath);
1742
2120
  applyRuntimeEnv(envValues);
1743
2121
  ensureEnvFileIgnored(envPath);
1744
2122
  console.log(chalk2.green("\n \u2713 .memory-core.env created"));
1745
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
+ }
1746
2138
  }
1747
- 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({
1748
2140
  message: "Project name?",
1749
2141
  default: process.cwd().split("/").pop() ?? "my-project"
1750
2142
  });
1751
2143
  const inferredProjectType = ["Nuxt.js"].includes(detected.framework) ? "fullstack" : ["React", "Vue.js", "Svelte"].includes(detected.framework) ? "frontend" : "backend";
1752
- const projectType = quick ? inferredProjectType : await select({
2144
+ const projectType = quick || skipProject ? readProjectConfig()?.projectType ?? inferredProjectType : await select({
1753
2145
  message: "Project type?",
1754
2146
  choices: [
1755
2147
  { value: "backend", name: "Backend \u2014 API, server, microservice" },
@@ -1759,8 +2151,8 @@ program.command("init").description("Initialize memory-core in the current proje
1759
2151
  });
1760
2152
  let backendArchitecture;
1761
2153
  if (projectType === "backend" || projectType === "fullstack") {
1762
- if (quick) {
1763
- 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");
1764
2156
  } else {
1765
2157
  const backendProfiles = listProfiles("backend");
1766
2158
  backendArchitecture = await select({
@@ -1774,14 +2166,14 @@ program.command("init").description("Initialize memory-core in the current proje
1774
2166
  }
1775
2167
  let frontendFramework;
1776
2168
  if (projectType === "frontend" || projectType === "fullstack") {
1777
- if (quick) {
2169
+ if (quick || skipProject) {
1778
2170
  const frameworkMap = {
1779
2171
  "Nuxt.js": "nuxt",
1780
2172
  React: "react",
1781
2173
  "Vue.js": "vue",
1782
2174
  Svelte: "svelte"
1783
2175
  };
1784
- frontendFramework = frameworkMap[detected.framework] ?? "react";
2176
+ frontendFramework = readProjectConfig()?.frontendFramework ?? (frameworkMap[detected.framework] ?? "react");
1785
2177
  } else {
1786
2178
  const frontendProfiles = listProfiles("frontend");
1787
2179
  frontendFramework = await select({
@@ -1793,43 +2185,20 @@ program.command("init").description("Initialize memory-core in the current proje
1793
2185
  });
1794
2186
  }
1795
2187
  }
1796
- const language = quick ? detected.language : await input({
2188
+ const language = quick || skipProject ? readProjectConfig()?.language ?? detected.language : await input({
1797
2189
  message: "Language?",
1798
2190
  default: detected.language
1799
2191
  });
1800
- const pullMemories = quick ? true : await confirm({
1801
- message: "Pull relevant memories from previous projects?",
1802
- default: true
1803
- });
1804
- let installCaveman = quick ? false : await confirm({
1805
- message: "Install caveman token saver? Downloads and runs the upstream installer.",
1806
- default: false
1807
- });
1808
- let cavemanIntensity = "full";
1809
- if (installCaveman) {
1810
- const allowRemoteInstaller = await confirm({
1811
- message: `Security check: download and execute installer from ${CAVEMAN_INSTALL_URL}?`,
1812
- default: false
1813
- });
1814
- if (!allowRemoteInstaller) {
1815
- installCaveman = false;
1816
- }
1817
- }
1818
- if (installCaveman) {
1819
- cavemanIntensity = await select({
1820
- message: "Caveman intensity?",
1821
- choices: [
1822
- { value: "full", name: "Full \u2014 caveman mode (default)" },
1823
- { value: "lite", name: "Lite \u2014 professional terseness" },
1824
- { value: "ultra", name: "Ultra \u2014 telegraphic, minimum words" }
1825
- ]
1826
- });
1827
- }
1828
2192
  const selectedAgents = quick ? AGENT_NAMES.filter((a) => a !== "Shared") : await (async () => {
1829
2193
  const { checkbox } = await import("@inquirer/prompts");
2194
+ const saved = new Set(readProjectConfig()?.agents ?? []);
1830
2195
  return checkbox({
1831
2196
  message: "Which AI agents do you want to generate files for?",
1832
- 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
+ })),
1833
2202
  instructions: " (Space to toggle, A to select all, Enter to confirm)"
1834
2203
  });
1835
2204
  })();
@@ -1848,6 +2217,42 @@ program.command("init").description("Initialize memory-core in the current proje
1848
2217
  ]
1849
2218
  });
1850
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
+ }
1851
2256
  const config = {
1852
2257
  projectName,
1853
2258
  projectType,
@@ -1856,34 +2261,33 @@ program.command("init").description("Initialize memory-core in the current proje
1856
2261
  language,
1857
2262
  caveman: { enabled: installCaveman, intensity: cavemanIntensity },
1858
2263
  agents: selectedAgents,
1859
- allowPatterns: [],
2264
+ allowPatterns: readProjectConfig()?.allowPatterns ?? [],
2265
+ commitRules: readProjectConfig()?.commitRules,
1860
2266
  autoSync: true
1861
2267
  };
1862
2268
  let memories = [];
1863
- if (pullMemories) {
1864
- const spinner2 = ora("Retrieving relevant memories\u2026").start();
1865
- try {
1866
- const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
1867
- const selection = await retrieveMemorySelection({
1868
- query: archQuery,
1869
- cwd: process.cwd(),
1870
- config,
1871
- limit: 20
1872
- });
1873
- memories = selection.included;
1874
- 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`));
1875
2280
  printMemorySelection(selection);
1876
- } catch (err) {
1877
- spinner2.warn(`Could not retrieve memories: ${err.message}`);
1878
2281
  }
2282
+ } catch {
1879
2283
  }
1880
2284
  if (installCaveman) {
1881
- const spinner2 = ora("Installing caveman token saver\u2026").start();
2285
+ const cavemanSpinner = ora("Installing caveman token saver\u2026").start();
1882
2286
  try {
1883
2287
  await installCavemanTokenSaver();
1884
- spinner2.succeed("Caveman installed");
2288
+ cavemanSpinner.succeed("Caveman installed");
1885
2289
  } catch (err) {
1886
- spinner2.warn(`Caveman install failed: ${err.message}`);
2290
+ cavemanSpinner.warn(`Caveman install failed: ${err.message}`);
1887
2291
  }
1888
2292
  }
1889
2293
  const spinner = ora("Generating AI agent context files\u2026").start();
@@ -1904,12 +2308,8 @@ program.command("init").description("Initialize memory-core in the current proje
1904
2308
  if (enableHook) {
1905
2309
  installHook(hookAdvisory);
1906
2310
  }
1907
- const status = await checkConnections(
1908
- process.env.DATABASE_URL ?? "",
1909
- process.env.OLLAMA_URL ?? "http://localhost:11434",
1910
- process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"
1911
- );
1912
- 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 });
1913
2313
  await closePool();
1914
2314
  });
1915
2315
  program.command("sync").description("Re-pull memories and regenerate AI agent files").action(async () => {
@@ -1960,27 +2360,31 @@ program.command("auto-sync [mode]").description("Show or change automatic agent
1960
2360
  });
1961
2361
  program.command("remember <text>").description("Save a new memory to the central database").option("-t, --type <type>", "Memory type (decision|rule|pattern|note)", "decision").option("-s, --scope <scope>", "Scope (global|project)", "project").option("--tags <tags>", "Comma-separated tags").option("-r, --reason <reason>", "Why this rule exists \u2014 helps agents understand intent and debug violations").option("--applies-to <items>", "Comma-separated situations where this memory applies").option("--avoid-when <items>", "Comma-separated situations where this memory should not be used").option("--example <items>", "Comma-separated examples that teach agents how to apply this memory").option("--source <source>", "Human-readable source for this memory").option("--no-sync", "Skip automatic agent file sync after saving").action(async (text, opts) => {
1962
2362
  const config = readProjectConfig();
2363
+ const scope = opts.scope?.trim() || "project";
1963
2364
  let reason = opts.reason;
1964
2365
  if (!reason) {
1965
2366
  reason = await input({
1966
- message: chalk2.dim("Why does this rule exist? (optional \u2014 helps agents debug violations)"),
2367
+ message: chalk2.dim("Why should this memory exist?"),
1967
2368
  default: ""
1968
2369
  });
1969
2370
  }
2371
+ const storedReason = memoryReasonOrFallback(reason, text, opts.type);
1970
2372
  const spinner = ora("Saving memory\u2026").start();
1971
2373
  try {
1972
2374
  await phase1.services.memoryEngine.remember({
1973
2375
  type: opts.type,
1974
- scope: opts.scope,
2376
+ scope,
1975
2377
  architecture: config?.backendArchitecture ?? config?.frontendFramework,
1976
- projectName: config?.projectName,
2378
+ projectName: scope === "project" ? config?.projectName : void 0,
1977
2379
  content: text,
1978
- reason: reason || void 0,
2380
+ reason: storedReason,
1979
2381
  context: buildMemoryContext(opts),
1980
2382
  tags: parseTags(opts.tags)
1981
2383
  });
1982
- const reasonLine = reason ? chalk2.gray(`
1983
- Why: ${reason}`) : "";
2384
+ const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
2385
+ writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
2386
+ const reasonLine = chalk2.gray(`
2387
+ Why: ${storedReason}`);
1984
2388
  spinner.succeed(chalk2.green(`Memory saved: "${text}"`) + reasonLine);
1985
2389
  await autoSyncGeneratedFiles(config, "remember", opts.sync);
1986
2390
  } catch (err) {
@@ -2067,6 +2471,8 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
2067
2471
  }
2068
2472
  spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
2069
2473
  if (inserted > 0) {
2474
+ const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
2475
+ writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
2070
2476
  await autoSyncGeneratedFiles(config, "import", opts.sync);
2071
2477
  }
2072
2478
  } catch (err) {
@@ -2162,7 +2568,7 @@ program.command("edit <id>").description("Edit a memory interactively").option("
2162
2568
  scope,
2163
2569
  title: title || void 0,
2164
2570
  content,
2165
- reason: reason || void 0,
2571
+ reason: memoryReasonOrFallback(reason, content, type),
2166
2572
  context: buildMemoryContext({ appliesTo, avoidWhen, example: examples, source }),
2167
2573
  tags: parseTags(tags)
2168
2574
  });
@@ -2205,6 +2611,8 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
2205
2611
  content: pattern,
2206
2612
  tags: ["ignore"]
2207
2613
  });
2614
+ const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
2615
+ writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
2208
2616
  console.log(chalk2.green(`Ignored pattern saved: "${pattern}"`));
2209
2617
  await autoSyncGeneratedFiles(config, "ignore", opts.sync);
2210
2618
  } catch (err) {
@@ -2244,6 +2652,61 @@ program.command("allow [pattern]").description("Manage project allow patterns in
2244
2652
  }));
2245
2653
  console.log(chalk2.green(`Allow pattern saved: "${pattern}"`));
2246
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
+ });
2247
2710
  program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
2248
2711
  const workflowPath = join3(process.cwd(), ".github", "workflows", "memory-core.yml");
2249
2712
  mkdirSync(dirname(workflowPath), { recursive: true });
@@ -2308,7 +2771,7 @@ program.command("uninstall").description("Remove memory-core from the current pr
2308
2771
  console.log(chalk2.gray(" \u2713 cleaned .gitignore memory-core block"));
2309
2772
  }
2310
2773
  });
2311
- 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) => {
2312
2775
  const statsPath = join3(process.cwd(), ".memory-core-stats.json");
2313
2776
  if (opts.reset) {
2314
2777
  const emptyStats = {
@@ -2330,14 +2793,46 @@ program.command("stats").description("Show violation counters recorded by check
2330
2793
  return;
2331
2794
  }
2332
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
+ };
2333
2801
  const printTop = (label, values = {}) => {
2334
2802
  console.log(chalk2.bold(`
2335
2803
  ${label}
2336
2804
  `));
2337
- Object.entries(values).sort((a, b) => b[1] - a[1]).slice(0, 10).forEach(([name, count], index) => {
2338
- 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}`);
2339
2809
  });
2340
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
+ }
2341
2836
  const liveRules = stats.live?.rules ?? {};
2342
2837
  const liveFiles = stats.live?.files ?? {};
2343
2838
  const hasLiveState = !!stats.live;
@@ -2361,6 +2856,87 @@ program.command("stats").description("Show violation counters recorded by check
2361
2856
  }
2362
2857
  }
2363
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
+ });
2364
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) => {
2365
2941
  const resolveDashboardPath = () => {
2366
2942
  if (typeof opts.path === "string" && opts.path.trim().length > 0) return opts.path;
@@ -2378,7 +2954,7 @@ program.command("dashboard").description("Start the live Svelte dashboard with W
2378
2954
  }
2379
2955
  return void 0;
2380
2956
  };
2381
- const { startDashboard } = await import("./dashboard-server-53HVL7LF.js");
2957
+ const { startDashboard } = await import("./dashboard-server-ZBGR4CO7.js");
2382
2958
  await startDashboard({
2383
2959
  port: parseInt(opts.port, 10),
2384
2960
  path: resolveDashboardPath(),
@@ -2424,6 +3000,10 @@ program.command("seed").description("Load all predefined memories into the datab
2424
3000
  skipped++;
2425
3001
  }
2426
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
+ }
2427
3007
  console.log(chalk2.bold.green(`
2428
3008
  Done. ${saved} memories seeded, ${skipped} skipped.
2429
3009
  `));
@@ -2541,7 +3121,7 @@ read:
2541
3121
  await closePool();
2542
3122
  });
2543
3123
  var provider = program.command("provider").description("Manage the code-checking provider configuration");
2544
- 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) => {
2545
3125
  try {
2546
3126
  const providerName = normalizeProvider(name);
2547
3127
  const runtimeEnv = readRuntimeEnv();
@@ -2559,6 +3139,7 @@ provider.command("set <name>").description("Set the code-checking provider (olla
2559
3139
  values.CHAT_MODEL = model2;
2560
3140
  values.OLLAMA_CHAT_MODEL = model2;
2561
3141
  delete values.CHAT_API_KEY;
3142
+ delete values.CHAT_BASE_URL;
2562
3143
  } else {
2563
3144
  delete values.OLLAMA_CHAT_MODEL;
2564
3145
  if (opts.apiKey) {
@@ -2568,6 +3149,16 @@ provider.command("set <name>").description("Set the code-checking provider (olla
2568
3149
  message: `${providerLabel(providerName)} API key?`
2569
3150
  });
2570
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
+ }
2571
3162
  }
2572
3163
  writeRuntimeEnv(values, runtimeEnv.envPath);
2573
3164
  applyRuntimeEnv(values);
@@ -2775,14 +3366,20 @@ graph.command("diff <leftSnapshotId> [rightSnapshotId]").description("Diff two s
2775
3366
  }
2776
3367
  });
2777
3368
  var hook = program.command("hook").description("Manage the pre-commit rule enforcement hook");
2778
- hook.command("install").description("Install pre-commit hook (advisory mode by default \u2014 logs violations, never blocks)").option("--advisory", "Log violations but never block commits (default)").option("--strict", "Block commits that violate your rules").action((opts) => {
3369
+ hook.command("install").description("Install pre-commit hook (advisory mode by default \u2014 logs violations, never blocks)").option("--advisory", "Log violations but never block commits (default)").option("--strict", "Block commits that violate your rules").option("--fast", "Use deterministic checks only in the hook").action((opts) => {
2779
3370
  const advisory = opts.strict ? false : true;
2780
- installHook(advisory);
3371
+ installHook(advisory, opts.fast ?? false);
2781
3372
  });
2782
3373
  hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
2783
3374
  uninstallHook();
2784
3375
  });
2785
- 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").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
+ }
2786
3383
  if (opts.ci && opts.all) {
2787
3384
  console.error(chalk2.red("\n Choose one mode: --ci or --all.\n"));
2788
3385
  process.exit(1);
@@ -2800,7 +3397,7 @@ program.command("check").description("Check staged changes against architecture
2800
3397
  if (summary.violations > 0) process.exit(1);
2801
3398
  return;
2802
3399
  }
2803
- await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false });
3400
+ await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false, fast: opts.fast ?? false });
2804
3401
  });
2805
3402
  program.command("watch").description("Watch source files and check violations in real-time on every save").option("--path <dir>", "Directory to watch (default: current directory)").option("--scan-on-start", "Run an initial full snapshot scan before watching file changes").option("--verbose", "Show diff size and model details per file").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
2806
3403
  await phase1.providers.watchService.start({