@shahmilsaari/memory-core 0.2.8 → 0.2.10

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
@@ -1,14 +1,29 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ embed
4
+ } from "./chunk-HAGRPKR3.js";
5
+ import {
6
+ closePool,
7
+ deleteMemory,
8
+ getMemory,
9
+ listMemories,
10
+ runMigrations,
11
+ saveMemory,
12
+ searchMemories,
13
+ updateMemory,
14
+ upsertMemory
15
+ } from "./chunk-73SRPNAL.js";
16
+ import "./chunk-KSLFLWB4.js";
2
17
 
3
18
  // src/cli.ts
4
19
  import { Command } from "commander";
5
20
  import { input, select, confirm } from "@inquirer/prompts";
6
21
  import chalk3 from "chalk";
7
22
  import ora from "ora";
8
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync2, appendFileSync } from "fs";
23
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync2, appendFileSync, rmSync, unlinkSync as unlinkSync2 } from "fs";
9
24
  import { join as join6, dirname as dirname2 } from "path";
10
25
  import { homedir } from "os";
11
- import { execSync as execSync3 } from "child_process";
26
+ import { execSync as execSync2 } from "child_process";
12
27
 
13
28
  // src/generator.ts
14
29
  import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync } from "fs";
@@ -535,109 +550,6 @@ var seeds = [
535
550
  { 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"] }
536
551
  ];
537
552
 
538
- // src/config.ts
539
- import { config } from "dotenv";
540
- import { existsSync as existsSync2 } from "fs";
541
- import { join as join2 } from "path";
542
- var localEnv = join2(process.cwd(), ".memory-core.env");
543
- config({ path: existsSync2(localEnv) ? localEnv : join2(process.cwd(), ".env") });
544
- var Config = {
545
- get databaseUrl() {
546
- return process.env.DATABASE_URL ?? "";
547
- },
548
- get ollamaUrl() {
549
- return process.env.OLLAMA_URL ?? "http://localhost:11434";
550
- },
551
- get ollamaModel() {
552
- return process.env.OLLAMA_MODEL ?? "nomic-embed-text";
553
- },
554
- get chatModel() {
555
- return process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
556
- }
557
- };
558
-
559
- // src/embedding.ts
560
- async function embed(text) {
561
- let response;
562
- try {
563
- response = await fetch(`${Config.ollamaUrl}/api/embeddings`, {
564
- method: "POST",
565
- headers: { "Content-Type": "application/json" },
566
- body: JSON.stringify({ model: Config.ollamaModel, prompt: text })
567
- });
568
- } catch {
569
- throw new Error(
570
- `Cannot reach Ollama at ${Config.ollamaUrl}. Run: ollama serve`
571
- );
572
- }
573
- if (!response.ok) {
574
- const body = await response.text();
575
- throw new Error(`Ollama embedding failed (${response.status}): ${body}`);
576
- }
577
- const data = await response.json();
578
- return data.embedding;
579
- }
580
-
581
- // src/db.ts
582
- import pg from "pg";
583
- var { Pool } = pg;
584
- var pool = null;
585
- function getPool() {
586
- if (!pool) {
587
- if (!Config.databaseUrl) {
588
- throw new Error("DATABASE_URL is not set. Add it to your .env or .memory-core.env file.");
589
- }
590
- pool = new Pool({ connectionString: Config.databaseUrl });
591
- }
592
- return pool;
593
- }
594
- async function runMigrations() {
595
- await getPool().query(
596
- `ALTER TABLE memories ADD COLUMN IF NOT EXISTS reason TEXT`
597
- );
598
- }
599
- async function saveMemory(memory) {
600
- const { type, scope, architecture, projectName, title, content, reason, tags, embedding } = memory;
601
- await getPool().query(
602
- `INSERT INTO memories (type, scope, architecture, project_name, title, content, reason, tags, embedding)
603
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
604
- [type, scope, architecture ?? null, projectName ?? null, title ?? null, content, reason ?? null, tags ?? [], `[${embedding.join(",")}]`]
605
- );
606
- }
607
- async function searchMemories(embedding, architecture, limit = 10) {
608
- const vector = `[${embedding.join(",")}]`;
609
- const params = [vector];
610
- let whereClause = "";
611
- if (architecture) {
612
- whereClause = `WHERE (architecture = $2 OR scope = 'global')`;
613
- params.push(architecture);
614
- }
615
- const client = await getPool().connect();
616
- try {
617
- await client.query("BEGIN");
618
- await client.query("SET LOCAL ivfflat.probes = 10");
619
- const result = await client.query(
620
- `SELECT id, type, scope, architecture, project_name, title, content, reason, tags,
621
- 1 - (embedding <=> $1) AS similarity
622
- FROM memories
623
- ${whereClause}
624
- ORDER BY embedding <=> $1
625
- LIMIT $${params.length + 1}`,
626
- [...params, limit]
627
- );
628
- await client.query("COMMIT");
629
- return result.rows;
630
- } finally {
631
- client.release();
632
- }
633
- }
634
- async function closePool() {
635
- if (pool) {
636
- await pool.end();
637
- pool = null;
638
- }
639
- }
640
-
641
553
  // src/retriever.ts
642
554
  async function retrieve(query, architecture, limit = 10) {
643
555
  const embedding = await embed(query);
@@ -645,13 +557,13 @@ async function retrieve(query, architecture, limit = 10) {
645
557
  }
646
558
 
647
559
  // src/project-detector.ts
648
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
649
- import { join as join3 } from "path";
560
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
561
+ import { join as join2 } from "path";
650
562
  function detectProject(cwd = process.cwd()) {
651
- const has = (file) => existsSync3(join3(cwd, file));
563
+ const has = (file) => existsSync2(join2(cwd, file));
652
564
  const readJson = (file) => {
653
565
  try {
654
- return JSON.parse(readFileSync2(join3(cwd, file), "utf-8"));
566
+ return JSON.parse(readFileSync2(join2(cwd, file), "utf-8"));
655
567
  } catch {
656
568
  return {};
657
569
  }
@@ -667,7 +579,7 @@ function detectProject(cwd = process.cwd()) {
667
579
  }
668
580
  if (has("manage.py")) {
669
581
  if (has("requirements.txt")) {
670
- const req = readFileSync2(join3(cwd, "requirements.txt"), "utf-8");
582
+ const req = readFileSync2(join2(cwd, "requirements.txt"), "utf-8");
671
583
  if (req.includes("djangorestframework")) {
672
584
  return { language: "Python", framework: "Django REST Framework" };
673
585
  }
@@ -707,24 +619,172 @@ function detectProject(cwd = process.cwd()) {
707
619
  }
708
620
 
709
621
  // src/hook.ts
710
- import { execSync } from "child_process";
711
- import { writeFileSync as writeFileSync2, existsSync as existsSync4, unlinkSync, readFileSync as readFileSync3, chmodSync } from "fs";
622
+ import { execSync, spawnSync } from "child_process";
623
+ import { writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync, readFileSync as readFileSync4, chmodSync } from "fs";
712
624
  import { join as join4 } from "path";
713
625
  import chalk from "chalk";
714
- async function resolveModel(ollamaUrl, chatModel) {
715
- try {
716
- const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3e3) });
717
- if (!res.ok) return chatModel;
718
- const data = await res.json();
719
- const models = data.models ?? [];
720
- const exact = models.find((m) => m.name === chatModel);
721
- if (exact) return exact.name;
722
- const prefixed = models.find((m) => m.name.startsWith(`${chatModel}:`));
723
- if (prefixed) return prefixed.name;
724
- } catch {
626
+
627
+ // src/chat.ts
628
+ function getChatConfig() {
629
+ const provider = process.env.CHAT_PROVIDER ?? "ollama";
630
+ const model = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
631
+ return {
632
+ provider,
633
+ model,
634
+ ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
635
+ apiKey: process.env.CHAT_API_KEY ?? ""
636
+ };
637
+ }
638
+ async function callOllama(cfg, messages) {
639
+ const res = await fetch(`${cfg.ollamaUrl}/api/chat`, {
640
+ method: "POST",
641
+ headers: { "Content-Type": "application/json" },
642
+ body: JSON.stringify({ model: cfg.model, messages, stream: false, format: "json" })
643
+ });
644
+ if (!res.ok) {
645
+ const body = await res.text();
646
+ if (body.includes("not found") || body.includes("model")) {
647
+ throw new Error(`MODEL_NOT_FOUND:${cfg.model}`);
648
+ }
649
+ throw new Error(body);
725
650
  }
726
- return chatModel;
651
+ const data = await res.json();
652
+ return data.message.content.trim();
727
653
  }
654
+ async function callOpenAI(cfg, messages) {
655
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
656
+ method: "POST",
657
+ headers: {
658
+ "Content-Type": "application/json",
659
+ "Authorization": `Bearer ${cfg.apiKey}`
660
+ },
661
+ body: JSON.stringify({
662
+ model: cfg.model,
663
+ messages,
664
+ response_format: { type: "json_object" }
665
+ })
666
+ });
667
+ if (!res.ok) throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
668
+ const data = await res.json();
669
+ return data.choices[0].message.content.trim();
670
+ }
671
+ async function callAnthropic(cfg, messages) {
672
+ const system = messages.find((m) => m.role === "system")?.content ?? "";
673
+ const userMessages = messages.filter((m) => m.role !== "system");
674
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
675
+ method: "POST",
676
+ headers: {
677
+ "Content-Type": "application/json",
678
+ "x-api-key": cfg.apiKey,
679
+ "anthropic-version": "2023-06-01"
680
+ },
681
+ body: JSON.stringify({
682
+ model: cfg.model,
683
+ max_tokens: 4096,
684
+ system,
685
+ messages: userMessages
686
+ })
687
+ });
688
+ if (!res.ok) throw new Error(`Anthropic API error ${res.status}: ${await res.text()}`);
689
+ const data = await res.json();
690
+ return data.content[0].text.trim();
691
+ }
692
+ async function callMiniMax(cfg, messages) {
693
+ const res = await fetch("https://api.minimax.io/v1/chat/completions", {
694
+ method: "POST",
695
+ headers: {
696
+ "Content-Type": "application/json",
697
+ "Authorization": `Bearer ${cfg.apiKey}`
698
+ },
699
+ body: JSON.stringify({
700
+ model: cfg.model,
701
+ messages,
702
+ response_format: { type: "json_object" }
703
+ })
704
+ });
705
+ if (!res.ok) throw new Error(`MiniMax API error ${res.status}: ${await res.text()}`);
706
+ const data = await res.json();
707
+ return data.choices[0].message.content.trim();
708
+ }
709
+ async function callChatModel(messages) {
710
+ const cfg = getChatConfig();
711
+ switch (cfg.provider) {
712
+ case "openai":
713
+ return callOpenAI(cfg, messages);
714
+ case "anthropic":
715
+ return callAnthropic(cfg, messages);
716
+ case "minimax":
717
+ return callMiniMax(cfg, messages);
718
+ default:
719
+ return callOllama(cfg, messages);
720
+ }
721
+ }
722
+ function getChatProviderLabel() {
723
+ const cfg = getChatConfig();
724
+ if (cfg.provider === "ollama") return `ollama (${cfg.model})`;
725
+ return `${cfg.provider} (${cfg.model})`;
726
+ }
727
+
728
+ // src/memory-file.ts
729
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
730
+ import { join as join3 } from "path";
731
+ var MEMORY_FILE = "memories.json";
732
+ function toPortableMemory(memory) {
733
+ return {
734
+ type: memory.type,
735
+ scope: memory.scope,
736
+ architecture: memory.architecture,
737
+ projectName: memory.project_name,
738
+ title: memory.title,
739
+ content: memory.content,
740
+ reason: memory.reason,
741
+ tags: memory.tags ?? []
742
+ };
743
+ }
744
+ function writeMemoryFile(memories, cwd = process.cwd()) {
745
+ const path = join3(cwd, MEMORY_FILE);
746
+ writeFileSync2(path, JSON.stringify(memories, null, 2) + "\n", "utf-8");
747
+ return path;
748
+ }
749
+ function readMemoryFile(cwd = process.cwd()) {
750
+ const path = join3(cwd, MEMORY_FILE);
751
+ if (!existsSync3(path)) {
752
+ throw new Error(`${MEMORY_FILE} not found. Run: memory-core export`);
753
+ }
754
+ return parseMemoryFile(readFileSync3(path, "utf-8"));
755
+ }
756
+ async function readMemoryFileFromUrl(url) {
757
+ const res = await fetch(url, { signal: AbortSignal.timeout(15e3) });
758
+ if (!res.ok) throw new Error(`Failed to download ${url}: HTTP ${res.status}`);
759
+ return parseMemoryFile(await res.text());
760
+ }
761
+ function parseMemoryFile(raw) {
762
+ const parsed = JSON.parse(raw);
763
+ if (!Array.isArray(parsed)) {
764
+ throw new Error(`${MEMORY_FILE} must be a JSON array`);
765
+ }
766
+ return parsed.map((item, index) => {
767
+ if (!item || typeof item !== "object") {
768
+ throw new Error(`Memory at index ${index} must be an object`);
769
+ }
770
+ const record = item;
771
+ if (typeof record.content !== "string" || record.content.trim() === "") {
772
+ throw new Error(`Memory at index ${index} is missing content`);
773
+ }
774
+ return {
775
+ type: typeof record.type === "string" ? record.type : "rule",
776
+ scope: typeof record.scope === "string" ? record.scope : "project",
777
+ architecture: typeof record.architecture === "string" ? record.architecture : void 0,
778
+ projectName: typeof record.projectName === "string" ? record.projectName : void 0,
779
+ title: typeof record.title === "string" ? record.title : void 0,
780
+ content: record.content,
781
+ reason: typeof record.reason === "string" ? record.reason : void 0,
782
+ tags: Array.isArray(record.tags) ? record.tags.filter((tag) => typeof tag === "string") : []
783
+ };
784
+ });
785
+ }
786
+
787
+ // src/hook.ts
728
788
  var reasonMap = new Map(
729
789
  seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
730
790
  );
@@ -745,6 +805,63 @@ else
745
805
  fi
746
806
  `;
747
807
  }
808
+ function recordViolations(violations) {
809
+ const statsPath = join4(process.cwd(), ".memory-core-stats.json");
810
+ let stats = { rules: {}, files: {} };
811
+ if (existsSync4(statsPath)) {
812
+ try {
813
+ stats = JSON.parse(readFileSync4(statsPath, "utf-8"));
814
+ } catch {
815
+ stats = { rules: {}, files: {} };
816
+ }
817
+ }
818
+ for (const violation of violations) {
819
+ stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
820
+ if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
821
+ }
822
+ writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
823
+ }
824
+ async function promptToSaveViolations(violations) {
825
+ if (!process.stdin.isTTY || violations.length === 0) return;
826
+ try {
827
+ const { confirm: confirm2, input: input2 } = await import("@inquirer/prompts");
828
+ const save = await confirm2({
829
+ message: "Save a caught violation as a project rule?",
830
+ default: false
831
+ });
832
+ if (!save) return;
833
+ const choices = violations.map((violation, index) => `${index + 1}. ${violation.rule}`);
834
+ const selected = violations.length === 1 ? violations[0] : violations[Number(await input2({ message: `Which violation? ${choices.join(" | ")}`, default: "1" })) - 1] ?? violations[0];
835
+ const reason = await input2({
836
+ message: "Why should this rule exist?",
837
+ default: selected.reason ?? selected.issue ?? ""
838
+ });
839
+ const { embed: embed2 } = await import("./embedding-PAYD2JYW.js");
840
+ const { upsertMemory: upsertMemory2 } = await import("./db-KU4EEG4Y.js");
841
+ await upsertMemory2({
842
+ type: "rule",
843
+ scope: "project",
844
+ content: selected.rule,
845
+ reason: reason || void 0,
846
+ tags: ["violation"],
847
+ embedding: await embed2(selected.rule)
848
+ });
849
+ console.log(chalk.green(" \u2713 Saved as project rule. Run memory-core sync to propagate it.\n"));
850
+ } catch (err) {
851
+ console.log(chalk.yellow(` Could not save violation: ${err.message}
852
+ `));
853
+ }
854
+ }
855
+ async function loadIgnorePatterns() {
856
+ try {
857
+ const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-KU4EEG4Y.js");
858
+ const ignores = await listMemories2({ type: "ignore", limit: 1e3 });
859
+ await closePool2();
860
+ return ignores.map((ignore) => ignore.content);
861
+ } catch {
862
+ return [];
863
+ }
864
+ }
748
865
  function installHook(advisory = true) {
749
866
  if (!existsSync4(".git")) {
750
867
  console.error(chalk.red("\n Not a git repository. Run from project root.\n"));
@@ -752,19 +869,19 @@ function installHook(advisory = true) {
752
869
  }
753
870
  const script = buildHookScript(advisory);
754
871
  if (existsSync4(HOOK_PATH)) {
755
- const existing = readFileSync3(HOOK_PATH, "utf-8");
872
+ const existing = readFileSync4(HOOK_PATH, "utf-8");
756
873
  if (existing.includes(HOOK_MARKER)) {
757
874
  const markerIndex = existing.indexOf(HOOK_MARKER);
758
875
  const before = markerIndex > 1 ? existing.slice(0, markerIndex).trimEnd() + "\n\n" : "";
759
- writeFileSync2(HOOK_PATH, before + script);
876
+ writeFileSync3(HOOK_PATH, before + script);
760
877
  chmodSync(HOOK_PATH, 493);
761
878
  const modeLabel2 = advisory ? chalk.cyan("advisory") : chalk.yellow("strict");
762
879
  console.log(chalk.green("\n \u2713 Pre-commit hook updated") + chalk.dim(` (${modeLabel2} mode)`));
763
880
  return;
764
881
  }
765
- writeFileSync2(HOOK_PATH, existing.trimEnd() + "\n\n" + script);
882
+ writeFileSync3(HOOK_PATH, existing.trimEnd() + "\n\n" + script);
766
883
  } else {
767
- writeFileSync2(HOOK_PATH, script);
884
+ writeFileSync3(HOOK_PATH, script);
768
885
  }
769
886
  chmodSync(HOOK_PATH, 493);
770
887
  const modeLabel = advisory ? "advisory (logs violations, never blocks)" : "strict (blocks commits on violations)";
@@ -777,14 +894,14 @@ function uninstallHook() {
777
894
  console.log(chalk.yellow("\n No pre-commit hook found.\n"));
778
895
  return;
779
896
  }
780
- const content = readFileSync3(HOOK_PATH, "utf-8");
897
+ const content = readFileSync4(HOOK_PATH, "utf-8");
781
898
  if (!content.includes(HOOK_MARKER)) {
782
899
  console.log(chalk.yellow("\n ArchMind hook not found in pre-commit \u2014 nothing to remove.\n"));
783
900
  return;
784
901
  }
785
902
  const markerIndex = content.indexOf(HOOK_MARKER);
786
903
  if (markerIndex > 1) {
787
- writeFileSync2(HOOK_PATH, content.slice(0, markerIndex).trimEnd() + "\n");
904
+ writeFileSync3(HOOK_PATH, content.slice(0, markerIndex).trimEnd() + "\n");
788
905
  } else {
789
906
  unlinkSync(HOOK_PATH);
790
907
  }
@@ -799,7 +916,8 @@ async function checkStaged(options = {}) {
799
916
  if (options.verbose) console.log(chalk.gray(" No source files staged \u2014 skipping rule check."));
800
917
  return;
801
918
  }
802
- diff = execSync(`git diff --cached -- ${stagedFiles.map((f) => `"${f}"`).join(" ")}`, { encoding: "utf-8" });
919
+ const result = spawnSync("git", ["diff", "--cached", "--", ...stagedFiles], { encoding: "utf-8" });
920
+ diff = result.stdout ?? "";
803
921
  } catch {
804
922
  console.error(chalk.red(" Failed to read staged diff."));
805
923
  process.exit(1);
@@ -810,38 +928,37 @@ async function checkStaged(options = {}) {
810
928
  }
811
929
  const configPath = join4(process.cwd(), ".memory-core.json");
812
930
  if (!existsSync4(configPath)) return;
813
- const config2 = JSON.parse(readFileSync3(configPath, "utf-8"));
931
+ const config = JSON.parse(readFileSync4(configPath, "utf-8"));
814
932
  const rules = [];
815
933
  const avoids = [];
816
- if (config2.backendArchitecture) {
817
- const profile = listProfiles("backend").find((p) => p.name === config2.backendArchitecture);
934
+ if (config.backendArchitecture) {
935
+ const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
818
936
  if (profile) {
819
937
  rules.push(...profile.rules);
820
938
  avoids.push(...profile.avoid);
821
939
  }
822
940
  }
823
- if (config2.frontendFramework) {
824
- const profile = listProfiles("frontend").find((p) => p.name === config2.frontendFramework);
941
+ if (config.frontendFramework) {
942
+ const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
825
943
  if (profile) {
826
944
  rules.push(...profile.rules);
827
945
  avoids.push(...profile.avoid);
828
946
  }
829
947
  }
830
948
  if (rules.length === 0) return;
831
- const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
832
- const chatModel = await resolveModel(ollamaUrl, process.env.OLLAMA_CHAT_MODEL ?? "llama3.2");
833
949
  const MAX_DIFF = 8e3;
834
950
  const truncated = diff.length > MAX_DIFF;
835
951
  const diffToSend = truncated ? diff.slice(0, MAX_DIFF) + "\n\n[diff truncated]" : diff;
836
952
  console.log(chalk.cyan("\n archmind \u2014 checking staged changes against rules\u2026"));
837
- if (options.verbose) {
838
- console.log(chalk.gray(` model: ${chatModel} rules: ${rules.length} diff: ${diff.length} chars${truncated ? " (truncated)" : ""}`));
953
+ if (options.verbose || options.debug) {
954
+ console.log(chalk.gray(` model: ${getChatProviderLabel()} rules: ${rules.length} diff: ${diff.length} chars${truncated ? " (truncated)" : ""}`));
839
955
  }
840
956
  const rulesWithReasons = rules.map((r, i) => {
841
957
  const why = reasonMap.get(r);
842
958
  return why ? `${i + 1}. ${r}
843
959
  WHY: ${why}` : `${i + 1}. ${r}`;
844
960
  }).join("\n");
961
+ const ignorePatterns = await loadIgnorePatterns();
845
962
  const systemPrompt = `You are a strict code reviewer enforcing architecture and framework rules.
846
963
  Analyze the git diff and identify ONLY clear, definite rule violations \u2014 not style preferences.
847
964
  Use the WHY for each rule to understand intent and judge edge cases correctly.
@@ -852,38 +969,34 @@ ${rulesWithReasons}
852
969
  Things that must never appear:
853
970
  ${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
854
971
 
972
+ Never flag these accepted project patterns:
973
+ ${ignorePatterns.length ? ignorePatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
974
+
855
975
  IMPORTANT: You MUST respond with a JSON object that has a "violations" key containing an array.
856
976
  For each violation include a "reason" field \u2014 copy the WHY from the rule to explain to the developer why this matters.
857
977
  Example with violations: {"violations":[{"rule":"Use functional components only","file":"User.tsx","line":3,"issue":"Class component used","suggestion":"Convert to a function component using hooks","reason":"Class components cannot use hooks and the entire React ecosystem now assumes functional components"}]}
858
978
  Example with no violations: {"violations":[]}
859
979
  Do not include any text outside the JSON object.`;
980
+ if (options.debug) {
981
+ console.log(chalk.gray("\n [debug] prompt:"));
982
+ 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"));
983
+ console.log(systemPrompt);
984
+ 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"));
985
+ console.log(chalk.gray(` [debug] diff length: ${diff.length} chars`));
986
+ console.log(chalk.dim(diffToSend));
987
+ 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"));
988
+ }
860
989
  let violations = [];
861
990
  try {
862
- const res = await fetch(`${ollamaUrl}/api/chat`, {
863
- method: "POST",
864
- headers: { "Content-Type": "application/json" },
865
- body: JSON.stringify({
866
- model: chatModel,
867
- messages: [
868
- { role: "system", content: systemPrompt },
869
- { role: "user", content: `Review this diff:
991
+ const raw = await callChatModel([
992
+ { role: "system", content: systemPrompt },
993
+ { role: "user", content: `Review this diff:
870
994
 
871
995
  ${diffToSend}` }
872
- ],
873
- stream: false,
874
- format: "json"
875
- })
876
- });
877
- if (!res.ok) {
878
- const body = await res.text();
879
- if (body.includes("not found") || body.includes("model")) {
880
- printModelMissing(chatModel);
881
- return;
882
- }
883
- throw new Error(body);
996
+ ]);
997
+ if (options.verbose || options.debug) {
998
+ console.log(chalk.gray(` raw response: ${options.debug ? raw : raw.slice(0, 200)}`));
884
999
  }
885
- const data = await res.json();
886
- const raw = data.message.content.trim();
887
1000
  try {
888
1001
  const parsed = JSON.parse(raw);
889
1002
  if (Array.isArray(parsed)) {
@@ -898,10 +1011,11 @@ ${diffToSend}` }
898
1011
  } catch {
899
1012
  violations = [];
900
1013
  }
901
- if (options.verbose) {
902
- console.log(chalk.gray(` raw response: ${raw.slice(0, 200)}`));
903
- }
904
1014
  } catch (err) {
1015
+ if (err.message?.startsWith("MODEL_NOT_FOUND:")) {
1016
+ printModelMissing(err.message.split(":")[1]);
1017
+ return;
1018
+ }
905
1019
  if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
906
1020
  console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 skipping rule check."));
907
1021
  console.log(chalk.gray(" Start it: ollama serve\n"));
@@ -937,6 +1051,96 @@ ${diffToSend}` }
937
1051
  console.log(chalk.dim(" To bypass (not recommended): git commit --no-verify"));
938
1052
  console.log(chalk.dim(' To save as memory: memory-core remember "<lesson>"'));
939
1053
  console.log();
1054
+ recordViolations(violations);
1055
+ await promptToSaveViolations(violations);
1056
+ process.exit(1);
1057
+ }
1058
+ function extractForbiddenPhrases(content) {
1059
+ const phrases = [];
1060
+ const normalized = content.replace(/\s+/g, " ");
1061
+ const patterns = [
1062
+ /\bnever\s+([^.;]+)/gi,
1063
+ /\bmust not\s+([^.;]+)/gi,
1064
+ /\bdo not\s+([^.;]+)/gi
1065
+ ];
1066
+ for (const pattern of patterns) {
1067
+ for (const match of normalized.matchAll(pattern)) {
1068
+ const phrase = match[1]?.trim();
1069
+ if (phrase && phrase.split(/\s+/).length >= 2) phrases.push(phrase.toLowerCase());
1070
+ }
1071
+ }
1072
+ return phrases;
1073
+ }
1074
+ function getCiDiff() {
1075
+ const baseRef = process.env.GITHUB_BASE_REF;
1076
+ const commands = [
1077
+ baseRef ? `git diff --unified=0 --diff-filter=ACMRT origin/${baseRef}...HEAD` : "",
1078
+ "git diff --unified=0 --diff-filter=ACMRT HEAD~1 HEAD",
1079
+ "git diff --cached --unified=0 --diff-filter=ACMRT"
1080
+ ].filter(Boolean);
1081
+ for (const command of commands) {
1082
+ try {
1083
+ const diff = execSync(command, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
1084
+ if (diff.trim()) return diff;
1085
+ } catch {
1086
+ }
1087
+ }
1088
+ return "";
1089
+ }
1090
+ async function checkCi(options = {}) {
1091
+ let memories;
1092
+ try {
1093
+ memories = readMemoryFile();
1094
+ } catch (err) {
1095
+ console.error(chalk.red(`
1096
+ CI check failed: ${err.message}
1097
+ `));
1098
+ process.exit(1);
1099
+ }
1100
+ const rules = memories.filter((memory) => memory.type !== "ignore");
1101
+ const ignores = memories.filter((memory) => memory.type === "ignore").map((memory) => memory.content.toLowerCase());
1102
+ const phrases = rules.flatMap(
1103
+ (memory) => extractForbiddenPhrases(memory.content).map((phrase) => ({ rule: memory.content, phrase }))
1104
+ );
1105
+ const diff = getCiDiff();
1106
+ const addedLines = diff.split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
1107
+ if (options.debug) {
1108
+ console.log(chalk.gray(`
1109
+ [debug] memories: ${memories.length}`));
1110
+ console.log(chalk.gray(` [debug] text rules: ${phrases.length}`));
1111
+ console.log(chalk.gray(` [debug] diff length: ${diff.length} chars
1112
+ `));
1113
+ }
1114
+ const violations = [];
1115
+ for (const line of addedLines) {
1116
+ const normalizedLine = line.toLowerCase();
1117
+ if (ignores.some((ignore) => normalizedLine.includes(ignore))) continue;
1118
+ for (const { rule, phrase } of phrases) {
1119
+ if (normalizedLine.includes(phrase)) {
1120
+ violations.push({
1121
+ rule,
1122
+ file: "diff",
1123
+ issue: `Added line contains forbidden phrase: "${phrase}"`
1124
+ });
1125
+ }
1126
+ }
1127
+ }
1128
+ if (violations.length === 0) {
1129
+ console.log(chalk.green(`
1130
+ \u2713 CI memory check passed (${rules.length} rules loaded from memories.json)
1131
+ `));
1132
+ return;
1133
+ }
1134
+ console.log(chalk.red.bold(`
1135
+ \u2717 ${violations.length} CI violation${violations.length > 1 ? "s" : ""} found
1136
+ `));
1137
+ violations.forEach((violation, index) => {
1138
+ console.log(chalk.bold(` [${index + 1}] ${violation.file}`));
1139
+ console.log(chalk.yellow(" Rule: ") + violation.rule);
1140
+ console.log(chalk.red(" Issue: ") + violation.issue);
1141
+ console.log();
1142
+ });
1143
+ recordViolations(violations);
940
1144
  process.exit(1);
941
1145
  }
942
1146
  function printModelMissing(model) {
@@ -949,27 +1153,13 @@ function printModelMissing(model) {
949
1153
 
950
1154
  // src/watcher.ts
951
1155
  import { watch } from "chokidar";
952
- import { execSync as execSync2 } from "child_process";
953
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1156
+ import { spawnSync as spawnSync2 } from "child_process";
1157
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
954
1158
  import { join as join5, relative } from "path";
955
1159
  import chalk2 from "chalk";
956
- async function resolveModel2(ollamaUrl, chatModel) {
957
- try {
958
- const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3e3) });
959
- if (!res.ok) return chatModel;
960
- const data = await res.json();
961
- const models = data.models ?? [];
962
- const exact = models.find((m) => m.name === chatModel);
963
- if (exact) return exact.name;
964
- const prefixed = models.find((m) => m.name.startsWith(`${chatModel}:`));
965
- if (prefixed) return prefixed.name;
966
- } catch {
967
- }
968
- return chatModel;
969
- }
970
1160
  function getFileLines(filePath) {
971
1161
  try {
972
- return readFileSync4(filePath, "utf-8").split("\n");
1162
+ return readFileSync5(filePath, "utf-8").split("\n");
973
1163
  } catch {
974
1164
  return [];
975
1165
  }
@@ -995,27 +1185,43 @@ var SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|s
995
1185
  var reasonMap2 = new Map(
996
1186
  seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
997
1187
  );
1188
+ function recordViolations2(violations) {
1189
+ const statsPath = join5(process.cwd(), ".memory-core-stats.json");
1190
+ let stats = { rules: {}, files: {} };
1191
+ if (existsSync5(statsPath)) {
1192
+ try {
1193
+ stats = JSON.parse(readFileSync5(statsPath, "utf-8"));
1194
+ } catch {
1195
+ stats = { rules: {}, files: {} };
1196
+ }
1197
+ }
1198
+ for (const violation of violations) {
1199
+ stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
1200
+ if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
1201
+ }
1202
+ writeFileSync4(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
1203
+ }
998
1204
  function loadConfig(cwd) {
999
1205
  const configPath = join5(cwd, ".memory-core.json");
1000
1206
  if (!existsSync5(configPath)) return null;
1001
1207
  try {
1002
- return JSON.parse(readFileSync4(configPath, "utf-8"));
1208
+ return JSON.parse(readFileSync5(configPath, "utf-8"));
1003
1209
  } catch {
1004
1210
  return null;
1005
1211
  }
1006
1212
  }
1007
- function getProfileRules(config2) {
1213
+ function getProfileRules(config) {
1008
1214
  const rules = [];
1009
1215
  const avoids = [];
1010
- if (config2.backendArchitecture) {
1011
- const profile = listProfiles("backend").find((p) => p.name === config2.backendArchitecture);
1216
+ if (config.backendArchitecture) {
1217
+ const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
1012
1218
  if (profile) {
1013
1219
  rules.push(...profile.rules);
1014
1220
  avoids.push(...profile.avoid);
1015
1221
  }
1016
1222
  }
1017
- if (config2.frontendFramework) {
1018
- const profile = listProfiles("frontend").find((p) => p.name === config2.frontendFramework);
1223
+ if (config.frontendFramework) {
1224
+ const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
1019
1225
  if (profile) {
1020
1226
  rules.push(...profile.rules);
1021
1227
  avoids.push(...profile.avoid);
@@ -1023,30 +1229,33 @@ function getProfileRules(config2) {
1023
1229
  }
1024
1230
  return { rules, avoids };
1025
1231
  }
1026
- async function checkFile(filePath, cwd, config2, verbose) {
1027
- const rel = relative(cwd, filePath);
1028
- let diff;
1232
+ async function loadIgnorePatterns2() {
1029
1233
  try {
1030
- diff = execSync2(`git diff HEAD -- "${rel}" 2>/dev/null || git diff --no-index /dev/null "${rel}" 2>/dev/null || true`, {
1031
- encoding: "utf-8",
1032
- cwd
1033
- });
1234
+ const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-KU4EEG4Y.js");
1235
+ const ignores = await listMemories2({ type: "ignore", limit: 1e3 });
1236
+ await closePool2();
1237
+ return ignores.map((ignore) => ignore.content);
1034
1238
  } catch {
1035
- try {
1036
- diff = execSync2(`git diff --no-index /dev/null "${rel}"`, { encoding: "utf-8", cwd });
1037
- } catch (e) {
1038
- diff = e.stdout ?? "";
1039
- }
1239
+ return [];
1240
+ }
1241
+ }
1242
+ async function checkFile(filePath, cwd, config, verbose, debug) {
1243
+ const rel = relative(cwd, filePath);
1244
+ let diff;
1245
+ const headResult = spawnSync2("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd });
1246
+ if (headResult.stdout?.trim()) {
1247
+ diff = headResult.stdout;
1248
+ } else {
1249
+ const noIndexResult = spawnSync2("git", ["diff", "--no-index", "/dev/null", rel], { encoding: "utf-8", cwd });
1250
+ diff = noIndexResult.stdout ?? "";
1040
1251
  }
1041
1252
  if (!diff.trim()) return;
1042
- const { rules, avoids } = getProfileRules(config2);
1253
+ const { rules, avoids } = getProfileRules(config);
1043
1254
  if (rules.length === 0) return;
1044
- const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
1045
- const chatModel = await resolveModel2(ollamaUrl, process.env.OLLAMA_CHAT_MODEL ?? "llama3.2");
1046
1255
  const MAX_DIFF = 6e3;
1047
1256
  const truncated = diff.length > MAX_DIFF;
1048
1257
  const diffToSend = truncated ? diff.slice(0, MAX_DIFF) + "\n\n[diff truncated]" : diff;
1049
- if (verbose) {
1258
+ if (verbose || debug) {
1050
1259
  console.log(chalk2.dim(`
1051
1260
  [watch] checking ${rel} (${diff.length} chars)\u2026`));
1052
1261
  }
@@ -1055,6 +1264,7 @@ async function checkFile(filePath, cwd, config2, verbose) {
1055
1264
  return why ? `${i + 1}. ${r}
1056
1265
  WHY: ${why}` : `${i + 1}. ${r}`;
1057
1266
  }).join("\n");
1267
+ const ignorePatterns = await loadIgnorePatterns2();
1058
1268
  const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
1059
1269
  Analyze the file diff and identify ONLY clear, definite rule violations.
1060
1270
  Use the WHY for each rule to understand intent and judge edge cases.
@@ -1065,36 +1275,33 @@ ${rulesWithReasons}
1065
1275
  Things that must never appear:
1066
1276
  ${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
1067
1277
 
1278
+ Never flag these accepted project patterns:
1279
+ ${ignorePatterns.length ? ignorePatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
1280
+
1068
1281
  IMPORTANT: Respond with JSON: {"violations":[...]} or {"violations":[]}.
1069
1282
  Each violation: {"rule":"...","file":"...","line":N,"issue":"...","suggestion":"...","reason":"..."}.
1070
1283
  No text outside the JSON.`;
1284
+ if (debug) {
1285
+ console.log(chalk2.gray("\n [debug] prompt:"));
1286
+ 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"));
1287
+ console.log(systemPrompt);
1288
+ 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"));
1289
+ console.log(chalk2.gray(` [debug] diff length: ${diff.length} chars`));
1290
+ console.log(chalk2.dim(diffToSend));
1291
+ 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"));
1292
+ }
1071
1293
  try {
1072
- const res = await fetch(`${ollamaUrl}/api/chat`, {
1073
- method: "POST",
1074
- headers: { "Content-Type": "application/json" },
1075
- body: JSON.stringify({
1076
- model: chatModel,
1077
- messages: [
1078
- { role: "system", content: systemPrompt },
1079
- { role: "user", content: `Review this diff for ${rel}:
1294
+ const raw = await callChatModel([
1295
+ { role: "system", content: systemPrompt },
1296
+ { role: "user", content: `Review this diff for ${rel}:
1080
1297
 
1081
1298
  ${diffToSend}` }
1082
- ],
1083
- stream: false,
1084
- format: "json"
1085
- })
1086
- });
1087
- if (!res.ok) {
1088
- const body = await res.text();
1089
- if (body.includes("not found") || body.includes("model")) {
1090
- console.log(chalk2.yellow(`
1091
- \u26A0 Chat model "${chatModel}" not found. Pull it: ollama pull ${chatModel}
1092
- `));
1093
- }
1094
- return;
1299
+ ]);
1300
+ if (debug) {
1301
+ console.log(chalk2.gray(" [debug] raw response:"));
1302
+ console.log(chalk2.dim(raw));
1303
+ 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"));
1095
1304
  }
1096
- const data = await res.json();
1097
- const raw = data.message.content.trim();
1098
1305
  let violations = [];
1099
1306
  try {
1100
1307
  const parsed = JSON.parse(raw);
@@ -1130,6 +1337,7 @@ ${diffToSend}` }
1130
1337
  if (v.suggestion) console.log(chalk2.green(" Fix: ") + v.suggestion);
1131
1338
  console.log();
1132
1339
  });
1340
+ recordViolations2(violations);
1133
1341
  console.log(chalk2.dim(' Fix violations or run: memory-core remember "<lesson>"'));
1134
1342
  console.log();
1135
1343
  } catch (err) {
@@ -1143,22 +1351,20 @@ ${diffToSend}` }
1143
1351
  }
1144
1352
  async function startWatch(options = {}) {
1145
1353
  const cwd = process.cwd();
1146
- const config2 = loadConfig(cwd);
1147
- if (!config2) {
1354
+ const config = loadConfig(cwd);
1355
+ if (!config) {
1148
1356
  console.error(chalk2.red("\n No .memory-core.json found. Run: memory-core init\n"));
1149
1357
  process.exit(1);
1150
1358
  }
1151
- const { rules } = getProfileRules(config2);
1359
+ const { rules } = getProfileRules(config);
1152
1360
  if (rules.length === 0) {
1153
1361
  console.log(chalk2.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to watch.\n"));
1154
1362
  process.exit(0);
1155
1363
  }
1156
1364
  const watchPath = options.path ?? cwd;
1157
- const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
1158
- const chatModel = await resolveModel2(ollamaUrl, process.env.OLLAMA_CHAT_MODEL ?? "llama3.2");
1159
1365
  console.log(chalk2.cyan("\n archmind watch \u2014 real-time rule enforcement\n"));
1160
1366
  console.log(chalk2.dim(` watching: ${watchPath}`));
1161
- console.log(chalk2.dim(` model: ${chatModel}`));
1367
+ console.log(chalk2.dim(` model: ${getChatProviderLabel()}`));
1162
1368
  console.log(chalk2.dim(` rules: ${rules.length}`));
1163
1369
  console.log(chalk2.dim(" ctrl+c to stop\n"));
1164
1370
  const pending = /* @__PURE__ */ new Map();
@@ -1197,14 +1403,14 @@ async function startWatch(options = {}) {
1197
1403
  }
1198
1404
  return;
1199
1405
  }
1200
- await checkFile(filePath, cwd, config2, options.verbose ?? false);
1406
+ await checkFile(filePath, cwd, config, options.verbose ?? false, options.debug ?? false);
1201
1407
  }, 300);
1202
1408
  pending.set(filePath, timer);
1203
1409
  };
1204
1410
  watcher.on("add", handle);
1205
1411
  watcher.on("change", handle);
1206
1412
  watcher.on("error", (err) => {
1207
- console.error(chalk2.red(` watcher error: ${err.message}`));
1413
+ console.error(chalk2.red(` watcher error: ${err instanceof Error ? err.message : String(err)}`));
1208
1414
  });
1209
1415
  process.on("SIGINT", () => {
1210
1416
  console.log(chalk2.dim("\n\n archmind watch stopped.\n"));
@@ -1216,7 +1422,7 @@ async function startWatch(options = {}) {
1216
1422
 
1217
1423
  // src/cli.ts
1218
1424
  function printBanner(projectName, agentCount, status) {
1219
- const pg2 = status ? status.postgresOk ? chalk3.green(" \u2713 PostgreSQL ") + chalk3.bold("connected") : chalk3.red(" \u2717 PostgreSQL ") + chalk3.bold("not connected \u2014 check DATABASE_URL") : chalk3.green(" \u2713 Memory ") + chalk3.bold("PostgreSQL + pgvector ready");
1425
+ const pg = status ? status.postgresOk ? chalk3.green(" \u2713 PostgreSQL ") + chalk3.bold("connected") : chalk3.red(" \u2717 PostgreSQL ") + chalk3.bold("not connected \u2014 check DATABASE_URL") : chalk3.green(" \u2713 Memory ") + chalk3.bold("PostgreSQL + pgvector ready");
1220
1426
  const ol = status ? status.ollamaOk ? chalk3.green(" \u2713 Ollama ") + chalk3.bold(`connected (model: ${status.chatModel})`) : chalk3.red(" \u2717 Ollama ") + chalk3.bold("not running \u2014 start with: ollama serve") : null;
1221
1427
  const lines = [
1222
1428
  "",
@@ -1231,7 +1437,7 @@ function printBanner(projectName, agentCount, status) {
1231
1437
  "",
1232
1438
  chalk3.green(` \u2713 Project `) + chalk3.bold(projectName),
1233
1439
  chalk3.green(` \u2713 Agents `) + chalk3.bold(`${agentCount} AI agents configured`),
1234
- pg2,
1440
+ pg,
1235
1441
  ...ol ? [ol] : [],
1236
1442
  "",
1237
1443
  chalk3.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\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"),
@@ -1247,13 +1453,13 @@ function printBanner(projectName, agentCount, status) {
1247
1453
  ];
1248
1454
  lines.forEach((l) => console.log(l));
1249
1455
  }
1250
- async function checkConnections(dbUrl, ollamaUrl, chatModel) {
1456
+ async function checkConnections(dbUrl, ollamaUrl2, chatModel) {
1251
1457
  const spinner = ora("Checking connections\u2026").start();
1252
1458
  let postgresOk = false;
1253
1459
  let ollamaOk = false;
1254
1460
  try {
1255
- const { Pool: Pool2 } = (await import("pg")).default;
1256
- const testPool = new Pool2({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
1461
+ const { Pool } = (await import("pg")).default;
1462
+ const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
1257
1463
  await testPool.query("SELECT 1");
1258
1464
  await testPool.end();
1259
1465
  postgresOk = true;
@@ -1261,7 +1467,7 @@ async function checkConnections(dbUrl, ollamaUrl, chatModel) {
1261
1467
  postgresOk = false;
1262
1468
  }
1263
1469
  try {
1264
- const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1470
+ const res = await fetch(`${ollamaUrl2}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1265
1471
  ollamaOk = res.ok;
1266
1472
  } catch {
1267
1473
  ollamaOk = false;
@@ -1276,28 +1482,74 @@ async function checkConnections(dbUrl, ollamaUrl, chatModel) {
1276
1482
  console.log();
1277
1483
  return { postgresOk, ollamaOk, chatModel };
1278
1484
  }
1279
- var { version } = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf-8"));
1485
+ var { version } = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf-8"));
1280
1486
  var CONFIG_FILE = ".memory-core.json";
1281
1487
  function readProjectConfig() {
1282
1488
  const path = join6(process.cwd(), CONFIG_FILE);
1283
1489
  if (!existsSync6(path)) return null;
1284
1490
  try {
1285
- return JSON.parse(readFileSync5(path, "utf-8"));
1491
+ return JSON.parse(readFileSync6(path, "utf-8"));
1286
1492
  } catch {
1287
1493
  return null;
1288
1494
  }
1289
1495
  }
1290
- function writeProjectConfig(config2) {
1291
- writeFileSync3(join6(process.cwd(), CONFIG_FILE), JSON.stringify(config2, null, 2));
1496
+ function writeProjectConfig(config) {
1497
+ writeFileSync5(join6(process.cwd(), CONFIG_FILE), JSON.stringify(config, null, 2));
1498
+ }
1499
+ function parseTags(tags) {
1500
+ return tags ? tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
1501
+ }
1502
+ function truncate(value, length) {
1503
+ if (!value) return "";
1504
+ return value.length > length ? `${value.slice(0, Math.max(0, length - 1))}\u2026` : value;
1505
+ }
1506
+ function printMemoryTable(memories, title = "Rules in memory") {
1507
+ console.log(chalk3.bold(`
1508
+ ${title} (${memories.length} total)
1509
+ `));
1510
+ console.log(chalk3.dim(" ID Type Scope Title / Content"));
1511
+ console.log(chalk3.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\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1512
+ memories.forEach((memory) => {
1513
+ const id = String(memory.id).padEnd(4);
1514
+ const type = memory.type.padEnd(10);
1515
+ const scope = memory.scope.padEnd(9);
1516
+ const label = truncate(memory.title || memory.content, 64);
1517
+ console.log(` ${id} ${type} ${scope} ${label}`);
1518
+ });
1519
+ console.log(chalk3.gray("\n Use: memory-core remove <id> | memory-core edit <id>\n"));
1292
1520
  }
1293
1521
  var program = new Command();
1294
1522
  program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version(version);
1295
- program.command("init").description("Initialize memory-core in the current project").action(async () => {
1523
+ program.command("init").description("Initialize memory-core in the current project").option("--quick", "Use smart defaults and skip optional prompts").action(async (opts) => {
1296
1524
  console.log(chalk3.bold.cyan("\n memory-core init\n"));
1297
1525
  const detected = detectProject();
1526
+ const quick = opts.quick ?? false;
1298
1527
  const envPath = join6(process.cwd(), ".memory-core.env");
1299
1528
  const hasEnv = existsSync6(envPath) || existsSync6(join6(process.cwd(), ".env")) || !!process.env.DATABASE_URL;
1300
- if (!hasEnv) {
1529
+ if (!hasEnv && quick) {
1530
+ const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
1531
+ const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
1532
+ const ollamaUrl2 = "http://localhost:11434";
1533
+ const chatModel = "llama3.2";
1534
+ const envContent = [
1535
+ `DATABASE_URL=${dbUrl}`,
1536
+ `OLLAMA_URL=${ollamaUrl2}`,
1537
+ `OLLAMA_MODEL=nomic-embed-text`,
1538
+ `OLLAMA_CHAT_MODEL=${chatModel}`
1539
+ ].join("\n") + "\n";
1540
+ writeFileSync5(envPath, envContent);
1541
+ process.env.DATABASE_URL = dbUrl;
1542
+ process.env.OLLAMA_URL = ollamaUrl2;
1543
+ process.env.OLLAMA_MODEL = "nomic-embed-text";
1544
+ process.env.OLLAMA_CHAT_MODEL = chatModel;
1545
+ const gitignorePath2 = join6(process.cwd(), ".gitignore");
1546
+ const gitignore = existsSync6(gitignorePath2) ? readFileSync6(gitignorePath2, "utf-8") : "";
1547
+ if (!gitignore.includes(".memory-core.env")) {
1548
+ appendFileSync(gitignorePath2, `${gitignore ? "\n" : ""}.memory-core.env
1549
+ `);
1550
+ }
1551
+ console.log(chalk3.green(" \u2713 .memory-core.env created with local defaults"));
1552
+ } else if (!hasEnv) {
1301
1553
  console.log(chalk3.dim(" No .memory-core.env found \u2014 let's set up your connection.\n"));
1302
1554
  const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
1303
1555
  let dbUrl = "";
@@ -1308,8 +1560,8 @@ program.command("init").description("Initialize memory-core in the current proje
1308
1560
  });
1309
1561
  const pgSpinner = ora(" Testing PostgreSQL connection\u2026").start();
1310
1562
  try {
1311
- const { Pool: Pool2 } = (await import("pg")).default;
1312
- const testPool = new Pool2({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
1563
+ const { Pool } = (await import("pg")).default;
1564
+ const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
1313
1565
  await testPool.query("SELECT 1");
1314
1566
  await testPool.end();
1315
1567
  pgSpinner.succeed(chalk3.green("PostgreSQL connected"));
@@ -1319,15 +1571,15 @@ program.command("init").description("Initialize memory-core in the current proje
1319
1571
  console.log(chalk3.yellow(" Please check that PostgreSQL is running and the URL is correct.\n"));
1320
1572
  }
1321
1573
  }
1322
- let ollamaUrl = "";
1574
+ let ollamaUrl2 = "";
1323
1575
  while (true) {
1324
- ollamaUrl = await input({
1576
+ ollamaUrl2 = await input({
1325
1577
  message: "Ollama URL?",
1326
- default: ollamaUrl || "http://localhost:11434"
1578
+ default: ollamaUrl2 || "http://localhost:11434"
1327
1579
  });
1328
1580
  const ollamaSpinner = ora(" Testing Ollama connection\u2026").start();
1329
1581
  try {
1330
- const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1582
+ const res = await fetch(`${ollamaUrl2}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1331
1583
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1332
1584
  ollamaSpinner.succeed(chalk3.green("Ollama connected"));
1333
1585
  break;
@@ -1336,69 +1588,117 @@ program.command("init").description("Initialize memory-core in the current proje
1336
1588
  console.log(chalk3.yellow(" Make sure Ollama is running: ollama serve\n"));
1337
1589
  }
1338
1590
  }
1591
+ const chatProvider = await select({
1592
+ message: "Which provider for code checking?",
1593
+ choices: [
1594
+ { name: "Local \u2014 Ollama (no API key, free)", value: "ollama" },
1595
+ { name: "OpenAI \u2014 gpt-4o, gpt-4o-mini", value: "openai" },
1596
+ { name: "Anthropic \u2014 claude-sonnet, claude-haiku", value: "anthropic" },
1597
+ { name: "MiniMax \u2014 MiniMax-Text-01, abab6.5s-chat", value: "minimax" }
1598
+ ]
1599
+ });
1339
1600
  let chatModel = "";
1340
- while (true) {
1341
- const chatModelChoice = await select({
1342
- message: "Which Ollama model for code checking?",
1343
- choices: [
1344
- { name: "llama3.2 (fast, 2GB, recommended for most machines)", value: "llama3.2" },
1345
- { name: "qwen2.5-coder (better code understanding, 4.7GB)", value: "qwen2.5-coder" },
1346
- { name: "mistral (balanced, 4.1GB)", value: "mistral" },
1347
- { name: "codellama (code-focused, 3.8GB)", value: "codellama" },
1348
- { name: "Other (enter manually)", value: "__custom__" }
1349
- ]
1350
- });
1351
- chatModel = chatModelChoice === "__custom__" ? await input({ message: "Model name?", default: "llama3.2" }) : chatModelChoice;
1352
- const modelSpinner = ora(` Checking if ${chatModel} is installed\u2026`).start();
1353
- try {
1354
- const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1355
- const data = await res.json();
1356
- const models = data.models ?? [];
1357
- const exact = models.find((m) => m.name === chatModel);
1358
- const prefixed = models.find((m) => m.name.startsWith(`${chatModel}:`));
1359
- const match = exact ?? prefixed;
1360
- if (match) {
1361
- chatModel = match.name;
1362
- modelSpinner.succeed(chalk3.green(`${chatModel} is installed and ready`));
1363
- break;
1364
- } else {
1365
- modelSpinner.fail(chalk3.red(`${chatModel} is not installed in your Ollama`));
1366
- console.log(chalk3.yellow(` Run: ollama pull ${chatModel} \u2014 or pick a different model.
1601
+ let chatApiKey = "";
1602
+ if (chatProvider === "ollama") {
1603
+ while (true) {
1604
+ const chatModelChoice = await select({
1605
+ message: "Which Ollama model for code checking?",
1606
+ choices: [
1607
+ { name: "llama3.2 (fast, 2GB, recommended for most machines)", value: "llama3.2" },
1608
+ { name: "qwen2.5-coder (better code understanding, 4.7GB)", value: "qwen2.5-coder" },
1609
+ { name: "mistral (balanced, 4.1GB)", value: "mistral" },
1610
+ { name: "codellama (code-focused, 3.8GB)", value: "codellama" },
1611
+ { name: "Other (enter manually)", value: "__custom__" }
1612
+ ]
1613
+ });
1614
+ chatModel = chatModelChoice === "__custom__" ? await input({ message: "Model name?", default: "llama3.2" }) : chatModelChoice;
1615
+ const modelSpinner = ora(` Checking if ${chatModel} is installed\u2026`).start();
1616
+ try {
1617
+ const res = await fetch(`${ollamaUrl2}/api/tags`, { signal: AbortSignal.timeout(5e3) });
1618
+ const data = await res.json();
1619
+ const models = data.models ?? [];
1620
+ const exact = models.find((m) => m.name === chatModel);
1621
+ const prefixed = models.find((m) => m.name.startsWith(`${chatModel}:`));
1622
+ const match = exact ?? prefixed;
1623
+ if (match) {
1624
+ chatModel = match.name;
1625
+ modelSpinner.succeed(chalk3.green(`${chatModel} is installed and ready`));
1626
+ break;
1627
+ } else {
1628
+ modelSpinner.fail(chalk3.red(`${chatModel} is not installed in your Ollama`));
1629
+ console.log(chalk3.yellow(` Run: ollama pull ${chatModel} \u2014 or pick a different model.
1367
1630
  `));
1631
+ }
1632
+ } catch {
1633
+ modelSpinner.warn(chalk3.yellow("Could not verify model \u2014 continuing anyway"));
1634
+ break;
1368
1635
  }
1369
- } catch {
1370
- modelSpinner.warn(chalk3.yellow("Could not verify model \u2014 continuing anyway"));
1371
- break;
1372
1636
  }
1637
+ } else {
1638
+ const modelChoices = {
1639
+ openai: [
1640
+ { name: "gpt-4o (best accuracy)", value: "gpt-4o" },
1641
+ { name: "gpt-4o-mini (fast, cheaper)", value: "gpt-4o-mini" },
1642
+ { name: "gpt-4-turbo (powerful, slower)", value: "gpt-4-turbo" },
1643
+ { name: "Other (enter manually)", value: "__custom__" }
1644
+ ],
1645
+ anthropic: [
1646
+ { name: "claude-sonnet-4-5 (best accuracy)", value: "claude-sonnet-4-5-20251001" },
1647
+ { name: "claude-haiku-4-5 (fast, cheaper)", value: "claude-haiku-4-5-20251001" },
1648
+ { name: "Other (enter manually)", value: "__custom__" }
1649
+ ],
1650
+ minimax: [
1651
+ { name: "MiniMax-Text-01 (flagship)", value: "MiniMax-Text-01" },
1652
+ { name: "abab6.5s-chat (fast, efficient)", value: "abab6.5s-chat" },
1653
+ { name: "Other (enter manually)", value: "__custom__" }
1654
+ ]
1655
+ };
1656
+ const modelChoice = await select({
1657
+ message: `Which ${chatProvider} model?`,
1658
+ choices: modelChoices[chatProvider]
1659
+ });
1660
+ chatModel = modelChoice === "__custom__" ? await input({ message: "Model name?" }) : modelChoice;
1661
+ chatApiKey = await input({
1662
+ message: `${chatProvider.charAt(0).toUpperCase() + chatProvider.slice(1)} API key?`
1663
+ });
1664
+ console.log(chalk3.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
1373
1665
  }
1374
- const envContent = [
1666
+ const envLines = [
1375
1667
  `DATABASE_URL=${dbUrl}`,
1376
- `OLLAMA_URL=${ollamaUrl}`,
1668
+ `OLLAMA_URL=${ollamaUrl2}`,
1377
1669
  `OLLAMA_MODEL=nomic-embed-text`,
1378
- `OLLAMA_CHAT_MODEL=${chatModel}`
1379
- ].join("\n") + "\n";
1380
- writeFileSync3(envPath, envContent);
1670
+ `CHAT_PROVIDER=${chatProvider}`,
1671
+ `CHAT_MODEL=${chatModel}`
1672
+ ];
1673
+ if (chatProvider === "ollama") envLines.push(`OLLAMA_CHAT_MODEL=${chatModel}`);
1674
+ if (chatApiKey) envLines.push(`CHAT_API_KEY=${chatApiKey}`);
1675
+ const envContent = envLines.join("\n") + "\n";
1676
+ writeFileSync5(envPath, envContent);
1381
1677
  process.env.DATABASE_URL = dbUrl;
1382
- process.env.OLLAMA_URL = ollamaUrl;
1678
+ process.env.OLLAMA_URL = ollamaUrl2;
1383
1679
  process.env.OLLAMA_MODEL = "nomic-embed-text";
1384
- process.env.OLLAMA_CHAT_MODEL = chatModel;
1680
+ process.env.CHAT_PROVIDER = chatProvider;
1681
+ process.env.CHAT_MODEL = chatModel;
1682
+ if (chatProvider === "ollama") process.env.OLLAMA_CHAT_MODEL = chatModel;
1683
+ if (chatApiKey) process.env.CHAT_API_KEY = chatApiKey;
1385
1684
  const gitignorePath2 = join6(process.cwd(), ".gitignore");
1386
1685
  if (existsSync6(gitignorePath2)) {
1387
- const gi = readFileSync5(gitignorePath2, "utf-8");
1686
+ const gi = readFileSync6(gitignorePath2, "utf-8");
1388
1687
  if (!gi.includes(".memory-core.env")) {
1389
1688
  appendFileSync(gitignorePath2, "\n.memory-core.env\n");
1390
1689
  }
1391
1690
  } else {
1392
- writeFileSync3(gitignorePath2, ".memory-core.env\n");
1691
+ writeFileSync5(gitignorePath2, ".memory-core.env\n");
1393
1692
  }
1394
1693
  console.log(chalk3.green("\n \u2713 .memory-core.env created"));
1395
1694
  console.log(chalk3.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
1396
1695
  }
1397
- const projectName = await input({
1696
+ const projectName = quick ? process.cwd().split("/").pop() ?? "my-project" : await input({
1398
1697
  message: "Project name?",
1399
1698
  default: process.cwd().split("/").pop() ?? "my-project"
1400
1699
  });
1401
- const projectType = await select({
1700
+ const inferredProjectType = ["Next.js", "Nuxt.js"].includes(detected.framework) ? "fullstack" : ["React", "Vue.js", "Svelte"].includes(detected.framework) ? "frontend" : "backend";
1701
+ const projectType = quick ? inferredProjectType : await select({
1402
1702
  message: "Project type?",
1403
1703
  choices: [
1404
1704
  { value: "backend", name: "Backend \u2014 API, server, microservice" },
@@ -1408,35 +1708,50 @@ program.command("init").description("Initialize memory-core in the current proje
1408
1708
  });
1409
1709
  let backendArchitecture;
1410
1710
  if (projectType === "backend" || projectType === "fullstack") {
1411
- const backendProfiles = listProfiles("backend");
1412
- backendArchitecture = await select({
1413
- message: "Backend architecture?",
1414
- choices: backendProfiles.map((p) => ({
1415
- value: p.name,
1416
- name: `${p.displayName} \u2014 ${p.description}`
1417
- }))
1418
- });
1711
+ if (quick) {
1712
+ backendArchitecture = detected.framework === "NestJS" ? "nestjs" : detected.framework === "Laravel" ? "laravel-service-repository" : "clean-architecture";
1713
+ } else {
1714
+ const backendProfiles = listProfiles("backend");
1715
+ backendArchitecture = await select({
1716
+ message: "Backend architecture?",
1717
+ choices: backendProfiles.map((p) => ({
1718
+ value: p.name,
1719
+ name: `${p.displayName} \u2014 ${p.description}`
1720
+ }))
1721
+ });
1722
+ }
1419
1723
  }
1420
1724
  let frontendFramework;
1421
1725
  if (projectType === "frontend" || projectType === "fullstack") {
1422
- const frontendProfiles = listProfiles("frontend");
1423
- frontendFramework = await select({
1424
- message: "Frontend framework?",
1425
- choices: frontendProfiles.map((p) => ({
1426
- value: p.name,
1427
- name: `${p.displayName} \u2014 ${p.description}`
1428
- }))
1429
- });
1726
+ if (quick) {
1727
+ const frameworkMap = {
1728
+ "Next.js": "nextjs",
1729
+ "Nuxt.js": "nuxt",
1730
+ React: "react",
1731
+ "Vue.js": "vue",
1732
+ Svelte: "svelte"
1733
+ };
1734
+ frontendFramework = frameworkMap[detected.framework] ?? "react";
1735
+ } else {
1736
+ const frontendProfiles = listProfiles("frontend");
1737
+ frontendFramework = await select({
1738
+ message: "Frontend framework?",
1739
+ choices: frontendProfiles.map((p) => ({
1740
+ value: p.name,
1741
+ name: `${p.displayName} \u2014 ${p.description}`
1742
+ }))
1743
+ });
1744
+ }
1430
1745
  }
1431
- const language = await input({
1746
+ const language = quick ? detected.language : await input({
1432
1747
  message: "Language?",
1433
1748
  default: detected.language
1434
1749
  });
1435
- const pullMemories = await confirm({
1750
+ const pullMemories = quick ? true : await confirm({
1436
1751
  message: "Pull relevant memories from previous projects?",
1437
1752
  default: true
1438
1753
  });
1439
- const installCaveman = await confirm({
1754
+ const installCaveman = quick ? false : await confirm({
1440
1755
  message: "Install caveman token saver? (~65-75% fewer tokens)",
1441
1756
  default: false
1442
1757
  });
@@ -1451,18 +1766,20 @@ program.command("init").description("Initialize memory-core in the current proje
1451
1766
  ]
1452
1767
  });
1453
1768
  }
1454
- const { checkbox } = await import("@inquirer/prompts");
1455
- const selectedAgents = await checkbox({
1456
- message: "Which AI agents do you want to generate files for?",
1457
- choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({ name, value: name, checked: true })),
1458
- instructions: " (Space to toggle, A to select all, Enter to confirm)"
1459
- });
1460
- const enableHook = await confirm({
1769
+ const selectedAgents = quick ? AGENT_NAMES.filter((a) => a !== "Shared") : await (async () => {
1770
+ const { checkbox } = await import("@inquirer/prompts");
1771
+ return checkbox({
1772
+ message: "Which AI agents do you want to generate files for?",
1773
+ choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({ name, value: name, checked: true })),
1774
+ instructions: " (Space to toggle, A to select all, Enter to confirm)"
1775
+ });
1776
+ })();
1777
+ const enableHook = quick ? true : await confirm({
1461
1778
  message: "Enable pre-commit hook?",
1462
1779
  default: true
1463
1780
  });
1464
1781
  let hookAdvisory = true;
1465
- if (enableHook) {
1782
+ if (enableHook && !quick) {
1466
1783
  const { select: selectMode } = await import("@inquirer/prompts");
1467
1784
  hookAdvisory = await selectMode({
1468
1785
  message: "Hook mode?",
@@ -1472,7 +1789,7 @@ program.command("init").description("Initialize memory-core in the current proje
1472
1789
  ]
1473
1790
  });
1474
1791
  }
1475
- const config2 = {
1792
+ const config = {
1476
1793
  projectName,
1477
1794
  projectType,
1478
1795
  backendArchitecture,
@@ -1495,7 +1812,7 @@ program.command("init").description("Initialize memory-core in the current proje
1495
1812
  if (installCaveman) {
1496
1813
  const spinner2 = ora("Installing caveman token saver\u2026").start();
1497
1814
  try {
1498
- execSync3(
1815
+ execSync2(
1499
1816
  "curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash",
1500
1817
  { stdio: "pipe", cwd: process.cwd() }
1501
1818
  );
@@ -1506,16 +1823,16 @@ program.command("init").description("Initialize memory-core in the current proje
1506
1823
  }
1507
1824
  const spinner = ora("Generating AI agent context files\u2026").start();
1508
1825
  const written = await generate(
1509
- { projectName, projectType, backendArchitecture, frontendFramework, language, memories, caveman: config2.caveman },
1826
+ { projectName, projectType, backendArchitecture, frontendFramework, language, memories, caveman: config.caveman },
1510
1827
  process.cwd(),
1511
1828
  [...selectedAgents, "Shared"]
1512
1829
  );
1513
- writeProjectConfig(config2);
1830
+ writeProjectConfig(config);
1514
1831
  spinner.succeed(`Generated ${written.written.length} files`);
1515
1832
  const gitignorePath = join6(process.cwd(), ".gitignore");
1516
1833
  const generatedPaths = written.written;
1517
1834
  if (generatedPaths.length > 0) {
1518
- const existing = existsSync6(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : "";
1835
+ const existing = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
1519
1836
  const toAdd = generatedPaths.filter((p) => !existing.includes(p));
1520
1837
  if (toAdd.length > 0) {
1521
1838
  const block = "\n# memory-core generated files\n" + toAdd.join("\n") + "\n";
@@ -1531,17 +1848,17 @@ program.command("init").description("Initialize memory-core in the current proje
1531
1848
  process.env.OLLAMA_URL ?? "http://localhost:11434",
1532
1849
  process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"
1533
1850
  );
1534
- printBanner(config2.projectName, written.written.length, status);
1851
+ printBanner(config.projectName, written.written.length, status);
1535
1852
  await closePool();
1536
1853
  });
1537
1854
  program.command("sync").description("Re-pull memories and regenerate AI agent files").action(async () => {
1538
- const config2 = readProjectConfig();
1539
- if (!config2) {
1855
+ const config = readProjectConfig();
1856
+ if (!config) {
1540
1857
  console.error(chalk3.red("No .memory-core.json found. Run: memory-core init"));
1541
1858
  process.exit(1);
1542
1859
  }
1543
1860
  const { checkbox } = await import("@inquirer/prompts");
1544
- const savedAgents = new Set(config2.agents ?? AGENT_NAMES.filter((a) => a !== "Shared"));
1861
+ const savedAgents = new Set(config.agents ?? AGENT_NAMES.filter((a) => a !== "Shared"));
1545
1862
  const selectedAgents = await checkbox({
1546
1863
  message: "Which agents do you want to sync?",
1547
1864
  choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({
@@ -1558,21 +1875,21 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
1558
1875
  const spinner = ora("Syncing memories\u2026").start();
1559
1876
  let memories = [];
1560
1877
  try {
1561
- const archQuery = [config2.backendArchitecture, config2.frontendFramework, config2.language].filter(Boolean).join(" ");
1562
- memories = await retrieve(archQuery, config2.backendArchitecture ?? config2.frontendFramework, 10);
1878
+ const archQuery = [config.backendArchitecture, config.frontendFramework, config.language].filter(Boolean).join(" ");
1879
+ memories = await retrieve(archQuery, config.backendArchitecture ?? config.frontendFramework, 10);
1563
1880
  spinner.text = `Found ${memories.length} memories \u2014 regenerating files\u2026`;
1564
1881
  } catch (err) {
1565
1882
  spinner.warn(`Could not retrieve memories: ${err.message}`);
1566
1883
  }
1567
1884
  const result = await generate(
1568
1885
  {
1569
- projectName: config2.projectName,
1570
- projectType: config2.projectType,
1571
- backendArchitecture: config2.backendArchitecture,
1572
- frontendFramework: config2.frontendFramework,
1573
- language: config2.language,
1886
+ projectName: config.projectName,
1887
+ projectType: config.projectType,
1888
+ backendArchitecture: config.backendArchitecture,
1889
+ frontendFramework: config.frontendFramework,
1890
+ language: config.language,
1574
1891
  memories,
1575
- caveman: config2.caveman
1892
+ caveman: config.caveman
1576
1893
  },
1577
1894
  process.cwd(),
1578
1895
  [...selectedAgents, "Shared"]
@@ -1588,7 +1905,7 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
1588
1905
  await closePool();
1589
1906
  });
1590
1907
  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").action(async (text, opts) => {
1591
- const config2 = readProjectConfig();
1908
+ const config = readProjectConfig();
1592
1909
  let reason = opts.reason;
1593
1910
  if (!reason) {
1594
1911
  reason = await input({
@@ -1602,8 +1919,8 @@ program.command("remember <text>").description("Save a new memory to the central
1602
1919
  await saveMemory({
1603
1920
  type: opts.type,
1604
1921
  scope: opts.scope,
1605
- architecture: config2?.backendArchitecture ?? config2?.frontendFramework,
1606
- projectName: config2?.projectName,
1922
+ architecture: config?.backendArchitecture ?? config?.frontendFramework,
1923
+ projectName: config?.projectName,
1607
1924
  content: text,
1608
1925
  reason: reason || void 0,
1609
1926
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [],
@@ -1619,12 +1936,12 @@ program.command("remember <text>").description("Save a new memory to the central
1619
1936
  await closePool();
1620
1937
  });
1621
1938
  program.command("search <query>").description("Search memories using semantic similarity").option("-n, --limit <n>", "Number of results", "5").action(async (query, opts) => {
1622
- const config2 = readProjectConfig();
1939
+ const config = readProjectConfig();
1623
1940
  const spinner = ora("Searching\u2026").start();
1624
1941
  try {
1625
1942
  const results = await retrieve(
1626
1943
  query,
1627
- config2?.backendArchitecture ?? config2?.frontendFramework,
1944
+ config?.backendArchitecture ?? config?.frontendFramework,
1628
1945
  parseInt(opts.limit, 10)
1629
1946
  );
1630
1947
  spinner.stop();
@@ -1648,6 +1965,222 @@ program.command("search <query>").description("Search memories using semantic si
1648
1965
  }
1649
1966
  await closePool();
1650
1967
  });
1968
+ program.command("export").description(`Export DB memories to ${MEMORY_FILE}`).option("-o, --output <file>", `Output file (default: ${MEMORY_FILE})`).action(async (opts) => {
1969
+ const spinner = ora("Exporting memories\u2026").start();
1970
+ try {
1971
+ const memories = await listMemories({ limit: 1e4 });
1972
+ const portable = memories.map(toPortableMemory);
1973
+ const outputPath = opts.output ? join6(process.cwd(), opts.output) : writeMemoryFile(portable);
1974
+ if (opts.output) {
1975
+ writeFileSync5(outputPath, JSON.stringify(portable, null, 2) + "\n", "utf-8");
1976
+ }
1977
+ spinner.succeed(`Exported ${portable.length} memories to ${outputPath}`);
1978
+ } catch (err) {
1979
+ spinner.fail(`Export failed: ${err.message}`);
1980
+ process.exit(1);
1981
+ } finally {
1982
+ await closePool();
1983
+ }
1984
+ });
1985
+ program.command("import").description(`Import memories from ${MEMORY_FILE}`).option("--url <url>", "Import memories from a remote URL").option("-f, --file <file>", `Import file (default: ${MEMORY_FILE})`).action(async (opts) => {
1986
+ const spinner = ora("Reading memories\u2026").start();
1987
+ try {
1988
+ const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(readFileSync6(join6(process.cwd(), opts.file), "utf-8")) : readMemoryFile();
1989
+ let inserted = 0;
1990
+ let skipped = 0;
1991
+ spinner.text = `Importing ${memories.length} memories\u2026`;
1992
+ for (const memory of memories) {
1993
+ const embedding = await embed(memory.content);
1994
+ const result = await upsertMemory({
1995
+ type: memory.type,
1996
+ scope: memory.scope,
1997
+ architecture: memory.architecture,
1998
+ projectName: memory.projectName,
1999
+ title: memory.title,
2000
+ content: memory.content,
2001
+ reason: memory.reason,
2002
+ tags: memory.tags,
2003
+ embedding
2004
+ });
2005
+ if (result === "inserted") inserted++;
2006
+ else skipped++;
2007
+ }
2008
+ spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
2009
+ } catch (err) {
2010
+ spinner.fail(`Import failed: ${err.message}`);
2011
+ process.exit(1);
2012
+ } finally {
2013
+ await closePool();
2014
+ }
2015
+ });
2016
+ program.command("list").description("List memories from the local database").option("--type <type>", "Filter by type").option("--scope <scope>", "Filter by scope").option("--arch <architecture>", "Filter by architecture").option("-n, --limit <n>", "Maximum rows to show", "200").action(async (opts) => {
2017
+ try {
2018
+ const memories = await listMemories({
2019
+ type: opts.type,
2020
+ scope: opts.scope,
2021
+ architecture: opts.arch,
2022
+ limit: parseInt(opts.limit, 10)
2023
+ });
2024
+ printMemoryTable(memories);
2025
+ } catch (err) {
2026
+ console.error(chalk3.red(`List failed: ${err.message}`));
2027
+ process.exit(1);
2028
+ } finally {
2029
+ await closePool();
2030
+ }
2031
+ });
2032
+ program.command("remove <id>").description("Remove a memory by ID").action(async (id) => {
2033
+ try {
2034
+ const deleted = await deleteMemory(parseInt(id, 10));
2035
+ if (!deleted) {
2036
+ console.log(chalk3.yellow(`No memory found with ID ${id}`));
2037
+ process.exit(1);
2038
+ }
2039
+ console.log(chalk3.green(`Removed memory ${id}`));
2040
+ } catch (err) {
2041
+ console.error(chalk3.red(`Remove failed: ${err.message}`));
2042
+ process.exit(1);
2043
+ } finally {
2044
+ await closePool();
2045
+ }
2046
+ });
2047
+ program.command("edit <id>").description("Edit a memory interactively").action(async (id) => {
2048
+ const memoryId = parseInt(id, 10);
2049
+ try {
2050
+ const existing = await getMemory(memoryId);
2051
+ if (!existing) {
2052
+ console.log(chalk3.yellow(`No memory found with ID ${id}`));
2053
+ process.exit(1);
2054
+ }
2055
+ const type = await input({ message: "Type?", default: existing.type });
2056
+ const scope = await input({ message: "Scope?", default: existing.scope });
2057
+ const title = await input({ message: "Title?", default: existing.title ?? "" });
2058
+ const content = await input({ message: "Content?", default: existing.content });
2059
+ const reason = await input({ message: "Reason?", default: existing.reason ?? "" });
2060
+ const tags = await input({ message: "Tags?", default: existing.tags.join(",") });
2061
+ const embedding = content === existing.content ? void 0 : await embed(content);
2062
+ await updateMemory(memoryId, {
2063
+ type,
2064
+ scope,
2065
+ title: title || void 0,
2066
+ content,
2067
+ reason: reason || void 0,
2068
+ tags: parseTags(tags),
2069
+ embedding
2070
+ });
2071
+ console.log(chalk3.green(`Updated memory ${id}`));
2072
+ } catch (err) {
2073
+ console.error(chalk3.red(`Edit failed: ${err.message}`));
2074
+ process.exit(1);
2075
+ } finally {
2076
+ await closePool();
2077
+ }
2078
+ });
2079
+ program.command("ignore [pattern]").description("Manage project-scoped false-positive ignore patterns").option("--list", "List ignored patterns").option("--remove <id>", "Remove an ignored pattern by ID").action(async (pattern, opts) => {
2080
+ try {
2081
+ if (opts.list) {
2082
+ printMemoryTable(await listMemories({ type: "ignore", limit: 1e3 }), "Ignored patterns");
2083
+ return;
2084
+ }
2085
+ if (opts.remove) {
2086
+ const deleted = await deleteMemory(parseInt(opts.remove, 10));
2087
+ if (!deleted) {
2088
+ console.log(chalk3.yellow(`No ignore pattern found with ID ${opts.remove}`));
2089
+ process.exit(1);
2090
+ }
2091
+ console.log(chalk3.green(`Removed ignore pattern ${opts.remove}`));
2092
+ return;
2093
+ }
2094
+ if (!pattern) {
2095
+ console.error(chalk3.red("Provide a pattern, --list, or --remove <id>"));
2096
+ process.exit(1);
2097
+ }
2098
+ const config = readProjectConfig();
2099
+ const embedding = await embed(pattern);
2100
+ await upsertMemory({
2101
+ type: "ignore",
2102
+ scope: "project",
2103
+ architecture: config?.backendArchitecture ?? config?.frontendFramework,
2104
+ projectName: config?.projectName,
2105
+ content: pattern,
2106
+ tags: ["ignore"],
2107
+ embedding
2108
+ });
2109
+ console.log(chalk3.green(`Ignored pattern saved: "${pattern}"`));
2110
+ } catch (err) {
2111
+ console.error(chalk3.red(`Ignore failed: ${err.message}`));
2112
+ process.exit(1);
2113
+ } finally {
2114
+ await closePool();
2115
+ }
2116
+ });
2117
+ program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
2118
+ const workflowPath = join6(process.cwd(), ".github", "workflows", "memory-core.yml");
2119
+ mkdirSync2(dirname2(workflowPath), { recursive: true });
2120
+ writeFileSync5(workflowPath, `name: memory-core
2121
+ on: [pull_request]
2122
+ jobs:
2123
+ check:
2124
+ runs-on: ubuntu-latest
2125
+ steps:
2126
+ - uses: actions/checkout@v4
2127
+ with:
2128
+ fetch-depth: 0
2129
+ - run: npx @shahmilsaari/memory-core check --ci
2130
+ `, "utf-8");
2131
+ console.log(chalk3.green(`Generated ${workflowPath}`));
2132
+ });
2133
+ program.command("reset").description("Remove memory-core generated files and local project config").option("--soft", "Only remove generated files; keep config and DB").option("--db", "Also drop the memories table after confirmation").action(async (opts) => {
2134
+ const generated = [...new Set(OUTPUT_FILES.map((file) => file.path))];
2135
+ let removed = 0;
2136
+ for (const relativePath of generated) {
2137
+ const target = join6(process.cwd(), relativePath);
2138
+ if (existsSync6(target)) {
2139
+ rmSync(target, { force: true });
2140
+ removed++;
2141
+ }
2142
+ }
2143
+ if (!opts.soft) {
2144
+ const configPath = join6(process.cwd(), CONFIG_FILE);
2145
+ if (existsSync6(configPath)) {
2146
+ unlinkSync2(configPath);
2147
+ removed++;
2148
+ }
2149
+ uninstallHook();
2150
+ }
2151
+ if (opts.db) {
2152
+ const ok = await confirm({
2153
+ message: "Drop the memories table from the configured database?",
2154
+ default: false
2155
+ });
2156
+ if (ok) {
2157
+ const { getPool } = await import("./db-KU4EEG4Y.js");
2158
+ await getPool().query("DROP TABLE IF EXISTS memories");
2159
+ await closePool();
2160
+ console.log(chalk3.yellow("Dropped memories table"));
2161
+ }
2162
+ }
2163
+ console.log(chalk3.green(`Reset complete. Removed ${removed} files.`));
2164
+ });
2165
+ program.command("stats").description("Show violation counters recorded by check and watch").action(() => {
2166
+ const statsPath = join6(process.cwd(), ".memory-core-stats.json");
2167
+ if (!existsSync6(statsPath)) {
2168
+ console.log(chalk3.yellow("\n No violation stats recorded yet.\n"));
2169
+ return;
2170
+ }
2171
+ const stats = JSON.parse(readFileSync6(statsPath, "utf-8"));
2172
+ const printTop = (label, values = {}) => {
2173
+ console.log(chalk3.bold(`
2174
+ ${label}
2175
+ `));
2176
+ Object.entries(values).sort((a, b) => b[1] - a[1]).slice(0, 10).forEach(([name, count], index) => {
2177
+ console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${count}`);
2178
+ });
2179
+ };
2180
+ printTop("Top rules", stats.rules);
2181
+ printTop("Top files", stats.files);
2182
+ console.log();
2183
+ });
1651
2184
  program.command("seed").description("Load all predefined memories into the database").option("--arch <architecture>", "Only seed a specific architecture (e.g. clean-architecture)").option("--force", "Re-seed even if memories already exist", false).action(async (opts) => {
1652
2185
  await runMigrations();
1653
2186
  const filtered = opts.arch ? seeds.filter((s) => s.architecture === opts.arch || s.architecture === "global") : seeds;
@@ -1660,7 +2193,7 @@ program.command("seed").description("Load all predefined memories into the datab
1660
2193
  const spinner = ora(`[${seed.architecture}] ${seed.title}`).start();
1661
2194
  try {
1662
2195
  const embedding = await embed(seed.content);
1663
- await saveMemory({
2196
+ const payload = {
1664
2197
  type: seed.type,
1665
2198
  scope: seed.scope,
1666
2199
  architecture: seed.architecture === "global" ? void 0 : seed.architecture,
@@ -1669,9 +2202,21 @@ program.command("seed").description("Load all predefined memories into the datab
1669
2202
  reason: seed.reason,
1670
2203
  tags: seed.tags,
1671
2204
  embedding
1672
- });
1673
- spinner.succeed(chalk3.gray(`[${seed.architecture}] ${seed.title}`));
1674
- saved++;
2205
+ };
2206
+ if (opts.force) {
2207
+ await saveMemory(payload);
2208
+ saved++;
2209
+ spinner.succeed(chalk3.gray(`[${seed.architecture}] ${seed.title}`));
2210
+ } else {
2211
+ const result = await upsertMemory(payload);
2212
+ if (result === "inserted") {
2213
+ saved++;
2214
+ spinner.succeed(chalk3.gray(`[${seed.architecture}] ${seed.title}`));
2215
+ } else {
2216
+ skipped++;
2217
+ spinner.info(chalk3.gray(`Already exists \u2014 [${seed.architecture}] ${seed.title}`));
2218
+ }
2219
+ }
1675
2220
  } catch (err) {
1676
2221
  spinner.warn(`Skipped \u2014 ${err.message}`);
1677
2222
  skipped++;
@@ -1725,12 +2270,12 @@ ${rulesText}
1725
2270
  const skipped = [];
1726
2271
  const writeFile2 = (filePath, content) => {
1727
2272
  mkdirSync2(dirname2(filePath), { recursive: true });
1728
- writeFileSync3(filePath, content, "utf-8");
2273
+ writeFileSync5(filePath, content, "utf-8");
1729
2274
  };
1730
2275
  const readJson = (filePath) => {
1731
2276
  if (!existsSync6(filePath)) return {};
1732
2277
  try {
1733
- return JSON.parse(readFileSync5(filePath, "utf-8"));
2278
+ return JSON.parse(readFileSync6(filePath, "utf-8"));
1734
2279
  } catch {
1735
2280
  return {};
1736
2281
  }
@@ -1755,9 +2300,9 @@ ${rulesText}
1755
2300
  writeFile2(target.path, JSON.stringify(processedVscode.settings, null, 2));
1756
2301
  written.push(target.label);
1757
2302
  } else if (target.type === "continue") {
1758
- const config2 = readJson(target.path);
1759
- config2["systemMessage"] = systemPrompt;
1760
- writeFile2(target.path, JSON.stringify(config2, null, 2));
2303
+ const config = readJson(target.path);
2304
+ config["systemMessage"] = systemPrompt;
2305
+ writeFile2(target.path, JSON.stringify(config, null, 2));
1761
2306
  written.push(target.label);
1762
2307
  } else if (target.type === "aider") {
1763
2308
  const aiderContent = `# Aider global config \u2014 synced by memory-core
@@ -1767,11 +2312,11 @@ read:
1767
2312
  writeFile2(target.path, aiderContent);
1768
2313
  written.push(target.label);
1769
2314
  } else if (target.type === "zed") {
1770
- const config2 = readJson(target.path);
1771
- const assistant = config2["assistant"] ?? {};
2315
+ const config = readJson(target.path);
2316
+ const assistant = config["assistant"] ?? {};
1772
2317
  assistant["system_prompt"] = systemPrompt;
1773
- config2["assistant"] = assistant;
1774
- writeFile2(target.path, JSON.stringify(config2, null, 2));
2318
+ config["assistant"] = assistant;
2319
+ writeFile2(target.path, JSON.stringify(config, null, 2));
1775
2320
  written.push(target.label);
1776
2321
  }
1777
2322
  } catch {
@@ -1796,10 +2341,14 @@ hook.command("install").description("Install pre-commit hook (advisory mode by d
1796
2341
  hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
1797
2342
  uninstallHook();
1798
2343
  });
1799
- program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--verbose", "Show model and diff details").action(async (opts) => {
1800
- await checkStaged({ verbose: opts.verbose ?? false });
2344
+ 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("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
2345
+ if (opts.ci) {
2346
+ await checkCi({ verbose: opts.verbose ?? false, debug: opts.debug ?? false });
2347
+ return;
2348
+ }
2349
+ await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false });
1801
2350
  });
1802
- 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("--verbose", "Show diff size and model details per file").action((opts) => {
1803
- startWatch({ path: opts.path, verbose: opts.verbose });
2351
+ 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("--verbose", "Show diff size and model details per file").option("--debug", "Show prompt, diff, and raw model response").action((opts) => {
2352
+ startWatch({ path: opts.path, verbose: opts.verbose, debug: opts.debug });
1804
2353
  });
1805
2354
  program.parseAsync(process.argv);