@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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
|
1248
|
-
|
|
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
|
|
1252
|
-
await this.
|
|
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-
|
|
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
|
|
139
|
+
memory-core ${checkArgs}${suffix}
|
|
139
140
|
elif [ -f "./node_modules/.bin/memory-core" ]; then
|
|
140
|
-
./node_modules/.bin/memory-core
|
|
141
|
+
./node_modules/.bin/memory-core ${checkArgs}${suffix}
|
|
141
142
|
elif [ -f "./dist/cli.js" ]; then
|
|
142
|
-
node ./dist/cli.js
|
|
143
|
+
node ./dist/cli.js ${checkArgs}${suffix}
|
|
143
144
|
else
|
|
144
|
-
npx --no-install memory-core
|
|
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:
|
|
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
|
|
724
|
-
|
|
725
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
2030
|
+
reason: storedReason,
|
|
1979
2031
|
context: buildMemoryContext(opts),
|
|
1980
2032
|
tags: parseTags(opts.tags)
|
|
1981
2033
|
});
|
|
1982
|
-
const reasonLine =
|
|
1983
|
-
Why: ${
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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.
|
|
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",
|