@shahmilsaari/memory-core 1.0.13 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -292,7 +292,7 @@ npx @shahmilsaari/memory-core remember "Use DTOs for all API responses" \
292
292
  |---|---|---|
293
293
  | `--type` | `decision` `rule` `pattern` `note` | `decision` |
294
294
  | `--scope` | `global` `project` | `project` |
295
- | `--reason` | any text | asked interactively |
295
+ | `--reason` | any text | asked interactively; fallback is stored if blank |
296
296
  | `--applies-to` | comma-separated use cases | none |
297
297
  | `--avoid-when` | comma-separated exceptions | none |
298
298
  | `--example` | comma-separated examples | none |
@@ -394,6 +394,7 @@ When `--path` is provided, it must point to the project root (the directory cont
394
394
 
395
395
  ```bash
396
396
  npx @shahmilsaari/memory-core check --staged # check staged files
397
+ npx @shahmilsaari/memory-core check --staged --fast # deterministic-only staged check
397
398
  npx @shahmilsaari/memory-core check --staged --verbose # with extra detail
398
399
  npx @shahmilsaari/memory-core check --staged --debug # show prompt, diff, and raw model output
399
400
  npx @shahmilsaari/memory-core check --ci # CI mode using memories.json
@@ -401,7 +402,7 @@ npx @shahmilsaari/memory-core check --all # scan all tracked source
401
402
  npx @shahmilsaari/memory-core check --all --path src/ # scan only tracked files under src/
402
403
  ```
403
404
 
404
- `--staged` is the same path used by the pre-commit hook. `--ci` reads `memories.json` and uses a deterministic CI-friendly diff check, so pull requests can enforce rules without a local database or Ollama setup.
405
+ `--staged` is the same path used by the pre-commit hook. Add `--fast` to skip AI and memory retrieval when you need a low-latency deterministic check. `--ci` reads `memories.json` and uses a deterministic CI-friendly diff check, so pull requests can enforce rules without a local database or Ollama setup.
405
406
  `--all` runs a full tracked-file snapshot check and exits non-zero if violations are found.
406
407
  `--all` and `--ci` are mutually exclusive in the same command.
407
408
 
@@ -413,10 +414,13 @@ npx @shahmilsaari/memory-core check --all --path src/ # scan only tracked files
413
414
  npx @shahmilsaari/memory-core hook install # advisory mode (default)
414
415
  npx @shahmilsaari/memory-core hook install --advisory # logs violations, never blocks
415
416
  npx @shahmilsaari/memory-core hook install --strict # blocks commits on violations
417
+ npx @shahmilsaari/memory-core hook install --fast # deterministic-only hook
416
418
  ```
417
419
 
418
420
  Installs a git pre-commit hook in the current project. Every time you run `git commit`, your code is checked against your architecture rules.
419
421
 
422
+ Add `--fast` to make the hook skip AI and memory retrieval for lower latency.
423
+
420
424
  **Advisory mode (default):** violations are logged so you can see them, but the commit always goes through. Useful for getting used to the rules without disrupting your flow.
421
425
 
422
426
  **Strict mode:** violations block the commit entirely. You see exactly what's wrong and how to fix it:
@@ -90,12 +90,17 @@ var Config = {
90
90
  };
91
91
 
92
92
  // src/embedding.ts
93
+ function getEmbeddingTimeoutMs() {
94
+ const raw = Number(process.env.EMBEDDING_TIMEOUT_MS ?? process.env.MEMORY_CORE_RETRIEVAL_TIMEOUT_MS ?? 5e3);
95
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 5e3;
96
+ }
93
97
  async function embed(text) {
94
98
  let response;
95
99
  try {
96
100
  response = await fetch(`${Config.ollamaUrl}/api/embeddings`, {
97
101
  method: "POST",
98
102
  headers: { "Content-Type": "application/json" },
103
+ signal: AbortSignal.timeout(getEmbeddingTimeoutMs()),
99
104
  body: JSON.stringify({ model: Config.ollamaModel, prompt: text })
100
105
  });
101
106
  } catch {
@@ -122,10 +127,25 @@ function getChatConfig() {
122
127
  apiKey: process.env.CHAT_API_KEY ?? ""
123
128
  };
124
129
  }
125
- async function callOllama(cfg, messages) {
130
+ function getDefaultTimeoutMs() {
131
+ const raw = Number(process.env.CHAT_TIMEOUT_MS ?? 2e4);
132
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 2e4;
133
+ }
134
+ function timeoutSignal(timeoutMs) {
135
+ return AbortSignal.timeout(timeoutMs ?? getDefaultTimeoutMs());
136
+ }
137
+ function normalizeChatError(err, timeoutMs) {
138
+ const ms = timeoutMs ?? getDefaultTimeoutMs();
139
+ if (err instanceof Error && err.name === "AbortError") {
140
+ return new Error(`TIMEOUT:${ms}`);
141
+ }
142
+ return err instanceof Error ? err : new Error(String(err));
143
+ }
144
+ async function callOllama(cfg, messages, options = {}) {
126
145
  const res = await fetch(`${cfg.ollamaUrl}/api/chat`, {
127
146
  method: "POST",
128
147
  headers: { "Content-Type": "application/json" },
148
+ signal: timeoutSignal(options.timeoutMs),
129
149
  body: JSON.stringify({ model: cfg.model, messages, stream: false, format: "json" })
130
150
  });
131
151
  if (!res.ok) {
@@ -138,13 +158,14 @@ async function callOllama(cfg, messages) {
138
158
  const data = await res.json();
139
159
  return data.message.content.trim();
140
160
  }
141
- async function callOpenAI(cfg, messages) {
161
+ async function callOpenAI(cfg, messages, options = {}) {
142
162
  const res = await fetch("https://api.openai.com/v1/chat/completions", {
143
163
  method: "POST",
144
164
  headers: {
145
165
  "Content-Type": "application/json",
146
166
  "Authorization": `Bearer ${cfg.apiKey}`
147
167
  },
168
+ signal: timeoutSignal(options.timeoutMs),
148
169
  body: JSON.stringify({
149
170
  model: cfg.model,
150
171
  messages,
@@ -155,7 +176,7 @@ async function callOpenAI(cfg, messages) {
155
176
  const data = await res.json();
156
177
  return data.choices[0].message.content.trim();
157
178
  }
158
- async function callAnthropic(cfg, messages) {
179
+ async function callAnthropic(cfg, messages, options = {}) {
159
180
  const system = messages.find((m) => m.role === "system")?.content ?? "";
160
181
  const userMessages = messages.filter((m) => m.role !== "system");
161
182
  const res = await fetch("https://api.anthropic.com/v1/messages", {
@@ -165,6 +186,7 @@ async function callAnthropic(cfg, messages) {
165
186
  "x-api-key": cfg.apiKey,
166
187
  "anthropic-version": "2023-06-01"
167
188
  },
189
+ signal: timeoutSignal(options.timeoutMs),
168
190
  body: JSON.stringify({
169
191
  model: cfg.model,
170
192
  max_tokens: 4096,
@@ -176,13 +198,14 @@ async function callAnthropic(cfg, messages) {
176
198
  const data = await res.json();
177
199
  return data.content[0].text.trim();
178
200
  }
179
- async function callMiniMax(cfg, messages) {
201
+ async function callMiniMax(cfg, messages, options = {}) {
180
202
  const res = await fetch("https://api.minimax.io/v1/chat/completions", {
181
203
  method: "POST",
182
204
  headers: {
183
205
  "Content-Type": "application/json",
184
206
  "Authorization": `Bearer ${cfg.apiKey}`
185
207
  },
208
+ signal: timeoutSignal(options.timeoutMs),
186
209
  body: JSON.stringify({
187
210
  model: cfg.model,
188
211
  messages,
@@ -193,17 +216,21 @@ async function callMiniMax(cfg, messages) {
193
216
  const data = await res.json();
194
217
  return data.choices[0].message.content.trim();
195
218
  }
196
- async function callChatModel(messages) {
219
+ async function callChatModel(messages, options = {}) {
197
220
  const cfg = getChatConfig();
198
- switch (cfg.provider) {
199
- case "openai":
200
- return callOpenAI(cfg, messages);
201
- case "anthropic":
202
- return callAnthropic(cfg, messages);
203
- case "minimax":
204
- return callMiniMax(cfg, messages);
205
- default:
206
- return callOllama(cfg, messages);
221
+ try {
222
+ switch (cfg.provider) {
223
+ case "openai":
224
+ return await callOpenAI(cfg, messages, options);
225
+ case "anthropic":
226
+ return await callAnthropic(cfg, messages, options);
227
+ case "minimax":
228
+ return await callMiniMax(cfg, messages, options);
229
+ default:
230
+ return await callOllama(cfg, messages, options);
231
+ }
232
+ } catch (err) {
233
+ throw normalizeChatError(err, options.timeoutMs);
207
234
  }
208
235
  }
209
236
  function getChatProviderLabel() {
@@ -218,6 +245,10 @@ import { createHash } from "crypto";
218
245
  var { Pool } = pg;
219
246
  var pool = null;
220
247
  var migrationsRun = false;
248
+ function readPositiveIntEnv(name, fallback) {
249
+ const raw = Number(process.env[name]);
250
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
251
+ }
221
252
  function hashMemoryContent(content) {
222
253
  return createHash("md5").update(content.trim()).digest("hex");
223
254
  }
@@ -226,7 +257,13 @@ function getPool() {
226
257
  if (!Config.databaseUrl) {
227
258
  throw new Error("DATABASE_URL is not set. Add it to your .env or .memory-core.env file.");
228
259
  }
229
- pool = new Pool({ connectionString: Config.databaseUrl });
260
+ const timeoutMs = readPositiveIntEnv("DATABASE_TIMEOUT_MS", 5e3);
261
+ pool = new Pool({
262
+ connectionString: Config.databaseUrl,
263
+ connectionTimeoutMillis: timeoutMs,
264
+ query_timeout: timeoutMs,
265
+ statement_timeout: timeoutMs
266
+ });
230
267
  }
231
268
  return pool;
232
269
  }
@@ -235,6 +272,7 @@ async function runMigrations() {
235
272
  const client = await getPool().connect();
236
273
  try {
237
274
  await client.query("BEGIN");
275
+ await client.query(`ALTER TABLE memories ALTER COLUMN scope SET DEFAULT 'project'`);
238
276
  await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS reason TEXT`);
239
277
  await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS content_hash TEXT`);
240
278
  await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS context JSONB NOT NULL DEFAULT '{}'::jsonb`);
@@ -1243,13 +1281,22 @@ var MemoryEngineService = class {
1243
1281
  }
1244
1282
  memoryRepository;
1245
1283
  embeddingProvider;
1284
+ withReason(input) {
1285
+ const reason = input.reason?.trim();
1286
+ return {
1287
+ ...input,
1288
+ reason: reason || `Captured as a ${input.type} memory because it should be remembered: ${input.content}`
1289
+ };
1290
+ }
1246
1291
  async remember(input) {
1247
- const embedding = await this.embeddingProvider.embed(input.content);
1248
- return this.memoryRepository.upsert({ ...input, embedding });
1292
+ const normalized = this.withReason(input);
1293
+ const embedding = await this.embeddingProvider.embed(normalized.content);
1294
+ return this.memoryRepository.upsert({ ...normalized, embedding });
1249
1295
  }
1250
1296
  async rememberForce(input) {
1251
- const embedding = await this.embeddingProvider.embed(input.content);
1252
- await this.memoryRepository.save({ ...input, embedding });
1297
+ const normalized = this.withReason(input);
1298
+ const embedding = await this.embeddingProvider.embed(normalized.content);
1299
+ await this.memoryRepository.save({ ...normalized, embedding });
1253
1300
  }
1254
1301
  async list(filters = {}) {
1255
1302
  return this.memoryRepository.list(filters);
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  retrieveMemorySelection,
21
21
  runMigrations,
22
22
  seeds
23
- } from "./chunk-PRRVI3YM.js";
23
+ } from "./chunk-ECYSBYMM.js";
24
24
 
25
25
  // src/cli.ts
26
26
  import { Command } from "commander";
@@ -122,8 +122,9 @@ var reasonMap = new Map(
122
122
  );
123
123
  var HOOK_PATH = join2(".git", "hooks", "pre-commit");
124
124
  var HOOK_MARKER = "# archmind-memory-core";
125
- function buildHookBody(advisory) {
125
+ function buildHookBody(advisory, fast = false) {
126
126
  const suffix = advisory ? " || true" : "";
127
+ const checkArgs = fast ? "check --staged --fast" : "check --staged";
127
128
  return `${HOOK_MARKER}${advisory ? " advisory" : ""}
128
129
  if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
129
130
  exit 0
@@ -135,20 +136,20 @@ if [ -n "\${SKIP_HOOKS:-}" ]; then
135
136
  exit 0
136
137
  fi
137
138
  if command -v memory-core >/dev/null 2>&1; then
138
- memory-core check --staged${suffix}
139
+ memory-core ${checkArgs}${suffix}
139
140
  elif [ -f "./node_modules/.bin/memory-core" ]; then
140
- ./node_modules/.bin/memory-core check --staged${suffix}
141
+ ./node_modules/.bin/memory-core ${checkArgs}${suffix}
141
142
  elif [ -f "./dist/cli.js" ]; then
142
- node ./dist/cli.js check --staged${suffix}
143
+ node ./dist/cli.js ${checkArgs}${suffix}
143
144
  else
144
- npx --no-install memory-core check --staged 2>/dev/null || exit 0
145
+ npx --no-install memory-core ${checkArgs} 2>/dev/null || exit 0
145
146
  fi
146
147
  `;
147
148
  }
148
- function buildHookScript(advisory) {
149
+ function buildHookScript(advisory, fast = false) {
149
150
  return `#!/bin/sh
150
151
 
151
- ${buildHookBody(advisory)}`;
152
+ ${buildHookBody(advisory, fast)}`;
152
153
  }
153
154
  function normalizeHookPreamble(content) {
154
155
  const lines = content.split("\n");
@@ -165,6 +166,26 @@ function normalizeHookPreamble(content) {
165
166
  }
166
167
  return normalized.join("\n").replace(/\n{3,}/g, "\n\n").trim();
167
168
  }
169
+ function readPositiveIntEnv(name, fallback) {
170
+ const raw = Number(process.env[name]);
171
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
172
+ }
173
+ function isFastCheck(options) {
174
+ return options.fast === true || process.env.MEMORY_CORE_CHECK_FAST === "1";
175
+ }
176
+ async function withTimeout(promise, timeoutMs, fallback) {
177
+ let timer;
178
+ try {
179
+ return await Promise.race([
180
+ promise,
181
+ new Promise((resolve2) => {
182
+ timer = setTimeout(() => resolve2(fallback), timeoutMs);
183
+ })
184
+ ]);
185
+ } finally {
186
+ if (timer) clearTimeout(timer);
187
+ }
188
+ }
168
189
  function recordViolations(violations, source = "hook") {
169
190
  const statsPath = join2(process.cwd(), ".memory-core-stats.json");
170
191
  let stats = { rules: {}, files: {} };
@@ -219,11 +240,12 @@ async function promptToSaveViolations(violations) {
219
240
  message: "Why should this rule exist?",
220
241
  default: selected.reason ?? selected.issue ?? ""
221
242
  });
243
+ const storedReason = reason.trim() || selected.reason || selected.issue || `Captured from violation: ${selected.rule}`;
222
244
  await app.services.memoryEngine.remember({
223
245
  type: "rule",
224
246
  scope: "project",
225
247
  content: selected.rule,
226
- reason: reason || void 0,
248
+ reason: storedReason,
227
249
  tags: ["violation"]
228
250
  });
229
251
  console.log(chalk.green(" \u2713 Saved as project rule. Run memory-core sync to propagate it.\n"));
@@ -493,10 +515,11 @@ ${JSON.stringify(options.allowPatterns, null, 2)}`;
493
515
  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"));
494
516
  }
495
517
  try {
518
+ const recheckTimeoutMs = readPositiveIntEnv("MEMORY_CORE_FALSE_POSITIVE_TIMEOUT_MS", 6e3);
496
519
  const raw = await callChatModel([
497
520
  { role: "system", content: systemPrompt },
498
521
  { role: "user", content: userPrompt }
499
- ]);
522
+ ], { timeoutMs: recheckTimeoutMs });
500
523
  const parsed = parseFalsePositiveDecisions(raw);
501
524
  if (!parsed.valid) return [];
502
525
  const existing = new Set(options.allowPatterns.map((pattern) => pattern.toLowerCase()));
@@ -637,13 +660,13 @@ function filterModelViolationsByStagedDiff(violations, stagedFiles, diff) {
637
660
  }
638
661
  return filtered;
639
662
  }
640
- function installHook(advisory = true) {
663
+ function installHook(advisory = true, fast = false) {
641
664
  if (!existsSync2(".git")) {
642
665
  console.error(chalk.red("\n Not a git repository. Run from project root.\n"));
643
666
  process.exit(1);
644
667
  }
645
- const script = buildHookScript(advisory);
646
- const body = buildHookBody(advisory).trimEnd();
668
+ const script = buildHookScript(advisory, fast);
669
+ const body = buildHookBody(advisory, fast).trimEnd();
647
670
  if (existsSync2(HOOK_PATH)) {
648
671
  const existing = readFileSync2(HOOK_PATH, "utf-8");
649
672
  if (existing.includes(HOOK_MARKER)) {
@@ -660,6 +683,7 @@ ${body}
660
683
  chmodSync(HOOK_PATH, 493);
661
684
  const modeLabel2 = advisory ? chalk.cyan("advisory") : chalk.yellow("strict");
662
685
  console.log(chalk.green("\n \u2713 Pre-commit hook updated") + chalk.dim(` (${modeLabel2} mode)`));
686
+ if (fast) console.log(chalk.gray(` Check mode: fast deterministic checks`));
663
687
  return;
664
688
  }
665
689
  writeFileSync2(HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
@@ -669,7 +693,7 @@ ${body}
669
693
  chmodSync(HOOK_PATH, 493);
670
694
  const modeLabel = advisory ? "advisory (logs violations, never blocks)" : "strict (blocks commits on violations)";
671
695
  console.log(chalk.green("\n \u2713 Pre-commit hook installed") + chalk.dim(` \u2014 ${modeLabel}`));
672
- console.log(chalk.gray(` Chat model: ${process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"}`));
696
+ console.log(chalk.gray(fast ? " Check mode: fast deterministic checks" : ` Chat model: ${process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"}`));
673
697
  console.log(chalk.gray(" To uninstall: memory-core hook uninstall\n"));
674
698
  }
675
699
  function uninstallHook() {
@@ -720,17 +744,22 @@ async function checkStaged(options = {}) {
720
744
  if (!existsSync2(configPath)) return;
721
745
  const config = JSON.parse(readFileSync2(configPath, "utf-8"));
722
746
  const { rules: fallbackRules, avoids } = getProfileRules(config);
723
- const [rules, ignores] = await Promise.all([
724
- loadRelevantRules(config, diff, stagedFiles, fallbackRules),
725
- loadIgnorePatterns()
747
+ const fast = isFastCheck(options);
748
+ const ruleLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_RULE_LOAD_TIMEOUT_MS", 2e3);
749
+ const ignoreLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_IGNORE_LOAD_TIMEOUT_MS", 1500);
750
+ const [rules, ignores] = fast ? [fallbackRules, []] : await Promise.all([
751
+ withTimeout(loadRelevantRules(config, diff, stagedFiles, fallbackRules), ruleLoadTimeoutMs, fallbackRules),
752
+ withTimeout(loadIgnorePatterns(), ignoreLoadTimeoutMs, [])
726
753
  ]);
727
754
  const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...ignores])];
728
755
  if (rules.length === 0) return;
729
- const modelInput = buildModelInputFromDiff(diff, 8e3);
756
+ const modelInputMaxChars = readPositiveIntEnv("MEMORY_CORE_MODEL_INPUT_MAX_CHARS", 8e3);
757
+ const modelInput = buildModelInputFromDiff(diff, modelInputMaxChars);
730
758
  console.log(chalk.cyan("\n archmind \u2014 checking staged changes against rules\u2026"));
731
759
  if (options.verbose || options.debug) {
732
760
  const sourceLabel = modelInput.source === "added-lines" ? "added lines" : "diff";
733
- console.log(chalk.gray(` model: ${getChatProviderLabel()} rules: ${rules.length} diff: ${diff.length} chars input: ${sourceLabel}${modelInput.truncated ? " (truncated)" : ""}`));
761
+ const modelLabel = fast ? "skipped (--fast)" : getChatProviderLabel();
762
+ console.log(chalk.gray(` model: ${modelLabel} rules: ${rules.length} diff: ${diff.length} chars input: ${sourceLabel}${modelInput.truncated ? " (truncated)" : ""}`));
734
763
  }
735
764
  const rulesWithReasons = rules.map((r, i) => {
736
765
  const why = reasonMap.get(r);
@@ -773,13 +802,19 @@ Do not include any text outside the JSON object.`;
773
802
  reasonLookup: reasonMap
774
803
  });
775
804
  let modelViolations = [];
776
- try {
805
+ let aiFallback = fast;
806
+ if (fast) {
807
+ if (options.verbose || options.debug) {
808
+ console.log(chalk.gray(" AI check skipped; running deterministic checks only."));
809
+ }
810
+ } else try {
811
+ const checkTimeoutMs = readPositiveIntEnv("MEMORY_CORE_CHECK_TIMEOUT_MS", readPositiveIntEnv("CHAT_TIMEOUT_MS", 2e4));
777
812
  const raw = await callChatModel([
778
813
  { role: "system", content: systemPrompt },
779
814
  { role: "user", content: `Review these staged changes:
780
815
 
781
816
  ${modelInput.text}` }
782
- ]);
817
+ ], { timeoutMs: checkTimeoutMs });
783
818
  if (options.verbose || options.debug) {
784
819
  console.log(chalk.gray(` raw response: ${options.debug ? raw : raw.slice(0, 200)}`));
785
820
  }
@@ -792,22 +827,32 @@ ${modelInput.text}` }
792
827
  } catch (err) {
793
828
  if (err.message?.startsWith("MODEL_NOT_FOUND:")) {
794
829
  printModelMissing(err.message.split(":")[1]);
830
+ aiFallback = true;
831
+ modelViolations = [];
832
+ } else if (err.message?.startsWith("TIMEOUT:")) {
833
+ const timeoutMs = err.message.split(":")[1];
834
+ console.log(chalk.yellow(`
835
+ \u26A0 AI check timed out after ${timeoutMs}ms \u2014 switching to fast deterministic checks for this run.`));
836
+ console.log(chalk.gray(" Set MEMORY_CORE_CHECK_TIMEOUT_MS to tune this.\n"));
837
+ aiFallback = true;
795
838
  modelViolations = [];
796
839
  } else if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
797
840
  console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 using deterministic checks only."));
798
841
  console.log(chalk.gray(" Start it: ollama serve\n"));
842
+ aiFallback = true;
799
843
  modelViolations = [];
800
844
  } else {
801
845
  console.log(chalk.yellow(`
802
846
  \u26A0 AI rule check failed: ${err.message}`));
803
847
  console.log(chalk.gray(" Using deterministic checks only.\n"));
848
+ aiFallback = true;
804
849
  modelViolations = [];
805
850
  }
806
851
  }
807
852
  modelViolations = filterModelViolationsByStagedDiff(modelViolations, stagedFiles, diff);
808
853
  let violations = dedupeViolations([...deterministicViolations, ...astViolations, ...modelViolations]);
809
854
  violations = applyAllowPatterns(violations, allowPatterns);
810
- if (violations.length > 0) {
855
+ if (!aiFallback && violations.length > 0) {
811
856
  const learnedPatterns = await learnGlobalIgnoresFromFalsePositives({
812
857
  diff,
813
858
  currentViolations: violations,
@@ -1305,6 +1350,11 @@ function buildMemoryContext(opts) {
1305
1350
  if (opts.source?.trim()) context.source = opts.source.trim();
1306
1351
  return Object.keys(context).length ? context : void 0;
1307
1352
  }
1353
+ function memoryReasonOrFallback(reason, content, type = "memory") {
1354
+ const trimmed = reason?.trim();
1355
+ if (trimmed) return trimmed;
1356
+ return `Captured as a ${type} memory because it should be remembered: ${content}`;
1357
+ }
1308
1358
  function truncate(value, length) {
1309
1359
  if (!value) return "";
1310
1360
  return value.length > length ? `${value.slice(0, Math.max(0, length - 1))}\u2026` : value;
@@ -1960,27 +2010,29 @@ program.command("auto-sync [mode]").description("Show or change automatic agent
1960
2010
  });
1961
2011
  program.command("remember <text>").description("Save a new memory to the central database").option("-t, --type <type>", "Memory type (decision|rule|pattern|note)", "decision").option("-s, --scope <scope>", "Scope (global|project)", "project").option("--tags <tags>", "Comma-separated tags").option("-r, --reason <reason>", "Why this rule exists \u2014 helps agents understand intent and debug violations").option("--applies-to <items>", "Comma-separated situations where this memory applies").option("--avoid-when <items>", "Comma-separated situations where this memory should not be used").option("--example <items>", "Comma-separated examples that teach agents how to apply this memory").option("--source <source>", "Human-readable source for this memory").option("--no-sync", "Skip automatic agent file sync after saving").action(async (text, opts) => {
1962
2012
  const config = readProjectConfig();
2013
+ const scope = opts.scope?.trim() || "project";
1963
2014
  let reason = opts.reason;
1964
2015
  if (!reason) {
1965
2016
  reason = await input({
1966
- message: chalk2.dim("Why does this rule exist? (optional \u2014 helps agents debug violations)"),
2017
+ message: chalk2.dim("Why should this memory exist?"),
1967
2018
  default: ""
1968
2019
  });
1969
2020
  }
2021
+ const storedReason = memoryReasonOrFallback(reason, text, opts.type);
1970
2022
  const spinner = ora("Saving memory\u2026").start();
1971
2023
  try {
1972
2024
  await phase1.services.memoryEngine.remember({
1973
2025
  type: opts.type,
1974
- scope: opts.scope,
2026
+ scope,
1975
2027
  architecture: config?.backendArchitecture ?? config?.frontendFramework,
1976
- projectName: config?.projectName,
2028
+ projectName: scope === "project" ? config?.projectName : void 0,
1977
2029
  content: text,
1978
- reason: reason || void 0,
2030
+ reason: storedReason,
1979
2031
  context: buildMemoryContext(opts),
1980
2032
  tags: parseTags(opts.tags)
1981
2033
  });
1982
- const reasonLine = reason ? chalk2.gray(`
1983
- Why: ${reason}`) : "";
2034
+ const reasonLine = chalk2.gray(`
2035
+ Why: ${storedReason}`);
1984
2036
  spinner.succeed(chalk2.green(`Memory saved: "${text}"`) + reasonLine);
1985
2037
  await autoSyncGeneratedFiles(config, "remember", opts.sync);
1986
2038
  } catch (err) {
@@ -2162,7 +2214,7 @@ program.command("edit <id>").description("Edit a memory interactively").option("
2162
2214
  scope,
2163
2215
  title: title || void 0,
2164
2216
  content,
2165
- reason: reason || void 0,
2217
+ reason: memoryReasonOrFallback(reason, content, type),
2166
2218
  context: buildMemoryContext({ appliesTo, avoidWhen, example: examples, source }),
2167
2219
  tags: parseTags(tags)
2168
2220
  });
@@ -2378,7 +2430,7 @@ program.command("dashboard").description("Start the live Svelte dashboard with W
2378
2430
  }
2379
2431
  return void 0;
2380
2432
  };
2381
- const { startDashboard } = await import("./dashboard-server-53HVL7LF.js");
2433
+ const { startDashboard } = await import("./dashboard-server-4WOUQTJN.js");
2382
2434
  await startDashboard({
2383
2435
  port: parseInt(opts.port, 10),
2384
2436
  path: resolveDashboardPath(),
@@ -2775,14 +2827,14 @@ graph.command("diff <leftSnapshotId> [rightSnapshotId]").description("Diff two s
2775
2827
  }
2776
2828
  });
2777
2829
  var hook = program.command("hook").description("Manage the pre-commit rule enforcement hook");
2778
- 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) => {
2830
+ 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").option("--fast", "Use deterministic checks only in the hook").action((opts) => {
2779
2831
  const advisory = opts.strict ? false : true;
2780
- installHook(advisory);
2832
+ installHook(advisory, opts.fast ?? false);
2781
2833
  });
2782
2834
  hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
2783
2835
  uninstallHook();
2784
2836
  });
2785
- program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--ci", `Check CI diff using ${MEMORY_FILE}`).option("--all", "Check all tracked source files, including already-committed files").option("--path <dir>", "Directory to check for --all mode (default: current directory)").option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
2837
+ program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--ci", `Check CI diff using ${MEMORY_FILE}`).option("--all", "Check all tracked source files, including already-committed files").option("--path <dir>", "Directory to check for --all mode (default: current directory)").option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").option("--fast", "Skip AI and memory retrieval; run deterministic checks only").action(async (opts) => {
2786
2838
  if (opts.ci && opts.all) {
2787
2839
  console.error(chalk2.red("\n Choose one mode: --ci or --all.\n"));
2788
2840
  process.exit(1);
@@ -2800,7 +2852,7 @@ program.command("check").description("Check staged changes against architecture
2800
2852
  if (summary.violations > 0) process.exit(1);
2801
2853
  return;
2802
2854
  }
2803
- await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false });
2855
+ await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false, fast: opts.fast ?? false });
2804
2856
  });
2805
2857
  program.command("watch").description("Watch source files and check violations in real-time on every save").option("--path <dir>", "Directory to watch (default: current directory)").option("--scan-on-start", "Run an initial full snapshot scan before watching file changes").option("--verbose", "Show diff size and model details per file").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
2806
2858
  await phase1.providers.watchService.start({
@@ -12,7 +12,7 @@ import {
12
12
  saveMemory,
13
13
  startWatch,
14
14
  updateMemory
15
- } from "./chunk-PRRVI3YM.js";
15
+ } from "./chunk-ECYSBYMM.js";
16
16
 
17
17
  // src/dashboard-server.ts
18
18
  import { createHash } from "crypto";
@@ -474,14 +474,16 @@ async function handleApi(req, res, url) {
474
474
  }
475
475
  const config = readProjectConfig();
476
476
  const activeArchitectures = inferProjectArchitectures(projectRoot, config);
477
+ const scope = typeof body.scope === "string" && body.scope.trim() ? body.scope.trim() : "project";
478
+ const reason = typeof body.reason === "string" && body.reason.trim() ? body.reason.trim() : `Captured as a ${typeof body.type === "string" ? body.type : "rule"} memory because it should be remembered: ${content}`;
477
479
  await saveMemory({
478
480
  type: typeof body.type === "string" ? body.type : "rule",
479
- scope: typeof body.scope === "string" ? body.scope : "project",
481
+ scope,
480
482
  architecture: typeof config?.backendArchitecture === "string" ? config.backendArchitecture : typeof config?.frontendFramework === "string" ? config.frontendFramework : activeArchitectures[0],
481
- projectName: typeof config?.projectName === "string" ? config.projectName : void 0,
483
+ projectName: scope === "project" && typeof config?.projectName === "string" ? config.projectName : void 0,
482
484
  title: typeof body.title === "string" ? body.title : void 0,
483
485
  content,
484
- reason: typeof body.reason === "string" && body.reason.trim() ? body.reason.trim() : void 0,
486
+ reason,
485
487
  context: {},
486
488
  tags: parseTags(body.tags),
487
489
  embedding: await embed(content)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shahmilsaari/memory-core",
3
- "version": "1.0.13",
3
+ "version": "1.0.16",
4
4
  "description": "Universal AI memory core — generate AI context files from architecture profiles with RAG support",
5
5
  "homepage": "https://memory-core.shahmilsaari.my/",
6
6
  "type": "module",