@shahmilsaari/memory-core 0.2.12 → 0.2.13
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.
|
@@ -82,8 +82,17 @@ async function listMemories(filters = {}) {
|
|
|
82
82
|
where.push(`scope = $${params.length}`);
|
|
83
83
|
}
|
|
84
84
|
if (filters.architecture) {
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
if (Array.isArray(filters.architecture)) {
|
|
86
|
+
params.push(filters.architecture);
|
|
87
|
+
where.push(`architecture = ANY($${params.length})`);
|
|
88
|
+
} else {
|
|
89
|
+
params.push(filters.architecture);
|
|
90
|
+
where.push(`architecture = $${params.length}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (filters.tags?.length) {
|
|
94
|
+
params.push(filters.tags);
|
|
95
|
+
where.push(`tags && $${params.length}::text[]`);
|
|
87
96
|
}
|
|
88
97
|
const limit = filters.limit ?? 200;
|
|
89
98
|
params.push(limit);
|
|
@@ -112,6 +121,40 @@ async function deleteMemory(id) {
|
|
|
112
121
|
const result = await getPool().query(`DELETE FROM memories WHERE id = $1`, [id]);
|
|
113
122
|
return (result.rowCount ?? 0) > 0;
|
|
114
123
|
}
|
|
124
|
+
async function deleteMemories(filters) {
|
|
125
|
+
await runMigrations();
|
|
126
|
+
const where = [];
|
|
127
|
+
const params = [];
|
|
128
|
+
if (filters.type) {
|
|
129
|
+
params.push(filters.type);
|
|
130
|
+
where.push(`type = $${params.length}`);
|
|
131
|
+
}
|
|
132
|
+
if (filters.scope) {
|
|
133
|
+
params.push(filters.scope);
|
|
134
|
+
where.push(`scope = $${params.length}`);
|
|
135
|
+
}
|
|
136
|
+
if (filters.architecture) {
|
|
137
|
+
if (Array.isArray(filters.architecture)) {
|
|
138
|
+
params.push(filters.architecture);
|
|
139
|
+
where.push(`architecture = ANY($${params.length})`);
|
|
140
|
+
} else {
|
|
141
|
+
params.push(filters.architecture);
|
|
142
|
+
where.push(`architecture = $${params.length}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (filters.tag) {
|
|
146
|
+
params.push(filters.tag);
|
|
147
|
+
where.push(`$${params.length} = ANY(tags)`);
|
|
148
|
+
}
|
|
149
|
+
if (where.length === 0) {
|
|
150
|
+
throw new Error("Refusing to bulk-delete without filters");
|
|
151
|
+
}
|
|
152
|
+
const result = await getPool().query(
|
|
153
|
+
`DELETE FROM memories WHERE ${where.join(" AND ")}`,
|
|
154
|
+
params
|
|
155
|
+
);
|
|
156
|
+
return result.rowCount ?? 0;
|
|
157
|
+
}
|
|
115
158
|
async function updateMemory(id, patch) {
|
|
116
159
|
await runMigrations();
|
|
117
160
|
const current = await getMemory(id);
|
|
@@ -145,14 +188,19 @@ async function updateMemory(id, patch) {
|
|
|
145
188
|
);
|
|
146
189
|
return result.rows[0] ?? null;
|
|
147
190
|
}
|
|
148
|
-
async function searchMemories(embedding,
|
|
191
|
+
async function searchMemories(embedding, architectures, limit = 10) {
|
|
149
192
|
await runMigrations();
|
|
150
193
|
const vector = `[${embedding.join(",")}]`;
|
|
151
194
|
const params = [vector];
|
|
152
195
|
let whereClause = "";
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
196
|
+
const selectedArchitectures = architectures ? (Array.isArray(architectures) ? architectures : [architectures]).filter(Boolean) : [];
|
|
197
|
+
if (selectedArchitectures.length > 0) {
|
|
198
|
+
whereClause = `WHERE (
|
|
199
|
+
architecture = ANY($2)
|
|
200
|
+
OR architecture IS NULL
|
|
201
|
+
OR architecture = 'global'
|
|
202
|
+
)`;
|
|
203
|
+
params.push(selectedArchitectures);
|
|
156
204
|
}
|
|
157
205
|
const client = await getPool().connect();
|
|
158
206
|
try {
|
|
@@ -190,6 +238,7 @@ export {
|
|
|
190
238
|
listMemories,
|
|
191
239
|
getMemory,
|
|
192
240
|
deleteMemory,
|
|
241
|
+
deleteMemories,
|
|
193
242
|
updateMemory,
|
|
194
243
|
searchMemories,
|
|
195
244
|
closePool
|
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";
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -1497,6 +1709,19 @@ function readProjectConfig() {
|
|
|
1497
1709
|
function writeProjectConfig(config) {
|
|
1498
1710
|
writeFileSync5(join6(process.cwd(), CONFIG_FILE), JSON.stringify(config, null, 2));
|
|
1499
1711
|
}
|
|
1712
|
+
function updateProjectConfig(mutator) {
|
|
1713
|
+
const current = readProjectConfig() ?? {
|
|
1714
|
+
projectName: process.cwd().split("/").pop() ?? "project",
|
|
1715
|
+
projectType: "backend",
|
|
1716
|
+
language: detectProject().language,
|
|
1717
|
+
caveman: { enabled: false, intensity: "full" },
|
|
1718
|
+
agents: [],
|
|
1719
|
+
allowPatterns: []
|
|
1720
|
+
};
|
|
1721
|
+
const updated = mutator(current);
|
|
1722
|
+
writeProjectConfig(updated);
|
|
1723
|
+
return updated;
|
|
1724
|
+
}
|
|
1500
1725
|
function parseTags(tags) {
|
|
1501
1726
|
return tags ? tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
1502
1727
|
}
|
|
@@ -1530,17 +1755,17 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1530
1755
|
if (!hasEnv && quick) {
|
|
1531
1756
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
1532
1757
|
const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
|
|
1533
|
-
const
|
|
1758
|
+
const ollamaUrl = "http://localhost:11434";
|
|
1534
1759
|
const chatModel = "llama3.2";
|
|
1535
1760
|
const envContent = [
|
|
1536
1761
|
`DATABASE_URL=${dbUrl}`,
|
|
1537
|
-
`OLLAMA_URL=${
|
|
1762
|
+
`OLLAMA_URL=${ollamaUrl}`,
|
|
1538
1763
|
`OLLAMA_MODEL=nomic-embed-text`,
|
|
1539
1764
|
`OLLAMA_CHAT_MODEL=${chatModel}`
|
|
1540
1765
|
].join("\n") + "\n";
|
|
1541
1766
|
writeFileSync5(envPath, envContent);
|
|
1542
1767
|
process.env.DATABASE_URL = dbUrl;
|
|
1543
|
-
process.env.OLLAMA_URL =
|
|
1768
|
+
process.env.OLLAMA_URL = ollamaUrl;
|
|
1544
1769
|
process.env.OLLAMA_MODEL = "nomic-embed-text";
|
|
1545
1770
|
process.env.OLLAMA_CHAT_MODEL = chatModel;
|
|
1546
1771
|
const gitignorePath2 = join6(process.cwd(), ".gitignore");
|
|
@@ -1572,15 +1797,15 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1572
1797
|
console.log(chalk3.yellow(" Please check that PostgreSQL is running and the URL is correct.\n"));
|
|
1573
1798
|
}
|
|
1574
1799
|
}
|
|
1575
|
-
let
|
|
1800
|
+
let ollamaUrl = "";
|
|
1576
1801
|
while (true) {
|
|
1577
|
-
|
|
1802
|
+
ollamaUrl = await input({
|
|
1578
1803
|
message: "Ollama URL?",
|
|
1579
|
-
default:
|
|
1804
|
+
default: ollamaUrl || "http://localhost:11434"
|
|
1580
1805
|
});
|
|
1581
1806
|
const ollamaSpinner = ora(" Testing Ollama connection\u2026").start();
|
|
1582
1807
|
try {
|
|
1583
|
-
const res = await fetch(`${
|
|
1808
|
+
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1584
1809
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1585
1810
|
ollamaSpinner.succeed(chalk3.green("Ollama connected"));
|
|
1586
1811
|
break;
|
|
@@ -1615,7 +1840,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1615
1840
|
chatModel = chatModelChoice === "__custom__" ? await input({ message: "Model name?", default: "llama3.2" }) : chatModelChoice;
|
|
1616
1841
|
const modelSpinner = ora(` Checking if ${chatModel} is installed\u2026`).start();
|
|
1617
1842
|
try {
|
|
1618
|
-
const res = await fetch(`${
|
|
1843
|
+
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1619
1844
|
const data = await res.json();
|
|
1620
1845
|
const models = data.models ?? [];
|
|
1621
1846
|
const exact = models.find((m) => m.name === chatModel);
|
|
@@ -1666,7 +1891,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1666
1891
|
}
|
|
1667
1892
|
const envLines = [
|
|
1668
1893
|
`DATABASE_URL=${dbUrl}`,
|
|
1669
|
-
`OLLAMA_URL=${
|
|
1894
|
+
`OLLAMA_URL=${ollamaUrl}`,
|
|
1670
1895
|
`OLLAMA_MODEL=nomic-embed-text`,
|
|
1671
1896
|
`CHAT_PROVIDER=${chatProvider}`,
|
|
1672
1897
|
`CHAT_MODEL=${chatModel}`
|
|
@@ -1676,7 +1901,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1676
1901
|
const envContent = envLines.join("\n") + "\n";
|
|
1677
1902
|
writeFileSync5(envPath, envContent);
|
|
1678
1903
|
process.env.DATABASE_URL = dbUrl;
|
|
1679
|
-
process.env.OLLAMA_URL =
|
|
1904
|
+
process.env.OLLAMA_URL = ollamaUrl;
|
|
1680
1905
|
process.env.OLLAMA_MODEL = "nomic-embed-text";
|
|
1681
1906
|
process.env.CHAT_PROVIDER = chatProvider;
|
|
1682
1907
|
process.env.CHAT_MODEL = chatModel;
|
|
@@ -1796,14 +2021,20 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1796
2021
|
frontendFramework,
|
|
1797
2022
|
language,
|
|
1798
2023
|
caveman: { enabled: installCaveman, intensity: cavemanIntensity },
|
|
1799
|
-
agents: selectedAgents
|
|
2024
|
+
agents: selectedAgents,
|
|
2025
|
+
allowPatterns: []
|
|
1800
2026
|
};
|
|
1801
2027
|
let memories = [];
|
|
1802
2028
|
if (pullMemories) {
|
|
1803
2029
|
const spinner2 = ora("Retrieving relevant memories\u2026").start();
|
|
1804
2030
|
try {
|
|
1805
2031
|
const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
|
|
1806
|
-
memories = await
|
|
2032
|
+
memories = await retrieveContextualMemories({
|
|
2033
|
+
query: archQuery,
|
|
2034
|
+
cwd: process.cwd(),
|
|
2035
|
+
config,
|
|
2036
|
+
limit: 20
|
|
2037
|
+
});
|
|
1807
2038
|
spinner2.succeed(`Found ${memories.length} relevant memories`);
|
|
1808
2039
|
} catch (err) {
|
|
1809
2040
|
spinner2.warn(`Could not retrieve memories: ${err.message}`);
|
|
@@ -1876,7 +2107,12 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
1876
2107
|
let memories = [];
|
|
1877
2108
|
try {
|
|
1878
2109
|
const archQuery = [config.backendArchitecture, config.frontendFramework, config.language].filter(Boolean).join(" ");
|
|
1879
|
-
memories = await
|
|
2110
|
+
memories = await retrieveContextualMemories({
|
|
2111
|
+
query: archQuery,
|
|
2112
|
+
cwd: process.cwd(),
|
|
2113
|
+
config,
|
|
2114
|
+
limit: 25
|
|
2115
|
+
});
|
|
1880
2116
|
spinner.text = `Found ${memories.length} memories \u2014 regenerating files\u2026`;
|
|
1881
2117
|
} catch (err) {
|
|
1882
2118
|
spinner.warn(`Could not retrieve memories: ${err.message}`);
|
|
@@ -1939,9 +2175,10 @@ program.command("search <query>").description("Search memories using semantic si
|
|
|
1939
2175
|
const config = readProjectConfig();
|
|
1940
2176
|
const spinner = ora("Searching\u2026").start();
|
|
1941
2177
|
try {
|
|
2178
|
+
const architectures = inferProjectArchitectures(process.cwd(), config);
|
|
1942
2179
|
const results = await retrieve(
|
|
1943
2180
|
query,
|
|
1944
|
-
|
|
2181
|
+
architectures,
|
|
1945
2182
|
parseInt(opts.limit, 10)
|
|
1946
2183
|
);
|
|
1947
2184
|
spinner.stop();
|
|
@@ -2044,6 +2281,22 @@ program.command("remove <id>").description("Remove a memory by ID").action(async
|
|
|
2044
2281
|
await closePool();
|
|
2045
2282
|
}
|
|
2046
2283
|
});
|
|
2284
|
+
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) => {
|
|
2285
|
+
try {
|
|
2286
|
+
const deleted = await deleteMemories({
|
|
2287
|
+
tag: opts.tag,
|
|
2288
|
+
scope: opts.scope,
|
|
2289
|
+
type: opts.type,
|
|
2290
|
+
architecture: opts.arch
|
|
2291
|
+
});
|
|
2292
|
+
console.log(chalk3.green(`Deleted ${deleted} memories`));
|
|
2293
|
+
} catch (err) {
|
|
2294
|
+
console.error(chalk3.red(`Forget failed: ${err.message}`));
|
|
2295
|
+
process.exit(1);
|
|
2296
|
+
} finally {
|
|
2297
|
+
await closePool();
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2047
2300
|
program.command("edit <id>").description("Edit a memory interactively").action(async (id) => {
|
|
2048
2301
|
const memoryId = parseInt(id, 10);
|
|
2049
2302
|
try {
|
|
@@ -2114,6 +2367,36 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
2114
2367
|
await closePool();
|
|
2115
2368
|
}
|
|
2116
2369
|
});
|
|
2370
|
+
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) => {
|
|
2371
|
+
if (opts.list) {
|
|
2372
|
+
const patterns = getAllowPatterns(readProjectConfig());
|
|
2373
|
+
if (patterns.length === 0) {
|
|
2374
|
+
console.log(chalk3.yellow("\n No allow patterns configured.\n"));
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
console.log(chalk3.bold("\n Allow patterns\n"));
|
|
2378
|
+
patterns.forEach((entry, index) => console.log(` ${index + 1}. ${entry}`));
|
|
2379
|
+
console.log();
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (opts.remove) {
|
|
2383
|
+
updateProjectConfig((config) => ({
|
|
2384
|
+
...config,
|
|
2385
|
+
allowPatterns: getAllowPatterns(config).filter((entry) => entry !== opts.remove)
|
|
2386
|
+
}));
|
|
2387
|
+
console.log(chalk3.green(`Removed allow pattern: "${opts.remove}"`));
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
if (!pattern) {
|
|
2391
|
+
console.error(chalk3.red("Provide a pattern, --list, or --remove <pattern>"));
|
|
2392
|
+
process.exit(1);
|
|
2393
|
+
}
|
|
2394
|
+
updateProjectConfig((config) => ({
|
|
2395
|
+
...config,
|
|
2396
|
+
allowPatterns: [.../* @__PURE__ */ new Set([...getAllowPatterns(config), pattern])]
|
|
2397
|
+
}));
|
|
2398
|
+
console.log(chalk3.green(`Allow pattern saved: "${pattern}"`));
|
|
2399
|
+
});
|
|
2117
2400
|
program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
|
|
2118
2401
|
const workflowPath = join6(process.cwd(), ".github", "workflows", "memory-core.yml");
|
|
2119
2402
|
mkdirSync2(dirname2(workflowPath), { recursive: true });
|
|
@@ -2154,7 +2437,7 @@ program.command("reset").description("Remove memory-core generated files and loc
|
|
|
2154
2437
|
default: false
|
|
2155
2438
|
});
|
|
2156
2439
|
if (ok) {
|
|
2157
|
-
const { getPool } = await import("./db-
|
|
2440
|
+
const { getPool } = await import("./db-5X5LTUCB.js");
|
|
2158
2441
|
await getPool().query("DROP TABLE IF EXISTS memories");
|
|
2159
2442
|
await closePool();
|
|
2160
2443
|
console.log(chalk3.yellow("Dropped memories table"));
|
|
@@ -2250,7 +2533,8 @@ program.command("global").description("Sync your memory into every AI agent glob
|
|
|
2250
2533
|
const spinner = ora("Fetching global memories\u2026").start();
|
|
2251
2534
|
let memories = [];
|
|
2252
2535
|
try {
|
|
2253
|
-
|
|
2536
|
+
const architectures = opts.arch ? [opts.arch] : inferProjectArchitectures(process.cwd(), readProjectConfig());
|
|
2537
|
+
memories = dedupeMemories(await retrieve("architecture rules coding standards", architectures, 30));
|
|
2254
2538
|
} catch (err) {
|
|
2255
2539
|
spinner.fail(`Could not fetch memories: ${err.message}`);
|
|
2256
2540
|
process.exit(1);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
closePool,
|
|
4
|
+
deleteMemories,
|
|
4
5
|
deleteMemory,
|
|
5
6
|
getMemory,
|
|
6
7
|
getPool,
|
|
@@ -11,10 +12,11 @@ import {
|
|
|
11
12
|
searchMemories,
|
|
12
13
|
updateMemory,
|
|
13
14
|
upsertMemory
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-25Y2KI7M.js";
|
|
15
16
|
import "./chunk-KSLFLWB4.js";
|
|
16
17
|
export {
|
|
17
18
|
closePool,
|
|
19
|
+
deleteMemories,
|
|
18
20
|
deleteMemory,
|
|
19
21
|
getMemory,
|
|
20
22
|
getPool,
|