@shahmilsaari/memory-core 0.2.14 → 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 CHANGED
@@ -169,6 +169,7 @@ For the full CLI reference, examples, and command behavior notes, see [COMMANDS.
169
169
 
170
170
  New setup-management commands:
171
171
  - `memory-core status` — show project name, provider/model, agents, hook state, generated files, and health checks
172
+ - `memory-core auto-sync` — show or change automatic agent file regeneration (`on`, `off`, or `status`)
172
173
  - `memory-core provider set <provider>` — switch code-checking provider without rerunning `init`
173
174
  - `memory-core model set <model>` — update chat or embedding model from the CLI
174
175
  - `memory-core model doctor` — verify database, Ollama, model installation, and cloud API key presence
@@ -189,7 +190,7 @@ Walks you through:
189
190
  - Hook mode — **advisory** (logs violations, never blocks) or **strict** (blocks commits)
190
191
  - Whether to enable caveman mode (optional token saver)
191
192
 
192
- Generates config files for every selected AI agent, saves your choices to `.memory-core.json`, and automatically adds all generated files to `.gitignore` under a `# memory-core generated files` block.
193
+ Generates config files for every selected AI agent, saves your choices to `.memory-core.json`, enables auto-sync by default, and automatically adds all generated files to `.gitignore` under a `# memory-core generated files` block.
193
194
 
194
195
  At the end, the banner shows live ✓/✗ status for PostgreSQL and Ollama so you know everything is working.
195
196
 
@@ -197,7 +198,7 @@ At the end, the banner shows live ✓/✗ status for PostgreSQL and Ollama so yo
197
198
 
198
199
  ### `sync` — Refresh all agent files
199
200
 
200
- After saving new memories, regenerate every agent file to include them.
201
+ Regenerate every agent file on demand. This still exists even though `remember`, `import`, `edit`, `remove`, `forget`, and `ignore` auto-sync by default after changing memories.
201
202
 
202
203
  ```bash
203
204
  npx @shahmilsaari/memory-core sync
@@ -209,6 +210,18 @@ Multi-selects which agents to sync (pre-checked from your `.memory-core.json` co
209
210
  3 updated, 11 already up to date
210
211
  ```
211
212
 
213
+ Use `--no-sync` on memory-changing commands when you want to save database changes without regenerating files immediately.
214
+
215
+ Manage the default:
216
+
217
+ ```bash
218
+ npx @shahmilsaari/memory-core auto-sync # show current setting
219
+ npx @shahmilsaari/memory-core auto-sync off # save only, sync manually later
220
+ npx @shahmilsaari/memory-core auto-sync on # restore default behavior
221
+ ```
222
+
223
+ Auto-sync failures are non-fatal. If regeneration cannot run, memory-core prints the reason and tells you to run `memory-core sync` manually.
224
+
212
225
  ---
213
226
 
214
227
  ### `remember` — Save a decision
@@ -358,12 +371,14 @@ npx @shahmilsaari/memory-core hook uninstall # remove the hook
358
371
  npx @shahmilsaari/memory-core list
359
372
  ```
360
373
 
361
- Shows all stored memories. Filter the results:
374
+ By default, shows memories relevant to the current project context: detected stack, current project name, plus shared/global memories. Use `--all` for the old database-wide view.
362
375
 
363
376
  ```bash
377
+ npx @shahmilsaari/memory-core list --all
364
378
  npx @shahmilsaari/memory-core list --type rule
365
379
  npx @shahmilsaari/memory-core list --scope global
366
380
  npx @shahmilsaari/memory-core list --arch clean-architecture
381
+ npx @shahmilsaari/memory-core list --project my-api
367
382
  npx @shahmilsaari/memory-core list --limit 50
368
383
  ```
369
384
 
@@ -372,6 +387,9 @@ npx @shahmilsaari/memory-core list --limit 50
372
387
  | `--type <type>` | Filter by type: `decision` `rule` `pattern` `note` |
373
388
  | `--scope <scope>` | Filter by scope: `global` `project` |
374
389
  | `--arch <architecture>` | Filter by architecture profile |
390
+ | `--project <name>` | Filter by project name |
391
+ | `--all` | Show the full shared database |
392
+ | `--current` | Explicitly use the current project context |
375
393
  | `--limit <n>` | Max results (default 200) |
376
394
 
377
395
  ---
@@ -568,7 +586,7 @@ npx @shahmilsaari/memory-core seed # load 281 best-practice rules
568
586
  npx @shahmilsaari/memory-core remember "All auth goes through middleware, never in controllers" \
569
587
  --type decision --scope global
570
588
 
571
- # Refresh agent files after saving new memories
589
+ # Optional: refresh agent files manually anytime
572
590
  npx @shahmilsaari/memory-core sync
573
591
 
574
592
  # Not sure how something was decided? Search.
@@ -656,6 +674,24 @@ Save an ignore pattern: `npx @shahmilsaari/memory-core ignore "your exception he
656
674
 
657
675
  ---
658
676
 
677
+ ## Release checks
678
+
679
+ Before publishing, run the same checks as CI:
680
+
681
+ ```bash
682
+ npm run lint
683
+ npm run typecheck
684
+ npm test
685
+ npm run build
686
+ npm run smoke:pack
687
+ npm run smoke:npx
688
+ ```
689
+
690
+ `smoke:pack` verifies the npm tarball includes the built CLI plus templates/profiles.
691
+ `smoke:npx` installs that tarball into a fresh temporary project and runs `npx --no-install memory-core init --quick`.
692
+
693
+ ---
694
+
659
695
  ## Roadmap
660
696
 
661
697
  | | Feature |
@@ -669,6 +705,7 @@ Save an ignore pattern: `npx @shahmilsaari/memory-core ignore "your exception he
669
705
  | ✓ | CI/CD — `ci-setup` generates GitHub Actions workflow for PR enforcement |
670
706
  | ✓ | Violation stats — see which rules fire most and which files break most |
671
707
  | ✓ | Agent selection — choose which agents to generate files for during init |
708
+ | ✓ | Auto-sync — memory-changing commands refresh selected agent files by default |
672
709
  | ✓ | Export / import — portable memories.json for version control and team sharing |
673
710
  | ✓ | List / remove / edit — full CRUD for stored memories |
674
711
  | ✓ | False positive tagging — `ignore` command saves exceptions for hook and watcher |
@@ -84,12 +84,16 @@ async function listMemories(filters = {}) {
84
84
  if (filters.architecture) {
85
85
  if (Array.isArray(filters.architecture)) {
86
86
  params.push(filters.architecture);
87
- where.push(`architecture = ANY($${params.length})`);
87
+ where.push(filters.includeGlobal ? `(architecture IS NULL OR architecture = ANY($${params.length}))` : `architecture = ANY($${params.length})`);
88
88
  } else {
89
89
  params.push(filters.architecture);
90
- where.push(`architecture = $${params.length}`);
90
+ where.push(filters.includeGlobal ? `(architecture IS NULL OR architecture = $${params.length})` : `architecture = $${params.length}`);
91
91
  }
92
92
  }
93
+ if (filters.projectName) {
94
+ params.push(filters.projectName);
95
+ where.push(filters.includeGlobal ? `(project_name IS NULL OR project_name = $${params.length})` : `project_name = $${params.length}`);
96
+ }
93
97
  if (filters.tags?.length) {
94
98
  params.push(filters.tags);
95
99
  where.push(`tags && $${params.length}::text[]`);
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-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 dedupeMemories(memories);
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 = dedupeMemories(options.memories);
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;
@@ -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-5X5LTUCB.js");
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-5X5LTUCB.js");
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 = JSON.parse(raw);
1027
- if (Array.isArray(parsed?.violations)) return parsed.violations;
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
- let violations = [];
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
- 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 = [];
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
- 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."));
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
- return;
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
- violations = await verifyViolations(diff, violations, allowPatterns, options.debug ?? false);
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"));
@@ -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-5X5LTUCB.js");
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,7 @@ 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"];
1700
1957
  var DEFAULT_OLLAMA_URL = "http://localhost:11434";
1701
1958
  var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
1702
1959
  var DEFAULT_CHAT_MODEL = "llama3.2";
@@ -1772,6 +2029,22 @@ function ensureEnvFileIgnored(envPath = getEnvPath()) {
1772
2029
  `);
1773
2030
  }
1774
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
+ }
1775
2048
  function normalizeProvider(value) {
1776
2049
  const provider2 = value.trim().toLowerCase();
1777
2050
  if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax") {
@@ -1877,6 +2150,9 @@ function printMemoryTable(memories, title = "Rules in memory") {
1877
2150
  });
1878
2151
  console.log(chalk3.gray("\n Use: memory-core remove <id> | memory-core edit <id>\n"));
1879
2152
  }
2153
+ function getCurrentListArchitectures(config) {
2154
+ return inferProjectArchitectures(process.cwd(), config).filter((architecture) => architecture !== "global");
2155
+ }
1880
2156
  function printStatusLine(label, value) {
1881
2157
  console.log(` ${chalk3.dim(label.padEnd(18))} ${value}`);
1882
2158
  }
@@ -1974,6 +2250,7 @@ async function printProjectStatus() {
1974
2250
  printStatusLine("Architectures", architectures.length ? architectures.join(", ") : chalk3.gray("none detected"));
1975
2251
  printStatusLine("Agents", config?.agents?.length ? `${config.agents.length} selected` : chalk3.gray("none saved"));
1976
2252
  printStatusLine("Caveman", config?.caveman?.enabled ? `enabled (${config.caveman.intensity})` : "disabled");
2253
+ printStatusLine("Auto sync", config?.autoSync === false ? "disabled" : "enabled");
1977
2254
  printStatusLine("Allow patterns", String(getAllowPatterns(config).length));
1978
2255
  printStatusLine("Env file", `${existsSync6(envPath) ? "present" : "missing"} (${envPath.split("/").pop()})`);
1979
2256
  printStatusLine("Memory file", existsSync6(memoryFilePath) ? MEMORY_FILE : chalk3.gray("not exported"));
@@ -2001,6 +2278,94 @@ async function printProjectStatus() {
2001
2278
  }
2002
2279
  console.log();
2003
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
+ }
2004
2369
  var program = new Command();
2005
2370
  program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version(version);
2006
2371
  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 +2577,20 @@ program.command("init").description("Initialize memory-core in the current proje
2212
2577
  message: "Pull relevant memories from previous projects?",
2213
2578
  default: true
2214
2579
  });
2215
- const installCaveman = quick ? false : await confirm({
2216
- message: "Install caveman token saver? (~65-75% fewer tokens)",
2580
+ let installCaveman = quick ? false : await confirm({
2581
+ message: "Install caveman token saver? Downloads and runs the upstream installer.",
2217
2582
  default: false
2218
2583
  });
2219
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
+ }
2220
2594
  if (installCaveman) {
2221
2595
  cavemanIntensity = await select({
2222
2596
  message: "Caveman intensity?",
@@ -2258,20 +2632,23 @@ program.command("init").description("Initialize memory-core in the current proje
2258
2632
  language,
2259
2633
  caveman: { enabled: installCaveman, intensity: cavemanIntensity },
2260
2634
  agents: selectedAgents,
2261
- allowPatterns: []
2635
+ allowPatterns: [],
2636
+ autoSync: true
2262
2637
  };
2263
2638
  let memories = [];
2264
2639
  if (pullMemories) {
2265
2640
  const spinner2 = ora("Retrieving relevant memories\u2026").start();
2266
2641
  try {
2267
2642
  const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
2268
- memories = await retrieveContextualMemories({
2643
+ const selection = await retrieveMemorySelection({
2269
2644
  query: archQuery,
2270
2645
  cwd: process.cwd(),
2271
2646
  config,
2272
2647
  limit: 20
2273
2648
  });
2649
+ memories = selection.included;
2274
2650
  spinner2.succeed(`Found ${memories.length} relevant memories`);
2651
+ printMemorySelection(selection);
2275
2652
  } catch (err) {
2276
2653
  spinner2.warn(`Could not retrieve memories: ${err.message}`);
2277
2654
  }
@@ -2279,10 +2656,7 @@ program.command("init").description("Initialize memory-core in the current proje
2279
2656
  if (installCaveman) {
2280
2657
  const spinner2 = ora("Installing caveman token saver\u2026").start();
2281
2658
  try {
2282
- execSync2(
2283
- "curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash",
2284
- { stdio: "pipe", cwd: process.cwd() }
2285
- );
2659
+ await installCavemanTokenSaver();
2286
2660
  spinner2.succeed("Caveman installed");
2287
2661
  } catch (err) {
2288
2662
  spinner2.warn(`Caveman install failed: ${err.message}`);
@@ -2296,15 +2670,11 @@ program.command("init").description("Initialize memory-core in the current proje
2296
2670
  );
2297
2671
  writeProjectConfig(config);
2298
2672
  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`));
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`));
2308
2678
  }
2309
2679
  }
2310
2680
  if (enableHook) {
@@ -2339,44 +2709,32 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
2339
2709
  console.log(chalk3.yellow(" No agents selected \u2014 nothing to sync."));
2340
2710
  process.exit(0);
2341
2711
  }
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}`);
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);
2355
2720
  }
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}`)));
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;
2376
2727
  }
2377
- await closePool();
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"));
2378
2736
  });
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) => {
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) => {
2380
2738
  const config = readProjectConfig();
2381
2739
  let reason = opts.reason;
2382
2740
  if (!reason) {
@@ -2401,6 +2759,7 @@ program.command("remember <text>").description("Save a new memory to the central
2401
2759
  const reasonLine = reason ? chalk3.gray(`
2402
2760
  Why: ${reason}`) : "";
2403
2761
  spinner.succeed(chalk3.green(`Memory saved: "${text}"`) + reasonLine);
2762
+ await autoSyncGeneratedFiles(config, "remember", opts.sync);
2404
2763
  } catch (err) {
2405
2764
  spinner.fail(`Failed: ${err.message}`);
2406
2765
  process.exit(1);
@@ -2455,9 +2814,10 @@ program.command("export").description(`Export DB memories to ${MEMORY_FILE}`).op
2455
2814
  await closePool();
2456
2815
  }
2457
2816
  });
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) => {
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) => {
2459
2818
  const spinner = ora("Reading memories\u2026").start();
2460
2819
  try {
2820
+ const config = readProjectConfig();
2461
2821
  const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(readFileSync6(join6(process.cwd(), opts.file), "utf-8")) : readMemoryFile();
2462
2822
  let inserted = 0;
2463
2823
  let skipped = 0;
@@ -2479,6 +2839,9 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
2479
2839
  else skipped++;
2480
2840
  }
2481
2841
  spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
2842
+ if (inserted > 0) {
2843
+ await autoSyncGeneratedFiles(config, "import", opts.sync);
2844
+ }
2482
2845
  } catch (err) {
2483
2846
  spinner.fail(`Import failed: ${err.message}`);
2484
2847
  process.exit(1);
@@ -2486,15 +2849,24 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
2486
2849
  await closePool();
2487
2850
  }
2488
2851
  });
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) => {
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) => {
2490
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;
2491
2857
  const memories = await listMemories({
2492
2858
  type: opts.type,
2493
2859
  scope: opts.scope,
2494
- architecture: opts.arch,
2860
+ architecture: architectures,
2861
+ projectName,
2862
+ includeGlobal: !opts.all,
2495
2863
  limit: parseInt(opts.limit, 10)
2496
2864
  });
2497
- printMemoryTable(memories);
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
+ }
2498
2870
  } catch (err) {
2499
2871
  console.error(chalk3.red(`List failed: ${err.message}`));
2500
2872
  process.exit(1);
@@ -2502,14 +2874,16 @@ program.command("list").description("List memories from the local database").opt
2502
2874
  await closePool();
2503
2875
  }
2504
2876
  });
2505
- 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) => {
2506
2878
  try {
2879
+ const config = readProjectConfig();
2507
2880
  const deleted = await deleteMemory(parseInt(id, 10));
2508
2881
  if (!deleted) {
2509
2882
  console.log(chalk3.yellow(`No memory found with ID ${id}`));
2510
2883
  process.exit(1);
2511
2884
  }
2512
2885
  console.log(chalk3.green(`Removed memory ${id}`));
2886
+ await autoSyncGeneratedFiles(config, "remove", opts.sync);
2513
2887
  } catch (err) {
2514
2888
  console.error(chalk3.red(`Remove failed: ${err.message}`));
2515
2889
  process.exit(1);
@@ -2517,8 +2891,9 @@ program.command("remove <id>").description("Remove a memory by ID").action(async
2517
2891
  await closePool();
2518
2892
  }
2519
2893
  });
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) => {
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) => {
2521
2895
  try {
2896
+ const config = readProjectConfig();
2522
2897
  const deleted = await deleteMemories({
2523
2898
  tag: opts.tag,
2524
2899
  scope: opts.scope,
@@ -2526,6 +2901,9 @@ program.command("forget").description("Bulk-delete memories by tag, scope, type,
2526
2901
  architecture: opts.arch
2527
2902
  });
2528
2903
  console.log(chalk3.green(`Deleted ${deleted} memories`));
2904
+ if (deleted > 0) {
2905
+ await autoSyncGeneratedFiles(config, "forget", opts.sync);
2906
+ }
2529
2907
  } catch (err) {
2530
2908
  console.error(chalk3.red(`Forget failed: ${err.message}`));
2531
2909
  process.exit(1);
@@ -2533,9 +2911,10 @@ program.command("forget").description("Bulk-delete memories by tag, scope, type,
2533
2911
  await closePool();
2534
2912
  }
2535
2913
  });
2536
- 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) => {
2537
2915
  const memoryId = parseInt(id, 10);
2538
2916
  try {
2917
+ const config = readProjectConfig();
2539
2918
  const existing = await getMemory(memoryId);
2540
2919
  if (!existing) {
2541
2920
  console.log(chalk3.yellow(`No memory found with ID ${id}`));
@@ -2558,6 +2937,7 @@ program.command("edit <id>").description("Edit a memory interactively").action(a
2558
2937
  embedding
2559
2938
  });
2560
2939
  console.log(chalk3.green(`Updated memory ${id}`));
2940
+ await autoSyncGeneratedFiles(config, "edit", opts.sync);
2561
2941
  } catch (err) {
2562
2942
  console.error(chalk3.red(`Edit failed: ${err.message}`));
2563
2943
  process.exit(1);
@@ -2565,8 +2945,9 @@ program.command("edit <id>").description("Edit a memory interactively").action(a
2565
2945
  await closePool();
2566
2946
  }
2567
2947
  });
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) => {
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) => {
2569
2949
  try {
2950
+ const config = readProjectConfig();
2570
2951
  if (opts.list) {
2571
2952
  printMemoryTable(await listMemories({ type: "ignore", limit: 1e3 }), "Ignored patterns");
2572
2953
  return;
@@ -2578,13 +2959,13 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
2578
2959
  process.exit(1);
2579
2960
  }
2580
2961
  console.log(chalk3.green(`Removed ignore pattern ${opts.remove}`));
2962
+ await autoSyncGeneratedFiles(config, "ignore remove", opts.sync);
2581
2963
  return;
2582
2964
  }
2583
2965
  if (!pattern) {
2584
2966
  console.error(chalk3.red("Provide a pattern, --list, or --remove <id>"));
2585
2967
  process.exit(1);
2586
2968
  }
2587
- const config = readProjectConfig();
2588
2969
  const embedding = await embed(pattern);
2589
2970
  await upsertMemory({
2590
2971
  type: "ignore",
@@ -2596,6 +2977,7 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
2596
2977
  embedding
2597
2978
  });
2598
2979
  console.log(chalk3.green(`Ignored pattern saved: "${pattern}"`));
2980
+ await autoSyncGeneratedFiles(config, "ignore", opts.sync);
2599
2981
  } catch (err) {
2600
2982
  console.error(chalk3.red(`Ignore failed: ${err.message}`));
2601
2983
  process.exit(1);
@@ -2673,7 +3055,6 @@ program.command("reset").description("Remove memory-core generated files and loc
2673
3055
  default: false
2674
3056
  });
2675
3057
  if (ok) {
2676
- const { getPool } = await import("./db-5X5LTUCB.js");
2677
3058
  await getPool().query("DROP TABLE IF EXISTS memories");
2678
3059
  await closePool();
2679
3060
  console.log(chalk3.yellow("Dropped memories table"));
@@ -12,7 +12,7 @@ import {
12
12
  searchMemories,
13
13
  updateMemory,
14
14
  upsertMemory
15
- } from "./chunk-25Y2KI7M.js";
15
+ } from "./chunk-DUUQHRIB.js";
16
16
  import "./chunk-KSLFLWB4.js";
17
17
  export {
18
18
  closePool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shahmilsaari/memory-core",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "Universal AI memory core — generate AI context files from architecture profiles with RAG support",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,9 @@
14
14
  "scripts": {
15
15
  "build": "tsup",
16
16
  "typecheck": "tsc --noEmit",
17
+ "lint": "node scripts/lint.mjs",
18
+ "smoke:pack": "node scripts/pack-smoke.mjs",
19
+ "smoke:npx": "node scripts/npx-init-smoke.mjs",
17
20
  "dev": "tsx src/cli.ts",
18
21
  "start": "node dist/cli.js",
19
22
  "test": "node --import tsx --test test/**/*.test.ts"
@@ -1,11 +1,10 @@
1
1
  # AGENTS.md — {{projectName}}
2
- <!-- Generated by memory-core on {{generatedAt}} — run: memory-core sync -->
2
+ <!-- Generated by memory-core on {{generatedAt}} — auto-sync keeps this fresh; manual refresh: memory-core sync -->
3
3
 
4
4
  **Type:** {{projectType}} | **Language:** {{language}}
5
5
  {{#if hasBackend}}**Backend:** {{backendArchitecture}}{{/if}}
6
6
  {{#if hasFrontend}}**Frontend:** {{frontendFramework}}{{/if}}
7
7
 
8
- ---
9
8
  {{#if hasBackend}}
10
9
  ## Backend Rules — {{backendArchitecture}}
11
10
  {{bullet backendRules}}
@@ -30,7 +29,7 @@
30
29
  {{#if hasMemories}}
31
30
 
32
31
  ---
33
- ## Memory from Previous Projects
32
+ ## Relevant Memory from Previous Projects
34
33
  {{#each memories}}
35
34
  - [{{type}}{{#if this.architecture}} · {{this.architecture}}{{/if}}] {{content}}
36
35
  {{/each}}
@@ -7,7 +7,6 @@ These rules apply to ALL AI agents in this project.
7
7
  {{#if hasBackend}}**Backend:** {{backendArchitecture}}{{/if}}
8
8
  {{#if hasFrontend}}**Frontend:** {{frontendFramework}}{{/if}}
9
9
 
10
- ---
11
10
  {{#if hasBackend}}
12
11
  ## Backend — {{backendArchitecture}}
13
12
  {{numbered backendRules}}
@@ -26,7 +25,7 @@ These rules apply to ALL AI agents in this project.
26
25
  {{#if hasMemories}}
27
26
 
28
27
  ---
29
- ## Inherited Decisions
28
+ ## Relevant Inherited Decisions
30
29
  {{#each memories}}
31
30
  {{@index}}. **[{{type}}]** {{content}}
32
31
  {{#if tags.length}}_tags: {{join tags ", "}}_{{/if}}
@@ -1,5 +1,5 @@
1
1
  # CLAUDE.md
2
- <!-- Generated by memory-core on {{generatedAt}} — run: memory-core sync to refresh -->
2
+ <!-- Generated by memory-core on {{generatedAt}} — auto-sync keeps this fresh; manual refresh: memory-core sync -->
3
3
 
4
4
  ## Project
5
5
  **Name:** {{projectName}}
@@ -1,5 +1,5 @@
1
1
  # Project Memory — {{projectName}}
2
- <!-- Generated by memory-core on {{generatedAt}} — run: memory-core sync to refresh -->
2
+ <!-- Generated by memory-core on {{generatedAt}} — auto-sync keeps this fresh; manual refresh: memory-core sync -->
3
3
 
4
4
  Architecture: {{architecture}}
5
5
  Stack: {{language}}
@@ -28,4 +28,4 @@ memory-core remember "Your architectural decision here"
28
28
  ---
29
29
  _Add memories: `memory-core remember "..."`_
30
30
  _Search memories: `memory-core search "..."`_
31
- _Refresh: `memory-core sync`_
31
+ _Auto-sync is enabled by default after memory changes. Manual refresh: `memory-core sync`._
@@ -1,5 +1,5 @@
1
1
  # Copilot Instructions
2
- <!-- Generated by memory-core on {{generatedAt}} — run: memory-core sync to refresh -->
2
+ <!-- Generated by memory-core on {{generatedAt}} — auto-sync keeps this fresh; manual refresh: memory-core sync -->
3
3
 
4
4
  Project: **{{projectName}}** · Type: **{{projectType}}** · Language: **{{language}}**
5
5
  {{#if hasBackend}}Backend: {{backendArchitecture}}{{/if}}{{#if hasFrontend}} | Frontend: {{frontendFramework}}{{/if}}
@@ -1,5 +1,5 @@
1
1
  # Cursor Rules — {{projectName}}
2
- # Generated by memory-core on {{generatedAt}} — run: memory-core sync
2
+ # Generated by memory-core on {{generatedAt}} — auto-sync keeps this fresh; manual refresh: memory-core sync
3
3
 
4
4
  Type: {{projectType}} | Language: {{language}}
5
5
  {{#if hasBackend}}Backend: {{backendArchitecture}}{{/if}}{{#if hasFrontend}} | Frontend: {{frontendFramework}}{{/if}}