@shahmilsaari/memory-core 0.1.2 → 0.1.3
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 +41 -1
- package/dist/cli.js +296 -65
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -318,6 +318,46 @@ npx @shahmilsaari/memory-core hook uninstall # remove the hook
|
|
|
318
318
|
|
|
319
319
|
---
|
|
320
320
|
|
|
321
|
+
### `watch` — Real-time rule checking
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
npx @shahmilsaari/memory-core watch
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Runs in your terminal and checks every source file as you save it. Violations print immediately — no commit needed.
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
archmind watch — real-time rule enforcement
|
|
331
|
+
|
|
332
|
+
watching: /your/project
|
|
333
|
+
model: llama3.2
|
|
334
|
+
rules: 24
|
|
335
|
+
ctrl+c to stop
|
|
336
|
+
|
|
337
|
+
[10:42:11] saved: src/controllers/user.ts
|
|
338
|
+
|
|
339
|
+
✗ 1 violation in src/controllers/user.ts
|
|
340
|
+
|
|
341
|
+
[1] src/controllers/user.ts:34
|
|
342
|
+
Rule: Thin controllers — business logic belongs in services
|
|
343
|
+
Why: Logic in controllers cannot be reused from CLI, queues, or other
|
|
344
|
+
entry points — it gets siloed and duplicated across handlers
|
|
345
|
+
Issue: Password hashing inside the route handler
|
|
346
|
+
Fix: Move to UserService.hashPassword()
|
|
347
|
+
|
|
348
|
+
Fix violations or run: memory-core remember "<lesson>"
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Options:
|
|
352
|
+
```bash
|
|
353
|
+
npx @shahmilsaari/memory-core watch --path src/ # watch a specific directory
|
|
354
|
+
npx @shahmilsaari/memory-core watch --verbose # show diff size and model details
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Ignores: `node_modules`, `dist`, `build`, `.git`, config files — only checks source code.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
321
361
|
### `check` — Manual rule check
|
|
322
362
|
|
|
323
363
|
```bash
|
|
@@ -452,7 +492,7 @@ The hook only checks source files (`.ts .tsx .js .jsx .py .php .rb .go .java .cs
|
|
|
452
492
|
| Feature | Description |
|
|
453
493
|
|---|---|
|
|
454
494
|
| CI/CD check | GitHub Actions workflow — fails PRs that violate rules |
|
|
455
|
-
| Watch mode | `memory-core watch` — checks files on save in real-time |
|
|
495
|
+
| ✓ Watch mode | `memory-core watch` — checks files on save in real-time |
|
|
456
496
|
| Violation memory | Auto-save caught violations as "never do X, do Y" memories |
|
|
457
497
|
| Rule analytics | Track which rules break most, which files are worst offenders |
|
|
458
498
|
| Team sync | `memory-core push/pull` — shared memory pool across the whole team |
|
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { input, select, confirm } from "@inquirer/prompts";
|
|
6
|
-
import
|
|
6
|
+
import chalk3 from "chalk";
|
|
7
7
|
import ora from "ora";
|
|
8
|
-
import { readFileSync as
|
|
9
|
-
import { join as
|
|
8
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync2, appendFileSync } from "fs";
|
|
9
|
+
import { join as join6, dirname as dirname2 } from "path";
|
|
10
10
|
import { homedir } from "os";
|
|
11
|
-
import { execSync as
|
|
11
|
+
import { execSync as execSync3 } from "child_process";
|
|
12
12
|
|
|
13
13
|
// src/generator.ts
|
|
14
14
|
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
@@ -626,10 +626,10 @@ function uninstallHook() {
|
|
|
626
626
|
console.log(chalk.green("\n \u2713 Pre-commit hook removed\n"));
|
|
627
627
|
}
|
|
628
628
|
async function checkStaged(options = {}) {
|
|
629
|
-
const
|
|
629
|
+
const SOURCE_EXTENSIONS2 = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
630
630
|
let diff;
|
|
631
631
|
try {
|
|
632
|
-
const stagedFiles = execSync("git diff --cached --name-only", { encoding: "utf-8" }).split("\n").filter((f) => f &&
|
|
632
|
+
const stagedFiles = execSync("git diff --cached --name-only", { encoding: "utf-8" }).split("\n").filter((f) => f && SOURCE_EXTENSIONS2.test(f));
|
|
633
633
|
if (stagedFiles.length === 0) {
|
|
634
634
|
if (options.verbose) console.log(chalk.gray(" No source files staged \u2014 skipping rule check."));
|
|
635
635
|
return;
|
|
@@ -782,57 +782,285 @@ function printModelMissing(model) {
|
|
|
782
782
|
console.log(chalk.gray(" Recommended: llama3.2 | qwen2.5-coder:3b | mistral\n"));
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
+
// src/watcher.ts
|
|
786
|
+
import { watch } from "chokidar";
|
|
787
|
+
import { execSync as execSync2 } from "child_process";
|
|
788
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
789
|
+
import { join as join5, relative } from "path";
|
|
790
|
+
import chalk2 from "chalk";
|
|
791
|
+
var SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
792
|
+
var IGNORE_PATTERNS = [
|
|
793
|
+
/node_modules/,
|
|
794
|
+
/\.git/,
|
|
795
|
+
/dist\//,
|
|
796
|
+
/build\//,
|
|
797
|
+
/coverage\//,
|
|
798
|
+
/\.memory-core/
|
|
799
|
+
];
|
|
800
|
+
var reasonMap2 = new Map(
|
|
801
|
+
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
802
|
+
);
|
|
803
|
+
function loadConfig(cwd) {
|
|
804
|
+
const configPath = join5(cwd, ".memory-core.json");
|
|
805
|
+
if (!existsSync5(configPath)) return null;
|
|
806
|
+
try {
|
|
807
|
+
return JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
808
|
+
} catch {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function getProfileRules(config2) {
|
|
813
|
+
const rules = [];
|
|
814
|
+
const avoids = [];
|
|
815
|
+
if (config2.backendArchitecture) {
|
|
816
|
+
const profile = listProfiles("backend").find((p) => p.name === config2.backendArchitecture);
|
|
817
|
+
if (profile) {
|
|
818
|
+
rules.push(...profile.rules);
|
|
819
|
+
avoids.push(...profile.avoid);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (config2.frontendFramework) {
|
|
823
|
+
const profile = listProfiles("frontend").find((p) => p.name === config2.frontendFramework);
|
|
824
|
+
if (profile) {
|
|
825
|
+
rules.push(...profile.rules);
|
|
826
|
+
avoids.push(...profile.avoid);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return { rules, avoids };
|
|
830
|
+
}
|
|
831
|
+
async function checkFile(filePath, cwd, config2, verbose) {
|
|
832
|
+
const rel = relative(cwd, filePath);
|
|
833
|
+
let diff;
|
|
834
|
+
try {
|
|
835
|
+
diff = execSync2(`git diff HEAD -- "${rel}" 2>/dev/null || git diff --no-index /dev/null "${rel}" 2>/dev/null || true`, {
|
|
836
|
+
encoding: "utf-8",
|
|
837
|
+
cwd
|
|
838
|
+
});
|
|
839
|
+
} catch {
|
|
840
|
+
try {
|
|
841
|
+
diff = execSync2(`git diff --no-index /dev/null "${rel}"`, { encoding: "utf-8", cwd });
|
|
842
|
+
} catch (e) {
|
|
843
|
+
diff = e.stdout ?? "";
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (!diff.trim()) return;
|
|
847
|
+
const { rules, avoids } = getProfileRules(config2);
|
|
848
|
+
if (rules.length === 0) return;
|
|
849
|
+
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
850
|
+
const chatModel = process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
|
|
851
|
+
const MAX_DIFF = 6e3;
|
|
852
|
+
const truncated = diff.length > MAX_DIFF;
|
|
853
|
+
const diffToSend = truncated ? diff.slice(0, MAX_DIFF) + "\n\n[diff truncated]" : diff;
|
|
854
|
+
if (verbose) {
|
|
855
|
+
console.log(chalk2.dim(`
|
|
856
|
+
[watch] checking ${rel} (${diff.length} chars)\u2026`));
|
|
857
|
+
}
|
|
858
|
+
const rulesWithReasons = rules.map((r, i) => {
|
|
859
|
+
const why = reasonMap2.get(r);
|
|
860
|
+
return why ? `${i + 1}. ${r}
|
|
861
|
+
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
862
|
+
}).join("\n");
|
|
863
|
+
const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
|
|
864
|
+
Analyze the file diff and identify ONLY clear, definite rule violations.
|
|
865
|
+
Use the WHY for each rule to understand intent and judge edge cases.
|
|
866
|
+
|
|
867
|
+
Rules to enforce:
|
|
868
|
+
${rulesWithReasons}
|
|
869
|
+
|
|
870
|
+
Things that must never appear:
|
|
871
|
+
${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
|
|
872
|
+
|
|
873
|
+
IMPORTANT: Respond with JSON: {"violations":[...]} or {"violations":[]}.
|
|
874
|
+
Each violation: {"rule":"...","file":"...","line":N,"issue":"...","suggestion":"...","reason":"..."}.
|
|
875
|
+
No text outside the JSON.`;
|
|
876
|
+
try {
|
|
877
|
+
const res = await fetch(`${ollamaUrl}/api/chat`, {
|
|
878
|
+
method: "POST",
|
|
879
|
+
headers: { "Content-Type": "application/json" },
|
|
880
|
+
body: JSON.stringify({
|
|
881
|
+
model: chatModel,
|
|
882
|
+
messages: [
|
|
883
|
+
{ role: "system", content: systemPrompt },
|
|
884
|
+
{ role: "user", content: `Review this diff for ${rel}:
|
|
885
|
+
|
|
886
|
+
${diffToSend}` }
|
|
887
|
+
],
|
|
888
|
+
stream: false,
|
|
889
|
+
format: "json"
|
|
890
|
+
})
|
|
891
|
+
});
|
|
892
|
+
if (!res.ok) {
|
|
893
|
+
const body = await res.text();
|
|
894
|
+
if (body.includes("not found") || body.includes("model")) {
|
|
895
|
+
console.log(chalk2.yellow(`
|
|
896
|
+
\u26A0 Chat model "${chatModel}" not found. Pull it: ollama pull ${chatModel}
|
|
897
|
+
`));
|
|
898
|
+
}
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const data = await res.json();
|
|
902
|
+
const raw = data.message.content.trim();
|
|
903
|
+
let violations = [];
|
|
904
|
+
try {
|
|
905
|
+
const parsed = JSON.parse(raw);
|
|
906
|
+
if (Array.isArray(parsed)) {
|
|
907
|
+
violations = parsed;
|
|
908
|
+
} else if (Array.isArray(parsed?.violations)) {
|
|
909
|
+
violations = parsed.violations;
|
|
910
|
+
} else if (parsed?.rule) {
|
|
911
|
+
violations = [parsed];
|
|
912
|
+
}
|
|
913
|
+
} catch {
|
|
914
|
+
violations = [];
|
|
915
|
+
}
|
|
916
|
+
if (violations.length === 0) {
|
|
917
|
+
console.log(chalk2.green(` \u2713 ${rel}`) + chalk2.dim(" \u2014 no violations"));
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
console.log(
|
|
921
|
+
chalk2.red.bold(`
|
|
922
|
+
\u2717 ${violations.length} violation${violations.length > 1 ? "s" : ""} in ${rel}
|
|
923
|
+
`)
|
|
924
|
+
);
|
|
925
|
+
violations.forEach((v, i) => {
|
|
926
|
+
const loc = v.line ? `${v.file ?? rel}:${v.line}` : v.file ?? rel;
|
|
927
|
+
console.log(chalk2.bold(` [${i + 1}] ${loc}`));
|
|
928
|
+
console.log(chalk2.yellow(" Rule: ") + v.rule);
|
|
929
|
+
const why = v.reason ?? reasonMap2.get(v.rule);
|
|
930
|
+
if (why) console.log(chalk2.dim(" Why: ") + chalk2.dim(why));
|
|
931
|
+
if (v.issue) console.log(chalk2.red(" Issue: ") + v.issue);
|
|
932
|
+
if (v.suggestion) console.log(chalk2.green(" Fix: ") + v.suggestion);
|
|
933
|
+
console.log();
|
|
934
|
+
});
|
|
935
|
+
console.log(chalk2.dim(' Fix violations or run: memory-core remember "<lesson>"'));
|
|
936
|
+
console.log();
|
|
937
|
+
} catch (err) {
|
|
938
|
+
if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (verbose) {
|
|
942
|
+
console.log(chalk2.yellow(` \u26A0 Check failed for ${rel}: ${err.message}`));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function startWatch(options = {}) {
|
|
947
|
+
const cwd = process.cwd();
|
|
948
|
+
const config2 = loadConfig(cwd);
|
|
949
|
+
if (!config2) {
|
|
950
|
+
console.error(chalk2.red("\n No .memory-core.json found. Run: memory-core init\n"));
|
|
951
|
+
process.exit(1);
|
|
952
|
+
}
|
|
953
|
+
const { rules } = getProfileRules(config2);
|
|
954
|
+
if (rules.length === 0) {
|
|
955
|
+
console.log(chalk2.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to watch.\n"));
|
|
956
|
+
process.exit(0);
|
|
957
|
+
}
|
|
958
|
+
const watchPath = options.path ?? cwd;
|
|
959
|
+
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
960
|
+
const chatModel = process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
|
|
961
|
+
console.log(chalk2.cyan("\n archmind watch \u2014 real-time rule enforcement\n"));
|
|
962
|
+
console.log(chalk2.dim(` watching: ${watchPath}`));
|
|
963
|
+
console.log(chalk2.dim(` model: ${chatModel}`));
|
|
964
|
+
console.log(chalk2.dim(` rules: ${rules.length}`));
|
|
965
|
+
console.log(chalk2.dim(" ctrl+c to stop\n"));
|
|
966
|
+
const pending = /* @__PURE__ */ new Map();
|
|
967
|
+
let ollamaWarned = false;
|
|
968
|
+
const watcher = watch(watchPath, {
|
|
969
|
+
ignored: (filePath) => {
|
|
970
|
+
if (IGNORE_PATTERNS.some((p) => p.test(filePath))) return true;
|
|
971
|
+
const isFile = !filePath.endsWith("/");
|
|
972
|
+
if (isFile && !SOURCE_EXTENSIONS.test(filePath)) return true;
|
|
973
|
+
return false;
|
|
974
|
+
},
|
|
975
|
+
ignoreInitial: true,
|
|
976
|
+
persistent: true,
|
|
977
|
+
awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
|
|
978
|
+
});
|
|
979
|
+
const handle = (filePath) => {
|
|
980
|
+
if (pending.has(filePath)) clearTimeout(pending.get(filePath));
|
|
981
|
+
const timer = setTimeout(async () => {
|
|
982
|
+
pending.delete(filePath);
|
|
983
|
+
console.log(chalk2.dim(`
|
|
984
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] saved: ${relative(cwd, filePath)}`));
|
|
985
|
+
try {
|
|
986
|
+
const ping = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(2e3) });
|
|
987
|
+
if (!ping.ok) throw new Error("not ok");
|
|
988
|
+
ollamaWarned = false;
|
|
989
|
+
} catch {
|
|
990
|
+
if (!ollamaWarned) {
|
|
991
|
+
console.log(chalk2.yellow(` \u26A0 Ollama not running at ${ollamaUrl} \u2014 skipping check.`));
|
|
992
|
+
console.log(chalk2.gray(" Start it: ollama serve\n"));
|
|
993
|
+
ollamaWarned = true;
|
|
994
|
+
}
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
await checkFile(filePath, cwd, config2, options.verbose ?? false);
|
|
998
|
+
}, 300);
|
|
999
|
+
pending.set(filePath, timer);
|
|
1000
|
+
};
|
|
1001
|
+
watcher.on("add", handle);
|
|
1002
|
+
watcher.on("change", handle);
|
|
1003
|
+
watcher.on("error", (err) => {
|
|
1004
|
+
console.error(chalk2.red(` watcher error: ${err.message}`));
|
|
1005
|
+
});
|
|
1006
|
+
process.on("SIGINT", () => {
|
|
1007
|
+
console.log(chalk2.dim("\n\n archmind watch stopped.\n"));
|
|
1008
|
+
watcher.close();
|
|
1009
|
+
process.exit(0);
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
785
1013
|
// src/cli.ts
|
|
786
1014
|
function printBanner(projectName, agentCount) {
|
|
787
1015
|
const lines = [
|
|
788
1016
|
"",
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1017
|
+
chalk3.cyan(" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 "),
|
|
1018
|
+
chalk3.cyan(" \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557"),
|
|
1019
|
+
chalk3.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551"),
|
|
1020
|
+
chalk3.cyan(" \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551"),
|
|
1021
|
+
chalk3.cyan(" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D"),
|
|
1022
|
+
chalk3.cyan(" \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D"),
|
|
795
1023
|
"",
|
|
796
|
-
|
|
1024
|
+
chalk3.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 ") + chalk3.bold.white("C O R E") + chalk3.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"),
|
|
797
1025
|
"",
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1026
|
+
chalk3.green(` \u2713 Project `) + chalk3.bold(projectName),
|
|
1027
|
+
chalk3.green(` \u2713 Agents `) + chalk3.bold(`${agentCount} AI agents configured`),
|
|
1028
|
+
chalk3.green(` \u2713 Memory `) + chalk3.bold("PostgreSQL + pgvector ready"),
|
|
801
1029
|
"",
|
|
802
|
-
|
|
1030
|
+
chalk3.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\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"),
|
|
803
1031
|
"",
|
|
804
|
-
|
|
1032
|
+
chalk3.bold(" Every AI agent in this project now follows your rules."),
|
|
805
1033
|
"",
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1034
|
+
chalk3.gray(" Next steps:"),
|
|
1035
|
+
chalk3.gray(' memory-core remember "Your architectural decision"'),
|
|
1036
|
+
chalk3.gray(' memory-core search "query"'),
|
|
1037
|
+
chalk3.gray(" memory-core sync"),
|
|
810
1038
|
""
|
|
811
1039
|
];
|
|
812
1040
|
lines.forEach((l) => console.log(l));
|
|
813
1041
|
}
|
|
814
1042
|
var CONFIG_FILE = ".memory-core.json";
|
|
815
1043
|
function readProjectConfig() {
|
|
816
|
-
const path =
|
|
817
|
-
if (!
|
|
1044
|
+
const path = join6(process.cwd(), CONFIG_FILE);
|
|
1045
|
+
if (!existsSync6(path)) return null;
|
|
818
1046
|
try {
|
|
819
|
-
return JSON.parse(
|
|
1047
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
820
1048
|
} catch {
|
|
821
1049
|
return null;
|
|
822
1050
|
}
|
|
823
1051
|
}
|
|
824
1052
|
function writeProjectConfig(config2) {
|
|
825
|
-
writeFileSync3(
|
|
1053
|
+
writeFileSync3(join6(process.cwd(), CONFIG_FILE), JSON.stringify(config2, null, 2));
|
|
826
1054
|
}
|
|
827
1055
|
var program = new Command();
|
|
828
1056
|
program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version("0.1.0");
|
|
829
1057
|
program.command("init").description("Initialize memory-core in the current project").action(async () => {
|
|
830
|
-
console.log(
|
|
1058
|
+
console.log(chalk3.bold.cyan("\n memory-core init\n"));
|
|
831
1059
|
const detected = detectProject();
|
|
832
|
-
const envPath =
|
|
833
|
-
const hasEnv =
|
|
1060
|
+
const envPath = join6(process.cwd(), ".memory-core.env");
|
|
1061
|
+
const hasEnv = existsSync6(envPath) || existsSync6(join6(process.cwd(), ".env")) || !!process.env.DATABASE_URL;
|
|
834
1062
|
if (!hasEnv) {
|
|
835
|
-
console.log(
|
|
1063
|
+
console.log(chalk3.dim(" No .memory-core.env found \u2014 let's set up your database connection.\n"));
|
|
836
1064
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
837
1065
|
const dbUrl = await input({
|
|
838
1066
|
message: "PostgreSQL connection URL?",
|
|
@@ -853,17 +1081,17 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
853
1081
|
process.env.OLLAMA_URL = ollamaUrl;
|
|
854
1082
|
process.env.OLLAMA_MODEL = "nomic-embed-text";
|
|
855
1083
|
process.env.OLLAMA_CHAT_MODEL = "llama3.2";
|
|
856
|
-
const gitignorePath =
|
|
857
|
-
if (
|
|
858
|
-
const gi =
|
|
1084
|
+
const gitignorePath = join6(process.cwd(), ".gitignore");
|
|
1085
|
+
if (existsSync6(gitignorePath)) {
|
|
1086
|
+
const gi = readFileSync5(gitignorePath, "utf-8");
|
|
859
1087
|
if (!gi.includes(".memory-core.env")) {
|
|
860
1088
|
appendFileSync(gitignorePath, "\n.memory-core.env\n");
|
|
861
1089
|
}
|
|
862
1090
|
} else {
|
|
863
1091
|
writeFileSync3(gitignorePath, ".memory-core.env\n");
|
|
864
1092
|
}
|
|
865
|
-
console.log(
|
|
866
|
-
console.log(
|
|
1093
|
+
console.log(chalk3.green("\n \u2713 .memory-core.env created"));
|
|
1094
|
+
console.log(chalk3.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
|
|
867
1095
|
}
|
|
868
1096
|
const projectName = await input({
|
|
869
1097
|
message: "Project name?",
|
|
@@ -944,7 +1172,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
944
1172
|
if (installCaveman) {
|
|
945
1173
|
const spinner2 = ora("Installing caveman token saver\u2026").start();
|
|
946
1174
|
try {
|
|
947
|
-
|
|
1175
|
+
execSync3(
|
|
948
1176
|
"curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash",
|
|
949
1177
|
{ stdio: "pipe", cwd: process.cwd() }
|
|
950
1178
|
);
|
|
@@ -966,7 +1194,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
966
1194
|
program.command("sync").description("Re-pull memories and regenerate all AI agent files").action(async () => {
|
|
967
1195
|
const config2 = readProjectConfig();
|
|
968
1196
|
if (!config2) {
|
|
969
|
-
console.error(
|
|
1197
|
+
console.error(chalk3.red("No .memory-core.json found. Run: memory-core init"));
|
|
970
1198
|
process.exit(1);
|
|
971
1199
|
}
|
|
972
1200
|
const spinner = ora("Syncing memories\u2026").start();
|
|
@@ -998,7 +1226,7 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
998
1226
|
let reason = opts.reason;
|
|
999
1227
|
if (!reason) {
|
|
1000
1228
|
reason = await input({
|
|
1001
|
-
message:
|
|
1229
|
+
message: chalk3.dim("Why does this rule exist? (optional \u2014 helps agents debug violations)"),
|
|
1002
1230
|
default: ""
|
|
1003
1231
|
});
|
|
1004
1232
|
}
|
|
@@ -1015,9 +1243,9 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
1015
1243
|
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [],
|
|
1016
1244
|
embedding
|
|
1017
1245
|
});
|
|
1018
|
-
const reasonLine = reason ?
|
|
1246
|
+
const reasonLine = reason ? chalk3.gray(`
|
|
1019
1247
|
Why: ${reason}`) : "";
|
|
1020
|
-
spinner.succeed(
|
|
1248
|
+
spinner.succeed(chalk3.green(`Memory saved: "${text}"`) + reasonLine);
|
|
1021
1249
|
} catch (err) {
|
|
1022
1250
|
spinner.fail(`Failed: ${err.message}`);
|
|
1023
1251
|
process.exit(1);
|
|
@@ -1035,16 +1263,16 @@ program.command("search <query>").description("Search memories using semantic si
|
|
|
1035
1263
|
);
|
|
1036
1264
|
spinner.stop();
|
|
1037
1265
|
if (results.length === 0) {
|
|
1038
|
-
console.log(
|
|
1266
|
+
console.log(chalk3.yellow("No memories found."));
|
|
1039
1267
|
} else {
|
|
1040
|
-
console.log(
|
|
1268
|
+
console.log(chalk3.bold(`
|
|
1041
1269
|
${results.length} results for "${query}"
|
|
1042
1270
|
`));
|
|
1043
1271
|
results.forEach((m, i) => {
|
|
1044
|
-
const sim = m.similarity ?
|
|
1045
|
-
console.log(
|
|
1046
|
-
console.log(
|
|
1047
|
-
if (m.tags?.length) console.log(
|
|
1272
|
+
const sim = m.similarity ? chalk3.gray(` (${(m.similarity * 100).toFixed(0)}% match)`) : "";
|
|
1273
|
+
console.log(chalk3.cyan(` ${i + 1}. [${m.type}] ${m.title ?? ""}`));
|
|
1274
|
+
console.log(chalk3.white(` ${m.content}`) + sim);
|
|
1275
|
+
if (m.tags?.length) console.log(chalk3.gray(` tags: ${m.tags.join(", ")}`));
|
|
1048
1276
|
console.log();
|
|
1049
1277
|
});
|
|
1050
1278
|
}
|
|
@@ -1057,7 +1285,7 @@ program.command("search <query>").description("Search memories using semantic si
|
|
|
1057
1285
|
program.command("seed").description("Load all predefined memories into the database").option("--arch <architecture>", "Only seed a specific architecture (e.g. clean-architecture)").option("--force", "Re-seed even if memories already exist", false).action(async (opts) => {
|
|
1058
1286
|
await runMigrations();
|
|
1059
1287
|
const filtered = opts.arch ? seeds.filter((s) => s.architecture === opts.arch || s.architecture === "global") : seeds;
|
|
1060
|
-
console.log(
|
|
1288
|
+
console.log(chalk3.bold.cyan(`
|
|
1061
1289
|
Seeding ${filtered.length} memories\u2026
|
|
1062
1290
|
`));
|
|
1063
1291
|
let saved = 0;
|
|
@@ -1076,14 +1304,14 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
1076
1304
|
tags: seed.tags,
|
|
1077
1305
|
embedding
|
|
1078
1306
|
});
|
|
1079
|
-
spinner.succeed(
|
|
1307
|
+
spinner.succeed(chalk3.gray(`[${seed.architecture}] ${seed.title}`));
|
|
1080
1308
|
saved++;
|
|
1081
1309
|
} catch (err) {
|
|
1082
1310
|
spinner.warn(`Skipped \u2014 ${err.message}`);
|
|
1083
1311
|
skipped++;
|
|
1084
1312
|
}
|
|
1085
1313
|
}
|
|
1086
|
-
console.log(
|
|
1314
|
+
console.log(chalk3.bold.green(`
|
|
1087
1315
|
Done. ${saved} memories seeded, ${skipped} skipped.
|
|
1088
1316
|
`));
|
|
1089
1317
|
await closePool();
|
|
@@ -1092,21 +1320,21 @@ program.command("global").description("Sync your memory into every AI agent glob
|
|
|
1092
1320
|
const home = homedir();
|
|
1093
1321
|
const GLOBAL_TARGETS = [
|
|
1094
1322
|
// Claude Code
|
|
1095
|
-
{ label: "Claude Code", path:
|
|
1323
|
+
{ label: "Claude Code", path: join6(home, ".claude/CLAUDE.md"), type: "md" },
|
|
1096
1324
|
// GitHub Copilot (VS Code)
|
|
1097
|
-
{ label: "Copilot", path:
|
|
1325
|
+
{ label: "Copilot", path: join6(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-copilot" },
|
|
1098
1326
|
// Cursor global rules
|
|
1099
|
-
{ label: "Cursor", path:
|
|
1327
|
+
{ label: "Cursor", path: join6(home, ".cursor/rules/memory-core.mdc"), type: "md" },
|
|
1100
1328
|
// Cline (VS Code)
|
|
1101
|
-
{ label: "Cline", path:
|
|
1329
|
+
{ label: "Cline", path: join6(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-cline" },
|
|
1102
1330
|
// Continue.dev global config
|
|
1103
|
-
{ label: "Continue.dev", path:
|
|
1331
|
+
{ label: "Continue.dev", path: join6(home, ".continue/config.json"), type: "continue" },
|
|
1104
1332
|
// Aider global config
|
|
1105
|
-
{ label: "Aider", path:
|
|
1333
|
+
{ label: "Aider", path: join6(home, ".aider.conf.yml"), type: "aider" },
|
|
1106
1334
|
// Zed global settings
|
|
1107
|
-
{ label: "Zed AI", path:
|
|
1335
|
+
{ label: "Zed AI", path: join6(home, ".config/zed/settings.json"), type: "zed" },
|
|
1108
1336
|
// Windsurf global rules
|
|
1109
|
-
{ label: "Windsurf", path:
|
|
1337
|
+
{ label: "Windsurf", path: join6(home, ".windsurf/rules/memory-core.md"), type: "md" }
|
|
1110
1338
|
];
|
|
1111
1339
|
const spinner = ora("Fetching global memories\u2026").start();
|
|
1112
1340
|
let memories = [];
|
|
@@ -1134,9 +1362,9 @@ ${rulesText}
|
|
|
1134
1362
|
writeFileSync3(filePath, content, "utf-8");
|
|
1135
1363
|
};
|
|
1136
1364
|
const readJson = (filePath) => {
|
|
1137
|
-
if (!
|
|
1365
|
+
if (!existsSync6(filePath)) return {};
|
|
1138
1366
|
try {
|
|
1139
|
-
return JSON.parse(
|
|
1367
|
+
return JSON.parse(readFileSync5(filePath, "utf-8"));
|
|
1140
1368
|
} catch {
|
|
1141
1369
|
return {};
|
|
1142
1370
|
}
|
|
@@ -1184,14 +1412,14 @@ read:
|
|
|
1184
1412
|
skipped.push(target.label);
|
|
1185
1413
|
}
|
|
1186
1414
|
}
|
|
1187
|
-
spinner.succeed(
|
|
1188
|
-
console.log(
|
|
1189
|
-
written.forEach((l) => console.log(
|
|
1415
|
+
spinner.succeed(chalk3.green(`Synced ${memories.length} memories \u2192 ${written.length} agents`));
|
|
1416
|
+
console.log(chalk3.green("\n Updated:"));
|
|
1417
|
+
written.forEach((l) => console.log(chalk3.gray(` \u2713 ${l}`)));
|
|
1190
1418
|
if (skipped.length) {
|
|
1191
|
-
console.log(
|
|
1192
|
-
skipped.forEach((l) => console.log(
|
|
1419
|
+
console.log(chalk3.yellow("\n Skipped (not installed):"));
|
|
1420
|
+
skipped.forEach((l) => console.log(chalk3.gray(` \u2717 ${l}`)));
|
|
1193
1421
|
}
|
|
1194
|
-
console.log(
|
|
1422
|
+
console.log(chalk3.bold("\n Every AI agent now follows your memory globally.\n"));
|
|
1195
1423
|
await closePool();
|
|
1196
1424
|
});
|
|
1197
1425
|
var hook = program.command("hook").description("Manage the pre-commit rule enforcement hook");
|
|
@@ -1204,4 +1432,7 @@ hook.command("uninstall").description("Remove the pre-commit hook").action(() =>
|
|
|
1204
1432
|
program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--verbose", "Show model and diff details").action(async (opts) => {
|
|
1205
1433
|
await checkStaged({ verbose: opts.verbose ?? false });
|
|
1206
1434
|
});
|
|
1435
|
+
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("--verbose", "Show diff size and model details per file").action((opts) => {
|
|
1436
|
+
startWatch({ path: opts.path, verbose: opts.verbose });
|
|
1437
|
+
});
|
|
1207
1438
|
program.parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shahmilsaari/memory-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Universal AI memory core — generate AI context files from architecture profiles with RAG support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@inquirer/prompts": "^5.0.0",
|
|
21
21
|
"chalk": "^5.3.0",
|
|
22
|
+
"chokidar": "^5.0.0",
|
|
22
23
|
"commander": "^12.0.0",
|
|
23
24
|
"dotenv": "^16.4.0",
|
|
24
25
|
"handlebars": "^4.7.8",
|