@shahmilsaari/memory-core 0.2.12 → 0.2.14

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
@@ -4,6 +4,7 @@ import {
4
4
  } from "./chunk-HAGRPKR3.js";
5
5
  import {
6
6
  closePool,
7
+ deleteMemories,
7
8
  deleteMemory,
8
9
  getMemory,
9
10
  listMemories,
@@ -12,7 +13,7 @@ import {
12
13
  searchMemories,
13
14
  updateMemory,
14
15
  upsertMemory
15
- } from "./chunk-73SRPNAL.js";
16
+ } from "./chunk-25Y2KI7M.js";
16
17
  import "./chunk-KSLFLWB4.js";
17
18
 
18
19
  // src/cli.ts
@@ -26,14 +27,161 @@ import { homedir } from "os";
26
27
  import { execSync as execSync2 } from "child_process";
27
28
 
28
29
  // src/generator.ts
29
- import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync } from "fs";
30
- import { join, dirname } from "path";
30
+ import { readFileSync as readFileSync2, readdirSync, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
31
+ import { join as join2, dirname } from "path";
31
32
  import { fileURLToPath } from "url";
32
33
  import Handlebars from "handlebars";
33
34
  import yaml from "js-yaml";
35
+
36
+ // src/project-detector.ts
37
+ import { existsSync, readFileSync } from "fs";
38
+ import { join } from "path";
39
+ function detectProject(cwd = process.cwd()) {
40
+ const has = (file) => existsSync(join(cwd, file));
41
+ const readJson = (file) => {
42
+ try {
43
+ return JSON.parse(readFileSync(join(cwd, file), "utf-8"));
44
+ } catch {
45
+ return {};
46
+ }
47
+ };
48
+ if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) {
49
+ return { language: "TypeScript", framework: "Next.js" };
50
+ }
51
+ if (has("artisan") && has("composer.json")) {
52
+ return { language: "PHP", framework: "Laravel" };
53
+ }
54
+ if (has("nuxt.config.ts") || has("nuxt.config.js")) {
55
+ return { language: "TypeScript", framework: "Nuxt.js" };
56
+ }
57
+ if (has("manage.py")) {
58
+ if (has("requirements.txt")) {
59
+ const req = readFileSync(join(cwd, "requirements.txt"), "utf-8");
60
+ if (req.includes("djangorestframework")) {
61
+ return { language: "Python", framework: "Django REST Framework" };
62
+ }
63
+ }
64
+ return { language: "Python", framework: "Django" };
65
+ }
66
+ if (has("go.mod")) {
67
+ return { language: "Go", framework: "Go" };
68
+ }
69
+ if (has("Cargo.toml")) {
70
+ return { language: "Rust", framework: "Rust" };
71
+ }
72
+ if (has("pubspec.yaml")) {
73
+ return { language: "Dart", framework: "Flutter" };
74
+ }
75
+ if (has("pom.xml")) {
76
+ return { language: "Java", framework: "Spring Boot" };
77
+ }
78
+ if (has("build.gradle") || has("build.gradle.kts")) {
79
+ return { language: "Kotlin", framework: "Kotlin/JVM" };
80
+ }
81
+ if (has("package.json")) {
82
+ const pkg = readJson("package.json");
83
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
84
+ if (deps["@nestjs/core"]) return { language: "TypeScript", framework: "NestJS" };
85
+ if (deps["express"]) return { language: "TypeScript", framework: "Express.js" };
86
+ if (deps["fastify"]) return { language: "TypeScript", framework: "Fastify" };
87
+ if (deps["react"]) return { language: "TypeScript", framework: "React" };
88
+ if (deps["vue"]) return { language: "TypeScript", framework: "Vue.js" };
89
+ if (deps["svelte"]) return { language: "TypeScript", framework: "Svelte" };
90
+ return { language: "TypeScript/JavaScript", framework: "Node.js" };
91
+ }
92
+ if (has("requirements.txt") || has("pyproject.toml")) {
93
+ return { language: "Python", framework: "Python" };
94
+ }
95
+ return { language: "Unknown", framework: "Unknown" };
96
+ }
97
+
98
+ // src/retriever.ts
99
+ async function retrieve(query, architecture, limit = 10) {
100
+ const embedding = await embed(query);
101
+ return searchMemories(embedding, architecture, limit);
102
+ }
103
+
104
+ // src/memory-selection.ts
105
+ var FRAMEWORK_ARCHITECTURE_MAP = {
106
+ Laravel: ["laravel-service-repository"],
107
+ "Next.js": ["nextjs"],
108
+ "Nuxt.js": ["nuxt"],
109
+ Go: ["go-api"],
110
+ NestJS: ["nestjs"],
111
+ React: ["react"],
112
+ "Vue.js": ["vue"],
113
+ Svelte: ["svelte"]
114
+ };
115
+ function normalizeText(value) {
116
+ return value.toLowerCase().replace(/[`"'()[\]{}.,:;!?/\\<>|=*+-]/g, " ").replace(/\s+/g, " ").trim();
117
+ }
118
+ function tokenSet(value) {
119
+ return new Set(
120
+ normalizeText(value).split(" ").filter((token) => token.length > 2)
121
+ );
122
+ }
123
+ function similarityScore(a, b) {
124
+ const left = tokenSet(a);
125
+ const right = tokenSet(b);
126
+ if (left.size === 0 || right.size === 0) return 0;
127
+ let intersection = 0;
128
+ for (const token of left) {
129
+ if (right.has(token)) intersection++;
130
+ }
131
+ return 2 * intersection / (left.size + right.size);
132
+ }
133
+ function mergeMemory(primary, secondary) {
134
+ const mergedTags = [.../* @__PURE__ */ new Set([...primary.tags ?? [], ...secondary.tags ?? []])];
135
+ const reason = [primary.reason, secondary.reason].filter(Boolean).join(" | ") || void 0;
136
+ return {
137
+ ...primary,
138
+ tags: mergedTags,
139
+ reason
140
+ };
141
+ }
142
+ function dedupeMemories(memories, threshold = 0.8) {
143
+ const deduped = [];
144
+ for (const memory of memories) {
145
+ const existingIndex = deduped.findIndex((candidate) => {
146
+ if (candidate.content_hash && memory.content_hash && candidate.content_hash === memory.content_hash) {
147
+ return true;
148
+ }
149
+ return similarityScore(candidate.content, memory.content) >= threshold;
150
+ });
151
+ if (existingIndex === -1) {
152
+ deduped.push(memory);
153
+ continue;
154
+ }
155
+ deduped[existingIndex] = mergeMemory(deduped[existingIndex], memory);
156
+ }
157
+ return deduped;
158
+ }
159
+ function inferProjectArchitectures(cwd = process.cwd(), config) {
160
+ const inferred = /* @__PURE__ */ new Set();
161
+ if (config?.backendArchitecture) inferred.add(config.backendArchitecture);
162
+ if (config?.frontendFramework) inferred.add(config.frontendFramework);
163
+ const detected = detectProject(cwd);
164
+ for (const architecture of FRAMEWORK_ARCHITECTURE_MAP[detected.framework] ?? []) {
165
+ inferred.add(architecture);
166
+ }
167
+ return [...inferred];
168
+ }
169
+ function getAllowPatterns(config) {
170
+ return [...new Set(config?.allowPatterns?.filter(Boolean) ?? [])];
171
+ }
172
+ function buildContextQuery(parts, maxLength = 1200) {
173
+ return parts.filter(Boolean).join("\n").slice(0, maxLength);
174
+ }
175
+ async function retrieveContextualMemories(options) {
176
+ const architectures = inferProjectArchitectures(options.cwd, options.config);
177
+ const memories = await retrieve(options.query, architectures, options.limit ?? 15);
178
+ return dedupeMemories(memories);
179
+ }
180
+
181
+ // src/generator.ts
34
182
  var __filename = fileURLToPath(import.meta.url);
35
183
  var __dirname = dirname(__filename);
36
- var PKG_ROOT = join(__dirname, "..");
184
+ var PKG_ROOT = join2(__dirname, "..");
37
185
  var OUTPUT_FILES = [
38
186
  { template: "CLAUDE.md.hbs", path: "CLAUDE.md", agent: "Claude Code" },
39
187
  { template: "copilot-instructions.md.hbs", path: ".github/copilot-instructions.md", agent: "GitHub Copilot" },
@@ -69,13 +217,13 @@ Handlebars.registerHelper(
69
217
  );
70
218
  Handlebars.registerHelper("json", (val) => JSON.stringify(val, null, 2));
71
219
  function loadProfile(name) {
72
- const profilePath = join(PKG_ROOT, "profiles", `${name}.yml`);
73
- if (!existsSync(profilePath)) throw new Error(`Profile not found: ${name}`);
74
- return yaml.load(readFileSync(profilePath, "utf-8"));
220
+ const profilePath = join2(PKG_ROOT, "profiles", `${name}.yml`);
221
+ if (!existsSync2(profilePath)) throw new Error(`Profile not found: ${name}`);
222
+ return yaml.load(readFileSync2(profilePath, "utf-8"));
75
223
  }
76
224
  function listProfiles(layer) {
77
- const files = readdirSync(join(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
78
- const all = files.map((f) => yaml.load(readFileSync(join(PKG_ROOT, "profiles", f), "utf-8")));
225
+ const files = readdirSync(join2(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
226
+ const all = files.map((f) => yaml.load(readFileSync2(join2(PKG_ROOT, "profiles", f), "utf-8")));
79
227
  if (!layer) return all;
80
228
  if (layer === "backend") return all.filter((p) => p.layer === "backend" || p.layer === "fullstack");
81
229
  if (layer === "frontend") return all.filter((p) => p.layer === "frontend" || p.layer === "fullstack");
@@ -84,6 +232,7 @@ function listProfiles(layer) {
84
232
  function buildTemplateData(options) {
85
233
  const backend = options.backendArchitecture ? loadProfile(options.backendArchitecture) : null;
86
234
  const frontend = options.frontendFramework ? loadProfile(options.frontendFramework) : null;
235
+ const dedupedMemories = dedupeMemories(options.memories);
87
236
  const allRules = [
88
237
  ...backend?.rules ?? [],
89
238
  ...frontend?.rules ?? []
@@ -127,8 +276,8 @@ function buildTemplateData(options) {
127
276
  avoid: allAvoid,
128
277
  description: [backend?.description, frontend?.description].filter(Boolean).join(" | "),
129
278
  // memories
130
- memories: options.memories,
131
- hasMemories: options.memories.length > 0,
279
+ memories: dedupedMemories,
280
+ hasMemories: dedupedMemories.length > 0,
132
281
  // misc
133
282
  language: options.language,
134
283
  caveman: options.caveman,
@@ -136,15 +285,15 @@ function buildTemplateData(options) {
136
285
  };
137
286
  }
138
287
  function renderTemplate(templateName, data) {
139
- const templatePath = join(PKG_ROOT, "templates", templateName);
140
- if (!existsSync(templatePath)) throw new Error(`Template not found: ${templateName}`);
141
- return Handlebars.compile(readFileSync(templatePath, "utf-8"))(data);
288
+ const templatePath = join2(PKG_ROOT, "templates", templateName);
289
+ if (!existsSync2(templatePath)) throw new Error(`Template not found: ${templateName}`);
290
+ return Handlebars.compile(readFileSync2(templatePath, "utf-8"))(data);
142
291
  }
143
292
  function writeFile(filePath, content) {
144
293
  const dir = dirname(filePath);
145
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
146
- if (existsSync(filePath)) {
147
- const existing = readFileSync(filePath, "utf-8");
294
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
295
+ if (existsSync2(filePath)) {
296
+ const existing = readFileSync2(filePath, "utf-8");
148
297
  if (existing === content) return "skipped";
149
298
  }
150
299
  writeFileSync(filePath, content, "utf-8");
@@ -156,8 +305,8 @@ async function generate(options, cwd = process.cwd(), onlyAgents) {
156
305
  const skipped = [];
157
306
  const files = onlyAgents ? OUTPUT_FILES.filter((f) => onlyAgents.includes(f.agent)) : OUTPUT_FILES;
158
307
  for (const output of files) {
159
- const targetPath = join(cwd, output.path);
160
- if (output.skipIfExists && existsSync(targetPath)) {
308
+ const targetPath = join2(cwd, output.path);
309
+ if (output.skipIfExists && existsSync2(targetPath)) {
161
310
  skipped.push(output.path);
162
311
  continue;
163
312
  }
@@ -554,71 +703,6 @@ var seeds = [
554
703
  { type: "rule", scope: "global", architecture: "svelte", title: "Avoid options API style \u2014 runes only", content: "Do not use the Svelte 4 options-style patterns (export let, $: reactive statements, $store subscriptions) in new Svelte 5 components. Use runes throughout.", reason: "Mixing the two reactivity systems in the same codebase creates two mental models, confuses new developers, and makes future migrations harder. Svelte 5 runes supersede every Svelte 4 pattern.", tags: ["svelte", "runes", "anti-pattern"] }
555
704
  ];
556
705
 
557
- // src/retriever.ts
558
- async function retrieve(query, architecture, limit = 10) {
559
- const embedding = await embed(query);
560
- return searchMemories(embedding, architecture, limit);
561
- }
562
-
563
- // src/project-detector.ts
564
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
565
- import { join as join2 } from "path";
566
- function detectProject(cwd = process.cwd()) {
567
- const has = (file) => existsSync2(join2(cwd, file));
568
- const readJson = (file) => {
569
- try {
570
- return JSON.parse(readFileSync2(join2(cwd, file), "utf-8"));
571
- } catch {
572
- return {};
573
- }
574
- };
575
- if (has("artisan") && has("composer.json")) {
576
- return { language: "PHP", framework: "Laravel" };
577
- }
578
- if (has("nuxt.config.ts") || has("nuxt.config.js")) {
579
- return { language: "TypeScript", framework: "Nuxt.js" };
580
- }
581
- if (has("manage.py")) {
582
- if (has("requirements.txt")) {
583
- const req = readFileSync2(join2(cwd, "requirements.txt"), "utf-8");
584
- if (req.includes("djangorestframework")) {
585
- return { language: "Python", framework: "Django REST Framework" };
586
- }
587
- }
588
- return { language: "Python", framework: "Django" };
589
- }
590
- if (has("go.mod")) {
591
- return { language: "Go", framework: "Go" };
592
- }
593
- if (has("Cargo.toml")) {
594
- return { language: "Rust", framework: "Rust" };
595
- }
596
- if (has("pubspec.yaml")) {
597
- return { language: "Dart", framework: "Flutter" };
598
- }
599
- if (has("pom.xml")) {
600
- return { language: "Java", framework: "Spring Boot" };
601
- }
602
- if (has("build.gradle") || has("build.gradle.kts")) {
603
- return { language: "Kotlin", framework: "Kotlin/JVM" };
604
- }
605
- if (has("package.json")) {
606
- const pkg = readJson("package.json");
607
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
608
- if (deps["@nestjs/core"]) return { language: "TypeScript", framework: "NestJS" };
609
- if (deps["express"]) return { language: "TypeScript", framework: "Express.js" };
610
- if (deps["fastify"]) return { language: "TypeScript", framework: "Fastify" };
611
- if (deps["react"]) return { language: "TypeScript", framework: "React" };
612
- if (deps["vue"]) return { language: "TypeScript", framework: "Vue.js" };
613
- if (deps["svelte"]) return { language: "TypeScript", framework: "Svelte" };
614
- return { language: "TypeScript/JavaScript", framework: "Node.js" };
615
- }
616
- if (has("requirements.txt") || has("pyproject.toml")) {
617
- return { language: "Python", framework: "Python" };
618
- }
619
- return { language: "Unknown", framework: "Unknown" };
620
- }
621
-
622
706
  // src/hook.ts
623
707
  import { execSync, spawnSync } from "child_process";
624
708
  import { writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync, readFileSync as readFileSync4, chmodSync } from "fs";
@@ -627,11 +711,11 @@ import chalk from "chalk";
627
711
 
628
712
  // src/chat.ts
629
713
  function getChatConfig() {
630
- const provider = process.env.CHAT_PROVIDER ?? "ollama";
631
- const model = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
714
+ const provider2 = process.env.CHAT_PROVIDER ?? "ollama";
715
+ const model2 = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
632
716
  return {
633
- provider,
634
- model,
717
+ provider: provider2,
718
+ model: model2,
635
719
  ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
636
720
  apiKey: process.env.CHAT_API_KEY ?? ""
637
721
  };
@@ -838,7 +922,7 @@ async function promptToSaveViolations(violations) {
838
922
  default: selected.reason ?? selected.issue ?? ""
839
923
  });
840
924
  const { embed: embed2 } = await import("./embedding-PAYD2JYW.js");
841
- const { upsertMemory: upsertMemory2 } = await import("./db-KU4EEG4Y.js");
925
+ const { upsertMemory: upsertMemory2 } = await import("./db-5X5LTUCB.js");
842
926
  await upsertMemory2({
843
927
  type: "rule",
844
928
  scope: "project",
@@ -855,7 +939,7 @@ async function promptToSaveViolations(violations) {
855
939
  }
856
940
  async function loadIgnorePatterns() {
857
941
  try {
858
- const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-KU4EEG4Y.js");
942
+ const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-5X5LTUCB.js");
859
943
  const ignores = await listMemories2({ type: "ignore", limit: 1e3 });
860
944
  await closePool2();
861
945
  return ignores.map((ignore) => ignore.content);
@@ -863,6 +947,90 @@ async function loadIgnorePatterns() {
863
947
  return [];
864
948
  }
865
949
  }
950
+ function getProfileRules(config) {
951
+ const rules = [];
952
+ const avoids = [];
953
+ if (config.backendArchitecture) {
954
+ const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
955
+ if (profile) {
956
+ rules.push(...profile.rules);
957
+ avoids.push(...profile.avoid);
958
+ }
959
+ }
960
+ if (config.frontendFramework) {
961
+ const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
962
+ if (profile) {
963
+ rules.push(...profile.rules);
964
+ avoids.push(...profile.avoid);
965
+ }
966
+ }
967
+ return { rules, avoids };
968
+ }
969
+ async function loadRelevantRules(config, diff, stagedFiles, fallbackRules) {
970
+ try {
971
+ const query = buildContextQuery([
972
+ stagedFiles.join("\n"),
973
+ diff.slice(0, 1200),
974
+ config.backendArchitecture,
975
+ config.frontendFramework,
976
+ config.language
977
+ ]);
978
+ const memories = await retrieveContextualMemories({
979
+ query,
980
+ cwd: process.cwd(),
981
+ config,
982
+ limit: 15
983
+ });
984
+ const selected = memories.filter((memory) => ["rule", "pattern", "decision"].includes(memory.type)).map((memory) => memory.content);
985
+ return selected.length > 0 ? selected : fallbackRules;
986
+ } catch {
987
+ return fallbackRules;
988
+ }
989
+ }
990
+ function applyAllowPatterns(violations, allowPatterns) {
991
+ if (allowPatterns.length === 0) return violations;
992
+ return violations.filter((violation) => {
993
+ const haystack = `${violation.rule}
994
+ ${violation.issue}
995
+ ${violation.file}`.toLowerCase();
996
+ return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
997
+ });
998
+ }
999
+ async function verifyViolations(diff, violations, allowPatterns, debug) {
1000
+ if (violations.length === 0) return violations;
1001
+ const systemPrompt = `You are verifying candidate architecture violations.
1002
+ Only keep violations that are directly supported by the diff.
1003
+ Reject speculative or weak matches.
1004
+ Treat these allowlisted patterns as intentional and valid:
1005
+ ${allowPatterns.length ? allowPatterns.map((pattern, index) => `${index + 1}. ${pattern}`).join("\n") : "(none)"}
1006
+
1007
+ Return strict JSON:
1008
+ {"violations":[{"rule":"...","file":"...","line":1,"issue":"...","suggestion":"...","reason":"..."}]}
1009
+ Do not include any text outside the JSON.`;
1010
+ const userPrompt = `Diff:
1011
+ ${diff.slice(0, 6e3)}
1012
+
1013
+ Candidate violations:
1014
+ ${JSON.stringify(violations, null, 2)}`;
1015
+ if (debug) {
1016
+ console.log(chalk.gray("\n [debug] verifier prompt:"));
1017
+ console.log(chalk.dim(systemPrompt));
1018
+ console.log(chalk.dim(userPrompt));
1019
+ 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"));
1020
+ }
1021
+ try {
1022
+ const raw = await callChatModel([
1023
+ { role: "system", content: systemPrompt },
1024
+ { role: "user", content: userPrompt }
1025
+ ]);
1026
+ const parsed = JSON.parse(raw);
1027
+ if (Array.isArray(parsed?.violations)) return parsed.violations;
1028
+ if (Array.isArray(parsed)) return parsed;
1029
+ return violations;
1030
+ } catch {
1031
+ return violations;
1032
+ }
1033
+ }
866
1034
  function installHook(advisory = true) {
867
1035
  if (!existsSync4(".git")) {
868
1036
  console.error(chalk.red("\n Not a git repository. Run from project root.\n"));
@@ -911,8 +1079,9 @@ function uninstallHook() {
911
1079
  async function checkStaged(options = {}) {
912
1080
  const SOURCE_EXTENSIONS2 = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
913
1081
  let diff;
1082
+ let stagedFiles = [];
914
1083
  try {
915
- const stagedFiles = execSync("git diff --cached --name-only", { encoding: "utf-8" }).split("\n").filter((f) => f && SOURCE_EXTENSIONS2.test(f));
1084
+ stagedFiles = execSync("git diff --cached --name-only", { encoding: "utf-8" }).split("\n").filter((f) => f && SOURCE_EXTENSIONS2.test(f));
916
1085
  if (stagedFiles.length === 0) {
917
1086
  if (options.verbose) console.log(chalk.gray(" No source files staged \u2014 skipping rule check."));
918
1087
  return;
@@ -930,22 +1099,9 @@ async function checkStaged(options = {}) {
930
1099
  const configPath = join4(process.cwd(), ".memory-core.json");
931
1100
  if (!existsSync4(configPath)) return;
932
1101
  const config = JSON.parse(readFileSync4(configPath, "utf-8"));
933
- const rules = [];
934
- const avoids = [];
935
- if (config.backendArchitecture) {
936
- const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
937
- if (profile) {
938
- rules.push(...profile.rules);
939
- avoids.push(...profile.avoid);
940
- }
941
- }
942
- if (config.frontendFramework) {
943
- const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
944
- if (profile) {
945
- rules.push(...profile.rules);
946
- avoids.push(...profile.avoid);
947
- }
948
- }
1102
+ const { rules: fallbackRules, avoids } = getProfileRules(config);
1103
+ const rules = await loadRelevantRules(config, diff, stagedFiles, fallbackRules);
1104
+ const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...await loadIgnorePatterns()])];
949
1105
  if (rules.length === 0) return;
950
1106
  const MAX_DIFF = 8e3;
951
1107
  const truncated = diff.length > MAX_DIFF;
@@ -959,7 +1115,6 @@ async function checkStaged(options = {}) {
959
1115
  return why ? `${i + 1}. ${r}
960
1116
  WHY: ${why}` : `${i + 1}. ${r}`;
961
1117
  }).join("\n");
962
- const ignorePatterns = await loadIgnorePatterns();
963
1118
  const systemPrompt = `You are a strict code reviewer enforcing architecture and framework rules.
964
1119
  Analyze the git diff and identify ONLY clear, definite rule violations \u2014 not style preferences.
965
1120
  Use the WHY for each rule to understand intent and judge edge cases correctly.
@@ -971,7 +1126,7 @@ Things that must never appear:
971
1126
  ${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
972
1127
 
973
1128
  Never flag these accepted project patterns:
974
- ${ignorePatterns.length ? ignorePatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
1129
+ ${allowPatterns.length ? allowPatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
975
1130
 
976
1131
  IMPORTANT: You MUST respond with a JSON object that has a "violations" key containing an array.
977
1132
  For each violation include a "reason" field \u2014 copy the WHY from the rule to explain to the developer why this matters.
@@ -1027,6 +1182,8 @@ ${diffToSend}` }
1027
1182
  `));
1028
1183
  return;
1029
1184
  }
1185
+ violations = await verifyViolations(diff, violations, allowPatterns, options.debug ?? false);
1186
+ violations = applyAllowPatterns(violations, allowPatterns);
1030
1187
  if (violations.length === 0) {
1031
1188
  console.log(chalk.green(" \u2713 No rule violations \u2014 commit allowed.\n"));
1032
1189
  return;
@@ -1144,10 +1301,10 @@ async function checkCi(options = {}) {
1144
1301
  recordViolations(violations);
1145
1302
  process.exit(1);
1146
1303
  }
1147
- function printModelMissing(model) {
1304
+ function printModelMissing(model2) {
1148
1305
  console.log(chalk.yellow(`
1149
- \u26A0 Chat model "${model}" not found in Ollama.`));
1150
- console.log(chalk.gray(` Pull a model: ollama pull ${model}`));
1306
+ \u26A0 Chat model "${model2}" not found in Ollama.`));
1307
+ console.log(chalk.gray(` Pull a model: ollama pull ${model2}`));
1151
1308
  console.log(chalk.gray(" Or set OLLAMA_CHAT_MODEL=<model> in .env"));
1152
1309
  console.log(chalk.gray(" Recommended: llama3.2 | qwen2.5-coder:3b | mistral\n"));
1153
1310
  }
@@ -1211,7 +1368,7 @@ function loadConfig(cwd) {
1211
1368
  return null;
1212
1369
  }
1213
1370
  }
1214
- function getProfileRules(config) {
1371
+ function getProfileRules2(config) {
1215
1372
  const rules = [];
1216
1373
  const avoids = [];
1217
1374
  if (config.backendArchitecture) {
@@ -1230,9 +1387,74 @@ function getProfileRules(config) {
1230
1387
  }
1231
1388
  return { rules, avoids };
1232
1389
  }
1390
+ async function loadRelevantRules2(config, rel, diff, fallbackRules) {
1391
+ try {
1392
+ const query = buildContextQuery([
1393
+ rel,
1394
+ diff.slice(0, 1200),
1395
+ config.backendArchitecture,
1396
+ config.frontendFramework,
1397
+ config.language
1398
+ ]);
1399
+ const memories = await retrieveContextualMemories({
1400
+ query,
1401
+ cwd: process.cwd(),
1402
+ config,
1403
+ limit: 15
1404
+ });
1405
+ const selected = memories.filter((memory) => ["rule", "pattern", "decision"].includes(memory.type)).map((memory) => memory.content);
1406
+ return selected.length > 0 ? selected : fallbackRules;
1407
+ } catch {
1408
+ return fallbackRules;
1409
+ }
1410
+ }
1411
+ function applyAllowPatterns2(violations, allowPatterns) {
1412
+ if (allowPatterns.length === 0) return violations;
1413
+ return violations.filter((violation) => {
1414
+ const haystack = `${violation.rule}
1415
+ ${violation.issue}
1416
+ ${violation.file}`.toLowerCase();
1417
+ return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
1418
+ });
1419
+ }
1420
+ async function verifyViolations2(diff, violations, allowPatterns, debug) {
1421
+ if (violations.length === 0) return violations;
1422
+ const systemPrompt = `You are verifying candidate architecture violations.
1423
+ Only keep violations that are directly supported by the diff.
1424
+ Reject speculative or weak matches.
1425
+ Treat these allowlisted patterns as intentional and valid:
1426
+ ${allowPatterns.length ? allowPatterns.map((pattern, index) => `${index + 1}. ${pattern}`).join("\n") : "(none)"}
1427
+
1428
+ Return strict JSON:
1429
+ {"violations":[{"rule":"...","file":"...","line":1,"issue":"...","suggestion":"...","reason":"..."}]}
1430
+ Do not include any text outside the JSON.`;
1431
+ const userPrompt = `Diff:
1432
+ ${diff.slice(0, 6e3)}
1433
+
1434
+ Candidate violations:
1435
+ ${JSON.stringify(violations, null, 2)}`;
1436
+ if (debug) {
1437
+ console.log(chalk2.gray("\n [debug] verifier prompt:"));
1438
+ console.log(chalk2.dim(systemPrompt));
1439
+ console.log(chalk2.dim(userPrompt));
1440
+ console.log(chalk2.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"));
1441
+ }
1442
+ try {
1443
+ const raw = await callChatModel([
1444
+ { role: "system", content: systemPrompt },
1445
+ { role: "user", content: userPrompt }
1446
+ ]);
1447
+ const parsed = JSON.parse(raw);
1448
+ if (Array.isArray(parsed?.violations)) return parsed.violations;
1449
+ if (Array.isArray(parsed)) return parsed;
1450
+ return violations;
1451
+ } catch {
1452
+ return violations;
1453
+ }
1454
+ }
1233
1455
  async function loadIgnorePatterns2() {
1234
1456
  try {
1235
- const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-KU4EEG4Y.js");
1457
+ const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-5X5LTUCB.js");
1236
1458
  const ignores = await listMemories2({ type: "ignore", limit: 1e3 });
1237
1459
  await closePool2();
1238
1460
  return ignores.map((ignore) => ignore.content);
@@ -1251,7 +1473,8 @@ async function checkFile(filePath, cwd, config, verbose, debug) {
1251
1473
  diff = noIndexResult.stdout ?? "";
1252
1474
  }
1253
1475
  if (!diff.trim()) return;
1254
- const { rules, avoids } = getProfileRules(config);
1476
+ const { rules: fallbackRules, avoids } = getProfileRules2(config);
1477
+ const rules = await loadRelevantRules2(config, rel, diff, fallbackRules);
1255
1478
  if (rules.length === 0) return;
1256
1479
  const MAX_DIFF = 6e3;
1257
1480
  const truncated = diff.length > MAX_DIFF;
@@ -1265,7 +1488,7 @@ async function checkFile(filePath, cwd, config, verbose, debug) {
1265
1488
  return why ? `${i + 1}. ${r}
1266
1489
  WHY: ${why}` : `${i + 1}. ${r}`;
1267
1490
  }).join("\n");
1268
- const ignorePatterns = await loadIgnorePatterns2();
1491
+ const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...await loadIgnorePatterns2()])];
1269
1492
  const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
1270
1493
  Analyze the file diff and identify ONLY clear, definite rule violations.
1271
1494
  Use the WHY for each rule to understand intent and judge edge cases.
@@ -1277,7 +1500,7 @@ Things that must never appear:
1277
1500
  ${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
1278
1501
 
1279
1502
  Never flag these accepted project patterns:
1280
- ${ignorePatterns.length ? ignorePatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
1503
+ ${allowPatterns.length ? allowPatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
1281
1504
 
1282
1505
  IMPORTANT: Respond with JSON: {"violations":[...]} or {"violations":[]}.
1283
1506
  Each violation: {"rule":"...","file":"...","line":N,"issue":"...","suggestion":"...","reason":"..."}.
@@ -1316,6 +1539,8 @@ ${diffToSend}` }
1316
1539
  } catch {
1317
1540
  violations = [];
1318
1541
  }
1542
+ violations = await verifyViolations2(diff, violations, allowPatterns, debug);
1543
+ violations = applyAllowPatterns2(violations, allowPatterns);
1319
1544
  if (violations.length === 0) {
1320
1545
  console.log(chalk2.green(` \u2713 ${rel}`) + chalk2.dim(" \u2014 no violations"));
1321
1546
  return;
@@ -1357,7 +1582,7 @@ async function startWatch(options = {}) {
1357
1582
  console.error(chalk2.red("\n No .memory-core.json found. Run: memory-core init\n"));
1358
1583
  process.exit(1);
1359
1584
  }
1360
- const { rules } = getProfileRules(config);
1585
+ const { rules } = getProfileRules2(config);
1361
1586
  if (rules.length === 0) {
1362
1587
  console.log(chalk2.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to watch.\n"));
1363
1588
  process.exit(0);
@@ -1369,7 +1594,6 @@ async function startWatch(options = {}) {
1369
1594
  console.log(chalk2.dim(` rules: ${rules.length}`));
1370
1595
  console.log(chalk2.dim(" ctrl+c to stop\n"));
1371
1596
  const pending = /* @__PURE__ */ new Map();
1372
- let ollamaWarned = false;
1373
1597
  const watcher = watch(watchPath, {
1374
1598
  ignored: [
1375
1599
  "**/node_modules/**",
@@ -1392,18 +1616,6 @@ async function startWatch(options = {}) {
1392
1616
  pending.delete(filePath);
1393
1617
  console.log(chalk2.dim(`
1394
1618
  [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] saved: ${relative(cwd, filePath)}`));
1395
- try {
1396
- const ping = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(2e3) });
1397
- if (!ping.ok) throw new Error("not ok");
1398
- ollamaWarned = false;
1399
- } catch {
1400
- if (!ollamaWarned) {
1401
- console.log(chalk2.yellow(` \u26A0 Ollama not running at ${ollamaUrl} \u2014 skipping check.`));
1402
- console.log(chalk2.gray(" Start it: ollama serve\n"));
1403
- ollamaWarned = true;
1404
- }
1405
- return;
1406
- }
1407
1619
  await checkFile(filePath, cwd, config, options.verbose ?? false, options.debug ?? false);
1408
1620
  }, 300);
1409
1621
  pending.set(filePath, timer);
@@ -1454,7 +1666,7 @@ function printBanner(projectName, agentCount, status) {
1454
1666
  ];
1455
1667
  lines.forEach((l) => console.log(l));
1456
1668
  }
1457
- async function checkConnections(dbUrl, ollamaUrl2, chatModel) {
1669
+ async function checkConnections(dbUrl, ollamaUrl, chatModel) {
1458
1670
  const spinner = ora("Checking connections\u2026").start();
1459
1671
  let postgresOk = false;
1460
1672
  let ollamaOk = false;
@@ -1468,7 +1680,7 @@ async function checkConnections(dbUrl, ollamaUrl2, chatModel) {
1468
1680
  postgresOk = false;
1469
1681
  }
1470
1682
  try {
1471
- const res = await fetch(`${ollamaUrl2}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1683
+ const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1472
1684
  ollamaOk = res.ok;
1473
1685
  } catch {
1474
1686
  ollamaOk = false;
@@ -1485,6 +1697,139 @@ async function checkConnections(dbUrl, ollamaUrl2, chatModel) {
1485
1697
  }
1486
1698
  var { version } = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf-8"));
1487
1699
  var CONFIG_FILE = ".memory-core.json";
1700
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434";
1701
+ var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
1702
+ var DEFAULT_CHAT_MODEL = "llama3.2";
1703
+ function getEnvPath() {
1704
+ const memoryEnv = join6(process.cwd(), ".memory-core.env");
1705
+ if (existsSync6(memoryEnv)) return memoryEnv;
1706
+ const dotEnv = join6(process.cwd(), ".env");
1707
+ return existsSync6(dotEnv) ? dotEnv : memoryEnv;
1708
+ }
1709
+ function parseEnvFile(raw) {
1710
+ const lines = raw.split(/\r?\n/);
1711
+ const values = {};
1712
+ for (const line of lines) {
1713
+ const trimmed = line.trim();
1714
+ if (!trimmed || trimmed.startsWith("#")) continue;
1715
+ const separatorIndex = trimmed.indexOf("=");
1716
+ if (separatorIndex === -1) continue;
1717
+ const key = trimmed.slice(0, separatorIndex).trim();
1718
+ const value = trimmed.slice(separatorIndex + 1).trim();
1719
+ if (key) values[key] = value;
1720
+ }
1721
+ return values;
1722
+ }
1723
+ function readRuntimeEnv() {
1724
+ const envPath = getEnvPath();
1725
+ const fileValues = existsSync6(envPath) ? parseEnvFile(readFileSync6(envPath, "utf-8")) : {};
1726
+ const values = {
1727
+ ...fileValues
1728
+ };
1729
+ for (const [key, value] of Object.entries(process.env)) {
1730
+ if (typeof value === "string" && value !== "") values[key] = value;
1731
+ }
1732
+ return { envPath, values };
1733
+ }
1734
+ function writeRuntimeEnv(values, envPath = getEnvPath()) {
1735
+ const orderedKeys = [
1736
+ "DATABASE_URL",
1737
+ "OLLAMA_URL",
1738
+ "OLLAMA_MODEL",
1739
+ "CHAT_PROVIDER",
1740
+ "CHAT_MODEL",
1741
+ "OLLAMA_CHAT_MODEL",
1742
+ "CHAT_API_KEY"
1743
+ ];
1744
+ const seen = /* @__PURE__ */ new Set();
1745
+ const lines = [];
1746
+ for (const key of orderedKeys) {
1747
+ const value = values[key];
1748
+ if (value) {
1749
+ lines.push(`${key}=${value}`);
1750
+ seen.add(key);
1751
+ }
1752
+ }
1753
+ for (const key of Object.keys(values).sort()) {
1754
+ if (seen.has(key)) continue;
1755
+ const value = values[key];
1756
+ if (value) lines.push(`${key}=${value}`);
1757
+ }
1758
+ writeFileSync5(envPath, `${lines.join("\n")}
1759
+ `, "utf-8");
1760
+ }
1761
+ function applyRuntimeEnv(values) {
1762
+ for (const [key, value] of Object.entries(values)) {
1763
+ process.env[key] = value;
1764
+ }
1765
+ }
1766
+ function ensureEnvFileIgnored(envPath = getEnvPath()) {
1767
+ const envFileName = envPath.split("/").pop() ?? ".memory-core.env";
1768
+ const gitignorePath = join6(process.cwd(), ".gitignore");
1769
+ const existing = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
1770
+ if (!existing.includes(envFileName)) {
1771
+ appendFileSync(gitignorePath, `${existing ? "\n" : ""}${envFileName}
1772
+ `);
1773
+ }
1774
+ }
1775
+ function normalizeProvider(value) {
1776
+ const provider2 = value.trim().toLowerCase();
1777
+ if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax") {
1778
+ return provider2;
1779
+ }
1780
+ throw new Error(`Unsupported provider "${value}". Use: ollama, openai, anthropic, minimax`);
1781
+ }
1782
+ function providerLabel(provider2) {
1783
+ switch (provider2) {
1784
+ case "openai":
1785
+ return "OpenAI";
1786
+ case "anthropic":
1787
+ return "Anthropic";
1788
+ case "minimax":
1789
+ return "MiniMax";
1790
+ default:
1791
+ return "Ollama";
1792
+ }
1793
+ }
1794
+ function redactDatabaseUrl(url) {
1795
+ if (!url) return "(not set)";
1796
+ return url.replace(/:\/\/([^:@/]+)(?::[^@/]+)?@/, "://$1:***@");
1797
+ }
1798
+ function getConfiguredProvider(values) {
1799
+ return normalizeProvider(values.CHAT_PROVIDER ?? "ollama");
1800
+ }
1801
+ function getConfiguredChatModel(values) {
1802
+ return values.CHAT_MODEL ?? values.OLLAMA_CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
1803
+ }
1804
+ async function resolveOllamaInstalledModel(ollamaUrl, model2) {
1805
+ const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1806
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1807
+ const data = await res.json();
1808
+ const models = data.models ?? [];
1809
+ const exact = models.find((entry) => entry.name === model2);
1810
+ const prefixed = models.find((entry) => entry.name.startsWith(`${model2}:`));
1811
+ return (exact ?? prefixed)?.name ?? null;
1812
+ }
1813
+ async function verifyDatabaseConnection(dbUrl) {
1814
+ if (!dbUrl) return "DATABASE_URL is not set";
1815
+ try {
1816
+ const { Pool } = (await import("pg")).default;
1817
+ const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
1818
+ await testPool.query("SELECT 1");
1819
+ await testPool.end();
1820
+ return null;
1821
+ } catch (err) {
1822
+ return err.message;
1823
+ }
1824
+ }
1825
+ async function verifyOllamaConnection(ollamaUrl) {
1826
+ try {
1827
+ const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1828
+ return res.ok ? null : `HTTP ${res.status}`;
1829
+ } catch (err) {
1830
+ return err.message;
1831
+ }
1832
+ }
1488
1833
  function readProjectConfig() {
1489
1834
  const path = join6(process.cwd(), CONFIG_FILE);
1490
1835
  if (!existsSync6(path)) return null;
@@ -1497,6 +1842,19 @@ function readProjectConfig() {
1497
1842
  function writeProjectConfig(config) {
1498
1843
  writeFileSync5(join6(process.cwd(), CONFIG_FILE), JSON.stringify(config, null, 2));
1499
1844
  }
1845
+ function updateProjectConfig(mutator) {
1846
+ const current = readProjectConfig() ?? {
1847
+ projectName: process.cwd().split("/").pop() ?? "project",
1848
+ projectType: "backend",
1849
+ language: detectProject().language,
1850
+ caveman: { enabled: false, intensity: "full" },
1851
+ agents: [],
1852
+ allowPatterns: []
1853
+ };
1854
+ const updated = mutator(current);
1855
+ writeProjectConfig(updated);
1856
+ return updated;
1857
+ }
1500
1858
  function parseTags(tags) {
1501
1859
  return tags ? tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
1502
1860
  }
@@ -1519,6 +1877,130 @@ function printMemoryTable(memories, title = "Rules in memory") {
1519
1877
  });
1520
1878
  console.log(chalk3.gray("\n Use: memory-core remove <id> | memory-core edit <id>\n"));
1521
1879
  }
1880
+ function printStatusLine(label, value) {
1881
+ console.log(` ${chalk3.dim(label.padEnd(18))} ${value}`);
1882
+ }
1883
+ async function runModelDoctor() {
1884
+ const { envPath, values } = readRuntimeEnv();
1885
+ const provider2 = getConfiguredProvider(values);
1886
+ const model2 = getConfiguredChatModel(values);
1887
+ const ollamaUrl = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
1888
+ const embeddingModel = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
1889
+ const dbUrl = values.DATABASE_URL ?? "";
1890
+ console.log(chalk3.bold("\n memory-core model doctor\n"));
1891
+ printStatusLine("Env file", existsSync6(envPath) ? envPath : `${envPath} ${chalk3.yellow("(will be created on first write)")}`);
1892
+ printStatusLine("Provider", provider2);
1893
+ printStatusLine("Chat model", model2);
1894
+ printStatusLine("Embedding model", embeddingModel);
1895
+ printStatusLine("Ollama URL", ollamaUrl);
1896
+ console.log();
1897
+ let ok = true;
1898
+ const dbError = await verifyDatabaseConnection(dbUrl);
1899
+ if (dbError) {
1900
+ ok = false;
1901
+ console.log(chalk3.red(" \u2717 PostgreSQL ") + chalk3.dim(dbError));
1902
+ } else {
1903
+ console.log(chalk3.green(" \u2713 PostgreSQL ") + chalk3.dim("connected"));
1904
+ }
1905
+ const ollamaError = await verifyOllamaConnection(ollamaUrl);
1906
+ if (ollamaError) {
1907
+ ok = false;
1908
+ console.log(chalk3.red(" \u2717 Ollama ") + chalk3.dim(ollamaError));
1909
+ } else {
1910
+ console.log(chalk3.green(" \u2713 Ollama ") + chalk3.dim("reachable"));
1911
+ }
1912
+ if (!ollamaError) {
1913
+ try {
1914
+ const installedEmbeddingModel = await resolveOllamaInstalledModel(ollamaUrl, embeddingModel);
1915
+ if (installedEmbeddingModel) {
1916
+ console.log(chalk3.green(" \u2713 Embedding ") + chalk3.dim(`${installedEmbeddingModel} installed`));
1917
+ } else {
1918
+ ok = false;
1919
+ console.log(chalk3.red(" \u2717 Embedding ") + chalk3.dim(`${embeddingModel} not installed in Ollama`));
1920
+ }
1921
+ } catch (err) {
1922
+ ok = false;
1923
+ console.log(chalk3.red(" \u2717 Embedding ") + chalk3.dim(err.message));
1924
+ }
1925
+ }
1926
+ if (provider2 === "ollama") {
1927
+ if (ollamaError) {
1928
+ ok = false;
1929
+ console.log(chalk3.red(" \u2717 Chat model ") + chalk3.dim("cannot verify while Ollama is unreachable"));
1930
+ } else {
1931
+ try {
1932
+ const installedChatModel = await resolveOllamaInstalledModel(ollamaUrl, model2);
1933
+ if (installedChatModel) {
1934
+ console.log(chalk3.green(" \u2713 Chat model ") + chalk3.dim(`${installedChatModel} installed`));
1935
+ } else {
1936
+ ok = false;
1937
+ console.log(chalk3.red(" \u2717 Chat model ") + chalk3.dim(`${model2} not installed in Ollama`));
1938
+ }
1939
+ } catch (err) {
1940
+ ok = false;
1941
+ console.log(chalk3.red(" \u2717 Chat model ") + chalk3.dim(err.message));
1942
+ }
1943
+ }
1944
+ } else {
1945
+ if (!values.CHAT_API_KEY) {
1946
+ ok = false;
1947
+ console.log(chalk3.red(` \u2717 ${providerLabel(provider2)} API`) + chalk3.dim(" CHAT_API_KEY is missing"));
1948
+ } else {
1949
+ console.log(chalk3.green(` \u2713 ${providerLabel(provider2)} API`) + chalk3.dim(" key configured"));
1950
+ console.log(chalk3.gray(" Remote provider connectivity is not verified live by doctor."));
1951
+ }
1952
+ }
1953
+ console.log();
1954
+ return { ok, provider: provider2, model: model2 };
1955
+ }
1956
+ async function printProjectStatus() {
1957
+ const config = readProjectConfig();
1958
+ const { envPath, values } = readRuntimeEnv();
1959
+ const provider2 = getConfiguredProvider(values);
1960
+ const model2 = getConfiguredChatModel(values);
1961
+ const architectures = inferProjectArchitectures(process.cwd(), config);
1962
+ const generatedFiles = OUTPUT_FILES.map((entry) => entry.path).filter((relativePath) => existsSync6(join6(process.cwd(), relativePath)));
1963
+ const hookPath = join6(process.cwd(), ".git", "hooks", "pre-commit");
1964
+ const memoryFilePath = join6(process.cwd(), MEMORY_FILE);
1965
+ const statsPath = join6(process.cwd(), ".memory-core-stats.json");
1966
+ const dbError = await verifyDatabaseConnection(values.DATABASE_URL ?? "");
1967
+ const ollamaError = await verifyOllamaConnection(values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
1968
+ console.log(chalk3.bold("\n memory-core status\n"));
1969
+ printStatusLine("Project", config?.projectName ?? process.cwd().split("/").pop() ?? "unknown");
1970
+ printStatusLine("Project type", config?.projectType ?? chalk3.yellow("not initialized"));
1971
+ printStatusLine("Language", config?.language ?? detectProject().language);
1972
+ printStatusLine("Backend arch", config?.backendArchitecture ?? chalk3.gray("\u2014"));
1973
+ printStatusLine("Frontend fw", config?.frontendFramework ?? chalk3.gray("\u2014"));
1974
+ printStatusLine("Architectures", architectures.length ? architectures.join(", ") : chalk3.gray("none detected"));
1975
+ printStatusLine("Agents", config?.agents?.length ? `${config.agents.length} selected` : chalk3.gray("none saved"));
1976
+ printStatusLine("Caveman", config?.caveman?.enabled ? `enabled (${config.caveman.intensity})` : "disabled");
1977
+ printStatusLine("Allow patterns", String(getAllowPatterns(config).length));
1978
+ printStatusLine("Env file", `${existsSync6(envPath) ? "present" : "missing"} (${envPath.split("/").pop()})`);
1979
+ printStatusLine("Memory file", existsSync6(memoryFilePath) ? MEMORY_FILE : chalk3.gray("not exported"));
1980
+ printStatusLine("Project config", existsSync6(join6(process.cwd(), CONFIG_FILE)) ? CONFIG_FILE : chalk3.gray("missing"));
1981
+ printStatusLine("Generated files", String(generatedFiles.length));
1982
+ printStatusLine("Hook", existsSync6(hookPath) ? "installed" : "not installed");
1983
+ printStatusLine("Stats file", existsSync6(statsPath) ? ".memory-core-stats.json" : chalk3.gray("none"));
1984
+ console.log();
1985
+ printStatusLine("Database URL", redactDatabaseUrl(values.DATABASE_URL ?? ""));
1986
+ printStatusLine("Ollama URL", values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
1987
+ printStatusLine("Embedding model", values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL);
1988
+ printStatusLine("Chat provider", provider2);
1989
+ printStatusLine("Chat model", model2);
1990
+ console.log();
1991
+ console.log(
1992
+ dbError ? chalk3.red(" \u2717 PostgreSQL ") + chalk3.dim(dbError) : chalk3.green(" \u2713 PostgreSQL ") + chalk3.dim("connected")
1993
+ );
1994
+ console.log(
1995
+ ollamaError ? chalk3.red(" \u2717 Ollama ") + chalk3.dim(ollamaError) : chalk3.green(" \u2713 Ollama ") + chalk3.dim("reachable")
1996
+ );
1997
+ if (provider2 !== "ollama") {
1998
+ console.log(
1999
+ values.CHAT_API_KEY ? chalk3.green(` \u2713 ${providerLabel(provider2)} API`) + chalk3.dim(" key configured") : chalk3.red(` \u2717 ${providerLabel(provider2)} API`) + chalk3.dim(" CHAT_API_KEY is missing")
2000
+ );
2001
+ }
2002
+ console.log();
2003
+ }
1522
2004
  var program = new Command();
1523
2005
  program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version(version);
1524
2006
  program.command("init").description("Initialize memory-core in the current project").option("--quick", "Use smart defaults and skip optional prompts").action(async (opts) => {
@@ -1530,25 +2012,19 @@ program.command("init").description("Initialize memory-core in the current proje
1530
2012
  if (!hasEnv && quick) {
1531
2013
  const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
1532
2014
  const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
1533
- const ollamaUrl2 = "http://localhost:11434";
1534
- const chatModel = "llama3.2";
1535
- const envContent = [
1536
- `DATABASE_URL=${dbUrl}`,
1537
- `OLLAMA_URL=${ollamaUrl2}`,
1538
- `OLLAMA_MODEL=nomic-embed-text`,
1539
- `OLLAMA_CHAT_MODEL=${chatModel}`
1540
- ].join("\n") + "\n";
1541
- writeFileSync5(envPath, envContent);
1542
- process.env.DATABASE_URL = dbUrl;
1543
- process.env.OLLAMA_URL = ollamaUrl2;
1544
- process.env.OLLAMA_MODEL = "nomic-embed-text";
1545
- process.env.OLLAMA_CHAT_MODEL = chatModel;
1546
- const gitignorePath2 = join6(process.cwd(), ".gitignore");
1547
- const gitignore = existsSync6(gitignorePath2) ? readFileSync6(gitignorePath2, "utf-8") : "";
1548
- if (!gitignore.includes(".memory-core.env")) {
1549
- appendFileSync(gitignorePath2, `${gitignore ? "\n" : ""}.memory-core.env
1550
- `);
1551
- }
2015
+ const ollamaUrl = DEFAULT_OLLAMA_URL;
2016
+ const chatModel = DEFAULT_CHAT_MODEL;
2017
+ const envValues = {
2018
+ DATABASE_URL: dbUrl,
2019
+ OLLAMA_URL: ollamaUrl,
2020
+ OLLAMA_MODEL: DEFAULT_EMBEDDING_MODEL,
2021
+ CHAT_PROVIDER: "ollama",
2022
+ CHAT_MODEL: chatModel,
2023
+ OLLAMA_CHAT_MODEL: chatModel
2024
+ };
2025
+ writeRuntimeEnv(envValues, envPath);
2026
+ applyRuntimeEnv(envValues);
2027
+ ensureEnvFileIgnored(envPath);
1552
2028
  console.log(chalk3.green(" \u2713 .memory-core.env created with local defaults"));
1553
2029
  } else if (!hasEnv) {
1554
2030
  console.log(chalk3.dim(" No .memory-core.env found \u2014 let's set up your connection.\n"));
@@ -1572,15 +2048,15 @@ program.command("init").description("Initialize memory-core in the current proje
1572
2048
  console.log(chalk3.yellow(" Please check that PostgreSQL is running and the URL is correct.\n"));
1573
2049
  }
1574
2050
  }
1575
- let ollamaUrl2 = "";
2051
+ let ollamaUrl = "";
1576
2052
  while (true) {
1577
- ollamaUrl2 = await input({
2053
+ ollamaUrl = await input({
1578
2054
  message: "Ollama URL?",
1579
- default: ollamaUrl2 || "http://localhost:11434"
2055
+ default: ollamaUrl || "http://localhost:11434"
1580
2056
  });
1581
2057
  const ollamaSpinner = ora(" Testing Ollama connection\u2026").start();
1582
2058
  try {
1583
- const res = await fetch(`${ollamaUrl2}/api/tags`, { signal: AbortSignal.timeout(5e3) });
2059
+ const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1584
2060
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1585
2061
  ollamaSpinner.succeed(chalk3.green("Ollama connected"));
1586
2062
  break;
@@ -1615,7 +2091,7 @@ program.command("init").description("Initialize memory-core in the current proje
1615
2091
  chatModel = chatModelChoice === "__custom__" ? await input({ message: "Model name?", default: "llama3.2" }) : chatModelChoice;
1616
2092
  const modelSpinner = ora(` Checking if ${chatModel} is installed\u2026`).start();
1617
2093
  try {
1618
- const res = await fetch(`${ollamaUrl2}/api/tags`, { signal: AbortSignal.timeout(5e3) });
2094
+ const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1619
2095
  const data = await res.json();
1620
2096
  const models = data.models ?? [];
1621
2097
  const exact = models.find((m) => m.name === chatModel);
@@ -1664,33 +2140,18 @@ program.command("init").description("Initialize memory-core in the current proje
1664
2140
  });
1665
2141
  console.log(chalk3.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
1666
2142
  }
1667
- const envLines = [
1668
- `DATABASE_URL=${dbUrl}`,
1669
- `OLLAMA_URL=${ollamaUrl2}`,
1670
- `OLLAMA_MODEL=nomic-embed-text`,
1671
- `CHAT_PROVIDER=${chatProvider}`,
1672
- `CHAT_MODEL=${chatModel}`
1673
- ];
1674
- if (chatProvider === "ollama") envLines.push(`OLLAMA_CHAT_MODEL=${chatModel}`);
1675
- if (chatApiKey) envLines.push(`CHAT_API_KEY=${chatApiKey}`);
1676
- const envContent = envLines.join("\n") + "\n";
1677
- writeFileSync5(envPath, envContent);
1678
- process.env.DATABASE_URL = dbUrl;
1679
- process.env.OLLAMA_URL = ollamaUrl2;
1680
- process.env.OLLAMA_MODEL = "nomic-embed-text";
1681
- process.env.CHAT_PROVIDER = chatProvider;
1682
- process.env.CHAT_MODEL = chatModel;
1683
- if (chatProvider === "ollama") process.env.OLLAMA_CHAT_MODEL = chatModel;
1684
- if (chatApiKey) process.env.CHAT_API_KEY = chatApiKey;
1685
- const gitignorePath2 = join6(process.cwd(), ".gitignore");
1686
- if (existsSync6(gitignorePath2)) {
1687
- const gi = readFileSync6(gitignorePath2, "utf-8");
1688
- if (!gi.includes(".memory-core.env")) {
1689
- appendFileSync(gitignorePath2, "\n.memory-core.env\n");
1690
- }
1691
- } else {
1692
- writeFileSync5(gitignorePath2, ".memory-core.env\n");
1693
- }
2143
+ const envValues = {
2144
+ DATABASE_URL: dbUrl,
2145
+ OLLAMA_URL: ollamaUrl,
2146
+ OLLAMA_MODEL: DEFAULT_EMBEDDING_MODEL,
2147
+ CHAT_PROVIDER: chatProvider,
2148
+ CHAT_MODEL: chatModel
2149
+ };
2150
+ if (chatProvider === "ollama") envValues.OLLAMA_CHAT_MODEL = chatModel;
2151
+ if (chatApiKey) envValues.CHAT_API_KEY = chatApiKey;
2152
+ writeRuntimeEnv(envValues, envPath);
2153
+ applyRuntimeEnv(envValues);
2154
+ ensureEnvFileIgnored(envPath);
1694
2155
  console.log(chalk3.green("\n \u2713 .memory-core.env created"));
1695
2156
  console.log(chalk3.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
1696
2157
  }
@@ -1796,14 +2257,20 @@ program.command("init").description("Initialize memory-core in the current proje
1796
2257
  frontendFramework,
1797
2258
  language,
1798
2259
  caveman: { enabled: installCaveman, intensity: cavemanIntensity },
1799
- agents: selectedAgents
2260
+ agents: selectedAgents,
2261
+ allowPatterns: []
1800
2262
  };
1801
2263
  let memories = [];
1802
2264
  if (pullMemories) {
1803
2265
  const spinner2 = ora("Retrieving relevant memories\u2026").start();
1804
2266
  try {
1805
2267
  const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
1806
- memories = await retrieve(archQuery, backendArchitecture ?? frontendFramework, 10);
2268
+ memories = await retrieveContextualMemories({
2269
+ query: archQuery,
2270
+ cwd: process.cwd(),
2271
+ config,
2272
+ limit: 20
2273
+ });
1807
2274
  spinner2.succeed(`Found ${memories.length} relevant memories`);
1808
2275
  } catch (err) {
1809
2276
  spinner2.warn(`Could not retrieve memories: ${err.message}`);
@@ -1876,7 +2343,12 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
1876
2343
  let memories = [];
1877
2344
  try {
1878
2345
  const archQuery = [config.backendArchitecture, config.frontendFramework, config.language].filter(Boolean).join(" ");
1879
- memories = await retrieve(archQuery, config.backendArchitecture ?? config.frontendFramework, 10);
2346
+ memories = await retrieveContextualMemories({
2347
+ query: archQuery,
2348
+ cwd: process.cwd(),
2349
+ config,
2350
+ limit: 25
2351
+ });
1880
2352
  spinner.text = `Found ${memories.length} memories \u2014 regenerating files\u2026`;
1881
2353
  } catch (err) {
1882
2354
  spinner.warn(`Could not retrieve memories: ${err.message}`);
@@ -1939,9 +2411,10 @@ program.command("search <query>").description("Search memories using semantic si
1939
2411
  const config = readProjectConfig();
1940
2412
  const spinner = ora("Searching\u2026").start();
1941
2413
  try {
2414
+ const architectures = inferProjectArchitectures(process.cwd(), config);
1942
2415
  const results = await retrieve(
1943
2416
  query,
1944
- config?.backendArchitecture ?? config?.frontendFramework,
2417
+ architectures,
1945
2418
  parseInt(opts.limit, 10)
1946
2419
  );
1947
2420
  spinner.stop();
@@ -2044,6 +2517,22 @@ program.command("remove <id>").description("Remove a memory by ID").action(async
2044
2517
  await closePool();
2045
2518
  }
2046
2519
  });
2520
+ program.command("forget").description("Bulk-delete memories by tag, scope, type, or architecture").option("--tag <tag>", "Delete memories with this tag").option("--scope <scope>", "Filter by scope").option("--type <type>", "Filter by type").option("--arch <architecture>", "Filter by architecture").action(async (opts) => {
2521
+ try {
2522
+ const deleted = await deleteMemories({
2523
+ tag: opts.tag,
2524
+ scope: opts.scope,
2525
+ type: opts.type,
2526
+ architecture: opts.arch
2527
+ });
2528
+ console.log(chalk3.green(`Deleted ${deleted} memories`));
2529
+ } catch (err) {
2530
+ console.error(chalk3.red(`Forget failed: ${err.message}`));
2531
+ process.exit(1);
2532
+ } finally {
2533
+ await closePool();
2534
+ }
2535
+ });
2047
2536
  program.command("edit <id>").description("Edit a memory interactively").action(async (id) => {
2048
2537
  const memoryId = parseInt(id, 10);
2049
2538
  try {
@@ -2114,6 +2603,36 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
2114
2603
  await closePool();
2115
2604
  }
2116
2605
  });
2606
+ program.command("allow [pattern]").description("Manage project allow patterns in .memory-core.json").option("--list", "List current allow patterns").option("--remove <pattern>", "Remove an allow pattern").action((pattern, opts) => {
2607
+ if (opts.list) {
2608
+ const patterns = getAllowPatterns(readProjectConfig());
2609
+ if (patterns.length === 0) {
2610
+ console.log(chalk3.yellow("\n No allow patterns configured.\n"));
2611
+ return;
2612
+ }
2613
+ console.log(chalk3.bold("\n Allow patterns\n"));
2614
+ patterns.forEach((entry, index) => console.log(` ${index + 1}. ${entry}`));
2615
+ console.log();
2616
+ return;
2617
+ }
2618
+ if (opts.remove) {
2619
+ updateProjectConfig((config) => ({
2620
+ ...config,
2621
+ allowPatterns: getAllowPatterns(config).filter((entry) => entry !== opts.remove)
2622
+ }));
2623
+ console.log(chalk3.green(`Removed allow pattern: "${opts.remove}"`));
2624
+ return;
2625
+ }
2626
+ if (!pattern) {
2627
+ console.error(chalk3.red("Provide a pattern, --list, or --remove <pattern>"));
2628
+ process.exit(1);
2629
+ }
2630
+ updateProjectConfig((config) => ({
2631
+ ...config,
2632
+ allowPatterns: [.../* @__PURE__ */ new Set([...getAllowPatterns(config), pattern])]
2633
+ }));
2634
+ console.log(chalk3.green(`Allow pattern saved: "${pattern}"`));
2635
+ });
2117
2636
  program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
2118
2637
  const workflowPath = join6(process.cwd(), ".github", "workflows", "memory-core.yml");
2119
2638
  mkdirSync2(dirname2(workflowPath), { recursive: true });
@@ -2154,7 +2673,7 @@ program.command("reset").description("Remove memory-core generated files and loc
2154
2673
  default: false
2155
2674
  });
2156
2675
  if (ok) {
2157
- const { getPool } = await import("./db-KU4EEG4Y.js");
2676
+ const { getPool } = await import("./db-5X5LTUCB.js");
2158
2677
  await getPool().query("DROP TABLE IF EXISTS memories");
2159
2678
  await closePool();
2160
2679
  console.log(chalk3.yellow("Dropped memories table"));
@@ -2250,7 +2769,8 @@ program.command("global").description("Sync your memory into every AI agent glob
2250
2769
  const spinner = ora("Fetching global memories\u2026").start();
2251
2770
  let memories = [];
2252
2771
  try {
2253
- memories = await retrieve("architecture rules coding standards", opts.arch, 20);
2772
+ const architectures = opts.arch ? [opts.arch] : inferProjectArchitectures(process.cwd(), readProjectConfig());
2773
+ memories = dedupeMemories(await retrieve("architecture rules coding standards", architectures, 30));
2254
2774
  } catch (err) {
2255
2775
  spinner.fail(`Could not fetch memories: ${err.message}`);
2256
2776
  process.exit(1);
@@ -2333,6 +2853,82 @@ read:
2333
2853
  console.log(chalk3.bold("\n Every AI agent now follows your memory globally.\n"));
2334
2854
  await closePool();
2335
2855
  });
2856
+ var provider = program.command("provider").description("Manage the code-checking provider configuration");
2857
+ 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) => {
2858
+ try {
2859
+ const providerName = normalizeProvider(name);
2860
+ const runtimeEnv = readRuntimeEnv();
2861
+ const values = { ...runtimeEnv.values };
2862
+ values.CHAT_PROVIDER = providerName;
2863
+ values.OLLAMA_URL = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
2864
+ values.OLLAMA_MODEL = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
2865
+ if (opts.model) {
2866
+ values.CHAT_MODEL = opts.model;
2867
+ } else if (!values.CHAT_MODEL && !values.OLLAMA_CHAT_MODEL) {
2868
+ values.CHAT_MODEL = DEFAULT_CHAT_MODEL;
2869
+ }
2870
+ if (providerName === "ollama") {
2871
+ const model2 = values.CHAT_MODEL ?? values.OLLAMA_CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
2872
+ values.CHAT_MODEL = model2;
2873
+ values.OLLAMA_CHAT_MODEL = model2;
2874
+ delete values.CHAT_API_KEY;
2875
+ } else {
2876
+ delete values.OLLAMA_CHAT_MODEL;
2877
+ if (opts.apiKey) {
2878
+ values.CHAT_API_KEY = opts.apiKey;
2879
+ } else if (!values.CHAT_API_KEY) {
2880
+ values.CHAT_API_KEY = await input({
2881
+ message: `${providerLabel(providerName)} API key?`
2882
+ });
2883
+ }
2884
+ }
2885
+ writeRuntimeEnv(values, runtimeEnv.envPath);
2886
+ applyRuntimeEnv(values);
2887
+ ensureEnvFileIgnored(runtimeEnv.envPath);
2888
+ console.log(chalk3.green(`Updated provider: ${providerName}`));
2889
+ console.log(chalk3.gray(` Chat model: ${getConfiguredChatModel(values)}`));
2890
+ } catch (err) {
2891
+ console.error(chalk3.red(`Provider update failed: ${err.message}`));
2892
+ process.exit(1);
2893
+ }
2894
+ });
2895
+ var model = program.command("model").description("Manage code-checking and embedding models");
2896
+ model.command("set <name>").description("Set the chat model used for code checking").option("--provider <provider>", "Override provider while setting the model").option("--embedding", "Set the embedding model instead of the chat model").action(async (name, opts) => {
2897
+ try {
2898
+ const runtimeEnv = readRuntimeEnv();
2899
+ const values = { ...runtimeEnv.values };
2900
+ if (opts.provider) values.CHAT_PROVIDER = normalizeProvider(opts.provider);
2901
+ const providerName = getConfiguredProvider(values);
2902
+ if (opts.embedding) {
2903
+ values.OLLAMA_MODEL = name;
2904
+ } else {
2905
+ values.CHAT_MODEL = name;
2906
+ if (providerName === "ollama") values.OLLAMA_CHAT_MODEL = name;
2907
+ }
2908
+ values.OLLAMA_URL = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
2909
+ values.OLLAMA_MODEL = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
2910
+ writeRuntimeEnv(values, runtimeEnv.envPath);
2911
+ applyRuntimeEnv(values);
2912
+ ensureEnvFileIgnored(runtimeEnv.envPath);
2913
+ console.log(chalk3.green(`Updated ${opts.embedding ? "embedding" : "chat"} model: ${name}`));
2914
+ console.log(chalk3.gray(` Provider: ${providerName}`));
2915
+ } catch (err) {
2916
+ console.error(chalk3.red(`Model update failed: ${err.message}`));
2917
+ process.exit(1);
2918
+ }
2919
+ });
2920
+ model.command("doctor").description("Verify provider, model, database, and Ollama setup").action(async () => {
2921
+ const result = await runModelDoctor();
2922
+ if (!result.ok) process.exit(1);
2923
+ });
2924
+ program.command("status").description("Show the current memory-core project and runtime setup").action(async () => {
2925
+ try {
2926
+ await printProjectStatus();
2927
+ } catch (err) {
2928
+ console.error(chalk3.red(`Status failed: ${err.message}`));
2929
+ process.exit(1);
2930
+ }
2931
+ });
2336
2932
  var hook = program.command("hook").description("Manage the pre-commit rule enforcement hook");
2337
2933
  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) => {
2338
2934
  const advisory = opts.strict ? false : true;