@shahmilsaari/memory-core 0.2.7 → 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/README.md +220 -57
- package/dist/chunk-73SRPNAL.js +196 -0
- package/dist/chunk-HAGRPKR3.js +30 -0
- package/dist/chunk-KSLFLWB4.js +32 -0
- package/dist/cli.js +954 -373
- package/dist/db-KU4EEG4Y.js +28 -0
- package/dist/embedding-PAYD2JYW.js +8 -0
- package/package.json +4 -2
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
|
|
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
|
|
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
|
|
649
|
-
import { join as
|
|
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) =>
|
|
563
|
+
const has = (file) => existsSync2(join2(cwd, file));
|
|
652
564
|
const readJson = (file) => {
|
|
653
565
|
try {
|
|
654
|
-
return JSON.parse(readFileSync2(
|
|
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(
|
|
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,59 +619,273 @@ function detectProject(cwd = process.cwd()) {
|
|
|
707
619
|
}
|
|
708
620
|
|
|
709
621
|
// src/hook.ts
|
|
710
|
-
import { execSync } from "child_process";
|
|
711
|
-
import { writeFileSync as
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}
|
|
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
|
-
|
|
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
|
);
|
|
731
791
|
var HOOK_PATH = join4(".git", "hooks", "pre-commit");
|
|
732
792
|
var HOOK_MARKER = "# archmind-memory-core";
|
|
733
|
-
|
|
734
|
-
|
|
793
|
+
function buildHookScript(advisory) {
|
|
794
|
+
const suffix = advisory ? " || true" : "";
|
|
795
|
+
return `#!/bin/sh
|
|
796
|
+
${HOOK_MARKER}${advisory ? " advisory" : ""}
|
|
735
797
|
if command -v memory-core >/dev/null 2>&1; then
|
|
736
|
-
memory-core check --staged
|
|
798
|
+
memory-core check --staged${suffix}
|
|
737
799
|
elif [ -f "./node_modules/.bin/memory-core" ]; then
|
|
738
|
-
./node_modules/.bin/memory-core check --staged
|
|
800
|
+
./node_modules/.bin/memory-core check --staged${suffix}
|
|
739
801
|
elif [ -f "./dist/cli.js" ]; then
|
|
740
|
-
node ./dist/cli.js check --staged
|
|
802
|
+
node ./dist/cli.js check --staged${suffix}
|
|
741
803
|
else
|
|
742
804
|
npx --no-install memory-core check --staged 2>/dev/null || exit 0
|
|
743
805
|
fi
|
|
744
806
|
`;
|
|
745
|
-
|
|
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
|
+
}
|
|
865
|
+
function installHook(advisory = true) {
|
|
746
866
|
if (!existsSync4(".git")) {
|
|
747
867
|
console.error(chalk.red("\n Not a git repository. Run from project root.\n"));
|
|
748
868
|
process.exit(1);
|
|
749
869
|
}
|
|
870
|
+
const script = buildHookScript(advisory);
|
|
750
871
|
if (existsSync4(HOOK_PATH)) {
|
|
751
|
-
const existing =
|
|
872
|
+
const existing = readFileSync4(HOOK_PATH, "utf-8");
|
|
752
873
|
if (existing.includes(HOOK_MARKER)) {
|
|
753
|
-
|
|
874
|
+
const markerIndex = existing.indexOf(HOOK_MARKER);
|
|
875
|
+
const before = markerIndex > 1 ? existing.slice(0, markerIndex).trimEnd() + "\n\n" : "";
|
|
876
|
+
writeFileSync3(HOOK_PATH, before + script);
|
|
877
|
+
chmodSync(HOOK_PATH, 493);
|
|
878
|
+
const modeLabel2 = advisory ? chalk.cyan("advisory") : chalk.yellow("strict");
|
|
879
|
+
console.log(chalk.green("\n \u2713 Pre-commit hook updated") + chalk.dim(` (${modeLabel2} mode)`));
|
|
754
880
|
return;
|
|
755
881
|
}
|
|
756
|
-
|
|
882
|
+
writeFileSync3(HOOK_PATH, existing.trimEnd() + "\n\n" + script);
|
|
757
883
|
} else {
|
|
758
|
-
|
|
884
|
+
writeFileSync3(HOOK_PATH, script);
|
|
759
885
|
}
|
|
760
886
|
chmodSync(HOOK_PATH, 493);
|
|
761
|
-
|
|
762
|
-
console.log(chalk.
|
|
887
|
+
const modeLabel = advisory ? "advisory (logs violations, never blocks)" : "strict (blocks commits on violations)";
|
|
888
|
+
console.log(chalk.green("\n \u2713 Pre-commit hook installed") + chalk.dim(` \u2014 ${modeLabel}`));
|
|
763
889
|
console.log(chalk.gray(` Chat model: ${process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"}`));
|
|
764
890
|
console.log(chalk.gray(" To uninstall: memory-core hook uninstall\n"));
|
|
765
891
|
}
|
|
@@ -768,14 +894,14 @@ function uninstallHook() {
|
|
|
768
894
|
console.log(chalk.yellow("\n No pre-commit hook found.\n"));
|
|
769
895
|
return;
|
|
770
896
|
}
|
|
771
|
-
const content =
|
|
897
|
+
const content = readFileSync4(HOOK_PATH, "utf-8");
|
|
772
898
|
if (!content.includes(HOOK_MARKER)) {
|
|
773
899
|
console.log(chalk.yellow("\n ArchMind hook not found in pre-commit \u2014 nothing to remove.\n"));
|
|
774
900
|
return;
|
|
775
901
|
}
|
|
776
902
|
const markerIndex = content.indexOf(HOOK_MARKER);
|
|
777
903
|
if (markerIndex > 1) {
|
|
778
|
-
|
|
904
|
+
writeFileSync3(HOOK_PATH, content.slice(0, markerIndex).trimEnd() + "\n");
|
|
779
905
|
} else {
|
|
780
906
|
unlinkSync(HOOK_PATH);
|
|
781
907
|
}
|
|
@@ -790,7 +916,8 @@ async function checkStaged(options = {}) {
|
|
|
790
916
|
if (options.verbose) console.log(chalk.gray(" No source files staged \u2014 skipping rule check."));
|
|
791
917
|
return;
|
|
792
918
|
}
|
|
793
|
-
|
|
919
|
+
const result = spawnSync("git", ["diff", "--cached", "--", ...stagedFiles], { encoding: "utf-8" });
|
|
920
|
+
diff = result.stdout ?? "";
|
|
794
921
|
} catch {
|
|
795
922
|
console.error(chalk.red(" Failed to read staged diff."));
|
|
796
923
|
process.exit(1);
|
|
@@ -801,38 +928,37 @@ async function checkStaged(options = {}) {
|
|
|
801
928
|
}
|
|
802
929
|
const configPath = join4(process.cwd(), ".memory-core.json");
|
|
803
930
|
if (!existsSync4(configPath)) return;
|
|
804
|
-
const
|
|
931
|
+
const config = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
805
932
|
const rules = [];
|
|
806
933
|
const avoids = [];
|
|
807
|
-
if (
|
|
808
|
-
const profile = listProfiles("backend").find((p) => p.name ===
|
|
934
|
+
if (config.backendArchitecture) {
|
|
935
|
+
const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
|
|
809
936
|
if (profile) {
|
|
810
937
|
rules.push(...profile.rules);
|
|
811
938
|
avoids.push(...profile.avoid);
|
|
812
939
|
}
|
|
813
940
|
}
|
|
814
|
-
if (
|
|
815
|
-
const profile = listProfiles("frontend").find((p) => p.name ===
|
|
941
|
+
if (config.frontendFramework) {
|
|
942
|
+
const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
|
|
816
943
|
if (profile) {
|
|
817
944
|
rules.push(...profile.rules);
|
|
818
945
|
avoids.push(...profile.avoid);
|
|
819
946
|
}
|
|
820
947
|
}
|
|
821
948
|
if (rules.length === 0) return;
|
|
822
|
-
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
823
|
-
const chatModel = await resolveModel(ollamaUrl, process.env.OLLAMA_CHAT_MODEL ?? "llama3.2");
|
|
824
949
|
const MAX_DIFF = 8e3;
|
|
825
950
|
const truncated = diff.length > MAX_DIFF;
|
|
826
951
|
const diffToSend = truncated ? diff.slice(0, MAX_DIFF) + "\n\n[diff truncated]" : diff;
|
|
827
952
|
console.log(chalk.cyan("\n archmind \u2014 checking staged changes against rules\u2026"));
|
|
828
|
-
if (options.verbose) {
|
|
829
|
-
console.log(chalk.gray(` model: ${
|
|
953
|
+
if (options.verbose || options.debug) {
|
|
954
|
+
console.log(chalk.gray(` model: ${getChatProviderLabel()} rules: ${rules.length} diff: ${diff.length} chars${truncated ? " (truncated)" : ""}`));
|
|
830
955
|
}
|
|
831
956
|
const rulesWithReasons = rules.map((r, i) => {
|
|
832
957
|
const why = reasonMap.get(r);
|
|
833
958
|
return why ? `${i + 1}. ${r}
|
|
834
959
|
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
835
960
|
}).join("\n");
|
|
961
|
+
const ignorePatterns = await loadIgnorePatterns();
|
|
836
962
|
const systemPrompt = `You are a strict code reviewer enforcing architecture and framework rules.
|
|
837
963
|
Analyze the git diff and identify ONLY clear, definite rule violations \u2014 not style preferences.
|
|
838
964
|
Use the WHY for each rule to understand intent and judge edge cases correctly.
|
|
@@ -843,38 +969,34 @@ ${rulesWithReasons}
|
|
|
843
969
|
Things that must never appear:
|
|
844
970
|
${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
|
|
845
971
|
|
|
972
|
+
Never flag these accepted project patterns:
|
|
973
|
+
${ignorePatterns.length ? ignorePatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
|
|
974
|
+
|
|
846
975
|
IMPORTANT: You MUST respond with a JSON object that has a "violations" key containing an array.
|
|
847
976
|
For each violation include a "reason" field \u2014 copy the WHY from the rule to explain to the developer why this matters.
|
|
848
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"}]}
|
|
849
978
|
Example with no violations: {"violations":[]}
|
|
850
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
|
+
}
|
|
851
989
|
let violations = [];
|
|
852
990
|
try {
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
body: JSON.stringify({
|
|
857
|
-
model: chatModel,
|
|
858
|
-
messages: [
|
|
859
|
-
{ role: "system", content: systemPrompt },
|
|
860
|
-
{ role: "user", content: `Review this diff:
|
|
991
|
+
const raw = await callChatModel([
|
|
992
|
+
{ role: "system", content: systemPrompt },
|
|
993
|
+
{ role: "user", content: `Review this diff:
|
|
861
994
|
|
|
862
995
|
${diffToSend}` }
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
})
|
|
867
|
-
});
|
|
868
|
-
if (!res.ok) {
|
|
869
|
-
const body = await res.text();
|
|
870
|
-
if (body.includes("not found") || body.includes("model")) {
|
|
871
|
-
printModelMissing(chatModel);
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
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)}`));
|
|
875
999
|
}
|
|
876
|
-
const data = await res.json();
|
|
877
|
-
const raw = data.message.content.trim();
|
|
878
1000
|
try {
|
|
879
1001
|
const parsed = JSON.parse(raw);
|
|
880
1002
|
if (Array.isArray(parsed)) {
|
|
@@ -889,10 +1011,11 @@ ${diffToSend}` }
|
|
|
889
1011
|
} catch {
|
|
890
1012
|
violations = [];
|
|
891
1013
|
}
|
|
892
|
-
if (options.verbose) {
|
|
893
|
-
console.log(chalk.gray(` raw response: ${raw.slice(0, 200)}`));
|
|
894
|
-
}
|
|
895
1014
|
} catch (err) {
|
|
1015
|
+
if (err.message?.startsWith("MODEL_NOT_FOUND:")) {
|
|
1016
|
+
printModelMissing(err.message.split(":")[1]);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
896
1019
|
if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
|
|
897
1020
|
console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 skipping rule check."));
|
|
898
1021
|
console.log(chalk.gray(" Start it: ollama serve\n"));
|
|
@@ -928,6 +1051,96 @@ ${diffToSend}` }
|
|
|
928
1051
|
console.log(chalk.dim(" To bypass (not recommended): git commit --no-verify"));
|
|
929
1052
|
console.log(chalk.dim(' To save as memory: memory-core remember "<lesson>"'));
|
|
930
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);
|
|
931
1144
|
process.exit(1);
|
|
932
1145
|
}
|
|
933
1146
|
function printModelMissing(model) {
|
|
@@ -940,27 +1153,13 @@ function printModelMissing(model) {
|
|
|
940
1153
|
|
|
941
1154
|
// src/watcher.ts
|
|
942
1155
|
import { watch } from "chokidar";
|
|
943
|
-
import {
|
|
944
|
-
import { existsSync as existsSync5, readFileSync as
|
|
1156
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1157
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
945
1158
|
import { join as join5, relative } from "path";
|
|
946
1159
|
import chalk2 from "chalk";
|
|
947
|
-
async function resolveModel2(ollamaUrl, chatModel) {
|
|
948
|
-
try {
|
|
949
|
-
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3e3) });
|
|
950
|
-
if (!res.ok) return chatModel;
|
|
951
|
-
const data = await res.json();
|
|
952
|
-
const models = data.models ?? [];
|
|
953
|
-
const exact = models.find((m) => m.name === chatModel);
|
|
954
|
-
if (exact) return exact.name;
|
|
955
|
-
const prefixed = models.find((m) => m.name.startsWith(`${chatModel}:`));
|
|
956
|
-
if (prefixed) return prefixed.name;
|
|
957
|
-
} catch {
|
|
958
|
-
}
|
|
959
|
-
return chatModel;
|
|
960
|
-
}
|
|
961
1160
|
function getFileLines(filePath) {
|
|
962
1161
|
try {
|
|
963
|
-
return
|
|
1162
|
+
return readFileSync5(filePath, "utf-8").split("\n");
|
|
964
1163
|
} catch {
|
|
965
1164
|
return [];
|
|
966
1165
|
}
|
|
@@ -986,27 +1185,43 @@ var SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|s
|
|
|
986
1185
|
var reasonMap2 = new Map(
|
|
987
1186
|
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
988
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
|
+
}
|
|
989
1204
|
function loadConfig(cwd) {
|
|
990
1205
|
const configPath = join5(cwd, ".memory-core.json");
|
|
991
1206
|
if (!existsSync5(configPath)) return null;
|
|
992
1207
|
try {
|
|
993
|
-
return JSON.parse(
|
|
1208
|
+
return JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
994
1209
|
} catch {
|
|
995
1210
|
return null;
|
|
996
1211
|
}
|
|
997
1212
|
}
|
|
998
|
-
function getProfileRules(
|
|
1213
|
+
function getProfileRules(config) {
|
|
999
1214
|
const rules = [];
|
|
1000
1215
|
const avoids = [];
|
|
1001
|
-
if (
|
|
1002
|
-
const profile = listProfiles("backend").find((p) => p.name ===
|
|
1216
|
+
if (config.backendArchitecture) {
|
|
1217
|
+
const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
|
|
1003
1218
|
if (profile) {
|
|
1004
1219
|
rules.push(...profile.rules);
|
|
1005
1220
|
avoids.push(...profile.avoid);
|
|
1006
1221
|
}
|
|
1007
1222
|
}
|
|
1008
|
-
if (
|
|
1009
|
-
const profile = listProfiles("frontend").find((p) => p.name ===
|
|
1223
|
+
if (config.frontendFramework) {
|
|
1224
|
+
const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
|
|
1010
1225
|
if (profile) {
|
|
1011
1226
|
rules.push(...profile.rules);
|
|
1012
1227
|
avoids.push(...profile.avoid);
|
|
@@ -1014,30 +1229,33 @@ function getProfileRules(config2) {
|
|
|
1014
1229
|
}
|
|
1015
1230
|
return { rules, avoids };
|
|
1016
1231
|
}
|
|
1017
|
-
async function
|
|
1018
|
-
const rel = relative(cwd, filePath);
|
|
1019
|
-
let diff;
|
|
1232
|
+
async function loadIgnorePatterns2() {
|
|
1020
1233
|
try {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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);
|
|
1025
1238
|
} catch {
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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 ?? "";
|
|
1031
1251
|
}
|
|
1032
1252
|
if (!diff.trim()) return;
|
|
1033
|
-
const { rules, avoids } = getProfileRules(
|
|
1253
|
+
const { rules, avoids } = getProfileRules(config);
|
|
1034
1254
|
if (rules.length === 0) return;
|
|
1035
|
-
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
1036
|
-
const chatModel = await resolveModel2(ollamaUrl, process.env.OLLAMA_CHAT_MODEL ?? "llama3.2");
|
|
1037
1255
|
const MAX_DIFF = 6e3;
|
|
1038
1256
|
const truncated = diff.length > MAX_DIFF;
|
|
1039
1257
|
const diffToSend = truncated ? diff.slice(0, MAX_DIFF) + "\n\n[diff truncated]" : diff;
|
|
1040
|
-
if (verbose) {
|
|
1258
|
+
if (verbose || debug) {
|
|
1041
1259
|
console.log(chalk2.dim(`
|
|
1042
1260
|
[watch] checking ${rel} (${diff.length} chars)\u2026`));
|
|
1043
1261
|
}
|
|
@@ -1046,6 +1264,7 @@ async function checkFile(filePath, cwd, config2, verbose) {
|
|
|
1046
1264
|
return why ? `${i + 1}. ${r}
|
|
1047
1265
|
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
1048
1266
|
}).join("\n");
|
|
1267
|
+
const ignorePatterns = await loadIgnorePatterns2();
|
|
1049
1268
|
const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
|
|
1050
1269
|
Analyze the file diff and identify ONLY clear, definite rule violations.
|
|
1051
1270
|
Use the WHY for each rule to understand intent and judge edge cases.
|
|
@@ -1056,36 +1275,33 @@ ${rulesWithReasons}
|
|
|
1056
1275
|
Things that must never appear:
|
|
1057
1276
|
${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
|
|
1058
1277
|
|
|
1278
|
+
Never flag these accepted project patterns:
|
|
1279
|
+
${ignorePatterns.length ? ignorePatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
|
|
1280
|
+
|
|
1059
1281
|
IMPORTANT: Respond with JSON: {"violations":[...]} or {"violations":[]}.
|
|
1060
1282
|
Each violation: {"rule":"...","file":"...","line":N,"issue":"...","suggestion":"...","reason":"..."}.
|
|
1061
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
|
+
}
|
|
1062
1293
|
try {
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
body: JSON.stringify({
|
|
1067
|
-
model: chatModel,
|
|
1068
|
-
messages: [
|
|
1069
|
-
{ role: "system", content: systemPrompt },
|
|
1070
|
-
{ 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}:
|
|
1071
1297
|
|
|
1072
1298
|
${diffToSend}` }
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
if (!res.ok) {
|
|
1079
|
-
const body = await res.text();
|
|
1080
|
-
if (body.includes("not found") || body.includes("model")) {
|
|
1081
|
-
console.log(chalk2.yellow(`
|
|
1082
|
-
\u26A0 Chat model "${chatModel}" not found. Pull it: ollama pull ${chatModel}
|
|
1083
|
-
`));
|
|
1084
|
-
}
|
|
1085
|
-
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"));
|
|
1086
1304
|
}
|
|
1087
|
-
const data = await res.json();
|
|
1088
|
-
const raw = data.message.content.trim();
|
|
1089
1305
|
let violations = [];
|
|
1090
1306
|
try {
|
|
1091
1307
|
const parsed = JSON.parse(raw);
|
|
@@ -1121,6 +1337,7 @@ ${diffToSend}` }
|
|
|
1121
1337
|
if (v.suggestion) console.log(chalk2.green(" Fix: ") + v.suggestion);
|
|
1122
1338
|
console.log();
|
|
1123
1339
|
});
|
|
1340
|
+
recordViolations2(violations);
|
|
1124
1341
|
console.log(chalk2.dim(' Fix violations or run: memory-core remember "<lesson>"'));
|
|
1125
1342
|
console.log();
|
|
1126
1343
|
} catch (err) {
|
|
@@ -1134,22 +1351,20 @@ ${diffToSend}` }
|
|
|
1134
1351
|
}
|
|
1135
1352
|
async function startWatch(options = {}) {
|
|
1136
1353
|
const cwd = process.cwd();
|
|
1137
|
-
const
|
|
1138
|
-
if (!
|
|
1354
|
+
const config = loadConfig(cwd);
|
|
1355
|
+
if (!config) {
|
|
1139
1356
|
console.error(chalk2.red("\n No .memory-core.json found. Run: memory-core init\n"));
|
|
1140
1357
|
process.exit(1);
|
|
1141
1358
|
}
|
|
1142
|
-
const { rules } = getProfileRules(
|
|
1359
|
+
const { rules } = getProfileRules(config);
|
|
1143
1360
|
if (rules.length === 0) {
|
|
1144
1361
|
console.log(chalk2.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to watch.\n"));
|
|
1145
1362
|
process.exit(0);
|
|
1146
1363
|
}
|
|
1147
1364
|
const watchPath = options.path ?? cwd;
|
|
1148
|
-
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
1149
|
-
const chatModel = await resolveModel2(ollamaUrl, process.env.OLLAMA_CHAT_MODEL ?? "llama3.2");
|
|
1150
1365
|
console.log(chalk2.cyan("\n archmind watch \u2014 real-time rule enforcement\n"));
|
|
1151
1366
|
console.log(chalk2.dim(` watching: ${watchPath}`));
|
|
1152
|
-
console.log(chalk2.dim(` model: ${
|
|
1367
|
+
console.log(chalk2.dim(` model: ${getChatProviderLabel()}`));
|
|
1153
1368
|
console.log(chalk2.dim(` rules: ${rules.length}`));
|
|
1154
1369
|
console.log(chalk2.dim(" ctrl+c to stop\n"));
|
|
1155
1370
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -1188,14 +1403,14 @@ async function startWatch(options = {}) {
|
|
|
1188
1403
|
}
|
|
1189
1404
|
return;
|
|
1190
1405
|
}
|
|
1191
|
-
await checkFile(filePath, cwd,
|
|
1406
|
+
await checkFile(filePath, cwd, config, options.verbose ?? false, options.debug ?? false);
|
|
1192
1407
|
}, 300);
|
|
1193
1408
|
pending.set(filePath, timer);
|
|
1194
1409
|
};
|
|
1195
1410
|
watcher.on("add", handle);
|
|
1196
1411
|
watcher.on("change", handle);
|
|
1197
1412
|
watcher.on("error", (err) => {
|
|
1198
|
-
console.error(chalk2.red(` watcher error: ${err.message}`));
|
|
1413
|
+
console.error(chalk2.red(` watcher error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1199
1414
|
});
|
|
1200
1415
|
process.on("SIGINT", () => {
|
|
1201
1416
|
console.log(chalk2.dim("\n\n archmind watch stopped.\n"));
|
|
@@ -1207,7 +1422,7 @@ async function startWatch(options = {}) {
|
|
|
1207
1422
|
|
|
1208
1423
|
// src/cli.ts
|
|
1209
1424
|
function printBanner(projectName, agentCount, status) {
|
|
1210
|
-
const
|
|
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");
|
|
1211
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;
|
|
1212
1427
|
const lines = [
|
|
1213
1428
|
"",
|
|
@@ -1222,7 +1437,7 @@ function printBanner(projectName, agentCount, status) {
|
|
|
1222
1437
|
"",
|
|
1223
1438
|
chalk3.green(` \u2713 Project `) + chalk3.bold(projectName),
|
|
1224
1439
|
chalk3.green(` \u2713 Agents `) + chalk3.bold(`${agentCount} AI agents configured`),
|
|
1225
|
-
|
|
1440
|
+
pg,
|
|
1226
1441
|
...ol ? [ol] : [],
|
|
1227
1442
|
"",
|
|
1228
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"),
|
|
@@ -1238,13 +1453,13 @@ function printBanner(projectName, agentCount, status) {
|
|
|
1238
1453
|
];
|
|
1239
1454
|
lines.forEach((l) => console.log(l));
|
|
1240
1455
|
}
|
|
1241
|
-
async function checkConnections(dbUrl,
|
|
1456
|
+
async function checkConnections(dbUrl, ollamaUrl2, chatModel) {
|
|
1242
1457
|
const spinner = ora("Checking connections\u2026").start();
|
|
1243
1458
|
let postgresOk = false;
|
|
1244
1459
|
let ollamaOk = false;
|
|
1245
1460
|
try {
|
|
1246
|
-
const { Pool
|
|
1247
|
-
const testPool = new
|
|
1461
|
+
const { Pool } = (await import("pg")).default;
|
|
1462
|
+
const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
|
|
1248
1463
|
await testPool.query("SELECT 1");
|
|
1249
1464
|
await testPool.end();
|
|
1250
1465
|
postgresOk = true;
|
|
@@ -1252,7 +1467,7 @@ async function checkConnections(dbUrl, ollamaUrl, chatModel) {
|
|
|
1252
1467
|
postgresOk = false;
|
|
1253
1468
|
}
|
|
1254
1469
|
try {
|
|
1255
|
-
const res = await fetch(`${
|
|
1470
|
+
const res = await fetch(`${ollamaUrl2}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1256
1471
|
ollamaOk = res.ok;
|
|
1257
1472
|
} catch {
|
|
1258
1473
|
ollamaOk = false;
|
|
@@ -1267,28 +1482,74 @@ async function checkConnections(dbUrl, ollamaUrl, chatModel) {
|
|
|
1267
1482
|
console.log();
|
|
1268
1483
|
return { postgresOk, ollamaOk, chatModel };
|
|
1269
1484
|
}
|
|
1270
|
-
var { version } = JSON.parse(
|
|
1485
|
+
var { version } = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1271
1486
|
var CONFIG_FILE = ".memory-core.json";
|
|
1272
1487
|
function readProjectConfig() {
|
|
1273
1488
|
const path = join6(process.cwd(), CONFIG_FILE);
|
|
1274
1489
|
if (!existsSync6(path)) return null;
|
|
1275
1490
|
try {
|
|
1276
|
-
return JSON.parse(
|
|
1491
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
1277
1492
|
} catch {
|
|
1278
1493
|
return null;
|
|
1279
1494
|
}
|
|
1280
1495
|
}
|
|
1281
|
-
function writeProjectConfig(
|
|
1282
|
-
|
|
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"));
|
|
1283
1520
|
}
|
|
1284
1521
|
var program = new Command();
|
|
1285
1522
|
program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version(version);
|
|
1286
|
-
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) => {
|
|
1287
1524
|
console.log(chalk3.bold.cyan("\n memory-core init\n"));
|
|
1288
1525
|
const detected = detectProject();
|
|
1526
|
+
const quick = opts.quick ?? false;
|
|
1289
1527
|
const envPath = join6(process.cwd(), ".memory-core.env");
|
|
1290
1528
|
const hasEnv = existsSync6(envPath) || existsSync6(join6(process.cwd(), ".env")) || !!process.env.DATABASE_URL;
|
|
1291
|
-
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) {
|
|
1292
1553
|
console.log(chalk3.dim(" No .memory-core.env found \u2014 let's set up your connection.\n"));
|
|
1293
1554
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
1294
1555
|
let dbUrl = "";
|
|
@@ -1299,8 +1560,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1299
1560
|
});
|
|
1300
1561
|
const pgSpinner = ora(" Testing PostgreSQL connection\u2026").start();
|
|
1301
1562
|
try {
|
|
1302
|
-
const { Pool
|
|
1303
|
-
const testPool = new
|
|
1563
|
+
const { Pool } = (await import("pg")).default;
|
|
1564
|
+
const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
|
|
1304
1565
|
await testPool.query("SELECT 1");
|
|
1305
1566
|
await testPool.end();
|
|
1306
1567
|
pgSpinner.succeed(chalk3.green("PostgreSQL connected"));
|
|
@@ -1310,15 +1571,15 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1310
1571
|
console.log(chalk3.yellow(" Please check that PostgreSQL is running and the URL is correct.\n"));
|
|
1311
1572
|
}
|
|
1312
1573
|
}
|
|
1313
|
-
let
|
|
1574
|
+
let ollamaUrl2 = "";
|
|
1314
1575
|
while (true) {
|
|
1315
|
-
|
|
1576
|
+
ollamaUrl2 = await input({
|
|
1316
1577
|
message: "Ollama URL?",
|
|
1317
|
-
default:
|
|
1578
|
+
default: ollamaUrl2 || "http://localhost:11434"
|
|
1318
1579
|
});
|
|
1319
1580
|
const ollamaSpinner = ora(" Testing Ollama connection\u2026").start();
|
|
1320
1581
|
try {
|
|
1321
|
-
const res = await fetch(`${
|
|
1582
|
+
const res = await fetch(`${ollamaUrl2}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1322
1583
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1323
1584
|
ollamaSpinner.succeed(chalk3.green("Ollama connected"));
|
|
1324
1585
|
break;
|
|
@@ -1327,69 +1588,117 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1327
1588
|
console.log(chalk3.yellow(" Make sure Ollama is running: ollama serve\n"));
|
|
1328
1589
|
}
|
|
1329
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
|
+
});
|
|
1330
1600
|
let chatModel = "";
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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.
|
|
1358
1630
|
`));
|
|
1631
|
+
}
|
|
1632
|
+
} catch {
|
|
1633
|
+
modelSpinner.warn(chalk3.yellow("Could not verify model \u2014 continuing anyway"));
|
|
1634
|
+
break;
|
|
1359
1635
|
}
|
|
1360
|
-
} catch {
|
|
1361
|
-
modelSpinner.warn(chalk3.yellow("Could not verify model \u2014 continuing anyway"));
|
|
1362
|
-
break;
|
|
1363
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`));
|
|
1364
1665
|
}
|
|
1365
|
-
const
|
|
1666
|
+
const envLines = [
|
|
1366
1667
|
`DATABASE_URL=${dbUrl}`,
|
|
1367
|
-
`OLLAMA_URL=${
|
|
1668
|
+
`OLLAMA_URL=${ollamaUrl2}`,
|
|
1368
1669
|
`OLLAMA_MODEL=nomic-embed-text`,
|
|
1369
|
-
`
|
|
1370
|
-
|
|
1371
|
-
|
|
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);
|
|
1372
1677
|
process.env.DATABASE_URL = dbUrl;
|
|
1373
|
-
process.env.OLLAMA_URL =
|
|
1678
|
+
process.env.OLLAMA_URL = ollamaUrl2;
|
|
1374
1679
|
process.env.OLLAMA_MODEL = "nomic-embed-text";
|
|
1375
|
-
process.env.
|
|
1376
|
-
|
|
1377
|
-
if (
|
|
1378
|
-
|
|
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;
|
|
1684
|
+
const gitignorePath2 = join6(process.cwd(), ".gitignore");
|
|
1685
|
+
if (existsSync6(gitignorePath2)) {
|
|
1686
|
+
const gi = readFileSync6(gitignorePath2, "utf-8");
|
|
1379
1687
|
if (!gi.includes(".memory-core.env")) {
|
|
1380
|
-
appendFileSync(
|
|
1688
|
+
appendFileSync(gitignorePath2, "\n.memory-core.env\n");
|
|
1381
1689
|
}
|
|
1382
1690
|
} else {
|
|
1383
|
-
|
|
1691
|
+
writeFileSync5(gitignorePath2, ".memory-core.env\n");
|
|
1384
1692
|
}
|
|
1385
1693
|
console.log(chalk3.green("\n \u2713 .memory-core.env created"));
|
|
1386
1694
|
console.log(chalk3.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
|
|
1387
1695
|
}
|
|
1388
|
-
const projectName = await input({
|
|
1696
|
+
const projectName = quick ? process.cwd().split("/").pop() ?? "my-project" : await input({
|
|
1389
1697
|
message: "Project name?",
|
|
1390
1698
|
default: process.cwd().split("/").pop() ?? "my-project"
|
|
1391
1699
|
});
|
|
1392
|
-
const
|
|
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({
|
|
1393
1702
|
message: "Project type?",
|
|
1394
1703
|
choices: [
|
|
1395
1704
|
{ value: "backend", name: "Backend \u2014 API, server, microservice" },
|
|
@@ -1399,35 +1708,50 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1399
1708
|
});
|
|
1400
1709
|
let backendArchitecture;
|
|
1401
1710
|
if (projectType === "backend" || projectType === "fullstack") {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
+
}
|
|
1410
1723
|
}
|
|
1411
1724
|
let frontendFramework;
|
|
1412
1725
|
if (projectType === "frontend" || projectType === "fullstack") {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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
|
+
}
|
|
1421
1745
|
}
|
|
1422
|
-
const language = await input({
|
|
1746
|
+
const language = quick ? detected.language : await input({
|
|
1423
1747
|
message: "Language?",
|
|
1424
1748
|
default: detected.language
|
|
1425
1749
|
});
|
|
1426
|
-
const pullMemories = await confirm({
|
|
1750
|
+
const pullMemories = quick ? true : await confirm({
|
|
1427
1751
|
message: "Pull relevant memories from previous projects?",
|
|
1428
1752
|
default: true
|
|
1429
1753
|
});
|
|
1430
|
-
const installCaveman = await confirm({
|
|
1754
|
+
const installCaveman = quick ? false : await confirm({
|
|
1431
1755
|
message: "Install caveman token saver? (~65-75% fewer tokens)",
|
|
1432
1756
|
default: false
|
|
1433
1757
|
});
|
|
@@ -1442,17 +1766,30 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1442
1766
|
]
|
|
1443
1767
|
});
|
|
1444
1768
|
}
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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({
|
|
1778
|
+
message: "Enable pre-commit hook?",
|
|
1453
1779
|
default: true
|
|
1454
1780
|
});
|
|
1455
|
-
|
|
1781
|
+
let hookAdvisory = true;
|
|
1782
|
+
if (enableHook && !quick) {
|
|
1783
|
+
const { select: selectMode } = await import("@inquirer/prompts");
|
|
1784
|
+
hookAdvisory = await selectMode({
|
|
1785
|
+
message: "Hook mode?",
|
|
1786
|
+
choices: [
|
|
1787
|
+
{ value: true, name: "Advisory \u2014 logs violations, never blocks commits (recommended)" },
|
|
1788
|
+
{ value: false, name: "Strict \u2014 blocks commits that violate your rules" }
|
|
1789
|
+
]
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
const config = {
|
|
1456
1793
|
projectName,
|
|
1457
1794
|
projectType,
|
|
1458
1795
|
backendArchitecture,
|
|
@@ -1475,7 +1812,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1475
1812
|
if (installCaveman) {
|
|
1476
1813
|
const spinner2 = ora("Installing caveman token saver\u2026").start();
|
|
1477
1814
|
try {
|
|
1478
|
-
|
|
1815
|
+
execSync2(
|
|
1479
1816
|
"curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash",
|
|
1480
1817
|
{ stdio: "pipe", cwd: process.cwd() }
|
|
1481
1818
|
);
|
|
@@ -1486,31 +1823,42 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1486
1823
|
}
|
|
1487
1824
|
const spinner = ora("Generating AI agent context files\u2026").start();
|
|
1488
1825
|
const written = await generate(
|
|
1489
|
-
{ projectName, projectType, backendArchitecture, frontendFramework, language, memories, caveman:
|
|
1826
|
+
{ projectName, projectType, backendArchitecture, frontendFramework, language, memories, caveman: config.caveman },
|
|
1490
1827
|
process.cwd(),
|
|
1491
1828
|
[...selectedAgents, "Shared"]
|
|
1492
1829
|
);
|
|
1493
|
-
writeProjectConfig(
|
|
1830
|
+
writeProjectConfig(config);
|
|
1494
1831
|
spinner.succeed(`Generated ${written.written.length} files`);
|
|
1832
|
+
const gitignorePath = join6(process.cwd(), ".gitignore");
|
|
1833
|
+
const generatedPaths = written.written;
|
|
1834
|
+
if (generatedPaths.length > 0) {
|
|
1835
|
+
const existing = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
|
|
1836
|
+
const toAdd = generatedPaths.filter((p) => !existing.includes(p));
|
|
1837
|
+
if (toAdd.length > 0) {
|
|
1838
|
+
const block = "\n# memory-core generated files\n" + toAdd.join("\n") + "\n";
|
|
1839
|
+
appendFileSync(gitignorePath, block);
|
|
1840
|
+
console.log(chalk3.green(` \u2713 Added ${toAdd.length} generated files to .gitignore`));
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1495
1843
|
if (enableHook) {
|
|
1496
|
-
installHook();
|
|
1844
|
+
installHook(hookAdvisory);
|
|
1497
1845
|
}
|
|
1498
1846
|
const status = await checkConnections(
|
|
1499
1847
|
process.env.DATABASE_URL ?? "",
|
|
1500
1848
|
process.env.OLLAMA_URL ?? "http://localhost:11434",
|
|
1501
1849
|
process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"
|
|
1502
1850
|
);
|
|
1503
|
-
printBanner(
|
|
1851
|
+
printBanner(config.projectName, written.written.length, status);
|
|
1504
1852
|
await closePool();
|
|
1505
1853
|
});
|
|
1506
1854
|
program.command("sync").description("Re-pull memories and regenerate AI agent files").action(async () => {
|
|
1507
|
-
const
|
|
1508
|
-
if (!
|
|
1855
|
+
const config = readProjectConfig();
|
|
1856
|
+
if (!config) {
|
|
1509
1857
|
console.error(chalk3.red("No .memory-core.json found. Run: memory-core init"));
|
|
1510
1858
|
process.exit(1);
|
|
1511
1859
|
}
|
|
1512
1860
|
const { checkbox } = await import("@inquirer/prompts");
|
|
1513
|
-
const savedAgents = new Set(
|
|
1861
|
+
const savedAgents = new Set(config.agents ?? AGENT_NAMES.filter((a) => a !== "Shared"));
|
|
1514
1862
|
const selectedAgents = await checkbox({
|
|
1515
1863
|
message: "Which agents do you want to sync?",
|
|
1516
1864
|
choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({
|
|
@@ -1527,21 +1875,21 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
1527
1875
|
const spinner = ora("Syncing memories\u2026").start();
|
|
1528
1876
|
let memories = [];
|
|
1529
1877
|
try {
|
|
1530
|
-
const archQuery = [
|
|
1531
|
-
memories = await retrieve(archQuery,
|
|
1878
|
+
const archQuery = [config.backendArchitecture, config.frontendFramework, config.language].filter(Boolean).join(" ");
|
|
1879
|
+
memories = await retrieve(archQuery, config.backendArchitecture ?? config.frontendFramework, 10);
|
|
1532
1880
|
spinner.text = `Found ${memories.length} memories \u2014 regenerating files\u2026`;
|
|
1533
1881
|
} catch (err) {
|
|
1534
1882
|
spinner.warn(`Could not retrieve memories: ${err.message}`);
|
|
1535
1883
|
}
|
|
1536
1884
|
const result = await generate(
|
|
1537
1885
|
{
|
|
1538
|
-
projectName:
|
|
1539
|
-
projectType:
|
|
1540
|
-
backendArchitecture:
|
|
1541
|
-
frontendFramework:
|
|
1542
|
-
language:
|
|
1886
|
+
projectName: config.projectName,
|
|
1887
|
+
projectType: config.projectType,
|
|
1888
|
+
backendArchitecture: config.backendArchitecture,
|
|
1889
|
+
frontendFramework: config.frontendFramework,
|
|
1890
|
+
language: config.language,
|
|
1543
1891
|
memories,
|
|
1544
|
-
caveman:
|
|
1892
|
+
caveman: config.caveman
|
|
1545
1893
|
},
|
|
1546
1894
|
process.cwd(),
|
|
1547
1895
|
[...selectedAgents, "Shared"]
|
|
@@ -1557,7 +1905,7 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
1557
1905
|
await closePool();
|
|
1558
1906
|
});
|
|
1559
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) => {
|
|
1560
|
-
const
|
|
1908
|
+
const config = readProjectConfig();
|
|
1561
1909
|
let reason = opts.reason;
|
|
1562
1910
|
if (!reason) {
|
|
1563
1911
|
reason = await input({
|
|
@@ -1571,8 +1919,8 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
1571
1919
|
await saveMemory({
|
|
1572
1920
|
type: opts.type,
|
|
1573
1921
|
scope: opts.scope,
|
|
1574
|
-
architecture:
|
|
1575
|
-
projectName:
|
|
1922
|
+
architecture: config?.backendArchitecture ?? config?.frontendFramework,
|
|
1923
|
+
projectName: config?.projectName,
|
|
1576
1924
|
content: text,
|
|
1577
1925
|
reason: reason || void 0,
|
|
1578
1926
|
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [],
|
|
@@ -1588,12 +1936,12 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
1588
1936
|
await closePool();
|
|
1589
1937
|
});
|
|
1590
1938
|
program.command("search <query>").description("Search memories using semantic similarity").option("-n, --limit <n>", "Number of results", "5").action(async (query, opts) => {
|
|
1591
|
-
const
|
|
1939
|
+
const config = readProjectConfig();
|
|
1592
1940
|
const spinner = ora("Searching\u2026").start();
|
|
1593
1941
|
try {
|
|
1594
1942
|
const results = await retrieve(
|
|
1595
1943
|
query,
|
|
1596
|
-
|
|
1944
|
+
config?.backendArchitecture ?? config?.frontendFramework,
|
|
1597
1945
|
parseInt(opts.limit, 10)
|
|
1598
1946
|
);
|
|
1599
1947
|
spinner.stop();
|
|
@@ -1617,6 +1965,222 @@ program.command("search <query>").description("Search memories using semantic si
|
|
|
1617
1965
|
}
|
|
1618
1966
|
await closePool();
|
|
1619
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
|
+
});
|
|
1620
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) => {
|
|
1621
2185
|
await runMigrations();
|
|
1622
2186
|
const filtered = opts.arch ? seeds.filter((s) => s.architecture === opts.arch || s.architecture === "global") : seeds;
|
|
@@ -1629,7 +2193,7 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
1629
2193
|
const spinner = ora(`[${seed.architecture}] ${seed.title}`).start();
|
|
1630
2194
|
try {
|
|
1631
2195
|
const embedding = await embed(seed.content);
|
|
1632
|
-
|
|
2196
|
+
const payload = {
|
|
1633
2197
|
type: seed.type,
|
|
1634
2198
|
scope: seed.scope,
|
|
1635
2199
|
architecture: seed.architecture === "global" ? void 0 : seed.architecture,
|
|
@@ -1638,9 +2202,21 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
1638
2202
|
reason: seed.reason,
|
|
1639
2203
|
tags: seed.tags,
|
|
1640
2204
|
embedding
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
|
|
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
|
+
}
|
|
1644
2220
|
} catch (err) {
|
|
1645
2221
|
spinner.warn(`Skipped \u2014 ${err.message}`);
|
|
1646
2222
|
skipped++;
|
|
@@ -1694,12 +2270,12 @@ ${rulesText}
|
|
|
1694
2270
|
const skipped = [];
|
|
1695
2271
|
const writeFile2 = (filePath, content) => {
|
|
1696
2272
|
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
1697
|
-
|
|
2273
|
+
writeFileSync5(filePath, content, "utf-8");
|
|
1698
2274
|
};
|
|
1699
2275
|
const readJson = (filePath) => {
|
|
1700
2276
|
if (!existsSync6(filePath)) return {};
|
|
1701
2277
|
try {
|
|
1702
|
-
return JSON.parse(
|
|
2278
|
+
return JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
1703
2279
|
} catch {
|
|
1704
2280
|
return {};
|
|
1705
2281
|
}
|
|
@@ -1724,9 +2300,9 @@ ${rulesText}
|
|
|
1724
2300
|
writeFile2(target.path, JSON.stringify(processedVscode.settings, null, 2));
|
|
1725
2301
|
written.push(target.label);
|
|
1726
2302
|
} else if (target.type === "continue") {
|
|
1727
|
-
const
|
|
1728
|
-
|
|
1729
|
-
writeFile2(target.path, JSON.stringify(
|
|
2303
|
+
const config = readJson(target.path);
|
|
2304
|
+
config["systemMessage"] = systemPrompt;
|
|
2305
|
+
writeFile2(target.path, JSON.stringify(config, null, 2));
|
|
1730
2306
|
written.push(target.label);
|
|
1731
2307
|
} else if (target.type === "aider") {
|
|
1732
2308
|
const aiderContent = `# Aider global config \u2014 synced by memory-core
|
|
@@ -1736,11 +2312,11 @@ read:
|
|
|
1736
2312
|
writeFile2(target.path, aiderContent);
|
|
1737
2313
|
written.push(target.label);
|
|
1738
2314
|
} else if (target.type === "zed") {
|
|
1739
|
-
const
|
|
1740
|
-
const assistant =
|
|
2315
|
+
const config = readJson(target.path);
|
|
2316
|
+
const assistant = config["assistant"] ?? {};
|
|
1741
2317
|
assistant["system_prompt"] = systemPrompt;
|
|
1742
|
-
|
|
1743
|
-
writeFile2(target.path, JSON.stringify(
|
|
2318
|
+
config["assistant"] = assistant;
|
|
2319
|
+
writeFile2(target.path, JSON.stringify(config, null, 2));
|
|
1744
2320
|
written.push(target.label);
|
|
1745
2321
|
}
|
|
1746
2322
|
} catch {
|
|
@@ -1758,16 +2334,21 @@ read:
|
|
|
1758
2334
|
await closePool();
|
|
1759
2335
|
});
|
|
1760
2336
|
var hook = program.command("hook").description("Manage the pre-commit rule enforcement hook");
|
|
1761
|
-
hook.command("install").description("Install pre-commit hook \u2014 blocks commits that violate your
|
|
1762
|
-
|
|
2337
|
+
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
|
+
const advisory = opts.strict ? false : true;
|
|
2339
|
+
installHook(advisory);
|
|
1763
2340
|
});
|
|
1764
2341
|
hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
|
|
1765
2342
|
uninstallHook();
|
|
1766
2343
|
});
|
|
1767
|
-
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) => {
|
|
1768
|
-
|
|
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 });
|
|
1769
2350
|
});
|
|
1770
|
-
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) => {
|
|
1771
|
-
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 });
|
|
1772
2353
|
});
|
|
1773
2354
|
program.parseAsync(process.argv);
|