@shahmilsaari/memory-core 0.1.1 → 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 +324 -57
- 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,53 +782,317 @@ 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();
|
|
1060
|
+
const envPath = join6(process.cwd(), ".memory-core.env");
|
|
1061
|
+
const hasEnv = existsSync6(envPath) || existsSync6(join6(process.cwd(), ".env")) || !!process.env.DATABASE_URL;
|
|
1062
|
+
if (!hasEnv) {
|
|
1063
|
+
console.log(chalk3.dim(" No .memory-core.env found \u2014 let's set up your database connection.\n"));
|
|
1064
|
+
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
1065
|
+
const dbUrl = await input({
|
|
1066
|
+
message: "PostgreSQL connection URL?",
|
|
1067
|
+
default: `postgresql://${dbUser}@localhost:5432/memory_core`
|
|
1068
|
+
});
|
|
1069
|
+
const ollamaUrl = await input({
|
|
1070
|
+
message: "Ollama URL?",
|
|
1071
|
+
default: "http://localhost:11434"
|
|
1072
|
+
});
|
|
1073
|
+
const envContent = [
|
|
1074
|
+
`DATABASE_URL=${dbUrl}`,
|
|
1075
|
+
`OLLAMA_URL=${ollamaUrl}`,
|
|
1076
|
+
`OLLAMA_MODEL=nomic-embed-text`,
|
|
1077
|
+
`OLLAMA_CHAT_MODEL=llama3.2`
|
|
1078
|
+
].join("\n") + "\n";
|
|
1079
|
+
writeFileSync3(envPath, envContent);
|
|
1080
|
+
process.env.DATABASE_URL = dbUrl;
|
|
1081
|
+
process.env.OLLAMA_URL = ollamaUrl;
|
|
1082
|
+
process.env.OLLAMA_MODEL = "nomic-embed-text";
|
|
1083
|
+
process.env.OLLAMA_CHAT_MODEL = "llama3.2";
|
|
1084
|
+
const gitignorePath = join6(process.cwd(), ".gitignore");
|
|
1085
|
+
if (existsSync6(gitignorePath)) {
|
|
1086
|
+
const gi = readFileSync5(gitignorePath, "utf-8");
|
|
1087
|
+
if (!gi.includes(".memory-core.env")) {
|
|
1088
|
+
appendFileSync(gitignorePath, "\n.memory-core.env\n");
|
|
1089
|
+
}
|
|
1090
|
+
} else {
|
|
1091
|
+
writeFileSync3(gitignorePath, ".memory-core.env\n");
|
|
1092
|
+
}
|
|
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"));
|
|
1095
|
+
}
|
|
832
1096
|
const projectName = await input({
|
|
833
1097
|
message: "Project name?",
|
|
834
1098
|
default: process.cwd().split("/").pop() ?? "my-project"
|
|
@@ -908,7 +1172,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
908
1172
|
if (installCaveman) {
|
|
909
1173
|
const spinner2 = ora("Installing caveman token saver\u2026").start();
|
|
910
1174
|
try {
|
|
911
|
-
|
|
1175
|
+
execSync3(
|
|
912
1176
|
"curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash",
|
|
913
1177
|
{ stdio: "pipe", cwd: process.cwd() }
|
|
914
1178
|
);
|
|
@@ -930,7 +1194,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
930
1194
|
program.command("sync").description("Re-pull memories and regenerate all AI agent files").action(async () => {
|
|
931
1195
|
const config2 = readProjectConfig();
|
|
932
1196
|
if (!config2) {
|
|
933
|
-
console.error(
|
|
1197
|
+
console.error(chalk3.red("No .memory-core.json found. Run: memory-core init"));
|
|
934
1198
|
process.exit(1);
|
|
935
1199
|
}
|
|
936
1200
|
const spinner = ora("Syncing memories\u2026").start();
|
|
@@ -962,7 +1226,7 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
962
1226
|
let reason = opts.reason;
|
|
963
1227
|
if (!reason) {
|
|
964
1228
|
reason = await input({
|
|
965
|
-
message:
|
|
1229
|
+
message: chalk3.dim("Why does this rule exist? (optional \u2014 helps agents debug violations)"),
|
|
966
1230
|
default: ""
|
|
967
1231
|
});
|
|
968
1232
|
}
|
|
@@ -979,9 +1243,9 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
979
1243
|
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [],
|
|
980
1244
|
embedding
|
|
981
1245
|
});
|
|
982
|
-
const reasonLine = reason ?
|
|
1246
|
+
const reasonLine = reason ? chalk3.gray(`
|
|
983
1247
|
Why: ${reason}`) : "";
|
|
984
|
-
spinner.succeed(
|
|
1248
|
+
spinner.succeed(chalk3.green(`Memory saved: "${text}"`) + reasonLine);
|
|
985
1249
|
} catch (err) {
|
|
986
1250
|
spinner.fail(`Failed: ${err.message}`);
|
|
987
1251
|
process.exit(1);
|
|
@@ -999,16 +1263,16 @@ program.command("search <query>").description("Search memories using semantic si
|
|
|
999
1263
|
);
|
|
1000
1264
|
spinner.stop();
|
|
1001
1265
|
if (results.length === 0) {
|
|
1002
|
-
console.log(
|
|
1266
|
+
console.log(chalk3.yellow("No memories found."));
|
|
1003
1267
|
} else {
|
|
1004
|
-
console.log(
|
|
1268
|
+
console.log(chalk3.bold(`
|
|
1005
1269
|
${results.length} results for "${query}"
|
|
1006
1270
|
`));
|
|
1007
1271
|
results.forEach((m, i) => {
|
|
1008
|
-
const sim = m.similarity ?
|
|
1009
|
-
console.log(
|
|
1010
|
-
console.log(
|
|
1011
|
-
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(", ")}`));
|
|
1012
1276
|
console.log();
|
|
1013
1277
|
});
|
|
1014
1278
|
}
|
|
@@ -1021,7 +1285,7 @@ program.command("search <query>").description("Search memories using semantic si
|
|
|
1021
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) => {
|
|
1022
1286
|
await runMigrations();
|
|
1023
1287
|
const filtered = opts.arch ? seeds.filter((s) => s.architecture === opts.arch || s.architecture === "global") : seeds;
|
|
1024
|
-
console.log(
|
|
1288
|
+
console.log(chalk3.bold.cyan(`
|
|
1025
1289
|
Seeding ${filtered.length} memories\u2026
|
|
1026
1290
|
`));
|
|
1027
1291
|
let saved = 0;
|
|
@@ -1040,14 +1304,14 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
1040
1304
|
tags: seed.tags,
|
|
1041
1305
|
embedding
|
|
1042
1306
|
});
|
|
1043
|
-
spinner.succeed(
|
|
1307
|
+
spinner.succeed(chalk3.gray(`[${seed.architecture}] ${seed.title}`));
|
|
1044
1308
|
saved++;
|
|
1045
1309
|
} catch (err) {
|
|
1046
1310
|
spinner.warn(`Skipped \u2014 ${err.message}`);
|
|
1047
1311
|
skipped++;
|
|
1048
1312
|
}
|
|
1049
1313
|
}
|
|
1050
|
-
console.log(
|
|
1314
|
+
console.log(chalk3.bold.green(`
|
|
1051
1315
|
Done. ${saved} memories seeded, ${skipped} skipped.
|
|
1052
1316
|
`));
|
|
1053
1317
|
await closePool();
|
|
@@ -1056,21 +1320,21 @@ program.command("global").description("Sync your memory into every AI agent glob
|
|
|
1056
1320
|
const home = homedir();
|
|
1057
1321
|
const GLOBAL_TARGETS = [
|
|
1058
1322
|
// Claude Code
|
|
1059
|
-
{ label: "Claude Code", path:
|
|
1323
|
+
{ label: "Claude Code", path: join6(home, ".claude/CLAUDE.md"), type: "md" },
|
|
1060
1324
|
// GitHub Copilot (VS Code)
|
|
1061
|
-
{ label: "Copilot", path:
|
|
1325
|
+
{ label: "Copilot", path: join6(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-copilot" },
|
|
1062
1326
|
// Cursor global rules
|
|
1063
|
-
{ label: "Cursor", path:
|
|
1327
|
+
{ label: "Cursor", path: join6(home, ".cursor/rules/memory-core.mdc"), type: "md" },
|
|
1064
1328
|
// Cline (VS Code)
|
|
1065
|
-
{ label: "Cline", path:
|
|
1329
|
+
{ label: "Cline", path: join6(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-cline" },
|
|
1066
1330
|
// Continue.dev global config
|
|
1067
|
-
{ label: "Continue.dev", path:
|
|
1331
|
+
{ label: "Continue.dev", path: join6(home, ".continue/config.json"), type: "continue" },
|
|
1068
1332
|
// Aider global config
|
|
1069
|
-
{ label: "Aider", path:
|
|
1333
|
+
{ label: "Aider", path: join6(home, ".aider.conf.yml"), type: "aider" },
|
|
1070
1334
|
// Zed global settings
|
|
1071
|
-
{ label: "Zed AI", path:
|
|
1335
|
+
{ label: "Zed AI", path: join6(home, ".config/zed/settings.json"), type: "zed" },
|
|
1072
1336
|
// Windsurf global rules
|
|
1073
|
-
{ label: "Windsurf", path:
|
|
1337
|
+
{ label: "Windsurf", path: join6(home, ".windsurf/rules/memory-core.md"), type: "md" }
|
|
1074
1338
|
];
|
|
1075
1339
|
const spinner = ora("Fetching global memories\u2026").start();
|
|
1076
1340
|
let memories = [];
|
|
@@ -1098,9 +1362,9 @@ ${rulesText}
|
|
|
1098
1362
|
writeFileSync3(filePath, content, "utf-8");
|
|
1099
1363
|
};
|
|
1100
1364
|
const readJson = (filePath) => {
|
|
1101
|
-
if (!
|
|
1365
|
+
if (!existsSync6(filePath)) return {};
|
|
1102
1366
|
try {
|
|
1103
|
-
return JSON.parse(
|
|
1367
|
+
return JSON.parse(readFileSync5(filePath, "utf-8"));
|
|
1104
1368
|
} catch {
|
|
1105
1369
|
return {};
|
|
1106
1370
|
}
|
|
@@ -1148,14 +1412,14 @@ read:
|
|
|
1148
1412
|
skipped.push(target.label);
|
|
1149
1413
|
}
|
|
1150
1414
|
}
|
|
1151
|
-
spinner.succeed(
|
|
1152
|
-
console.log(
|
|
1153
|
-
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}`)));
|
|
1154
1418
|
if (skipped.length) {
|
|
1155
|
-
console.log(
|
|
1156
|
-
skipped.forEach((l) => console.log(
|
|
1419
|
+
console.log(chalk3.yellow("\n Skipped (not installed):"));
|
|
1420
|
+
skipped.forEach((l) => console.log(chalk3.gray(` \u2717 ${l}`)));
|
|
1157
1421
|
}
|
|
1158
|
-
console.log(
|
|
1422
|
+
console.log(chalk3.bold("\n Every AI agent now follows your memory globally.\n"));
|
|
1159
1423
|
await closePool();
|
|
1160
1424
|
});
|
|
1161
1425
|
var hook = program.command("hook").description("Manage the pre-commit rule enforcement hook");
|
|
@@ -1168,4 +1432,7 @@ hook.command("uninstall").description("Remove the pre-commit hook").action(() =>
|
|
|
1168
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) => {
|
|
1169
1433
|
await checkStaged({ verbose: opts.verbose ?? false });
|
|
1170
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
|
+
});
|
|
1171
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",
|