@shahmilsaari/memory-core 1.0.13 → 1.0.18

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 |
@@ -393,58 +393,56 @@ When `--path` is provided, it must point to the project root (the directory cont
393
393
  ### `check` — Manual check (for CI)
394
394
 
395
395
  ```bash
396
- npx @shahmilsaari/memory-core check --staged # check staged files
397
- npx @shahmilsaari/memory-core check --staged --verbose # with extra detail
398
- npx @shahmilsaari/memory-core check --staged --debug # show prompt, diff, and raw model output
399
- npx @shahmilsaari/memory-core check --ci # CI mode using memories.json
400
- npx @shahmilsaari/memory-core check --all # scan all tracked source files (not just staged changes)
401
- npx @shahmilsaari/memory-core check --all --path src/ # scan only tracked files under src/
396
+ npx @shahmilsaari/memory-core check --staged # check staged files
397
+ npx @shahmilsaari/memory-core check --staged --fast # deterministic-only staged check
398
+ npx @shahmilsaari/memory-core check --staged --verbose # with extra detail
399
+ npx @shahmilsaari/memory-core check --staged --debug # show prompt, diff, and raw model output
400
+ npx @shahmilsaari/memory-core check --commit-msg # check .git/COMMIT_EDITMSG
401
+ npx @shahmilsaari/memory-core check --commit-msg <file> # check a specific message file
402
+ npx @shahmilsaari/memory-core check --ci # CI mode using memories.json
403
+ npx @shahmilsaari/memory-core check --all # scan all tracked source files
404
+ npx @shahmilsaari/memory-core check --all --path src/ # scan only tracked files under src/
402
405
  ```
403
406
 
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
- `--all` runs a full tracked-file snapshot check and exits non-zero if violations are found.
406
- `--all` and `--ci` are mutually exclusive in the same command.
407
+ `--staged` is the same path used by the pre-commit hook. Add `--fast` to skip AI and memory retrieval for a low-latency deterministic check. `--commit-msg` validates a commit message file against `commitRules` this is called automatically by the `commit-msg` hook. `--ci` reads `memories.json` with no database or Ollama required. `--all` and `--ci` are mutually exclusive.
407
408
 
408
409
  ---
409
410
 
410
- ### `hook install` — Install the pre-commit hook
411
+ ### `hook install` — Install hooks
411
412
 
412
413
  ```bash
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
- Installs a git pre-commit hook in the current project. Every time you run `git commit`, your code is checked against your architecture rules.
420
+ Installs **two** git hooks:
421
+ - `.git/hooks/pre-commit` — checks staged code against architecture rules.
422
+ - `.git/hooks/commit-msg` — checks commit messages against `commitRules` (see `commit-rules` command).
419
423
 
420
- **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.
424
+ **Advisory mode (default):** violations are logged but the commit always goes through.
421
425
 
422
- **Strict mode:** violations block the commit entirely. You see exactly what's wrong and how to fix it:
426
+ **Strict mode:** violations block the commit. You see exactly what's wrong:
423
427
 
424
428
  ```
425
429
  ✗ 2 rule violations found — commit blocked
426
430
 
427
- [1] src/controllers/user.ts:32
428
- Rule: Thin controllers business logic belongs in services
429
- Why: Logic in controllers cannot be reused from other entry points
430
- — it gets siloed and duplicated across handlers
431
- Issue: Password validation logic inside route handler
432
- Fix: Move to UserService.validateCredentials()
431
+ [1] Thin controllers ×2
432
+ src/controllers/user.ts:32 Password validation in route handler
433
+ src/controllers/auth.ts:18 DB query inside controller
433
434
 
434
435
  [2] src/domain/user.entity.ts:5
435
436
  Rule: Domain has zero external imports
436
- Why: Framework imports tie business logic to infrastructure
437
437
  Issue: Imports 'typeorm' directly
438
438
  Fix: Define IUserRepository interface in domain/
439
439
 
440
- To bypass: git commit --no-verify
441
- To save as memory: memory-core remember "<lesson>"
440
+ To bypass: MEMORY_CORE_SKIP_HOOK=1 git commit
441
+ Manage rules: memory-core commit-rules --list
442
442
  ```
443
443
 
444
- The hook mode is chosen during `init` — no separate step needed unless you want to change it later.
445
-
446
444
  ```bash
447
- npx @shahmilsaari/memory-core hook uninstall # remove the hook
445
+ npx @shahmilsaari/memory-core hook uninstall # remove both hooks
448
446
  ```
449
447
 
450
448
  ---
@@ -578,6 +576,28 @@ Stores lightweight per-project allowlist strings in `.memory-core.json`. Hook an
578
576
 
579
577
  ---
580
578
 
579
+ ### `commit-rules` — Enforce commit message format
580
+
581
+ ```bash
582
+ npx @shahmilsaari/memory-core commit-rules "^(feat|fix|chore)" --message "Use conventional commits"
583
+ npx @shahmilsaari/memory-core commit-rules "^WIP" --message "Don't commit WIP" --negate
584
+ npx @shahmilsaari/memory-core commit-rules "PROJ-\d+" --message "Reference a JIRA ticket" --advisory
585
+ npx @shahmilsaari/memory-core commit-rules --list
586
+ npx @shahmilsaari/memory-core commit-rules --remove "^WIP"
587
+ ```
588
+
589
+ Stores regex-based rules in `.memory-core.json` under `commitRules`. The `commit-msg` git hook (installed by `hook install`) validates every commit message against these rules before the commit is accepted.
590
+
591
+ | Flag | What it does |
592
+ |---|---|
593
+ | `--message <msg>` | Required. Error message shown on violation. |
594
+ | `--negate` | Pattern must NOT match (default: must match) |
595
+ | `--advisory` | Warn only — do not block the commit |
596
+ | `--list` | Show all saved commit rules |
597
+ | `--remove <pattern>` | Remove a rule by pattern |
598
+
599
+ ---
600
+
581
601
  ### `ci-setup` — GitHub Actions integration
582
602
 
583
603
  ```bash
@@ -592,13 +612,33 @@ Generates `.github/workflows/memory-core.yml`. Adds a PR check that runs `npx @s
592
612
 
593
613
  ```bash
594
614
  npx @shahmilsaari/memory-core stats
615
+ npx @shahmilsaari/memory-core stats --tune
595
616
  npx @shahmilsaari/memory-core stats --reset
596
617
  ```
597
618
 
598
- Shows which rules fire most often and which files have the most violations.
599
- - If live watch state exists, `stats` shows current live counters.
600
- - Otherwise it shows historical counters recorded over time.
601
- - Use `--reset` to clear counters and recent violation history.
619
+ Shows which rules fire most often and which files have the most violations. Each rule shows hit count and false-positive rate where available.
620
+
621
+ - `--tune` shows only noisy rules (>40% false-positive rate) with their exact disable commands.
622
+ - `--reset` clears all counters and recent violation history.
623
+ - Live watch-state counters are shown when available.
624
+
625
+ ---
626
+
627
+ ### `tune` — Silence noisy rules
628
+
629
+ ```bash
630
+ npx @shahmilsaari/memory-core tune
631
+ npx @shahmilsaari/memory-core tune --threshold 30
632
+ npx @shahmilsaari/memory-core tune --yes
633
+ ```
634
+
635
+ Interactively reviews rules with a high false-positive rate and adds them to `allowPatterns` in `.memory-core.json`.
636
+
637
+ | Flag | What it does |
638
+ |---|---|
639
+ | `--threshold <n>` | False-positive % cutoff (default `40`) |
640
+ | `--min-count <n>` | Minimum hits required (default `5`) |
641
+ | `--yes` | Silence all qualifying rules without prompting |
602
642
 
603
643
  ---
604
644
 
@@ -852,20 +892,24 @@ npm run smoke:npx
852
892
  | ✓ | NestJS profile and 39 rules |
853
893
  | ✓ | Hook auto-prompt — hook mode offered during init, no separate step needed |
854
894
  | ✓ | CI/CD — `ci-setup` generates GitHub Actions workflow for PR enforcement |
855
- | ✓ | Violation stats — see which rules fire most and which files break most |
856
895
  | ✓ | Agent selection — choose which agents to generate files for during init |
857
896
  | ✓ | Auto-sync — memory-changing commands refresh selected agent files by default |
858
897
  | ✓ | Export / import — portable memories.json for version control and team sharing |
859
898
  | ✓ | List / remove / edit — full CRUD for stored memories |
860
- | ✓ | False positive tagging — `ignore` command saves exceptions for hook and watcher |
861
- | ✓ | Cleanup commands — `reset` regenerates/reinitializes files; `uninstall` removes memory-core from a project |
862
- | ✓ | Test suite — smoke tests for all core commands and providers |
899
+ | ✓ | False positive tagging — `ignore` and `allow` save exceptions for hook and watcher |
900
+ | ✓ | Cleanup commands — `reset` regenerates/reinitializes files; `uninstall` removes memory-core |
901
+ | ✓ | Test suite — 94 tests using Node.js built-in `node:test` |
863
902
  | ✓ | Multi-provider code checking — Ollama, OpenAI, Anthropic, MiniMax |
864
903
  | ✓ | Context-aware retrieval — surface the most relevant rules for the file being edited |
865
904
  | ✓ | Setup management — `status`, `provider set`, `model set`, `model doctor` |
866
905
  | ✓ | `--debug` flag — verbose output for diagnosing hook and watcher issues |
867
- | ✓ | Local Svelte dashboard — WebSocket live feed, runtime status, stats, and architecture-filtered rules |
868
- | ✓ | Live config reload dashboard updates when `.env`, `.memory-core.env`, project config, or stats change |
906
+ | ✓ | Local Svelte dashboard — WebSocket live feed, runtime status, stats, and rules browser |
907
+ | ✓ | Rule cache5-min TTL cache, invalidated by config or DB version change |
908
+ | ✓ | Batch suppression — same rule ≥3× on same file auto-suppressed with a notice |
909
+ | ✓ | False-positive tracking — `{ count, falsePositives }` stored per rule in stats |
910
+ | ✓ | Violation clustering — violations grouped by rule, compact multi-location display |
911
+ | ✓ | Auto-tuning — `memory-core tune` silences noisy rules interactively |
912
+ | ✓ | Commit message linting — `commit-rules` + `commit-msg` git hook |
869
913
  | | Model guidance during init — recommend a model based on machine specs |
870
914
  | | Violation → rule pipeline — auto-suggest a new rule when the same violation repeats |
871
915
  | | Team sync — shared database so the whole team works from the same rule set |
@@ -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 {
@@ -119,13 +124,29 @@ function getChatConfig() {
119
124
  provider,
120
125
  model,
121
126
  ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
122
- apiKey: process.env.CHAT_API_KEY ?? ""
127
+ apiKey: process.env.CHAT_API_KEY ?? "",
128
+ baseUrl: process.env.CHAT_BASE_URL ?? ""
123
129
  };
124
130
  }
125
- async function callOllama(cfg, messages) {
131
+ function getDefaultTimeoutMs() {
132
+ const raw = Number(process.env.CHAT_TIMEOUT_MS ?? 2e4);
133
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 2e4;
134
+ }
135
+ function timeoutSignal(timeoutMs) {
136
+ return AbortSignal.timeout(timeoutMs ?? getDefaultTimeoutMs());
137
+ }
138
+ function normalizeChatError(err, timeoutMs) {
139
+ const ms = timeoutMs ?? getDefaultTimeoutMs();
140
+ if (err instanceof Error && err.name === "AbortError") {
141
+ return new Error(`TIMEOUT:${ms}`);
142
+ }
143
+ return err instanceof Error ? err : new Error(String(err));
144
+ }
145
+ async function callOllama(cfg, messages, options = {}) {
126
146
  const res = await fetch(`${cfg.ollamaUrl}/api/chat`, {
127
147
  method: "POST",
128
148
  headers: { "Content-Type": "application/json" },
149
+ signal: timeoutSignal(options.timeoutMs),
129
150
  body: JSON.stringify({ model: cfg.model, messages, stream: false, format: "json" })
130
151
  });
131
152
  if (!res.ok) {
@@ -138,13 +159,15 @@ async function callOllama(cfg, messages) {
138
159
  const data = await res.json();
139
160
  return data.message.content.trim();
140
161
  }
141
- async function callOpenAI(cfg, messages) {
142
- const res = await fetch("https://api.openai.com/v1/chat/completions", {
162
+ async function callOpenAICompat(cfg, messages, options = {}) {
163
+ const base = (cfg.baseUrl ?? "").replace(/\/$/, "") || "https://api.openai.com/v1";
164
+ const res = await fetch(`${base}/chat/completions`, {
143
165
  method: "POST",
144
166
  headers: {
145
167
  "Content-Type": "application/json",
146
168
  "Authorization": `Bearer ${cfg.apiKey}`
147
169
  },
170
+ signal: timeoutSignal(options.timeoutMs),
148
171
  body: JSON.stringify({
149
172
  model: cfg.model,
150
173
  messages,
@@ -155,7 +178,7 @@ async function callOpenAI(cfg, messages) {
155
178
  const data = await res.json();
156
179
  return data.choices[0].message.content.trim();
157
180
  }
158
- async function callAnthropic(cfg, messages) {
181
+ async function callAnthropic(cfg, messages, options = {}) {
159
182
  const system = messages.find((m) => m.role === "system")?.content ?? "";
160
183
  const userMessages = messages.filter((m) => m.role !== "system");
161
184
  const res = await fetch("https://api.anthropic.com/v1/messages", {
@@ -165,6 +188,7 @@ async function callAnthropic(cfg, messages) {
165
188
  "x-api-key": cfg.apiKey,
166
189
  "anthropic-version": "2023-06-01"
167
190
  },
191
+ signal: timeoutSignal(options.timeoutMs),
168
192
  body: JSON.stringify({
169
193
  model: cfg.model,
170
194
  max_tokens: 4096,
@@ -176,13 +200,14 @@ async function callAnthropic(cfg, messages) {
176
200
  const data = await res.json();
177
201
  return data.content[0].text.trim();
178
202
  }
179
- async function callMiniMax(cfg, messages) {
203
+ async function callMiniMax(cfg, messages, options = {}) {
180
204
  const res = await fetch("https://api.minimax.io/v1/chat/completions", {
181
205
  method: "POST",
182
206
  headers: {
183
207
  "Content-Type": "application/json",
184
208
  "Authorization": `Bearer ${cfg.apiKey}`
185
209
  },
210
+ signal: timeoutSignal(options.timeoutMs),
186
211
  body: JSON.stringify({
187
212
  model: cfg.model,
188
213
  messages,
@@ -193,22 +218,31 @@ async function callMiniMax(cfg, messages) {
193
218
  const data = await res.json();
194
219
  return data.choices[0].message.content.trim();
195
220
  }
196
- async function callChatModel(messages) {
221
+ async function callChatModel(messages, options = {}) {
197
222
  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);
223
+ try {
224
+ switch (cfg.provider) {
225
+ case "openai":
226
+ case "openai-compatible":
227
+ return await callOpenAICompat(cfg, messages, options);
228
+ case "anthropic":
229
+ return await callAnthropic(cfg, messages, options);
230
+ case "minimax":
231
+ return await callMiniMax(cfg, messages, options);
232
+ default:
233
+ return await callOllama(cfg, messages, options);
234
+ }
235
+ } catch (err) {
236
+ throw normalizeChatError(err, options.timeoutMs);
207
237
  }
208
238
  }
209
239
  function getChatProviderLabel() {
210
240
  const cfg = getChatConfig();
211
241
  if (cfg.provider === "ollama") return `ollama (${cfg.model})`;
242
+ if (cfg.provider === "openai-compatible") {
243
+ const host = cfg.baseUrl ? new URL(cfg.baseUrl).hostname : "custom";
244
+ return `openai-compat/${host} (${cfg.model})`;
245
+ }
212
246
  return `${cfg.provider} (${cfg.model})`;
213
247
  }
214
248
 
@@ -218,6 +252,10 @@ import { createHash } from "crypto";
218
252
  var { Pool } = pg;
219
253
  var pool = null;
220
254
  var migrationsRun = false;
255
+ function readPositiveIntEnv(name, fallback) {
256
+ const raw = Number(process.env[name]);
257
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
258
+ }
221
259
  function hashMemoryContent(content) {
222
260
  return createHash("md5").update(content.trim()).digest("hex");
223
261
  }
@@ -226,7 +264,13 @@ function getPool() {
226
264
  if (!Config.databaseUrl) {
227
265
  throw new Error("DATABASE_URL is not set. Add it to your .env or .memory-core.env file.");
228
266
  }
229
- pool = new Pool({ connectionString: Config.databaseUrl });
267
+ const timeoutMs = readPositiveIntEnv("DATABASE_TIMEOUT_MS", 5e3);
268
+ pool = new Pool({
269
+ connectionString: Config.databaseUrl,
270
+ connectionTimeoutMillis: timeoutMs,
271
+ query_timeout: timeoutMs,
272
+ statement_timeout: timeoutMs
273
+ });
230
274
  }
231
275
  return pool;
232
276
  }
@@ -235,6 +279,7 @@ async function runMigrations() {
235
279
  const client = await getPool().connect();
236
280
  try {
237
281
  await client.query("BEGIN");
282
+ await client.query(`ALTER TABLE memories ALTER COLUMN scope SET DEFAULT 'project'`);
238
283
  await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS reason TEXT`);
239
284
  await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS content_hash TEXT`);
240
285
  await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS context JSONB NOT NULL DEFAULT '{}'::jsonb`);
@@ -1243,13 +1288,22 @@ var MemoryEngineService = class {
1243
1288
  }
1244
1289
  memoryRepository;
1245
1290
  embeddingProvider;
1291
+ withReason(input) {
1292
+ const reason = input.reason?.trim();
1293
+ return {
1294
+ ...input,
1295
+ reason: reason || `Captured as a ${input.type} memory because it should be remembered: ${input.content}`
1296
+ };
1297
+ }
1246
1298
  async remember(input) {
1247
- const embedding = await this.embeddingProvider.embed(input.content);
1248
- return this.memoryRepository.upsert({ ...input, embedding });
1299
+ const normalized = this.withReason(input);
1300
+ const embedding = await this.embeddingProvider.embed(normalized.content);
1301
+ return this.memoryRepository.upsert({ ...normalized, embedding });
1249
1302
  }
1250
1303
  async rememberForce(input) {
1251
- const embedding = await this.embeddingProvider.embed(input.content);
1252
- await this.memoryRepository.save({ ...input, embedding });
1304
+ const normalized = this.withReason(input);
1305
+ const embedding = await this.embeddingProvider.embed(normalized.content);
1306
+ await this.memoryRepository.save({ ...normalized, embedding });
1253
1307
  }
1254
1308
  async list(filters = {}) {
1255
1309
  return this.memoryRepository.list(filters);