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