@shahmilsaari/memory-core 0.2.13 → 0.2.15
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 +74 -10
- package/dist/{chunk-25Y2KI7M.js → chunk-DUUQHRIB.js} +6 -2
- package/dist/cli.js +844 -151
- package/dist/{db-5X5LTUCB.js → db-VLOR7L6Q.js} +1 -1
- package/package.json +4 -1
- package/templates/AGENTS.md.hbs +2 -3
- package/templates/AI_RULES.md.hbs +1 -2
- package/templates/CLAUDE.md.hbs +1 -1
- package/templates/PROJECT_MEMORY.md.hbs +2 -2
- package/templates/copilot-instructions.md.hbs +1 -1
- package/templates/cursorrules.hbs +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,13 +7,14 @@ import {
|
|
|
7
7
|
deleteMemories,
|
|
8
8
|
deleteMemory,
|
|
9
9
|
getMemory,
|
|
10
|
+
getPool,
|
|
10
11
|
listMemories,
|
|
11
12
|
runMigrations,
|
|
12
13
|
saveMemory,
|
|
13
14
|
searchMemories,
|
|
14
15
|
updateMemory,
|
|
15
16
|
upsertMemory
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-DUUQHRIB.js";
|
|
17
18
|
import "./chunk-KSLFLWB4.js";
|
|
18
19
|
|
|
19
20
|
// src/cli.ts
|
|
@@ -24,7 +25,6 @@ import ora from "ora";
|
|
|
24
25
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync2, appendFileSync, rmSync, unlinkSync as unlinkSync2 } from "fs";
|
|
25
26
|
import { join as join6, dirname as dirname2 } from "path";
|
|
26
27
|
import { homedir } from "os";
|
|
27
|
-
import { execSync as execSync2 } from "child_process";
|
|
28
28
|
|
|
29
29
|
// src/generator.ts
|
|
30
30
|
import { readFileSync as readFileSync2, readdirSync, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
|
|
@@ -112,6 +112,17 @@ var FRAMEWORK_ARCHITECTURE_MAP = {
|
|
|
112
112
|
"Vue.js": ["vue"],
|
|
113
113
|
Svelte: ["svelte"]
|
|
114
114
|
};
|
|
115
|
+
var KNOWN_ARCHITECTURE_KEYS = /* @__PURE__ */ new Set([
|
|
116
|
+
...Object.values(FRAMEWORK_ARCHITECTURE_MAP).flat(),
|
|
117
|
+
"angular",
|
|
118
|
+
"clean-architecture",
|
|
119
|
+
"express",
|
|
120
|
+
"fastify",
|
|
121
|
+
"hexagonal",
|
|
122
|
+
"modular-monolith",
|
|
123
|
+
"mvc",
|
|
124
|
+
"react-native"
|
|
125
|
+
]);
|
|
115
126
|
function normalizeText(value) {
|
|
116
127
|
return value.toLowerCase().replace(/[`"'()[\]{}.,:;!?/\\<>|=*+-]/g, " ").replace(/\s+/g, " ").trim();
|
|
117
128
|
}
|
|
@@ -139,6 +150,33 @@ function mergeMemory(primary, secondary) {
|
|
|
139
150
|
reason
|
|
140
151
|
};
|
|
141
152
|
}
|
|
153
|
+
function memoryArchitectureKeys(memory) {
|
|
154
|
+
if (memory.architecture && memory.architecture !== "global") {
|
|
155
|
+
return [memory.architecture];
|
|
156
|
+
}
|
|
157
|
+
return (memory.tags ?? []).filter((tag) => KNOWN_ARCHITECTURE_KEYS.has(tag));
|
|
158
|
+
}
|
|
159
|
+
function getStackReason(memory, activeArchitectures) {
|
|
160
|
+
const architectureKeys = memoryArchitectureKeys(memory);
|
|
161
|
+
if (architectureKeys.length === 0) {
|
|
162
|
+
return {
|
|
163
|
+
included: true,
|
|
164
|
+
reason: "global memory: no architecture-specific tag"
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const matched = architectureKeys.filter((architecture) => activeArchitectures.has(architecture));
|
|
168
|
+
if (matched.length > 0) {
|
|
169
|
+
return {
|
|
170
|
+
included: true,
|
|
171
|
+
reason: `matched active architecture: ${matched.join(", ")}`
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const active = [...activeArchitectures].join(", ") || "none detected";
|
|
175
|
+
return {
|
|
176
|
+
included: false,
|
|
177
|
+
reason: `excluded: tagged for ${architectureKeys.join(", ")}; active stack is ${active}`
|
|
178
|
+
};
|
|
179
|
+
}
|
|
142
180
|
function dedupeMemories(memories, threshold = 0.8) {
|
|
143
181
|
const deduped = [];
|
|
144
182
|
for (const memory of memories) {
|
|
@@ -160,6 +198,9 @@ function inferProjectArchitectures(cwd = process.cwd(), config) {
|
|
|
160
198
|
const inferred = /* @__PURE__ */ new Set();
|
|
161
199
|
if (config?.backendArchitecture) inferred.add(config.backendArchitecture);
|
|
162
200
|
if (config?.frontendFramework) inferred.add(config.frontendFramework);
|
|
201
|
+
if (config?.projectType === "backend" && !config.backendArchitecture) {
|
|
202
|
+
inferred.add("clean-architecture");
|
|
203
|
+
}
|
|
163
204
|
const detected = detectProject(cwd);
|
|
164
205
|
for (const architecture of FRAMEWORK_ARCHITECTURE_MAP[detected.framework] ?? []) {
|
|
165
206
|
inferred.add(architecture);
|
|
@@ -169,13 +210,63 @@ function inferProjectArchitectures(cwd = process.cwd(), config) {
|
|
|
169
210
|
function getAllowPatterns(config) {
|
|
170
211
|
return [...new Set(config?.allowPatterns?.filter(Boolean) ?? [])];
|
|
171
212
|
}
|
|
213
|
+
function filterRelevantMemories(memories, config, cwd = process.cwd()) {
|
|
214
|
+
return explainMemorySelection(memories, config, cwd).included;
|
|
215
|
+
}
|
|
216
|
+
function explainMemorySelection(memories, config, cwd = process.cwd(), threshold = 0.8) {
|
|
217
|
+
const activeArchitectures = inferProjectArchitectures(cwd, config);
|
|
218
|
+
const activeSet = new Set(activeArchitectures);
|
|
219
|
+
const included = [];
|
|
220
|
+
const decisions = [];
|
|
221
|
+
for (const memory of memories) {
|
|
222
|
+
const stackDecision = getStackReason(memory, activeSet);
|
|
223
|
+
if (!stackDecision.included) {
|
|
224
|
+
decisions.push({
|
|
225
|
+
memory,
|
|
226
|
+
status: "excluded",
|
|
227
|
+
reason: stackDecision.reason
|
|
228
|
+
});
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const existingIndex = included.findIndex((candidate) => {
|
|
232
|
+
if (candidate.content_hash && memory.content_hash && candidate.content_hash === memory.content_hash) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
return similarityScore(candidate.content, memory.content) >= threshold;
|
|
236
|
+
});
|
|
237
|
+
if (existingIndex === -1) {
|
|
238
|
+
included.push(memory);
|
|
239
|
+
decisions.push({
|
|
240
|
+
memory,
|
|
241
|
+
status: "included",
|
|
242
|
+
reason: stackDecision.reason
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
included[existingIndex] = mergeMemory(included[existingIndex], memory);
|
|
247
|
+
decisions.push({
|
|
248
|
+
memory,
|
|
249
|
+
status: "excluded",
|
|
250
|
+
reason: `duplicate or near-duplicate of memory #${included[existingIndex].id}`
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
included,
|
|
255
|
+
excluded: decisions.filter((decision) => decision.status === "excluded"),
|
|
256
|
+
decisions,
|
|
257
|
+
activeArchitectures
|
|
258
|
+
};
|
|
259
|
+
}
|
|
172
260
|
function buildContextQuery(parts, maxLength = 1200) {
|
|
173
261
|
return parts.filter(Boolean).join("\n").slice(0, maxLength);
|
|
174
262
|
}
|
|
175
263
|
async function retrieveContextualMemories(options) {
|
|
264
|
+
return (await retrieveMemorySelection(options)).included;
|
|
265
|
+
}
|
|
266
|
+
async function retrieveMemorySelection(options) {
|
|
176
267
|
const architectures = inferProjectArchitectures(options.cwd, options.config);
|
|
177
268
|
const memories = await retrieve(options.query, architectures, options.limit ?? 15);
|
|
178
|
-
return
|
|
269
|
+
return explainMemorySelection(memories, options.config, options.cwd);
|
|
179
270
|
}
|
|
180
271
|
|
|
181
272
|
// src/generator.ts
|
|
@@ -229,10 +320,15 @@ function listProfiles(layer) {
|
|
|
229
320
|
if (layer === "frontend") return all.filter((p) => p.layer === "frontend" || p.layer === "fullstack");
|
|
230
321
|
return all;
|
|
231
322
|
}
|
|
232
|
-
function buildTemplateData(options) {
|
|
323
|
+
function buildTemplateData(options, cwd = process.cwd()) {
|
|
233
324
|
const backend = options.backendArchitecture ? loadProfile(options.backendArchitecture) : null;
|
|
234
325
|
const frontend = options.frontendFramework ? loadProfile(options.frontendFramework) : null;
|
|
235
|
-
const dedupedMemories =
|
|
326
|
+
const dedupedMemories = filterRelevantMemories(options.memories, {
|
|
327
|
+
projectType: options.projectType,
|
|
328
|
+
backendArchitecture: options.backendArchitecture,
|
|
329
|
+
frontendFramework: options.frontendFramework,
|
|
330
|
+
language: options.language
|
|
331
|
+
}, cwd);
|
|
236
332
|
const allRules = [
|
|
237
333
|
...backend?.rules ?? [],
|
|
238
334
|
...frontend?.rules ?? []
|
|
@@ -300,7 +396,7 @@ function writeFile(filePath, content) {
|
|
|
300
396
|
return "written";
|
|
301
397
|
}
|
|
302
398
|
async function generate(options, cwd = process.cwd(), onlyAgents) {
|
|
303
|
-
const data = buildTemplateData(options);
|
|
399
|
+
const data = buildTemplateData(options, cwd);
|
|
304
400
|
const written = [];
|
|
305
401
|
const skipped = [];
|
|
306
402
|
const files = onlyAgents ? OUTPUT_FILES.filter((f) => onlyAgents.includes(f.agent)) : OUTPUT_FILES;
|
|
@@ -711,11 +807,11 @@ import chalk from "chalk";
|
|
|
711
807
|
|
|
712
808
|
// src/chat.ts
|
|
713
809
|
function getChatConfig() {
|
|
714
|
-
const
|
|
715
|
-
const
|
|
810
|
+
const provider2 = process.env.CHAT_PROVIDER ?? "ollama";
|
|
811
|
+
const model2 = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
|
|
716
812
|
return {
|
|
717
|
-
provider,
|
|
718
|
-
model,
|
|
813
|
+
provider: provider2,
|
|
814
|
+
model: model2,
|
|
719
815
|
ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
|
|
720
816
|
apiKey: process.env.CHAT_API_KEY ?? ""
|
|
721
817
|
};
|
|
@@ -922,7 +1018,7 @@ async function promptToSaveViolations(violations) {
|
|
|
922
1018
|
default: selected.reason ?? selected.issue ?? ""
|
|
923
1019
|
});
|
|
924
1020
|
const { embed: embed2 } = await import("./embedding-PAYD2JYW.js");
|
|
925
|
-
const { upsertMemory: upsertMemory2 } = await import("./db-
|
|
1021
|
+
const { upsertMemory: upsertMemory2 } = await import("./db-VLOR7L6Q.js");
|
|
926
1022
|
await upsertMemory2({
|
|
927
1023
|
type: "rule",
|
|
928
1024
|
scope: "project",
|
|
@@ -939,7 +1035,7 @@ async function promptToSaveViolations(violations) {
|
|
|
939
1035
|
}
|
|
940
1036
|
async function loadIgnorePatterns() {
|
|
941
1037
|
try {
|
|
942
|
-
const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-
|
|
1038
|
+
const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-VLOR7L6Q.js");
|
|
943
1039
|
const ignores = await listMemories2({ type: "ignore", limit: 1e3 });
|
|
944
1040
|
await closePool2();
|
|
945
1041
|
return ignores.map((ignore) => ignore.content);
|
|
@@ -996,6 +1092,124 @@ ${violation.file}`.toLowerCase();
|
|
|
996
1092
|
return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
|
|
997
1093
|
});
|
|
998
1094
|
}
|
|
1095
|
+
function normalizeViolation(value) {
|
|
1096
|
+
if (!value || typeof value !== "object") return null;
|
|
1097
|
+
const candidate = value;
|
|
1098
|
+
if (typeof candidate.rule !== "string" || typeof candidate.issue !== "string") return null;
|
|
1099
|
+
return {
|
|
1100
|
+
rule: candidate.rule,
|
|
1101
|
+
file: typeof candidate.file === "string" ? candidate.file : "diff",
|
|
1102
|
+
line: typeof candidate.line === "number" ? candidate.line : void 0,
|
|
1103
|
+
issue: candidate.issue,
|
|
1104
|
+
suggestion: typeof candidate.suggestion === "string" ? candidate.suggestion : void 0,
|
|
1105
|
+
reason: typeof candidate.reason === "string" ? candidate.reason : void 0
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function parseModelViolations(raw) {
|
|
1109
|
+
const candidates = [
|
|
1110
|
+
raw,
|
|
1111
|
+
raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "")
|
|
1112
|
+
];
|
|
1113
|
+
const objectStart = raw.indexOf("{");
|
|
1114
|
+
const objectEnd = raw.lastIndexOf("}");
|
|
1115
|
+
if (objectStart !== -1 && objectEnd > objectStart) {
|
|
1116
|
+
candidates.push(raw.slice(objectStart, objectEnd + 1));
|
|
1117
|
+
}
|
|
1118
|
+
const arrayStart = raw.indexOf("[");
|
|
1119
|
+
const arrayEnd = raw.lastIndexOf("]");
|
|
1120
|
+
if (arrayStart !== -1 && arrayEnd > arrayStart) {
|
|
1121
|
+
candidates.push(raw.slice(arrayStart, arrayEnd + 1));
|
|
1122
|
+
}
|
|
1123
|
+
for (const candidate of candidates) {
|
|
1124
|
+
try {
|
|
1125
|
+
const parsed = JSON.parse(candidate);
|
|
1126
|
+
const items = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.violations) ? parsed.violations : parsed?.rule ? [parsed] : null;
|
|
1127
|
+
if (!items) continue;
|
|
1128
|
+
return {
|
|
1129
|
+
valid: true,
|
|
1130
|
+
violations: items.map(normalizeViolation).filter((violation) => violation !== null)
|
|
1131
|
+
};
|
|
1132
|
+
} catch {
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return { valid: false, violations: [] };
|
|
1136
|
+
}
|
|
1137
|
+
function getAddedLines(diff) {
|
|
1138
|
+
const lines = [];
|
|
1139
|
+
let currentFile = "diff";
|
|
1140
|
+
let newLineNumber = 0;
|
|
1141
|
+
for (const line of diff.split("\n")) {
|
|
1142
|
+
if (line.startsWith("+++ b/")) {
|
|
1143
|
+
currentFile = line.slice("+++ b/".length);
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
1147
|
+
if (hunk) {
|
|
1148
|
+
newLineNumber = Number(hunk[1]);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
1152
|
+
lines.push({
|
|
1153
|
+
file: currentFile,
|
|
1154
|
+
line: Number.isFinite(newLineNumber) ? newLineNumber : void 0,
|
|
1155
|
+
content: line.slice(1)
|
|
1156
|
+
});
|
|
1157
|
+
newLineNumber += 1;
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
if (!line.startsWith("-") && newLineNumber > 0) {
|
|
1161
|
+
newLineNumber += 1;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return lines;
|
|
1165
|
+
}
|
|
1166
|
+
function dedupeViolations(violations) {
|
|
1167
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1168
|
+
const deduped = [];
|
|
1169
|
+
for (const violation of violations) {
|
|
1170
|
+
const key = [
|
|
1171
|
+
violation.rule,
|
|
1172
|
+
violation.file,
|
|
1173
|
+
violation.line ?? "",
|
|
1174
|
+
violation.issue
|
|
1175
|
+
].join("\0");
|
|
1176
|
+
if (seen.has(key)) continue;
|
|
1177
|
+
seen.add(key);
|
|
1178
|
+
deduped.push(violation);
|
|
1179
|
+
}
|
|
1180
|
+
return deduped;
|
|
1181
|
+
}
|
|
1182
|
+
function findDeterministicViolations(diff, rules, avoids, allowPatterns = []) {
|
|
1183
|
+
const rulePhrases = rules.flatMap(
|
|
1184
|
+
(rule) => extractForbiddenPhrases(rule).map((phrase) => ({ rule, phrase }))
|
|
1185
|
+
);
|
|
1186
|
+
const avoidPhrases = avoids.map((avoid) => ({
|
|
1187
|
+
rule: `Avoid: ${avoid}`,
|
|
1188
|
+
phrase: avoid.toLowerCase()
|
|
1189
|
+
}));
|
|
1190
|
+
const phrases = [...rulePhrases, ...avoidPhrases].filter((item) => item.phrase.length > 0);
|
|
1191
|
+
if (phrases.length === 0) return [];
|
|
1192
|
+
const violations = [];
|
|
1193
|
+
for (const addedLine of getAddedLines(diff)) {
|
|
1194
|
+
const normalizedLine = addedLine.content.toLowerCase();
|
|
1195
|
+
if (allowPatterns.some((pattern) => normalizedLine.includes(pattern.toLowerCase()))) {
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
for (const { rule, phrase } of phrases) {
|
|
1199
|
+
if (normalizedLine.includes(phrase)) {
|
|
1200
|
+
violations.push({
|
|
1201
|
+
rule,
|
|
1202
|
+
file: addedLine.file,
|
|
1203
|
+
line: addedLine.line,
|
|
1204
|
+
issue: `Added line contains forbidden phrase: "${phrase}"`,
|
|
1205
|
+
suggestion: "Remove this pattern or add an explicit ignore memory if it is intentional.",
|
|
1206
|
+
reason: reasonMap.get(rule)
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return dedupeViolations(violations);
|
|
1212
|
+
}
|
|
999
1213
|
async function verifyViolations(diff, violations, allowPatterns, debug) {
|
|
1000
1214
|
if (violations.length === 0) return violations;
|
|
1001
1215
|
const systemPrompt = `You are verifying candidate architecture violations.
|
|
@@ -1023,9 +1237,8 @@ ${JSON.stringify(violations, null, 2)}`;
|
|
|
1023
1237
|
{ role: "system", content: systemPrompt },
|
|
1024
1238
|
{ role: "user", content: userPrompt }
|
|
1025
1239
|
]);
|
|
1026
|
-
const parsed =
|
|
1027
|
-
if (
|
|
1028
|
-
if (Array.isArray(parsed)) return parsed;
|
|
1240
|
+
const parsed = parseModelViolations(raw);
|
|
1241
|
+
if (parsed.valid) return parsed.violations;
|
|
1029
1242
|
return violations;
|
|
1030
1243
|
} catch {
|
|
1031
1244
|
return violations;
|
|
@@ -1142,7 +1355,8 @@ Do not include any text outside the JSON object.`;
|
|
|
1142
1355
|
console.log(chalk.dim(diffToSend));
|
|
1143
1356
|
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"));
|
|
1144
1357
|
}
|
|
1145
|
-
|
|
1358
|
+
const deterministicViolations = findDeterministicViolations(diff, rules, avoids, allowPatterns);
|
|
1359
|
+
let modelViolations = [];
|
|
1146
1360
|
try {
|
|
1147
1361
|
const raw = await callChatModel([
|
|
1148
1362
|
{ role: "system", content: systemPrompt },
|
|
@@ -1153,36 +1367,29 @@ ${diffToSend}` }
|
|
|
1153
1367
|
if (options.verbose || options.debug) {
|
|
1154
1368
|
console.log(chalk.gray(` raw response: ${options.debug ? raw : raw.slice(0, 200)}`));
|
|
1155
1369
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
violations = parsed.violations;
|
|
1162
|
-
} else if (parsed?.rule) {
|
|
1163
|
-
violations = [parsed];
|
|
1164
|
-
} else {
|
|
1165
|
-
violations = [];
|
|
1166
|
-
}
|
|
1167
|
-
} catch {
|
|
1168
|
-
violations = [];
|
|
1370
|
+
const parsed = parseModelViolations(raw);
|
|
1371
|
+
if (parsed.valid) {
|
|
1372
|
+
modelViolations = parsed.violations;
|
|
1373
|
+
} else {
|
|
1374
|
+
console.log(chalk.yellow(" \u26A0 AI returned invalid JSON \u2014 using deterministic checks only."));
|
|
1169
1375
|
}
|
|
1170
1376
|
} catch (err) {
|
|
1171
1377
|
if (err.message?.startsWith("MODEL_NOT_FOUND:")) {
|
|
1172
1378
|
printModelMissing(err.message.split(":")[1]);
|
|
1173
|
-
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 skipping rule check."));
|
|
1379
|
+
modelViolations = [];
|
|
1380
|
+
} else if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
|
|
1381
|
+
console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 using deterministic checks only."));
|
|
1177
1382
|
console.log(chalk.gray(" Start it: ollama serve\n"));
|
|
1178
|
-
|
|
1383
|
+
modelViolations = [];
|
|
1384
|
+
} else {
|
|
1385
|
+
console.log(chalk.yellow(`
|
|
1386
|
+
\u26A0 AI rule check failed: ${err.message}`));
|
|
1387
|
+
console.log(chalk.gray(" Using deterministic checks only.\n"));
|
|
1388
|
+
modelViolations = [];
|
|
1179
1389
|
}
|
|
1180
|
-
console.log(chalk.yellow(`
|
|
1181
|
-
\u26A0 Rule check failed: ${err.message}
|
|
1182
|
-
`));
|
|
1183
|
-
return;
|
|
1184
1390
|
}
|
|
1185
|
-
|
|
1391
|
+
modelViolations = await verifyViolations(diff, modelViolations, allowPatterns, options.debug ?? false);
|
|
1392
|
+
let violations = dedupeViolations([...deterministicViolations, ...modelViolations]);
|
|
1186
1393
|
violations = applyAllowPatterns(violations, allowPatterns);
|
|
1187
1394
|
if (violations.length === 0) {
|
|
1188
1395
|
console.log(chalk.green(" \u2713 No rule violations \u2014 commit allowed.\n"));
|
|
@@ -1301,10 +1508,10 @@ async function checkCi(options = {}) {
|
|
|
1301
1508
|
recordViolations(violations);
|
|
1302
1509
|
process.exit(1);
|
|
1303
1510
|
}
|
|
1304
|
-
function printModelMissing(
|
|
1511
|
+
function printModelMissing(model2) {
|
|
1305
1512
|
console.log(chalk.yellow(`
|
|
1306
|
-
\u26A0 Chat model "${
|
|
1307
|
-
console.log(chalk.gray(` Pull a model: ollama pull ${
|
|
1513
|
+
\u26A0 Chat model "${model2}" not found in Ollama.`));
|
|
1514
|
+
console.log(chalk.gray(` Pull a model: ollama pull ${model2}`));
|
|
1308
1515
|
console.log(chalk.gray(" Or set OLLAMA_CHAT_MODEL=<model> in .env"));
|
|
1309
1516
|
console.log(chalk.gray(" Recommended: llama3.2 | qwen2.5-coder:3b | mistral\n"));
|
|
1310
1517
|
}
|
|
@@ -1454,7 +1661,7 @@ ${JSON.stringify(violations, null, 2)}`;
|
|
|
1454
1661
|
}
|
|
1455
1662
|
async function loadIgnorePatterns2() {
|
|
1456
1663
|
try {
|
|
1457
|
-
const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-
|
|
1664
|
+
const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-VLOR7L6Q.js");
|
|
1458
1665
|
const ignores = await listMemories2({ type: "ignore", limit: 1e3 });
|
|
1459
1666
|
await closePool2();
|
|
1460
1667
|
return ignores.map((ignore) => ignore.content);
|
|
@@ -1633,6 +1840,55 @@ async function startWatch(options = {}) {
|
|
|
1633
1840
|
});
|
|
1634
1841
|
}
|
|
1635
1842
|
|
|
1843
|
+
// src/remote-install.ts
|
|
1844
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1845
|
+
var CAVEMAN_INSTALL_URL = "https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh";
|
|
1846
|
+
var MAX_INSTALLER_BYTES = 2e5;
|
|
1847
|
+
var TRUSTED_INSTALL_HOSTS = /* @__PURE__ */ new Set(["raw.githubusercontent.com"]);
|
|
1848
|
+
function assertTrustedInstallerUrl(url) {
|
|
1849
|
+
const parsed = new URL(url);
|
|
1850
|
+
if (parsed.protocol !== "https:") {
|
|
1851
|
+
throw new Error("Remote installer URL must use HTTPS");
|
|
1852
|
+
}
|
|
1853
|
+
if (!TRUSTED_INSTALL_HOSTS.has(parsed.hostname)) {
|
|
1854
|
+
throw new Error(`Remote installer host is not trusted: ${parsed.hostname}`);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
function defaultRunScript(script) {
|
|
1858
|
+
return spawnSync3("bash", ["-s"], {
|
|
1859
|
+
input: script,
|
|
1860
|
+
encoding: "utf-8",
|
|
1861
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
async function fetchRemoteInstaller(url, fetchImpl = fetch) {
|
|
1865
|
+
assertTrustedInstallerUrl(url);
|
|
1866
|
+
const response = await fetchImpl(url, {
|
|
1867
|
+
signal: AbortSignal.timeout(15e3)
|
|
1868
|
+
});
|
|
1869
|
+
if (!response.ok) {
|
|
1870
|
+
throw new Error(`Failed to download installer (${response.status})`);
|
|
1871
|
+
}
|
|
1872
|
+
const script = await response.text();
|
|
1873
|
+
if (script.length > MAX_INSTALLER_BYTES) {
|
|
1874
|
+
throw new Error(`Installer is too large (${script.length} bytes)`);
|
|
1875
|
+
}
|
|
1876
|
+
if (script.includes("\0")) {
|
|
1877
|
+
throw new Error("Installer contains null bytes");
|
|
1878
|
+
}
|
|
1879
|
+
return script;
|
|
1880
|
+
}
|
|
1881
|
+
async function installCavemanTokenSaver(options = {}) {
|
|
1882
|
+
const url = options.url ?? CAVEMAN_INSTALL_URL;
|
|
1883
|
+
const script = await fetchRemoteInstaller(url, options.fetchImpl ?? fetch);
|
|
1884
|
+
const result = (options.runScript ?? defaultRunScript)(script);
|
|
1885
|
+
if (result.error) throw result.error;
|
|
1886
|
+
if (result.status !== 0) {
|
|
1887
|
+
const stderr = result.stderr ? String(result.stderr).trim() : "";
|
|
1888
|
+
throw new Error(stderr || `Installer exited with status ${result.status}`);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1636
1892
|
// src/cli.ts
|
|
1637
1893
|
function printBanner(projectName, agentCount, status) {
|
|
1638
1894
|
const pg = status ? status.postgresOk ? chalk3.green(" \u2713 PostgreSQL ") + chalk3.bold("connected") : chalk3.red(" \u2717 PostgreSQL ") + chalk3.bold("not connected \u2014 check DATABASE_URL") : chalk3.green(" \u2713 Memory ") + chalk3.bold("PostgreSQL + pgvector ready");
|
|
@@ -1697,6 +1953,156 @@ async function checkConnections(dbUrl, ollamaUrl, chatModel) {
|
|
|
1697
1953
|
}
|
|
1698
1954
|
var { version } = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1699
1955
|
var CONFIG_FILE = ".memory-core.json";
|
|
1956
|
+
var LOCAL_GENERATED_FILES = [".memory-core-stats.json"];
|
|
1957
|
+
var DEFAULT_OLLAMA_URL = "http://localhost:11434";
|
|
1958
|
+
var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
|
|
1959
|
+
var DEFAULT_CHAT_MODEL = "llama3.2";
|
|
1960
|
+
function getEnvPath() {
|
|
1961
|
+
const memoryEnv = join6(process.cwd(), ".memory-core.env");
|
|
1962
|
+
if (existsSync6(memoryEnv)) return memoryEnv;
|
|
1963
|
+
const dotEnv = join6(process.cwd(), ".env");
|
|
1964
|
+
return existsSync6(dotEnv) ? dotEnv : memoryEnv;
|
|
1965
|
+
}
|
|
1966
|
+
function parseEnvFile(raw) {
|
|
1967
|
+
const lines = raw.split(/\r?\n/);
|
|
1968
|
+
const values = {};
|
|
1969
|
+
for (const line of lines) {
|
|
1970
|
+
const trimmed = line.trim();
|
|
1971
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1972
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
1973
|
+
if (separatorIndex === -1) continue;
|
|
1974
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
1975
|
+
const value = trimmed.slice(separatorIndex + 1).trim();
|
|
1976
|
+
if (key) values[key] = value;
|
|
1977
|
+
}
|
|
1978
|
+
return values;
|
|
1979
|
+
}
|
|
1980
|
+
function readRuntimeEnv() {
|
|
1981
|
+
const envPath = getEnvPath();
|
|
1982
|
+
const fileValues = existsSync6(envPath) ? parseEnvFile(readFileSync6(envPath, "utf-8")) : {};
|
|
1983
|
+
const values = {
|
|
1984
|
+
...fileValues
|
|
1985
|
+
};
|
|
1986
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1987
|
+
if (typeof value === "string" && value !== "") values[key] = value;
|
|
1988
|
+
}
|
|
1989
|
+
return { envPath, values };
|
|
1990
|
+
}
|
|
1991
|
+
function writeRuntimeEnv(values, envPath = getEnvPath()) {
|
|
1992
|
+
const orderedKeys = [
|
|
1993
|
+
"DATABASE_URL",
|
|
1994
|
+
"OLLAMA_URL",
|
|
1995
|
+
"OLLAMA_MODEL",
|
|
1996
|
+
"CHAT_PROVIDER",
|
|
1997
|
+
"CHAT_MODEL",
|
|
1998
|
+
"OLLAMA_CHAT_MODEL",
|
|
1999
|
+
"CHAT_API_KEY"
|
|
2000
|
+
];
|
|
2001
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2002
|
+
const lines = [];
|
|
2003
|
+
for (const key of orderedKeys) {
|
|
2004
|
+
const value = values[key];
|
|
2005
|
+
if (value) {
|
|
2006
|
+
lines.push(`${key}=${value}`);
|
|
2007
|
+
seen.add(key);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
for (const key of Object.keys(values).sort()) {
|
|
2011
|
+
if (seen.has(key)) continue;
|
|
2012
|
+
const value = values[key];
|
|
2013
|
+
if (value) lines.push(`${key}=${value}`);
|
|
2014
|
+
}
|
|
2015
|
+
writeFileSync5(envPath, `${lines.join("\n")}
|
|
2016
|
+
`, "utf-8");
|
|
2017
|
+
}
|
|
2018
|
+
function applyRuntimeEnv(values) {
|
|
2019
|
+
for (const [key, value] of Object.entries(values)) {
|
|
2020
|
+
process.env[key] = value;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
function ensureEnvFileIgnored(envPath = getEnvPath()) {
|
|
2024
|
+
const envFileName = envPath.split("/").pop() ?? ".memory-core.env";
|
|
2025
|
+
const gitignorePath = join6(process.cwd(), ".gitignore");
|
|
2026
|
+
const existing = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
|
|
2027
|
+
if (!existing.includes(envFileName)) {
|
|
2028
|
+
appendFileSync(gitignorePath, `${existing ? "\n" : ""}${envFileName}
|
|
2029
|
+
`);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
function appendMissingGitignoreEntries(entries, heading) {
|
|
2033
|
+
const gitignorePath = join6(process.cwd(), ".gitignore");
|
|
2034
|
+
const existing = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
|
|
2035
|
+
const existingEntries = new Set(
|
|
2036
|
+
existing.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
2037
|
+
);
|
|
2038
|
+
const toAdd = entries.filter((entry) => !existingEntries.has(entry));
|
|
2039
|
+
if (toAdd.length === 0) {
|
|
2040
|
+
return 0;
|
|
2041
|
+
}
|
|
2042
|
+
const prefix = existing.trim().length > 0 ? "\n" : "";
|
|
2043
|
+
appendFileSync(gitignorePath, `${prefix}${heading}
|
|
2044
|
+
${toAdd.join("\n")}
|
|
2045
|
+
`);
|
|
2046
|
+
return toAdd.length;
|
|
2047
|
+
}
|
|
2048
|
+
function normalizeProvider(value) {
|
|
2049
|
+
const provider2 = value.trim().toLowerCase();
|
|
2050
|
+
if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax") {
|
|
2051
|
+
return provider2;
|
|
2052
|
+
}
|
|
2053
|
+
throw new Error(`Unsupported provider "${value}". Use: ollama, openai, anthropic, minimax`);
|
|
2054
|
+
}
|
|
2055
|
+
function providerLabel(provider2) {
|
|
2056
|
+
switch (provider2) {
|
|
2057
|
+
case "openai":
|
|
2058
|
+
return "OpenAI";
|
|
2059
|
+
case "anthropic":
|
|
2060
|
+
return "Anthropic";
|
|
2061
|
+
case "minimax":
|
|
2062
|
+
return "MiniMax";
|
|
2063
|
+
default:
|
|
2064
|
+
return "Ollama";
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
function redactDatabaseUrl(url) {
|
|
2068
|
+
if (!url) return "(not set)";
|
|
2069
|
+
return url.replace(/:\/\/([^:@/]+)(?::[^@/]+)?@/, "://$1:***@");
|
|
2070
|
+
}
|
|
2071
|
+
function getConfiguredProvider(values) {
|
|
2072
|
+
return normalizeProvider(values.CHAT_PROVIDER ?? "ollama");
|
|
2073
|
+
}
|
|
2074
|
+
function getConfiguredChatModel(values) {
|
|
2075
|
+
return values.CHAT_MODEL ?? values.OLLAMA_CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
|
|
2076
|
+
}
|
|
2077
|
+
async function resolveOllamaInstalledModel(ollamaUrl, model2) {
|
|
2078
|
+
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
2079
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2080
|
+
const data = await res.json();
|
|
2081
|
+
const models = data.models ?? [];
|
|
2082
|
+
const exact = models.find((entry) => entry.name === model2);
|
|
2083
|
+
const prefixed = models.find((entry) => entry.name.startsWith(`${model2}:`));
|
|
2084
|
+
return (exact ?? prefixed)?.name ?? null;
|
|
2085
|
+
}
|
|
2086
|
+
async function verifyDatabaseConnection(dbUrl) {
|
|
2087
|
+
if (!dbUrl) return "DATABASE_URL is not set";
|
|
2088
|
+
try {
|
|
2089
|
+
const { Pool } = (await import("pg")).default;
|
|
2090
|
+
const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
|
|
2091
|
+
await testPool.query("SELECT 1");
|
|
2092
|
+
await testPool.end();
|
|
2093
|
+
return null;
|
|
2094
|
+
} catch (err) {
|
|
2095
|
+
return err.message;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
async function verifyOllamaConnection(ollamaUrl) {
|
|
2099
|
+
try {
|
|
2100
|
+
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
2101
|
+
return res.ok ? null : `HTTP ${res.status}`;
|
|
2102
|
+
} catch (err) {
|
|
2103
|
+
return err.message;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
1700
2106
|
function readProjectConfig() {
|
|
1701
2107
|
const path = join6(process.cwd(), CONFIG_FILE);
|
|
1702
2108
|
if (!existsSync6(path)) return null;
|
|
@@ -1744,6 +2150,222 @@ function printMemoryTable(memories, title = "Rules in memory") {
|
|
|
1744
2150
|
});
|
|
1745
2151
|
console.log(chalk3.gray("\n Use: memory-core remove <id> | memory-core edit <id>\n"));
|
|
1746
2152
|
}
|
|
2153
|
+
function getCurrentListArchitectures(config) {
|
|
2154
|
+
return inferProjectArchitectures(process.cwd(), config).filter((architecture) => architecture !== "global");
|
|
2155
|
+
}
|
|
2156
|
+
function printStatusLine(label, value) {
|
|
2157
|
+
console.log(` ${chalk3.dim(label.padEnd(18))} ${value}`);
|
|
2158
|
+
}
|
|
2159
|
+
async function runModelDoctor() {
|
|
2160
|
+
const { envPath, values } = readRuntimeEnv();
|
|
2161
|
+
const provider2 = getConfiguredProvider(values);
|
|
2162
|
+
const model2 = getConfiguredChatModel(values);
|
|
2163
|
+
const ollamaUrl = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
|
|
2164
|
+
const embeddingModel = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
|
|
2165
|
+
const dbUrl = values.DATABASE_URL ?? "";
|
|
2166
|
+
console.log(chalk3.bold("\n memory-core model doctor\n"));
|
|
2167
|
+
printStatusLine("Env file", existsSync6(envPath) ? envPath : `${envPath} ${chalk3.yellow("(will be created on first write)")}`);
|
|
2168
|
+
printStatusLine("Provider", provider2);
|
|
2169
|
+
printStatusLine("Chat model", model2);
|
|
2170
|
+
printStatusLine("Embedding model", embeddingModel);
|
|
2171
|
+
printStatusLine("Ollama URL", ollamaUrl);
|
|
2172
|
+
console.log();
|
|
2173
|
+
let ok = true;
|
|
2174
|
+
const dbError = await verifyDatabaseConnection(dbUrl);
|
|
2175
|
+
if (dbError) {
|
|
2176
|
+
ok = false;
|
|
2177
|
+
console.log(chalk3.red(" \u2717 PostgreSQL ") + chalk3.dim(dbError));
|
|
2178
|
+
} else {
|
|
2179
|
+
console.log(chalk3.green(" \u2713 PostgreSQL ") + chalk3.dim("connected"));
|
|
2180
|
+
}
|
|
2181
|
+
const ollamaError = await verifyOllamaConnection(ollamaUrl);
|
|
2182
|
+
if (ollamaError) {
|
|
2183
|
+
ok = false;
|
|
2184
|
+
console.log(chalk3.red(" \u2717 Ollama ") + chalk3.dim(ollamaError));
|
|
2185
|
+
} else {
|
|
2186
|
+
console.log(chalk3.green(" \u2713 Ollama ") + chalk3.dim("reachable"));
|
|
2187
|
+
}
|
|
2188
|
+
if (!ollamaError) {
|
|
2189
|
+
try {
|
|
2190
|
+
const installedEmbeddingModel = await resolveOllamaInstalledModel(ollamaUrl, embeddingModel);
|
|
2191
|
+
if (installedEmbeddingModel) {
|
|
2192
|
+
console.log(chalk3.green(" \u2713 Embedding ") + chalk3.dim(`${installedEmbeddingModel} installed`));
|
|
2193
|
+
} else {
|
|
2194
|
+
ok = false;
|
|
2195
|
+
console.log(chalk3.red(" \u2717 Embedding ") + chalk3.dim(`${embeddingModel} not installed in Ollama`));
|
|
2196
|
+
}
|
|
2197
|
+
} catch (err) {
|
|
2198
|
+
ok = false;
|
|
2199
|
+
console.log(chalk3.red(" \u2717 Embedding ") + chalk3.dim(err.message));
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
if (provider2 === "ollama") {
|
|
2203
|
+
if (ollamaError) {
|
|
2204
|
+
ok = false;
|
|
2205
|
+
console.log(chalk3.red(" \u2717 Chat model ") + chalk3.dim("cannot verify while Ollama is unreachable"));
|
|
2206
|
+
} else {
|
|
2207
|
+
try {
|
|
2208
|
+
const installedChatModel = await resolveOllamaInstalledModel(ollamaUrl, model2);
|
|
2209
|
+
if (installedChatModel) {
|
|
2210
|
+
console.log(chalk3.green(" \u2713 Chat model ") + chalk3.dim(`${installedChatModel} installed`));
|
|
2211
|
+
} else {
|
|
2212
|
+
ok = false;
|
|
2213
|
+
console.log(chalk3.red(" \u2717 Chat model ") + chalk3.dim(`${model2} not installed in Ollama`));
|
|
2214
|
+
}
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
ok = false;
|
|
2217
|
+
console.log(chalk3.red(" \u2717 Chat model ") + chalk3.dim(err.message));
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
} else {
|
|
2221
|
+
if (!values.CHAT_API_KEY) {
|
|
2222
|
+
ok = false;
|
|
2223
|
+
console.log(chalk3.red(` \u2717 ${providerLabel(provider2)} API`) + chalk3.dim(" CHAT_API_KEY is missing"));
|
|
2224
|
+
} else {
|
|
2225
|
+
console.log(chalk3.green(` \u2713 ${providerLabel(provider2)} API`) + chalk3.dim(" key configured"));
|
|
2226
|
+
console.log(chalk3.gray(" Remote provider connectivity is not verified live by doctor."));
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
console.log();
|
|
2230
|
+
return { ok, provider: provider2, model: model2 };
|
|
2231
|
+
}
|
|
2232
|
+
async function printProjectStatus() {
|
|
2233
|
+
const config = readProjectConfig();
|
|
2234
|
+
const { envPath, values } = readRuntimeEnv();
|
|
2235
|
+
const provider2 = getConfiguredProvider(values);
|
|
2236
|
+
const model2 = getConfiguredChatModel(values);
|
|
2237
|
+
const architectures = inferProjectArchitectures(process.cwd(), config);
|
|
2238
|
+
const generatedFiles = OUTPUT_FILES.map((entry) => entry.path).filter((relativePath) => existsSync6(join6(process.cwd(), relativePath)));
|
|
2239
|
+
const hookPath = join6(process.cwd(), ".git", "hooks", "pre-commit");
|
|
2240
|
+
const memoryFilePath = join6(process.cwd(), MEMORY_FILE);
|
|
2241
|
+
const statsPath = join6(process.cwd(), ".memory-core-stats.json");
|
|
2242
|
+
const dbError = await verifyDatabaseConnection(values.DATABASE_URL ?? "");
|
|
2243
|
+
const ollamaError = await verifyOllamaConnection(values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
|
|
2244
|
+
console.log(chalk3.bold("\n memory-core status\n"));
|
|
2245
|
+
printStatusLine("Project", config?.projectName ?? process.cwd().split("/").pop() ?? "unknown");
|
|
2246
|
+
printStatusLine("Project type", config?.projectType ?? chalk3.yellow("not initialized"));
|
|
2247
|
+
printStatusLine("Language", config?.language ?? detectProject().language);
|
|
2248
|
+
printStatusLine("Backend arch", config?.backendArchitecture ?? chalk3.gray("\u2014"));
|
|
2249
|
+
printStatusLine("Frontend fw", config?.frontendFramework ?? chalk3.gray("\u2014"));
|
|
2250
|
+
printStatusLine("Architectures", architectures.length ? architectures.join(", ") : chalk3.gray("none detected"));
|
|
2251
|
+
printStatusLine("Agents", config?.agents?.length ? `${config.agents.length} selected` : chalk3.gray("none saved"));
|
|
2252
|
+
printStatusLine("Caveman", config?.caveman?.enabled ? `enabled (${config.caveman.intensity})` : "disabled");
|
|
2253
|
+
printStatusLine("Auto sync", config?.autoSync === false ? "disabled" : "enabled");
|
|
2254
|
+
printStatusLine("Allow patterns", String(getAllowPatterns(config).length));
|
|
2255
|
+
printStatusLine("Env file", `${existsSync6(envPath) ? "present" : "missing"} (${envPath.split("/").pop()})`);
|
|
2256
|
+
printStatusLine("Memory file", existsSync6(memoryFilePath) ? MEMORY_FILE : chalk3.gray("not exported"));
|
|
2257
|
+
printStatusLine("Project config", existsSync6(join6(process.cwd(), CONFIG_FILE)) ? CONFIG_FILE : chalk3.gray("missing"));
|
|
2258
|
+
printStatusLine("Generated files", String(generatedFiles.length));
|
|
2259
|
+
printStatusLine("Hook", existsSync6(hookPath) ? "installed" : "not installed");
|
|
2260
|
+
printStatusLine("Stats file", existsSync6(statsPath) ? ".memory-core-stats.json" : chalk3.gray("none"));
|
|
2261
|
+
console.log();
|
|
2262
|
+
printStatusLine("Database URL", redactDatabaseUrl(values.DATABASE_URL ?? ""));
|
|
2263
|
+
printStatusLine("Ollama URL", values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
|
|
2264
|
+
printStatusLine("Embedding model", values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL);
|
|
2265
|
+
printStatusLine("Chat provider", provider2);
|
|
2266
|
+
printStatusLine("Chat model", model2);
|
|
2267
|
+
console.log();
|
|
2268
|
+
console.log(
|
|
2269
|
+
dbError ? chalk3.red(" \u2717 PostgreSQL ") + chalk3.dim(dbError) : chalk3.green(" \u2713 PostgreSQL ") + chalk3.dim("connected")
|
|
2270
|
+
);
|
|
2271
|
+
console.log(
|
|
2272
|
+
ollamaError ? chalk3.red(" \u2717 Ollama ") + chalk3.dim(ollamaError) : chalk3.green(" \u2713 Ollama ") + chalk3.dim("reachable")
|
|
2273
|
+
);
|
|
2274
|
+
if (provider2 !== "ollama") {
|
|
2275
|
+
console.log(
|
|
2276
|
+
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")
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
console.log();
|
|
2280
|
+
}
|
|
2281
|
+
function printMemorySelection(selection, limit = 4) {
|
|
2282
|
+
const active = selection.activeArchitectures.join(", ") || "none detected";
|
|
2283
|
+
console.log(chalk3.gray(` Stack filter: ${active}`));
|
|
2284
|
+
const included = selection.decisions.filter((decision) => decision.status === "included");
|
|
2285
|
+
if (included.length > 0) {
|
|
2286
|
+
console.log(chalk3.gray(` Included ${included.length}:`));
|
|
2287
|
+
for (const decision of included.slice(0, limit)) {
|
|
2288
|
+
console.log(chalk3.gray(` + ${decision.memory.content} (${decision.reason})`));
|
|
2289
|
+
}
|
|
2290
|
+
if (included.length > limit) {
|
|
2291
|
+
console.log(chalk3.gray(` \u2026 ${included.length - limit} more included`));
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
if (selection.excluded.length > 0) {
|
|
2295
|
+
console.log(chalk3.gray(` Excluded ${selection.excluded.length}:`));
|
|
2296
|
+
for (const decision of selection.excluded.slice(0, limit)) {
|
|
2297
|
+
console.log(chalk3.gray(` - ${decision.memory.content} (${decision.reason})`));
|
|
2298
|
+
}
|
|
2299
|
+
if (selection.excluded.length > limit) {
|
|
2300
|
+
console.log(chalk3.gray(` \u2026 ${selection.excluded.length - limit} more excluded`));
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
function getConfiguredAgents(config) {
|
|
2305
|
+
const agents = config.agents?.length ? config.agents : AGENT_NAMES.filter((agent) => agent !== "Shared");
|
|
2306
|
+
return [.../* @__PURE__ */ new Set([...agents, "Shared"])];
|
|
2307
|
+
}
|
|
2308
|
+
async function syncGeneratedFiles(config, agents, options = {}) {
|
|
2309
|
+
const memorySpinner = ora(options.label ?? "Syncing memories\u2026").start();
|
|
2310
|
+
let memories = [];
|
|
2311
|
+
try {
|
|
2312
|
+
const archQuery = [config.backendArchitecture, config.frontendFramework, config.language].filter(Boolean).join(" ");
|
|
2313
|
+
const selection = await retrieveMemorySelection({
|
|
2314
|
+
query: archQuery,
|
|
2315
|
+
cwd: process.cwd(),
|
|
2316
|
+
config,
|
|
2317
|
+
limit: 25
|
|
2318
|
+
});
|
|
2319
|
+
memories = selection.included;
|
|
2320
|
+
memorySpinner.succeed(`Found ${memories.length} memories`);
|
|
2321
|
+
if (options.showSelection) {
|
|
2322
|
+
printMemorySelection(selection);
|
|
2323
|
+
}
|
|
2324
|
+
} catch (err) {
|
|
2325
|
+
memorySpinner.warn(`Could not retrieve memories: ${err.message}`);
|
|
2326
|
+
}
|
|
2327
|
+
const spinner = ora("Regenerating agent files\u2026").start();
|
|
2328
|
+
const result = await generate(
|
|
2329
|
+
{
|
|
2330
|
+
projectName: config.projectName,
|
|
2331
|
+
projectType: config.projectType,
|
|
2332
|
+
backendArchitecture: config.backendArchitecture,
|
|
2333
|
+
frontendFramework: config.frontendFramework,
|
|
2334
|
+
language: config.language,
|
|
2335
|
+
memories,
|
|
2336
|
+
caveman: config.caveman
|
|
2337
|
+
},
|
|
2338
|
+
process.cwd(),
|
|
2339
|
+
agents
|
|
2340
|
+
);
|
|
2341
|
+
spinner.succeed(
|
|
2342
|
+
`Synced \u2014 ${chalk3.green(`${result.written.length} updated`)}, ${chalk3.dim(`${result.skipped.length} already up to date`)}`
|
|
2343
|
+
);
|
|
2344
|
+
if (result.written.length > 0) {
|
|
2345
|
+
result.written.forEach((file) => console.log(chalk3.gray(` \u2713 ${file}`)));
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
async function autoSyncGeneratedFiles(config, action, enabled = true) {
|
|
2349
|
+
if (!enabled) {
|
|
2350
|
+
console.log(chalk3.gray(" Auto-sync skipped (--no-sync). Run memory-core sync when ready."));
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
if (!config) {
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
if (config.autoSync === false) {
|
|
2357
|
+
console.log(chalk3.gray(" Auto-sync disabled for this project. Run memory-core sync when ready."));
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
try {
|
|
2361
|
+
await syncGeneratedFiles(config, getConfiguredAgents(config), {
|
|
2362
|
+
label: `Auto-syncing agent files after ${action}\u2026`
|
|
2363
|
+
});
|
|
2364
|
+
} catch (err) {
|
|
2365
|
+
console.log(chalk3.yellow(` Auto-sync skipped: ${err.message}`));
|
|
2366
|
+
console.log(chalk3.gray(" Run memory-core sync manually when ready."));
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
1747
2369
|
var program = new Command();
|
|
1748
2370
|
program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version(version);
|
|
1749
2371
|
program.command("init").description("Initialize memory-core in the current project").option("--quick", "Use smart defaults and skip optional prompts").action(async (opts) => {
|
|
@@ -1755,25 +2377,19 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1755
2377
|
if (!hasEnv && quick) {
|
|
1756
2378
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
1757
2379
|
const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
|
|
1758
|
-
const ollamaUrl =
|
|
1759
|
-
const chatModel =
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
const gitignorePath2 = join6(process.cwd(), ".gitignore");
|
|
1772
|
-
const gitignore = existsSync6(gitignorePath2) ? readFileSync6(gitignorePath2, "utf-8") : "";
|
|
1773
|
-
if (!gitignore.includes(".memory-core.env")) {
|
|
1774
|
-
appendFileSync(gitignorePath2, `${gitignore ? "\n" : ""}.memory-core.env
|
|
1775
|
-
`);
|
|
1776
|
-
}
|
|
2380
|
+
const ollamaUrl = DEFAULT_OLLAMA_URL;
|
|
2381
|
+
const chatModel = DEFAULT_CHAT_MODEL;
|
|
2382
|
+
const envValues = {
|
|
2383
|
+
DATABASE_URL: dbUrl,
|
|
2384
|
+
OLLAMA_URL: ollamaUrl,
|
|
2385
|
+
OLLAMA_MODEL: DEFAULT_EMBEDDING_MODEL,
|
|
2386
|
+
CHAT_PROVIDER: "ollama",
|
|
2387
|
+
CHAT_MODEL: chatModel,
|
|
2388
|
+
OLLAMA_CHAT_MODEL: chatModel
|
|
2389
|
+
};
|
|
2390
|
+
writeRuntimeEnv(envValues, envPath);
|
|
2391
|
+
applyRuntimeEnv(envValues);
|
|
2392
|
+
ensureEnvFileIgnored(envPath);
|
|
1777
2393
|
console.log(chalk3.green(" \u2713 .memory-core.env created with local defaults"));
|
|
1778
2394
|
} else if (!hasEnv) {
|
|
1779
2395
|
console.log(chalk3.dim(" No .memory-core.env found \u2014 let's set up your connection.\n"));
|
|
@@ -1889,33 +2505,18 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1889
2505
|
});
|
|
1890
2506
|
console.log(chalk3.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
|
|
1891
2507
|
}
|
|
1892
|
-
const
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
if (chatProvider === "ollama")
|
|
1900
|
-
if (chatApiKey)
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
process.env.OLLAMA_URL = ollamaUrl;
|
|
1905
|
-
process.env.OLLAMA_MODEL = "nomic-embed-text";
|
|
1906
|
-
process.env.CHAT_PROVIDER = chatProvider;
|
|
1907
|
-
process.env.CHAT_MODEL = chatModel;
|
|
1908
|
-
if (chatProvider === "ollama") process.env.OLLAMA_CHAT_MODEL = chatModel;
|
|
1909
|
-
if (chatApiKey) process.env.CHAT_API_KEY = chatApiKey;
|
|
1910
|
-
const gitignorePath2 = join6(process.cwd(), ".gitignore");
|
|
1911
|
-
if (existsSync6(gitignorePath2)) {
|
|
1912
|
-
const gi = readFileSync6(gitignorePath2, "utf-8");
|
|
1913
|
-
if (!gi.includes(".memory-core.env")) {
|
|
1914
|
-
appendFileSync(gitignorePath2, "\n.memory-core.env\n");
|
|
1915
|
-
}
|
|
1916
|
-
} else {
|
|
1917
|
-
writeFileSync5(gitignorePath2, ".memory-core.env\n");
|
|
1918
|
-
}
|
|
2508
|
+
const envValues = {
|
|
2509
|
+
DATABASE_URL: dbUrl,
|
|
2510
|
+
OLLAMA_URL: ollamaUrl,
|
|
2511
|
+
OLLAMA_MODEL: DEFAULT_EMBEDDING_MODEL,
|
|
2512
|
+
CHAT_PROVIDER: chatProvider,
|
|
2513
|
+
CHAT_MODEL: chatModel
|
|
2514
|
+
};
|
|
2515
|
+
if (chatProvider === "ollama") envValues.OLLAMA_CHAT_MODEL = chatModel;
|
|
2516
|
+
if (chatApiKey) envValues.CHAT_API_KEY = chatApiKey;
|
|
2517
|
+
writeRuntimeEnv(envValues, envPath);
|
|
2518
|
+
applyRuntimeEnv(envValues);
|
|
2519
|
+
ensureEnvFileIgnored(envPath);
|
|
1919
2520
|
console.log(chalk3.green("\n \u2713 .memory-core.env created"));
|
|
1920
2521
|
console.log(chalk3.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
|
|
1921
2522
|
}
|
|
@@ -1976,11 +2577,20 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1976
2577
|
message: "Pull relevant memories from previous projects?",
|
|
1977
2578
|
default: true
|
|
1978
2579
|
});
|
|
1979
|
-
|
|
1980
|
-
message: "Install caveman token saver?
|
|
2580
|
+
let installCaveman = quick ? false : await confirm({
|
|
2581
|
+
message: "Install caveman token saver? Downloads and runs the upstream installer.",
|
|
1981
2582
|
default: false
|
|
1982
2583
|
});
|
|
1983
2584
|
let cavemanIntensity = "full";
|
|
2585
|
+
if (installCaveman) {
|
|
2586
|
+
const allowRemoteInstaller = await confirm({
|
|
2587
|
+
message: `Security check: download and execute installer from ${CAVEMAN_INSTALL_URL}?`,
|
|
2588
|
+
default: false
|
|
2589
|
+
});
|
|
2590
|
+
if (!allowRemoteInstaller) {
|
|
2591
|
+
installCaveman = false;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
1984
2594
|
if (installCaveman) {
|
|
1985
2595
|
cavemanIntensity = await select({
|
|
1986
2596
|
message: "Caveman intensity?",
|
|
@@ -2022,20 +2632,23 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2022
2632
|
language,
|
|
2023
2633
|
caveman: { enabled: installCaveman, intensity: cavemanIntensity },
|
|
2024
2634
|
agents: selectedAgents,
|
|
2025
|
-
allowPatterns: []
|
|
2635
|
+
allowPatterns: [],
|
|
2636
|
+
autoSync: true
|
|
2026
2637
|
};
|
|
2027
2638
|
let memories = [];
|
|
2028
2639
|
if (pullMemories) {
|
|
2029
2640
|
const spinner2 = ora("Retrieving relevant memories\u2026").start();
|
|
2030
2641
|
try {
|
|
2031
2642
|
const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
|
|
2032
|
-
|
|
2643
|
+
const selection = await retrieveMemorySelection({
|
|
2033
2644
|
query: archQuery,
|
|
2034
2645
|
cwd: process.cwd(),
|
|
2035
2646
|
config,
|
|
2036
2647
|
limit: 20
|
|
2037
2648
|
});
|
|
2649
|
+
memories = selection.included;
|
|
2038
2650
|
spinner2.succeed(`Found ${memories.length} relevant memories`);
|
|
2651
|
+
printMemorySelection(selection);
|
|
2039
2652
|
} catch (err) {
|
|
2040
2653
|
spinner2.warn(`Could not retrieve memories: ${err.message}`);
|
|
2041
2654
|
}
|
|
@@ -2043,10 +2656,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2043
2656
|
if (installCaveman) {
|
|
2044
2657
|
const spinner2 = ora("Installing caveman token saver\u2026").start();
|
|
2045
2658
|
try {
|
|
2046
|
-
|
|
2047
|
-
"curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash",
|
|
2048
|
-
{ stdio: "pipe", cwd: process.cwd() }
|
|
2049
|
-
);
|
|
2659
|
+
await installCavemanTokenSaver();
|
|
2050
2660
|
spinner2.succeed("Caveman installed");
|
|
2051
2661
|
} catch (err) {
|
|
2052
2662
|
spinner2.warn(`Caveman install failed: ${err.message}`);
|
|
@@ -2060,15 +2670,11 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2060
2670
|
);
|
|
2061
2671
|
writeProjectConfig(config);
|
|
2062
2672
|
spinner.succeed(`Generated ${written.written.length} files`);
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
if (toAdd.length > 0) {
|
|
2069
|
-
const block = "\n# memory-core generated files\n" + toAdd.join("\n") + "\n";
|
|
2070
|
-
appendFileSync(gitignorePath, block);
|
|
2071
|
-
console.log(chalk3.green(` \u2713 Added ${toAdd.length} generated files to .gitignore`));
|
|
2673
|
+
const gitignoreEntries = [...written.written, ...LOCAL_GENERATED_FILES];
|
|
2674
|
+
if (gitignoreEntries.length > 0) {
|
|
2675
|
+
const added = appendMissingGitignoreEntries(gitignoreEntries, "# memory-core generated files");
|
|
2676
|
+
if (added > 0) {
|
|
2677
|
+
console.log(chalk3.green(` \u2713 Added ${added} generated files to .gitignore`));
|
|
2072
2678
|
}
|
|
2073
2679
|
}
|
|
2074
2680
|
if (enableHook) {
|
|
@@ -2103,44 +2709,32 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
2103
2709
|
console.log(chalk3.yellow(" No agents selected \u2014 nothing to sync."));
|
|
2104
2710
|
process.exit(0);
|
|
2105
2711
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
limit: 25
|
|
2115
|
-
});
|
|
2116
|
-
spinner.text = `Found ${memories.length} memories \u2014 regenerating files\u2026`;
|
|
2117
|
-
} catch (err) {
|
|
2118
|
-
spinner.warn(`Could not retrieve memories: ${err.message}`);
|
|
2712
|
+
await syncGeneratedFiles(config, [...selectedAgents, "Shared"], { showSelection: true });
|
|
2713
|
+
await closePool();
|
|
2714
|
+
});
|
|
2715
|
+
program.command("auto-sync [mode]").description("Show or change automatic agent file sync (on|off)").action((mode) => {
|
|
2716
|
+
const config = readProjectConfig();
|
|
2717
|
+
if (!config) {
|
|
2718
|
+
console.error(chalk3.red("No .memory-core.json found. Run: memory-core init"));
|
|
2719
|
+
process.exit(1);
|
|
2119
2720
|
}
|
|
2120
|
-
const
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
language: config.language,
|
|
2127
|
-
memories,
|
|
2128
|
-
caveman: config.caveman
|
|
2129
|
-
},
|
|
2130
|
-
process.cwd(),
|
|
2131
|
-
[...selectedAgents, "Shared"]
|
|
2132
|
-
);
|
|
2133
|
-
const updatedCount = result.written.length;
|
|
2134
|
-
const skippedCount = result.skipped.length;
|
|
2135
|
-
spinner.succeed(
|
|
2136
|
-
`Synced \u2014 ${chalk3.green(`${updatedCount} updated`)}, ${chalk3.dim(`${skippedCount} already up to date`)}`
|
|
2137
|
-
);
|
|
2138
|
-
if (result.written.length > 0) {
|
|
2139
|
-
result.written.forEach((f) => console.log(chalk3.gray(` \u2713 ${f}`)));
|
|
2721
|
+
const normalized = mode?.trim().toLowerCase();
|
|
2722
|
+
if (!normalized || normalized === "status") {
|
|
2723
|
+
console.log(chalk3.bold("\n Auto-sync\n"));
|
|
2724
|
+
console.log(` Status: ${config.autoSync === false ? chalk3.yellow("disabled") : chalk3.green("enabled")}`);
|
|
2725
|
+
console.log(chalk3.gray(" Manual sync is always available: memory-core sync\n"));
|
|
2726
|
+
return;
|
|
2140
2727
|
}
|
|
2141
|
-
|
|
2728
|
+
if (normalized !== "on" && normalized !== "off") {
|
|
2729
|
+
console.error(chalk3.red("Use: memory-core auto-sync [on|off|status]"));
|
|
2730
|
+
process.exit(1);
|
|
2731
|
+
}
|
|
2732
|
+
const enabled = normalized === "on";
|
|
2733
|
+
writeProjectConfig({ ...config, autoSync: enabled });
|
|
2734
|
+
console.log(chalk3.green(`Auto-sync ${enabled ? "enabled" : "disabled"}`));
|
|
2735
|
+
console.log(chalk3.gray(" Manual sync is always available: memory-core sync"));
|
|
2142
2736
|
});
|
|
2143
|
-
program.command("remember <text>").description("Save a new memory to the central database").option("-t, --type <type>", "Memory type (decision|rule|pattern|note)", "decision").option("-s, --scope <scope>", "Scope (global|project)", "project").option("--tags <tags>", "Comma-separated tags").option("-r, --reason <reason>", "Why this rule exists \u2014 helps agents understand intent and debug violations").action(async (text, opts) => {
|
|
2737
|
+
program.command("remember <text>").description("Save a new memory to the central database").option("-t, --type <type>", "Memory type (decision|rule|pattern|note)", "decision").option("-s, --scope <scope>", "Scope (global|project)", "project").option("--tags <tags>", "Comma-separated tags").option("-r, --reason <reason>", "Why this rule exists \u2014 helps agents understand intent and debug violations").option("--no-sync", "Skip automatic agent file sync after saving").action(async (text, opts) => {
|
|
2144
2738
|
const config = readProjectConfig();
|
|
2145
2739
|
let reason = opts.reason;
|
|
2146
2740
|
if (!reason) {
|
|
@@ -2165,6 +2759,7 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
2165
2759
|
const reasonLine = reason ? chalk3.gray(`
|
|
2166
2760
|
Why: ${reason}`) : "";
|
|
2167
2761
|
spinner.succeed(chalk3.green(`Memory saved: "${text}"`) + reasonLine);
|
|
2762
|
+
await autoSyncGeneratedFiles(config, "remember", opts.sync);
|
|
2168
2763
|
} catch (err) {
|
|
2169
2764
|
spinner.fail(`Failed: ${err.message}`);
|
|
2170
2765
|
process.exit(1);
|
|
@@ -2219,9 +2814,10 @@ program.command("export").description(`Export DB memories to ${MEMORY_FILE}`).op
|
|
|
2219
2814
|
await closePool();
|
|
2220
2815
|
}
|
|
2221
2816
|
});
|
|
2222
|
-
program.command("import").description(`Import memories from ${MEMORY_FILE}`).option("--url <url>", "Import memories from a remote URL").option("-f, --file <file>", `Import file (default: ${MEMORY_FILE})`).action(async (opts) => {
|
|
2817
|
+
program.command("import").description(`Import memories from ${MEMORY_FILE}`).option("--url <url>", "Import memories from a remote URL").option("-f, --file <file>", `Import file (default: ${MEMORY_FILE})`).option("--no-sync", "Skip automatic agent file sync after import").action(async (opts) => {
|
|
2223
2818
|
const spinner = ora("Reading memories\u2026").start();
|
|
2224
2819
|
try {
|
|
2820
|
+
const config = readProjectConfig();
|
|
2225
2821
|
const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(readFileSync6(join6(process.cwd(), opts.file), "utf-8")) : readMemoryFile();
|
|
2226
2822
|
let inserted = 0;
|
|
2227
2823
|
let skipped = 0;
|
|
@@ -2243,6 +2839,9 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
|
|
|
2243
2839
|
else skipped++;
|
|
2244
2840
|
}
|
|
2245
2841
|
spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
|
|
2842
|
+
if (inserted > 0) {
|
|
2843
|
+
await autoSyncGeneratedFiles(config, "import", opts.sync);
|
|
2844
|
+
}
|
|
2246
2845
|
} catch (err) {
|
|
2247
2846
|
spinner.fail(`Import failed: ${err.message}`);
|
|
2248
2847
|
process.exit(1);
|
|
@@ -2250,15 +2849,24 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
|
|
|
2250
2849
|
await closePool();
|
|
2251
2850
|
}
|
|
2252
2851
|
});
|
|
2253
|
-
program.command("list").description("List memories from the local database").option("--type <type>", "Filter by type").option("--scope <scope>", "Filter by scope").option("--arch <architecture>", "Filter by architecture").option("-n, --limit <n>", "Maximum rows to show", "200").action(async (opts) => {
|
|
2852
|
+
program.command("list").description("List memories from the local database").option("--type <type>", "Filter by type").option("--scope <scope>", "Filter by scope").option("--arch <architecture>", "Filter by architecture").option("--project <name>", "Filter by project name").option("--current", "Only show memories relevant to the current project (default)").option("--all", "Show all memories in the database").option("-n, --limit <n>", "Maximum rows to show", "200").action(async (opts) => {
|
|
2254
2853
|
try {
|
|
2854
|
+
const config = readProjectConfig();
|
|
2855
|
+
const architectures = opts.arch ? opts.arch : opts.all ? void 0 : getCurrentListArchitectures(config);
|
|
2856
|
+
const projectName = opts.project ? opts.project : opts.all || opts.arch ? void 0 : config?.projectName;
|
|
2255
2857
|
const memories = await listMemories({
|
|
2256
2858
|
type: opts.type,
|
|
2257
2859
|
scope: opts.scope,
|
|
2258
|
-
architecture:
|
|
2860
|
+
architecture: architectures,
|
|
2861
|
+
projectName,
|
|
2862
|
+
includeGlobal: !opts.all,
|
|
2259
2863
|
limit: parseInt(opts.limit, 10)
|
|
2260
2864
|
});
|
|
2261
|
-
|
|
2865
|
+
const title = opts.all ? "All memories" : `Current project memories${architectures ? ` (${Array.isArray(architectures) ? architectures.join(", ") : architectures})` : ""}`;
|
|
2866
|
+
printMemoryTable(memories, title);
|
|
2867
|
+
if (!opts.all) {
|
|
2868
|
+
console.log(chalk3.gray(" Showing current project context plus shared/global memories. Use --all for the full database.\n"));
|
|
2869
|
+
}
|
|
2262
2870
|
} catch (err) {
|
|
2263
2871
|
console.error(chalk3.red(`List failed: ${err.message}`));
|
|
2264
2872
|
process.exit(1);
|
|
@@ -2266,14 +2874,16 @@ program.command("list").description("List memories from the local database").opt
|
|
|
2266
2874
|
await closePool();
|
|
2267
2875
|
}
|
|
2268
2876
|
});
|
|
2269
|
-
program.command("remove <id>").description("Remove a memory by ID").action(async (id) => {
|
|
2877
|
+
program.command("remove <id>").description("Remove a memory by ID").option("--no-sync", "Skip automatic agent file sync after removal").action(async (id, opts) => {
|
|
2270
2878
|
try {
|
|
2879
|
+
const config = readProjectConfig();
|
|
2271
2880
|
const deleted = await deleteMemory(parseInt(id, 10));
|
|
2272
2881
|
if (!deleted) {
|
|
2273
2882
|
console.log(chalk3.yellow(`No memory found with ID ${id}`));
|
|
2274
2883
|
process.exit(1);
|
|
2275
2884
|
}
|
|
2276
2885
|
console.log(chalk3.green(`Removed memory ${id}`));
|
|
2886
|
+
await autoSyncGeneratedFiles(config, "remove", opts.sync);
|
|
2277
2887
|
} catch (err) {
|
|
2278
2888
|
console.error(chalk3.red(`Remove failed: ${err.message}`));
|
|
2279
2889
|
process.exit(1);
|
|
@@ -2281,8 +2891,9 @@ program.command("remove <id>").description("Remove a memory by ID").action(async
|
|
|
2281
2891
|
await closePool();
|
|
2282
2892
|
}
|
|
2283
2893
|
});
|
|
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) => {
|
|
2894
|
+
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").option("--no-sync", "Skip automatic agent file sync after deletion").action(async (opts) => {
|
|
2285
2895
|
try {
|
|
2896
|
+
const config = readProjectConfig();
|
|
2286
2897
|
const deleted = await deleteMemories({
|
|
2287
2898
|
tag: opts.tag,
|
|
2288
2899
|
scope: opts.scope,
|
|
@@ -2290,6 +2901,9 @@ program.command("forget").description("Bulk-delete memories by tag, scope, type,
|
|
|
2290
2901
|
architecture: opts.arch
|
|
2291
2902
|
});
|
|
2292
2903
|
console.log(chalk3.green(`Deleted ${deleted} memories`));
|
|
2904
|
+
if (deleted > 0) {
|
|
2905
|
+
await autoSyncGeneratedFiles(config, "forget", opts.sync);
|
|
2906
|
+
}
|
|
2293
2907
|
} catch (err) {
|
|
2294
2908
|
console.error(chalk3.red(`Forget failed: ${err.message}`));
|
|
2295
2909
|
process.exit(1);
|
|
@@ -2297,9 +2911,10 @@ program.command("forget").description("Bulk-delete memories by tag, scope, type,
|
|
|
2297
2911
|
await closePool();
|
|
2298
2912
|
}
|
|
2299
2913
|
});
|
|
2300
|
-
program.command("edit <id>").description("Edit a memory interactively").action(async (id) => {
|
|
2914
|
+
program.command("edit <id>").description("Edit a memory interactively").option("--no-sync", "Skip automatic agent file sync after editing").action(async (id, opts) => {
|
|
2301
2915
|
const memoryId = parseInt(id, 10);
|
|
2302
2916
|
try {
|
|
2917
|
+
const config = readProjectConfig();
|
|
2303
2918
|
const existing = await getMemory(memoryId);
|
|
2304
2919
|
if (!existing) {
|
|
2305
2920
|
console.log(chalk3.yellow(`No memory found with ID ${id}`));
|
|
@@ -2322,6 +2937,7 @@ program.command("edit <id>").description("Edit a memory interactively").action(a
|
|
|
2322
2937
|
embedding
|
|
2323
2938
|
});
|
|
2324
2939
|
console.log(chalk3.green(`Updated memory ${id}`));
|
|
2940
|
+
await autoSyncGeneratedFiles(config, "edit", opts.sync);
|
|
2325
2941
|
} catch (err) {
|
|
2326
2942
|
console.error(chalk3.red(`Edit failed: ${err.message}`));
|
|
2327
2943
|
process.exit(1);
|
|
@@ -2329,8 +2945,9 @@ program.command("edit <id>").description("Edit a memory interactively").action(a
|
|
|
2329
2945
|
await closePool();
|
|
2330
2946
|
}
|
|
2331
2947
|
});
|
|
2332
|
-
program.command("ignore [pattern]").description("Manage project-scoped false-positive ignore patterns").option("--list", "List ignored patterns").option("--remove <id>", "Remove an ignored pattern by ID").action(async (pattern, opts) => {
|
|
2948
|
+
program.command("ignore [pattern]").description("Manage project-scoped false-positive ignore patterns").option("--list", "List ignored patterns").option("--remove <id>", "Remove an ignored pattern by ID").option("--no-sync", "Skip automatic agent file sync after changing ignore patterns").action(async (pattern, opts) => {
|
|
2333
2949
|
try {
|
|
2950
|
+
const config = readProjectConfig();
|
|
2334
2951
|
if (opts.list) {
|
|
2335
2952
|
printMemoryTable(await listMemories({ type: "ignore", limit: 1e3 }), "Ignored patterns");
|
|
2336
2953
|
return;
|
|
@@ -2342,13 +2959,13 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
2342
2959
|
process.exit(1);
|
|
2343
2960
|
}
|
|
2344
2961
|
console.log(chalk3.green(`Removed ignore pattern ${opts.remove}`));
|
|
2962
|
+
await autoSyncGeneratedFiles(config, "ignore remove", opts.sync);
|
|
2345
2963
|
return;
|
|
2346
2964
|
}
|
|
2347
2965
|
if (!pattern) {
|
|
2348
2966
|
console.error(chalk3.red("Provide a pattern, --list, or --remove <id>"));
|
|
2349
2967
|
process.exit(1);
|
|
2350
2968
|
}
|
|
2351
|
-
const config = readProjectConfig();
|
|
2352
2969
|
const embedding = await embed(pattern);
|
|
2353
2970
|
await upsertMemory({
|
|
2354
2971
|
type: "ignore",
|
|
@@ -2360,6 +2977,7 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
2360
2977
|
embedding
|
|
2361
2978
|
});
|
|
2362
2979
|
console.log(chalk3.green(`Ignored pattern saved: "${pattern}"`));
|
|
2980
|
+
await autoSyncGeneratedFiles(config, "ignore", opts.sync);
|
|
2363
2981
|
} catch (err) {
|
|
2364
2982
|
console.error(chalk3.red(`Ignore failed: ${err.message}`));
|
|
2365
2983
|
process.exit(1);
|
|
@@ -2437,7 +3055,6 @@ program.command("reset").description("Remove memory-core generated files and loc
|
|
|
2437
3055
|
default: false
|
|
2438
3056
|
});
|
|
2439
3057
|
if (ok) {
|
|
2440
|
-
const { getPool } = await import("./db-5X5LTUCB.js");
|
|
2441
3058
|
await getPool().query("DROP TABLE IF EXISTS memories");
|
|
2442
3059
|
await closePool();
|
|
2443
3060
|
console.log(chalk3.yellow("Dropped memories table"));
|
|
@@ -2617,6 +3234,82 @@ read:
|
|
|
2617
3234
|
console.log(chalk3.bold("\n Every AI agent now follows your memory globally.\n"));
|
|
2618
3235
|
await closePool();
|
|
2619
3236
|
});
|
|
3237
|
+
var provider = program.command("provider").description("Manage the code-checking provider configuration");
|
|
3238
|
+
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) => {
|
|
3239
|
+
try {
|
|
3240
|
+
const providerName = normalizeProvider(name);
|
|
3241
|
+
const runtimeEnv = readRuntimeEnv();
|
|
3242
|
+
const values = { ...runtimeEnv.values };
|
|
3243
|
+
values.CHAT_PROVIDER = providerName;
|
|
3244
|
+
values.OLLAMA_URL = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
|
|
3245
|
+
values.OLLAMA_MODEL = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
|
|
3246
|
+
if (opts.model) {
|
|
3247
|
+
values.CHAT_MODEL = opts.model;
|
|
3248
|
+
} else if (!values.CHAT_MODEL && !values.OLLAMA_CHAT_MODEL) {
|
|
3249
|
+
values.CHAT_MODEL = DEFAULT_CHAT_MODEL;
|
|
3250
|
+
}
|
|
3251
|
+
if (providerName === "ollama") {
|
|
3252
|
+
const model2 = values.CHAT_MODEL ?? values.OLLAMA_CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
|
|
3253
|
+
values.CHAT_MODEL = model2;
|
|
3254
|
+
values.OLLAMA_CHAT_MODEL = model2;
|
|
3255
|
+
delete values.CHAT_API_KEY;
|
|
3256
|
+
} else {
|
|
3257
|
+
delete values.OLLAMA_CHAT_MODEL;
|
|
3258
|
+
if (opts.apiKey) {
|
|
3259
|
+
values.CHAT_API_KEY = opts.apiKey;
|
|
3260
|
+
} else if (!values.CHAT_API_KEY) {
|
|
3261
|
+
values.CHAT_API_KEY = await input({
|
|
3262
|
+
message: `${providerLabel(providerName)} API key?`
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
3267
|
+
applyRuntimeEnv(values);
|
|
3268
|
+
ensureEnvFileIgnored(runtimeEnv.envPath);
|
|
3269
|
+
console.log(chalk3.green(`Updated provider: ${providerName}`));
|
|
3270
|
+
console.log(chalk3.gray(` Chat model: ${getConfiguredChatModel(values)}`));
|
|
3271
|
+
} catch (err) {
|
|
3272
|
+
console.error(chalk3.red(`Provider update failed: ${err.message}`));
|
|
3273
|
+
process.exit(1);
|
|
3274
|
+
}
|
|
3275
|
+
});
|
|
3276
|
+
var model = program.command("model").description("Manage code-checking and embedding models");
|
|
3277
|
+
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) => {
|
|
3278
|
+
try {
|
|
3279
|
+
const runtimeEnv = readRuntimeEnv();
|
|
3280
|
+
const values = { ...runtimeEnv.values };
|
|
3281
|
+
if (opts.provider) values.CHAT_PROVIDER = normalizeProvider(opts.provider);
|
|
3282
|
+
const providerName = getConfiguredProvider(values);
|
|
3283
|
+
if (opts.embedding) {
|
|
3284
|
+
values.OLLAMA_MODEL = name;
|
|
3285
|
+
} else {
|
|
3286
|
+
values.CHAT_MODEL = name;
|
|
3287
|
+
if (providerName === "ollama") values.OLLAMA_CHAT_MODEL = name;
|
|
3288
|
+
}
|
|
3289
|
+
values.OLLAMA_URL = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
|
|
3290
|
+
values.OLLAMA_MODEL = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
|
|
3291
|
+
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
3292
|
+
applyRuntimeEnv(values);
|
|
3293
|
+
ensureEnvFileIgnored(runtimeEnv.envPath);
|
|
3294
|
+
console.log(chalk3.green(`Updated ${opts.embedding ? "embedding" : "chat"} model: ${name}`));
|
|
3295
|
+
console.log(chalk3.gray(` Provider: ${providerName}`));
|
|
3296
|
+
} catch (err) {
|
|
3297
|
+
console.error(chalk3.red(`Model update failed: ${err.message}`));
|
|
3298
|
+
process.exit(1);
|
|
3299
|
+
}
|
|
3300
|
+
});
|
|
3301
|
+
model.command("doctor").description("Verify provider, model, database, and Ollama setup").action(async () => {
|
|
3302
|
+
const result = await runModelDoctor();
|
|
3303
|
+
if (!result.ok) process.exit(1);
|
|
3304
|
+
});
|
|
3305
|
+
program.command("status").description("Show the current memory-core project and runtime setup").action(async () => {
|
|
3306
|
+
try {
|
|
3307
|
+
await printProjectStatus();
|
|
3308
|
+
} catch (err) {
|
|
3309
|
+
console.error(chalk3.red(`Status failed: ${err.message}`));
|
|
3310
|
+
process.exit(1);
|
|
3311
|
+
}
|
|
3312
|
+
});
|
|
2620
3313
|
var hook = program.command("hook").description("Manage the pre-commit rule enforcement hook");
|
|
2621
3314
|
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) => {
|
|
2622
3315
|
const advisory = opts.strict ? false : true;
|