@shahmilsaari/memory-core 0.2.13 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -711,11 +807,11 @@ import chalk from "chalk";
711
807
 
712
808
  // src/chat.ts
713
809
  function getChatConfig() {
714
- const provider = process.env.CHAT_PROVIDER ?? "ollama";
715
- const model = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
810
+ const provider2 = process.env.CHAT_PROVIDER ?? "ollama";
811
+ const model2 = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
716
812
  return {
717
- provider,
718
- model,
813
+ provider: provider2,
814
+ model: model2,
719
815
  ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
720
816
  apiKey: process.env.CHAT_API_KEY ?? ""
721
817
  };
@@ -922,7 +1018,7 @@ async function promptToSaveViolations(violations) {
922
1018
  default: selected.reason ?? selected.issue ?? ""
923
1019
  });
924
1020
  const { embed: embed2 } = await import("./embedding-PAYD2JYW.js");
925
- const { upsertMemory: upsertMemory2 } = await import("./db-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"));
@@ -1301,10 +1508,10 @@ async function checkCi(options = {}) {
1301
1508
  recordViolations(violations);
1302
1509
  process.exit(1);
1303
1510
  }
1304
- function printModelMissing(model) {
1511
+ function printModelMissing(model2) {
1305
1512
  console.log(chalk.yellow(`
1306
- \u26A0 Chat model "${model}" not found in Ollama.`));
1307
- console.log(chalk.gray(` Pull a model: ollama pull ${model}`));
1513
+ \u26A0 Chat model "${model2}" not found in Ollama.`));
1514
+ console.log(chalk.gray(` Pull a model: ollama pull ${model2}`));
1308
1515
  console.log(chalk.gray(" Or set OLLAMA_CHAT_MODEL=<model> in .env"));
1309
1516
  console.log(chalk.gray(" Recommended: llama3.2 | qwen2.5-coder:3b | mistral\n"));
1310
1517
  }
@@ -1454,7 +1661,7 @@ ${JSON.stringify(violations, null, 2)}`;
1454
1661
  }
1455
1662
  async function loadIgnorePatterns2() {
1456
1663
  try {
1457
- const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-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,156 @@ async function checkConnections(dbUrl, ollamaUrl, chatModel) {
1697
1953
  }
1698
1954
  var { version } = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf-8"));
1699
1955
  var CONFIG_FILE = ".memory-core.json";
1956
+ var LOCAL_GENERATED_FILES = [".memory-core-stats.json"];
1957
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434";
1958
+ var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
1959
+ var DEFAULT_CHAT_MODEL = "llama3.2";
1960
+ function getEnvPath() {
1961
+ const memoryEnv = join6(process.cwd(), ".memory-core.env");
1962
+ if (existsSync6(memoryEnv)) return memoryEnv;
1963
+ const dotEnv = join6(process.cwd(), ".env");
1964
+ return existsSync6(dotEnv) ? dotEnv : memoryEnv;
1965
+ }
1966
+ function parseEnvFile(raw) {
1967
+ const lines = raw.split(/\r?\n/);
1968
+ const values = {};
1969
+ for (const line of lines) {
1970
+ const trimmed = line.trim();
1971
+ if (!trimmed || trimmed.startsWith("#")) continue;
1972
+ const separatorIndex = trimmed.indexOf("=");
1973
+ if (separatorIndex === -1) continue;
1974
+ const key = trimmed.slice(0, separatorIndex).trim();
1975
+ const value = trimmed.slice(separatorIndex + 1).trim();
1976
+ if (key) values[key] = value;
1977
+ }
1978
+ return values;
1979
+ }
1980
+ function readRuntimeEnv() {
1981
+ const envPath = getEnvPath();
1982
+ const fileValues = existsSync6(envPath) ? parseEnvFile(readFileSync6(envPath, "utf-8")) : {};
1983
+ const values = {
1984
+ ...fileValues
1985
+ };
1986
+ for (const [key, value] of Object.entries(process.env)) {
1987
+ if (typeof value === "string" && value !== "") values[key] = value;
1988
+ }
1989
+ return { envPath, values };
1990
+ }
1991
+ function writeRuntimeEnv(values, envPath = getEnvPath()) {
1992
+ const orderedKeys = [
1993
+ "DATABASE_URL",
1994
+ "OLLAMA_URL",
1995
+ "OLLAMA_MODEL",
1996
+ "CHAT_PROVIDER",
1997
+ "CHAT_MODEL",
1998
+ "OLLAMA_CHAT_MODEL",
1999
+ "CHAT_API_KEY"
2000
+ ];
2001
+ const seen = /* @__PURE__ */ new Set();
2002
+ const lines = [];
2003
+ for (const key of orderedKeys) {
2004
+ const value = values[key];
2005
+ if (value) {
2006
+ lines.push(`${key}=${value}`);
2007
+ seen.add(key);
2008
+ }
2009
+ }
2010
+ for (const key of Object.keys(values).sort()) {
2011
+ if (seen.has(key)) continue;
2012
+ const value = values[key];
2013
+ if (value) lines.push(`${key}=${value}`);
2014
+ }
2015
+ writeFileSync5(envPath, `${lines.join("\n")}
2016
+ `, "utf-8");
2017
+ }
2018
+ function applyRuntimeEnv(values) {
2019
+ for (const [key, value] of Object.entries(values)) {
2020
+ process.env[key] = value;
2021
+ }
2022
+ }
2023
+ function ensureEnvFileIgnored(envPath = getEnvPath()) {
2024
+ const envFileName = envPath.split("/").pop() ?? ".memory-core.env";
2025
+ const gitignorePath = join6(process.cwd(), ".gitignore");
2026
+ const existing = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
2027
+ if (!existing.includes(envFileName)) {
2028
+ appendFileSync(gitignorePath, `${existing ? "\n" : ""}${envFileName}
2029
+ `);
2030
+ }
2031
+ }
2032
+ function appendMissingGitignoreEntries(entries, heading) {
2033
+ const gitignorePath = join6(process.cwd(), ".gitignore");
2034
+ const existing = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
2035
+ const existingEntries = new Set(
2036
+ existing.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
2037
+ );
2038
+ const toAdd = entries.filter((entry) => !existingEntries.has(entry));
2039
+ if (toAdd.length === 0) {
2040
+ return 0;
2041
+ }
2042
+ const prefix = existing.trim().length > 0 ? "\n" : "";
2043
+ appendFileSync(gitignorePath, `${prefix}${heading}
2044
+ ${toAdd.join("\n")}
2045
+ `);
2046
+ return toAdd.length;
2047
+ }
2048
+ function normalizeProvider(value) {
2049
+ const provider2 = value.trim().toLowerCase();
2050
+ if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax") {
2051
+ return provider2;
2052
+ }
2053
+ throw new Error(`Unsupported provider "${value}". Use: ollama, openai, anthropic, minimax`);
2054
+ }
2055
+ function providerLabel(provider2) {
2056
+ switch (provider2) {
2057
+ case "openai":
2058
+ return "OpenAI";
2059
+ case "anthropic":
2060
+ return "Anthropic";
2061
+ case "minimax":
2062
+ return "MiniMax";
2063
+ default:
2064
+ return "Ollama";
2065
+ }
2066
+ }
2067
+ function redactDatabaseUrl(url) {
2068
+ if (!url) return "(not set)";
2069
+ return url.replace(/:\/\/([^:@/]+)(?::[^@/]+)?@/, "://$1:***@");
2070
+ }
2071
+ function getConfiguredProvider(values) {
2072
+ return normalizeProvider(values.CHAT_PROVIDER ?? "ollama");
2073
+ }
2074
+ function getConfiguredChatModel(values) {
2075
+ return values.CHAT_MODEL ?? values.OLLAMA_CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
2076
+ }
2077
+ async function resolveOllamaInstalledModel(ollamaUrl, model2) {
2078
+ const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
2079
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2080
+ const data = await res.json();
2081
+ const models = data.models ?? [];
2082
+ const exact = models.find((entry) => entry.name === model2);
2083
+ const prefixed = models.find((entry) => entry.name.startsWith(`${model2}:`));
2084
+ return (exact ?? prefixed)?.name ?? null;
2085
+ }
2086
+ async function verifyDatabaseConnection(dbUrl) {
2087
+ if (!dbUrl) return "DATABASE_URL is not set";
2088
+ try {
2089
+ const { Pool } = (await import("pg")).default;
2090
+ const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
2091
+ await testPool.query("SELECT 1");
2092
+ await testPool.end();
2093
+ return null;
2094
+ } catch (err) {
2095
+ return err.message;
2096
+ }
2097
+ }
2098
+ async function verifyOllamaConnection(ollamaUrl) {
2099
+ try {
2100
+ const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
2101
+ return res.ok ? null : `HTTP ${res.status}`;
2102
+ } catch (err) {
2103
+ return err.message;
2104
+ }
2105
+ }
1700
2106
  function readProjectConfig() {
1701
2107
  const path = join6(process.cwd(), CONFIG_FILE);
1702
2108
  if (!existsSync6(path)) return null;
@@ -1744,6 +2150,222 @@ function printMemoryTable(memories, title = "Rules in memory") {
1744
2150
  });
1745
2151
  console.log(chalk3.gray("\n Use: memory-core remove <id> | memory-core edit <id>\n"));
1746
2152
  }
2153
+ function getCurrentListArchitectures(config) {
2154
+ return inferProjectArchitectures(process.cwd(), config).filter((architecture) => architecture !== "global");
2155
+ }
2156
+ function printStatusLine(label, value) {
2157
+ console.log(` ${chalk3.dim(label.padEnd(18))} ${value}`);
2158
+ }
2159
+ async function runModelDoctor() {
2160
+ const { envPath, values } = readRuntimeEnv();
2161
+ const provider2 = getConfiguredProvider(values);
2162
+ const model2 = getConfiguredChatModel(values);
2163
+ const ollamaUrl = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
2164
+ const embeddingModel = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
2165
+ const dbUrl = values.DATABASE_URL ?? "";
2166
+ console.log(chalk3.bold("\n memory-core model doctor\n"));
2167
+ printStatusLine("Env file", existsSync6(envPath) ? envPath : `${envPath} ${chalk3.yellow("(will be created on first write)")}`);
2168
+ printStatusLine("Provider", provider2);
2169
+ printStatusLine("Chat model", model2);
2170
+ printStatusLine("Embedding model", embeddingModel);
2171
+ printStatusLine("Ollama URL", ollamaUrl);
2172
+ console.log();
2173
+ let ok = true;
2174
+ const dbError = await verifyDatabaseConnection(dbUrl);
2175
+ if (dbError) {
2176
+ ok = false;
2177
+ console.log(chalk3.red(" \u2717 PostgreSQL ") + chalk3.dim(dbError));
2178
+ } else {
2179
+ console.log(chalk3.green(" \u2713 PostgreSQL ") + chalk3.dim("connected"));
2180
+ }
2181
+ const ollamaError = await verifyOllamaConnection(ollamaUrl);
2182
+ if (ollamaError) {
2183
+ ok = false;
2184
+ console.log(chalk3.red(" \u2717 Ollama ") + chalk3.dim(ollamaError));
2185
+ } else {
2186
+ console.log(chalk3.green(" \u2713 Ollama ") + chalk3.dim("reachable"));
2187
+ }
2188
+ if (!ollamaError) {
2189
+ try {
2190
+ const installedEmbeddingModel = await resolveOllamaInstalledModel(ollamaUrl, embeddingModel);
2191
+ if (installedEmbeddingModel) {
2192
+ console.log(chalk3.green(" \u2713 Embedding ") + chalk3.dim(`${installedEmbeddingModel} installed`));
2193
+ } else {
2194
+ ok = false;
2195
+ console.log(chalk3.red(" \u2717 Embedding ") + chalk3.dim(`${embeddingModel} not installed in Ollama`));
2196
+ }
2197
+ } catch (err) {
2198
+ ok = false;
2199
+ console.log(chalk3.red(" \u2717 Embedding ") + chalk3.dim(err.message));
2200
+ }
2201
+ }
2202
+ if (provider2 === "ollama") {
2203
+ if (ollamaError) {
2204
+ ok = false;
2205
+ console.log(chalk3.red(" \u2717 Chat model ") + chalk3.dim("cannot verify while Ollama is unreachable"));
2206
+ } else {
2207
+ try {
2208
+ const installedChatModel = await resolveOllamaInstalledModel(ollamaUrl, model2);
2209
+ if (installedChatModel) {
2210
+ console.log(chalk3.green(" \u2713 Chat model ") + chalk3.dim(`${installedChatModel} installed`));
2211
+ } else {
2212
+ ok = false;
2213
+ console.log(chalk3.red(" \u2717 Chat model ") + chalk3.dim(`${model2} not installed in Ollama`));
2214
+ }
2215
+ } catch (err) {
2216
+ ok = false;
2217
+ console.log(chalk3.red(" \u2717 Chat model ") + chalk3.dim(err.message));
2218
+ }
2219
+ }
2220
+ } else {
2221
+ if (!values.CHAT_API_KEY) {
2222
+ ok = false;
2223
+ console.log(chalk3.red(` \u2717 ${providerLabel(provider2)} API`) + chalk3.dim(" CHAT_API_KEY is missing"));
2224
+ } else {
2225
+ console.log(chalk3.green(` \u2713 ${providerLabel(provider2)} API`) + chalk3.dim(" key configured"));
2226
+ console.log(chalk3.gray(" Remote provider connectivity is not verified live by doctor."));
2227
+ }
2228
+ }
2229
+ console.log();
2230
+ return { ok, provider: provider2, model: model2 };
2231
+ }
2232
+ async function printProjectStatus() {
2233
+ const config = readProjectConfig();
2234
+ const { envPath, values } = readRuntimeEnv();
2235
+ const provider2 = getConfiguredProvider(values);
2236
+ const model2 = getConfiguredChatModel(values);
2237
+ const architectures = inferProjectArchitectures(process.cwd(), config);
2238
+ const generatedFiles = OUTPUT_FILES.map((entry) => entry.path).filter((relativePath) => existsSync6(join6(process.cwd(), relativePath)));
2239
+ const hookPath = join6(process.cwd(), ".git", "hooks", "pre-commit");
2240
+ const memoryFilePath = join6(process.cwd(), MEMORY_FILE);
2241
+ const statsPath = join6(process.cwd(), ".memory-core-stats.json");
2242
+ const dbError = await verifyDatabaseConnection(values.DATABASE_URL ?? "");
2243
+ const ollamaError = await verifyOllamaConnection(values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
2244
+ console.log(chalk3.bold("\n memory-core status\n"));
2245
+ printStatusLine("Project", config?.projectName ?? process.cwd().split("/").pop() ?? "unknown");
2246
+ printStatusLine("Project type", config?.projectType ?? chalk3.yellow("not initialized"));
2247
+ printStatusLine("Language", config?.language ?? detectProject().language);
2248
+ printStatusLine("Backend arch", config?.backendArchitecture ?? chalk3.gray("\u2014"));
2249
+ printStatusLine("Frontend fw", config?.frontendFramework ?? chalk3.gray("\u2014"));
2250
+ printStatusLine("Architectures", architectures.length ? architectures.join(", ") : chalk3.gray("none detected"));
2251
+ printStatusLine("Agents", config?.agents?.length ? `${config.agents.length} selected` : chalk3.gray("none saved"));
2252
+ printStatusLine("Caveman", config?.caveman?.enabled ? `enabled (${config.caveman.intensity})` : "disabled");
2253
+ printStatusLine("Auto sync", config?.autoSync === false ? "disabled" : "enabled");
2254
+ printStatusLine("Allow patterns", String(getAllowPatterns(config).length));
2255
+ printStatusLine("Env file", `${existsSync6(envPath) ? "present" : "missing"} (${envPath.split("/").pop()})`);
2256
+ printStatusLine("Memory file", existsSync6(memoryFilePath) ? MEMORY_FILE : chalk3.gray("not exported"));
2257
+ printStatusLine("Project config", existsSync6(join6(process.cwd(), CONFIG_FILE)) ? CONFIG_FILE : chalk3.gray("missing"));
2258
+ printStatusLine("Generated files", String(generatedFiles.length));
2259
+ printStatusLine("Hook", existsSync6(hookPath) ? "installed" : "not installed");
2260
+ printStatusLine("Stats file", existsSync6(statsPath) ? ".memory-core-stats.json" : chalk3.gray("none"));
2261
+ console.log();
2262
+ printStatusLine("Database URL", redactDatabaseUrl(values.DATABASE_URL ?? ""));
2263
+ printStatusLine("Ollama URL", values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
2264
+ printStatusLine("Embedding model", values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL);
2265
+ printStatusLine("Chat provider", provider2);
2266
+ printStatusLine("Chat model", model2);
2267
+ console.log();
2268
+ console.log(
2269
+ dbError ? chalk3.red(" \u2717 PostgreSQL ") + chalk3.dim(dbError) : chalk3.green(" \u2713 PostgreSQL ") + chalk3.dim("connected")
2270
+ );
2271
+ console.log(
2272
+ ollamaError ? chalk3.red(" \u2717 Ollama ") + chalk3.dim(ollamaError) : chalk3.green(" \u2713 Ollama ") + chalk3.dim("reachable")
2273
+ );
2274
+ if (provider2 !== "ollama") {
2275
+ console.log(
2276
+ values.CHAT_API_KEY ? chalk3.green(` \u2713 ${providerLabel(provider2)} API`) + chalk3.dim(" key configured") : chalk3.red(` \u2717 ${providerLabel(provider2)} API`) + chalk3.dim(" CHAT_API_KEY is missing")
2277
+ );
2278
+ }
2279
+ console.log();
2280
+ }
2281
+ function printMemorySelection(selection, limit = 4) {
2282
+ const active = selection.activeArchitectures.join(", ") || "none detected";
2283
+ console.log(chalk3.gray(` Stack filter: ${active}`));
2284
+ const included = selection.decisions.filter((decision) => decision.status === "included");
2285
+ if (included.length > 0) {
2286
+ console.log(chalk3.gray(` Included ${included.length}:`));
2287
+ for (const decision of included.slice(0, limit)) {
2288
+ console.log(chalk3.gray(` + ${decision.memory.content} (${decision.reason})`));
2289
+ }
2290
+ if (included.length > limit) {
2291
+ console.log(chalk3.gray(` \u2026 ${included.length - limit} more included`));
2292
+ }
2293
+ }
2294
+ if (selection.excluded.length > 0) {
2295
+ console.log(chalk3.gray(` Excluded ${selection.excluded.length}:`));
2296
+ for (const decision of selection.excluded.slice(0, limit)) {
2297
+ console.log(chalk3.gray(` - ${decision.memory.content} (${decision.reason})`));
2298
+ }
2299
+ if (selection.excluded.length > limit) {
2300
+ console.log(chalk3.gray(` \u2026 ${selection.excluded.length - limit} more excluded`));
2301
+ }
2302
+ }
2303
+ }
2304
+ function getConfiguredAgents(config) {
2305
+ const agents = config.agents?.length ? config.agents : AGENT_NAMES.filter((agent) => agent !== "Shared");
2306
+ return [.../* @__PURE__ */ new Set([...agents, "Shared"])];
2307
+ }
2308
+ async function syncGeneratedFiles(config, agents, options = {}) {
2309
+ const memorySpinner = ora(options.label ?? "Syncing memories\u2026").start();
2310
+ let memories = [];
2311
+ try {
2312
+ const archQuery = [config.backendArchitecture, config.frontendFramework, config.language].filter(Boolean).join(" ");
2313
+ const selection = await retrieveMemorySelection({
2314
+ query: archQuery,
2315
+ cwd: process.cwd(),
2316
+ config,
2317
+ limit: 25
2318
+ });
2319
+ memories = selection.included;
2320
+ memorySpinner.succeed(`Found ${memories.length} memories`);
2321
+ if (options.showSelection) {
2322
+ printMemorySelection(selection);
2323
+ }
2324
+ } catch (err) {
2325
+ memorySpinner.warn(`Could not retrieve memories: ${err.message}`);
2326
+ }
2327
+ const spinner = ora("Regenerating agent files\u2026").start();
2328
+ const result = await generate(
2329
+ {
2330
+ projectName: config.projectName,
2331
+ projectType: config.projectType,
2332
+ backendArchitecture: config.backendArchitecture,
2333
+ frontendFramework: config.frontendFramework,
2334
+ language: config.language,
2335
+ memories,
2336
+ caveman: config.caveman
2337
+ },
2338
+ process.cwd(),
2339
+ agents
2340
+ );
2341
+ spinner.succeed(
2342
+ `Synced \u2014 ${chalk3.green(`${result.written.length} updated`)}, ${chalk3.dim(`${result.skipped.length} already up to date`)}`
2343
+ );
2344
+ if (result.written.length > 0) {
2345
+ result.written.forEach((file) => console.log(chalk3.gray(` \u2713 ${file}`)));
2346
+ }
2347
+ }
2348
+ async function autoSyncGeneratedFiles(config, action, enabled = true) {
2349
+ if (!enabled) {
2350
+ console.log(chalk3.gray(" Auto-sync skipped (--no-sync). Run memory-core sync when ready."));
2351
+ return;
2352
+ }
2353
+ if (!config) {
2354
+ return;
2355
+ }
2356
+ if (config.autoSync === false) {
2357
+ console.log(chalk3.gray(" Auto-sync disabled for this project. Run memory-core sync when ready."));
2358
+ return;
2359
+ }
2360
+ try {
2361
+ await syncGeneratedFiles(config, getConfiguredAgents(config), {
2362
+ label: `Auto-syncing agent files after ${action}\u2026`
2363
+ });
2364
+ } catch (err) {
2365
+ console.log(chalk3.yellow(` Auto-sync skipped: ${err.message}`));
2366
+ console.log(chalk3.gray(" Run memory-core sync manually when ready."));
2367
+ }
2368
+ }
1747
2369
  var program = new Command();
1748
2370
  program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version(version);
1749
2371
  program.command("init").description("Initialize memory-core in the current project").option("--quick", "Use smart defaults and skip optional prompts").action(async (opts) => {
@@ -1755,25 +2377,19 @@ program.command("init").description("Initialize memory-core in the current proje
1755
2377
  if (!hasEnv && quick) {
1756
2378
  const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
1757
2379
  const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
1758
- const ollamaUrl = "http://localhost:11434";
1759
- const chatModel = "llama3.2";
1760
- const envContent = [
1761
- `DATABASE_URL=${dbUrl}`,
1762
- `OLLAMA_URL=${ollamaUrl}`,
1763
- `OLLAMA_MODEL=nomic-embed-text`,
1764
- `OLLAMA_CHAT_MODEL=${chatModel}`
1765
- ].join("\n") + "\n";
1766
- writeFileSync5(envPath, envContent);
1767
- process.env.DATABASE_URL = dbUrl;
1768
- process.env.OLLAMA_URL = ollamaUrl;
1769
- process.env.OLLAMA_MODEL = "nomic-embed-text";
1770
- process.env.OLLAMA_CHAT_MODEL = chatModel;
1771
- const gitignorePath2 = join6(process.cwd(), ".gitignore");
1772
- const gitignore = existsSync6(gitignorePath2) ? readFileSync6(gitignorePath2, "utf-8") : "";
1773
- if (!gitignore.includes(".memory-core.env")) {
1774
- appendFileSync(gitignorePath2, `${gitignore ? "\n" : ""}.memory-core.env
1775
- `);
1776
- }
2380
+ const ollamaUrl = DEFAULT_OLLAMA_URL;
2381
+ const chatModel = DEFAULT_CHAT_MODEL;
2382
+ const envValues = {
2383
+ DATABASE_URL: dbUrl,
2384
+ OLLAMA_URL: ollamaUrl,
2385
+ OLLAMA_MODEL: DEFAULT_EMBEDDING_MODEL,
2386
+ CHAT_PROVIDER: "ollama",
2387
+ CHAT_MODEL: chatModel,
2388
+ OLLAMA_CHAT_MODEL: chatModel
2389
+ };
2390
+ writeRuntimeEnv(envValues, envPath);
2391
+ applyRuntimeEnv(envValues);
2392
+ ensureEnvFileIgnored(envPath);
1777
2393
  console.log(chalk3.green(" \u2713 .memory-core.env created with local defaults"));
1778
2394
  } else if (!hasEnv) {
1779
2395
  console.log(chalk3.dim(" No .memory-core.env found \u2014 let's set up your connection.\n"));
@@ -1889,33 +2505,18 @@ program.command("init").description("Initialize memory-core in the current proje
1889
2505
  });
1890
2506
  console.log(chalk3.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
1891
2507
  }
1892
- const envLines = [
1893
- `DATABASE_URL=${dbUrl}`,
1894
- `OLLAMA_URL=${ollamaUrl}`,
1895
- `OLLAMA_MODEL=nomic-embed-text`,
1896
- `CHAT_PROVIDER=${chatProvider}`,
1897
- `CHAT_MODEL=${chatModel}`
1898
- ];
1899
- if (chatProvider === "ollama") envLines.push(`OLLAMA_CHAT_MODEL=${chatModel}`);
1900
- if (chatApiKey) envLines.push(`CHAT_API_KEY=${chatApiKey}`);
1901
- const envContent = envLines.join("\n") + "\n";
1902
- writeFileSync5(envPath, envContent);
1903
- process.env.DATABASE_URL = dbUrl;
1904
- process.env.OLLAMA_URL = ollamaUrl;
1905
- process.env.OLLAMA_MODEL = "nomic-embed-text";
1906
- process.env.CHAT_PROVIDER = chatProvider;
1907
- process.env.CHAT_MODEL = chatModel;
1908
- if (chatProvider === "ollama") process.env.OLLAMA_CHAT_MODEL = chatModel;
1909
- if (chatApiKey) process.env.CHAT_API_KEY = chatApiKey;
1910
- const gitignorePath2 = join6(process.cwd(), ".gitignore");
1911
- if (existsSync6(gitignorePath2)) {
1912
- const gi = readFileSync6(gitignorePath2, "utf-8");
1913
- if (!gi.includes(".memory-core.env")) {
1914
- appendFileSync(gitignorePath2, "\n.memory-core.env\n");
1915
- }
1916
- } else {
1917
- writeFileSync5(gitignorePath2, ".memory-core.env\n");
1918
- }
2508
+ const envValues = {
2509
+ DATABASE_URL: dbUrl,
2510
+ OLLAMA_URL: ollamaUrl,
2511
+ OLLAMA_MODEL: DEFAULT_EMBEDDING_MODEL,
2512
+ CHAT_PROVIDER: chatProvider,
2513
+ CHAT_MODEL: chatModel
2514
+ };
2515
+ if (chatProvider === "ollama") envValues.OLLAMA_CHAT_MODEL = chatModel;
2516
+ if (chatApiKey) envValues.CHAT_API_KEY = chatApiKey;
2517
+ writeRuntimeEnv(envValues, envPath);
2518
+ applyRuntimeEnv(envValues);
2519
+ ensureEnvFileIgnored(envPath);
1919
2520
  console.log(chalk3.green("\n \u2713 .memory-core.env created"));
1920
2521
  console.log(chalk3.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
1921
2522
  }
@@ -1976,11 +2577,20 @@ program.command("init").description("Initialize memory-core in the current proje
1976
2577
  message: "Pull relevant memories from previous projects?",
1977
2578
  default: true
1978
2579
  });
1979
- const installCaveman = quick ? false : await confirm({
1980
- 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.",
1981
2582
  default: false
1982
2583
  });
1983
2584
  let cavemanIntensity = "full";
2585
+ if (installCaveman) {
2586
+ const allowRemoteInstaller = await confirm({
2587
+ message: `Security check: download and execute installer from ${CAVEMAN_INSTALL_URL}?`,
2588
+ default: false
2589
+ });
2590
+ if (!allowRemoteInstaller) {
2591
+ installCaveman = false;
2592
+ }
2593
+ }
1984
2594
  if (installCaveman) {
1985
2595
  cavemanIntensity = await select({
1986
2596
  message: "Caveman intensity?",
@@ -2022,20 +2632,23 @@ program.command("init").description("Initialize memory-core in the current proje
2022
2632
  language,
2023
2633
  caveman: { enabled: installCaveman, intensity: cavemanIntensity },
2024
2634
  agents: selectedAgents,
2025
- allowPatterns: []
2635
+ allowPatterns: [],
2636
+ autoSync: true
2026
2637
  };
2027
2638
  let memories = [];
2028
2639
  if (pullMemories) {
2029
2640
  const spinner2 = ora("Retrieving relevant memories\u2026").start();
2030
2641
  try {
2031
2642
  const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
2032
- memories = await retrieveContextualMemories({
2643
+ const selection = await retrieveMemorySelection({
2033
2644
  query: archQuery,
2034
2645
  cwd: process.cwd(),
2035
2646
  config,
2036
2647
  limit: 20
2037
2648
  });
2649
+ memories = selection.included;
2038
2650
  spinner2.succeed(`Found ${memories.length} relevant memories`);
2651
+ printMemorySelection(selection);
2039
2652
  } catch (err) {
2040
2653
  spinner2.warn(`Could not retrieve memories: ${err.message}`);
2041
2654
  }
@@ -2043,10 +2656,7 @@ program.command("init").description("Initialize memory-core in the current proje
2043
2656
  if (installCaveman) {
2044
2657
  const spinner2 = ora("Installing caveman token saver\u2026").start();
2045
2658
  try {
2046
- execSync2(
2047
- "curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash",
2048
- { stdio: "pipe", cwd: process.cwd() }
2049
- );
2659
+ await installCavemanTokenSaver();
2050
2660
  spinner2.succeed("Caveman installed");
2051
2661
  } catch (err) {
2052
2662
  spinner2.warn(`Caveman install failed: ${err.message}`);
@@ -2060,15 +2670,11 @@ program.command("init").description("Initialize memory-core in the current proje
2060
2670
  );
2061
2671
  writeProjectConfig(config);
2062
2672
  spinner.succeed(`Generated ${written.written.length} files`);
2063
- const gitignorePath = join6(process.cwd(), ".gitignore");
2064
- const generatedPaths = written.written;
2065
- if (generatedPaths.length > 0) {
2066
- const existing = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
2067
- const toAdd = generatedPaths.filter((p) => !existing.includes(p));
2068
- if (toAdd.length > 0) {
2069
- const block = "\n# memory-core generated files\n" + toAdd.join("\n") + "\n";
2070
- appendFileSync(gitignorePath, block);
2071
- console.log(chalk3.green(` \u2713 Added ${toAdd.length} generated files to .gitignore`));
2673
+ const gitignoreEntries = [...written.written, ...LOCAL_GENERATED_FILES];
2674
+ if (gitignoreEntries.length > 0) {
2675
+ const added = appendMissingGitignoreEntries(gitignoreEntries, "# memory-core generated files");
2676
+ if (added > 0) {
2677
+ console.log(chalk3.green(` \u2713 Added ${added} generated files to .gitignore`));
2072
2678
  }
2073
2679
  }
2074
2680
  if (enableHook) {
@@ -2103,44 +2709,32 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
2103
2709
  console.log(chalk3.yellow(" No agents selected \u2014 nothing to sync."));
2104
2710
  process.exit(0);
2105
2711
  }
2106
- const spinner = ora("Syncing memories\u2026").start();
2107
- let memories = [];
2108
- try {
2109
- const archQuery = [config.backendArchitecture, config.frontendFramework, config.language].filter(Boolean).join(" ");
2110
- memories = await retrieveContextualMemories({
2111
- query: archQuery,
2112
- cwd: process.cwd(),
2113
- config,
2114
- limit: 25
2115
- });
2116
- spinner.text = `Found ${memories.length} memories \u2014 regenerating files\u2026`;
2117
- } catch (err) {
2118
- spinner.warn(`Could not retrieve memories: ${err.message}`);
2712
+ await syncGeneratedFiles(config, [...selectedAgents, "Shared"], { showSelection: true });
2713
+ await closePool();
2714
+ });
2715
+ program.command("auto-sync [mode]").description("Show or change automatic agent file sync (on|off)").action((mode) => {
2716
+ const config = readProjectConfig();
2717
+ if (!config) {
2718
+ console.error(chalk3.red("No .memory-core.json found. Run: memory-core init"));
2719
+ process.exit(1);
2119
2720
  }
2120
- const result = await generate(
2121
- {
2122
- projectName: config.projectName,
2123
- projectType: config.projectType,
2124
- backendArchitecture: config.backendArchitecture,
2125
- frontendFramework: config.frontendFramework,
2126
- language: config.language,
2127
- memories,
2128
- caveman: config.caveman
2129
- },
2130
- process.cwd(),
2131
- [...selectedAgents, "Shared"]
2132
- );
2133
- const updatedCount = result.written.length;
2134
- const skippedCount = result.skipped.length;
2135
- spinner.succeed(
2136
- `Synced \u2014 ${chalk3.green(`${updatedCount} updated`)}, ${chalk3.dim(`${skippedCount} already up to date`)}`
2137
- );
2138
- if (result.written.length > 0) {
2139
- result.written.forEach((f) => console.log(chalk3.gray(` \u2713 ${f}`)));
2721
+ const normalized = mode?.trim().toLowerCase();
2722
+ if (!normalized || normalized === "status") {
2723
+ console.log(chalk3.bold("\n Auto-sync\n"));
2724
+ console.log(` Status: ${config.autoSync === false ? chalk3.yellow("disabled") : chalk3.green("enabled")}`);
2725
+ console.log(chalk3.gray(" Manual sync is always available: memory-core sync\n"));
2726
+ return;
2140
2727
  }
2141
- 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"));
2142
2736
  });
2143
- program.command("remember <text>").description("Save a new memory to the central database").option("-t, --type <type>", "Memory type (decision|rule|pattern|note)", "decision").option("-s, --scope <scope>", "Scope (global|project)", "project").option("--tags <tags>", "Comma-separated tags").option("-r, --reason <reason>", "Why this rule exists \u2014 helps agents understand intent and debug violations").action(async (text, opts) => {
2737
+ program.command("remember <text>").description("Save a new memory to the central database").option("-t, --type <type>", "Memory type (decision|rule|pattern|note)", "decision").option("-s, --scope <scope>", "Scope (global|project)", "project").option("--tags <tags>", "Comma-separated tags").option("-r, --reason <reason>", "Why this rule exists \u2014 helps agents understand intent and debug violations").option("--no-sync", "Skip automatic agent file sync after saving").action(async (text, opts) => {
2144
2738
  const config = readProjectConfig();
2145
2739
  let reason = opts.reason;
2146
2740
  if (!reason) {
@@ -2165,6 +2759,7 @@ program.command("remember <text>").description("Save a new memory to the central
2165
2759
  const reasonLine = reason ? chalk3.gray(`
2166
2760
  Why: ${reason}`) : "";
2167
2761
  spinner.succeed(chalk3.green(`Memory saved: "${text}"`) + reasonLine);
2762
+ await autoSyncGeneratedFiles(config, "remember", opts.sync);
2168
2763
  } catch (err) {
2169
2764
  spinner.fail(`Failed: ${err.message}`);
2170
2765
  process.exit(1);
@@ -2219,9 +2814,10 @@ program.command("export").description(`Export DB memories to ${MEMORY_FILE}`).op
2219
2814
  await closePool();
2220
2815
  }
2221
2816
  });
2222
- program.command("import").description(`Import memories from ${MEMORY_FILE}`).option("--url <url>", "Import memories from a remote URL").option("-f, --file <file>", `Import file (default: ${MEMORY_FILE})`).action(async (opts) => {
2817
+ program.command("import").description(`Import memories from ${MEMORY_FILE}`).option("--url <url>", "Import memories from a remote URL").option("-f, --file <file>", `Import file (default: ${MEMORY_FILE})`).option("--no-sync", "Skip automatic agent file sync after import").action(async (opts) => {
2223
2818
  const spinner = ora("Reading memories\u2026").start();
2224
2819
  try {
2820
+ const config = readProjectConfig();
2225
2821
  const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(readFileSync6(join6(process.cwd(), opts.file), "utf-8")) : readMemoryFile();
2226
2822
  let inserted = 0;
2227
2823
  let skipped = 0;
@@ -2243,6 +2839,9 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
2243
2839
  else skipped++;
2244
2840
  }
2245
2841
  spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
2842
+ if (inserted > 0) {
2843
+ await autoSyncGeneratedFiles(config, "import", opts.sync);
2844
+ }
2246
2845
  } catch (err) {
2247
2846
  spinner.fail(`Import failed: ${err.message}`);
2248
2847
  process.exit(1);
@@ -2250,15 +2849,24 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
2250
2849
  await closePool();
2251
2850
  }
2252
2851
  });
2253
- program.command("list").description("List memories from the local database").option("--type <type>", "Filter by type").option("--scope <scope>", "Filter by scope").option("--arch <architecture>", "Filter by architecture").option("-n, --limit <n>", "Maximum rows to show", "200").action(async (opts) => {
2852
+ program.command("list").description("List memories from the local database").option("--type <type>", "Filter by type").option("--scope <scope>", "Filter by scope").option("--arch <architecture>", "Filter by architecture").option("--project <name>", "Filter by project name").option("--current", "Only show memories relevant to the current project (default)").option("--all", "Show all memories in the database").option("-n, --limit <n>", "Maximum rows to show", "200").action(async (opts) => {
2254
2853
  try {
2854
+ const config = readProjectConfig();
2855
+ const architectures = opts.arch ? opts.arch : opts.all ? void 0 : getCurrentListArchitectures(config);
2856
+ const projectName = opts.project ? opts.project : opts.all || opts.arch ? void 0 : config?.projectName;
2255
2857
  const memories = await listMemories({
2256
2858
  type: opts.type,
2257
2859
  scope: opts.scope,
2258
- architecture: opts.arch,
2860
+ architecture: architectures,
2861
+ projectName,
2862
+ includeGlobal: !opts.all,
2259
2863
  limit: parseInt(opts.limit, 10)
2260
2864
  });
2261
- 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
+ }
2262
2870
  } catch (err) {
2263
2871
  console.error(chalk3.red(`List failed: ${err.message}`));
2264
2872
  process.exit(1);
@@ -2266,14 +2874,16 @@ program.command("list").description("List memories from the local database").opt
2266
2874
  await closePool();
2267
2875
  }
2268
2876
  });
2269
- program.command("remove <id>").description("Remove a memory by ID").action(async (id) => {
2877
+ program.command("remove <id>").description("Remove a memory by ID").option("--no-sync", "Skip automatic agent file sync after removal").action(async (id, opts) => {
2270
2878
  try {
2879
+ const config = readProjectConfig();
2271
2880
  const deleted = await deleteMemory(parseInt(id, 10));
2272
2881
  if (!deleted) {
2273
2882
  console.log(chalk3.yellow(`No memory found with ID ${id}`));
2274
2883
  process.exit(1);
2275
2884
  }
2276
2885
  console.log(chalk3.green(`Removed memory ${id}`));
2886
+ await autoSyncGeneratedFiles(config, "remove", opts.sync);
2277
2887
  } catch (err) {
2278
2888
  console.error(chalk3.red(`Remove failed: ${err.message}`));
2279
2889
  process.exit(1);
@@ -2281,8 +2891,9 @@ program.command("remove <id>").description("Remove a memory by ID").action(async
2281
2891
  await closePool();
2282
2892
  }
2283
2893
  });
2284
- program.command("forget").description("Bulk-delete memories by tag, scope, type, or architecture").option("--tag <tag>", "Delete memories with this tag").option("--scope <scope>", "Filter by scope").option("--type <type>", "Filter by type").option("--arch <architecture>", "Filter by architecture").action(async (opts) => {
2894
+ program.command("forget").description("Bulk-delete memories by tag, scope, type, or architecture").option("--tag <tag>", "Delete memories with this tag").option("--scope <scope>", "Filter by scope").option("--type <type>", "Filter by type").option("--arch <architecture>", "Filter by architecture").option("--no-sync", "Skip automatic agent file sync after deletion").action(async (opts) => {
2285
2895
  try {
2896
+ const config = readProjectConfig();
2286
2897
  const deleted = await deleteMemories({
2287
2898
  tag: opts.tag,
2288
2899
  scope: opts.scope,
@@ -2290,6 +2901,9 @@ program.command("forget").description("Bulk-delete memories by tag, scope, type,
2290
2901
  architecture: opts.arch
2291
2902
  });
2292
2903
  console.log(chalk3.green(`Deleted ${deleted} memories`));
2904
+ if (deleted > 0) {
2905
+ await autoSyncGeneratedFiles(config, "forget", opts.sync);
2906
+ }
2293
2907
  } catch (err) {
2294
2908
  console.error(chalk3.red(`Forget failed: ${err.message}`));
2295
2909
  process.exit(1);
@@ -2297,9 +2911,10 @@ program.command("forget").description("Bulk-delete memories by tag, scope, type,
2297
2911
  await closePool();
2298
2912
  }
2299
2913
  });
2300
- program.command("edit <id>").description("Edit a memory interactively").action(async (id) => {
2914
+ program.command("edit <id>").description("Edit a memory interactively").option("--no-sync", "Skip automatic agent file sync after editing").action(async (id, opts) => {
2301
2915
  const memoryId = parseInt(id, 10);
2302
2916
  try {
2917
+ const config = readProjectConfig();
2303
2918
  const existing = await getMemory(memoryId);
2304
2919
  if (!existing) {
2305
2920
  console.log(chalk3.yellow(`No memory found with ID ${id}`));
@@ -2322,6 +2937,7 @@ program.command("edit <id>").description("Edit a memory interactively").action(a
2322
2937
  embedding
2323
2938
  });
2324
2939
  console.log(chalk3.green(`Updated memory ${id}`));
2940
+ await autoSyncGeneratedFiles(config, "edit", opts.sync);
2325
2941
  } catch (err) {
2326
2942
  console.error(chalk3.red(`Edit failed: ${err.message}`));
2327
2943
  process.exit(1);
@@ -2329,8 +2945,9 @@ program.command("edit <id>").description("Edit a memory interactively").action(a
2329
2945
  await closePool();
2330
2946
  }
2331
2947
  });
2332
- program.command("ignore [pattern]").description("Manage project-scoped false-positive ignore patterns").option("--list", "List ignored patterns").option("--remove <id>", "Remove an ignored pattern by ID").action(async (pattern, opts) => {
2948
+ program.command("ignore [pattern]").description("Manage project-scoped false-positive ignore patterns").option("--list", "List ignored patterns").option("--remove <id>", "Remove an ignored pattern by ID").option("--no-sync", "Skip automatic agent file sync after changing ignore patterns").action(async (pattern, opts) => {
2333
2949
  try {
2950
+ const config = readProjectConfig();
2334
2951
  if (opts.list) {
2335
2952
  printMemoryTable(await listMemories({ type: "ignore", limit: 1e3 }), "Ignored patterns");
2336
2953
  return;
@@ -2342,13 +2959,13 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
2342
2959
  process.exit(1);
2343
2960
  }
2344
2961
  console.log(chalk3.green(`Removed ignore pattern ${opts.remove}`));
2962
+ await autoSyncGeneratedFiles(config, "ignore remove", opts.sync);
2345
2963
  return;
2346
2964
  }
2347
2965
  if (!pattern) {
2348
2966
  console.error(chalk3.red("Provide a pattern, --list, or --remove <id>"));
2349
2967
  process.exit(1);
2350
2968
  }
2351
- const config = readProjectConfig();
2352
2969
  const embedding = await embed(pattern);
2353
2970
  await upsertMemory({
2354
2971
  type: "ignore",
@@ -2360,6 +2977,7 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
2360
2977
  embedding
2361
2978
  });
2362
2979
  console.log(chalk3.green(`Ignored pattern saved: "${pattern}"`));
2980
+ await autoSyncGeneratedFiles(config, "ignore", opts.sync);
2363
2981
  } catch (err) {
2364
2982
  console.error(chalk3.red(`Ignore failed: ${err.message}`));
2365
2983
  process.exit(1);
@@ -2437,7 +3055,6 @@ program.command("reset").description("Remove memory-core generated files and loc
2437
3055
  default: false
2438
3056
  });
2439
3057
  if (ok) {
2440
- const { getPool } = await import("./db-5X5LTUCB.js");
2441
3058
  await getPool().query("DROP TABLE IF EXISTS memories");
2442
3059
  await closePool();
2443
3060
  console.log(chalk3.yellow("Dropped memories table"));
@@ -2617,6 +3234,82 @@ read:
2617
3234
  console.log(chalk3.bold("\n Every AI agent now follows your memory globally.\n"));
2618
3235
  await closePool();
2619
3236
  });
3237
+ var provider = program.command("provider").description("Manage the code-checking provider configuration");
3238
+ provider.command("set <name>").description("Set the code-checking provider (ollama, openai, anthropic, minimax)").option("--model <model>", "Chat model to set alongside the provider").option("--api-key <key>", "API key for cloud providers").action(async (name, opts) => {
3239
+ try {
3240
+ const providerName = normalizeProvider(name);
3241
+ const runtimeEnv = readRuntimeEnv();
3242
+ const values = { ...runtimeEnv.values };
3243
+ values.CHAT_PROVIDER = providerName;
3244
+ values.OLLAMA_URL = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
3245
+ values.OLLAMA_MODEL = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
3246
+ if (opts.model) {
3247
+ values.CHAT_MODEL = opts.model;
3248
+ } else if (!values.CHAT_MODEL && !values.OLLAMA_CHAT_MODEL) {
3249
+ values.CHAT_MODEL = DEFAULT_CHAT_MODEL;
3250
+ }
3251
+ if (providerName === "ollama") {
3252
+ const model2 = values.CHAT_MODEL ?? values.OLLAMA_CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
3253
+ values.CHAT_MODEL = model2;
3254
+ values.OLLAMA_CHAT_MODEL = model2;
3255
+ delete values.CHAT_API_KEY;
3256
+ } else {
3257
+ delete values.OLLAMA_CHAT_MODEL;
3258
+ if (opts.apiKey) {
3259
+ values.CHAT_API_KEY = opts.apiKey;
3260
+ } else if (!values.CHAT_API_KEY) {
3261
+ values.CHAT_API_KEY = await input({
3262
+ message: `${providerLabel(providerName)} API key?`
3263
+ });
3264
+ }
3265
+ }
3266
+ writeRuntimeEnv(values, runtimeEnv.envPath);
3267
+ applyRuntimeEnv(values);
3268
+ ensureEnvFileIgnored(runtimeEnv.envPath);
3269
+ console.log(chalk3.green(`Updated provider: ${providerName}`));
3270
+ console.log(chalk3.gray(` Chat model: ${getConfiguredChatModel(values)}`));
3271
+ } catch (err) {
3272
+ console.error(chalk3.red(`Provider update failed: ${err.message}`));
3273
+ process.exit(1);
3274
+ }
3275
+ });
3276
+ var model = program.command("model").description("Manage code-checking and embedding models");
3277
+ model.command("set <name>").description("Set the chat model used for code checking").option("--provider <provider>", "Override provider while setting the model").option("--embedding", "Set the embedding model instead of the chat model").action(async (name, opts) => {
3278
+ try {
3279
+ const runtimeEnv = readRuntimeEnv();
3280
+ const values = { ...runtimeEnv.values };
3281
+ if (opts.provider) values.CHAT_PROVIDER = normalizeProvider(opts.provider);
3282
+ const providerName = getConfiguredProvider(values);
3283
+ if (opts.embedding) {
3284
+ values.OLLAMA_MODEL = name;
3285
+ } else {
3286
+ values.CHAT_MODEL = name;
3287
+ if (providerName === "ollama") values.OLLAMA_CHAT_MODEL = name;
3288
+ }
3289
+ values.OLLAMA_URL = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
3290
+ values.OLLAMA_MODEL = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
3291
+ writeRuntimeEnv(values, runtimeEnv.envPath);
3292
+ applyRuntimeEnv(values);
3293
+ ensureEnvFileIgnored(runtimeEnv.envPath);
3294
+ console.log(chalk3.green(`Updated ${opts.embedding ? "embedding" : "chat"} model: ${name}`));
3295
+ console.log(chalk3.gray(` Provider: ${providerName}`));
3296
+ } catch (err) {
3297
+ console.error(chalk3.red(`Model update failed: ${err.message}`));
3298
+ process.exit(1);
3299
+ }
3300
+ });
3301
+ model.command("doctor").description("Verify provider, model, database, and Ollama setup").action(async () => {
3302
+ const result = await runModelDoctor();
3303
+ if (!result.ok) process.exit(1);
3304
+ });
3305
+ program.command("status").description("Show the current memory-core project and runtime setup").action(async () => {
3306
+ try {
3307
+ await printProjectStatus();
3308
+ } catch (err) {
3309
+ console.error(chalk3.red(`Status failed: ${err.message}`));
3310
+ process.exit(1);
3311
+ }
3312
+ });
2620
3313
  var hook = program.command("hook").description("Manage the pre-commit rule enforcement hook");
2621
3314
  hook.command("install").description("Install pre-commit hook (advisory mode by default \u2014 logs violations, never blocks)").option("--advisory", "Log violations but never block commits (default)").option("--strict", "Block commits that violate your rules").action((opts) => {
2622
3315
  const advisory = opts.strict ? false : true;