@shahmilsaari/memory-core 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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-25Y2KI7M.js";
17
+ } from "./chunk-WUL7HLAA.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 dedupeMemories(memories);
269
+ return explainMemorySelection(memories, options.config, options.cwd);
179
270
  }
180
271
 
181
272
  // src/generator.ts
@@ -216,6 +307,23 @@ Handlebars.registerHelper(
216
307
  (arr) => Array.isArray(arr) ? arr.map((i, idx) => `${idx + 1}. ${i}`).join("\n") : ""
217
308
  );
218
309
  Handlebars.registerHelper("json", (val) => JSON.stringify(val, null, 2));
310
+ Handlebars.registerHelper("memoryBlock", (memory) => {
311
+ const meta = [memory.type, memory.architecture].filter(Boolean).join(" \xB7 ");
312
+ const label = memory.title ? `${memory.title}: ${memory.content}` : memory.content;
313
+ const lines = [`- [${meta || "memory"}] ${label}`];
314
+ if (memory.reason) lines.push(` Why: ${memory.reason}`);
315
+ if (memory.context?.appliesTo?.length) lines.push(` Use when: ${memory.context.appliesTo.join("; ")}`);
316
+ if (memory.context?.avoidWhen?.length) lines.push(` Avoid when: ${memory.context.avoidWhen.join("; ")}`);
317
+ if (memory.context?.examples?.length) {
318
+ lines.push(" Examples:");
319
+ for (const example of memory.context.examples) lines.push(` - ${example}`);
320
+ }
321
+ if (memory.tags?.length) lines.push(` Tags: ${memory.tags.join(", ")}`);
322
+ if (memory.project_name || memory.context?.source) {
323
+ lines.push(` Source: ${memory.context?.source ?? memory.project_name}`);
324
+ }
325
+ return new Handlebars.SafeString(lines.join("\n"));
326
+ });
219
327
  function loadProfile(name) {
220
328
  const profilePath = join2(PKG_ROOT, "profiles", `${name}.yml`);
221
329
  if (!existsSync2(profilePath)) throw new Error(`Profile not found: ${name}`);
@@ -229,10 +337,15 @@ function listProfiles(layer) {
229
337
  if (layer === "frontend") return all.filter((p) => p.layer === "frontend" || p.layer === "fullstack");
230
338
  return all;
231
339
  }
232
- function buildTemplateData(options) {
340
+ function buildTemplateData(options, cwd = process.cwd()) {
233
341
  const backend = options.backendArchitecture ? loadProfile(options.backendArchitecture) : null;
234
342
  const frontend = options.frontendFramework ? loadProfile(options.frontendFramework) : null;
235
- const dedupedMemories = dedupeMemories(options.memories);
343
+ const dedupedMemories = filterRelevantMemories(options.memories, {
344
+ projectType: options.projectType,
345
+ backendArchitecture: options.backendArchitecture,
346
+ frontendFramework: options.frontendFramework,
347
+ language: options.language
348
+ }, cwd);
236
349
  const allRules = [
237
350
  ...backend?.rules ?? [],
238
351
  ...frontend?.rules ?? []
@@ -300,7 +413,7 @@ function writeFile(filePath, content) {
300
413
  return "written";
301
414
  }
302
415
  async function generate(options, cwd = process.cwd(), onlyAgents) {
303
- const data = buildTemplateData(options);
416
+ const data = buildTemplateData(options, cwd);
304
417
  const written = [];
305
418
  const skipped = [];
306
419
  const files = onlyAgents ? OUTPUT_FILES.filter((f) => onlyAgents.includes(f.agent)) : OUTPUT_FILES;
@@ -823,9 +936,28 @@ function toPortableMemory(memory) {
823
936
  title: memory.title,
824
937
  content: memory.content,
825
938
  reason: memory.reason,
939
+ context: memory.context,
826
940
  tags: memory.tags ?? []
827
941
  };
828
942
  }
943
+ function normalizeStringArray(value) {
944
+ if (!Array.isArray(value)) return void 0;
945
+ const entries = value.filter((entry) => typeof entry === "string" && entry.trim() !== "");
946
+ return entries.length ? entries : void 0;
947
+ }
948
+ function parseContext(value) {
949
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
950
+ const record = value;
951
+ const context = {};
952
+ const appliesTo = normalizeStringArray(record.appliesTo);
953
+ const avoidWhen = normalizeStringArray(record.avoidWhen);
954
+ const examples = normalizeStringArray(record.examples);
955
+ if (appliesTo) context.appliesTo = appliesTo;
956
+ if (avoidWhen) context.avoidWhen = avoidWhen;
957
+ if (examples) context.examples = examples;
958
+ if (typeof record.source === "string" && record.source.trim() !== "") context.source = record.source;
959
+ return Object.keys(context).length ? context : void 0;
960
+ }
829
961
  function writeMemoryFile(memories, cwd = process.cwd()) {
830
962
  const path = join3(cwd, MEMORY_FILE);
831
963
  writeFileSync2(path, JSON.stringify(memories, null, 2) + "\n", "utf-8");
@@ -864,6 +996,7 @@ function parseMemoryFile(raw) {
864
996
  title: typeof record.title === "string" ? record.title : void 0,
865
997
  content: record.content,
866
998
  reason: typeof record.reason === "string" ? record.reason : void 0,
999
+ context: parseContext(record.context),
867
1000
  tags: Array.isArray(record.tags) ? record.tags.filter((tag) => typeof tag === "string") : []
868
1001
  };
869
1002
  });
@@ -922,7 +1055,7 @@ async function promptToSaveViolations(violations) {
922
1055
  default: selected.reason ?? selected.issue ?? ""
923
1056
  });
924
1057
  const { embed: embed2 } = await import("./embedding-PAYD2JYW.js");
925
- const { upsertMemory: upsertMemory2 } = await import("./db-5X5LTUCB.js");
1058
+ const { upsertMemory: upsertMemory2 } = await import("./db-MF3VKVKH.js");
926
1059
  await upsertMemory2({
927
1060
  type: "rule",
928
1061
  scope: "project",
@@ -939,7 +1072,7 @@ async function promptToSaveViolations(violations) {
939
1072
  }
940
1073
  async function loadIgnorePatterns() {
941
1074
  try {
942
- const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-5X5LTUCB.js");
1075
+ const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-MF3VKVKH.js");
943
1076
  const ignores = await listMemories2({ type: "ignore", limit: 1e3 });
944
1077
  await closePool2();
945
1078
  return ignores.map((ignore) => ignore.content);
@@ -996,6 +1129,124 @@ ${violation.file}`.toLowerCase();
996
1129
  return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
997
1130
  });
998
1131
  }
1132
+ function normalizeViolation(value) {
1133
+ if (!value || typeof value !== "object") return null;
1134
+ const candidate = value;
1135
+ if (typeof candidate.rule !== "string" || typeof candidate.issue !== "string") return null;
1136
+ return {
1137
+ rule: candidate.rule,
1138
+ file: typeof candidate.file === "string" ? candidate.file : "diff",
1139
+ line: typeof candidate.line === "number" ? candidate.line : void 0,
1140
+ issue: candidate.issue,
1141
+ suggestion: typeof candidate.suggestion === "string" ? candidate.suggestion : void 0,
1142
+ reason: typeof candidate.reason === "string" ? candidate.reason : void 0
1143
+ };
1144
+ }
1145
+ function parseModelViolations(raw) {
1146
+ const candidates = [
1147
+ raw,
1148
+ raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "")
1149
+ ];
1150
+ const objectStart = raw.indexOf("{");
1151
+ const objectEnd = raw.lastIndexOf("}");
1152
+ if (objectStart !== -1 && objectEnd > objectStart) {
1153
+ candidates.push(raw.slice(objectStart, objectEnd + 1));
1154
+ }
1155
+ const arrayStart = raw.indexOf("[");
1156
+ const arrayEnd = raw.lastIndexOf("]");
1157
+ if (arrayStart !== -1 && arrayEnd > arrayStart) {
1158
+ candidates.push(raw.slice(arrayStart, arrayEnd + 1));
1159
+ }
1160
+ for (const candidate of candidates) {
1161
+ try {
1162
+ const parsed = JSON.parse(candidate);
1163
+ const items = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.violations) ? parsed.violations : parsed?.rule ? [parsed] : null;
1164
+ if (!items) continue;
1165
+ return {
1166
+ valid: true,
1167
+ violations: items.map(normalizeViolation).filter((violation) => violation !== null)
1168
+ };
1169
+ } catch {
1170
+ }
1171
+ }
1172
+ return { valid: false, violations: [] };
1173
+ }
1174
+ function getAddedLines(diff) {
1175
+ const lines = [];
1176
+ let currentFile = "diff";
1177
+ let newLineNumber = 0;
1178
+ for (const line of diff.split("\n")) {
1179
+ if (line.startsWith("+++ b/")) {
1180
+ currentFile = line.slice("+++ b/".length);
1181
+ continue;
1182
+ }
1183
+ const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
1184
+ if (hunk) {
1185
+ newLineNumber = Number(hunk[1]);
1186
+ continue;
1187
+ }
1188
+ if (line.startsWith("+") && !line.startsWith("+++")) {
1189
+ lines.push({
1190
+ file: currentFile,
1191
+ line: Number.isFinite(newLineNumber) ? newLineNumber : void 0,
1192
+ content: line.slice(1)
1193
+ });
1194
+ newLineNumber += 1;
1195
+ continue;
1196
+ }
1197
+ if (!line.startsWith("-") && newLineNumber > 0) {
1198
+ newLineNumber += 1;
1199
+ }
1200
+ }
1201
+ return lines;
1202
+ }
1203
+ function dedupeViolations(violations) {
1204
+ const seen = /* @__PURE__ */ new Set();
1205
+ const deduped = [];
1206
+ for (const violation of violations) {
1207
+ const key = [
1208
+ violation.rule,
1209
+ violation.file,
1210
+ violation.line ?? "",
1211
+ violation.issue
1212
+ ].join("\0");
1213
+ if (seen.has(key)) continue;
1214
+ seen.add(key);
1215
+ deduped.push(violation);
1216
+ }
1217
+ return deduped;
1218
+ }
1219
+ function findDeterministicViolations(diff, rules, avoids, allowPatterns = []) {
1220
+ const rulePhrases = rules.flatMap(
1221
+ (rule) => extractForbiddenPhrases(rule).map((phrase) => ({ rule, phrase }))
1222
+ );
1223
+ const avoidPhrases = avoids.map((avoid) => ({
1224
+ rule: `Avoid: ${avoid}`,
1225
+ phrase: avoid.toLowerCase()
1226
+ }));
1227
+ const phrases = [...rulePhrases, ...avoidPhrases].filter((item) => item.phrase.length > 0);
1228
+ if (phrases.length === 0) return [];
1229
+ const violations = [];
1230
+ for (const addedLine of getAddedLines(diff)) {
1231
+ const normalizedLine = addedLine.content.toLowerCase();
1232
+ if (allowPatterns.some((pattern) => normalizedLine.includes(pattern.toLowerCase()))) {
1233
+ continue;
1234
+ }
1235
+ for (const { rule, phrase } of phrases) {
1236
+ if (normalizedLine.includes(phrase)) {
1237
+ violations.push({
1238
+ rule,
1239
+ file: addedLine.file,
1240
+ line: addedLine.line,
1241
+ issue: `Added line contains forbidden phrase: "${phrase}"`,
1242
+ suggestion: "Remove this pattern or add an explicit ignore memory if it is intentional.",
1243
+ reason: reasonMap.get(rule)
1244
+ });
1245
+ }
1246
+ }
1247
+ }
1248
+ return dedupeViolations(violations);
1249
+ }
999
1250
  async function verifyViolations(diff, violations, allowPatterns, debug) {
1000
1251
  if (violations.length === 0) return violations;
1001
1252
  const systemPrompt = `You are verifying candidate architecture violations.
@@ -1023,9 +1274,8 @@ ${JSON.stringify(violations, null, 2)}`;
1023
1274
  { role: "system", content: systemPrompt },
1024
1275
  { role: "user", content: userPrompt }
1025
1276
  ]);
1026
- const parsed = JSON.parse(raw);
1027
- if (Array.isArray(parsed?.violations)) return parsed.violations;
1028
- if (Array.isArray(parsed)) return parsed;
1277
+ const parsed = parseModelViolations(raw);
1278
+ if (parsed.valid) return parsed.violations;
1029
1279
  return violations;
1030
1280
  } catch {
1031
1281
  return violations;
@@ -1142,7 +1392,8 @@ Do not include any text outside the JSON object.`;
1142
1392
  console.log(chalk.dim(diffToSend));
1143
1393
  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
1394
  }
1145
- let violations = [];
1395
+ const deterministicViolations = findDeterministicViolations(diff, rules, avoids, allowPatterns);
1396
+ let modelViolations = [];
1146
1397
  try {
1147
1398
  const raw = await callChatModel([
1148
1399
  { role: "system", content: systemPrompt },
@@ -1153,36 +1404,29 @@ ${diffToSend}` }
1153
1404
  if (options.verbose || options.debug) {
1154
1405
  console.log(chalk.gray(` raw response: ${options.debug ? raw : raw.slice(0, 200)}`));
1155
1406
  }
1156
- try {
1157
- const parsed = JSON.parse(raw);
1158
- if (Array.isArray(parsed)) {
1159
- violations = parsed;
1160
- } else if (Array.isArray(parsed?.violations)) {
1161
- violations = parsed.violations;
1162
- } else if (parsed?.rule) {
1163
- violations = [parsed];
1164
- } else {
1165
- violations = [];
1166
- }
1167
- } catch {
1168
- violations = [];
1407
+ const parsed = parseModelViolations(raw);
1408
+ if (parsed.valid) {
1409
+ modelViolations = parsed.violations;
1410
+ } else {
1411
+ console.log(chalk.yellow(" \u26A0 AI returned invalid JSON \u2014 using deterministic checks only."));
1169
1412
  }
1170
1413
  } catch (err) {
1171
1414
  if (err.message?.startsWith("MODEL_NOT_FOUND:")) {
1172
1415
  printModelMissing(err.message.split(":")[1]);
1173
- return;
1174
- }
1175
- if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
1176
- console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 skipping rule check."));
1416
+ modelViolations = [];
1417
+ } else if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
1418
+ console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 using deterministic checks only."));
1177
1419
  console.log(chalk.gray(" Start it: ollama serve\n"));
1178
- return;
1420
+ modelViolations = [];
1421
+ } else {
1422
+ console.log(chalk.yellow(`
1423
+ \u26A0 AI rule check failed: ${err.message}`));
1424
+ console.log(chalk.gray(" Using deterministic checks only.\n"));
1425
+ modelViolations = [];
1179
1426
  }
1180
- console.log(chalk.yellow(`
1181
- \u26A0 Rule check failed: ${err.message}
1182
- `));
1183
- return;
1184
1427
  }
1185
- violations = await verifyViolations(diff, violations, allowPatterns, options.debug ?? false);
1428
+ modelViolations = await verifyViolations(diff, modelViolations, allowPatterns, options.debug ?? false);
1429
+ let violations = dedupeViolations([...deterministicViolations, ...modelViolations]);
1186
1430
  violations = applyAllowPatterns(violations, allowPatterns);
1187
1431
  if (violations.length === 0) {
1188
1432
  console.log(chalk.green(" \u2713 No rule violations \u2014 commit allowed.\n"));
@@ -1454,7 +1698,7 @@ ${JSON.stringify(violations, null, 2)}`;
1454
1698
  }
1455
1699
  async function loadIgnorePatterns2() {
1456
1700
  try {
1457
- const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-5X5LTUCB.js");
1701
+ const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-MF3VKVKH.js");
1458
1702
  const ignores = await listMemories2({ type: "ignore", limit: 1e3 });
1459
1703
  await closePool2();
1460
1704
  return ignores.map((ignore) => ignore.content);
@@ -1633,6 +1877,55 @@ async function startWatch(options = {}) {
1633
1877
  });
1634
1878
  }
1635
1879
 
1880
+ // src/remote-install.ts
1881
+ import { spawnSync as spawnSync3 } from "child_process";
1882
+ var CAVEMAN_INSTALL_URL = "https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh";
1883
+ var MAX_INSTALLER_BYTES = 2e5;
1884
+ var TRUSTED_INSTALL_HOSTS = /* @__PURE__ */ new Set(["raw.githubusercontent.com"]);
1885
+ function assertTrustedInstallerUrl(url) {
1886
+ const parsed = new URL(url);
1887
+ if (parsed.protocol !== "https:") {
1888
+ throw new Error("Remote installer URL must use HTTPS");
1889
+ }
1890
+ if (!TRUSTED_INSTALL_HOSTS.has(parsed.hostname)) {
1891
+ throw new Error(`Remote installer host is not trusted: ${parsed.hostname}`);
1892
+ }
1893
+ }
1894
+ function defaultRunScript(script) {
1895
+ return spawnSync3("bash", ["-s"], {
1896
+ input: script,
1897
+ encoding: "utf-8",
1898
+ stdio: ["pipe", "pipe", "pipe"]
1899
+ });
1900
+ }
1901
+ async function fetchRemoteInstaller(url, fetchImpl = fetch) {
1902
+ assertTrustedInstallerUrl(url);
1903
+ const response = await fetchImpl(url, {
1904
+ signal: AbortSignal.timeout(15e3)
1905
+ });
1906
+ if (!response.ok) {
1907
+ throw new Error(`Failed to download installer (${response.status})`);
1908
+ }
1909
+ const script = await response.text();
1910
+ if (script.length > MAX_INSTALLER_BYTES) {
1911
+ throw new Error(`Installer is too large (${script.length} bytes)`);
1912
+ }
1913
+ if (script.includes("\0")) {
1914
+ throw new Error("Installer contains null bytes");
1915
+ }
1916
+ return script;
1917
+ }
1918
+ async function installCavemanTokenSaver(options = {}) {
1919
+ const url = options.url ?? CAVEMAN_INSTALL_URL;
1920
+ const script = await fetchRemoteInstaller(url, options.fetchImpl ?? fetch);
1921
+ const result = (options.runScript ?? defaultRunScript)(script);
1922
+ if (result.error) throw result.error;
1923
+ if (result.status !== 0) {
1924
+ const stderr = result.stderr ? String(result.stderr).trim() : "";
1925
+ throw new Error(stderr || `Installer exited with status ${result.status}`);
1926
+ }
1927
+ }
1928
+
1636
1929
  // src/cli.ts
1637
1930
  function printBanner(projectName, agentCount, status) {
1638
1931
  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 +1990,7 @@ async function checkConnections(dbUrl, ollamaUrl, chatModel) {
1697
1990
  }
1698
1991
  var { version } = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf-8"));
1699
1992
  var CONFIG_FILE = ".memory-core.json";
1993
+ var LOCAL_GENERATED_FILES = [".memory-core-stats.json"];
1700
1994
  var DEFAULT_OLLAMA_URL = "http://localhost:11434";
1701
1995
  var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
1702
1996
  var DEFAULT_CHAT_MODEL = "llama3.2";
@@ -1772,6 +2066,22 @@ function ensureEnvFileIgnored(envPath = getEnvPath()) {
1772
2066
  `);
1773
2067
  }
1774
2068
  }
2069
+ function appendMissingGitignoreEntries(entries, heading) {
2070
+ const gitignorePath = join6(process.cwd(), ".gitignore");
2071
+ const existing = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
2072
+ const existingEntries = new Set(
2073
+ existing.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
2074
+ );
2075
+ const toAdd = entries.filter((entry) => !existingEntries.has(entry));
2076
+ if (toAdd.length === 0) {
2077
+ return 0;
2078
+ }
2079
+ const prefix = existing.trim().length > 0 ? "\n" : "";
2080
+ appendFileSync(gitignorePath, `${prefix}${heading}
2081
+ ${toAdd.join("\n")}
2082
+ `);
2083
+ return toAdd.length;
2084
+ }
1775
2085
  function normalizeProvider(value) {
1776
2086
  const provider2 = value.trim().toLowerCase();
1777
2087
  if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax") {
@@ -1858,6 +2168,21 @@ function updateProjectConfig(mutator) {
1858
2168
  function parseTags(tags) {
1859
2169
  return tags ? tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
1860
2170
  }
2171
+ function parseList(value) {
2172
+ const entries = value?.split(",").map((entry) => entry.trim()).filter(Boolean) ?? [];
2173
+ return entries.length ? entries : void 0;
2174
+ }
2175
+ function buildMemoryContext(opts) {
2176
+ const context = {};
2177
+ const appliesTo = parseList(opts.appliesTo);
2178
+ const avoidWhen = parseList(opts.avoidWhen);
2179
+ const examples = parseList(opts.example);
2180
+ if (appliesTo) context.appliesTo = appliesTo;
2181
+ if (avoidWhen) context.avoidWhen = avoidWhen;
2182
+ if (examples) context.examples = examples;
2183
+ if (opts.source?.trim()) context.source = opts.source.trim();
2184
+ return Object.keys(context).length ? context : void 0;
2185
+ }
1861
2186
  function truncate(value, length) {
1862
2187
  if (!value) return "";
1863
2188
  return value.length > length ? `${value.slice(0, Math.max(0, length - 1))}\u2026` : value;
@@ -1877,6 +2202,9 @@ function printMemoryTable(memories, title = "Rules in memory") {
1877
2202
  });
1878
2203
  console.log(chalk3.gray("\n Use: memory-core remove <id> | memory-core edit <id>\n"));
1879
2204
  }
2205
+ function getCurrentListArchitectures(config) {
2206
+ return inferProjectArchitectures(process.cwd(), config).filter((architecture) => architecture !== "global");
2207
+ }
1880
2208
  function printStatusLine(label, value) {
1881
2209
  console.log(` ${chalk3.dim(label.padEnd(18))} ${value}`);
1882
2210
  }
@@ -1974,6 +2302,7 @@ async function printProjectStatus() {
1974
2302
  printStatusLine("Architectures", architectures.length ? architectures.join(", ") : chalk3.gray("none detected"));
1975
2303
  printStatusLine("Agents", config?.agents?.length ? `${config.agents.length} selected` : chalk3.gray("none saved"));
1976
2304
  printStatusLine("Caveman", config?.caveman?.enabled ? `enabled (${config.caveman.intensity})` : "disabled");
2305
+ printStatusLine("Auto sync", config?.autoSync === false ? "disabled" : "enabled");
1977
2306
  printStatusLine("Allow patterns", String(getAllowPatterns(config).length));
1978
2307
  printStatusLine("Env file", `${existsSync6(envPath) ? "present" : "missing"} (${envPath.split("/").pop()})`);
1979
2308
  printStatusLine("Memory file", existsSync6(memoryFilePath) ? MEMORY_FILE : chalk3.gray("not exported"));
@@ -2001,6 +2330,94 @@ async function printProjectStatus() {
2001
2330
  }
2002
2331
  console.log();
2003
2332
  }
2333
+ function printMemorySelection(selection, limit = 4) {
2334
+ const active = selection.activeArchitectures.join(", ") || "none detected";
2335
+ console.log(chalk3.gray(` Stack filter: ${active}`));
2336
+ const included = selection.decisions.filter((decision) => decision.status === "included");
2337
+ if (included.length > 0) {
2338
+ console.log(chalk3.gray(` Included ${included.length}:`));
2339
+ for (const decision of included.slice(0, limit)) {
2340
+ console.log(chalk3.gray(` + ${decision.memory.content} (${decision.reason})`));
2341
+ }
2342
+ if (included.length > limit) {
2343
+ console.log(chalk3.gray(` \u2026 ${included.length - limit} more included`));
2344
+ }
2345
+ }
2346
+ if (selection.excluded.length > 0) {
2347
+ console.log(chalk3.gray(` Excluded ${selection.excluded.length}:`));
2348
+ for (const decision of selection.excluded.slice(0, limit)) {
2349
+ console.log(chalk3.gray(` - ${decision.memory.content} (${decision.reason})`));
2350
+ }
2351
+ if (selection.excluded.length > limit) {
2352
+ console.log(chalk3.gray(` \u2026 ${selection.excluded.length - limit} more excluded`));
2353
+ }
2354
+ }
2355
+ }
2356
+ function getConfiguredAgents(config) {
2357
+ const agents = config.agents?.length ? config.agents : AGENT_NAMES.filter((agent) => agent !== "Shared");
2358
+ return [.../* @__PURE__ */ new Set([...agents, "Shared"])];
2359
+ }
2360
+ async function syncGeneratedFiles(config, agents, options = {}) {
2361
+ const memorySpinner = ora(options.label ?? "Syncing memories\u2026").start();
2362
+ let memories = [];
2363
+ try {
2364
+ const archQuery = [config.backendArchitecture, config.frontendFramework, config.language].filter(Boolean).join(" ");
2365
+ const selection = await retrieveMemorySelection({
2366
+ query: archQuery,
2367
+ cwd: process.cwd(),
2368
+ config,
2369
+ limit: 25
2370
+ });
2371
+ memories = selection.included;
2372
+ memorySpinner.succeed(`Found ${memories.length} memories`);
2373
+ if (options.showSelection) {
2374
+ printMemorySelection(selection);
2375
+ }
2376
+ } catch (err) {
2377
+ memorySpinner.warn(`Could not retrieve memories: ${err.message}`);
2378
+ }
2379
+ const spinner = ora("Regenerating agent files\u2026").start();
2380
+ const result = await generate(
2381
+ {
2382
+ projectName: config.projectName,
2383
+ projectType: config.projectType,
2384
+ backendArchitecture: config.backendArchitecture,
2385
+ frontendFramework: config.frontendFramework,
2386
+ language: config.language,
2387
+ memories,
2388
+ caveman: config.caveman
2389
+ },
2390
+ process.cwd(),
2391
+ agents
2392
+ );
2393
+ spinner.succeed(
2394
+ `Synced \u2014 ${chalk3.green(`${result.written.length} updated`)}, ${chalk3.dim(`${result.skipped.length} already up to date`)}`
2395
+ );
2396
+ if (result.written.length > 0) {
2397
+ result.written.forEach((file) => console.log(chalk3.gray(` \u2713 ${file}`)));
2398
+ }
2399
+ }
2400
+ async function autoSyncGeneratedFiles(config, action, enabled = true) {
2401
+ if (!enabled) {
2402
+ console.log(chalk3.gray(" Auto-sync skipped (--no-sync). Run memory-core sync when ready."));
2403
+ return;
2404
+ }
2405
+ if (!config) {
2406
+ return;
2407
+ }
2408
+ if (config.autoSync === false) {
2409
+ console.log(chalk3.gray(" Auto-sync disabled for this project. Run memory-core sync when ready."));
2410
+ return;
2411
+ }
2412
+ try {
2413
+ await syncGeneratedFiles(config, getConfiguredAgents(config), {
2414
+ label: `Auto-syncing agent files after ${action}\u2026`
2415
+ });
2416
+ } catch (err) {
2417
+ console.log(chalk3.yellow(` Auto-sync skipped: ${err.message}`));
2418
+ console.log(chalk3.gray(" Run memory-core sync manually when ready."));
2419
+ }
2420
+ }
2004
2421
  var program = new Command();
2005
2422
  program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version(version);
2006
2423
  program.command("init").description("Initialize memory-core in the current project").option("--quick", "Use smart defaults and skip optional prompts").action(async (opts) => {
@@ -2212,11 +2629,20 @@ program.command("init").description("Initialize memory-core in the current proje
2212
2629
  message: "Pull relevant memories from previous projects?",
2213
2630
  default: true
2214
2631
  });
2215
- const installCaveman = quick ? false : await confirm({
2216
- message: "Install caveman token saver? (~65-75% fewer tokens)",
2632
+ let installCaveman = quick ? false : await confirm({
2633
+ message: "Install caveman token saver? Downloads and runs the upstream installer.",
2217
2634
  default: false
2218
2635
  });
2219
2636
  let cavemanIntensity = "full";
2637
+ if (installCaveman) {
2638
+ const allowRemoteInstaller = await confirm({
2639
+ message: `Security check: download and execute installer from ${CAVEMAN_INSTALL_URL}?`,
2640
+ default: false
2641
+ });
2642
+ if (!allowRemoteInstaller) {
2643
+ installCaveman = false;
2644
+ }
2645
+ }
2220
2646
  if (installCaveman) {
2221
2647
  cavemanIntensity = await select({
2222
2648
  message: "Caveman intensity?",
@@ -2258,20 +2684,23 @@ program.command("init").description("Initialize memory-core in the current proje
2258
2684
  language,
2259
2685
  caveman: { enabled: installCaveman, intensity: cavemanIntensity },
2260
2686
  agents: selectedAgents,
2261
- allowPatterns: []
2687
+ allowPatterns: [],
2688
+ autoSync: true
2262
2689
  };
2263
2690
  let memories = [];
2264
2691
  if (pullMemories) {
2265
2692
  const spinner2 = ora("Retrieving relevant memories\u2026").start();
2266
2693
  try {
2267
2694
  const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
2268
- memories = await retrieveContextualMemories({
2695
+ const selection = await retrieveMemorySelection({
2269
2696
  query: archQuery,
2270
2697
  cwd: process.cwd(),
2271
2698
  config,
2272
2699
  limit: 20
2273
2700
  });
2701
+ memories = selection.included;
2274
2702
  spinner2.succeed(`Found ${memories.length} relevant memories`);
2703
+ printMemorySelection(selection);
2275
2704
  } catch (err) {
2276
2705
  spinner2.warn(`Could not retrieve memories: ${err.message}`);
2277
2706
  }
@@ -2279,10 +2708,7 @@ program.command("init").description("Initialize memory-core in the current proje
2279
2708
  if (installCaveman) {
2280
2709
  const spinner2 = ora("Installing caveman token saver\u2026").start();
2281
2710
  try {
2282
- execSync2(
2283
- "curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash",
2284
- { stdio: "pipe", cwd: process.cwd() }
2285
- );
2711
+ await installCavemanTokenSaver();
2286
2712
  spinner2.succeed("Caveman installed");
2287
2713
  } catch (err) {
2288
2714
  spinner2.warn(`Caveman install failed: ${err.message}`);
@@ -2296,15 +2722,11 @@ program.command("init").description("Initialize memory-core in the current proje
2296
2722
  );
2297
2723
  writeProjectConfig(config);
2298
2724
  spinner.succeed(`Generated ${written.written.length} files`);
2299
- const gitignorePath = join6(process.cwd(), ".gitignore");
2300
- const generatedPaths = written.written;
2301
- if (generatedPaths.length > 0) {
2302
- const existing = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
2303
- const toAdd = generatedPaths.filter((p) => !existing.includes(p));
2304
- if (toAdd.length > 0) {
2305
- const block = "\n# memory-core generated files\n" + toAdd.join("\n") + "\n";
2306
- appendFileSync(gitignorePath, block);
2307
- console.log(chalk3.green(` \u2713 Added ${toAdd.length} generated files to .gitignore`));
2725
+ const gitignoreEntries = [...written.written, ...LOCAL_GENERATED_FILES];
2726
+ if (gitignoreEntries.length > 0) {
2727
+ const added = appendMissingGitignoreEntries(gitignoreEntries, "# memory-core generated files");
2728
+ if (added > 0) {
2729
+ console.log(chalk3.green(` \u2713 Added ${added} generated files to .gitignore`));
2308
2730
  }
2309
2731
  }
2310
2732
  if (enableHook) {
@@ -2339,44 +2761,32 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
2339
2761
  console.log(chalk3.yellow(" No agents selected \u2014 nothing to sync."));
2340
2762
  process.exit(0);
2341
2763
  }
2342
- const spinner = ora("Syncing memories\u2026").start();
2343
- let memories = [];
2344
- try {
2345
- const archQuery = [config.backendArchitecture, config.frontendFramework, config.language].filter(Boolean).join(" ");
2346
- memories = await retrieveContextualMemories({
2347
- query: archQuery,
2348
- cwd: process.cwd(),
2349
- config,
2350
- limit: 25
2351
- });
2352
- spinner.text = `Found ${memories.length} memories \u2014 regenerating files\u2026`;
2353
- } catch (err) {
2354
- spinner.warn(`Could not retrieve memories: ${err.message}`);
2764
+ await syncGeneratedFiles(config, [...selectedAgents, "Shared"], { showSelection: true });
2765
+ await closePool();
2766
+ });
2767
+ program.command("auto-sync [mode]").description("Show or change automatic agent file sync (on|off)").action((mode) => {
2768
+ const config = readProjectConfig();
2769
+ if (!config) {
2770
+ console.error(chalk3.red("No .memory-core.json found. Run: memory-core init"));
2771
+ process.exit(1);
2355
2772
  }
2356
- const result = await generate(
2357
- {
2358
- projectName: config.projectName,
2359
- projectType: config.projectType,
2360
- backendArchitecture: config.backendArchitecture,
2361
- frontendFramework: config.frontendFramework,
2362
- language: config.language,
2363
- memories,
2364
- caveman: config.caveman
2365
- },
2366
- process.cwd(),
2367
- [...selectedAgents, "Shared"]
2368
- );
2369
- const updatedCount = result.written.length;
2370
- const skippedCount = result.skipped.length;
2371
- spinner.succeed(
2372
- `Synced \u2014 ${chalk3.green(`${updatedCount} updated`)}, ${chalk3.dim(`${skippedCount} already up to date`)}`
2373
- );
2374
- if (result.written.length > 0) {
2375
- result.written.forEach((f) => console.log(chalk3.gray(` \u2713 ${f}`)));
2773
+ const normalized = mode?.trim().toLowerCase();
2774
+ if (!normalized || normalized === "status") {
2775
+ console.log(chalk3.bold("\n Auto-sync\n"));
2776
+ console.log(` Status: ${config.autoSync === false ? chalk3.yellow("disabled") : chalk3.green("enabled")}`);
2777
+ console.log(chalk3.gray(" Manual sync is always available: memory-core sync\n"));
2778
+ return;
2376
2779
  }
2377
- await closePool();
2780
+ if (normalized !== "on" && normalized !== "off") {
2781
+ console.error(chalk3.red("Use: memory-core auto-sync [on|off|status]"));
2782
+ process.exit(1);
2783
+ }
2784
+ const enabled = normalized === "on";
2785
+ writeProjectConfig({ ...config, autoSync: enabled });
2786
+ console.log(chalk3.green(`Auto-sync ${enabled ? "enabled" : "disabled"}`));
2787
+ console.log(chalk3.gray(" Manual sync is always available: memory-core sync"));
2378
2788
  });
2379
- 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) => {
2789
+ 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("--applies-to <items>", "Comma-separated situations where this memory applies").option("--avoid-when <items>", "Comma-separated situations where this memory should not be used").option("--example <items>", "Comma-separated examples that teach agents how to apply this memory").option("--source <source>", "Human-readable source for this memory").option("--no-sync", "Skip automatic agent file sync after saving").action(async (text, opts) => {
2380
2790
  const config = readProjectConfig();
2381
2791
  let reason = opts.reason;
2382
2792
  if (!reason) {
@@ -2395,12 +2805,14 @@ program.command("remember <text>").description("Save a new memory to the central
2395
2805
  projectName: config?.projectName,
2396
2806
  content: text,
2397
2807
  reason: reason || void 0,
2398
- tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [],
2808
+ context: buildMemoryContext(opts),
2809
+ tags: parseTags(opts.tags),
2399
2810
  embedding
2400
2811
  });
2401
2812
  const reasonLine = reason ? chalk3.gray(`
2402
2813
  Why: ${reason}`) : "";
2403
2814
  spinner.succeed(chalk3.green(`Memory saved: "${text}"`) + reasonLine);
2815
+ await autoSyncGeneratedFiles(config, "remember", opts.sync);
2404
2816
  } catch (err) {
2405
2817
  spinner.fail(`Failed: ${err.message}`);
2406
2818
  process.exit(1);
@@ -2428,6 +2840,10 @@ program.command("search <query>").description("Search memories using semantic si
2428
2840
  const sim = m.similarity ? chalk3.gray(` (${(m.similarity * 100).toFixed(0)}% match)`) : "";
2429
2841
  console.log(chalk3.cyan(` ${i + 1}. [${m.type}] ${m.title ?? ""}`));
2430
2842
  console.log(chalk3.white(` ${m.content}`) + sim);
2843
+ if (m.reason) console.log(chalk3.gray(` why: ${m.reason}`));
2844
+ if (m.context?.appliesTo?.length) console.log(chalk3.gray(` use when: ${m.context.appliesTo.join("; ")}`));
2845
+ if (m.context?.avoidWhen?.length) console.log(chalk3.gray(` avoid when: ${m.context.avoidWhen.join("; ")}`));
2846
+ if (m.context?.examples?.length) console.log(chalk3.gray(` examples: ${m.context.examples.join("; ")}`));
2431
2847
  if (m.tags?.length) console.log(chalk3.gray(` tags: ${m.tags.join(", ")}`));
2432
2848
  console.log();
2433
2849
  });
@@ -2455,9 +2871,10 @@ program.command("export").description(`Export DB memories to ${MEMORY_FILE}`).op
2455
2871
  await closePool();
2456
2872
  }
2457
2873
  });
2458
- 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) => {
2874
+ 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) => {
2459
2875
  const spinner = ora("Reading memories\u2026").start();
2460
2876
  try {
2877
+ const config = readProjectConfig();
2461
2878
  const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(readFileSync6(join6(process.cwd(), opts.file), "utf-8")) : readMemoryFile();
2462
2879
  let inserted = 0;
2463
2880
  let skipped = 0;
@@ -2472,6 +2889,7 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
2472
2889
  title: memory.title,
2473
2890
  content: memory.content,
2474
2891
  reason: memory.reason,
2892
+ context: memory.context,
2475
2893
  tags: memory.tags,
2476
2894
  embedding
2477
2895
  });
@@ -2479,6 +2897,9 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
2479
2897
  else skipped++;
2480
2898
  }
2481
2899
  spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
2900
+ if (inserted > 0) {
2901
+ await autoSyncGeneratedFiles(config, "import", opts.sync);
2902
+ }
2482
2903
  } catch (err) {
2483
2904
  spinner.fail(`Import failed: ${err.message}`);
2484
2905
  process.exit(1);
@@ -2486,15 +2907,24 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
2486
2907
  await closePool();
2487
2908
  }
2488
2909
  });
2489
- 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) => {
2910
+ 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) => {
2490
2911
  try {
2912
+ const config = readProjectConfig();
2913
+ const architectures = opts.arch ? opts.arch : opts.all ? void 0 : getCurrentListArchitectures(config);
2914
+ const projectName = opts.project ? opts.project : opts.all || opts.arch ? void 0 : config?.projectName;
2491
2915
  const memories = await listMemories({
2492
2916
  type: opts.type,
2493
2917
  scope: opts.scope,
2494
- architecture: opts.arch,
2918
+ architecture: architectures,
2919
+ projectName,
2920
+ includeGlobal: !opts.all,
2495
2921
  limit: parseInt(opts.limit, 10)
2496
2922
  });
2497
- printMemoryTable(memories);
2923
+ const title = opts.all ? "All memories" : `Current project memories${architectures ? ` (${Array.isArray(architectures) ? architectures.join(", ") : architectures})` : ""}`;
2924
+ printMemoryTable(memories, title);
2925
+ if (!opts.all) {
2926
+ console.log(chalk3.gray(" Showing current project context plus shared/global memories. Use --all for the full database.\n"));
2927
+ }
2498
2928
  } catch (err) {
2499
2929
  console.error(chalk3.red(`List failed: ${err.message}`));
2500
2930
  process.exit(1);
@@ -2502,14 +2932,16 @@ program.command("list").description("List memories from the local database").opt
2502
2932
  await closePool();
2503
2933
  }
2504
2934
  });
2505
- program.command("remove <id>").description("Remove a memory by ID").action(async (id) => {
2935
+ program.command("remove <id>").description("Remove a memory by ID").option("--no-sync", "Skip automatic agent file sync after removal").action(async (id, opts) => {
2506
2936
  try {
2937
+ const config = readProjectConfig();
2507
2938
  const deleted = await deleteMemory(parseInt(id, 10));
2508
2939
  if (!deleted) {
2509
2940
  console.log(chalk3.yellow(`No memory found with ID ${id}`));
2510
2941
  process.exit(1);
2511
2942
  }
2512
2943
  console.log(chalk3.green(`Removed memory ${id}`));
2944
+ await autoSyncGeneratedFiles(config, "remove", opts.sync);
2513
2945
  } catch (err) {
2514
2946
  console.error(chalk3.red(`Remove failed: ${err.message}`));
2515
2947
  process.exit(1);
@@ -2517,8 +2949,9 @@ program.command("remove <id>").description("Remove a memory by ID").action(async
2517
2949
  await closePool();
2518
2950
  }
2519
2951
  });
2520
- program.command("forget").description("Bulk-delete memories by tag, scope, type, or architecture").option("--tag <tag>", "Delete memories with this tag").option("--scope <scope>", "Filter by scope").option("--type <type>", "Filter by type").option("--arch <architecture>", "Filter by architecture").action(async (opts) => {
2952
+ 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) => {
2521
2953
  try {
2954
+ const config = readProjectConfig();
2522
2955
  const deleted = await deleteMemories({
2523
2956
  tag: opts.tag,
2524
2957
  scope: opts.scope,
@@ -2526,6 +2959,9 @@ program.command("forget").description("Bulk-delete memories by tag, scope, type,
2526
2959
  architecture: opts.arch
2527
2960
  });
2528
2961
  console.log(chalk3.green(`Deleted ${deleted} memories`));
2962
+ if (deleted > 0) {
2963
+ await autoSyncGeneratedFiles(config, "forget", opts.sync);
2964
+ }
2529
2965
  } catch (err) {
2530
2966
  console.error(chalk3.red(`Forget failed: ${err.message}`));
2531
2967
  process.exit(1);
@@ -2533,9 +2969,10 @@ program.command("forget").description("Bulk-delete memories by tag, scope, type,
2533
2969
  await closePool();
2534
2970
  }
2535
2971
  });
2536
- program.command("edit <id>").description("Edit a memory interactively").action(async (id) => {
2972
+ program.command("edit <id>").description("Edit a memory interactively").option("--no-sync", "Skip automatic agent file sync after editing").action(async (id, opts) => {
2537
2973
  const memoryId = parseInt(id, 10);
2538
2974
  try {
2975
+ const config = readProjectConfig();
2539
2976
  const existing = await getMemory(memoryId);
2540
2977
  if (!existing) {
2541
2978
  console.log(chalk3.yellow(`No memory found with ID ${id}`));
@@ -2546,6 +2983,10 @@ program.command("edit <id>").description("Edit a memory interactively").action(a
2546
2983
  const title = await input({ message: "Title?", default: existing.title ?? "" });
2547
2984
  const content = await input({ message: "Content?", default: existing.content });
2548
2985
  const reason = await input({ message: "Reason?", default: existing.reason ?? "" });
2986
+ const appliesTo = await input({ message: "Applies to? (comma-separated)", default: existing.context?.appliesTo?.join(",") ?? "" });
2987
+ const avoidWhen = await input({ message: "Avoid when? (comma-separated)", default: existing.context?.avoidWhen?.join(",") ?? "" });
2988
+ const examples = await input({ message: "Examples? (comma-separated)", default: existing.context?.examples?.join(",") ?? "" });
2989
+ const source = await input({ message: "Source?", default: existing.context?.source ?? "" });
2549
2990
  const tags = await input({ message: "Tags?", default: existing.tags.join(",") });
2550
2991
  const embedding = content === existing.content ? void 0 : await embed(content);
2551
2992
  await updateMemory(memoryId, {
@@ -2554,10 +2995,12 @@ program.command("edit <id>").description("Edit a memory interactively").action(a
2554
2995
  title: title || void 0,
2555
2996
  content,
2556
2997
  reason: reason || void 0,
2998
+ context: buildMemoryContext({ appliesTo, avoidWhen, example: examples, source }),
2557
2999
  tags: parseTags(tags),
2558
3000
  embedding
2559
3001
  });
2560
3002
  console.log(chalk3.green(`Updated memory ${id}`));
3003
+ await autoSyncGeneratedFiles(config, "edit", opts.sync);
2561
3004
  } catch (err) {
2562
3005
  console.error(chalk3.red(`Edit failed: ${err.message}`));
2563
3006
  process.exit(1);
@@ -2565,8 +3008,9 @@ program.command("edit <id>").description("Edit a memory interactively").action(a
2565
3008
  await closePool();
2566
3009
  }
2567
3010
  });
2568
- 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) => {
3011
+ 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) => {
2569
3012
  try {
3013
+ const config = readProjectConfig();
2570
3014
  if (opts.list) {
2571
3015
  printMemoryTable(await listMemories({ type: "ignore", limit: 1e3 }), "Ignored patterns");
2572
3016
  return;
@@ -2578,13 +3022,13 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
2578
3022
  process.exit(1);
2579
3023
  }
2580
3024
  console.log(chalk3.green(`Removed ignore pattern ${opts.remove}`));
3025
+ await autoSyncGeneratedFiles(config, "ignore remove", opts.sync);
2581
3026
  return;
2582
3027
  }
2583
3028
  if (!pattern) {
2584
3029
  console.error(chalk3.red("Provide a pattern, --list, or --remove <id>"));
2585
3030
  process.exit(1);
2586
3031
  }
2587
- const config = readProjectConfig();
2588
3032
  const embedding = await embed(pattern);
2589
3033
  await upsertMemory({
2590
3034
  type: "ignore",
@@ -2596,6 +3040,7 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
2596
3040
  embedding
2597
3041
  });
2598
3042
  console.log(chalk3.green(`Ignored pattern saved: "${pattern}"`));
3043
+ await autoSyncGeneratedFiles(config, "ignore", opts.sync);
2599
3044
  } catch (err) {
2600
3045
  console.error(chalk3.red(`Ignore failed: ${err.message}`));
2601
3046
  process.exit(1);
@@ -2673,7 +3118,6 @@ program.command("reset").description("Remove memory-core generated files and loc
2673
3118
  default: false
2674
3119
  });
2675
3120
  if (ok) {
2676
- const { getPool } = await import("./db-5X5LTUCB.js");
2677
3121
  await getPool().query("DROP TABLE IF EXISTS memories");
2678
3122
  await closePool();
2679
3123
  console.log(chalk3.yellow("Dropped memories table"));